2、指针与基本类型:int指针、char指针、float指针、void指针、指针的大小
好,咱们今天来聊聊指针和基本类型的关系。说实话,很多初学者觉得指针难,其实一大半的困惑都卡在这里——「为什么不同类型的指针,长得好像差不多,用起来却天差地别?」
我个人习惯把指针理解成「带类型标签的地址」。地址本身只是个数字,但有了类型,编译器才知道怎么去解读那块内存。你想想看,这就像快递柜的格子——每个格子都有编号(地址),但格子大小不同,里面装的东西也不同。
2.1 int指针、char指针、float指针——它们到底差在哪?
先看一段最基础的代码:
int a = 42;
int *p_int = &a;
char c = 'A';
char *p_char = &c;
float f = 3.14f;
float *p_float = &f;
这三个指针,p_int、p_char、p_float,它们存储的都是地址。但区别在哪?
核心区别一:解引用时读取的字节数不同
*p_int会从地址开始读取 4 个字节(假设 int 是 32 位)*p_char只读取 1 个字节*p_float读取 4 个字节,但按浮点数格式解释
我在项目中遇到过这样一个坑:有人用 char* 去遍历一个 int 数组,想逐字节查看数据。想法没错,但写代码时忘了做强制类型转换,结果解引用出来全是乱码。嗯,这里要注意——指针的类型决定了编译器怎么「切」这块内存。
关键理解:指针的类型不是给地址本身贴标签,而是告诉编译器「从这个地址开始,按什么规则读取/写入数据」。
核心区别二:指针运算的步长不同
这个我当年学的时候也迷糊过一阵子。看例子:
int arr_int[3] = {10, 20, 30};
char arr_char[3] = {'A', 'B', 'C'};
int *pi = arr_int;
char *pc = arr_char;
pi++; // 地址增加 sizeof(int) = 4 字节
pc++; // 地址增加 sizeof(char) = 1 字节
为什么会这样?因为 p + 1 在 C 语言里不是简单地加 1,而是加 sizeof(指针指向的类型)。说白了,编译器帮你做了乘法——p + n 实际地址 = p + n * sizeof(*p)。
| 指针类型 | sizeof(指向类型) | p + 1 增加的字节数 | 典型用途 |
|---|---|---|---|
| int* | 4 | 4 | 遍历 int 数组 |
| char* | 1 | 1 | 遍历字符串、逐字节操作 |
| float* | 4 | 4 | 遍历 float 数组 |
| double* | 8 | 8 | 遍历 double 数组 |
小技巧:如果你不确定指针加 1 到底跳多少字节,直接打印 sizeof(*p) 看看。我调试时经常这么干,比翻手册快多了。
2.2 void指针——「万能插座」也有规矩
void* 是个特殊的存在。它只存地址,不存类型信息。你可以把任何类型的指针赋值给 void*:
int a = 100;
void *vp = &a; // 合法,不报错
char c = 'X';
vp = &c; // 也合法
但注意——你不能直接解引用 void 指针。因为编译器不知道它指向什么类型,不知道该读几个字节。
// 错误写法
// printf("%d", *vp); // 编译报错
// 正确写法:先转回具体类型
printf("%d", *(int*)vp);
我曾经在写一个通用数据缓存模块时,大量用了 void*。好处是接口很通用,任何类型的数据都能往里塞。坏处是——你必须在取出时记得原来的类型,否则解引用就崩了。嗯,这就像你把钥匙放进了万能抽屉,但得记住哪把钥匙开哪把锁。
避坑指南:我曾经在嵌入式项目里犯过一个错——用 void* 传了一个结构体指针,然后在回调函数里忘了做类型转换就直接解引用。结果程序跑起来随机崩溃,查了两天才找到原因。记住:void* 只负责「搬运」地址,不负责「解释」数据。
2.3 指针的大小——跟类型无关,跟平台有关
这个问题我面试时经常问,很多人会答错。你猜猜看,在同一个平台上,int*、char*、float*、void* 的大小分别是多少?
答案是——它们的大小完全一样。
为什么?因为指针存的是地址,而地址的长度只取决于 CPU 的寻址能力,跟指向什么类型的数据无关。
printf("int* 大小: %zu 字节\n", sizeof(int*));
printf("char* 大小: %zu 字节\n", sizeof(char*));
printf("float* 大小: %zu 字节\n", sizeof(float*));
printf("void* 大小: %zu 字节\n", sizeof(void*));
在 32 位系统上,输出全是 4 字节。在 64 位系统上,输出全是 8 字节。
| 平台 | 地址总线宽度 | 指针大小 |
|---|---|---|
| 8 位单片机(如 8051) | 16 位(部分) | 2 字节(或混合模式) |
| 32 位 ARM/x86 | 32 位 | 4 字节 |
| 64 位 x86_64/ARM64 | 64 位 | 8 字节 |
重要结论:指针的大小由「地址总线宽度」决定,与指针本身的类型无关。但指针的「步长」和「解引用方式」由指向的类型决定。这两个概念千万别搞混了。
我在做嵌入式开发时,经常需要在 32 位 MCU 和 64 位 Linux 之间移植代码。如果代码里写了 sizeof(int*) 的硬编码假设(比如假设永远是 4),那移植到 64 位平台就炸了。所以我的习惯是——永远用 sizeof(指针) 动态获取大小,别写死数字。
2.4 本章知识结构图
下面这张图帮你理清指针与基本类型的关系:
2.5 总结一下
这一章的内容其实就三句话:
- 指针的类型决定了你解引用时怎么解释那块内存,以及指针加减时的步长。
- void 指针是个「类型擦除」的工具,灵活但危险,用之前必须转回具体类型。
- 指针的大小跟类型无关,只跟 CPU 的寻址能力有关。32 位系统 4 字节,64 位系统 8 字节。
我个人觉得,理解这三条,指针的基本功就算打牢了。后面咱们聊数组和指针的关系时,你会发现这些知识会反复用到。