3. 指针的本质
指针,说白了就是地址。很多初学者一听「指针」两个字就觉得玄乎,其实它没那么神秘。我刚开始学C语言那会儿,也被绕得晕头转向,后来想明白了一件事:指针就是个存放内存地址的变量,仅此而已。
嗯,咱们今天就把指针的里里外外掰扯清楚。从定义声明,到算术运算,再到数组、多级指针、函数指针,最后聊聊指针的大小和类型。这一套下来,你基本就能把指针玩明白了。
一、指针的定义与声明
先看最基本的写法:
int a = 10;
int *p = &a; // p 存放的是 a 的地址
这里的 int *p,读作「指向整型的指针 p」。星号 * 告诉编译器:我要声明一个指针变量。而 &a 是取地址操作符,拿到 a 在内存中的位置。
我个人习惯在声明时把星号紧贴变量名,写成 int *p 而不是 int* p。为什么?因为这样能避免一个常见的坑:
int* p, q; // 你以为 p 和 q 都是指针?
// 实际上:p 是指针,q 是普通 int 变量
写成 int *p, q; 就一目了然了——只有 p 带星号,q 不带。这个细节我见过不少新手栽跟头。
二、指针的算术运算
指针能做加减运算,但它的加减和普通整数不一样。你想想看,如果指针加1,到底是加1个字节,还是加1个元素的大小?
答案是:加1个元素的大小。
int arr[] = {10, 20, 30, 40};
int *p = arr; // p 指向 arr[0]
p++; // p 现在指向 arr[1]
printf("%d\n", *p); // 输出 20
这里 p++ 实际上让地址增加了 sizeof(int) 个字节(通常是4字节)。这就是指针算术运算的核心:偏移量由指针的类型决定。
我在项目中遇到过一个问题:有人用 char* 遍历 int 数组,结果每次只跳1个字节,数据全读错了。嗯,这就是类型没搞对。
指针之间也可以相减,得到的是两个指针之间的元素个数:
int arr[5];
int *p1 = &arr[0];
int *p2 = &arr[3];
ptrdiff_t diff = p2 - p1; // diff = 3
注意,指针相加是没有意义的——两个地址加在一起算什么?编译器也不会让你这么干。
三、指针与数组的关系
数组名和指针之间,有着千丝万缕的联系。说白了,数组名在大多数情况下会被隐式转换为指向首元素的指针。
int arr[4] = {1, 2, 3, 4};
int *p = arr; // 等价于 int *p = &arr[0];
printf("%d\n", p[2]); // 输出 3,p[2] 等价于 *(p+2)
你看,指针也可以用下标访问,数组名也可以当指针用。但有一个关键区别:数组名不是左值,你不能对它赋值。
int arr[4];
arr = NULL; // 编译错误!数组名不是变量
我曾经在代码审查时看到有人写 arr++,想遍历数组。结果编译报错,他还一脸懵。数组名不是指针变量,它是个常量地址,不能修改。
四、多级指针
多级指针,就是指向指针的指针。听起来绕,其实画个图就清楚了。
int a = 10;
int *p = &a; // 一级指针
int **pp = &p; // 二级指针
int ***ppp = &pp; // 三级指针
要拿到 a 的值,需要逐层解引用:***ppp。每一层星号剥开一层包装。
多级指针最常见的用途是:在函数中修改指针本身的值。比如你要在函数里分配内存,并把地址传回给调用者:
void alloc_mem(int **ptr, int size) {
*ptr = (int*)malloc(size * sizeof(int));
}
int main() {
int *arr = NULL;
alloc_mem(&arr, 10);
// 现在 arr 指向了堆上分配的内存
free(arr);
return 0;
}
如果不传二级指针,只传一级指针,函数内部修改的是指针的副本,调用者那边根本拿不到新地址。这个坑我踩过,调试了半天才发现问题。
五、函数指针
函数指针,就是指向函数的指针。函数本身也有地址,存在代码段里。我们可以通过指针来调用函数。
int add(int a, int b) {
return a + b;
}
int (*func_ptr)(int, int) = &add;
// 或者简写:int (*func_ptr)(int, int) = add;
int result = func_ptr(3, 4); // 调用 add 函数
printf("%d\n", result); // 输出 7
声明函数指针的语法有点怪:返回类型 (*指针名)(参数列表)。括号不能省,否则就成了返回指针的函数声明。
函数指针的典型应用场景是回调函数。比如 qsort 排序时,你需要传入一个比较函数:
int cmp(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int arr[] = {3, 1, 4, 1, 5};
qsort(arr, 5, sizeof(int), cmp);
我在写嵌入式驱动时,经常用函数指针来实现硬件抽象层。不同的硬件平台,注册不同的函数指针,上层代码完全不用改。这种设计模式非常灵活。
六、指针的大小与类型
指针的大小,取决于运行平台的地址总线宽度。32位系统上,指针是4字节;64位系统上,指针是8字节。不管指针指向什么类型,它的大小都一样。
printf("char*: %zu\n", sizeof(char*)); // 64位系统输出 8
printf("int*: %zu\n", sizeof(int*)); // 64位系统输出 8
printf("double*: %zu\n", sizeof(double*)); // 64位系统输出 8
但是,指针的类型决定了它如何解释内存。同样是地址0x1000,char* 解引用只读1个字节,int* 读4个字节,double* 读8个字节。这就是为什么指针需要类型。
有一个特例是 void*,它是无类型指针。它可以存放任何类型的地址,但不能直接解引用——编译器不知道要读几个字节。使用时需要先转换成具体类型。
int a = 42;
void *vp = &a;
// printf("%d\n", *vp); // 编译错误
printf("%d\n", *(int*)vp); // 先转成 int*,再解引用
malloc 返回的就是 void*,所以需要强制类型转换。不过C语言中,void* 可以隐式转换为其他指针类型,但C++不行。嗯,这个细节在跨语言编程时要注意。
知识体系总览
下面这张图,把指针的核心知识点串了起来。你可以把它当作一张地图,随时回来对照。
指针这东西,说白了就是一把双刃剑。用好了,代码灵活高效;用不好,段错误、内存泄漏、野指针,各种问题接踵而至。我见过太多线上事故,最后定位到都是指针越界或者空指针解引用。
嗯,这一章的内容就到这里。记住:多写、多调试、多画内存图。指针没那么可怕,你只是需要一点时间和耐心去熟悉它。