C++學習筆記(6)

44.談談重載(overload)覆蓋(override)與隱藏

       這三個概念都是與OO中的多態有關係的。如果單是區別重載與覆蓋這兩個概念是比較容易的,但是隱藏這一概念卻使問題變得有點複雜了,下面說說它們的區別吧。

       重載是指不同的函數使用相同的函數名,但是函數的參數個數或類型不同。調用的時候根據函數的參數來區別不同的函數。

       覆蓋(也叫重寫)是指在派生類中重新對基類中的虛函數(注意是虛函數)重新實現。即函數名和參數都一樣,只是函數的實現體不一樣。

       隱藏是指派生類中的函數把基類中相同名字的函數屏蔽掉了。隱藏與另外兩個概念表面上看來很像,很難區分,其實他們的關鍵區別就是在多態的實現上。什麼叫多態?簡單地說就是一個接口,多種實現吧。

       還是引用一下別人的代碼來說明問題吧(引用自林銳的《高質量C/C++編程指南》)。

仔細看下面的代碼:

#include <iostream.h>

    class Base

{

public:

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

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

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

};

    class Derived : public Base

{

public:

    virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }

void g(int x){ cout << "Derived::g(int) " << x << endl; }

            void h(float x){ cout << "Derived::h(float) " << x << endl; }

};

看出什麼了嗎?下面說明一下:

1)函數Derived::f(float)覆蓋了Base::f(float)。

2)函數Derived::g(int)隱藏了Base::g(float),而不是重載。

3)函數Derived::h(float)隱藏了Base::h(float),而不是覆蓋。

       嗯,概念大概明白了,但是在實際的編程中,我們會因此遇到什麼問題呢?再看下面的代碼:

void main(void)

{

Derived  d;

Base *pb = &d;

Derived *pd = &d;

// Good : behavior depends solely on type of the object

pb->f(3.14f); // Derived::f(float) 3.14

pd->f(3.14f); // Derived::f(float) 3.14

 

 

// Bad : behavior depends on type of the pointer

pb->g(3.14f); // Base::g(float) 3.14

pd->g(3.14f); // Derived::g(int) 3        (surprise!)

 

 

// Bad : behavior depends on type of the pointer

pb->h(3.14f); // Base::h(float) 3.14      (surprise!)

pd->h(3.14f); // Derived::h(float) 3.14

}

在第一種調用中,函數的行爲取決於指針所指向的對象。在第二第三種調用中,函數的行爲取決於指針的類型。所以說,隱藏破壞了面向對象編程中多態這一特性,會使得OOP人員產生混亂。

不過隱藏也並不是一無是處,它可以幫助編程人員在編譯時期找出一些錯誤的調用。但我覺得還是應該儘量使用隱藏這一些特性,該加virtual時就加吧。

45.調用約定

調用約定(Calling convention)決定以下內容:函數參數的壓棧順序,由調用者還是被調用者把參數彈出棧,以及產生函數修飾名的方法。MFC支持以下調用約定:

1_cdecl::

按從右至左的順序壓參數入棧,由調用者把參數彈出棧。對於“C”函數或者變量,修飾名是在函數名前加下劃線。對於“C++”函數,有所不同。

如函數void test(void)的修飾名是_test;對於不屬於一個類的“C++”全局函數,修飾名是?test@@ZAXXZ

這是MFC缺省調用約定。由於是調用者負責把參數彈出棧,所以可以給函數定義個數不定的參數,如printf函數。

2_stdcall

按從右至左的順序壓參數入棧,由被調用者把參數彈出棧。對於“C”函數或者變量,修飾名以下劃線爲前綴,然後是函數名,然後是符號“@”及參數的字節數,如函數int func(int a, double b)的修飾名是_func@12。對於“C++”函數,則有所不同。

所有的Win32 API函數都遵循該約定。

3_fastcall

