C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum

C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum

OverView of Content

如有引用參考請詳註出處,感謝

本文將探討結構體(struct)在C語言中的重要性與用法。首先,將介紹結構體的概念,包括如何定義、存取以及同時宣告和定義變數的方法。我也會討論如何使用 definetypedef 來簡化結構體的宣告。 接著,我們將探討結構體的初始化方法,包括順序初始化和指定/複合初始化。透過學習這些初始化方法,讀者將能夠更好地理解如何初始化複雜的結構體變數。

然後,我們將討論結構體與函數之間的關係,包括函數如何傳回結構體、以及如何使用變數和指標來傳遞結構體參數。 接著,我們將關注實現結構體時需要注意的細節,例如 自動對齊、手動對齊(透過 pragma pack__attribute__)、以及在資料結構中如何使用結構體。

此外,還將介紹聯合體(Union)和枚舉(Enum)的概念和用法,包括它們的使用、特性,以及在程式設計中如何判斷大小端和使用枚舉類型。透過深入學習這些內容,讀者將能夠更好地理解和應用C語言中的結構體、聯合體和枚舉類型。


struct 結構概念

整合變數的一個關鍵字 struct,用於變數、引數都十分的方便

結構主要的目的是在 整合參數,以往在全域需告變數時,必須分開去操控,難以記憶其關鍵字,而使用 struct 就可以整合所有相關的變數

A. 方便於管理

B. 方便使用

struct 結構 - 定義

● 通常來說我們會把有相關的變數使用結構一併管理,使用 struct 關鍵字 + 大括號 {}

結構的 型態定義 並不佔有記憶體空間


// 分開宣告
char name[];
int age;
long id;

// 使用 struct 整合相關變數
struct Info_T {
    char *name;
    int age;
    long id;
};

struct 結構 - 訪問

struct 結構的訪問方式有兩種

A. 1. 結構的變數使用符號 . 來訪問成員;2. 結構的指標 (pointer)則使用 -> 訪問


void visit_struct() {
    // 結構的變數
    struct Info_T info;

    info.name = "Sean";
    info.age = 28;
    info.id = 123132;

    printf("name: %s\n", info.name);
    printf("age: %d\n", info.age);
    printf("id: %d\n\n", info.id);

    // 結構的指標
    struct Info_T *pinfo = &info;

    printf("Ptr name: %s\n", pinfo->name);
    printf("Ptr age: %d\n", pinfo->age);
    printf("Ptr id: %d\n", pinfo->id);
}

B. 手動使用偏移量計算,並調整指標的位置來訪問


void visit_struct_by_ptr() {
    struct Info_T info;

    info.name = "Sean";
    info.age = 28;
    info.id = 123132;

    printf("&name: %p\n", &info.name);
    printf("&age: %p\n", &info.age);
    printf("&id: %p\n\n", &info.id);

    // 轉換為結構的第一個地址
    char *p = (char *) &info;

    *p = "Alien";
    *(p + sizeof(char*)) = 10;    // 加上 char* 指針大小的位移(篇一到 age 變數)
    
    // 加上 char* 跟 int 大小的位移(偏移到 id 變數)
    // 並同時將指標強制轉型為 int*  ,才能賦予 int 類型的數值
    *((int*) (p + sizeof(char*) + sizeof(int))) = 998877;

    printf("p: %p, name: %s\n", p, info.name);
    printf("p: %p, age: %d\n", (p + sizeof(char*)), info.age);
    printf("p: %p, id: %ld\n\n", (p + sizeof(char*) + sizeof(int)), info.id);
}

struct 結構 - 宣告、定義

宣告出來後就在記憶體內佔有空間

● 在上方我們宣告了 Info_T 這個結構,在呼叫時就要使用全名呼叫(也就是要包含 struct 關鍵字)

● 完整的型態為 struct Info_T,而要使用該型態時就必須全型態名


// 型態宣告
struct Info_T {
    char *name;
    int age;
    long id;
};

// 變數定義
struct Info_T info1;

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

struct Info_T {
    char *name;
    int age;
    long id;
};

// 接收傳值的 Function
void printInfo(struct Info_T info) {
    printf("name: %s\n", info.name);
    printf("age: %i\n", info.age);
    printf("id: %li\n\n", info.id);
}

int main(void) {
    // 定義兩個結構變量
    struct Info_T info1, info2;

    // 使用 '.' 指定定義結構內容
    info1.name = "Alien";
    info1.age = 20;
    info1.id = 9627;
    // 傳值呼叫 Function
    printInfo(info1);

    info2.name = "Pan";
    info2.age = 17;
    info2.id = 33;
    printInfo(info2);

    return EXIT_SUCCESS;
}

