1. 程式人生 > >5、C_數組&字符串&結構體&共用體&枚舉

5、C_數組&字符串&結構體&共用體&枚舉

常用 字節對齊 底層 不知道 div 修改 自動 當我 stdio.h

程序中內存從哪裏來

三種內存來源:棧(stack)、堆(heap)、數據區(.date);
  • 棧(stack)
  • 運行自動分配、自動回收,不需要程序員手工幹預;
  • 棧內存可以反復使用;
  • 棧反復使用後,程序不會清理棧,因此,棧是臟的,使用時可能分配到原來保留的值;
  • 函數不能返回棧變量的指針,因為這個空間是臨時的;
  • 棧會溢出,如果在函數中無窮的分配內存;
  • 堆(heap)
  • 堆管理器是操作系統的一個模塊,堆管理內存分配靈活,按需分配;
  • 堆管理器管理著很大的操作系統內存塊,各個進程按需申請使用,用完釋放;
  • 堆內存需要使用malloc申請,free釋放;
  • 堆內存是反復使用的,使用者用完釋放前不會清楚,因此是臟的;
  • 堆內存只在malloc和free之間使用,在這段時間外,不能再訪問,否則可能出現不可預料的後果;
堆的使用:
  • void * 是一個指針類型,malloc返回的是一個void * 類型的指針,實質上malloc返回的死堆管理器分配的那段內存空間的首地址(malloc返回的值其實是一個數字,這個數字表示一個內存額地址);
  • malloc幫我們分配內存時只分配了內存空間,至於這個空間用來存儲什麽類型的元素,由程序員自己決定;
  • void類型,表示萬能類型。void的意思是這個數據的類型當前是不確定的,在需要的時候可以強制轉換成任意類型,void *是一個指著類型,這個指針本身占4個字節,指向的元素的類型是不確定的,也可以說這個指針指向任何類型的元素;
  • 使用malloc申請一片內存後,需要判斷 if(NULL == *p) ,申請失敗時返回NULL,所以在使用前要檢查是否為NULL;
  • malloc申請的內存使用完後,要使用free(*p)釋放內存,在使用free釋放內存之前,指向這個內存的指針p一定不能丟(也就是不能給p另外賦值)。因為p一旦丟失,這段malloc申請的內存就永遠丟失了(內存泄露),直到當前程序結束時,操作系統才會回收這段內存。
malloc的一些細節:
  • malloc(0):如果真的malloc(0)返回的是NULL還是一個有效指針?答案是:實際分配了16Byte的一段內存並且返回了這段內存的地址。這個答案不是確定的,因為C語言並沒有明確規定malloc(0)時的表現,由各malloc函數庫的實現者來定義。
  • malloc(4):gcc中的malloc默認最小是以16B為分配單位的。如果malloc小於16B的大小時都會返回一個16字節的大小的內存。malloc實現時沒有實現任意字節的分配而是允許一些大小的塊內存的分配。
  • malloc(20)去訪問第25、第250、第2500····會怎麽樣?實戰中:120字節處正確,1200字節處正確····終於繼續往後訪問總有一個數字處開始段錯誤了。
代碼段、數據段、bss段
  • 代碼段:代碼段是程序中的可執行部分,直觀理解就是由函數堆疊組成的;
  • 數據段:數據段就是程序中的數據,直觀理解就是C語言程序中的全局變量。(全局變量才算是程序的數據,局部變量不算是程序的數據,只能算是函數的數據);
  • bss段:(又叫ZI(zero initial)段),bss段的數據的特點就是初始化為0,bss段本質上屬於數據段,bss段就是被初始化為0的數據段。
  • 註意:數據段(.data) 和 bss 段的區別和聯系:二者本質上沒有區別,都是用來存放C程序中的全局變量的,區別在於:把顯示初始化為非零的全局變量存在.data段中,把顯示初始化為0或者並未顯示初始化的全局變量放在bss段。(C語言規定未顯式初始化的全局變量值默認為0)
有些特殊數據段會被放到代碼段:
  • 在代碼段中,也有可能包含一些只讀的常熟變量,例如字符串常量等,程序段為程序代碼在內存中的映射,一個程序可以在內存中有多個副本。C語言中使用 char *p = "linux";定義字符串時,字符串"linux"存放在代碼段(有時候放在只讀數據段:.ro.data,取決於平臺);也就是說,"linux"字符串實際上是一個常量字符,而不是變量字符串,因此,不能使用指針p去改變它;
  • const型常量:C語言中const關鍵字用來定義常量,常量就是不能被改變的量。const用法有兩種:
  • 第一種:編譯器const修飾變量放在普通代碼段去,使其不能修改(各種單片機編譯器);
  • 第二種:由編譯器來檢查以確保const型的常量不會被修改,實際上const型的常量還是和普通變量一樣,放在數據段(gcc中是這樣實現的);
  • 顯式初始化為非零的全局變量和靜態局部變量放在數據段:
  • 放在.data段的變量有2種:第一種:顯示初始化為非零的全局變量;第二種:靜態局部變量,也就是static修飾的局部變量。(普通局部變量分配在棧上,靜態局部變量分配在數據段)
  • 未初始化或顯式初始化為0的全局變量放在bss段
  • bss段和.data段並沒有本質區別,幾乎可以不用明確去區分這兩種。
總結:
  • 相同點:三種獲取內存的方法,都可以給程序提供可用內存,都可以用來定義變量給程序用。
  • 不同點:棧內存對應C中的普通局部變量(別的變量還用不了棧,而且棧是自動的,由編譯器和運行時環境共同來提供服務的,程序員無法手工控制);堆內存完全是獨立於我們的程序存在和管理的,程序需要堆內存時可以去手工申請malloc,使用完成後必須盡快free釋放。(堆內存對程序就好象公共圖書館對於人);數據段對於程序來說對應C程序中的全局變量和靜態局部變量。
C語言的字符串類型 C語言沒有原生字符串類型
  • 很多高級語言像java、C#等就有字符串類型,有個String來表示字符串,用法和int這些很像,可以String s1 = "linux";來定義字符串類型的變量。
  • C語言沒有String類型,C語言中的字符串是通過字符指針來間接實現的。
C語言使用指針來管理字符串
  • C語言中定義字符串方法:char *p = "linux";此時p就叫做字符串,但是實際上p只是一個字符指針(本質上就是一個指針變量,只是p指向了一個字符串的起始地址而已)。
C語言中字符串的本質:
  • 指針指向頭、固定尾部的地址相連的一段內存
  • 字符串就是一串字符。字符反映在現實中就是文字、符號、數字等人用來表達的字符,反映在編程中字符就是字符類型的變量。C語言中使用ASCII編碼對字符進行編程,編碼後可以用char型變量來表示一個字符。字符串就是多個字符打包在一起共同組成的。
  • 字符串在內存中其實就是多個字節連續分布構成的(類似於數組,字符串和字符數組非常像)
  • C語言中字符串有3個核心要點:第一是用一個指針指向字符串頭;第二是固定尾部(字符串總是以‘\0‘來結尾);第三是組成字符串的各字符彼此地址相連。
  • ‘\0‘是一個ASCII字符,其實就是編碼為0的那個字符(真正的0,和數字0是不同的,數字0有它自己的ASCII編碼)。要註意區分‘\0‘和‘0‘和0.(0等於‘\0‘,‘0‘等於48)
  • ‘\0‘作為一個特殊的數字被字符串定義為(幸運的選為)結尾標誌。產生的副作用就是:字符串中無法包含‘\0‘這個字符。(C語言中不可能存在一個包含‘\0‘字符的字符串),這種思路就叫“魔數”(魔數就是選出來的一個特殊的數字,這個數字表示一個特殊的含義,你的正式內容中不能包含這個魔數作為內容)。
註意:
  • 指向字符串的指針和字符串本身是分開的兩個東西
  • char *p = "linux";在這段代碼中,p本質上是一個字符指針,占4字節;"linux"分配在代碼段,占6個字節;實際上總共耗費了10個字節,這10個字節中:4字節的指針p叫做字符串指針(用來指向字符串的,理解為字符串的引子,但是它本身不是字符串),5字節的用來存linux這5個字符的內存才是真正的字符串,最後一個用來存‘\0‘的內存是字符串結尾標誌(本質上也不屬於字符串)。
