雖然很難找到一本不討論多態性的C++書籍或雜誌,但是,大多數這類討論使多態性和C++虛函數的使用看起來很難。我打算在這篇文章中通過從幾個方面和結合一些例子使讀者理解在C++中的虛函數實現技術。說明一點,寫這篇文章只是想和大家交流學習經驗因為本人學識淺薄,難免有一些錯誤和不足,希望大家批評和指正,在此深表感謝。
在類中,如果什麼都沒有,則類佔用1個位元組,一旦類中有其他的佔用空間成員,則這1個位元組就不在計算之內,如一個類只有一個int則佔用4位元組而不是5位元組。
如果只有成員函數,則還是只佔用1個位元組,因為類函數不佔用空間
虛函數因為存在一個虛函數表,需要4個位元組,資料成員對象如果為指標則為4位元組,注意有位元組對齊,如果為13位元組,則進位到16位元組空間。
一、 基本概念
首先,C++通過虛函數實現多態."無論發送訊息的對象屬於什麼類,它們均發送具有同一形式的訊息,對訊息的處理方式可能隨接手訊息的對象而變"的處理方式被稱為多態性。"在某個基類上建立起來的類的層次構造中,可以對任何一個衍生類別的對象中的同名過程進行調用,而被調用的過程提供的處理可以隨其所屬的類而變。"虛函數首先是一種成員函數,它可以在該類的衍生類別中被重新定義並被賦予另外一種處理功能。
二、 虛函數的定義與衍生類別中的重定義
class 類名{
public:
virtual 成員函數說明;
}
class 類名:基類名{
public:
virtual 成員函數說明;
}
三、 虛函數在記憶體中的結構
1.我們先看一個例子:
#include "iostream.h"
#include "string.h"
class A {
public:
virtual void fun0() { cout << "A::fun0" << endl; }
};
int main(int argc, char* argv[])
{
A a;
cout << "Size of A = " << sizeof(a) << endl;
return 0;
}
結果如下:Size of A = 4
2.如果再添加一個虛函數:virtual void fun1() { cout << "A::fun" << endl;}
得到相同的結果。如果去掉函數前面的virtual修飾符
class A {
public:
void fun0() { cout << "A::fun0" << endl; }
};
int main(int argc, char* argv[])
{
A a;
cout << "Size of A = " << sizeof(a) << endl;
return 0;
}
結果如下:Size of A = 1
3.在看下面的結果:
class A {
public:
virtual void fun0() { cout << "A::fun0" << endl; }
int a;
int b;
};
int main(int argc, char* argv[])
{
A a;
cout << "Size of A = " << sizeof(a) << endl;
return 0;
}
結果如下:Size of A = 12
其實虛函數在記憶體中結構是這樣的:
圖一
在window2000下指標在記憶體中佔4個位元組,虛函數在一個虛函數表(VTABLE)中儲存函數地址。在看下面例子。
class A {
public:
virtual void fun0() { cout << "A::fun0" << endl; }
virtual void fun1() { cout << "A::fun1" << endl; }
};
int main(int argc, char* argv[])
{
A a;
cout << "Size of A = " << sizeof(a) << endl;
return 0;
}
結果如下:結果如下:
Size of A = 4
虛函數的記憶體結構如下,你也可以通過函數指標,先找到虛函數表(VTABLE),然後訪問每個函數地址來驗證這種結構,在國外網站作者是:Zeeshan Amjad寫的"ATL on the Hood中有詳細介紹"
圖二
4.我們再來看看繼承中虛函數的記憶體結構,先看下面的例子
class A {
public:
virtual void f() { }
};
class B {
public:
virtual void f() { }
};
class C {
public:
virtual void f() { }
};
class Drive : public A, public B, public C {
};
int main() {
Drive d;
cout << "Size is = " << sizeof(d) << endl;
return 0;
}
結果如下:Size is = 12 ,相信大家一看下面的結構圖就會很清楚,
圖三
5.我們再來看看用虛函數實現多態性,先看個例子:
class A {
public:
virtual void f() { cout << "A::f" << endl; }
};
class B :public A{
public:
virtual void f() { cout << "B::f" << endl;}
};
class C :public A {
public:
virtual void f() { cout << "C::f" << endl;}
};
class Drive : public C {
public:
virtual void f() { cout << "D::f" << endl;}
};
int main(int argc, char* argv[])
{
A a;
B b;
C c;
Drive d;
a.f();
b.f();
c.f();
d.f();
return 0;
}
結果:A::f
B::f
C::f
D::f
不用解釋,相信大家一看就明白什麼道理。注意:多態不是函數重載
6.用虛函數實現動態串連 在編譯期間,C++編譯器根據程式傳遞給函數的參數或者函數傳回型別來決定程式使用那個函數,然後編譯器用正確的的函數替換每次啟動。這種基於編譯器的替換被稱為靜態串連,他們在程式運行之前執行。另一方面,當程式執行多態性時,替換是在程式執行期進行的,這種運行期間替換被稱為動態串連。如下例子:
class A{
public:
virtual void f(){cout << "A::f" << endl;};
};
class B:public A{
public:
virtual void f(){cout << "B::f" << endl;};
};
class C:public A{
public:
virtual void f(){cout << "C::f" << endl;};
};
void test(A *a){
a->f();
};
int main(int argc, char* argv[])
{
B *b=new B;
C *c=new C;
char choice;
do{
cout<<"type B for class B,C for class C:"<<endl;
cin>>choice;
if(choice==''b'')
test(b);
else if(choice==''c'')
test(c);
}while(1);
cout<<endl<<endl;
return 0;
}
在上面的例子中,如果把類A,B,C中的virtual修飾符去掉,看看列印的結果,然後再看下面一個例子想想兩者的聯絡。如果把B和C中的virtual修飾符去掉,又會怎樣,結果和沒有去掉一樣。
7.在基類中調用繼承類的函數(如果此函數是虛函數才能如此) 還是先看例子:
class A {
public:
virtual void fun() {
cout << "A::fun" << endl;
}
void show() {
fun();
}
};
class B : public A {
public:
virtual void fun() {
cout << "B::fun" << endl;
}
};
int main() {
A a;
a.show();
return 0;
}
列印結果:A::fun
在6中的例子中,test(A *a)其實有一個繼承類指標向基類指標隱式轉化的過程。可以看出利用虛函數我們可以在基類調用繼承類函數。但如果不是虛函數,繼承類指標轉化為基類指標後只可以調用基類函數。反之,如果基類指標向繼承類指標轉化的情況怎樣,這隻能進行顯示轉化,轉化後的繼承類指標可以調用基類和繼承類指標。如下例子:
class A {
public:
void fun() {
cout << "A::fun" << endl;
}
};
class B : public A {
public:
void fun() {
cout << "B::fun" << endl;
}
void fun0() {
cout << "B::fun0" << endl;
}
};
int main() {
A *a=new A;
B *b=new B;
A *pa;
B *pb;
pb=static_cast<B *>(a); //基類指標向繼承類指標進行顯示轉化
pb->fun0();
pb->fun();
return 0;
}
參考資料:
1.科學出版社 《C++程式設計》
2.Zeeshan Amjad 《ATL on the Hood》
補充(本人總結):
class CA
{
public:
CA(void);
virtual void f(){int a = 10;}
virtual void g(){ }
private:
int c;
};
此時sizeof(CA) 等於8;
class CA
{
public:
CA(void);
virtual void f(){int a = 10;}
private:
int c;
};
此時sizeof(CA) 等於8;
class CA
{
public:
CA(void);
virtual void f(){int a = 10;}
private:
int c;
char ch;
};
此時sizeof(CA) 等於12;
class CA
{
public:
CA(void);
virtual void f(){int a = 10;}
private:
int c;
char ch1;
char ch2
}; 此時sizeof(CA) 等於12;
class CA
{
public:
CA(void);
virtual void f(){int a = 10;}
private:
int c;
char ch1;
int d;
}; 此時sizeof(CA) 等於16;
因為類的分布也要考慮位元組對齊的機制
C++中的static關鍵字繼承自C語言,包含兩方面含義:靜態儲存和可見度控制。定義成靜態類型的變數會被儲存在全域資料區,定義成靜態類型的變數或者函數只能在本編譯單元內被訪問到,這樣在不同檔案中定義同名的變數就不會發生重複定義錯誤了。C++中static的應用分為兩個方面:一是用於普通的函數和變數,二是在類中的應用。
一、在普通函數和變數中的應用
1、靜態全域變數
在全域變數前面加上static,該變數就成了一個靜態全域變數。靜態全域變數有如下特點:
1)、變數儲存在全域資料區;
2)、變數會被自動初始化為0;
3)、靜態全域變數在整個編譯單元(整個檔案)內都是可見的,而在檔案之外是不可見的。
靜態變數自動初始化為0的特性可以給程式開發帶來某些便利,比如定義一個字串數組為了保證字元結尾為'/0'需要遍曆整個數組進行初始化,但是定義成靜態字串數組會被自動填滿為0,'/0'就是0,所以說利用這些特性可以增加程式的運行效率。對於當前編譯單元內需要共用的全域變數可以設定為靜態,這樣既不用擔心在其他檔案中有重名的問題又能滿足要求。
如下代碼展示了靜態全域變數的特性:
view plain copy to clipboard print ?
// file1.cpp
#include <iostream>
using namespace std;
static int n = 1;
void fun2();
void fun1() {
cout << n << endl;
}
int main() {
fun1();
fun2();
return 1;
}
view plain copy to clipboard print ?
// file2.cpp
#include <iostream>
using namespace std;
extern int n;
void fun2() {
cout << n << endl;
}
分別編譯兩個檔案,然後串連,出現錯誤,說找不到n,這就說明n只在file1.cpp中有效file2.cpp。現在將file2中的extern int n改為int n = 2,重新編譯運行,將輸出:1 2,這說明file1和file2分別用了自己檔案中定義的n,這就是可見度控制。
2、靜態局部變數
在函數體內定義一個局部變數,變數被儲存在堆棧中,函數返回後堆棧被恢複變數就被釋放了。但是靜態局部變數和靜態全域變數相同,都是儲存在全域資料區,在沒有被顯式初始化的情況下也會被初始化為0,函數返回後仍然有效,函數重入後資料依然是上次的結果,這和全域變數有相同的效果,但是全域變數不只是屬於函數本身,其他代碼也能看到很容易破壞變數的內容,所以靜態局部變數在某些需要儲存函數運行狀態的時候很有用。
例如:
view plaincopy to clipboardprint?
// file3.cpp
#include <iostream>
using namespace std;
void fun() {
static int n = 1;
cout << n << endl;
n++;
}
int main() {
fun();
fun();
fun();
return 1;
}
編譯運行,結果將輸出1 2 3,這說明函數每次調用n的值都被儲存了下來。
3、靜態函數
在函數定義前加上static就將函數定義為靜態了,靜態函數的可見度與靜態全域變數相同,只在當前檔案中有效,不能被其他檔案看到。除了這些特點靜態函數和其它函數沒有什麼區別,這一點也就是體現出了static關鍵字在控制可見度方面的用處。
view plain copy to clipboard print ?
// file4.cpp
#include <iostream>
using namespace std;
static void fun() {
cout << "fun" << endl;
}
view plain copy to clipboard print ?
// file5.cpp
void fun();
int main() {
fun();
}
單獨編譯這兩個檔案,串連成一個程式會出現錯誤:找不到fun()函數,將file4中fun函數定義中的static去掉就OK了。
二、在類中的應用
1、類中的靜態成員變數
在類的成員變數聲明前加上static,該變數就成為了類的靜態成員變數,例如:
view plain copy to clipboard print ?
// file6.cpp
#include <iostream>
using namespace std;
class A {
int size;
public:
static int n;
};
int A::n = 1;
int main() {
A a, b;
A::n++;
cout << "A::n = " << A::n << endl;
cout << "a.n = " << a.n <<endl;
a.n++;
cout << "b.n = " << b.n << endl;
cout << "sizeof(A) = " << sizeof(A) << endl;
cout << "sizeof(a) = " << sizeof(a) << endl;
return 1;
}
編譯運行,產生如下輸出:
A::n = 2
a.n = 2
b.n = 3
sizeof(A) = 4
sizeof(a) = 4
可以看出類的靜態成員變數是屬於類本身的,而不是屬於某個對象的,所有對象看到的n值都相同,然後看類A和對象a的尺寸,只有int的大小4,這說明靜態成員變數和對象本身並不在同一個儲存地區,實際上類的靜態存成員變數和其他靜態變數一樣也是儲存在全域資料區的,這個問題我在去麥石應聘筆試的時候曾經遇到過,再關注一下。
對靜態成員變數的初始化可以採用如下形式:
<類型名> <類名>::<靜態成員變數名> = <值>
對於靜態成員變數的引用可以通過如下形式:
<類名>::<靜態成員變數名> = <值>
<對象名>.<靜態成員變數名>
類的靜態成員變數的意義在於某些類可能存在所有對象公用的屬性,這個時候就可以使用靜態成員變數,不僅可以節省儲存空間,而且當公用屬性改變後所有對象都能看到,不需要再通知所有的對象。而且類的靜態成員變數和普通成員變數一樣都可以設定存取權限控制,比如設定成private,則該變數只能在類內部被調用。
2、類的靜態成員函數
類的靜態成員函數與靜態成員變數一樣,屬於類而不屬於對象。一般類的成員函數都有一個隱含的this指標當做參數,這樣函數就可以訪問對象的成員變數,但是靜態成員函數沒有這個參數,所以靜態成員函數不能訪問對象的成員變數,而只能訪問其他靜態成員。下面是使用靜態成員函數的例子:
view plain copy to clipboard print ?
// file7.cpp
#include <iostream>
using namespace std;
class A {
int n;
static int m;
public:
void fun1() {
// 一般函數可以調用靜態和非靜態成員,無特殊限制
cout << n + m << endl;
fun2();
}
static void fun2() {
cout << m << endl;
// 錯誤:靜態成員函數不能調用非靜態成員
//cout << n << endl;
//fun1();
}
};
int main() {
A a;
A::fun2();
a.fun1();
a.fun2();
}
可以看出靜態成員函數和靜態成員變數的調用方式是相同的。
三、總結
C++語言當中有很多細節問題,熟練地在代碼中應用這些可以大大的提高我們程式的健壯性和靈活性。最近在應聘工作,這些問題很容易被作為考題,在這裡系統的總結一下。
class CA
{
public:
CA(void);
virtual void f(){int a = 10;}
private:
int c;
char ch;
};