目录
一. 前言
本小节跟小伙伴们分享结构体,枚举,联合体的知识,希望能给小伙伴带来收获。
二. 正文
2.1结构体
2.11结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构的声明
struct tag{member - list ;} variable - list ;
例如描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不能丢
特殊的声明(匿名声明)
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
值得注意的是:匿名的结构体仅创建时的变量有效,如:x, a[20],*p是匿名,后面无法再次使用匿名结构体来创建新的结构体变量。
2.12 结构体内存对齐(比较热门的考点)
接下来我们来深入了解一下:结构体内存大小
//计算出结构体内存大小
struct S1
{
char c1; //1字节
int i; // 4字节
char c2; // 1字节
};
// 结果: 12个字节
这是为什么呢?
这里我们来了解一下吧
结构体内存对齐规则:
- 1. 第一个成员在与结构体变量偏移量为0的地址处。
- 2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 。VS 中默认的值为 8,linux默认对齐数为本身,如:int 对齐数为4
- 3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
这里我们以上一个的例子来解析:
如何进行检测呢?
这里给大家介绍一个宏----offsetof, 他的功能就是返回结构体成员所在内存的偏移量。
头文件: #include<stddef.h>
运用:
#include<stddef.h>
#include<stdio.h>
struct S3
{
double d;// 对齐数 8
char c; // 对齐数1
int i; // 对齐数4
}s3;
int main()
{
printf("%u\n", offsetof(struct S4, c1)); // 0
printf("%u\n", offsetof(struct S4, s3)); // 8
printf("%u\n", offsetof(struct S4, d)); // 12
return 0;
}
上面这个例子只实践了前面三个原则,我们再来一个嵌套结构体的例子:
//练习4-结构体嵌套问题
struct S3
{
double d; // 对齐数 8
char c; // 对齐数1
int i; // 对齐数4
};
// S3 的大小: 16个字节
struct S4
{
char c1;
struct S3 s3; // 对齐数 8 , 16字节
double d; // 对齐数8
};
printf("%d\n", sizeof(struct S4));
// 结果: 32
如图:
2.13 为啥要结构体内存对齐?
其中大部分资料是这样解释的:
- 1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能 在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 2. 性能原因:
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。总体来说:结构体的内存对齐是拿 空间 来换取 时间 的做法。
那么我们如何做到减少空间的占用?
让占用空间小的成员尽量集中在一起。
2.14修改默认对齐数
之前我们见过了
#pragma
这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
如:
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1; // 1 8 -> 1
int i; // 4 8-> 4
char c2; // 1 8-> 1
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1) //设置默认对齐数为8
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
值得注意的是:#pragma pack() 修改的默认对齐数一般是 2 ^ n,很少会有奇数。
2.15 函数传参
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致
性能 的下降
。
结论:
结构体传参的时候,要传结构体的地址。
2.16 位段
结构体讲完就得讲讲结构体实现
位段
的能力。
什么是位段
位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int 、 unsigned int 或 signed int 。2. 位段的成员名后边有一个冒号和一个数字,表示的是所占bit位。
比如:
struct A
{
int _a:2; // 2 bit
int _b:5; // 5bit
int _c:10;
int _d:30;
}; // 问那 struct A的大小是多少?结果是 8个字节
位段的内存分配
1.
位段的成员可以是
int, unsigned int, signed int
或者是
char
(属于整形家族)类型
2.
位段的空间上是按照需要以
4
个字节(
int
)或者
1
个字节(
char
)的方式来开辟的。
3.
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
那么在内存中具体如何分配的呢?
//一个例子
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;
//空间是如何开辟的?
解析:
思路 :首先会将一个字节放满或者不够时,再创建1个字节空间。
这里要和之前的大小端字节序进行区分,这次的移动单位是bit,后者单位是byte。
2.17 位段存在的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。2. 位段中 最大位 的数目不能确定。( 16 位机器最大 16 , 32 位机器最大 32 ,写成 27 ,在 16 位机 器会出问题。3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
具体运用在网络协议中,感兴趣的同学可以了解了解。
总结:
跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
2.2 枚举
就是将某种情况可能的结果列举在一起,在使用的时候,直接用情况所对的数据。
如:
12个月就能 一,一列举
性别也可以一, 一列举
运用如下:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
{}
中的内容是枚举类型的可能取值,也叫 枚举常量
。
如果未初始化,默认从
0
开始,一次递增
1
,当然在定义的时候也可以赋初值。(注:同#define 修饰常量一样,数据放在常量区,一旦定义就
不允许修改
。)
如:
enum a
{
c, // 默认值为 0
b, // 1
k = 200, //赋值为200
z, // 由前+ 1,为 201
t // 同理 202
};
2. 21枚举的优点
- 为什么使用枚举?
我们可以使用
#define
定义常量,为什么非要使用枚举?
枚举的优点:
- 1. 增加代码的可读性和可维护性 (少几个#define)
- 2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
(在c++里面就会对 枚举常量赋值时 对右侧数据进行枚举判断,是否是枚举成员)
- 3. 防止了命名污染(可以通过枚举名进行限制范围,避免重命名)
- 4. 便于调试 (可以调试,找到对应的值,不像#define定义,预编译时就替换了,不好溯源)
- 5. 使用方便,一次可以定义多个常量
2.3 联合体
联合类型的定义
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员
公用同一块 空间
(所以联合也叫共用体)。 比如:
#include<stdio.h>
union p1
{
char i;
int z;
}a;
int main()
{
printf("%p\n", &(a.i));
printf("%p\n", &(a.z));
return 0;
}
结果:
联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为
联合至少得有能力保存最大的那个成员)。
- 联合体内存大小也要遵循结构体内存对齐原则。
面试题:试想一下如何利用联合的特点判断机器的大端与小端?
#include<stdio.h>
union p1
{
char i;
int z;
}a;
int main()
{
a.i = 5;
a.z = 1;
a.i == 1 ? printf("小端") : printf("大端");
return 0;
}
结果是小端,利用char i 只能读取首字节数据,可以判断出int z修改的值存在于首字节,所以机器为小端。
三. 结语
本小节到这里就结束了,感谢小伙伴的浏览,如果有什么建议欢迎在评论区评论。