• Effective C++ 学习笔记(15)


    尽量使用“传引用”而不是“传值”


      C 语言中,什么都是通过传值来实现的,C++继承了这一传统并将它作为默认方式。除非明确指定,函数的形参总是通过“实参的拷贝”来初始化的,函数的调用者得到的也是函数返回值的拷贝。

      “通过值来传递一个对象”的具体含义是由这个对象的类的拷贝构造函数定义的。这使得传值成为一种非常昂贵的操作。例如,看下面这个(只是假想的)类的结构:

      

    class Person
    {
    public:
    Person();
    ~Person();

    ...

    private:
    string name, address;
    };

    class Student:public Person
    {
    public:
    Student();
    ~Student();

    ...
    private:
    string schoolName, schoolAddress;
    };

    Student returnStudent(Student s)
    {
    return s;
    }

    Student plato;

    returnStudent(plato);

      这个看起来无关痛痒的函数调用过程,其内部究竟发生了些什么呢?

      简单地说就是:首先,调用了 Student 的拷贝构造函数用以将s 初始化为plato;然后再次调用Student 的拷贝构造函数用以将函数返回值对象初始化为s;接着,s 的析构函数被调用;最后,returnStudent 返回值对象的析构函数被调用。所以,这个什么也没做的函数的成本是两个Student 的拷贝构造函数加上两个Student 析构函数。

      但没完,还有!Student 对象中有两个string 对象,所以每次构造一个Student对象时必须也要构造两个string 对象。Student 对象还是从Person 对象继承而来的,所以每次构造一个Student 对象时也必须构造一个Person 对象。一个Person 对象内部有另外两个string 对象,所以每个Person 的构造也必然伴随另两个string 的构造。所以,通过值来传递一个Student 对象最终导致调用了一个Student 拷贝构造函数,一个Person 拷贝构造函数,四个string 拷贝构造函数。当Student 对象被摧毁时,每个构造函数对应一个析构函数的调用。所以,通过值来传递一个Student 对象的最终开销是六个构造函数和六个析构函数。因为returnStudent 函数使用了两次传值(一次对参数,一次对返回值),这个函数总共调用了十二个构造函数和十二个析构函数!

      为避免这种潜在的昂贵的开销,就不要通过值来传递对象,而要通过引用:

      

    const Student& returnStudent(const Student& s)
    {
    return s; }

      这会非常高效:没有构造函数或析构函数被调用,因为没有新的对象被创建。

      通过引用来传递参数还有另外一个优点:它避免了所谓的“切割问题(slicing problem)”。当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。这往往不是你所想要的。例如,假设设计这么一套实现图形窗口系统的类:

      

    class Window 
    {
    public:
    string name() const; // 返回窗口名
    virtual void display() const; // 绘制窗口内容
    };
    class WindowWithScrollBars: public Window
    {
    public:
    virtual void display() const;
    };

      每个 Window 对象都有一个名字,可以通过name 函数得到;每个窗口都可以被显示,着可以通过调用display 函数实现。display 声明为virtual 意味着一个简单的Window 基类对象被显示的方式往往和价格昂贵的WindowWithScrollBars 对象被显示的方式不同(见条款36,37,M33)。

      现在假设写一个函数来打印窗口的名字然后显示这个窗口。下面是一个用错误的方法写出来的函数:

    // 一个受“切割问题”困扰的函数
    void printNameAndDisplay(Window w)
    {
    cout
    << w.name();
    w.display();
    }

      想象当用一个WindowWithScrollBars 对象来调用这个函数时将发生什么:

    WindowWithScrollBars wwsb;
    printNameAndDisplay(wwsb);

      参数w 将会作为一个Windows 对象而被创建(它是通过值来传递的,记得吗?),所有wwsb 所具有的作为WindowWithScrollBars 对象的行为特性都被“切割”掉了。printNameAndDisplay 内部,w 的行为就象是一个类Window的对象(因为它本身就是一个Window 的对象),而不管当初传到函数的对象类型是什么。尤其是, printNameAndDisplay 内部对display 的调用总是Window::display,而不是WindowWithScrollBars::display。

      解决切割问题的方法是通过引用来传递w:

      

    // 一个不受“切割问题”困扰的函数
    void printNameAndDisplay(const Window& w)
    {
    cout
    << w.name();
    w.display();
    }

      现在 w 的行为就和传到函数的真实类型一致了。为了强调w 虽然通过引用传递但在函数内部不能修改,就要采纳条款21 的建议将它声明为const。

      传递引用是个很好的做法,但它会导致自身的复杂性,最大的一个问题就是别名问题,这在条款17 进行了讨论。另外,更重要的是,有时不能用引用

    来传递对象,参见条款23。最后要说的是,,引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——

    例如int— — 传值实际上会比传引用更高效。

  • 相关阅读:
    微信客服系统开发SDK使用教程-给好友发消息任务
    微信客服系统开发SDK使用教程-客户端选择微信号登陆/登出通知
    微信客服系统开发SDK使用教程-客户端退出通知
    php优秀框架codeigniter学习系列——CI_Security类学习
    php优秀框架codeigniter学习系列——CI_Output类的学习
    php优秀框架codeigniter学习系列——CI_Router类学习
    My IELTS result has come out 我的雅思成绩出来了
    Travel notes in Vietnam
    asp.net学习
    makefile简单学习
  • 原文地址:https://www.cnblogs.com/DanielZheng/p/2127152.html
Copyright © 2020-2023  润新知