19 自定义类型:结构体、联合体、枚举

目录

一、结构体

(一)结构体类型的定义

(二)结构体变量的创建和初始化

1、结构体变量的创建

(1)定义完结构体后再创建变量

(2)在定义结构体的同时创建变量

(3)匿名结构体变量创建

2、结构体变量的初始化

(三)结构成员的引用

(四)结构的自引用

(五)结构体内存对齐

1、对齐规则

2、练习

3、为什么存在对齐

4、优化

5、修改默认对齐数

(六)结构体传参

(七)结构体实现位段

1、位段的定义

2、位段的内存分配

3、位段的跨平台问题

4、位段的使用

5、位段使用的注意事项

二、联合

(一)联合体类型的定义

(二)联合体的特点

(三)联合体大小的计算

(四)联合体的使用

1、使用例之商品展示

2、使用例之判断机器大小端

三、枚举

(一)枚举类型的定义

(二)枚举类型的优点

(三)枚举类型的使用


一、结构体

(一)结构体类型的定义

        定义一个结构体类型的一般形式:

        例如定义一个学生结构体类型:

struct Student
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};//分号不要漏

        注意:

        ① 结尾的分号不能漏写;

        ② 结构与数组的对比:
        相同点:都是一些值的集合,元素有一个或者多个;
        不同:数组中的元素类型相同,而结构中的元素类型可以是不同的。

(二)结构体变量的创建和初始化

1、结构体变量的创建

        结构体变量的创建有三种方法:

(1)定义完结构体后再创建变量

        如下演示:

struct Student
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};

int main()
{
	struct Student s1;//创建结构体变量

	return 0;
}
(2)在定义结构体的同时创建变量

        如下演示:

struct Student
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}s2, s3;

        一般形式为:

(3)匿名结构体变量创建

        如下演示:

struct
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}s4;

        一般形式如下:

        注意:

        匿名结构体(不完全定义)只能在声明时创建变量;

        两个结构体匿名的情况下,即使成员完全相等,编译器也会把这两个结构体当作是完全不同的;

        匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

2、结构体变量的初始化

        如下所示:

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};

int main()
{
	//按照结构体成员的顺序初始化 
	struct Stu s = { "张三", 20, "男", "20230818001" };
	printf("name: %s\n", s.name);
	printf("age : %d\n", s.age);
	printf("sex : %s\n", s.sex);
	printf("id : %s\n", s.id);

	//按照指定的顺序初始化 
	struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥" };
	printf("name: %s\n", s2.name);
	printf("age : %d\n", s2.age);
	printf("sex : %s\n", s2.sex);
	printf("id : %s\n", s2.id);
	return 0;
}

(三)结构成员的引用

        前面定义了结构体类型,也定义了某些变量为该类型的变量,接下来学习结构成员的引用。

        ① 不能将结构体变量作为一个整体进行引用,如上面定义的s1、s2,都是结构体变量但不能拿来直接用,只能对结构体变量中的各个成员分别引用。

        引用的方式为:

        例如,s.age 表示 s 变量中的 age 成员,可以对该成员直接赋值。看如下代码

s.age = 21;//将21赋给s变量中的成员age

        上面的 “ . ” 叫作结构体成员运算符,它的优先级非常高,与圆括号( )平级。

        ② 若结构体中嵌套结构体,要使用低级的结构体成员,就要使用 “ . ” 结构体成员运算符一级一级地向下找结构体变量。

        ③ 结构体中的成员又称为成员变量,可以直接接看作普通变量,像普通变量一样进行备种运算,如下代码:

s.age = s.age + s2.age;

        ④ 因为成员变量也相当于普通变量,所以它们也是有地址的,如下代码:

int* p = &s.age; 

        ⑤ 结构体指针中,使用“ ->” 对结构体成员进行引用

(四)结构的自引用

        要实现结构的自引用,就要在成员中包含同结构类型的指针,如下代码:

struct Node
{
	int data;
	struct Node* next;
};

(五)结构体内存对齐

        在计算结构体大小(占多少字节)的时候,就会涉及到结构体内存对齐的问题

1、对齐规则

开始:

        ① 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处;

中间:

        ② 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;

        ③ 对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值;

        -VS中默认对齐数为 8;

        - Linux中gcc没有默认对齐数,对齐数就是成员自身大小。

        ④ 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

结尾:

        ⑤ 结构体总大小为最大对齐数(结构体中每个成员变量通过与默认对齐数比较后,都有⼀个对齐数,选出所有对齐数中最大的)的整数倍;

2、练习

//练习1 
struct S1
{
	char c1;
	int i;
	char c2;
};
printf("%d\n", sizeof(struct S1));
//练习2 
struct S2
{
	char c1;
	char c2;
	int i;
};
printf("%d\n", sizeof(struct S2));
//练习3 
struct S3
{
	double d;
	char c;
	int i;
};
printf("%d\n", sizeof(struct S3));
//练习4-结构体嵌套问题 
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
printf("%d\n", sizeof(struct S4));

        结果如下:

