由結構體對齊所引發的對C++類物件記憶體模型的思考(一)
(注:本文的實驗環境是在VS201X中進行的)
結構體對齊
一般而言,結構體變數記憶體中成員的排布如下:(從第一個成員依次向下排)
結構體資料的地址開始於第一個宣告的成員的地址,結束於最後一個成員的地址。在他們中間,按照宣告的順序儲存著所有的資料成員,但真相遠非如此,還存在著記憶體對齊的問題。對於類似於下面這樣的結構體:
struct Test {
char a;
double b;
char c;
};
int _tmain(int argc, _TCHAR* argv[])
{
Test obj;
obj.a = 'a';
obj.b = 1564654.325;
obj.c = 'b';
cout << sizeof(Test);
}
其輸出結果為:24有些剛學的童鞋可能會問,char佔一個位元組,double佔八個位元組,8+1+1不是等於10麼,難道電腦出問題了???好,為了一探究竟,下面我們檢視其記憶體:
0x0036FAC8是obj的起始地址,也就是char型別的成員a的地址,但是比較奇怪的是,在a後面並沒有僅接著就儲存b,而是空了7個位元組之後再儲存b,b佔了8個位元組,之後c佔據了一個位元組之後(在記憶體中已經顯示出b),整個結構體並未結束,在其後又空置了7個位元組,故整個結構體佔24個位元組.這對於不瞭解記憶體對齊規則的人來說是很困惑的。
記憶體對齊規則
通過查閱相關文獻,記憶體對齊規則主要為以下三點:
下面結合之前的那個程式碼對以上規則進行解釋: 第一個成員a為字元型,佔一個位元組,儲存的位置是0x0036FAC8,儲存之後,第二個成員b為雙精度浮點型,佔8個位元組,同時編譯器預設規定的對齊數為8,這兩個數取較小值,還是8,所以它儲存的位置偏移應該為8的整數倍,剛好0x0036FAC8-0x0036FAD0為8,因此在0x0036FAD0這個地方儲存b佔用8個位元組,之後準備開始儲存c,c是字元型,佔據一個位元組,系統預設對齊數為8,二者取其小,它儲存的地址應該是1的整數倍,很明顯,任何偏移都是1的整數倍,直接存在了b的後面。到這裡本該結束了,但是還有規則3,即在進行b的儲存的時候選出來的某個數是8,在儲存c的時候選出來的某個數是1,在8和1中選出一個最大值,結構體的大小應該為8的最小整數倍,現在的結構體大小為8+8+1也就是17,最小整數倍只能是24了。所以在c的後面又空置了7個位元組。促成了結構體的24個位元組的大小。 還有兩點要說明: 1 以上提到了一個編譯器預設的對齊數,這個數是可以更改的。使用#pragma pack (1//2//4//8//16)來更改,只能改為1,2,4,8,16中的一個值。 2 在網路傳輸,使用結構體指標指向某一個資料區獲取資料,讀取檔案等時候,經常由於結構體的對齊問題而出錯,是一個值得注意的問題。
實戰練習
希望通過以下的例子能夠再次熟悉結構體的對齊:
struct Test {
char a;
double b;
int n;
};
int main()
{
Test obj;
obj.a = 'c';
obj.b = 889089;
obj.n = 1234;
cout << sizeof(Test);
return 0;
}
輸出結果:24
struct Test {
char a;
int n;
double b;
};
int main()
{
Test obj;
obj.a = 'c';
obj.b = 889089;
obj.n = 1234;
cout << sizeof(Test);
return 0;
}
輸出結果:16不知各位讀者注意到沒,結構體中不同型別的資料排列不同,得到的結構體大小也將不同,就是由於記憶體對齊所造成的。
類物件記憶體模型
1.普通類的記憶體模型:
從一段簡單的程式碼看起:
class Test {
public:
char a;
double b;
char c;
static int d;
static int e;
void fun()
{
printf("Hello world!");
}
};
int main()
{
Test Teobj;
Teobj.a = 'a';
Teobj.b = 1564654.325;
Teobj.c = 'b';
cout << sizeof(Test);
return 0;
}
輸出結果為24; 我們把上一篇的例子中struct改成class關鍵字,再在class中新增pubilc屬性,保證在外部能訪問資料,又新增了兩個靜態成員和一個成員函式。顯示結果與結構體是一模一樣的。由此可以驗證:
2.有繼承關係時物件的記憶體模型
class Base
{
public:
Base() :a(0x1), b(0x2), c(0x3),d('d'), e('e') {}
int a;
int b;
int c;
double d;
char e;
};
class Inherit1 :public Base
{
public:
Inherit1() :m_Inherit1a(0xF) {}
int m_Inherit1a;
};
class CTest
{
public:
CTest() :m_Inherit1a(0xF) {}
int m_Inherit1a;
};
int main() {
Base obj1;
CTest obj2;
Inherit1 obj3;
cout << "基類大小 :" << sizeof(obj1) << endl;
cout << "派生類大小 :" << sizeof(obj3) << endl;
cout << "測試類大小:" << sizeof(obj2) << endl;
return 0;
}
其輸出結果為:
我們發現結果出乎我們的意料,sizeof(基類)+sizeof(測試類)與sizeof(派生類)居然不相等!那是為什麼呢?下面我們來分析這個結果:首先看一看派生類物件,其記憶體模型圖如下:
可以看出前三個值是1,2,3,也就是在基類建構函式中初始化的資料成員,可以驗證,在派生類物件中,開始存放的是基類的資料成員。
前3個int型成員佔據12個位元組後,之後是double型,依據之前的記憶體對齊規則,我們可以推測,成員d所在位置偏移需要為8的整數倍,故而再隔了4個位元組之後開始存放d,而上圖正好驗證了我們的推測!隨後存放成員e,此時基類大小已經為25了,但是基類總大小應該為8的整數倍(參見上面的記憶體規則),所以後面又空置了7個位元組(用cc來填充),隨後儲存的是派生類中的資料成員,僅有一個int型,總大小是36。
但是真相併非如此,後面又補了4個位元組,那是因為整個派生類物件的對齊同時考慮基類和派生類,故而大小為8的整數倍.我們單看基類和測試類,它們大小分別為32位元組和4位元組,它們之間是沒有繼承關係的,有了繼承關係組合到一起後的派生類竟然超過沒有繼承關係時兩個類大小的總和!
有點意思.既然基類和派生類會"擴充"變大,那麼它們會融合麼??下面看程式碼:
class Base
{
public:
Base() :a(0x1), b(0x2) {}
int a;
char b;
};
class Inherit1 :public Base
{
public:
Inherit1() :m_Inherit1a(0xF) {}
char m_Inherit1a;
};
class CTest
{
public:
CTest() :m_Inherit1a(0xF) {}
char m_Inherit1a;
};
int main()
{
Base obj1;
CTest obj2;
Inherit1 obj3;
cout << "基類大小 :" << sizeof(obj1) << endl;
cout << "測試類大小:" << sizeof(obj2) << endl;
cout << "派生類大小 :" << sizeof(obj3) << endl;
return 0;
}
輸出結果:
派生類記憶體模型如下
:
可以看出,臨近的兩個char型別資料並沒有融合到一起,基類與子類涇渭分明。並且又由於整體對齊的原因,又是繼承關係的子類大小大於基類與測試類大小的總和。下面,我們再看一段程式碼 :
class Base
{
public:
Base() :a(0x1), b(0x2), c(0x3), d('a'), e('a') {}
int a;
int b;
int c;
int d;
char e;
};
class Inherit1 :public Base
{
public:
Inherit1() :c1('A'), m_Inherit1a(0xF) {}
char c1;
double m_Inherit1a;
};
class CTest
{
public:
CTest() :c('A'), m_Inherit1a(0xF) {}
char c;
double m_Inherit1a;
};
int main()
{
Base obj1;
CTest obj2;
Inherit1 obj3;
cout << "基類大小 :" << sizeof(obj1) << endl;
cout << "測試類大小:" << sizeof(obj2) << endl;
cout << "派生類大小 :" << sizeof(obj3) << endl;
return 0;
}
輸出結果:
檢視派生類物件記憶體模型:
可以看出,父類與子類,依然涇渭分明,派生類部分被擠榨的僅為12個位元組,整體上來說32位元組還是保證了為8的倍數。此時就出現了讓我們大跌眼鏡的一步,具有繼承關係的派生類的大小竟然小於父類與測試類大小的和。通過之前的分析,我們可以得出具有繼承關係的派生類的大小既可以大於也可以小於測試類大小的和,那麼可以等於麼?答案是顯而易見的。下面我們看程式碼:
class Base
{
public:
Base() :a(0x1), b(0x2), c(0x3), d('a'), e('a') {}
int a;
int b;
int c;
int d;
int e;
};
class Inherit1 :public Base
{
public:
Inherit1() :c1(0x8), m_Inherit1a(0xF) {}
int c1;
int m_Inherit1a;
};
class CTest
{
public:
CTest() :c(0x8), m_Inherit1a(0xF) {}
int c;
int m_Inherit1a;
};
int main()
{
Base obj1;
CTest obj2;
Inherit1 obj3;
cout << "基類大小 :" << sizeof(obj1) << endl;
cout << "測試類大小:" << sizeof(obj2) << endl;
cout << "派生類大小 :" << sizeof(obj3) << endl;
return 0;
}
輸出結果:
至於這裡為什麼是等於的,我想通過之前的分析,大家應該明白其中的緣由,這裡就不多說了.
總結:
3.派生類與基類之間的轉換
class Base
{
public:
Base() :a(0x1), b(0x2) {}
int a;
char b;
};
class Inherit1 :public Base
{
public:
Inherit1() :m_Inherit1a(0xF) {}
char m_Inherit1a;
};
class Inherit2 :Base
{
public:
int m_Inherit2a;
int m_Inherit2b;
};
int main()
{
Base obj1;
Inherit1 obj2;
Inherit2 obj3;
Base* pBase;
obj1 = obj2; //不報錯
obj1 = obj3; //報錯
obj1 = (Base)obj3; //報錯
pBase = &obj2; //不報錯
pBase = &obj3; //報錯
pBase = (Base*)&obj3;//不報錯
return 0;
}
這段程式碼中,公有繼承的子類物件或者指標是可以直接賦值給父類的。Inherit2 是私有繼承自Base類的,當派生類是私有或者保護繼承於基類時,是不能直接把派生類的地址賦值給基類的,需要強制轉換。 一般我們使用的都是指標,當我們把派生類的地址賦值給一個基類的時候,基類就會按照基類的方式去訪問這段記憶體,不過,派生類的開頭儲存的正是由基類繼承來的資料,類物件的記憶體模型完美支援這一點。另外從物件導向的角度來看,這麼做是很合理的,比如動物類和兔子類,很明顯兔子是動物,自然能夠賦值,但是反過來,我說動物是兔子,這就未免會有問題了,下面探討一下基類向派生類的轉換。 當基類轉換為派生類的時候,就按照派生類的方式去訪問記憶體,在基類的記憶體中這塊區域中,應該沒有什麼問題,但是當越過基類記憶體的時候,能訪問到什麼,修改了什麼就很難說了。
int main()
{
Base obj1;
Inherit1 obj2;
Inherit2 obj3;
Base* pBase;
Inherit1* pInherit1;
Inherit2* pInherit2;
obj1 = obj2; //不報錯
pBase = &obj2; //不報錯
pBase = (Base*)&obj3;//不報錯
obj2 = (Inherit1)obj1; //報錯
pInherit1 = (Inherit1*)&obj1;
pInherit2 = (Inherit2*)&obj1;
return 0;
}
基類向派生類進行轉換需要強制轉換,並且也僅限於指標。
總結:
相關文章
- 由結構體對齊所引發的對C++類物件記憶體模型的思考(二)2017-12-12結構體C++物件記憶體模型
- C++ struct結構體記憶體對齊2022-03-22C++Struct結構體記憶體
- 結構體記憶體對齊2020-11-21結構體記憶體
- C++ 記憶體對齊2017-02-11C++記憶體
- c 結構體記憶體對齊詳解2021-04-13結構體記憶體
- C結構體中資料的記憶體對齊問題2018-08-20結構體記憶體
- 記憶體對齊2024-03-18記憶體
- 從 CPU 角度理解 Go 中的結構體記憶體對齊2022-01-20Go結構體記憶體
- struct結構體大小的計算(記憶體對齊)2021-10-09Struct結構體記憶體
- C/C++記憶體對齊詳解2021-01-19C++記憶體
- C/C++記憶體對齊原則2023-02-05C++記憶體
- GO 記憶體對齊2020-11-21Go記憶體
- 理解記憶體對齊2020-11-06記憶體
- C/C++結構體對齊測試2024-06-04C++結構體
- JVM記憶體結構、Java記憶體模型和Java物件模型2019-06-22JVM記憶體Java模型物件
- 記憶體位元組對齊2014-10-27記憶體
- 關於記憶體對齊2012-02-21記憶體
- C# 記憶體對齊2024-10-06C#記憶體
- C/C++—— 記憶體位元組對齊規則2016-04-01C++記憶體
- 由作業題引發對C++引用的一些思考2017-03-23C++
- Netty原始碼解析 -- 記憶體對齊類SizeClasses2020-11-22Netty原始碼記憶體
- iOS 記憶體位元組對齊2019-05-12iOS記憶體
- C語言記憶體對齊2024-10-02C語言記憶體
- C/C++ 結構體位元組對齊詳解2016-12-29C++結構體
- 淺談JVM記憶體結構 和 Java記憶體模型 和 Java物件模型2020-12-22JVM記憶體Java模型物件
- 由mv命令引發的對inode的思考2020-09-01
- 結構體成員對齊的問題2013-01-02結構體
- 由SQL語句執行過程觸發對Oracle體系結構的思考2014-11-27SQLOracle
- c++記憶體中位元組對齊問題詳解2014-03-30C++記憶體
- JAVA的記憶體模型及結構2017-05-22Java記憶體模型
- Slab: 保證JVM的堆記憶體對齊2013-06-20JVM記憶體
- C 語言結構體的對齊原則2016-12-13結構體
- iOS探索 記憶體對齊&malloc原始碼2020-01-02iOS記憶體原始碼
- 記憶體對齊巨集定義的簡明解釋2022-01-28記憶體
- C語言中結構體struct的對齊問題2019-03-25C語言結構體Struct
- Java常見知識點彙總(⑱)——Jvm記憶體結構、Java記憶體模型、Java物件模型的區別2020-03-16JavaJVM記憶體模型物件
- Windows+GCC下記憶體對齊的常見問題2013-11-27WindowsGC記憶體
- Java物件記憶體模型2022-01-16Java物件記憶體模型