结构&联合&枚举

A program without a loop and a structured variable isn’t worth writing. [^1]

目录


结构&联合&枚举


零 前言

可以参考学习的文章:https://mp.weixin.qq.com/s/NkXZSdM-gnAuG7_jAM8ZiA

一 结构变量

前面我们说过数组有两个重要特性:

  • 数组所有的元素具有相同的数据类型
  • 选择数组元素需要指明元素的位置(下标)

结构和数组有很大不同。结构的元素(C 语言中的说法是成员)可以具有不同类型。而且每个结构成员都有名字,访问结构体成员需要指明结构成员的名字而不是位置。

在一些编程语言中,经常把结构体称为记录(record),把结构体的成员称为字段(field)。

0. 结构变量的声明

假如需要记录存储在仓库中的零件。我们可能需要记录零件的编号,名称和数量。我们可以使用结构体:

struct{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1, part2;

struct{...}指明类型,part1,part2是这种类型的变量。

结构体在内存中是按照声明顺序存储的。

至于细化到字节,结构体是否也是紧挨着存储的,这里我们可以留个悬念,大家自行猜测一下。(如果你想了解,可以参考文章:https://mp.weixin.qq.com/s/uG1ZNWbmXAYPL4Rs4uqoKQ)

1. 结构变量的初始化

我们可以在定义结构体的同时初始化:

struct{
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1 = {528, "Disk drive", 10}
part2 = {914, "Printer cable", 5};

初始化式中的值必须按照结构体成员的顺序进行显示。

结构初始化式遵循的原则类似于数组的。初始化式必须是常量(C99 中允许使用变量)。初始化式中的成员可以少于它所初始化的结构,“剩余的”成员用 0 作为初始值。特别的,剩余的字符串应为空字符串。

2. 指定初始化(C99)

特性和数组一样,比如:

struct {
int number;
char name[NAME_LEN + 1];
int on_hand;
}part1 = {.name = "Disk", 123};

number 被默认为 0,.name 直接跳过 number 初始化 name,123 初始化的成员为 .name 后一个成员。

3. 对结构的操作

访问成员方式如下:

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 具有不同的类型

0. 结构标记的声明

结构标记(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类型的结构彼此兼容。

1. 结构类型的定义

使用 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;

2. 结构作为参数和返回值

结构作为参数

函数:

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);

给函数传递结构和从函数返回结构都需要生成结构所有成员的副本,这回可能会产生一定数量的系统开销。为了避免这种开销,常传递或返回指向结构的指针来代替传递或返回结构本身。下一节中,我们将会看到这样的应用。

3. 复合字面量(C99)

略。

三 嵌套的结构和结构数组

0. 嵌套的结构

把一种结构嵌套在另一种结构中经常是非常有用的。比如:

定义一个结构存储一个人的姓名:

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");

1. 结构数组

声明一个数组用来存储 100 个零件信息:

struct part Part[100];

访问零件数组中下标为 i 的元素的结构成员:

Part[i].number = 883;

使存储在零件数组中下标为 i 的元素的姓名变为空字符串,可以写成:

Part[i].name[0] = '\0';

2. 结构数组的初始化

初始化结构数组与初始化多维数组的方法非常相似。比如:

struct person_name{
char first[FRIST_NAME_LEN + 1];
char last[LAST_NAME_LEN + 1];
}name[] = { {"San", "Zhang"}, {"Si", "Li"} };

与数组一样,指定初始化(C99)也适用于这种情况。

程序:维护零件数据库

此程序用来维护仓库存储的零件信息的数据库。程序围绕一个结构数组构建,且每个结构包含以下信息:零件编号,名称和数量。程序将支持下列操作:

  • 添加新零件信息。如果零件已经存在,或数据库已满,显示出错信息。
  • 给定零件编号,显示零件的名称,数量信息。如果零件编号不存在,那么给出出错信息。
  • 给定零件编号,改变零件的数量。如果零件编号不存在,给出出错消息。
  • 显示列出数据库中的全部信息。零件必须按照录入顺序显示。
  • 终止程序的执行

使用:

  • i:插入
  • s:搜索
  • u:更新
  • p:显示
  • q:退出

分表表示这种操作,与程序得到会话如下:

Enter operation code: i
Enter part number: 833
Enter part name: Disk Drive
Enter quantity on hand: 90
Enter operation code: i
Enter part number: 788
Enter part name: USB 3.0
Enter quantity on hand: 67
Enter operation code: s
Enter part number: 832
Part not found.
Enter operation code: 833
Illegal code.
Enter operation code: s
Enter part number: 833
Part name: Disk Drive
Quantity on hand: 90
Enter operation code: u
Enter part number: 788
Enter change in quantity on hand(- means minus): 3
Enter operation code: p
Part Number Part Name Quantity on Hand
833 Disk Drive 90
788 USB 3.0 70
Enter operation code: q

注意:菜单可以没有

因为 readline 函数和这个程序的主干没有太大关系,我们用单独的头文件和源文件包含它。

readline.h

#ifndef READLINE_H
#define READLINE_H

/***********************************************************
*
* read_line: Skips leading white-space characters, then
* reads the remainder of the input line and
* stores it in str. Truncates the line if its
* length exceeds n. Return the number of
* characters stores.
*
***********************************************************/

int read_line(char str[], int n);

#endif

readline.c

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<ctype.h>
#include"readline.h"

int read_line(char str[], int n) {

int ch, i = 0;

while (isspace(ch = getchar()))
;

while (ch != '\n' && ch != EOF) {
if (i < n)
str[i++] = ch;

ch = getchar();
}
str[i] = '\0';

return i;
}


inventory.c

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include"readline.h"

#define NAME_LEN 20
#define MAX_PARTS 100

struct part {
int number;
char name[NAME_LEN + 1];
int on_hand;
}inventory[MAX_PARTS];

int num_parts = 0; //number of parts current stored

void menu();
int find_part(int number);
void insert();
void search();
void update();
void print();


int main(void) {

char code = 'a';

menu();

for (;;) {
printf("Enter operation code: ");
scanf(" %c", &code);
while (getchar() != '\n') // ships to end of line
;
switch (code) {
case 'i': insert(); break;
case 's': search(); break;
case 'u': update(); break;
case 'p': print(); break;
case 'q': return 0;
default: printf("Illegal code.\n"); break;
}
}




return 0;
}

void menu() {

printf(" ==================================\n");
printf(" * *\n");
printf(" * i: insert *\n");
printf(" * s: search *\n");
printf(" * u: undate *\n");
printf(" * p: print *\n");
printf(" * q: quit *\n");
printf(" * *\n");
printf(" ==================================\n");
}


/**********************************************************
*
* find_part: Looks up a part number in the inventory
* array.Returns the array index if the part
* number is found;otherwise,return -1
*
***********************************************************/
int find_part(int number) {

int i;

for (i = 0; i < num_parts; i++) {
if (inventory[i].number == number)
return i;
}

return -1;
}


/**********************************************************
*
* insert: Inserts the part into the database.Prints
* an error message and returns prematurely
* if the part already exists or the database
* is full.
*
***********************************************************/
void insert() {

int part_number;

if (num_parts == MAX_PARTS) {
printf("Database is full; can't add more parts.\n");
return;
}

printf("Enter part number: ");
scanf("%d", &part_number);

if (find_part(part_number) >= 0) {
printf("Part already exists.\n");
return;
}

inventory[num_parts].number = part_number;
printf("Enter part name: ");
read_line(inventory[num_parts].name, NAME_LEN);
printf("Enter quantity on hand: ");
scanf("%d", &inventory[num_parts].on_hand);
num_parts++;
}


/************************************************************
*
* search: Look up a part by the number user enters.
* If the part exists, prints the name and quantity
* on hand;if not, print an error message.
*
************************************************************/
void search() {

int index, number;

printf("Enter part number: ");
scanf("%d", &number);

index = find_part(number);

if (index == -1) {
printf("Part not found.\n");
return;
}

printf("Part name: %s\n", inventory[index].name);
printf("Quantity on hand: %d\n", inventory[index].on_hand);

}


/************************************************************
*
* update: Prompts user to enter a number.
* Print an error message if the part doesn't exist;
* otherwise,prompts the user to enter change in
* quantity on hand and updates the database.
*
************************************************************/

void update() {

int number, index, change;

printf("Enter part number: ");
scanf("%d", &number);

index = find_part(number);

if (index == -1) {
printf("Part not found.\n");
return;
}

printf("Enter change in quantity on hand(- means minus): ");
scanf("%d", &change);
inventory[index].on_hand += change;

}


/************************************************************
*
* print: Print a listing of all parts in the database,
* showing the part number,part name and quantity
* on hand.Parts are printed in the order in which
* they were entered into the database.
*
************************************************************/

void print() {

int i;

printf("Part Number Part Name Quantity on Hand\n");
for (i = 0; i < num_parts; i++) {
printf("%6d%20s%15d\n", inventory[i].number, inventory[i].name, inventory[i].on_hand);
}
}

四 联合

像结构一样,联合(union)也是由一个或多个成员构成,而且这些成员可以具有不同的类型。但是,编译器只为联合中最大的成员分配足够的空间。联合的成员在这个空间内彼此覆盖,给一个成员赋予新值也会改变其他成员的值。

union {
double d;
int i;
}u;

struct {
double d;
int i;
}s;

结构变量 s 和 联合变量 u 只有一处不同:s 的成员存储在不同的内存地址中;u 的成员存储在同一内存地址中。如图:

u.i = 3;
u.d = 1.0;

如果把一个值存储到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};

只能初始化一个成员,不一定是第一个。

0. 使用联合节省空间

有三种商品,每种商品都有库存,价格;这些商品还具有以下其他特性:

  • 书籍:书名,作者,页数
  • 杯子:设计
  • 衬衫:设计,可选颜色,可选尺寸

假如我们设计包含上面特性的结构:

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

1. 使用联合构造混合的数据结构

假设需要数组元素是 int 值和 double 值的混合。因为数组元素必须是相同类型,我们可以应用联合数组:

typedef union{
int i;
double d;
}Number;

Number number_array[1000];

number_array[0].i = 1;
number_array[1].d = 1.1;

2. 为联合添加“标记字段”

联合面临的主要问题是:不容易确定联合最后改变的成员,因此对联合成员的访问可能是无意义的。

前面程序中 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;

参考资料:《C语言程序设计:现代方法》

[^1]: 没有循环和结构变量的程序不值得写。Epigrams on Programming 编程警句