指针与数组的核心机制

多级指针

一级指针存放着变量的地址,理所当然,二级指针自然是存着一级指针的地址:

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;
}

遍历赋值也是同样的道理,如果对这块不清楚,可以看一下数组的内存示意图:

指针的类型:决定步长与安全

指针就是记录了一块地址,那么它的类型有什么作用?

  1. 告知指针的偏移大小,例如 int (*)[4] 类型的指针,+1 跳过会整个数组(16 个字节长度);而普通的 int* 类型的指针 +1 只会跳过 4 个字节;

  2. 告知指针的取值范围,防止指针取到非法的数据。例如,解引用 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

不管怎么样,日常开发中,传参直接写函数名就行,调用时直接写指针

相关推荐
黄林晴6 小时前
Room 3.0 正式发布!包名彻底重构,KMP 成为核心主线
android·android jetpack
三少爷的鞋6 小时前
Kotlin 协程环境下的 DCL 懒加载:别把线程时代的经验直接搬过来
android
plainGeekDev7 小时前
Gson → kotlinx.serialization
android·java·kotlin
CYY9520 小时前
Compose 入门篇
android·kotlin
杉氧1 天前
Compose 时代的 MVI 架构:如何用单向数据流驱动复杂 UI?
android·架构·android jetpack
杉氧1 天前
Modifier 的艺术:为什么链式调用的顺序决定了UI 的生命周期?
android·架构·android jetpack
李斯维1 天前
腾讯 XLog 日志框架 Android 端接入
android·android studio·android jetpack
黄林晴1 天前
Kotlin Toolchain 0.11 发布:Amper 正式更名,统一 kotlin 命令
android·kotlin