C++詞典

Const

五、對於函數

  1. void Fuction1 ( const int r );  

此處爲參數傳遞C++ const變量值,意義是變量初值不能被函數改變

  1. const int Fuction1 (int);  

此處返回const值,意思指返回的原函數裏的變量的初值不能被修改,但是函數按值返回的這個變量被製成副本,能不能被修改就沒有了意義,它可以被賦給任何的const或非const類型變量,完全不需要加上這個const關鍵字。但這隻對於內部類型而言(因爲內部類型返回的肯定是一個值,而不會返回一個變量,不會作爲左值使用),對於用戶自定義類型,返回值是常量是非常重要的,見下面條款。

  1. Class CX; //內部有構造函數,聲明如CX(int r =0)  
  2. CX Fuction1 () { return CX(); }  
  3. const CX Fuction2 () { return CX(); } 

如有上面的自定義類CX,和函數Fuction1()和Fuction2(),我們進行如下操作時:

  1. Fuction1() = CX(1); //沒有問題,可以作爲左值調用  
  2. Fuction2() = CX(1); //編譯錯誤,const返回值禁止作爲左值調用。
    因爲左值把返回值作爲變量會修改其返回值,const聲明禁止這種修改。 

4.函數中指針的C++ const變量傳遞和返回:

  1. int F1 (const char * pstr);  

作爲傳遞的時候使用const修飾可以保證不會通過這個指針來修改傳遞參數的初值,這裏在函數內部任何修改*pstr的企圖都會引起編譯錯誤。

  1. const char * F2 ();  

意義是函數返回的指針指向的對象是一個const對象,它必須賦給一個同樣是指向const對象的指針。

  1. const char * const F3();  

比上面多了一個const,這個const的意義只是在他被用作左值時有效,它表明了這個指針除了指向const對象外,它本身也不能被修改,所以就不能當作左值來處理。

5.函數中引用的const傳遞:

  1. void F1 ( const X& px);  

這樣的一個C++ const變量引用傳遞和最普通的函數按值傳遞的效果是一模一樣的,他禁止對引用的對象的一切修改,唯一不同的是按值傳遞會先建立一個類對象的副本,然後傳遞過去,而它直接傳遞地址,所以這種傳遞比按值傳遞更有效。

另外只有引用的const傳遞可以傳遞一個臨時對象,因爲臨時對象都是const屬性,且是不可見的,他短時間存在一個局部域中,所以不能使用指針,只有引用的const傳遞能夠捕捉到這個傢伙。


內聯函數

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

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

我們可以看到宏有一些難以避免的問題,怎麼解決呢?前面我們已經盡力替換了。
下面我們用內聯函數來解決這些問題。

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

(4)內聯函數和宏的區別?
內聯函數和宏的區別在於,宏是由預處理器對宏進行替代,而內聯函數是通過編譯器控制來實現的。而且內聯函數是真正的函數,只是在需要用到的時候,內聯函數像宏一樣的展開,所以取消了函數的參數壓棧,減少了調用的開銷。你可以象調用函數一樣來調用內聯函數,而不必擔心會產生於處理宏的一些問題。內聯函數與帶參數的宏定義進行下比較,它們的代碼效率是一樣,但是內聯歡函數要優於宏定義,因爲內聯函數遵循的類型和作用域規則,它與一般函數更相近,在一些編譯器中,一旦關上內聯擴展,將與一般函數一樣進行調用,比較方便。 

(5)什麼時候用內聯函數?
內聯函數在C++類中,應用最廣的,應該是用來定義存取函數。我們定義的類中一般會把數據成員定義成私有的或者保護的,這樣,外界就不能直接讀寫我們類成員的數據了。對於私有或者保護成員的讀寫就必須使用成員接口函數來進行。如果我們把這些讀寫成
員函數定義成內聯函數的話,將會獲得比較好的效率。
Class A
{
Private:
int nTest;
 Public:
int readtest() { return nTest;}
void settest(int I) { nTest=I; }
}

