读书笔记 effctive c++ Item 20 优先使用按const-引用传递(by-reference-to-const)而不是按值传递(by value)
1. 按值传递参数会有效率问题
默认情况下,C++向函数传入或者从函数传出对象都是按值传递(pass by value)(从C继承过来的典型特性)。除非你指定其他方式,函数参数会用实际参数值的拷贝进行初始化,函数调用者会获得函数返回值的一份拷贝。这些拷贝由对象的拷贝构造函数生成。这使得按值传递(pass-by-value)变成一项昂贵的操作。举个例子,考虑下面的类继承体系(Item 7):
1 class Person { 2 3 public: 4 5 Person(); // parameters omitted for simplicity 6 7 virtual ~Person(); // see Item 7 for why this is virtual 8 9 ... 10 11 private: 12 13 std::string name; 14 15 std::string address; 16 17 }; 18 19 class Student: public Person { 20 21 public: 22 23 Student(); // parameters again omitted 24 25 virtual ~Student(); 26 27 ... 28 29 private: 30 31 std::string schoolName; 32 33 std::string schoolAddress; 34 35 };
现在考虑下面的代码,在这里我们调用了一个函数,validateStudent,这个函数有一个Student参数(按值),返回值表示验证是否通过:
1 bool validateStudent(Student s); // function taking a Student 2 3 // by value 4 5 Student plato; // Plato studied under Socrates 6 7 bool platoIsOK = validateStudent(plato); // call the function
当函数被调用时会发生什么?
很清楚,Student拷贝构造函数会被调用,用plato来初始化参数s。同样很清楚的是,当validateStudent函数返回后s会被销毁。所以这个函数参数传递的开销是分别调用了构造函数和析构函数。
但这不是所有的开销。一个Student对象中有两个string对象,所以每次你构建一个Student对象的时候你必须构造两个string对象。Student对象继承自Person对象,所以每次你构建一个Student对象你必须构造一个Person对象。一个Person对象中有两个额外的string对象,所以每个Person构造函数同样需要对两个额外的string进行构造。最后结果是按值传递一个Student对象导致对Student拷贝构造函数的一次调用,对Person拷贝构造函数的一次调用,对stirng拷贝构造函数的四次调用。当Student对象的拷贝被释放时,每个构造函数对应的析构函数要被调用,所以按值传递一个Student对象的总开销是6次构造和6次析构!!
2. 按const引用传递会更高效
这是正确的并且令人满意的行为。毕竟,你需要的是所有对象被可靠的初始化和销毁。并且,如果有一种方法能够绕过这些构造函数和析构函数就再好不过了。这种方法是存在的,就是:按const引用进行传递(pass by reference-to-const)。
1 bool validateStudent(const Student& s);
这种用法更具效率:没有构造函数或者析构函数被调用,因为没有新的对象被创建。在修订后版本的参数声明中,const是很重要的。validataStudent的原始版本有一个按值传递的Studetn参数,调用者会知道对被传递进去的Student参数的任何可能的修改都会被屏蔽掉;validateStudent只是在修改它的一份拷贝。现在Student被按照引用进行传递,将其声明为const同样是必须的,否则调用者就会为传递进去的参数是否被修改而担心。
3. 按const引用传递能避免切片问题
按引用传递参数同样避免了切片(slicing)问题。当一个派生类对象被当作一个基类对象被传递时(按值传递),基类的拷贝构造函数会被调用,“使对象的行为看起来像派生类对象“这个特定的特性被“切掉”了。留给你的只剩下一个基类对象,因为是一个基类的构造函数创建了它。这是你永远不希望看到的。举个例子,假设你正在一些类上进行工作,这些类实现了图形化窗口系统:
1 class Window { 2 3 public: 4 5 ... 6 7 std::string name() const; // return name of window 8 9 virtual void display() const; // draw window and contents 10 11 }; 12 13 class WindowWithScrollBars: public Window { 14 15 public: 16 17 ... 18 19 virtual void display() const; 20 21 };
所有的窗口对象都有一个名字,你可以通过name函数来获取它,并且所有的窗口都能被显示出来,你可以通过触发display函数来实现。Display函数为虚函数的事实告诉你基类Windows对象的显示方式同WindowWithScrollBars对象的显示方式是不同的(Item 34和Item 36)。
现在假设你实现了一个函数,先打印窗口的名字然后让窗口显示出来。下面是实现这样一个函数的错误的方式:
1 void printNameAndDisplay(Window w) // incorrect! parameter 2 3 { // may be sliced! 4 5 std::cout << w.name(); 6 7 w.display(); 8 9 }
考虑当你使用一个WindowWithScrollBars对象作为参数调用这个函数会发生什么:
1 WindowWithScrollBars wwsb; 2 3 printNameAndDisplay(wwsb);
参数w将会被构造,它是按值传递的,所以w作为一个Window对象,所有让wwsb看起来像一个WIndowWithScrollBars对象的特定信息都会被切除。在printNameAndDispay内部,w的行为总是会像Window对象一样(因为他是一个Window类的对象),而不管传入函数的参数类型是什么。特别的,在printNameAndDisplay内部对display的调用总是会调用Window::display,永远不会调用WindowWithScrollBars::display。
解决切片问题的方法是将w按const引用传递进去(by reference-to-const):
1 void printNameAndDisplay(const Window& w) // fine, parameter won’t 2 3 { // be sliced 4 5 std::cout << w.name(); 6 7 w.display(); 8 9 }
现在w的行为会和传入参数的实际类型一致了。
4. 什么情况下按值传递是合理的
如果你偷看一下C++编译器的底层,你将会发现引用是按照指针来进行实现的,所以按引用传递一些东西就意味着传递一个指针。因此,如果你有一个内建类型的对象(例如int)按值传递比按引用传递效率更高。对于内建类型来说,当你在按值传递和按引用传递之间进行选择时,选择按值传递是合理的。这对于STL中的迭代器和函数对象同样适用,因为按照惯例,它们被设计成按值传递。迭代器和函数对象的设计者有责任留意下面两个问题:高效的拷贝和不用忍受切片问题。(这是一个规则如何被改变的例子,取决于你使用C++的哪一部分 见 Item 1。)
5. 并不是对象小就应该按值传递
内建类型占用了很少的内存,所以一些人得出结论:所有这样的小的类型都是按值传递的候选者,即使它们是用户定义的类型。这个原因是靠不住的。因为一个对象占用内存少并不意味这调用它的拷贝构造函数不昂贵。许多对象——这些对象中的大多数STL容器——仅仅包含一个指针,但是拷贝这些对象会拷贝它们指向的所有东西。这可是非常昂贵的操作。
即使是当小对象的拷贝构造函数的调用开销很小时,也会有性能问题。一些编译器对于内建类型和用户自定义类型有不同的对待方式,即使它们有相同的底层表示(underlying representation)。举个例子,一些编译器拒绝将只含有一个double数值的对象放入缓存中,却很高兴的为一个赤裸裸的double这么做。当这类事情发生的时候,将这些对象按引用传递会更好,因为编译器会将指针(引用的实现)放入缓存中。
另外一个小的用户自定义类型不是按值传递的好的候选者的原因是,作为用户自定义类型,它们的大小会发生变化。一个类型现在可能很小但是在将来的发布中可能会变的更大,因为它的内部实现可能发生变化。当你切换到一个不同的C++实现时事情也有可能发生变化。举个例子,标准库的string类型的一些实现比其他实现大6倍。
一般情况下,你能够对“按值传递是不昂贵的”进行合理假设的唯一类型就是内建类型和STL迭代器以及函数对象。对于其它的任何类型,遵循这个条款的建议,优先使用按const引用传递而不是按值传递。
6. 总结
- 优先使用按const-引用传递而不是按值传递。它更具效率并且能够避免切片问题。
- 这个规则不适用于内建类型,STL迭代器和函数对象类型。对于它们来说,按值传递通常是合适的。
作者:
HarlanC
博客地址:
http://www.cnblogs.com/harlanc/
个人博客:
http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出,
原文链接