struct 可以在同一個名稱下擁有多種資料型態。使用struct能讓資料的存取和處理更為靈活。 Chap 15 struct與資料結構 struct 可以在同一個名稱下擁有多種資料型態。使用struct能讓資料的存取和處理更為靈活。
struct與資料結構 15.1 struct的宣告和使用 15.2 由struct構成的陣列 15.3 struct資料型態與函數參數的傳遞 15.4 struct實例的動態宣告 15.5 指標成員與資料結構 15.6 union資料型態 15.7 enum資料型態
struct的宣告和使用 組成份子稱為成員 (member) 或資料欄位 (data field)。 成員可以是各種不同的資料型態 (複合式資料型態)。 關鍵字struct是英文 structure (結構) 的縮寫,此種資料結構又稱為記錄(record)。
struct資料型態的宣告範例 Employee包括的資料成員有Name (姓名)、Phone (電話號碼) 以及Id (編號) 三種: { char Name[20]; char Phone[10]; int Id; }; // 注意這要用到「;」!
Struct 在記憶體中的儲存方式 各成員的儲存位置是連續的:
定義了兩個名稱分別為Ea和Eb的Employee變數。 由某一資料型態定義的變數稱為該資料型態的實例 (instance)。 使用struct 資料型態定義變數 可以使用標準的定義敘述。例如: Employee Ea, Eb; 定義了兩個名稱分別為Ea和Eb的Employee變數。 由某一資料型態定義的變數稱為該資料型態的實例 (instance)。
定義變數時一併給予初始值 例如,上面的敘述可進一步寫成: Employee Ea = {"Ann", "02384125", 105}; Employee Eb = {"Joanne", "03544132", 106};
要存取Employee變數的個別資料欄位 必需同時給定變數名稱和資料成員名稱,中間用一個成員運算符號 (member operator)「.」隔開。例如: Ea.Name // 其值目前分別為 “Ann” Eb.Phone // 其值目前分別為“02384125” Ea.Id // 其值目前分別為105 分別用來代表Ea這個Employee變數的三個成員,其值目前分別為“Ann”,“02384125”和105。這個語法基本上和我們在10.1節介紹的成員函數的語法是一致的。
範例程式TestStruct.cpp 如何使用struct宣告自訂的資料型態,以及各欄位內的資料如何存取。
範例程式 檔案 TestStruct.cpp // TestStruct.cpp #include <iostream> using namespace std; struct Employee { char Name[20]; char Phone[10]; int Id; }; // ----- 主程式 ----------------------------
int main() { EmployeeEa= {"Ann", "02384125", 105}; Employee Eb = {"Joanne", "03544132", 106}; cout << "Ea 的資料是:\n" << "姓名 : " << Ea.Name << '\n' << "電話號碼: " << Ea.Phone << '\n' << "編號 : " << Ea.Id << endl; cout << "Eb 的資料是:\n" << "姓名 : " << Eb.Name << '\n' << "電話號碼: " << Eb.Phone << '\n' << "編號 : " << Eb.Id << endl; return 0; }
執行結果 Ea 的資料是: 姓名 : Ann 電話號碼: 02384125 編號 : 105 Eb 的資料是: 姓名 : Joanne 編號 : 105 Eb 的資料是: 姓名 : Joanne 電話號碼: 03544132 編號 : 106
合併struct資料型態的宣告和變數的定義 例如: struct { char Name[20]; char Phone[10]; int Id; } Ea, Eb; 由於粗體字的部份本身就是已經是新定義的資料型態之具體內容,不用再取個名稱來代表它。
比較資料型態變數的語法
由struct構成的陣列 Employee Officer[50]; 結合陣列和struct,可以一次完成很多具有相同結構的struct變數的定義。 例如,可以使用 Employee Officer[50]; 同時定義從Officer[0] 到Officer[49],共50個Employee變數
struct陣列各欄位的資料 cout << Officer[8].Name << endl; cout << Officer[12].Phone << endl; cout << Officer[40].Id << endl;
範例程式StructArray.cpp 允許使用者逐一輸入各陣列元素的各成員值 (每輸入一個項目後,要按兩次Enter鍵)。
範例程式 檔案 StructArray.cpp #include <iostream> using namespace std; const int NameSize = 20; const int PhoneSize = 10; struct Employee { char Name[NameSize]; char Phone[PhoneSize]; }; int main() const int Size = 2; Employee Officer[Size]; cout << "共 " << Size << " 個 Officers:\n";
for (int i=0; i<Size; i++) { cout << "請輸入 Officer[" << I << "] 的姓名: "; cin.getline(Officer[i].Name, NameSize, '\n'); cout << "電話號碼: "; cin.getline(Officer[i].Phone, PhoneSize, '\n'); } cout << "Officer[" << i << "] 的資料是:\n" << "姓名 : " << Officer[i].Name << '\n' << "電話號碼: " << Officer[i].Phone << '\n'; return 0;
執行結果 共2個Officers: 請輸入Officer[0] 的姓名: Alan John 電話號碼: 03-4521234 請輸入 Officer[1] 的姓名: Peter Pan 電話號碼: 02-4354512 Officer[0] 的資料是: 姓名 : Alan John 電話號碼: 03-4521234 Officer[1] 的資料是: 姓名 : Peter Pan
struct資料型態與函數參數的傳遞 由struct所定義的實例被用來做為參數傳遞時,其預設的語意是傳值 (pass-by-value)。 也就是說,在「被呼叫函數」內部將另外產生一個複製資料,而不會影響「呼叫函數」內的資料。
用struct所定義的實例來傳遞參數 例如,呼叫敘述可以寫成: 而被呼叫函數則可以定義成: ShowMember(Ea); 而被呼叫函數則可以定義成: void ShowMember(Employee A) { cout << "資料的詳細內容是:\n" << "姓名 : " << A.Name << '\n' << "電話號碼: " << A.Phone << '\n' << "編號 : " << A.Id << endl; return; }
使用傳參照 (pass by reference) 改變struct實例的內容 例如: ChangeName(Ea, “Jackson”); 對應的「被呼叫函數」則定義成: void ChangeName (Employee& A, char NewName[]) { strcpy(A.Name, NewName); return; }
範例程式 檔案 StructFnc.cpp // StructFnc.cpp #include <iostream> using namespace std; struct Employee { char Name[20]; char Phone[10]; int Id; }; void ShowMember(Employee A) cout << "資料的詳細內容是:\n" << "姓名 : " << A.Name << '\n' << "電話號碼: " << A.Phone << '\n' << "編號 : " << A.Id << endl; return; }
void ChangeName (Employee& A, char NewName[]) { strcpy(A.Name, NewName); return; } // ============= 主程式 ======================== int main() { Employee Ea = {"Ann", "02384125", 105}; Employee Eb = {"Joanne", "03544132", 106}; ShowMember(Ea); ShowMember(Eb); ChangeName(Ea, "Jackson"); cout << "執行 ChangeName() 後:\n"; return 0; }
執行結果 資料的詳細內容是: 姓名 : Ann 電話號碼: 02384125 編號 : 105 姓名 : Joanne 編號 : 105 姓名 : Joanne 電話號碼: 03544132 編號 : 106 執行 ChangeName() 後: 姓名 : Jackson
使用指標改變struct實例的內容 使用傳址 (pass-by-address) 來達到使用參照的目的: 「被呼叫函數」則定義為 ChangeId(&Ea, 00128); 「被呼叫函數」則定義為 void ChangeId(Employee* pE, int NewId) { (*pE).Id=NewId; return; }
pE與Ea之間的關係
C++ 的具象指標符號 -> 將 (*pE).Name寫成: pE->Name 表示「由pE指向的struct變數內的成員 Name」。
進一步改寫 ChangeId() void ChangeId(Employee* pE, int NewId) { pE->Id = NewId; return; }
範例程式 檔案 StructFnc2.cpp // StructFnc2.cpp #include <iostream> using namespace std; struct Employee { char Name[20]; char Phone[10]; int Id; }; void ShowMember(Employee A) cout << "資料的詳細內容是:\n" << "姓名 : " << A.Name << '\n' << "電話號碼: " << A.Phone << '\n' << "編號 : " << A.Id << endl; return; }
void ChangeName (Employee& A, char NewName[]) { strcpy(A.Name, NewName); return; } void ChangeId(Employee* pE, int NewId) { pE->Id = NewId; return;} // ========= 主程式 ======================== int main() { Employee Ea = {"Ann", "02384125", 105}; Employee Eb = {"Joanne", "03544132", 106}; ShowMember(Ea); ShowMember(Eb); ChangeId(&Ea, 208); cout << "執行 ChangeId() 後:\n"; return 0; }
執行結果 資料的詳細內容是: 姓名 : Ann 電話號碼: 02384125 編號 : 105 姓名 : Joanne 編號 : 105 姓名 : Joanne 電話號碼: 03544132 編號 : 106 執行 ChangeId() 後: 編號 : 208
亦即struct實例的動態記憶體配置(dynamic memory allocation)。 下列敘述則可以在執行時才臨時決定陣列的大小: int Size; cin >> Size; Employee* pE = new Employee[Size];
struct實例的動態記憶體配置和回收 執行後會依指定的大小在記憶體的特殊區域,稱為記憶堆 (heap) 的地方,規劃出需要的記憶空間,並把第一個變數的開頭位址存入指標內。 如果此陣列不再需要,可以執行下列的敘述回收記憶體空間: delete [] pE;
例如,要取用第k個陣列元素內的成員Id,下述語法都是正確的: 使用陣列下標或指標算數存取內部成員 例如,要取用第k個陣列元素內的成員Id,下述語法都是正確的: Labor[k].Id (*(Labor + k)).Id (Labor + k)->Id pE[k].Id (*(pE + k)).Id (pE + k)->Id
範例程式DynStruct.cpp 示範動態產生由struct實例所構成的陣列,稱為Employee,之完整語法,並在事後回收記憶空間。
範例程式 檔案 DynStruct.cpp // DynStruct.cpp #include <iostream> using std::cin; using std::cout; struct Employee { char Name[20]; char Phone[10]; int Id; }; // --------- 主程式 ------------------------
int main() { int Size; cout << "請輸入 Employee 的數目:\n"; cin >> Size; Employee* pE = new Employee[Size]; delete [] pE; return 0; }
指標成員與資料結構 struct所宣告的資料型態可以使用「指標」做為成員。 指標成員可以指向自己所在的struct資料型態,稱為「自我參照」(auto-reference)。例如: Struct Data { int Id; Data* pD; };
串列 (lists) 定義一串的Data變數: 圖示如下: Data D1, D2, D3; D1.pD = &D2; D2.pD = &D3; 圖示如下: 串列最後一個元素內的指標值為NULL (亦即 ‘\0’),用來做為檢查串列是否「到此為止」的根據。
串列 (lists) 「節點」(node): 每一個用來儲存資料的元素第一個節點稱為 「開頭」(head): 第一個節點。 「結尾」(tail): 最後的節點。
在陣列插入一個元素 必需同時將V[1] 及其之後的所有元素往右移,且無法應付陣列因長度增加而記憶空間可能不足的問題。 int* V = new int [Size]; 必需同時將V[1] 及其之後的所有元素往右移,且無法應付陣列因長度增加而記憶空間可能不足的問題。
「串列」可以帶來的便利 使用串列 (linked list) 可以較有效率地完成元素增刪的動作。 設想原先有A, B, C 三個元素串接在一起形成一個串列:
假設使用struct宣告了一個名叫 Element 的自訂資料形態: 一個串列的範例 假設使用struct宣告了一個名叫 Element 的自訂資料形態: struct Element { int Value; Element* Next; };
動態產生任意數目的Element (各實例的值在此為0, 2, 4, 6, …): cout << "請輸入 Element 的數目:\n"; cin >> Size; Element* pE = new Element[Size]; for (int i=0; i<(Size-1); i++) pE[i].Next = pE + i +1; pE[Size-1].Next = NULL; for (int i=0; i<(Size); i++) pE[i].Value = i*2;
顯示現有串列元素 不斷更換指標使它指向下一個元素的位址。 Element* pShow; for (pShow = pE; pShow != NULL; pShow=pShow->Next) cout << pShow->Value << ' '; 不斷更換指標使它指向下一個元素的位址。
以while迴圈顯示現有串列元素 Element* pShow=pE; while (pShow != NULL) { cout << pShow->Value << ' '; pShow = pShow->Next; }
將顯示串列內容的功能封裝到函數中 void ShowElement(Element* pShow) { while (pShow != NULL) cout << pShow->Value << ' '; pShow = pShow->Next; }
範例程式 檔案 ListStruct.cpp // ListStruct.cpp #include <iostream> using namespace std; struct Element { int Value; Element* Next; }; void ShowElement(Element* pShow) while (pShow != NULL) cout << pShow->Value << ' '; pShow = pShow->Next; }
// ---主程式------------------------ int main() { int Size; cout << "請輸入 Element 的數目:\n"; cin >> Size; Element* pE = new Element[Size]; for (int i=0; i<(Size-1); i++) pE[i].Next = pE + i +1; pE[Size-1].Next = NULL; for (int i=0; i<(Size); i++) pE[i].Value = i*2; cout << "Element 的內容是:\n"; ShowElement(pE); delete [] pE; return 0; }
執行結果 請輸入Element的數目: 5 Element的內容是: 0 2 4 6 8
雙向鏈結串列 (doubly- linked list) 能夠自由在串列中往返尋找。 每個節點內都含有兩個指標,分別指向下一個節點及上一個節點。
「雙向鏈結串列」的宣告 struct Node { int Value; Node* Previous; Node* Next; };
樹狀結構 (tree) 二維結構。 開頭的節點稱為「根」(root)。 「層」(layer): 與根的距離相同的所有節點。 「葉」(leaves): 不再指向下一層節點的所有節點。
二元樹 (binary tree) 由於每個節點都只有兩個指標向下一層的兩個節點,形成一個「品」字型的局部結構,因此稱為「二元樹」。
union資料型態 可以在其內擁有多種資料型態,但一次只能有一種資料型態。例如: union Data { float FloatValue; double DoubleValue; char CharValue; int IntValue; }; Data D1; 每個Data變數擁有的記憶空間由佔有最大空間的成員 (此例為D1.DoubleValue) 所決定。使用union的目的在於節省記憶體。
enum資料型態 enum是enumerate (列舉) 的簡寫。 由enum所定義的任何實例 (instance) 只能擁有當初enum宣告時所列舉的值之一。例如: enum Direction {Up, Down, Left, Right}; Direction x; 我們只能把這四種可能的值指定給x。例如: x = Right; 在電腦內部 {Up, Down, Left, Right} 分別和 {0, 1, 2, 3} 比對。
可以在宣告enum 時給予各成員確定的數值 例如: enum Check {Error = -1, Suspicious, Acceptable = 5, OK = 10}; 沒有特別指定的的值為其前一個成員 (亦即Error) 的值加1, 因此suspicious的內值為 –1 + 1 = 0。
enum資料型態 使用enum型態的目的是為了增進程式的可讀性,常與switch和if等判斷式結合使用。例如: int N; cin >> N; switch (N) { case Up: cout << “Moving Up!\n”; break; case Down: cout << “Moving Down!\n”; case Left: cout << “Moving Left!\n”;
case Right: cout << “Moving Right!\n”; break; default: cout << “Static\n”; }
範例程式 檔案 TestEnum.cpp case Up: cout << "Moving Up!\n"; break; #include <iostream> using namespace std; enum Direction {Up, Down, Left, Right}; // -------- 主程式 ------------------------ int main() { int N; cout << "請輸入期望的運動方向\n"; cout << "(0=Up, 1=Down, 2=Left, 3=Right):\n"; cin >> N; switch (N) case Up: cout << "Moving Up!\n"; break;
case Down: cout << "Moving Down!\n"; break; case Left: cout << "Moving Left!\n"; case Right: cout << "Moving Right!\n"; default: cout << "Static\n"; } return 0;
執行結果 請輸入期望的運動方向 (0=Up, 1=Down, 2=Left, 3=Right): 2 Moving Left!