Effective C++ 2e Item23

本文围绕C++语言展开,探讨函数返回对象和引用的问题。以有理数类的operator*函数为例,分析在堆栈、堆上创建对象返回引用以及返回静态对象引用的错误做法,指出写必须返回新对象的函数应返回新对象,编译器会进行优化。

条款23: 必须返回一个对象时不要试图返回一个引用

据说爱因斯坦曾提出过这样的建议:尽可能地让事情简单,但不要过于简单。在C++语言中相似的说法应该是:尽可能地使程序高效,但不要过于高效。

一旦程序员抓住了“传值”在效率上的把柄(参见条款22),他们会变得十分极端,恨不得挖出每一个隐藏在程序中的传值操作。岂不知,在他们不懈地追求纯粹的“传引用”的过程中,他们会不可避免地犯另一个严重的错误:传递一个并不存在的对象的引用。这就不是好事了。

看一个表示有理数的类,其中包含一个友元函数,用于两个有理数相乘:

class Rational {
public:
  Rational(int numerator = 0, int denominator = 1);

  ...

private:
  int n, d;              // 分子和分母

friend
  const Rational                      // 参见条款21:为什么
    operator*(const Rational& lhs,    // 返回值是const
              const Rational& rhs)    
};

inline const Rational operator*(const Rational& lhs,
                                const Rational& rhs)
{
  return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

很明显,这个版本的operator*是通过传值返回对象结果,如果不去考虑对象构造和析构时的开销,你就是在逃避作为一个程序员的责任。另外一件很明显的事实是,除非确实有必要,否则谁都不愿意承担这样一个临时对象的开销。那么,问题就归结于:确实有必要吗?

答案是,如果能返回一个引用,当然就没有必要。但请记住,引用只是一个名字,一个其它某个已经存在的对象的名字。无论何时看到一个引用的声明,就要立即问自己:它的另一个名字是什么呢?因为它必然还有另外一个什么名字(见条款M1)。拿operator*来说,如果函数要返回一个引用,那它返回的必须是其它某个已经存在的Rational对象的引用,这个对象包含了两个对象相乘的结果。

但,期望在调用operator*之前有这样一个对象存在是没道理的。也就是说,如果有下面的代码:

Rational a(1, 2);                // a = 1/2
Rational b(3, 5);                // b = 3/5
Rational c = a * b;              // c 为 3/10

期望已经存在一个值为3/10的有理数是不现实的。如果operator* 一定要返回这样一个数的引用,就必须自己创建这个数的对象。

一个函数只能有两种方法创建一个新对象:在堆栈里或在堆上。在堆栈里创建对象时伴随着一个局部变量的定义,采用这种方法,就要这样写operator*:

// 写此函数的第一个错误方法
inline const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs)
{
  Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
  return result;
}

这个方法应该被否决,因为我们的目标是避免构造函数被调用,但result必须要象其它对象一样被构造。另外,这个函数还有另外一个更严重的问题,它返回的是一个局部对象的引用,关于这个错误,条款31进行了深入的讨论。

那么,在堆上创建一个对象然后返回它的引用呢?基于堆的对象是通过使用new产生的,所以应该这样写operator*:

// 写此函数的第二个错误方法
inline const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs)
{
  Rational *result =
    new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
  return *result;
}

首先,你还是得负担构造函数调用的开销,因为new分配的内存是通过调用一个适当的构造函数来初始化的(见条款5和M8)。另外,还有一个问题:谁将负责用delete来删除掉new生成的对象呢?

实际上,这绝对是一个内存泄漏。即使可以说服operator*的调用者去取函数返回值地址,然后用delete去删除它(绝对不可能——条款31展示了这样的代码会是什么样的),但一些复杂的表达式会产生没有名字的临时值,程序员是不可能得到的。例如:

Rational w, x, y, z;

w = x * y * z;

两个对operator*的调用都产生了没有名字的临时值,程序员无法看到,因而无法删除。(再次参见条款31)

也许,你会想你比一般的熊——或一般的程序员——要聪明;也许,你注意到在堆栈和堆上创建对象的方法避免不了对构造函数的调用;也许,你想起了我们最初的目标是为了避免这种对构造函数的调用;也许,你有个办法可以只用一个构造函数来搞掂一切;也许,你的眼前出现了这样一段代码:operator*返回一个“在函数内部定义的静态Rational对象”的引用:

