關於Heading first C的心得筆記,還有改編書中的範例當練習。
擁有多種型別成員的struct
在撰寫程式時會發現,我們常會利用相同片段的資料來處理不同的事,如下範例:
/* Print out the personal informaion */void personal_info(const char *name, const char *sick_type, float days, int age){
printf("%s got %s for %.1f days. She/he is %d.\n", name, sick_type, days, age);
}
/* Print the label for the document */void label(const char *name, const char *sick_type, float days, int age){
printf("Name: %s\nType of sick: %s, %.1f days\nAge: %d\n", name, sick_type, days, age);
}
/* const char*: 將傳入一字串的指標,而非字串值,並且無法改動指向位址上的數值 *//* 若想以字串的方式傳入,則將型別定義為char array,如char[20]即可 */
當實際使用函數時,我們的程式碼將得重複輸入相同的4個參數,使得程式碼顯得有些凌亂,如下面這樣:
int main(){
personal_info("John", "fever", 3.5, 23);
label("John", "fever", 3.5, 23);
return 0;
}
但是其實兩段函數的輸入參數,都是屬於同一位病人所擁有的資料片段,然而由於這些資料片段並不一定為相同型別,因此僅能放入單一型別變數的Array將無法滿足我們的使用情境,此時可以考慮使用struct結構來將這些變數集結起來。
struct的定義、宣告
struct這個關鍵字的含意是結構化的資料類型(structured data type),因此struct讓程式設計師可以將這些不同型別的資料通通綁成一塊,變成一個新的資料類型,範例如下:
struct patient{
const char *name;
const char *sick_type;
float days;
int age;
};
透過上述程式就可以創建由數個資料片段所組成的一個自定義的資料類型,struct與array是有點類似的,其不同之處在於(1)struct為固定長度,(2)每一個結構內的成員都有各自作為辨識的名字。初始化結構的程式碼與array相似,唯一需要注意的是需要按照結構內的順序,將變數填入,如patient結構內的順序則為name、sick_type、days以及age:
struct patient p1 = {"John", "fever", 3.5, 23};
還有另外一種指定成員的初始化方式,適用於需要對大型的結構進行賦值,或是只需要初始化某幾個成員變數的情況,此時使用如下的程式碼將會讓人更容易理解:
struct patient p1 = {.name = "John", .age = 23};
使用指定成員的方式的賦值順序並不受限制,任意安排順序也可以成功指定。
使用struct後的程式碼 - 改寫(上)
知道如何自定義struct,現在回頭來改寫我們的函數以及main()。在main()內加入patient結構的定義,並使用patient這個結構作為函數參數,代替重複傳入四個片段的資料,使得main()看起來更清爽。
#include <stdio.h>
struct patient{
const char *name;
const char *sick_type;
float days;
int age;
};
/* Print out the personal informaion */void personal_info(struct patient p){
...
}
/* Print the label for the document */void label(struct patient p){
...
}
int main(){
struct patient John = {"John", "fever", 3.5, 23};
personal_info(John);
label(John);
return 0;
}
取得stuct成員變數數值
以Array的方式來取得struct成員變數是行不通的,在編譯的過程中將會出現error:
struct patient John = {"John", "fever", 3.5, 23};
printf("Name: %s\n", John[0]);
正確的取值方法是透過"."運算子搭配其成員名稱,執行如下的程式碼後,便能成功取得並輸出Name成員的內容:
struct patient John = {"John", "fever", 3.5, 23};
printf("Name: %s\n", John.name);
使用struct後的程式碼 - 改寫(下)
了解如何取得結構內成員的數值後,便來著手修改personal_info()以及label()函數內容的部分。修改後完整的程式碼如下:
#include <stdio.h>
struct patient{
const char *name;
const char *sick_type;
float days;
int age;
};
/* Print out the personal informaion */void personal_info(struct patient p){
printf("%s got %s for %.1f days. She/he is %d.\n", p.name, p.sick_type, p.days, p.age);
}
/* Print the label for the document */void label(struct patient p){
printf("Name: %s\nType of sick: %s, %.1f days\nAge: %d\n", p.name, p.sick_type, p.days, p.age);
}
int main(){
struct patient John = {"John", "fever", 3.5, 23};
personal_info(John);
label(John);
return 0;
}
總結
使用struct結構可以讓程式碼較為簡潔且更容易理解。使用結構化資料類型另外的好處為,可以在不更動到函數參數的狀況下,修改其成員的變數,舉例來說,若在patient結構內新增了一個叫做int gender的成員,對personal_info()以及label()來說,傳入的參數為整個patient結構,函數並不需要知道結構成員使否有修改,函數只在乎他所使用到的變數是否仍然存在。因此使用結構除了可以讓我們的程式碼更好閱讀之外,也讓程式碼更方便進行維護與修正。
插曲1:使用typedef讓struct的定義與宣告變得簡潔
目前看來使用struct會讓程式語言在撰寫上顯得有點繁瑣,首先在定義結構時會需要使用到一次'struct'這個關鍵字(keyword),然後為結構定義實體時又需要再一次的使用到'struct',範例如下:
struct todo_item{
int item_no;
const char *title;
float time_to_spend;
};
...
struct todo_item m1 = {1, "Clean table", 30};
這時候可以使用typedef,藉由為struct取一個別名(alias)的方式來簡化整個步驟所需要撰寫的程式碼。首先在定義結構的struct關鍵字之前之前加入typedef,然後在結構定義的大括號之後加上自定義的別名名稱,上述之程式碼將修改成如下的形式:
typedef struct todo_item{
int item_no;
const char *title;
float time_to_spend;
} item;
...
item m1 = {1, "Clean table", 30};
此時編譯器將會視item為struct todo_item。
只是這樣一來我們的結構便有了兩個名字,一個是宣告時給的名字,即結構名稱(todo_item),另一個則是透過typedef給的別名(item),然而我們並不需要同時給編譯器這兩個名字,因此便有了更簡化的作法,將定義結構時的名字也省略(struct todo_item),僅透過typedef給予別名,此後在程式碼中使用別名即可,簡化後程式碼如下:
typedef struct {
int item_no;
const char *title;
float time_to_spend;
} item;
...
item m1 = {1, "Clean table", 30};
以typedef定義結構的原始碼將如下:
#include <stdio.h>
typedef struct {
const char *name;
const char *sick_type;
float days;
int age;
}patient;
/* Print out the personal informaion */void personal_info(patient p){
printf("%s got %s for %.1f days. She/he is %d.\n", p.name, p.sick_type, p.days, p.age);
}
/* Print the label for the document */void label(patient p){
printf("Name: %s\nType of sick: %s, %.1f days\nAge: %d\n", p.name, p.sick_type, p.days, p.age);
}
int main(){
patient John = {"John", "fever", 3.5, 23};
personal_info(John);
label(John);
return 0;
}
插曲2:函數內的只是副本,使用指標保留變數異動
在先前的patient範例中增加seeDoctorAgain()函數,並在main()中呼叫,程式執行後如下圖,我們發現雖然在seeDoctorAgain()中patient.days有被增加到6.5,但是回到main()時,卻又是原本的3.5。
void seeDoctorAgain(patient p){
p.days = p.days + 3;
printf("\nToo Bad, She/he have been sick for %.1f days.\n", p.days);
}
int main(){
patient Jahn = {"John", "fever", 3.5, 23};
personal_info(John);
label(John);
seeDoctorAgain(John);
label(John);
return 0;
}
這是因為傳入函數的是patient結構的副本,一份複製出來的John的資料,當seeDoctorAgain()結束時,這個複製的John也會跟著一起消失,此時較好的方式是將參數改成指標,讓函數前往該指標位址尋找John的資料,修改完成後執行程式將可以發現結構成員days的數值已經成功地被修改。
void seeDoctorAgain(patient *p){
p->days = p->days +3;
printf("\nToo Bad, She/he have been sick for %.1f days.\n", p->days);
}
...
seeDoctorAgain(&John);
/* (*p).days = p->days *//* the computer evaluates the dot operator before it evaluates the * */
Comments
Post a Comment