C語言的結(jié)構(gòu)體你了解嗎
結(jié)構(gòu)體內(nèi)存對(duì)齊
當(dāng)我們創(chuàng)建一個(gè)結(jié)構(gòu)體變量時(shí),內(nèi)存就會(huì)開辟一塊空間,那么在創(chuàng)建結(jié)構(gòu)體變量時(shí)內(nèi)存到底是怎么開辟空間的呢?會(huì)開辟多大的空間呢?我們來看一下下面的代碼
struct S { int i; char c; char b; }; struct G { char c; int i; char b; }; int main() { struct S u; struct G g; printf("%d\n", sizeof(u)); printf("%d\n", sizeof(g)); return 0; }
在這個(gè)代碼中,我們創(chuàng)建了兩個(gè)結(jié)構(gòu)體類型,并用這兩個(gè)類型創(chuàng)建了兩個(gè)結(jié)構(gòu)體變量,當(dāng)變量被創(chuàng)建的時(shí)候,內(nèi)存就會(huì)為這些變量開辟空間,而在這兩個(gè)變量中,我們都創(chuàng)建了兩個(gè)char類型的成員和一個(gè)int類型的成員,不同的是這三個(gè)成員的排列順序不同,然后我們來打印一下這兩個(gè)變量所占的字節(jié),來看一下有什么不同
我們發(fā)現(xiàn),這兩個(gè)結(jié)構(gòu)體變量雖然里面的元素類型一樣,但是他們的大小并不一樣,而他們之間的區(qū)別就是結(jié)構(gòu)體成員的排列順序不同,所以我們可以知道,結(jié)構(gòu)體成員的排列順序是會(huì)影響結(jié)構(gòu)體的大小的,那么他到底是怎么影響的呢,這就和結(jié)構(gòu)體的創(chuàng)建規(guī)則有關(guān)。
結(jié)構(gòu)體在創(chuàng)建時(shí)要進(jìn)行內(nèi)存對(duì)齊,對(duì)齊的規(guī)則是:
1.第一個(gè)成員在與結(jié)構(gòu)體變量偏移量為0的地址處。
2.其他成員變量要對(duì)齊到某個(gè)數(shù)字(對(duì)齊數(shù))的整數(shù)倍的地址處。
對(duì)齊數(shù) = 編譯器默認(rèn)的一個(gè)對(duì)齊數(shù) 與 該成員大小的較小值。
VS中默認(rèn)的值為83.結(jié)構(gòu)體總大小為最大對(duì)齊數(shù)(每個(gè)成員變量都有一個(gè)對(duì)齊數(shù))的整數(shù)倍。
4.如果嵌套了結(jié)構(gòu)體的情況,嵌套的結(jié)構(gòu)體對(duì)齊到自己的最大對(duì)齊數(shù)的整數(shù)倍處,結(jié)構(gòu)體的整 體大小就是所有最大對(duì)齊數(shù)(含嵌套結(jié)構(gòu)體的對(duì)齊數(shù))的整數(shù)倍。
下面我們來根據(jù)對(duì)齊規(guī)則來算一個(gè)下上面的兩個(gè)結(jié)構(gòu)體的大?。?/p>
struct S { int i; char c; char b; };
首先,我們先看struct S,第一個(gè)成員是int類型,占4個(gè)字節(jié),它的對(duì)齊數(shù)就是4,第二個(gè)是char類型,占一個(gè)字節(jié),對(duì)齊數(shù)是1,一個(gè)放在1的整數(shù)位上,也就是可以接著往下放,那就是在int的4個(gè)字節(jié)后面緊跟著來存放,第三個(gè)也是char類型,對(duì)齊數(shù)也是1,那么也可以接著放在后面,三個(gè)成員的最大對(duì)齊數(shù)是4,根據(jù)對(duì)齊規(guī)則,結(jié)構(gòu)體的大小是最大對(duì)齊數(shù)的整數(shù)倍,而我們剛剛算完這三個(gè)占的大小已經(jīng)是6個(gè)字節(jié)了,所以為了對(duì)齊,我們要浪費(fèi)兩個(gè)字節(jié)的空間,使這個(gè)結(jié)構(gòu)體的大小為8個(gè)字節(jié),是最大對(duì)齊數(shù)的2倍。
struct G { char c; int i; char b; };
我們?cè)賮砜吹诙€(gè),這里,我們的第一個(gè)元素變成了char類型,占一個(gè)字節(jié),但是第二個(gè)成員是int類型,對(duì)齊數(shù)為4,根據(jù)對(duì)齊規(guī)則要對(duì)齊到4的整數(shù)倍處,那就只能放在結(jié)構(gòu)體開始儲(chǔ)存的位置往后4個(gè)字節(jié)的地址處,加上它本身占4個(gè)字節(jié),這時(shí)候我們?cè)趦?nèi)存中就使用了8個(gè)字節(jié),然后在存入第三個(gè)成員char類型,對(duì)齊數(shù)是1,可以直接在后面存放,最大對(duì)齊數(shù)還是4,但是我們已經(jīng)使用了9個(gè)字節(jié),所以這時(shí)候的大小應(yīng)該是12個(gè)字節(jié),4的3倍。
以上的結(jié)果也是符合剛剛的運(yùn)行結(jié)果的。
下面我們來看一個(gè)結(jié)構(gòu)體里嵌套了一個(gè)結(jié)構(gòu)體時(shí)的例子:
struct G { char c; int i; char b; }; struct S { char a; int i; struct G c; };
我們?cè)诮Y(jié)構(gòu)體struct S中嵌套了一個(gè)結(jié)構(gòu)體struct G,根據(jù)對(duì)齊規(guī)則,我們嵌套的結(jié)構(gòu)體一個(gè)對(duì)齊到自己的最大對(duì)齊數(shù)的整數(shù)倍上,那就是4的整數(shù)倍,而第一個(gè)成員是char,第二個(gè)是int,int對(duì)齊到4的整數(shù)倍上,那前兩個(gè)成員就占了8個(gè)字節(jié),而struct G正好對(duì)齊到int的后面,大小我們剛剛算出來是12個(gè)字節(jié),加起來就是20個(gè)字節(jié),下面我們來驗(yàn)證一下
與運(yùn)行結(jié)果相同,那么我們的計(jì)算就沒有問題。
那么我們結(jié)構(gòu)體在儲(chǔ)存時(shí)為什么要進(jìn)行內(nèi)存對(duì)齊呢?主要有以下兩個(gè)原因。
1.平臺(tái)原因(移植原因):
不是所有的硬件平臺(tái)都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
2.性能原因:
數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊。
原因在于,為了訪問未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對(duì)齊的內(nèi)存訪問僅需要一次訪問。
跟主要的可能是第二種原因,為了增加處理器的訪問效率,我們選擇了用浪費(fèi)空間的方式,來使我們的處理器可以一次性的訪問到我們需要的元素,用空間來換時(shí)間。
根據(jù)我們的對(duì)齊規(guī)則我們也可以發(fā)現(xiàn),如果我們想要在創(chuàng)建結(jié)構(gòu)體變量時(shí)節(jié)省空間,我們應(yīng)該盡量讓小的成員集中在一起,這樣可以減少空間的浪費(fèi),節(jié)省空間。
在規(guī)則中我們看到,計(jì)算每個(gè)成員的對(duì)齊數(shù)時(shí)要選擇默認(rèn)對(duì)齊數(shù)與該成員大小的較小值,在vs編譯器中這個(gè)默認(rèn)對(duì)齊數(shù)是8,而在有的編譯器中沒有默認(rèn)對(duì)齊數(shù),如gcc編譯器的默認(rèn)對(duì)齊數(shù)就是成員自身的大小,當(dāng)然,這個(gè)默認(rèn)對(duì)齊數(shù)也是可以該的,而我們?nèi)绾蝸硇薷哪J(rèn)對(duì)齊數(shù)呢,我們看下面的代碼
struct H { char c1; double d; char c2; }; int main() { struct H h; printf("%d\n", sizeof(h)); return 0; }
根據(jù)我們的對(duì)齊規(guī)則,這個(gè)結(jié)構(gòu)體的大小應(yīng)該是24個(gè)字節(jié),我們來運(yùn)行一下看看結(jié)果
如果我們要把它的默認(rèn)對(duì)齊數(shù)改為4,那么我們?cè)賮碇匦掠?jì)算一下,首先第一個(gè)char類型占一個(gè)字節(jié),然后double類型占8個(gè)字節(jié),但是對(duì)齊數(shù)為4,對(duì)齊到4的倍數(shù),就可以在第4個(gè)字節(jié)的位置開始存儲(chǔ),這時(shí)候前兩個(gè)只占12個(gè)字節(jié),最后一個(gè)char占一個(gè)字節(jié),最大對(duì)齊數(shù)為4,大小為4的倍數(shù),應(yīng)該為16,我們來驗(yàn)證一下
根據(jù)運(yùn)行的結(jié)果我們可以看到,確實(shí)是改變了默認(rèn)對(duì)齊數(shù)
修改默認(rèn)對(duì)齊數(shù)的方法就是在結(jié)構(gòu)體類型前加上#pragma pack(n),n表示修改后的默認(rèn)對(duì)齊數(shù)的值(一般都是2的次方數(shù),當(dāng)改為1時(shí),表示不存在對(duì)齊),在結(jié)構(gòu)體類型的后面加上#pragma pack()表示取消修改。
#pragma pack(4) struct H { char c1; double d; char c2; }; #pragma pack()
如上面的代碼,表示我們只把#pragma pack(4)~#pragma pack()之間的結(jié)構(gòu)體類型的默認(rèn)對(duì)齊數(shù)改為了4。
結(jié)構(gòu)體傳參
在學(xué)習(xí)函數(shù)的時(shí)候我們?cè)?jīng)學(xué)到,函數(shù)在調(diào)用時(shí)有兩種方法,一種是傳值調(diào)用,一種是傳址調(diào)用C語言–函數(shù),我們來看下面的代碼。
struct S { int data[1000]; int num; }; //結(jié)構(gòu)體傳參 void print1(struct S s) { printf("%d\n", s.num); } //結(jié)構(gòu)體地址傳參 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { struct S s = { {1,2,3,4}, 1000 }; print1(s); //傳結(jié)構(gòu)體 print2(&s); //傳地址 return 0; }
在這個(gè)代碼中,我們的print函數(shù)的目的是打印結(jié)構(gòu)體變量中的一個(gè)成員,print1傳參時(shí)傳的就是結(jié)構(gòu)體變量的值,print2傳的就是結(jié)構(gòu)體變量的地址,他們的不同點(diǎn)在于傳值的時(shí)候,我們的形參是實(shí)參的一份臨時(shí)拷貝,也就是說,當(dāng)我把結(jié)構(gòu)體變量的以傳值的方式傳參給函數(shù)時(shí),當(dāng)我們調(diào)用這個(gè)函數(shù),內(nèi)存就會(huì)把這個(gè)結(jié)構(gòu)體拷貝一份,當(dāng)我們的結(jié)構(gòu)體變量比較小時(shí)還好,但是當(dāng)這個(gè)結(jié)構(gòu)體變量里的成員非常多,占據(jù)的空間非常大時(shí),就會(huì)導(dǎo)致系統(tǒng)開銷比較大,性能下降,而如果我們使用傳址的方式,我們一個(gè)地址的大小也就4或者8個(gè)字節(jié),就沒有上面的問題,所以在結(jié)構(gòu)體傳參時(shí),我們盡量要傳地址,既可以節(jié)省時(shí)間,也可以節(jié)省空間。
結(jié)構(gòu)體實(shí)現(xiàn)位段
什么是位段
位段的聲明和結(jié)構(gòu)是類似的,有兩個(gè)不同:
1.位段的成員必須是 char、int、unsigned int 或signed int 。
2.位段的成員名后邊有一個(gè)冒號(hào)和一個(gè)數(shù)字。
如
struct A { int a : 2; int b : 5; int c : 10; int d : 30; };
這里的A就是位段,每個(gè)成員后面的數(shù)字代表他們需要的二進(jìn)制位。
位段在內(nèi)存中的存儲(chǔ)
1.位段的成員可以是 int unsigned int signed int 或者是 char (屬于整形家族)類型
2.位段的空間上是按照需要以4個(gè)字節(jié)( int )或者1個(gè)字節(jié)( char )的方式來開辟的。
3.位段涉及很多不確定因素,位段是不跨平臺(tái)的,注重可移植的程序應(yīng)該避免使用位段。
我們來舉個(gè)例子
struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct S s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4; return 0; }
我們來研究這個(gè)位段在內(nèi)存中是如何儲(chǔ)存的
首先,我們看成員a,它占3個(gè)二進(jìn)制位,是一個(gè)char類型,我們需要?jiǎng)?chuàng)建出一個(gè)字節(jié)的空間,也就是8個(gè)二進(jìn)制位,a要走了3個(gè),我們假設(shè)在存儲(chǔ)是,我們存儲(chǔ)的順序是與小段存儲(chǔ)的方式類似,也是從右向左存儲(chǔ)的,那么a在內(nèi)存中存儲(chǔ)的位置應(yīng)該是后面的三個(gè)二進(jìn)制位
低地址->高地址 ******** aaa
假設(shè)一個(gè)代表內(nèi)存中的一個(gè)二進(jìn)制位,這八個(gè)代表一個(gè)字節(jié),這3個(gè)a上面對(duì)應(yīng)的*就是a在這一個(gè)字節(jié)中所占的空間。
b需要4個(gè)二進(jìn)制位,而我們剛剛創(chuàng)建的一個(gè)字節(jié)中還有5個(gè)二進(jìn)制位,所以我們把b的4個(gè)二進(jìn)制位放在a的后面。
低地址->高地址 ******** bbbbaaa
這就是a與b在內(nèi)存中的存儲(chǔ)情況,而這是c需要個(gè)二進(jìn)制位,我們只剩下一個(gè)二進(jìn)制位了,而c又是一個(gè)char類型,所以我們需要再創(chuàng)建一個(gè)字節(jié)的空間,而當(dāng)我們創(chuàng)建好了新的空間,c的個(gè)二進(jìn)制位應(yīng)該怎么儲(chǔ)存呢,我們是接著第一個(gè)字節(jié)把剩下的二進(jìn)制位用完還是在我們新創(chuàng)建的字節(jié)里重新儲(chǔ)存呢,假設(shè)內(nèi)存在存儲(chǔ)時(shí)選擇直接浪費(fèi)掉哪個(gè)二進(jìn)制位,在新的空間進(jìn)行儲(chǔ)存,這時(shí)內(nèi)存中的存儲(chǔ)分布應(yīng)該是這樣
低地址->高地址 ******** ******** bbbbaaa ccccc
存儲(chǔ)d時(shí),d需要4個(gè)二進(jìn)制位,第二個(gè)字節(jié)中的二進(jìn)制位也不夠,所以我們?cè)賱?chuàng)建一個(gè)字節(jié),存儲(chǔ)d
低地址->高地址 ******** ******** ******** bbbbaaa ccccc dddd
如果我們的假設(shè)沒錯(cuò)的話,這一個(gè)就是a,b,c,d這4個(gè)成員在內(nèi)存中的儲(chǔ)存位置,然后我們又對(duì)這4個(gè)成員進(jìn)行了賦值
a=10=>1010(二進(jìn)制數(shù))=>010(3個(gè)二進(jìn)制位)
b=12=>1100(二進(jìn)制數(shù))=>1100(4個(gè)二進(jìn)制位)
c=3=>11(二進(jìn)制數(shù))=>00011(3個(gè)二進(jìn)制位)
d=4=>100(二進(jìn)制數(shù))=>0100(4個(gè)二進(jìn)制位)
當(dāng)我們用上面的數(shù)據(jù)對(duì)我們剛剛的位段進(jìn)行賦值,那么這個(gè)位段在內(nèi)存中存儲(chǔ)的內(nèi)容應(yīng)該是這樣的
低地址->高地址 0bbbbaaa 000ccccc 0000dddd 01100010 00000011 00000100(二進(jìn)制) 62 03 04 (十六進(jìn)制)
根據(jù)我們的計(jì)算,我們發(fā)現(xiàn),如果位段按照我們剛剛的假設(shè)來存儲(chǔ),那么在內(nèi)存中存儲(chǔ)的內(nèi)容應(yīng)該是62 03 03,那我們現(xiàn)在來調(diào)試一下看看
結(jié)果與我們推斷的一樣,那么就說明當(dāng)前的編譯器位段的存儲(chǔ)是按照我們假設(shè)的方式來存儲(chǔ)的。
位段的問題
但是位段在C語言中的規(guī)定又有許多不確定的地方,
1.int 位段被當(dāng)成有符號(hào)數(shù)還是無符號(hào)數(shù)是不確定的。
2.位段中最大位的數(shù)目不能確定。(16位機(jī)器最大16,32位機(jī)器最大32,寫成27,在16位機(jī) 器會(huì)出問題。
3.位段中的成員在內(nèi)存中從左向右分配,還是從右向左分配標(biāo)準(zhǔn)尚未定義。
4.當(dāng)一個(gè)結(jié)構(gòu)包含兩個(gè)位段,第二個(gè)位段成員比較大,無法容納于第一個(gè)位段剩余的位時(shí),是 舍棄剩余的位還是利用,這是不確定的。
不同的平臺(tái)可能對(duì)上述的問題有不同的規(guī)定,所以位段是不能跨平臺(tái)的
跟結(jié)構(gòu)相比,位段可以達(dá)到同樣的效果,但是可以很好的節(jié)省空間,但是有跨平臺(tái)的問題存在。
位段的使用環(huán)境是在我們傳輸一個(gè)數(shù)據(jù)包時(shí),可以使用位段使數(shù)據(jù)包在不能壓縮的情況下,所占的空間最小。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
C語言簡(jiǎn)單實(shí)現(xiàn)銀行ATM存取款功能
這個(gè)是大一時(shí)期寫的。大四的時(shí)候整理了一下(本人C語言學(xué)的也不太好)。肯定很多不足和存在漏洞的地方、僅供借鑒、僅供借鑒,代碼中有大量注釋,新手看起來也沒有困難2021-11-11基于C語言實(shí)現(xiàn)簡(jiǎn)單學(xué)生成績(jī)管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了基于C語言實(shí)現(xiàn)簡(jiǎn)單學(xué)生成績(jī)管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08全排列算法的非遞歸實(shí)現(xiàn)與遞歸實(shí)現(xiàn)的方法(C++)
本篇文章是對(duì)全排列算法的非遞歸實(shí)現(xiàn)與遞歸實(shí)現(xiàn)的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05C語言中使用快速排序算法對(duì)元素排序的實(shí)例詳解
這篇文章主要介紹了C語言中使用快速排序算法對(duì)元素排序的實(shí)例詳解,文中細(xì)分了幾個(gè)情況來舉例,在注釋里有說明,需要的朋友可以參考下2016-04-04