// 写此函数的第三个错误方法
inline const Rational& operator*(const Rational& lhs,
                                 const Rational& rhs)
{
  static Rational result;      // 将要作为引用返回的
                               // 静态对象

  lhs和rhs 相乘,结果放进result;

  return result;
}

这个方法看起来好象有戏,虽然在实际实现上面的伪代码时你会发现,不调用一个Rational构造函数是不可能给出result的正确值的,而避免这样的调用正是我们要谈论的主题。就算你实现了上面的伪代码,但,你再聪明也不能最终挽救这个不幸的设计。

想知道为什么,看看下面这段写得很合理的用户代码:

bool operator==(const Rational& lhs,      // Rationals的operator==
                const Rational& rhs);     //

Rational a, b, c, d;

...

if ((a * b) == (c * d)) {

  处理相等的情况;

} else {

  处理不相等的情况;

}

看出来了吗?((a*b) == (c*d)) 会永远为true,不管a,b,c和d是什么值!

用等价的函数形式重写上面的相等判断语句就很容易明白发生这一可恶行为的原因了:

if (operator==(operator*(a, b), operator*(c, d)))

注意当operator==被调用时,总有两个operator*刚被调用,每个调用返回operator*内部的静态Rational对象的引用。于是,上面的语句实际上是请求operator==对“operator*内部的静态Rational对象的值”和“operator*内部的静态Rational对象的值”进行比较,这样的比较不相等才怪呢!

幸运的话,我以上的说明应该足以说服你:想“在象operator*这样的函数里返回一个引用”实际上是在浪费时间。但我没幼稚到会相信幸运总会光临自己。一些人——你们知道这些人是指谁——此刻会在想,“唔,上面那个方法,如果一个静态变量不够用,也许可以用一个静态数组……”

请就此打住!我们难道还没受够吗?

我不能让自己写一段示例代码来太高这个设计,因为即使只抱有上面这种想法都足以令人感到羞愧。首先,你必须选择一个n,指定数组的大小。如果n太小,就会没地方储存函数返回值,这和我们前面否定的那个“采用单个静态变量的设计”相比没有什么改进。如果n太大,就会降低程序的性能,因为函数第一次被调用时数组中每个对象都要被创建。这会带来n个构造函数和n个析构函数的开销,即使这个函数只被调用一次。如果说"optimization"(最优化)是指提高软件的性能的过程, 那么现在这种做法简直可以称为"pessimization"(最差化)。最后,想想怎么把需要的值放到数组的对象中以及需要多大的开销?在对象间传值的最直接的方法是通过赋值,但赋值的开销又有多大呢?一般来说,它相当于调用一个析构函数(摧毁旧值)再加上调用一个构造函数(拷贝新值)。但我们现在的目标正是为了避免构造和析构的开销啊!面对现实吧:这个方法也绝对不能选用。

所以,写一个必须返回一个新对象的函数的正确方法就是让这个函数返回一个新对象。对于Rational的operator*来说,这意味着要不就是下面的代码(就是最初看到的那段代码),要不就是本质上和它等价的代码:

