外观
Stage07|Modern C++ Foundation
Lesson082|Stack、Heap、Pointer 与 Reference
Tags: #C++17 #Memory #Pointer #ReferenceDifficulty: ⭐⭐⭐⭐☆
一、问题场景:函数返回后指针为什么坏了
cpp
const char* makeName() {
std::string name = "Player";
return name.c_str();
}函数返回后 name 已析构,返回的地址成为悬空指针。它有时还能打印,只是因为那块内存尚未被覆盖,并不代表代码正确。
二、本课目标
- 区分自动存储期、动态存储期和静态存储期。
- 理解对象、地址、指针和引用的关系。
- 识别空指针、野指针、悬空指针和越界。
- 明白
const在指针两侧的含义。 - 用 RAII 容器替代裸
new/delete。
三、回到 TaskSystem:任务对象放在哪里
第一版可以让 runTask() 只打印文字,但真实系统需要保存任务输入。局部对象适合短暂计算,动态对象适合跨作用域转移,但“放到 Heap”本身不等于解决生命周期。先决定任务由队列拥有,还是由调用者借用,再选择值、引用、指针或智能指针。
四、Stack 和 Heap 是工程模型
局部自动对象通常随作用域结束而析构:
cpp
void tick() {
int score = 10;
std::string name = "Player";
} // 两个对象的生命周期在这里结束动态对象由分配操作创建,必须有明确所有者:
cpp
auto values = std::make_unique<int[]>(128);“栈快、堆慢”只是粗略经验。真正重要的是生命周期、分配次数、局部性和所有权。编译器也可能进行逃逸分析和优化,不能把教学图当作物理内存保证。
五、指针与引用
cpp
void resetByPointer(Player* player) {
if (player) player->hp = 100;
}
void resetByReference(Player& player) {
player.hp = 100;
}指针可以表达“可能没有对象”,引用通常表达“调用期间必须存在”。二者都不自动拥有对象。
text
T* 可为空、可改指向、通常表示观察或可选参数
T& 不为空语义、不能重新绑定、通常表示必需参数
const T& 只读借用,常用于避免大对象复制六、const 的位置
cpp
const int* p1; // 指向的 int 不可通过 p1 修改
int* const p2 = &x; // p2 本身不能改指向
const int* const p3 = &x; // 两者都不可改从变量名向两侧读,能减少记忆错误。
七、数组、指针和边界
裸指针不携带长度:
cpp
void sum(const int* data, std::size_t size);现代接口优先使用能表达边界的类型,例如 C++20 的 std::span,或在 C++17 使用 std::vector、std::array 和迭代器区间。
八、可编译验证:地址与生命周期
cpp
#include <iostream>
#include <memory>
#include <vector>
int main() {
int local = 7;
int* observer = &local;
int& alias = local;
auto owned = std::make_unique<int>(9);
std::vector<int> values{1, 2, 3};
alias = 8;
std::cout << &local << ' ' << observer << ' ' << *owned << '\n';
std::cout << values.at(2) << '\n';
}用 -Wall -Wextra -pedantic 编译。再故意访问 values[10],分别比较 operator[]、at() 和 AddressSanitizer 的行为。
bash
g++ -std=c++17 -Wall -Wextra -fsanitize=address -g main.cpp -o memory_lab九、TaskSystem 的接口选择
cpp
void submit(Task task); // 复制/移动一份任务,队列拥有它
void inspect(const Task& task); // 只读借用,调用期间必须有效
bool trySubmit(Task* optional); // 可选观察,不转移所有权接口先表达所有权,再谈性能。若队列保存调用方局部对象的引用,函数返回后就会悬空;若把所有东西都改成裸指针,调用者又无法知道谁负责释放。
十、典型故障诊断
| 症状 | 高风险原因 | 证据 |
|---|---|---|
| 随机崩溃 | 悬空或越界 | ASan 堆栈、对象销毁日志 |
| 同一地址内容变化 | 生命周期结束后被复用 | 断点和写入监视 |
| 内存持续增长 | 所有权丢失 | 分配/释放计数、泄漏报告 |
| 空地址崩溃 | 未检查可选指针 | 崩溃地址和调用参数 |
十一、Cocos/Native 连接
JSB 暴露给 JavaScript 的 C++ 对象可能被两套生命周期管理。一个非空 native 指针不等于对象仍有业务效力;跨线程回调也不能持有短生命周期栈对象的地址。优先让异步任务复制必要数据或持有明确的智能指针。
十二、练习与答案
1. 引用是否拥有对象?
不拥有。引用只是别名;被引用对象必须比引用的使用期更长。
2. 为什么返回局部变量的值通常安全,返回其地址不安全?
返回值会复制或移动到调用方;地址仍指向已结束生命周期的局部对象。
3. 什么情况下参数适合用 const T&?
对象必须存在、函数只读且复制成本明显时。小型标量按值传递通常更直接。
十三、本课总结
地址不是生命周期,非空也不代表有效。接口应该通过值、引用、指针和所有权类型表达对象是否必需、是否可变、由谁释放。
