第 20 講:結(jié)構(gòu)體
何為結(jié)構(gòu)體?
前文我們學(xué)習(xí)了非常多的數(shù)據(jù)類型,比如基本的數(shù)據(jù)類型 int
、float
等等,也學(xué)習(xí)了指針類型 int *
、char *
類型,以及無(wú)類型指針類型 void *
,數(shù)組 []
和函數(shù) ()
。不過,這些基礎(chǔ)數(shù)據(jù)類型并不能夠滿足我們的所有需求。比如,我們把“學(xué)生”看作一種類型,這個(gè)類型里包含一些基本信息,諸如成績(jī),學(xué)號(hào)姓名等等。這可能嗎?
當(dāng)然,今天要講到的結(jié)構(gòu)體(Structure)就是為了解決這種復(fù)合結(jié)構(gòu)類型的基本知識(shí)點(diǎn)。
結(jié)構(gòu)體的聲明格式如下:
比如,學(xué)生類型的書寫格式是這樣的:
也可以簡(jiǎn)寫:
這樣就定義好了一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體類型是 student
類型的。
請(qǐng)注意,這種特殊類型和函數(shù)一樣,需要放在獨(dú)立的位置上,而不是嵌套到函數(shù)里,這樣全局級(jí)別才能訪問到,否則我們就無(wú)法在想要的位置使用它們。當(dāng)然,C 語(yǔ)言確實(shí)不允許把結(jié)構(gòu)體的聲明(定義)語(yǔ)句(上面那一坨)放進(jìn)函數(shù)之中。
結(jié)構(gòu)體的使用
我們嘗試為結(jié)構(gòu)體里的每一個(gè)成員賦值,并輸出它們。不過,我們需要開始復(fù)雜一點(diǎn)的邏輯了。前文學(xué)過了如何聲明堆內(nèi)存,所以這里我們也可以使用這種東西來在堆內(nèi)存里創(chuàng)建結(jié)構(gòu)體變量。不過請(qǐng)注意,結(jié)構(gòu)體是無(wú)法在棧內(nèi)存里分配的,所以我們必須為其賦值。
student
首先為堆內(nèi)存里分配 student
類型足夠使用的內(nèi)存空間,然后把這塊內(nèi)存的地址返回給變量 a
。注意,返回的類型是 void *
,所以需要經(jīng)過一次強(qiáng)制轉(zhuǎn)換。
在完成分配后,判斷一下它是否為 NULL
是一個(gè)好習(xí)慣。如果不為 NULL
則說明分配內(nèi)存是成功的,于是我們開始為內(nèi)部的所有信息點(diǎn)進(jìn)行賦值。
它和數(shù)組不同,數(shù)組分配的空間是連續(xù)的,但由于數(shù)組內(nèi)的所有元素的類型全是一樣的,所以有索引這種東西的存在;而結(jié)構(gòu)體里,我們不能保證所有自己寫的字段的類型一樣,所以我們確實(shí)沒有辦法通過索引器 []
來獲取每一個(gè)成員的信息,而是通過成員訪問運(yùn)算符 .
(Member Access Operator)來取得它們。
不過,請(qǐng)注意,前文使用的是堆內(nèi)存分配的模式,所以得到的分配結(jié)果顯然是指針類型的,于是要將指針“拆解”為普通類型(取得內(nèi)容),需要使用間接訪問運(yùn)算符 *
對(duì)指針變量加以修飾:*xiaoMing
,這才是真正的“內(nèi)容”。然后,我們對(duì)這個(gè)具體的結(jié)果再次使用成員訪問運(yùn)算符來取值:(*xiaoMing).age
。請(qǐng)注意,這種取值需要為變量名和取內(nèi)容運(yùn)算符用小括號(hào)括起來。如果不括起來,C 語(yǔ)言將認(rèn)為執(zhí)行內(nèi)容是先獲取 xiaoMing.age
,然后再對(duì)這個(gè)變量取內(nèi)容(執(zhí)行 *
取內(nèi)容),而顯然取內(nèi)容是針對(duì)于指針變量而言的,但此時(shí)就算 xiaoMing.age
奏效,沒有語(yǔ)法問題,而 age
字段是我們規(guī)定的數(shù)值類型的,而不是指針,所以 C 語(yǔ)言會(huì)為此報(bào)錯(cuò),提示你“xiaoMing.age
不是一個(gè)指針類型”。
同理,其它的元素都可以使用這種方式進(jìn)行賦值,但需要注意的是,字符串的賦值方式,一般我們不要去使用 scanf("%s", &字符指針或字符數(shù)組變量)
的模式,因?yàn)?scanf
函數(shù)向來都是不處理空格的,所以它遇到空格直接就認(rèn)為是輸入結(jié)束了;而名字里可能是英文名,所以此時(shí)是可能含有空格的,故我們需要使用 gets
函數(shù)來輸入。
另外,gets
函數(shù)需要給定一個(gè)參數(shù),這個(gè)參數(shù)必須是一個(gè)字符數(shù)組變量,而不是指針。指針并不能保證這個(gè)指向的內(nèi)存塊是可以被修改的,甚至不能保證這一塊內(nèi)存是可以被修改的。字符數(shù)組在聲明期間就已經(jīng)創(chuàng)建好了一系列內(nèi)存空間,所以我們完全可以對(duì)其進(jìn)行修改和使用。所以字符指針和字符數(shù)組的區(qū)別在于這一點(diǎn):字符數(shù)組可修改內(nèi)部元素,而字符指針不一定表示的是一個(gè)數(shù)組,而可能僅僅是一個(gè)普通的指針變量。前文說過,數(shù)組可以在賦值的時(shí)候退化為指針,但反之不然。
記得最后添加 free
函數(shù)釋放資源,否則會(huì)造成內(nèi)存泄漏的 bug。
我覺得 (*v).f
的訪問模式太難寫代碼了,有沒有簡(jiǎn)化版?
我也這么覺得,我們每次為字段賦值都得“無(wú)情地”加入小括號(hào),然后才能往后加字段信息。這樣寫確實(shí)麻煩了。所以,C 語(yǔ)言提供了一個(gè)語(yǔ)法糖(簡(jiǎn)化版書寫格式),叫指針成員訪問成員運(yùn)算符 ->
(Pointer Member Access Operator)。
仔細(xì)體會(huì)兩種寫法,這兩種寫法是等價(jià)的。由于 ->
符號(hào)和 .
符號(hào)都是訪問運(yùn)算符,所以這兩個(gè)運(yùn)算符挨著兩邊的英文單詞(標(biāo)識(shí)符),而不用在中間添加額外的空格。
一些燒腦的結(jié)構(gòu)體使用模型
來一個(gè)可能會(huì)犯錯(cuò)的用法
我們嘗試不為變量賦值,就開始為字段里放數(shù)據(jù),看看會(huì)怎么樣。
請(qǐng)注意,這種用法是錯(cuò)誤的,而且很可能會(huì)引起報(bào)錯(cuò)。因?yàn)?variable
并沒有賦值就開始用了,在 C 語(yǔ)言里規(guī)定,沒有(成功)賦初始值的時(shí)候,變量都是不起作用的,即沒有真正的內(nèi)存可以對(duì)應(yīng)上這個(gè)變量。于是我們對(duì)一個(gè)根本就不知道是哪里的對(duì)象進(jìn)行賦值,或字段賦值,顯然是不可能找到正確答案的。
其實(shí),不只是這種情況,就連普通的變量也是如此:
這兩種輸出都不會(huì)得到你想要的結(jié)果。我知道你可能會(huì)認(rèn)為這里的輸出可能是 0,不過……
確實(shí)不一定真的是 0,有時(shí)候是一個(gè)莫名其喵的數(shù)字,看得頭皮發(fā)麻。
結(jié)構(gòu)體的嵌套聲明模式
假設(shè)我們有兩個(gè)結(jié)構(gòu)體:
那么上文給出的 b
結(jié)構(gòu)體類型是被允許的嗎,即 b
結(jié)構(gòu)體的某一個(gè)字段是別的結(jié)構(gòu)體類型?
答案是,是的。我們完全可以創(chuàng)建一個(gè)結(jié)構(gòu)體,使得這個(gè)結(jié)構(gòu)體的 field
字段是另外一個(gè)結(jié)構(gòu)體的類型。從邏輯上講,這完全是不矛盾的。
不過,如果你創(chuàng)建這種類型的結(jié)構(gòu)體,你需要注意初始化。假如我們現(xiàn)在有這樣兩個(gè)結(jié)構(gòu)體:
second
然后對(duì)其進(jìn)行賦值。
首先,p->d
是為了取出 p
指針指向的字段 d
的信息,它是 first
結(jié)構(gòu)體類型的,而且不是指針;然后,對(duì)這個(gè)得到的結(jié)果再使用 .
成員訪問運(yùn)算符來獲取字段 a
和 b
的信息,或者說賦值。
這樣便可完成真正的賦值操作。
結(jié)構(gòu)體的遞歸聲明模式
前文我們說到,結(jié)構(gòu)體內(nèi)部是可以存放指針,也可以放普通變量的。那么,是否存在一種可能,讓結(jié)構(gòu)體(假設(shè)為 a
類型)內(nèi)部也有一個(gè) a
類型的字段呢,即:
這種寫法是可以的嗎?顯然,我們從邏輯上說不通,因?yàn)槲覀円獎(jiǎng)?chuàng)建好一個(gè) field
字段,由于它是 a
類型的結(jié)構(gòu)體類型,所以我們就不得不先為結(jié)構(gòu)體 a
類型創(chuàng)建好。但顯然此時(shí)還在創(chuàng)建其中的字段,整個(gè)結(jié)構(gòu)體并未完成創(chuàng)建,所以這是相違背的。
實(shí)際上確實(shí)如此。C 語(yǔ)言不允許遞歸聲明:結(jié)構(gòu)體類型內(nèi)的字段不允許是當(dāng)前結(jié)構(gòu)體類型的,不過,你可以聲明同樣類型的結(jié)構(gòu)體指針變量。
這樣書寫和存儲(chǔ)是沒有問題的,因?yàn)閺牡览砩现v,ptr
字段在這里僅僅表示的是一個(gè)可以指向這個(gè)類型的地址信息罷了,它是一個(gè)地址,所以并不和結(jié)構(gòu)體類型的本身有關(guān),所以這樣的聲明方式是被允許的。而且,這種聲明模式廣泛存在于一些復(fù)雜的結(jié)構(gòu)里,例如鏈表(Linked List),它具有一個(gè)數(shù)據(jù)字段(data)和一個(gè)下一節(jié)點(diǎn)地址的字段(next);而這里的 data
字段就是指向下一個(gè)鏈表節(jié)點(diǎn)的,它依然是當(dāng)前類型的,只不過是指針。
下面代碼可以展示這一點(diǎn):
輸出的結(jié)果如下:

對(duì)一個(gè)結(jié)構(gòu)體變量使用 sizeof
會(huì)如何呢?前文不是就用到堆內(nèi)存分配了嗎?
這個(gè)數(shù)值實(shí)際上并不需要關(guān)心,但一般在考試題里會(huì)認(rèn)為所有字段占據(jù)的內(nèi)存空間的總和。但實(shí)際上并不一定是這樣,結(jié)果可能比這個(gè)數(shù)值大一些,當(dāng)然也可能是一樣的。
舉個(gè)例子。就前文的學(xué)生類型結(jié)構(gòu)體,它由四個(gè)字段構(gòu)成:
在內(nèi)存里,很有可能存儲(chǔ)的方式是這樣的:

當(dāng)然,我也只能說是“可能”,因?yàn)榫唧w的分配模式是根據(jù)內(nèi)存怎么樣找起來快怎么分配的模式來分配的。如果這種分配方式,使得代碼運(yùn)算速度快的話,那么這種肯定就是內(nèi)存里存儲(chǔ)這個(gè)結(jié)構(gòu)體的方式。也有可能把 age
拿到下面來,也可能把 isGirl
拿下來,等等。
不過,按照基本考試的方法,把它們所占字節(jié)數(shù)加起來就是整個(gè)結(jié)構(gòu)體所占大小了:4 + 4 + 1 + 20 = 29 字節(jié),所以,
sizeof(struct student)
=sizeof(variable)
= 29。