概要:

  • 在更多情况下,数据的组织结构是非常复杂的,往往不是简单的几个变量甚至数组就能够表示的,许多有关联但不同类型的数据经常需要统一处理,为此,我们需要一种特殊的数据类型来将不同类型的数据组织起来,C语言提供了结构体类型来实现这种目的.

前置知识:

  • 基本数据类型和数组

引入问题

问题描述

现在需要存储一家书店的所有图书信息,并能够打印出来目录清单.为了简化问题,我们假设每种书只有以下信息:书名,作者,出版年份,页数,库存,价格.

同样为了简化问题,假设这家书店最多能购入一千种图书.我们的目的仅仅想要输入并存储一系列书籍信息,并按需求打印出来.

问题分析

我们的目的是要存储每种书的各种信息,每种书有如下信息需要存储:

  1. 书名—一个字符串
  2. 作者—一个字符串
  3. 出版年份—一个四位整数
  4. 页数—一个整数
  5. 库存—一个整数
  6. 价格—一个浮点数

显然他们是不同类型的数据,我们如果只考虑使用一系列数组来存储:

1
2
3
4
5
6
7
8
9
10
#define MAXN 1000 // 最多存储1000种书
int main(){
char book_name[MAXN][31]; // 二维字符数组,每一行最多存储30个书名字符
char author_name[MAXN][41]; // 二维字符数组,每一行最多存储30个作者字符
int publication_year[MAXN]; // 存储出版年份
int page_cnt[MAXN]; // 存储页数
int stock_cnt[MAXN]; // 存储库存数目
float price[MAXN]; // 单精度浮点数存储价格
return 0;
}

其中,对每个数组而言,第i个元素就是书店中第i+1本书(注意下标从0开始,这里+1结合实际)的信息.

例如第i本书的书名为book_name[i],其页数为page_cnt[i],就这么简单.

但是有一个很大问题,假设我们有一个函数用于处理某种书的各种信息,我们就需要将上面这些数组全部传递给这个函数,十分麻烦.

如果我们能够将各种书的同类信息分开,反而将每本书的各种不同信息组合在一起,我们就可以仅仅向这个函数传递特定的这一本书的信息,而无需将这些数组统统塞过去.


那么问题立刻转移为(注意学会将实际问题抽象为技术问题):如何将若干不同数据类型的数据包含在一个整合的数据结构中.

C语言提供结构体数据类型来实现该目的.

struct-结构体类型

一个不太恰当的比较

考虑数组和结构体:

  • C语言的数组允许定义可存储大量同类型数据的变量.

  • 而结构体则允许定义可存储若干不同类型数据的变量.

这种简单但不恰当的比较帮助你有一个大概的第一印象.

定义结构体

结构体(简称结构)属于自定义类型,因为一个结构中包含的各种数据项均由用户自行定义.

使用关键字struct来定义一个结构体(类型),语法为:

1
2
3
4
5
6
struct <结构体名>{
<基本数据类型1> <成员1>;
<基本数据类型2> <成员2>;
<基本数据类型3> <成员3>;
......
}[结构体变量名1],[结构体变量名2],......;

需要注意的是,结构体名的地位相当于基本数据类型int,double…等等,他是一个类型,而不是实际的变量.

接下来就用该类型去声明一个实际的变量,即结构体变量名,同样,可以连续声明多个变量.

举一个简单的例子,我们用一个结构体来存储一对整数,用来表示一个二维坐标:

1
2
3
struct COORD{ // 结构体名往往用大写
int x,y; // 与平时一样,同类型变量可以声明在一行
}coord; // 声明一个struct COORD类型的变量coord

我们声明了一个结构体类型struct COORD,然后又声明了一个struct COORD类型的变量coord,它包含2个成员x和y,均为int类型.

需要注意的是,C语言中,使用一个结构体类型必须要在前面加上struct这个关键字,直接使用COORD是错误的.


当然,声明结构体类型和声明结构体变量是可以分开的,我们完全可以在前面实现声明该结构体类型COORD,然后在后面适当的位置声明变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
// 当然,声明结构体类型和声明结构体变量是可以分开的,我们完全可以在前面实现声明该结构体类型COORD,然后在后面`适当的位置`声明变量:
struct COORD {
int x;
int y;
}; // 在这里声明了一个全局的结构体类型COORD,这个类型可以在整个程序中使用
int main() {
struct COORD point; // 在这里声明了一个局部的结构体变量point,这个变量只能在main函数中使用
point.x = 10;
point.y = 20;
printf("x=%d, y=%d\n", point.x, point.y); // x=10, y=20
return 0;
}

第8行中,我们完全可以把struct COORD看成类似于int,char这样的类型,声明该类型的变量即可.

结构体变量的初始化

