《Effective C++》资源管理:条款20

条款20:宁以pass-by-reference-to-const替换pass-by-value

在默认情况下,C++函数传递参数是继承C的方式,是值传递(pass by value)。这样传递的都是实际实参的副本,这个副本是通过调用复制构造函数来创建的。有时候创建副本代价非常昂贵。例如一下继承体系

class Person{public:Person();virtual ~Person();……private:std::string name;std::string address;};class Student:public Person{public:Student();~Student();……private:std::string schoolName;std::string schoolAddress;};现在考虑一个函数validateStudent,它需要一个Student实参,以passbyvalue方式传递。bool validateStudent(Student s);//pass by valueStudent plato;bool platIsOK=validateStudent(plato);当函数被调用时,copy构造函数会被调用,用plato构造s。在返回时,s会被析构。那么passbyvalue的代价就是Student的一次构造和一次析构。但是Student构造和析构时又发生了什么?它内部有两个string对象,所以会有两个string对象的构造和析构。Student继承自Person,又加上Person的构造和析构,Person内又有两个string对象,因此还要加上2个string对象的构造和析构。总共是六次构造和六次析构。

pass byvalue是正确的,但是其效率低下。以pass by reference-to-const方式传递,可以回避所有构造函数和析构函数。

bool validateStudent(const Student& s);这种方式传递,没有新对象创建,所以自然没有构造和析构函数的调用。参数中,以const修饰是比较重要的,原先的passbyvalue,原先的值自然不会被修改。现在以pass by reference方式传递,,函数validateStudent内使用的对象和传进来的同同一个对象,为了防止在函数内修改,加上const限制。

以pass by reference方式传递,还可以避免对象切割(slicing)问题。一个派生类(derived class)对象以passbyvalue方式传递,当被视为一个基类对象(base class)时,基类对象的copy构造函数会被调用,此时派生类部分全部被切割掉了,仅仅留下一个baseclass部分。

在C++编译器的底层,reference往往以指针实现出来,所以passbyreference通常意味着真正传递是指针。但是对于内置类型,passbyvalue往往比passbyreference更高效。所以在使用STL函数和迭代器时,习惯上都被设计出passbyvalue。当设计迭代器和函数时,设计者有责任查看哪种传递方式更为高效,是否会有切割问题的影响。这个规则的改变适用于你使用C++的哪一部分。(条款1)

通常,内置类型都比较小,因此有人认小型types都适合passbyvalue,用户自己定义的class亦然。但是对象小并不意味着copy构造函数代价小。许多对象(包括STL容器),内涵的成员只不过是一两个指针,但是复制这种对象时,要复制指针指向的每一样东西,这个代价很可能十分昂贵。

还有一个理由,某些编译器对待内置类型和用户自定义类型的”态度“截然不同,即使两者有着相同的底层描述。例如,某些编译器拒绝把一个double组成的对象放进缓存器内,但是却乐意在一个正规基础上光秃秃的doubles上这么做。当这种事情发生时,应该以by reference方式传递此对象,因为编译器当然会把指针放进缓存器。

用户自定义的小型types,可能还会发生变化,将来也许会变大,其内部实现可能会改变,所以用户自定义的小型type在使用pass by value时要慎重。

一般情况下,可以假设内置类型和STL迭代器和函数对象以pass by value是代价不昂贵。其他时候最好以pass by reference to const替换掉pass by value。

条款21:必须返回对象时,别妄想返回其reference

在掌握了pass by reference后,刚开始一心一意想把所有pass by value替换为passbyreference。这时往往会犯下一个错误:传递一些reference指向不存在的对象。考虑一个用以表现有理数乘积的class。

class Rational{public:Rational(int numerator=0, int denominator=1);……private:int n, d;friendconst Rational operator*(const Rational& lhs,const Rational& rhs);};这个版本的operator*用byvalue的方式返回其计算结果(对象)。这样返回的代价是一个对象的创建+析构+另一个对象的创建。

一个对象的创建是指,在这个operator*函数中,创建一个新对象来保存结果,之后用这个用这个新对象返回,返回时用它初始化另一个对象。之后这个新对象析构。

但是如果用by reference方式传递就不会有任何代价。但是reference只是名称,代表一个已经存在的对象,任何时候看到reference都应该问自己,它的另一个名称是什么?如果上述operator*返回一个reference,那么它一定指向一个存在的Rational对象。

Rational a(1, 2);// 1/2Rational b(3, 5);// 3/5Rational c=a*b;// 3/10这时返回一个值为3/10的Rational对象。但是这个对象原先并不存在,这时如果返回reference,那么必须在函数operator*内创建这个Rational对象。

函数创建新对象有两种途径,在stack上或在heap上创建。如果定义local变量,那么就在stack上创建

const Rational& operator*(const Rational& lhs, const Rational& rhs){Rational result(lhs.n* rhs.n, lhs.d* rhs.d);return result;}上面的做法,没有避免调用构造函数,result像任何对象一样由构造函数构造起来。上面还有一个错误:这个函数返回reference执行result,但是result是个local对象,它在函数退出前被销毁了。

考虑在heap上创建对象

const Rational& operator*(const Rational& lhs, const Rational& rhs){Rational* result=new Rational(lhs.n* rhs.n, lhs.d* rhs.d);return *result;}还是要付出一个构造函数的代价,分配的内存将以一个适当的构造函数初始化。现在又面临另一个问题:谁负责给你new出来的对象实施delete?却又小到连一粒嫉妒的沙石也不能容纳

《Effective C++》资源管理:条款20

相关文章:

你感兴趣的文章:

标签云: