C提高~複合數據結構類型

複合數據結構類型

引言

C語言中複雜的組合數據類型,如數組、字符串、結構體、共用體、枚舉等其他組合類型,它們是如何在內存中開闢空間的,以及這些組合數據類型的特點又是如何在內存中體現出來的。現有的操作系統計算機上,爲了實現內存的高效利用,操作系統對所有物理內存進行了統一的內存管理,所以應用程序表現出來的都是虛擬內存

管理方式

在C語言程序中,存放數據所能使用的內存空間大概分爲四種情況:棧(stack)、堆(heap)、數據區(.data和.bss區)和常量區(.ro.data)

棧內存特點

空間實現自動管理運行時空間自動分配,運行結束空間自動回收。棧是自動管理的,程序員不需要手工干預,方便簡單,因此棧又稱爲自動管理區

能夠被反覆使用:棧內存在程序中用的都是一塊內存空間,程序通過自動開闢和自動釋放,會反覆使用這一塊空間。

髒內存棧內存由於反覆使用每次使用後程序不會去清空內容,當下一次該空間再次被分配時上一次使用的值會還在

臨時性:函數不能返回棧變量的指針,因爲該空間在函數運行結束之後就會被釋放

/*函數不能返回局部變量的地址,因爲該函數執行完後,存於函數棧的局部變量就已經不存了,如果返回地址
其訪問該空間的話,該空間很有可能已經被別人獲取正在使用了*/
#include <stdio.h>

int func(void){
  /*a是局部變量,分配在棧上又叫棧變量(臨時變量)*/
  int a = 4;
  printf("&a = %p\n",&a);
  return &a;
}

void func(void){
  int a = 33;
  int b = 33;
  int c = 33;
  printf("int func2, &a = %p\n",&a);
}

int main(void){
  int *p = NULL;
  p = func();
  func2();
  func2();
  printf("p = %p\n");
  printf("*p = %d.\n",*p);    //運行之後*p等於33,證明棧內存用完之後是髒的,臨時的

  return 0;
}

運行結果

&a = 000000000062FDDC
int func2, &a = 000000000062FDD4
int func2, &a = 000000000062FDD4
p = 00007FFDCD56FA30
*p = 0.

func()函數運行結束函數體內定義的局部變量自動釋放,這時看出p地址和&a是不一致的,*p值爲0,未初始化的變量默認值爲0;

/*棧出溢出:因爲操作系統事先給定了棧的大小,
  如果在函數中無窮盡的分配局部變量,棧內存總能用完*/
#include <stdio.h>

void stack_overflow(void){
  int a[10000000] = {0};
  a[10000000 -1]  = 12;
  return ;
}

//下面函數爲遞歸函數
void stack_overflow2(void){
  int a = 2;
  stack_overflow2();
  return ;
}


int main(void){
  //stack_overflow(); //core dumped
  stack_overflow2();
  return 0;
}

上面兩個函數的運行結果Segmentation fault(core dumped)證明棧溢出了。

堆內存特點

靈活:堆時另一種管理形式的內存區域,堆內存的管理靈活。

內存量大:堆內存空間很大,進程可以按需手動申請,使用完手動釋放。

程序手動申請和釋放:寫代碼去申請malloc和釋放free。

髒內存:堆內存也是反覆使用的,而且使用者用完釋放前不會清除,因此惹事髒的。

臨時性:堆內存在malloc後和free之前的這期間可以被訪問。在malloc之前free之後不能訪問,否則會有不可預料的後果。

#include <stdio.h>
#include <stdlib.h>

int main(void){
  //需要一個1000個int類型元素的數組
  //第一步:申請和綁定
  int *p = (int *)malloc(1000*sizeof(int));
  //第二步:檢驗申請是否成功
  if(p == NULL){
    printf("malloc error.\n");
    return -1;
  }
  //第三步:使用申請的內存
  *(p+0) = 1;
  *(p+1) = 2;
  
  //第四步:釋放
  free(p);
  return 0;
}

