读书笔记_Effective_C++_条款二十:宁以pass-by-reference-to-const替换pass-by-value
默认情况下,C++的函数调用是传值调用,也就是说形参拷贝实参的内容。举书上的例子:
1 class Person 2 { 3 private: 4 string name; 5 string address; 6 public: 7 Person(){} 8 virtual ~Person(){} 9 }; 10 11 class Student: public Person 12 { 13 private: 14 string schoolName; 15 string schoolAddress; 16 public: 17 Student(){} 18 ~Student(){} 19 };
假设有个函数接口:
void PrintStudent(Student ss);
那么如果这样调用:
1 Student s; 2 PrintStudent(s);
形参ss就会把实参s完完整整地复制一遍,这其中先后调用了Person的构造函数和Student的构造函数,而每个类中又包含两个string对象,所以总共会调用6次构造函数(4个string构造、Person构造和Student构造,注意这里的构造包括构造函数和拷贝构造函数),相应也会调用6次析构函数(注意当有派生关系存在时,基类的析构函数应该声明成虚的,这个在条款七中提到过)。
但事实上,在PrintStudent里面其实只是打印Student的信息,并没有对这个对象做出修改,所以完整的拷贝显然既浪费时间,又浪费空间。如果你习惯使用C,那么会想到把形参作成指针,这样拷贝只是拷贝一个指针的内容而已,像下面这样:
void PrintStudent(Student* ss);
调用的时候只要:
PrintStudent(&s);
就可以了。
对于C++而言,还可以使用引用,像这样:
void PrintStudent(Student& ss);
调用的时候只要:
PrintStudent(s);
就可以了,引用在原来代码的基础上只是多加了一个&,看上去比指针更自然,这算是C++的一个优点吧,在函数体中也可以避免对指针的操作,从而减少出错的可能性。
如果看汇编代码,就可以发现,引用的底层还是用指针实现的,所以两者看上去不同,但底层的原理却是相同的,就是对一个对象的地址进行操作,因此传引用的本质是传对象的地址。
传对象的地址是对原有对象直接操作,而不是它复本,所以这其中没有任何构造函数发生,是一种非常好的传参方法。但这种方法也有一定的“危险”性,与指针的功能相同,程序员可以在这个函数内部修改Student的内容,这样会出现一种现象,在主函数中Student是学生A,但通过这个函数之后,却变成了学生B。除非你刻意这样做,但像PrintStudent这种打印函数,是不应该修改原始内容的。
所以pass-by-reference还是不够的,用pass-by-reference-to-const才更“安全”,像这样:
void PrintStudent(const Student& ss);
这样即使程序员在函数实现中不小心修改了Student对象的内容,编译器也会把这个错误指出来。
除了上面所言,传引用调用可以减少构造函数的调用外,还可以防止对象切割,对象切割发生在存在派生关系的类中,比如:
void PrintStudent(Person ss);
调用时:
Studnet s;
PrintSudent(s);
这个时候就发生了对象切割,因为PrintStudnet的函数形参是基类,它没有派生类多出来的结构,所以当传值调用时,ss只能保存基类的内容,对于多出的派生类的内容作丢弃处理。
但如果换成传引用调用,像这样:
void PrintStudnet(Person &ss);
或者:
void PrintStudnet(Person *ss);
因为本质传的是地址,所以不存在对象的切割(地址类型Person&,可以理解为引用类型或者指针类型,存放的都是对象的地址,对于32位机而言,是4字节的整数,Person型的地址只是告诉编译器应该把这段地址的内容解释成什么)。Person&只是告诉编译器它保存的地址对应的内容是一个Person类型的,它会优先把这个内容往Person上去套,但如果里面有虚函数,即使用了virtual关键字,那么编译的时候就会往类中安插一个虚指针,这个虚指针的指向将在运行时决定,这就是多态的机制了,它会调用实际传入的那个对象的虚函数,而不是基类对象的虚函数。
如果没有接触过多态,上面说的可能就会有难度,其实也可以简单的理解成,如果不加引用或指针,那么形参就会复制实参,但形参是基类的,它没有实参多出来的那部分,所以它就不能复制了,只能丢弃了;但如果加了引用或指针,那么无论是什么类型的,都是保存实参的地址,地址是个好东西啊,它不会发生切割,不会丢弃。
总结一下pass-by-reference-to-const的优点:一是可以节省资源复制的时间和空间,二是可以避免切割,触发多态,在运行时决定调用谁的虚函数。
那么是不是pass-by-reference-to-const一定好呢?答案显示是否定的。一是任何事物都不是绝对的,二是C++默认是pass-by-value,自然有它的道理。
可以这样解释,因为pass-by-reference传的是地址,在32位机上,就是4字节,但如果参数是一个字节的char或者bool,那么这时候用char&或bool&来传引用就不划来了。一般地,当参数是基本类型时,还是用pass-by-value的效率更高,这个忠告也适用于STL的迭代器和函数对象。
对于复杂的类型,还是建议pass-by-reference-to-const,即使这个类很小。但事实上,一个类看似很小,比如只有一个指针,但这个指针也许指向的是一个庞然大物,在深拷贝的时候,所花费的代价还是很可观的。另一方面,谁也不能保证类的内容总是一成不变的,很有可能需要扩充,这样原本看似很小的类,过一段时间就成了“胖子”了。
最后总结一下:
1. 尽量以pass-by-reference-to-const来代替pass-by-value,前者通常比较高效,并可以避免切割
2. 以上规则并不适用于内置类型、STL迭代器和函数对象,对它们而言,pass-by-value是更好的选择