文章目录
- 前言
- 14.1 示例问题:创建图书目录
- 14.2建立结构声明
- 14.3定义结构变量
- 14.3.1 初始化结构
- 14.3.2 访问结构成员
- 14.3.2 结构的初始化器
- 14.4 结构数组
- 14.4.1 声明结构数组
- 14.4.2 标识结构数组的成员
- 14.3 嵌套结构
- 14.6 指向结构的指针
- 14.6.1 声明和初始化结构指针
- 14.6.2 用指针访问成员
- 14.7 向函数传递结构的信息
- 14.7.1传递结构成员
- 14.7.2 传递结构的地址
- 14.7.3 传递结构
- 14.7.4 其他结构特征
- 如果您发现文章有错误请与我留言,感谢
前言
14.1 示例问题:创建图书目录
创建一个图书目录,包含一本书的书名,作者和价格:
如果能把图书信息装在一个数组内,其中每个元素包含一本书的相关信息。
所以我们需要一种即能包含字符串又能包含数字的数据形式,而且还要保持各信息的独立。
利用struct变量创建一个图书目录,包含一本书的书名,作者和价格:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#define MAXTITLE 41
#define MAXAUTH 31
char* s_gets(char* st, int n);
struct book {
//结构模板 标记是book
char title[MAXTITLE];
char author[MAXAUTH];
float value;
};//结构模板结束
int main()
{
struct book library;//把library声明为一个book类型的变量
printf("Please enter the book title.\n");
s_gets(library.title, MAXTITLE);
printf("Please enter author.\n");
s_gets(library.author, MAXAUTH);
printf("Now enter the value.\n");
scanf("%f", &library.value);
printf("%s by %s : $%.2f\n", library.title, library.author, library.value);
printf("%s: \"%s\" ($%.2f)\n", library.author, library.title, library.value);
printf("Done.\n");
}
char* s_gets(char* st, int n)
{
char* ret_val;
char* find;
ret_val = fgets(st, n, stdin);
if (ret_val)//ret_val != EOF
{
find = strchr(st, '\n');
if (find)
*find = '\0';//如果地址不是NULL,就放置一个空字符
else//如果未找到换行符
while (getchar() != '\n')//循环读取输入缓冲区中的剩余字符,直到遇到换行符为止。这样可以清空输入缓冲区,防止下一次输入被之前的输入影响。
continue;
}
return ret_val;
}
详解
这段代码定义了一个函数s_gets,用于从标准输入中获取用户输入的字符串,并将其存储在指定的字符数组中。下面是代码的解释:
char* s_gets(char* st, int n):这是函数的定义,它接受两个参数,一个是指向字符数组的指针st,另一个是整数n,表示最多可以读取的字符数。
char* ret_val; :定义一个指向字符的指针ret_val,用于存储fgets函数的返回值。
char* find; :定义一个指向字符的指针find,用于在输入的字符串中查找换行符。
ret_val = fgets(st, n, stdin); :调用fgets函数从标准输入中读取用户输入的字符串,并将其存储在字符数组st中,最多读取n个字符。函数返回值被赋给ret_val。
if (ret_val):检查fgets函数的返回值,如果返回值不为NULL,表示成功读取了用户输入的字符串。
find = strchr(st, '\n'); :在读取的字符串中查找换行符\n的位置,并将其地址赋给find指针。
if (find):如果找到了换行符。
* find = '\0'; :将换行符替换为字符串结束符\0,这样可以截断字符串,只保存换行符之前的内容。
else:如果未找到换行符。
while (getchar() != '\n'):循环读取输入缓冲区中的剩余字符,直到遇到换行符为止。这样可以清空输入缓冲区,防止下一次输入被之前的输入影响。
continue; :继续下一次循环。
return ret_val; :返回fgets函数的返回值,即读取的字符串或者NULL。
总的来说,这段代码的作用是安全地从标准输入中获取用户输入的字符串,并处理换行符以及输入缓冲区中的剩余字符。
14.2建立结构声明
这样该函数就创建一个结构变量dickens,该变量的结构布局book(标记)
14.3定义结构变量
结构有两层含义。一层含义是“结构布局”,刚才已经讨论过了。结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。下一步是创建一个结构变量,即是结构的另一层含义。程序中创建结构变量的一行是:
struct book library;
编译器执行这行代码便创建了一个结构变量library。编译器使用book模板为该变量分配空间:一个内含MAXTITL个元素的char数组、一个内含MAXAUTL个元素的char数组和一个float类型的变量。这些存储空间都与一个名称library结合在一起
如图:
在结构变量的声明中,struct book所起的作用相当于一般声明中的int或float。例如,可以定义两个 struct book类型的变量,或者甚至是指向struct book类型结构的指针
struct book doyle, panshin, * ptbook;
结构变量doyle和penshin中都包含title,author,value部分,
14.3.1 初始化结构
初始化变量和数组如下: int count = 0;
int fibo[7] = {0,1,1,2,3,5,8};
结构变量是否也可以这样初始化?是的,可以。初始化一个结构变量与初始化数组的语法类似:
struct book library = (
"The Pious Pirate and the Devious Damsel",
"Renee Vivotte",
1.95);
简而言之,我们使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔。因此, title成员可以被初始化为一个字符串,value成员可以被初始化为一个数字。为了让初始化项与结构中
各成员的关联更加明显,我们让每个成员的初始化项独占一行。这样做只是为了提高代码的可读性
14.3.2 访问结构成员
结构就像一个“超级数组”,它可以储存各种各样的类型。数组可以用下标访问各元素,访问结构中的成员要通过点运算符(.)
例如
library.value即访问library的value部分。可以像使用任何float类型变量那样使用library.value。与此类似,可以像使用字符数组那样使用library.title。
因此,程序中有s_gets(library.title,MAXTITL) ;和 scanf(“%f”, &library.value);这样的代码。
本质上,.title、.author和,value 的作用相当于book结构的下标。
注意,虽然library是一个结构,但是library.value是一个float类型的变量,可以像使用其他float类型变量那样使用它。
例如,scanf(“%f”,…)需要一个float类型变量的地址,而&library.float正好符合要求。.比&的优先级高,因此这个表达式和&(library.float)一样。
如果还有一个相同类型的结构变量,可以用相同的方法:
struct book bill, newt;
s_gets(bil1.title,MAXTITL);
s_gets(newt.title,MAXTITL);
.title 引用book 结构的第1个成员。注意,程序清单14.1中的程序以两种不同的格式打印了 library结构变量中的内容。这说明可以自行决定如何使用结构成员。
14.3.2 结构的初始化器
其语法与数组初始化类似,但是结构初始化要使用点运算符和成员名标识的元素。例如,只初始化book结构的value成员.
struct book surprise = {.value = 10.99};
可以按照任意位置初始化:
struct book surprise = {.value = 10.99,
.author = "Xia",
.title = "C Primer Plus"};
另外,对特定成员的最后一次赋值才是她实际获得的值
struct book surprise = {.value = 10.99,
.author = "Xia",
20.99};
赋给value的值为20.99,因为在声明结构中他在author之后,20.99将10.99覆盖了
14.4 结构数组
接下来,我们要把14.1中的程序拓展成处理多本书。显然,每本书的基本信息都可以用一个book类型的结构
变量来表示。所以我们可以使用这一类型的结构数组来处理多本书。
struct book library [MAXBKS];//book类型的结构数组
代码如下
#define _CRT_SECURE_NO_WARNINGS
#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()
{
struct book library[MAXBKS]; /*book类型结构的数组*/
int count = 0;
int index;
printf("Please enter the book title.\n");
printf("Preas [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 thr value.\n");
scanf("%f", &library[count].value);
count++;
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)
*find = '\0';
else
while (getchar() != '\n')//循环读取缓冲区的剩余字符,直到遇到换行符位置。这样可以清空缓冲区防止下一次输入被影响
continue;
}
return ret_val;
}
在程序中定义了一个结构体book
,包含书籍的标题、作者和价格。同时定义了一个结构体数组library
用于存储书籍信息。用户可以输入书籍的标题、作者和价格,程序会逐个存储在library
数组中。用户可以输入多达100本书籍的信息,超过100本书籍或者输入空行时,停止输入。
char * s_gets(char * st, int n)
函数用于安全地获取用户输入的字符串,避免缓冲区溢出。在main
函数中,通过调用s_gets
函数获取用户输入的书籍标题和作者。用户输入价格时使用scanf
函数,然后清除输入行中剩余的字符。
最后,程序会打印出用户输入的书籍列表,包括书籍的标题、作者和价格。如果没有输入书籍信息,则会输出"No books? Too bad."。
这段代码中的这行语句是一个while
循环的条件判断语句,含义如下:
count < MAXBKS
:表示当前已经输入的书籍数量count
小于书籍的最大数量限制MAXBKS
,即还有空间可以继续输入书籍信息。s_gets(library[count].title, MAXTITL) != NULL
:调用s_gets
函数获取用户输入的书籍标题,并判断返回值是否不为NULL
。如果用户输入了有效的书籍标题,则继续执行循环;如果用户输入了空行或者发生了错误,则循环结束。library[count].title[0] != '\0'
:检查当前输入的书籍标题的第一个字符是否不是空字符。如果用户输入了空行(即标题的第一个字符是空字符),则循环结束。
综合起来,这行语句的含义是:只要当前输入的书籍数量未达到最大限制且用户输入了有效的书籍标题(不是空行),就继续循环接收用户输入的书籍信息。
14.4.1 声明结构数组
声明结构数组和声明其他类型的数组类似。
下面是一个声明结构数组的例子: struct book library [MAXBKS];
以上代码把library声明为一个内含MAXBKS个元素的数组。数组的每个元素都是一个book类型的数组。
因此,library[0]是第1个book类型的结构变量,library[1]是第2个book类型的结构变量,以此类推。参看图14.2可以帮助读者理解。数组名library本身不是结构名,它是一个数组名,该数组中的每个元素都是struct book类型的结构变量。
14.4.2 标识结构数组的成员
为了标识结构数组中的成员,可以采用访问单独结构的规则:在结构名后面加一个点运算符,再在点运算符后面写上成员名。如下所示:
library[0].value /第1个数组元素与value相关联/
library[4].title /*第5个数组元素与title相关联./
注意,数组下标紧跟在library后面,不是成员名后面:
library.value[2] //错误
library[2],value // 正确
使用1ibrary[2].value的原因是:1ibrary[2]是结构变量名,正如1ibrary(1]是另一个变量名。
顺带一提,下面的表达式代表什么?
library[2].title[4]
这是 library 数组第3个结构变量(library[2]部分)中书名的第5个字符(title[4]部分)。
该例指出,点运算符右侧的下标作用于各个成员,点运算符左侧的下标作用与结构数组。
最后,总结一下:
library // 一个book结构的数组
library[2] // 一个数组元素,该元素是book结构
library(2].title // 一个char数组(library[2]的title成员)
library[2].title[4] // 数组中library[2]元素的title 成员的一个字符
14.3 嵌套结构
有时,在一个结构中包含另一个结构(即嵌套结构)很方便。
例如,Shalala Pirosky创建了一个有关她朋友信息的结构。显然,结构中需要一个成员表示朋友的姓名。然而,名字可以用一个数组来表示,其中包含名和姓这两个成员。
程序清单14.3是一个简单的示例:
#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, ts!\n”, fellow.handle,first);
从左往右解释fellow.handle.first:
(fellow.handle).first
也就是说,找到fellow,然后找到fellow的handle的成员,再找到handle的first 成员。
14.6 指向结构的指针
至少有4个理由可以解释为何要使用指向结构的指针。
-
第一,就像指向数组的指针比数组本身更容易操控(如,排序问题)一样,指向结构的指针通常比结构本身更容易操控。
-
第二,在一些早期的C实现中,结构不能作为参数传递给函数,但是可以传递指向结构的指针。
-
第三,即使能传递一个结构,传递指针通常更有效率。
-
第四,一些用于表示数据的结构中包含指向其他结构的指针。
下面的程序(程序清单14.4)演示了如何定义指向结构的指针和如何用这样的指针访问结构的成员。
#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;
}
我们先来看如何创建指向guy类型结构的指针,然后再分析如何通过该指针指定结构的成员。
14.6.1 声明和初始化结构指针
声明结构指针很简单:
struct guy * him;
首先是关键字 struct ,其次是结构标记 guy ,然后是一个 * 号,最后跟着指针名
该声明并未创建一个新的结构,但是指针him现在可以指向任意现有的guy类型的结构。
例如,如果 barney是一个guy类型的结构,可以这样写:
him = &barney;
和数组不同的是,结构名并不是结构的地址,因此要在结构名前面加上&运算符。
在本例中,fellow是一个结构数组,这意味着fellow[0]是一个结构。
所以,要让him指向 fellow[0],可以这样写:
him = &fellow[0];
输出的前两行说明赋值成功。比较这两行发现,him指向fellow[0],him + 1指向 fellow[1]。
注意,him加1相当于him指向的地址加84。在十六进制中,874-820=54(十六进制)=84(十进制),因为每个guy结构都占用84字节的内存:names.first占用20字节,names,last占用20字节,favfood占用20字节,job占用20字节,income占用4字节(假设系统中float占用4字节)。
14.6.2 用指针访问成员
指针him 指向结构变量fellow[0],如何通过him 获得fellow[0]的成员的值?程序清单14.4中的第3行输出演示了两种方法。
第1种方法也是最常用的方法:使用->运算符。该运算符由一个连接号(-)后跟一个大于号(>)组成。我们有下面的关系:
如果him == &barney,那么 him->income 即是 barney.income
如果him == &fellow[0],那么him->income即是fellow[0].income
换句话说,->运算符后面的结构指针和.运算符后面的结构名工作方式相同(不能写成him.incone,因为him不是结构名)。
这里要着重理解him是一个指针, 是该指针所指向结构的一个成员。所以在该例中,him->income是一个float类型的变量
但是hime->income。
第2种方法是,以这样的顺序指定结构成员的值:如果him == &fellow[0],
那么*him == fellow[0],因为&和 *是一对互逆运算符。因此,可以做以下替代:
fellow(0].income ==(*him).income
必须要使用圆括号,因为.运算符比*运算符的优先级高。
总之,如果him是指向guy类型结构barney的指针,下面的关系恒成立:
barney.income ==(*him).income ==him->income //假设 him -- &barney
接下来,我们来学习结构和函数的交互。
14.7 向函数传递结构的信息
ANSIC允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。如果你只关心结构中的某一部分,也可以把结构的成员作为参数。我们接下来将分析这3种传递方式,首先介绍以结构成员作为参数的情况。
14.7.1传递结构成员
只要结构成员是一个具有单个值的数据类型(即,int及其相关类型、char、float、double 或指针),便可把它作为参数传递给接受该特定类型的函数。程序清单14.5中的财务分析程序(初级版本)演示了这一点,该程序把客户的银行账户添加到他/她的储蓄和贷款账户中。
#include <stdio.h>
#define FUNDLEN 50
struct funds {
char bank[FUNDLEN];//声明结构
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(double x, double y);
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);
}
14.7.2 传递结构的地址
这次我们把结构的地址作为参数
#include <stdio.h>
#define FUNDLEN 50
struct funds {
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(const struct funds *monney); /*参数是一个指针*/
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)
{
return (money->bankfund + money->savefund);//通过->来访问结构中的变量
}
结果同上
sum()函数使用指向funds结构的指针(money)作为它的参数。把地址&stan传递给该函数,使得指针money 指向结构 stan。然后通过->运算符获取stan.bankfund 和stan.savefund的值。由于该函数不能改变指针所指向值的内容,所以把money声明为一个指向const的指针。
虽然该函数并未使用其他成员,但是也可以访问它们。注意,必须使用&运算符来获取结构的地址。和数组名不同,结构名不是其地址的别名
14.7.3 传递结构
把结构本身作为参数传递给函数:
#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 os $%.2f.\n", sum(stan));
return 0;
}
double sum( struct funds moolah)
{
return (moolah.bankfund + moolah.savefund);
}
结果同上
14.7.4 其他结构特征
现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。
也就是说,
如果n_data和o_data都是相同类型的结构,可以这样做:
o_data = n_data; //把一个结构赋值给另一个结构
这条语句把ndata的每个成员的值都赋给。data的相应成员。即使成员是数组,也能完成赋值。
另外,还可以把一个结构初始化为相同类型的另一个结构:
struct names right_field = {"Ruthie", "George"};
struct names captain = right_field; //把一个结构初始化为另一个结构
现在的C(包括ANSIC),函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回
把结构作为函数参数可以把结构的信息传送给函数:
把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。
结构指针也允许这种双向通信,因此可以选择任一种方法来解决编程问题。我们通过另一组程序示例来演示这两种方法。
为了对比这两种方法,我们先编写一个程序以传递指针的方式处理结构,然后以传递结构和返回结构的方式重写该程序。
1.以传递指针的方式处理结构:
#include <stdio.h>
#include <string.h>
#define NLEN 30
struct namect {
char fname[NLEN];
char lname[NLEN];
int letters;
};
void getinfo(struct namect *);
void makeinfo(struct namect *);
void showinfo(const struct namect *);
char * s_gets(char * st, int n);
int main(void)
{
struct namect person;
getinfo(&person);
makeinfo(&person);
showinfo(&person);
return 0;
}
void getinfo(struct namect * pst)
{
printf("Please enter your first name.\n");
s_gets(pst->fname, NLEN);
printf("Please entre your last name.\n");
s_gets(pst->lname, NLEN);
}
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);
}
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)
*find = '\0'; //如果地址不是NULL,在此处放置一个空字符
else
while (getchar() != '\n') //处理输入行的剩余字符
continue;
}
return ret_val;
}
例如
getinfo(&person);
makeinfo(&person);
showinfo(&person);
s_gets(pst->fname, NLEN);
s_gets(pst->lname, NLEN);
第一个函数输入,第二个函数计算长度,第三个函数输出。
2.传递并用函数返回结构:
#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)
*find = '\0'; //如果地址不是NULL,在此处放置一个空字符
else
while (getchar() != '\n') //处理输入行的剩余字符
continue;
}
return ret_val;
}
运行结果同上
总的来说,用指针传递时优点是占空间小,效率高。但是可能不安全,需要const.用结构参数和返回值时每个函数都会创建自己的副本(备份),当值很大时会很麻烦