C++函数的返回值是否应该用引用?具体规则详述
一种情况是返回右值引用,用来初始化普通值(触发移动构造)public:// 返回对成员变量的右值引用// 1. my_obj 在这里被创建,其生命周期持续到 main 函数结束// 2. 调用成员函数,它返回一个指向 my_obj.cpu_mat 的右值引用。// 因为 my_obj 还活着,所以 my_obj.cpu_mat 也活着。这个引用是有效的。
黄金法则: 引用(无论是左值引用
&还是右值引用&&)仅仅是另一个对象的“别名”或“标签”。当那个原始对象被销毁后,这个“别名”就变成了悬垂引用(Dangling Reference),使用它就是未定义行为。
现在,我们来分析四种最关键的情景。
情景一:返回对【函数局部变量】的引用(危险!悬垂引用)
这是最典型的错误。
cv::Mat& create_and_return_mat() {
cv::Mat local_mat(100, 100, CV_8U); // 1. local_mat 在这里被创建
return local_mat; // 2. 返回一个指向 local_mat 的右值引用
} // 3. 函数结束,local_mat 在这里被销毁!
int main() {
cv::Mat& dangling_ref = create_and_return_mat(); // 4. dangling_ref 现在指向一块已经释放的内存
// 后续任何对 dangling_ref 的使用都是未定义行为!程序可能会崩溃。
}
cv::Mat&& create_and_return_mat() {
cv::Mat local_mat(100, 100, CV_8U); // 1. local_mat 在这里被创建
return local_mat; // 2. 返回一个指向 local_mat 的右值引用
} // 3. 函数结束,local_mat 在这里被销毁!
int main() {
cv::Mat&& dangling_ref = create_and_return_mat(); // 4. dangling_ref 现在指向一块已经释放的内存
// 后续任何对 dangling_ref 的使用都是未定义行为!程序可能会崩溃。
}
- 生命周期分析:
local_mat的生命周期被严格限制在create_and_return_mat函数的{}范围内。- 函数返回了一个指向
local_mat的右值引用。 - 函数执行完毕,
local_mat被销毁,其占用的栈内存被回收。 dangling_ref虽然成功拿到了一个引用,但它引用的对象已经“灰飞烟灭”了。
- 结论:绝对禁止返回对函数局部变量的任何引用(无论是
&还是&&)。
最后补充 ,返回函数局部变量的情形下,千万不要试图在函数的返回参数面前加引用,否则函数结束后,对应的引用会变成“悬垂引用”,类似于空指针,因为原对象已经被销毁了。正常来说,想要返回一个变量又不想触发拷贝,直接在return的时候std move就行了(C++17后编译器会自动move,可以不用特地写出来)
情景二:返回对【类成员变量】的右值引用
一种情况是返回右值引用,用来初始化普通值(触发移动构造)
class MyObject {
public:
cv::Mat cpu_mat;
MyObject() : cpu_mat(200, 200, CV_8U) {}
// 返回对成员变量的右值引用
cv::Mat&& getMatAsRvalueRef() {
return std::move(cpu_mat);
}
};
int main() {
MyObject my_obj; // 1. my_obj 在这里被创建,其生命周期持续到 main 函数结束
// 2. 调用成员函数,它返回一个指向 my_obj.cpu_mat 的右值引用。
// 因为 my_obj 还活着,所以 my_obj.cpu_mat 也活着。这个引用是有效的。
cv::Mat new_mat = my_obj.getMatAsRvalueRef(); //移动初始化
// 3. 在上面这一行代码执行期间:
// - getMatAsRvalueRef() 返回的有效引用被 new_mat 的移动构造函数接收。
// - new_mat “窃取”了 my_obj.cpu_mat 的内部数据。
// - my_obj.cpu_mat 变为空矩阵。
// 4. my_obj 对象本身依然存活,但其内部状态已被破坏。
} // 5. main 函数结束,my_obj 在这里被销毁。
另一种情况,函数依然返回右值引用,但是不用普通值来接:
//假如写成
cv::Mat&& new_mat1 = my_obj.getMatAsRvalueRef();
//或者
const cv::Mat& new_mat2 = my_obj.getMatAsRvalueRef();
//不会发生任何构造,此时的cpumat虽然经由move变成了右值引用,但是没有人触发相关的移动构造,仍完好的存在于my_obj的内存空间
这种情况通常是一个函数希望返回右值然后其他值来接走所有权,但是你不想夺走其所有权,只想访问,就会这样
情景三:返回对【类成员变量】的左值引用
这里方便起见直接用一个引用传入一个参数再传出,效果是一样的。
返回左值引用时的接收方式差异
用左值引用来接收 当函数返回左值引用时,使用左值引用接收会直接绑定到函数返回的原始对象,不产生任何拷贝或移动操作。这种方式允许通过接收的引用修改原始对象,且生命周期与原始对象绑定。
int& getRef(int& x) { return x; }
int main() {
int a = 10;
int& ref = getRef(a); // ref直接绑定到a
ref = 20; // 修改a的值
// a的值现在为20
}
用普通值来接收 若用普通值接收左值引用返回值,会发生拷贝构造(或移动构造,若返回值是右值)。此时接收的变量是原始对象的独立副本,对其修改不会影响原始对象。
int& getRef(int& x) { return x; }
int main() {
int a = 10;
int val = getRef(a); // 拷贝a的值到val
val = 20; // 仅修改val,a仍为10
}
情景四:绑定到【临时对象】与生命周期延长(C++ 的一个重要特例)
这是一个非常特殊的规则,但对于理解右值引用至关重要。
当一个右值引用(或 const 左值引用)直接绑定到一个**纯右值(prvalue,通常指函数返回的临时对象)**时,这个临时对象的生命周期会被延长,直到和该引用的生命周期相同。
cv::Mat create_temp_mat() {
return cv::Mat(300, 300, CV_8U); // 按值返回一个临时对象
}
int main() {
// 1. create_temp_mat() 返回一个临时的 cv::Mat 对象。
// 2. 右值引用 mat_ref 绑定到这个临时对象。
// 3. C++ 规则介入:这个临时对象的生命周期被延长,直到 mat_ref 离开作用域。
cv::Mat&& mat_ref = create_temp_mat();
const cv::Mat & mat_ref2 = create_temp_mat();
std::cout << "临时对象的生命周期被延长了,尺寸: "
<< mat_ref.cols << "x" << mat_ref.rows << std::endl; // 完全安全!
} // 4. main 函数结束,mat_ref 离开作用域,此时被延长的那个临时对象才被销毁。
- 生命周期分析:
create_temp_mat()创建的临时对象,在正常情况下,应该在cv::Mat&& mat_ref = ...;这行代码结束时就被销毁。- 但是,因为它被一个右值引用
mat_ref捕获了,它的生命周期被“奇迹般地”延长,与mat_ref的生命周期同步。
- 结论:这是 C++ 中一个强大且安全的特性,它允许我们以零拷贝的方式持有和使用临时对象。
补充表格
C++ 函数返回值全面总结表
为了方便理解,我们假设操作的类型是 std::string,因为它是一个典型的、支持移动语义的资源管理类。
注意一点 函数实际返回的类型可以与函数签名的不一致,比如下面第一部分的第四项,这里实际上发生类类型的自动转化 用右值自动初始化普通值这也是为什么RVO会被破坏
第一部分:返回 Type (按值返回)
核心思想:函数创建一个新值的“所有权”,并将其转移给调用者。这是最常见、最安全的方式。
| 函数签名 | 函数实现 (return 语句) |
调用方接收方式 | 发生的操作 | 性能 | 安全性 | 解释与备注 |
|---|---|---|---|---|---|---|
string func() |
string s = ...; return s; |
string obj = func(); |
RVO/NRVO (返回值优化) | 最优 | 绝对安全 | 编译器会直接在 obj 的内存上构造对象,避免任何拷贝或移动。这是C++编译器的“魔法”。 |
string func() |
string s = ...; return s; |
string&& ref = func(); |
生命周期延长 | 最优 | 绝对安全 | 函数返回的临时对象被 ref 绑定,其生命周期被延长至与 ref 相同。没有拷贝或移动。 |
string func() |
string s = ...; return s; |
const string& ref = func(); |
生命周期延长 | 最优 | 绝对安全 | 与上一条相同,这是临时对象生命周期延长规则的另一个应用。非常常见的只读用法。 |
string func() |
string s = ...; return std::move(s); |
string obj = func(); |
强制移动构造 | 良好 | 安全 | 反模式! std::move 会阻止RVO,强制进行一次移动构造。虽然安全,但性能劣于RVO。永远不要对局部变量使用 std::move 来返回。 |
第二部分:返回 Type& (按左值引用返回)
核心思想:函数返回一个已存在对象的“别名”或“代理”,不产生新对象。
| 函数签名 | 函数实现 (return 语句) |
调用方接收方式 | 发生的操作 | 性能 | 安全性 | 解释与备注 |
|---|---|---|---|---|---|---|
string& func() |
static string s; return s; |
string& ref = func(); |
引用绑定 | 最优 | 安全 | ref 成为 static s 的别名。s 的生命周期是整个程序,所以是安全的。可以修改 s。 |
string& func() |
static string s; return s; |
string obj = func(); |
拷贝构造 | 较差 | 安全 | obj 是通过拷贝构造函数从 s 创建的一个全新副本。发生了深拷贝。 |
string& func() |
string s = ...; return s; |
string& ref = func(); |
绑定到悬垂引用 | N/A | 极度危险 (UB) | 经典错误! 函数返回了对局部变量 s 的引用。函数结束后 s 被销毁,ref 成为悬垂引用。 |
第三部分:返回 Type&& (按右值引用返回)(有一种例外没列出来就是之前说的生命周期延长,那是个特例)
核心思想:函数返回一个“可被移动的”已存在对象的“别名”。这是为高级移动语义和完美转发设计的。
| 函数签名 | 函数实现 (return 语句) |
调用方接收方式 | 发生的操作 | 性能 | 安全性 | 解释与备注 |
|---|---|---|---|---|---|---|
string&& func() |
string s = ...; return std::move(s); |
string&& ref = func(); |
绑定到悬垂引用 | N/A | 极度危险 (UB) | 绝对错误! 和返回左值引用一样,返回了对局部变量的引用。ref 成为悬垂引用,生命周期延长规则不适用。 |
string&& func() |
string s = ...; return std::move(s); |
string obj = func(); |
移动构造 (但源已销毁) | N/A | 极度危险 (UB) | obj 尝试从一个已经销毁的局部变量 s 中移动资源,导致未定义行为。 |
class C { string m; string&& take() && { return std::move(m); } }; |
return std::move(member_var); |
string obj = C().take(); |
移动构造 | 最优 | 安全 (高级用法) | C() 是右值,可以调用 && 限定的 take()。obj 安全地从临时 C 对象的成员 m 中移动了资源。这是返回右值引用的正确场景之一。 |
因此,在绝大多数情况下,当你想从一个将亡对象中移出其成员时,应该优先选择按值返回,并在 return 语句中使用 std::move。此外,目前实践中在返回值上使用引用几乎只有左值引用了,甚至返回的stdmove都很少用(因为RVO优化)
这是一个非常深刻且关键的问题!它揭示了函数返回值类型中 & 和 && 的真正含义,以及它们如何与“函数调用表达式本身是右值”这一规则相互作用。
您的理解方向非常正确:“表示将要被绑定的变量类型”,但我们可以把这个概念说得更精确一些。
让我们分解这个问题:
1. “函数调用表达式本身的值类别” vs “函数声明的返回类型”
这是两个需要严格区分的概念。
-
函数声明的返回类型 (Declared Return Type): 这是你在函数签名中写的东西,比如
int,std::string,std::string&,const std::string&,std::string&&。它是一个静态的类型信息,是函数与调用者之间的“契约”。 -
函数调用表达式的值类别 (Value Category of the Call Expression): 这是当你实际调用函数时,整个表达式
my_function()在C++语法中所扮演的角色。它的值类别(左值、prvalue右值、xvalue右值)是由函数声明的返回类型决定的。
规则非常直接:
| 函数声明的返回类型 | 函数调用表达式 my_function() 的值类别 |
|---|---|
返回一个值 (e.g., int, std::string) |
prvalue (纯右值) |
返回一个左值引用 (e.g., T&, const T&) |
lvalue (左值) |
返回一个右值引用 (e.g., T&&, const T&&) |
xvalue (消亡值,一种右值) |
所以,“函数返回值总是一个右值”这句话不够精确,需要修正:
精确的说法: 当函数按值返回时,其调用表达式是一个 prvalue (纯右值)。
而当函数按引用返回时,其调用表达式就变成了左值或xvalue。
更多推荐



所有评论(0)