- 结构(structure)可能具有不同类型的值(member)的集合。
- 联合(nuion)可存储不同类型的值,每次储存一个成员(member),无法储存全部成员
- 枚举(enumeration)一种整数类型,它的值由程序员来命名
结构是目前为止最重要的一种类型。
结构
结构变量
数组有两个重要特性:
- 数组的所有元素具有相同的类型
- 为了选择数组元素需要指明元素的位置
结构具有与数组非常不同的特性:
- 结构的每个成员可以拥有不同的类型
- 结构的每个成员都有自己的名字,为了原则结构的成员需要指明成员的名字而不是位置
结构变量的声明
struct part{
int number;//零件编号
char name[NAME_LENGTH + 1];//零件名称
int on_hand; //现有数量
}part1,part2;
struct {
int number;
char name[NAME_LENGTH + 1];
int on_hand;
}part3,part4;
struct part_a{
int number;
char name[NAME_LENGTH + 1];
int on_hand;
};
三种声明方式 struct{…}指明了类型 可命名也可不命名 part1,part2则是具有这种类型的变量。
结构体在内存中的存储
结构的成员在内存中是按顺序存储的。为了说明part1在内存中存储的形式,现在假设:
- part1存储在地址为2000的内存单元中。
- 每个整数在内存中占两个字节。
- #NAME_LENGTH 25
- 成员之间没有间隙
根据这些假设,part1在内存中的样子如下图所示:
通常情况下不需要画出如此详细的结构。这里更抽象地表示结构,用一系列方框表示:
或者用水平方向的盒子代替垂直方向的方块
命名空间
每个结构表示一种新的范围。任何声明在此范围内的成员名称都不会和程序中的其他名字冲突。C语言的术语表示每个结构都为它的成员设置了单独的命名空间(name space)。
结构变量的初始化
和数组一样,结构变量也可以在声明的时候进行初始化。将存储的值用大括号括起来。
#define NAME_LENGTH 15
struct part{
int number;//零件编号
char name[NAME_LENGTH + 1];//零件名称
int on_hand; //现有数量
}part1 = {
528,
"Disk drive",
10
};
在内存中存储为
注意
用于结构初始化的表达式必须是常量,初始化式可以短于它所初始化的结构,”剩余的“成员都用0作为它的初值。
对结构的操作
- 访问成员
访问成员通过 结构名.成员名
例如part1.number = 10;
- 赋值运算(对结构整体的唯一操作,成员的赋值通过访问成员作为左值来完成)
part1 = part2;
将两个相同声明的结构整体进行复制。
数组不能用=来复制,使用结构内数组会获得更大的惊喜。
struct{
int a[10];
}a1,a2 = {1,2,3,4,5,6,7,8,9,0};
a1 = a2;
通过上述代码就完成了a2.a到a1.a的复制。
问题:地址是相同的吗?值是相同的吗?
a1 = a2;
printf("数组a1的地址为%d\n",&a1.a);
printf("数组a2的地址为%d\n",&a2.a);
for(int i = 0; i < 10; i++){
printf("数组a2的第%d个分量为%d\n",i,a2.a[i]);
结论:通过结构体的复制进行数组赋值,实现了不同地址赋予相同值。
警告
除了赋值运算,C语言没有提供其他关于整体结构的操作。特别是不能使用运算符==和!=来判定两个结构是否相等或不等。
结构类型
struct {
int number;
char name[NAME_LENGTH + 1];
int on_hand;
}part1;
struct {
int number;
char name[NAME_LENGTH + 1];
int on_hand;
}part2;
很遗憾,按照C语言的规则,part1和part2不具有一致的类型。这样的结果是
- 不能用赋值运算;
- 因为part1和part2的类型没有名字,所以也就不能用他们当作函数调用的参数了。
为了克服这两个困难,需要为表示结构的类型定义名字,而不是为特定结构的变量命名。C语言提供两种结构命名方式:
- 声明“结构标记”;
- 使用typedef来定义类型名。
结构标记的声明
结构标记(structure tag)是用于标识某种特定结构类型的名字。下面的例子声明了名为part的结构标记。
struct part{
int number;
char name [NAME_LEN-1];
int on_hand;
};
注意,右大括号后的分号,必须用它来表示声明的结束。
如果无意间忽略了结构声明末尾的分号,有可能导致意料之外的错误。考虑:
struct part{
int number;
char name [NAME_LEN-1];
int on_hand;
} /***WRONG--semicolon***/
f(void){
...
return 0;
}
因为丢失了函数f的返回值类型,所以通常将默认为int类型。然而,在此例中,由于前面的结构生命没有正常终止,所以编译器会假设struct part的返回类型。编译器直到执行函数中第一条return语句时才会发现错误。结果是:含义模糊的出错信息。
一旦产生了标记part,就可以用它来声明变量了:struct part part1,part2;
可惜的是,不能通过漏掉单词struct来缩写这个声明:part part1,part2;
part不是类型名。如果没有单词struct,它没有任何意义。
因为结构标记只有在前面放置了单词struct才会有意义,所以他们不会和程序中用到的其他名字发生冲突。程序中拥有名为part的变量是非常合法的。
结构类型的定义
作为声明结构标记的替换,还可以用typedef来定义真实的类型名。例如,可以按照如下方式定义名为art的类型:
typedef struct {
int number;
char name[NAME_LENGTH-1];
int on_hand;
}Part;
注意,类型Part 的名字必须出现在定义的末尾,而不是在单词struct的后边。
可以像内置类型那样使用Part。例如,可以用它声明变量:Part part1,part2;
因为类型Part是typedef的名字,所以不允许书写struct Part。无论在那里声明,所有的Part类型的变量都是一致的(可复制的)。
当需要命名结构时,通常既可以选择声明结构标记也可以使用typedef。但是,如果用于链表,必须强制声明结构标记。为什么?
结构类型的实际参数和返回值
函数可以有结构类型的参数和返回值。
void print_part (struct part p){
printf("Part number: %d\n", p.number);
printf("Part name: %s\n", p.name);
printf("Quantity on hand: %d\n", p.on_hand);
}
struct part build_part(int number, const char* name,int on_hand){
struct part p;
p.number = number;
strcpy (p.name, name);
p.on_hand = on_hand;
return p;
}
part1 = build_part(444,"dasda",10);
这个build_part和JAVA中类的构造器非常像。
public class Part{
int number;
String name;
int on_hand;
public Part(int number, String name, int on_hand){
this.number = number;
this.name = name;
this.on_hand = on_hand;
}
}
给函数传递结构和从函数返回结构都要求结构中所有成员的副本。这样的结果是,这些操作对程序强加了一定数量的系统开销,特别是结构很大的时候。为了避免这种系统开销,有时用传递指向结构的指针来代替结构本身是一个明智的做法。类似的,可以使函数返回指向结构的指针来代替结构本身。
数组和结构的嵌套
嵌套的结构
结构数组
数组和就够最常见的组合就是具有结构元素的数组,例如struct Part inventory[100];
能够存储100种零件的信息。为了访问某个零件,可以使用下标方式。
访问零件名种的单独字符要求用下标方式(选择特定的零件),跟着是成员选择(选择成员name),再跟着是下标(选择零件名中的字符)。为了使存储在inventory[i]种的名字变成空字符串,可以写成inventory[i].name[0] = '\0';
结构数组的初始化
初始化结构数组的工作和初始化多维数组的方式非常类似。每个结构都拥有自己的大括号括起来的初始符,数组的初始符会在结构初始符的外围简单括上另一对大括号。
初始化结构数组的原因之一是计划把它作为程序执行期间不改变的信息数据库。例如,假设用到的程序在打国际长途电话时会需要访问国家代码。首先,将设置结构来存储国家名和相应代码;
struct dialing_code{
char *country;
int code;
};
注意,country是个指针而不是字符数组。如果计划用结构dialing_code作为变量可能有问题,但是这里不这样做。当初始化结构dialing_code时,country会结束指向字符串字面量。
接下来,声明这类结构的数组并初始化它:
const struct dialing_code country_code[] = {
{"A", 1}, {"B", 2},
{"C", 3}, {"D", 4}
}
作业
开发一个相对较大二代程序,此程序用来维护仓库存储的零件的信息数据库。程序围绕一个结构数组建立,且每个结构包含以下信息:零件的编号、零件的名称以及某种零件的数量。程序将支持下列操作:
- 添加新零件编号、名称和当前现在的数量。如果零件已在数据库内,或者数据库已满,那么程序必须显示出错信息。
- 给定零件编号,显示出零件的名称和当前现有的数量。如果零件编号不在数据库中,那么程序必须显示出错信息。
- 给定零件编号,改变现有的零件数量。如果零件编号不在数据库内,那么程序必须显示出错误信息。
- 显示表格列出数据库的全部信息。零件必须按照录入的顺序显示出来。
- 终止程序的执行。
联合
和结构一样,联合(union)也是由一个或者多个成员组成的,而且这些成员可能具有不同的数据类型。但是,编译器只为联合中最大的成员分配足够的内存空间。联合的成员在这个空间内彼此覆盖。这样的结果是,给一个成员赋予新值也会改变所有其他成员的值。
联合的声明和结构相同,将struct替换为union
事实上,联合和结构只有一处不同:s的成员存储在不同的内存地址中,u的成员存储在相同的内存地址中。
访问方法相同。
联合有几个重要的应用,现在讨论其中的两种:
- List item