C/C++ 入门:指针
指针难,不是因为语法多,而是因为它要求你开始从“变量的值”进一步看到“变量在内存里的位置”。
📚 基本概念速读
| 名称 | 定义 | 省流 |
|---|---|---|
| 地址 | 变量在内存中的位置编号 | 住址 |
| 指针 | 保存地址的变量 | 存地址 |
| 取地址 | 用 & 得到变量地址 |
问住址 |
| 解引用 | 用 * 通过地址访问目标值 |
按地址找值 |
| 栈 | 自动管理的函数调用内存区域 | 自动生灭 |
| 堆 | 手动申请、手动释放的内存区域 | 手动管理 |
| 空指针 | 不指向任何有效对象的指针 | 指向空 |
| 野指针 | 指向未知或无效位置的指针 | 危险地址 |
| 悬空指针 | 原对象已失效但指针还留着 | 地址过期 |
🧩 程序内存分区极简认识
程序运行时,内存不是一整块随便用,而是可以粗略分成几个区域。初学阶段不需要记所有细节,先建立心智模型。
1 | +----------------------+ |
更简单地说:
| 区域 | 放什么 | 谁管理 | 常见特点 |
|---|---|---|---|
| 栈 | 局部变量、函数参数 | 编译器和运行时自动管理 | 快,函数结束自动释放 |
| 堆 | 手动申请的大块数据 | 程序员管理 | 灵活,但容易泄漏 |
| 全局区 | 全局变量、static 变量 |
程序启动到结束 | 生命周期长 |
| 常量区 | 字符串字面量、只读数据 | 编译器和系统管理 | 通常不允许修改 |
这只是入门模型。不同系统和编译器的真实内存布局会更复杂,但这个模型足够帮助理解指针。
📦 栈内存
局部变量生命周期
在函数里定义的普通局部变量,通常位于栈上。
1 | int add(int a, int b) |
这里的 a、b、result
都和这次函数调用绑定。函数开始时它们被创建,函数结束时它们被销毁。
可以这样理解:
1 | 调用 add(3, 5) |
函数结束自动销毁
看一个危险例子:
1 | int* bad() |
x
是局部变量,函数结束后就没了。return &x;
返回的是一个已经失效的地址。
初学阶段先记住:不要返回局部变量的地址。
🧱 堆内存
手动分配
堆内存的生命周期不受函数作用域直接限制。C++ 里可以用 new
申请,用 delete 释放。
1 | int* p = new int; |
这段代码做了几件事:
| 代码 | 含义 |
|---|---|
new int |
在堆上申请一个 int 空间 |
int* p |
用指针 p 保存这块空间的地址 |
*p = 10; |
通过地址把值写进去 |
delete p; |
释放这块堆内存 |
p = nullptr; |
避免指针继续保存旧地址 |
生命周期不受函数限制
下面这个函数返回堆内存地址是可以的:
1 | int* createNumber() |
因为 new int 申请的是堆内存,不会随着
createNumber 函数结束自动销毁。
但代价是:谁申请,谁负责释放。忘记
delete 会造成内存泄漏。
📍 地址、取地址与解引用
地址
每个变量都存放在内存中的某个位置,这个位置可以理解为地址。
1 | int x = 10; |
&x 表示取变量 x 的地址。
指针变量
指针变量专门用来保存地址。
1 | int x = 10; |
读法:
| 代码 | 含义 |
|---|---|
int x = 10; |
创建一个整数变量 |
&x |
得到 x 的地址 |
int* p |
创建一个指向 int 的指针 |
p = &x |
让 p 保存 x 的地址 |
解引用
*p 表示通过指针 p
保存的地址,访问那个地址上的值。
1 | int x = 10; |
可以粗略理解为:
1 | x: 10 |
* 的两个常见含义
* 在不同位置含义不同:
| 写法 | 含义 |
|---|---|
int* p |
定义一个指针变量 |
*p = 20 |
解引用,通过指针访问目标值 |
这也是指针语法容易让新手混乱的地方。
🕳️ nullptr
空指针
如果一个指针暂时不指向任何有效对象,应该让它等于
nullptr。
1 | int* p = nullptr; |
nullptr 的意思是:这个指针现在不保存有效地址。
判空
使用指针前,常见做法是先判断它是否为空:
1 | int* p = nullptr; |
也可以简写:
1 | if (p) { |
初学阶段建议写清楚 p != nullptr,可读性更好。
避免和 NULL 混用
C 语言里常见 NULL,现代 C++ 更推荐
nullptr。
1 | int* p1 = nullptr; // 推荐 |
原因是 nullptr 有明确的空指针类型,更适合 C++
的类型检查。
🔧 指针做函数参数
修改外部变量
值传递只能修改副本:
1 | void change(int x) |
指针参数可以修改外部变量:
1 |
|
调用 change(&x) 时,传进去的是 x
的地址。函数内部用 *p 找到原来的变量并修改它。
传数组
数组作为函数参数时,通常会退化成指针。
1 | void printArray(int* arr, int length) |
这里的 arr 可以理解为指向数组第一个元素的指针。
传缓冲区
指针也常用来传一段可写缓冲区。
1 | void fillZero(int* buffer, int length) |
传缓冲区时,通常要同时传长度。只传指针,函数不知道后面有多少个元素。
🧮 指针与数组
数组名退化
很多表达式里,数组名会退化为指向首元素的指针。
1 | int nums[3] = {10, 20, 30}; |
nums 在这里相当于 &nums[0]。
1 | cout << nums << endl; |
这两个输出通常会看到相同的地址。
指针遍历数组
可以通过指针移动遍历数组:
1 | int nums[3] = {10, 20, 30}; |
p + i 不是简单加 i 个字节,而是向后移动
i 个 int 的位置。
也可以写成更常见的数组下标:
1 | for (int i = 0; i < 3; i++) { |
p[i] 本质上可以理解为 *(p + i)。
越界风险
指针不会自动知道数组长度。
1 | int nums[3] = {10, 20, 30}; |
p[3] 访问的是第 4 个元素,但数组只有 3
个元素。越界访问可能输出奇怪值,也可能直接崩溃。
初学阶段写数组函数时,尽量把指针和长度一起传:
1 | void printArray(int* arr, int length) |
🧷 指针数组、字符指针、数组指针
这三个名字很像,但含义不同。
指针数组
指针数组本质上是数组,数组里的每个元素都是指针。
1 | int a = 10; |
读法:arr 是一个数组,里面放了 3 个
int*。
字符指针
字符指针常用来指向字符串字面量:
1 | const char* text = "hello"; |
这里建议写
const char*,因为字符串字面量通常位于只读区域,不应该被修改。
不要这样写:
1 | char* text = "hello"; // 不推荐 |
如果需要可修改的字符数组,应写成数组:
1 | char text[] = "hello"; |
数组指针
数组指针本质上是指针,它指向一个数组。
1 | int nums[3] = {10, 20, 30}; |
int (*p)[3] 的读法是:p
是一个指针,指向一个包含 3 个 int 的数组。
注意括号很关键:
| 写法 | 含义 |
|---|---|
int* arr[3] |
指针数组:数组里有 3 个 int* |
int (*p)[3] |
数组指针:指向含 3 个 int 的数组 |
⚠️ 野指针与悬空指针
未初始化指针
未初始化指针保存的是不确定地址。
1 | int* p; |
正确做法:
1 | int* p = nullptr; |
指针定义时,如果暂时没有明确指向,就先设为 nullptr。
释放后继续使用
释放堆内存后,指针里的旧地址还在,但那块内存已经不属于你。
1 | int* p = new int; |
释放后建议立刻置空:
1 | delete p; |
这样后续可以通过判空避免继续使用旧地址。
栈内存销毁后指针残留
下面这个例子会产生悬空指针:
1 | int* bad() |
函数结束后,x
已经销毁,但外面拿到的指针还残留着原来的地址。
这种指针看起来像有值,实际上已经不能安全使用。
常见危险总结
| 问题 | 示例 | 结果 |
|---|---|---|
| 未初始化 | int* p; |
指向未知位置 |
| 空指针解引用 | *nullptr |
程序崩溃风险 |
| 释放后使用 | delete p; *p |
使用已释放内存 |
| 返回局部变量地址 | return &x; |
指向已销毁栈对象 |
| 数组越界 | arr[100] |
访问不属于自己的内存 |
✅ 指针使用最小准则
| 准则 | 原因 |
|---|---|
| 指针定义时初始化 | 避免野指针 |
不知道指向哪里就用 nullptr |
明确表示空 |
| 解引用前先确认有效 | 避免崩溃 |
new 后要 delete |
避免内存泄漏 |
delete 后置为 nullptr |
避免悬空指针误用 |
| 数组指针参数同时传长度 | 避免越界 |
| 不返回局部变量地址 | 栈内存函数结束会失效 |
✅ 总结
指针的本质是地址;学指针,就是学会通过地址间接访问和管理内存。
先把三件事分清:&
取地址、指针保存地址、*
通过地址访问值。再逐步理解栈、堆、数组退化和悬空指针,指针就不会只是一堆符号。
Happy Hacking! 🎉