如果最後沒有將分配的堆內存空間釋放的話,這塊內存空間會被一直佔用,只有當整個程序終止後纔會釋放。所以對堆內存來說,使用完後及時使用free釋放空間就顯得非常重要,否則會導致內存泄露,即內存空間都還在 ,但是該空間被之前程序使用後,後續不再使用了,但是它卻一直佔着

malloc

使用malloc分配空間時,返回實際上是一個void *類型的指針(地址),該地址是本次申請內存空間的首字節地址,失敗返回NULL(使用前需檢查是否爲NULL),使用完需用free釋放void類型(空類型)不表示沒有類型,而表示萬能類型,在需要時在具體指定void*表示是一個無類型指針,對於32位系統,指針本都是4個字節

值得注意的是:調用free歸還p所指向的堆內存之前指向這段內存的指針p的指向需要發生改變指向了其他的地方的話,必須通過一箇中間指針變量先記住p指向的堆空間,之後free時才能通過這個中間變量釋放之前p所指向的堆空間,否則就會造成之前p所指的堆空間無法釋放導致內存泄露的發生

內存中的各個段

代碼段、數據段、bss段

編譯器在編譯程序時,程序會按照一定的結構被劃分成各個不同的段進行組織,即.text、.bss、.data段等

代碼段(.text):代碼存放程序的代碼部分,程序中各種函數的指令就存放在該段

數據段:又稱數據區、靜態數據區、靜態區程序中的靜態變量空間開闢於此,值得注意:全局變量是整個程序的公共財產,而局部變量只是函數的私有財產

.bss段:又叫ZI(Zero Initial)段,所有未初始化的靜態變量的空間就開闢於此,這個段會自動將未初始化靜態空間初始化爲0

注意:數據段(.data)和.bss段實際上沒有本質區別,都是用來存放程序中的靜態變量,只是.data存放顯式初始化爲非0的靜態數據,而.bss中存放那些顯式初始化爲0或者未顯式初始化的靜態數據

特殊數據會被放到代碼段

#include <stdio.h>
int main(void){
  char *p = "linux";
  *(p+0) = 'f';        //運行報錯,因爲是字符串常量,不能被修改
  printf("p = %s.\n",p);

  return 0;
}

內存管理方式小結

棧、堆和靜態這三種內存管理方式都可以爲程序提供內存空間。棧空間用於開闢局部變量空間,實現自動內存管理;對於堆內存,程序中需要使用malloc進行手動申請,使用完後必須使用free進行釋放,實現手動內存管理靜態數據區的數據段,專門用於開闢全局變量和靜態變量不需要程序員參與管理。

只是在函數內部臨時使用作用範圍希望被侷限在函數內部,即定義局部變量

堆內存和數據段幾乎擁有完全相同的屬性,大部分時候是可以相互替換。但是他們生命週期不同堆內存的生命週期是從malloc開始到free結束,而靜態變量程序一開始執行就被開闢,直到整個程序結束纔回收,伴隨程序運行一直存在。所以,若變量只是在程序的一個階段期間有用適合使用堆內存空間;若變量需要在程序運行的整個過程中一直存在適合使用全局變量

字符串類型

C語言使用指針來管理字符串,例如char *p = "linux",此時p爲字符串,但p本質上是一個指針變量p中存放了字符串的第一個字符的地址,該地址即爲字符串的地址

字符串的本質

字符串的本質爲指向字符串的存放空間的指針,C中使用ASCII碼對字符進行編碼,編碼後用char型變量來表示一個字符,所以字符串就是由多個字符打包在一起共同組成的,本質上和字符數組沒有什麼區別,只是使用了‘\0’字符作爲結尾符反映在內存中字符串是由多個字節連續分佈構成的,每個字符佔用了一個字節。其中涉及要點如下:

  1. 用一個指針指向字符串頭
  2. 固定尾部(字符串總是以'\0'來結尾);
  3. 組成字符串的各字符的地址彼此連續

