600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > 条款20 宁以pass-by-reference-to-const替换pass-by-value

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

时间:2023-01-09 16:25:10

相关推荐

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

总结:

1、尽量以pass-by-reference-to-const替换pass-by-value。前者更高效且可以避免切断问题。

2、这条规则并不适用于内建类型及STL中的迭代器和函数对象类型。对于它们,pass-by-value通常更合适。

缺省情况下,C++以传值方式将对象传入或传出函数(这是一个从C继承来的特性)。除非你另外指定,否则函数的参数就会以实际参数的副本进行初始化,而函数的调用者会收到函数返回值的一个复件。这个复件由对象的拷贝构造函数生成,这就使得传值成为一个代价不菲的操作。例如,考虑下面这个类继承体系:

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实参(以传值方式),并返回它是否有效:

bool validateStudent(Student s); // 函数以by value方式接受StudentStudent plato;bool platoIsOK = validateStudent(plato); //call the function

很明显,Student的拷贝构造函数被调用,用plato来初始化参数s。同样明显的是,当 validateStudent返回时,s就会被销毁。所以这个函数的参数传递代价是一次Student的拷贝构造函数的调用和一次Student的析构函数的调用。

但这还不是全部。Student对象内部包含两个string对象,Student对象还要从一个 Person对象继承,Person对象内部又包含两个额外的string对象。最终,以传值方式传递一个Student对象的后果就是引起一次Student的拷贝构造函数的调用,一次Person的拷贝构造函数的调用,以及四次string的拷贝构造函数调用。当Student对象的拷贝被销毁时,每一个构造函数的调用都对应一个析构函数的调用,所以以传值方式传递一个Student的全部代价是六个构造函数和六个析构函数

这是正确和值得的行为。毕竟,你希望全部对象都得到可靠的初始化和销毁。尽管如此,pass by reference-to-const方式会更好:

bool validateStudent(const Student& s);

这样做非常有效:没有任何构造函数和析构函数被调用,因为没有新的对象被构造。修改后参数声明中的const是非常重要的,原先validateStudent以by-value方式接受一个Student参数,所以调用者知道函数绝不会对它们传入的Student做任何改变,validateStudent只能改变它的复件。现在Student以引用方式传递,同时将它声明为const是必要的,否则调用者必然担心validateStudent改变了它们传入的Student

以传引用方式传递参数还可以避免切断问题(slicing problem)。当一个派生类对象作为一个基类对象被传递(传值方式),基类的拷贝构造函数被调用,而那些使得对象行为像一个派生类对象的特化性质被“切断”了,只剩下一个纯粹的基类对象例如,假设你在一组实现一个图形窗口系统的类上工作:

class Window {public:...std::string name() const; // 返回窗口名称virtual void display() const; // 显示窗口及其内容};class WindowWithScrollBars: public Window {public:...virtual void display() const;};

所有Window对象都有一个名字(name函数),而且所有的窗口都可以显示(display函数)。display为 virtual的事实清楚地告诉你:基类的Window对象的显示方法有可能不同于专门的WindowWithScrollBars对象的显示方法。现在,假设你想写一个函数打印出一个窗口的名字,并随后显示这个窗口。以下是错误示范:

void printNameAndDisplay(Window w) //incorrect! 参数可能被切割{std::cout << w.name();w.display();}

考虑当你用一个 WindowWithScrollBars 对象调用这个函数时会发生什么:

WindowWithScrollBars wwsb;printNameAndDisplay(wwsb);

参数w将被作为一个Window对象构造——它是被传值的,而且使wwsb表现得像一个 WindowWithScrollBars对象的特殊信息都被切断了。在printNameAndDisplay中,全然不顾传递给函数的那个对象的类型,w将始终表现得像一个Window 类的对象(因为其类型是Window)。因此在printNameAndDisplay中调用display将总是调用 Window::display,绝不会是WindowWithScrollBars::display。绕过切断问题的方法就是以passby reference-to-const方式传递w:

void printNameAndDisplay(const Window& w){ // 参数不会被切割std::cout << w.name();w.display();}

现在传进来的窗口是什么类型,w就表现出那种类型。用指针实现引用是非常典型的做法,所以pass by reference实际上通常意味着传递一个指针。由此可以得出结论,如果你有一个内置类型对象(一个int),以传值方式传递它常常比传引用方式更高效;同样的建议也适用于 STL 中的迭代器和函数对象。

一个对象小,并不意味着调用它的拷贝构造函数就是廉价的。很多对象(包括大多数STL容器)内含的东西只比一个指针多一些,但是拷贝这样的对象必须同时拷贝它们指向的每一样东西,那将非常昂贵。即使当小对象有一个廉价的拷贝构造函数,也会存在性能问题。一些编译器对内置类型和用户自定义类型并不一视同仁,即使他们有同样的底层表示。例如,一些编译器拒绝将仅由一个double组成的对象放入一个寄存器中,即使通常它们非常愿意将一个纯粹的double 放入那里。当这种事发生,你以传引用方式传递这样的对象更好一些,因为编译器理所当然会将一个指针(引用的实现)放入寄存器。

小的用户定义类型不一定是传值的上等候选者的另一个原因是:作为用户定义类型,它的大小常常变化。通常情况下,你能合理地假设传值廉价的类型仅有内置类型及STL中的迭代器和函数对象。对其他任何类型,请尽量以pass-by-reference-to-const替换pass-by-value。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。