本章介绍以下内容:
关键字:struct、union、typedef
运算符:.、->
什么是C结构,如何创建结构模板和结构变量
如何访问结构的成员,如何编写处理结构的函数
联合和指向函数的指针
设计程序时,最重要的步骤之一是选择表示数据的方法。在许多情况下,简单变量甚至是数组还不够。为此,C提供了结构变量(structure variable)提高你表示数据的能力,它能让你创造新的形式。如果熟悉Pascal的记录(record),应该很容易理解结构。如果不懂Pascal也没关系,本章将详细介绍C结构。我们先通过一个示例来分析为何需要C结构,学习如何创建和使用结构。
创建的结构有3部分,每个部分都称为成员(member)或字段(field)
结构声明(structure declaration)描述了一个结构的组织布局
声明类似下面这样:
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
该声明并未创建实际的数据对象,只描述了该对象由什么组成。〔有时,我们把结构声明称为模板,因为它勾勒出结构是如何储存数据的
首先是关键字 struct,它表明跟在其后的是一个结构,后面是一个可选的标记(该例中是 book),稍后程序中可以使用该标记引用该结构
struct book library;
这把library声明为一个使用book结构布局的结构变量
在结构声明中,用一对花括号括起来的是结构成员列表。每个成员都用自己的声明来描述
成员可以是任意一种C的数据类型,甚至可以是其他结构!右花括号后面的分号是声明所必需的,表示结构布局定义结束
可以把这个声明放在所有函数的外部(如本例所示),也可以放在一个函数定义的内部。如果把结构声明置于一个函数的内部,它的标记就只限于该函数内部使用。如果把结构声明置于函数的外部,那么该声明之后的所有函数都能使用它的标记
结构的标记名是可选的。但是以程序示例中的方式建立结构时(在一处定义结构布局,在另一处定义实际的结构变量),必须使用标记
结构有两层含义。一层含义是“结构布局”,刚才已经讨论过了。结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。下一步是创建一个结构变量,即是结构的另一层含义
在结构变量的声明中,struct book所起的作用相当于一般声明中的int或float
图14.1 一个结构的内存分配
就计算机而言,下面的声明:
struct book library;
是以下声明的简化:
struct book {
char title[MAXTITL];
char author[AXAUTL];
float value;
} library; /* 声明的右右花括号后跟变量名*/
声明结构的过程和定义结构变量的过程可以组合成一个步骤。如下所示,组合后的结构声明和结构变量定义不需要使用结构标记:
struct { /* 无结构标记 */
char title[MAXTITL];
char author[MAXAUTL];
float value;
} library;
然而,如果打算多次使用结构模板,就要使用带标记的形式;或者,使用本章后面介绍的typedef
初始化一个结构变量(ANSI之前,不能用自动变量初始化结构;ANSI之后可以用任意存储类别)与初始化数组的语法类似:
struct book library = {
"The Pious Pirate and the Devious Damsel",
"Renee Vivotte",
1.95
};
我们使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔
为了让初始化项与结构中各成员的关联更加明显,我们让每个成员的初始化项独占一行。这样做只是为了提高代码的可读性,对编译器而言,只需要用逗号分隔各成员的初始化项即可
如果初始化静态存储期的变量(如,静态外部链接、静态内部链接或静态无链接),必须使用常量值。这同样适用于结构。如果初始化一个静态存储期的结构,初始化列表中的值必须是常量表达式。如果是自动存储期,初始化列表中的值可以不是常量
结构类似于一个“超级数组”
使用结构成员运算符——点(.)访问结构中的成员
C99和C11为结构提供了指定初始化器(designated initializer)[1],其语法与数组的指定初始化器类似。但是,结构的指定初始化器使用点运算符和成员名(而不是方括号和下标)标识特定的元素
struct book surprise = { .value = 10.99};
可以按照任意顺序使用指定初始化器:
struct book gift = { .value = 25.99,
.author = "James Broadfool",
.title = "Rue for the Toad"};
在指定初始化器后面的普通初始化器,为指定成员后面的成员提供初始值。另外,对特定成员的最后一次赋值才是它实际获得的值
struct book gift= {.value = 18.90,
.author = "Philionna Pestle",
0.25};
赋给value的值是0.25
由于该数组是自动存储类别的对象,其中的信息被储存在栈(stack)中。如此大的数组需要很大一块内存,这可能会导致一些问题。如果在运行时出现错误,可能抱怨栈大小或栈溢出,你的编译器可能使用了一个默认大小的栈,这个栈对于该例而言太小。要修正这个问题,可以使用编译器选项设置栈大小为10000,以容纳这个结构数组;或者可以创建静态或外部数组(这样,编译器就不会把数组放在栈中);或者可以减小数组大小为16
struct book library[MAXBKS];
以上代码把library声明为一个内含MAXBKS个元素的数组。数组的每个元素都是一个book类型的数组。因此,library[0]是第1个book类型的结构变量,library[1]是第2个book类型的结构变量,以此类推。参看图14.2 可以帮助读者理解。数组名library本身不是结构名,它是一个数组名,该数组中的每个元素都是struct book类型的结构变量。
图14.2 一个结构数组library[MAXBKS]
为了标识结构数组中的成员,可以采用访问单独结构的规则:在结构名后面加一个点运算符,再在点运算符后面写上成员名
数组下标紧跟在library后面,不是成员名后面:
library.value[2] // 错误
library[2].value // 正确
while (getchar() != '\n')
continue; /* 清理输入行 */
前面章节介绍过,这段代码弥补了scanf()函数遇到空格和换行符就结束读取的问题
scanf()函数接受1、2、.、5和0,但是把\n留在输入序列中。如果没有上面两行清理输入行的代码,就会把留在输入序列中的换行符当作空行读入,程序以为用户发送了停止输入的信号。我们插入的这两行代码只会在输入序列中查找并删除\n,不会处理其他字符。这样s_gets()就可以重新开始下一次输入
注意如何在结构声明中创建嵌套结构。和声明int类型变量一样,进行简单的声明:
struct names handle;
该声明表明handle是一个struct name类型的变量。当然,文件中也应包含结构names的声明。
其次,注意如何访问嵌套结构的成员,这需要使用两次点运算符:
printf("Hello, %s!\n", fellow.handle.first);
从左往右解释fellow.handle.first:
(fellow.handle).first
第一,就像指向数组的指针比数组本身更容易操控(如,排序问题)一样,指向结构的指针通常比结构本身更容易操控。第二,在一些早期的C实现中,结构不能作为参数传递给函数,但是可以传递指向结构的指针。第三,即使能传递一个结构,传递指针通常更有效率。第四,一些用于表示数据的结构中包含指向其他结构的指针
声明结构指针很简单:
struct guy * him;
首先是关键字 struct,其次是结构标记 guy,然后是一个星号(*),其后跟着指针名。这个语法和其他指针声明一样
和数组不同的是,结构名并不是结构的地址,因此要在结构名前面加上&运算符
him加1相当于him指向的地址加84。在十六进制中,874 - 820 = 54(十六进制)=84(十进制),因为每个guy结构都占用84字节的内存:names.first占用20字节,names.last占用20字节,favfood占用20字节,job占用20字节,income占用4字节(假设系统中float占用4字节)
顺带一提,在有些系统中,一个结构的大小可能大于它各成员大小之和
第1种方法也是最常用的方法:使用->运算符。该运算符由一个连接号(-)后跟一个大于号(>)组成。我们有下面的关系:
如果him == &barney,那么him->income 即是 barney.income
如果him == &fellow[0],那么him->income 即是 fellow[0].income
换句话说,->运算符后面的结构指针和.运算符后面的结构名工作方式相同(不能写成him.incone,因为him不是结构名)
第2种方法是,以这样的顺序指定结构成员的值:如果him == &fellow[0],那么*him == fellow[0],因为&和*是一对互逆运算符。因此,可以做以下替代:
fellow[0].income == (*him).income
必须要使用圆括号,因为.运算符比*运算符的优先级高。
总之,如果him是指向guy类型结构barney的指针,下面的关系恒成立:
barney.income == (*him).income == him->income // 假设 him == &barney
ANSI C允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。如果你只关心结构中的某一部分,也可以把结构的成员作为参数
如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址:
modify(&stan.bankfund);
必须使用&运算符来获取结构的地址。和数组名不同,结构名只是其地址的别名
现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。也就是说,如果n_data和o_data都是相同类型的结构,可以这样做:
o_data = n_data; // 把一个结构赋值给另一个结构
这条语句把n_data的每个成员的值都赋给o_data的相应成员。即使成员是数组,也能完成赋值
还可以把一个结构初始化为相同类型的另一个结构:
struct names right_field = {"Ruthie", "George"};
struct names captain = right_field; // 把一个结构初始化为另一个结构
现在的C(包括ANSI C),函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。结构指针也允许这种双向通信
第一,为了传递结构本身,函数的参数必须是person,而不是&person。那么,相应的形式参数应声明为struct namect,而不是指向该类型的指针。第二,可以通过返回一个结构,把结构的信息返回给main()
把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。被调函数中的某些操作可能会意外影响原来结构中的数据。不过,ANSI C新增的const限定符解决了这个问题。例如,如果在程序清单14.8中,showinfo()函数中的代码改变了结构的任意成员,编译器会捕获这个错误
把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚
传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递函数所需的成员更合理。
通常,程序员为了追求效率会使用结构指针作为函数参数,如需防止原始数据被意外修改,使用const限定符。按值传递结构是处理小型结构最常用的方法。
因此,如果要用结构储存字符串,用字符数组作为成员比较简单。用指向 char 的指针也行,但是误用会导致严重的问题
匿名结构是一个没有名称的结构成员
在C11中,可以用嵌套的匿名成员结构定义person:
struct person
{
int id;
struct {char first[20]; char last[20];}; // 匿名结构
};
初始化ted的方式相同:
struct person ted = {8483, {"Ted", "Grass"}};
但是,在访问ted时简化了步骤,只需把first看作是person的成员那样使用它:
puts(ted.first);
当然,也可以把first和last直接作为person的成员,删除嵌套循环
可以把数组名作为数组中第1个结构的地址传递给函数。
然后可以用数组表示法访问数组中的其他结构。注意下面的函数调用与使用数组名效果相同:
sum(&jones[0], N)
因为jones和&jones[0]的地址相同,使用数组名是传递结构地址的一种间接的方法
这些形式包括队列、二叉树、堆、哈希表和图表。许多这样的形式都由链式结构(linked structure)组成。通常,每个结构都包含一两个数据项和一两个指向其他同类型结构的指针。这些指针把一个结构和另一个结构链接起来,并提供一种路径能遍历整个彼此链接的结构。例如,图14.3演示了一个二叉树结构,每个单独的结构(或节点)都和它下面的两个结构(或节点)相连
联合(union)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)
创建联合和创建结构的方式相同,需要一个联合模板和联合变量。可以用一个步骤定义联合,也可以用联合标记分两步定义
union hold {
int digit;
double bigfl;
char letter;
};
根据以上形式声明的结构可以储存一个int类型、一个double类型和char类型的值
声明的联合只能储存一个int类型的值或一个double类型的值或char类型的值
下面定义了3个与hold类型相关的变量:
union hold fit; // hold类型的联合变量
union hold save[10]; // 内含10个联合变量的数组
union hold * pu; // 指向hold类型联合变量的指针
第1个声明创建了一个单独的联合变量fit。编译器分配足够的空间以便它能储存联合声明中占用最大字节的类型
联合只能储存一个值,这与结构不同。有 3 种初始化的方法:把一个联合初始化为另一个同类型的联合;初始化联合的第1个元素;或者根据C99标准,使用指定初始化器
union hold valA;
valA.letter = 'R';
union hold valB = valA; // 用另一个联合来初始化
union hold valC = {88}; // 初始化联合的digit 成员
union hold valD = {.bigfl = 118.2}; // 指定初始化器
点运算符表示正在使用哪种数据类型。在联合中,一次只储存一个值。即使有足够的空间,也不能同时储存一个char类型值和一个int类型值
和用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符:
用一个成员把值储存在一个联合中,然后用另一个成员查看内容,这种做法有时很有用
联合的另一种用法是,在结构中储存与其成员有从属关系的信息
即匿名联合是一个结构或联合的无名联合成员
flits.owncar.socsecurity 代替flits.ownerinfo.owncar.socsecurity
总结:结构和联合运算符
成员运算符:.
一般注释:
该运算符与结构或联合名一起使用,指定结构或联合的一个成员。如果name是一个结构的名称, member是该结构模版指定的一个成员名,下面标识了该结构的这个成员:
name.member
name.member的类型就是member的类型。联合使用成员运算符的方式与结构相同。
示例:
struct {
int code;
float cost;
} item;
item.code = 1265;
间接成员运算符:->
一般注释:
该运算符和指向结构或联合的指针一起使用,标识结构或联合的一个成员。假设ptrstr是指向结构的指针,member是该结构模版指定的一个成员,那么:
ptrstr->member
标识了指向结构的成员。联合使用间接成员运算符的方式与结构相同。
示例:
struct {
int code;
float cost;
} item, * ptrst;
ptrst = &item;
ptrst->code = 3451;
最后一条语句把一个int类型的值赋给item的code成员。如下3个表达式是等价的:
ptrst->code item.code (*ptrst).code
可以用枚举类型(enumerated type)声明符号名称来表示整型常量。使用enum关键字,可以创建一个新“类型”并指定它可具有的值(实际上,enum常量是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)。枚举类型的目的是提高程序的可读性。它的语法与结构的语法相同
enum spectrum {red, orange, yellow, green, blue, violet};
enum spectrum color;
虽然枚举符(如red和blue)是int类型,但是枚举变量可以是任意整数类型,前提是该整数类型可以储存枚举常量。例如,spectrum的枚举符范围是0~5,所以编译器可以用unsigned char来表示color变量
C枚举的一些特性并不适用于C++。例如,C允许枚举变量使用++运算符,但是C++标准不允许。所以,如果编写的代码将来会并入C++程序,那么必须把上面例子中的color声明为int类型,才能C和C++都兼容
red成为一个有名称的常量,代表整数0。类似地,其他标识符都是有名称的常量,分别代表1~5。只要是能使用整型常量的地方就可以使用枚举常量。例如,在声明数组时,可以用枚举常量表示数组的大小;在switch语句中,可以把枚举常量作为标签
默认情况下,枚举列表中的常量都被赋予0、1、2等。因此,下面的声明中nina的值是3:
enum kids {nippy, slats, skippy, nina, liz};
在枚举声明中,可以为枚举常量指定整数值:
enum levels {low = 100, medium = 500, high = 2000};
如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。例如,假设有如下的声明:
enum feline {cat, lynx = 10, puma, tiger};
那么,cat的值是0(默认),lynx、puma和tiger的值分别是10、11、12。
枚举类型的目的是为了提高程序的可读性和可维护性。如果要处理颜色,使用red和blue比使用0和1更直观。注意,枚举类型只能在内部使用。如果要输入color中orange的值,只能输入1,而不是单词orange。或者,让程序先读入字符串"orange",再将其转换为orange代表的值
因为枚举类型是整数类型,所以可以在表达式中以使用整数变量的方式使用enum变量。它们用在case语句中很方便
C语言使用名称空间(namespace)标识程序中的各部分,即通过名称来识别
作用域是名称空间概念的一部分:两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突。名称空间是分类别的。在特定作用域中的结构标记、联合标记和枚举标记都共享相同的名称空间,该名称空间与普通变量使用的空间不同。这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量
尽管如此,以两种不同的方式使用相同的标识符会造成混乱。另外,C++不允许这样做,因为它把标记名和变量名放在相同的名称空间中
typedef工具是一个高级数据特性,利用typedef可以为某一类型自定义名称
这方面与#define类似,但是两者有3处不同:
与#define不同,typedef创建的符号名只受限于类型,不能用于值。
typedef由编译器解释,不是预处理器。
在其受限范围内,typedef比#define更灵活
该定义的作用域取决于typedef定义所在的位置。如果定义在函数中,就具有局部作用域,受限于定义所在的函数。如果定义在函数外面,就具有文件作用域
通常,typedef定义中用大写字母表示被定义的名称,以提醒用户这个类型名实际上是一个符号缩写
也可以用小写:
typedef unsigned char byte;
typedef中使用的名称遵循变量的命名规则
使用typedef还能提高程序的可移植性
typedef的一些特性与#define的功能重合。例如:
#define BYTE unsigned char
这使预处理器用BYTE替换unsigned char。但是也有#define没有的功能:
typedef char * STRING;
没有typedef关键字,编译器将把STRING识别为一个指向char的指针变量。有了typedef关键字,编译器则把STRING解释成一个类型的标识符,该类型是指向char的指针。因此:
STRING name, sign;
相当于:
char * name, * sign;
但是,如果这样假设:
#define STRING char *
然后,下面的声明:
STRING name, sign;
将被翻译成:
char * name, sign;
这导致只有name才是指针
还可以把typedef用于结构:
typedef struct complex {
float real;
float imag;
} COMPLEX;
然后便可使用COMPLEX类型代替complex结构来表示复数。使用typedef的第1个原因是:为经常出现的类型创建一个方便、易识别的类型名。例如,前面的例子中,许多人更倾向于使用 STRING 或与其等价的标记
用typedef来命名一个结构类型时,可以省略该结构的标签:
typedef struct {double x; double y;} rect;
使用typedef的第2个原因是:typedef常用于给复杂的类型命名。例如,下面的声明:
typedef char (* FRPTC ()) [5];
把FRPTC声明为一个函数类型,该函数返回一个指针,该指针指向内含5个char类型元素的数组
typedef并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签
表14.1 声明时可使用的符号
int board[8][8]; // 声明一个内含int数组的数组
int ** ptr; // 声明一个指向指针的指针,被指向的指针指向int
int * risks[10]; // 声明一个内含10个元素的数组,每个元素都是一个指向int的指针
int (* rusks)[10]; // 声明一个指向数组的指针,该数组内含10个int类型的值
int * oof[3][4]; // 声明一个3×4 的二维数组,每个元素都是指向int的指针
int (* uuf)[3][4]; // 声明一个指向3×4二维数组的指针,该数组中内含int类型值
int (* uof[3])[4]; // 声明一个内含3个指针元素的数组,其中每个指针都指向一个内含4个int类型元素的数组
要看懂以上声明,关键要理解*、()和[]的优先级。记住下面几条规则。
1.数组名后面的[]和函数名后面的()具有相同的优先级。它们比*(解引用运算符)的优先级高。因此下面声明的risk是一个指针数组,不是指向数组的指针:
int * risks[10];
2.[]和()的优先级相同,由于都是从左往右结合,所以下面的声明中,在应用方括号之前,*先与rusks结合。因此rusks是一个指向数组的指针,该数组内含10个int类型的元素:
int (* rusks)[10];
3.[]和()都是从左往右结合。因此下面声明的goods是一个由12个内含50个int类型值的数组组成的二维数组,不是一个有50个内含12个int类型值的数组组成的二维数组:
int goods[12][50];
函数指针常用作另一个函数的参数,告诉该函数要使用哪一个函数
void (*pf)(char *); // pf 是一个指向函数的指针
在声明函数指针时必须把*和指针名括起来。如果省略第1个圆括号会导致完全不同的情况:
void *pf(char *); // pf 是一个返回字符指针的函数
要声明一个指向特定类型函数的指针,可以先声明一个该类型的函数,然后把函数名替换成(*pf)形式的表达式。然后,pf就成为指向该类型函数的指针
声明了函数指针后,可以把类型匹配的函数地址赋给它。在这种上下文中,函数名可以用于表示函数的地址:
void ToUpper(char *);
void ToLower(char *);
int round(double);
void (*pf)(char *);
pf = ToUpper; // 有效,ToUpper是该类型函数的地址
pf = ToLower; //有效,ToUpper是该类型函数的地址
pf = round; // 无效,round与指针类型不匹配
pf = ToLower(); // 无效,ToLower()不是地址
也可以用函数指针访问函数。奇怪的是,有两种逻辑上不一致的语法可以这样做,下面解释:
void ToUpper(char *);
void ToLower(char *);
void (*pf)(char *);
char mis[] = "Nina Metier";
pf = ToUpper;
(*pf)(mis); // 把ToUpper 作用于(语法1)
pf = ToLower;
pf(mis); // 把ToLower 作用于(语法2)
但是,为了与现有代码兼容,ANSI C认为这两种形式(本例中是(*pf)(mis)和pf(mis))等价。后续的标准也延续了这种矛盾的和谐
图14.4 函数名的用法
C 结构提供在相同的数据对象中储存多个不同类型数据项的方法。可以使用标记来标识一个具体的结构模板,并声明该类型的变量。通过成员点运算符(.)可以使用结构模版中的标签来访问结构的各个成员。
如果有一个指向结构的指针,可以用该指针和间接成员运算符(->)代替结构名和点运算符来访问结构的各成员。和数组不同,结构名不是结构的地址,要在结构名前使用&运算符才能获得结构的地址。
一贯以来,与结构相关的函数都使用指向结构的指针作为参数。现在的C允许把结构作为参数传递,作为返回值和同类型结构之间赋值。然而,传递结构的地址通常更有效。
联合使用与结构相同的语法。然而,联合的成员共享一个共同的存储空间。联合同一时间内只能储存一个单独的数据项,不像结构那样同时储存多种数据类型。也就是说,结构可以同时储存一个int类型数据、一个double类型数据和一个char类型数据,而相应的联合只能保存一个int类型数据,或者一个double类型数据,或者一个char类型数据。
通过枚举可以创建一系列代表整型常量(枚举常量)的符号和定义相关联的枚举类型。
typedef工具可用于建立C标准类型的别名或缩写。
函数名代表函数的地址,可以把函数的地址作为参数传递给其他函数,然后这些函数就可以使用被指向的函数。如果把特定函数的地址赋给一个名为pf的函数指针,可以通过以下两种方式调用该函数:
#include
...
double (*pdf)(double);
double x;
pdf = sin;
x = (*pdf)(1.2); // 调用sin(1.2)
x = pdf(1.2); // 同样调用 sin(1.2)