存儲多個字符的2種方式:字符串和字符數組
  • 我們有多個連續字符(典型就是linux這個字符串)需要存儲,實際上有兩種方式:第一種就是字符串;第二種是字符數組。
字符數組初始化與sizeof、strlen
  • sizeof是C語言的一個關鍵字,也是C語言的一個運算符(sizeof使用時是sizeof(類型或變量名),所以很多人誤以為sizeof是函數,其實不是),sizeof運算符用來返回一個類型或者是變量所占用的內存字節數。為什麽需要sizeof?主要原因一是int、double等原生類型占幾個字節和平臺有關;二是C語言中除了ADT之外還有UDT,這些用戶自定義類型占幾個字節無法一眼看出,所以用sizeof運算符來讓編譯器幫忙計算。
  • strlen是一個C語言庫函數,這個庫函數的原型是:size_t strlen(const char *s);這個函數接收一個字符串的指針,返回這個字符串的長度(以字節為單位)。註意一點是:strlen返回的字符串長度是不包含字符串結尾的‘\0‘的。我們為什麽需要strlen庫函數?因為從字符串的定義(指針指向頭、固定結尾、中間依次相連)可以看出無法直接得到字符串的長度,需要用strlen函數來計算得到字符串的長度。
  • sizeof(數組名)得到的永遠是數組的元素個數(也就是數組的大小),和數組中有無初始化,初始化多、少等是沒有關系的;strlen是用來計算字符串的長度的,只能傳遞合法的字符串進去才有意義,如果隨便傳遞一個字符指針,但是這個字符指針並不是字符串是沒有意義的。
  • 當我們定義數組時如果沒有明確給出數組大小,則必須同時給出初始化式,編譯器會根據初始化式去自動計算數組的大小(數組定義時必須給出大小,要麽直接給,要麽給初始化式)
字符串初始化與sizeof、strlen
  • char *p = "linux"; sizeof(p)得到的永遠是4,因為這時候sizeof測的是字符指針p本身的長度,和字符串的長度是無關的。
  • strlen剛好用來計算字符串的長度。
字符數組與字符串的本質差異(內存分配角度)
  • 字符數組char a[] = "linux";來說,定義了一個數組a,數組a占6字節,右值"linux"本身只存在於編譯器中,編譯器將它用來初始化字符數組a後丟棄掉(也就是說內存中是沒有"linux"這個字符串的);這句就相當於是:char a[] = {‘l‘, ‘i‘, ‘n‘, ‘u‘, ‘x‘, ‘\0‘};
  • 字符串char *p = "linux";定義了一個字符指針p,p占4字節,分配在棧上;同時還定義了一個字符串"linux",分配在只讀數據段:.rodata;然後把代碼段中的字符串(一共占6字節)的首地址(也就是‘l‘的地址)賦值給p。
  • 總結對比:字符數組和字符串有本質差別。字符數組本身是數組,數組自身自帶內存空間,可以用來存東西(所以數組類似於容器);而字符串本身是指針,本身永遠只占4字節,而且這4個字節還不能用來存有效數據,所以只能把有效數據存到別的地方,然後把地址存在p中。
  • 也就是說字符數組自己存那些字符;字符串一定需要額外的內存來存那些字符,字符串本身只存真正的那些字符所在的內存空間的首地址。
C語言之結構體概述 結構體類型是一種自定義類型
  • C語言中的2中類型:原生類型和自定義類型;
