轉(zhuǎn)載自http://www.youranshare.com/blog/sid/92.html
在C++
中霞幅,結(jié)構(gòu)體和類它們都是有構(gòu)造函數(shù)娩脾、析構(gòu)函數(shù)和成員函數(shù)的赵誓,他們兩者的根本區(qū)別就是:結(jié)構(gòu)體中訪問控制默認(rèn)是public
的,而類中默認(rèn)的訪問控制是private
的。對于C++
中的結(jié)構(gòu)體而言俩功,public
幻枉、protected
、private
的訪問都是在編譯期進(jìn)行檢查的绑雄,當(dāng)越權(quán)訪問的時候展辞,編譯過程中會給出此類的錯誤并給與提示,在編譯成功后万牺,程序在執(zhí)行的過程中不會有任何的檢查和限制罗珍,這一點你可以通過類的指針偏移做一下測試。因此在反匯編中脚粟,C++
中的結(jié)構(gòu)體與類沒有分別覆旱,兩者的原理是相同的,只是類型名稱不同核无。
說一下對象內(nèi)存的布局
對于類和對象的關(guān)系想必你已經(jīng)很了解了扣唱,類是一個抽象的概念,對象是一個實例化的具體存在的团南。這里我們以一個簡單的類噪沙,談一下類與對象的關(guān)系:
#include <iostream>
using namespace std;
class CNumber{
public:
CNumber(){
m_One = 1;
m_Two = 2;
}
int GetNumberOne(){
return m_One;
}
int GetNumberTwo(){
return m_Two;
}
private:
int m_One;
int m_Two;
};
int main()
{
CNumber num;
cout << num.GetNumberOne() << endl;
//嘗試著用指針訪問
cout << *(int*)(void*) &num << endl;
cout << *((int*)(void*)&num + 1) << endl;
return 0;
}
我們知道類的私有成員是不能被直接訪問到的,但是我們在程序運行后通過指針的偏移還是依然可以讀取內(nèi)存的數(shù)據(jù),就像結(jié)構(gòu)體一樣吐根,如圖所示我們的運行結(jié)果:
可以發(fā)現(xiàn)正歼,將對象的指針強制轉(zhuǎn)換成我們需要的數(shù)據(jù)類型,然后通過指針在內(nèi)存中的偏移可以訪問到對象的私有成員!這也就說明了C++
中類保護(hù)屬性是編譯的時候進(jìn)行檢測的!
C++
中類的成員變量和結(jié)構(gòu)體一樣拷橘,也是按照順序依次放到內(nèi)存中的局义,先定義的數(shù)據(jù)成員放到地地址,后定義的數(shù)據(jù)成員放到高地址處冗疮,但是對象的大小只包含數(shù)據(jù)成員萄唇,類的成員函數(shù)屬于執(zhí)行代碼,不屬于類對象的數(shù)據(jù)术幔。
你可以通過sizeof
獲取到CNumber
對象的大小另萤,它們的大小都是8Byte
,這8Byte
字節(jié)是由類中的兩個數(shù)據(jù)成員主城特愿,它們都是int
類型仲墨,格子的長度都為4Byte
。從內(nèi)存的布局上來看揍障,類與一個數(shù)組非常相似目养,都是由多個數(shù)據(jù)成員組成,但是類的能力要遠(yuǎn)遠(yuǎn)大過數(shù)組毒嫡。類的成員變量類型定義非常廣泛癌蚁,除了本身對象之外幻梯,任何已知的數(shù)據(jù)類型都可以在內(nèi)種作為成員變量進(jìn)行定義。
為什么在類中不能定義自身的對象呢努释?因為類需要在申請內(nèi)存的過程中計算出自身的實際大小碘梢,用于實例化對象。如果你在類中定義了自身的對象變量伐蒂,那么在計算類中各個數(shù)據(jù)成員長度時煞躬,又會因為回到自身導(dǎo)致無限遞歸下去,而這個遞歸沒有出口逸邦,所以不能在類中定義自身的對象成員變量恩沛。但是指針是可以的,因為指針的長度是確定的缕减,也就是相當(dāng)于一個常量值雷客,因此定義自身的指針是不會影響到類本身大小的計算的。根據(jù)上面所說的東西桥狡,我們是否可以定義一個公式用于描述類的對象長度呢搅裙?也許你會認(rèn)為下面的公式可以用于計算對象的長度:
? 對象長度= sizeof(成員1) + …+szieof(成員2)+…+sizeof(成員n)
這里我明確的告訴你,這個公式是錯誤的裹芝,對象大小的計算遠(yuǎn)遠(yuǎn)沒有那么簡單部逮,即是我們拋開虛函數(shù)和繼承的原因,仍然有三種情況能夠推翻此公式:
空類
內(nèi)存對齊問題
靜態(tài)數(shù)據(jù)成員
當(dāng)出現(xiàn)上面的情況時嫂易,類的對象長度的計算就需要小心了:
空類: 就是說類內(nèi)部沒有任何數(shù)據(jù)成員甥啄,但是實際上類對象的長度可是為1
字節(jié)
內(nèi)存對齊: 在VC++6.0
中,類和結(jié)構(gòu)體中的數(shù)據(jù)成員是根據(jù)它們在類或者結(jié)構(gòu)體中出現(xiàn)的順序來依次申請內(nèi)存的炬搭,但是由于內(nèi)存對齊的原因,它們并不會一定像數(shù)組那樣內(nèi)存是連續(xù)排列的穆桂,由于數(shù)據(jù)類型不同宫盔,因此占用的內(nèi)存空間大小也會不同,在申請內(nèi)存的時候會按照一定的規(guī)則.
我們以一個結(jié)構(gòu)體為例子來看看這個結(jié)構(gòu)體中成員變量地址的排列:
struct TagTest{
char a;
int b;
};
如圖所示享完,可以看到a
,b
的地址相差為4
字節(jié)灼芭,這也就是說a
,b
這兩個變量是沒有連續(xù)的分配在內(nèi)存中的,這中現(xiàn)象就是內(nèi)存地址對齊導(dǎo)致的.
在為結(jié)構(gòu)體和類中的數(shù)據(jù)成員分配內(nèi)存的時候般又,結(jié)構(gòu)體中的當(dāng)前數(shù)據(jù)成員類型長度為M
彼绷,指定對齊值為N
(編譯器指定的,例如為8Byte
)茴迁,那么實際上的對齊值q = Min(M,N)
寄悯,也就是說這個數(shù)據(jù)成員所在的地址必須為q
的倍數(shù),所以在上面的結(jié)構(gòu)體中堕义,數(shù)據(jù)成員b
猜旬,它的對齊值為q=Min(4,8)
也就是4
,所以b
的地址必須為4
的倍數(shù),雖然前面的數(shù)據(jù)成員的地址a
為18086720
洒擦,b
只是占用了1
個字節(jié)椿争,但是b
的地址需要為4
的倍數(shù),那么就需要在數(shù)據(jù)成員a
后面偏移3
個字節(jié)的位置放置b
熟嫩,如圖所示的a
和b
的內(nèi)存分布:
下面我們來分析一下a
,b
的地址分配過程:
首先, 開始分配a
的地址秦踪,a
為一個char
類型,它的對齊值M=1
掸茅,在VC++6.0
中編譯器的默認(rèn)對齊值為8
椅邓,那么q = Min(1,8)
,也就是q=1
,地址需要是1
的倍數(shù)倦蚪,所以直接分配地址就行了希坚。例如分配了地址為18086720
(十進(jìn)制)
其次,開始分配b
的地址,b
是一個int
類型對齊值為4Byte
陵且,那么q = Min(4,8)
裁僧,也就是q = 4
,它所在的地址需要是4
的倍數(shù)慕购,如果此時我們直接將b
分配在a
的后面聊疲,那么b
的地址將會是18086721
,不滿足對齊的地址沪悲,所以需要往后偏移3
個字節(jié)获洲,對應(yīng)的地址就是18086724
,這個地址滿足對齊值4
殿如,所以系統(tǒng)給b
分配的地址為18086724
贡珊,分配完畢。
通過上面的分配我們可以看到涉馁,這個結(jié)構(gòu)體一共占用了8Byte
门岔,而不是我們認(rèn)為的5
字節(jié)。另外值得說的是a
后面的那3
個字節(jié)里面可不是填充的0x00
烤送,一般系統(tǒng)都會填充0xCC
寒随。
看完上面的東西,有些童鞋肯定感覺到了一個問題:“結(jié)構(gòu)體本身也是一種數(shù)據(jù)類型帮坚,既然是數(shù)據(jù)類型妻往,就像int
一樣,結(jié)構(gòu)體本身也是有對齊的吧“.
答案是肯定的试和,數(shù)據(jù)類型都是有對齊值的讯泣,數(shù)據(jù)結(jié)構(gòu)本身也是一種類型,與int
基本類型無差別灰署,當(dāng)然也存在對齊值的問題,下面我給出一個數(shù)據(jù)結(jié)構(gòu):
struct STest{
double da; // 8字節(jié)大小
int ib; // 4字節(jié)大小
short sc; // 2字節(jié)大小
};
這個數(shù)據(jù)結(jié)構(gòu)STest
的大小是16
而不是14
判帮,為什么局嘁?
這是因為結(jié)構(gòu)體本身也是一種數(shù)據(jù)類型,當(dāng)然它也有對應(yīng)的字節(jié)對齊處理晦墙,這里我們將會討論一下對齊值對結(jié)構(gòu)體整體大小的影響悦昵,如果按照VC++6.0
默認(rèn)的8
字節(jié)對齊,那么對于一個結(jié)構(gòu)體來說它的對齊值依然滿足公式q=Min(M,N)
(VC++6.0
中N=8
)晌畅,但是需要注意的是這里的M
應(yīng)該是結(jié)構(gòu)體中的數(shù)據(jù)成員類型的最大值但指,就像結(jié)構(gòu)體STest
,它的對齊值按照最大的數(shù)據(jù)成員double da
抗楔,對齊值也就是 8Byte
棋凳,這樣一來可以計算出結(jié)構(gòu)體STest
的對齊值為8
,所以編譯器在STest
的最后一個成員short sc
后面有增加了2
個字節(jié)用于填充結(jié)構(gòu)體,使得整體大小為16
字節(jié)连躏,這樣就滿足了對齊的要求剩岳。
通過上面的介紹可以看出,結(jié)構(gòu)體的對齊值是根據(jù)結(jié)構(gòu)體內(nèi)部最大的成員長度動態(tài)調(diào)整的入热,可不是固定的8
字節(jié)拍棕,也可以是4
字節(jié)。
在C++
中勺良,雖然存在默認(rèn)的對齊值绰播,但是這個默認(rèn)值也是可以修改的,我們可以使用預(yù)編譯指令 #pragma pack(N)
來指定對齊大小尚困,例如我們下面的代碼蠢箩,將對齊值設(shè)置為1
字節(jié):
#pragma pack(1)
struct STest{
double da; // 8字節(jié)大小
int ib; // 4字節(jié)大小
short sc; // 2字節(jié)大小
};
運行結(jié)果如下圖所示,這里的sizeof
計算出的結(jié)構(gòu)就是 14
字節(jié)了:
經(jīng)過預(yù)編譯指令后事甜,將對齊值調(diào)整為1
字節(jié)谬泌,根據(jù)對齊規(guī)則,q=Min(4,1)
得出對齊值為1
字節(jié)逻谦,既然是1
字節(jié)呵萨,辣么就不用想了,直接就是14
字節(jié)的大小了跨跨。
但是要注意的是,你使用#pragma pack(N)
設(shè)置的對齊值可不一定會生效的囱皿,這是因為q=Min(M,N)
勇婴,要知道你的N
要是太大的話~~q
最終還是等于M
的,就像你設(shè)置對齊值為128
嘱腥,根本沒啥用嘛= =
耕渴,對齊值的計算流程總的來說:將設(shè)定的對齊值與結(jié)構(gòu)體中最大的基本類型數(shù)據(jù)成員的長度進(jìn)行比較,取兩者之間的較小者齿兔。
當(dāng)結(jié)構(gòu)體中以數(shù)組作為成員的時候橱脸,將會根據(jù)數(shù)組 元素類型 的長度計算對齊值础米,而不是按照數(shù)組的整體大小去計算。
結(jié)構(gòu)體含有數(shù)組類型的對齊
當(dāng)結(jié)構(gòu)體中以數(shù)組作為成員的時候添诉,將會根據(jù) 數(shù)組元素 的長度計算對齊值屁桑,而不是根據(jù)數(shù)組的整體長度來計算,例如下面的代碼:
Struct{
Char cChar; // 占用一個字節(jié)內(nèi)存
Char cArray[4]; // 占用多少字節(jié)內(nèi)存呢栏赴?
Short sShort; // 應(yīng)該占用2字節(jié)內(nèi)存
}
按照對齊的規(guī)定蘑斧,cChar
與cArry
它們都是char
類型的數(shù)據(jù),內(nèi)存對齊沒有縫隙须眷,不需要插入空白的數(shù)據(jù)竖瘾。但是當(dāng)cArray
與sShort
對齊的時候,cChar
與cArray
已經(jīng)在內(nèi)存中占用了5
個字節(jié)花颗,此時按照結(jié)構(gòu)體中當(dāng)前的數(shù)據(jù)類型short
進(jìn)行對齊的時候捕传,就需要在cArray
后面在插入一個字節(jié)就OK
了,其結(jié)構(gòu)如下圖所示:
結(jié)構(gòu)體內(nèi)部的數(shù)據(jù)成員已經(jīng)對齊了扩劝,下面就是處理結(jié)構(gòu)體本身的對齊值問題了庸论。根據(jù)結(jié)構(gòu)體中的數(shù)據(jù)成元類型得到,最大的數(shù)據(jù)成員sShort
占2
個字節(jié)今野,其余成員都是1字節(jié)的大小葡公,在默認(rèn)的情況下,對齊值為8
条霜,根據(jù)公式q = Min(M,N)
計算得出該結(jié)構(gòu)體的對齊值為2
催什,而此時結(jié)構(gòu)體的總大小為8
字節(jié),也就是說無需填入數(shù)據(jù)即可滿足對齊要求.
當(dāng)結(jié)構(gòu)體中出現(xiàn)結(jié)構(gòu)體類型的數(shù)據(jù)成員的時候宰睡,不會將嵌套的結(jié)構(gòu)體類型的整體長度參與到對齊值的計算中蒲凶,而是以嵌套定義的結(jié)構(gòu)體所使用的對齊值進(jìn)行對齊計算,如下面的代碼所示:
struct tagOne
{
char cChar; // 占用1字節(jié)
char cArray[4]; // 占用5字節(jié)
short sShort; // 占用2字節(jié)
};
struct tagTwo
{
int nInt; // 占用4字節(jié)
tagOne one; // 占用8字節(jié)
};
在上面的結(jié)構(gòu)體中拆内,雖然tagOne
占用了8
字節(jié)大小旋圆,但是由于其對齊值為2
,所以在計算tagTwo
的對齊值的時候參數(shù)one
的對齊值是2
麸恍,所以根據(jù)對齊計算的公式q=Min(M,N)
得出數(shù)據(jù)結(jié)構(gòu)tagTwo
的對齊值為4
灵巧,占用了12
個字節(jié)!而不是以8
對齊占用16
字節(jié)抹沪。
靜態(tài)數(shù)據(jù)成員
當(dāng)類中的數(shù)據(jù)成員被修飾為靜態(tài)成員的時候刻肄,對象的計算方法又會發(fā)生變化。因為雖然靜態(tài)數(shù)據(jù)成員是在類內(nèi)部進(jìn)行定義融欧,但是它與靜態(tài)局部變量是類似的敏弃,存放的位置和全局變量一致。只是編譯器增加了作用域的檢查噪馏,在作用域外不可見麦到,同類對象將共享有靜態(tài)數(shù)據(jù)成員的空間.