讀書筆記 effctive c++ Item 20 優先使用按const-引用傳遞(by-reference-to-const)而不是按值傳遞(by value)。本站提示廣大學習愛好者:(讀書筆記 effctive c++ Item 20 優先使用按const-引用傳遞(by-reference-to-const)而不是按值傳遞(by value))文章只能為提供參考,不一定能成為您想要的結果。以下是讀書筆記 effctive c++ Item 20 優先使用按const-引用傳遞(by-reference-to-const)而不是按值傳遞(by value)正文
默認情況下,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. 總結