C++學習筆記

1.二義性和虛基類(轉)
 定義基類CBase,並定義CBase的派生類CDerived1CDerived2,在定義CDerived1CDerived2的派生類CDerived12,觀察二義性。 
   
   
代碼如下:
/************************************************************************
* 二義性問題
***********************************************************************
*/

//基類
class CBase
{
    
int a;
public:
    
int x;
    
void func();
}
;

//派生類1
class CDerive1:public CBase
{
    
int b;
public:
    
int y;
    
void func1();
}
;

//派生類2
class CDerive2:public CBase
{
    
int c;
public:
    
int z;
    
void func2();
}
;

//子派生類
class CDerive12:CDerive1,CDerive2
{
    
int d;
public:
    
int yz;
    
int func12()
    
{
        x
=10;    //error C2385: 'CDerived12::x' is ambiguous
        func();    //error C2385: 'CDerived12::func' is ambiguous
    }

}
;

void main()
{
    CDerived12 obj;
}
    
    
說明:
對於x=10;語句提示的警告如下:

warning C4385: could be the 'x' in base 'CBase' of base 'CDerive1' of class 'CDerive12' or the 'x' in base 'CBase' of base 'CDerive2' of class 'CDerive12'

 

對於func();語句提示的警告如下:

warning C4385: could be the 'func' in base 'CBase' of base 'CDerive1' of class 'CDerive12' or the 'func' in base 'CBase' of base 'CDerive2' of class 'CDerive12'

 

解析:

當一個基類被一個派生類間接繼承多次時,或者說在多條繼承鏈路有公共的基類,那麼該基類就會存在多個備份,系統無法分辨對基類成員的引用是通過哪個派生類繼承來的。於是編譯器對於這種不確定性問題發出錯誤信息。

上圖的例子,4個類的繼承關係如下:

  
   
    
   
所以,在函數func12中訪問x或者func函數時,編譯器就不知道是CDerive1CBase繼承來的,還是CDerive2CBase繼承來的。

 

要注意二義性(ambiguity)檢查是在訪問控制或類型檢查之前進行的,因此當不同基類成員中具有相同名字時就會出現二義性,即使只有一個名字可以被派生類訪問或只有一個名字的類型與要求相符。

 

解決二義性問題的方法:

1.利用範圍運算符指明所要調用的成員的類屬範圍;

2.在派生類中重新定義一個與基類中同名的成員,使該成員隱蔽基類的同名成員;

3.將公共基類說明爲虛基類,避免在派生類中保留多個基類的備份,而只保存一個實例

 

虛基類:

虛基類的定義:在定義派生類時,要在基類描述前加關鍵字virtual。這稱爲虛基類機制

 

引入虛基類的原因:

防止二義性;

使派生類中只有公共基類的一個數據副本;

2.關於虛函數:
關於虛函數:
1)虛函數和非虛函數調用方式有什麼不同
    非虛成員函數是靜態確定的,換句話說,該成員函數在編譯時就會被靜態地選擇。
    然而,虛成員函數是動態確定的,換句話說,成員函數在運行時才被動態地選擇,該選擇基於對象的類型,而不是指向該對象的指針或引用的類型。這被稱作“動態綁定”。
2)虛函數和重載有什麼不同
    虛函數看來於函數重載有些共通之處,但是函數重載在編譯期間就可以確定下來我們要使用的函數,是可預測的;而虛函數在運行時刻才能確定到具體的函數,是不可預測的,對於虛函數這一特性有一個專用術語----晚綁定,運用虛函數這種方法叫做函數覆蓋。
3)虛函數不可能是內聯的,原因看起來似乎也很明顯:
   (1)虛函數是在運行時機制而內聯函數特性是一個編譯時的機制;
   (2)聲明一個內聯的虛函數會使程序在執行的時間的產生多個函數拷貝,這將導致大量的空間的浪費

3.關於內聯函數:
(1)什麼是內聯函數?
內聯函數是指那些定義在類體內的成員函數,即該函數的函數體放在類體內。

(2)爲什麼要引入內聯函數?
當然,引入內聯函數的主要目的是:解決程序中函數調用的效率問題。另外,前面我們講到了宏,裏面有這麼一個例子:
#define ABS(x) ((x)>0? (x):-(x))
當++i出現時,宏就會歪曲我們的意思,換句話說就是:宏的定義很容易產生二意性。
  
