结构和其他数据形式
设计程序时,最重要的步骤之一是选择表示数据的方法。在许多情况下,简单变量甚至是数组还不够。为此,C提供了结构变量(structure variable)提高你表示数据的能力,它能让你创造新的形式。
每个部分都称为成员(member)或字段(field)。这3部分中,一部分储存书名,一部分储存作者名,一部分储存价格。下面是必须掌握的3个技巧:
1.为结构建立一个格式或样式;
2.声明一个适合该样式的变量;
3.访问结构变量的各个部分。
1. 建立结构声明
结构声明(structure declaration)描述了一个结构的组织布局。
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
// 首先是关键字 struct,它表明跟在其后的是一个结构,
// 后面是一个可选的标记(该例中是 book),稍后程序中可以使用该标记引用该结构。
// 所以,我们在后面的程序中可以这样声明:
struct book library;
2. 定义结构变量
// 在结构变量的声明中,struct book所起的作用相当于一般声明中的int或float。
// 例如,可以定义两个struct book类型的变量,或者甚至是指向struct book类型结构的指针:
struct book doyle, panshin, * ptbook;
// 结构变量doyle和panshin中都包含title、author和value部分。
// 指针ptbook可以指向doyle、panshin或任何其他book类型的结构变量
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。
// 这是定义结构变量的一个方面,在这个例子中,并未初始化结构变量。
2.1 初始化结构
struct book library = {
"The Pious Pirate and the Devious Damsel",
"Renee Vivotte",
1.95
};
/*
* 我们使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔。
* 注意 初始化结构和类别储存期
* 如果初始化静态存储期的变量(如,静态外部链接、静态内部链接或静态无链接),必须使用常量值。
* 如果初始化一个静态存储期的结构,初始化列表中的值必须是常量表达式。
* 如果是自动存储期,初始化列表中的值可以不是常量。
*/
2.2 访问结构成员
// 使用结构成员运算符———点(.)访问结构中的成员。
library.value 即访问 library 的 value 部分。
2.3 结构的初始化器
结构的指定初始化器使用点运算符和成员名(而不是方括号和下标)标识特定的元素。
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,因为它在结构声明中紧跟在author成员之后。
// 新值0.25取代了之前的18.9。
3. 结构数组
显然,每本书的基本信息都可以用一个 book 类型的结构变量来表示。为描述两本书,需要使用两个变量,以此类推。可以使用这一类型的结构数组来处理多本书。
声明结构数组
struct book library[MAXBKS];
// 数组的每个元素都是一个book类型的数组。
// 数组名library本身不是结构名,它是一个数组名,该数组中的每个元素都是struct book类型的结构变量。
访问数组中的结构成员
// 为了标识结构数组中的成员,可以采用访问单独结构的规则:
// 在结构名后面加一个点运算符,再在点运算符后面写上成员名。
library[0].value /* 第1个数组元素与value 相关联 */
library[4].title /* 第5个数组元素与title 相关联 */
// 注意,数组下标紧跟在library后面,不是成员名后面:
library.value[2] // 错误
library[2].value // 正确
// 使用library[2].value的原因是:library[2]是结构变量名,正如library[1]是另一个变量名。
// 总结
library // 一个book 结构的数组
library[2] // 一个数组元素,该元素是book结构
library[2].title // 一个char数组(library[2]的title成员)
library[2].title[4] // 数组中library[2]元素的title 成员的一个字符
4. 嵌套结构
// friend.c -- 嵌套结构示例
#include <stdio.h>
#define LEN 20
const char * msgs[5] =
{
" Thank you for the wonderful evening, ",
"You certainly prove that a ",
"is a special kind of guy.We must get together",
"over a delicious ",
" and have a few laughs"
};
struct names { // 第1个结构
char first[LEN];
char last[LEN];
};
struct guy { // 第2个结构
struct names handle; // 嵌套结构
char favfood[LEN];
char job[LEN];
float income;
};
int main(void)
{
struct guy fellow = { // 初始化一个结构变量
{ "Ewen", "Villard" },
"grilled salmon",
"personality coach",
68112.00
};
printf("Dear %s, \n\n", fellow.handle.first);
printf("%s%s.\n", msgs[0], fellow.handle.first);
printf("%s%s\n", msgs[1], fellow.job);
printf("%s\n", msgs[2]);
printf("%s%s%s", msgs[3], fellow.favfood, msgs[4]);
if (fellow.income > 150000.0)
puts("!!");
else if (fellow.income > 75000.0)
puts("!");
else
puts(".");
printf("\n%40s%s\n", " ", "See you soon,");
printf("%40s%s\n", " ", "Shalala");
return 0;
}
首先,注意如何在结构声明中创建嵌套结构。和声明int类型变量一样,进行简单的声明:
struct names handle;
该声明表明handle是一个struct name类型的变量。当然,文件中也应包含结构names的声明。
其次,注意如何访问嵌套结构的成员,这需要使用两次点运算符:
printf(“Hello, %s!\n”, fellow.handle.first);
从左往右解释fellow.handle.first:(fellow.handle).first
也就是说,找到fellow,然后找到fellow的handle的成员,再找到handle的first成员。
5. 指向结构的指针
至少有 4 个理由可以解释为何要使用指向结构的指针。
第一,就像指向数组的指针比数组本身更容易操控(如,排序问题)一样,指向结构的指针通常比结构本身更容易操控。
第二,在一些早期的C实现中,结构不能作为参数传递给函数,但是可以传递指向结构的指针。
第三,即使能传递一个结构,传递指针通常更有效率。
第四,一些用于表示数据的结构中包含指向其他结构的指针。
/* friends.c -- 使用指向结构的指针 */
#include <stdio.h>
#define LEN 20
struct names {
char first[LEN];
char last[LEN];
};
struct guy {
struct names handle;
char favfood[LEN];
char job[LEN];
float income;
};
int main(void)
{
struct guy fellow[2] = {
{ { "Ewen", "Villard" },
"grilled salmon",
"personality coach",
68112.00
},
{ { "Rodney", "Swillbelly" },
"tripe",
"tabloid editor",
432400.00
}
};
struct guy * him; /* 这是一个指向结构的指针 */
printf("address #1: %p #2: %p\n", &fellow[0], &fellow[1]);
him = &fellow[0]; /* 告诉编译器该指针指向何处 */
printf("pointer #1: %p #2: %p\n", him, him + 1);
printf("him->income is $%.2f: (*him).income is $%.2f\n",him->income, (*him).income);
him++; /* 指向下一个结构 */
printf("him->favfood is %s: him->handle.last is %s\n",him->favfood, him->handle.last);
return 0;
}
5.1 声明和初始化结构指针
struct guy * him;
// 首先是关键字 struct,其次是结构标记 guy,然后是一个星号(*),其后跟着指针名。
// 声明并未创建一个新的结构,但是指针him现在可以指向任意现有的guy类型的结构。
// 例如,如果barney是一个guy类型的结构,可以这样写:him = &barney;
// 和数组不同的是,结构名并不是结构的地址,因此要在结构名前面加上&运算符
在有些系统中,一个结构的大小可能大于它各成员大小之和。这是因为系统对数据进行校准的过程中产生了一些“缝隙”。例如,有些系统必须把每个成员都放在偶数地址上,或4的倍数的地址上。在这种系统中,结构的内部就存在未使用的“缝隙”。
5.2 用指针访问成员
第1种方法也是最常用的方法:使用->运算符。该运算符由一个连接号(-)后跟一个大于号(>)组成。我们有下面的关系:
如果him == &barney,那么him->income 即是 barney.income
如果him == &fellow[0],那么him->income 即是 fellow[0].income
->运算符后面的结构指针和.运算符后面的结构名工作方式相同(不能写成him.incone,因为him不是结构名)。
要着重理解him是一个指针,但是him->income是该指针所指向结构的一个成员。所以在该例中,him->income是一个float类型的变量。
第2种方法是,以这样的顺序指定结构成员的值:如果him == &fellow[0],那么*him == fellow[0],因为&和*是一对互逆运算符。因此,可以做以下替代:
fellow[0].income == (*him).income
必须要使用圆括号,因为.运算符比*运算符的优先级高。
//总之,如果him是指向guy类型结构barney的指针,下面的关系恒成立:
barney.income == (*him).income == him->income // 假设him == & barney
6. 向函数传递结构的信息
程序员可以选择是传递结构本身,还是传递指向结构的指针。如果你只关心结构中的某一部分,也可以把结构的成员作为参数。
6.1 传递结构成员
/* funds1.c -- 把结构成员作为参数传递 */
#include <stdio.h>
#define FUNDLEN 50
struct funds {
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(double, double);
int main(void)
{
struct funds stan = {
"Garlic-Melon Bank",
4032.27,
"Lucky's Savings and Loan",
8543.94
};
printf("Stan has a total of $%.2f.\n",sum(stan.bankfund, stan.savefund));
return 0;
}
/* 两个double类型的数相加 */
double sum(double x, double y)
{
return(x + y);
}
运行该程序后输出如下:
Stan has a total of $12576.21.
看来,这样传递参数没问题。注意,sum()函数既不知道也不关心实际的参数是否是结构的成员,它只要求传入的数据是double类型。
当然,如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址:
modify(&stan.bankfund);
这是一个更改银行账户的函数。
把结构的信息告诉函数的第2种方法是,让被调函数知道自己正在处理一个结构。
6.2 传递结构的地址
/* funds2.c -- 传递指向结构的指针 */
double sum(const struct funds *); /* 参数是一个指针 */
double sum(const struct funds * money)
{
return(money->bankfund + money->savefund);
}
6.3 传递结构
/* funds3.c -- 传递一个结构 */
double sum(struct funds moolah); /* 参数是一个结构 */
double sum(struct funds moolah)
{
return(moolah.bankfund + moolah.savefund);
}
7. 结构特性
现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。也就是说,如果n_data和o_data都是相同类型的结构,可以这样做:
o_data = n_data; // 把一个结构赋值给另一个结构
函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。
7.1 结构和结构指针的选择
把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。被调函数中的某些操作可能会意外影响原来结构中的数据。(可用const限定符解决这个问题。)
把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递函数所需的成员更合理。
7.2 结构中的字符数组和字符指针
#define LEN 20
struct names {
char first[LEN];
char last[LEN];
};
struct pnames {
char * first;
char * last;
};
struct names accountant;
struct pnames attorney;
puts("Enter the last name of your accountant:");
scanf("%s", accountant.last);
puts("Enter the last name of your attorney:");
scanf("%s", attorney.last); /* 这里有一个潜在的危险 */
对于会计师(accountant),他的名储存在accountant结构变量的last成员中,该结构中有一个储存字符串的数组。
对于律师(attorney),scanf()把字符串放到attorney.last表示的地址上。由于这是未经初始化的变量,地址可以是任何值,因此程序可以把名放在任何地方。
如果走运的话,程序不会出问题,至少暂时不会出问题,否则这一操作会导致程序崩溃。因此,如果要用结构储存字符串,用字符数组作为成员比较简单。用指向 char 的指针也行,但是误用会导致严重的问题。
8. 联合简介
联合(union)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。
union hold {
int digit;
double bigfl;
char letter;
};
// 根据以上形式声明的结构可以储存一个int类型、一个double类型和char类型的值。
// 声明的联合只能储存一个int类型的值或一个double类型的值或char类型的值。
8.1 使用联合
union hold fit; // hold类型的联合变量
union hold * pu; // 指向hold类型联合变量的指针
fit.digit = 23; // 把 23 储存在 fit,占2字节
fit.bigfl = 2.0; // 清除23,储存 2.0,占8字节
fit.letter = 'h'; // 清除2.0,储存h,占1字节
pu = &fit;
x = pu->digit; // 相当于 x = fit.digit
联合的另一种用法是,在结构中储存与其成员有从属关系的信息。例如,假设用一个结构表示一辆汽车。如果汽车属于驾驶者,就要用一个结构成员来描述这个所有者。如果汽车被租赁,那么需要一个成员来描述其租赁公司。可以用下面的代码来完成
struct owner {
char socsecurity[12];
...
};
struct leasecompany {
char name[40];
char headquarters[40];
...
};
union data {
struct owner owncar;
struct leasecompany leasecar;
};
struct car_data {
char make[15];
int status; /* 私有为0,租赁为1 */
union data ownerinfo;
...
};
// 假设flits是car_data类型的结构变量,
// 如果flits.status为0,程序将使用flits.ownerinfo.owncar.socsecurity
// 如果flits.status为1,程序则使用flits.ownerinfo.leasecar.name
8.2 匿名联合
struct owner {
char socsecurity[12];
...
};
struct leasecompany {
char name[40];
char headquarters[40];
...
};
struct car_data {
char make[15];
int status; /* 私有为0,租赁为1 */
union {
struct owner owncar;
struct leasecompany leasecar;
};
...
};
// 现在,如果 flits 是 car_data 类型的结构变量
// 可以用flits.owncar.socsecurity 代替flits.ownerinfo.owncar.socsecurity
总结:结构和联合运算符
成员运算符:.
一般注释:
该运算符与结构或联合名一起使用,指定结构或联合的一个成员。如果name是一个结构的名称, member是该结构模版指定的一个成员名,下面标识了该结构的这个成员:
name.member
name.member的类型就是member的类型。联合使用成员运算符的方式与结构相同。
间接成员运算符:->
一般注释:
该运算符和指向结构或联合的指针一起使用,标识结构或联合的一个成员。假设ptrstr是指向结构的指针,member是该结构模版指定的一个成员,那么:
ptrstr->member
标识了指向结构的成员。联合使用间接成员运算符的方式与结构相同。
9. 枚举类型
可以用枚举类型(enumerated type)声明符号名称来表示整型常量。
enum spectrum {
red,
orange,
yellow,
green,
blue,
violet
};
enum spectrum color;
9.1 enum常量
只要是能使用整型常量的地方就可以使用枚举常量。例如,在声明数组时,可以用枚举常量表示数组的大小;在switch语句中,可以把枚举常量作为标签。
9.2 默认值
默认情况下,枚举列表中的常量都被赋予0、1、2等。因此,下面的声明中nina的值是3:
enum kids {nippy, slats, skippy, nina, liz};
9.3 赋值
// 在枚举声明中,可以为枚举常量指定整数值:
enum levels {low = 100, medium = 500, high = 2000};
// 如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。例如,假设有如下的声明:
enum feline {cat, lynx = 10, puma, tiger};
//那么,cat的值是0(默认),lynx、puma和tiger的值分别是10、11、12
10. typedef简介
typedef工具是一个高级数据特性,利用typedef可以为某一类型自定义名称。这方面与#define类似,但是两者有3处不同:
与#define不同,typedef创建的符号名只受限于类型,不能用于值。
typedef unsigned char BYTE;
// 随后,便可使用BYTE来定义变量:
BYTE x, y[10], * z
11. 其他复杂的声明
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类型元素的数组