(6)如何使用內聯函數?
我們可以用inline來定義內聯函數。
inline int A (int x) { return 2*x; }
不過,任何在類的說明部分定義的函數都會被自動的認爲是內聯函數。

(7)內聯函數的優缺點?
我們可以把它作爲一般的函數一樣調用,但是由於內聯函數在需要的時候,會像宏一樣展開,所以執行速度確比一般函數的執行速度要快。當然,內聯函數也有一定的侷限性。就是函數中的執行代碼不能太多了,如果,內聯函數的函數體過大,一般的編譯器會放棄內聯方式,而採用普通的方式調用函數。(換句話說就是,你使用內聯函數,只不過是向編譯器提出一個申請,編譯器可以拒絕你的申請)這樣,內聯函數就和普通函數執行效率一樣了。

(8)如何禁止函數進行內聯?
如果使用VC++,可以使用/Ob命令行參數。當然,也可以在程序中使用 #pragma auto_inline達到相同的目的。

(9)注意事項:
1.在內聯函數內不允許用循環語句和開關語句。
2.內聯函數的定義必須出現在內聯函數第一次被調用之前。

using namespace std

      對於一個存在着標準輸入輸出的C++控制檯程序,一般會在#include <iostream>的下一行發現一句話,using namespace std。這句話其實就表示了所有的標準庫函數都在標準命名空間std中進行了定義。其作用就在於避免發生重命名的問題。
  1. 關於namespace
  C++引入了命名空間namespace主要解決了多個程序員在編寫同一個項目中可能出現的函數等重名的現象。解決方法就是加上自己的命名空間。比如下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
 
namespace ZhangSan
{
    int a=10;//張三把10賦值給了變量a
}
namespace LiSi
{
    int a=5;//李四把10賦值給了變量a
}
 
void main()
{
    int a=1;
    cout<<"張三定義的a="<<ZhangSan::a<<endl;
    cout<<"李四定義的a="<<LiSi::a<<endl;
    cout<<"主函數定義的a="<<a<<endl;   
}
  上例中的“ZhangSan::a”和“LiSi::a”分別表示了調用張三命名空間中的a變量和李四命名空間中的a變量。這樣的好處顯而易見,那就是雖然張三和李四這兩個程序員都定義了一個變量a,但是並不會出現重名的危險。
運行結果爲:
 
  
  2. 關於using namespace *
  顧名思義,using namespace * 就表示釋放命名空間* 中間的東西。好處在於我們在程序裏面就不用在每個函數的頭上都加上*::來調用。比如說如果上面那個程序,如果我們不在using namespace std,那麼我們就需要在主函數中的標準輸出流cout函數前面加上std,寫成
 
std::cout
表示調用std空間裏面的標準輸出流cout。但是有些時候我們也不能圖這個方便,比如說如果在主函數中將命名空間ZhangSan和LiSi的中所定義的變量釋放出來,如下例1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
 
namespace ZhangSan
{
    int a=10;//張三把10賦值給了變量a
}
namespace LiSi
{
    int a=5;//李四把10賦值給了變量a
}
 
void main()
{
    int a=1;
    using namespace ZhangSan;
    using namespace LiSi;
    cout<<a<<endl;
}
這個程序輸出結果爲:
如果我們在主函數中把 int a=1給刪除,如下例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
 
namespace ZhangSan
{
    int a=10;//張三把10賦值給了變量a
}
namespace LiSi
{
    int a=5;//李四把10賦值給了變量a
}
 
void main()
{
    using namespace ZhangSan;
    using namespace LiSi;
    cout<<a<<endl;
}
會發現根本就不會通過編譯,輸出的錯誤信息爲:
error C2872: “a”: 不明確的符號
  分析可以看出,上面這個例2會引起歧義。因爲ZhangSan中間的a被釋放出來,同理LiSi中間的a也被釋放出來了。那麼編譯器就不知道到底哪個纔是需要輸出的a,自然就會引起歧義了。同理,在例1中,編譯器同樣不知道到底哪個纔是需要輸出的a,於是它只採用了主函數中自己定義的a,這樣程序也不會報錯,但是隻會輸出1,自然結果就如上面的圖所示了。


