8.1 结构体

8.1.1 从一个简单例题说起

例题:21点36分23秒后再过3小时28分47秒是几点几分几秒?

题目本身很简单,最初的时间加上经过的时间后,再进行简单的处理就可以得到答案,下面直接给出源代码。

程序代码8-1

8.1 结构体 - 图1

运行结果如图8-1所示。

8.1 结构体 - 图2

图8-1 一个简单的例题

题目本身没有什么难度,代码也很容易读懂。这个问题的特点在于,时间本身只有一个,但却是分别用时、分、秒3个int类型的变量共同描述的。在较小的问题中,这样比较粗糙的数据结构不会引起什么问题。但是可以想见的是,当问题较大、数据较多的时候这种处理数据的方法势必使代码更加复杂、正确性更难以保证、逻辑上缺乏条理且很不自然。而且,用多个单独的变量描述问题时,数据在函数之间传递非常麻烦、容易出错,况且函数也不可能返回多个数据。

为了使代码对数据和算法的描述更加自然、更有条理,C语言针对这类数据提供了一种描述手段,即所谓“结构体”(Structure)数据类型。这种数据类型用于描述某种具有几个“分量”(这些“分量”的类型可能相同、也可能不同)的数据,并且建立了这些“分量”都从属于一个共同的整体这样一种数据逻辑关系。

C语言建立这种数据结构的方法是,把具有这样关系的各个单一的数据聚合成为一个量,这个量具有若干个从属于这个量的“分量”,在逻辑上这若干个“分量”是相互关联的。这就是所谓结构体数据类型的概念。

这种结构体数据类型和数组或函数类型一样,也是一种由程序设计者自己负责构造的“衍生型”的数据类型。因此在使用这种类型的变量之前,必须首先构造或定义出这种数据类型的结构,这需要借助关键字struct完成。

8.1.2 声明结构体的类型

声明结构体的类型有这样几个方面的含义:为这种新的自定义类型取名;描述这种类型有几个什么类型的“分量”;为各个“分量”取名。称呼这种“分量”的标准术语是成员(1)(Members)。其一般形式是:

8.1 结构体 - 图3

比如前面例题中具有3个分量的时间,可以这样定义其类型:

8.1 结构体 - 图4

这里shijian是代码书写者根据标识符法则为所要定义的结构体类型所取的特定标识,术语叫做“标记”(Tag)。然而结构体类型完整的名字是struct shijian而不是shijian(这点和C++不同),这个类型的名字和纯粹由关键字描述的类型的名字(比如int)具有同样的语法地位,可以用这个类型的名字定义变量、进行类型转换运算或进一步构造新的数据类型等。

{}里面是对这种类型的数据所具有的各个成员的类型及各个成员的名称的描述。本例中所声明的是“struct shijian”这种类型,这种类型的数据有3个成员,皆为int类型,成员的名称分别为shi、fen、miao。

声明结构体类型时,{}的后面需要有一个“;”,此外“}”的前面也有“;”,这两个分号常被初学者所遗漏。实际上,前一个分号的意义是对某个成员的描述的结束标志,后一个分号是结构体类型声明的结束标志。

练习

1.为分子、分母皆为整数的分数数据声明一个结构体类型。

2.为由年、月、日这样表示时间的数据声明一个结构体类型。

8.1.3 定义结构体变量

自己定义了结构体的类型之后,就可以使用这种类型的名字定义结构体变量。对于例题来说,需要定义两个这样的变量,一个用来表示初始的时间,另一个用来表示时间的增量。

需要再次重申的是,完整的结构体类型的名字是struct shijian。如图8-2所示,变量的定义就可以写作:

8.1 结构体 - 图5

图8-2 结构体变量

8.1 结构体 - 图6

cs和zl都具有3个分量,在内存所占据的空间一般大于或等于shi、fen、miao部分所占据的空间之和。原因在于,C语言只规定了这些分量在内存中存放空间上的先后,但并没有要求它们必须紧邻。出于运行速度方面的考虑,在有些环境下各个分量之间确实可能有不被用到的内存空隙,所以不要凭空揣测结构体变量所占用空间的大小。如果代码中确实需要这个数据,请使用“sizeof”运算符计算。

这样,原来代码中需要定义6个独立的变量,现在只需要定义两个。而且各个分量被符合逻辑地聚合在一起了。下面的问题是如何使用这样的变量及其各个分量。

练习

1.用前节定义的分数数据结构体类型定义两个变量。

2.用前节定义的表示时间的数据的结构体类型定义一个变量。

8.1.4 结构体数据的基本运算

结构体类型数据的一个基本运算是“.”运算,这个运算是用来访问(Access,读或写)结构体类型数据的成员的。结构体变量与成员名称做“.”运算得到的就是该结构体变量对应的分量,比如:

8.1 结构体 - 图7

这个表达式是int类型的,与int的运算规则一致。而且这个表达式可以作为左值参与运算。

这样,如果希望前面定义的cs这个结构体变量的3个成员分别为21、36和23,在代码中可以用下面的语句表达:

8.1 结构体 - 图8

“.”运算是C语言中优先级别最高的运算之一,和“()”、“[]”一样,结合性从左到右。

结构体变量另一个最常见的运算是赋值,和数组类型不同的是,这种类型的数据可以整体赋值。由于这个缘故,实参可以是结构体类型,只要对应的形参也是同样的结构体类型即可。

此外,由于结构体类型的量在逻辑上可以视为一个整体,因此也可以作为函数的返回值,如此就可以实现函数在事实上返回多个值的效果,尽管在名义上函数返回的依然只是一个结构体的值。

这样,前面的代码也可以用结构体这样的数据类型实现。

