C语言教程-14_1-初识结构体
概要:
- 在更多情况下,数据的组织结构是非常复杂的,往往不是简单的几个变量甚至数组就能够表示的,许多有关联但不同类型的数据经常需要统一处理,为此,我们需要一种特殊的数据类型来将不同类型的数据组织起来,C语言提供了
结构体类型
来实现这种目的.
前置知识:
- 基本数据类型和数组
引入问题
问题描述
现在需要存储一家书店的所有图书信息,并能够打印出来目录清单.为了简化问题,我们假设每种书只有以下信息:书名,作者,出版年份,页数,库存,价格.
同样为了简化问题,假设这家书店最多能购入一千种图书.我们的目的仅仅想要输入并存储一系列书籍信息,并按需求打印出来.
问题分析
我们的目的是要存储每种书的各种信息,每种书有如下信息需要存储:
- 书名—一个字符串
- 作者—一个字符串
- 出版年份—一个四位整数
- 页数—一个整数
- 库存—一个整数
- 价格—一个浮点数
显然他们是不同类型的数据,我们如果只考虑使用一系列数组来存储:
1 |
|
其中,对每个数组而言,第i个元素就是书店中第i+1本书(注意下标从0开始,这里+1结合实际)的信息.
例如第i本书的书名为book_name[i]
,其页数为page_cnt[i]
,就这么简单.
但是有一个很大问题,假设我们有一个函数用于处理某种书的各种信息,我们就需要将上面这些数组全部传递给这个函数,十分麻烦.
如果我们能够将各种书的同类信息分开,反而将每本书的各种不同信息组合在一起,我们就可以仅仅向这个函数传递特定的这一本书的信息,而无需将这些数组统统塞过去.
那么问题立刻转移为(注意学会将实际问题抽象为技术问题):如何将若干不同数据类型的数据包含在一个整合的数据结构中.
C语言提供结构体数据类型
来实现该目的.
struct-结构体类型
一个不太恰当的比较
考虑数组和结构体:
-
C语言的数组允许定义可存储大量同类型数据的变量.
-
而结构体则允许定义可存储若干不同类型数据的变量.
这种简单但不恰当的比较帮助你有一个大概的第一印象.
定义结构体
结构体
(简称结构)属于自定义类型,因为一个结构
中包含的各种数据项均由用户自行定义.
使用关键字struct
来定义一个结构体(类型),语法为:
1 | struct <结构体名>{ |
需要注意的是,结构体名
的地位相当于基本数据类型int
,double
…等等,他是一个类型,而不是实际的变量.
接下来就用该类型去声明一个实际的变量,即结构体变量名
,同样,可以连续声明多个变量.
举一个简单的例子,我们用一个结构体来存储一对整数,用来表示一个二维坐标:
1 | struct COORD{ // 结构体名往往用大写 |
我们声明了一个结构体类型struct COORD
,然后又声明了一个struct COORD
类型的变量coord
,它包含2个成员x和y,均为int类型.
需要注意的是,C语言中,使用一个结构体类型必须要在前面加上struct
这个关键字,直接使用COORD是错误的.
当然,声明结构体类型和声明结构体变量是可以分开的,我们完全可以在前面实现声明该结构体类型COORD,然后在后面适当的位置
声明变量:
1 |
|
第8行中,我们完全可以把struct COORD
看成类似于int
,char
这样的类型,声明该类型的变量即可.
结构体变量的初始化
结构体变量可以使用花括号{}
包括的一系列值进行初始化
,同时还有一些特殊的初始化写法.
列表初始化
和其他变量相同,可以对结构体变量在定义时指定初始值,不过,要使用花括号{}
依次对其成员初始化:
1 |
|
需要注意的是,如果初始化列表中的项不全(即有成员未被初始化),则他们默认被零初始化
,即整数初始化为0,浮点数被初始化为正零,指针被初始化为对应类型的空指针等等.
嵌套初始化
若结构体的成员是另一个结构体/数组/联合体等,则依次用嵌套的花括号来初始化,如果内层没有嵌套的花括号,则将当前花括号中的项依次初始化对应的成员(即省略嵌套的内层括号).
看cppreference的解释和例子:
指派初始化式
与数组类似,可以使用.成员
形式的结构体指派符来初始化指定的成员:
1 |
|
运行结果为x=3,y=4
.
注意这里.x和.y的顺序与他们在结构体中的顺序不同,在C语言是允许的,需要注意的是C++程序会报错,注意不要使用g++编译.
另外十分重要的一点是,有指派符指定的成员
后继的无指派符的初始化式
,会继续初始化先前有指派符指定的成员
之后的结构体成员.
这句话会很拗口,上代码就知道了:
1 |
|
运行结果:
访问结构体成员
结构体变量是一个整体,我们实际访问的是该变量中具体的成员,使用成员访问运算符(.)
来访问其成员.
注:为省事叫该运算符为点运算符
也行.
1 |
|
很简单,输出x=3,y=4
.
这里的coord.x和coord.y与普通的int变量没有任何区别,只不过他们是coord的成员罢了,依然可以进行赋值,运算,取地址等所有操作.
1 |
|
运行结果:
注意
不能包含自身类型的成员
下面的代码是一个错误的示范:
1 |
|
很简单的道理,一个结构体类型如果包含自身类型的成员,那么就会无限递归下去,结构体的大小就无法确定,导致编译报错.
不过,可以包含指向自身类型的指针,后面会讲解.
结构体与数组
数组作为结构体成员
结构体内可以包含任意类型的成员,甚至可以包含另一个结构体类型的成员(注意不能是自身类型)
数组也可以作为结构体的成员,与常规的数组并无二致:
1 |
|
运行结果:
当然也有特殊性—结构体类型变量可以互相赋值,如果有数组成员照样可以整体复制过去,而不同于普通的数组:
1 |
|
运行结果:
柔性数组
本节前置知识:指针,数组,结构体与指针,动态内存分配
PS:我本来认为这里暂时没有必要加入柔性数组
的内容,对于新手来说为时尚早,不仅没有用武之地,反而会增加理解负担.不过考虑知识框架的完整性,还是放进来,读者选择性阅读.
柔性数组
产生于对动态结构体的需求.
考虑使用结构体实现网络数据包的存储.如果我们使用一个结构体来封装网络数据包,使用定长的缓冲区,为了防止缓冲溢出,缓冲区一般设置的足够大.那么当数据包实际大小没有那么多时,就会导致数组空间大量冗余:
1 | // 定长数组的缓冲区,默认大小为1K |
当data中实际荷载的数据少于1K时(平时通信大多都是小包),就会浪费大量的空间,甚至会消耗很多流量.
另一种思路是将data成员换成指针,每个实例分配不同长度的内存:
1 | // 动态分配内存的缓冲区 |
这种方法可以让缓冲区大小可变,但是缺点也很明显,结构体本身和数据缓冲区是分开的(不连续),需要分别进行管理,导致内存碎片,加大内存管理的难度.
C99后,使用柔性数组成员可以直接构造变长的结构体,既提高内存利用率,又减少内存碎片.
柔性数组使用如下:
1 | // 柔性数组实现的缓冲区 |
需要注意的是,data的长度为0,它并不在结构体中占用任何空间!其仅仅起到一个占位的作用,数组名代表它只是一个偏移量!(了解结构体底层实现的朋友一定知道我在说什么)
只有在内存分配后data才会有其用武之地,指向多分配的变长部分内存.
如此为使用柔性数组的结构体分配内存:
1 | if ((buffer = (struct Buffer*)malloc(sizeof(struct Buffer) + sizeof(char) * cur_length)) != NULL) { |
使用完后直接将整个结构体的内存一次free即可,无需两次free:
1 | free(buffer); |
结构体数组
结构体是一种自定义类型,也可以声明各元素均为结构体类型的数组,即结构体数组.
1 |
|
运行结果:
中场休息-解决引例(的一部分)
到现在我们已经有一定能力解决前面提出的问题了(其实,还差了指针),这里把要实现的数据项重新放出来:
- 书名—一个字符串
- 作者—一个字符串
- 出版年份—一个四位整数
- 页数—一个整数
- 库存—一个整数
- 价格—一个浮点数
使用结构体很容易实现这样一种书的类型:
1 | // 书籍结构体 |
我们也许可以再进一步,实现这家书店的信息:
1 | // 书店的各种信息 |
现在数据类型定义好了,接下来可以使用他们来解决问题了,不过这个步骤还是留待后面再说吧,因为还没有讲结构体与指针
.
本节讲解了结构体的基本内容,接下来会讲解结构体与指针的关系,该部分尽管不是很难,但对后续实现各种数据结构十分重要.
——WAHAHA 2024.2.9
除夕快乐~~~
上一篇:C语言教程-13_4-函数指针