結構體使用時先定義結構體類型再用類型定義變量;
  • 結構體定義時需要先定義結構體類型,然後再用類型定義變量;
  • 也可以在定義結構體類型的同時定義結構體變量
  • #include <stdio.h>
    #include <string.h>
    
    struct peple
    {
      char name[20];
      int age;
    };
    struct student
    {
      int s1;
      char s2;
      double s3;
      
    }s;
    
    int main()
    {
      struct peple zhangsan;
      strcpy(zhangsan.name,"張三"); //結構體中的數組要使用strcpy進行賦值;
      zhangsan.age = 19;
      printf("%s,.%d\n",zhangsan.name,zhangsan.age);

從數組到結構體的進步之處
  • 結構體可以認為是從數組發展而來的。其實數組和結構體都算是數據結構的範疇了,數組就是最簡單的數據結構、結構體比數組更復雜一些,鏈表、哈希表之類的比結構體又復雜一些;二叉樹、圖等又更復雜一些。
  • 數組有2個明顯的缺陷:第一個是定義時必須明確給出大小,且這個大小在以後不能再更改;第二個是數組要求所有的元素的類型必須一致。更復雜的數據結構中就致力於解決數組的這兩個缺陷。
  • 結構體是用來解決數組的第二個缺陷的,可以將結構體理解為一個其中元素類型可以不相同的數組。結構體完全可以取代數組,只是在數組可用的範圍內數組比結構體更簡單。
  •   // 結構體 . 訪問和 -> 訪問,實質上都是指針訪問呢,只是編譯器對此作了優化;
      // 下面是對 . 訪問的 指針式理解
      s.s1 = 4;     // int *p1 = (int *)&s; *p1 = 4;
      s.s2 = e;   // char *p2 = (char *)((int)&s + 4); *p2 = ‘e‘;
      s.s3 = 3.3;   // double *p3 = (double *)((int)&s + 8); *p3 = 3.3;
    
      printf("%d, %c, %f\n",s.s1,s.s2,s.s3);
    
      int *p1 = (int *)&s;
      char *p2 = (char *)((int)&s + 4);
      double *p3 = (double *)((int)&s + 8); //這裏是 +8, 而不是 +5
    
      printf("%d, %c, %f\n",*p1,*p2,*p3);
      return 0;
    }



結構體的對齊訪問1
    參考閱讀blog:
  • http://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html
  • http://blog.csdn.net/sno_guo/article/details/8042332
什麽是結構體對齊訪問
  • 結構體中元素的訪問其實本質上還是用指針方式,結合這個元素在整個結構體中的偏移量和這個元素的類型來進行訪問的。
  • 但是實際上結構體的元素的偏移量比我們上節講的還要復雜,因為結構體要考慮元素的對齊訪問,所以每個元素實際占的字節數和自己本身的類型所占的字節數不一定完全一樣。(譬如char c實際占字節數可能是1,也可以是2,也可能是3,也可以能4····)
  • 一般來說,我們用 . 的方式來訪問結構體元素時,我們是不用考慮結構體的元素對齊的。因為編譯器會幫我們處理這個細節。但是因為C語言本身是很底層的語言,而且做嵌入式開發經常需要從內存角度,以指針方式來處理結構體及其中的元素,因此還是需要掌握結構體對齊規則。
結構體為何要對齊訪問
  • 結構體中元素對齊訪問主要原因是為了配合硬件,也就是說硬件本身有物理上的限制,如果對齊排布和訪問會提高效率,否則會大大降低效率。
  • 內存本身是一個物理器件(DDR內存芯片,SoC上的DDR控制器),本身有一定的局限性:如果內存每次訪問時按照4字節對齊訪問,那麽效率是最高的;如果你不對齊訪問效率要低很多。
  • 還有很多別的因素和原因,導致我們需要對齊訪問。譬如Cache的一些緩存特性,還有其他硬件(譬如MMU、LCD顯示器)的一些內存依賴特性,所以會要求內存對齊訪問。
  • 對比對齊訪問和不對齊訪問:對齊訪問犧牲了內存空間,換取了速度性能;而非對齊訪問犧牲了訪問速度性能,換取了內存空間的完全利用。
結構體對齊的規則和運算
  • 編譯器本身可以設置內存對齊的規則,有以下的規則需要記住:
  • 第一個:32位編譯器,一般編譯器默認對齊方式是4字節對齊。
總結:結構體對齊的分析要點和關鍵:
  • 結構體對齊要考慮:結構體整體本身必須安置在4字節對齊處,結構體對齊後的大小必須4的倍數(編譯器設置為4字節對齊時,如果編譯器設置為8字節對齊,則這裏的4是8)
  • 結構體中每個元素本身都必須對其存放,而每個元素本身都有自己的對齊規則。
  • 編譯器考慮結構體存放時,以滿足以上2點要求的最少內存需要的排布來算。
