程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++學習筆記(五) 類雜談

C++學習筆記(五) 類雜談

編輯:C++入門知識

const成員函數
const成員函數的存在的價值主要在於const對象。我們知道const對象是不可以被修改的,為了保證const對象不能被修改,編譯器規定const對象只能調用const修飾的成員函數,它會檢查該類成員函數以保證調用此函數不會修改對象的狀態。
Const對象只能調用const成員函數,而非const對象則是所有的成員函數都可以調用,但是,有時候,我們也希望const對象存在一個非const的版本。例如定義一個累加器類Accu:
[cpp] 
class Accu { 
    public: 
        Accu():data(0){} 
        Accu& add(int d){data += d; return *this;} 
        const Accu&show() const{cout<<data; return *this;}; 
    private: 
        int data; 

可以看出,show函數只是顯示結果,所以它是const成員函數。現在有一種編程風格叫做鏈式編程風格,用上面的類,我們可以如下編寫代碼:
[cpp] 
Accu ac; 
ac.add(1).add(30).show(); 

由於每個函數都返回了對象本身的引用,所以我們可以以鏈式的風格調用程序,這種風格的好處我就不談了,現在主要說明它的問題。細心的童鞋可能會發現,show函數只能放在最後被調用,因為它是const成員函數,它返回的是對對象的const引用,也就是說如下寫法將形成編譯錯誤:
[cpp] 
ac.add(1).add(30).show().add(6); 

不能在const對象上調用非const成員函數。

要解決這個問題,我們需要對show函數進行重載,添加一個非const的版本。
[cpp] 
Accu&show() {cout<<data; return *this;}; 

這樣子,問題就解決了。這裡面其實存在著這樣一種函數匹配優先級,那就是非const成員函數比const成員函數的優先級更高。所以重載後,非const對象調用的一定是非const版本的show函數,只有const對象才會調用const版本的show函數。

mutable數據成員
現實中可能有這樣的需求,要求即使在const對象中,該成員變量也可以被修改,這時候就可以用mutable關鍵字對該成員進行修飾,這樣即使在從const成員函數中也可以修改該成員的值了。

class 作用域
我們知道,在類的成員函數內部可以直接引用類中定義的成員變量,或是類型等,即使該成員函數是定義在類的外部,實際上,在參數列表中也可以直接引用的,只是返回值類型除外。原因是編譯在處理的時候只有遇到函數名時才會決定其作用域,也就是說凡是定義在函數名之後的都可以直接引用類的成員,而定義在它之前的返回值自然是除外的。請看下面這個例子:
[cpp] 
class A { 
    public: 
        typedef int byte32; 
        byte32 test(byte32 a); 
}; 
A::byte32 A::test(byte32 a) { 
//some code 

byte32是定義在類A內部的類型,因為返回值處在class作用域之外,所以需要加A::修飾,但參數列表是定義在函數名之後的,所以算是在class作用域之內,因此不需要加A::修飾。

講到這裡,就順便說一下C++的名稱查找機制吧,考慮下面的定義:
[cpp] 
typedef string Type; 
Type initVal(); 
class Exercise { 
public: 
// ... 
Type test(); 
typedef double Type; 
Type setVal(Type); 
Type initVal(); 
private: 
int val; 
}; 
Type Exercise::setVal(Type parm) { 
val = parm + initVal(); 

你能清楚的說出這裡面的Type都分別是在哪裡定義的嗎?

C++的名稱查找機制是這樣的,如果查找的是類型名,那麼首先在該類型使用的函數(或block)裡尋找,如果找不到,再到它所在的類中該函數定義之前的部分去尋找,如果,仍然找不到,則到該類的定義之前去尋找(如果該成員函數定義在類外,則是到該成員函數的定義之前去尋找);如果查找的是變量名或函數名,其區別在於,當查找class作用域時是搜索整個class而不僅僅是該變量的使用之前的部分。

所以,我們再回頭看一下代碼,class上面的Type initVal()的返回類型自然就是哪個全局Type了,而在類中的Type initVal()中的Type是什麼呢。因為這個Type不是在函數內部使用的,所以省去了查找函數作用域的一步,它直接查找類作用域,這樣就找到了在類中定義的Type。setVal函數的聲明和這是一樣的,都是類中定義的Type,關鍵看它的定義,我們前面說過,成員函數的參數列表是在class作用域中的,所以參數列表中Type是類中定義的Type,但返回值就不一樣了,它是那個全局Type。現在看該函數的內部,對於變量parm,編譯器首先搜索函數內部,發現了參數列表上的parma,就是它了。但是val和initVal在函數內部都木有聲明,所以,下一步就是搜索整個class作用域,發現了它們的聲明位置。在類中還有一個函數聲明Type test();由於它的聲明放在類中Type的定義之前,我們前面說過,編譯對於類型名在class作用域中的查找時只查找它在使用的地方之前的部分,所以這裡它是找不到Type的定義的,那麼接下來編譯器就會去搜查類定義之外的環境,從而找到全局的Type定義。

嗯,講的似乎有點亂,這裡。本來不想寫這一部分的,因為在實際的開發中命名重復是需要盡量避免的,所以這一部分實際上是理論意義大於實踐意義,為了筆記的完善,我還是把它寫上了,如果看不太明白就直接跳過吧。
初始化列表
在類的構造函數裡,C++為我們提供了一個機制,用於對類成員進行初始,這就是初始化列表。考慮下面構造函數的兩種定義方式:
[cpp] 
A(B& pb) { 
<span style="white-space:pre">  </span>b = pb; 

 
A(B& pb): b(pb) { 

在第二種方式裡,冒號後面的其實就是初始化列表,當這部分省略時,編譯器會使用默認的構造函數為每個類成員生成初始化語句,所以第一種方式就相當於:
[cpp] 
A(B& pb):b() { 
<span style="white-space:pre">  </span>b = pb; 

考慮這兩種方式,如果類型B是原生數據類型,二者之間實際上是沒有什麼區別的,但如果是類類型,就得考慮二者之間的性能開銷問題了,第一種方式首先調用默認的構造函數,然後再進行賦值,第二種方式則直接調用拷貝構造函數,假設這兩個構造函數定義如下:
[cpp] 
B():C(0) { 

 
B(B& b):C(b.c) { 

可以看出在這種情況下,默認構造函數和拷貝構造函數的性能是一樣的,但第一種方式多了賦值的開銷,如果說這種方式,它們的性能開銷差異還不夠名顯的話,那麼再考慮下面的情況:
[cpp] 
A(C& c):b() { 
<span style="white-space:pre">  </span>b = B(c); 

 
A(C& c):b(c) { 

這次,底一種是先調用了默認的構造函數,然後再調用了一個非默認構造函數,接著再調用了一個=操作(這裡可以重載),而第二種方式則只需要調用一次非默認的構造函數就可以了。
所以,總結,在一些情況下,使用初始化列表和在構造函數體裡面進行初始化,它們的性能是沒有多少差異的,但在另一些情況下,使用初始化列表將具有更高的效率。這是從效率上來說的,實際上,在某些情況下,如成員變量並沒有提供默認構造函數時,這就要求我們必須要使用初始化列表了。所以,當二者都處於備選時,初始化列表往往是我們更優的選擇,但這只是一種建議,而非強制的要求,具體情況還得看我們的業務邏輯。


作者:justaipanda

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