指向字符串的指針變量空間和字符串存放的空間是分開的

還是看這個例子char *p = "linux"p是一個字符指針變量,佔4個字節。p可以是全局變量或局部變量;而"linux"存儲於代碼段,佔6個字節,實際上總共消耗了10個字節,其中4個字節用於存放字符串第一個字符的地址,5個字節用於存放linux這五個字符,最後一個用於存放‘\0’字符串結尾符

存儲多個字符的兩種方式----字符串和字符數組

當有多個連續字符需要存儲時,有兩種方式,第一種是字符串,第二種是字符數組,如下:

#include <stdio.h>
int main(void){
  char *p = "linux";    //字符串
  char a[] = "linux";   //字符數組
  return 0;
}

sizeof

sizeof是C語言中一個關鍵字,也是一個運算符,使用方法爲sizeof(類型或變量名)sizeof運算符返回的是類型或者變量所佔用的字節數。一像int、double等原生類型佔用字節數和平臺有關,使用sizeof可測試不同平臺下類型所佔用的字節數;二除了ADT之外還有UDT,用戶自定義類型中佔用字節數可以使用sizeof運算符查看。

strlen庫函數

size_t strlen(const char *s);

用於計算並返回字符串你的實際長度,該函數接受一個字符串的指針,返回值爲字符串的長度(以字節爲單位)。注意:strlen返回的字符串長度不包含結尾'\0'。

#include <stdio.h>
#include <string.h>
int main(){
  char *p = "linux";
  int len = strlen(p);

  printf("sizeof(p) = %d.\n",sizeof(p));
  printf("len = %d\n",len);    //len = 5
  return 0;
}
//64爲操作系統
sizeof(p) = 8. 
len = 5

小結:在sizeof是測試字符指針變量p本身的長度,和字符串長度無關,而strlen是用來計算字符串中字符個數的(不包含'\0'),所以strlen的結果爲5。
       sizeof(數組名)得到的永遠是數組字節數與有無初始化沒有任何關係strlen用於計算字符串的長度只有傳遞合法的字符串地址纔有效,若隨便傳遞一個字符指針,但該字符指針指向的並不是一個字符串,是沒有意義的,如下例:

#include <stdio.h>
#include <string.h>

int main(void){
  char a[] = "windows";    //a[0] = 'w';a[1]='i';.....a[6] = 's';a[7]='\0';
  printf("sizoef(a) = %d.\n",sizeof(a));    //8
  printf("strlen(a) = %d.\n",strlen(a));    //7
  
  char b[5] = "windows";    //字符串“Windows”個數大於5所以編譯器會將字符串裏的字符'w'和's'去掉
  printf("sizoef(b) = %d.\n",sizeof(b));    //5
  printf("strlen(b) = %d.\n",strlen(b));    //5

  char c[5] = {0};                        //c[0] = 0;
  printf("sizoef(c) = %d.\n",sizeof(c));    //5
  printf("strlen(c) = %d.\n",strlen(c));    //0
   
  return 0;
}

運行結果

char b[5] = "windows"; [Warning] initializer-string for array of chars is too long
sizoef(a) = 8.
strlen(a) = 7.
sizoef(b) = 5.
strlen(b) = 5.
sizoef(c) = 5.
strlen(c) = 0.

小結:若在定義數組時,沒有明確給出數組大小,需要在初始化時給定,編譯器會根據初始化時的字符個數去自動計算數組空間大小。

字符數組與字符串

字符數組char a[] = "linux";定義一個數組a,數組a佔6個字節右值"linux"本身只存在於編譯器中編譯器用它來初始化字符數組a後就棄掉,該字符串的字符被存放於數組中,等價於 char a [] = {'l','i','n','u','x'};

 