typedef

四個用途

用途一:

定義一種類型的別名,而不只是簡單的宏替換。可以用作同時聲明指針型的多個對象。比如:
char* pa, pb; // 這多數不符合我們的意圖,它只聲明瞭一個指向字符變量的指針, 
// 和一個字符變量;
以下則可行:
typedef char* PCHAR; // 一般用大寫
PCHAR pa, pb; // 可行,同時聲明瞭兩個指向字符變量的指針
雖然:
char *pa, *pb;
也可行,但相對來說沒有用typedef的形式直觀,尤其在需要大量指針的地方,typedef的方式更省事。

用途二:

用在舊的C的代碼中(具體多舊沒有查),幫助struct。以前的代碼中,聲明struct新對象時,必須要帶上struct,即形式爲: struct 結構名 對象名,如:
struct tagPOINT1
{
int x;
int y;
};
struct tagPOINT1 p1;

而在C++中,則可以直接寫:結構名 對象名,即:
tagPOINT1 p1;

估計某人覺得經常多寫一個struct太麻煩了,於是就發明了:
typedef struct tagPOINT
{
int x;
int y;
}POINT;

POINT p1; // 這樣就比原來的方式少寫了一個struct,比較省事,尤其在大量使用的時候

或許,在C++中,typedef的這種用途二不是很大,但是理解了它,對掌握以前的舊代碼還是有幫助的,畢竟我們在項目中有可能會遇到較早些年代遺留下來的代碼。

用途三:

用typedef來定義與平臺無關的類型。
比如定義一個叫 REAL 的浮點類型,在目標平臺一上,讓它表示最高精度的類型爲:
typedef long double REAL; 
在不支持 long double 的平臺二上,改爲:
typedef double REAL; 
在連 double 都不支持的平臺三上,改爲:
typedef float REAL; 
也就是說,當跨平臺時,只要改下 typedef 本身就行,不用對其他源碼做任何修改。
標準庫就廣泛使用了這個技巧,比如size_t。
另外,因爲typedef是定義了一種類型的新別名,不是簡單的字符串替換,所以它比宏來得穩健(雖然用宏有時也可以完成以上的用途)。

用途四:

爲複雜的聲明定義一個新的簡單的別名。方法是:在原來的聲明裏逐步用別名替換一部分複雜聲明,如此循環,把帶變量名的部分留到最後替換,得到的就是原聲明的最簡化版。舉例:

1. 原聲明:int *(*a[5])(int, char*);
變量名爲a,直接用一個新別名pFun替換a就可以了:
typedef int *(*pFun)(int, char*); 
原聲明的最簡化版:
pFun a[5];

2. 原聲明:void (*b[10]) (void (*)());
變量名爲b,先替換右邊部分括號裏的,pFunParam爲別名一:
typedef void (*pFunParam)();
再替換左邊的變量b,pFunx爲別名二:
typedef void (*pFunx)(pFunParam);
原聲明的最簡化版:
pFunx b[10];

3. 原聲明:doube(*)() (*e)[9]; 
變量名爲e,先替換左邊部分,pFuny爲別名一:
typedef double(*pFuny)();
再替換右邊的變量e,pFunParamy爲別名二
typedef pFuny (*pFunParamy)[9];
原聲明的最簡化版:
pFunParamy e;

理解複雜聲明可用的“右左法則”:
從變量名看起,先往右,再往左,碰到一個圓括號就調轉閱讀的方向;括號內分析完就跳出括號,還是按先右後左的順序,如此循環,直到整個聲明分析完。舉例:
int (*func)(int *p);
首先找到變量名func,外面有一對圓括號,而且左邊是一個*號,這說明func是一個指針;然後跳出這個圓括號,先看右邊,又遇到圓括號,這說明(*func)是一個函數,所以func是一個指向這類函數的指針,即函數指針,這類函數具有int*類型的形參,返回值類型是int。
int (*func[5])(int *);
func右邊是一個[]運算符,說明func是具有5個元素的數組;func的左邊有一個*,說明func的元素是指針(注意這裏的*不是修飾func,而是修飾func[5]的,原因是[]運算符優先級比*高,func先跟[]結合)。跳出這個括號,看右邊,又遇到圓括號,說明func數組的元素是函數類型的指針,它指向的函數具有int*類型的形參,返回值類型爲int。

