当前位置: 首页 > news >正文

Item21--必须返回对象时,别妄想返回其 reference

1. 核心矛盾:我们为什么想返回引用?

学习了 Item 20 后,你可能觉得:“传值(pass-by-value)太慢了,有构造和析构的开销。那我干脆把函数的返回值也改成引用吧!”

场景设定

假设你要实现一个有理数类 Rational,并重载乘法运算符:

class Rational {
public:Rational(int numerator = 0, int denominator = 1);// ...
private:int n, d;// 友元函数,用于支持乘法friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};

我们的目标是让 Rational a = b * c; 能够运行。 这时候,如果你试图将 operator* 的返回值声明为 const Rational&,你会遇到一个逻辑上的死胡同:这个被引用的对象,到底存在于哪里?

Item 21 列举了三种常见的错误尝试,说明为什么这样做是行不通的。


2. 错误尝试一:返回局部变量的引用 (On the Stack)

这是最致命的错误。你可能试图在函数内部创建一个对象,然后返回它的引用:

// ❌ 绝对错误:返回局部对象的引用
const Rational& operator*(const Rational& lhs, const Rational& rhs) {Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // 局部对象,在栈上return result; 
}

为什么不行?

  • 生命周期问题result 是一个局部变量。当函数 operator* 执行结束时,result 会被自动销毁(析构函数被调用)。
  • 后果:你返回的引用指向了一块“已经销毁”的内存。调用者拿到的引用是 Dangling Reference(悬空引用)。一旦使用它,就会导致未定义行为(程序崩溃或数据错乱)。

3. 错误尝试二:返回堆对象的引用 (On the Heap)

既然栈上的对象会被销毁,那你可能想到用 new 在堆上创建对象:

// ❌ 绝对错误:内存泄漏的温床
const Rational& operator*(const Rational& lhs, const Rational& rhs) {Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); // 堆上创建return *result; 
}

为什么不行?

  • 内存泄漏:C++ 中谁申请谁释放。这里虽然对象活着,但谁来负责 delete 它呢

  • 无法挽回的场景

    Rational w, x, y, z;
    w = x * y * z; 
    

    这行代码实际上是 operator*(x, y) 返回一个引用,然后再和 z 乘。这里发生了两次 new 调用,但没有指针变量能让你去 delete 它们。这些内存永远泄露了。


4. 错误尝试三:返回静态局部变量的引用 (Static Local Variable)

这是为了避免前两个问题而产生的“投机取巧”写法:

// ❌ 错误:逻辑错误 + 线程不安全
const Rational& operator*(const Rational& lhs, const Rational& rhs) {static Rational result; // 静态变量,全剧只有一份result = /* 计算 lhs * rhs 的值 */;return result;
}

为什么不行?

  1. 线程安全性:静态变量是共享的,多线程环境下如果不加锁,会引发严重的数据竞争。

  2. 逻辑正确性:即使是单线程,它也会导致荒谬的结果。请看下面的比较操作:

    Rational a, b, c, d;
    if ((a * b) == (c * d)) {// ...
    }
    

    结果:这个 if 判断永远为 true原因

    • a * b 修改了静态变量 result 并返回其引用。
    • c * d 再次修改了同一个静态变量 result 并返回其引用。
    • 最终比较的是 result == result,当然永远相等!

解决方法

1. 移动语义 (Move Semantics, C++11)

这是现代 C++ 最重要的特性之一。如果编译器无法消除临时对象(即无法使用 RVO),它不再进行“深拷贝”,而是进行“移动”。

以前的痛点 (C++98)

当你 return result; 时,如果 result 是一个包含巨大数组的对象(比如 std::vector),编译器必须申请新内存,把数组里的数据一个个复制过去(Deep Copy)。

现在的做法 (C++11 及以后)

编译器意识到 result 是一个即将销毁的右值 (Rvalue)。它会调用移动构造函数 (Move Constructor),而不是拷贝构造函数。

  • 操作:仅仅是把 result 内部的指针“偷”过来,赋给接收者,并将 result 内部置空。
  • 代价:几次指针赋值的开销(接近 0),无论对象有多大。
class BigObject {int* data;
public:// 移动构造函数BigObject(BigObject&& other) noexcept : data(other.data) {other.data = nullptr; // 偷走资源,置空原对象}// ...
};BigObject createHugeObject() {BigObject temp;// ... 填充数据 ...return temp; // 触发移动构造,瞬间完成,无需深拷贝
}

2. 强制返回值优化 (Guaranteed Copy Elision, C++17)

C++17 将以前编译器的“可选优化”变成了“语言标准”。

在很多情况下,现代 C++ 根本不会调用拷贝构造函数,甚至连移动构造函数都不调用。对象是直接在调用者的栈空间上构造的。这被称为 Zero-Copy Pass-By-Value

