第十四章 结构和其他数据形式
14.1 示例问题:创建图书目录
知识点
结构体声明不创建实际的数据对象,只描述该对象由什么组成;
右括号后面的分号是声明所必须的,表示结构体的结束;
结构体声明在函数内部,就仅在函数内部,换而言之,结构体相对于函数的位置类似于局部变量和全局变量;
结构有两层含义:1,告诉编译器如何表示数据,以及申请空间;2,创建结构变量。
多次使用结构体则必须带标记,也就是结构体的名字;
初始化只需按照数据格式创建实际数据,无需带上形式结构;
如果初始化一个静态存储期的结构,初始化列表中的值必须是常量表达式,如果是自动存储期,初始化列表中的值可以不是常量;
/*book.c -- 演示结构体的使用*/
/*20210501 2204*/
#include <stdio.h>
#include <string.h>
#define MAX_TITLE 41 /*最大标题长度*/
#define MAXAUTHOR_NAME 31 /*作者名字的最大长度*/
char *s_gets(char *st, int n);
/*结构体和Python中的类相似,初始化相当于Python中的类实例化*/
struct book{ /*结构模板:标记是book,相当于类名*/
char title[MAX_TITLE];
char author[MAXAUTHOR_NAME];
float value;
}; /*必须使用分号标记结构体建立的结束*/
int main(void){
struct book library; /*把变量library声明为一个book类型的变量*/
printf("Please enter the library title.\n");
s_gets(library.title, MAX_TITLE); /*访问结构体中的title部分*/
printf("Now enter the author.\n");
s_gets(library.author, MAXAUTHOR_NAME);
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");
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 = '\n'; /*在此处放置一个空字符*/
else
while(getchar()!= '\n')
continue; /*处理输入行中的剩余字符*/
}
return ret_val;
}
14.4 结构数组
声明结构数组
结构体标识符 结构体名字 结构体实例 struct book library[MAX_BOOKS]
标识结构数组的成员
在结构数组中,实例名后面的数组下标访问控制对应的是第一级目录,存放了书的本数,表示第几本书,其后属性后的数组下标访问控制对应的是书的价格,书的名字,或者作者的访问,可以访问字符串的第几个字符
对结构数组示例程序的讨论:
/*manybook.c -- 学习结构数组,设置栈大小*/
/*20210502 0939*/
#include <stdio.h>
#include <string.h>
#define MAX_TITLE 40
#define MAX_AUTHOR_NAME 40
#define MAX_BOOKS 100
char *s_gets(char *st, int n);
struct book{
char title[MAX_TITLE];
char author[MAX_AUTHOR_NAME];
float value;
};
int main(void){
struct book library[MAX_BOOKS]; /*book 类型结构的数组*/
int count = 0;
int index;
printf("Please enter the book title.\n");
printf("Please [enter] at the start os a line to stop.\n");
while (count < MAX_BOOKS && s_gets(library[count].title, MAX_TITLE) != NULL && library[count].title[0] != '\0')
{
printf("Now enter the author.\n");
s_gets(library[count].author, MAX_AUTHOR_NAME);
printf("Now enter the value.\n");
scanf("%f", &library[count++].value);
while (getchar() != '\n'){
continue; /*清理输入行*/
}
if (count < MAX_BOOKS){
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;
}
14.5 嵌套结构
/*friend.c -- 演示嵌套结构*/
/*20210502 10:45*/
#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{ /*第一个结构*/
char first[LEN]; /*分别对应first name*/
char last[LEN]; /*分别对应last name*/
};
struct guy{ /*第二个结构*/
struct names handle; /*嵌套结构,引入第一个结构体,便于进一步描述人物的特征*/
char favoritefood[LEN];
char job[LEN];
double income; /*Clang-Tidy: Narrowing conversion from 'double' to 'float'*/
};
int main(void){
struct guy fellow ={ /*初始化一个结构*/
{"Daffodil", "Villard"}, /*人物的名字,有两个备选*/
"grilled salmon", /*一种食物*/
"bus driver", /*职业,个人教练*/
68112.00}; /*收入*/
printf("Dear %s, \n\n", fellow.handle.first); /*fellow 是第二个结构体,handle是第一个结构体*/
printf("%s%s.\n", msgs[0], fellow.handle.first); /*fellow是初始化后的guy结构体,也就是说再不被二次修改的情况下,调用该结构体的结果就是初始化的内容*/
printf("%s%s.\n", msgs[1], fellow.job); /*msgs是前面的文本数组,这里分别对每一行进行文本填充,而文本就来自于结构体*/
printf("%s\n", msgs[2]);
printf("%s%s%s", msgs[3], fellow.favoritefood, 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;
}
14.6.1 声明和初始化结构指针
使用结构体指针的优点
- 运算效率更高;
- 对内存地址的操作更加底层,实现更加趋近于CPU运算的本质;
程序代码
/*friends.c -- 使用指向结构的指针*/
/*20210502 17:27*/
#include <stdio.h>
#define LEN 20
struct names{ /*第一个结构体,定义一个人名字的两个组成部分*/
char first[LEN];
char last[LEN];
};
struct guy{ /*第二个结构体,调用并且实例化第一个结构体,定义一个人最喜欢的食物,工作和收入*/
struct names handle;
char favoritefood[LEN];
char job[LEN];
float income;
};
int main(void){
struct guy fellow[2] = { /*使用第二个结构体,并且为其生成两组默认的人物信息,该处使用数组*/
{{"Daffodil", "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]); /*打印fellow数组结构体中第一个元素和第二个元素的地址*/
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 -> favoritefood is %s: him -> handle.last is %s\n", him->favoritefood, him->handle.last);
return 0;
}
程序输出
用#2 - #1地址,得出的结果是54(十六进制),84(十进制),指的是每一个guy占用84个字节;
存在结构体大小大于各个成员大小之和,因为有些系统必须把每一个成员都放在偶数的地址,或者成四倍的地址上【系统对数据校准】,结构内部存在“缝隙”。
address #1: 0x7fffffffdcc0 #2: 0x7fffffffdd14
pointer #1: 0x7fffffffdcc0 #2: 0x7fffffffdd14
him -> income is $68112.00: (*him).income is $68112.00
him -> favoritefood is tripe: him -> handle.last is Swillbelly
对代码的理解
首先我们创建了一个结构体的指针—-等同于创建数组指针,将结构体实例数组的首元素赋值给该指针—-完成数组指针的创建,建立名称与数组元素的链接;
其次,【->】 运算符近似于【fellow.handle】运算符,(*him).income == fellow[0].income,必须使用括号进行解引用地址运算,因为前者的运算优先级低于后者;
最后,将数组首元素赋值给结构体指针时,必须使用&运算符,取地址;
14.7.1 向函数传递结构的信息
注意:结构体中属性的数据类型和将来要调入的函数形式参数类型要匹配
/*funds.c -- 把结构成员作为参数传递*/
/*20210502 1831*/
#include <stdio.h>
#define FUNDLEN 50
double sum(double, double); /*双精度浮点数求和*/
struct funds{
char bank[FUNDLEN];
double bankfund; /*结构体中属性的数据类型和将来要调入的函数形式参数类型要匹配*/
char save[FUNDLEN];
double savefund; /*结构体中属性的数据类型和将来要调入的函数形式参数类型要匹配*/
};
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 sum(double x, double y){
return x + y;
}
14.7.2 传递结构的地址
代码说明
子函数的函数声明必须放在结构体的后面,因为该子函数包含特定的结构体,子函数的形式参数就是特定的结构体,只不过传递的是指针形式的结构体实例对象,否则,就会导致结构体内部的属性对子函数不可见,从而导致实例化指针传参失败,进而导致向子函数传递的,取地址运算后的指针与结构体的数据类型不匹配,结构体的数据类型是int类型;
/*funds.c -- 向函数传递指向结构的指针*/
/*20210502 1851*/
#include <stdio.h>
#define FUNDLEN 50
struct funds{
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(const struct funds *money); /*传入后的参数是常量参数,意味着不能修改结构体原属性的数值*/
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); /*按照需要从结构体实例中提取属性进行运算*/
}
14.7.3 向函数传递结构体
代码说明
- 在调用子函数时,编译器根据子函数形式参数创造了一个结构体副本,然后套用结构体的初始化数据进行计算;
- 由于moolah是一个结构,所以使用英文句号,而不是【->】,更因为传递给子函数的是结构体而非指针;
/*funds.c -- 向函数传递一个结构*/
/*20210502 2009*/
#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); /*不是指针,直接使用英文句号调用,否则使用【->】*/
}
14.7.4 其他结构特征
如果使用字符字符指针来代替字符数组,容易造成程序崩溃,因为调用的数据是已有的,并为对原始属性进行初始化;
有如下代码
#define NAMELEN20struct names{ char firstname[NAMELEN]; char lastname[NAMELEN];};struct pnames{ char *first; char *last;};struct names veep = {"zhangsan", "lisi"};struct pnames treas = {"wangwu", "zhaoliu"};struct names accountant;struct names 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); /*未经初始化的变量*/
第一个版本代码说明
主函数存储了结构体的实例对象,这个实例对象相当于一个存储器,用来存储三个子函数调用和回传的数据(也就是结构体指定的属性值);
各个子函数,甚至是子函数和主函数之间通过指向结构体的指针完成数据的调用和存储;
/*names1.c -- 使用指向结构的指针*//*20210502 2029*/#include <stdio.h>#include <string.h>#define NAMELEN 30struct namect{ char firstname[NAMELEN]; char lastname[NAMELEN]; int letters;};void getinfo(struct namect *);void makeinfo(struct namect *);void showinfo(const struct namect *); /*注意:漏掉const关键字*/char *s_gets(char *st, int n);int main(void){ /*数据传递是以指针作为媒介,直接修改结构体实例对象的属性值*/ struct namect person; /*相当于一个socket,放在主函数中,用于各个子函数调用和回传数据*/ getinfo(&person); /*向主函数中的实例对象回传用户输入的数据*/ makeinfo(&person); /*通过指针获取getinfo函数回传给实例对象的属性值,然后通过计算将名字长度回传给person.letters*/ showinfo(&person); /*读取实例对象的属性值*/ return 0;}/*getinfo函数从用户输入中获取数据,通过pst指针将数据传入person结构体,分别传入姓和名*/void getinfo(struct namect *pst){ printf("Please enter your first name.\n"); s_gets(pst->firstname, NAMELEN); /*获取输入的姓*/ printf("Please enter your last name.\n"); s_gets(pst->lastname, NAMELEN); /*获取输入的名*/}/*makeinfo自运算姓和名的长度,并且通过pst指针【即结构体实例对象person的地址,在这里相当于一个rocket管道】将总长度传入结构体person*/void makeinfo(struct namect *pst){ pst->letters = strlen(pst->firstname) + strlen(pst->lastname);}void showinfo(const struct namect *pst){ /*注意:漏掉const关键字,否则会引起类型冲突*/ printf("%s %s, your name contains %d letters.\n", pst->firstname, pst->lastname, 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){ /*如果地址不是NULL*/ *find = '\0'; /*在此处放置一个空字符*/ } else{ while (getchar() != '\n'){ continue; } } } return ret_val;
第二个版本代码说明
该版本对数据的传递是通过复制结构体属性的方式
/*names2.c -- 传递并且返回结构*//*202100502 2144*/#include <stdio.h>#include <string.h>#define NAMELEN 30struct namect{ char firstname[NAMELEN]; char lastname[NAMELEN]; int letters;};struct namect getinfo(void); /*getinfo函数不需要传入参数,但是会返回值类型为结构体*/struct namect makeinfo(struct namect); /*makeinfo 函数需要传入一个结构体作为参数,同时返回值的类型也是一个结构体*/void showinfo(struct namect); /*showinfo 需要一个结构体作为参数,但是返回值为空*/char *s_gets(char *st, int n);int main(void){ struct namect person; /*创建一个结构体实例对象*/ person = getinfo(); /*将getinfo函数的返回值传递给实例对象person*/ person = makeinfo(person); /*将makeinfo函数的返回值传递给实例对象person*/ showinfo(person); /*showinfo函数读取实例对象person的参数*/ return 0;}struct namect getinfo(void){ /*该结构体没有分号标记是因为只使用一次*/ struct namect temp; printf("Please enter your first name.\n"); s_gets(temp.firstname, NAMELEN); printf("Please enter your last name.\n"); s_gets(temp.lastname, NAMELEN); return temp;}struct namect makeinfo(struct namect info){ /*该结构体没有分号标记是因为只使用一次*/ info.letters = strlen(info.firstname) + strlen(info.lastname); return info;}void showinfo(struct namect info){ printf("%s %s, your name contains %d letters.\n", info.firstname, info.lastname, 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'; } else{ while (getchar() != '\n'){ continue; } } } return ret_val;}
结构体在函数之间传递方式的选择
如果用指针传递结构体的属性,有以下优缺点:
- 兼容性强,老版本也可以使用;
- 使用指针,运算效率高;
如果使用结构体传递参数,有以下优缺点:
函数处理数据副本,保护了原始数据;
- 较老版本不兼容;
- 传递结构浪费时间和存储空间
14.7.7 结构,指针和malloc()
代码说明
这一个版本的代码在主函数和子函数之间传递的都是指针类型的数据,也是使用指针指定的结构访问结构体实例对象的属性值。
需要注意的是:在处理赋值的函数:getinfo()中使用了strcpy()函数从临时字符串中拷贝数据,在字符串获取或者说存储数据之前,首先调用子函数处理输入过程中的空白字符和换行;
/*names.c -- 使用指针和malloc*//*20210503 1019*/#include <stdio.h>#include <string.h> /*提供strcpy(), strlen()的原型*/#include <stdlib.h> /*提供malloc(), free()的原型*/#define SLEN 81struct namect{ char *firstname; /*使用指针作为属性*/ char *lastname; /*使用指针作为属性*/ int letters;};void getinfo(struct namect *); /*传入指针数据类型的结构体实例对象,返回值为空*/void makeinfo(struct namect *); /*传入指针数据类型的结构体实例对象, 返回值为空*/void showinfo(const struct namect *); /*无需修改,所以加上了const关键字,传入指针数据类型的结构体实例对象, 返回值为空*/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 last name.\n"); s_gets(temp, SLEN); /*函数对数组中临时存储对象进行处理,剔除换行符和空格*/ pst->firstname = (char *)malloc(strlen(temp) + 1); /*分配内存以便于动态存储名字,存储空间的数量就是临时数组的长度加1*/ strcpy(pst->firstname, temp); /*将临时字符串中的字符拷贝到姓指针上*/ printf("Please enter your last name.\n"); s_gets(temp, SLEN); pst->lastname = (char *)malloc(strlen(temp) + 1); /*分配内存以便动态存储姓名*/ strcpy(pst->lastname, temp); /*将临时字符串中的字符拷贝到姓指针上*/}void makeinfo(struct namect *pst){ pst->letters = strlen(pst->firstname) + strlen(pst->lastname); /*字符长度相加*/}void showinfo(const struct namect *pst){ printf("%s %s, your name contains %d letters.\n", pst->firstname, pst->lastname, pst->letters);}void cleanup(struct namect *pst){ /*分别释放firstname & lastname 的内存空间,malloc() & free()配合使用*/ free(pst->firstname); free(pst->lastname);}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; /*returning ‘char *’ from a function with return type ‘char’ makes integer from pointer without a cast [-Wint-conversion]*/}
14.7.8 复合字面量
代码说明
复合字面量是一种临时的结构量,首先对结构体实例化,产生一个实例化对象,然后再对其进行属性值的完全赋值,优点是可以对实例化对象反复赋值。这里的实例化对象就相当于一张可以擦写的RAM;
复合字面量具有和普通实例化结构体一样的性质,既可以作为函数的参数进行传递,也可以传递整个结构体的地址;
复合字面量在所有函数的外部,则具有静态存储期,在代码块内部则具有自动存储期;
同时,还可以使用指定初始化器;
/*complit.c -- 复合字面量*//*20210503 1215*/#include <stdio.h>#define MAX_TITLE 41#define MAX_AUTHOR 31struct book{ char title[MAX_TITLE]; char author[MAX_AUTHOR]; 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", "Tom Jack", 11.25 }; } else{ readfirst = (struct book){ /*使用复合字面量,实例化后,使用实例对象对属性值的完全赋值,可以造多个*/ "Nice Hat of Mr.Bouncy,", "Fred Winsome", 5.99 }; } printf("Your assigned reading:\n"); printf("%s by %s: $%.2f\n", readfirst.title, readfirst.author, readfirst.value); return 0;}
14.7.9 伸缩型数组成员
代码说明
伸缩型数组成员的名字,应该来源其内存申请方式,在予其内存空间的过程中,是给整个结构体一份空间,然后单独给结构体中的数组一份内存空间,这样就使得处于结构体内部的数组属性拥有更大的灵活性;
规则学习
- 伸缩型数组成员必须是结构中最后一个成员;
- 结构中必须至少有一个成员;
- 数组成员方括号为空,也就是不预设数组长度;
注意事项
- 不能直接对伸缩型结构进行赋值或者拷贝;
- 不能按值传递伸缩型结构体;
- 不能将其作为一个数组的嵌套数组和另外一个结构的嵌套结构;
/*flexible_members.c -- 伸缩型数组成员*//*20210503 1335*/#include <stdio.h>#include <stdlib.h>struct flexibility{ size_t count; /*typedef unsigned long size_t*/ double average; double scores[]; /*任意大小的空数组*/};void showFlexibility(const struct flexibility *p); /*声明函数原型,返回值为空,形式参数为结构体指针*/int main(void){ struct flexibility *pf1, *pf2; /*分别生成一个结构体指针*/ int n = 5; int i; int total = 0; pf1 = malloc(sizeof(struct flexibility) + n * sizeof(double)); /*为结构和数组分配存储空间*/ pf1->count = n; /*对于结构体指针中的计数属性直接传递数值*/ for (i = 0; i < n; i++){ pf1->scores[i] = 20.0 - i; /*对于结构体指针中的数组:分数属性则使用遍历传递多个数值*/ total += pf1->scores[i]; /*total相对于结构体来说是一个外部变量,但是对于所有数组元素和的计算依然依赖遍历*/ } pf1->average = total / n; /*对于结构体中平均数属性,只需要一次计算即可的出正确的值,所以无需放置在遍历内部*/ showFlexibility(pf1); n = 9; total = 0; pf2 = malloc(sizeof(struct flexibility) + n * sizeof(double)); pf2->count = n; for (i = 0; i < n; i ++){ pf2->scores[i] = 20.0 - i / 2.0; total += pf2->scores[i]; } pf2->average = total / n; showFlexibility(pf2); free(pf1); free(pf2); return 0;}void showFlexibility(const struct flexibility *p){ int i; printf("Score:"); for (i = 0; i < p->count; i++){ printf("%g ", p->scores[i]); /*注意此处打印有一个空格,否则所有数字会连在一起*/ } printf("\nAverage:%g\n", p->average);}
14.7.10 匿名结构
代码比较
struct names{ char first[20]; char last[20];};struct person{ int id; struct names name;}; /*当我们调用内部结构体属性时,使用这样的代码:person.name.first*/struct person{ int id; struct{char fist[20]; char last[20];};}; /*当我们在匿名结构中调用内部结构体属性时,使用这样的代码: person.first*/
结论
匿名结构用于嵌套结构体中;
优点在于,调用的时候无需穿层,因为内部结构体没有名字;
14.7.11使用结构数组的函数
代码说明
结构体数组的使用和普通数组并无差异;
/*funds4.c -- 把结构数组传递给函数*//*20210503 1455*/#include <stdio.h>#define FUNDLEN 50#define N 2struct funds{ /*该结构中有两个字符型数组属性,和两个双精度浮点型属性*/ char bank[FUNDLEN]; double bankfund; char save[FUNDLEN]; double savefund;};/*结构体数组名字的地址也是数组首元素的地址,所以有以下等价式: money = &jones[0]*/double sum(const struct funds money[], int n); /*向该函数传递的是结构体数组以及数组元素的个数【也是遍历次数】,从1开始计算个数*/int main(void){ struct funds jones[N] = { {"Garlic-Melon Bank", 4032.27, "Savings and Loan of Lucky", 8543.94}, { "Bank of Honest Hack", 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){ /*注意限定修饰符,不可修改结构体数组的数值*/ double total; int i; for (i = 0, total = 0; i < n; i++){ total += money[i].bankfund + money[i].savefund; /*结构体俩属性值直接相加,由于是累加,需要使用遍历*/ }; return (total);}
14.8将结构内容保存到文件当中
描述
整个结构称为记录,单独属性称为字段;
检索需要确定字段的开始位置和结束位置,可以通过固定字段宽度格式来解决,然而却不是最优解;
二进制存储方式可能因操作系统而异,很可能不具有移植性;
18.1保存结构的程序示例
代码说明
该程序,包括注释在内共有94行,是目前为止最复杂的C程序;
不过不要紧,学习到后面,知识逐渐综合起来,一份程序会使用多个语法结构;
特别值的注意的是:书写代码的时候,边写代码边写注释,这次代码就是血的教训,虽然看起来只有几十行代码,但是由于结构比较复杂,虽然和python相比使用花括号主动控制代码块,但是多起来之后缩进是一个大问题;
跨行太多,识别非常困难,如果一个位置出错,会引起关联错误,查找的时候只能从头开始,那么这份代码这次读了三遍;
/*booksave.c -- 将结构体存储到文件当中*//*20210503 1533*/#include <stdio.h>#include <stdlib.h>#include <string.h>#define MAX_TITLE 40#define MAX_AUTHOR 40#define MAX_BOOKS 10char *s_gets(char *st, int n);struct book{ char title[MAX_TITLE]; char author[MAX_AUTHOR]; float value;};int main(void){ struct book library[MAX_BOOKS]; int count = 0; int index, filecount; FILE *pointerbooks; /*typedef struct _IO_FILE FILE.The opaque type of streams. This is the definition used elsewhere.*/ int size = sizeof(struct book); if ((pointerbooks = fopen("book.dat", "a+b")) == NULL) /*a+部分允许程序读取整个文件并且在末尾添加内容, b 是ANSI的一种标识方法,表示程序将使用二进制文件格式*/ { /*选择二进制是因为fread() & fwrite()函数要使用二进制文件*/ fputs("Can't open book.dat file\n", stderr); exit(1); } rewind(pointerbooks); /*撤销指针*/ while (count < MAX_BOOKS && fread(&library[count], size, 1, pointerbooks) == 1) /*每次把一个结构读到结构数组中,当数组已满或读完文件时停止*/ { /*对条件的解读:fread()中的第一个参数是一个定位指针,定位到结构变量开始的位置,size是拷贝的大小,1表明一次拷贝一块结构体,pointerbooks是目标位置*/ 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 == MAX_BOOKS) { fputs("The book.dat file is full.", stderr); exit(2); } puts("Please add new book titles."); puts("Please [enter] at the start of a line to stop."); while (count < MAX_BOOKS && s_gets(library[count].title, MAX_TITLE) != NULL && library[count].title[0] != '\0') /*提示用户输入并且接受输入*/ { /*循环开始时,count变量的值是第一个循环结束后的值,该循环将新输入的项添加到数组的末尾*/ puts("Now enter the author."); s_gets(library[count].author, MAX_AUTHOR); puts("Now enter the value."); scanf("%f", &library[count++].value); while (getchar() != '\n') continue; if (count < MAX_BOOKS) { 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[count].title, library[count].author, library[count].value); } /*调用fwrite()将结构大小的块写入文件,由于&library[filecount]是数组中第一个新结构的地址,所以拷贝从这里开始*/ fwrite(&library[filecount], size, count - filecount, pointerbooks); /*count - filecount求值得到新添加的书籍数量*/ else { puts("No books? Too bad.\n"); } puts("Bye.\n"); fclose(pointerbooks); 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;}
14.10联合简介
概念
结构是多对多,可以用指针多对一,但联合只能多对一,类似于集线器,最终只能连接到电脑的一个插口,且同时只能有一个输入,在联合中,不管有多少个数据结构,同时只能用一个,也可以把联合理解为一个选择器;
初始化
- 把一个联合初始化为另外一个联合;
- 初始化联合的第一个元素;
- 使用指定初始化器;
union hold valD = {.bigfl = 118.2};
特点
- 点运算符和【->】使用情形与结构一致;
- 用法:用一个成员存储值,用另外i一个成员查看内容;在结构中存储从属关系;
- 匿名联合与匿名结构方法一致;
14.11 枚举类型
特性
- 只要能使用int类型就可以使用枚举类型;
- 枚举的实例化使得其成为了一个形式上的数组,其中包含多个枚举符;
- C允许枚举符,C++不允许,为了兼容,可将其改为int类型;
- 只要能使用整型常量,就能使用枚举常量;
- 枚举常量表示数组的大小,在switch语句中,可以把枚举常量作为标签;
- 可以为枚举常量指定整数值,若只给一个枚举常量赋值,则其后枚举常量自动递增;
代码说明
/*enum.c -- 使用枚举类型的值*//*20210503 2046*/#include <stdio.h>#include <string.h> /*提供strcmp(), strchr()函数的原型*/#include <stdbool.h> /*C99特性*/char *s_gets(char *st, int n); /*形式参数为指针和一个int类型的数字*/enum spectrum /*声明枚举类型*/{ red, orange, yellow, green, blue, violet};const char *colors[] = {"red", "orange", "yellow", "green", "blue", "violet"}; /*声明不可修改的字符类型的颜色常量数组*/#define LEN 30int main(void){ char choice[LEN]; /*声明choice数组*/ enum spectrum color; /*声明枚举实例对象color*/ bool color_is_found = false; /*声明布尔值变量,且初初始值为false*/ puts("Enter a color (empty line to quit):"); /*建立提示输入的语句*/ while (s_gets(choice, LEN) != NULL && choice[0] != '\0'){ for (color = red; color <= violet; color++) /*枚举类型每一个对象的数据类型都是int,并且默认递增*/ { if (strcmp(choice, colors[color]) == 0){ color_is_found = true; break; } } if (color_is_found){ switch (color){ /*选择结构,满足其中一个例子,则输出对应的值*/ case red: puts("Rose 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("Sky is 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){ *find = '\0'; } else{ while (getchar() != '\n') continue; } } return ret_val;}
程序输出(2021-05-03程序14.15enum.c输出结果 .png)
14.11.5共享名称空间
C语言通过名称空间(namespace)标识程序中各个部分,即通过名称来识别程序部件;
作用域是名称空间概念的一部分;
名称空间分类别,在特定作用域下,结构标记,联合标记和枚举标记都共享相同的名称空间;
该名称空间与普通变量使用的名称空间不一样;
例如:二者不会冲突
struct rect{double x; double y;};int rect;
14.12 typedef简介
比较
typedef创建的符号只受限于类型,不能用于值(类型标识符)
也就是说用在函数定义是合理的,但是不能作为变量的数值
typedef是由编译器解释,不是预处理器(可以简单理解为,给数据类型起一个别名)
在其受限范围内,typedef比#define更灵活
typedef并没有创建新的数据类型,仅仅是为已经存在的数据类型增加一个方便使用的标签
特点
作用域取决于typedef定义所处的位置,在函数中为局部作用域,函数外面则为文件作用域
在所有函数外定义的标识符是全局标识符,全局标识符的作用域就是文件作用域,表示从创建开始到文件结束都是可见的
通常用大写字母表示被定义的名称,提醒用户该类型名实际是一个符号的缩写【英语中用大写字母表示专有名词】
14.13其他复杂的声明
识别复杂声明的技巧—-特指指针和数组的混合使用
数组名后面的[]和函数名后面的()有相同的优先级,均比解引用运算符的优先级高
读写和理解的时候,从右往左
14.14函数和指针
定义
函数指针存储着指向函数代码起始处的指针
如何创建函数指针?
将函数名替换为pf,比如 void (**pf)(char )