頭兩個DWORD類型或者佔更少字節的參數被放入ECXEDX寄存器,其他剩下的參數按從右到左的順序壓入棧。由被調用者把參數彈出棧,對於“C”函數或者變量,修飾名以“@”爲前綴,然後是函數名,接着是符號“@”及參數的字節數,如函數int func(int a, double b)的修飾名是@func@12。對於“C++”函數,有所不同。

未來的編譯器可能使用不同的寄存器來存放參數。

4thiscall

僅僅應用於“C++”成員函數。this指針存放於CX寄存器,參數從右到左壓棧。thiscall不是關鍵詞,因此不能被程序員指定。

naked call

採用1-4的調用約定時,如果必要的話,進入函數時編譯器會產生代碼來保存ESIEDIEBXEBP寄存器,退出函數時則產生代碼恢復這些寄存器的內容。naked call不產生這樣的代碼。

5naked call不是類型修飾符,故必須和_declspec共同使用,如下:

__declspec( naked ) int func( formal_parameters )

{

// Function body

}

6)過時的調用約定

原來的一些調用約定可以不再使用。它們被定義成調用約定_stdcall或者_cdecl。例如:

#define CALLBACK __stdcall

#define WINAPI __stdcall

#define WINAPIV __cdecl

#define APIENTRY WINAPI

#define APIPRIVATE __stdcall

#define PASCAL __stdcall

46.關於複製構造函數

       也許很多C++的初學者都知道什麼是構造函數,但是對複製構造函數(copy constructor)卻還很陌生。對於我來說,在寫代碼的時候能用得上覆制構造函數的機會並不多,不過這並不說明覆制構造函數沒什麼用,其實複製構造函數能解決一些我們常常會忽略的問題。

       爲了說明覆制構造函數作用,我先說說我們在編程時會遇到的一些問題。對於C++中的函數,我們應該很熟悉了,因爲平常經常使用;對於類的對象,我們也很熟悉,因爲我們也經常寫各種各樣的類,使用各種各樣的對象;對於指針的操作,我們也不陌生吧?嗯,如果你還不瞭解上面三個概念的話,我想這篇文章不太適合你,不過看看也無礙^_^。我們經常使用函數,傳遞過各種各樣的參數給函數,不過把對象(注意是對象,而不是對象的指針或對象的引用)當作參數傳給函數的情況我們應該比較少遇見吧,而且這個對象的構造函數還涉及到一些內存分配的操作。嗯,這樣會有什麼問題呢?

       把參數傳遞給函數有三種方法,一種是值傳遞,一種是傳地址,還有一種是傳引用。前者與後兩者不同的地方在於:當使用值傳遞的時候,會在函數裏面生成傳遞參數的一個副本,這個副本的內容是按位從原始參數那裏複製過來的,兩者的內容是相同的。當原始參數是一個類的對象時,它也會產生一個對象的副本,不過在這裏要注意。一般對象產生時都會觸發構造函數的執行,但是在產生對象的副本時卻不會這樣,這時執行的是對象的複製構造函數。爲什麼會這樣?嗯,一般的構造函數都是會完成一些成員屬性初始化的工作,在對象傳遞給某一函數之前,對象的一些屬性可能已經被改變了,如果在產生對象副本的時候再執行對象的構造函數,那麼這個對象的屬性又再恢復到原始狀態,這並不是我們想要的。所以在產生對象副本的時候,構造函數不會被執行,被執行的是一個默認的構造函數。當函數執行完畢要返回的時候,對象副本會執行析構函數,如果你的析構函數是空的話,就不會發生什麼問題,但一般的析構函數都是要完成一些清理工作,如釋放指針所指向的內存空間。這時候問題就可能要出現了。假如你在構造函數裏面爲一個指針變量分配了內存,在析構函數裏面釋放分配給這個指針所指向的內存空間,那麼在把對象傳遞給函數至函數結束返回這一過程會發生什麼事情呢?首先有一個對象的副本產生了,這個副本也有一個指針,它和原始對象的指針是指向同塊內存空間的。函數返回時,對象的析構函數被執行了,即釋放了對象副本里面指針所指向的內存空間,但是這個內存空間對原始對象還是有用的啊,就程序本身而言,這是一個嚴重的錯誤。然而錯誤還沒結束,當原始對象也被銷燬的時候,析構函數再次執行,對同一塊系統動態分配的內存空間釋放兩次是一個未知的操作,將會產生嚴重的錯誤。

       上面說的就是我們會遇到的問題。解決問題的方法是什麼呢?首先我們想到的是不要以傳值的方式來傳遞參數,我們可以用傳地址或傳引用。沒錯,這樣的確可以避免上面的情況,而且在允許的情況下,傳地址或傳引用是最好的方法,但這並不適合所有的情況,有時我們不希望在函數裏面的一些操作會影響到函數外部的變量。那要怎麼辦呢?可以利用複製構造函數來解決這一問題。複製構造函數就是在產生對象副本的時候執行的,我們可以定義自己的複製構造函數。在複製構造函數裏面我們申請一個新的內存空間來保存構造函數裏面的那個指針所指向的內容。這樣在執行對象副本的析構函數時,釋放的就是複製構造函數裏面所申請的那個內存空間。

       除了將對象傳遞給函數時會存在以上問題,還有一種情況也會存在以上問題,就是當函數返回對象時,會產生一個臨時對象,這個臨時對象和對象的副本性質差不多。

       關於複製構造函數的性質還有很多,不過一時說不清楚,頭腦還比較亂。^_^

       C++真是複雜,不過複雜得來還挺有意思。