--實作結果--

struct 結構宣告 - 同時定義變數

● 可以在定義的同時一起宣告出結構變數;變數方至於 大括號 {} 之後,並可宣告多個變數


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

struct Info_T {
    char *name;
    int age;
    long id;
} Default_1, Default_2;    // 定義兩個變數

void printInfo(struct Info_T info) {
    printf("name: %s\n", info.name);
    printf("age: %i\n", info.age);
    printf("id: %li\n\n", info.id);
}

int main(void) {

    Default_1.name = "Hello";
    Default_1.age = 12;
    Default_1.id = 123;
    printInfo(Default_1);

    Default_2.name = "World";
    Default_2.age = 21;
    Default_2.id = 321;
    printInfo(Default_2);

    return EXIT_SUCCESS;
}

--實作結果--

struct 簡化宣告 - define、typedef

● 以往必須使用 struct 全結構才可進行宣告變數的動作,以下有兩種方法 #definetypedef 可以簡化該行為

#definetypedef 兩者是有差別的,#define 是在預編譯期間做替換,而 typedef 則是在編譯期間定義出新類型

A. 使用宏定義關鍵自 #define:其格式為 #define <新名稱> <struct 原先名稱>

● 如果使用分開定義那要特別注意,C 是順序式編程(命令式設計注重程式的順序),要把簡化定義寫在下方


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

// #define Info struct Info_T  不可寫在上方,因為還未定義

struct Info_T{
    char *name;
    int age;
    long id;
};

// 簡化定義寫在下方
// 將結構 `struct Info_T` 定義為 `Info`
#define Info struct Info_T

// 引數使用 Info 簡化
void printInfo(Info info) {
    printf("name: %s\n", info.name);
    printf("age: %i\n", info.age);
    printf("id: %li\n\n", info.id);
}

int main(void) {

    // 宣告使用 Info 簡化
    Info info;
    info.name = "Alien";
    info.age = 21;
    info.id = 9528;
    printInfo(info);

    return EXIT_SUCCESS;
}

B. 使用類型關鍵字 typedef 定義:其格式與 #define 相反,格式如 #define <struct 原先名稱> <新名稱> (記得使用分號 ; 結尾)


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

struct Info_T{
    char *name;
    int age;
    long id;
};

// 定義新類型
typedef struct Info_T Info;

// 引數使用 Info 簡化
void printInfo(Info info) {
    printf("name: %s\n", info.name);
    printf("age: %i\n", info.age);
    printf("id: %li\n\n", info.id);
}

int main(void) {

    // 宣告使用 Info 簡化
    Info info;
    info.name = "Alien";
    info.age = 21;
    info.id = 9528;
    printInfo(info);

    return EXIT_SUCCESS;
}

如果覺得分開宣告很麻煩的話,也可以將 structtypedef 兩個關鍵字合併使用,範例如下


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

// struct`、`typedef` 兩個關鍵字合併使用
// 定義出一個「新類型 Info」,並且該類型為 struct
typedef struct Info_T {
    char *name;
    int age;
    long id;
} Info;

// 引數使用 Info 簡化
void printInfo(Info info) {
    printf("name: %s\n", info.name);
    printf("age: %i\n", info.age);
    printf("id: %li\n\n", info.id);
}

int main(void) {

    // 宣告使用 Info 簡化
    Info info;
    info.name = "Alien";
    info.age = 21;
    info.id = 9528;
    printInfo(info);

    return EXIT_SUCCESS;
}

struct 結構 - 初始化

struct 結構初始化有分為多種方式,接下來要介紹的就是不同的結構初始化的方案(以下是針對靜態初始化的結構做介紹)

結構順序初始化 - 預設初始化

● 預設初始化是 手動 按照順序 對其結構內的變數進行初始化


#include <stdio.h>

struct Info {
  int age;
  int height;
  int weight;
};

void printInfo(struct Info *i) {
    printf("age: %d\n", i->age);
    printf("height: %d\n", i->height);
    printf("weight: %d\n", i->weight);
}

int main()
{
    // 順序初始化
    struct Info myInfo_1 = {
        25, 170, 65    
    };

    printInfo(&myInfo_1);

    printf("Hello World");

    return 0;
}

--實作--

結構 - 指定、複合初始化

● 使用 指定、複合文字初始化時 時要注意,當沒有被指定到的值會初始化為 0

指定初始化有 C 版本的限制C99 之後 才能指定參數初始化,像是 C90C85 等等都不可指定初始化

