结构体
定义结构体
例如,如果用实部和虚部表示一个复数,我们可以写成由两个double型组成的结构体:
1 2 3 struct complex_struct { double x, y; };
这一句定义了标识符complex_struct
(同样遵循标识符的命名规则),这种标识符在C语言中称为Tag,struct complex_struct { double x, y; }
整个可以看作一个类型名,就像int或double一样,只不过它是一个复合类型,如果用这个类型名来定义变量,可以这样写:
1 2 3 struct complex_struct { double x, y; } z1, z2;
注意后面的;
不能少。类型定义也是一种声明,声明都要以;
号结尾。
也可以在定义了`complex_struct`后直接用普通的声明语句来声明 z1 和 z2:
1 struct complex_struct z1 , z2 ;
### 访问结构体成员 ###
每个复数变量都有两个成员(Member)x和y,可以用`.`运算符(`.`号,Period)来访问,这两个成员的存储空间是相邻的,合在一起组成复数变量的存储空间。看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #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只能通过表达式z.x来访问,编译器可以从语法上区分哪个x是变量x,哪个x是变量z的成员x。
### 初始化和赋值 ###
结构体变量也可以在定义时初始化,例如:
1 struct complex_struct z = { 3.0 , 4.0 };
Initializer中的数据依次赋给结构体的各成员。如果Initializer中的数据比结构体的成员多,编译器会报错,但如果只是末尾多个逗号则不算错。如果Initializer中的数据比结构体的成员少,未指定的成员将用0来初始化,就像未初始化的全局变量一样。例如以下几种形式的初始化都是合法的:
1 2 3 4 double x = 3.0 ;struct complex_struct z1 = { x, 4.0 , }; struct complex_struct z2 = { 3.0 , }; struct complex_struct z3 = { 0 };
注意,z1必须是局部变量才能用另一个变量x的值来初始化它的成员,如果是全局变量就只能用常量表达式来初始化。这也是C99的新特性,C89只允许在`{}`中使用常量表达式来初始化,无论是初始化全局变量还是局部变量。
`{}`这种语法不能用于结构体的赋值,例如这样是错误的 ^[在 C99 后支持使用一种新的语法语法 Compound Literal,例如:` z1 = (struct complex_struct){3.0, 4.0};`。]:
1 2 struct complex_struct z1 ;z1 = { 3.0 , 4.0 };
### 实例:构造复数 ###
现在我们来实现一个完整的复数运算程序。在上一节我们已经定义了复数的结构体类型,现在需要围绕它定义一些函数。复数可以用直角坐标或极坐标表示,直角坐标做加减法比较方便,极坐标做乘除法比较方便。如果我们定义的复数结构体是直角坐标的,那么应该提供极坐标的转换函数,以便在需要的时候可以方便地取它的模和辐角。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <math.h> struct complex_struct { double x, y; }; double real_part (struct complex_struct z) { return z.x; } double img_part (struct complex_struct z) { return z.y; } double magnitude (struct complex_struct z) { return sqrt (z.x * z.x + z.y * z.y); } double angle (struct complex_struct z) { return atan2 (z.y, z.x); } struct complex_struct make_from_real_img (double x, double y) { struct complex_struct z ; z.x = x; z.y = y; return z; } struct complex_struct make_from_mag_ang (double r, double A) { struct complex_struct z ; z.x = r * cos (A); z.y = r * sin (A); return z; } struct complex_struct add_complex (struct complex_struct z1, struct complex_struct z2) { return make_from_real_img( real_part(z1) + real_part(z2), img_part(z1) + img_part(z2) ); } struct complex_struct sub_complex (struct complex_struct z1, struct complex_struct z2) { return make_from_real_img( real_part(z1) - real_part(z2), img_part(z1) - img_part(z2) ); } struct complex_struct mul_complex (struct complex_struct z1, struct complex_struct z2) { return make_from_mag_ang( magnitude(z1) * magnitude(z2), angle(z1) + angle(z2) ); } struct complex_struct div_complex (struct complex_struct z1, struct complex_struct z2) { return make_from_mag_ang( magnitude(z1) / magnitude(z2), angle(z1) - angle(z2) ); }
可以看出,复数加减乘除运算的实现并没有直接访问结构体 complex_struct 的成员x和y,而是把它看成一个整体,通过调用相关函数来取它的直角坐标和极坐标。这样就可以非常方便地替换掉结构体 complex_struct 的存储表示,例如改为用极坐标来存储。
### 嵌套结构体 ###
结构体也是一种递归定义:结构体的成员具有某种数据类型,而结构体本身也是一种数据类型。换句话说,结构体的成员可以是另一个结构体,即结构体可以嵌套定义。例如我们在复数的基础上定义复平面上的线段:
1 2 3 4 struct Segment { struct complex_struct start ; struct complex_struct end ; };
#### 初始化 ####
嵌套结构体可以嵌套地初始化,例如:
1 struct Segment s = { {1.0 , 2.0 }, { 4.0 , 6.0 } };
也可以平坦(Flat)地初始化。例如:
1 struct Segment s = { 1.0 , 2.0 , 4.0 , 6.0 };
甚至可以把两种方式混合使用(这样可读性很差,应该避免):
1 struct Segment s = { { 1.0 , 2.0 }, 4.0 , 6.0 };
利用C99的新特性也可以做 Memberwise Initialization,例如:
1 struct Segment s = { .start.x = 1.0 , .end .x = 2.0 };
#### 访问结构体成员 ####
访问嵌套结构体的成员要用到多个`.`运算符,例如:
1 2 3 s.start.t = RECTANGULAR; s.start.a = 1.0 ; s.start.b = 2.0 ;
## 枚举 ##
### 定义 ###
enum关键字的作用和struct关键字类似,例如:
1 enum coordinate_type { RECTANGULAR, POLAR };
`enum coordinate_type` 表示一个枚举(Enumeration)类型。**枚举类型的成员是常量**,它们的值由编译器自动分配,例如定义了上面的枚举类型之后,RECTANGULAR就表示常量0,POLAR表示常量1。如果不希望从0开始分配,可以这样定义:
1 enum coordinate_type { RECTANGULAR = 1 , POLAR };
这样,RECTANGULAR就表示常量1,而POLAR表示常量2。枚举常量也是一种整型,其值在编译时确定,因此也可以出现在常量表达式中,可以用于初始化全局变量或者作为case分支的判断条件。
有一点需要注意,虽然结构体的成员名和变量名不在同一命名空间中,但枚举的成员名却和变量名在同一命名空间中,所以会出现命名冲突。例如这样是不合法的:
1 2 3 4 5 6 7 int main (void ) { enum coordinate_type { RECTANGULAR = 1 , POLAR }; int RECTANGULAR; printf ("%d %d\n" , RECTANGULAR, POLAR); return 0 ; }
### 赋值 ###
枚举类型的对象的初始化或赋值,只能通过其枚举成员或同一枚举类型的其他对象来进行,例如:
1 2 3 4 Points pt3d = point3d; Points pt2w = 3 ; pt2w = polygon; pt2w = pt3d;
注意把 3 赋给 Points 对象是非法的,即使 3 与一个 Points 枚举成员相关联。
### 实例 ###
可以对在上一节中的 complex_struct 进行改进,让其同时支持两种直角坐标和极坐标两种存储格式,方法是为 complex_struct 结构体添加一个枚举常量用作数据类型标志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 enum coordinate_type {RECTANGULAR, POLAR};struct complex_struct { enum coordinate_type t; double a, b; }; struct complex_struct make_from_real_img (double x, double y) { struct complex_struct z ; z.t = RECTANGULAR; z.a = x; z.b = y; return z; } struct complex_struct make_from_mag_ang (double r, double A) { struct complex_struct z ; z.t = POLAR; z.a = r; z.b = A; return z; }
如果数据类型标志为0,那么两个浮点数就表示直角坐标,如果数据类型标志为1,那么两个浮点数就表示极坐标。这样,直角坐标和极坐标的数据都可以适配(Adapt)到 complex_struct 结构体中,无需转换和损失精度。
## 数组 ##
数组(Array)也是一种复合数据类型,它由一系列相同类型的元素(Element)组成。
### 定义数组 ###
#### 简单数组 ####
例如定义一个由4个int型元素组成的数组count:
和[结构体](/wiki/c-compound-type.html)成员类似,数组count的4个元素的存储空间也是相邻的。
#### 定义复合类型数组 ####
结构体成员可以是基本数据类型,也可以是复合数据类型,数组中的元素也是如此。根据组合规则,我们可以定义一个由4个结构体元素组成的数组:
1 2 3 struct complex_struct { double x, y; } a[4 ];
### 访问数组元素 ###
数组中的元素通过下标(或者叫索引,Index)来访问。例如前面定义的由4个int型元素组成的数组count图示如下:
1 2 3 4 5 0 1 2 3 +------|------|------|------+ | | | | | count | 0 | 0 | 0 | 0 | +------|------|------|------+
整个数组占了4个int型的存储单元,存储单元用小方框表示,里面的数字是存储在这个单元中的数据(假设都是0),而框外面的数字是下标,这四个单元分别用count[0]、count[1]、count[2]、count[3]来访问。
和我们平常数数的习惯不同,数组元素是从“0”开始数的。大多数编程语言都是这么规定的,所以计算机术语中有 Zeroth 这个词。
这种数组下标的表达式不仅可以表示存储单元中的值,也可以表示存储单元本身,也就是说可以做左值,因此以下语句都是正确的:
1 2 3 count[0 ] = 7 ; count[1 ] = count[0 ] * 2 ; ++count[2 ];
数组下标也可以是表达式,但表达式的值必须是整型的。例如:
1 2 int i = 10 ;count[i] = count[i+1 ];
使用数组下标不能超出数组的长度范围,这一点在使用变量做数组下标时尤其要注意。C编译器并不检查count[-1]或是count[100]这样的访问越界错误 ,编译时能顺利通过,所以属于运行时错误。但有时候这种错误很隐蔽,发生访问越界时程序可能并不会立即崩溃,而执行到后面某个正确的语句时却有可能突然崩溃。所以从一开始写代码时就要小心避免出问题,事后依靠调试来解决问题的成本是很高的。
初始化
数组也可以像结构体一样初始化,未赋初值的元素也是用0来初始化,例如:
1 int count[4 ] = { 3 , 2 , };
则count[0]等于3, count[1]等于2,后面两个元素等于0。如果定义数组的同时初始化它,也可以不指定数组的长度,例如:
1 int count[] = { 3 , 2 , 1 , };
编译器会根据Initializer有三个元素确定数组的长度为3。利用C99的新特性也可以做 Memberwise Initialization:
1 int count[4 ] = { [2 ] = 3 };
示例
下面举一个完整的例子:
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> int main (void ) { int count[4 ] = { 3 , 2 , }, i; for (i = 0 ; i < 4 ; i++) printf ("count[%d]=%d\n" , i, count[i]); return 0 ; }
多维数组
就像结构体可以嵌套一样,数组也可以嵌套,一个数组的元素可以是另外一个数组,这样就构成了多维数组(Multi-dimensional Array)。例如定义并初始化一个二维数组:
1 int a[3 ][2 ] = { 1 , 2 , 3 , 4 , 5 };
数组a有3个元素,a[0]、a[1]、a[2]。每个元素也是一个数组,例如a[0]是一个数组,它有两个元素a[0][0]、a[0][1],这两个元素的类型是int,值分别是1、2,同理,数组a[1]的两个元素是3、4,数组a[2]的两个元素是5、0。如下图所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 +----|----+ 概 0 | 1 | 2 | 念 +----|----+ 模 1 | 3 | 4 | 型 +----|----+ 3 | 5 | 0 | +----|----+ 物理模型 a[0][0] a[1][0] a[2][0] +----|----|----|----|----|----+ | 1 | 2 | 3 | 4 | 5 | 0 | +----|----|----|----|----|----+ a[0][1] a[1][1] a[2][1]
从概念模型上看,这个二维数组是三行两列的表格,元素的两个下标分别是行号和列号。从物理模型上看,这六个元素在存储器中仍然是连续存储的,就像一维数组一样,相当于把概念模型的表格一行一行接起来拼成一串,C语言的这种存储方式称为Row-major 方式,而有些编程语言(例如FORTRAN)是把概念模型的表格一列一列接起来拼成一串存储的,称为Column-major 方式。
多维数组也可以像嵌套结构体一样用嵌套Initializer初始化,例如上面的二维数组也可以这样初始化:
1 2 3 int a[][2 ] = { { 1 , 2 }, { 3 , 4 }, { 5 , } };
注意,除了第一维的长度可以由编译器自动计算而不需要指定,其余各维都必须明确指定长度 。
利用C99的新特性也可以做 Memberwise Initialization,例如:
1 int a[3 ][2 ] = { [0 ][1 ] = 9 , [2 ][1 ] = 8 };
字符串
字符串可以看作一个数组,它的每个元素是字符型的,例如字符串"Hello, world.\n"图示如下:
1 2 3 +----|----|----|----|----|---------|----|----|----|----|----|----|----|----+ | h | e | l | l | o | , | | w | o | r | l | d | . | \n | \0 | +----|----|----|----|----|----|----|----|----|----|----|----|----|----|----+
注意每个字符串末尾都有一个字符\0
做结束符,这里的\0是ASCII码的八进制表示,也就是ASCII码为0的Null字符,在C语言中这种字符串也称为以零结尾的字符串(Null-terminated String)。数组元素可以通过数组名加下标的方式访问,而字符串字面值也可以像数组名一样使用,可以加下标访问其中的字符:
1 char c = "Hello, world.\n" [0 ];
初始化
字符数组也可以用一个字符串字面值来初始化:
相当于
1 char str[10 ] = { 'H' , 'e' , 'l' , 'l' , 'o' , '\0' };
str的后四个元素没有指定,自动初始化为0,即Null字符。注意,虽然字符串字面值"Hello"是只读的,但用它初始化的数组str却是可读可写的。数组str中保存了一串字符,以’\0’结尾,也可以叫字符串。在本书中只要是以Null字符结尾的一串字符都叫字符串,不管是像str这样的数组,还是像"Hello"这样的字符串字面值。
如果用于初始化的字符串字面值比数组还长,比如:
1 char str[10 ] = "Hello, world.\n" ;
则数组str只包含字符串的前10个字符,不包含Null字符,这种情况编译器会给出警告。如果要用一个字符串字面值准确地初始化一个字符数组,最好的办法是不指定数组的长度,让编译器自己计算:
1 char str[] = "Hello, world.\n" ;
字符串字面值的长度包括Null字符在内一共15个字符,编译器会确定数组str的长度为15。
有一种情况需要特别注意,如果用于初始化的字符串字面值比数组刚好长出一个Null字符的长度,比如:
1 char str[14 ] = "Hello, world.\n" ;
则数组str不包含Null字符,并且编译器不会给出警告。
多维字符数组
多维字符数组也可以嵌套使用字符串字面值做Initializer,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> void print_day (int day) { char days[8 ][10 ] = { "" , "Monday" , "Tuesday" , "Wednesday" , "Thursday" , "Friday" , "Saturday" , "Sunday" }; if (day < 1 || day > 7 ) printf ("Illegal day number!\n" ); printf ("%s\n" , days[day]); } int main (void ) { print_day(2 ); return 0 ; }
这个程序中定义了一个多维字符数组char days[8][10];,为了使1~7刚好映射到days[1]~days[7],我们把days[0]空出来不用,所以第一维的长度是8,为了使最长的字符串"Wednesday"能够保存到一行,末尾还能多出一个Null字符的位置,所以第二维的长度是10。
类型总结
C语言类型总结
C语言的类型分为函数类型、对象类型和不完全类型三大类。对象类型又分为标量类型和非标量类型。指针类型属于标量类型,因此也可以做逻辑与、或、非运算的操作数和if、for、while的控制表达式,NULL指针表示假,非NULL指针表示真。不完全类型是暂时没有完全定义好的类型,编译器不知道这种类型该占几个字节的存储空间,例如:
1 2 3 struct s ;union u;char str[];
具有不完全类型的变量可以通过多次声明组合成一个完全类型,比如数组str声明两次:
1 2 char str[];char str[10 ];
当编译器碰到第一个声明时,认为str是一个不完全类型,碰到第二个声明时str就组合成完全类型了,如果编译器处理到程序文件的末尾仍然无法把str组合成一个完全类型,就会报错。
不完全的结构体类型
不完全的结构体类型有重要作用:
1 2 3 4 5 6 7 struct s { struct t *pt ; }; struct t { struct s *ps ; };
struct s和struct t各有一个指针成员指向另一种类型。编译器从前到后依次处理,当看到struct s { struct t* pt; };
时,认为struct t是一个不完全类型,pt是一个指向不完全类型的指针,尽管如此,这个指针却是完全类型,因为不管什么指针都占4个字节存储空间 ,这一点很明确。然后编译器又看到struct t { struct s *ps; };
,这时struct t有了完整的定义,就组合成一个完全类型了,pt的类型就组合成一个指向完全类型的指针。由于struct s在前面有完整的定义,所以struct s *ps;
也定义了一个指向完全类型的指针。
这样的类型定义是错误的:
1 2 3 4 5 6 7 struct s { struct t ot ; }; struct t { struct s os ; };
编译器看到struct s { struct t ot; };
时,认为struct t是一个不完全类型,无法定义成员ot,因为不知道它该占几个字节。所以结构体中可以递归地定义指针成员,但不能递归地定义变量成员,你可以设想一下,假如允许递归地定义变量成员,struct s中有一个struct t,struct t中又有一个struct s,struct s又中有一个struct t,这就成了一个无穷递归的定义。
一个结构体的递归定义
以上是两个结构体构成的递归定义,一个结构体也可以递归定义:
1 2 3 4 struct s { char data[6 ]; struct s * next ; };
当编译器处理到第一行struct s {时,认为struct s是一个不完全类型,当处理到第三行struct s *next;时,认为next是一个指向不完全类型的指针,当处理到第四行};时,struct s成了一个完全类型,next也成了一个指向完全类型的指针。类似这样的结构体是很多种数据结构的基本组成单元,如链表、二叉树等。
分析复杂的类型
可以想像得到,如果把指针和数组、函数、结构体层层组合起来可以构成非常复杂的类型。在分析复杂声明时,要借助typedef把复杂声明分解成几种基本形式 :
T *p;
,p是指向T类型的指针。
T a[];
,a是由T类型的元素组成的数组,但有一个例外,如果a是函数的形参,则相当于T *a;
T1 f(T2, T3...);
,f是一个函数,参数类型是T2、T3等等,返回值类型是T1。
我们分解一下这个复杂声明:
1 int (*(*fp)(void *))[10 ];
fp和*号括在一起,说明fp是一个指针,指向T1类型:
1 2 typedef int (*T1(void *))[10]; T1 *fp;
T1应该是一个函数类型,参数是void *,返回值是T2类型:
1 2 3 typedef int (*T2)[10]; typedef T2 T1 (void *) ;T1 *fp;
T2和*号括在一起,应该也是个指针,指向T3类型:
1 2 3 4 typedef int T3[10 ];typedef T3 *T2;typedef T2 T1 (void *) ;T1 *fp;
显然,T3是一个int数组,由10个元素组成。分解完毕。