3、为什么存在对齐

        大部分的参考资料都是这样说的:

        ① 平台原因(移植原因):

        不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

        ② 性能原因:

        数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。

        假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

        总体来说:结构体的内存对齐是拿空间来换取时间的做法。

4、优化

        既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起:

//例如: 
struct S1
{
	char c1;
	int i;
	char c2;
};//大小为12

struct S2
{
	char c1;
	char c2;
	int i;
};//大小为8

5、修改默认对齐数

        #pragma 这个预处理指令,可以改变编译器的默认对齐数。

#pragma pack(1)//设置默认对齐数为1 
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认 

int main()
{
	printf("%d\n", sizeof(struct S));
	return 0;
}

        结果为:6

(六)结构体传参

struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参 
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//结构体地址传参 
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	print1(s); //传结构体 
	print2(&s); //传地址 
	return 0;
}

        以上函数,print2更好,原因:

        函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

        结论:结构体传参的时候,需要传结构体的地址

(七)结构体实现位段

1、位段的定义

        位段的定义和结构是类似的,有两个不同:

        ① 位段成员必须是 int、unsigned int 或 signed int,在C99中位段成员的类型也可以选择其他类型;

        ② 位段的成员名后边有一个冒号和一个数字。

        例如:

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

        A就是一个位段类型。

        注意:

        位段中的位,指的是二进制位(bit位)

        成员后面的数字是指可以使用多少二进制位(bit位)

2、位段的内存分配

        ① 位段的成员可以是int、unsigned int、 signed int或者是char等类型;

        ② 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的;

        ③ 位段涉及很多不确定因素,位段不是跨平台的,注重可移植的程序应该避免使用位段。

        ④ 不确定因素:

        (①)给定了空间后,位段在空间内部是从左向右使用,还是从右向左使用,这个不确定;
        (②)当剩下的空间不足以放下下一个成员的时候,空间是浪费还是使用,不确定。

        例子:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

        上面代码空间开辟过程:

        ① 假设从右向左使用空间,当空间不够用时,直接开辟新空间;

        ② 因为是按照需要开辟空间的,首先先开辟 8 个比特位,空间从右向左的3位bit位存放s.a二进制截取的三位,然后存放s.b二进制截取的4位,剩下1位不够放s.c,直接开辟新的8个比特位空间,从右向左存放s.c二进制截取的5位,剩下的3位又不够存放s.d,所以再开辟8个比特位的空间,从右向左存放s.d二进制截取的4位;

        ③ 上面过程中总共开辟了3次8个比特位的空间,所以该位段的内存大小为3个字节。

        过程图示如下:

3、位段的跨平台问题

        ① int 位段被当成有符号数还是无符号数是不确定的。

        ② 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,若写成27,代码应用在16位机器会出问题)

        ③ 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。

        ④ 当⼀个结构包含两个位段,第⼆个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

        总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

4、位段的使用

        在网络协议中,经常使用位段。

5、位段使用的注意事项

        位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的;

        所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。

        如下所示:

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	struct A sa = { 0 };
	scanf("%d", &sa._b);//这是错误的 

	//正确的⽰范 
	int b = 0;
	scanf("%d", &b);
	sa._b = b;
	return 0;
}

二、联合

(一)联合体类型的定义

        像结构体⼀样,联合体也是由一个或者多个成员构成,这些成员可以不同的类型。

        但是编译器只为最大的成员分配足够的内存空间,联合体的特点是所有成员共用同一块内存空间,所以联合体也叫:共用体。

        给联合体其中一个成员赋值,其他成员的值也跟着变化。

        如下代码演示:

#include <stdio.h>
//联合类型的定义
union Un
{
	char c;
	int i;
};
int main()
{
	//联合变量的创建
	union Un un = { 0 };

	//计算连个变量的大小
	printf("%d\n", sizeof(un));

	return 0;
}

        结果为:4。

(二)联合体的特点

        联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

        联合体中的成员,同一时间只能使用一个,因为他们公用一个空间。

代码一:

#include <stdio.h>
//联合类型的定义
union Un
{
	char c;
	int i;
};
int main()
{
	//联合变量的创建
	union Un un = { 0 };

	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c));
	printf("%p\n", &un);

	return 0;
}

        输出的结果为:

代码二:

#include <stdio.h>
//联合类型的定义
union Un
{
	char c;
	int i;
};
int main()
{
	//联合变量的创建
	union Un un = { 0 };

	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);

	return 0;
}

        输出结果为:

        分析:代码1输出的三个地址一模一样,代码2的输出,我们发现将 i 的第4个字节的内容修改为55了,内存分布图如下:

        再对比一下相同成员的结构体和联合体的内存布局情况:

结构体:

struct S
{
    char c;
    int i;
};

struct S s = {0};

联合体:

