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

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

时间:2022-11-29 22:14:39

相关推荐

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

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

条款20:宁以pass-by-reference-to-const替换pass-by-value1、值传递影响效率的原因2、高效操作:按const引用传递3、按const引用传递能避免对象切片/截断问题4、内置类型建议传值调用5、牢记总结

《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:

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

1、值传递影响效率的原因

缺省情况下,C++以传值的方式向函数传入或者从函数传出对象。除非你另外指定,否则函数参数都会以实际参数值的复件(副本)为初值,这些副本都是由对象的拷贝构造函数产出。这使得按值传递(pass-by-value)变成一项昂贵的操作。

值传递为什么会造成代码执行效率低?因为传值调用的时候,是传递对象的一个副本,因此是调用对象的拷贝构造函数将对象复制一份,然后传递给函数。效率比较低。

举个例子,考虑下面的类继承体系:有一个基类(Person)与一个派生类(Student),并且定义一个函数(validateStudent),参数接受一个Student对象(传值调用)

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;};

现在我们有如下的代码:

bool validateStudent(Student s);Student plato;//定义Student对象bool platoIsOK = validateStudent(plato); //传值调用

当函数被调用(传值调用)时会发生什么?

①执行6次构造函数:plato传入进函数的时候,需要调用1次Student的构造函数,Student构造函数执行前需要先构造Person ,因此还会调用1次Person类的构造函数。Person和Student两个类中共有4个string成员变量,因此还需要调用string的构造函数4次

②执行6次析构函数:当函数执行完成之后,传入validateStudent函数中的plato副本还需要执行析构函数,那么执行析构函数的时候分别要对应执行6次析构函数(Student+Person+4个string成员变量)

2、高效操作:按const引用传递

这是正确且值得拥有的行为。毕竟,你希望你的所有对象都能够被确实到构造和析构。但,如果有一种方法能够绕过这些构造函数和析构函数就再好不过了。这种方法是存在的,就是:按const引用进行传递(pass by reference-to-const)。

const:如果不想对象在函数被修改,那么就以const修饰

引用:引用在编译器底层为指针形式。因此使用引用把对象传递给函数,是直接将对象传递给函数,而不是将对象的副本传递给函数(避免了构造函数的调用以及析构函数的调用)

bool validateStudent(const Student& s);

这种用法更加高效:没有构造函数或者析构函数被调用,因为没有新的对象被创建。在修订后版本的参数声明中,const是很重要的。validataStudent的原始版本有一个按值传递的Studetn参数,调用者会知道对被传递进去的Student参数的任何可能的修改都会被屏蔽掉;validateStudent只是在修改它的一份拷贝。现在Student被按照引用进行传递,将其声明为const同样是必须的,否则调用者就会为传递进去的参数是否被修改而担心。

3、按const引用传递能避免对象切片/截断问题

对象切片/截断:如果将对象直接以传值方式调用,会造成对象的切片/截断问题。这种现象一般发生在函数的参数为基类类型,但是却将派生类对象传递给函数。

按引用传递参数同样避免了切片(slicing)问题。当一个派生类对象被当作一个基类对象被传递时(按值传递),基类的拷贝构造函数会被调用,“使对象的行为看起来像派生类对象“这个特定的特性被“切掉”了。留给你的只剩下一个基类对象,因为是一个基类的构造函数创建了它。这是你永远不希望看到的。

下面来看一个例子:有一个基类(Window)与一个派生类(WindowWithScrollBars)实现了图形化窗口系统:

class Window{public:...std::string name()const;virtual void display()const;};class WindowsWithScrollBars :public Window{public:...virtual void display()const;};

所有的窗口对象都有一个名字,你可以通过name函数来获取它,并且所有的窗口都能被显示出来,你可以通过触发display函数来实现。Display函数为虚函数(virtual)的事实告诉你基类Windows对象的显示方式同WindowWithScrollBars对象的显示方式是不同的。

假设你实现了一个函数,先打印窗口的名字然后让窗口显示出来。下面是错误示范:

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

当你使用一个WindowWithScrollBars对象作为参数调用这个函数会发生什么:

WindowsWithScrollBars wwsb;printNameAndDisplay(wwsb); //WindowsWithScrollBars对象会被截断

参数w将会被构造,它是按值传递的,所以w作为一个Window对象,所有让wwsb看起来像一个WIndowWithScrollBars对象的特定信息都会被切除。在printNameAndDispay内部,w的行为总是会像Window对象一样(因为他是一个Window类的对象),而不管传入函数的参数类型是什么。特别的,在printNameAndDisplay内部对display的调用总是会调用Window::display,永远不会调用WindowWithScrollBars::display。

解决切片问题的方法以const引用传递(by reference-to-const)w:

void printNameAndDisplay(const Window& w) //很好,参数不会被切割{std::cout << w.name();w.display();}int main(){WindowsWithScrollBars wwsb;printNameAndDisplay(wwsb); //传入的就是WindowsWithScrollBars类型的对象return 0;}

现在w的行为会和传入参数的实际类型一致了。

4、内置类型建议传值调用

如果窥视一下C++编译器的底层,你将会发现引用是按照指针来进行实现的,所以按引用传递一些东西就意味着传递一个指针。因此,如果你有一个内置类型的对象(例如int)按值传递比按引用传递效率更高。对于内置类型来说,当你在按值传递和按引用传递之间进行选择时,选择按值传递是合理的。这对于STL中的迭代器和函数对象同样适用,因为按照惯例,它们被设计成按值传递。迭代器和函数对象的设计者有责任留意下面两个问题:高效的拷贝和不用忍受切片问题。

内置类型占用了很少的内存,所以一些人得出结论:所有这样的小的类型都是按值传递的候选者,即使它们是用户定义的类型。这个原因是靠不住的。因为一个对象占用内存少并不意味这调用它的拷贝构造函数不昂贵。许多对象——这些对象中的大多数STL容器——仅仅包含一个指针,但是拷贝这些对象会拷贝它们指向的所有东西。这可是非常昂贵的操作。

即使是当小对象的拷贝构造函数的调用开销很小时,也会有性能问题。一些编译器对于内置类型和用户自定义类型有不同的对待方式,即使它们有相同的底层表示(underlying representation)。举个例子,一些编译器拒绝将只含有一个double数值的对象放入缓存中,却很高兴的为一个赤裸裸的double这么做。当这类事情发生的时候,将这些对象按引用传递会更好,因为编译器会将指针(引用的实现)放入缓存中。

另外一个小的用户自定义类型不是按值传递的好的候选者的原因是,作为用户自定义类型,它们的大小会发生变化。一个类型现在可能很小但是在将来的发布中可能会变的更大,因为它的内部实现可能发生变化。当你切换到一个不同的C++实现时事情也有可能发生变化。举个例子,标准库的string类型的一些实现比其他实现大6倍。

一般情况下,你能够对“按值传递是不昂贵的”进行合理假设的唯一类型就是内置类型和STL迭代器以及函数对象。对于其它的任何类型,遵循这个条款的建议,优先使用按const引用传递而不是按值传递。

5、牢记

尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题

以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,传值调用往往比较合适

总结

期待大家和我交流,留言或者私信,一起学习,一起进步!

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