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

1.前言

缺省情况下C++以by value方式传递对象至函数。除非另外指定了,否则函数参数都是以实际实参的副本为初值,而调用端所获得的亦是函数返回值的一个副本。这些副本是由对象的copy构造函数产出,这导致pass-by-value成为费时的操作。考虑以下class继承体系:

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实参(by value)并返回它是否有效:

bool validateStudent(Student s);//函数以by value方式接受学生
Student plato;
bool platoIsOk=validateStudent(plato);//调用函数

当上述程序代码运行时,发生什么事情呢?

首先,Student的copy构造函数会被调用,以plato为蓝本将s初始化。同样明显地,当validateStudent返回,s会被销毁。因此,对此函数而言,参数的传递成本是“一次Student copy构造函数调用,加上一次Studnet析构函数调用”。

再细一点分析,Student对象内有两个string对象,所以每次构造一个Student对象也就构造了两个string对象。此外Studnet对象继承自Person对象,所以每次构造Student对象也必须构造出一个Person对象。一个Person7对象又有两个string对象在其中,因此每一次Person构造动作又需要承担两个sting构造动作。最终结果即以by value方式传递一个Student对象会导致调用一次Student copy构造函数,一次Person copy构造函数,四次string copy构造函数。当函数内的那个Student复件被销毁,每一个构造函数调用动作都需要一个对应的析构函数调用动作。因此,以by value方式传递一个Student对象,总体成本是“六次构造函数和六次析构函数”。

以上操作是很正常的操作行为,毕竟希望所有对象都能够被构造和析构。尽管如此,如果有办法避免那些构造和析构操作就太好了,以下就是本节的重点,即pass by reference-to-const。


2为什么用pass by reference-to-const

示例:

bool validataStudent(const Student& s);

这种传递方式的效率高的多,主要是因为:没有任何构造函数和析构函数被调用,因此没有任何新对象被创建。修订后的这个参数声明中的const是重要的。原先的validateStudent以by value方式接受一个Student参数,因此调用者知道它们受到保护,函数内绝对不会对传入的Student作任何改变;validateStudent只能够对其副本做修改。但现在不一样,现在Student以by reference方式传递,将它声明为const是必要的。因为这样的话调用者就不能够更改Student里面的变量值。

另外,以by reference方式传递参数可以避免slicing(对象)切割问题。当一个derived class对象并且以by value方式传递,同时derived class被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。这是一种很正常的情况,因为正是base class构造函数建立了它。但这种情况绝不是我们想要的。假设你在一组classes上工作,用来实现一个图形窗口系统:

class Window{

    public:
        ...
        std::string name() const;//返回窗口名称
        virtual void display() const;//显示窗口和其内容

};

class WindowWithScrollBars:public Window{

    public:
        ...
        virtual void display() const;
};

在u这里,所有的Window对象都带有一个名称,我们可以通过name函数取得它。所有窗口都可以显示,可以通过display函数完成它。display是个virtual函数,这表明base class Window对象的显示方式和WindowWithScrollBars对象的显示方式不同。

假设你希望写个函数打印窗口名称,然后显示该窗口,下面是一个错误的例子:

void printNameAndDisplay(Window w)//不正确,参数可能被切割
{
    std::cout<

当调用上述函数并将给传递一个WindowWithScrollBars对象,会发生什么事情呢?

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

在这里,参数w会被构造成一个Window对象,它是passed by value。而造成wwsb之所以是个WindowWithScrollBars对象的所有特化信息都会被切除。 在printNameAndDisplay函数内无论传递过来的对象原本是什么类型,参数w均像一个Window对象。因此在printNameAndDisplay内调用display,调用的总是Window::display,绝不会是WindowWithcrollBars::display.

解决切割问题的方法就是by reference-to-const的方式传递w:

void printNameAndDisplay(const Window& w)//很好,参数不会被切割
{
    std::cout<

现在,传进来的窗口是什么类型,w就表现什么类型。

深究c++底层编译器,就会发现reference往往以指针实现出来,因此pass by reference通常意味真正传递的是指针。因此如果有个对象属于内置类型(例如int),pass by value往往比pass by reference的效率高些。通常情况下,pass-bu-value并不费时的唯一对象就是内置类型和STL的迭代器和函数对象。至于其它任何东西都请遵守本条款。

你可能感兴趣的:(c++,开发语言)