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

条款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实参,以pass by value方式传递。

bool validateStudent(Student s);//pass by value
Student plato;
bool platIsOK=validateStudent(plato);

当函数被调用时,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方式传递,可以回避所有构造函数和析构函数。

bool validateStudent(const Student& s);

这种方式传递,没有新对象创建,所以自然没有构造和析构函数的调用。参数中,以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。

class Rational{
public:
	Rational(int numerator=0, int denominator=1);
	……
private:
	int n, d;
	friend
	const Rational operator*(const Rational& lhs,
							 const Rational& rhs);
};

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

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

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

Rational a(1, 2);// 1/2
Rational b(3, 5);// 3/5
Rational 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?

即使客户使用时谨慎,但是还是有可能造成以下内存泄露:

Rational w,x,y,z;
w=x*y*z;

上述语句调用了两次operator*,使用了两次new,也就需要两次delete。但是没办法让使用者进行那些delete操作,因为没有让他们取得operator*返回的reference背后的指针。这会导致内存泄露。

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

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	static Rational result;
	result=……;
	return result;
}

暂且不说上述代码在多线程时有什么问题,先看一下下面的调用的有什么问题:

bool operator==(const Rational& lhs, const Rational& rhs);//为比较Rational对象而写的
Rational a, b, c, d;
……
if((a*b)==(c*d))
{//乘积相等时,
	doSomething();
}
else//乘积不等时
{
	doOtherthing();
}

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

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

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

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

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

时间: 2024-10-13 00:49:53

《Effective C++》资源管理:条款20-条款21的相关文章

《MORE EFFECTIVE C++》条款20 条款21

条款20 协助编译器实现返回值优化 当重载运算符的时候,比如+ - * / 这类运算符,该函数返回的值一定是个右值(即不能是引用),那么执行一次运算的开销可能会在临时对象上调用多次构造函数和析构函数,这笔开销还是很大的.现在的新编译器已经可以对这种情况进行优化了,甚至优化到连开销都没有,只是有一定的适用范围.如果可以返回一个匿名的临时对象,并且利用构造函数来得到结果对象,那么就有可能被优化到零开销.注意,有名字的对象意味着返回值优化不可用. 假设有如下的代码: 1 node a(2); 2 no

EC读书笔记系列之11:条款20、21

条款20 宁以pass-by-reference-to-const替换pass-by-value 记住: ★尽量以pass-by-reference-to-const替换pass-by-value.前者通常高效,并可避免切割问题 ★以上规则并不适用于内置类型,以及STL的迭代器和函数对象.那些应用pass-by-value 条款21 必须返回对象时,别妄想返回其reference 记住: ★绝不要返回pointer或reference指向一个local stack对象(如函数里的局部对象):或返

《Effective C++》条款20宁以pass-by-reference-to-const替换pass-by-value

<Effective C++> 条款20:宁以pass-by-reference-to-const替换pass-by-value 缺省情况下C++以by value方式传递对象至函数.除非你另外知道,否则函数参数都是以实际参数的副本为初值,而调用端所获得的亦是函数返回值的一个复件.这些复件系由copy构造函数产出,这可能使得pass-by-value成为昂贵的费时的操作. 通过pass-by-reference-to-const的传递方式效率高的多:原因是没有任何构造函数或析构函数被调用,因为

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

(一) 调用函数的时候如果传递参数pass-by-value,那么函数参数都是以实际实参的副本为初值,调用端所获得的亦是函数返回值的一个复件. 看下面代码: class Person { public: Person(); virtual ~Person(); private: string name; string address; }; class Student : public Person { public: Student(); ~Student(); private: string

effective C++ 读书笔记 条款20

条款20:宁以 pass-by-reference-to-const 替换 pass -by -value 1:采用后者效率高:看代码: #include <iostream> using namespace std; class Person { public: Person() { cout<<"Person()"<<endl; } Person(const Person& p) { cout<<"Person(co

《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.原

Effective C++_笔记_条款08_别让异常逃离析构函数

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码: 1: class Widget{ 2: public: 3: ... 4: ~Widget() {...} //假设这个可能吐出一个异常 5: }; 6:  7: void doSomething() 8: { 9: vector<Widget> v ; //v在这里被自动销毁 10: ...

Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数,这些“编译器生成版”的行为:将被烤对象的所有成员变量都做一份拷贝. 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为.编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你.所以自己实现copying函数时,请遵循一条规则:如果你为c

Effective C++ 阅读笔记_条款27 尽量少做转型动作

Effective C++ 阅读笔记_条款27 尽量少做转型动作 1.转型的三种形式,可以分为两大类. (1)旧式转型(old-style casts) (1.1) (T) expresstion (1.2) T (expression) (2) 新式转型(c++-style casts) (2.1)const_cast<T> (expression) (2.2)dynamic_cast<T> (expression) (2.3)reinterpret_cast<T>