1. 程式人生 > >C語言自定義型別:結構體、列舉、聯合

C語言自定義型別:結構體、列舉、聯合

結構體

結構是一些值的集合,這些值成為成員變數。結構的每個成員可以是不同型別的變數。

結構體的宣告
struct tag //結構體型別名
{
    member-list; //成員列表
}variable-list; //變數列表

例如描述一個學生:

struct Stu
{
   char name[20];
   int age;
   char sex[5];
   char id[20];
}; //注意分號不能丟
結構體的自引用

結構體中可以包含該結構體本身嗎?答案是可以的。
正確的自引用方式:

struct Node
{
   int data;
   struct Node* next;
};
結構體變數的定義和初始化

定義變數:

struct Point
{
   int x;
   int y;
}p1;  //宣告型別的同時定義變數p1
struct Point p2; //定義結構體變數p2 

初始化:

//定義變數的同時賦初值
struct Point p3 = {x,y};

struct Stu //型別宣告
{
   char name[15]; //名字
   int age; //年齡
}; struct Stu s = {"zhangsan",20}; //初始化 struct Node { int data; struct Point p; struct Node *next; }n1={10,{4,5},NULL}; //結構體巢狀初始化 struct Node n2 = {20,{5,6},NULL}; //結構體巢狀初始化
結構體成員的訪問

結構體的成員可以是標量、陣列、指標,甚至是其他結構體。
結構體變數訪問成員是通過點操作符訪問的,如果結構體變數是指標變數,則通過 ” -> ” 操作符來訪問。例如:
這裡寫圖片描述
可以看到s有成員name和age,訪問成員:

//點操作符訪問成員
struct Stu s;
strcpy(s.name,"zhangsan"); //使用.訪問name成員
s.age = 20; //使用.訪問age成員

//->操作符訪問成員
struct Stu *p;
strcpy(p->name,"lisi"); //使用->訪問name成員
p->age = 20; //使用->訪問age成員
結構體記憶體對齊

我們先來看幾道練習題:

//練習1
struct s1
{
   char c1;
   int i;
   char c2;
};
printf("%d\n",sizeof(struct s1));

程式的輸出結果是12。
分析一下:
這裡寫圖片描述

//練習2
struct s2
{
   double d;//對齊數為8
   char c;//對齊數為1
   int i;//對齊數為4
};
printf("%d\n",sizeof(struct s2));

程式的輸出結果為16。分析過程同上。

//練習3-結構體巢狀問題
struct s3
{
   char c1;//對齊數為1
   struct s2;//對齊數為8 因為s2中最大對齊數是8
   double d;//對齊數是8
};
printf("%d\n",sizeof(struct s3));

程式的輸出結果為32。分析過程同上。
結構體對齊原因

  • 平臺原因:不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。
  • 效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要做兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。

結構體記憶體對齊規則

  • 第一個成員在與結構體變數偏移量為0的地址處。
  • 其他成員變數要對齊到某個數字(對齊數)的整數倍的地址處。
    對齊數 = 編譯器預設的一個對齊數與該成員大小的較小值(VS中為8,Linux中為4)

  • 結構體總大小為最大對齊數的的整數倍。

  • 如果嵌套了結構體,巢狀的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含巢狀結構體的對齊數)的整數倍。
修改預設對齊數

我們可以通過#pragma這個預處理指令,來改變我們的預設對齊數。
如:

#include<stdio.h>
#pragma pack(8) //設定預設對齊數為8
struct S1
{
   char c1;
   int i;
   char c2;
};
#pragma pack() //取消設定的對齊數,還原為預設

結構體在對齊方式不合適的時候,我們可以自己更改預設對齊數。

結構體傳參

有如下程式碼:

#include <stdio.h>
struct S
{
   int data[1000];
   int num;
};
struct S s = {{1,2,3,4},1000};
//結構體傳參
void print1(struct S s)
{
   printf("%d\n",s.num);
}
//結構體地址傳參
void print2(struct S* ps)
{
   printf("%d\n",ps->num);
}
int main()
{
   print1(s);//傳結構體
   print2(&s);//傳地址
   return 0;
}

在上述的程式碼中,一個是傳結構體,一個是傳結構體地址,使用時應首選print2函式。
原因:函式傳參的時候,引數是需要壓棧的。如果傳遞一個結構體物件的時候,結構體過大,引數壓棧的系統開銷比較大,所以會導致效能的下降。
結構體在傳參的時候,要傳結構體的地址。

位段

位段的宣告和結構體是類似的,有兩個不同:
1、位段的成員必須是int、unsigned int或signed int。
2、位段的成員名後邊有一個冒號和一個數字。
如:

