9、自定义类型
结构
结构基础
建立结构声明
结构声明(structure declaration)描述了一个结构的组织布局。声明类似如下:
struct book
{
char title[40];
char author[20];
float value;
};
该声明描述了一个由两个字符数组和一个float 类型变量组成的结构。该声明并未创建实际的数据对象,只描述了该对象由什么组成。
首先是关键字struct,它表明跟在其后的是一个结构
后面是一个可选的标记(该例中是book)
再后面使用大括号括起来,记得使用 ; 结尾
在大括号中声明该结构有那些数据对象
定义结构变量
结构有两层含义。
- 一层含义是“结构布局(结构声明)”。结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。
- 下一步是创建一个结构变量,即是结构的另一层含义。
假设结构布局如下
struct book
{
char title[40];
char author[20];
float value;
};
那么程序中创建结构变量如下:
struct book library;
编译器执行这行代码便创建了一个结构变量library。编译器使用book 模板为该变量分配空间:一个内含40个元素的 char 数组、一个内含 20 个元素的 char 数组和一个 float 类型的变量。这些存储空间都与一个名称library结合在一起
在结构变量的声明中,struct book所起的作用相当于一般声明中的int或float。
例如,可以定义两个struct book类型的变量,或者甚至是指向struct book类型结构的指针:
struct book doyle, panshin, *ptbook;
初始化结构
初始化变量和数组如下:
int count = 0;
int fibo[3] = {1,2,3};
初始化结构与初始化数组的语法相似
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// 结构布局
struct book
{
char title[40];
char author[20];
float value;
};
int main()
{
// 初始化结构体变量library
struct book library = {
"道德经",
"老子",
18.8
};
return 0;
}
简而言之,我们使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔,最后用分号结尾。
为了让初始化项与结构中各成员的关联更加明显,我们让每个成员的初始化项独占一行。这样做只是为了提高代码的可读性,对编译器而言,只需要用逗号分隔各成员的初始化项即可。
初始化结构和类别存储期
注意:如果初始化静态存储期的变量,必须使用常量值。这同样适用于结构。
初始化一个静态存储期的结构,初始化列表中的值必须是常量表达式。如果是自动存储期,初始化列表中的值可以不是常量。
访问结构成员
结构类似一个超级数组,这个超级数组中的元素可以是 int 类型、 char 类型、float 类型等等。使用结构成员运算符:点( . )就可以访问结构中的成员。.title 、.author、 .value就是library这个超级数组的下标
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct book
{
char title[40];
char author[20];
float value;
};
int main()
{
struct book library = {
"道德经",
"老子",
18.8
};
// 使用点(.)运算符访问结构成员
printf("library.title = %s\n", library.title);
printf("library.author = %s\n", library.author);
printf("library.value = %f\n", library.value);
return 0;
}
虽然library是一个结构,但是 library.value 是一个float类型的变量,可以像使用其他float类型变量那样使用他
如果还有一个相同类型的结构变量,可以用相同的方法:
struct book bill, newt; printf("%s\n", bill.title); printf("%s\n", newt.title);
结构的初始化器
结构的指定初始化器使用点运算符和成员名标识指定的元素。例如:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct book
{
char title[40];
char author[20];
float value;
};
int main()
{
// 只初始化 book 结构的 value成员:
struct book gift = { .value = 18.8 };
// 可以按照任意顺序使用指定初始化器
struct book library = {
.value = 18.8,
.author = "老子",
.title = "道德经"
};
return 0;
}
对待成员的最后一次赋值才是实际获得的值,如下:
struct book library = {
.value = 18.8,
.author = "老子",
0.33
};
赋给 value 的值是0.25,因为它在结构声明中紧跟在author成员之后。新值0.25 取代了之前的18.8
结构数组
声明结构数组
声明结构数组和声明其他类型的数组类似
假设结构布局如下:
struct book
{
char title[40];
char author[20];
float value;
};
则结构数组声明如下:
struct book library[2];
以上代码把1ibrary 声明为一个内含 2 个元素的数组。数组的每个元素都是一个book 类型的数组。
因此,library[0]是第1个book类型的结构变量,library[1]是第2个book 类型的结构变量
数组名1ibrary本身不是结构名,它是一个数组名,该数组中的每个元素都是struct book类型的结构变量。
标识结构数组的成员
为了标识结构数组中的成员,可以采用访问单独结构的规则:在结构名后面加一个点运算符,在点运算符后面写上成员名。如下
// 结构布局
struct book
{
char title[40];
char author[20];
float value;
};
// 声明结构数组
struct book library[2];
// 标识结构数组的成员
library[0].value;
library[1].value;
library.value[2]; // 错误
嵌套结构
在一个结构中包含另一个结构就称为嵌套结构
创建嵌套结构
和声明int、float类型变量一样,在结构中写要嵌套的结构类型,再跟上变量名
示例如下
// 建立一个结构布局
struct people
{
char name[20];
int age;
};
// 再建立一个结构布局,里面嵌套了上面的那个结构
struct book
{
char title[40];
float price;
// 结构类型 struct people 加上变量名 author,在book结构中嵌套了另一个结构people
struct people author;
};
初始化嵌套结构
与多维数组的初始化类似,初始化嵌套的结构成员时,使用大括号括起来
示例如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct people
{
char name[20];
int age;
};
struct book
{
char title[40];
float price;
struct people author;
};
int main()
{
// 初始结构
struct book library = {
"道德经",
18.8,
// 在book结构中初始化people结构。使用大括号括起来
{"老子", 80}
};
return 0;
}
访问嵌套结构的成员
需要使用两次点运算符
示例如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct people
{
char name[20];
int age;
};
struct book
{
char title[40];
float price;
struct people author;
};
int main()
{
struct book library = {
"道德经",
18.8,
{"老子", 80}
};
printf("书名: %s\n", library.title);
printf("价格: %f\n", library.price);
// 需要使用两次点运算符才能访问到
printf("作者姓名: %s\n", library.author.name);
printf("作者年龄: %d\n", library.author.age);
return 0;
}
指向结构的指针
声明和初始化结构指针
声明结构指针很简单:struct guy *him;
首先是关键字 struct,其次是结构标记guy,然后是一个星号(*),其后跟着指针名。这个语法和其他指针声明一样。
该声明并未创建一个新的结构,但是指针 him现在可以指向任意现有的 guy 类型的结构。
例如,如果barney是一个guy类型的结构,可以这样写:him = &barney;
和数组不同的是,结构名并不是结构的地址,因此要在结构名前面加上&运算符。
示例如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
// guy类型结构布局
struct guy
{
...
};
int main()
{
// 声明guy类型的结构
struct guy barney;
// 声明指向guy类型的指针
struct guy* him;
// 指针可以指向任意现有的guy类型的结构
him = &barney;
return 0;
}
用指针访问成员
-
第一种方法,也是最常用的方法:使用 (->)运算符
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> struct book { char title[40]; char author[20]; float value; }; int main() { struct book barney = { .value = 18.8, .author = "老子", .title = "道德经" }; struct book* him; him = &barney; // 指针使用(->)运算符访问结构成员 printf("him->title = %s\t\tbarney.title = %s\n", him->title, barney.title); printf("him->author = %s\t\tbarney.author = %s\n", him->author, barney.author); printf("him->value = %f\t\tbarney.value = %f\n", him->value, barney.value); return 0; } // 运行结果 /* him->title = 道德经 barney.title = 道德经 him->author = 老子 barney.author = 老子 him->value = 18.799999 barney.value = 18.799999 */
-
第二种方法:先将指针解引用再使用点运算符访问成员。注意,解引用要用括号括起来,因为 * 运算符的优先级高于点运算符
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> struct book { char title[40]; char author[20]; float value; }; int main() { struct book barney = { .value = 18.8, .author = "老子", .title = "道德经" }; struct book* him; him = &barney; // 先将指针解引用再使用点运算符访问成员 printf("(*him).title = %s\t\tbarney.title = %s\n", (*him).title, barney.title); printf("(*him).author = %s\t\tbarney.author = %s\n", (*him).author, barney.author); printf("(*him).value = %f\t\tbarney.value = %f\n", (*him).value, barney.value); return 0; }
-
总结:如果him是指向book类型结构barney的指针,下面的关系恒成立
barney.title == (*him).title == him->title // 假设him == &barney
向函数传递结构
传递结构成员
只要结构成员是一个具有单个值的数据类型(即,int及其相关类型、char、float、double 或指针),便可把它作为参数传递给接受该特定类型的函数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct book
{
char title[40];
char author[20];
float value;
};
void print_price(float price)
{
printf("%f\n", price);
}
int main()
{
struct book barney = {
.value = 18.8,
.author = "老子",
.title = "道德经"
};
// 将结构体成员value传入函数
print_price(barney.value);
return 0;
}
print_price函数不知道也不关心实际的参数是否是结构的成员,他只要求传入的数据时float类型
如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址:
print_price(&barney.value);
传递结构(值传递)
直接把结构作为参数传递给函数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct book
{
char title[40];
char author[20];
float value;
};
// 函数要求传入book类型的结构
void print_book(struct book library)
{
printf("library.title = %s\n", library.title);
printf("library.author = %s\n", library.author);
printf("library.value = %f\n", library.value);
}
int main()
{
struct book barney = {
.value = 18.8,
.author = "老子",
.title = "道德经"
};
// 将book类型的结构barney传入函数
print_book(barney);
return 0;
}
调用 print_book()时,编译器根据 book 类型的结构布局,在新的内存中创建了一个自动变量library,library 的成员值与传入函数的barney成员的值一模一样
也就是说,使用值传递的方式,就是将传入的结构复制一份,然后再在函数中操作他,所以使用值传递的方式,被调函数修改结构中的值,不会影响主调函数中结构的值,因为操作的不是同一个内存
把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。
传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。
传递结构的地址
将结构的地址作为参数传递给函数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct book
{
char title[40];
char author[20];
float value;
};
// 函数要求传入book类型的结构地址
void print_price(struct book *library)
{
printf("被调函数:library->title = %s\n", library->title);
printf("被调函数:library->author = %s\n", library->author);
printf("被调函数:library->value = %f\n", library->value);
}
int main()
{
struct book barney = {
.value = 18.8,
.author = "老子",
.title = "道德经"
};
// 传入book类型的结构barney的地址
print_price(&barney);
return 0;
}
使用地址传递的方式,被调函数修改结构的值,主调函数结构中的值也会跟着修改,因为操作的都是同一个内存
把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快只需要传递一个地址。
缺点是无法保护数据。被调函数中的某些操作可能会意外影响原来结构中的数据。不过,ANSIC新增的 const 限定符解决了这个问题。
结构数组作为参数
类似于普通数组传参给函数,将int、float等等这些类型名换成结构的名称就行了
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct book
{
char title[40];
char author[20];
float price;
};
// 该函数需要结构数组作为参数。struct book结构类型,library[]表示是一个数组,其中library是一个变量名
void print_price(struct book library[])
{
printf("library[0].title = %s\n", library[0].title);
printf("library[0].author = %s\n", library[0].author);
printf("library[0].value = %f\n", library[0].price);
printf("library[1].title = %s\n", library[1].title);
printf("library[1].author = %s\n", library[1].author);
printf("library[1].value = %f\n", library[1].price);
}
int main()
{
// 创建结构体数组
struct book barney[2] =
{
{
"红楼梦",
"曹雪芹",
38.8
},
{
"三国演义",
"罗贯中",
28.8
}
};
// 因为数组名就是元素首地址,所以不需要使用取址符&
print_price(barney);
return 0;
}
也可以把数组中第一个结构的地址传给函数
print_price(&barney[0]); // 因为barney和 &barney[0]都是指向同一个地址
接收结构数组的函数原型也可以使用指针表示法
void print_price(struct book *library);
结构的其他特性
-
现在的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(包括 ANSIC),函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。
把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。
结构指针也允许这种双向通信,因此可以选择任一种方法来解决编程问题。
复合字面量和结构
C99的复合字面量特性可用于结构和数组。如果只需要一个临时结构值,复合字面量很好用。
例如,可以使用复合字面量创建一个数组作为函数的参数或赋给另一个结构。语法是把类型名放在圆括号中,后面紧跟一个用花括号括起来的初始化列表。
例如,下面是struct book类型的复合字面量:
(struct book){"The Idiot", "Fyodor Dostoyevsky", 6.99}
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct book
{
char title[40];
char author[20];
float value;
};
// 需要结构体参数
void print_price(struct book library)
{
printf("library.title = %s\n", library.title);
printf("library.author = %s\n", library.author);
printf("library.value = %f\n", library.value);
}
int main()
{
// 使用复合字面量为函数传参
print_price((struct book) { "The Idiot", "Fyodor Dostoyevsky", 6.99 });
return 0;
}
伸缩型数组成员
C99新增了一个特性:伸缩型数组成员(fexible arraymember),利用这项特性声明的结构,其最后一个数组成员具有一些特性。
- 第1个特性是,该数组不会立即存在。
- 第2个特性是,使用这个伸缩型数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样。
声明一个伸缩型数组成员有如下规则:
- 伸缩型数组成员必须是结构的最后一个成员;
- 结构中必须至少有一个成员:
- 伸缩数组的声明类似于普通数组,只是它的方括号中是空的。
使用示例:
struct flex
{
int count;
double average;
double scores[]; // 伸缩型数组成员
};
声明一个 struct flex 类型的结构变量时,不能用 scores 做任何事,因为没有给这个数组预留存储空间。
实际上,C99的意图并不是让你声明struct flex 类型的变量,而是希望你声明一个指向 struct flex类型的指针,然后用 ma11oc()来分配足够的空间,以储存structflex 类型结构的常规内容和伸缩型数组成员所需的额外空间。
例如,假设用scores 表示一个内含5个double 类型值的数组,可以这样做:
struct flex * pf; // 声明一个flex类型结构的指针 // 使用malloc函数为结构和数组分配空间 pf = malloc(sizeof(double) * 5 + sizeof(struct flex)); /* 现在有足够的存储空间储存count、average和一个内含5个double 类型值的数组。可以用指针Pf 访问这些成员 */ pf->count =5; //设置 count 成员 pf->scores[2]=18.5; //访问数组成员的一个元素
使用伸缩型数组成员要注意的地方:
-
第一,不能用结构进行赋值或拷贝:
struct flex *pf1, *pf2;//*pf1 和*pf2 都是结构 ... *pf2 = *pfl; //不要这样做
这样做只能拷贝除伸缩型数组成员以外的其他成员。确实要进行拷贝,应使用memcpy()函数
-
第二,不要以按值方式把这种结构传递给结构。原因与第一点相同,按值传递一个参数与赋值类似。要把结构的地址传递给函数。
-
第三,不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员。
匿名结构
匿名结构是一个没有名称的结构成员。为了理解他的工作原理,先看一下嵌套结构
struct names
{
char first[20];
char last[20];
};
struct person
{
int id;
struct names name; // person结构中嵌套names结构
};
// 初始化
struct person ted = {2333, {"DDDD", "GGGG"}};
// 访问names的成员值需要使用两次点运算符
ted.name.first;
在C11中,可以使用嵌套的匿名成员结构定义person
struct person
{
int id;
// 匿名结构。只用创建一个结构布局就可以完成结构嵌套
struct {
char first[20];
char last[20];
};
};
// 初始化的方式与不匿名一样
struct person ted = {2333, {"DDDD", "GGGG"}};
// 访问first和last时不需要使用两次点运算符了,直接将first和last当作时person的成员
ted.first;
ted.last;
// 当然,也可以把first和last直接作为person的成员,删除嵌套循环。匿名特性在嵌套联合中更加有用
把结构内容保存到文件
联合
联合基础
联合与结构的布局、声明、初始化、使用等等都极其相似
-
联合(union)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。
-
其典型的用法是,设计一种表,以储存既无规律、事先也不知道顺序的混合类型。
-
使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型。
-
创建联合和创建结构的方式相同,需要一个联合布局和联合变量,再将struct关键字改为union关键字即可。可以用一个步骤定义联合,也可以用联合标记分两步定义。
下面是一个带标记的联合布局与结构布局对比:
// 联合的布局
union hold{
int digit;
double bigfl;
char letter;
};
// 结构的布局
struct uct{
int digit;
double bigfl;
char letter;
};
根据以上形式声明的结构可以储存一个 int 类型加上一个 double 类型和一个 char 类型的值。
然而,声明的联合只能储存一个int 类型的值或一个 double 类型的值或一个 char 类型的值。
联合与结构类型变量对比
- 下面是与hold联合类型的相关变量:
union hold fit; // hold类型的联合变量
union hold save[10]; // hold类型的联合数组
union hold * pu; // 指向hold类型的联合指针
- 下面是与uct结构类型的相关变量:
struct uct ufit; // uct类型的结构变量
struct uct usave[10]; // uct类型的结构数组
struct uct * upu; // 指向uct类型的结构指针
联合与结构的初始化对比
需要注意的是,联合只能存储一个值,这与结构不同。
-
初始化联合,有三种方法:
第一种:初始化为另一个同类型的联合
// 创建一个hold类型的联合:valA,并赋值为'R' union hold valA; valA.letter = 'R'; union hold valB = valA; // 初始化为另一个同类型的联合
第二种:初始化联合的第一个元素
union hold valC = {88}; // 初始化联合的digit成员
第三种:根据C99标准,使用初始化器
union hold valD = {.bigfl = 118.2}; // 指定初始化器
-
初始化结构,也有三种方法
第一种:初始化为另一个同类型的结构
// 创建一个uct类型的结构:uvalA struct uct uvalA; uvalA.digit = 10; uvalA.bigfl = 12.34; uvalA.letter = 'U'; struct uct uvalB = uvalA; // 初始化为另一个同类型的结构
第二种:按默认顺序初始化结构
// 按默认顺序初始化结构 struct uct uvalC = { 10, 12.34, 'U' };
第三种:根据C99标准,使用初始化器,可以不按照顺序
// 使用初始化器 struct uct uvalD = { .letter = 'U', .bigfl = 12.34, .bigit = 10 };
联合的使用和结构对比
-
使用联合,访问成员也是使用点运算符,指针访问成员也使用间接运算符(->)
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> union hold { int digit; double bigfl; char letter; }; int main() { // 声明fit类型的联合变量 union hold fit; /* 使用点运算符访问成员 */ fit.digit = 11; // 此时联合存储的是int类型的值 printf("fit.digit = %d\n", fit.digit); fit.bigfl = 12.3; // 将联合存储的值改为double类型 printf("fit.bigfl = %f\n", fit.bigfl); // 联合一次只能存储一个值,再访问digit就是不确定的值,因为此时存储的是double类型的值 printf("fit.digit = %d\n", fit.digit); /* 联合指针使用(->)间接运算符访问成员 */ // 指向hold类型的联合指针 union hold* pu; pu = &fit; printf("pu->bigfl = %f\n", pu->bigfl); printf("(*pu).bigfl = %f\n", (*pu).bigfl); return 0; }
-
使用结构
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> struct uct { int digit; double bigfl; char letter; }; int main() { // 初始化结构 struct uct ufit ={ 10, 12.34, 'U' }; printf("ufit.digit = %d\n", ufit.digit); printf("ufit.bigfl = %f\n", ufit.bigfl); printf("ufit.letter = %c\n", ufit.letter); /* 结构指针 */ struct uct* upu; upu = &ufit; printf("upu->digit = %d\n", upu->digit); printf("upu->bigfl = %f\n", upu->bigfl); printf("upu->letter = %c\n", upu->letter); printf("(*upu).digit = %d\n", (*upu).digit); printf("(*upu).bigfl = %f\n", (*upu).bigfl); printf("(*upu).letter = %c\n", (*upu).letter); return 0; }
匿名联合
匿名联合和匿名结构的工作原理相同,即匿名联合是一个结构或联合的无名联合成员。
struct uct
{
int a;
union {char c;double d;}; // 匿名联合
};
例如,假设用一个结构表示一辆汽车。如果汽车属于驾驶者,就要用一个结构成员来描述这个所有者。如果汽车被租赁,那么需要一个成员来描述租赁公司。
-
不使用匿名联合的情况
// 描述汽车所有者 struct owner { char name[12]; ... }; // 描述租赁公司 struct lease_company { char headquarters[40]; ... }; union data { struct owner owncar; struct lease_company leasecar; }; // 汽车信息 struct car_data { char make[15]; int status; // 私有为0,租赁为1 union data owneriofo; ... };
假设flits是car_data类型的结构变量。如果 flits.status = 0 ,程序将使用 flits.ownerifo.owncar.name;如果 flits.status = 1 , 程序将使用 flits.ownerinfo.leasecar.headquarters。
-
使用匿名联合可以将程序修改为以下
// 描述汽车所有者 struct owner { char name[12]; ... }; // 描述租赁公司 struct lease_company { char headquarters[40]; ... }; // 汽车信息 struct car_data { char make[15]; int status; // 私有为0,租赁为1 union // 匿名联合 { struct owner owncar; struct lease_company leasecar; }; ... };
现在,如果flits是car_data类型的结构变量,可以用flits.owncar.name代替flits.ownerinfo.owncar.name。
枚举
枚举的语法及使用
-
可以用枚举类型(enumerated type)声明符号名称来表示整型常量。
-
使用enum关键字,可以创建一个新“类型”并指定他可具有的值(实际上,enum常量就是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)。
-
枚举类型的目的是提高程序的可读性。他的语法与结构类似
声明枚举
// 创建了spetrum作为标记名,允许把 enum spetrum作为一个类型名使用
enum spectrum
{
red,
orange,
yellow,
green,
blue,
violet
};
// 该声明将color作为enum spetrum 类型的变量
enum spectrum color;
第1个声明中花括号内的标识符枚举了spectrum 变量可能有的值。因此,color 可能的值是red、orange、yellow等。这些符号常量被称为枚举符(enumerator)。
使用枚举
color = blue;
if(color == yellow)
{
...
}
for(color = red; color <= violet; color++)
{
...
}
直接将枚举当作int类型使用即可。
枚举变量可以是任意整型,前提是该整型可以存储枚举常量。例如:spectrum的枚举范围是0~5,所以编译器可以用 unsingned char 来表示color变量
注意:C枚举的一些特性并不适用于C++。例如,C允许枚举变量使用++运算符,但是C++标准不允许。所以,如果编写的代码将来会并入C++程序,那么必须把上面例子中的color声明为 int 类型才能 C 和 C++都兼容。
enum常量
enum spectrum
{
red,
orange,
yellow,
green,
blue,
violet
};
上面的red、orange等等从技术层面看,他们都是int类型的常量。
printf("red = %d, orange = %d", red, orange);
// 输出结果如下:
/*
red = 0, orange = 1
*/
只要是能使用整型常量的地方就可以使用枚举常量。例如,在声明数组时,可以用枚举常量表示数组的大小;在switch语句中,可以把枚举常量作为标签。
枚举的默认值
默认情况下,枚举列表中的常量都按顺序依次赋值为0、1、2、3、。。。
enum kids {a, b, c, d};
// 默认情况下,a = 0,b = 1,c = 2,d = 3
赋值
在枚举声明中,可以为枚举常量指定整数值:
enum levels {a = 100, b = 4000, c = 500};
如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后续的常量会被赋予后续的值
enum felin {cat, lynx = 10, puma, tiger};
那么,cat的值是默认值:0;lynx、puma、tiger的值分别为:10、11、12
共享名称空间
C语言使用名称空间(namespace)标识程序中的各部分,即通过名称来识别。
作用域是名称空间概念的一部分:两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突。
名称空间是分类别的。在特定作用域中的结构标记、联合标记和枚举标记都共享相同的名称空间,该名称空间与普通变量使用的空间不同。
这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量。例如,在中,下面的代码不会产生冲突:
struct rect{double x;double y;};
int rect;//在C中不会产生冲突
尽管如此,以两种不同的方式使用相同的标识符会造成混乱。
另外,C++不允许这样做,因为它把标记名和变量名放在相同的名称空间中。
typedef
typedef 工具是一个高级数据特性,利用typedef 可以为某一类型自定义名称。这方面与#define类似,但是两者有3处不同:
- 与#define 不同,typedef创建的符号名只受限于类型,不能用于值。
- typedef由编译器解释,不是预处理器。
- 在其受限范围内,typedef比#define 更灵活。
使用typedef
typedef unsigned char u_int8;
u_int8 x = 0xff; // 等价于 unsigned char x = 0xff;
也就是说typedef可以给现有的数据类型起别名,上例就是将 unsigned char 起了个别名叫 u_int8,这样使用unsigned char和使用u_int8的作用一模一样
typedef与#define
typedef的一些特性与#define的功能重合,例如:
#define u_int8 unsigned char
typedef unsigned char u_int8;
预处理器也会用unsigned char 替换u_int8
但是typedef也有#define没有的功能
typedef char * STRING; // 编译器把STRING解释成字符指针的标识符
STRING name, sing; // 等价于:char * name, * sing; name和sign都是指针类型
#define STRING char *
STRING name, sign; // 等价于:char * name, sing; 只有name是指针类型
这里可以看出typedef和#define的区别,#define只是做了一个替换的工作,而typedef是给类型名起一个别名
typedef与结构
typedef struct complex{
float real;
float imag;
} COMPLEX;
// 然后就可以使用COMPLEX代替struct complex来声明结构变量
COMPLEX a = {
1.2,
1.5
};
用typedef命名一个结构类型时,可以省略该结构的标签
typedef struct{
float real;
float imag;
} COMPLEX;
COMPLEX a = {
1.2,
3.4
};
typedef常用于给复杂的类型命名
例如,下面的声明
typedef char(*FRPTC())[5];
把FRPTC声明为一个函数类型,该函数返回一个指针,该指针指向内含5个char 类型元素的数组
使用typedef时要记住,typedef并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签
通过结构、联合和typede,C提供了有效处理数据的工具和处理可移植数据的工具。
其他复杂声明
符号 | 含义 |
---|---|
* | 表示一个指针 |
() | 表示一个函数 |
[] | 表示一个数组 |
一些复杂声明的示例
int board[8][8]; // 声明一个内含int 数组的数组
int ** ptr; // 声明一个指向指针的指针,被指向的指针指向int
int * risks[10]; // 声明一个内含 10个元素的数组,每个元素都是一个指向 int 的指针
int (* rusks)[10]; // 声明一个指向数组的指针,该数组内含10个int 类型的值
int * oof[3][4]; // 声明一个3x4的二维数组,每个元素都是指向int的指针
int (* uuf)[3][4]; // 声明一个指向3x4二维数组的指针,该数组中内含int类型值
int (* uof[3])[4]; // 声明一个内含3个指针元素的数组,其中每个指针都指向一个内含4个int类型元素的数组
优先级
-
数组名后面的[]和函数名后面的()具有相同的优先级。他们比*(解引用运算符)的优先级高。因此下面声明的risk是一个指针数组,不是指向数组的指针:int *risks[10];
-
[]和()的优先级相同,由于都是从左往右结合,所以下面的声明中,在应用方括号之前,*先与rusks结合。因此rusks是一个指向数组的指针,该数组内含10个int类型的元素:int (*rusks)[10];
-
[]和()都是从左往右结合。因此下面声明的 goods是一个由12个内含50个int 类型值的数组组成的二维数组,不是一个有50个内含12个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 类型值的数组
p_arr5 p2; // p2 是一个指向数组的指针,该数组内含5个int 类型的值
arrp10 ap; // ap 是一个内含 10 个指针的数组,每个指针都指向一个内含5个int 类型值的数组
指向函数的指针
通常,函数指针常用作于另一个函数的参数,告诉函数要使用哪一个函数
-
函数的地址:指向函数的指针中存储着函数代码的起始地址
-
声明函数指针:声明一个数据指针时,必须声明指针所指向的数据类型。声明一个函数指针时,必须声明指针指向的函数类型。为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型。如下
// 函数声明 void ToUpper(char *); // 把字符串中的字符转换成大写字符
ToUpper()函数的类型是“带char*类型参数、返回类型是 void 的函数”。下面声明了一个指针pf指向该函数类型:
// 指向函数的指针pf void (*pf)(char*);
从该声明可以看出,第1对圆括号把*和pf括起来,表明pf是一个指向函数的指针。
因此,(*pf)是一个参数列表为(char *)、返回类型为 void的函数。
注意:把函数名 ToUpper 替换为表达式(*pf)是创建指向函数指针最简单的方式。所以,如果想声明一个指向某类型函数的指针,可以写出该函数的原型后把函数名替换成(*pf)形式的表达式,创建函数指针声明。
由于运算符优先级的规则,在声明函数指针时必须把*和指针名括起来。如果省略第1个圆括号会导致完全不同的情况
void *pf(char *);// pf 是一个返回字符指针的函数
总结:要声明一个指向特定类型函数的指针,可以先声明一个该类型的函数,然后把函数名替换成(*pf)形式的表达式。然后,Pf就成为指向该类型函数的指针。
-
函数指针赋值:在这种上下文中,函数名可以用于表示函数的地址:
// 函数声明 void ToUpper(char *); void ToLower(char *); int round(double); // 声明函数指针 void (*pf)(char *); // 函数指针赋值 pf = ToUpper; // 有效,ToUpper是该类型函数的地址 pf = ToLower; // 有效,ToLower是该类型函数的地址 pf = round; // 无效,round与指针类型不匹配 pf = ToLower(); // 无效,ToLower()不是地址,而且ToLower()的返回值是void类型
-
函数指针访问函数:有两种逻辑不一致的语法
// 函数声明 void ToUpper(char *); // 创建字符串 char mis[] = "abcd"; // 声明函数指针 void (*pf)(char *); // 语法一: (*pf)(mis); // 语法二: pf(mis);
这两种语法都合情合理。
- 语法一:由于pf 指向ToUpper 函数,那么 *pf 就相当于ToUpper函数,所以语法一的
(*pf)(mis);
与ToUpper(mis);
等价; - 语法二:由于函数名是指针,那么指针和函数名可以互换使用,所以
pf(mis);
和ToUpper(mis);
等价
历史的原因,贝尔实验室的C和 UNIX的开发者采用第1种形式,而伯克利的UNIX推广者却采用第2种形式。K&RC不允许第2种形式。但是,为了与现有代码兼容,ANSIC认为这两种形式等价。后续的标准也延续了这种矛盾的和谐。
- 语法一:由于pf 指向ToUpper 函数,那么 *pf 就相当于ToUpper函数,所以语法一的
-
使用函数指针:作为函数的参数是数据指针最常用的方法之一,函数指针也是如此。例如如下声明
void show(void (*pf)(char *), char *str);
他声明了两个形参:fp和str
- fp形参是一个函数指针,fp指向的函数要可以接受char * 类型的参数,且返回值为void;
- str指向一个char类型的值
简单使用示例
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> // 形参类型void、返回值类型void的函数1 void show1() { printf("111\n"); } // 形参类型void、返回值类型void的函数2 void show2() { printf("222\n"); } // 形参类型void、返回值类型void的函数3 void show3() { printf("333\n"); } // 参数需要形参为void、返回值为void的函数指针 void show(void (*pf)()) { // 调用函数指针指向的函数 pf(); } int main() { // 初始化函数指针,该函数指针指向形参为void、返回值为void的函数 void (*pf)() = show1; // 获取用户输入 int num = 0; printf("输入1~3的整数\n"); scanf("%d", &num); // 根据用户输入的值,给函数指针赋予不同的函数地址 switch (num) { case 1: pf = show1; break; case 2: pf = show2; break; case 3: pf = show3; break; } // 将函数指针传入函数 show(pf); return 0; }
-
typedef与函数指针
// 用V_FP_CHARP表示函数指针类型,且该函数指针的类型为:形参是char *,返回值是void的函数 typedef void (*V_FP_CHARP)(char *); // 等价于:void show(void (*pf)(char *), char *); void show (V_FP_SCHART fp, char *); // 该函数接受两个形参:函数指针fp和char * 类型 // 声明一个函数指针 V_PF_CHARP pfun; // 等价于:void(*pfun)(char *);
更复杂一点:声明一给函数指针的数组
V_PF_CHARP arpf[4] = {a, b, c, d}; // a、b、c、d都是void类型返回值、char *类型形参的函数 // 等价于: void (*pf[4])(char*) = { a,b,c,d };
整体展示:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> typedef void(*V_FP_CHARP)(char*); void show(V_FP_CHARP pf, char* str) // 等价于:void show(void (*pf)(char *), char* str) { } void a(char* str) { } void b(char* str) { } void c(char* str) { } void d(char* str) { } int main() { V_FP_CHARP pfun; // 等价于:void (*pfun)(char*); V_FP_CHARP arpf[4] = { a,b,c,d }; // 等价于void (*arpf[4])(char*) = { a,b,c,d }; return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY