【Effective C++】设计与声明——reference篇
以pass-by-reference-to-const替换pass-by-value
缺省情况下C++以by value方式传递对象至(或来自)函数。除非你另外指定,否则函数参数都是以实际实参的复件(副本)为初值,而调用端所获得的亦是函数返回的一个复件,这些复件(副本)由对象的copy构造函数产出,这可能使得pass-by-value成为昂贵的(费时的)操作。
好处1:高效,避免副本对象的创建
考虑以下的继承体系:
class Person{
public:
Person();
virtual ~Person();
private:
string name;
string address;
};
class Student:public Person{
public:
Student();
~Student();
private:
string schoolName;
string schoolAddress;
};
现在考虑以下代码,
bool validateStudent(Student s); //函数需要一个student实参(by value)并返回它是否有效
Student plato;
bool IsOK = validateStudent(plato); //调用函数
在调用函数时,Student的copy构造函数会被调用,以plato为蓝本将s初始化,在函数返回时,s又会被销毁。因此对此函数而言,参数的传递成本是“一次Student Copy构造函数调用,加上一次Student析构函数调用”。注意,Student对象内有两个string对象,所以每次构造一个Student对象也就构造了两个string对象。此外,Student对象继承自Person对象,所以每次构造Student对象也必须构造出一个Person对象,Person对象又有两个string对象在其中。。。因此以by-value方式传递一个Student对象,总体成本是“六次构造函数和六次析构函数”!
如果传递对象的引用,高效的多:
bool validateStudent(const Student& s);
没有新的对象被创建,const也会保证不对s做任何修改。
好处2:避免对象切割问题
当一个derived class对象以value方式传递并视为一个base class对象,base class的copy构造函数会被调用,derived class对象的特性会被全部切割掉:
class Person{
public:
...
string name() const;
};
class Student:public Person{
public:
...
string name() const;
};
调用:
void show(Person p){
cout<<p.name();
}
Student s;
show(s);
此时调用的是Person::name()。解决切割问题的方法是以引用传递参数:
void show(const Person& p)
注意:内置类型,STL的迭代器和函数对象,往往pass-by-value比较适当。
必须返回对象时,别妄想返回其reference
考虑有理数Rational,有个友元操作符*,返回Rational对象。
Rational operator*( const Rational &lhs, const Rational &rhs ) { Rational result = ... return result; }
返回对象,导致临时对象的构造,析构。效率低,因此会想返回方法内局部对象的引用,这种方法不可行,为什么?
1. 定义一个局部变量,就是在stack空间创建对象,方法执行完毕,局部对象销毁。假如返回方法内局部对象的引用,方法执行完,局部对象销毁,这时候,引用指向一堆垃圾。
2.或者在heap上构造一个对象,返回引用。这也不可行,首先,要求客户端负责delete,这不合理。其次,退一步说,就算客户负责,执行delete。但是有些情况,客户无法执行delete。比如:Rational d = a*b*c; a*b的结果没有暴露出来,客户无法执行delete。
3. 既然方法内的局部对象会销毁,你可能会想返回一个reference指向方法内的local static对象,但是这会导致下面的诡异情况:无论何时,a*b == c*d 总是为真。为什么?
local static对象只初始化一次,a*b修改local static对象的值,并返回其引用,c*d也修改local static对象的值,并返回其引用。它们是同一个对象的别名,当然永远相等。只不过,后一次的修改覆盖了前一个修改。
因此,对于这种情况,就让方法返回对象。为了正确的行为,必须付出一定代价。需要说明的是,通过RVO技术,编译器可以进行优化,避免这种代价。
结论:绝不要返回pointer或reference指向一个local stack对象,返回reference指向一个heap-allocated对象,返回pointer或reference指向一个local static对象而可能同时需要多个这样的对象。