gcc支持但不推薦的對齊指令:#pragma pack()、#pragma pack(n) (n=1/2/4/8)
  • #pragma是用來指揮編譯器,或者說設置編譯器的對齊方式的。編譯器的默認對齊方式是4,但是有時候我不希望對齊方式是4,而希望是別的(譬如希望1字節對齊,也可能希望是8,甚至可能希望128字節對齊)。
  • 常用的設置編譯器編譯器對齊命令有2種:第一種是#pragma pack(),這種就是設置編譯器1字節對齊(有些人喜歡講:設置編譯器不對齊訪問,還有些講:取消編譯器對齊訪問);第二種是#pragma pack(4),這個括號中的數字就表示我們希望多少字節對齊。
  • 我們需要#prgama pack(n)開頭,以#pragma pack()結尾,定義一個區間,這個區間內的對齊參數就是n。
  • #prgma pack的方式在很多C環境下都是支持的,但是gcc雖然也可以不過不建議使用。
gcc推薦的對齊指令__attribute__((packed))、__attribute__((aligned(n)))
  • __attribute__((packed))使用時直接放在要進行內存對齊的類型定義的後面,然後它起作用的範圍只有加了這個東西的這一個類型。packed的作用就是取消對齊訪問。類似於 #prgama pack(1) 的作用;
  • __attribute__((aligned(n)))使用時直接放在要進行內存對齊的類型定義的後面,然後它起作用的範圍只有加了這個東西的這一個類型。它的作用是讓整個結構體變量整體進行n字節對齊(註意是結構體變量整體n字節對齊,而不是結構體內各元素也要n字節對齊)
  • 總結:#prgama pack(n)對齊,是結構體中每一個變量字節對齊;__attribute__((aligned(n)))是結構體整體字節對齊
  • #include <stdio.h>
    
    typedef struct E
    {               // 共占24字節    共占9字節      共占20字節      共占24字節      共占24字節
        short i;    //   2          2               2           2
        short j;    //   2          2               2           2
        char m;     //   1(1+3)     1               1(1+1)      1(1+3)
        int n;      //   4          4               4           4
        struct A a; //   12         9               10          12
    }E;
    
    #pragma pack()
    typedef struct
    {               // 共占9字節   
        short i;    //   2         
        short j;    //   2          
        char m;     //   1     
        int n;      //   4          
    }__attribute__((packed)) CC;
    
                    // 1字節對齊        2字節對齊        4字節對齊       8字節對齊
    struct mystruct111
    {               // 共占12字節       共占12字節          共占12字節      共占16字節                    
        int a;    // 4            4            4               4
        char b;    // 1            1            1               1
        short c;    // 2            2            2               2
        short d;    // 2            2            2               2
    }__attribute__((aligned(8))) My111;



offsetof宏與container_of宏
結構體指針訪問各個元素的原理:
  • 通過結構體整體變量來訪問其中各個元素,本質上是通過指針方式來訪問的,形式上是通過 . 的方式來訪問的(這時候其實是編譯器幫我們自動計算了偏移量);
offsetof宏:
  • offsetof宏的作用是:用宏來計算結構體中某個元素和結構體首地址的偏移量(其實質是通過編譯器來幫我們計算)。
  • offsetof宏的原理:虛擬一個type類型結構體變量,然後用type.member的方式來訪問那個member元素,繼而得到member相對於整個變量首地址的偏移量。
  • 學習思路:第一步先學會用offsetof宏,第二步再去理解這個宏的實現原理。
  • offsetof宏解析:
  1. #define offsetof(TYPE, MEMBER) (int)(&((TYPE *)0) -> MEMBER ),
  2. (TYPE *)0:這是一個強制類型轉換,把0地址強制類型轉換成一個指針,這個指針指向一個TYPE類型的結構體變量。(實際上這個結構體變量可能不存在,但是只要我不去解引用這個指針就不會出錯)。
  3. ((TYPE *)0)->MEMBER:(TYPE *)0是一個TYPE類型結構體變量的指針,通過指針指針來訪問這個結構體變量的member元素
  4. &((TYPE *)0)->MEMBER:等效於&(((TYPE *)0)->MEMBER),意義就是得到member元素的地址。但是因為整個結構體變量的首地址是0,所以這個宏返回的是member元素相對於整個結構體變量的首地址的偏移量,類型是int;
