第14章-结构和其他数据形式


建立结构声明:

struct book {
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};

把library声明为一个使用book结构布局的结构变量:

struct book library;

定义结构变量:

结构有两层含义。一层含义是“结构布局”,刚才已经讨论过了。结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。下一步是创建一个结构变量,即是结构的另一层含义。程序中创建结构变量的一行是:

struct book library;

编译器执行这行代码便创建了一个结构变量library。编译器使用book模板为该变量分配空间:一个内含MAXTITL个元素的char数组、一个内含MAXAUTL个元素的char数组和一个float类型的变量。这些存储空间都与一个名称library结合在一起(见图14.1)。

组合后的结构声明和结构变量定义不需要使用结构标记:

struct { /* 无结构标记 */
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
} library;

初始化结构:

struct book library = {
    "The Pious Pirate and the Devious Damsel",
    "Renee Vivotte",
    1.95
};

在第12章中提到过,如果初始化静态存储期的变量(如,静态外部链接、静态内部链接或静态无链接),必须使用常量值。这同样适用于结构。如果初始化一个静态存储期的结构,初始化列表中的值必须是常量表达式。如果是自动存储期,初始化列表中的值可以不是常量。

访问结构成员:

使用结构成员运算符——点(.)访问结构中的成员。例如,library.value即访问library的value部分。可以像使用任何float类型变量那样使用library.value。

结构的初始化器:

只初始化book结构的value成员,可以这样做:

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。

结构数组:

下面程序创建了一个内含100个结构变量的数组。由于该数组是自动存储类别的对象,其中的信息被储存在栈(stack)中。如此大的数组需要很大一块内存,这可能会导致一些问题。如果在运行时出现错误,可能抱怨栈大小或栈溢出,你的编译器可能使用了一个默认大小的栈,这个栈对于该例而言太小。要修正这个问题,可以使用编译器选项设置栈大小为10000,以容纳这个结构数组;或者可以创建静态或外部数组(这样,编译器就不会把数组放在栈中);或者可以减小数组大小为16。为何不一开始就使用较小的数组?这是为了让读者意识到栈大小的潜在问题,以便今后再遇到类似的问题,可以自己处理好。

/* 包含多本书的图书目录 */
#include <stdio.h>
#include <string.h>
char* s_gets(char *st, int n);
#define MAXTITL  40
#define MAXAUTL  40
#define MAXBKS 100 /* 书籍的最大数量 */
struct book { /* 简历 book 模板  */
	char title[MAXTITL];
	char author[MAXAUTL];
	float value;
};
int main(void) {
	struct book library[MAXBKS]; /* book 类型结构的数组 */
	int count = 0;
	int index;
	printf("Please enter the book title.\n");
	printf("Press [enter] at the start of a line to stop.\n");
	while (count < MAXBKS && 
            s_gets(library[count].title, MAXTITL) != NULL &&
            library[count].title[0] != '\0') {
		printf("Now enter the author.\n");
		s_gets(library[count].author, MAXAUTL);
		printf("Now enter the value.\n");
		scanf("%f", &library[count++].value);
		while (getchar() != '\n') /* 清理输入行*/
			continue;
		if (count < MAXBKS)
			printf("Enter the next title.\n");
	}
	if (count > 0) {
		printf("Here is the list of your books:\n");
		for (index = 0; index < count; index++)
			printf("%s by %s: $%.2f\n", library[index].title,
					library[index].author, library[index].value);
	} else
		printf("No books? Too bad.\n");
	return 0;
}
char* s_gets(char *st, int n) {
	char *ret_val;
	char *find;
	ret_val = fgets(st, n, stdin);
	if (ret_val) {
		find = strchr(st, '\n'); // 查找换行符
		if (find) // 如果地址不是 NULL,
			*find = '\0'; // 在此处放置一个空字符
			else
			while (getchar() != '\n') // 处理输入行中剩余的字符
				continue;
}
return ret_val;
}

声明结构数组:

struct book library[MAXBKS];

标识结构数组的成员:

总结一下:

library        // 一个book 结构的数组

library[2]      // 一个数组元素,该元素是book结构

library[2].title    // 一个char数组(library[2]的title成员)

library[2].title[4]  // 数组中library[2]元素的title 成员的一个字

嵌套结构:

// 嵌套结构示例
#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;
}

指向结构的指针:

至少有 4 个理由可以解释为何要使用指向结构的指针。

第一,就像指向数组的指针比数组本身更容易操控(如,排序问题)一样,指向结构的指针通常比结构本身更容易操控。

第二,在一些早期的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;
}
address #1: 0x7fff5fbff820 #2: 0x7fff5fbff874
pointer #1: 0x7fff5fbff820 #2: 0x7fff5fbff874
him->income is $68112.00: (*him).income is $68112.00
him->favfood is tripe: him->handle.last is Swillbelly

声明和初始化结构指针:

struct guy * him;

如果barney是一个guy类型的结构,可以这样写:

him = &barney;

 fellow 是一个结构数组,这意味着 fellow[0]是一个结构。所以,要让 him 指向fellow[0],可以这样写:

him = &fellow[0];

用指针访问成员:

第1种方法也是最常用的方法:使用->运算符。该运算符由一个连接号(-)后跟一个大于号(>)组成。我们有下面的关系:

如果him == &barney,那么him->income 即是 barney.income
如果him == &fellow[0],那么him->income 即是 fellow[0].income

第2种方法是,以这样的顺序指定结构成员的值:如果him == &fellow[0],那么*him == fellow[0],因为&和*是一对互逆运算符。因此,可以做以下替代:

fellow[0].income == (*him).income

必须要使用圆括号,因为.运算符比*运算符的优先级高。

总之,如果him是指向guy类型结构barney的指针,下面的关系恒成立:

barney.income == (*him).income == him->income // 假设 him == &barney

向函数传递结构的信息:

一个结构比一个单独的值复杂,所以难怪以前的C实现不允许把结构作为参数传递给函数。当前的实现已经移除了这个限制,ANSI C允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。如果你只关心结构中的某一部分,也可以把结构的成员作为参数。

传递结构成员:

只要结构成员是一个具有单个值的数据类型(即,int及其相关类型、char、float、double或指针),便可把它作为参数传递给接受该特定类型的函数。

/* 把结构成员作为参数传递 */
#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.

当然,如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址

modify(&stan.bankfund);

这是一个更改银行账户的函数。

传递结构的地址:

/* 传递指向结构的指针 */
#include <stdio.h>
#define FUNDLEN 50
struct funds {
	char bank[FUNDLEN];
	double bankfund;
	char save[FUNDLEN];
	double savefund;
};
double sum(const struct funds*); /* 参数是一个指针 */
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));
	return 0;
}
double sum(const struct funds *money) { // 注意添加const限制
	return (money->bankfund + money->savefund);
}
Stan has a total of $12576.21.

