传值返回
// 传值返回(非优化版本,强制拷贝构造)
Date func()
{
Date d1;
Date d2;
if (time(0) % 2)
{
return d1; // 编译器无法预测 取消RVO优化
}
else
{
return d2;
}
}
int main()
{
const Date& ref = func();
return 0;
}
看一下具体行为(非优化版本):
传值返回
画一个函数栈帧理解一下(优化版本):
┌──────────────────────────────┐
│ main() │
│ ┌──────────────────────────┐ │
│ │ ref = func() │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ func() │ │ │
│ │ │ ┌───────────────┐ │ │ │
│ │ │ │ Date d; │ │ │ │
│ │ │ │ (构造#1) │ │ │ │
│ │ │ └───────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌───────────────┐ │ │ │
│ │ │ │ return d; │ │ │ │
│ │ │ │ => 拷贝构造#2 │ │ │ │
│ │ │ └───────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ d 析构 (析构#1) │ │ │
│ │ └─────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ const Date& ref │ │ │
│ │ │ 绑定返回值的临时 │ │ │
│ │ │ (有时会额外复制) │ │ │
│ │ │ => 析构#2, 析构#3 │ │ │
│ │ └──────────────────────┘ │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘
总结:
[func 调用栈]
┌───────────────┐
│ Date d; │ ← 局部变量 d
│ │
│ return d; │ ← 拷贝构造!临时#1
└───────────────┘
│
▼
[main 调用栈]
┌───────────────┐
│ const Date& ref; │ ← 绑定返回值临时
└───────────────┘
析构:
1️⃣ 局部 d 析构(离开 func)
2️⃣ 返回值临时析构(ref 生命周期结束)
可能还有:
3️⃣ 有的编译器生成额外延续临时,也会析构
C++
值返回需要拷贝,RVO 省掉拷贝;const& 会延续临时,多次析构是自然现象。d
析构 1 次(func
结束),return
时临时值析构 1 次,main
里 ref
绑定的延续临时析构 1 次
传引用返回
而使用引用返回会生成d1
d2
的别名,减少了拷贝
ref
是tmp
,是d1
d2
的别名
看一下具体行为:
传引用返回
分析:
+----------------------------+
| main() |
| |
| const Date& ref = func(); |
| (悬空引用) |
+----------------------------+
|
▼
+----------------------------+
| func() |
| |
| +----------------------+ |
| | Date d1; | |
| | (栈上局部变量) | |
| +----------------------+ |
| |
| +----------------------+ |
| | Date d2; | |
| | (栈上局部变量) | |
| +----------------------+ |
| |
| if (...) return d1; |
| else return d2; |
| |
| 【func() 结束】 |
| ├─ d1 析构 |
| ├─ d2 析构 |
+----------------------------+
为什么没有拷贝?
func
的返回值是 Date&
(引用),不是按值返回。
所以 return d1
/ return d2
返回的就是 局部变量本身的别名,不需要拷贝,也就不会走拷贝构造。
为什么会析构两次?
1️⃣ d1
和 d2
都是 func
的局部变量,存在 func
的栈帧里。
2️⃣ if
分支随机选择:
return d1;
时,main
拿到 d1
的引用
return d2;
时,main
拿到 d2
的引用
无论你拿到谁的引用,func
一旦返回,d1
和 d2
都会自动析构。
所以 func
结束时: d1
会析构 d2
会析构
再加一段代码:
// 传引用返回
Date& func()
{
Date d1;
Date d2;
if (time(0) % 2)
{
return d1; // 编译器无法预测 取消优化
}
else
{
return d2;
}
}
int func1()
{
int a = 1;
int b = 2;
int c = 3;
return a + b + c;
}
int main()
{
Date& ref = func();
ref.Print();
return 0;
}
此时输出结果为:
为什么?我们看下函数栈帧
Step 1:
[func 栈帧]
┌───────────────┐
│ Date d1; │
│ Date d2; │
└───────────────┘
Step 2:
return d1/d2 的地址 ----> 被 ref 接住
[main 栈帧]
┌───────────────┐
│ Date& ref ----┼────────┐
└───────────────┘ │
│
▼
指向 func 的局部变量
Step 3:
func 返回后
--> func 栈帧销毁
--> d1/d2 内存仍然是旧的,但悬空
Step 4:
调用 func1()
[func1 栈帧]
┌───────────────┐
│ int a; │
│ int b; │
│ int c; │
└───────────────┘
⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️⬆️
这里可能会复用func 原来的栈空间
Step 5:
ref.Print() 访问到的其实是
a,b,c 占用的区域里的数据
===> 输出被覆盖的脏数据
C++
函数如果返回局部变量引用,栈帧销毁后,这个引用就悬空。
如果后面又有新的函数调用分配局部变量,就可能覆盖原来的内存区域。
这会导致引用指向的内容被篡改,输出是垃圾值,属于未定义行为。
#【核心原则】
什么时候用传值返回?
【场景】
返回一个临时的新对象
局部变量需要返回给调用者
返回结果和内部对象没必要共享(调用者想要自己的拷贝)
【常见例子】
操作符重载(比如
operator+
)工厂函数(生成新对象)
普通函数局部变量返回
想要触发拷贝/移动构造(可由 RVO 优化掉)
【优点】
不会有悬空引用/指针
生命周期安全,C++ 会自动管理临时对象
编译器可做 RVO/NRVO 优化
【示例】
Date AddDays(int n) { Date result = *this; result._day += n; return result; // 新对象 }
什么时候用传引用返回?
【场景】
返回调用者自己传进来的对象(必须是外部的)
返回成员变量的引用(对象内部状态,需要对外可修改)
返回容器里的元素引用
链式调用(比如
operator=
)
【常见例子】
赋值运算符重载
operator=
容器元素访问
operator[]
/.at()
返回引用成员 getter/setter(需要对外暴露可修改性)
【优点】
不会发生多余拷贝
可以直接对原对象修改
【关键风险】
不能返回局部变量引用!
只能返回:
外部传入的变量
成员变量
全局或
static
变量
【示例】
// 赋值运算符重载 Date& operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; // 返回自身引用,支持链式 a=b=c } // 成员变量引用 int& GetMonth() { return _month; }
## 【典型错误示范】
```cpp
Date& BadFunc() {
Date d; // 局部变量
return d; // ❌ 返回局部变量引用 = UB
}
解释:
离开作用域d
被析构,引用悬空,行为未定义。
总结:
在 C++ 里,传值返回和传引用返回的选择,核心看返回值要不要和原来的对象共享。
如果是局部变量或者新建对象,比如 operator+
,就必须传值返回,这样才能把局部结果安全拷贝或者移动出来。
如果是内部状态或者链式调用,比如 operator=
或 vector::operator[]
,就传引用返回,这样可以直接在原对象上操作,省掉拷贝。
唯一需要注意的是局部变量绝不能传引用返回,不然栈帧一结束,引用就悬空了,行为是未定义的。
另外现代编译器对值返回会做 RVO 优化,很多时候根本不会产生拷贝开销。