15. 【C语言】C语言的灵魂:指针初体验

📅 2026/7/5 14:56:59
15. 【C语言】C语言的灵魂:指针初体验
前面十四篇文章我们盖好了地基变量存数据函数拆模块数组管批量。但你有没有觉得还缺一块关键拼图——为什么我们可以在函数里修改数组却改不了普通变量为什么scanf要用取地址为什么说 C 语言“贴近硬件”答案就在指针。指针是 C 语言最强大、也最让初学者头疼的特性。但不要怕——你已经有变量、内存、作用域的扎实基础理解指针就只剩一层窗户纸。今天我们就来捅破它。一、为什么需要指针先看三个你或许已经遇到的困惑困惑一为什么scanf要加intage;scanf(%d,age);// 这个 是什么困惑二为什么在函数里修改数组外面也能看到变化voidfill_zero(intarr[],intn){for(inti0;in;i)arr[i]0;// 外面的数组也变了}困惑三为什么我们写的swap函数没用voidswap(inta,intb){inttempa;ab;btemp;// 外面纹丝不动}这三个困惑都指向同一个核心概念如何直接访问和操作内存中的数据。普通变量通过名字访问但有时候我们需要通过地址来访问——这就引出了指针。二、指针是什么简单说指针就是一个变量但它里面存的不是普通数值而是另一个变量的内存地址。inta10;int*pa;// p 里存的是 a 的地址这里a是一个普通的int变量里面存的是10。a是取地址运算符得到a在内存里的地址比如0x7ffd1c。p是一个指针变量它里面存的就是那个地址。*p是间接访问运算符解引用沿着p存的地址找到a读写它的值。关系图变量 a: [10] -- 地址 0x100 ^ | 指针 p: [0x100] -- p 自己的地址是 0x200所以*p就是a本人。你写*p 20;a就变成了 20。三、声明指针变量声明指针时在类型后面加一个*int*p;// p 是指向 int 的指针char*ch;// ch 是指向 char 的指针double*dp;// dp 是指向 double 的指针声明时可以初始化inta5;int*pa;// p 指向 a也可以分开写int*p;pa;// 让 p 指向 a注意int *p, q;中只有p是指针q是普通int。如果想声明两个指针写int *p, *q;。这是 C 语法的一个小陷阱。四、取地址与*解引用这两个运算符是指针的基本功必须熟练掌握。1.取地址把任意变量的地址取出来返回指向该类型的指针。intx42;printf(%p\n,(void*)x);// 打印 x 的地址%p专门用来打印地址指针值需要强制转换为void*。2.*解引用间接访问通过指针访问它所指向的变量。inta10;int*pa;printf(%d\n,*p);// 输出 10等价于 printf(%d\n, a);*p20;// 把 a 改成 20printf(%d\n,a);// 输出 20*p可以出现在赋值号的左边左值用来修改指向的值。五、指针作为函数参数实现真正的“交换”还记得第十二篇那个失败的swap吗现在用指针来拯救它。#includestdio.hvoidswap(int*a,int*b){inttemp*a;// 取 a 指向的值*a*b;// 把 b 指向的值赋给 a 指向的位置*btemp;}intmain(void){intx5,y10;printf(交换前: x%d, y%d\n,x,y);swap(x,y);// 传 x 和 y 的地址printf(交换后: x%d, y%d\n,x,y);return0;}输出交换前: x5, y10 交换后: x10, y5成功因为swap接收的是x和y的地址它通过*a和*b直接修改了main里那两个变量的内存。这就是指针的核心威力让函数有能力修改外部的变量。六、指针与数组一对亲兄弟1. 数组名就是首元素的地址intarr[5]{10,20,30,40,50};printf(%p\n,(void*)arr);// 数组名直接当指针用printf(%p\n,(void*)arr[0]);// 和上面一样数组名arr在表达式中会被自动转换成指向首元素的指针。所以int*parr;// p 指向 arr[0]现在你可以用指针来访问数组元素printf(%d\n,*p);// arr[0] 10printf(%d\n,*(p1));// arr[1] 20printf(%d\n,*(p2));// arr[2] 30p 1不是地址值加 1 个字节而是加1 * sizeof(int)个字节——也就是跳过整个元素指向下一个int。这称为指针算术。2. 指针算术指针加整数 n地址值增加n * sizeof(指向的类型)。intarr[5]{10,20,30,40,50};int*parr;// 指向 arr[0]pp1;// 指向 arr[1]p;// 指向 arr[2]p2;// 指向 arr[4]也可以用下标访问指针int*parr;printf(%d\n,p[2]);// 等价于 arr[2]输出 30为什么数组和指针这么亲因为arr[i]在底层被编译器翻译成*(arr i)。这两者完全等价。甚至你可以写i[arr]会被翻译成*(i arr)也是一样的但千万别真这么写。3. 用指针遍历数组#includestdio.hintmain(void){intarr[]{2,4,6,8,10};int*p;for(parr;parr5;p){printf(%d ,*p);}printf(\n);return0;}p arr 5判断指针是否越过了数组末尾arr 5指向最后一个元素之后的位置不能解引用但可以做比较。七、数组作为函数参数的本质现在我们可以解释那个困惑了为什么函数里修改数组外面也会变voidmodify(intarr[],intn){arr[0]999;}实际上编译器看到int arr[]时会把它当成int *arr。函数调用时传进来的是数组首地址的副本而不是整个数组的副本。通过这个地址函数可以直接修改原数组。modify(my_array,5);// 传的是 my_array[0]所以arr[0] 999;等价于*(arr 0) 999;直接写到了原数组的内存上。这就是为什么数组“按引用传递”的真相——它传的是地址值。八、void*指针初识有一种特殊的指针类型void*无类型指针。它可以指向任何类型的数据但不能直接解引用因为编译器不知道它指向的数据类型大小。inta10;void*vpa;// 可以指向 int// printf(%d\n, *vp); // 错误不能解引用 void*printf(%d\n,*(int*)vp);// 先强制转换回 int*再解引用void*常用于通用内存操作函数比如malloc、memcpy后面讲动态内存时会遇到。九、常见错误与陷阱1. 使用未初始化的指针int*p;*p10;// 危险p 指向哪里可能是随机地址导致崩溃指针必须指向合法的内存已声明的变量、数组、动态分配的内存才能解引用。2. 返回局部变量的地址int*bad_func(void){intx100;returnx;// 函数返回后 x 已销毁返回的地址无效}这是经典的“悬空指针”错误。要返回指针可以返回静态局部变量、全局变量或动态分配的内存的地址。3. 解引用空指针int*pNULL;printf(%d\n,*p);// 段错误NULL 是空地址不允许访问NULL是一个宏表示空指针。在解引用前一定要确保指针非空。4. 指针类型不匹配inta10;double*dpa;// 编译器警告类型不兼容printf(%f\n,*dp);// 未定义行为不同类型的指针不要随便互指除非你很明白自己在做什么并且用强制转换。5. 野指针指针指向的内存已经释放free 之后但指针还在指向的地址无效。后面动态内存部分会细讲。十、小结今天你第一次触碰了 C 语言的灵魂——指针。它们不是什么神秘魔法只是存地址的变量。但它们解锁了直接修改外部变量的能力swap终于能用了高效操作数组的方式指针算术理解了数组作为函数参数的本质你现在知道了取地址*解引用。指针变量声明用int *p;数组名就是首元素地址arr[i]就是*(arr i)函数传数组本质是传地址所以能在函数内修改原数组。指针的旅途才刚刚开始。下一篇我们会深入指针与数组更复杂的关系——指针数组、数组指针、多级指针以及字符串与指针的紧密联系。当你能轻松玩弄指针时你就真正拥有了 C 语言。课后小练习写一个函数void increment(int *p)让传入的整数加 1。在main中测试。使用指针遍历一个double数组打印所有元素观察指针加 1 时地址增加了多少字节。写一个函数int array_sum(int *arr, int n)用指针算术而不是下标计算数组元素的和。分析以下代码错在哪里int*get_pointer(void){intval5;returnval;}intmain(void){int*pget_pointer();printf(%d\n,*p);return0;}为什么输出可能不是 5用静态局部变量怎么修正我们下期见获取本系列示例代码请访问 GitCode 仓库。