指针的本质:内存地址与间接访问

说实话,指针这东西,刚学的时候我也觉得玄乎。

记得我第一次写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 返回两个指针之间的元素个数

本章知识体系

下面这张图,帮你理清指针的核心逻辑:

指针的本质:内存地址与间接访问 内存:连续的字节序列,每个字节有唯一地址 地址 0x100 地址 0x104 地址 0x108 地址 0x10C 42 100 255 7 指针变量 p 存放地址 0x100 *p 解引用 读取地址0x100的值 → 42 核心要点 ① 指针 = 存放地址的变量 ② & 取地址运算符,* 解引用运算符 ③ 指针大小由平台决定(32位=4B,64位=8B) ④ 指针加减运算自动按元素大小缩放

这张图把内存、地址、指针、解引用之间的关系画清楚了。你多看几遍,再动手写写代码,很快就能掌握。

指针的本质,说白了就是间接访问。你不需要直接操作内存,只需要操作地址,让程序替你去读写。这种间接性,给了C语言极大的灵活性,也带来了风险——但这就是C语言的魅力所在。

记住:指针不是洪水猛兽。理解它的本质,养成好习惯,它就是你手里最锋利的工具。