由于该函数不能改变指针所指向值的内容,所以把money声明为一个指向const的指针。

传递结构:

/* 传递一个结构 */
#include <stdio.h>
#define FUNDLEN 50
struct funds {
	char bank[FUNDLEN];
	double bankfund;
	char save[FUNDLEN];
	double savefund;
};
double sum(struct funds moolah); /* 参数是一个结构 */
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));
	return 0;
}
double sum(struct funds moolah) {
	return (moolah.bankfund + moolah.savefund);
}
Stan has a total of $12576.21.

调用sum()时,编译器根据funds模板创建了一个名为moolah的自动结构变量。然后,该结构的各成员被初始化为 stan结构变量相应成员的值的副本。因此,程序使用原来结构的副本进行计算。由于moolah是一个结构,所以该程序使用moolah.bankfund,而不是moolah->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),函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。结构指针也允许这种双向通信,因此可以选择任一种方法来解决编程问题。

/* 传递并返回结构 */
#include <stdio.h>
#include <string.h>
#define NLEN 30
struct namect {
	char fname[NLEN];
	char lname[NLEN];
	int letters;
};
struct namect getinfo(void);
struct namect makeinfo(struct namect);
void showinfo(struct namect);
char* s_gets(char *st, int n);
int main(void) {
	struct namect person;
	person = getinfo();
	person = makeinfo(person);
	showinfo(person);
	return 0;
}
struct namect getinfo(void) {
	struct namect temp;
	printf("Please enter your first name.\n");
	s_gets(temp.fname, NLEN);
	printf("Please enter your last name.\n");
	s_gets(temp.lname, NLEN);
	return temp;
}
struct namect makeinfo(struct namect info) {
	info.letters = strlen(info.fname) + strlen(info.lname);
	return info;
}
void showinfo(struct namect info) {
	printf("%s %s, your name contains %d letters.\n", info.fname, info.lname, info.letters);
}
char* s_gets(char *st, int n) {
	char *ret_val;
	char *find;
	ret_val = fgets(st, n, stdin);
	if (ret_val) {
		find = strchr(st, '\n'); // 查找换行符
		if (find) // 如果地址不是 NULL,
			*find = '\0'; // 在此处放置一个空字符
		else
			while (getchar() != '\n')
				continue; // 处理输入行的剩余部分
	}
	return ret_val;
}
Please enter your first name.
|Viola
Please enter your last name.
|Plunderfest
Viola Plunderfest, your name contains 16 letters.

结构和结构指针的选择:

通常,程序员为了追求效率会使用结构指针作为函数参数,如需防止原始数据被意外修改,使用const限定符。按值传递结构是处理小型结构最常用的方法。

结构中的字符数组和字符指针:

#define LEN 20
struct names {
    char first[LEN];
    char last[LEN];
};

其中的结构声明可以这样写:

struct pnames {
    char * first;
    char * last;
};

考虑下面的代码:

struct names veep = {"Talia", "Summers"};// 两个字符数组参数
struct pnames treas = {"Brad", "Fallingjaw"};// 两个字符指针参数
printf("%s and %s\n", veep.first, treas.first);

以上代码都没问题,也能正常运行,但是思考一下字符串被储存在何处。对于struct names类型的结构变量veep,以上字符串都储存在结构内部,结构总共要分配40字节储存姓名。然而,对于struct pnames类型的结构变量treas,以上字符串储存在编译器储存常量的地方。结构本身只储存了两个地址,在我们的系统中共占16字节。尤其是,struct pnames结构不用为字符串分配任何存储空间。它使用的是储存在别处的字符串(如,字符串常量或数组中的字符串)。简而言之,在pnames结构变量中的指针应该只用来在程序中管理那些已分配和在别处分配的字符串。

我们看看这种限制在什么情况下出问题。考虑下面的代码:

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 的指针也行,但是误用会导致严重的问题。

结构、指针和malloc():

如果使用malloc()分配内存并使用指针储存该地址,那么在结构中使用指针处理字符串就比较合理。这种方法的优点是,可以请求malloc()为字符串分配合适的存储空间。可以要求用4字节储存"Joe"和用18字节储存"Rasolofomasoandro"。用这种方法改写上面的程序清单并不费劲。主要是更改结构声明(用指针代替数组)和提供一个新版本的getinfo()函数。新的结构声明如下:

struct namect {
    char * fname; // 用指针代替数组
    char * lname;
    int letters;
};

新版本的getinfo()把用户的输入读入临时数组中,调用malloc()函数分配存储空间,并把字符串拷贝到新分配的存储空间中。对名和姓都要这样做:

void getinfo(struct namect *pst) {
	char temp[SLEN];
	printf("Please enter your first name.\n");
	s_gets(temp, SLEN);
	pst->fname = (char*) malloc(strlen(temp) + 1); // 分配内存储存名
	strcpy(pst->fname, temp); // 把名拷贝到已分配的内存
	printf("Please enter your last name.\n");
	s_gets(temp, SLEN);
	pst->lname = (char*) malloc(strlen(temp) + 1);
	strcpy(pst->lname, temp);
}

一个完整的例子:

// 使用指针和 malloc()
#include <stdio.h>
#include <string.h> // 提供 strcpy()、strlen() 的原型
#include <stdlib.h> // 提供 malloc()、free() 的原型
#define SLEN 81
struct namect {
	char *fname; // 使用指针
	char *lname;
	int letters;
};
void getinfo(struct namect*); // 分配内存
void makeinfo(struct namect*);
void showinfo(const struct namect*);
void cleanup(struct namect*); // 调用该函数时释放内存
char* s_gets(char *st, int n);
int main(void) {
	struct namect person;
	getinfo(&person);
	makeinfo(&person);
	showinfo(&person);
	cleanup(&person);
	return 0;
}
void getinfo(struct namect *pst) {
	char temp[SLEN];
	printf("Please enter your first name.\n");
	s_gets(temp, SLEN);
	pst->fname = (char*) malloc(strlen(temp) + 1); // 分配内存以储存名
	strcpy(pst->fname, temp); // 把名拷贝到动态分配的内存中
	printf("Please enter your last name.\n");
	s_gets(temp, SLEN);
	pst->lname = (char*) malloc(strlen(temp) + 1);
	strcpy(pst->lname, temp);
}
void makeinfo(struct namect *pst) {
	pst->letters = strlen(pst->fname) + strlen(pst->lname);
}
void showinfo(const struct namect *pst) {
	printf("%s %s, your name contains %d letters.\n", pst->fname, pst->lname, pst->letters);
}
void cleanup(struct namect *pst) {
	free(pst->fname);
	free(pst->lname);
}
char* s_gets(char *st, int n) {
	char *ret_val;
	char *find;
	ret_val = fgets(st, n, stdin);
	if (ret_val) {
		find = strchr(st, '\n'); // 查找换行符
		if (find) // 如果地址不是 NULL,
			*find = '\0'; // 在此处放置一个空字符
		else
			while (getchar() != '\n')
				continue; // 处理输入行的剩余部分
	}
	return ret_val;
}
Please enter your first name.
Floresiensis
Please enter your last name.
Mann
Floresiensis Mann, your name contains 16 letters.