结构体变量可以使用花括号{}包括的一系列值进行初始化,同时还有一些特殊的初始化写法.

列表初始化

和其他变量相同,可以对结构体变量在定义时指定初始值,不过,要使用花括号{}依次对其成员初始化:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
struct COORD{
int x,y;
}coord={3,4}; // x和y依次初始化为3和4
printf("x=%d,y=%d",coord.x,coord.y);
return 0;
}

需要注意的是,如果初始化列表中的项不全(即有成员未被初始化),则他们默认被零初始化,即整数初始化为0,浮点数被初始化为正零,指针被初始化为对应类型的空指针等等.

嵌套初始化

若结构体的成员是另一个结构体/数组/联合体等,则依次用嵌套的花括号来初始化,如果内层没有嵌套的花括号,则将当前花括号中的项依次初始化对应的成员(即省略嵌套的内层括号).

看cppreference的解释和例子:

image-20240207003320547

指派初始化式

与数组类似,可以使用.成员形式的结构体指派符来初始化指定的成员:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
struct COORD {
int x, y, z;
}coord = { .y = 4,.x = 3 };
printf("x=%d,y=%d", coord.x, coord.y);
return 0;
}

运行结果为x=3,y=4.

注意这里.x和.y的顺序与他们在结构体中的顺序不同,在C语言是允许的,需要注意的是C++程序会报错,注意不要使用g++编译.


另外十分重要的一点是,有指派符指定的成员后继的无指派符的初始化式,会继续初始化先前有指派符指定的成员之后的结构体成员.

这句话会很拗口,上代码就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main() {
struct VECTOR_5 {
int a, b, c, d, e;
}vec = { .c = 3,4,5,.a = 1,2 };
// 4和5被依次用于初始化c后面的d和e;
// 2被用于初始化a后面的b

// 访问输出变量vec的每一个成员
printf("vec.a = %d, vec.b = %d, vec.c = %d, vec.d = %d, vec.e = %d\n",
vec.a, vec.b, vec.c, vec.d, vec.e);
return 0;
}

运行结果:

image-20240207004759961

访问结构体成员

结构体变量是一个整体,我们实际访问的是该变量中具体的成员,使用成员访问运算符(.)来访问其成员.

注:为省事叫该运算符为点运算符也行.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
struct COORD{
int x,y;
}coord={3,4}; // x和y分别初始化为3和4
printf("x=%d,y=%d",coord.x,coord.y); // 获取成员x和y的值
return 0;
}

很简单,输出x=3,y=4.

这里的coord.x和coord.y与普通的int变量没有任何区别,只不过他们是coord的成员罢了,依然可以进行赋值,运算,取地址等所有操作.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {
struct COORD {
int x, y;
}coord = { 3,4 };
int *p = &coord.x; // 获取成员x的地址
*p = 5; // 间接修改成员x的值
printf("x=%d,y=%d\n", coord.x, coord.y); // 获取成员x和y的值
coord.y++; // 修改成员y的值,其中成员运算符的优先级高于自增运算符
printf("x=%d,y=%d\n", coord.x, coord.y); // 获取成员x和y的值
return 0;
}

运行结果:

image-20240207183139962

注意

不能包含自身类型的成员

下面的代码是一个错误的示范:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main() {
// 结构体不能包含自身类型的成员
struct Node {
int data;
Node next;
}; // 编译错误

return 0;
}

很简单的道理,一个结构体类型如果包含自身类型的成员,那么就会无限递归下去,结构体的大小就无法确定,导致编译报错.

不过,可以包含指向自身类型的指针,后面会讲解.

结构体与数组

数组作为结构体成员

结构体内可以包含任意类型的成员,甚至可以包含另一个结构体类型的成员(注意不能是自身类型)

数组也可以作为结构体的成员,与常规的数组并无二致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main() {
struct {
int x;
int a[3];
} s;
s.x = 1;
for(int i = 0; i < 3; i++) {
s.a[i] = i;
}
printf("%d %d %d %d\n", s.x, s.a[0], s.a[1], s.a[2]);
return 0;
}

运行结果:

image-20240207232917986

当然也有特殊性—结构体类型变量可以互相赋值,如果有数组成员照样可以整体复制过去,而不同于普通的数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int main() {
// 与普通的数组不同
// 结构体的数组成员可以互相赋值
struct TEST{
int a;
int arr[10];
};
struct TEST t1, t2;
t1.a = 1;
for (int i = 0; i < 10; i++) {
t1.arr[i] = i;
}
t2 = t1;
printf("t2.a = %d\n", t2.a);
printf("t2.arr = {\n ");
for (int i = 0; i < 10; i++) {
printf("%d, ", t2.arr[i]);
}
printf("\n}\n");
// 可以看出,结构体的数组成员可以互相赋值,完整的复制
return 0;
}

