OverView of Content
如有引用參考請詳註出處,感謝
本文將探討結構體(struct)在C語言中的重要性與用法。首先,將介紹結構體的概念,包括如何定義、存取以及同時宣告和定義變數的方法。我也會討論如何使用 define
和 typedef
來簡化結構體的宣告。 接著,我們將探討結構體的初始化方法,包括順序初始化和指定/複合初始化。透過學習這些初始化方法,讀者將能夠更好地理解如何初始化複雜的結構體變數。
然後,我們將討論結構體與函數之間的關係,包括函數如何傳回結構體、以及如何使用變數和指標來傳遞結構體參數。 接著,我們將關注實現結構體時需要注意的細節,例如 自動對齊、手動對齊(透過 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 全結構才可進行宣告變數的動作,以下有兩種方法 #define
、typedef
可以簡化該行為
●
#define
、typedef
兩者是有差別的,#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;
}
如果覺得分開宣告很麻煩的話,也可以將 struct
、typedef
兩個關鍵字合併使用,範例如下
#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 之後 才能指定參數初始化,像是 C90
、C85
等等都不可指定初始化
● 結構指定初始化:
#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;
}
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 語言與系統開發
● C 語言與系統開發:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用