字符串char *p = "linux"定義了一個字符指針p,p佔4個字節,分配在棧上;同時定義了一個字符串“linux”,分配在代碼段中,然後把代碼段中的字符串的首地址(即‘l’的地址)賦值給p;

 

總結對比,字符數組自帶內存空間,可以直接存放字符數據,而字符串只是一個字符指針變量,只佔4個字節,字符只能存到別地方,然後把其首地址存在p中

結構體

定義結構體時需要先聲明結構體類型,然後在用結構體類型來定義結構體變量,也可以在定義結構體類型的同時定義結構體變量

//1 定義類型
struct people{
  char name[20];
  int age;
};

//2 定義類型的同時定義變量
struct student{
  char name[20];
  int age;
};

//3 將結構體struct student重命名爲s1,s1是一個類型名,不是變量
typedef struct student{
  char name[20];
  int age;
}s1;

數組與結構體比較

數組有兩個缺陷,第一個定義時必須明確給出大小且以後大小無法更改第二個數組要求所有元素類型必須一致結構體用來解決數組第二個缺陷,可將結構體理解爲其中元素類型可以不相同的數組;只是通常請款下數組使用較簡單;

數組訪問方式有兩種下標方式和指針方式,但實質上都是指針方式結構體變量中的元素訪問方式只有一種,用句點.或箭頭 ->方式,其兩種方式實質一樣使用地址進行訪問;當使用指針時可使用句點.訪問,只是寫法複雜,而使用箭頭替代,更加簡潔

結構體對齊訪問

結構體中元素訪問,本質上還是指針方式,結構該元素在整個結構體中偏移量和該元素類型來訪問,但實際上結構體的元素偏移量比較複雜,還需考慮元素對齊訪問,結構體實際佔用的字節數與所有成員佔用字節數總和不一定相等

#include <stdio.h>
struct s{
  char c;
  int a;
};

int main(){
  printf("sizeof(struct s) = %d.\n",sizeof(struct s));    //8
  
  return 0;
}

訪問結構體元素時需要對齊訪問,主要爲了配合硬件,即硬件本身有物理上的限制,對齊排布和訪問可以提高訪問效率內存本身是一個物理器件(DDR內存芯片,SOC上的DDR控制器),本身有一定的侷限性:如果內存每次訪問時按照4個字節對齊訪問,效率最高(犧牲內存空間換取速度性能);若不對齊訪問,效率要低很多。

結構體對齊的規則和運算

編譯器本身可設置內存對齊規則,以下規則需要記住:

32位編譯器,一般編譯器默認對齊方式是4個字節

#include <stdio.h>
struct mystruct1{            //1字節對齊        //4字節對齊
  int a;                     // 4              // 4
  char b;                   //  1             //  2
  short c;                 //   2            //   2
};

int main(void){
  //4字節對齊
  printf("sizeof(struct mystruct1) = %d.\n",sizeof(struct mystruct1));
  return 0;
}

運行結果

sizeof(struct mystruct1) = 8.

整個結構體變量4個字節對齊是有編譯器保證的,第一個元素a的第一個字節地址爲整合結構體的起始地址,所以自然是4字節對齊的。但是a的結束地址由下一個元素說了算,然後第二元素b,因爲上一個元素a本身佔4個字節,本身就是對齊的。所以留給b的開始地址也是4個字節對齊地址。所以b可直接放b放的位置就決定了a一共佔4個字節,因爲不需要填充b的起始地址定了後,結束地址不能定(因爲可能需要填充),結束地址要由下一個元素來定。然後第三個元素c,short類型需要2個字節必須放在類似0、2、4、8這樣地址處,不能存放在1,3這樣的奇數地址處,因此c不能緊挨着b存放,需要在b之後添加1字節的填充(padding),然後在開始放c當整個結構體的所有元素都對齊存放後,還沒結束,因爲整個結構體大小還要是4的整數倍

