简单说下数组特性:
- 数组所有的元素具有相同的数据类型
- 选择数组元素需要指明元素的位置(下标)
结构体
结构体和数组有很大不同。结构体的元素(C 语言中的说法是成员)可以具有不同类型。而且每个结构成员都有名字,访问结构体成员需要指明结构成员的名字而不是位置。
在一些编程语言中,经常把结构体称为记录(record),把结构体的成员称为字段(field)。
1. 结构变量的声明
假如需要记录存储在仓库中的零件编号,名称和数量。我们可以使用结构体:
struct{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1, part2;
struct{...}
指明类型,part1,part2
是这种类型的变量。
结构体在内存中是按照声明顺序存储的。
2. 结构变量的初始化
我们可以在定义结构体的同时初始化:
struct{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1 = {528, "Disk drive", 10}
part2 = {914, "Printer cable", 5};
初始化式中的值必须按照结构体成员的顺序进行显示。
结构初始化式遵循的原则类似于数组的。初始化式必须是常量(C99 中允许使用变量)。初始化式中的成员可以少于它所初始化的结构,“剩余的”成员用 0 作为初始值。特别的,剩余的字符串应为空字符串。
3. 指定初始化(C99)
特性和数组一样,比如:
struct {
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1 = {.name = "Disk", 123};
number 被默认为 0,.name
直接跳过 number 初始化 name,123 初始化的成员为 .name 后一个成员。
4. 对结构的操作
访问成员方式如下:
printf("Part number: %d\n", part1.number);
printf("Part name: %s\n", part1.name);
printf("Quantity on hand: %d\n", part.on_hand);
结构的成员是左值,所以可以出现在赋值运算的左侧:
part1.number = 258;
part1.on_hand++;
.
其实就是一个 C 语言的运算符。.
运算符的优先级几乎高于所有其他运算符,所以思考:
scanf("%d", &part1.on_hand);
&
计算的是 part1.on_hand
的地址
赋值运算:
part1 = part2;
等价于:
part1.number = part2.number;
strcpy(part1.name, part2.name);
part1.on_hand = part2.on_hand;
如果这个结构内含有数组,数组也会被复制。
但是不能使用 ==
和 !=
运算符判定两个结构是否相等。
二 结构类型
如果我们要在程序的不同位置声明变量,我们就需要定义表示一种结构类型的名字。
试思考:
struct{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1;
在程序的某处,为了描述一个零件,我们写了上面的代码。但是,现在在程序的另一处有需要一个零件,直接增加一个变量:
struct{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1, part2;
这种方式固然可行,但是有些“呆”。
那么,如果我们再次定义一个相同的“零件类型”:
struct{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part2;
请注意:part1 和 part2 具有不同的类型
1. 结构标记的声明
结构标记(struct 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;
}
f(){
...
return 0;
}
由于前面的结构声明没有正常终止,所以编译器会假设函数 f 返回值是 struct part 类型的,所以直到 f 中的第一条 return 语句才会发现错误。
声明变量:
struct part part1, part2;
注意:不能省略 struct
也因为结构标记只有在 part 前放置 struct 才有意义,所以声明名为 part 的变量是完全合法的。(但是容易混淆)
声明结构标记和结构变量可以放在一起:
struct part{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1, part2;
所有声明为 struct part
类型的结构彼此兼容。
2. 结构类型的定义
使用 typedef
定义名为 part 的结构类型:
typedef struct {
int number;
char name[NAME_LEN + 1];
int on_hand;
}part;
如此,我们就可以像上面那样声明结构变量:
part part1, part2;
因为类型名为 part
所以书写 struct part 是不合法的。
如果你也想可以使用 struct part
,那你可以这样声明:
typedef struct part{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part;
3. 结构作为参数和返回值
结构作为参数
函数:
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);
}
调用方式:
print_part(part1);
结构作为返回值
函数:
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(527, "Disk", 10);
给函数传递结构和从函数返回结构都需要生成结构所有成员的副本,这回可能会产生一定数量的系统开销。为了避免这种开销,常传递或返回指向结构的指针来代替传递或返回结构本身。下一节中,我们将会看到这样的应用。
4. 复合字面量(C99)
略。
三 嵌套的结构和结构数组
1. 嵌套的结构
把一种结构嵌套在另一种结构中经常是非常有用的。比如:
定义一个结构存储一个人的姓名:
struct person_name{
char first[FRIST_NAME_LEN + 1];
char last[LAST_NAME_LEN + 1];
};
定义一个结构存储学生信息:
struct student{
struct person_name name;
int ID, age;
char gender;
}student1;
访问 student1 的名和姓需要应用两次.
:
strcpy(student1.name.first, "Fred");
2. 结构数组
声明一个数组用来存储 100 个零件信息:
struct part Part[100];
访问零件数组中下标为 i 的元素的结构成员:
Part[i].number = 883;
使存储在零件数组中下标为 i 的元素的姓名变为空字符串,可以写成:
Part[i].name[0] = '\0';
3. 结构数组的初始化
初始化结构数组与初始化多维数组的方法非常相似。比如:
struct person_name{
char first[FRIST_NAME_LEN + 1];
char last[LAST_NAME_LEN + 1];
}name[] = { {"San", "Zhang"}, {"Si", "Li"} };
与数组一样,指定初始化(C99)也适用于这种情况。
结构变量 s 和 联合变量 u 只有一处不同:s 的成员存储在不同的内存地址中;u 的成员存储在同一内存地址中。如图:
如果把一个值存储到u.d
中,那么先前存储在 u.i
中的值会丢失。类似的,改变 u.i
也会影响u.d
。
联合的性质几乎和结构一样。
联合的初始化方式和结构也很相似,但是,只有联合的第一个成员可以获得初始值。例如,如下初始化方式可以使得联合 u 的成员 i 的值为 0:
union {
double d;
int i;
}u = {0};
注意:花括号是必需的。
指定初始化(C99):
union {
double d;
int i;
}u = {.i = 3};
只能初始化一个成员,不一定是第一个。
4. 使用联合节省空间
有三种商品,每种商品都有库存,价格;这些商品还具有以下其他特性:
- 书籍:书名,作者,页数
- 杯子:设计
- 衬衫:设计,可选颜色,可选尺寸
假如我们设计包含上面特性的结构:
struct catlog_item{
int stock_number;
double price;
int item_type;
char title[TITLE_LEN + 1];
char author[AUTHOR_LEN + 1];
int num_page;
char design[DESIGN_LEN + 1];
int colors;
int sizes;
};
item_type
的值是 BOOK,MUG,SHIRT 之一。
上面这种结构体比较浪费空间,因为对于某种特定商品,结构中只有部分字段是有用的。(当然你也可以定义三个结构体,我也建议这么做。)
现在我们引用联合:
struct catlog_item{
int stock_number;
double price;
int item_type;
union{
struct{
char title[TITLE_LEN + 1];
char author[AUTHOR_LEN + 1];
int num_page;
}book;
struct{
char design[DESIGN_LEN + 1];
}mug;
struct{
char design[DESIGN_LEN + 1];
int colors;
int sizes;
}shirt;
}item;
}catlog;
书籍名称可以用以下方式显示:
printf("%s\n", catlog.item.book.title);
把值存储在联合的一个成员中,然后访问另一个成员通常是不可取的。但是,如果联合的两个或多个成员是结构,而且这些结构最初的一个或多个成员是匹配的(顺序相同,类型兼容,名字可以不一样)。如果当前某个结构有效,其他结构中的匹配成员也有效。
联合 item 中,mug 和 shirt 第一个字段是匹配的。比如,如果我们给 mug 的成员 design 赋值:
strcpy(catlog.item.mug.design, "Cat");
结构 shirt 的第一个成员也具有相同的值:
printf("%s", catlog.item.shirt.design); //Cat
5. 使用联合构造混合的数据结构
假设需要数组元素是 int 值和 double 值的混合。因为数组元素必须是相同类型,我们可以应用联合数组:
typedef union{
int i;
double d;
}Number;
Number number_array[1000];
number_array[0].i = 1;
number_array[1].d = 1.1;
6. 为联合添加“标记字段”
联合面临的主要问题是:不容易确定联合最后改变的成员,因此对联合成员的访问可能是无意义的。
前面程序中 item_type 就是标记字段,用来帮助我们确定当前商品种类。
为了记录这种信息,我们可以把联合嵌入一个结构中,此结构还有另一个成员:“标记字段”或者“判别式”,用来提示当前存储在联合中的内容。比如定义如下结构:
#define INT_KIND 0
#deinf DOUBLE_KIND 1
typedef struct{
int kind;
union{
int i;
double d;
}u;
}Number;
当需要访问存放在联合中的成员时,可以使用函数:
void print_number(Number n){
if(n.kind == INT_KIND)
printf("%d", n.u.i);
else
printf("%f", n.u.d);
}
注意:每次对联合成员赋值,都需要由程序改变标记字段的内容
五 枚举
C 语言为具有可能值较少的变量提供了一种专用类型 —— 枚举类型(enumeration type)
定义扑克花色:
enum{
CLUBS,
DIAMONDS,
HEARTS,
SPADES,
}s1;
CLUBS 的值为 0,DIAMAND 值为 1,后面的每个增加 1 ,以此类推。
如果没有枚举类型,我们需要一个个的来 #define
#define CLUBS 0
#define DIAMANDS 1
#define HEARTS 2
#define SPADES 3
这样无疑会增加程序的复杂度,也会降低同种情况的联系,让程序变得难以阅读。
0. 枚举类型声明
1)
enum suit{
CLUBS,
DIAMONDS,
HEARTS,
SPADES,
};
enum suit s1, s2;
2)
typedef enum{
CLUBS,
DIAMONDS,
HEARTS,
SPADES,
}Suit;
Suit s1, s2;
C89 中,使用枚举创建布尔类型:
typedef enum{TRUE, FALSE}Bool;
如果要使用枚举变量:
Suit suit = CLUBS;
Bool flag = TRUE;
枚举类型的变量可以赋值为任意枚举列出来的枚举常量。但是枚举常量可以赋值给普通整型变量,普通整型变量也可以赋值给枚举类型的变量。这是因为 C 语言对于枚举和整数的使用比较混乱,没有明确界限。
1. 枚举作为整数
在系统内部,C 语言会把枚举变量和常量作为整数来处理。默认情况下,编译器将 0,1,... 赋值给枚举常量。
我们可以为枚举常量自由选择不同的值。现在假设希望用 1 到 4 代表牌的花色,我们可以这样定义:
enum suit{
CLUBS = 1,
DIAMONDS = 2,
HEARTS = 3,
SPADES = 4,
};
我们知道后一个枚举常量比前一个大 1,所以,我们也可以简化为:
enum suit{
CLUBS = 1,
DIAMONDS,
HEARTS,
SPADES,
};
也可以换为任意整数:
enum suit{
CLUBS = 10,
DIAMONDS = 20,
HEARTS = 15,
SPADES = 40,
};
2. 使用枚举声明“标记字段”
现在我们可以不用宏的值来表示标记字段的含义了:
typedef struct{
enum {INT_KIND, DOUBLE_KIND} kind;
union{
int i;
double d;
}u;
}Number