container_of宏:
  • 作用:知道一個結構體中某個元素的指針,反推這個結構體變量的指針。有了container_of宏,我們可以從一個元素的指針得到整個結構體變量的指針,繼而得到結構體中其他元素的指針。
  • typeof關鍵字的作用是:typepef(a)時由變量a得到a的類型,typeof就是由變量名得到變量數據類型的。
  • 這個宏的工作原理:先用typeof得到member元素的類型定義成一個指針,然後用這個指針減去該元素相對於整個結構體變量的偏移量(偏移量用offsetof宏得到的),減去之後得到的就是整個結構體變量的首地址了,再把這個地址強制類型轉換為type *即可。
  • #include <stdio.h>
    
    // TYPE是結構體類型,MEMBER是結構體中一個元素的元素名
    // 這個宏返回的是member元素相對於整個結構體變量的首地址的偏移量,類型是int
    #define offsetof(TYPE, MEMBER)      (int)(&((TYPE *)0) -> MEMBER )
    
    // ptr是指向結構體元素member的指針,type是結構體類型,member是結構體中一個元素的元素名
    // 這個宏返回的就是指向整個結構體變量的指針,類型是(type *)
    #define container_of(ptr, type, member) ({                const typeof(((type *)0)->member) * __mptr = (ptr);        (type *)((char *)__mptr - offsetof(type, member)); })
    
    struct cc
    {
        char a;
        short b;
        int c;
    };
    
    int main(void)
    {
        struct cc  s;
        s.b = 12;
        struct cc *pS = NULL;
        short *p = &(s.c);
        pS = container_of(p, struct cc, c);     
        printf("&s.a = %p\n", &s);          //&s.a = 0xbfd88d44
        printf("&s.c = %p\n", p);           //&s.c = 0xbfd88d48
        printf("&pS = %p\n",pS);            //&pS  = 0xbfd88d44
    
        printf("&s.b = %p\n", &(s.b));      //&s.b = 0xbfd88d46
        printf("&s.b = %p\n", &(pS->b));    //&s.b = 0xbfd88d48
        printf("pS.b = %d\n", pS->b);       //12
    
        return 0;
    }



學習指南和要求:
  • 最基本要求是:必須要會這兩個宏的使用。就是說能知道這兩個宏接收什麽參數,返回什麽值,會用這兩個宏來寫代碼。看見代碼中別人用這兩個宏能理解什麽意思。
  • 升級要求:能理解這兩個宏的工作原理,能表述出來。(有些面試筆試題會這麽要求)
  • 更高級要求:能自己寫出這兩個宏(不要著急,慢慢來)
共用體 共用體類型的定義、變量定義和使用
  • 共用體union和結構體struct在類型定義、變量定義、使用方法上很相似。
  • 共用體和結構體的不同:結構體類似於一個包裹,結構體中的成員彼此是獨立存在的,分布在內存的不同單元中,他們只是被打包成一個整體叫做結構體而已;共用體中的各個成員其實是一體的,彼此不獨立,他們使用同一個內存單元。可以理解為:有時候是這個元素,有時候是那個元素。更準確的說法是同一個內存空間有多種解釋方式。
  • 共用體union就是對同一塊內存中存儲的二進制的不同的理解方式。
  • 在有些書中把union翻譯成聯合(聯合體),這個名字不好。現在翻譯成共用體比較合適。
  • union的sizeof測到的大小實際是union中各個元素裏面占用內存最大的那個元素的大小。因為可以存的下這個就一定能夠存的下其他的元素。
  • union中的元素不存在內存對齊的問題,因為union中實際只有1個內存空間,都是從同一個地址開始的(開始地址就是整個union占有的內存空間的首地址),所以不涉及內存對齊。
  • #include <stdio.h>
    
    union myunion
    {
        int a;
        float b;
        char c;
        double d;
    };
    struct aa
    {
        char i;
        int j;
        double d;
    }a1;
    
    int main(void)
    {
        union myunion t1; 
        t1.a = 1123477881;
        printf("value = %f.\n", t1.b);  //123.456001
        
        int a = 1123477881;
        printf("指針方式:%f.\n", *((float *)&a));   //123.456001
    
        t1.a = 12;
        printf("s1.b = %d.\n", t1.b);
        printf("s1 = %d\n", sizeof(union myunion));  //8
        printf("a1 = %d\n", sizeof(struct aa)); //16   
        
        return 0;
    }



    共用體和結構體的相同和不同
  • 相同點就是操作語法幾乎相同。
  • 不同點是本質上的不同。struct是多個獨立元素(內存空間)打包在一起;union是一個元素(內存空間)的多種不同解析方式。
