第12讲:深入理解指针(2)——指针的“安全锁”与“传址魔法”
🧭 第12讲:深入理解指针(2)——指针的“安全锁”与“传址魔法” 🔐✨
指针进阶:学会保护、规避风险、高效传参!
📚 目录
const
修饰指针:给指针上“安全锁” 🔒- 野指针:指针界的“野狗” 🐕
assert
断言:程序员的“调试哨兵” 🛡️- 指针的使用与传址调用:让函数真正“改变世界” 🔄
- 学习收获总结 ✅
const
修饰指针:给指针上“安全锁” 🔒
🔐 const
修饰变量:只读的“保护罩”
变量默认可修改,但 const
可以让它“只读”。
int m = 0;
m = 20; // ✅ 合法const int n = 0;
n = 20; // ❌ 编译错误!n 被 const 保护
📌 本质:const
是语法层面的限制,防止直接修改变量。
⚠️ 但“绕路”修改是可能的(不推荐):
const int n = 0;
int *p = &n; // 警告:类型不匹配
*p = 20; // ❌ 虽可能成功,但破坏了 const 的初衷
🎯 问题:如果
n
被const
修饰,就不该被修改。
✅ 解决方案:用const
修饰指针,防止通过指针篡改!
🧩 const
修饰指针变量:位置决定权限
const
在 *
左右,意义大不同!
指针声明 | 含义 | 可修改? |
---|---|---|
int *p | 普通指针 | 指向内容 ✅,指针本身 ✅ |
const int *p 或 int const *p | 指向常量的指针 | 指向内容 ❌,指针本身 ✅ |
int * const p | 常量指针 | 指向内容 ✅,指针本身 ❌ |
const int * const p | 指向常量的常量指针 | 全部 ❌ |
✅ 代码验证
// 1. const 在 * 左边:内容不可改
const int *p1 = &n;
// *p1 = 10; // ❌ 错误:不能通过 p1 修改内容
p1 = &m; // ✅ 正确:可以改变 p1 指向// 2. const 在 * 右边:指针本身不可改
int * const p2 = &n;
*p2 = 10; // ✅ 正确:可以通过 p2 修改内容
// p2 = &m; // ❌ 错误:不能改变 p2 的指向// 3. 两边都有 const:完全锁定
const int * const p3 = &n;
// *p3 = 10; // ❌
// p3 = &m; // ❌
📌 记忆口诀:
“左定内容,右定指针”
const
在*
左 → 内容不能变
const
在*
右 → 指针不能变
野指针:指针界的“野狗” 🐕
⚠️ 什么是野指针?
野指针:指向无效或未知内存地址的指针。
访问野指针可能导致程序崩溃、数据损坏!
🚨野指针三大成因
❌ 成因1:指针未初始化
局部指针变量未初始化时,值是随机的垃圾值!
int *p; // 野指针!p 的值是随机的
*p = 100; // ❌ 危险!写入未知地址,程序崩溃
❌ 成因2:指针越界访问
指针访问了数组范围之外的内存。
int arr[10] = {0};
int *p = &arr[0];
for (int i = 0; i <= 11; i++) {*(p++) = i; // ❌ 当 i=10,11 时,p 越界 → 野指针
}
❌ 成因3:指向已释放的空间
函数返回局部变量的地址,局部变量在函数结束后被销毁。
int* test() {int n = 100;return &n; // ❌ 危险!n 是局部变量,函数结束后空间释放
}int main() {int *p = test();printf("%d\n", *p); // ❌ 野指针访问,结果未定义return 0;
}
✅如何规避野指针?
🛡️ 指针初始化
- 明确指向 → 直接赋地址
- 不明确 → 赋值为
NULL
int num = 10;
int *p1 = # // ✅
int *p2 = NULL; // ✅ 安全的“空值”
📌
NULL
是一个宏,值为0
,表示“空地址”,不可读写。
#define NULL ((void*)0) // 标准定义
🛡️ 小心越界
- 指针只能访问申请过的内存范围。
- 循环条件要精确,避免
<=
误用。
🛡️指针不再使用时置 NULL
,使用前检查有效性 ✅
当指针完成使命后,及时将其置为 NULL
,这是一种极佳的编程习惯。
🌳 为什么? 约定俗成的规则是:绝不访问
NULL
指针。
这样做相当于把“野狗”拴在树上,使其不再危害系统。
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];// 使用指针遍历数组
for (int i = 0; i < 10; i++) {*(p++) = i;
}// 使用完毕,及时“拴住野狗”
p = NULL; // ✅ 野指针被“管理”起来
🐕 使用前检查:绕开“拴住的野狗”
即使野狗被拴住,我们也要绕着走,不能去挑逗它。
对于指针,使用前必须检查是否为NULL
:
// 下次使用前,先检查
if (p != NULL) {// 安全使用 p
} else {p = &arr[0]; // 重新赋有效地址if (p != NULL) {// 再次确认后使用}
}
📌 核心原则:
- 用完即置
NULL
→ 防止后续误用 - 使用前必检查 → 确保指针有效
🎯 比喻升华:
- 野指针 = 野狗 → 放任不管,危害系统
p = NULL
= 拴狗绳 → 将风险“管理”起来if (p != NULL)
= 观察狗是否被拴住 → 安全第一
🛡️ 避免返回局部变量地址
- 局部变量生命周期仅限函数内。
- 如需返回数据,考虑:
- 静态变量(
static
) - 动态内存(
malloc
) - 传入指针参数
- 静态变量(
assert
断言:程序员的“调试哨兵” 🛡️
🚨 什么是 assert
?
assert
是 <assert.h>
中的宏,用于运行时断言检查。
#include <assert.h>
assert(p != NULL); // 如果 p 为 NULL,程序终止并报错
✅ assert()
的工作原理
assert()
接受一个表达式作为参数:
条件 | 结果 |
---|---|
表达式为真(非零) | assert() 不产生任何作用,程序继续运行 |
表达式为假(为零) | assert() 触发,程序终止,并在标准错误流 stderr 中输出一条错误信息,内容包括: • 失败的表达式 • 文件名 • 行号 |
📌 示例:若
p == NULL
,则报错:Assertion failed: p != NULL, file example.c, line 10
🌟 assert()
的优势
- 自动定位错误:无需手动打印,自动显示文件名和行号,快速定位问题源头。
- 可开关控制:无需修改代码即可全局启用/禁用。
- 提高代码健壮性:在开发阶段捕获逻辑错误,防止程序带病运行。
🔧 如何关闭 assert()
?
在 #include <assert.h>
前定义宏 NDEBUG
:
#define NDEBUG
#include <assert.h>
// 所有 assert() 被编译器忽略
🔄 灵活切换:
- 出现问题?→ 注释掉
#define NDEBUG
→ 重新编译 →assert()
重新启用- 确认无误?→ 取消注释 → 重新编译 →
assert()
被禁用
⚠️ assert()
的缺点
- 增加运行时间:每次执行都引入额外的检查,影响性能。
🎯 最佳实践:Debug 与 Release 版本
版本 | assert() 状态 | 说明 |
---|---|---|
Debug(调试版) | ✅ 启用 | 帮助程序员快速排查问题 |
Release(发布版) | ✅ 禁用 | 通常在编译时自动优化掉,不影响用户程序效率 |
📌 集成开发环境(如 VS)的行为:
在 Release 模式下,默认会定义NDEBUG
,从而自动移除所有assert()
检查,确保发布版本的高性能。
指针的使用与传址调用:让函数真正“改变世界” 🔄
🧩 strlen
模拟实现
目标:统计字符串 \0
之前的字符个数。
#include <assert.h>int my_strlen(const char *str) {assert(str != NULL); // 断言指针非空int count = 0;while (*str != '\0') {count++;str++;}return count;
}int main() {int len = my_strlen("abcdef");printf("长度: %d\n", len); // 输出 6return 0;
}
📌 关键点:
- 参数用
const char*
:保证函数内不修改字符串 - 使用
assert
:防止空指针传入 - 指针遍历:
str++
逐个字符移动
🔄 传值调用 vs 传址调用
❌ 传值调用:失败的交换
void Swap1(int x, int y) {int tmp = x;x = y;y = tmp; // ❌ 只交换了形参,不影响实参
}int main() {int a = 10, b = 20;Swap1(a, b); // a, b 的值没变return 0;
}
📌 传值调用特点:
- 实参 → 形参:值拷贝
- 形参是独立副本,修改不影响实参
✅ 传址调用:成功的交换
void Swap2(int *px, int *py) {int tmp = *px;*px = *py;*py = tmp; // ✅ 通过地址修改主函数变量
}int main() {int a = 10, b = 20;printf("交换前: a=%d, b=%d\n", a, b);Swap2(&a, &b); // 传地址printf("交换后: a=%d, b=%d\n", a, b);return 0;
}
✅ 输出:
交换前: a=10, b=20 交换后: a=20, b=10
📌 传址调用特点:
- 传递变量的地址
- 函数内通过
*指针
间接访问和修改主函数变量- 实现了函数对外部数据的真正修改
🎯 何时使用传址调用?
场景 | 调用方式 |
---|---|
仅读取数据、计算 | ✅ 传值调用 |
需要修改主函数变量 | ✅ 传址调用 |
传递大型结构体(避免拷贝开销) | ✅ 传址调用 |
🌟 核心价值:
传址调用让函数与主调函数建立真实联系,实现数据的双向交互。
✅ 学习收获总结
技能 | 掌握情况 |
---|---|
✅ 理解 const 修饰指针的四种形式及权限 | ✔️ |
✅ 掌握野指针的三大成因及规避方法 | ✔️ |
✅ 熟练使用 assert 进行调试断言 | ✔️ |
✅ 理解传值与传址调用的本质区别 | ✔️ |
✅ 能编写安全、高效的指针函数 | ✔️ |
🎯 指针是C语言的“双刃剑”:
用得好,代码高效灵活;用不好,程序崩溃难调。
你已掌握安全与传参的核心,继续前行,成为指针大师!💪🔥
💬 需要本讲的
assert
使用场景清单 / 野指针检测工具 / 传址调用练习题?欢迎继续提问,我为你准备!