结构体
# 结构体
有时候需要将不同类型的数据组合为一个整体,以便于引用。例如,一名学生有学号、姓名、性别、年龄、地址等属性,如果针对学生的学号、姓名、年龄等都单独定义一个变量,那么在有 多名学生时,变量就难以分清。为此,C 语言提供结构体来管理不同类型的数据组合。
# 结构体与结构体指针
# 结构体的定义、引用、初始化
声明一个结构体类型的一般形式为
// struct 结构体名 {成员表列};
struct student {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
2
3
4
5
6
7
8
9
10
先声明结构体类型,再定义变量名。例如,
struct student student1;
结构体的 scanf 读取和输出。
#include <stdio.h>
#include <stdlib.h>
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
}; //结构体类型声明,注意最后一定要加分号
int main()
{
struct student s = {1001, "lele", 'M', 20, 85.4, "Shenzhen"}; //定义及初始化
struct student sarr[3];
int i;
printf("%d %s %c %d %f %s\n", s.num, s.name, s.sex, s.age, s.score, s.addr);
for (i = 0; i < 3; i++)
{
scanf("%d%s %c%d%f%s", &sarr[i].num, sarr[i].name, &sarr[i].sex, &sarr[i].age, &sarr[i].score, sarr[i].addr);
}
for (i = 0; i < 3; i++)
{
printf("%d %s %c %d %f %s\n", sarr[i].num, sarr[i].name, sarr[i].sex, sarr[i].age, sarr[i].score, sarr[i].addr);
}
system("pause");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
结构体类型声明要放在 main 函数之前,这样 main 函数中才可以使用这个结构体,工作中往往把结构体声明放在头文件中。注意,结构体类型声明最后一定要加分号,否则会编译不通。另 外,定义结构体变量时,使用 struct student 来定义,不能只有 struct 或 student,否则也会 编译不通,sarr 是结构体数组变量。
结构体的初始化只能在一开始定义,如果 struct student s={1001,"lele",'M',20,85.4,"Shenzhen"} 已经执行,即 struct student s 已经定义,就不能 再执行 s={1001,"lele",'M',20,85.4,"Shenzhen"}。如果结构体变量已经定义,那么只能对它 的每个成员单独赋值,如 s.num=1003。
采用 “结构体变量名。成员名” 的形式来访问结构体成员,例如用 s.num 访问学号。在进行打印输出时,必须访问到成员,而且 printf 中的 % 类型要与各成员匹配。使用 scanf 读取标准输 入时,也必须是各成员取地址,然后进行存储,不可以写成 & s,即不可以直接对结构体变量取地 址。整型数据(% d)、浮点型数据(% f)、字符串型数据(% s)都会忽略空格,但是字符型数据(% c) 不会忽略空格,所以如果要读取字符型数据,那么就要在待读取的字符数据与其他数据之间加入空格。
# 结构体指针
一个结构体变量的指针就是该变量所占据的内存段的起始地址。可以设置一个指针变量,用它指向一个结构体变量,此时该指针变量的值是结构体变量的起始地址。指针变量也可以用来指向结构体数组中的元素。
#include <stdio.h> #include <stdlib.h>
//结构体指针
struct student
{
int num;
char name[20];
char sex;
};
int main()
{
struct student s = {1001, "wangle", 'M'};
struct student sarr[3] = {1001, "lilei", 'M', 1005, "zhangsan", 'M', 1007, "lili", 'F'};
struct student *p; //定义结构体指针
int num;
p = &s;
printf("%d %s %c\n", p->num, p->name, p->sex);
p = sarr;
printf("%d %s %c\n", (*p).num, (*p).name, (*p).sex); //方式一获取成员
printf("%d %s %c\n", p->num, p->name, p->sex); //方式二获取成员
printf("------------------------------\n");
num = p->num++;
printf("num=%d,p->num=%d\n", num, p->num);
num = p++->num;
printf("num=%d,p->num=%d\n", num, p->num);
system("pause");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# typedef 的使用
前面定义结构体变量时使用的语句是 struct student s,以这种方式来定义结构体变量有些麻烦,即每次都需要写 struct student。那么有没有简单一些的定义方式,可以选择使用 typedef 声明新的类型名来代替已有的类型名。
typedef struct student //使用 typedef 定义别名,结构体的名称也可以省略,因为定义了别名,所以结构体名称可以不定义。
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
}student_t; // 在分号前指定别名
//结构体指针
typedef struct student
{
int num;
char name[20];
char sex;
} stu, *pstu; //pstu 等价于 struct student*
typedef int INTEGER;
int main(){
student_t s = {1001, "lele", 'M', 20, 85.4, "Shenzhen"};
stu s = {1001, "wangle", 'M'};
pstu p;
INTEGER i = 10;
p = &s;
printf("i=%d,p->num=%d\n", i, p->num);
system("pause");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
使用 stu 定义结构体变量和使用 struct student 定义结构体变量是等价的;使用 INTEGER 定义变量 i 和使用 int 定义变量 i 是等价的;pstu 等价于 struct student*,所以 p 是结构体指针变量。
# 链表的增删查改
# 链表
链表是一种动态地进行存储分配的常见的、重要的数据结构。链表中的头指针存放一个地址,该地址指向一个元素结点 (包含用户需要的实际数据和链接结点的地址),这样我们就只需定义一个头指针,即可不断地增加结点,最后一个结点内的指针要存储为 NULL,用于判断是否到达链表尾部。
如下图所示,链表的头指针 head 只需存储第一个结点的首地址,通过遍历,即可访问每个元素。

用结构体建立链表的格式如下:
struct student
{
int num;
float score;
struct student *next;
};
2
3
4
5
6
其中结构体成员 num 和 score 用来存放结点中的有用数据(用户需要用到的数据),next 是指针 类型的成员,它指向 struct student 类型数据(就是 next 所在的结构体类型),如下图所示。

# 增删查改链表
链表结点的插入方法分为尾插法(即尾部插入法)、头插法(即头部插入法)和有序插入法。为了高效地实现链表的增删查改操作,往往使用两个指针进行操作
- 头插法:新建结点,插入的值进行初始化,判断链表是否为空,如何为空,新结点赋值给头指针,尾指针,如果不为空,新结点的 next 指向原有头结点,新结点作为头结点
- 尾插法:新建结点,插入的值进行初始化,判断链表是否为空,如何为空,新结点赋值给头指针,尾指针,如果不为空,尾节点的 next 指针指向新结点,新结点作为尾结点
- 有序插入:新建结点,插入的值进行初始化,判断链表是否为空,如何为空,新结点赋值给头指针,尾指针, 如果不为空,如果头结点的值大于要插入的值,头插法,如果没有则遍历链表,找到比插入值大的结点位置,新结点的 next 指针指向当前结点,前一个结点的 next 指针指向新结点。 如果没有找到插入位置,则说明链表中没有比插入值大的结点,此时直接使用尾插法,放置到尾部。
list.h
#include <stdio.h>
#include <stdlib.h>
typedef struct student {
int num;
float score;
struct student *pNext;
} Student_t, *pStudent_t;
// 链表头插法
void listHeadInsert(pStudent_t *ppHead, Student_t **ppTail, int val);
// 链表尾插法
void listTailInsert(pStudent_t *ppHead, pStudent_t *ppTail, int val);
// 链表有序插入
void listSortInsert(pStudent_t *ppHead, pStudent_t *ppTail, int val);
// 打印链表
void listPrint(pStudent_t pHead);
// 链表删除节点操作
void listDelete(pStudent_t *ppHead, pStudent_t *ppTail, int val);
// 链表根据查找序号修改成绩操作
void listModify(pStudent_t pHead, int, float);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
list.c
#include "list.h"
// 链表头插法 形参写 pStudent_t *ppHead 和 Student_t **ppTail 都可以
// 前者为结构体的指针的指针,后者为结构体地址指针的指针,同为二级指针。
void listHeadInsert(pStudent_t *ppHead, Student_t **ppTail, int val) {
// calloc(size_t num, size_t size) 参数1为申请多少个指定类型的空间,参数2为单个指定类型的大小
// 总申请空间 = num * size,再强转为结构体指针类型,返回指向申请空间的起始地址
pStudent_t pNew = (pStudent_t)calloc(1, sizeof(Student_t));
pNew->num = val;
//判断链表是否为空
if (*ppHead == NULL) {
*ppHead = pNew;
*ppTail = pNew;
} else {
//头插法 将新增元素置为头节点
pNew->pNext = *ppHead;
*ppHead = pNew;
}
}
//打印链表
void listPrint(pStudent_t pHead) {
while (pHead) {
printf("%3d %5.2f\n", pHead->num, pHead->score);
pHead = pHead->pNext;
}
}
// 链表尾插法
void listTailInsert(pStudent_t *ppHead, pStudent_t *ppTail, int val) {
pStudent_t pNew = (pStudent_t)calloc(1, sizeof(Student_t));
pNew->num = val;
//判断链表是否为空
if (*ppHead == NULL) {
*ppHead = pNew;
*ppTail = pNew;
} else {
//尾插法 将新增元素置为尾节点
(*ppTail)->pNext = pNew;
*ppTail = pNew;
}
}
// 链表有序插入
void listSortInsert(pStudent_t *ppHead, pStudent_t *ppTail, int val) {
pStudent_t pNew = (pStudent_t)calloc(1, sizeof(Student_t));
pStudent_t pCur; // 当前节点
pStudent_t pPre; // 前置节点
pCur = pPre = *ppHead; // 都先指向头节点
pNew->num = val;
//判断链表是否为空 如为空直接置为头节点
if (*ppHead == NULL) {
*ppHead = pNew;
*ppTail = pNew;
} else if (pCur->num > val) {
// 当前节点为头节点
// 判断当前节点(头节点)是否小于将要插入的节点值,小于直接插入到头部,因为是有序小到大,反之倒序需要检查tail尾节点
pNew->pNext = *ppHead;
*ppHead = pNew;
} else {
// 将要插入节点的值大于当前节点(头节点),需要对节点进行遍历
while (pCur) {
// 找到比将要插入的节点大的当前节点
if (pCur->num > val) {
//将它的前置节点设置为将要插入节点
pPre->pNext = pNew;
//再将插入节点的后置节点设置为当前节点 即直接在前置和当前节点中间插入pNew节点
pNew->pNext = pCur;
//记得break
break;
}
//当前节点还是比pNew小,则继续遍历,将前置节点设置为当前节点
pPre = pCur;
//再将当前节点往后移
pCur = pCur->pNext;
}
// 如果当前节点为null,说明链表中没有比将要插入的值大,直接放置于前置节点的后置节点(即尾部)
if (pCur == NULL) {
//当前pPre为链表尾部,pCur已经越界,直接将pPre的后置节点设置为pNew
pPre->pNext = pNew;
// 记得更新尾部指针
*ppTail = pNew;
}
}
}
// 链表删除节点操作
void listDelete(pStudent_t *ppHead, pStudent_t *ppTail, int val) {
pStudent_t pCur = *ppHead;
pStudent_t pPre = *ppHead;
if (pCur == NULL) {
printf("链表为空\n");
} else if (pCur->num == val) {
// 删除节点是头节点
*ppHead = pCur->pNext;
free(pCur); // 释放空间
pCur = NULL; // 防止指针变量成为野指针 指向已经free的地址
// 如果头节点的后置节点也为NULL,说明当前链表就只有一个节点(即头尾节点都是一个元素),所以把尾节点也赋值为NULL
if (*ppHead == NULL) {
*ppTail = NULL;
}
} else {
//不是头节点 则需要遍历链表
// 删除是中间的节点
while (pCur) {
//遍历的当前节点是 要删除节点
if (pCur->num == val) {
//当前节点的前置节点的next指向后置节点,跳过当前节点
pPre->pNext = pCur->pNext;
break;
}
//前置节点更新
pPre = pCur;
//当前节点更新
pCur = pCur->pNext;
}
if (pCur == NULL) {
printf("在链表没有找到将要删除的节点\n");
return;
}
if (pCur == *ppTail) {
*ppTail = pPre;
}
free(pCur);
pCur = NULL;
}
}
// 链表根据查找序号修改成绩操作
void listModify(pStudent_t pHead, int num, float score) {
while (pHead) {
if(pHead->num == num){
pHead->score=score;
break;
}
pHead = pHead->pNext;
}
if(pHead == NULL){
printf("没有该序号的节点,请重新输入。");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
main.c
#include "list.h"
int main() {
pStudent_t phead = NULL, ptail = NULL;
int num;
float score;
// 链表构建
while (scanf("%d", &num) != EOF) {
// 头插法
// listHeadInsert(&phead, &ptail, num);
// 尾插法
// listTailInsert(&phead, &ptail, num);
// 有序插入
listSortInsert(&phead, &ptail, num);
}
listPrint(phead);
// fflush(stdout);// 刷新标准输出
// 链表删除节点操作
// while(printf("输入要删除元素的值:"),fflush(stdout),scanf("%d",&num) != EOF){
// listDelete(&phead, &ptail, num);
// listPrint(phead);
// }
// 链表根据查找序号修改成绩操作
while (printf("输入要修改元素序号和成绩的值:"), fflush(stdout), scanf("%d%f", &num,&score) != EOF) {
listModify(phead, num,score);
listPrint(phead);
}
system("pause");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 共用体与枚举
# 共用体
使几个不同的变量共占同一段内存的结构称为共用体结构,共用体变量所占的内存长度等于最长的成员的长度,如下图所示。

定义共用体类型变量的一般形式如下:
union 共用体名 {
成员表列
}变量表列;
2
3
定义共用体变量的方法如下:
union data {
int i;
char ch;
float f;
}a,b,c;
//或者
union data {
int i;
char ch;
float f;
};union data a,b,c;
2
3
4
5
6
7
8
9
10
11
如果共用体变量 a 的起始地址为 1000,那么共用体内成员 i、ch、f 的起始地址均为 1000。
- 同一段内存可以用来存放几种不同类型的成员,但在每个瞬时只能存放其中的一种,而不能同时存放几种。
- 共用体变量中起作用的成员是最后一次存入的成员,存入一个新成员后,原来起作用的成员就失去了作用。
- 共用体变量的地址和它的各成员的地址都是同一地址。
- 不能对共用体变量名赋值,也不能企图引用共用体变量名来得到一个值。
- 共用体类型可以出现在结构体类型定义中,也可以定义共用体数组;结构体可以出现在共用体类型定义中,数组也可以作为共用体的成员。
# 枚举
枚举就是将变量的值一一列举出来,变量的值只限于在所列举的值的范围内。
声明枚举类型时要使用 enum 函数。例如:
enum weekday{sun,mon,tue,wed,thu,fri,sat};
定义变量的例子如下:
enum weekday workday,week-day;
enum{sun,mon,tue,wed,thu,fri,sat}weekday; //默认从0-N开始
enum{sun=3,mon,tue,wed,thu,fri,sat}week-day; //起始从3开始一直到N+3;即3,4,5,6,7,8,9
enum{sun,mon,tue,wed=77,thu,fri,sat}workday; //从0开始直到到77的位置从77开始;即0,1,2,77,78,79,80
int main(){
enum weekday workday;
workday=mon;//赋值操作默认为下标1赋值
print("workday=%d\n",workdy);//"workday=1
}
2
3
4
5
6
7
8
9
10
- C 语言在编译时对枚举元素是按常量处理的,故称枚举常量。因为它们不是变量,故不能对它们赋值。
- 枚举元素作为常量是有值的,C 语言编译时,按定义的顺序使它们的值为 0,1, 2,...。
- 枚举值可以用来做判断和比较。
- 整数一般不直接赋给一个枚举变量,而要通过把枚举常量赋给枚举变量。
← 函数 常用的数据结构与算法→