面试高频考点:传值返回和传引用返回的区别

传值返回

// 传值返回(非优化版本,强制拷贝构造)
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 次,mainref 绑定的延续临时析构 1 次


传引用返回

而使用引用返回会生成d1 d2的别名,减少了拷贝
在这里插入图片描述

reftmp,是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️⃣ d1d2 都是 func 的局部变量,存在 func 的栈帧里。
2️⃣ if 分支随机选择:
return d1; 时,main 拿到 d1 的引用
return d2; 时,main 拿到 d2 的引用
无论你拿到谁的引用,func 一旦返回,d1d2 都会自动析构。
所以 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 优化,很多时候根本不会产生拷贝开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值