我們可以看到宏有一些難以避免的問題,怎麼解決呢?前面我們已經盡力替換了。

下面我們用內聯函數來解決這些問題。

(3)爲什麼inline能取代宏?
1、 inline 定義的類的內聯函數,函數的代碼被放入符號表中,在使用時直接進行替換,(像宏一樣展開),沒有了調用的開銷,效率也很高。
2、 很明顯,類的內聯函數也是一個真正的函數,編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確。然後進行一系列的相關檢查,就像對待任何一個真正的函數一樣。這樣就消除了它的隱患和侷限性。
3、 inline 可以作爲某個類的成員函數,當然就可以在其中使用所在類的保護成員及私有成員。

(4)內聯函數和宏的區別?
內聯函數和宏的區別在於,宏是由預處理器對宏進行替代,而內聯函數是通過編譯器控制來實現的。而且內聯函數是真正的函數,只是在需要用到的時候,內聯函數像宏一樣的展開,所以取消了函數的參數壓棧,減少了調用的開銷。你可以象調用函數一樣來調用內聯函數,而不必擔心會產生於處理宏的一些問題。內聯函數與帶參數的宏定義進行下比較,它們的代碼效率是一樣,但是內聯歡函數要優於宏定義,因爲內聯函數遵循的類型和作用域規則,它與一般函數更相近,在一些編譯器中,一旦關上內聯擴展,將與一般函數一樣進行調用,比較方便。
(5)內聯函數的優缺點?
我們可以把它作爲一般的函數一樣調用,但是由於內聯函數在需要的時候,會像宏一樣展開,所以執行速度確比一般函數的執行速度要快。當然,內聯函數也有一定的侷限性。就是函數中的執行代碼不能太多了,如果,內聯函數的函數體過大,一般的編譯器會放棄內聯方式,而採用普通的方式調用函數。(換句話說就是,你使用內聯函數,只不過是向編譯器提出一個申請,編譯器可以拒絕你的申請)這樣,內聯函數就和普通函數執行效率一樣了。

4.關於構造函數和析構函數:
     構造函數和析構函數是負責對象的創建和撤銷的特殊成員函數。構造函數的作用是創建對象時進行初始化,析構函數的作用是釋放對象時清理現場,二者作用相反,名稱也正好相反。構造函數和析構函數之所以稱爲特殊的成員函數,是因爲二者都沒有類型說明符且程序中不能直接調用,在創建和撤銷對象時由系統調用自動執行。

1) 構造函數

   構造函數的名稱必須同類名一致,以便系統能識別爲構造函數。當使用聲明語句創建一個對象時隱含調用相應類的構造函數,爲對象分配空間和初始化。
    構造函數的特點:
   造函數不能指定返回類型,函數體中不允許有返回值。
   1.構造函數的說明可以在類體內,也可以在類體外,放在類體外的構造函數名前要加上"類名::"。
   2.構造函數可以有一個或多個參數,也可以沒有參數,當對象初始化時,需要定義帶參數的構造函數。
   3.構造函數可以重載,一個類可以定義多個參數個數不同的構造函數。
   4.如果一個類沒有定義任何構造函數,那麼C++就自動建立一個默認的構造函數,僅創建對象而不作任何初始化。
   5.默認構造函數是空函數,無參數,不能重載。

2 )拷貝構造函數

   當用一個已知對象初始化另一個對象時,系統將自動調用拷貝構造函數進行對象之間的值的拷貝。拷貝構造函數實際上也是一種構造函數,其名稱與類名相同。
   拷貝構造函數的特點如下:
   1.拷貝函數不能指定返回類型,函數體中不允許有返回值。
   2.拷貝函數的說明可以在類體內,也可以在類體外。放在類體外的拷貝函數名前要加上"類名::"。
   3.拷貝構造函數只有一個參數,並且該參數是所在類的對象的引用。
   4.拷貝構造函數的格式爲: <類名>::<拷貝構造函數名>(const <類名> & <引用名>)。
   5.如果一個類沒有定義任何拷貝構造函數,那麼C++就自動建立一個默認拷貝構造函數。該函數的功能是將已知對象的所有數據成員的值拷貝給相應對象的所有數據成員。

