Skip to content

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 已析构,返回的地址成为悬空指针。它有时还能打印,只是因为那块内存尚未被覆盖,并不代表代码正确。

二、本课目标 ​

  1. 区分自动存储期、动态存储期和静态存储期。
  2. 理解对象、地址、指针和引用的关系。
  3. 识别空指针、野指针、悬空指针和越界。
  4. 明白 const 在指针两侧的含义。
  5. 用 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&? ​

对象必须存在、函数只读且复制成本明显时。小型标量按值传递通常更直接。

十三、本课总结 ​

地址不是生命周期,非空也不代表有效。接口应该通过值、引用、指针和所有权类型表达对象是否必需、是否可变、由谁释放。