結構體對齊總結

當編譯器將結構體設置爲4個字節,稱之爲自動對齊,反之,使用#program進行對齊是就是手動對齊。設置手動對齊的命令有兩種:

  • 第一種#program pack(),設置編譯器1個字節對齊(即取消對齊);
  • 第二種#program pack(n),表示n=4表示4字節對齊,若爲n=8即爲8字節對齊;
#include <stdio.h>
#pragma pack(4)           //4字節對齊

struct mystruct1{
  int a;      // 4
  char b;     // 1(+1) 2
  short c;    // 2
};

struct mystruct2{
  char a;     //1(+3) 4
  int b;     // 4
  short c;   // 2(+2) 4
};

typedef struct mystruct5{
  int a;                // 4
  struct mystruct1 s1;  // 8
  double b;            //  8
  int c;               //  4
}MyS;

struct stu{
  char sex;           //1(+3) 4
  int length;         // 4
  char name[10];     // 1*10(+2) 12
};
#pragma pack() 

int main(void){
  printf("sizeof(struct mystruct1) = %d.\n",sizeof(struct mystruct1));
  printf("sizeof(struct mystruct2) = %d.\n",sizeof(struct mystruct2));
  printf("sizeof(MyS) = %d.\n",sizeof(MyS));
  printf("sizeof(struct stu) = %d.\n",sizeof(struct stu));
  return 0;
}

運行結果

sizeof(struct mystruct1) = 8.
sizeof(struct mystruct2) = 12.
sizeof(MyS) = 24.
sizeof(struct stu) = 20.

沒有字節對齊(1字節對齊)

sizeof(struct mystruct1) = 7.
sizeof(struct mystruct2) = 7.
sizeof(MyS) = 23.
sizeof(struct stu) = 15.

GCC推薦對齊指令

使用_attribute_((packed))和_attribute_((aligned(n)))時,直接放在類型定義後面,那麼該類型就以指定的方式進行對齊。packed作用是取消對齊,aligned(n)表示對齊方式。接下來看個例子

#include <stdio.h>
//不使用內存對齊
struct mystruct11{
  int a;
  char b;
  short c;
}_attribute_((packed));

//使用內存對齊
/*
 aligned(n):當n(1、2、4)小於或等於4時,結構體struct mystruct22對齊字節是22(4、2、2、4)
            當n大於4時,結構體struct mystruct22的對齊字節:n
*/
struct mystruct22{
  int a;
  char b;
  short c;
  short d;
}_attribute_((aligned(1024))) My22;


offsetof宏與container_of宏

結構體變量訪問各個元素,本質上是通過指針方式來訪問的,形式上是句點“.”的方式來訪問(這時其實是編譯器幫我們自動計算了偏移量)。

offsetof宏的作用是計算結構中某個元素相對結構首地址的偏移量,實質是通過編譯器來幫用戶計算。原理是虛擬一個TYPE類型的結構體變量,然後TYPE.MEMBER的方式訪問MEMBER元素,繼而得到MEMBER相對於整個變量首地址的偏移量。

#include <stdio.h>

#define offsetof(TYPE,MEMBMER)  ((int)&((TYPE *)0)->MEMBMER)

struct mystruct{
  char a;
  int b;
  short c;
};

/*
TYPE是結構體類型,MEMBMER是結構體中一個元素的元素名
這個宏返回的是MEMBMER元素相對於整個結構體變量的首地址的偏移量,類型是int
*/

int main(void){
  struct mystruct s1;
  s1.b = 12;

  int *p = (int *)((char *)&s1 + 4);
  printf("*p = &d.\n", *p);    //根據結構體對齊計算得出的
  
  return 0;
}

運行結果

*p = 12.

(TYPE *)0是一個強制類型轉換,把0地址強制類型轉換成一個指針, 該指針指向一個TYPE類型的結構體變量,實際上這個結構體變量可能不存在,但是只要不去解引用這個指針就不會出錯

