《Effective C++》学习笔记——条款21

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

四、Designs and Declarations

Rule 21:Don‘t try to return a reference when you must return an object

规则 21:必须返回对象时,别妄想返回其reference

1.原因

看了我们 条款20 ,如果领悟了 pass-by-value 的效率牵连层面,或许就恨不得都用 pass-by-reference 来替代。但这时,可能会出现一些致命的错误——开始传递一些指向其实并不存在的的对象。

先来看一下这个例子,这是一个表现 有理数 的类。

<span style="font-family:Comic Sans MS;">class Rational  {
public:
    Rational( int numerator = 0 , int denominator = 1 );
    ...
private:
    int n,d;    // n 为分子,d 为分母
    friend const Rational operator* ( const Rational& lhs, const Rational& rhs);
};</span>

这里的 operator* 用 by value 方式返回其计算结果,但会有额外的开销,当然你也可以完全不管,但这明显逃避了你的专业责任,因为很简单的,改而传递 reference 就不需要支付额外代价。

但是,要记住所谓的 reference 只是个名称,代表某个既有对象。所以如果看到一个reference声明式,你应该问自己它的另一个名称是什么。

以上述 operator* 为例,如果它返回一个 reference,后者一定指向某个既有的Rational 对象,内含两个 Rational对象的乘积。

但是,我们无法期望这样的一个 Rational对象在 调用 operator* 之前就存在。

所以,如果 operator* 要返回一个 reference 指向如此数值,它必须自己创建那个 Rational对象。

2.解决

>1<  通过函数创建新对象。

函数创建新对象的途径有两种:在stack空间 或 在 heap 空间创建。

① 在stack空间创建

就是定义一个 local 变量,用这方法来写 operator* :

<span style="font-family:Comic Sans MS;">const Rational& operator* ( const Rational& lhs, const Rational& rhs)
{
    Rational result( lhs.n * rhs.n , lhs.d * rhs.d );    // 糟糕的代码
    return result;
}</span>

这种做法不好,因为你的目标是 避免调用构造函数,而 result 却必须像任何对象一样的由构造函数够早起来。

但更严重的是,这个函数返回的reference是 local对象,而local对象在函数退出前就会被销毁。也就是说,reference 指向的是一个已经被销毁的对象。

② 在 heap内构造一个对象

Heap-based对象由 new 创建,所以它的 operator* :

<span style="font-family:Comic Sans MS;">const Rational& operator* ( const Rational& lhs, const Rational& rhs)
{
    Rational* result = new Rational( lhs.n * rhs.n,lhs.d * rhs.d );
    return *result;
}</span>

但是这个依旧需要付出一个 调用构造函数 的代价,

而且这又有一个新的问题:谁该对着被你 new 出来的对象 执行 delete。

尤其是,遇到下面这段代码:

<span style="font-family:Comic Sans MS;">Rational w,x,y,z;
w = x * y *z;    // 等价于 operator*(operator*(x,y),z)</span>

在这里同一个语句内,调用了两个 operator* ,也就是说 new 了两次,即要 delete两次。

但是这里却没有合理的方法让 operator* 使用者进行这些 delete 的调用,因为没有合理的方法来获取 operator* 返回的references背后隐藏的指针。

这些都将导致 资源泄露!

>2< 让 operator* 返回的reference 指向一个被定义于函数内部 static 对象

因为上述,无论 on-the-stack 或 on-the-heap做法,都因为对 operator* 返回的结果调用构造函数而困扰,所以可以尝试这种方法。

<span style="font-family:Comic Sans MS;">const Rational& operator* ( const Rational& lhs, const Rational& rhs )
{
    static Rational result;
    result = ...;
    return result;
}</span>

但是,如果遇到下面这种代码:

<span style="font-family:Comic Sans MS;">bool operator==( const Rational& lhs,const  Rational& rhs);    // 针对Rational而写的 operator==
Rational a,b,c,d;
...
if( (a*b) == (c*d) )
{
    // 当乘积相等,做适当动作
}
else
{
    // 当乘积不等,做相应动作
}</span>

每次判断 (a*b)==(c*d) 都将会是 true!

Why? 因为 两次operator* 虽然都各自改变了 static Rantional 对象值。但由于它们返回的都是 reference ,因此调用端看到的永远是 static Rantional对象的 "现值"!

>扩展    如果static Rantional 对象不行,那....static array 怎么样?

首先,你必须选择 array 大小n。 如果n太小,那空间可能不够;如果n太大,会因此降低程序效率,因为array内的每一个对象都会在第一次被调用时构造完成。

如果上面这个无法说服你,那下面这个理由呢?  如何将你需要的值放进array内。如果那么做成本又是多少。即使以vector替换array也不会让情况好转。

>3<  正确答案该出来了

让那个函数返回一个新对象。

<span style="font-family:Comic Sans MS;font-size:12px;">inline const Rantional operator* ( const Rational& lhs,const Rational& rhs )
{
    return Rational(lhs.n*rhs.n,lhs.d*rhs.d);
}</span>

当然这么做,需要承担 operator* 返回值的构造和析构的成本。