3 )析構函數

   析構函數的名稱與類名相同且前面加有非運算符"~",表明其功能和構造函數相反。
   析構函數的特點如下:
   1.拷貝函數不能指定返回類型,函數體中不允許有返回值。
   2.析構函數的說明可以在類體內,也可以在類體外。放在類體外的析構函數名前要加上"類名::"。
   3.析構函數沒有參數,因此析構函數不能重載,一個類只能定義一個析構函數。
   4.如果一個類沒有定義任何析構函數,那麼C++就自動建立一個默認的析構函數,只執行清理任務。
   5.默認析構函數是空函數,無參數,不能重載。
   6.析構函數的調用次序和構造函數的調用次序正好相反。
   在以下情況下,析構函數會自動被調用:
   ⑴如果一個對象被定義在一個函數體內,當這個函數結束時,該對象的析構函數被自動調用。
   ⑵若使用new運算符動態創建一個對象,在使用delete運算符釋放時,delete將會自動調用析構函數。


構造函數可以重載,析構函數不能重載(沒有參數)

5.關於#define與const
從功能上講都是對常量定義提供支持的一種機制,但是#define和const比起來還是有很多缺陷。

1) 在C++中採用const來定義常量,這樣程序在編譯的時候,提供安全類型檢查,而define只是簡單的進行了一個字符串替換。

2)很多集成開發環境都提供對const常量的調試,沒有多define的調試。

3)const 定義的常量,都內存中會分配相應大小的空間,而define沒有空間分配,只是在程序與處理的時候被替換掉。很有點類似word軟件中的replace功能。

6.關於static:
C中static函數只有在其所在的文件內有效。
c++類靜態數據成員與類靜態成員函數:
1)類的靜態成員是屬於類的而不是屬於哪一個對象的,所以靜態成員的使用應該是類名稱加域區分符加成員名稱的。
2)靜態成員函數的特性類似於靜態成員的使用,同樣與對象無關,調用方法爲類名稱加域區分符加成員函數名稱。
3)靜態成員函數由於與對象無關係,所以在其中是不能對類的普通成員進行直接操作的,靜態成員函數與普通成員函數的差別就在於缺少this指針。

默認static成員會被編譯器用0值初始化。靜態成員不能在構造函數中初始化,因爲靜態成員是所有對象共享的,而構造函數是每一個對象產生都會被調用,靜態函數初始化一般在CPP文件中,也就是在類的外部,而寫法一般是:
返回值     類名::變量名=初值.有點象是定義變量.

靜態成員是以static type 變量名,作爲格式,在內存中,只存儲一份,各個位置均可對其進行修改的成員。
  在類中,靜態成員可以實現多個對象之間的數據共享,並且使用靜態數據成員還不會破壞隱藏的原則,即保證了安全性。因此,靜態成員是類的所有對象中共享的成員,而不是某個對象的成員。
        使用靜態數據成員可以節省內存,因爲它是所有對象所公有的,因此,對多個對象來說,靜態數據成員只存儲一處,供所有對象共用。靜態數據成員的值對每個對象都是一樣,但它的值是可以更新的。只要對靜態數據成員的值更新一次,保證所有對象存取更新後的相同的值,這樣可以提高時間效率。
1)靜態數據成員在定義或說明時前面加關鍵字static。
2)靜態成員初始化與一般數據成員初始化不同。靜態數據成員初始化的格式如下:
<數據類型><類名>::<靜態數據成員名>=<值>
這表明:
                (1) 初始化在類體外進行,而前面不加static,以免與一般靜態變量或對象相混淆
                (2) 初始化時不加該成員的訪問權限控制符private,public等。
                (3) 初始化時使用作用域運算符來標明它所屬類,因此,靜態數據成員是類的成員,而不是對象的成員。
3)靜態數據成員是靜態存儲的,它是靜態生存期,必須對它進行初始化。
4)引用靜態數據成員時,採用如下格式:
<類名>::<靜態成員名>

靜態成員的使用範圍
1)用來保存對象的個數。
2)作爲一個標記,標記一些動作是否發生,比如:文件的打開狀態,打印機的使用狀態,等等。
3)存儲鏈表的第一個或者最後一個成員的內存地址

靜態數據,只能在外部初始化,靜態函數只能處理靜態數據成員。常量在構造函數初始化時進行。如:
class A
{
public:
A():PI(3.14159){}
static void Count(){ icount++; }


private:
static int icount;
const double PI;
};

A::icount = 0;
A obj1;
A::Count;

*構造函數非公有,可以限制某些形式的類對象的創建。
發佈了28 篇原創文章 · 獲贊 2 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章