對於((TYPE)0)->MEMBMER來說,(TYEP *)0表示一個TYPE類型的結構體指針。通過指針來訪問這個結構體變量的MEMBMER元素&((TYPE *))->MEMBMER等效於&(((TYPE *)0)->MEMBMER)-&(((TYPE *)0)),這就得到成員的偏移量

container_of宏

/*
  ptr是指向結構體元素member的指針,type是結構體類型,member是結構體中一個元素名
  該宏返回是指向該結構體變量的指針,類型是(type *)
*/
#define container_of(ptr,type,member)({
const typedef ((type *)0->member)*__mptr = (ptr); \
(type *)((char* )__mptr - offsetof(type,member));
})

作用:知道一個結構體變量中某個成員的指針反推該結構體變量的指針。有了container_of宏,可以從一個成員的指針得到整個結構體變量的指針,繼而得到結構體中其他成員的指針

typeof關鍵字的作用:通過typeof(a)由變量a得到a的類型,所以typeof的作用是由變量得到變量的數據類型

宏的工作原理:先用typeof得到member成員類型將member成員的指針轉成自己類型的指針,然後用該指針減去該成員相對於整個結構體變量的偏移量偏移量用offset宏得到),之後得到整個結構體變量的首地址,在把該地址強制類型轉換爲type*

小結

  1. 基本要求:必須會用這兩個宏,知道他們接收什麼參數,返回什麼值,會用兩個宏來寫代碼,理解別人代碼中兩個宏的意思
  2. 升級要求:能理解這兩個宏的工作原理,能表述出來。
  3. 高級要求:能自己寫出這兩個宏

共用體(union)

共用體例子分析

#include <stdio.h>
struct mystruct{
  int a;
  char b;
};
/*
a和b其實指向同一塊內存空間,只是對這塊內存空間的兩種不同的解析方式。
若使用u1.a,那麼就按照int類型來解析這個內存空間;
若使用u1.b,那麼就按照char類型來解析這塊內存空間;
*/
union myunion{
  int a;
  char b;
};

int main(void){
  struct mystruct s1;
  s1.a = 23;
  printf("s1.b = %d.\n",s1.b);    //s1.b = 0;結論s1.a和s1.b是獨立無關的

  union myunion u1;               //共用體變量的定義
  u1.a = 24;
  printf("u1.b = %d.\n",u1.b); 
  u1.a = 'c';                      //共用體元素的使用
  printf("u1.b = %c.\n",u1.b);    //u1.b =23;結論是u1.b和u1.a是相關的,a和b地址一樣
                                 //充分說明a和b指向同一塊內存,只是對這塊內存解析規則有所不同
  u1.b = 'd';  
  printf("u1.b = %c.\n",u1.b); 
  return 0;
}

運行結果

s1.b = 0.
u1.b = 24.
u1.b = c.
u1.b = d.

共用體總結

共用體union和結構體struct類型聲明、變量定義和使用方法上很相似

共用體和結構體不同結構體類似於一個包裹,其中的成員彼此是獨立存在的,分佈在內存的不同單元中,它們只是被打包成了一個整體叫結構體;共用體中的各個成員其實是一體的,彼此不獨立,使用同一個內存單元。可理解爲:同一個內存空間有多種解釋方式(有時候爲這個元素,有時候是那個元素)。

union的sizeof測到大小實際是union中各個元素裏佔用內存最大的那個元素的大小

union中元素不存在內存對齊問題,因爲union中實際只有一個內存空間,都是從一個地址開始的開始地址就是整個union佔用的內存空間的首地址,所以不涉及內存對齊。

共用體和結構體區別

  1. 相同點:操作語法幾乎相同;
  2. 不同點:struct是多個獨立元素(內存空間)打包在一起;union是一個元素(內存空間)的多種解析方式

主要用途

  1. 在那種對同一單元進行多種不同規則解析的情況下;
  2. C語言中可用指針和強制類型轉換代替共用體完成同樣的功能,但共用體的方式更簡單、便捷、好理解。

大小端模式

大端模式(big endian)和小端模式(little endian),最早是在串口等通信中一次只能發送1個字節。當要發送一個int類型的數就遇到問題,int類型有4個字節,按照:byte0,byte1,byte2 byte3這樣順序發送,還是按照byte3 byte2 byte1 byte0順序發送?規則就是發送方和接受方必須按照同樣的字節順序來通信,否則就會出現錯誤,這就叫通信系統中的大小端模式

現在將大小端米模式更多指計算機存儲系統的大小端,在計算機內存/硬盤/Nand中,存儲系統是32位但數據仍是按照字節爲單位存放的。於是32位的二進制在內存中存儲時有兩種分佈方式高字節對應低地址(大端模式)、高字節對應高地址(小端模式)以大端模式存儲,其內存佈局如下所示:

 

 

大端模式

以小端存儲,其內存佈局如下:

小端模式

大端模式和小端模式本身沒有對錯、沒有優劣,理論上按照大端或小端都可以,但是要求存儲時和讀取模式是必須一致。實際應用中,大端如C51單片機,小端如ARM(大部分用小端模式,大端模式不算多)。寫代碼時,需要用代碼來檢測當前系統的大小端。如用C語言寫一個函數來測試當前機器的大小端模式

使用union來測試機器的大小端模式

#include <stdio.h>
/*
共用體中:a和b都是從u1的低地址開始的
假設u1所在的4字節地址分別是:0、1、2、3的話
那麼a自然就是0、1、2、3;b所在地址是0而不是3
*/
union myunion{
  int a;
  char b;
};

//若是小端模式則返回1,大端模式則返回0
int is_little_endian(void){
  union myunion u1;
  u1.a = 1;
  return u1.b;
}

int main(void){
  int i = is_little_endian();
  if(1 == i){
    printf("小端模式\n");
  }else{
    printf("大端模式\n");
  }

  return 0;
}

運行結果(x86-GCC-64)

小端模式

分析:

如果是以小端模式存儲,u1.a內存佈局如下所示:

則u1.b = 1;共用體中元素使用同一塊內存空間;

如果是以大端模式存儲,u1.a的內存佈局如下:

則u1.b等於0;爲什麼將只使用一個字節?b是char類型變量,佔一個字節。 

用指針方式來測試機器的大小端

#include <stdio.h>

int is_little_endian2(void){
  int a = 1;
  char b = *((char *)(&a));        //指針方式其實就是共用體的本質
  
  return b;
}

int main(void){
  int i = is_little_endian2();
   if(1 == i){
     printf("小端模式\n");   
  }else{
    printf("大端模式\n");
  }

  return 0;
}

分析:char b = *((char *)(&a))

 

如果a = 1是以小端模式,則b等於1;如果a = 1是以大端模式,則b等於在0;

信系統中的大小端(數組的大小端)

如果通過串口發送一個0x12345678給接收方,但是因爲串口本身限制,只能以字節爲單位來發送,所以需要發4次接受方分4次接收,內容分別是0x12、0x34、0x56、0x78。接收方接受發到這四個字節之後需要去重組得到0x12345678,而不是得到0x78563412。所以通信雙方需要有一個約定,先發/先接的是高位還是低位?這就是通信中的大小端問題,一般,先發低字節較小端;先發高字節叫大端。實際操作中,在通信協議裏會去定義大小端,明確告訴你先發的是低字節還是高字節。

在通信協議中大小端是非常重要的,不管是使用別人定義的通信協議而還是自己定義的通信協議,一定都要注意在通信協議中註明大小端問題

枚舉enum

枚舉的作用