复合字面量和结构(C99):

C99 的复合字面量特性可用于结构和数组。如果只需要一个临时结构值,复合字面量很好用。例如,可以使用复合字面量创建一个数组作为函数的参数或赋给另一个结构。语法是把类型名放在圆括号中,后面紧跟一个用花括号括起来的初始化列表。例如,下面是struct book类型的复合字面量:

(struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99};
/* 复合字面量 */
#include <stdio.h>
#define MAXTITL 41
#define MAXAUTL 31
struct book { // 结构模版:标记是 book
	char title[MAXTITL];
	char author[MAXAUTL];
	float value;
};
int main(void) {
	struct book readfirst;
	int score;
	printf("Enter test score: ");
	scanf("%d", &score);
	if (score >= 84)
		readfirst = (struct book) {
			"Crime and Punishment",
			"Fyodor Dostoyevsky",
			11.25
		};
	else
		readfirst = (struct book) {
			"Mr.Bouncy's Nice Hat",
			"Fred Winsome",
			5.99
		};
	printf("Your assigned reading:\n");
	printf("%s by %s: $%.2f\n", readfirst.title, readfirst.author, readfirst.value);
	return 0;
}

还可以把复合字面量作为函数的参数。如果函数接受一个结构,可以把复合字面量作为实际参数传递:

struct rect {double x; double y;};
double rect_area(struct rect r){return r.x * r.y;}; // 参数是struct rect类型
...
double area;
area = rect_area( (struct rect) {10.5, 20.0});

值210被赋给area。

如果函数接受一个地址,可以传递复合字面量的地址:

struct rect {double x; double y;};
double rect_areap(struct rect * rp){return rp->x * rp->y;}; // 参数是struct rect *类型
...
double area;
area = rect_areap( &(struct rect) {10.5, 20.0});

值210被赋给area。

复合字面量在所有函数的外部,具有静态存储期;如果复合字面量在块中,则具有自动存储期。复合字面量和普通初始化列表的语法规则相同。这意味着,可以在复合字面量中使用指定初始化器。

伸缩型数组成员(C99):

C99新增了一个特性:伸缩型数组成员(flexible array member),利用这项特性声明的结构,其最后一个数组成员具有一些特性。第1个特性是,该数组不会立即存在。第2个特性是,使用这个伸缩型数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样。这可能听起来很奇怪,所以我们来一步步地创建和使用一个带伸缩型数组成员的结构。

首先,声明一个伸缩型数组成员有如下规则:

➀伸缩型数组成员必须是结构的最后一个成员;

➁结构中必须至少有一个成员;

➂伸缩数组的声明类似于普通数组,只是它的方括号中是空的。

下面用一个示例来解释以上几点:

struct flex {
    int count;
    double average;
    double scores[]; // 伸缩型数组成员
}

声明一个struct flex类型的结构变量时,不能用scores做任何事,因为没有给这个数组预留存储空间。实际上,C99的意图并不是让你声明struct flex类型的变量,而是希望你声明一个指向struct flex类型的指针,然后用malloc()来分配足够的空间,以储存struct flex类型结构的常规内容和伸缩型数组成员所需的额外空间。例如,假设用scores表示一个内含5个double类型值的数组,可以这样做:

struct flex * pf;
// 请求为一个结构和一个数组分配存储空间
pf = malloc(sizeof(struct flex) + 5 * sizeof(double));

现在有足够的存储空间储存count、average和一个内含5个double类型值的数组。可以用指针pf访问这些成员:

pf->count = 5;      // 设置 count 成员
pf->scores[2] = 18.5; // 访问数组成员的一个元素

扩展例子,让伸缩型数组成员在第1种情况下表示5个值,在第2种情况下代表9个值。该程序也演示了如何编写一个函数处理带伸缩型数组元素的结构。

// 伸缩型数组成员(C99新增特性)
#include <stdio.h>
#include <stdlib.h>
struct flex {
	size_t count;
	double average;
	double scores[]; // 伸缩型数组成员
};
void showFlex(const struct flex *p);
int main(void) {
	struct flex *pf1, *pf2;
	int n = 5;
	int i;
	int tot = 0;
	pf1 = malloc(sizeof(struct flex) + n * sizeof(double)); // 为结构和数组分配存储空间
	pf1->count = n;
	for (i = 0; i < n; i++) {
		pf1->scores[i] = 20.0 - i;
		tot += pf1->scores[i];
	}
	pf1->average = tot / n;
	showFlex(pf1);
	n = 9;
	tot = 0;
	pf2 = malloc(sizeof(struct flex) + n * sizeof(double));
	pf2->count = n;
	for (i = 0; i < n; i++) {
		pf2->scores[i] = 20.0 - i / 2.0;
		tot += pf2->scores[i];
	}
	pf2->average = tot / n;
	showFlex(pf2);
	free(pf1);
	free(pf2);
	return 0;
}
void showFlex(const struct flex *p) {
	int i;
	printf("Scores : ");
	for (i = 0; i < p->count; i++)
		printf("%g ", p->scores[i]);
	printf("\nAverage: %g\n", p->average);
}
Scores : 20 19 18 17 16
Average: 18
Scores : 20 19.5 19 18.5 18 17.5 17 16.5 16
Average: 17

带伸缩型数组成员的结构确实有一些特殊的处理要求。

㊀不能用结构进行赋值或拷贝。

struct flex * pf1, *pf2; // *pf1 和*pf2 都是结构
...
*pf2 = *pf1;        // 不要这样做

这样做只能拷贝除伸缩型数组成员以外的其他成员。确实要进行拷贝,应使用memcpy()函数。

void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字符到存储区 str1

㊁不要以按值方式把这种结构传递给结构。

原因相同,按值传递一个参数与赋值类似。要把结构的地址传递给函数。

㊂不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员。

这种类似于在结构中最后一个成员是伸缩型数组的情况,称为struct hack。除了伸缩型数组成员在声明时用空的方括号外,struct hack特指大小为0的数组。然而,struct hack是针对特殊编译器(GCC)的,不属于C标准。这种伸缩型数组成员方法是标准认可的编程技巧。

