• 读书笔记 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迭代器和函数对象类型。对于它们来说,按值传递通常是合适的。
  • 相关阅读:
    Microsoft Internet Explorer 多个不明细节远程代码执行漏洞
    给妈妈写程序
    平安夜,100篇
    我的DV,Sony HC90E
    听相声
    嘉陵江的石头,也许吧
    一步一个脚印-产品升级随笔(3)-学会拍板
    一步一个脚印-产品升级随笔(1)-vision
    《网站重构》网络资源总结
    一步一个脚印-产品升级随笔(2)-为什么需要BS架构的产品
  • 原文地址:https://www.cnblogs.com/harlanc/p/6440868.html
Copyright © 2020-2023  润新知