适用对象:学完指针初阶,了解地址和解引用的同学
本章目标:掌握 const 修饰指针的三种写法 + 彻底理解野指针的成因与规避方法
引入:两把锁,一把锁谁?
上一章我们学了指针的基础操作------指针可以读取 和修改它指向的内存,就像一把万能钥匙,能开能改。
但有时候我们只想让它能读不能改 ,或者固定指向不许乱动------就像给锁加了一把限制,让万能钥匙只能在特定范围内使用。
这一章我们就来学两件事:
const------ 给指针加「锁」,限制它的读写权限- 野指针 ------ 那些乱指的指针,为什么危险,怎么避开
第一部分:const 修饰指针------左右有别
1.1 const 的核心作用
const 是英文「constant(常数)」的缩写,意思是「不变的」。把它加在变量前面,这个变量的值就不能被修改了:
c
const int n = 10;
n = 20; // ❌ 编译错误:表达式必须是可修改的左值
但这是变量层面的锁。如果你想在指针层面 加锁,const 放在不同的位置,锁住的对象完全不同。
🔑 一个口诀记住 const 的位置含义 :
「左锁内容,右锁方向,两边都加,两个都绑」
左边 = 锁「指针指向的内容」不让改
右边 = 锁「指针本身存的地址」不让改
1.2 const 在 * 左边------锁内容
c
int m = 10;
int n = 20;
const int *p1 = &m; // 等价写法:int const *p1 = &m;
*p1 = 30; // ❌ 编译错误:不能通过 p1 修改 m 的值
p1 = &n; // ✅ 可以:p1 本身(存的地址)可以改变
语义 :这张便签(p1)上写的内容不能被改,但便签本身可以换成另一张(指向别处)。
1.3 const 在 * 右边------锁方向(锁指针本身)
c
int m = 10;
int n = 20;
int * const p2 = &m;
*p2 = 30; // ✅ 可以:可以通过 p2 修改 m 的值
p2 = &n; // ❌ 编译错误:p2 本身是常量,不能再指向别处
语义 :这张便签(p2)被钉死在桌子上,不能换成另一张便签;但便签上的内容(m 的值)可以改。
1.4 两边都有------两个都绑
c
int m = 10;
int n = 20;
const int * const p3 = &m;
*p3 = 30; // ❌ 不能改内容
p3 = &n; // ❌ 不能改指向
语义:便签被钉死,内容也被锁住,只能老老实实待在原地。
1.5 速查对比表
| 写法 | 能否修改 *p |
能否让 p 指向别处 |
适用场景 |
|---|---|---|---|
int *p |
✅ 可以 | ✅ 可以 | 普通指针 |
const int *p |
❌ 不能 | ✅ 可以 | 只读参数,防止函数内部意外修改 |
int * const p |
✅ 可以 | ❌ 不能 | 全局固定指向某个变量 |
const int * const p |
❌ 不能 | ❌ 不能 | 双重保险 |
1.6 实战场景:函数参数加 const
为什么要给函数参数加 const?看这个例子:
c
#include <stdio.h>
// 没有 const 的版本
int my_strlen(char *str) {
int count = 0;
while (*str) {
count++;
str++;
}
return count;
}
// 有 const 的版本(更安全)
int my_strlen_safe(const char *str) {
int count = 0;
while (*str) {
count++;
str++;
}
return count;
}
加了 const 之后,如果你不小心在函数里写了 *str = '\0',编译器会直接报错------把错误提前到编译阶段,而不是等到程序跑起来才发现问题。
⚠️ 提醒 :
const不是彻底的安全锁------如果有人通过另一个途径(比如直接用变量名n而不是指针p)修改,const 拦不住。C 语言的 const 是「语法层面」的限制,不是硬件层面的写保护。真正的安全靠的是良好的编程习惯。
第二部分:野指针------最危险的 Bug 制造机
2.1 什么是野指针?
野指针 :指向了一块随机地址 / 已被释放的内存 / 未初始化的空间 的指针。
对野指针解引用 = 访问非法内存 = 程序轻则数据损坏,重则直接崩溃(Coredump / Segmentation Fault)。
把指针想象成一张便签:
| 正常便签 | 野指针便签 |
|---|---|
| 上面写着有效房间号,你可以正常进出 | 上面写着随机涂鸦/已经被拆掉的房间号,你不知道会发生什么 |
2.2 野指针成因一:指针未初始化
最容易犯的错误,也是最常见的一个坑。
c
#include <stdio.h>
int main()
{
int *p; // ❌ 局部指针不初始化,默认是随机值(垃圾地址)
*p = 100; // 往随机地址写入数据------程序大概率崩溃
printf("%d", *p); // 未定义行为:读了一块不属于你的内存
return 0;
}
🏠 类比:你去前台拿了张空白便签,上面没有任何房间号,你就直接往上面写的地址冲进去------很可能冲进别人的房间,或者闯进一堵墙。
正确做法:定义指针时立即初始化,要么指向明确地址,要么指向 NULL。
c
int *p = NULL; // ✅ 初始化为 NULL,明确表示「目前不指向任何地方」
2.3 野指针成因二:指针越界访问
数组的大小是固定的,指针只能在它指向的范围内活动,越界了就是野指针。
c
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // arr[0] ~ arr[4] 是合法的
for (int i = 0; i <= 5; i++) { // ⚠️ i=5 时越界!
printf("%d ", *(p + i)); // p+5 指向 arr[5],但 arr 根本没有第6个元素
}
return 0;
}
这段代码在 i=5 时访问了 arr[5],而数组只有 arr[0]~arr[4],越界访问了一个未知内存区域------轻则读到垃圾值,重则程序崩溃。
⚠️ 提醒:数组越界不会在编译时报错,C 语言不检查数组下标。这是 C 语言最大的特点之一,也是最大的坑之一。程序可能跑很多次都没事,然后某一次突然崩掉------这种 Bug 最难调试,因为行为不确定。
2.4 野指针成因三:返回局部变量的地址
函数内部的局部变量,在函数结束时内存会被系统回收(栈帧弹出)。如果返回它的地址,这个地址指向的空间实际上已经不存在了。
c
#include <stdio.h>
int *test()
{
int n = 100; // n 在栈上分配
return &n; // ❌ 返回了 n 的地址,但函数结束后 n 的内存被回收
} // 此时返回的 &n 是一个「空房间号」
int main()
{
int *p = test();
printf("%d\n", *p); // ❌ 解引用野指针:n 的内存已被系统收回
return 0;
}
为什么系统要回收局部变量的内存?
🍽️ 类比:你去餐厅吃饭,餐厅给你一个临时座位(局部变量)。你吃完饭离开(函数返回),服务员把这个座位收回去给了别人(内存被回收)。你离开后给朋友发消息说「来 5 楼 12 号座位找我」------朋友到了那个座位可能已经换了人,甚至座位都被拆了(内存被覆写)。
正确做法 :如果需要函数返回指针,要么返回动态分配的内存(malloc),要么让调用者传入指针由调用者提供空间:
c
// 方案一:调用者提供空间
void test(int *p) {
*p = 100;
}
// 方案二:返回动态分配的内存(需要手动 free)
int *test2() {
int *p = malloc(sizeof(int));
*p = 100;
return p; // malloc 的内存在 free 之前一直有效
}
2.5 野指针成因四:指针指向的空间被 free / 释放
在使用 malloc/free 的场景中,free 之后的指针仍然指向那块已经被回收的内存------同样是野指针。
c
#include <stdlib.h>
int *p = malloc(sizeof(int)); // 动态分配了一块内存
*p = 42;
free(p); // 释放内存,这块内存还给系统了
*p = 100; // ❌ 野指针:p 仍然指向那块已被释放的内存
安全写法 :free(p) 之后,立即把 p 置为 NULL。
c
free(p);
p = NULL; // ✅ 置为 NULL 后,解引用会触发程序崩溃(而非读垃圾数据)
// 崩溃位置明确,更容易调试
第三部分:如何规避野指针------四条铁律
铁律一:指针一定要初始化
c
int *p = NULL; // ✅ 暂时不指向任何地方
int a;
int *p2 = &a; // ✅ 明确指向某个变量
铁律二:不要越界访问
数组和指针操作的合法范围,永远是你申请的那块内存区域。访问之前先想清楚:这块内存有多大?最多能访问到哪?
铁律三:指针用完置 NULL,使用前检查
c
int *p = NULL;
p = &a; // 使用前确保 p 不是 NULL
if (p != NULL) {
printf("%d", *p);
}
p = NULL; // 用完后及时置 NULL,形成习惯
为什么要检查 NULL?因为 NULL 是一个特殊的地址------操作系统保证地址 0 永远不可访问,所以解引用 NULL 会直接崩溃,而不是读到一个垃圾值。越早崩溃越好,这叫「Fail Fast」原则。
铁律四:不要返回局部变量的地址
函数内部定义的非静态局部变量,在函数返回时生命周期就结束了。它们的地址不能作为函数的返回值。
第四部分:assert 断言------程序的安检门
assert() 是 <assert.h> 提供的调试工具,作用是:在运行时不满足条件,立刻终止程序并告诉你哪一行出了问题。
c
#include <assert.h>
int my_strlen(const char *str)
{
assert(str != NULL); // 如果 str 是 NULL,程序直接崩溃并报错
int count = 0;
while (*str != '\0') {
count++;
str++;
}
return count;
}
assert() 的意义是把潜在的 Bug 在开发阶段就暴露出来,而不是让它溜到用户手里才发作。
💡 什么时候用 assert:在函数入口处验证「不可能为 NULL / 不可能越界」的前提条件。如果这个条件理论上永远不应该发生,但你又想确保万无一失,就加 assert。
第五部分:经典面试题精讲
面试题一:const 在 * 左边 vs 右边的区别
c
int a = 10, b = 20;
// ① const 在 * 左边
const int *p1 = &a;
p1 = &b; // ✅
*p1 = 100; // ❌
// ② const 在 * 右边
int * const p2 = &a;
p2 = &b; // ❌
*p2 = 100; // ✅
// ③ 两边都有
const int * const p3 = &a;
p3 = &b; // ❌
*p3 = 100; // ❌
面试回答技巧 :不要只背结论,要学会用生活比喻:「* 左边锁内容,内容不能改;* 右边锁方向,指针不能换地方。」
面试题二:以下代码为什么会崩溃?
c
#include <stdio.h>
#include <stdlib.h>
char *get_hello() {
char s[] = "hello"; // 局部数组,存放在栈上
return s; // ❌ 返回栈上数组的地址
}
int main() {
char *p = get_hello();
printf("%s\n", p); // 读到的可能是垃圾值或崩溃
return 0;
}
答案 :s 是局部变量,函数 get_hello() 结束时栈帧被弹出,s 的内存区域被回收。返回的地址指向的是一块已无效的内存,printf 读到的内容是未定义行为------可能输出乱码,可能程序直接崩溃。
修正方法:
c
// 方法一:静态局部变量
char *get_hello() {
static char s[] = "hello"; // static 让 s 存放在静态区,函数结束后仍然有效
return s;
}
// 方法二:动态分配(调用者负责 free)
char *get_hello() {
char *s = malloc(6);
s[0] = 'h'; s[1] = 'e'; s[2] = 'l'; s[3] = 'l'; s[4] = 'o'; s[5] = '\0';
return s;
}
面试题三:NULL 是什么?为什么解引用 NULL 会崩溃?
c
int *p = NULL;
printf("%d", *p); // 程序崩溃:Segmentation Fault
答案 :C 语言标准规定地址 0(即 NULL)是「不可访问」的。操作系统将地址 0 所在的内存页设置为完全禁止访问,所以任何对它读或写的操作都会被操作系统直接拦截,导致程序异常终止。
为什么这样设计? 因为 NULL = 0 本身是一个约定俗成的「空地址」标记。如果操作系统允许访问地址 0,你可能会无意中读到或写到一个有意义的地方,产生难以调试的错误。禁止访问反而是一种保护机制。
本章总结
const 修饰指针的四种情况:
int *p; → 普通指针,两个都能改
const int *p; → 内容不能改,方向可以改
int * const p; → 内容可以改,方向不能改
const int * const p; → 两个都不能改
野指针四大成因:
| 成因 | 关键词 |
|---|---|
| 未初始化 | int *p; → *p = 100; |
| 越界访问 | arr[10],循环写到 i<=10 |
| 返回局部变量地址 | return &n; |
| free 后未置 NULL | free(p); *p = 100; |
规避野指针四条铁律:
① 初始化为 NULL
② 不越界
③ 用完置 NULL,用前检查
④ 不返回局部变量地址