匿名结构(C11):

 匿名结构是一个没有名称的结构成员。为了理解它的工作原理,我们先考虑如何创建嵌套结构:

struct names {
    char first[20];
    char last[20];
};
struct person {
    int id;
    struct names name; // 嵌套结构成员
};
struct person ted = {8483, {"Ted", "Grass"}};

这里,name成员是一个嵌套结构,可以通过类似ted.name.first的表达式访问"ted":

puts(ted.name.first);

在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的成员,删除嵌套循环。匿名特性在嵌套联合中更加有用,我们在本章后面介绍。

使用结构数组的函数:

假设一个函数要处理一个结构数组。由于数组名就是该数组的地址,所以可以把它传递给函数。另外,该函数还需访问结构模板。为了理解该函数的工作原理,程序清单把前面的金融程序扩展为两人,所以需要一个内含两个funds结构的数组。

/* 把结构数组传递给函数 */
#include <stdio.h>
#define FUNDLEN 50
#define N 2
struct funds {
	char bank[FUNDLEN];
	double bankfund;
	char save[FUNDLEN];
	double savefund;
};
double sum(const struct funds money[], int n);
int main(void) {
	struct funds jones[N] = { 
			{ "Garlic-Melon Bank", 4032.27, "Lucky's Savings and Loan", 8543.94 },
			{ "Honest Jack's Bank", 3620.88, "Party Time Savings", 3802.91 }
	};
	printf("The Joneses have a total of $%.2f.\n", sum(jones, N));
	return 0;
}
double sum(const struct funds money[], int n) {// 注意添加 const 关键字
	double total;
	int i;
	for (i = 0, total = 0; i < n; i++)
		total += money[i].bankfund + money[i].savefund;
	return (total);
}
The Joneses have a total of $20000.00.

数组名jones是该数组的地址,即该数组首元素(jones[0])的地址。因此,指针money的初始值相当于通过下面的表达式获得:

money = &jones[0];

可以把数组名作为数组中第1个结构的地址传递给函数。然后可以用数组表示法访问数组中的其他结构。注意下面的函数调用与使用数组名效果相同:

sum(&jones[0], N);

可以修改sum()函数为:double sum(const struct funds * money, int n); 效果和调用方法一样。

由于sum()函数不能改变原始数据,所以该函数使用了ANSI C的限定符const。

把结构内容保存到文件中:

由于结构可以储存不同类型的信息,所以它是构建数据库的重要工具。例如,可以用一个结构储存雇员或汽车零件的相关信息。最终,我们要把这些信息储存在文件中,并且能再次检索。数据库文件可以包含任意数量的此类数据对象。储存在一个结构中的整套信息被称为记录(record),单独的项被称为字段(field)。本节我们来探讨这个主题。

或许储存记录最没效率的方法是用fprintf()。例如,回忆程序清单中的book结构:

#define MAXTITL 40
#define MAXAUTL 40
struct book {
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};

如果pbook标识一个文件流,那么通过下面这条语句可以把信息储存在struct book类型的结构变量primer中:

fprintf(pbooks, "%s %s %.2f\n", primer.title, primer.author, primer.value);

对于一些结构(如,有 30 个成员的结构),这个方法用起来很不方便。另外,在检索时还存在问题,因为程序要知道一个字段结束和另一个字段开始的位置。虽然用固定字段宽度的格式可以解决这个问题(例如,"%39s%39s%8.2f"),但是这个方法仍然很笨拙。

更好的方案是使用fread()和fwrite()函数读写结构大小的单元。回忆一下,这两个函数使用与程序相同的二进制表示法。例如:

fwrite(&primer, sizeof(struct book), 1, pbooks);

定位到 primer 结构变量开始的位置,并把结构中所有的字节都拷贝到与 pbooks 相关的文件中。sizeof(struct book)告诉函数待拷贝的一块数据的大小,1 表明一次拷贝一块数据。带相同参数的fread()函数从文件中拷贝一块结构大小的数据到&primer指向的位置。简而言之,这两个函数一次读写整个记录,而不是一个字段。

以二进制表示法储存数据的缺点是,不同的系统可能使用不同的二进制表示法,所以数据文件可能不具可移植性。甚至同一个系统,不同编译器设置也可能导致不同的二进制布局。

保存结构的程序示例:

为了演示如何在程序中使用这些函数,我们把程序清单修改为一个新的版本,把书名保存在book.dat文件中。如果该文件已存在,程序将显示它当前的内容,然后允许在文件中添加内容。

/* 在文件中保存结构中的内容 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXTITL 40
#define MAXAUTL 40
#define MAXBKS 10 /* 最大书籍数量 */
char* s_gets(char *st, int n);
struct book { /* 建立 book 模板 */
	char title[MAXTITL];
	char author[MAXAUTL];
	float value;
};
int main(void) {
	struct book library[MAXBKS]; /* 结构数组 */
	int count = 0;
	int index, filecount;
	FILE *pbooks;
	int size = sizeof(struct book);
	if ((pbooks = fopen("book.dat", "a+b")) == NULL) {
		fputs("Can't open book.dat file\n", stderr);
		exit(1);
	}
	rewind(pbooks); /* 定位到文件开始 */
	while (count < MAXBKS && fread(&library[count], size, 1, pbooks) == 1) {
		if (count == 0)
			puts("Current contents of book.dat:");
		printf("%s by %s: $%.2f\n", library[count].title, library[count].author, library[count].value);
		count++;
	}
	filecount = count;
	if (count == MAXBKS) {
		fputs("The book.dat file is full.", stderr);
		exit(2);
	}
	puts("Please add new book titles.");
	puts("Press [enter] at the start of a line to stop.");
	while (count < MAXBKS 
                && s_gets(library[count].title, MAXTITL) != NULL 
                && library[count].title[0] != '\0') {
		puts("Now enter the author.");
		s_gets(library[count].author, MAXAUTL);
		puts("Now enter the value.");
		scanf("%f", &library[count++].value);
		while (getchar() != '\n')
			continue; /* 清理输入行 */
		if (count < MAXBKS)
			puts("Enter the next title.");
	}
	if (count > 0) {
		puts("Here is the list of your books:");
		for (index = 0; index < count; index++)
			printf("%s by %s: $%.2f\n", library[index].title, library[index].author, library[index].value);
        // size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
        // 把 ptr 所指向的数组中的数据写入到给定流 stream 中。
        // size -- 这是要被写入的【每个元素】的大小,以字节为单位。
        // nmemb -- 这是【元素】的个数,每个元素的大小为 【size】 字节。
		fwrite(&library[filecount], size, count - filecount, pbooks);
	} else
		puts("No books? Too bad.\n");
	puts("Bye.\n");
	fclose(pbooks);
	return 0;
}
char* s_gets(char *st, int n) {
	char *ret_val;
	char *find;
	ret_val = fgets(st, n, stdin);
	if (ret_val) {
		find = strchr(st, '\n'); // 查找换行符
		if (find) // 如果地址不是 NULL,
			*find = '\0'; // 在此处放置一个空字符
		else
			while (getchar() != '\n')
				continue; // 清理输入行
	}
	return ret_val;
}
$ booksave
Please add new book titles.
Press [enter] at the start of a line to stop.
|Metric Merriment
Now enter the author.
|Polly Poetica
Now enter the value.
|18.99
Enter the next title.
|Deadly Farce
Now enter the author.
|Dudley Forse
Now enter the value.
|15.99
Enter the next title.
|[enter]
Here is the list of your books:
Metric Merriment by Polly Poetica: $18.99
Deadly Farce by Dudley Forse: $15.99
Bye.
$ booksave
Current contents of book.dat:
Metric Merriment by Polly Poetica: $18.99
Deadly Farce by Dudley Forse: $15.99
Please add new book titles.
|The Third Jar
Now enter the author.
|Nellie Nostrum
Now enter the value.
|22.99
Enter the next title.
|[enter]
Here is the list of your books:
Metric Merriment by Polly Poetica: $18.99
Deadly Farce by Dudley Forse: $15.99
The Third Jar by Nellie Nostrum: $22.99
Bye.