枚舉在C語言中其實就是一些符號常量集,枚舉定義了一些符號,該符號的本質是Int類型的常量,每個符號和一個常量綁定。符號表示一個自定義的識別碼,編譯碼對枚舉的認知是符號常量所綁定的那個int類型的數字。

#include <stdio.h>
//這個枚舉用來表示函數返回值,ERROR表示錯,RIGHT表示對
enum return_value{
  ERROR,
  RIGHT,
};
enum return_value func1(void);

int main(void){
  enum return_value r = func1();
  if(r == RIGHT)        //不是r.RIGHT,也不是return_value.RIGHT
  {
     printf("函數執行正確\n");
  }
  else{
     printf("函數執行錯誤\n");
  }
  printf("ERROR = %d.\n",ERROR);    //ERROR = 0
  printf("RIGHT = %d.\n",RIGHT);    //RIGHT = 1
  
  return 0;
}

enum return_value_func1(void){
  enum return_value r1;
  r1 = ERROR;
  
  return r1;
} 

運行結果

函數執行錯誤
ERROR = 0.
RIGHT = 1.

對於枚舉符號變量來講,數字不重要,符號才重要符號對應的數字只要彼此不相同即可,沒有別的要求。所以一般情況下我們都不會明確指定這個符號所對應的數字,而是讓編譯器自動分配編譯器自動分配原則是,從0開始依次增加,如果用戶定義一個值,則從定義的那個值開始往後一次增加

宏定義和枚舉區別

枚舉是將很多有關聯的符號封裝在一個枚舉中,而宏定義是完成分散的。什麼時候用枚舉?當要定義的常量是一個有限集合時(如一個星期有7天,一個月有31天,一年有12個月等)最適合枚舉。不能用枚舉的情況下(定義常量符號之間無關聯或者無限的)用宏定義。

宏定義最先出現,用來解決符號常量的問題,後來發現有時候定義的符號常量彼此之間有關聯(多選一的關係),可以用宏定義,但不貼切,於是發現了枚舉來解決該情況。

枚舉定義

//1 分別定義類型和變量
enum week{
  SUN,        //SUN = 0
  MON,        //MON = 1
  TUE,
  WEN,
  THU,
  FRI,
  SAT
};
  enum week today;

//2 定義類型的同時定義變量
enum week{
  SUN,        //SUN = 0
  MON,        //MON = 1
  TUE,
  WEN,
  THU,
  FRI,
  SAT,
};
  enum week today1;
// 3 定義類型的同時定義變量
enum {
  SUN,        //SUN = 0
  MON,        //MON = 1
  TUE,
  WEN,
  THU,
  FRI,
  SAT
};

//4 用typedef定義枚舉類型別名,並在後面使用別名進行遍歷定義
typedef enum week{
  SUN,        //SUN = 0
  MON,        //MON = 1
  TUE,
  WEN,
  THU,
  FRI,
  SAT
};
  week today3;
// 5 用typedef定義枚舉的別名
typedef enum{
  SUN,        //SUN = 0
  MON,        //MON = 1
  TUE,
  WEN,
  THU,
  FRI,
  SAT

}week;

不能有重名的枚舉類型

即在一個文件中不能有兩個或兩個以上的enum被typedef成相同的別名。因爲將兩種不同類型重名爲相同的別名,這會讓gcc在還原別名時遇到困惑。如下定義:

typedef int INT; typedef char INT;

那麼INT到底被譯爲int還是char,就無法確定。

不能有重名的枚舉成員

兩個struct類型內的成員可以重名,而兩個enum類型中成員卻不可以重名。因爲struct類型成員的訪問方式爲“變量名.成員”,而enum成員的訪問方式爲“成員名”,因此若兩個enum類型中有重名的成員,那代碼中訪問這個成員時到底訪問的是enum中的哪個成員呢,無法確定。但是兩個#define宏定義是可以重名的,該宏名真正的值取決於最後一次定義的值。編譯器會給出警告單不會出現error

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章