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

程序中內存從哪裏來

三種內存來源:棧(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)去訪問第2五、第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語言的一個關鍵字,sizeof也是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:
什麼是結構體對齊訪問
  • 結構體中元素的訪問其實本質上仍是用指針方式,結合這個元素在整個結構體中的偏移量和這個元素的類型來進行訪問的。
  • 可是實際上結構體的元素的偏移量比咱們上節講的還要複雜,由於結構體要考慮元素的對齊訪問,因此每一個元素實際佔的字節數和本身自己的類型所佔的字節數不必定徹底同樣。(譬如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次接收,內容分別是:0x十二、0x3四、0x5六、0x78.接收方接收到這4個字節以後須要去重組獲得0x12345678(而不是獲得0x78563412).
  • 因此在通訊雙方須要有一個默契,就是:先發/先接的是高位仍是低位?這就是通訊中的大小端問題。
  • 通常來講是:先發低字節叫小端;先發高字節就叫大端。(我不能肯定)實際操做中,在通訊協議裏面會去定義大小端,明確告訴你先發的是低字節仍是高字節。
  • 在通訊協議中,大小端是很是重要的,你們使用別人定義的通訊協議仍是本身要去定義通訊協議,必定都要注意標明通訊協議中大小端的問題。
 

枚舉

枚舉是用來幹嗎的?
  • 枚舉在C語言中實際上是一些符號常量集。直白點說:枚舉定義了一些符號,這些符號的本質就是int類型的常量,每一個符號和一個常量綁定。這個符號就表示一個自定義的一個識別碼,編譯器對枚舉的認知就是符號常量所綁定的那個int類型的數字。
  • 枚舉中的枚舉值都是常量,怎麼驗證?
  • 枚舉符號常量和其對應的常量數字相對來講,數字不重要,符號才重要。符號對應的數字只要彼此不相同便可,沒有別的要求。因此通常狀況下咱們都不明確指定這個符號所對應的數字,而讓編譯器自動分配。(編譯器自動分配的原則是:從0開始依次增長。若是用戶本身定義了一個值,則從那個值開始日後依次增長)
C語言爲什麼須要枚舉
  • C語言沒有枚舉是能夠的。使用枚舉其實就是對一、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; }
相關文章
相關標籤/搜索