C語(yǔ)言、C++內(nèi)存對(duì)齊問(wèn)題詳解
這也可以?
#include <iostream>
using namespace std;
struct Test_A
{
char a;
char b;
int c;
};
struct Test_B
{
char a;
int c;
char b;
};
struct Test_C
{
int c;
char a;
char b;
};
int main()
{
struct Test_A a;
memset(&a, 0, sizeof(a));
struct Test_B b;
memset(&b, 0, sizeof(b));
struct Test_C c;
memset(&c, 0, sizeof(c));
// Print the memory size of the struct
cout<<sizeof(a)<<endl;
cout<<sizeof(b)<<endl;
cout<<sizeof(c)<<endl;
return 0;
}
好了,一段簡(jiǎn)單的程序,上面的這段程序輸出是什么?如果你很懂,也就會(huì)知道我接下來(lái)要講什么了,可以略過(guò)了;如果,你不知道,或者還很模糊,請(qǐng)繼續(xù)閱讀。
這是為什么?
上面這段程序的輸出結(jié)果如下(windows 8.1 + visual studio 2012 update3下運(yùn)行):
// Print the memory size of the struct
cout<< sizeof(a)<<endl; // 8bytes
cout<< sizeof(b)<<endl; // 12bytes
cout<< sizeof(c)<<endl; // 8bytes
很奇怪么?定義的三個(gè)結(jié)構(gòu)體,只是換了一下結(jié)構(gòu)體中定義的成員的先后順序,怎么最終得到的結(jié)構(gòu)體所占用的內(nèi)存大小卻不一樣呢?很詭異么?好了,這就是我這里要總結(jié)的內(nèi)存對(duì)齊概念了。
內(nèi)存對(duì)齊
內(nèi)存對(duì)齊的問(wèn)題主要存在于理解struct和union等復(fù)合結(jié)構(gòu)在內(nèi)存中的分布。許多實(shí)際的計(jì)算機(jī)系統(tǒng)對(duì)基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制,它們會(huì)要求這些數(shù)據(jù)的首地址的值是某個(gè)數(shù)k(通常它為4或8)的倍數(shù),這就是所謂的內(nèi)存對(duì)齊。這個(gè)值k在不同的CPU平臺(tái)下,不同的編譯器下表現(xiàn)也有所不同,現(xiàn)在我們涉及的主流的編譯器是Microsoft的編譯器和GCC。
對(duì)于我們這種做上層應(yīng)用的程序員來(lái)說(shuō),真的是很少考慮內(nèi)存對(duì)齊這個(gè)問(wèn)題的,內(nèi)存對(duì)齊對(duì)于上層程序員來(lái)說(shuō),是“透明的”。內(nèi)存對(duì)齊,可以說(shuō)是編譯器做的工作,編譯器為程序中的每個(gè)數(shù)據(jù)塊安排在適當(dāng)?shù)膬?nèi)存位置上。很多時(shí)候,我們要寫(xiě)出效率更高的代碼,此時(shí)我們就需要去了解這種內(nèi)存對(duì)齊的概念,以及編譯器在后面到底偷偷摸摸干了點(diǎn)什么。特別是對(duì)于C和C++程序員來(lái)說(shuō),理解和掌握內(nèi)存對(duì)齊更是重要的。
為什么要有內(nèi)存對(duì)齊呢?該占用多大的內(nèi)存,那就開(kāi)辟對(duì)應(yīng)大小的內(nèi)存就好了,好比上面的結(jié)構(gòu)體,兩個(gè)char類型和一個(gè)int類型,大小應(yīng)該是6bytes才對(duì)啊,怎么又是8bytes,又是12bytes的啊?對(duì)于內(nèi)存對(duì)齊,主要是為了提高程序的性能,數(shù)據(jù)結(jié)構(gòu),特別是棧,應(yīng)該盡可能地在自然邊界上對(duì)齊。原因在于,為了訪問(wèn)未對(duì)其的內(nèi)存,處理器需要做兩次內(nèi)存訪問(wèn);然而,對(duì)齊的內(nèi)存訪問(wèn)僅僅需要一次內(nèi)存訪問(wèn)。
在計(jì)算機(jī)中,字、雙字和四字在自然邊界上不需要在內(nèi)存中對(duì)齊(對(duì)字、雙字和四字來(lái)說(shuō),自然邊界分別是偶數(shù)地址,可以被4整除的地址和可以被8整除的地址)。如果一個(gè)字或雙字操作數(shù)跨越了4字節(jié)邊界,或者一個(gè)四字操作數(shù)跨越了8字節(jié)邊界,就被認(rèn)為是未對(duì)齊的,從而需要兩次總線周期來(lái)訪問(wèn)內(nèi)存。一個(gè)字起始地址是奇數(shù),但卻沒(méi)有跨越字邊界,就被認(rèn)為是對(duì)齊的,能夠在一個(gè)總線周期中被訪問(wèn)。綜上所述,內(nèi)存對(duì)齊可以用一句話來(lái)概括——數(shù)據(jù)項(xiàng)只能存儲(chǔ)在地址是數(shù)據(jù)項(xiàng)大小的整數(shù)倍的內(nèi)存位置上。
我們?cè)賮?lái)看看一個(gè)簡(jiǎn)答的例子:
#include <stdio.h>
struct Test
{
char a;
int b;
int c;
char d;
};
int main()
{
struct Test structTest;
printf("&a=%p\n", &structTest.a);
printf("&b=%p\n", &structTest.b);
printf("&c=%p\n", &structTest.c);
printf("&d=%p\n", &structTest.d);
printf("sizeof(Test)=%d\n", sizeof(structTest));
return 0;
}
輸出結(jié)果如下:
&a=00C7FA44
&b=00C7FA48
&c=00C7FA4C
&d=00C7FA50
sizeof(Test)=16
結(jié)構(gòu)體Test的成員變量b占用字節(jié)數(shù)為4bytes,所以只能存儲(chǔ)在4的整數(shù)倍的位置上,由于a只占用1一個(gè)字節(jié),而a的地址00C7FA44和b的地址00C7FA48之間相差4bytes,這就說(shuō)明,a其實(shí)也占用了4個(gè)字節(jié),這樣才能保證b的起始地址是4的整數(shù)倍。這就是內(nèi)存對(duì)齊。如果沒(méi)有內(nèi)存對(duì)齊,我們?cè)倌蒙厦娴拇a作為例子,則可能輸出結(jié)果如下:
&a=ffbff5e8
&b=ffbff5e9
&c=ffbff5ed
&d=ffbff5f1
sizeof(Test)=10
可以看到,a占用了一個(gè)字節(jié),緊接著a之后就是b;之前也說(shuō)了,內(nèi)存對(duì)齊是操作系統(tǒng)為了快速訪問(wèn)內(nèi)存而采用的一種策略,簡(jiǎn)單來(lái)說(shuō),就是為了防止變量的二次訪問(wèn)。操作系統(tǒng)在訪問(wèn)內(nèi)存時(shí),每次讀取一定的長(zhǎng)度(這個(gè)長(zhǎng)度就是操作系統(tǒng)的默認(rèn)對(duì)齊系數(shù),或者是默認(rèn)對(duì)齊系數(shù)的整數(shù)倍)。沒(méi)有了內(nèi)存對(duì)齊,當(dāng)我們讀取變量c時(shí),第一次讀取0xffbff5e8~0xffbff5ef的內(nèi)存,第二次讀取0xffbff5f0~0xffbff5f8的內(nèi)存,由于變量c所占用的內(nèi)存跨越了兩片地址區(qū)域,為了正確得到變量c的值,就需要讀取兩次,將兩次內(nèi)存合并進(jìn)行整合,這樣就降低了內(nèi)存的訪問(wèn)效率。
我在這里說(shuō)了這么多,也挺繞口,這就是內(nèi)存對(duì)齊的規(guī)則。在C++中,每個(gè)特定平臺(tái)上的編譯器都有自己的內(nèi)存對(duì)齊規(guī)則,下面我們就內(nèi)存對(duì)齊的規(guī)則進(jìn)行總結(jié)。
內(nèi)存對(duì)齊規(guī)則
每個(gè)特定平臺(tái)上的編譯器都有自己的默認(rèn)“對(duì)齊系數(shù)”。我們可以通過(guò)預(yù)編譯命令#pragma pack(k),k=1,2,4,8,16來(lái)改變這個(gè)系數(shù),其中k就是需要指定的“對(duì)齊系數(shù)”;也可以使用#pragma pack()取消自定義字節(jié)對(duì)齊方式。具體的對(duì)齊規(guī)則如下:
規(guī)則1:struct或者union的數(shù)據(jù)成員對(duì)齊規(guī)則:第一個(gè)數(shù)據(jù)成員放在offset為0的地方,對(duì)齊按照#pragma pack指定的數(shù)值和自身占用字節(jié)數(shù)中,二者比較小的那個(gè)進(jìn)行對(duì)齊;比如;
#pragma pack(4) // 指定對(duì)齊系數(shù)為4,當(dāng)占用字節(jié)數(shù)大于等于4時(shí),就按照4進(jìn)行對(duì)齊
struct Test
{
char x1;
short x2;
float x3;
char x4;
};
x1占用字節(jié)數(shù)為1,1 < 4,按照對(duì)齊系數(shù)1進(jìn)行對(duì)齊,所以x1放置在offset為0的位置;
x2占用字節(jié)數(shù)為2,2 < 4,按照對(duì)齊系數(shù)2進(jìn)行對(duì)齊,所以x2放置在offset為2,3的位置;
x3占用字節(jié)數(shù)為4,4 = 4,按照對(duì)齊系數(shù)4進(jìn)行對(duì)齊,所以x3放置在offset為4,5,6,7的位置;
x4占用字節(jié)數(shù)為1,1 < 4,按照對(duì)齊系數(shù)1進(jìn)行對(duì)齊,所以x4放置在offset為8的位置;
現(xiàn)在已經(jīng)占了9bytes的內(nèi)存空間了,但是實(shí)際在visual studio 2012中實(shí)測(cè)為12bytes,為什么呢?看下一條規(guī)則。
規(guī)則2:struct或者union的整體對(duì)齊規(guī)則:在數(shù)據(jù)成員完成各自對(duì)齊以后,struct或者union本身也要進(jìn)行對(duì)齊,對(duì)齊將按照#pragma pack指定的數(shù)值和struct或者union中最大數(shù)據(jù)成員長(zhǎng)度中比較小的那個(gè)進(jìn)行;
繼續(xù)使用規(guī)則1種的例子進(jìn)行解釋,按照規(guī)則1的理解,struct Test已經(jīng)占用了9bytes,實(shí)際為什么是12bytes呢?根據(jù)規(guī)則2,在所有成員對(duì)齊完成以后,struct或者union自身也要進(jìn)行對(duì)齊;我們?cè)O(shè)定的對(duì)齊系數(shù)為4,而struct Test中占用字節(jié)數(shù)最大的是float類型的x3,由于x3占用字節(jié)數(shù)小于或等于設(shè)定的對(duì)齊系數(shù)4,所以struct或者union整體需要按照4bytes進(jìn)行對(duì)齊,也就是說(shuō),struct或者union占用的字節(jié)數(shù)必須能夠被4整除,好了。struct Test已經(jīng)占用了9bytes了,10bytes不能被4整除,11bytes也不能,12bytes正好;所以,struct Test最終占用的字節(jié)數(shù)為12bytes。
上述兩條規(guī)則就是內(nèi)存對(duì)齊的基本規(guī)則,先局部對(duì)齊,后整體對(duì)齊。
實(shí)例分析
總結(jié)了那么多的規(guī)則,不來(lái)點(diǎn)實(shí)際的code,總覺(jué)的少點(diǎn)什么,好吧。以下就按照上述總結(jié)的內(nèi)存對(duì)齊規(guī)則,來(lái)進(jìn)行一些實(shí)際的代碼分析(注:測(cè)試環(huán)境Windows 8.1 + Visual Studio 2012 update 3)。
測(cè)試代碼如下,先確認(rèn)測(cè)試環(huán)境:
#include <iostream>
using namespace std;
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
int main()
{
cout<<"sizeof(char)"<<sizeof(char)<<endl; // 1byte
cout<<"sizeof(short)"<<sizeof(short)<<endl; // 2bytes
cout<<"sizeof(int)"<<sizeof(int)<<endl; // 4bytes
cout<<"sizeof(double)"<<sizeof(double)<<endl; // 8bytes
return 0;
}
我分別設(shè)置#pragma pack(k),k=1,2,4,8,16進(jìn)行測(cè)試。
#pragma pack(1) // 設(shè)定對(duì)齊系數(shù)為1
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規(guī)則1,對(duì)成員變量進(jìn)行對(duì)齊:
x1 <= 1,按照1進(jìn)行對(duì)齊,x1占用0;
x2 > 1,按照1進(jìn)行對(duì)齊,x2占用1,2,3,4,5,6,7,8;
x3 > 1,按照1進(jìn)行對(duì)齊,x3占用9,10;
x4 > 1,按照1進(jìn)行對(duì)齊,x4占用11,12,13,14;
x5 > 1,按照1進(jìn)行對(duì)齊,x5占用15;
最后使用規(guī)則2,對(duì)struct整體進(jìn)行對(duì)齊:
x2占用內(nèi)存最大,為8bytes,8bytes > 1byte,所以整體按照1進(jìn)行對(duì)齊;16%1=0。
所以,在#pragma pack(1) 的情況下,struct Test占用內(nèi)存為16bytes;內(nèi)存占用如下圖所示:
#pragma pack(2) // 設(shè)定對(duì)齊系數(shù)為2
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規(guī)則1,對(duì)成員變量進(jìn)行對(duì)齊:
x1 <= 2,按照1進(jìn)行對(duì)齊,x1占用0;
x2 > 2,按照2進(jìn)行對(duì)齊,x2占用2,3,4,5,6,7,8,9;
x3 >= 2,按照2進(jìn)行對(duì)齊,x3占用10,11;
x4 > 2,按照2進(jìn)行對(duì)齊,x4占用12,13,14,15;
x5 < 2,按照1進(jìn)行對(duì)齊,x5占用16;
最后使用規(guī)則2,對(duì)struct整體進(jìn)行對(duì)齊:
x2占用內(nèi)存最大,為8bytes,8bytes > 2byte,所以整體按照2進(jìn)行對(duì)齊;17%2!=0
所以,在#pragma pack(2) 的情況下,struct Test占用內(nèi)存為18bytes;內(nèi)存占用如下圖所示:
#pragma pack(4) // 設(shè)定對(duì)齊系數(shù)為4
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規(guī)則1,對(duì)成員變量進(jìn)行對(duì)齊:
x1 <= 4,按照1進(jìn)行對(duì)齊,x1占用0;
x2 > 4,按照4進(jìn)行對(duì)齊,x2占用4,5,6,7,8,9,10,11;
x3 < 4,按照2進(jìn)行對(duì)齊,x3占用12,13;
x4 >= 4,按照4進(jìn)行對(duì)齊,x4占用16,17,18,19;
x5 < 4,按照1進(jìn)行對(duì)齊,x5占用20;
最后使用規(guī)則2,對(duì)struct整體進(jìn)行對(duì)齊:
x2占用內(nèi)存最大,為8bytes,8bytes > 4byte,所以整體按照4進(jìn)行對(duì)齊;21%4!=0
所以,在#pragma pack(4) 的情況下,struct Test占用內(nèi)存為24bytes;內(nèi)存占用如下圖所示:
#pragma pack(8) // 設(shè)定對(duì)齊系數(shù)為8
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規(guī)則1,對(duì)成員變量進(jìn)行對(duì)齊:
x1 <= 8,按照1進(jìn)行對(duì)齊,x1占用0;
x2 >= 8,按照8進(jìn)行對(duì)齊,x2占用8,9,10,11,12,13,14,15;
x3 < 8,按照2進(jìn)行對(duì)齊,x3占用16,17;
x4 <= 8,按照4進(jìn)行對(duì)齊,x4占用20,21,22,23;
x5 < 8,按照1進(jìn)行對(duì)齊,x5占用24;
最后使用規(guī)則2,對(duì)struct整體進(jìn)行對(duì)齊:
x2占用內(nèi)存最大,為8bytes,8bytes >= 8byte,所以整體按照8進(jìn)行對(duì)齊;25%8!=0
所以,在#pragma pack(8) 的情況下,struct Test占用內(nèi)存為32bytes;內(nèi)存占用如下圖所示:
#pragma pack(16) // 設(shè)定對(duì)齊系數(shù)為16
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規(guī)則1,對(duì)成員變量進(jìn)行對(duì)齊:
x1 < 16,按照1進(jìn)行對(duì)齊,x1占用0;
x2 < 16,按照8進(jìn)行對(duì)齊,x2占用8,9,10,11,12,13,14,15;
x3 < 16,按照2進(jìn)行對(duì)齊,x3占用16,17;
x4 < 16,按照4進(jìn)行對(duì)齊,x4占用20,21,22,23;
x5 < 16,按照1進(jìn)行對(duì)齊,x5占用24;
最后使用規(guī)則2,對(duì)struct整體進(jìn)行對(duì)齊:
x2占用內(nèi)存最大,為8bytes,16bytes >= 8byte,所以整體按照8進(jìn)行對(duì)齊;25%8!=0
所以,在#pragma pack(16) 的情況下,struct Test占用內(nèi)存為32bytes;內(nèi)存占用如下圖所示:
總結(jié)
經(jīng)過(guò)上面的實(shí)例分析,我對(duì)內(nèi)存對(duì)齊有了全面的認(rèn)識(shí)和了解?,F(xiàn)在再回過(guò)來(lái)看看文章開(kāi)頭的那段代碼,問(wèn)題就迎刃而解了,同時(shí)經(jīng)過(guò)這段代碼,讓我們認(rèn)識(shí)到定義struct或者union時(shí),也是有講解的。在以后的編碼生涯時(shí),是不是又要多考慮一些呢?糾結(jié)~
相關(guān)文章
Visual Studio 2019配置OpenCV4.1.1詳細(xì)圖解教程
這篇文章主要介紹了Visual Studio 2019配置OpenCV4.1.1詳細(xì)圖解教程 ,需要的朋友可以參考下2020-02-02深入C/C++浮點(diǎn)數(shù)在內(nèi)存中的存儲(chǔ)方式詳解
本篇文章是對(duì)C/C++浮點(diǎn)數(shù)在內(nèi)存中的存儲(chǔ)方式進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05OpenCV實(shí)現(xiàn)圖像去噪算法的步驟詳解
這篇文章主要為大家介紹了OpenCV中圖像去噪算法的原理,文中通過(guò)示例為大家詳細(xì)講解了圖像去噪算法的使用,感興趣的小伙伴可以了解一下2022-06-06c++函數(shù)中的指針參數(shù)與地址參數(shù)區(qū)別介紹
c++函數(shù)中的指針參數(shù)與地址參數(shù)區(qū)別介紹;可供參考2012-11-11c++調(diào)用python實(shí)現(xiàn)圖片ocr識(shí)別
所謂c++調(diào)用python,實(shí)際上就是在c++中把整個(gè)python當(dāng)作一個(gè)第三方庫(kù)引入,然后使用特定的接口來(lái)調(diào)用python的函數(shù)或者直接執(zhí)行python腳本,本文介紹的是調(diào)用python實(shí)現(xiàn)圖片ocr識(shí)別,感興趣的可以了解下2023-09-09C++ 中重載和運(yùn)算符重載加號(hào)實(shí)現(xiàn)矩陣相加實(shí)例代碼
這篇文章主要介紹了C++ 中重載和運(yùn)算符重載加號(hào)實(shí)現(xiàn)矩陣相加實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-03-03淺談#ifndef,#define,#endif的作用和用法
下面小編就為大家?guī)?lái)一篇淺談#ifndef,#define,#endif的作用和用法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12