結構指定初始化


#include <stdio.h>

struct Info {
  int age;
  int height;
  int weight;
};

void printInfo(struct Info *i) {
    printf("age: %d\n", i->age);
    printf("height: %d\n", i->height);
    printf("weight: %d\n", i->weight);
}

int main()
{
    struct Info myInfo_1 = {
    // 不按照順序初始化,也可以選定初始化
        .weight = 65,
        .height = 170 
    };

    printInfo(&myInfo_1);

    return 0;
}

--實作--

結構複合初始化


#include <stdio.h>

struct Info {
  int age;
  int height;
  int weight;
};

void printInfo(struct Info *i) {
    printf("age: %d\n", i->age);
    printf("height: %d\n", i->height);
    printf("weight: %d\n", i->weight);
}

int main()
{
    struct Info myInfo_1 = {17, 168, 55};

    //"1. "
    myInfo_1.age = 20;
    myInfo_1.height = 168;
    myInfo_1.weight = 58;

    //"2. "    結構複合初始化
    myInfo_1 = (struct Info) {.weight = 60, .age = 20, .height = 168};
    printInfo(&myInfo_1);

    //"3. "    結構複合初始化
    myInfo_1 = (struct Info) {.weight = 60, .height = 168};
    printInfo(&myInfo_1);

    return 0;
}

A. 一般在指定結構的值實必須分別指定

B. 可以透過複合文字一次指定全部,不必按照順序,可特別指定

C. 該範例使用 複合文字初始化 但沒有指定到 age,該 age 即初始化為 0


struct 結構 & 函數

使用結構型態可以簡化函數的引數數量


// 舊方法
void printInfo_1(char* str, int age, int id) {
    //... TODO
}

// 使用 struct 類型
void printInfo_2(struct Info_T info) {
    //... TODO
}

函數的原型宣告

函數的原型宣告中的引數(參數)可以省去


// 原型宣告 - 保持引數
void printInfo_2(struct Info_T info);

// 原型宣告 - 省去引數
void printInfo_3(struct Info_T);

函數回傳結構 - 變數、指標

● 函數的返回值假設要回傳結構,可以有兩種方案:1. 用結構的型態回傳整組變數,也可以 2. 回傳結構的指標


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

typedef struct Info_T{
    char *name;
    int age;
    long id;
}Info;

// 引數使用 Info 簡化
Info FixInfo(Info info) {

    info.id = 9123;
    info.age += 1;

    return info;
}

void printInfo(struct Info_T info) {
    printf("name: %s\n", info.name);
    printf("age: %i\n", info.age);
    printf("id: %li\n\n", info.id);
}

int main(void) {

    // 宣告使用 Info 簡化
    Info info;
    info.name = "Alien";
    info.age = 21;
    info.id = 9528;
    info = FixInfo(info);
    printInfo(info);

    return EXIT_SUCCESS;
}


struct 實現注意 - 對齊

在電腦中 struct 在記憶體中的排列方式,以及佔用空間 對於我們了解計算機底層十分重要,以下章節將說明「資料對齊」跟「結構」的關係

自動對齊

● 在 struct 中宣告其變數,在記憶體中的排版式固定的 (這取決於編譯器),這個概念由如 JVM 對象創建時設定的對象頭,其中的 Padding 對齊 (方便於總線讀取數據)

對齊訪問的特點

對齊訪問是「犧牲空間來換取效率」,而非對齊則相反,效率低不過空間消耗也相對低

對齊的數

這會有關到硬體的數據總線的多寡;如果是 32 條,就是一次讀取 32 bit 的數據(對齊 32),64 則知一次 64 bit 數據(對齊 64)


#include <stdio.h>

struct {
    float a;
    int b;
    char c;
    float d;
} Test;

int main()
{
    //"1. "
    printf("float size %d byte\n", sizeof Test.a);
    printf("int size %d byte\n", sizeof Test.b);
    printf("char size %d byte\n", sizeof Test.c);
    printf("float size %d byte\n", sizeof Test.d);

    //"2. "
    printf("a address: %p\n", &Test.a);
    printf("b address: %p\n", &Test.b);
    printf("c address: %p\n", &Test.c);
    printf("d address: %p\n", &Test.d);

    return 0;
}

A. 用 sizeof 算出的大小雖然是正確,char 是 1 Byte

B. 取 Addr 出來看 char 卻占了 4 個 byte(這就是編譯器幫我們自動對齊的展現)

--實做--

從實際抓取到的參數值,它的地址是以 4 為倍數 編排

手動對齊 - pragma pack

