程序員C語言快速上手——高級篇(九)

高級篇

結構體

背景

結構體是一種聚合數據類型,C語言的數組也是一種聚合數據類型,它們顯著的區別是,數組是相同數據類型的集合,而結構體可以是不同數據類型的集合。

假如要表示一個學生,那麼我們可能需要聲明多個變量

// 姓名
char *name;
// 年齡
int age;
// 編號
char *number;
// 年級
char *grade;

這在實際操作中非常麻煩,我們需要一種新的數據類型,將這些信息存放在一起,而不是這樣分散的去表示和操作。數組顯然是無法滿足這個需求的,因爲數組只能存放相同的數據類型,一個學生的信息,可能需要多種數據類型來表示,比如考試成績,這個就需要float類型來表示。

結構體的聲明與使用

爲了實現類似上述的這種需求,結構體就誕生了

// 聲明一個結構體
struct student {
    char *name;
    int age;
    char *number;
    char *grade;
};

結構體聲明的一般格式

struct 標籤名 {
	成員變量1
	成員變量2
    ……
};

結構體被聲明之後,就相當於產生了一個新的數據類型,我們可以如下使用:

#include <stdio.h>
// 聲明一個結構體
struct student
{
    char *name;
    int age;
    char *number;
    char *grade;
};

int main(){
    // 聲明結構體變量:stu
    struct student stu;
    // 爲結構體中的成員賦值
    stu.name = "zhangsan";
    stu.age = 19;
    stu.number = "A010";
    stu.grade = "18級"; 
    // 訪問結構體中各個成員變量的內容
    printf("學生信息:%s,%d,%s,%s\n",stu.name,stu.age,stu.number,stu.grade);
    return 0;
}

打印結果:

學生信息:zhangsan,19,A010,18級

有幾個點需要注意:

  1. 使用關鍵字struct + 標籤名 + 一對花括號 + 分號 來聲明結構體。在花括號中,聲明結構體需要包含的變量,這些變量被稱爲結構體的成員變量,或成員字段。一定要注意,結尾的分號不能掉
  2. 在使用時,需將struct + 標籤名合起來看做一種新的類型,然後使用我們熟知的數據類型 + 變量名的格式來聲明一個結構體變量。例如struct student stu;,這裏struct student是一種新類型,stu則是變量名。這裏一定要注意,聲明結構體和聲明結構體變量完全是兩回事
  3. 使用英文句號.來訪問結構體中的成員變量,這被稱爲結構體成員訪問符。

通過這個例子,我們可以直觀感知到,結構體就如同分組一樣,將一夥人分成一個小組,然後給這個組命名,以後就用這個組名來代表這一夥人。結構體的出現,可以使代碼變得簡潔,例如以前我們需要將一個學生的信息作爲參數傳入到一個函數中處理,那麼我們就必須給這函數定義一系列形式參數,現在則只需要定義一個結構體參數就行了,這個好處就如同將各種小物件打包在一起,出門只需要推一個旅行箱一樣方便。

// 結構體類型做函數參數
void printStudent(struct student s){
    printf("學生信息:%s,%d,%s,%s\n",s.name,s.age,s.number,s.grade);
}

int main(){
    struct student stu;
    stu.name = "zhangsan";
    stu.age = 19;
    stu.number = "A010";
    stu.grade = "18級";
    
    printStudent(stu);
    return 0;
}

結構體僅僅是構造了一種新的組合數據類型,其使用與普通的基本類型大致相同,它既可作爲函數參數也可作爲函數的返回值。

結構體變量的初始化

以上是通過結構體變量來訪問成員變量來逐個進行賦值的,實際上結構體可以在聲明的同時進行初始化,這點類似於數組。

按順序初始化

struct student stu={"zhangsan",19,"A010","18級"};

按照聲明結構體時的成員變量的順序,在花括號中依次填入其值,如同數組初始化。

缺省的順序初始化

與數組類似的,我們也可以進行缺省的初始化,如下,這樣就只會對前兩個成員變量賦值,後面省略的變量會被進行零值初始化。

struct student stu={"zhangsan",19};
printStudent(stu);

打印結果:

學生信息:zhangsan,19,(null),(null)

指針變量的零值就是NULL,可以看到省略的最後兩個成員變量被賦了零值。

零值初始化

前面一直強調,局部變量應當先初始化再使用,當結構體變量做局部變量時,也應當遵循。以上代碼示例是沒有遵循的,通常人們都喜歡先聲明,後面就直接去操作了,忽略了初始化步驟,特別是當我們不確定結構體成員變量的值時,就會先聲明放在那,這是不好的習慣,我們應當先做零值初始化。

正確示範

int main(){
    // 聲明同時對所有成員變量做零值初始化
    struct student stu={NULL};

    // 初始化之後再去使用,正確!
    stu.name = "zhangsan";
    stu.age = 19;
    stu.number = "A010";
    // stu.grade = "18級";
    
    printStudent(stu);
    return 0;
}

錯誤示範

int main(){
    // 聲明但不初始化
    struct student stu;

    // 直接就去使用,但省略了對grade變量的賦值
    stu.name = "zhangsan";
    stu.age = 19;
    stu.number = "A010";
    // stu.grade = "18級";
    
    printStudent(stu);
    return 0;
}

以上代碼異常退出,這是因爲聲明時未對結構體的所有成員進行零值初始化,在使用時,又沒有對全部成員進行賦值,導致成員變量出現了野指針的情況。局部變量的特點是,聲明但不初始化,那麼它的值是隨機的,如果是指針變量,那麼它可能會指向一個隨機的內存空間,這可能是一個不允許訪問的內存空間,這就是所謂野指針。

除了可以使用NULL做零值初始化,也可以使用0,如下

struct student stu={0};

在GCC編譯器中,甚至可以直接省略什麼都不寫,但不推薦,因爲微軟的VC編譯器不支持,這樣寫的代碼兼容性太差。

struct student stu={};

指定成員初始化

按順序初始化是不夠靈活的,而且還需要記憶結構體成員變量的順序,當結構體成員變量比較多時,就有些糟心了。因此C99標準推出了新的語法,指定成員變量名進行初始化

    struct student stu={.age=18, .name="張三"};
    printStudent(stu);

在成員變量名前面加上一個成員訪問符.,然後使用=號的形式進行初始化賦值,多個之間用逗號分隔。這種新的語法有兩個明顯的好處,一是語義化表達,每個值對應哪個成員變量非常清晰;第二是無序,不用再去關心成員變量聲明時的順序了。與順序初始化相同的,沒有被指定的成員變量,則會被自動的初始化爲零值。

這種結構體初始化方式是我推薦的,它極大的提升了代碼可讀性,而且這種被稱爲聲明式語法的表達,正是目前其他高級編程語言所流行的趨勢。當我們掌握C語言再去學習Go語言時,會發現Go的結構體都是這樣去初始化的。另外值得說明的是,這種語法雖然是C99的新特性,但是好東西,微軟的VC編譯器也是會支持的,在VS2013及以上版本,可以放心使用。你的VC6.0還不淘汰,更待何時?VS2017早出免費版了,你是否還在用盜版的、破解的VC編譯器?

結構體與內存

先看一個現象

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

int main(){
    struct A struA={0};
    printf("size is %d\n",sizeof(struA));
    return 0;
}

打印結果:

siez is 8

我們將結構體成員變量聲明的順序調整一下,在次打印

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

打印結果:

siez is 12

這裏就發生了很奇怪的現象,第一次我們使用sizeof獲取的結構體佔用內存大小是8,當調整成員變量聲明的順序後,即將char b;int a;順序交換,其他都不變,結構體佔用的內存大小增加了,變成了12,爲什麼會出現這樣的情況呢?

有一些教材上說,結構體佔用的內存大小就等於結構體各個成員變量佔用的內存大小之和,這裏char 1個字節,int4個字節,short2個字節,加起來是7字節,怎麼跑出了8個字節呢?顯然這種說法是存在問題的。要把這些問題全部搞清楚,就得了解結構體在內存中的分佈情況。

首先我們得開動腦筋,學會該怎麼去研究和分析問題,有時候一些資料自相矛盾時,我們感到迷糊時,都得自己想辦法去探索和試驗,所謂實踐出真知,放到這裏再合適不過了。

#include <stdio.h>

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

int main(){
	struct A struA = {0};
	// 打印結構體的內存地址
	printf("A address is %x\n", &struA);
	// 分別打印結構體每個成員變量的內存地址
	printf("A.a address is %x\n", &struA.a);
	printf("A.b address is %x\n", &struA.b);
	printf("A.c address is %x\n", &struA.c);
	return 0;
}

觀察打印結果:

A address is 46fd30
A.a address is 46fd30
A.b address is 46fd34
A.c address is 46fd36

我們觀察到的第一個現象是,結構體變量的內存地址和它的第一個成員變量的地址是相同的。這一點和數組很相似,數組變量的地址與數組第一個元素的地址也是相同的。

第二個現象是,結構體在內存中的佈局,是將它的所有成員變量,按照聲明時的順序連續排列到內存空間中。這個也很容易看出來,變量abc的內存地址編號都是有順序的,aint類型,佔用四個內存單元,它的起始地址是46fd30,緊隨其後的b變量起始地址正好是46fd34

再看示例:

    // 設置有效值,查看內存分佈情況
	struct A struA = {18,'a',127};
	printf("A address is %x\n", &struA);
	printf("A.a address is %x\n", &struA.a);
	printf("A.b address is %x\n", &struA.b);
	printf("A.c address is %x\n", &struA.c);

我們可以使用Visual Studio工具來查看真實內存分佈圖,爲了便於理解,以下字節中的值都是用我們熟悉的十進制表示的
在這裏插入圖片描述
注意,這裏-52是VC編譯器將未使用的字節做自動填充時所用的十進制默認值,對應的也就是我們前面章節說的16進制數0xcc,它表示這個字節沒有被使用。一定要搞清楚,0xcc的含義和0是不同的,如果這個字節的值是0,那麼表示它是被使用的,只是它此刻的值就是0而已。

將結構體成員變量聲明的順序調整一下,再次查看內存佈局

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

struct A struA = { 'a', 18, 127 };

在這裏插入圖片描述
觀察上圖,很容易發現規律,char類型本來是一個字節,但是現在在結構體中卻佔了4個字節,int a緊隨其後,接下來short c在結構體中又佔了4個字節!

相信此時,大家心中已經有了一個模糊的答案。編譯器爲了提升內存訪問的性能,它會做一件事,用通俗的話說,它會對結構體分組訪問,通常在用32位來表示int類型的硬件平臺上,它會將每四個字節分成一組來進行訪問,這樣可以提升內存訪問效率。譬如上面的例子,結構體中有三個變量abc,如果不分組,正常情況下要向內存一個字節一個字節的讀取數據,這樣效率會比較低,但是分組訪問就不一樣了,假設我們約定4個字節爲一組,那麼int a正好是四個字節,第一組就訪問它,剩下的char bshort c加起來總共3個字節,正好可以湊成第二組,這樣一來,三個變量,只需要分兩次訪問就Ok了,大大減少了對內存的訪問次數,提升了性能。

以上就是C語言中,所謂的結構體內存對齊的概念。帶給我們的啓示就是,在聲明結構體成員變量時,不要隨意去排列成員變量的順序,要有意識的去安排變量的順序適應內存對齊,這樣可以減少結構體佔用的內存大小。

如下這種排列就是不合理的,導致編譯器做內存對齊時,將其分成三組,每組4個字節,這使得該結構體佔用的內存變成了12字節。而將int a放在第一個成員位置時,編譯器內存對齊後,結構體僅佔用8字節大小。這正解惑了我們一開始提出的疑問。

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

有人可能會疑問,char bint a爲什麼不貼在一起放,它們加起來雖是5個字節,但我們可以把前四個字節歸爲一組啊?這裏要注意,int類型的四個字節是看成一個整體的。舉個生活中的例子,我們都知道拼車可以提升小汽車的使用效率,這也是之前流行順風車的一個原因,假如順風車有三個空位,現在正好有三個人想拼車,但是其中一個人噸位比較大,他一個人必須要佔兩個人的位置才能坐下,那麼現在該如何安排這三個人呢?我想,正常情況下這一趟都是裝不下三個人的,必須分兩批次,而且一個人也不能被分成兩半,這個噸位大的人只能佔後排兩個位置是比較合理的,剩下兩個人,一個坐副駕駛,一個再打另一輛車。想通了這個例子,基本也就想通了結構體內存對齊來提升效率的概念。

有些善於發現問題的朋友可能會想到,在64位系統裏,long類型表示8字節,那麼結構體怎麼進行內存對齊呢?實際上,上面僅僅是打比方來說明問題,不同的編譯器,其結構體內存對齊的規則也不盡相同,並不是簡單的僅僅按照4字節來對齊。Windows下的VC編譯器,主要按照4字節或8字節來對齊,而Linux下的GCC則使用2字節或4字節來對齊,這個對齊參數被稱爲對齊模數

如果我們不想優化性能,在某些特殊場景下,不希望某個結構體做內存對齊,則可以通過預編譯指令進行設置

// 傳入1,指定不做內存對齊,在結束處pack()不傳參,恢復內存對齊
# pragma pack(1)
struct A
{
    char b;
    int a;
    short c;
};
# pragma pack()

int main(){
    struct A struA = { 0 };
	printf("size is %d\n",sizeof(struA));
    return 0;
}

# pragma pack(1)# pragma pack()將不希望內存對齊的結構體包裹起來,再次查看打印結果

size is 7

結構體與指針

結構體與數組很像,本質上就是指的一片特定的連續的內存空間,結構體成員就在這邊內存空間中按順序分佈。那麼所謂結構體指針,也就是指向該結構體的指針,結合結構體內存分佈知識可知,這個指針實際上就是保存了結構體空間的初始地址。

int main(){
    // 聲明並初始化一個結構體變量
    struct student stu = {0};
    // 聲明一個結構體指針變量,並指向一個結構體
    struct student *p_stu = &stu;

    // 通過結構體指針訪問成員
	printf("學生信息:%s,%d,%s,%s\n",p_stu->name,p_stu->age,p_stu->number,p_stu->grade);
    return 0;
}

事實上,將結構體作爲一種新的類型,那麼結構體指針與其他類型的指針用法也是相似的,唯一需要注意的地方是,結構體變量訪問成員,使用成員訪問符.,而結構體指針變量是不同的,它使用一個小箭頭->來訪問,要注意這兩者的區別,萬萬不能混淆。

在C語言中,除了數組做函數參數是地址傳遞外,其他所有類型都是值傳遞,結構體也是如此。因而,在將結構體傳入一個函數內部時,應當考慮使用結構體指針,避免對結構體做內存拷貝,用以提升性能。

結構體的其他聲明方式

上面的結構體聲明方式只是一般方式,除此之外,還有各種怪異的聲明方式,大多數是不推薦的,但是要能看懂別人的代碼。

聲明結構體同時還聲明結構體變量

int main(){
    // 聲明結構體的同時,再聲明兩個結構體變量a、b
    struct student
    {
        int age;
        char *name;
        char *number;
        char *grade;
    }a,b;


    // 再聲明一個結構體變量c
    struct student c = {0};
    return 0;
}

還可以在聲明結構體並聲明結構體變量的同時初始化

int main(){
    struct student
    {
        int age;
        char *name;
        char *number;
        char *grade;
    }a={0},b={.name="李四"};

    printf("%s",b.name);
    return 0;
}

聲明匿名的結構體

聲明結構體時的標籤名是可以省略的

    // 聲明一個結構體,並省略標籤名,同時聲明兩個結構體變量a、b
    struct
    {
        int age;
        char *name;
        char *number;
        char *grade;
    }a,b;

匿名結構體與有名字的結構體有顯著的區別,因爲它沒有名字,必須在聲明的同時聲明好需要的結構體變量,後面它是沒法再去聲明新的結構體變量的。這種用法有一個用處,如果我只指定聲明一個結構體變量,那麼全局就只有一個該結構體變量,後面無法定義新的結構體變量了。

結構體類型定義

在結構體的一般聲明格式中,當我們聲明好一個結構體後,使用的時候還需要將struct關鍵字+標籤名作爲一個整體來聲明新的結構體變量,如struct student stu;,這樣的語法表達非常麻煩。實際上在C語言中,結構體聲明通常是和另一關鍵字typedef結合起來使用的。

// 使用typedef時,省略結構體標籤名
typedef struct{
    int age;
    char *name;
    char *number;
    char *grade;
} Student;

typedef struct{
    int x;
    int y;
} Point;

int main(){
    // 聲明結構體變量
    Student stu = {0};
    Point point = {10,20};
    return 0;
}

以上的結構體使用方式,才真正符合我們的編程直覺,看起來更像C++、Java中的類的使用。通常的,我們應該在頭文件中用以上方式聲明結構體,然後在源文件中包含頭文件,使用相應的結構體。

小拓展
typedef是一個可以用來定義類型別名的關鍵字,它並不僅僅是用在結構體聲明中

typedef 舊類型名 新別名;
#define false 0
#define true 1
typedef int bool;
typedef char byte;

int main(){
    bool b=false;
    byte stream[10];
    return 0;
}

要注意,typedef定義的類型別名後面,一定要跟上分號結束。

結構體總結

  1. 在聲明結構體變量的時候,編譯器就爲其分配內存空間
  2. 結構體在內存中的分佈,是一片連續的內存空間
  3. 結構體指針保存的是結構體在內存空間的起始地址
  4. 結構體的總內存大小並不一定等於其全部成員變量內存大小之和,當存在內存對齊時,可能會多佔用一些額外的空間
  5. 結構體變量使用.訪問成員,結構體指針使用->訪問成員
  6. 聲明結構體時,建議結合typedef關鍵字創建別名
  7. 結構體可以嵌套使用,即將一個結構體作爲另一個結構體的成員

歡迎關注我的公衆號:編程之路從0到1

編程之路從0到1

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