运行结果:

image-20240209154142760

柔性数组

本节前置知识:指针,数组,结构体与指针,动态内存分配

PS:我本来认为这里暂时没有必要加入柔性数组的内容,对于新手来说为时尚早,不仅没有用武之地,反而会增加理解负担.不过考虑知识框架的完整性,还是放进来,读者选择性阅读.

柔性数组产生于对动态结构体的需求.


考虑使用结构体实现网络数据包的存储.如果我们使用一个结构体来封装网络数据包,使用定长的缓冲区,为了防止缓冲溢出,缓冲区一般设置的足够大.那么当数据包实际大小没有那么多时,就会导致数组空间大量冗余:

1
2
3
4
5
6
//  定长数组的缓冲区,默认大小为1K
# define MAX_LENGTH 1024
struct Buffer {
int len;
char data[MAX_LENGTH];
};

当data中实际荷载的数据少于1K时(平时通信大多都是小包),就会浪费大量的空间,甚至会消耗很多流量.


另一种思路是将data成员换成指针,每个实例分配不同长度的内存:

1
2
3
4
5
//  动态分配内存的缓冲区
struct Buffer {
int len;
char *data;
};

这种方法可以让缓冲区大小可变,但是缺点也很明显,结构体本身和数据缓冲区是分开的(不连续),需要分别进行管理,导致内存碎片,加大内存管理的难度.


C99后,使用柔性数组成员可以直接构造变长的结构体,既提高内存利用率,又减少内存碎片.

柔性数组使用如下:

1
2
3
4
5
//  柔性数组实现的缓冲区
struct Buffer {
int len;
char data[0]; // 长度为0
};

需要注意的是,data的长度为0,它并不在结构体中占用任何空间!其仅仅起到一个占位的作用,数组名代表它只是一个偏移量!(了解结构体底层实现的朋友一定知道我在说什么)

只有在内存分配后data才会有其用武之地,指向多分配的变长部分内存.

如此为使用柔性数组的结构体分配内存:

1
2
3
4
if ((buffer = (struct Buffer*)malloc(sizeof(struct Buffer) + sizeof(char) * cur_length)) != NULL) {
buffer->len = cur_length; // 存储长度
memcpy(buffer->data,payload_data,cur_length); // 复制有效荷载数据
}

使用完后直接将整个结构体的内存一次free即可,无需两次free:

1
2
free(buffer);
buffer = NULL;

结构体数组

结构体是一种自定义类型,也可以声明各元素均为结构体类型的数组,即结构体数组.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main() {
// 该结构体包含三个成员变量,分别存储学生的姓名,年龄和分数
struct student {
char name[20];
int age;
float score;
};
// 使用该结构体定义一个数组stu,存储三个学生的信息
// 每个元素使用花括号{}初始化,分别存储学生的姓名,年龄和分数
struct student stu[3] = {{"Tom", 18, 90.5}, {"Jerry", 19, 88.5}, {"Marry", 17, 85.5}};
// 使用for循环遍历数组stu,输出每个学生的信息
for (int i = 0; i < 3; i++) {
printf("name:%s, age:%d, score:%.1f\n", stu[i].name, stu[i].age, stu[i].score);
}
return 0;
}

运行结果:

image-20240207234113278

中场休息-解决引例(的一部分)

到现在我们已经有一定能力解决前面提出的问题了(其实,还差了指针),这里把要实现的数据项重新放出来:

  1. 书名—一个字符串
  2. 作者—一个字符串
  3. 出版年份—一个四位整数
  4. 页数—一个整数
  5. 库存—一个整数
  6. 价格—一个浮点数

使用结构体很容易实现这样一种书的类型:

1
2
3
4
5
6
7
8
9
// 书籍结构体
struct Book {
char name[100]; // 书名
char author[100]; // 作者
int year; // 出版年份
int page; // 页数
int stock; // 库存
float price; // 价格
};

我们也许可以再进一步,实现这家书店的信息:

1
2
3
4
5
6
7
8
9
// 书店的各种信息
#define MAX_BOOKS 1000 // 最多1000种书目
struct BookStore {
char name[100]; // 书店名
char address[100]; // 地址
int phone; // 电话
struct Book books[MAX_BOOKS]; // Book结构体类型的数组作为BookStore结构体的成员
int bookCount; // 书籍数量
};

现在数据类型定义好了,接下来可以使用他们来解决问题了,不过这个步骤还是留待后面再说吧,因为还没有讲结构体与指针.


本节讲解了结构体的基本内容,接下来会讲解结构体与指针的关系,该部分尽管不是很难,但对后续实现各种数据结构十分重要.

——WAHAHA 2024.2.9

除夕快乐~~~


上一篇:C语言教程-13_4-函数指针

下一篇:C语言教程-14_2-结构体与指针