程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 由一道題目想到的C++編譯器優化問題

由一道題目想到的C++編譯器優化問題

編輯:C++入門知識

這兩天看到了一個問題,看似簡單,但是用的知識著實不少,原題如下:


[cpp]
#include "stdafx.h"  
 
class Base 

public: 
    Base(){} 
    virtual ~Base(){} 
    Base(const Base &other);            // 只聲明, 沒定義  
private: 
    Base &operator=(const Base &other); 
} ; 
 
int _tmain(int argc, _TCHAR* argv[]) 

    const Base &b = Base() ;        // 為什麼沒有導致鏈接錯誤? 應該調用拷貝構造函數才對, 然而我只聲明沒定義!     
 
    return 0; 

#include "stdafx.h"

class Base
{
public:
    Base(){}
    virtual ~Base(){}
    Base(const Base &other);            // 只聲明, 沒定義
private:
    Base &operator=(const Base &other);
} ;

int _tmain(int argc, _TCHAR* argv[])
{
    const Base &b = Base() ;        // 為什麼沒有導致鏈接錯誤? 應該調用拷貝構造函數才對, 然而我只聲明沒定義!  

    return 0;
}

我一開始想到的就是編譯器優化了:推測下編譯器應該做了優化: const Base &b = Base() ;這麼寫的話,按照語義是:

1.調用Base的構造函數

2.調用Base的賦值函數 b=臨時對象但是編譯器大概認為沒必要這麼兩步走,直接調用了Base的構造函數。。。。

後面經過大俠A的指導,指出問題所在:

Base b = a 的時候, b 還沒有構造, 所以需要先對 b 進行構造, 再緊接著進行賦值, 你說 c++ 編譯器的設計者, 能不優化一下麼? 所以在所有 c++ 規定了這個行為, 即 Base b = a 就是Base
 b(a), 這也是 c++ 語法的 sweet.


恩。。。。那麼暫時我們就認為const Base &b = Base() ; 被編譯器變成了Base b(Base()),那麼還是應該會調用默認構造函數和拷貝構造函數啊,可以明明程序並沒有報錯!!!

關鍵人物大俠B出現了:


[cpp]
個人以為,const Base &b = Base() ; 引用是直接綁定在右邊的那個匿名對象上的,所以木有發生拷貝構造,所以跟拷貝構造函數是否定義木有關系。 
參考 ISO/IEC 14882:2003(E)   
8.5.3 References 
第4 5條款 
— Otherwise, the reference shall be to a non-volatile const type (i.e., cv1 shall be const).  
個人以為,const Base &b = Base() ; 引用是直接綁定在右邊的那個匿名對象上的,所以木有發生拷貝構造,所以跟拷貝構造函數是否定義木有關系。
參考 ISO/IEC 14882:2003(E) 
8.5.3 References
第4 5條款
— Otherwise, the reference shall be to a non-volatile const type (i.e., cv1 shall be const). [cpp]
[Example: 
double& rd2 = 2.0; // error: not an lvalue and reference not const  
int i = 2; 
double& rd3 = i; // error: type mismatch and reference not const  
—end example] 
— If the initializer expression is an rvalue, with T2 a class type, and “cv1 T1” is reference-compatible with “cv2 T2,” the reference is bound in one of the following ways (the choice is implementation-defined): 
— The reference is bound to the object represented by the rvalue (see 3.10) or to a sub-object within that object. 
— A temporary of type “cv1 T2” [sic] is created, and a constructor is called to copy the entire rvalue object into the temporary. The reference is bound to the temporary or to a sub-object within the temporary.93) 
[Example:
double& rd2 = 2.0; // error: not an lvalue and reference not const
int i = 2;
double& rd3 = i; // error: type mismatch and reference not const
—end example]
— If the initializer expression is an rvalue, with T2 a class type, and “cv1 T1” is reference-compatible with “cv2 T2,” the reference is bound in one of the following ways (the choice is implementation-defined):
— The reference is bound to the object represented by the rvalue (see 3.10) or to a sub-object within that object.
— A temporary of type “cv1 T2” [sic] is created, and a constructor is called to copy the entire rvalue object into the temporary. The reference is bound to the temporary or to a sub-object within the temporary.93)
看著這麼一大堆英文又迷糊了。。。。靜下心來好好讀了下:

關鍵在於這段話