但是要知道,C++和所有编程语言一样,允许编译器实现者施行最优化,用以改善产出码的效率。因此某些情况下,这些成本可以被安全的消除。

所以,当你必须在"返回一个reference和返回一个object"之间抉择时,你的工作就是挑出行为正确的那一个。然后让编译器厂商做剩余的东西吧。

3.请记住

★绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个local static 对象而有可能同时需要多个这样的对象。条款4已经为 "在单线程环境中合理返回reference指向一个local static对象"提供了一份设计实例。

**************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

时间: 2024-10-29 09:42:18

《Effective C++》学习笔记——条款21的相关文章

effective c++学习笔记条款20-22

条款20:用引用传递代替值传递 1.尽量以引用传递来代替传值传递,前者比较高效,并且可以避免切割问题 2.以上规则不适用于内置类型,以及STL的迭代器,和函数对象 条款21:必须返回对象时,别妄想返回对象的引用 1.绝对不要返回指针和引用指向一个局部对象或者静态局部对象而有可能需要多个这样的对象,条款4已经为在单线程环境合理返回&指向一个局部静态提供了一份设计实例.(保护初始化顺序) 条款22:将成员变量声明为private 1.切记将成员变量声明为private.这可赋予客户访问数据的一致性,

effective C++ 读书笔记 条款21

条款21 :必须返回对象时,别妄想返回其reference 条款20里面虽然说传引用比传值好用,但是不能传递一些 reference指向其实并不存在的对象 上代码: #include <iostream> using namespace std; class Rational { public: // Rational() // { // } Rational(int m = 0, int n = 0 ) { } ~Rational() { } private: int n, d; /* 运算

effective c++学习笔记条款23-25

条款23:宁可用非成员,非友元函数来替代成员函数 1.非成员函数提供了更好的封装性,这个函数内不能访问类的私有成员,封装的越严密我们对类的数据就可以弹性越大的操纵,因为可见这些数据的客户越少,反之数据影响的客户也就越少. 2.c++比较自然的做法-(关系到标准库numplace的组织结构),可以把不同便捷函数放到不同Namespace去,让客户来决定要用的非成员函数功能,这是类不能提供的. 条款24:若所有参数皆需类型转换,请为此采用非成员函数. 1.如果你需要为某个函数的所有参数(包括被thi

effective c++学习笔记条款11-13

条款11: 1.令赋值运算符返回一个&,因为STL,string都是这样做的,除非你有足够好的理由不这样做. 2.处理自我赋值的方法----(1).在没有成功获取对象数据时不要删除自己的数据,避免发生异常后原对象指针是一个悬浮指针 (2).判断自我赋值的检查操作会耗费不少时间,可以用swap交换数据技术来优化---(1)形参为赋值而来,(2)形参为静态引用,多加一个函数内拷贝操作.

effective c++学习笔记条款8-10

条款7:为多态基类声明虚析构函数 1.一个基类指针接受一个派生类对象的地址时,对该指针delete,仅仅释放基类部分 2.给所有类都带上虚析构函数是个馊主意,会带有vptr指向一个函数指针数组,扩大不必要的对象大小,除非补偿vptr,否则没有移植性. 3.string类和STL不含有虚析构函数,然而一些用户 却将他们作为基类,运用   delete指向派生类的基类指针,导致错误[c++11添加了禁止派生性质],他们不适合当基类. 4,手头上没有合适的纯虚函数,但你确实需要一个抽象类,把析构函数声

effective c++学习笔记条款4-7

条款4:确定对象被使用前已经初始化 一. 变量在不同情况下可能会初始化,也可能不会初始化. 注意初始化和赋值的区别. 1.在类中内置类型不会发生隐式初始化,自定义有默认构造函数的能被默认初始化 所以在构造类时务必初始化内置类型,最好给自定义的对象显示初始化避免在函数体中赋值浪费资源. 2.内置类型在函数体内不会初始化,在函数体外自动初始化为0. 二. 1.const和引用类型必须初始化,不可能赋值 三 1.当类实在是有较多构造函数,并且总是要对一些成员数据重复初始化,可以考虑将那些“赋值和初始化

effective c++学习笔记条款17-19

条款17:以独立语句将New对象放置入智能指针. 1.以独立语句将newed对象放置入智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露. void name(shared_ptr<管理对象类型>(new 管理对象类型),其它函数)),New被分配内存不一定马上放入管理对象,因为有其它函数干扰,这不是独立语句. 条款18:让接口容易被正确使用,不易被误用. 1.好的接口很容易被正确使用,不容易被误用.你应该在你的所有接口中努力达成这些性质. 2.“促进正确使用”的办法包括接

effective c++学习笔记条款35-37

#include<iostream> using namespace std; class A { public: void asd() { pri(); } private: /*virtual*/ void pri() { cout << "基类函数" << endl; } }; class B :public A { private: void pri() /*override*/ { cout << "派生类函数&quo

effective c++学习笔记条款29-31

条款29:为异常安全而努力是值得的[回顾] 1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏,这样的函数分为3种可能的保证:基本型,强烈型,不抛异常型 2.“强烈保证”往往能通过copying and swap 来实现出来,但并非所有函数都可实现或者具备现实意义. 3.函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全中”的最弱者.