场景 A:返回临时对象 (RVO)

// C++17 标准保证:这里没有拷贝,也没有移动
Rational operator*(const Rational& lhs, const Rational& rhs) {return Rational(lhs.n * rhs.n, lhs.d * rhs.d); 
}// 调用
Rational r = a * b; 

解析:编译器看到 return Rational(...),它直接在 r 的内存地址上执行构造函数。中间没有任何临时对象产生。

场景 B:具名返回值优化 (NRVO)

虽然 C++17 标准只强制了 RVO(返回临时量),但主流编译器(GCC, Clang, MSVC)对 NRVO(返回局部变量)的优化也非常成熟。

Rational func() {Rational result; // 局部变量// ... 操作 result ...return result;   // 现代编译器通常直接将 result 构造在接收者的位置
}

3. 智能指针 (Smart Pointers)

如果你确实需要在堆(Heap)上创建对象并返回(比如工厂模式,或者对象太大不适合放在栈上),Item 21 中提到的 new 导致的内存泄漏问题,现在用 std::unique_ptr 完美解决。

// ✅ 现代写法:返回 unique_ptr
std::unique_ptr<Window> createWindow(string type) {if (type == "scroll")return std::make_unique<WindowWithScrollBars>(); // 自动管理内存elsereturn std::make_unique<Window>();
}// 调用
auto w = createWindow("scroll"); 
// 即使 w 离开作用域,unique_ptr 会自动 delete,没有内存泄漏风险
// unique_ptr 也是通过“移动语义”返回的,效率极高

4. 常见误区:千万不要画蛇添足

在学习了移动语义后,很多初学者会犯一个典型的错误:显式调用 std::move

// ❌ 错误做法:阻碍了 RVO/NRVO
Rational func() {Rational result;return std::make_move_iterator(result); // 或者是 return std::move(result);
}

为什么这是错的?

  • 这会强制编译器使用“移动构造”。
  • 但是,“移动”虽然便宜,依然有开销(指针赋值)。
  • 而编译器的 RVO/NRVO 是零开销(直接构造)。
  • 显式写 std::move 会破坏 RVO 的条件,导致性能反而下降。

总结:现代 C++ 对 Item 21 的补充

Item 21 的核心建议 “必须返回对象时,就返回对象值,不要返回引用” 依然是金科玉律。

但在现代 C++ 中,你不必再为这个建议感到内疚,因为:

  1. C++17 保证:返回临时对象是零拷贝(Zero-Copy)。
  2. C++11 保证:即使需要拷贝,优先使用移动(Move),代价极低。
  3. 智能指针:让返回堆对象变得安全且无泄漏。

结论:大胆地写 return val; 吧,现代编译器和 C++ 标准会帮你处理好剩下的一切。

http://www.jsqmd.com/news/115963/

相关文章:

  • Item15--在资源管理类中提供对原始资源的访问
  • 1985-2024年中国绿色专利数据库(绿色技术专利分类)
  • Item22--将成员变量声明为 private
  • Item16--`new` 与 `delete` 的对应规则
  • 3777. 使子字符串变交替的最少删除次数
  • item11--在 operator= 中处理“自我赋值
  • 预见2026:家居新品首秀平台选择战略——五大核心展会深度评估与推荐 - 匠子网络
  • Item20--宁以 pass-by-reference-to-const 替换 pass-by-value
  • 研究生必备!8个免费AI论文工具,半天生成5000字问卷论文还有高信度数据
  • Item20--宁以 pass-by-reference-to-const 替换 pass-by-value
  • Item17--以独立语句将 `new` 到的对象置入智能指针
  • Item17--以独立语句将 `new` 到的对象置入智能指针
  • 3433. 统计用户被提及情况
  • Item19--设计 class 犹如设计 type
  • 国外软件,安装即时专业版!
  • Item19--设计 class 犹如设计 type
  • basic_regex
  • c++狼人杀
  • 宠物识别丨基于弱监督学习的宠物视频内容自动标注技术实践 - 指南
  • 朴易天下:道家修行的专业术语分享
  • 个人投资者的落地路径:从“说人话,做量化”到实盘前的三道关
  • 神经网络中的 block 和 module
  • item13--使用对象管理资源
  • 深入解析:蓝桥杯基础算法精讲:模拟与高精度运算实战指南
  • item12-- 拷贝一个对象的所有组成部分
  • sub_match
  • sub_match
  • 抽奖机随机号码生成:3 种算法实现 + 测试全解析(附完整代码)
  • 【零基础精通】Python 字符串全解析:从字符序列到不可变对象的深度构建
  • item14--谨慎考虑资源管理类的拷贝行为