C++箴言:用傳引用給const取代傳值

字號:

缺省情況下,C++ 以傳值方式將對象傳入或傳出函數(shù)(這是一個從 C 繼承來的特性)。除非你特別指定其它方式,否則函數(shù)的參數(shù)就會以實際參數(shù)(actual argument)的拷貝進行初始化,而函數(shù)的調(diào)用者會收到函數(shù)返回值的一個拷貝。這個拷貝由對象的拷貝構(gòu)造函數(shù)生成。這就使得傳值(pass-by-value)成為一個代價不菲的操作。例如,考慮下面這個類層級結(jié)構(gòu):
    class Person {
    public:
    Person(); // parameters omitted for simplicity
    virtual ~Person(); // see Item 7 for why this is virtual
    ...
    private:
    std::string name;
    std::string address;
    };
    class Student: public Person {
    public:
    Student(); // parameters again omitted
    ~Student();
    ...
    private:
    std::string schoolName;
    std::string schoolAddress;
    };
    現(xiàn)在,考慮以下代碼,在此我們調(diào)用一個函數(shù)—— validateStudent,它得到一個 Student 參數(shù)(以傳值的方式),并返回它是否驗證有效的結(jié)果:
    bool validateStudent(Student s); // function taking a Student
    // by value
    Student plato; // Plato studied under Socrates
    bool platoIsOK = validateStudent(plato); // call the function
    當(dāng)這個函數(shù)被調(diào)用時會發(fā)生什么呢?
    很明顯,Student 的拷貝構(gòu)造函數(shù)被調(diào)用,用 plato 來初始化參數(shù) s。同樣明顯的是,當(dāng) validateStudent 返回時,s 就會被銷毀。所以這個函數(shù)的參數(shù)傳遞代價是一次 Student 的拷貝構(gòu)造函數(shù)的調(diào)用和一次 Student 的析構(gòu)函數(shù)的調(diào)用。
    但這還不是全部。一個 Student 對象內(nèi)部包含兩個 string 對象,所以每次你構(gòu)造一個 Student 對象的時候,你也必須構(gòu)造兩個 string 對象。一個 Student 對象還要從一個 Person 對象繼承,所以每次你構(gòu)造一個 Student 對象的時候,你也必須構(gòu)造一個 Person 對象。一個 Person 對象內(nèi)部又包含兩個額外的 string 對象,所以每個 Person 的構(gòu)造也承擔(dān)著另外兩個 string 的構(gòu)造。最終,以傳值方式傳遞一個 Student 對象的后果就是引起一次 Student 的拷貝構(gòu)造函數(shù)的調(diào)用,一次 Person 的拷貝構(gòu)造函數(shù)的調(diào)用,以及四次 string 的拷貝構(gòu)造函數(shù)調(diào)用。當(dāng) Student 對象的拷貝被銷毀時,每一個構(gòu)造函數(shù)的調(diào)用都對應(yīng)一個析構(gòu)函數(shù)的調(diào)用,所以以傳值方式傳遞一個 Student 的全部代價是六個構(gòu)造函數(shù)和六個析構(gòu)函數(shù)!
    好了,這是正確的和值得的行為。畢竟,你希望你的全部對象都得到可靠的初始化和銷毀。盡管如此,如果有一種辦法可以繞過所有這些構(gòu)造和析構(gòu)過程,應(yīng)該變得更好,這就是:傳引用給 const(pass by reference-to-const):
    bool validateStudent(const Student& s);
    這樣做非常有效:沒有任何構(gòu)造函數(shù)和析構(gòu)函數(shù)被調(diào)用,因為沒有新的對象被構(gòu)造。被修改的參數(shù)聲明中的 const 是非常重要的。 validateStudent 的最初版本接受一個 Student 值參數(shù),所以調(diào)用者知道它們屏蔽了函數(shù)對它們傳入的 Student 的任何可能的改變;validateStudent 也只能改變它的一個拷貝。現(xiàn)在 Student 以引用方式傳遞,同時將它聲明為 const 是必要的,否則調(diào)用者必然擔(dān)心 validateStudent 改變了它們傳入的 Student。
    以傳引用方式傳遞參數(shù)還可以避免切斷問題(slicing problem)。當(dāng)一個派生類對象作為一個基類對象被傳遞(傳值方式),基類的拷貝構(gòu)造函數(shù)被調(diào)用,而那些使得對象的行為像一個派生類對象的特殊特性被“切斷”了。你只剩下一個純粹的基類對象——這沒什么可吃驚的,因為是一個基類的構(gòu)造函數(shù)創(chuàng)建了它。這幾乎絕不是你希望的。例如,假設(shè)你在一組實現(xiàn)一個圖形窗口系統(tǒng)的類上工作:
    class Window {
    public:
    ...
    std::string name() const; // return name of window
    virtual void display() const; // draw window and contents
    };
    class WindowWithScrollBars: public Window {
    public:
    ...
    virtual void display() const;
    };
    所有 Window 對象都有一個名字,你能通過 name 函數(shù)得到它,而且所有的窗口都可以顯示,你可一個通過調(diào)用 display 函數(shù)來做到這一點。display 為 virtual 的事實清楚地告訴你:一個純粹的基類的 Window 對象的顯示方法有可能不同于專門的 WindowWithScrollBars 對象的顯示方法。