一、定義和使用結構體變量
1. 定義結構體類型
前面定義使用的變量基本是相互獨立、五無在聯繫的,在內存中的地址也是互不相干的。但在實際生活和工作中,有些數據是有內在聯繫的、成組出現的。例如,一個學生的學號、姓名、性別、年齡等,是屬於同一個學生的。如果將這些變量分別定義為相互獨立的簡單變量,難以反映他們之間的內在聯繫,而數組又只能存放同一類型的數據。
C 語言允許用户自己建立又不同類型數據組成的數據結構,稱為結構體(structure)。其他一些高級語言中也稱為“記錄”(record)。聲明一個結構體類型的一般形式為:
struct 結構體名 { 成員表列 };
其中,struct 是聲明結構體類型的關鍵字,不可省略。結構體名由用户指定,又稱為結構體標記(structure tag),以區別於其他結構體類型。成員表列(member list)也稱為域表(field list),每一個成員是結構體中的一個域。成員名命名規則與變量名相同。結構體的成員也可以是另一個結構體。
也成為上面的例子聲明結構體類型如下:
struct Student {
char id[10];
char name[20];
char sex;
int age;
}
結構體類型和系統提供的標準類型具有相似作用,都可以用來定義變量。
2. 定義結構體類型變量
定義了結構體類型後,未來在程序中使用結構體類型的數據,應該定義結構體類型變量,並在其中存放數據。可以採取 3 種方法定義結構體類型變量。
先聲明結構體類型,再定義該類型的變量
可以使用上面定義的結構體類型 struct Student 來定義變量:
struct Student student1, student2;
這種形式和定義其他基本類型的變量是相似的。上面定義了 student1 和 student2 為 struct Student 類型的變量。
這種方式是聲明類型和定義變量分離,在聲明類型後可以隨時定義變量,比較靈活。
在聲明類型的同時定義變量
這種定義方法的一般形式為:
struct 結構體名 {
成員表列
} 變量名錶列;
例如:
struct Student {
char id[10];
char name[20];
char sex;
int age;
} student1, student2;
小型程序中,該方法可以直接看到結構體的結構,比較直觀方便。但在大型程序中,往往要求對結構體類型的聲明和對變量的定義分別放在不同地方,以使程序結構清晰,便於維護。
不指定類型名而直接定義結構體類型變量
其一般形式為:
struct {
成員表列
} 變量名錶列;
該方法指定了一個無名的結構體類型,由於沒有結構體名,因此不宜再次以此結構體類型去定義其他變量。
注意:
- 結構體類型與結構體變量是不同的概念,編譯時對類型不分配空間,只對變量分配空間。
- 結構體類型中的成員名可以和程序中的變量名相同,但二者不代表同一個對象。
- 結構體變量中的成員(即“域”)可以單獨使用,它的作用和地位相當於普通變量。
3. 結構體變量的初始化和引用
定義結構體時,可以對其進行初始化,然後可以引用該變量。
#include <stdio.h>
int main() {
struct Student {
char id[10];
char name[20];
char sex;
int age;
} a = {"20220629", "liuyuxin", 'M', 19}; // 定義結構體變量 a 並初始化
printf("NO.: %s\nName: %s\nSex: %c\nAge: %d", a.id, a.name, a.sex, a.age);
return 0;
}
// NO.: 20220629
// Name: liuyuxin
// Sex: M
// Age: 19
對結構體變量初始化時,初始化列表是用花括號括起來的一些常量,這些常量依次賦值給結構體變量中的各成員。C 99 標準允許支隊某一成員初始化,如:
struct Student b = {.name = "liuyuxin"}; // 成員名前由成員運算符 .
.name 代表結構體變量 b 中的成員 b.name。其他未被初始化的數值類型成員被系統初始化為 0,字符型成員被初始化為 \0,指針型成員被初始化為 NULL。
單獨引用結構體變量中某一成員的值,其方法為:
結構體變量名.成員名
如上面程序中的 a.id 和 a.name 等。也可以在程序中對這些成員賦值:
b.age = 19;
注意:不同企圖通過輸出結構體變量名來達到輸出結構體變量所有成員的值!只能逐一對各個成員進行輸入輸出。
如果成員本身又是一個結構體類型,則要用若干個成員運算符,一級一級找到最低一級的成員。只能對最低一級的成員進行賦值、存取和運算。但同類的結構體變量可以相互賦值,如:
student1 = student2; // student1 和 student2 都為 struct Student 類型變量
二、結構體數組
一個結構體變量中可以存放一組有關聯的數據,如果有多組這樣的數組需要參加運算,顯然應該使用數組,這就是結構體數組。結構體數組的每個元素都是一個結構體類型的數據,都分別包括各個成員項。
定義結構體數組的一般形式為:
struct 結構體名 {
成員表列
} 數組名[數組長度];
對結構體數組的初始化可以在定義數組後面加上一個初值表列。
struct Student {
char name[20];
int age;
} class[3] = {"liu", 19, "yuxin", 20};
三、結構體指針
1. 指向結構體變量的指針
結構體指針就是指向結構體變量的指針,即結構體變量的起始地址。指向結構體對象的指針變量可以既可以指向結構體變量,也可以指向結構體數組中的元素,其基類型必須與結構體變量的類型相同。
struct Student* p; // p 可以指向 struct Student 類型的變量或數組元素
當 p 指向一個結構體變量 stu 時,以下 3 種用法等價:
stu.成員名如:stu.name。(*p).成員名如:(*p).name。p->成員名如:p->name。
2. 指向結構體數組的指針
可以用指針變量指向結構體數組的元素。
#include <stdio.h>
struct Student {
char name[20];
int age;
};
struct Student class[3] = {{"liu", 19}, {"yu", 20}, {"xin", 21}};
int main() {
struct Student* p;
printf(" Name Age\n");
for (p = class; p < class + 3; p++)
printf("%5s%8d\n", p->name, p->age);
return 0;
}
// Name Age
// liu 19
// yu 20
// xin 21
3. 用結構體變量和結構體變量的指針做函數參數
將一個結構體變量的值傳遞給一個函數由 3 個方法:
- 用結構體變量的成員做實參,傳遞給形參。用法與普通變量做形參一致,屬於“值傳遞”的方式。
- 用結構體變量做實參,形參也必須是同類型的結構體變量,也屬於“值傳遞”方式。這種方式在時間和空間上的開銷較大,且如果在執行被調用函數過程中該變量形參的值,該值不能返回主調函數,往往造成使用上的不便,因此較少使用。
- 用指向結構體變量的(或數組元素)的指針做實參,將結構體變量(或數組元素)的地址傳給形參。
下面的程序實現了輸入 3 個學生 3 門課程的成績,輸出平均成績最高的學生信息:
#include <stdio.h>
#define N 3 // 學生數為 3
struct Student { // 建立結構體類型
char name[20];
float score[3];
float aver;
};
int main() {
void input(struct Student stu[]);
struct Student max(struct Student stu[]);
void print(struct Student stud);
struct Student stu[N], *p = stu;
input(p);
print(max(p));
return 0;
}
void input(struct Student stu[]) { // 輸入學生姓名、3 門課成績
int i;
for (i = 0; i < N; i++) {
scanf("%s %f %f %f", stu[i].name, &stu[i].score[0], &stu[i].score[1],
&stu[i].score[2]);
stu[i].aver =
(stu[i].score[0] + stu[i].score[1] + stu[i].score[2]) / 3.0;
}
}
struct Student max(struct Student stu[]) { // 求平均成績最高的學生
int i, m = 0;
for (i = 0; i < N; i++) {
if (stu[i].aver > stu[m].aver)
m = i;
}
return stu[m];
}
void print(struct Student stud) { // 輸出成績最高的學生的信息
printf("Name: %s\nScore: %2.lf, %2.lf, %2.lf\nAver: %2.lf\n", stud.name,
stud.score[0], stud.score[1], stud.score[2], stud.aver);
}
四、用指針處理鏈表
1. 鏈表
鏈表是一種常見的重要的數據結構,它是動態地進行存儲分配的一種結構。用數組存放數據時,必須事先定義固定的數組長度(即元素個數),這樣有時會浪費內存。鏈表則沒有這種缺點,它根據需要開闢內存單元。下圖表示最簡單的一種鏈表(單向鏈表)的結構。
鏈表有一個“頭指針”變量(圖中的 head),它存放一個地址,該地址指向一個元素。鏈表中每一個元素稱為“結點”,每個結點都應包括兩個部分:
- 用户需要用的實際數據;
- 下一個結點的地址。
可以看出,head 指向第 1 個元素,第 1 個元素又指向第 2 個元素……直到最後一個元素,該元素不再指向其他元素,它稱為“表尾”,它的地址部分放一個空地址 NULL,鏈表到此結束。
鏈表中各元素在內存種的地址可以是不連續的。要找到某一元素,必須先找到上一個元素,根據它提供的地址才能找到下一個元素。如果不提供頭指針 head,則整個鏈表都無法訪問。
顯然,鏈表這種數據結構,必須使用指針才能實現,即一個結點種應包含一個指針變量,用它存放下一結點的地址。用結構體變量去建立鏈表是最合適的。在結構體變量中使用指針型成員來存放下一個結點的地址(一個指針類型的成員既可以指向其他類型的結構體數據,也可以指向自己所在的結構體類型數據)。如:
struct Student {
char name[20];
int age;
struct Student* next; // 指針變量,指向結構體變量
};
上面的程序中,name 和 age 用來存放結點中的有用數據。next 是 struct Student 類型中的成員,而它又指向自己所在的結構體類型的數據。用這種方法就建立了一個鏈表。
2. 建立簡單的靜態鏈表
下面的程序建立和輸出了一個簡單鏈表:
#include <stdio.h>
#include <string.h>
struct Student {
char name[20];
int age;
struct Student* next;
};
int main() {
struct Student a, b, c, *head, *p;
strcpy(a.name, "liu");
a.age = 18;
strcpy(b.name, "yu");
b.age = 19;
strcpy(c.name, "xin");
c.age = 20;
head = &a;
a.next = &b;
b.next = &c;
c.next = NULL;
p = head;
do {
printf("Name: %s\tAge: %d\n", p->name, p->age);
p = p->next;
} while (p != NULL);
return 0;
}
// Name: liu Age: 18
// Name: yu Age: 19
// Name: xin Age: 20
3. 建立動態鏈表
建立動態鏈表是指在程序執行過程中從無到有地建立起一個鏈表,即逐個地開闢結點和輸入各結點數據,並建立起前後相鏈的關係。這需要用到08 - 指針中介紹的動態內存分配及其有關函數。
#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct Student)
struct Student {
long id;
int age;
struct Student* next;
};
int n;
struct Student* creat(void) { // 建立鏈表
struct Student *head, *p1, *p2;
n = 0;
p1 = p2 = (struct Student*)malloc(LEN);
scanf("%ld %d", &p1->id, &p1->age);
head = NULL;
while (p1->id != 0) {
n = n + 1;
if (n == 1)
head = p1;
else
p2->next = p1;
p2 = p1;
p1 = (struct Student*)malloc(LEN);
scanf("%ld %d", &p1->id, &p1->age);
}
p2->next = NULL;
return (head);
}
void print(struct Student* head) { // 輸出鏈表
struct Student* p = head;
if (head != NULL) {
do {
printf("\nID: %ld\tAge: %d", p->id, p->age);
p = p->next;
} while (p != NULL);
}
}
int main() {
struct Student* head;
head = creat();
print(head);
return 0;
}
// 001 18
// 002 19
// 003 20
// 0 0
//
// ID: 1 Age: 18
// ID: 2 Age: 19
// ID: 3 Age: 20
五、共用體類型
1. 共用體類型
有時候想用一段內存單元存放不同類型的數據,這些數據佔用的字節數可能不同,但都從同一地址開始存放,也就是使用覆蓋技術,後一個數據覆蓋前一個數據。這種使幾個不同的變量共享同一段內存的結構,稱為共用體類型結構。也有譯為“聯合”。
定義共用體類型的一般結構為:
union 共用體名 {
成員表列
} 變量表列;
與結構體類型相似,共用體類型也可以定義類型的同時定義變量、先定義類型後定義變量、直接定義變量。
// 定義類型的同時定義變量
union Data {
int i;
char j;
float k;
} a, b, c;
// 先定義類型後定義變量
union Data {
int i;
char j;
float k;
};
union Data a, b, c;
// 直接定義變量
union {
int i;
char j;
float k;
} a, b, c;
注意:結構體變量所佔內存長度是各成員所佔內存長度之和;共用體變量所佔內存長度等於最長成員的長度。
2. 引用共用體變量
共用體變量只有定義之後才能引用,但不能引用它本身,只能引用其中的成員。引用方法結構體變量相似:
共用體變量名.成員名
3. 共用體類型的特點
共用體類型數據有以下特點:
-
同一個內存段可以用來存放幾種不同類型的成員,但在每一瞬時只能存放其中一個成員,而不是同時存放幾個。
union Data { int i; char j; float k; } a; a.i = 97; printf("%d %c %f", a.i, a.j, a.k); // 97 a 0.000000 -
可以對共用體類型初始化,但初始化表中只能有一個常量。
union Data { int i; char j; float k; } a = {1, 'a', 1.5}; // 錯誤 union Data a = {16} // 正確 union Data a = {.j='a'} // 正確 - 共用體變量中起作用的成員是最後一次被賦值的成員,在對共用體變量中的一個成員賦值後,原有變量存儲單元中的值就被取代。
- 共用體變量的地址和它各成員的地址都是同一地址。
- 不能對共用體變量名賦值,也不能企圖引用變量名來得到一個值。C 99 允許同類型的共用體變量相互賦值。
- C 99 允許使用共用體變量及指向共用體變量的指針做函數參數。
- 共用體類型可以出現在結構體類型定義中,也可以定義共用體數組。反之,結構體也可以出現在共用體類型定義中,數組也可以作為共用體的成員。
六、枚舉類型
如果一個變量只有幾種可能的值,則可以定義為枚舉(enumeration)類型,所謂“枚舉”就是指把可能的值一一列舉出來,變量的值只限於列舉出來的值的範圍內。
定義枚舉類型的一般形式為:
enum 枚舉名 {枚舉元素};
枚舉元素也稱為枚舉常量。
可以先定義枚舉類型,再定義枚舉變量,如:
enum Weekday { sun, mon, tue, wed, thu, fri, sat };
enum Weekday workday, weekend;
也可以不聲明枚舉類型名,直接定義枚舉變量:
enum { sun, mon, tue, wed, thu, fri, sat } workday, weekend;
枚舉變量的值只能限於定義中的枚舉元素的值:
workday = mon; // 正確
workday = monday; // 錯誤
需要注意的是:
- C 編譯對枚舉類型的枚舉元素按常量處理,故稱枚舉常量。不能因為它們是標識符(有名字)而把它們看作變量,不能對它們賦值。
-
每一個枚舉元素都代表一個整數,C 語言編譯按定義時的順序默認它們的值為 0,1,2,3,4……。在上面的定義中,
sun的值自動設為 0,mon的值為 1,……sat的值為 6。因此下面兩個語句等價:workday = mon; workday = 1; -
枚舉常量可以引用、輸出、判斷,還可以人為指定枚舉常量的數值:
enum Weekdaty { sun = 7, mon = 1, tue, wed, thu, fri, sat } workday; workday = sun; if (workday > mon) printf("%d", workday); // 7
七、聲明新類型名
除了使用 C 提供的標準類型和自定義的結構體、共用體、枚舉類型外,還可以使用 typedef 指定新的類型名來代替已有的類型名。
1. 用新類型名代替原有類型名
typedef int Integer; // 指定用 Integer 代表 int
typedef float Real; // 指定用 Real 代表 float
2. 用簡單類型名代替複雜類型名
定義一個簡單類型名代替複雜類型名的方法是:按定義變量的方式,把變量名換為新類型名,並且在最前面加 typedef,就聲明瞭一個新類型名代表原來的類型。
命名一個新的類型名代表結構體類型:
typedef struct {
char name[20];
int age;
} Data; // 指定用 Data 代表結構體類型
Data stu1, stu2; // 用新類型名定義變量
命名一個新的類型名代表數組類型:
typedef int Num[100]; // 聲明 Num 為整型數組類型名
Num a; // 用新類型名定義一個整形數組
命名一個新類型名代表指針類型:
typedef char* String; // 聲明 String 為字符指針類型
String p, s[10]; // 定義一個字符指針變量和一個字符指針數組
命名一個新類型名代表指向函數的指針類型:
typedef int (*Pointer)(); // 聲明 Pointer 為指向函數的指針類型
Pointer p1, p2; // 定義 p1,p1 為 Pointer 類型的指針變量
不同源文件中用到同一類型數據(尤其是像數組、指針、結構體、共用體等類型數據)時,常用 typedef 聲明一些數據類型。可以把所有的 typedef 名稱聲明單獨放在一個頭文件中,然後在需要用到它們的文件中用 #include 指令把它們包含到文件中。這樣編程者就不需要在各文件中自己定義 typedef 名稱了。該方法有利於程序的通用與移植,降低了程序對硬件特性的依賴性。
Reference:
譚浩強《C程序設計(第五版)》