指针是 C 语言赋予程序员的上帝之手,它允许我们直接操作内存。用好了,它是神兵利器;用不好,它是程序崩溃的根源。这次将带你深入内存,理解指针的本质。
思维导图


一、 指针基础概念
1.1 什么是地址?
计算机内存就像一个巨大的公寓楼,每个字节都有一个唯一的门牌号,这就是地址 。
变量名只是门牌号的别名,指针则是专门用来存放门牌号的变量。
1.2 语法结构:定义与赋值
语法: 类型 *指针变量名;
c
int a = 10;
int *p; // 1. 定义:p 是一个指向 int 的指针
p = &a; // 2. 赋值:把 a 的地址给 p(p 指向 a)
int *q = &a; // 3. 定义并初始化(推荐写法)
1.3 两个核心运算符
& (取地址) :获取变量在内存中的位置。&a 得到的是一个十六进制的地址值。
* (解引用/间接访问) :拿着钥匙开门。访问指针指向的那个位置里的数据。
代码示例:用指针修改变量
c
int a = 10;
int *p = &a; // p 存储了 a 的地址
printf("a 的地址: %p\n", (void*)p);
printf("通过指针看 a: %d\n", *p);
*p = 20; // 核心操作!远程修改 a 的值
printf("a 现在是: %d\n", a); // 输出 20
1.4 指针类型决定视野
为什么指针需要类型?int * 和 char * 存的都是地址(在 64 位系统上都是 8 字节),区别在哪?
区别在于解引用时的视野:
int *p:*p一次性读取 4 个字节。
char *q:*q一次性读取 1 个字节。
代码验证:
c
int x = 0x12345678;
char *p = (char*)&x;
printf("%x", *p); // 在小端序机器上,输出 78 (只读了最低字节)
二、 致命三剑客:NULL、野指针、悬空指针
2.1 NULL 指针
NULL 是一个宏,通常定义为 (void*)0不指向任何有效内存。
最佳实践: 定义指针时如果暂时不用,务必初始化为 NULL。
2.2 野指针
定义了指针但没初始化。它指向哪里?只有上帝知道(垃圾值)。
c
int *p; // 野指针!
// *p = 100; // 极度危险!可能覆盖关键系统数据导致崩溃
2.3 悬空指针
指针指向的内存已经被释放,但指针还在。这是最难调试的 Bug 之一。
经典错误:返回局部变量地址
c
int* get_val() {
int x = 10;
return &x; // 错误!函数结束 x 就销毁了
}
int main() {
int *p = get_val(); // p 变成了悬空指针
// printf("%d", *p); // 未定义行为,数据可能已经被覆盖
}
三、 指针实战:交换两个数
这是理解地址传递最经典的代码。
代码示例:
c
// 接收地址,拥有"远程修改"的能力
void swap(int *a, int *b) {
int temp = *a; // 取出 a 指向的值
*a = *b; // 把 b 指向的值赋给 a 指向的地方
*b = temp; // 把 temp 赋给 b 指向的地方
}
int main() {
int x = 5, y = 10;
swap(&x, &y); // 必须传地址!
printf("x=%d, y=%d\n", x, y); // 输出 x=10, y=5
return 0;
}
四、 指针与数组
4.1 数组名的真面目
在表达式中,数组名 arr 会退化为指向首元素的指针。
核心等价公式:
arr[i] ≡ *(arr + i)
4.2 用指针遍历数组
这种写法在嵌入式开发和操作系统内核中非常常见,效率极高。
代码示例:
c
int arr[] = {10, 20, 30};
int *p = arr; // 指向开头
for (int i = 0; i < 3; i++) {
// 写法 1:下标法(最直观)
printf("%d ", arr[i]);
// 写法 2:指针算术法(偏移)
printf("%d ", *(p + i));
// 写法 3:指针移动法(步进)
printf("%d ", *p++); // 取值后,指针指向下一个
}
五、 指针算术与边界
5.1 +1 到底加多少?
指针加 1,不是地址值加 1,而是跳过一个数据类型的大小。
char *p: p+1 地址增加 1 字节。
int *p: p+1 地址增加 4 字节。
double *p: p+1 地址增加 8 字节。
代码验证步长:
c
int nums[] = {1, 2};
int *p = nums;
printf("当前: %p\n", (void*)p);
p++;
printf("加一: %p\n", (void*)p); // 观察地址差值,一定是 4
5.2 两个指针相减
如果两个指针指向同一个数组,p1 - p2 得到的是它们之间相隔的元素个数 (不是字节数)。这是实现 strlen 的基础。
手写 strlen (指针版):
c
size_t my_strlen(const char *s) {
const char *start = s;
while (*s != '\0') {
s++; // 指针一直往后走
}
return s - start; // 尾地址 - 头地址 = 长度
}
六、 const 与指针(绕口令时间)
这是面试必考题,记住这句口诀:
const 在 * 左边,锁的是物(内容)
const 在 * 右边,锁的是人(指针)
6.1 三种形态对比
c
int x = 10;
int y = 20;
// 1. 指向常量的指针 (Pointer to Const)
// "我不能改里面的值,但我可以指向别人"
const int *p1 = &x;
// *p1 = 30; // 错!内容被锁
p1 = &y; // 对!指针没锁
// 2. 常量指针 (Const Pointer)
// "我可以改里面的值,但我不能指向别人"
int * const p2 = &x;
*p2 = 30; // 对!内容没锁
// p2 = &y; // 错!指针被锁
// 3. 指向常量的常量指针 (双重锁定)
// "我都不能改"
const int * const p3 = &x;
七、 练习题
题目 1: 64 位系统下,int *p 和 char *q 占用的内存大小分别是多少?
题目 2: int a = 10; int *p = &a;,*&a 的结果是什么?&*p 的结果是什么?
题目 3: 为什么指针被释放(free)后,最好立即置为 NULL?
题目 4: 若 short *p = 0x1000; (short占2字节),p + 2 的地址值是多少?
题目 5: int arr[5]; int *p = arr;,表达式 p[2] 合法吗?它等价于什么?
题目 6: 下面代码输出什么?
c
int a[] = {1, 2, 3, 4};
int *p = a;
printf("%d", *p++);
printf("%d", *p);
题目 7: 解析声明:const char * p 和 char * const p 的区别。
题目 8: 如何定义一个指针,指向一个 int 类型的指针(二级指针)?
题目 9: 只有 void* 指针可以直接赋值给其他类型的指针而不需要强转(在 C 中),对吗?
题目 10: 下面代码有什么致命问题?
c
int *p;
*p = 100;
题目 11: 两个指针相加(p1 + p2)有意义吗?两个指针相减(p1 - p2)有意义吗?
题目 12: 在大端序(Big Endian)机器上,int a = 0x12345678; char *p = (char*)&a;,*p 的值是多少?
题目 13: 什么是"野指针"?与"悬空指针"有何区别?
题目 14: 函数参数 void func(const int *p) 里的 const 想表达什么意图?
题目 15: int a[3][4]; int *p = a; 这种赋值会报警告吗?为什么?
八、 解析
题 1 解析
答案: 都是 8 字节。
详解:
指针也是变量,用来存地址。在 64 位系统上,地址总线宽度是 64 位,所以所有类型的指针大小都是 8 字节。
题 2 解析
答案: 10 和 a 的地址。
详解:
*和&互为逆运算,相互抵消。
题 3 解析
答案: 防止悬空指针(Dangling Pointer)误用。
详解:
释放内存后,指针变量里的地址值还在。如果不置空,后续代码可能会误以为这块内存还能用,导致难以排查的 Bug。
题 4 解析
答案: 0x1004。
详解:
步长 = sizeof(short) = 2。
0x1000 + 2 * 2 = 0x1004。
题 5 解析
答案: 合法。等价于 arr[2] 或 *(p+2)。
详解:
指针和数组名在下标访问上是语法糖 关系。
[]运算符本质上就是指针加法后解引用。
题 6 解析
答案: 12。
详解:
*p++先取值*p(1),然后p自增指向下一个。第二次打印*p就是 2。
题 7 解析
答案:
前者指针指向的内容不可改(内容只读);后者指针本身的指向不可改(指针只读)。
题 8 解析
答案: int **pp;。
详解:
二级指针,存放的是一级指针的地址。
题 9 解析
答案: 对。
详解:
void*是通用指针,C 语言允许它隐式转换为任意类型指针(注意:C++ 不允许)。
题 10 解析
答案: 野指针解引用。
详解:
p没有初始化,指向随机地址。写入可能导致程序立即崩溃。
题 11 解析
答案: 相加无意义;相减有意义。
详解:
两个地址相加没有物理意义。同类型指针相减表示中间相隔的元素个数(不是字节数)。
题 12 解析
答案: 0x12。
详解:
大端序:高位字节存低地址。
p指向首字节(低地址),所以拿到的是高位数据0x12。
题 13 解析
答案:
野指针:未初始化的指针。
悬空指针:指向已释放内存的指针。
题 14 解析
答案: 只读承诺。
详解:
告诉调用者:"把你的数据传给我,我保证只读取,绝不修改。"这是一种契约。
题 15 解析
答案: 会报警告。
详解:
a的类型是int (*)[4](指向包含4个int的数组的指针),p的类型是int *。虽然数值可能一样,但步长不同,类型不兼容。

日期:2025年2月12日
专栏:C语言