共用體的主要用途
  • 共用體就用在那種對同一個內存單元進行多種不同規則解析的這種情況下。
  • C語言中其實是可以沒有共用體的,用指針和強制類型轉換可以替代共用體完成同樣的功能,但是共用體的方式更簡單、更便捷、更好理解。
大小端模式 什麽是大小端模式
  • 大端模式(big endian)和小端模式(little endian)。最早是小說中出現的詞,和計算機本來沒關系的。
  • 後來計算機通信發展起來後,遇到一個問題就是:在串口等串行通信中,一次只能發送1個字節。這時候我要發送一個int類型的數就遇到一個問題。int類型有4個字節,我是按照:byte0 byte1 byte2 byte3這樣的順序發送,還是按照byte3 byte2 byte1 byte0這樣的順序發送。規則就是發送方和接收方必須按照同樣的字節順序來通信,否則就會出現錯誤。這就叫通信系統中的大小端模式。這是大小端這個詞和計算機掛鉤的最早問題。
  • 現在我們講的這個大小端模式,更多是指計算機存儲系統的大小端。在計算機內存/硬盤/Nnad中。因為存儲系統是32位的,但是數據仍然是按照字節為單位的。於是乎一個32位的二進制在內存中存儲時有2種分布方式:高字節對應高地址(小端模式)、高字節對應低地址(大端模式)
  • 大端模式和小端模式本身沒有對錯,沒有優劣,理論上按照大端或小端都可以,但是要求必須存儲時和讀取時按照同樣的大小端模式來進行,否則會出錯。
  • 現實的情況就是:有些CPU公司用大端(譬如C51單片機);有些CPU用小端(譬如ARM)。(大部分是用小端模式,大端模式的不算多)。於是乎我們寫代碼時,當不知道當前環境是用大端模式還是小端模式時就需要用代碼來檢測當前系統的大小端。
技術分享圖片 經典筆試題:
  • 用C語言寫一個函數來測試當前機器的大小端模式。
  • 用union來測試機器的大小端模式
  • #include <stdio.h>
    
    union endian    //共用體都是從地地址開始訪問的
    {
        char i;
        int j;
    }s;
    
    //小端模式返回1,否則為大端模式
    int is_little_endian1(void)
    {
        s.j = 1;    // 地址0的那個字節內是1(小端)或者0(大端)
        return s.i;
    }
    int is_little_endian2(void)
    {
        int i = 1;
        char p = *((char *)(&i));
        return p;
    }
    int main(void)
    {
        char i;
        // i = is_little_endian2(); //union測試
        i = is_little_endian2();    //指針測試
        if(i == 1)
        {
            printf("小端模式\n");
        }
        else 
        {
            printf("大端模式\n");
        }
    
        return 0;
    }



指針方式來測試機器的大小端    
看似可行實則不行的測試大小端方式:位與、移位、強制類型轉化
  • 位與運算。
  • 結論:位與的方式無法測試機器的大小端模式。(表現就是大端機器和小端機器的&運算後的值相同的)
  • 理論分析:位與運算是編譯器提供的運算,這個運算是高於內存層次的(或者說&運算在二進制層次具有可移植性,也就是說&的時候一定是高字節&高字節,低字節&低字節,和二進制存儲無關)。
  • 移位
  • 結論:移位的方式也不能測試機器大小端。
  • 理論分析:原因和&運算符不能測試一樣,因為C語言對運算符的級別是高於二進制層次的。右移運算永遠是將低字節移除,而和二進制存儲時這個低字節在高位還是低位無關的。
  • 強制類型轉換
  • 同上