union Un
{
	char c;
	int i;
};

union Un un = { 0 };

        内存布局图如下:左边是结构体,右边是联合体。

(三)联合体大小的计算

        ① 联合的大小至少是最大成员的大小。

        ② 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

        如下代码演示:

#include <stdio.h>

union Un1
{
	char c[5]; //大小为5,对齐数为1
	int i; //大小为4,对齐数为4
};//最大字节为5,最大对齐数为4,所以应该扩大到对齐数的倍数,最终大小为8


union Un2
{
	short c[7];//大小为14,对齐数为2
	int i;//大小为4,对齐数为4
};//最大字节为14,最大对齐数为4,所以应该扩大到对齐数的倍数,最终大小为16

int main()
{
	printf("%d\n", sizeof(union Un1));
	printf("%d\n", sizeof(union Un2));
	return 0;
}

        结果为:8 和 16。

(四)联合体的使用

1、使用例之商品展示

        使用联合体是可以节省空间的,例如:

        要搞一个活动,要上线一个礼品兑换单,礼品兑换单中有三种商品:图书、杯子、衬衫。

        每一种商品都有:库存量、价格、商品类型和商品类型相关的其他信息,相关信息如下:

图书:书名、作者、页数

杯子:设计

衬衫:设计、可选颜色、可选尺寸

        我们可以直接写出结构体来包括所有内容:

struct gift_list
{
	//公共属性
	int stock_number;//库存量
	double price; //定价
	int item_type;//商品类型

	//特殊属性
	char title[20];//书名
	char author[20];//作者
	int num_pages;//页数
	char design[30];//设计
	int colors;//颜色
	int sizes;//尺寸
};

        上述的结构其实设计的很简单,用起来也方便,但是结构的设计中包含了所有礼品的各种属性,这样使得结构体的大小就会偏大,比较浪费内存。因为对于礼品兑换单中的商品来说,只有部分属性信息 是常用的。比如:商品是图书,就不需要design、colors、sizes。

        所以我们就可以把公共属性单独写出来,剩余属于各种商品本身的属性使用联合体起来,这样就可以减少所需的内存空间,⼀定程度上节省了内存。

        如下代码所示:

struct gift_list
{
	//公共属性
	int stock_number;//库存量
	double price; //定价
	int item_type;//商品类型

	union
	{
		struct
		{
			char title[20];//书名
			char author[20];//作者
			int num_pages;//页数
		}book;

		struct
		{
			char design[30];//设计
		}mug;

		struct
		{
			char design[30];//设计
			int colors;//颜色
			int sizes;//尺寸
		}shirt;
	}item;
};

2、使用例之判断机器大小端

        如下代码可判断当前机器的大小端

int main()
{
	union
	{
		int i;
		char a;
	}un;
	un.i = 1;
	printf("%d", un.a);

	return 0;
}

        如果打印出1,则是小端机器;如果打印出0,则是大端机器。

三、枚举

(一)枚举类型的定义

        枚举的关键字是:enum,是单词Enumerate的缩写,意思为列举、排列说明的意思。

        枚举顾名思义就是一一列举。

        把可能的取值一一列举。

        比如我们现实生活中:

        ① 一周的星期一到星期日是有限的7天,可以一一列举

        ② 性别有:男、女、保密,也可以一一列举

        ③ 月份有12个月,也可以一一列举

        ④ 三原色,也是可以意义列举

        入下代码所示:

enum Day//星期
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

enum Sex//性别
{
	MALE,
	FEMALE,
	SECRET
};

enum color//颜色
{
	RED,
	GREEN,
	BLUE
};

        上面定义的enum Day、enum Sex、enum color都是枚举类型。

        { } 中的内容是枚举类型的可能取值,也叫枚举常量,枚举常量之间使用逗号隔开,最后一个枚举常量不加符号结尾。

        这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值,如下代码所示:

enum color//颜色
{
	RED = 2,
	GREEN = 6,
	BLUE = 10
};

(二)枚举类型的优点

        我们可以使用 #define 定义常量,为什么非要使用枚举?

        枚举的优点:

        ① 枚举可以让文字对应数字,增加代码的可读性和可维护性。

        ② 和 #define 定义的标识符比较,枚举有类型检查,更加严谨。

        ③ 便于调试,预处理阶段会删除 #define 定义的符号。

        ④ 使用方便,一次可以定义多个常量。

        ⑤ 枚举常量是遵循作用域规则的,枚举声明在函数内,只能在函数内使用。

(三)枚举类型的使用

        使用枚举常量给枚举变量赋值。

        如下代码所示:

enum color//颜色
{
	RED = 2,
	GREEN = 6,
	BLUE = 10
};

enum color clr = RED;//使用枚举常量给枚举变量赋值

        在C语言中,可以拿整数给枚举变量赋值,但是在c++是不行的,c++的类型检测比较严格。

 


        以上内容仅供分享,若有错误,请多指正

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值