漫談c語言結構體
相信大家對於結構體都不陌生。在此,分享出本人對C語言結構體的研究和學習的總結。如果你發現這個總結中有你以前所未掌握的,那本文也算是有點價值了。當然,水平有限,若發現不足之處懇請指出。程式碼檔案test.c我放在下面。
在此,我會圍繞以下2個問題來分析和應用C語言結構體:
1. C語言中的結構體有何作用
2. 結構體成員變數記憶體對齊有何講究(重點)
對於一些概念的說明,我就不把C語言教材上的定義搬上來。我們坐下來慢慢聊吧。
==============================================================================================================================================
1. 結構體有何作用
三個月前,教研室裡一個學長在華為南京研究院的面試中就遇到這個問題。當然,這只是面試中最基礎的問題。如果問你你怎麼回答?
我的理解是這樣的,C語言中結構體至少有以下三個作用:
(1)有機地組織了物件的屬性。
比如,在STM32的RTC開發中,我們需要資料來表示日期和時間,這些資料通常是年、月、日、時、分、秒。如果我們不用結構體,那麼就需要定義6個變數來表示。這樣的話程式的資料結構是鬆散的,我們的資料結構最好是“高內聚,低耦合”的。所以,用一個結構體來表示更好,無論是從程式的可讀性還是可移植性還是可維護性皆是:
typedef struct //公曆日期和時間結構體
{
vu16 year;
vu8 month;
vu8 date;
vu8 hour;
vu8 min;
vu8 sec;
}_calendar_obj;
_calendar_obj calendar; //定義結構體變數
(2)以修改結構體成員變數的方法代替了函式(入口引數)的重新定義。
如果說結構體有機地組織了物件的屬性表示結構體“中看”,那麼以修改結構體成員變數的方法代替函式(入口引數)的重新定義就表示了結構體“中用”。繼續以上面的結構體為例子,我們來分析。假如現在我有如下函式來顯示日期和時間:
void DsipDateTime( _calendar_obj DateTimeVal)
那麼我們只要將一個_calendar_obj這個結構體型別的變數作為實參呼叫DsipDateTime()即可,DsipDateTime()通過DateTimeVal的成變數來實現內容的顯示。如果不用結構體,我們很可能需要寫這樣的一個函式:
void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 hour,vu8 min,vu8 sec)
顯然這樣的形參很不可觀,資料結構管理起來也很繁瑣。如果某個函式的返回值得是一個表示日期和時間的資料,那就更復雜了。這只是一方面。
另一方面,如果使用者需要表示日期和時間的資料中還要包含星期(周),這個時候,如果之前沒有用機構體,那麼應該在DsipDateTime()函式中在增加一個形參vu8 week:
void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 week,vu8 hour,vu8 min,vu8 sec)
可見這種方法來傳遞引數非常繁瑣。所以以結構體作為函式的入口引數的好處之一就是
函式的宣告void DsipDateTime( _calendar_obj DateTimeVal)不需要改變,只需要增加結構體的成員變數,然後在函式的內部實現上對calendar.week作相應的處理即可。這樣,在程式的修改、維護方面作用顯著。
typedef struct //公曆日期和時間結構體
{
vu16 year;
vu8 month;
vu8 date;
vu8 week;
vu8 hour;
vu8 min;
vu8 sec;
}_calendar_obj;
_calendar_obj calendar; //定義結構體變數
(3)結構體的記憶體對齊原則可以提高CPU對記憶體的訪問速度(以空間換取時間)。
並且,結構體成員變數的地址可以根據基地址(以偏移量offset)計算。我們先來看看下面的一段簡單的程式,對於此程式的分析會在第2部分結構體成員變數記憶體對齊中詳細說明。
}
#include<stdio.h> int main() { struct //宣告結構體char_short_long { char c; short s; long l; }char_short_long; struct //宣告結構體long_short_char { long l; short s; char c; }long_short_char; struct //宣告結構體char_long_short { char c; long l; short s; }char_long_short; printf(" \n"); printf(" Size of char = %d bytes\n",sizeof(char)); printf(" Size of shrot = %d bytes\n",sizeof(short)); printf(" Size of long = %d bytes\n",sizeof(long)); printf(" \n"); //char_short_long printf(" Size of char_short_long = %d bytes\n",sizeof(char_short_long)); printf(" Addr of char_short_long.c = 0x%p (10進位制:%d)\n",&char_short_long.c,&char_short_long.c); printf(" Addr of char_short_long.s = 0x%p (10進位制:%d)\n",&char_short_long.s,&char_short_long.s); printf(" Addr of char_short_long.l = 0x%p (10進位制:%d)\n",&char_short_long.l,&char_short_long.l); printf(" \n"); printf(" \n"); //long_short_char printf(" Size of long_short_char = %d bytes\n",sizeof(long_short_char)); printf(" Addr of long_short_char.l = 0x%p (10進位制:%d)\n",&long_short_char.l,&long_short_char.l); printf(" Addr of long_short_char.s = 0x%p (10進位制:%d)\n",&long_short_char.s,&long_short_char.s); printf(" Addr of long_short_char.c = 0x%p (10進位制:%d)\n",&long_short_char.c,&long_short_char.c); printf(" \n"); printf(" \n"); //char_long_short printf(" Size of char_long_short = %d bytes\n",sizeof(char_long_short)); printf(" Addr of char_long_short.c = 0x%p (10進位制:%d)\n",&char_long_short.c,&char_long_short.c); printf(" Addr of char_long_short.l = 0x%p (10進位制:%d)\n",&char_long_short.l,&char_long_short.l); printf(" Addr of char_long_short.s = 0x%p (10進位制:%d)\n",&char_long_short.s,&char_long_short.s); printf(" \n"); return 0; }
程式的執行結果如下(注意:括號內的資料是成員變數的地址的十進位制形式):
2. 結構體成員變數記憶體對齊
首先,我們來分析一下上面程式的執行結果。前三行說明在我的程式中,char型佔1個位元組,short型佔2個位元組,long型佔4個位元組。char_short_long、long_short_char和char_long_short是三個結構體成員相同但是成員變數的排列順序不同。並且從程式的執行結果來看,
Size of char_short_long = 8 bytes
Size of long_short_char = 8 bytes
Size of char_long_short = 12 bytes //比前兩種情況大4 byte !
並且,還要注意到,1 byte (char)+ 2 byte (short)+ 4 byte (long) = 7 byte,而不是8 byte。
所以,結構體成員變數的放置順序影響著結構體所佔的記憶體空間的大小。一個結構體變數所佔記憶體的大小不一定等於其成員變數所佔空間之和。如果一個使用者程式或者作業系統(比如uC/OS-II)中存在大量結構體變數時,這種記憶體佔用必須要進行優化,也就是說,結構體內部成員變數的排列次序是有講究的。
結構體成員變數到底是如何存放的呢?
在這裡,我就不賣關子了,直接給出如下結論,在沒有#pragma pack巨集的情況下:
原則1 結構(struct或聯合union)的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員儲存的起始位置要從該成員大小的整數倍開始(比如int在32位機為4位元組,則要從4的整數倍地址開始儲存)。
原則2 結構體的總大小,也就是sizeof的結果,必須是其內部最大成員的整數倍,不足的要補齊。
*原則3 結構體作為成員時,結構體成員要從其內部最大元素大小的整數倍地址開始儲存。(struct a裡存有struct b,b裡有char,int,double等元素時,那麼b應該從8的整數倍地址處開始儲存,因為sizeof(double) = 8 bytes)
這裡,我們結合上面的程式來分析(暫時不討論原則3)。
先看看char_short_long和long_short_char這兩個結構體,從它們的成員變數的地址可以看出來,這兩個結構體符合原則1和原則2。注意,在 char_short_long的成員變數的地址中, char_short_long.s的地址是1244994,也就是說,1244993是“空的”,只是被“佔位”了!
再看看char_long_short這個結構體,char_long_short的地址分佈情況如下表:
成員變數 |
成員變數十六進位制地址 |
成員變數十進位制地址 |
char_long_short.c |
0x0012FF2C |
1244972 |
char_long_short.l |
0x0012FF30 |
1244976 |
char_long_short.s |
0x0012FF34 |
1244980 |
可見,其記憶體分佈圖如下,共12 bytes:
地址 |
1244972 |
1244973 |
1244974 |
1244975 |
1244976 |
1244977 |
1244978 |
1244979 |
1244980 |
1244981 |
1244982 |
1244983 |
成員 |
.c |
|
|
|
.l |
.s |
|
|
首先,1244972能被1整除,所以char_long_short.c放在1244972處沒有問題(其實,就char型成員變數自身來說,其放在任何地址單元處都沒有問題),根據原則1,在之後的1244973~1244975中都沒有能被4(因為sizeof(long)=4bytes)整除的,1244976能被4整除,所以char_long_short.l應該放在1244976處,那麼同理,最後一個.s(sizeof(short)=2 bytes)是應該放在1244980處。
是不是這樣就結束了?不是,還有原則2。根據原則2的要求,char_long_short這個結構體所佔的空間大小應該是其佔記憶體空間最大的成員變數的大小的整數倍。如果我們到此就結束了,那麼char_long_short所佔的記憶體空間是1244972~1244981共計10bytes,不符合原則2,所以,必須在最後補齊2個 bytes(1244982~1244983)。
至此,一個結構體的記憶體佈局完成了。
下面我們按照上述原則,來驗證這樣的分析是不是正確。按上面的分析,地址單元1244973、1244974、1244975以及1244982、1244983都是空的(至少char_long_short未用到,只是“佔位”了)。如果我們的分析是正確的,那麼,定義這樣一個結構體,其所佔記憶體也應該是12 bytes:
struct //宣告結構體char_long_short_new
{
char c;
char add1; //補齊空間
char add2; //補齊空間
char add3; //補齊空間
long l;
short s;
char add4; //補齊空間
char add5; //補齊空間
}char_long_short_new;
可見,我們的分析是正確的。至於原則3,大家可以自己程式設計驗證,這裡就不再討論了。
所以,無論你是在VC6.0還是Keil C51,還是Keil MDK中,當你需要定義一個結構體時,只要你稍微留心結構體成員變數記憶體對齊這一現象,就可以在很大程度上節約MCU的RAM。這一點不僅僅應用於實際程式設計,在很多大型公司,比如IBM、微軟、百度、華為的筆試和麵試中,也是常見的。