inline const Rational operator*(const Rational& lhs,
                                const Rational& rhs)
{
  return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

的确,这会导致“operator*的返回值构造和析构时带来的开销”,但归根结底它只是用小的代价换来正确的程序运行行为而已。况且,你所担心的开销还有可能永远不会出现:和所有程序设计语言一样,C++允许编译器的设计者采用一些优化措施来提高所生成的代码的性能,所以,在有些场合,operator*的返回值会被安全地除去(见条款M20)。当编译器采用了这种优化时(当前大部分编译器这么做),程序和以前一样继续工作,只不过是运行速度比你预计的要快而已。

以上讨论可以归结为:当需要在返回引用和返回对象间做决定时,你的职责是选择可以完成正确功能的那个。至于怎么让这个选择所产生的代价尽可能的小,那是编译器的生产商去想的事。

<think> 我们参考了用户提供的引用内容,其中提到了内存管理、多线程编程和STL等内容。用户已经学完C++基础语法、STL和多线程,接下来应该学习什么? 根据C++的学习路径,在掌握了这些基础之后,可以继续深入学习以下方向: 1. 深入模板元编程:C++的模板是强大且复杂的特性,学习模板元编程可以提高代码的抽象能力和复用性。 2. 现代C++特性(C++11/14/17/20):学习lambda表达式、智能指针、移动语义、并发库等现代特性。 3. 设计模式:掌握常用的设计模式,提高软件设计能力。 4. 网络编程:学习使用C++进行网络编程,如socket编程,以及网络库(如Boost.Asio)的使用。 5. 系统级编程:包括操作系统相关的API调用、进程管理、文件系统操作等。 6. 性能优化:学习如何分析和优化C++程序的性能,包括内存访问模式、缓存友好设计等。 7. 跨平台开发:了解如何在不同的操作系统(如Windows、Linux)上开发和移植C++程序。 8. 深入学习并发编程:虽然已经学习了多线程,但并发编程还有更多高级主题,如无锁编程、线程池、条件变量等。 我们根据这些方向,为用户推荐接下来的学习内容,并生成相关问题。 </think>在学完C++基础语法、STL和多线程后,建议按照以下路径继续深入学习,这些内容能帮助你构建更专业的C++开发能力: ### 1. 现代C++特性(C++11/14/17/20) ```cpp // 示例:现代C++核心特性 auto lambda = [](auto x) { // C++14泛型lambda return std::make_unique<int>(x*2); // 智能指针 }; std::vector<int> v{1,2,3}; // 初始化列表 for(auto&& item : v) { // 范围for循环 std::cout << item << std::endl; } ``` - **关键特性**:智能指针(`unique_ptr/shared_ptr`)、移动语义、lambda表达式、类型推导(`auto`)、并发库(`<future>`) - **学习资源**:《Effective Modern C++》(Scott Meyers)、cppreference.com ### 2. 内存管理进阶 ```cpp // 智能指针应用场景 class Resource { public: Resource() { std::cout << "资源创建\n"; } ~Resource() { std::cout << "资源释放\n"; } }; auto processResource() { auto res = std::make_shared<Resource>(); // 自动内存管理 return res; // 引用计数管理生命周期 } ``` - **重点内容**: - 智能指针的原理与使用场景 - 自定义内存分配器 - 内存泄漏检测工具(Valgrind、AddressSanitizer) - RAII(资源获取即初始化)设计模式 ### 3. 网络编程(结合多线程) ```cpp // 简易TCP服务器示例 void start_server() { boost::asio::io_context io; tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080)); auto handle_connection = [&] { tcp::socket socket(io); acceptor.accept(socket); std::thread(handle_client, std::move(socket)).detach(); }; } ``` - **推荐方向**: - Socket编程基础 - 网络库:Boost.Asio/Poco/libevent - HTTP/TCP协议实现 - 高并发服务器架构 ### 4. 设计模式与架构 ```cpp // 工厂模式示例 class Widget { public: virtual void draw() = 0; static std::unique_ptr<Widget> create(WidgetType type); // 工厂方法 }; ``` - **常用模式**: - 创建型:工厂/单例/建造者 - 结构型:适配器/代理/组合 - 行为型:观察者/策略/命令 - **学习建议**:《设计模式:可复用面向对象软件的基础》 ### 5. 跨平台开发 - **关键技术**: - CMake构建系统 - 平台特定API封装(Windows/Linux/macOS) - 条件编译与宏定义 - 跨平台GUI框架(Qt/WxWidgets) ### 6. 性能优化 ```cpp // 缓存友好设计示例 struct alignas(64) CacheLine { // 64字节对齐 int data[16]; }; ``` - **优化方向**: - CPU缓存机制 - 分支预测优化 - SIMD指令集(AVX/SSE) - 性能分析工具(perf、VTune) ### 7. 项目实践建议 ```mermaid graph LR A[基础语法] --> B[STL容器] B --> C[多线程] C --> D[网络编程] D --> E[设计模式] E --> F[性能优化] F --> G[完整项目] ``` - **项目类型**: - 简易数据库系统 - HTTP服务器 - 实时数据处理系统 - 游戏引擎模块
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值