楔子
- make_number 方法中 Amf0Value a; a 是局部变量,return a 以后,a 离开了它的作用域,不会失效吗
C++
static Amf0Value make_number(double v) {
Amf0Value a;
a.type = AMF0_NUMBER;
a.number = v;
return a;
}
黄金法则
看这个"东西"在函数返回之后,是否还"活着"。活着 → 安全;已经被销毁了却还在引用它 → 悬空(dangling),未定义行为。
逐个场景过一遍
1. 传值返回(return by value)------ 永远安全
cpp
Amf0Value make_number(double v) {
Amf0Value a; // 局部变量
return a; // 返回的是"值",销毁a之前会先把内容搬给调用者
}
调用者拿到的是独立的新数据,跟原来的 a 无关。a 死不死不重要。这是 amf0.h 里 make_number/make_bool/make_string 这些工厂方法的写法,天然安全。
为什么安全 :函数返回类型是 Amf0Value(值),不是 Amf0Value&(引用)也不是 Amf0Value*(指针)。return a; 这一步,是在 a 被销毁之前,先把 a 的内容拷贝 (或者移动)一份给调用者。等这个"搬运"动作完成之后,函数才真正结束,局部变量 a 才被销毁------调用者手里的东西跟 a 已经没关系了。
类比 Java :Java 里 Amf0Value a = new Amf0Value();,a 本质是一个指向堆上对象 的引用,方法返回 a 返回的也是这个引用(地址),指向的还是堆上同一个对象,对象生死 由 GC 管,跟方法作用域无关。C++ 不一样:Amf0Value a; 默认是栈上的真实对象,作用域 结束是真的会销毁的。正因为这样,C++ "返回值"这个动作,语义上就是"把值搬到外面", 而不是"返回一个指向它的地址"。
2. 返回局部变量的引用/指针 ------ 永远危险 ⚠️
cpp
Amf0Value& make_number_bad(double v) {
Amf0Value a;
return a; // 函数结束 a 被销毁,这个引用悬空
}
Amf0Value* make_number_bad2(double v) {
Amf0Value a;
return &a; // 同样悬空
}
记住"局部变量 + 引用/指针返回"这个组合就行------这是最经典也是最容易踩的坑。
3. 返回参数的引用 ------ 看参数本身是什么✅✅✅✅
cpp
// 安全:外面传进来的对象,函数结束后它在调用者那边继续活着
const std::string& first_nonempty(const std::string& a, const std::string& b) {
return a.empty() ? b : a; // a、b 都是调用者的对象,没被这个函数销毁
}
// 危险:value 是"传值"进来的参数,本质也是这个函数的局部变量
const std::string& make_bad(std::string value) {
return value; // value 是这个函数自己的局部拷贝,函数结束就销毁了
}
4. 返回类成员变量的引用/指针 ------ 看这个对象(*this)活多久
cpp
struct Amf0Value {
std::vector<std::pair<std::string, std::shared_ptr<Amf0Value>>> obj;
const Amf0Value* get(const std::string& key) const {
for (auto& kv : obj) if (kv.first == key) return kv.second.get();
return nullptr;
}
};
这个 get() 项目里就有------只要拿到这个指针的时候,那个 Amf0Value 对象本身还没被 销毁,就是安全的。但如果把这个对象整个销毁了,之前 get() 拿到的指针立刻变悬空, 这不是函数的问题,是调用方要自己保证"对象还活着才能用它返回的指针"。
5. 更隐蔽的近亲问题:容器重新分配导致的失效
这个跟"局部变量"无关,但是同一类"东西已经不在了却还在用"的问题,值得一起记住:
cpp
const Amf0Value* p = obj_value.get("app"); // 假设此刻拿到了一个指针
obj_value.set("newKey", ...); // vector push_back,可能触发扩容重新分配内存
// p 现在可能已经悬空了!因为 vector 扩容时会把所有元素搬到新内存,
// 旧内存被释放,p 指向的地址不再有效
这叫迭代器/引用失效(iterator/reference invalidation) ,跟局部变量销毁是两码事, 但本质都是"你手里的指针/引用指向的内存已经不归你想的那个对象用了"。规则:只要还 可能对容器做增删操作,就不要长期持有指向它内部元素的指针/引用。
6. new 出来的对象、shared_ptr ------ 不会悬空,但换了一种风险
cpp
Amf0Value* p = new Amf0Value(); // 堆上分配,函数结束不会自动销毁
return p; // 这个指针指向的对象还活着,不悬空
- 这种不会"失效",但换成了谁负责
delete它的问题------忘记删就是内存泄漏,删两次 就是 double free ⚠️ ⚠️ ⚠️ 。 Amf0Value::obj用std::shared_ptr而不是裸指针,就是为了让这 块内存的生命周期自动管理,不用手动跟踪该不该 delete,这也是现代 C++ 处理这类问题的 标准做法。
一张表记住所有情况
| 返回什么 | 安不安全 | 原因 |
|---|---|---|
| 局部变量的值 | ✅ 安全 | 销毁前先拷贝/移动出去 |
| 局部变量的引用/指针 | ❌ 危险 | 函数结束就销毁,引用/指针悬空 |
| 传引用进来的参数的引用 | ✅ 安全 | 对象在调用者那边,函数没有权力销毁它 |
| 传值进来的参数的引用 | ❌ 危险 | 参数本身也是局部变量 |
| 类成员的引用/指针 | ⚠️ 看情况 | 只要 *this 对象还活着就安全 |
| 容器内部元素的指针(之后又改了容器) | ❌ 危险 | 容器扩容/删除会让内部指针失效 |
new/shared_ptr 管理的对象 |
✅ 不悬空 | 但要小心生命周期归属(泄漏/重复释放) |
记忆口诀:先问自己"这块内存归谁管、什么时候会被回收",再看你手里的引用/指针会 不会活得比它管理的内存还长。
顺带一提:RVO(返回值优化)
- 现代 C++ 编译器在"传值返回局部变量 "这种场景下,通常会做 RVO(Return Value Optimization)------直接把局部变量构造在调用者接收返回值的那块内存上,连拷贝这一步都 省了。
- 就算编译器没做这个优化,C++11 之后也会自动优先用"移动"而不是"拷贝"(
Amf0Value里的std::string、std::vector这些成员移动起来很便宜)。 这是性能层面的细节,不影 响"安不安全"这个问题------安全性上,只要是传值返回,就没有失效风险。