指针的本质:内存地址与间接访问
说实话,指针这东西,刚学的时候我也觉得玄乎。
记得我第一次写C程序,看到*p这种写法,脑子里全是问号。后来在嵌入式项目里调试了三天三夜,才真正搞明白——指针就是个数字,一个代表内存位置编号的数字。
内存地址:每个字节都有门牌号
你的电脑内存,说白了就是一大排连续的字节。
每个字节都有一个唯一的编号,这个编号就是地址。就像你家的门牌号一样,从0开始,一直往上排。
核心概念:地址是数字,不是数据本身。它告诉你数据住在哪,而不是数据是什么。
举个例子:
int a = 42;
printf("变量a的地址是:%p\n", &a);
printf("变量a的值是:%d\n", a);
这里&a取到的,就是变量a在内存中的门牌号。而a本身,是门牌号里住的那个值——42。
我在项目中遇到过一个问题:有个同事把地址打印出来,当成普通整数去加减,结果程序跑飞了。嗯,地址虽然是个数字,但它不是普通整数。你想想看,门牌号能随便加减吗?
指针变量:专门存地址的盒子
指针变量,就是用来存放地址的变量。
它的声明方式很特别:
int *p; // p是一个指针变量,它可以存放int类型变量的地址
char *q; // q是一个指针变量,它可以存放char类型变量的地址
这里的*号,告诉编译器:我要声明一个指针,不是普通变量。
| 声明 | 含义 |
|---|---|
int *p; |
p是指向int的指针 |
char *q; |
q是指向char的指针 |
double *r; |
r是指向double的指针 |
我个人习惯把指针声明写成int* p;,这样int*看起来像一个整体——"指向int的指针类型"。不过C语言语法解析时,*是跟变量名结合的,所以两种写法都行。
小技巧:声明多个指针时,每个变量前都要加*。比如int *p, *q;,而不是int* p, q;——后者只有p是指针,q是普通int。
间接访问:通过地址找到数据
有了地址,怎么拿到里面的数据?
用*运算符。这叫解引用,也叫间接访问。
int a = 42;
int *p = &a; // p保存了a的地址
printf("p的值(地址):%p\n", p);
printf("*p的值(数据):%d\n", *p); // 输出42
*p = 100; // 通过指针修改a的值
printf("a的新值:%d\n", a); // 输出100
你看,*p就像一把钥匙,通过地址找到了那个房间,然后你可以看里面的东西,也可以换掉里面的东西。
为什么会这样?因为*p和a指向的是同一块内存。改*p就是改a,改a就是改*p。
指针的大小:跟类型无关,跟平台有关
很多人以为int*是4字节,char*是1字节。错!
指针的大小只取决于你的系统是32位还是64位:
- 32位系统:所有指针都是4字节
- 64位系统:所有指针都是8字节
printf("int* 的大小:%zu\n", sizeof(int*));
printf("char* 的大小:%zu\n", sizeof(char*));
printf("double* 的大小:%zu\n", sizeof(double*));
// 在64位系统上,全部输出8
指针的类型信息,只告诉编译器:从这个地址开始,往后读多少个字节。比如int*读4个字节,double*读8个字节。
警告:我曾经见过一个bug,有人在64位系统上把指针强转成int,结果高32位被截断了。程序在测试环境跑得好好的,上线就崩。排查了两天才找到原因——指针不是int,别乱转!
空指针与野指针:两个极端
空指针:指向地址0的指针。通常用NULL表示。
int *p = NULL;
if (p != NULL) {
*p = 42; // 安全:先检查再使用
}
野指针:指向随机地址的指针。这是大坑。
int *p; // 未初始化,p的值是随机的
*p = 42; // 危险!你根本不知道写到了哪里
我曾经在维护一个老项目时,发现有个函数里声明了指针没初始化,然后直接用了。程序偶尔崩溃,偶尔正常。查了三天,最后用Valgrind跑了一遍,才发现是野指针的问题。
好习惯:声明指针时立即初始化。不知道指向哪?先赋NULL。用完指针后,也记得置NULL。这样就算误用,也能在检查时发现。
指针的算术运算:不是简单的加减
指针加1,不是地址加1,而是加一个"元素"的大小。
int arr[] = {10, 20, 30, 40};
int *p = arr; // 指向arr[0]
printf("p = %p\n", p);
printf("p+1 = %p\n", p+1); // 地址增加了4(int的大小)
printf("*(p+1) = %d\n", *(p+1)); // 输出20
你想想看,如果指针加1只是地址加1,那int*加1后指向的地址,其实在int的中间位置,根本读不出完整数据。所以编译器自动帮你做了缩放——加的是sizeof(类型)。
| 操作 | 实际效果 |
|---|---|
p + n |
地址增加 n * sizeof(类型) |
p - n |
地址减少 n * sizeof(类型) |
p1 - p2 |
返回两个指针之间的元素个数 |
本章知识体系
下面这张图,帮你理清指针的核心逻辑:
这张图把内存、地址、指针、解引用之间的关系画清楚了。你多看几遍,再动手写写代码,很快就能掌握。
指针的本质,说白了就是间接访问。你不需要直接操作内存,只需要操作地址,让程序替你去读写。这种间接性,给了C语言极大的灵活性,也带来了风险——但这就是C语言的魅力所在。
记住:指针不是洪水猛兽。理解它的本质,养成好习惯,它就是你手里最锋利的工具。