- 结构体的定义和使用 {#1-结构体的定义和使用} ===========================
在前面的章节中学习了数组,它描述的是一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。
有时我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号/姓名/性别/年龄/地址
等属性。显然单独定义以上变量比较繁琐,数据不便于管理。
C语言中给出了另一种构造数据类型------结构体。
1.1 定义和初始化 {#1-1-定义和初始化}
声明一个结构体类型的语法是:
|---------------------|----------------------------------------------------|
| 1 2 3 4 5 6
| struct 结构体名 { 成员类型 成员名1; 成员类型 成员名2; ... };
|
结构体类型是一种复合类型,说得更直白一些就是自定义类型,通过上面的语法将结构体类型定义出来之后,就可以基于这种类型定义变量了。
定义结构体变量的语法为:
|-----------|--------------------------|
| 1
| struct 结构体名 变量名;
|
在一个结构体变量内部有若干个成员,访问结构体成员的语法为:
|-----------|--------------------|
| 1
| 结构体变量名.成员名
|
其实,定义结构体变量有三种方式,如下图:
-
先声明结构体类型,再定义相应的变量
-
在声明类型的同时定义变量
-
直接定义结构体类型变量(无类型名)
结构体类型和结构体变量关系:
-
结构体类型
:指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元
。 -
结构体变量
:系统根据结构体类型(内部成员状况)为之分配空间
。
|---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| // 结构体类型的定义 struct Student { char name[50]; int age; }; // 先定义类型,再定义变量(常用) struct Student s1 = { "tom", 18 }; // 定义类型同时定义变量 struct Student2 { char name[50]; int age; }s2 = { "lily", 22 }; struct { char name[50]; int age; }s3 = { "jack", 25 };
|
上面的示例代码中,基于三种方式定义了三个结构体变量s1、s2、s3
,并通过{}
对结构体变量中的各个成员依次进行了初始化。
1.2 成员访问 {#1-2-成员访问}
在一个结构体变量内部有若干个成员,访问结构体成员的语法为:
|-----------|--------------------|
| 1
| 结构体变量名.成员名
|
通过上面的方式我们就可以读出结构体变量中各个成员的值,或者给结构体变量的成员进行赋值操作。
|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <stdio.h> #include <string.h> //结构体类型的定义 struct Student { char name[50]; int age; }; int main() { struct Student s1; // 如果是普通变量,通过点运算符操作结构体成员 strcpy(s1.name, "luffy"); s1.age = 18; printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age); // 结构体变量赋值 struct Student s2 = s1; printf("s2.name = %s, s2.age = %d\n", s2.name, s2.age); return 0; }
|
相同类型的两个结构体变量,可以相互赋值,上面代码中把s1
变量各个成员的值拷贝到了s2
变量的各个成员对应的内存中。s1
和s2
只是成员变量的值一样而已,它们对应的是不同的两块内存。
1.3 嵌套使用 {#1-3-嵌套使用}
由于结构体是一种复合类型,有时候需要通过一个结构体来描述相对复杂的数据,此时可能就需要让多个结构体之间进行嵌套。
|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 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
| #include <stdio.h> #include <string.h> struct Person { char name[20]; char sex; // M-Man, W-Woman }; struct Student { int id; struct Person info; }; int main() { struct Student s; s.id = 1; s.info.sex = 'W'; strcpy(s.info.name, "Lucy"); struct Student s1 = { 2, "Tom", 'M'}; printf("s.id = %d, s.info.name=%s, s.info.sex=%c\n", s.id, s.info.name, s.info.sex); printf("s1.id = %d, s1.info.name=%s, s1.info.sex=%c\n", s1.id, s1.info.name, s1.info.sex); return 0; }
|
在上面的示例代码中,每个Student
变量中都包含一个Person
变量,二者之间是父子节点的关系。
- 结构体数组和指针 {#2-结构体数组和指针} =========================
2.1 结构体数组 {#2-1-结构体数组}
如果想要记录某个学生的信息可以使用结构体,如果想要记录班级或者小组的学生信息就需要定义多个结构体变量,此时就可以使用数组。
下面的示例程序中使用结构体数组
对小组中的学生成绩进行了统计:
|------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 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
| // 统计学生成绩 struct Student { int num; char name[20]; char sex; float score; }; int main() { //定义一个含有5个元素的结构体数组并将其初始化 struct Student stu[5] = { { 101, "Li ping", 'M', 45 }, { 102, "Zhang ping", 'M', 62.5 }, { 103, "He fang", 'W', 93 }, { 104, "Cheng ling", 'W', 87 }, { 105, "Wang ming", 'M', 58 } }; int loser = 0; float avg, total = 0; for (int i = 0; i < 5; i++) { total += stu[i].score; // 计算总分 if (stu[i].score < 60) { loser += 1; // 统计不及格人的分数 } } printf("total = %.2f\n", total);// 打印总分数 avg = total / 5; // 计算平均分数 printf("average = %.2f, loser count = %d\n", avg, loser); for (int i = 0; i < 5; i++) { printf(" name = %s, score = %.2f\n", stu[i].name, stu[i].score); } return 0; }
|
结构体数组的使用方式和非结构体数组相同,都是通过[]
对数组中的元素进行访问,只不过结构体数组元素更加复杂,还需要使用该元素再通过.
的方式对结构体成员进行数据的读或写操作。
2.2 结构体指针 {#2-2-结构体指针}
2.2.1 普通结构体指针 {#2-2-1-普通结构体指针}
如果定义了一个结构体变量,我们可以再定义一个相同类型的指针
,使用这个指针指向结构体变量的地址,示例代码如下:
|---------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include<stdio.h> //结构体类型的定义 struct Student { char name[50]; int age; }; int main() { struct Student s1 = { "lily", 18 }; // 如果是指针变量,通过->操作结构体成员 struct Student* p = &s1; printf("p->name = %s, p->age=%d\n", p->name, p->age); printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age); return 0; }
|
关于结构体内部成员的访问有两种情况:
- 基于结构体
变量
进行访问,语法为:变量名.成员名
- 基于结构体
指针
进行访问,语法为:指针名->成员名
如果对结构体指针通过*
进行了解引用(比如代码中的 *p
),那么它们的组合将不再是指针,在访问结构体成员的时候需要通过.
进行操作(比如:(*p).name
)
2.2.2 堆区结构体指针 {#2-2-2-堆区结构体指针}
如果在函数体内部定义结构体变量,那么该变量对应的内存位于栈区,如果在函数体外部定义结构体变量,那么该变量对应的内存位于全局数据区,除此之外我们还可以在函数体内部使用malloc
动态申请一块堆区内存,并将这块内存的地址保存到一个指针变量中。
下面的示例代码给大家演示了如何给结构体动态申请一块堆内存:
|---------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 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
| #include <stdio.h> #include <string.h> #include <stdlib.h> //结构体类型的定义 struct Student { char name[50]; int age; }; int main() { struct Student* p; p = (struct Student*)malloc(sizeof(struct Student)); // 如果是指针变量,通过->操作结构体成员 strcpy(p->name, "Monkey·D·Luffy"); p->age = 22; printf("p->name = %s, p->age = %d\n", p->name, p->age); printf("(*p).name = %s, (*p).age = %d\n", (*p).name, (*p).age); free(p); p = NULL; return 0; }
|
在C语言中,通过malloc/calloc/realloc
函数动态申请的堆内存是不会自动释放的,在使用完毕之后需要手动进行释放,释放函数是free
。
2.2.3 结构体嵌套指针 {#2-2-3-结构体嵌套指针}
在定义结构体的时候,结构体成员可以是基础数据类型,可以是结构体类型,也可以是指针。如果是指针,它自身占4字节或者8字节的存储空间并且指针指向的位置是随机的,在使用的时候需要让它指向一个正确的、合法的地址。
下面是一段结构体嵌套指针的示例代码:
|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 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
| #include <stdio.h> #include <string.h> #include <stdlib.h> //结构体类型的定义 struct Student { char* name; // 一级指针 int age; }; int main() { char* str = "Monkey·D·Luffy"; struct Student* p = (struct Student*)malloc(sizeof(struct Student)); p->name = (char*)malloc(strlen(str) + 1); strcpy(p->name, "Monkey·D·Luffy"); p->age = 22; printf("p->name = %s, p->age = %d\n", p->name, p->age); printf("(*p).name = %s, (*p).age = %d\n", (*p).name, (*p).age); if (p->name != NULL) { free(p->name); p->name = NULL; } if (p != NULL) { free(p); p = NULL; } return 0; }
|
在上面的程序中,一共手动申请了两块堆内存:
结构体指针 p
,通过malloc
操作,该指针指向的内存大小为4+4=8
字节- age 整形变量,4字节
- name 整形指针,Win32平台4字节(Win64平台8字节),不能存储数据
结构体成员 p->name
,如果想要通过指针来存储数据,那么指针必须要指向一块有效的存储空间,这块存储空间默认是没有的,所以需要再通过malloc
分配出这块堆内存,并把得到的地址保存到p->name
指针中。
所以在释放内存的时候,需要对这两个指针依次进行判断,然后再调用free
函数,手动的将堆内存还给操作系统。
- 结构体做函数参数 {#3-结构体做函数参数} =========================
3.1 普通结构体变量 {#3-1-普通结构体变量}
在定义函数的时候,可以将形参指定为结构体类型,示例代码如下:
|------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 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
| #include <stdio.h> #include <string.h> // 结构体类型的定义 struct Student { char name[50]; int age; }; //函数参数为结构体普通变量 void initStudent(struct Student tmp) { strcpy(tmp.name, "robin"); tmp.age = 35; printf("tmp.name = %s, tmp.age = %d\n", tmp.name, tmp.age); } int main() { struct Student s = { "Luffy", 20 }; initStudent(s); printf("s.name = %s, s.age = %d\n", s.name, s.age); return 0; }
|
程序输出的结果如下:
|-------------|-------------------------------------------------------------------|
| 1 2
| tmp.name = robin, tmp.age = 35 s.name = Luffy, s.age = 20
|
上面程序中定义了一个函数initStudent(struct Student tmp)
,通过该函数来设置结构体变量中的初始值,但是通过输出的信息可以看出外部变量s
中的值并没有被修改,原因是这样的:结构体变量的传递方式是值传递,值传递过程中会发生数据的拷贝,在 initStudent 函数内部修改是形参对应的内存中的值,对实参变量的内存数据没有任何影响,想要解决这个问题需要将参数修改为结构体指针类型。
3.2 结构体指针变量 {#3-2-结构体指针变量}
修改一下上面的代码,将参数的类型由结构体类型替换为结构体指针类型:
|---------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 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
| #include <stdio.h> #include <string.h> // 结构体类型的定义 struct Student { char name[50]; int age; }; //函数参数为结构体普通变量 void initStudent(struct Student* tmp) { strcpy(tmp->name, "robin"); tmp->age = 35; printf("tmp.name = %s, tmp.age = %d\n", tmp->name, tmp->age); } int main() { struct Student s = { "Luffy", 20 }; printf("s.name = %s, s.age = %d\n", s.name, s.age); initStudent(&s); printf("s.name = %s, s.age = %d\n", s.name, s.age); return 0; }
|
程序输出的结果如下:
|---------------|----------------------------------------------------------------------------------------------|
| 1 2 3
| s.name = Luffy, s.age = 20 tmp.name = robin, tmp.age = 35 s.name = robin, s.age = 35
|
可以看到,调用了initStudent
函数之后,实参s
中的数据确实被修改了,因为函数参数传递的是地址,在函数体内部形参指针指向的内存和实参变量对应的内存是同一块
,通过这种方式就可以在函数体内部来修改函数体外部的变量数据了。
如果函数的形参是指针类型,我们还可以使用const
关键字对其进行修饰,这样就可以限制使用者只能通过形参读内存数据,而不能修改内存数据。
|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| // 结构体类型的定义 struct Student { char name[50]; int age; }; void func1(struct Student* const p) { p->age = 10; // ok p = NULL; // error } void func2(const struct Student* p) { p = NULL; // ok p->age = 10; // error } void func3(const struct Student* const p) { p = NULL; // error p->age = 10; // error }
|
在上面的示例代码中一共定义了三个函数,每个函数的参数类型都不同:
void func1(struct Student* const p)
- const 在 * 号右侧,修饰指针
- 指针指向的地址不可变,指向的地址中的值可变
void func2(const struct Student* p)
- const 在 * 号左侧,修饰指针指向的内存地址中的值
- 指针指向的地址可变,指向的地址中的值不可变
void func3(const struct Student* const p)
- const 在 * 号左右两侧,修饰指针和指针指向的内存地址中的值
- 指针指向的地址不可变,指向的地址中的值也不可变
3.3 结构体数组 {#3-3-结构体数组}
除了使用结构体指针,结构体数组名也可以作为函数的形参,示例代码如下:
|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 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
| #include <stdio.h> #include <string.h> //结构体类型的定义 struct Student { char name[50]; int age; }; // void initStudent(struct Student tmp[100], int n) // void initStudent(struct Student* tmp, int n) void initStudent(struct Student tmp[], int n) { int i = 0; for (i = 0; i < n; i++) { sprintf(tmp->name, "name-%d", i); tmp->age = 20 + i; tmp++; } } int main() { struct Student s[3] = { 0 }; int i = 0; int size = sizeof(s) / sizeof(s[0]); initStudent(s, size); //数组名传递 for (i = 0; i < size; i++) { printf("%s, %d\n", s[i].name, s[i].age); } return 0; }
|
在上面的代码中将initStudent
函数的形参指定为了结构体数组类型,其实它有两种书写形式:
void initStudent(struct Student tmp[], int n)
- 不指定数组的容量,通过实参进行推导
void initStudent(struct Student tmp[100], int n)
- 指定实参的容量,数组大小需要和实参数组的大小一致
但是,不论将数组参数指定为何种形式,最终都将退化为指针,也就是这种形式:
|-----------|-------------------------------------------------------|
| 1
| void initStudent(struct Student* tmp, int n);
|
因此,在函数体内部是不能通过sizeof
运算符计算出指针指向的内存所占用的内存地址的大小的,如果需要在函数体内部使用这个值,则需要额外添加参数来进行数据的传递,这就是参数n
的由来。