前面十四篇文章,我们盖好了地基:变量存数据,函数拆模块,数组管批量。但你有没有觉得还缺一块关键拼图——为什么我们可以在函数里修改数组,却改不了普通变量?为什么scanf要用&取地址?为什么说 C 语言“贴近硬件”?
答案就在指针。
指针是 C 语言最强大、也最让初学者头疼的特性。但不要怕——你已经有变量、内存、作用域的扎实基础,理解指针就只剩一层窗户纸。今天,我们就来捅破它。
一、为什么需要指针?
先看三个你或许已经遇到的困惑:
困惑一:为什么scanf要加&?
intage;scanf("%d",&age);// 这个 & 是什么?困惑二:为什么在函数里修改数组,外面也能看到变化?
voidfill_zero(intarr[],intn){for(inti=0;i<n;i++)arr[i]=0;// 外面的数组也变了!}困惑三:为什么我们写的swap函数没用?
voidswap(inta,intb){inttemp=a;a=b;b=temp;// 外面纹丝不动}这三个困惑都指向同一个核心概念:如何直接访问和操作内存中的数据。普通变量通过名字访问,但有时候我们需要通过地址来访问——这就引出了指针。
二、指针是什么?
简单说:指针就是一个变量,但它里面存的不是普通数值,而是另一个变量的内存地址。
inta=10;int*p=&a;// 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 的指针声明时可以初始化:
inta=5;int*p=&a;// p 指向 a也可以分开写:
int*p;p=&a;// 让 p 指向 a注意:int *p, q;中,只有p是指针,q是普通int。如果想声明两个指针,写int *p, *q;。这是 C 语法的一个小陷阱。
四、&取地址与*解引用
这两个运算符是指针的基本功,必须熟练掌握。
1.&取地址
把任意变量的地址取出来,返回指向该类型的指针。
intx=42;printf("%p\n",(void*)&x);// 打印 x 的地址%p专门用来打印地址(指针值),需要强制转换为void*。
2.*解引用(间接访问)
通过指针访问它所指向的变量。
inta=10;int*p=&a;printf("%d\n",*p);// 输出 10,等价于 printf("%d\n", a);*p=20;// 把 a 改成 20printf("%d\n",a);// 输出 20*p可以出现在赋值号的左边(左值),用来修改指向的值。
五、指针作为函数参数:实现真正的“交换”
还记得第十二篇那个失败的swap吗?现在用指针来拯救它。
#include<stdio.h>voidswap(int*a,int*b){inttemp=*a;// 取 a 指向的值*a=*b;// 把 b 指向的值赋给 a 指向的位置*b=temp;}intmain(void){intx=5,y=10;printf("交换前: x=%d, y=%d\n",x,y);swap(&x,&y);// 传 x 和 y 的地址printf("交换后: x=%d, y=%d\n",x,y);return0;}输出:
交换前: x=5, y=10 交换后: x=10, y=5成功!因为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*p=arr;// p 指向 arr[0]现在你可以用指针来访问数组元素:
printf("%d\n",*p);// arr[0] = 10printf("%d\n",*(p+1));// arr[1] = 20printf("%d\n",*(p+2));// arr[2] = 30p + 1不是地址值加 1 个字节,而是加1 * sizeof(int)个字节——也就是跳过整个元素,指向下一个int。这称为指针算术。
2. 指针算术
指针加整数 n,地址值增加n * sizeof(指向的类型)。
intarr[5]={10,20,30,40,50};int*p=arr;// 指向 arr[0]p=p+1;// 指向 arr[1]p++;// 指向 arr[2]p+=2;// 指向 arr[4]也可以用下标访问指针:
int*p=arr;printf("%d\n",p[2]);// 等价于 arr[2],输出 30为什么数组和指针这么亲?因为arr[i]在底层被编译器翻译成*(arr + i)。这两者完全等价。甚至你可以写i[arr],会被翻译成*(i + arr),也是一样的(但千万别真这么写)。
3. 用指针遍历数组
#include<stdio.h>intmain(void){intarr[]={2,4,6,8,10};int*p;for(p=arr;p<arr+5;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*(无类型指针)。它可以指向任何类型的数据,但不能直接解引用,因为编译器不知道它指向的数据类型大小。
inta=10;void*vp=&a;// 可以指向 int// printf("%d\n", *vp); // 错误!不能解引用 void*printf("%d\n",*(int*)vp);// 先强制转换回 int*,再解引用void*常用于通用内存操作函数,比如malloc、memcpy,后面讲动态内存时会遇到。
九、常见错误与陷阱
1. 使用未初始化的指针
int*p;*p=10;// 危险!p 指向哪里?可能是随机地址,导致崩溃指针必须指向合法的内存(已声明的变量、数组、动态分配的内存)才能解引用。
2. 返回局部变量的地址
int*bad_func(void){intx=100;return&x;// 函数返回后 x 已销毁,返回的地址无效}这是经典的“悬空指针”错误。要返回指针,可以返回静态局部变量、全局变量或动态分配的内存的地址。
3. 解引用空指针
int*p=NULL;printf("%d\n",*p);// 段错误!NULL 是空地址,不允许访问NULL是一个宏,表示空指针。在解引用前,一定要确保指针非空。
4. 指针类型不匹配
inta=10;double*dp=&a;// 编译器警告,类型不兼容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),用指针算术(而不是下标)计算数组元素的和。 - 分析以下代码错在哪里:
为什么输出可能不是 5?用静态局部变量怎么修正?int*get_pointer(void){intval=5;return&val;}intmain(void){int*p=get_pointer();printf("%d\n",*p);return0;}
我们下期见!
💡获取本系列示例代码请访问 GitCode 仓库。