本文內容由「嵌入式ARM」整理自網絡
參考公眾號:我贏職場、0基礎學單片機(森林木)、老九學堂(大雄)、嵌入式時代
(整理本文出於傳播相關技術知識,版權歸原作者所有。)
//聲明一個結構體 struct book {char title[MAXTITL];char author[MAXAUTL];float value;};這個聲明描述了一個由兩個字符數組和一個float變量組成的結構體。但是注意,它並沒有創建一個實際的數據對象,而是描述了一個組成這類對象的元素。因此,我們有時候也將結構體聲明叫做模板,因為它勾勒出數據該如何存儲,並沒有實例化數據對象。1、首先使用關鍵字struct,它表示接下來是一個結構體。2、後面是一個可選的標誌(book),它是用來引用該結構體的快速標記。struct book library;//把library設為一個可以使用book結構體的結構體變量,則library這個變量就包含了其book結構體中的所有元素3、接下來就是一個花括號,括起了結構體成員列表,及每個成員變量,使用的都是其自己的聲明方式來描述,用分號來結束描述;例如:char title[MAXTITL];字符數組就是這樣聲明的,用分號結束;注意:其中每個成員可以使用任何一種C數據結構甚至是其他的結構體,也是可以的;4、在結束花括號後的分號表示結構體設計定義的結束。關於其struct聲明的位置,也就是這段代碼要放到哪裡。同樣這也是具有作用域的。這種聲明如果放在任何函數的外面,那麼則可選標記可以在本文件中,該聲明的後面的所有函數都可以使用。如果這種聲明在某個函數的內部,則它的標記只能在內部使用,並且在其聲明之後;關於我們不斷說的,標記名是可選的,那麼我們什麼時候可以省略,什麼時候一定不能省略呢?如果是上面那種聲明定義的方法,並且想在一個地方定義結構體設計,而在其他地方定義實際的結構體變量,那麼就必須使用標記;可以省略,設計的同時就創建該結構體變量,但是這種設計是一次性的。struct 結構體名(也就是可選標記名){ 成員變量;};//使用分號表示定義結束。
C語言結構體定義的三種方式#include <stdio.h>struct student //結構體類型的說明與定義分開。聲明{int age; float score; char sex; };int main (){struct student a={ 20,79,'f'}; printf("年齡:%d 分數:%.2f 性別:%c\n", a.age, a.score, a.sex );return 0;#include <stdio.h>struct student /*聲明時直接定義*/{int age; float score; char sex; /*這種方式不環保,只能用一次*/} a={21,80,'n'};int main (){printf("年齡:%d 分數:%.2f 性別:%c\n", a.age, a.score, a.sex );#include <stdio.h>struct //直接定義結構體變量,沒有結構體類型名。這種方式最爛{int age;float score;char sex;} t={21,79,'f'};int main (){printf("年齡:%d 分數:%f 性別:%c\n", t.age, t.score, t.sex);return 0;}return 0;}}
定義結構體變量
之前我們結構體類型的定義(結構體的聲明)只是告訴編譯器該如何表示數據,但是它沒有讓計算機為其分配空間。我們要使用結構體,那麼就需要創建變量,也就是結構體變量;看到這條指令,編譯器才會創建一個結構體變量library,此時編譯器才會按照book模板為該變量分配內存空間,並且這裡存儲空間都是以這個變量結合在一起的。這也是後面訪問結構體變量成員的時候,我們就要用到結構體變量名來訪問。在結構體聲明中,struct book所起到的作用就像int,,,,等基礎數據類型名作用一樣。定義兩個struct book結構體類型的結構體變量,還定義了一個指向該結構體的指針,其ss指針可以指向s1,s2,或者任何其他的book結構體變量。struct book{ char … …. ….. }librar;現在還是回到剛才提及的那個問題,可選標誌符什麼時候可以省略;struct{ char title[MAXTITL]; char author[MAXAUTL];float value;}library;
//注意這裡不再是定義聲明結構體類型,而是直接創建結構體變量了,這個編譯器會分配內存的;
//這樣的確可以省略標識符也就是結構體名,但是只能使用一次;因為這是;聲明結構體的過程和定義結構體變量的過程和在了一起;並且個成員變量沒有初始化的;
//如果你想多次使用一個結構體模塊,這樣子是行不通的;用typedef定義新類型名來代替已有類型名,即給已有類型重新命名;typedef int Elem; typedef struct{ int date; }STUDENT;STUDENT stu1,stu2;struct 結構體名{成員列表;}變量名列表;struct book{char title[MAXTITL];char author[MAXAUTL];float value;}s1,s2直接定義結構體類型變量,就是第二種中省略結構體名的情況;這種方式不能指明結構體類型名而是直接定義結構體變量,並且在值定義一次結構體變量時適用,無結構體名的結構體類型是無法重複使用的。也就是說,後面程序不能再定義此類型變量了,除非再寫一次重複的struct。
對於結構體變量的初始化int a = 0;int array[4] = {1,2,3,4};也是使用花括號括起來,用逗號分隔的初始化好項目列表,注意每個初始化項目必須要和要初始化的結構體成員類型相匹配。struct book s1={//對結構體初始化 "yuwen", "guojiajiaoyun", 22.5 };//要對應起來,用逗號分隔開來,與數組初始化一樣;加入一點小知識;關於結構體初始化和存儲類時期的問題;如果要初始化一個具有靜態存儲時期的結構體,初始化項目列表中的值必須是常量表達式;注意如果在定義結構體變量的時候沒有初始化,那麼後面就不能全部一起初始化了;意思就是:
/////////這樣是可以的,在定義變量的時候就初始化了;struct book s1={//對結構體初始化 "guojiajiaoyun", "yuwen", 22.5 };/////////這種就不行了,在定義變量之後,若再要對變量的成員賦值,那麼只能單個賦值了;struct book s1; s1={ "guojiajiaoyun", "yuwen", 22.5 };
只能;
s1.title = "yuwen";...訪問結構體成員
結構體就像一個超級數組,在這個超級數組內,一個元素可以是char類型,下個元素就可以是flaot類型,再下個還可以是int數組型,這些都是存在的。在數組裡面我們通過下標可以訪問一個數組的各個元素,那麼如何訪問結構體中的各個成員呢?注意,點其結合性是自左至右的,它在所有的運算符中優先級是最高的;例如,s1.title指的就是s1的title部分,s1.author指的就是s1的author部分,s1.value指的就是s1的value部分。然後就可以像字符數組那樣使用s1.title,像使用float數據類型一樣使用s1.value;注意,s1;雖然是個結構體,但是s1.value卻是float型的。因此s1.value就相當於float類型的變量名一樣,按照float類型來使用;例如;printf(「%s\n%s\n%f」,s1.title,s1.author,s1.value);//訪問結構體變量元素注意scanf(「%d」,&s1.value); 這語句存在兩個運算符,&和結構成員運算符點。按照道理我們應該將(s1。value括起來,因為他們是整體,表示s1的value部分)但是我們不括起來也是一樣的,因為點的優先級要高於&。如果其成員本身又是一種結構體類型,那麼可以通過若干個成員運算符,一級一級的找到最低一級成員再對其進行操作;struct date{ int year; int month; int day;};struct student{ char name[10]; struct date birthday;}student1;//若想引用student的出生年月日,可表示為;student.brithday.year;brithday是student的成員;year是brithday的成員;可以將一個結構體變量作為一個整體賦值給另一相同類型的結構體變量,可以到達整體賦值的效果;這個成員變量的值都將全部整體賦值給另外一個變量;不能將一個結構體變量作為一個整體進行輸入和輸出;在輸入輸出結構體數據時,必須分別指明結構體變量的各成員;小結:除去「相同類型的結構體變量可以相互整體賦值」外,其他情況下,不能整體引用,只能對各個成員分別引用;
結構體長度16位編譯器
char :1個字節
char*(即指針變量): 2個字節
short int : 2個字節
int: 2個字節
unsigned int : 2個字節
float: 4個字節
double: 8個字節
long: 4個字節
long long: 8個字節
unsigned long: 4個字節
32位編譯器
char :1個字節
char*(即指針變量): 4個字節(32位的尋址空間是2^32, 即32個bit,也就是4個字節。同理64位編譯器)
short int : 2個字節
int: 4個字節
unsigned int : 4個字節
float: 4個字節
double: 8個字節
long: 4個字節
long long: 8個字節
unsigned long: 4個字節typedef struct{ char addr; char name; int id;}PERSON;通過printf("PERSON長度=%d字節\n",sizeof(PERSON));可以看到結果:
結構體字節對齊char ss[20]={0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29};printf("0x%02x,0x%02x,0x%02x\n",ps->addr,ps->name,ps->id);printf("PERSON長度=%d字節\n",sizeof(PERSON));可以看到addr和name都只佔一個字節,但是未滿4位元組,跳過2位元組後才是id的值,這就是4位元組對齊。結構體成員有int型,會自動按照4位元組對齊。typedef struct{ char addr; int id; char name;}PERSON;typedef struct{ int id; char addr; char name;}PERSON;typedef struct{ char addr; char name; char id;}PERSON;typedef struct{ char addr; char name; int id;}PERSON;typedef struct{ char age; PERSON ps1;}STUDENT;先定義結構體類型PERSON,再定義結構體STUDENT,PERSON作為它的一個成員。STUDENT *stu=(STUDENT *)ss;printf("0x%02x,0x%02x,0x%02x,0x%02x\n",stu->ps1.addr,stu->ps1.name,stu->ps1.id,stu->age);printf("STUDENT長度=%d字節\n",sizeof(STUDENT));typedef struct{ PERSON ps1; char age;}STUDENT;結構體嵌套其實沒有太意外的東西,只要遵循一定規律即可://對於「一錘子買賣」,只對最終的結構體變量感興趣,其中A、B也可刪,不過最好帶著 struct A{ struct B{ int c; } b; } a; //使用如下方式訪問:a.b.c = 10;struct A{ struct B{ int c; }b; struct B sb; }a;a.b.c = 11; printf("%d\n",a.b.c); a.sb.c = 22; printf("%d\n",a.sb.c);但是如果嵌套的結構體B是在A內部才聲明的,並且沒定義一個對應的對象實體b,這個結構體B的大小還是不算進結構體A中。(結構體長度、結構體字節對齊、結構體嵌套內容來源於公眾號「0基礎學單片機」,作者:森林木,感謝原作者的分享)
struct結構體,在結構體定義的時候不能申請內存空間,不過如果是結構體變量,聲明的時候就可以分配——兩者關係就像C++的類與對象,對象才分配內存(不過嚴格講,作為代碼段,結構體定義部分「.text」真的就不佔空間了麼?當然,這是另外一個範疇的話題)。結構體的大小通常(只是通常)是結構體所含變量大小的總和,下面列印輸出上述結構體的size:printf("size of struct man:%d\n",sizeof(struct man)); printf("size:%d\n",sizeof(Huqinwei));結果毫無懸念,都是28:分別是char數組20,int變量4,浮點變量4. 對於結構體中比較小的成員,可能會被強行對齊,造成空間的空置,這和讀取內存的機制有關,為了效率。通常32位機按4位元組對齊,小於的都當4位元組,有連續小於4位元組的,可以不著急對齊,等到湊夠了整,加上下一個元素超出一個對齊位置,才開始調整,比如3+2或者1+4,後者都需要另起(下邊的結構體大小是8bytes),相關例子就多了,不贅述。struct s { char a; short b; int c; }相應的,64位機按8位元組對齊。不過對齊不是絕對的,用#pragma pack()可以修改對齊,如果改成1,結構體大小就是實實在在的成員變量大小的總和了。和C++的類不一樣,結構體不可以給結構體內部變量初始化,。#include<stdio.h> //直接帶變量名struct stuff{ // char job[20] = "Programmer"; // char job[]; // int age = 27; // float height = 185; };C++的結構體變量的聲明定義和C有略微不同,說白了就是更「面向對象」風格化,要求更低。
如果函數的參數比較多,很容易產生「重複C語言代碼」,例如:int get_video(char **name, long *address, int *size, time_t *time, int *alg){ ...}int handle_video(char *name, long address, int size, time_t time, int alg){ ...}int send_video(char *name, long address, int size, time_t time, int alg){ ...}上述C語言代碼定義了三個函數:get_video() 用於獲取一段視頻信息,包括:視頻的名稱,地址,大小,時間,編碼算法。
然後 handle_video() 函數根據視頻的這些參數處理視頻,之後 send_video() 負責將處理後的視頻發送出去。下面是一次調用:char *name = NULL;long address;int size, alg;time_t time;
get_video(&name, &address, &size, &time, &alg);handle_video(name, address, size, time, alg);send_video(name, address, size, time, alg);從上面這段C語言代碼來看,為了完成視頻的一次「獲取」——「處理」——「發送」操作,C語言程序不得不定義多個變量,並且這些變量需要重複寫至少三遍。
雖說C語言程序的代碼風格因人而異,但是「重複的代碼」永遠是應盡力避免的,原因本專欄已經分析多次。不管怎麼說,每次使用這幾個函數,都需要定義很多臨時變量,總是非常麻煩的。所以,這種情況下,完全可以使用C語言的結構體語法:struct video_info{ char *name; long address; int size; int alg; time_t time;};定義好 video_info 結構體後,上述三個C語言函數的參數可以如下寫,請看:int get_video(struct video_info *vinfo){ ...}int handle_video(struct video_info *vinfo){ ...}int send_video(struct video_info *vinfo){ ...}修改後的C語言代碼明顯精簡多了,在函數內部,視頻的各個信息可以通過結構體指針 vinfo 訪問,例如:printf("video name: %s\n", vinfo->name);long addr = vinfo->address;int size = vinfo->size;事實上,使用結構體 video_info 封裝視頻信息的各個參數後,調用這幾個修改後的函數也是非常簡潔的:struct video_info vinfo = {0};
get_video(&vinfo);handle_video(&vinfo);send_video(&vinfo);從上述C語言代碼可以看出,使用修改後的函數只需定義一個臨時變量,整個代碼變得非常精簡。
讀者應該注意到了,修改之前的 handle_video() 和 send_video() 函數原型如下:int handle_video(char *name, long address, int size, time_t time, int alg);int send_video(char *name, long address, int size, time_t time, int alg);根據這段C語言代碼,我們知道 handle_video() 和 send_video() 函數只需要讀取參數信息,並不再修改參數,那為什麼使用結構體 video_info 封裝數據,修改後的 handle_video() 和 send_video() 函數參數是 struct video_info *指針型呢?int handle_video(struct video_info *vinfo);int send_video(struct video_info *vinfo);既然 handle_video() 和 send_video() 函數只需要讀取參數信息,那我們就無需再使用指針型了呀?的確如此,這兩個函數的參數直接使用 struct video_info 型也是可以的:int handle_video(struct video_info vinfo){ ...}int send_video(struct video_info vinfo){ ...}似乎這種寫法和使用 struct video_info *指針型 參數的區別,無非就是函數內部訪問數據的方式改變了而已。但是,如果讀者能夠想到我們之前討論過的C語言函數的「棧幀」概念,應該能夠發現,使用指針型參數的 handle_video() 和 send_video() 函數效率更好,開銷更小。嵌入式開發中,C語言位結構體用途詳解
在嵌入式開發中,經常需要表示各種系統狀態,位結構體的出現大大方便了我們,尤其是在進行一些硬體層操作和數據通信時。但是在使用位結構體的過程中,是否深入思考一下它的相關屬性?是否真正用到它的便利性,來提高系統效率?
下面將進行一些相關實驗(這裡以項目開發中的實際代碼為例):[cpp] view plain copy print?//data structure except for number structure typedef struct symbol_struct { uint_32 SYMBOL_TYPE :5; uint_32 reserved_1 :4;
uint_32 SYMBOL_NUMBER :7; uint_32 SYMBOL_ACTIVE :1; uint_32 SYMBOL_INDEX :8; uint_32 reserved_2 :8;
}SYMBOL_STRUCT,_PTR_ SYMBOL_STRUCT_PTR;分析:這裡定義了一個位結構體類型SYMBOL_STRUCT,那麼用該類型定義的變量都哪些屬性呢?
WORDS是定義的另一個外層類型定義封裝,可以把它當作變量來看待。WORDS變量裡前5個數據域的地址都是0x1ffff082c,而reserved_2的地址0x1fff0830,緊接著的PressureState變量是0x1fff0834。
開始以為:reserved_1和SYMBOL_TYPE不在一個地址上,因為他們5+4共9位,超過了1個字節地址,但實際他們共用首地址了;而且reserved_2隻定義了8位,竟然實際佔用了4個字節(0x1fff0834 - 0x1fff0830),我本來是想讓他佔用1個字節的。WORDS整體佔了8個字節(0x1fff0834 - 0x1fff082c),設計時分析佔用5個字節(SYMBOL_TYPE 1個;reserved_1 1個;SYMBOL_NUMBER+SYMBOL_ACTIVE 1個;SYMBOL_INDEX 1個;reserved_2 1個)。uint_32 reserved_2 : 8; 佔用4個字節,估計是uint_32在起作用,而這裡寫的8位,只是我使用的有效位數,另外24位空閒,如果在下面再定義一個uint_32 reserved_3 : 8,地址也是一樣的,都是以uint_32為單位取地址。同理,上面的5個變量,共用一個地址就不足為奇了。而且有效位的分配不是連續進行的,例如SYMBOL_TYPE+reserved_1 共9位,超過了一個字節,索性系統就分配兩個字節給他們,每人一個;SYMBOL_NUMBER+SYMBOL_ACTIVE 共8位,一個字節就能搞定。[cpp] view plain copy print?//data structure except for number structure typedef struct symbol_struct { uint_8 SYMBOL_TYPE :5; //data type,have the affect on "data display type" uint_8 reserved_1 :4;
uint_8 SYMBOL_NUMBER :7; //effective data number in one element uint_8 SYMBOL_ACTIVE :1; //symbol active status
uint_8 SYMBOL_INDEX :8; //data index in norflash,result is related to "xxx_BASE_ADDR" uint_8 reserved_2 :8; }SYMBOL_STRUCT,_PTR_ SYMBOL_STRUCT_PTR;當換成uint_8後,可以看到地址空間佔用大大減小,reserved_2隻佔用1個字節(0x1fff069f - 0x1fff069e),其他變量也都符合上面的結論猜想。但是,注意看上面黃色和紅色的語句,總感覺有些勉強,那麼我又會想,前兩個變量數據域是9位,那麼他們實際上是不是真正的獨立呢?雖然在uint_8上面他們是不同的地址,在uint_32的時候是不是也是不同的地址空間呢?
本來假設: 由前2次試驗的結論,一共佔用8個字節,節空間佔用:(2+4)+(4+4)+(2+2+4)+(2+2)+(6)。可是,實際效果並不是想的那樣。實際只佔用了4個字節,系統並沒有按照預想的方式,為RESERVED變量分配4個字節。這些數據域,整體相加一共32位,佔用4個字節(不考慮數據對齊問題)。而實際確實是佔用了4個字節,唯一的原因就是:這些數據域以緊湊的方式連結,沒有任何空閒位。實際是不是這樣呢?
這裡為了驗證是否緊湊連結,用到了一個union數據,後面會講到用union不會對數據組織方式有任何影響,看實際與上次的一樣,也能分析出來。主要是分析第2和第3個數據域是否緊密連結的。OBJECT_ACTIVE_PRE賦值0b00001111,NUMBER_ACTIVE賦值0b00000101,其他變量都是0,看到WORD數值0b1011111000000。分析WORD數據,可以看到這款MCU還是小端格式(高位數據在高端,低位數據在低端,這裡不對大小端進行討論),斷開數據變成(0)10111 11000000,正好是0101+1111,OBJECT_ACTIVE_PRE數據域,跨越了兩個字節,並不是剛開始設想的那樣。這就印證了上面的緊密連結的結論,也符合數據結果輸出。
可以看到,RESERVED數據域已經不再屬於4個地址空間內了(0x1fff0518 - 0x1fff051b),但是他們整體加起來還是32個位域。這說明數據中間肯定有「空隙」存在了,空隙在哪?看一下NUMBER_STATE,如果緊密的話它應該跟NUMBER_ACTIVE在同一個字節地址上,可是他們並不在一塊,「空隙」就存在這裡。這兩個結構體有什麼不一樣?數據類型不一致,一個是uint_32,一個是uint_8。綜上所述:數據類型影響的是編譯器在分配物理空間時的大小單位,uint_32是以4個字節為單位,而後面的位域則是指在已經分配好的物理空間內部再緊湊的方式分配數據位,當物理空間不能滿足位域時,那麼系統就再次以一定大小單位進行物理空間分配,這個單位就是上面提到的uint_8或者uint_32。舉例:上面uint_32時,這些位域不管是不是在一個字節地址上,如果能夠緊湊的分配在一個4位元組空間大小上,就直接緊湊分配。如果不能則繼續分配(總空間超過4位元組),則再次以4位元組空間分配,並把新的位域建立在新的地址空間上(條目1上的就是)。當uint_8時,很明顯如果位域不能緊湊的放在一個字節空間上,那麼就從新分配新的1位元組空間大小,道理是一樣的。
可以看到,系統並沒有因為位結構體上面有uint_4的4位元組變量或者共用體類型,就改變分配策略把位域都擠到4位元組之內,看來他們是沒有什麼實質性聯繫的。這裡把uint_32改成uint_8,或者把位結構體也替換掉,經我試驗證明,都是沒有任何影響的。
1、在操作位結構體時,要關注變量的位域是否在一個變量類型(uint_32或者uint_8)上,判斷佔用空間大小2、除了位域,還要關注變量定義類型,因為編譯器空間分配始終是按類型分配的,位域只是指出了有效位(小於類型佔用空間),而且如果位域大於類型空間,編譯器直接報錯(如 uint_8 test :15,可自行實驗)。3、這兩個因素都影響變量佔用空間大小,具體可以結合調試窗口,通過地址分配分析判斷4、最重要的一點:上面的所有結果,都是基於我自己的CodeWarrior10.2和MQX3.8分析出來的,不同的編譯環境和作業系統,都可能會有不同的結果;而且即便是環境相同,編譯器的配置和優化選項都有可能影響系統處理結果。結論並不重要,主要想告訴大家這一塊隱藏陷阱,在以後處理類似問題時,要注意分析避讓並掌握方法。-END-