struct A
{
   int _a:2; //給_a分配2位
   int _b:5; //給_b分配5位
   int _c:10; //給_c分配10位
   int _d:30; //給_d分配30位
};

A就是一個位段型別。那麼位段A的大小是多少呢 ?答案是8位元組。
分析一下:
這裡寫圖片描述
同結構體相比,位段可以達到同樣的效果,可以很好地節省空間,但是有跨平臺的問題存在。
位段的跨平臺問題

  1. int位段被當成有符號數還是無符號數是不確定的。
  2. 位段中最大位的數目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16位機器上會出現問題)
  3. 位段中的成員在記憶體中是從左向右分配,還是從右向左分配,標準尚未定義。
  4. 當一個結構包含兩個位段,第二個位段成員比較大, 無法容納於第一個位段剩餘的位時,是捨棄剩餘的位還是利用,這是不確定的。

列舉

列舉顧名思義就是一一列舉,把可能的取值一一列舉。比如我們現實生活中:

一週的星期一到星期日是有限的7天,可以一一列舉。
性別有:男、女、保密,也可以一一列舉。
月份有12個月,也可以一一列舉。
列舉型別的定義
如:

enum Day //星期
{
   Mon,
   Tues,
   Wed,
   Thur,
   Fri,
   Sat,
   Sun
};

enum Sex //性別
{
   MALE,
   FAMALE,
   SECRET
};

以上定義的都是列舉型別,{}中的內容是列舉型別的可能取值,也叫列舉常量。這些可能取值都是有值得,預設從0開始一次遞增1,當然在定義的時候也可以賦初值。如:

enum Sex //性別
{
   MALE=1,
   FAMALE=2,
   SECRET=4
};

列舉的優點

  • 增加程式碼的可讀性和可維護性
  • 和#define定義的識別符號比較列舉有型別檢查,更加嚴謹
  • 防止了命名汙染
  • 便與除錯
  • 使用方便,一次可以定義多個常量

可以利用列舉型別實現一個狀態機來完成C註釋到C++註釋的轉換。


聯合(共用體)

聯合也是一種特殊的自定義型別,這種型別定義的變數也包含一系列的成員,特徵是這些成員共用同一塊空間(所以聯合也叫共用體)。
如:

//聯合型別的宣告
union Un
{
   char c;
   int i;
};
//聯合型別的定義
union Un un;
//計算聯合的大小
printf("%d\n",sizeof(union Un));

程式的輸出結果為4。分析一下:
這裡寫圖片描述
聯合大小的計算

  • 聯合的大小至少是最大成員的大小。
  • 當最大成員大小不是最大對齊數的整數倍的時候,就要對齊到最大對齊數的整數倍。
    例如:
union Un1
{
  char c[5]; //對齊數為1,大小為5
  int i; //對齊數為4,大小為4
};
//Un1的大小為8,因為最大成員大小為5,不是最大對齊數的整數倍,要對齊,所以為8

union Un2
{
   short c[7]; //對齊數為2,大小為14
   int i; //對齊數為4,大小為4
};
//Un2的大小為16,因為最大成員大小為14,不是最大對齊數的整數倍,要對齊,所以為16
大小端問題

判斷當前計算機的大小端儲存。
大端:大端位元組序儲存,是指資料的低位儲存在記憶體的高地址中,資料的高位儲存在記憶體的低地址中。
小端:小端位元組序儲存,是指資料的低位儲存在記憶體的低地址中,資料的高位儲存在記憶體的高地址中。
根據聯合的優點,可寫出程式碼如下:

#include <stdio.h>

int check_sys()
{
    union Un
    {
        char c;
        int i;
    }un;
    un.i = 1;
    return un.c;
}

int main()
{
    int ret = 0;
    ret = check_sys();
    if(ret == 1)
        printf("小端\n");
    else
        printf("大端\n");
    return 0;
}
聯合和結構體巧妙使用

將long型別的IP地址,轉換為點分十進位制的表示形式。
程式碼如下:

#include <stdio.h>

union ip_addr
{
    unsigned long addr;
    struct 
    {
        unsigned char c1;
        unsigned char c2;
        unsigned char c3;
        unsigned char c4;
    }ip;
};

int main()
{
    union ip_addr my_ip;
    my_ip.addr = 19216801;
    printf("%d.%d.%d.%d\n",my_ip.ip.c1,my_ip.ip.c2,my_ip.ip.c3,my_ip.ip.c4);
    return 0;
}

以上內容均為學習過程中的總結,如有不足之處,請指正。