47.按位或運算和按位與運算

       在一個很大的系統中,我們經常會見到有類似以下的宏定義:

       #define DATA1 0x00000001

       #define DATA2 0x00000002

#define DATA3 0x00000004

#define DATA4 0x00000008

#define DATA5 0x00000010

…….

#define DATAn 0x04000000

那些16進制的數可不是任意寫上去的哦。如果把上面的十六進制轉成二進制來看的話,那麼:

0x00000001就是00000000000000000000000000000001

0x00000002就是00000000000000000000000000000010

0x00000004就是00000000000000000000000000000100

0x00000008就是00000000000000000000000000001000

0x00000010就是00000000000000000000000000010000

0x04000000就是00000100000000000000000000000000

當你把兩個數進行或運算的話,得出的結果就有兩個位爲1,把三個數進行或運算的話,那麼結果就有三個位爲1。例如:

#define MYDATA (DATA2 | DATA4)

那麼MYDATA的值就是00000000000000000000000000001010,即把DATA2DATA4不同位置的“1”信息整合到一個新的32位數據上了。

如果某函數返回來一個DWORD的結果給你,這個結果是多個DWORD數據進行或運算後得來的,我們就可以用與運算來判斷某個DWORD數據是否參與了之前或運算,舉例說明:

如果得到一個DWORD的返回值,將它保存在dwData中。之後我們選擇需要判斷的數據來與dwData進行與運算,例如選擇DATA3,有 DATA3&dwData,如果結果爲0,那麼DATA3並沒有參與之前的或運算,否則就是有參與。

利用這種特性,我們可以很方便地對參數進行組合,同樣也可以很方便地判斷某個參數是否參與過這種組合。

48.關於extern “C”

       使用extern “C”可以聲明一個函數或變量是使用C庫來進行鏈接的。一般來說,在編譯源代碼的時候,C++編譯器會對函數或變量進行一些函數名或變量名的轉換,例如對於函數foo(int a,int b)C++編譯器會將函數的名字變成_foo_int_int,這樣做是爲了支持C++中函數的重載特性。然而在C中是沒有函數重載的,所以C編譯器就不會進行這樣的名字轉換,它只是將函數名變成_foo而已。當我們在C++的環境中使用C編譯器所編譯的庫文件時,如果不使用extern “C”來聲明一個C庫函數,那麼在鏈接的時候就會找不到正確的C庫函數。在C++源文件中使用extern “C”可以避免C++編譯器對函數名或變量名所做的那些轉換,這樣在鏈接的時候就可以找到正確的C庫函數啦。

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