C语言里的结构是一种复合数据类型,如下述代码中的Employee类型。在该类型的单一对象里,同时存储了员工姓名、是否已退休、月薪、性别等信息。
1 | struct Employee { |
在使用C++进行程序设计时,我们通常使用第13章中讨论的类(class)▲来达到类似目的,结构体并非必须,故本章以在线方式提供。
版权声明
本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。
本文不可以以纸质出版为目的进行改编、摘抄。
11. 结构
一尺之棰,日取其半,万世不竭。 —— 庄子
朵拉同学家里运营着一家工厂,她希望编写一个程序来管理员工档案。考虑到每个雇员都有姓名、ID、性别、月薪等信息,她组织了如下的数据结构来存储雇员信息。
1 | typedef enum { |
在上述数据结构中,朵拉在内存中开辟了1000个员工档案的存储空间,其中,第i个员工的身份证号储存在idEmployees数组的下标i处,性别存储在genderEmployees数组的下标i处 …
这种数据结构主要有两个缺点:
数组的元素个数是固定的,当员工数量显著少于1000时,内存浪费,员工数量超过1000时,溢出。这个问题需要通过本书第19章介绍的容器类▲来解决。
同一个员工的信息分散在不同的数组里,使用不便。这个问题可以通过C语言的结构(struct)类型来解决。
11.1 结构定义
下述C语言程序定义并使用了一个名为Employee的结构类型。
1 | //Project - EmployeeStruct |
上述程序的执行结果为:
1 | &e = 000000000061FE00, size = 20 |
说明:在读者的计算机上,执行结果中的地址很可能与本书不同。
🚩第9 ~ 14行:定义了一个名为Employee的结构(struct),它有4个数据成员(data member),这4个数据成员分别负责记录一个雇员的姓名、是否已退休、月薪及性别信息。
在上述定义之后,在C语言中,struct Employee便可以当成一个数据类型来使用。从面向对象程序设计的角度看,struct Employee类型与int、float、char一样,都是数据类型,区别在于,后3个是语言原生的,struct Employee是程序员通过编程“介绍”给编译器的。
类似地,可以通过typedef定义一个名为Employee的结构类型,避免每次使用都必须带上struct关键字的烦恼:
1 | typedef struct { |
🚩第17 ~ 18行:定义了一名类型为struct Employee的对象e,并对其进行了初始化。与联合对象类似,其初始化值必须包裹在{ }之中。本例中,通过”.”操作符分别给各成员赋值。
1 | struct Employee e = {"Jack Ma",false,9000,male}; //各成员初始值必须按顺序提供 |
如上行代码所示:当{ }内的成员值按结构内成员定义顺序给出时,{ }内的成员名称可以省略。
🚩第20 ~ 24行:分别打印e、e.sName、e.bRetired、e.iSalary和e.gender的地址及字节数。与联合类型不同,结构的不同成员之间并不共享内存,它们是同一块内存的不同组成部分,相互之间是独立的。根据执行结果,我们画出了结构对象e的内存结构,如图11-1所示。
如图11-1所示,结构对象e占20个字节的空间,其数据成员按定义顺序依次排列在这20个字节的空间内。其中:
e.sName类型为char[],占10个字节;
e.bRetired类型为bool,占1个字节,编译器出于数据对齐▲的原因,在该成员后安排了1个字节的空白区域;
e.iSalary类型int,占4个字节;
e.gender类型为GenderType枚举型,占4个字节。
11.2 结构对象
在完成定义之后,结构类型在理论上与其他数据类型,比如int没有什么不同。下述C语言代码进一步展示了结构对象的使用方法。
1 | //Project - EmployeeInfo |
上述程序的执行结果为:
1 | ------Employee Information-------- |
🚩第25 ~ 26行:定义并初始化了一个名为e的Employee类型对象。第26行通过“.”操作符访问e的iSalary成员。
🚩第28 ~ 29行:定义了一个指向Employee结构的指针p。当通过指针p访问结构成员时,通过“->”操作符实现。p->bRetired表示p所指向的Employee类型结构对象的bRetired成员。
🚩第30行:对p应用间接操作符*之后,(*p)即表示p所指向的结构对象,此处为e。然后再对e应用“.”操作符,访问该结构对象的bRetired成员。这种访问方法跟p->bRetired等效。
🚩第32行:将结构对象e的指针传递给printEmployee()函数,该函数负责打印参数指针所指向的“雇员”信息。
🚩第16 ~ 22行:printEmployee()函数用于打印雇员信息。请读者注意形参e的类型为const Employee*,即指向常量Employee对象的指针。这个形参定义带来了三项益处。
避免直接对Employee对象e进行传值。直接传值将“创建”一个e的复制品,该传值的代价为20个字节。而传递指针,无论对象本身有多大,指针的尺寸是固定的4字节(32位编译器)或者8字节(64位编译器)。
避免在函数内意外修改p所指向的对象。按照printEmployee()函数的预期用途,这种“名不副实”的修改极其有害。
让printEmployee()函数的使用者放心地将对象的指针传递给该函数使用。因为该函数“声称”不会修改参数指针指向的对象。
当然,在C++里,我们更倾向于使用常量型的引用,而不是指针。
1 | Employee e = {"Jack Ma", false, 9000, male}; |
C语言允许在同类型结构对象间进行整体赋值,上述代码的第3行将e完整复制到f。
1 | float computeTax(Employee obj){ //用于计算员工的个税 |
上述computeTax()函数的形参obj将导致传值调用(call by value)的发生。computeTax(e)将实参e复制传递给形参obj,本例中的代价为20个字节。为提高传参效率,应尽量避免对大对象进行传值。
练习巩固 👣
11-1 (人生使用进度)定义日期(Date)结构类型,其中应包括年、月、日三个成员。请实现函数lifeProgress()并编写代码加以验证。该函数接受你的出生日期及当前日期作为参数,计算并返回你的人生使用进度。
1 | float lifeProgress(struct Date birth, struct Date today); |
说明:按2019年我国居民人均预期寿命77.3岁进行估算;为降低难度,初始版本可以不考虑闰年,按每年365天计算,假设某人出生6900天,则其人生使用进度为6900/(77.3*365)。
11-2 (虚虚实实)结合typedef语句定义复数(Complex)结构类型,其中应包括浮点数类型的实部和虚部两个成员。请实现函数add()并编写代码加以验证。该函数用于计算并返回两个复数对象的和。
1 | Complex add(const Complex a, const Complex b); |
11-3 (三维向量)对于三维向量V1(x1,y1,z1)和V2(x2,y2,z2),其加法运算定义为:
$$
(x1,y1,z1) + (x2,y2,z2) = (x1+x2,y1+y2,z1+z2)
$$
减法运算定义为:
$$
(x1,y1,z1) - (x2,y2,z2) = (x1-x2,y1-y2,z1-z2)
$$
模长定义为:
$$
|(x,y,z)| = \sqrt{x^2+y^2+z^2}
$$
请定义结构类型Vector3D,其三个分量为整数;请设计函数实现三维向量的加、减及求模长功能;编写合适的代码加以验证。
11.3 复合字面量
复合字面量(compound literals),顾名思义,是由多个普通字面量组合而得。如果需要一个临时的结构对象,复合字面量很好用。下述C语言代码演示了复合字面量的用法。
1 | //Project - CompoundLiteral |
上述代码的执行结果为:
1 | fArea1 = 6.000000, fArea2 = 6.000000 |
🚩第20行:(Rect){15,10}即为一个Rect类型的复合字面量,注意类似于显式类型转换语法的(Rect)部分在这里是不可或缺的。该行创建了一个Rect类型的临时对象,然后把它赋值给r。
🚩第22行:使用复合字面量做为函数的实际参数。
🚩第23行:对复合字面量临时对象取地址,传地址调用函数。
11.4 结构对象数组
1 | struct Employee a[3]; |
对于编译器而言,a、b都是3个元素的数组,其数组名均为数组首元素的地址。区别在于,a的元素类型为struct Employee,b的元素类型为int。
下述C语言程序演示了结构对象数组的使用语法。
1 | //Project - EmployeeArray |
上述程序的执行结果为:
1 | Name: Dorothy Henry, Salary: 5200 |
🚩第17 ~ 21行:定义并初始化了包含3个Employee对象的数组es0。由于元素是结构对象,所以提供初始值的{ }内包含了3个复合字面量。请注意Employee是经由typedef定义的结构类型,在使用时可以省略前边的struct关键字。es0数组属于自动变量,其内存分配在栈里,生命周期由编译器负责管理。
🚩第23行:通过calloc()函数在堆里申请了3个sizeof(Employee)的空间,然后赋值给指针es1。根据第6章中的讨论,指向Employee对象的指针es1,可以当成数组名使用。
🚩第24 ~ 25行:通过循环,把数组es0中的三个结构体对象赋值给es1“数组”中的元素。
🚩第27 ~ 28行:es0是包含Employee对象的数组的数组名,es0[1]即为该数组下标为1的元素,它是一个结构体对象。es0[1].iSalary即为该结构对象的iSalary成员,本例中,指Dorothy的Salary。
🚩第30 ~ 32行:按第6章指针运算部分的讨论,p++并不是把p的值(地址)加1,而是增加一个sizeof(Employee)。p++执行后,p事实上指向es1“数组”中下标为1的元素,即Dorothy。p->iSalary即为p所指向的结构体对象Dorothy的iSalary成员。
🚩第34 ~ 35行:分别打印es0、es1数组的第1个元素的sName及iSalary。执行结果反应了前述数据修改的成果,Dorothy的工资由5000变成了5200。
🚩第37行:释放通过calloc()函数申请的堆空间。请注意,上述程序中作者为了演示指向Employee的指针运算(p++),有意新增了一个指针变量p,而没有直接对es1进行++操作。这很重要,动态内存的地址宜保持原值,因为它们是内存回收的唯一依据。
练习巩固 👣
11-4 (三维向量的较量)编程从键盘读入10个Vector3D三维向量(参考练习11-3),然后使用任意一种排序算法按照模长递增排序并依次输出。
11.5 结构的嵌套
前述结构体包含了char[]、bool、int等各种类型的数据成员。当一个结构的成员类型也是一个结构时,称为结构的嵌套。下述C语言程序演示了一个结构嵌套的示例。
1 | //Project - CircleStruct |
上述程序的执行结果为:
1 | center = (-12, 0), radius = 4.100000 |
🚩第4 ~ 12行:定义了一个Circle类型的结构,其包含两个成员,分别是圆心ptCenter以及半径fRadius。其中,ptCenter的类型为Point结构,这属于结构嵌套的范畴。
🚩第19行:定义并初始化了Circle结构对象c。请注意{ }包裹的初始值的格式,其中,{0,100}部分用于初始化c的ptCenter成员,4.1F用于初始化fRadius成员。
🚩第21行:c.ptCenter代表c的ptCenter成员,而该成员是Point结构类型的对象,可以直接把一个struct Point类型的复合字面量赋值给它。该赋值完成后,ptCenter的x和y成员都被置为0。
🚩第22行:c.ptCenter.y代表c的ptCenter成员的y成员。
🚩第24行:调用horizontalMove()函数将c横向移动-12个坐标单位。
🚩第14 ~ 16行:形参c是一个指针,它指向被移动的Circle对象,offset代表移动偏移量。c->ptCenter.x代表指针c所指向的结构对象的ptCenter成员的x成员。由于这个函数预期要修改c所指向的圆,因此c的类型定义为Circle*,而不是const Circle*。
🚩第26 ~ 27行:打印c的值。从执行结果看,调用horizontalMove()函数对c的横向移动是成功的,其圆心坐标变成了(-12, 0)。
扩展阅读 结构体中的柔性数组成员* |
---|
通过柔性数组成员,C语言中的结构体对象可以相对灵活地存储不确定数量的数据。 http://codelearn.club/2022/05/flexiblearraymember/ |
练习巩固 👣
11-5 (凸六边形的面积)平面内的六边形可以用六个点坐标表示。
请设计Point结构表示一个坐标点。
请设计Hexagon结构表示一个六边形,其内包括6个Point成员。
如图11-2所示,当六边形为凸六边形时,可以将其拆分成四个三角形,利用海伦-秦九韶公式分别计算三角形的面积并相加,即得凸六边形的面积。请实现凸六边形面积计算函数computeHexagonArea()并编写合适的代码加以验证。
1 | float computeHexagonArea(Hexagon h); |