C语言指针进阶:const 和野指针——给指针加锁,向野指针宣战

适用对象:学完指针初阶,了解地址和解引用的同学

本章目标:掌握 const 修饰指针的三种写法 + 彻底理解野指针的成因与规避方法


引入:两把锁,一把锁谁?

上一章我们学了指针的基础操作------指针可以读取修改它指向的内存,就像一把万能钥匙,能开能改。

但有时候我们只想让它能读不能改 ,或者固定指向不许乱动------就像给锁加了一把限制,让万能钥匙只能在特定范围内使用。

这一章我们就来学两件事:

  1. const ------ 给指针加「锁」,限制它的读写权限
  2. 野指针 ------ 那些乱指的指针,为什么危险,怎么避开

第一部分: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,用前检查

④ 不返回局部变量地址

相关推荐
dok121 小时前
Johannes 《Linux内核模块与设备驱动开发:编写Linux驱动程序》 (11)kmalloc 动态内存分配和struct file中的私有数据
linux·驱动开发·算法
hansang_IR1 小时前
【题解】[COCI 2024/2025 #2] 流明 / Blistavost
c++·算法·动态规划·dp
程序喵大人1 小时前
【C++进阶】STL算法与函数对象 - 01 让算法去处理一段迭代器范围
开发语言·c++·算法·函数对象
Keven_111 小时前
算法札记:二叉树前序、中序、后序遍历及对应序列重要性质
数据结构·算法·深度优先
cui_ruicheng1 小时前
Python数据分析(十三):Seaborn 统计可视化
开发语言·python·信息可视化·数据分析
Delite8021 小时前
微库仑氧化法硫氯检测:油品分析中测量误差的控制要点
算法
宵时待雨1 小时前
优选算法专题12:队列
算法·leetcode·职场和发展
tianyu2341 小时前
双指针循环匹配金额——打款记录与账单的自动对账算法
java·算法·双指针循环·匹配金额