参考资料:
C语言中文网:http://c.biancheng.net/
《C语言编程魔法书基于C11标准》
视频教程:C语音深入剖析班(国嵌 唐老师主讲)
枚举类型
C语言提供了一种枚举(Enum)类型,能够列出所有可能的取值,并给它们取一个名字。
枚举类型的定义形式为:
enum typeName{ valueName1, valueName2, valueName3, ...... };
enum
是一个新的关键字,专门用来定义枚举类型,这也是它在C语言中的唯一用途;typeName
是枚举类型的名字;valueName1, valueName2, valueName3, ......
是每个值对应的名字的列表。注意最后的;
不能少,以上这种声明枚举的形式可以完整地称为枚举说明符。其中,枚举标识符在C语言标准中又被称为枚举标签,枚举符列表由一个个枚举符构成,而一个个枚举符则是一个枚举常量,或带有一个常量表达式的枚举常量。
enum week; //声明一个week的枚举类型
例如,列出一个星期有几天:
enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun }; //定义一个week的枚举类型
可以看到,我们仅仅给出了名字,却没有给出名字对应的值,这是因为枚举值默认从 0 开始,往后逐个加 1(递增);也就是说,week 中的 Mon、Tues … Sun 对应的值分别为 0、1 … 6。
我们也可以给每个名字都指定一个值:
enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };
更为简单的方法是只给第一个名字指定值:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
这样枚举值就从 1 开始递增,跟上面的写法是等效的。
枚举是一种类型,通过它可以定义枚举变量:
enum week a, b, c;
也可以在定义枚举类型的同时定义变量:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c;
有了枚举变量,就可以把列表中的值赋给它:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };enum week a = Mon, b = Wed, c = Sat;
或者:
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;
【示例】
#include <stdio.h>
int main(){
enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;
// Tues = 2,wed = 3,Thurs = 4,Fri = 5,Sat = 6,Sun = 7
return 0;
}
需要注意的两点是:
-
枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。
-
Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。
枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
匿名枚举
#include <stdio.h>
int main()
{
enum
{
DICE_ONE = 1,
DICE_TWO,
DICE_THREE,
DICE_FOUR,
DICE_FIVE,
DICE_SIX
//定义了一个匿名枚举类型,并用它定义了一个全局变量g_dice
} g_dice;//不赋值的话那么g_dice就是0
printf("g_dice = %d\n",g_dice);
return 0;
}
结果:
g_dice = 0
不给枚举类型定义名字就是匿名枚举,这样做的优点就是不用想名字,缺点就是不能再使用这个类型的枚举,也就是说这个枚举类型只有变量g_dice
枚举的运算
#include <stdio.h>
#include <stdint.h>
int main()
{
enum
{
DICE_ONE = 1,
DICE_TWO,
DICE_THREE,
DICE_FOUR,
DICE_FIVE,
DICE_SIX
//定义了一个匿名枚举类型,并用它定义了一个全局变量g_dice
} g_dice;
//定义了一个名为LIGHT的枚举类型
static enum LIGHT
{
LIGHT_RED = -2, //-2
LIGHT_ORANGE, //-1
LIGHT_YELLOW = 1, //1
LIGHT_GREEN, //2
LIGHT_BLUE, //3
LIGHT_INDIGO = LIGHT_RED + -100, //-102
LIGHT_PURPLE //-101
//定义了一个景泰的enum LIGHT变量s_list
//并用LIGHT——BLUE枚举常量对其初始化
} s_light = LIGHT_BLUE;
//枚举变量也可以进行算术运算,尽管并不推荐这么做
g_dice += 2;
//一个枚举变量也能赋值给一个整数变量
int32_t a = g_dice;
//一个整数变量也能赋值给一个枚举变量,尽管并不推荐这么做。
//此时light变量不是任一其效的枚举常量值
enum LIGHT light = a;
printf("light is: %d\n",light);
return 0;
}
结果:
light is: 2
枚举的运算也就是对枚举变量中的元素进行运算,枚举变量中的元素虽然可以赋值,也可以运算,但并不推荐这么使用,容易出现未知的BUG
枚举元素的大小
#include <stdio.h>
#include <stdint.h>
int main()
{
enum NUMBER
{
DICE_ONE = 1,
DICE_TWO,
DICE_THREE,
DICE_FOUR,
DICE_FIVE,
DICE_SIX
//定义了一个匿名枚举类型,并用它定义了一个全局变量g_dice
} g_dice;
printf("%d,%d,%d,%d,%d,%d,%d,%d", sizeof(DICE_ONE), sizeof(DICE_TWO), sizeof(DICE_THREE), sizeof(DICE_FOUR), sizeof(DICE_FIVE),
sizeof(DICE_SIX), sizeof(int), sizeof(enum NUMBER));
return 0;
}
结果:
4,4,4,4,4,4,4,4
从结果可看出枚举元素的大小是和int类型的大小一样的,就连enum NUMBER
大小也与int一样,这也验证了枚举在编译阶段将名字替换成对应的值的结果,实际上在运行的时候都会变成对应的值,然后在计算sizeof()的时候就成了例如sizeof(1)
这样的结果,因为在C语言中常量整数在加载到了内存中某认的类型就是int类型,所以大小就为int的大小
强制类型转换
#include <stdio.h>
int main()
{
enum SOME_ENUM
{
SOME_ENUM1,
SOME_ENUM2,
SOME_ENUM3
}se = SOME_ENUM1;
//将(se + 2)的结果类型显示转换为enum SOME_ENUM类型
enum SOME_ENUM se2 = (enum SOME_ENUM)(se + 2);
printf("se2 = %d\n",se2);
return 0;
}
结果:
se2 = 2
枚举的强制类型转换和基本数据类型中的强制类型转换基本一致,如int转成long类型,这里只是把se
运算后的结果(int类型)转成se2
一致的类型进行赋值,不进行强制类型转换也是可以的,但可能会造成未知BUG,所以还是强制转换一下比较好
结构体(Struct)
在实际的编程过程中,我们往往还需要一组类型不同的数据,例如对于学生信息登记表,姓名为字符串,学号为整数,年龄为整数,所在的学习小组为字符,成绩为小数。
在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:
struct 结构体名{
结构体所包含的变量或数组
};
结构体是一种集合,它里面包含了多个变量,它们的类型可以相同,也可以不同,每个这样的变量都称为结构体的成员(Member)。请看下面的一个例子:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
};
stu 为结构体名,它包含了 5 个成员,分别是 name、num、age、group、score。结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
注意大括号后面的分号
;
不能少,这是一条完整的语句。
结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。
像 int、float、char 等是由C语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可以包含多个基本类型的数据,也可以包含其他的结构体,我们将它称为复杂数据类型或构造数据类型。
结构体变量
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
struct stu stu1, stu2;
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct
不能少。
stu 就像一个“模板”,定义出来的变量都具有相同的性质。也可以将结构体比作“图纸”,将结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的。
你也可以在定义结构体的同时定义结构体变量:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
将变量放在结构体定义的最后即可。
如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:
struct{ //没有写 stu
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。
成员的获取和赋值
结构体使用点号.
获取单个成员。获取结构体成员的一般格式为:
结构体变量名.成员名;
通过这种方式可以获取成员的值,也可以给成员赋值:
#include <stdio.h>
#include <stdlib.h>
int main()
{
struct {
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1;
//给结构体成员赋值
stu1.name = "Tom";
stu1.num = 12;
stu1.age = 18;
stu1.group = 'A';
stu1.score = 136.5;
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
system("pause");
return 0;
}
运行结构:
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!
除了可以对成员进行逐一赋值,也可以在定义时整体赋值,例如:
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1, stu2 = { "Tom", 12, 18, 'A', 136.5 };
不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值。
需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。
匿名结构体
#include <stdio.h>
#include <stdint.h>
int main(int argc, const char * argv[])
{
struct
{
int8_t a;
int16_t b;
int32_t c;
float d;
} s = {10,11,12,3.14}; //匿名结构体
printf("a = %d,b = %d, c = %d, d = %f\n",s.a,s.b,s.c,s.d);
return 0;
}
结果:
a = 10,b = 11, c = 12, d = 3.140000
匿名结构体也就是没有定义名字的结构体,后续就没办法再进行这个类型变量的定义或声明,只有s这个变量,这也很容易理解,因为结构体定义后就相当于定义了一个类型,然后只有定义了名字,才等于有一个新的类型,这样才能用这个类型去不断的定义新的变量。
强制类型转换
#include <stdio.h>
#include <stdint.h>
int main(int argc, const char * argv[])
{
struct s
{
int8_t a;
int16_t b;
int32_t c;
float d;
}; //匿名结构体
struct s test_a;
test_a = (struct s){10,20,30,3.14}; //强制类型转换
printf("a = %d , b = %d , c = %d ,d = %f\n",test_a.a,test_a.b,test_a.c,test_a.d);
return 0;
}
结果
a = 10 , b = 20 , c = 30 ,d = 3.140000
强制类型转换需要与结构体的结构相同(结构体中有多少个成员那么就有多少个数据),并且需要与结构体中的成员类型相同(也就是浮点数要对应浮点数,整数要对应整数)
联合体
在C语言中,还有另外一种和结构体非常类似的语法,叫做联合体(Union),它的定义格式为:
union 联合体名{成员列表};
结构体和联合体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而联合体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),联合体占用的内存等于最长的成员占用的内存。联合体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
联合体也是一种自定义类型,可以通过它来创建变量,例如:
union data{ int n; char ch; double f;};
union data a, b, c;
上面是先定义联合体,再创建变量,也可以在定义联合体的同时创建变量:
union data{ int n; char ch; double f;} a, b, c;
如果不再定义新的变量,也可以将联合体的名字省略:
union{ int n; char ch; double f;} a, b, c;
联合体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、b、c)也占用 8 个字节的内存,请看下面的演示:
#include <stdio.h>
union data{
int n;
char ch;
short m;
};
int main(){
union data a;
printf("%d, %d\n", sizeof(a), sizeof(union data) );
a.n = 0x40;
printf("%X, %c, %hX\n", a.n, a.ch, a.m); //联合体获取成员元素的方法与结构体一样
a.ch = '9';
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.m = 0x2059;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.n = 0x3E25AD54;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
return 0;
}
运行结果:
4,4
40, @, 40
39, 9, 39
2059, Y,2059
3E25AD54 ,T ,AD54
这段代码不但验证了联合体的长度,还说明联合体成员之间会相互影响,修改一个成员的值会影响其他成员。
要想理解上面的输出结果,弄清成员之间究竟是如何相互影响的,就得了解各个成员在内存中的分布。以上面的 data 为例,各个成员在内存中的分布如下:
成员 n、ch、m 在内存中“对齐”到一头,对 ch 赋值修改的是前一个字节,对 m 赋值修改的是前两个字节,对 n 赋值修改的是全部字节。也就是说,ch、m 会影响到 n 的一部分数据,而 n 会影响到 ch、m 的全部数据。
匿名联合体
#include <stdio.h>
int main(int argc, const char * argv[])
{
union {
char a;
short b;
int c;
} n; //匿名联合体
n.c = 65535;
printf("a = %d,b = %d,c = %d\n",n.a,n.b,n.c);
return 0;
}
结果:
a = -1,b = -1,c = 65535
匿名联合体也就是没有定义名字的联合体,后续就没办法再进行这个类型变量的定义或声明,只有n这个变量。
强制类型转换
#include <stdio.h>
int main(int argc, const char * argv[])
{
union n {
char a;
short b;
int c;
};
union n n1;
n1 = (union n)65535; //强制类型转换
printf("a = %d,b = %d,c = %d\n",n1.a,n1.b,n1.c);
return 0;
}
结果:
a = -1,b = -1,c = 65535
因为联合体所占的内存大小为最大类型(int)的内存大小,所以65535强制类型转换可以存放到联合体中
位域
位域是C语言比较独特的语法之一。通过位域,我们可以将一串比特流进行结构化描述,这在通信领域用得尤为广泛。位域是在结构体或联合体中指定位宽的成员。我们通常都用结构体来指定位域,尽管联合体也可以指定成员的位宽,但基本没有什么实际意义,因为联合体的成员都共享同一存储单元,
如果结构体中的某一成员要作为位域,那么它必须是一个整数类型(包括布尔和枚举类型),而不能是浮点或其它类型。指定该位域宽度的表达式也应该是一个整数常量表达式,且表达式的值不能是负数。位域的基本表达式为:
<类型> <标识符> : <位宽表达式>;
这里<位宽表达式>就是用于指定此位域的宽度(即取值范围),单位是比特。
struct BitField
{
int32_t a : 5; //a由5个比特构成,取值范围在[-16,15]
uint32_t b : 6; //b由6个比特构成,取值范围在[0,63]
int32_t c : 7; //c由7个比特构成,取值范围在[-64,63]
}
BitField的成员a、b和c都是位域。其中,a的宽度为5个⽐特,由于它是带符号整数,因此这5个⽐特中最⾼位需要作为符号位,因此其取值范围在[-2^4 ,2^4 -1]。成员b的宽度为6个⽐特,由于它是⼀个⽆
符号整型,因此其取值范围在[0,2^6 -1]。整个BitField类型⼤⼩为4个字节。由于a、b、c这3个成员的宽度加起来为18个⽐特,少于32⽐特,⼀般C语⾔实现会将其扩充到4个字节(正好是⼀个int32_t类型的宽度)
C语言对位域的限制
- 不能对位域成员做取地址操作;
- 位域成员不能作为sizeof的操作数;
- 不能用对齐属性来修饰位域;
- 指定位域宽的常量值不能超过该类型可表示的范围。
错误用法
#include <stdio.h>
#include <stdint.h>
int main(int argc, const char * argv[])
{
struct BitField
{
_Alignas(int32_t) int32_t a : 5; //这句报错!_Alignas属性不能用于位域
uint32_t b : 6;
char c :9; //这句报错!一个char类型对象最多只能由8个比特构成
}bf = {10,20};
int32_t * p = &bf.a; //这句报错!不能对位域做取地址操作
size_t size = sizeof(bf.b); //这句报错!sizeof操作符不能用于位域
}
实例
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
int main(int argc, const char * argv[])
{
enum MyEnum
{
ENUM1,
ENUM2,
ENUM3
};
struct MyStruct
{
int32_t a : 6; //a的范围为[-32,31]
int16_t b : 5; //b的范围为[-16,15]
int8_t c : 8; //c的范围为[-128,127]
char x : sizeof(enum MyEnum); //相当于: char x :4
bool y : 1;
enum MyEnum e : ENUM3; //相当于:enum MyEnum e : 2
}s = {0x18,0x0a,0x77,'\0',true,ENUM3};
//s的二进制表示:(0) 10 1 0000 0111011 (00000) 01010 011000
//整理后得:0101 0000 0111 0111 0000 0010 1001 1000
printf("The size is: %zu\n", sizeof(s));
//输出0x50770298
printf("The content of s is: 0x%.8x\n",*(uint32_t*)&s);
}
运行结果:
The size is: 12
The content of s is: 0x00000018
我们在main函数⾥定义了⼀个名为MyStruct
的结构体类型,其中的成员均为位域。我们看到,位域成员的类型可以是各类整数类型、字符类型、布尔类型以及枚举类型。⽽指定位域宽度的整数常量
表达式可以是整数字⾯量、sizeof
操作符所得到的结果、枚举值等,它们必须是在编译时能确定值的常量。
匿名位域
在C11标准中,位域可以是匿名的。当某个位域只给出其类型及宽度,⽽不给出标识符名时,该位域则称为匿名位域。匿名位域⼀般仅⽤作 ⽐特填充(通常C语⾔的实现都⽤0来填充),⽽不能被直接访问该填充区域。⽽当匿名位域的宽度为0时,则表⽰该位域是其前⾯的位域所组成的存储区域的末尾,即作为⼀个结束标志⽽使⽤。该存储区域的后续⽐特都将 被填充,其下⾯的位域都将作为⼀个新的存储区域。
例子
#include <stdio.h>
#include <stdint.h>
int main(int argc, const char * argv[])
{
struct
{
int32_t a : 8;
//这里使用了一个匿名位域,使得位域a与位域b之间空出8比特,
//并且这空出的8比特均用0来填充
int32_t : 8;
int32_t b : 16;
//这里分别给成员a和b进行初始化
//对象s仍然具有两个成员(a和b),中间填充部分无法范文
}s = {0x21,0x6543};
//这里输出:s is:0x65430021
printf("s is:0x%.8X\n",*(uint32_t*)&s);
struct
{
int32_t a : 8;
//这里使用了一个匿名位域,且宽度为0,
//这样位域a所在的整个区域后续都将被0填充,然后终结该区域
int32_t : 0;
//位域b将被安排在位域a的下一个存储区域,而不会跟a存放在同一区域
int32_t b : 16;
//下面的x与y都会与位域b存放在同一存储区域
int16_t x : 8;
int16_t y : 8;
//这里分别给成员a,b,x,和y进行初始化
}t = {0x10,0x4321,0x65,0x76};
//这里输出:t is: 0x0000432100000010
printf("t is: 0x%.16llX\n",*(uint64_t*)&t);
return 0;
}
运行结果
s is:0x65430021
t is: 0x0000432100000010