第三问:指针的本质——地址就是一切
指针,说白了就是地址。这个道理我当年学C语言时听了不下十遍,但真正理解它,是在一次调试内存泄漏的深夜。嗯,今天我们就来把指针这层窗户纸彻底捅破。
一、指针与数组:名字不同,本质相通
数组名是什么?是首元素的地址。指针是什么?是存放地址的变量。它们俩在底层其实是一回事。
我见过不少新手纠结于「数组名是不是指针」这个问题。我的答案是:在表达式中,数组名会被隐式转换为指向首元素的指针,但数组名本身不是变量,你不能给它赋值。
核心理解:
arr[i]等价于*(arr + i)&arr[i]等价于arr + i- 数组名
arr是常量地址,不能做arr++
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p指向数组首元素
// 两种访问方式完全等价
printf("arr[2] = %d\n", arr[2]); // 30
printf("*(p+2) = %d\n", *(p+2)); // 30
// 指针可以移动,数组名不行
p++; // 合法,p现在指向arr[1]
// arr++; // 编译错误
printf("现在p指向: %d\n", *p); // 20
return 0;
}
我的习惯:在需要遍历数组时,我更喜欢用指针而不是下标。因为指针自增比下标计算更直接,编译器优化后效率也更高。但要注意边界,别越界了。
二、指针与函数:传值还是传址?
C语言函数参数默认是传值的。你想在函数里修改外面的变量?必须传地址。这就是指针在函数中的核心作用。
我曾经在项目中写过一个交换函数,传了两个int进去,结果发现外面根本没变。排查了半天,才意识到自己忘了传指针。嗯,从那以后,凡是需要修改实参的函数,我第一反应就是加星号。
#include <stdio.h>
// 错误示范:传值
void swap_wrong(int a, int b) {
int temp = a;
a = b;
b = temp;
// 这里的修改不影响外面
}
// 正确示范:传地址
void swap_right(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
swap_wrong(x, y);
printf("swap_wrong后: x=%d, y=%d\n", x, y); // 5, 10 没变
swap_right(&x, &y);
printf("swap_right后: x=%d, y=%d\n", x, y); // 10, 5 交换成功
return 0;
}
避坑指南:我曾经在嵌入式驱动中,把一个结构体直接传进函数,结果栈空间爆了。结构体太大时,一定要传指针,而不是传值。传指针只占4或8字节,传值可能几百字节。
三、多级指针:指向指针的指针
多级指针听起来吓人,其实逻辑很简单:一级指针存普通变量的地址,二级指针存一级指针的地址,以此类推。
你想想看,什么时候需要二级指针?当你想在函数里修改传入的指针本身时。比如动态分配内存,你想让函数内部malloc后,把地址带回给外面的指针变量。
#include <stdio.h>
#include <stdlib.h>
// 用二级指针修改指针本身
void alloc_array(int **p, int size) {
*p = (int *)malloc(size * sizeof(int));
if (*p == NULL) {
printf("内存分配失败\n");
return;
}
// 初始化
for (int i = 0; i < size; i++) {
(*p)[i] = i * 10;
}
}
int main() {
int *arr = NULL;
// 传入 &arr,即二级指针
alloc_array(&arr, 5);
if (arr != NULL) {
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
free(arr);
}
return 0;
}
多级指针的典型场景:
- 二维数组的动态分配与访问
- 函数需要修改指针参数本身(如上面例子)
- 链表、树等数据结构的节点操作
- 命令行参数
char **argv
四、知识体系总览
下面这张图,是我梳理的指针知识脉络。你看完应该能明白,指针、数组、函数这三者是怎么串起来的。
五、实战中的指针思维
指针不是语法糖,它是C语言与硬件对话的桥梁。我做了这么多年嵌入式,总结出三条指针心法:
- 明确指向:每次声明指针,问自己「这个指针指向什么类型?」
- 检查空指针:解引用前,先判断是否为NULL。我曾经因为没检查,在STM32上直接HardFault。
- 注意生命周期:不要返回局部变量的地址。函数结束后,栈上的东西就没了。
我的调试技巧:遇到指针相关bug,我习惯先打印地址值。用 printf("%p", ptr) 看看指针到底指向哪里。很多时候,地址一出来,问题就清楚了一半。
指针这东西,说白了就是地址。数组名是地址,函数参数传地址,多级指针存地址。你只要抓住「地址」这两个字,指针就不再神秘。
公众号:蓝海资料掘金营,微信deep3321