[cpp]
— If the initializer expression is an rvalue, with T2 a class type, and “cv1 T1” is reference-compatible with “cv2 T2,” the reference is bound in one of the following ways (the choice is implementation-defined)://如果初始化表達的右邊是個類類型,並且左邊是一個對右值的常量引用,那麼這種情況呢,可以由編譯器用以下兩種方式實現(兩者選一):  
— The reference is bound to the object represented by the rvalue (see 3.10) or to a sub-object within that object.//直接將引用綁定到右值  
— A temporary of type “cv1 T2” [sic] is created, and a constructor is called to copy the entire rvalue object into the temporary. The reference is bound to the temporary or to a sub-object within the temporary.//臨時對象被創建,並且調用拷貝構造函數將臨時對象拷貝到右值,再將引用綁定到臨時對象。 
— If the initializer expression is an rvalue, with T2 a class type, and “cv1 T1” is reference-compatible with “cv2 T2,” the reference is bound in one of the following ways (the choice is implementation-defined)://如果初始化表達的右邊是個類類型,並且左邊是一個對右值的常量引用,那麼這種情況呢,可以由編譯器用以下兩種方式實現(兩者選一):
— The reference is bound to the object represented by the rvalue (see 3.10) or to a sub-object within that object.//直接將引用綁定到右值
— A temporary of type “cv1 T2” [sic] is created, and a constructor is called to copy the entire rvalue object into the temporary. The reference is bound to the temporary or to a sub-object within the temporary.//臨時對象被創建,並且調用拷貝構造函數將臨時對象拷貝到右值,再將引用綁定到臨時對象。
顯然我測試到的VC、GCC都選擇了第一種做法直接將引用綁定到右值。。。。~

關於引用的幾點說明,摘自於網絡:http://www.BkJia.com/kf/201203/121502.html


[cpp]
/*
(1)&在此不是求地址運算,而是起標識作用。
 
(2)類型標識符是指目標變量的類型。
 
(3)聲明引用時,必須同時對其進行初始化。
 
(4)引用聲明完畢後,相當於目標變量名有兩個名稱,即該目標原名稱和引用名,且不能再把該引用名作為其他變量名的別名。
 
ra=1; 等價於 a=1;
 
(5)聲明一個引用,不是新定義了一個變量,它只表示該引用名是目標變量名的一個別名,
它本身不是一種數據類型,因此引用本身不占存儲單元,系統也不給引用分配存儲單元。
故:對引用求地址,就是對目標變量求地址。&ra與&a相等。
 
(6)不能建立數組的引用。因為數組是一個由若干個元素所組成的集合,所以無法建立一個數組的別名。
*/ 
/*
(1)&在此不是求地址運算,而是起標識作用。

(2)類型標識符是指目標變量的類型。

(3)聲明引用時,必須同時對其進行初始化。

(4)引用聲明完畢後,相當於目標變量名有兩個名稱,即該目標原名稱和引用名,且不能再把該引用名作為其他變量名的別名。

ra=1; 等價於 a=1;

(5)聲明一個引用,不是新定義了一個變量,它只表示該引用名是目標變量名的一個別名,
它本身不是一種數據類型,因此引用本身不占存儲單元,系統也不給引用分配存儲單元。
故:對引用求地址,就是對目標變量求地址。&ra與&a相等。

(6)不能建立數組的引用。因為數組是一個由若干個元素所組成的集合,所以無法建立一個數組的別名。
*/

[cpp]
引用作為返回值,必須遵守以下規則: 
 
  (1)不能返回局部變量的引用。這條可以參照Effective C++[1]的Item 31。主要原因是局部變量會在函數返回後被銷毀,因此被返回的引用就成為了"無所指"的引用,程序會進入未知狀態。 
 
  (2)不能返回函數內部new分配的內存的引用。這條可以參照Effective C++[1]的Item 31。雖然不存在局部變量的被動銷毀問題,可對於這種情況(返回函數內部new分配內存的引用),又面臨其它尴尬局面。例如,被函數返回的引用只是作為一個臨時變量出現,而沒有被賦予一個實際的變量,那麼這個引用所指向的空間(由new分配)就無法釋放,造成memory leak。 
 
  (3)可以返回類成員的引用,但最好是const。這條原則可以參照Effective C++[1]的Item 30。主要原因是當對象的屬性是與某種業務規則(business rule)相關聯的時候,其賦值常常與某些其它屬性或者對象的狀態有關,因此有必要將賦值操作封裝在一個業務規則當中。如果其它對象可以獲得該屬性的非常量引用(或指針),那麼對該屬性的單純賦值就會破壞業務規則的完整性。 
 
  (4)引用與一些操作符的重載: 
 
  流操作符<<和>>,這兩個操作符常常希望被連續使用,例如:cout << "hello" << endl; 因此這兩個操作符的返回值應該是一個仍然支持這兩個操作符的流引用。可選的其它方案包括:返回一個流對象和返回一個流對象指針。但是對於返回一個流對象,程序必須重新(拷貝)構造一個新的流對象,也就是說,連續的兩個<<操作符實際上是針對不同對象的!這無法讓人接受。對於返回一個流指針則不能連續使用<<操作符。因此,返回一個流對象引用是惟一選擇。這個唯一選擇很關鍵,它說明了引用的重要性以及無可替代性,也許這就是C++語言中引入引用這個概念的原因吧。 賦值操作符=。這個操作符象流操作符一樣,是可以連續使用的,例如:x = j = 10;或者(x=10)=100;賦值操作符的返回值必須是一個左值,以便可以被繼續賦值。因此引用成了這個操作符的惟一返回值選擇。 

摘自 xiakan008的專欄
 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved