前陣子做了個新功能,第一次用 C 語言實作一個結構(structure),所幸時間還算充裕,讓我前後修改三、四次,才終於定案並完工。這篇文章記錄著我陸陸續續修改的心路歷程。
先舉個例子,現在有幾座固定只能裝 8 本書的書架,而我想記錄每座書架上已經放了幾本書、每本書的書名與國際標準書號(ISBN, )。書名可以使用字符指標的形式(char *)儲存,但是國際標準書號必須用字符矩陣的形式(char [])儲存。因為之後要對國際標準書號的尾碼做些風騷的操作……呃,我是指,要去驗證最後一碼的確認碼計算正確。
當然還有更多值得記錄的資料,但這個例子只是想要重現儲存指標的矩陣與雙重矩陣被包含在同一個結構中,可以讓初學者多麼崩潰,也可能只有我很崩潰啦……
定義基本的 struct
先依照目標定義一個結構,由於書本的數量一定會是整數,所以定義為 integer,其餘就如前文所述。另外,突然冒出來的 13 是現行的 ISBN 長度,以前是固定只有 10 碼啦……
1 | struct Bookshelf { |
使用 typedef 簡化聲明
每次聲明都要在前面打 struct
搞得我有點煩,馬上就給 Bookshelf
一個別名吧!順便也把寫死的那兩個數字獨立定義,之後要修改就只需要在最前面的 define
區塊變更。
1 | #define MAX_BOOKS_CNT 8 |
最後完工的結構大概就長這個樣子,但其實它曾經長得超級複雜。最初我得到的指示要求盡量減少記憶體的使用量,最好使用 malloc 動態分配記憶體空間,讓我爬資料爬了一週才實作出來。結果成品被評為太過複雜,操作上容易出錯,未來維護不易。重新討論後才發現需要的空間其實沒多大,就修改方向為固定大小的結構了。
取用結構中的成員
在 C 語言之中,有兩個運算子(operator)可以取得結構中的資料。有趣的是,我找不到其中一個運算子的正式稱呼,連英文都沒有。這邊就採取英文俗名的直譯來介紹:
.
點運算子(dot operator)->
箭頭運算子(arrow operator)
點運算子作用於結構
先從簡單的點運算子開始舉例,當要在上述結構中儲存資料時,可以這麼做:
1 | bs1.title[0] = "Example 1"; |
接著,再使用點運算子取出結構中的資料,並打印於畫面上:
1 | printf("count=%d\n", bs1.count); |
如何處理結構指標
為了方便起見,可以把「儲存資料至結構中」獨立為一個函數:
1 | void addBook(BOOKSHELF *bs, char *isbn, char *title) |
以 C 語言來說,函數的引數在此處必須是指標,不然會有自以為自己寫進去的錯覺。剛從 C# 轉過來時,我常常被取值與取址搞得一個頭兩個大,雖有看到資料說 C# 之中分為 by value 與 by reference 兩種呼叫,但是當年的我根本還沒用到如此高深的 C#……總而言之,要讓資料確實寫入引數中,必須使用指標。
這時,由於點運算子(.)的優先度高於取值的一元運算子(*),導致需要添加很多的括號才能完成上述函數,否則 *bs.count
隱含的是 *(bs.count)
,這不是我們想要的結果。
為簡化而生的箭頭運算子
雖然不清楚淵源,但我想,箭頭運算子(->)是為了簡化這個繁複的過程而誕生。
讓「先對結構指標取值,再對該結構取用結構成員」這兩個步驟的表達,由 (*bs).count
簡化為 bs->count
,使程式碼變得更精簡、更易讀。以此運算子改寫上述函數,會得到:
1 | void addBook(BOOKSHELF *bs, char *isbn, char *title) |
這樣是不是變得簡單明瞭了呢?
初始化結構成員
最後,在寫這篇文章的範例時,我發現剛被聲明的結構,內容基本上是亂碼。在儲存資料前,不小心取用到結構成員,往往會發生悲劇。如果不是太在意效能的話,可以在聲明時,一併將結構成員初始化為 0 或者依情境直接指定第一筆資料:
1 | /* Initialize a struct to 0. */ |
這樣就不用擔心,聲明結構後立即取用,會不會出現各種亂碼啦!
一個可編譯的範例
以下附上書寫本文時的測試範例。通常我都會使用 gcc -Wall <file name>
,以便確認所有的警告訊息。
1 | #include <stdio.h> |
以下則是執行結果:
1 | chihyu@chihyu-demo-ubuntu:~/exercise/struct-exercise$ ./a.out |