程序代码8-2

8.1 结构体 - 图9

8.1 结构体 - 图10

程序输出:

8.1 结构体 - 图11

在这段代码中有这样一些新的语法内容:结构体类型的声明,用结构体的类型的名字定义结构体变量、声明函数原型、声明形参的数据类型,函数返回结构体类型的值,结构体类型变量整体赋值及对结构体成员的访问。请在阅读时注意体会。

从代码中可以看到,由于使用了结构体类型的变量,在main()函数中,变量的个数减少了;函数jg()的参数减少了且返回了一个包含多个成员值的结构体类型的值;此外jg()函数的意义更加明显,代码可读性得到了增强。这都是使用结构体这种类型带来的好处。

当然这段代码还有许多不足的地方,比如给结构体变量各个成员赋值的部分比前一个代码要稍微烦琐些。这些将在后面逐步加以改进。

需要补充说明的一点是,本例中结构体数据类型的位置表明的意义是,在代码中声明了这个类型之后直到该源文件的最后,代码中的任何一处都可以使用这个数据类型。

练习

1.编写程序,求两个分数和。

2.编写程序,输入年、月、日,输出第二天是哪年、哪月、哪日?

8.1.5 结构体变量赋初值及成员值的输入问题

程序代码8-2中给结构体变量各个成员赋值的部分之所以比程序代码8-1要稍微烦琐些的原因是,在程序代码8-1中定义变量时直接给变量赋了初值。对结构体类型的变量也可以在定义变量的时候直接赋初值,其方法和为数组赋初值非常类似。这样,在不改动代码其他部分的前提下,main()可以改写为下面的形式:

程序代码8-3(片段)

8.1 结构体 - 图12

和数组类似,也可以不给出结构体变量全部成员的初始值而只给出部分成员的初始值。例如:

8.1 结构体 - 图13

此外,由于对结构体变量成员的引用可以是一个和成员类型相同的左值,因此在需要的时候也可以用scanf()等函数从标准输入设备输入值。举例来说,如果需要从键盘输入cs.shi的值,可以通过下面的函数调用完成:

8.1 结构体 - 图14

这与输入普通的int变量值没有什么不同。

8.1.6 结构体“常量”(C99)

所谓字面量,是指直接写出的那些常量,比如123、23.4、'A'等。在C99中同样允许直接写出结构体类型的“常量”。由于这种量是由几个常数分量聚合而成的,所以叫做复合字面量(Compound Literal)。

然而仅仅根据几个常数分量,编译器尚不足以判断该量的类型,所以代码中书写这种常量时还必须写明该量的类型。比如:

8.1 结构体 - 图15

就是一个分量分别为3、28、47且类型为struct shijian的结构体的“复合字面量”,或者也可以理解为结构体类型的“常量”。这个常量可以出现在代码中任何允许struct shijian类型量出现的地方。当然,它不能够被赋值,因为它完全是一个结构体类型的“常量”。

不仅如此,C99还允许对结构体类型的常量依据分量的名称(而不是依照次序)指定各个分量的值,比如:

8.1 结构体 - 图16

下面代码演示了这种复合字面量的应用。

程序代码8-4

8.1 结构体 - 图17

8.1 结构体 - 图18

代码中有3处使用到了这种字面复合量:一处用来赋值,一处作为函数实参,还有一处是对结构体成员的访问。可以看出,这种量完全可以视为一种结构体类型的“常量”。

在本例中,这种复合字面量的使用只是一种语法层面上的演示,不表明从写代码的角度一定应该这样使用。实际上这段代码有许多地方尚有可推敲之处,在后面将逐步加以完善。

此外,需要说明的是,对于支持C99的编译器,在结构体变量赋初值的时候,也允许按照分量名称(而不是依照顺序)对全部成员或部分成员赋初值。

8.1.7 一个不太专业的技巧

对于初学者来说,结构体的一个令人感觉有些别扭的地方是数据类型的名字较长,而且是由两个部分组成的:一部分是C语言提供的关键字struct,另一部分是自己给出的标识符。一个显得不那么专业的技巧是,利用编译预处理命令进行一些视觉上的改善。如前面的例题中,可以在程序开头写上:

8.1 结构体 - 图19

这样就可以把SHIJIAN作为一个类型的名字来使用了,可以用它来定义变量、类型转换或描写函数原型等。如:

8.1 结构体 - 图20

可以写做:

8.1 结构体 - 图21

总之,所有出现类型名struct shijian的地方都可以简单地写为SHIJIAN。

这种做法并不很正规,后面将会看到,还有另外一种更通用的方法对复杂的类型的名称进行简化。

程序代码8-5

8.1 结构体 - 图22

8.1 结构体 - 图23

运行结果:

8.1 结构体 - 图24

8.1.8 结构体的其他定义方式及无名的结构体

根据前面的内容可以看到,使用自定义的结构体时,必须经过两个步骤。

(1)声明结构体的类型。

(2)根据所声明的结构体的类型定义结构体变量。

这两个步骤不一定需要分别进行,有时也可以同时进行,如:

8.1 结构体 - 图25

只有变量和结构体类型的作用范围相同的时候才能这样做。而把类型声明与是义变量分别进行的写法显然具有更好的灵活性,因为这样可以使结构体类型在代码中全局有效,而结构体变量的有效区间则是局部的。

有时也可以不为结构体的类型取名,而直接在描述完结构体的结构之后定义变量,如:

8.1 结构体 - 图26

可以看到这同样是以牺牲代码的灵活性为代价的(即类型的范围与结构体变量的作用范围必须一致),因此通常的情况下这种写法并不提倡。