多级指针
一级指针存放着变量的地址,理所当然,二级指针自然是存着一级指针的地址:
c
#include <stdio.h>
int main() {
int a = 100;
int* p = &a;
int** pp = &p;
printf("p's address is %p == %p\n", &p, pp);
// 两次解引用,才能获取a的值
// 因为pp存着p的地址,第一次取值获取的是p的地址,第二次取值相当于*p,得到a的值
printf("a'value is %d == %d\n", *p, *(*pp));
return 0;
}
回想之前房间和纸条的比喻并不难理解,你可以简单记为:指针有多少个 * 号,就要经过多少次解引用才能获取原始值。
如 int*** p = ...;,需要解引用三次。多级指针在日常开发中使用频次不高,但如果要在函数内部修改外部指针的指向时,就需要用到了,日常先理解其机制就行。
数组与指针:首地址与整个数组地址
C语言中的数组定义和遍历是这样的:
c
#include <stdio.h>
int main() {
// 变量名是arr,类型是int []
int arr[] = { 3,2,1 };
// sizeof(arr) 即可获取整个数组的字节大小
// 除以每个元素的大小(sizeof(arr[0]))就是数组的长度
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
printf("The %d-th element is %d\n", i + 1, arr[i]);
}
return 0;
}
当你打印数组和数组的地址时,你会发现一个问题:这两个打印的结果相同。
c
#include <stdio.h>
int main() {
int arr[] = { 0, 1, 2, 3 };
printf("arr's value is %p\n", arr);
printf("arr's address is %p\n", &arr);
printf("arr's first element's address is %p\n", &(arr[0]));
// arr == &arr == &(arr[0])
}
虽然 arr 和 &arr 打印出的地址值完全一样,但本质区别在于编译器给的类型不同:
arr 和 &(arr[0]) 表示的是数组的首地址 (arr 数组名在表达式中会自动隐式转换为数组首元素的地址),解引用可以获取第一个元素的值,指针 +1 时,会移动数组元素类型的字节长度;
而 &arr 表示的是整个数组的地址 ,代表整个数组的起始地址,解引用值还是数组的地址。指针 +1 时,会跳过整个数组,以长度为 4 的 int 数组为例,&arr+1 会跳过 16 个字节。
c
#include <stdio.h>
int main() {
int arr[] = { 0, 1, 2, 3 };
// 数组的地址,p是一个指针,类型是int (*)[4]
int (*p)[4] = &arr;
printf("the array's address %p\n", &arr);
// 第一个元素的地址,*(&arr)等价于arr
printf("the first element'address is %p == %p == %p\n", arr, *(&arr), &arr[0]);
// 第一个元素的值
printf("the first element'value is %d == %d\n", *arr, *(&arr[0]));
// 第三个元素的值
printf("the third element is %d == %d\n", *(arr + 2), *(&arr[0] + 2));
// 数组末尾的地址
int len = sizeof(arr) / sizeof(arr[0]);
printf("the value of the element after the last element of the array is %p == %p", &arr[len - 1] + 1, &arr + 1); // &arr + 1 跨越了整个数组
}
我们回头来看一个问题:为什么 arr[i] 能够获取数组元素?arr 不难道是首元素的地址吗?
其实 [] 运算符解引用和指针偏移的语法糖,arr 数组名首先会隐式转为 &arr[0],然后向后偏移 i 个元素,最后进行解引用操作,取出内存中的值,所以它能够取到元素,arr[i] 语法等价于 *(arr+i)。
现在,下面这道题一定难不倒你:
c
#include <stdio.h>
int main() {
int a = 100;
int* p = &a;
int** pp = &p;
if (p == pp[0]) {
printf("p == pp[0]");
}
else
{
printf("p != pp[0]");
}
return 0;
}
指针偏移:遍历数组
使用指针遍历打印数组也很简单,只需这样:
c
#include <stdio.h>
int main() {
int arr[] = { 0, 1, 2, 3 };
int* p = arr; // &arr[0]
int len = sizeof(arr) / sizeof(arr[0]);
// 指针不越界,就执行循环
for (; p < &arr[len]; p++)
{
printf("%d\n", *p);
}
return 0;
}
遍历赋值也是同样的道理,如果对这块不清楚,可以看一下数组的内存示意图:

指针的类型:决定步长与安全
指针就是记录了一块地址,那么它的类型有什么作用?
-
告知指针的偏移大小,例如
int (*)[4]类型的指针,+1 跳过会整个数组(16 个字节长度);而普通的int*类型的指针 +1 只会跳过 4 个字节; -
告知指针的取值范围,防止指针取到非法的数据。例如,解引用 int 类型的指针,会得到一个 int 值,不会出现取到 double 值的现象。
函数指针:回调函数
函数指针一般用于回调,观察下面的经典示例:
c
#include <stdio.h>
// 加法
int add(int a, int b) {
return a + b;
}
// 减法
int minus(int a, int b) {
return a - b;
}
int calc(int num1, int num2, int(*op)(int, int)) {
return op(num1, num2);
}
int main() {
int a = 10, b = 3;
int res1 = calc(a, b, add);
int res2 = calc(a, b, minus);
printf("calc add: %d + %d = %d\n", a, b, res1);
printf("calc minus: %d - %d = %d\n", a, b, res2);
return 0;
}
可以得知函数指针的标准写法就是:返回值类型(*指针变量名称)(参数的类型列表)。
另外,函数名本身就是其地址(隐式转换为函数指针),在传参时不用加 &,如果加上结果相同,因为 &add 是显式获取函数的地址,两者完全等价。
对函数指针解引用,结果还是函数指针,这是编译器干预的,识别到就会将解引用还原为函数指针。
所以存在着这样的等价链条:
csharp
op ≡ *op ≡ **op ≡ add ≡ &add
不管怎么样,日常开发中,传参直接写函数名就行,调用时直接写指针。