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

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

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

<span style="font-size:14px;">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;
};</span>

现在考虑一个函数validateStudent,它需要一个Student实参,以pass by value方式传递。
<span style="font-size:14px;">bool validateStudent(Student s);//pass by value
Student plato;
bool platIsOK=validateStudent(plato);</span>

当函数被调用时,copy构造函数会被调用,用plato构造s。在返回时,s会被析构。那么pass by value的代价就是Student的一次构造和一次析构。但是Student构造和析构时又发生了什么?它内部有两个string对象,所以会有两个string对象的构造和析构。Student继承自Person,又加上Person的构造和析构,Person内又有两个string对象,因此还要加上2个string对象的构造和析构。总共是六次构造和六次析构。

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

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

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

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

通常,内置类型都比较小,因此有人认小型types都适合pass by value,用户自己定义的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替换为pass by reference。这时往往会犯下一个错误:传递一些reference指向不存在的对象。考虑一个用以表现有理数乘积的class。

<span style="font-size:14px;">class Rational{
public:
	Rational(int numerator=0, int denominator=1);
	……
private:
	int n, d;
	friend
	const Rational operator*(const Rational& lhs,
							 const Rational& rhs);
};</span>

这个版本的operator*用by value的方式返回其计算结果(对象)。这样返回的代价是一个对象的创建+析构+另一个对象的创建。

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

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

<span style="font-size:14px;">Rational a(1, 2);// 1/2
Rational b(3, 5);// 3/5
Rational c=a*b;// 3/10</span>

这时返回一个值为3/10的Rational对象。但是这个对象原先并不存在,这时如果返回reference,那么必须在函数operator*内创建这个Rational对象。

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

<span style="font-size:14px;">const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	Rational result(lhs.n* rhs.n, lhs.d* rhs.d);
	return result;
}</span>

上面的做法,没有避免调用构造函数,result像任何对象一样由构造函数构造起来。上面还有一个错误:这个函数返回reference执行result,但是result是个local对象,它在函数退出前被销毁了。

考虑在heap上创建对象

<span style="font-size:14px;">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-size:14px;">Rational w,x,y,z;
w=x*y*z;</span>
上述语句调用了两次operator*,使用了两次new,也就需要两次delete。但是没办法让使用者进行那些delete操作,因为没有让他们取得operator*返回的reference背后的指针。这会导致内存泄露。

在上面的两种做法中(在stack和在heap上创建对象),都因为operator*返回结果调用构造函数而受到惩罚。我们最初的目标是避免如此的构造函数的调用。还有一个办法避免任何构造函数的调用:让operator*返回一个指向在函数内部定义的static Rational对象:

<span style="font-size:14px;">const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	static Rational result;
	result=……;
	return result;
}</span>

暂且不说上述代码在多线程时有什么问题,先看一下下面的调用的有什么问题:
<span style="font-size:14px;">bool operator==(const Rational& lhs, const Rational& rhs);//为比较Rational对象而写的
Rational a, b, c, d;
……
if((a*b)==(c*d))
{//乘积相等时,
	doSomething();
}
else//乘积不等时
{
	doOtherthing();
}</span>

上述表达式(a*b)==(c*d)总是返回true。因为operator*返回的对象都是指向operator*内部定义的static对象。这个对象只有一个,当计算后者时,前者被覆盖。因此永远是两个相同的Rational对象的作比较。

一个必须返回新对象的函数的正确写法,就是让那个函数返回一个新对象。就这么简单。上述operator*正确的写法:

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

这样,你需要承受operator*返回值的构造和析构成本,但是从长远来看,那只是为了获得正确的行为而付出的一个小小的代价。但万一代价比较恐怖,承受不起,这是别忘了C++和所有编程语言一样,允许编译器实现者施行最优化,用以改善产出代码的效率,却不改变其可观察的行为。如果编译器施行优化,你的程序将保持它们该有的行为,但运行起来比预期更快。

以上可以总结为:在返回一个reference和返回一个object之间抉择时,挑出行为正确的那个。让编译器厂商为你尽可能降低成本吧!


你可能感兴趣的:(C++,C++,effective,引用,reference)