到目前为止我们使用的大多数数据类型都具有单一的值,例如整数、字符、布尔值、浮点数,这些可称为基本数据类型(Primitive Type)。但字符串是一个例外,它由很多字符组成,像这种由基本类型组成的数据类型称为复合数据类型(Compound Type),正如表达式和语句有组合规则一样,由基本类型组成复合类型也有一些组合规则,例如本章要讲的结构体,以及第 8 章 数组要讲的数组和字符串。复合数据类型一方面可以从整体上当作一个数据使用,另一方面也可以分别访问它的各组成单元,复合数据类型的这种两面性提供了一种数据抽象(Data Abstraction)的方法。[SICP]指出,在学习一门编程语言时,要特别注意以下三方面:
本节将以结构体为例来讲解数据类型的组合和抽象。至于过程抽象我们已经见过最简单的形式,就是把一组语句用一个函数名封装起来,当作一个整体使用,以后我们还会介绍更复杂的过程抽象。
现在我们用C语言表示一个复数。如果从直角座标系来看,复数由实部和虚部组成,如果从极座标系来看,复数由模和辐角组成,两种座标系可以相互转换。如下图所示
比如用实部和虚部表示一个复数,我们可以采用两个double
型组成的结构体:
struct complex_struct { double x, y; };
这样定义了complex_struct
这个标识符,既然是标识符,那么它的命名规则就和变量一样,但它不表示一个变量,而表示一个类型,这种标识符在C语言中称为Tag,struct complex_struct { double x, y; }
整个可以看作一个类型名,就像int
或double
一样,只不过它是一个复合类型[12],如果用这个类型名来定义变量,可以这样写:
struct complex_struct { double x, y; } z1, z2;
这样z1
和z2
就是两个变量名,变量定义后面带个;号是我们早就习惯的。但即使像上面那样只定义了complex_struct
这个Tag而不定义变量,后面的;号也不能少。这点一定要注意,结构体定义后面少;号是初学者很常犯的错误。不管是用上面两种形式的哪一种形式定义了complex_struct
这个Tag,以后都可以直接用struct complex_struct
来代替类型名了。例如可以这样定义另外两个复数变量:
struct complex_struct z3, z4;
如果在定义结构体类型的同时定义了变量,也可以不必写Tag,例如:
struct { double x, y; } z1, z2;
但这样就没有办法再次引用这个结构体类型了,因为它没有名字。每个复数变量都有两个成员(Member)x和y,可以用.运算符(.号,Period)来访问,这两个成员的存储空间是相邻的[13],合在一起组成复数变量的存储空间。看下面的例子:
例 7.1. 定义和访问结构体
#include <stdio.h> int main(void) { struct complex_struct { double x, y; } z; double x = 3.0; z.x = x; z.y = 4.0; if (z.y < 0) printf("z=%f%fi\n", z.x, z.y); else printf("z=%f+%fi\n", z.x, z.y); return 0; }
注意上例中变量x和变量z的成员x的名字并不冲突,因为变量z的成员x总是用.运算符来访问的,编译器可以区分开哪个x是变量x,哪个x是变量z的成员x,它们属于不同的命名空间(Name Space)。Tag也可以定义在函数外面,就像全局变量一样,这样定义的Tag在其定义之后的各函数中都可以使用。例如:
struct complex_struct { double x, y; }; int main(void) { struct complex_struct z; ......
结构体变量也可以在定义时初始化,例如:
struct complex_struct z = { 3.0, 4.0 };
Initializer中的数据依次赋给结构体的成员。如果Initializer中的数据比结构体的成员多,编译器会报错,但如果只是末尾多个逗号不算错。如果Initializer中的数据比结构体的成员少,未指定的成员将用0来初始化,就像未初始化的全局变量一样。例如以下几种形式的初始化都是合法的:
double x = 3.0; struct complex_struct z1 = { x, 4.0, }; /* z1.x=3.0, z1.y=4.0 */ struct complex_struct z2 = { 3.0, }; /* z2.x=3.0, z2.y=0.0 */ struct complex_struct z3 = { }; /* z3.x=0.0, z3.y=0.0 */
其中,z1必须是函数的局部变量才能用变量x来初始化,如果是全局变量就只能用常量表达式来初始化。尽管结构体的初始化可以用这种语法,结构体赋值却不行,例如这样是错误的:
struct complex_struct z1; z1 = { 3.0, 4.0 };
以前使用基本数据类型时,能用来初始化的表达式就能用来赋值,在这一点上结构体的语法规则有点不同[14]。结构体类型的值用在表达式中有很多限制,不像基本数据类型那么自由,比如+-*/等算术运算符和&&、||、!等逻辑运算符都不能作用于结构体类型,if
、while
的控制表达式的值也不能是结构体类型。严格来说,可以做算术运算的类型称为算术类型(Arithmetic Type),算术类型包括整型和浮点型。可以做逻辑与、或、非运算的操作数或者if
、for
、while
的控制表达式的类型称为标量类型(Scalar Type),标量类型包括算术类型和以后要讲的指针类型。
结构体类型之间用赋值运算符是允许的,用一个结构体初始化另一个结构体也是允许的,例如:
struct complex_struct z1 = { 3.0, 4.0 }; struct complex_struct z2 = z1; z1 = z2;
同样地,z2必须是局部变量才能用变量z1来初始化。既然可以这样用,那么结构体可以当作函数的参数和返回值来传递就在意料之中了:
struct complex_struct add_complex(struct complex_struct z1, struct complex_struct z2) { z1.x = z1.x + z2.x; z1.y = z1.y + z2.y; return z1; }
这个函数实现了两个复数相加,如果在main
函数中这样调用:
struct complex_struct z = { 3.0, 4.0 }; z = add_complex(z, z);
那么调用传参的过程如下图所示:
变量z在main
函数的栈帧中,参数z1
和z2
在add_complex
函数的栈帧中,z的值分别赋给z1
和z2
。在这个函数里,z2
的实部和虚部被累加到z1
中,然后return z1;
,可以看成是:
把z1
拷到一个临时变量里。
函数返回并释放栈帧。
把临时变量的值拷给变量z,释放临时变量。