C語言虛表
A. c語言中,有虛函數嗎
有虛函數的話就有虛表,虛表保存虛函數地址,一個地址佔用的長度根據編譯器不同有可能不同,vs裡面是8個位元組,在devc++裡面是4個位元組。類和結構體的對齊方式相同,有兩條規則
1、數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset為0的地方,以後每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。
2、結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行
下面是卜搏我收集的關於內存對齊的一篇很好的文章:
在最近的項目中,我們涉及到了「內存對齊」技術。對於大部分程序員來說,「內存對齊」對他們來說都應該是「透明的」。「內存對齊」應該是編譯器的 「管轄范圍」。編譯器為程序中的每個「數據單元」安排在適當的位置上。但是C語言的一個特點就是太靈活,太強大,它允許你干預「內存對齊」。如果你想了解更加底層的秘密,「內存對齊」對你就不應該再透明了。
一、內存對齊的原因
大部分的參考資料都是如是說的:
1、平台原因(移植原因):型搜祥不是所有的硬體平台都能訪問任意地址上的任意數據的;某些硬體平台只能在某些地址處取某些特定類型的數據,否則拋出硬體異常。
2、性能原因:數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在於,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
二、對齊規則
每個特定平台上的編譯器都有自己的默認「對齊系數」(也叫對齊模數)。程序員可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的「對齊系數」。
規則:
1、數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset為0的地方,以後每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。
2、結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。
3、結合1、2顆推斷:當#pragma pack的n值等於或超過所有數據成員長度的時候,這個n值的大小將不產生任何效果。
三、試驗
我們通過一系列例子的詳細說明來證明這個規則吧!
我試驗用漏圓的編譯器包括GCC 3.4.2和VC6.0的C編譯器,平台為Windows XP + Sp2。
我們將用典型的struct對齊來說明。首先我們定義一個struct:
#pragma pack(n) /* n = 1, 2, 4, 8, 16 */
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack(n)
首先我們首先確認在試驗平台上的各個類型的size,經驗證兩個編譯器的輸出均為:
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
我們的試驗過程如下:通過#pragma pack(n)改變「對齊系數」,然後察看sizeof(struct test_t)的值。
1、1位元組對齊(#pragma pack(1))
輸出結果:sizeof(struct test_t) = 8 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(1)
struct test_t {
int a; /* 長度4 < 1 按1對齊;起始offset=0 0%1=0;存放位置區間[0,3] */
char b; /* 長度1 = 1 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2 > 1 按1對齊;起始offset=5 5%1=0;存放位置區間[5,6] */
char d; /* 長度1 = 1 按1對齊;起始offset=7 7%1=0;存放位置區間[7] */
};
#pragma pack()
成員總大小=8
2) 整體對齊
整體對齊系數 = min((max(int,short,char), 1) = 1
整體大小(size)=$(成員總大小) 按 $(整體對齊系數) 圓整 = 8 /* 8%1=0 */ [注1]
2、2位元組對齊(#pragma pack(2))
輸出結果:sizeof(struct test_t) = 10 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(2)
struct test_t {
int a; /* 長度4 > 2 按2對齊;起始offset=0 0%2=0;存放位置區間[0,3] */
char b; /* 長度1 < 2 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2 = 2 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1 < 2 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊系數 = min((max(int,short,char), 2) = 2
整體大小(size)=$(成員總大小) 按 $(整體對齊系數) 圓整 = 10 /* 10%2=0 */
3、4位元組對齊(#pragma pack(4))
輸出結果:sizeof(struct test_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(4)
struct test_t {
int a; /* 長度4 = 4 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1 < 4 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2 < 4 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1 < 4 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊系數 = min((max(int,short,char), 4) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊系數) 圓整 = 12 /* 12%4=0 */
4、8位元組對齊(#pragma pack(8))
輸出結果:sizeof(struct test_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(8)
struct test_t {
int a; /* 長度4 < 8 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1 < 8 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2 < 8 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1 < 8 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊系數 = min((max(int,short,char), 8) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊系數) 圓整 = 12 /* 12%4=0 */
5、16位元組對齊(#pragma pack(16))
輸出結果:sizeof(struct test_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(16)
struct test_t {
int a; /* 長度4 < 16 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1 < 16 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2 < 16 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1 < 16 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊系數 = min((max(int,short,char), 16) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊系數) 圓整 = 12 /* 12%4=0 */
四、結論
8位元組和16位元組對齊試驗證明了「規則」的第3點:「當#pragma pack的n值等於或超過所有數據成員長度的時候,這個n值的大小將不產生任何效果」。另外內存對齊是個很復雜的東西,上面所說的在有些時候也可能不正確。呵呵^_^
[注1]
什麼是「圓整」?
舉例說明:如上面的8位元組對齊中的「整體對齊」,整體大小=9 按 4 圓整 = 12
圓整的過程:從9開始每次加一,看是否能被4整除,這里9,10,11均不能被4整除,到12時可以,則圓整結束。
B. 如何用C語言實現面向對象
可以通過以下方法實現面向對象:
1、封裝
封裝就是把數據和方法打包到一個類裡面。其實C語言編程者應該都已經接觸過了,C 標准庫 中的 fopen(), fclose(), fread(), fwrite()等函數的操作對象就是 FILE。
數據內容就是 FILE,數據的讀寫操作就是 fread()、fwrite(),fopen() 類比於構造函數,fclose() 就是析構函數。
2、繼承
繼承就是基於現有的一個類去定義一個新類,這樣有助於重用代碼,更好的組織代碼。在 C 語言裡面,去實現單繼承也非常簡單,只要把基類放到繼承類的第一個數據成員的位置就行了。
例如,我們現在要創建一個 Rectangle 類,我們只要繼承 Shape 類已經存在的屬性和操作,再添加不同於 Shape 的屬性和操作到 Rectangle 中。
3、多態 C++
語言實現多態就是使用虛函數。在 C 語言裡面,也可以實現多態。 現在,我們又要增加一個圓形,並且在 Shape 要擴展功能,我們要增加 area() 和 draw() 函數。
但是 Shape 相當於抽象類,不知道怎麼去計算自己的面積,更不知道怎麼去畫出來自己。而且,矩形和圓形的面積計算方式和幾何圖像也是不一樣的。
4、虛表和虛指針
虛表(Virtual Table)是這個類所有虛函數的函數指針的集合。
虛指針(Virtual Pointer)是一個指向虛表的指針。這個虛指針必須存在於每個對象實例中,會被所有子類繼承。
5、在構造函數中設置vptr
在每一個對象實例中,vptr 必須被初始化指向其 vtbl。最好的初始化位置就是在類的構造函數中。
事實上,在構造函數中,C++ 編譯器隱式的創建了一個初始化的vptr。在 C 語言裡面, 我們必須顯示的初始化vptr。下面就展示一下,在 Shape 的構造函數裡面,如何去初始化這個 vptr。