也可以記住2個模式:
type (*)(....)函數指針 
type (*)[]數組指針


虛函數表

 

C++ 瞭解的人都應該知道虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實現的。簡稱爲V-Table。在這個表中,主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了這個實例的內存中,所以,當我們用父類的指針來操作一個子類的時候,這張虛函數表就顯得由爲重要了,它就像一個地圖一樣,指明瞭實際所應該調用的函數。

 

這裏我們着重看一下這張虛函數表。C++的編譯器應該是保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證取到虛函數表的有最高的性能——如果有多層繼承或是多重繼承的情況下)。 這意味着我們通過對象實例的地址得到這張虛函數表,然後就可以遍歷其中函數指針,並調用相應的函數。

 

聽我扯了那麼多,我可以感覺出來你現在可能比以前更加暈頭轉向了。 沒關係,下面就是實際的例子,相信聰明的你一看就明白了。

 

假設我們有這樣的一個類:

 

class Base {

     public:

            virtual void f() { cout << "Base::f" << endl; }

            virtual void g() { cout << "Base::g" << endl; }

            virtual void h() { cout << "Base::h" << endl; }

 

};

 

按照上面的說法,我們可以通過Base的實例來得到虛函數表。 下面是實際例程:

 

          typedef void(*Fun)(void);

 

            Base b;

 

            Fun pFun = NULL;

 

            cout << "虛函數表地址:" << (int*)(&b) << endl;

            cout << "虛函數表 — 第一個函數地址:" << (int*)*(int*)(&b) << endl;

 

            // Invoke the first virtual function 

            pFun = (Fun)*((int*)*(int*)(&b));

            pFun();

 

實際運行經果如下:(Windows XP+VS2003,  Linux 2.6.22 + GCC 4.1.3)

 

虛函數表地址:0012FED4

虛函數表 — 第一個函數地址:0044F148

Base::f

 

 

通過這個示例,我們可以看到,我們可以通過強行把&b轉成int *,取得虛函數表的地址,然後,再次取址就可以得到第一個虛函數的地址了,也就是Base::f(),這在上面的程序中得到了驗證(把int*強制轉成了函數指針)。通過這個示例,我們就可以知道如果要調用Base::g()Base::h(),其代碼如下:

 

            (Fun)*((int*)*(int*)(&b)+0);  // Base::f()

            (Fun)*((int*)*(int*)(&b)+1);  // Base::g()

            (Fun)*((int*)*(int*)(&b)+2);  // Base::h()

 

這個時候你應該懂了吧。什麼?還是有點暈。也是,這樣的代碼看着太亂了。沒問題,讓我畫個圖解釋一下。如下所示:

注意:在上面這個圖中,我在虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符“/0”一樣,其標誌了虛函數表的結束。這個結束標誌的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛函數表,如果值是0,表示是最後一個虛函數表。

 

 

下面,我將分別說明“無覆蓋”和“有覆蓋”時的虛函數表的樣子。沒有覆蓋父類的虛函數是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是爲了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。

 

一般繼承(無虛函數覆蓋)

 

下面,再讓我們來看看繼承時的虛函數表是什麼樣的。假設有如下所示的一個繼承關係:

 

 

請注意,在這個繼承關係中,子類沒有重載任何父類的函數。那麼,在派生類的實例中,其虛函數表如下所示:

 

對於實例:Derive d; 的虛函數表如下:

 

我們可以看到下面幾點:

1)虛函數按照其聲明順序放於表中。

2)父類的虛函數在子類的虛函數前面。

 

我相信聰明的你一定可以參考前面的那個程序,來編寫一段程序來驗證。

 

 

 

