很多人第一次看到 return a; 时,都会担心局部变量离开作用域后马上销毁,返回值是不是也跟着失效。这个疑问很典型,但真正的判断标准并不是“它是不是局部变量”,而是函数返回后,你手里拿到的到底是一个独立的值,还是仍然依附于原对象生命周期的引用或指针。下面按几种常见场景拆开讲,顺手把容易混淆的容器失效、成员返回和 RVO 一起梳理清楚。
判断核心:对象还活着,返回结果才安全
判断一个返回值能不能安全使用,核心逻辑可以压缩成一句话:函数返回后,被你访问的那块对象或内存是否还活着。
如果对象仍然存在,那么返回结果就是安全的;如果对象已经被销毁,而你保留的又是指向它的引用或指针,那么这就是典型的悬空(dangling),继续使用会落入未定义行为。
所以问题的重点从来不是“函数结束了没有”,而是“返回出去的东西,是否还依赖函数内部那份已经结束生命周期的对象”。
传值返回为什么安全
先看最容易被误判的一种情况:

static Amf0Value make_number(double v) {
Amf0Value a;
a.type = AMF0_NUMBER;
a.number = v;
return a;
}
这段代码是安全的。原因很简单:函数返回类型是 Amf0Value,也就是按值返回。调用者拿到的是一个独立的返回值,而不是局部变量 a 本身的别名。
从语义上说,return a; 的意思不是“把 a 的地址交出去”,而是“在 a 被销毁前,把它的值交给调用者”。不管这个过程最终表现为拷贝、移动,还是编译器直接做返回值优化,调用者拿到的都不是那个即将销毁的局部对象本体。
这也是 amf0.h 里 make_number、make_bool、make_string 这类工厂函数常见且推荐的写法:语义清晰,生命周期天然安全。
如果你熟悉 Java,可以做个反向对比。Java 里 new Amf0Value() 通常得到的是一个对象引用;而在 C++ 中,Amf0Value a; 默认就是一个真实对象。也正因为如此,C++ 的“返回值”本质上是把值交出去,而不是把局部对象继续借给外部使用。
哪些写法会直接产生悬空引用或指针
真正危险的是返回局部变量的引用或指针:
Amf0Value& make_number_bad(double v) {
Amf0Value a;
return a; // 函数结束 a 被销毁,这个引用悬空
}
Amf0Value* make_number_bad2(double v) {
Amf0Value a;
return &a; // 同样悬空
}
这里的问题不在 a 是局部变量,而在于你返回的是“指向 a 的通道”。函数一结束,a 生命周期就结束了,但外部手里还留着它的引用或地址,于是悬空。
这个组合可以直接记成一条高危规则:局部变量 + 返回引用/指针 = 基本必错。只要看到这种模式,就应该立刻警觉。
返回参数或成员时,关键看谁负责生命周期
返回参数的引用:先区分引用传参还是值传参
// 安全:外面传进来的对象,函数结束后它在调用者那边继续活着
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 是这个函数自己的局部拷贝,函数结束就销毁了
}
这两段代码的差别,正好说明“看起来都是参数,生命周期却完全不同”。

如果参数类型是 const T&,函数只是借用了调用者提供的对象,函数本身不会销毁它,因此返回这个引用通常是安全的。
但如果参数类型是 T value,那它在函数体内已经变成了一份新的局部副本。此时把它的引用返回出去,本质上和“返回局部变量引用”没有区别,依然会悬空。
返回成员变量的引用或指针:看 *this 还能活多久
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 对象还存在,通常就是安全的;一旦宿主对象已经析构,之前拿到的指针也会立刻失效。这类接口的风险不在函数返回瞬间,而在调用方是否正确管理了对象生命周期。
更容易忽略的问题:容器扩容也会让指针失效
有些返回值问题并不是由函数作用域直接引起,但本质上仍然是“对象不在了,你还在继续用旧地址”。最典型的例子就是容器重新分配内存:
const Amf0Value* p = obj_value.get("app"); // 假设此刻拿到了一个指针
obj_value.set("newKey", ...); // vector push_back,可能触发扩容重新分配内存
// p 现在可能已经悬空了!因为 vector 扩容时会把所有元素搬到新内存,
// 旧内存被释放,p 指向的地址不再有效
这就是常说的迭代器、引用或指针失效。虽然它不是“函数结束后局部变量销毁”那一路问题,但判断方式完全一致:你手里的地址是否还对应原来的那块有效对象。
因此,只要一个容器后续还可能发生插入、删除、扩容等操作,就不适合长期持有指向其内部元素的裸指针或引用。尤其是 std::vector 这类连续存储容器,扩容时整体搬迁非常常见。
堆对象和智能指针:不会凭空悬空,但风险没有消失
Amf0Value* p = new Amf0Value(); // 堆上分配,函数结束不会自动销毁 return p; // 这个指针指向的对象还活着,不悬空
这里返回的不是局部栈对象,而是堆上分配的对象地址,所以函数结束时对象不会自动销毁。从“返回瞬间会不会悬空”这个角度看,它确实安全一些。
但风险只是换了个位置:谁来 delete 这块内存?如果没人释放,就是内存泄漏;如果多次释放,就是 double free。也就是说,它不再是“过早销毁”的问题,而变成了“生命周期归属不清”的问题。
现代 C++ 更推荐用智能指针管理这类对象。像文中提到的 std::shared_ptr,目的就是把释放责任绑定到对象所有权上,减少手工管理带来的泄漏和重复释放风险。
一张表快速判断常见返回方式
| 返回什么 | 安不安全 | 原因 |
|---|---|---|
| 局部变量的值 | ✅ 安全 | 销毁前先拷贝/移动出去 |
| 局部变量的引用/指针 | ❌ 危险 | 函数结束就销毁,引用/指针悬空 |
| 传引用进来的参数的引用 | ✅ 安全 | 对象在调用者那边,函数没有权力销毁它 |
| 传值进来的参数的引用 | ❌ 危险 | 参数本身也是局部变量 |
| 类成员的引用/指针 | ⚠️ 看情况 | 只要 *this 对象还活着就安全 |
| 容器内部元素的指针(之后又改了容器) | ❌ 危险 | 容器扩容/删除会让内部指针失效 |
| new/shared_ptr 管理的对象 | ✅ 不悬空 | 但要小心生命周期归属(泄漏/重复释放) |
实战里可以先问自己两个问题:这块内存归谁管理?它会在什么时候被回收?只要你的引用或指针活得比底层对象更久,问题基本就确定了。
RVO 解决的是性能,不是安全性争议
顺着“传值返回是否划算”这个问题,很多人还会想到 RVO(Return Value Optimization)。
- 现代 C++ 编译器在“返回局部对象值”这种场景下,通常会做 RVO,直接把对象构造到调用方接收返回值的位置上,省掉额外拷贝。
- 即便没有触发 RVO,C++11 之后也往往会优先使用移动语义,而不是昂贵的深拷贝。像
std::string、std::vector这样的成员,移动成本通常比想象中低得多。
但要注意,RVO 回答的是“这样写快不快”,不是“这样写安不安全”。安全性的结论前面已经很明确了:只要是按值返回局部对象,本身没有失效风险;真正需要警惕的,是把局部对象的引用或地址交给外部。