如果不是特殊需求建議少用(pragma pack

● 在前面我們可以看到編譯器自動幫我們實現的對齊;有自動對齊當然就有 手動對齊透過 #pragma pack([對齊 Byte 數量]) 關鍵字設定對齊

● 定義的格式:#pragma pack() 開頭、#pragma pack() 結尾

A. 一字節 (1 Byte) 對齊 範例如下


#pragma pack(1)
struct Class {
    int BookCount;
    short chairCount;
    char neckName[11];
};

void align_by_pragma_pack() {
    printf("Class struct size: %d\n", sizeof(struct Class));
}
#pragma pack()    // 注意需要結尾

從結果來看,它可以適時減少空間的消耗

B. 兩字節 (2 Byte) 對齊 範例如下


#pragma pack(2)    // 修改為 2
struct Class {
    int BookCount;
    short chairCount;
    char neckName[11];
};

void align_by_pragma_pack() {
    printf("Class struct size: %d\n", sizeof(struct Class));
}
#pragma pack()    // 注意需要結尾

C. 兩字節 (4 Byte) 對齊


#pragma pack(4)    // 修改為 4
struct Class {
    int BookCount;
    short chairCount;
    char neckName[11];
};

void align_by_pragma_pack() {
    printf("Class struct size: %d\n", sizeof(struct Class));
}
#pragma pack()    // 注意需要結尾

GCC 手動對齊 - __attribute__

● GCC 手動對齊的方式有兩種:使用時直接放置在結構(struct)的後方即可

__attribute__((packed))其中 packed 是代表取消對齊

__attribute__((aligned(n)))aligned(n) 代表元素要對齊的大小

A. 使用 __attribute__ 取消編譯器的對齊


#include <stdio.h>

struct MyClass {
    int BookCount;
    short chairCount;
    char neckName[11];
}__attribute__((packed));

void packed_by_gcc() {
    printf("packed struct size: %d\n", sizeof(struct MyClass));
}

B. 1024 對齊:這裡我們看兩個情況

● 結構大小 不超過 1024:表示編譯時元素須在 1024 內對齊


#include <stdio.h>

typedef struct {
    int BookCount;
    short chairCount;
    char neckName[11];
}__attribute__((aligned(1024))) MyClass_2;

void aligned_by_gcc() {
    printf("packed struct size: %d\n", sizeof(MyClass_2));
}

● 結構大小 超過 1024:超過 1024 則新增一個對齊大小


#include <stdio.h>

typedef struct {
    int BookCount;
    short chairCount;
    char neckName[1025];        // 故意超出 1024 的大小
}__attribute__((aligned(1024))) MyClass_2;

void aligned_by_gcc() {
    printf("packed struct size: %d\n", sizeof(MyClass_2));
}


struct 數據結構應用

struct & Array

結構陣列:可以定義、初始化多個結構形成的陣列,範例如下


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

typedef struct Info_T{
    char *name;
    int age;
    long id;
} Info;

void printInfo(struct Info_T info) {
    printf("name: %s\n", info.name);
    printf("age: %i\n", info.age);
    printf("id: %li\n\n", info.id);
}

int main(void) {

    // 定義結構陣列,並透過順序初始化!
    Info infos[3] = {
            {"Alien", 21, 1111},
            {"Pan", 13, 2222},
            {"Kyle", 15, 3333},
    };

    printInfo(infos[0]);
    printInfo(infos[1]);
    printInfo(infos[2]);

    return EXIT_SUCCESS;
}

reference link

struct & Linked

● 可以使用 struct 來製作一個串列 Linked,這樣就可以達到一種 數據結構的 LinkedList,以下使用指標來指向下一個位子,範例如下


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

typedef struct Info_T {
    char name[5];
    int age;
    long id;

    // 下一個
    struct Info_T *next;
}Info;

// Reference 引數
void printInfo(struct Info_T* info) {
    printf("name: %s\n", info->name);
    printf("age: %i\n", info->age);
    printf("id: %li\n\n", info->id);
}

int main(void) {

    Info info1 = {"Alien", 18, 111, NULL};
    Info info2 = {"Pan", 20, 222, NULL};
    Info info3 = {"Kyle", 12, 333, NULL};

    info1.next = &info2;
    info2.next = &info3;

    Info* temp = &info1;

    while(temp != NULL) {
        printInfo(temp);

        // 使用指標取值必須使用 '->' 符號(優先級問題),不然就不需使用 (*temp)
        temp = temp->next;	
    }

    return EXIT_SUCCESS;
}

--實作--


Union 聯合體

中文又稱為 聯合體共用體;簡單來說就是 Union 內部成員是共用空間

Union 使用、特性

● Union 所有成員 共享空間


union MyUnion_T {
    int a;
    short b;
    char c;
};

void base_use_union() {
    union MyUnion_T u;

    u.a = 0x0F000F0F;

    printf("a value: %d\n", u.a);    // 還在 int 範圍內,全部顯示
    printf("b value: %d\n", u.b);    // 超過 short 範圍,顯示 0x0F0F 的數值
    printf("c value: %d\n", u.c);    // 超過 char 範圍,顯示 0x0F 的數值
}

不存在對齊問題,因為 Union 就是一個空間,都是從同一個地址開始


union MyUnion_T {
    int a;
    short b;
    char c;
};

void union_addr() {
    union MyUnion_T u;

    printf("a addr: %p\n", &u.a);    
    printf("b addr: %p\n", &u.b);
    printf("c addr: %p\n", &u.c);
}

所有成員都是相同的起始地址

Union 其實功能跟使用指標強制轉型一樣,透過強制轉型來讓它使用不同的格式分析指標;接下來 使用指標的轉型來展現 Union 的功能


void ptr_replace_union() {
    union MyUnion_T u;

    u.a = 0x0F000F01;

    char *p = (char*) &u;
    printf("u addr: %p, p addr: %p\n", &u, p);

    printf("ptr a value: %d\n", *((int*) p));
    printf("ptr b value: %d\n", *((short*) p));
    printf("ptr c value: %d\n", *((char*) p));

}

大端小端概念

大、小端這原本是出現在小說中的說詞,後來才用到電腦中表達序列化規則

● 在串列序列化時會碰到傳送數據的順序問題,回到最基礎的傳送 int 類型,假設傳送一個 int 類型的數據是 4 Byte,那就分為兩種情況 (請看下圖

A. 假設有 Byte 0 ~ 3,「高 Byte」對「高地址」,就稱為 小端(對電腦讀取友善)

B. 假設有 Byte 3 ~ 0,「高 Byte」對「低地址」,就稱為 大端(對人類讀取友善)

● 以下是 int i = 0xF0,也就是 0x000000F0 在大小端中的分布分部是以 Byte 為單位,低位元為 0xF0,高位元為 0x00

小端的 Byte 分佈

大端的 Byte 分佈

Union 判斷大小端

使用 Union 就可以測定大小端

● 數據從記憶體低位置開始存,存的順序才由大小端決定


union Endian_t {
    int iVal;
    char cVal;
};

int is_little_endian(void) {
    int val = 0x00000001;	// 0x00 is height, 0x01 is low

    union Endian_t t;
    t.iVal = val;

    if(t.cVal == 0x01) {
        // little_endian
        return 1;
    }

    return 0;
}

低地址存低位元就可以讀出數據

● 同樣可以使用指標判定大小端


int is_little_endian_ptr(void) {
    int val = 0x00000001;	// 0x00 is height, 0x01 is low

    char* p = (char*) &val;


    if(*p == 0x01) {
        // little_endian
        return 1;
    }

    return 0;
}

低地址存高位元就則無法讀出數據


Enum 枚舉

Enum 枚舉,定義一個符號,並且該符號與 常量綁定

Enum 使用

A. Enum 數值當常量基礎使用:enum 可指定其數值(可正可負);如果在中途切換為別的變數,也會從那個變數開始加(自動會往後加)


#include <stdio.h>

enum Test_T {
    A = 0,
    B,
    C,
    D = 1,
    E,
    F,
    G = -1,
    H
};
void base_enum() {
    printf("A: %i\n", A);
    printf("B: %i\n", B);
    printf("C: %i\n", C);
    printf("D: %i\n", D);
    printf("E: %i\n", E);
    printf("F: %i\n", F);
    printf("G: %i\n", G);
    printf("H: %i\n", H);
}

B. 分開定義類型、變量


enum week {
    SUN,
    MON,
    TUE,
    WEN,
    THU,
    FRI,
    SAT
};

void base_use_2() {
    enum week w;
}

C. 使用 typedef 定義新類型


typedef enum {
    SUN_2,
    MON_2,
    TUE_2,
    WEN_2,
    THU_2,
    FRI_2,
    SAT_2
} week_t;

void base_use_3() {
    week_t w;
}

● 由於 Enum 是全局常量,所以一個文件中不可以重複定義


更多的 C 語言相關文章

關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言

編譯器、系統開念

編譯器、系統開念:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節

C 語言與系統開發

C 語言與系統開發:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響