再次运行booksave.c程序把这3本书作为当前的文件记录打印出来。

程序要点:

首先,以"a+b"模式打开文件。a+部分允许程序读取和向文件末尾追加内容。b 是 ANSI的一种标识方法,表明程序将使用二进制文件格式。对于不接受b模式的UNIX系统,可以省略b,因为UNIX只有一种文件形式。对于早期的ANSI实现,要找出和b等价的表示法。

我们选择二进制模式是因为fread()和fwrite()函数要使用二进制文件。虽然结构中有些内容是文本,但是value成员不是文本。如果使用文本编辑器查看book.dat,该结构本文部分的内容显示正常,但是数值部分的内容不可读,甚至会导致文本编辑器出现乱码。

rewind()函数确保文件指针位于文件开始处,为读文件做好准备。

第1个while循环每次把一个结构读到结构数组中,当数组已满或读完文件时停止。变量filecount统计已读结构的数量。

第2个while按下循环提示用户进行输入,并接受用户的输入。和之前程序清单一样,当数组已满或用户在一行的开始处按下Enter键时,循环结束。

注意,该循环开始时count变量的值是第1个循环结束后的值。该循环把新输入项添加到数组的末尾。

然后for循环打印文件和用户输入的数据。因为该文件是以附加模式打开,所以新写入的内容添加到文件现有内容的末尾。

我们本可以用一个循环在文件末尾一次添加一个结构,但还是决定用fwrite()一次写入一块数据。对表达式count - filecount求值得新添加的书籍数量,然后调用fwrite()把结构大小的块写入文件。由于表达式&library[filecount]是数组中第1个新结构的地址,所以拷贝就从这里开始。

也许该例是把结构写入文件和检索它们的最简单的方法,但是这种方法浪费存储空间,因为这还保存了结构中未使用的部分。该结构的大小是2×40×sizeof(char)+sizeof(float),在我们的系统中共84字节。实际上不是每个输入项都需要这么多空间。但是,让每个输入块的大小相同在检索数据时很方便。

另一个方法是使用可变大小的记录。为了方便读取文件中的这种记录,每个记录以数值字段规定记录的大小。这比上一种方法复杂。通常,这种方法涉及接下来要介绍的“链式结构”和第16章的动态内存分配。

链式结构:

在结束讨论结构之前, 我们想简要介绍一下结构的多种用途之一: 创建新的数据形式。计算机用户已经开发出的一些数据形式比我们提到过的数组和简单结构更有效地解决特定的问题。这些形式包括队列、 二叉树、 堆、 哈希表和图表。 许多这样的形式都由链式结构(linked structure)组成。通常,每个结构都包含一两个数据项和一两个指向其他同类型结构的指针。这些指针把一个结构和另一个结构链接起来,并提供一种路径能遍历整个彼此链接的结构。例如,图14.3演示了一个二叉树结构,每个单独的结构(或节点)都和它下面的两个结构(或节点)相连。

图14.3中显示的分级或树状的结构是否比数组高效?考虑一个有10级节点的树的情况。它有2^{10}-1(或1023) 个节点,可以储存1023个单词。如果这些单词以某种规则排列,那么可以从最顶层开始,逐级向下移动查找单词,最多只需移动9次便可找到任意单词。如果把这些单词都放在一个数组中,最多要查找1023个元素才能找出所需的单词。
如果你对这些高级概念感兴趣,可以阅读一些关于数据结构的书籍。使用C结构,可以创建和使用那些书中介绍的各种数据形式。另外,第17章中也介绍了一些高级数据形式。

联合简介:

联合(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。编译器分配足够的空间以便它能储存联合声明中占用最大字节的类型。在本例中,占用空间最大的是double类型的数据。在我们的系统中,double类型占64位,即8字节。第2个声明创建了一个数组save,内含10个元素,每个元素都是8字节。第3个声明创建了一个指针,该指针变量储存hold类型联合变量的地址。
可以初始化联合。需要注意的是,联合只能储存一个值,这与结构不同。有 3 种初始化的方法:1.把一个联合初始化为另一个同类型的联合;2.初始化联合的第1个元素;3.根据C99标准,使用指定初始化器:

union hold valA;
valA.letter = 'R';
union hold valB = valA; // 用另一个联合来初始化
union hold valC = {88}; // 初始化联合的digit 成员
union hold valD = {.bigfl = 118.2}; // 指定初始化器

使用联合:

下面是联合的一些用法:

fit.digit = 23;   //把 23 储存在 fit, 【占】2字节
fit.bigfl = 2.0;  // 【清除】23, 储存 2.0, 【占】8字节
fit.letter = 'h'; // 【清除】2.0, 储存h, 【占】1字节

 点运算符表示正在使用哪种数据类型。在联合中,一次只储存一个值。即使有足够的空间,也不能同时储存一个char类型值和一个int类型值。编写代码时要注意当前储存在联合中的数据类型。
和用指针访问结构使用->运算符一样, 用指针访问联合时也要使用->运算符:

pu = &fit;
x = pu->digit; // 相当于 x = fit.digit

 不要像下面的语句序列这样:

fit.letter = 'A';
flnum = 3.02*fit.bigfl; // 错误

 以上语句序列是错误的,因为储存在 fit 中的是 char 类型,但是下一行却假定 fit 中的内容是double类型。
不过,用一个成员把值储存在一个联合中,然后用另一个成员查看内容,这种做法有时很有用。下一章的程序清单就会给出了一个这样的例子。
联合的另一种用法是,在结构中储存与其成员有从属关系的信息。例如,假设用一个结构表示一辆汽车。如果汽车属于驾驶者, 就要用一个结构成员来描述这个所有者。如果汽车被租赁,那么需要一个成员来描述其租赁公司。可以用下面的代码来完成:

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。

匿名联合(C11):

匿名联合和匿名结构的工作原理相同, 即匿名联合是一个结构或联合的无名联合成员。 例如, 我们重新定义car_data结构如下:

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的类型。 联合使用成员运算符的方式与结构相同。
示例:

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;

 第1个声明创建了spetrum作为标记名,允许把enum spetrum作为一个类型名使用。第2个声明使color作为该类型的变量。第1个声明中花括号内的标识符枚举了spectrum变量可能有的值。因此,color 可能的值是 red、orange、 yellow 等。这些符号常量被称为枚举符(enumerator)。然后,便可这样用:

int c;
color = blue;
if (color == yellow)
    ...;
for (color = red; color <= violet; color++)
    ...;

 虽然枚举符(如red和blue)是int类型,但是枚举变量可以是任意整数类型,前提是该整数类型可以储存枚举常量。例如, spectrum的枚举符范围是0~5,所以编译器可以用unsigned char来表示color变量。

顺带一提,C枚举的一些特性并不适用于C++。例如,C允许枚举变量使用++运算符,但是C++标准不允许。所以,如果编写的代码将来会并入C++程序,那么必须把上面例子中的color声明为int类型,才能C和C++都兼容。

enum常量:

blue和red到底是什么? 从技术层面看, 它们是int类型的常量。 例如, 假定有前面的枚举声明, 可以这样写:

printf("red = %d, orange = %d\n", red, orange);

 其输出如下:

red = 0, orange = 1

 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。

enum的用法:

枚举类型的目的是为了提高程序的可读性和可维护性。 如果要处理颜色, 使用red和blue比使用0和1更直观。 注意, 枚举类型只能在内部使用。 如果要输入color中orange的值, 只能输入1, 而不是单词orange。 或者, 让程序先读入字符串"orange", 再将其转换为orange代表的值。因为枚举类型是整数类型, 所以可以在表达式中以使用整数变量的方式使用enum变量。 它们用在case语句中很方便。

/* 使用枚举类型的值 */
#include <stdio.h>
#include <string.h> // 提供 strcmp()、 strchr()函数的原型
#include <stdbool.h> // C99 特性
char* s_gets(char *st, int n);
enum spectrum {
	red, orange, yellow, green, blue, violet
};
const char *colors[] = { "red", "orange", "yellow", "green", "blue", "violet" };
#define LEN 30
int main(void) {
	char choice[LEN];
	enum spectrum color;
	bool color_is_found = false;
	puts("Enter a color (empty line to quit):");
	while (s_gets(choice, LEN) != NULL && choice[0] != '\0') {
		for (color = red; color <= violet; color++) {
			if (strcmp(choice, colors[color]) == 0) {
				color_is_found = true;
				break;
			}
		}
		if (color_is_found)
			switch (color) {
			case red:
				puts("Roses are red.");
				break;
			case orange:
				puts("Poppies are orange.");
				break;
			case yellow:
				puts("Sunflowers are yellow.");
				break;
			case green:
				puts("Grass is green.");
				break;
			case blue:
				puts("Bluebells are blue.");
				break;
			case violet:
				puts("Violets are violet.");
				break;
			}
		else
			printf("I don't know about the color %s.\n", choice);
		color_is_found = false;
		puts("Next color, please (empty line to quit):");
	}
	puts("Goodbye!");
	return 0;
}
char* s_gets(char *st, int n) {
	char *ret_val;
	char *find;
	ret_val = fgets(st, n, stdin);
	if (ret_val) {
		find = strchr(st, '\n'); // 查找换行符
		if (find) // 如果地址不是 NULL,
			*find = '\0'; // 在此处放置一个空字符
		else
			while (getchar() != '\n')
				continue; // 清理输入行
	}
	return ret_val;
}

当输入的字符串与color数组的成员指向的字符串相匹配时, for循环结束。 如果循环找到匹配的颜色, 程序就用枚举变量的值与作为case标签的枚举常量匹配。 下面是该程序的一个运行示例:

Enter a color (empty line to quit):
|blue
Bluebells are blue.
Next color, please (empty line to quit):
|orange
Poppies are orange.
Next color, please (empty line to quit):
|purple
I don't know about the color purple.
Next color, please (empty line to quit):
|
Goodbye!

共享名称空间:

C语言使用名称空间(namespace) 标识程序中的各部分, 即通过名称来识别。 作用域是名称空间概念的一部分: 两个不同作用域的同名变量不冲突; 两个相同作用域的同名变量冲突。 名称空间是分类别的。 在特定作用域中的结构标记、 联合标记和枚举标记都共享相同的名称空间, 该名称空间与普通变量使用的空间不同。 这意味着在相同作用域中变量和标记的名称可以相同, 不会引起冲突, 但是不能在相同作用域中声明两个同名标签或同名变量。 例如, 在C中, 下面的代码不会产生冲突:

struct rect { double x; double y; };
int rect; // 在C中不会产生冲突

尽管如此, 以两种不同的方式使用相同的标识符会造成混乱。 另外,C++不允许这样做, 因为它把标记名和变量名放在相同的名称空间中。

typedef简介:

typedef工具是一个高级数据特性, 利用typedef可以为某一类型自定义名称。 这方面与#define类似, 但是两者有3处不同:
与#define不同, typedef创建的符号名只受限于类型, 不能用于值。
typedef由编译器解释, 不是预处理器。
在其受限范围内, typedef比#define更灵活。
下面介绍typedef的工作原理。 假设要用BYTE表示1字节的数组。 只需像定义个char类型变量一样定义BYTE, 然后在定义前面加上关键字typedef即可:

typedef unsigned char BYTE;

 随后, 便可使用BYTE来定义变量:

BYTE x, y[10], * z;

 该定义的作用域取决于typedef定义所在的位置。 如果定义在函数中, 就具有局部作用域, 受限于定义所在的函数。 如果定义在函数外面, 就具有文件作用域。
通常, typedef定义中用大写字母表示被定义的名称, 以提醒用户这个类型名实际上是一个符号缩写。 当然, 也可以用小写:

typedef unsigned char byte;

使用typedef还能提高程序的可移植性。 例如, 我们之前提到的sizeof运算符的返回类型: size_t类型, 以及time()函数的返回类型: time_t类型。 C标准规定sizeof和time()返回整数类型, 但是让实现来决定具体是什么整数类型。 其原因是, C 标准委员会认为没有哪个类型对于所有的计算机平台都是最优选择。 所以, 标准委员会决定建立一个新的类型名(如, time_t) , 并让实现使用typedef来设置它的具体类型。 以这样的方式, C标准提供以下通用原型:

time_t time(time_t *);

time_t 在一个系统中是 unsigned long, 在另一个系统中可以是 unsigned long long。 只要包含time.h头文件, 程序就能访问合适的定义, 你也可以在代码中声明time_t类型的变量。
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;

 还可以把typedef用于结构:

typedef struct complex {
    float real;
    float imag;
} COMPLEX;

 用typedef来命名一个结构类型时, 可以省略该结构的标签:

typedef struct {double x; double y;} rect;

 假设这样使用typedef定义的类型名:

rect r1 = {3.0, 6.0};
rect r2 = r1;

 以上代码将被翻译成:

struct {double x; double y;} r1= {3.0, 6.0};
struct {double x; double y;} r2;
r2 = r1;

 这两个结构在声明时都没有标记, 它们的成员完全相同(成员名及其类型都匹配) , C认为这两个结构的类型相同, 所以r1和r2间的赋值是有效操作。

使用typedef的第2个原因是: typedef常用于给复杂的类型命名。 例如,下面的声明:

typedef char (* FRPTC ()) [5];

把FRPTC声明为一个函数类型, 该函数返回一个指针, 该指针指向内含5个char类型元素的数组。

通过结构、 联合和typedef, C提供了有效处理数据的工具和处理可移植数据的工具。

其他复杂的声明:

C 允许用户自定义数据形式。 虽然我们常用的是一些简单的形式, 但是根据需要有时还会用到一些复杂的形式。 在一些复杂的声明中, 常包含下面的符号:

下面是一些较复杂的声明示例:

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

把以上规则应用于下面的声明:

int * oof[3][4];

[3]比*的优先级高, 由于从左往右结合, 所以[3]先与oof结合。 因此,oof首先是一个内含3个元素的数组。 然后再与[4]结合, 所以oof的每个元素都是内含4个元素的数组。 *说明这些元素都是指针。 最后, int表明了这4个元素都是指向int的指针。 因此, 这条声明要表达的是: foo是一个内含3个元素的数组, 其中每个元素是由4个指向int的指针组成的数组。 简而言之, oof是一个3×4的二维数组, 每个元素都是指向int的指针。 编译器要为12个指针预留存储空间。
现在来看下面的声明:

int (* uuf)[3][4];

圆括号使得*先与uuf结合, 说明uuf是一个指针, 所以uuf是一个指向3×4的int类型二维数组的指针。 编译器要为一个指针预留存储空间。

根据这些规则, 还可以声明:

char * fump(int); // 返回字符指针的函数
char (* frump)(int); // 指向函数的指针, 该函数的返回类型为char
char (* flump[3])(int); // 内含3个指针的数组, 每个指针都指向返回类型为char的函数

这3个函数都接受int类型的参数。
可以使用typedef建立一系列相关类型:

typedef int arr5[5];
typedef arr5 * p_arr5;
typedef p_arr5 arrp10[10];
arr5 togs; // togs 是一个内含5个int类型值的数组==【int arr5[5]】
p_arr5 p2; // p2 是一个指向数组的指针, 该数组内含5个int类型的值==【int (*p2)[5]】
arrp10 ap; // ap 是一个内含10个指针的数组, 每个指针都指向一个内含5个int类型值的数组==【(*ap[10])[5]】

函数和指针:

通过上一节的学习可知, 可以声明一个指向函数的指针。 这个复杂的玩意儿到底有何用处? 通常, 函数指针常用作另一个函数的参数, 告诉该函数要使用哪一个函数。 例如, 排序数组涉及比较两个元素, 以确定先后。 如果元素是数字, 可以使用>运算符; 如果元素是字符串或结构, 就要调用函数进行比较。 C库中的 qsort()函数可以处理任意类型的数组, 但是要告诉qsort()使用哪个函数来比较元素。 为此, qsort()函数的参数列表中, 有一个参数接受指向函数的指针。 然后, qsort()函数使用该函数提供的方案进行排序, 无论这个数组中的元素是整数、 字符串还是结构。
首先, 什么是函数指针? 假设有一个指向int类型变量的指针, 该指针储存着这个int类型变量储存在内存位置的地址。同样, 函数也有地址, 因为函数的机器语言实现由载入内存的代码组成。 指向函数的指针中储存着函数代码的起始处的地址。
其次, 声明一个数据指针时, 必须声明指针所指向的数据类型。 声明一个函数指针时, 必须声明指针指向的函数类型。 为了指明函数类型, 要指明函数签名, 即函数的返回类型和形参类型。 例如, 考虑下面的函数原型:

void ToUpper(char *); // 把字符串中的字符转换成大写字符

ToUpper()函数的类型是“带char * 类型参数、 返回类型是void的函数”。下面声明了一个指针pf指向该函数类型:

void (*pf)(char *); // pf 是一个指向函数的指针

 从该声明可以看出, 第1对圆括号把*和pf括起来, 表明pf是一个指向函数的指针。 因此, (*pf)是一个参数列表为(char *)、 返回类型为void的函数。

前面提到过, 由于运算符优先级的规则, 在声明函数指针时必须把*和指针名括起来。 如果省略第1个圆括号会导致完全不同的情况:

void *pf(char *); // 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)

 由于历史的原因, 贝尔实验室的C和UNIX的开发者采用第1种形式, 而伯克利的UNIX推广者却采用第2种形式。 K&R C不允许第2种形式。 但是,为了与现有代码兼容, ANSI C认为这两种形式(本例中是(*pf)(mis)和pf(mis)) 等价。 后续的标准也延续了这种矛盾的和谐。

作为函数的参数是数据指针最常见的用法之一, 函数指针亦如此。 例如, 考虑下面的函数原型:

void show(void (* fp)(char *), char * str);

 这看上去让人头晕。 它声明了两个形参: fp和str。 fp形参是一个函数指针, str是一个数据指针。 更具体地说, fp指向的函数接受char * 类型的参数, 其返回类型为void; str指向一个char类型的值。 因此, 假设有上面的声明, 可以这样调用函数:

show(ToLower, mis); /* show()使用ToLower()函数: fp = ToLower */
show(pf, mis); /* show()使用pf指向的函数: fp = pf */

 show()如何使用传入的函数指针? 是用fp()语法还是(*fp)()语法调用函数:

void show(void (* fp)(char *), char * str) {
    (*fp)(str); /* 把所选函数作用于str */
    puts(str); /* 显示结果 */
}

 这里的show()首先用fp指向的函数转换str, 然后显示转换后的字符串。

顺带一提, 把带返回值的函数作为参数传递给另一个函数有两种不同的方法。 例如, 考虑下面的语句:

function1(sqrt); /* 传递sqrt()函数的地址 */
function2(sqrt(4.0)); /* 传递sqrt()函数的返回值 */
/* 使用函数指针 */
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define LEN 81
char* s_gets(char *st, int n);
char showmenu(void);
void eatline(void); // 读取至行末尾
void show(void (*fp)(char*), char *str);
void ToUpper(char*); // 把字符串转换为大写
void ToLower(char*); // 把字符串转换为小写
void Transpose(char*); // 大小写转置
void Dummy(char*); // 不更改字符串
int main(void) {
	char line[LEN];
	char copy[LEN];
	char choice;
	void (*pfun)(char*); // 声明一个函数指针, 被指向的函数接受char *类型的参数, 无返回值
	puts("Enter a string (empty line to quit):");
	while (s_gets(line, LEN) != NULL && line[0] != '\0') {
		while ((choice = showmenu()) != 'n') {
			switch (choice) { // switch语句设置指针
			case 'u':
				pfun = ToUpper;
				break;
			case 'l':
				pfun = ToLower;
				break;
			case 't':
				pfun = Transpose;
				break;
			case 'o':
				pfun = Dummy;
				break;
			}
			strcpy(copy, line); // 为show()函数拷贝一份
			show(pfun, copy); // 根据用户的选择, 使用选定的函数
		}
		puts("Enter a string (empty line to quit):");
	}
	puts("Bye!");
	return 0;
}
char showmenu(void) {
	char ans;
	puts("Enter menu choice:");
	puts("u) uppercase l) lowercase");
	puts("t) transposed case o) original case");
	puts("n) next string");
	ans = getchar(); // 获取用户的输入
	ans = tolower(ans); // 转换为小写
	eatline(); // 清理输入行
	while (strchr("ulton", ans) == NULL) {
		puts("Please enter a u, l, t, o, or n:");
		ans = tolower(getchar());
		eatline();
	}
	return ans;
}
void eatline(void) {
	while (getchar() != '\n')
		continue;
}
void ToUpper(char *str) {
	while (*str) {
		*str = toupper(*str);
		str++;
	}
}
void ToLower(char *str) {
	while (*str) {
		*str = tolower(*str);
		str++;
	}
}
void Transpose(char *str) {
	while (*str) {
		if (islower(*str))
			*str = toupper(*str);
		else if (isupper(*str))
			*str = tolower(*str);
		str++;
	}
}
void Dummy(char *str) {} //不改变字符串

void show(void (*fp)(char*), char *str) {
	(*fp)(str); // 把用户选定的函数作用于str
	puts(str); // 显示结果
}
char* s_gets(char *st, int n) {
	char *ret_val;
	char *find;
	ret_val = fgets(st, n, stdin);
	if (ret_val) {
		find = strchr(st, '\n'); // 查找换行符
		if (find) // 如果地址不是NULL,
			*find = '\0'; // 在此处放置一个空字符
		else
			while (getchar() != '\n')
				continue; // 清理输入行中剩余的字符
	}
	return ret_val;
}
Enter a string (empty line to quit):
|Does C make you feel loopy?
Enter menu choice:
u) uppercase l) lowercase
t) transposed case o) original case
n) next string
|t
dOES c MAKE YOU FEEL LOOPY?
Enter menu choice:
u) uppercase l) lowercase
t) transposed case o) original case
n) next string
|l
does c make you feel loopy?
Enter menu choice:
u) uppercase l) lowercase
t) transposed case o) original case
n) next string
|n
Enter a string (empty line to quit):
Bye!