一般繼承(有虛函數覆蓋)

 

覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什麼樣子?假設,我們有下面這樣的一個繼承關係。

 

 

 

爲了讓大家看到被繼承過後的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()。那麼,對於派生類的實例,其虛函數表會是下面的一個樣子:

 

 

我們從表中可以看到下面幾點,

1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。

2)沒有被覆蓋的函數依舊。

 

這樣,我們就可以看到對於下面這樣的程序,

 

            Base *b = new Derive();

 

            b->f();

 

b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。

 

 

 

多重繼承(無虛函數覆蓋)

 

下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關係。注意:子類並沒有覆蓋父類的函數。

 

 

 

對於子類實例中的虛函數表,是下面這個樣子:

 

我們可以看到:

1)  每個父類都有自己的虛表。

2)  子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

 

這樣做就是爲了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

 

 

 

 

多重繼承(有虛函數覆蓋)

 

下面我們再來看看,如果發生虛函數覆蓋的情況。

 

下圖中,我們在子類中覆蓋了父類的f()函數。

 

 

 

下面是對於子類實例中的虛函數表的圖:

 

 

我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,並調用子類的f()了。如:

 

            Derive d;

            Base1 *b1 = &d;

            Base2 *b2 = &d;

            Base3 *b3 = &d;

            b1->f(); //Derive::f()

            b2->f(); //Derive::f()

            b3->f(); //Derive::f()

 

            b1->g(); //Base1::g()

            b2->g(); //Base2::g()

            b3->g(); //Base3::g()

注意(安全性)

 每次寫C++的文章,總免不了要批判一下C++。這篇文章也不例外。通過上面的講述,相信我們對虛函數表有一個比較細緻的瞭解了。水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛函數表來乾點什麼壞事吧。

 

一、通過父類型的指針訪問子類自己的虛函數

我們知道,子類沒有重載父類的虛函數是一件毫無意義的事情。因爲多態也是要基於函數重載的。雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛函數,但我們根本不可能使用下面的語句來調用子類的自有虛函數:

 

          Base1 *b1 = new Derive();

            b1->f1();  //編譯出錯

 

任何妄圖使用父類指針想調用子類中的未覆蓋父類的成員函數的行爲都會被編譯器視爲非法,所以,這樣的程序根本無法編譯通過。但在運行時,我們可以通過指針的方式訪問虛函數表來達到違反C++語義的行爲。(關於這方面的嘗試,通過閱讀後面附錄的代碼,相信你可以做到這一點)

 

 

二、訪問non-public的虛函數

另外,如果父類的虛函數是private或是protected的,但這些非public的虛函數同樣會存在於虛函數表中,所以,我們同樣可以使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易做到的。

 

如:

 

class Base {

    private:

            virtual void f() { cout << "Base::f" << endl; }

 

};

 

class Derive : public Base{

 

};

 

typedef void(*Fun)(void);

 

void main() {

    Derive d;

    Fun  pFun = (Fun)*((int*)*(int*)(&d)+0);

    pFun();

}

 

 



注:上述內容中,

<const>轉自51CTO.com的博客http://developer.51cto.com/art/201002/182348.htm,他的博客http://www.51cto.com/

<內聯函數>轉自大龍的博客http://www.cppblog.com/fwxjj/archive/2007/04/20/22352.html,他的博客http://www.cppblog.com/fwxjj/

<using namespace std>轉自uniqueliu的博客http://www.cnblogs.com/uniqueliu/archive/2011/07/10/2102238.html,他的博客http://www.cnblogs.com/uniqueliu/

<虛函數表>轉自陳皓的博客http://blog.csdn.net/haoel/article/details/1948051/,他的博客http://my.csdn.net/haoel

<typedef>轉自漫步雲端的博客http://www.cnblogs.com/charley_yang/archive/2010/12/15/1907384.html,他的博客http://www.cnblogs.com/charley_yang/

(想看更詳細的內容,或有任何的疑問、看法、建議,請訪問作者原博客,留下你的寶貴意見。)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章