5.自定义数据类型

参考资料:
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;
}

需要注意的两点是:

  1. 枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的(严格来说是 main() 函数内部),不能再定义与它们名字相同的变量。

  2. 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 为例,各个成员在内存中的分布如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5pb3VOp7-1580915265127)(img\1.jpg)]

成员 n、ch、m 在内存中“对齐”到一头,对 ch 赋值修改的是前一个字节,对 m 赋值修改的是前两个字节,对 n 赋值修改的是全部字节。也就是说,ch、m 会影响到 n 的一部分数据,而 n 会影响到 ch、m 的全部数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2kgD45vB-1580915265128)(img\2.jpg)]

匿名联合体

#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语言对位域的限制

  1. 不能对位域成员做取地址操作;
  2. 位域成员不能作为sizeof的操作数;
  3. 不能用对齐属性来修饰位域;
  4. 指定位域宽的常量值不能超过该类型可表示的范围。

错误用法

#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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值