注意, ToUpper()、 ToLower()、 Transpose()和 Dummy()函数的类型都相同, 所以这 4 个函数都可以赋给pfun指针。 该程序把pfun作为show()的参数, 但是也可以直接把这4个函数中的任一个函数名作为参数, 如show(Transpose, copy)。
这种情况下, 可以使用typedef。 例如, 该程序中可以这样写:

typedef void (*V_FP_CHARP)(char *);
void show (V_FP_CHARP fp, char *);
V_FP_CHARP pfun;

如果还想更复杂一些, 可以声明并初始化一个函数指针的数组:

V_FP_CHARP arpf[4] = {ToUpper, ToLower, Transpose, Dummy};

然后把showmenu()函数的返回类型改为int, 如果用户输入u, 则返回0;如果用户输入l, 则返回2; 如果用户输入t, 则返回2, 以此类推。 可以把程序中的switch语句替换成下面的while循环:

index = showmenu();
while (index >= 0 && index <= 3) {
    strcpy(copy, line); /* 为show()拷贝一份 */
    show(arpf[index], copy); /* 使用选定的函数 */
    index = showmenu();
}

虽然没有函数数组, 但是可以有函数指针数组。
以上介绍了使用函数名的4种方法: 定义函数、 声明函数、 调用函数和作为指针。 图14.4进行了总结。

 程序使用了string.h头文件中的标准库函数strchr():

while (strchr("ulton", ans) == NULL)

 该函数在字符串"ulton"中查找字符ans首次出现的位置, 并返回一个指向该字符的指针。 如果没有找到该字符, 则返回空指针。 因此, 上面的while循环头可以用下面的while循环头代替, 但是上面的用起来更方便:

while (ans != 'u' && ans != 'l' && ans != 't' && ans != 'o' && ans != 'n')

 待检查的项越多, 使用strchr()就越方便。

关键概念:

我们在编程中要表示的信息通常不只是一个数字或一些列数字。 程序可能要处理具有多种属性的实体。 例如, 通过姓名、 地址、 电话号码和其他信息表示一名客户; 或者, 通过电影名、 发行人、 播放时长、 售价等表示一部电影DVD。 C结构可以把这些信息都放在一个单元内。 在组织程序时这很重要, 因为这样可以把相关的信息都储存在一处, 而不是分散储存在多个变量中。
设计结构时, 开发一个与之配套的函数包通常很有用。 例如, 写一个以结构(或结构的地址) 为参数的函数打印结构内容, 比用一堆printf()语句强得多。 因为只需要一个参数就能打印结构中的所有信息。 如果把信息放到零散的变量中, 每个部分都需要一个参数。 另外, 如果要在结构中增加一个成员, 只需重写函数, 不必改写函数调用。 这在修改结构时很方便。
联合声明与结构声明类似。 但是, 联合的成员共享相同的存储空间, 而且在联合中同一时间内只能有一个成员。 实质上, 可以在联合变量中储存一个类型不唯一的值。
enum 工具提供一种定义符号常量的方法, typedef 工具提供一种为基本或派生类型创建新标识符的方法。
指向函数的指针提供一种告诉函数应使用哪一个函数的方法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值