通信系統中的大小端(數組的大小端)
  • 譬如要通過串口發送一個0x12345678給接收方,但是因為串口本身限制,只能以字節為單位來發送,所以需要發4次;接收方分4次接收,內容分別是:0x12、0x34、0x56、0x78.接收方接收到這4個字節之後需要去重組得到0x12345678(而不是得到0x78563412).
  • 所以在通信雙方需要有一個默契,就是:先發/先接的是高位還是低位?這就是通信中的大小端問題。
  • 一般來說是:先發低字節叫小端;先發高字節就叫大端。(我不能確定)實際操作中,在通信協議裏面會去定義大小端,明確告訴你先發的是低字節還是高字節。
  • 在通信協議中,大小端是非常重要的,大家使用別人定義的通信協議還是自己要去定義通信協議,一定都要註意標明通信協議中大小端的問題。
枚舉 枚舉是用來幹嘛的?
  • 枚舉在C語言中其實是一些符號常量集。直白點說:枚舉定義了一些符號,這些符號的本質就是int類型的常量,每個符號和一個常量綁定。這個符號就表示一個自定義的一個識別碼,編譯器對枚舉的認知就是符號常量所綁定的那個int類型的數字。
  • 枚舉中的枚舉值都是常量,怎麽驗證?
  • 枚舉符號常量和其對應的常量數字相對來說,數字不重要,符號才重要。符號對應的數字只要彼此不相同即可,沒有別的要求。所以一般情況下我們都不明確指定這個符號所對應的數字,而讓編譯器自動分配。(編譯器自動分配的原則是:從0開始依次增加。如果用戶自己定義了一個值,則從那個值開始往後依次增加)
C語言為何需要枚舉
  • C語言沒有枚舉是可以的。使用枚舉其實就是對1、0這些數字進行符號化編碼,這樣的好處就是編程時可以不用看數字而直接看符號。符號的意義是顯然的,一眼可以看出。而數字所代表的含義除非看文檔或者註釋。
  • 宏定義的目的和意義是:不用數字而用符號。從這裏可以看出:宏定義和枚舉有內在聯系。宏定義和枚舉經常用來解決類似的問題,他們倆基本相當可以互換,但是有一些細微差別。
宏定義和枚舉的區別
  • 枚舉是將多個有關聯的符號封裝在一個枚舉中,而宏定義是完全散的。也就是說枚舉其實是多選一。
  • 什麽情況下用枚舉?當我們要定義的常量是一個有限集合時(譬如一星期有7天,譬如一個月有31天,譬如一年有12個月····),最適合用枚舉。(其實宏定義也行,但是枚舉更好)
  • 不能用枚舉的情況下(定義的常量符號之間無關聯,或者無限的)用宏定義。
  • 總結:宏定義先出現,用來解決符號常量的問題;後來人們發現有時候定義的符號常量彼此之間有關聯(多選一的關系),用宏定義來做雖然可以但是不貼切,於是乎發明了枚舉來解決這種情況。
  • 枚舉的定義和使用
  • #include <stdio.h>
    
    //這個枚舉用來表示函數返回值,error表示錯誤,right表示正確
    enum return_value
    {
        error = 12,  //枚舉值是全局的,直接自己就可以用;
        right ,  //因為枚舉是全局的,所以所有的枚舉類型中,常量符號都不能相同
    };
    enum return_value func1(void)
    {
        enum return_value r1 = right;
        return (r1);
    }
    
    int main(void)
    {
        printf("error = %d\n", error);
        printf("right = %d\n", right);
        enum return_value s = func1();
        if(s == error)
        {
            printf("函數執行錯誤\n");
        }
        else
        {
            printf("函數執行正確\n");
        }
        return 0;
    }




5、C_數組&字符串&結構體&共用體&枚舉