目录
一.结构体
1.1 结构体的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。结构体是为了描述一些糅杂多种类型变量的复杂对象而产生的。
1.2 结构的声明
例如描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
要描述一个学生有很多需要进行描述,所以这是一个结构体。
1.3 特殊声明
看以下代码:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20],*p;
p = &x;//这样的代码合法吗?
虽然说两个结构体的成员和位置一模一样,但是p=&x这样的操作是不合法的。
1.4 结构体的自引用
在一个结构体包含一个类型为结构本身的成员是否可以呢?
struct Node
{
int data;
struct Node next;
};
//这样的代码合理吗?
这样的结构体是否合理呢?如果你认为合理,那么可以计算一下,sizeof(struct Node)的大小,你会发现,这是一个无限套娃,next里面还有next,这样结构体的空间就是不可知的,所以这样定义结构体是不合法的如果想自引用,那么应该保存它的地址,像这样:
struct Node
{
int data;
struct Node* next;
};
1.5 结构体变量的定义和初始化
struct Stu
{
char name[15];//名字
int age;//年龄
}p1;//p1 是变量,类型是struct Pointstruct Stu s = { "zhangsan",20 };//初始化
struct Node
{
int data;
struct Stu p1;
struct Node* next;
}n1 = { 10,{"zhangsan",20},NULL};//结构体嵌套初始化
1.6 结构体内存对齐(计算结构体的大小)
在这里我们可能有点疑惑,如果单纯按照字节来计算的话两个结构体应该都是6,但是它们的大小不仅不是6,而且它们虽然成员相同,但是大小也不一样。从这里我们可以推测结构体有其独特的内存管理,以及它的大小和成员的位置也有关系。
结构体内存对齐规则
1.第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
3.对齐数==编译器默认的一个对齐数(看编译器)与该成员大小的较小值--vs默认量是8
4.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
5.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍
如果按照这样一个对齐规则就可以明白上述编译器为什么会出现那样的结果。
首先看S1:
根据规则我们可以推出它的大小符合编译器的结果12.
同样我们可以分析一下S2:
通过这样的分析,我们就能发现它确实答案是如此,但是具体怎么验证呢?这我们可以借助一个宏offsetof(type,member),这个宏是计算偏移量的,当然我们也要引它的头文件
#include<stddef.h>.
验证:
结果恰如我们推测的那般。
嵌套结构体计算空间大小:
这个题目如果把S3的大小作为对齐数去计算的话,那么很明显S4的大小应该是4,而不是3.所以我们在这里要看一下对齐规则的最后一条
5.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍
所以这里遇到结构体的对齐数应该看这个结构体里面最大对齐数,而不是把结构体的大小当作对齐数。
1.7 结构体对齐数的设置
在vs编译器中默认对齐数是8,而在gcc中是没有默认对齐数的,那么我们有时可能会觉得这个对齐数有点大,浪费了不少空间,那么是否可以更改呢?
答案是肯定的。只不过我们在这里要用到一个预处理指令#pragma.
在修改之前,S的大小应该是12,而在修改后由于对齐规则对齐数==编译器默认的一个对齐数(看编译器)与该成员大小的较小值--vs默认量是8。而发生了改变。那么在这里我们需要注意一点
#pragma pack(num)之后,如果仅针对这一个结构体记得在该结构体后面再#pragma pack()复原为默认对齐数。这样不影响后面的结构体对齐。
如果仅仅从节省空间的角度来说,全部修改为1无疑是最节省空间的,但是系统并没有这么做,这是为什么呢?
1.8 内存对齐的优点
1.平台原因(移植原因)--不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能再某些地址处取某些特定类型的数据,比如0 4 8等偏移量处,否则就会抛出硬件异常。
2.性能原因---数据结构(尤其是栈)应该可能地在自然边界上对齐--原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次内存访问。所以这里是拿空间换时间。
1.9 结构体传参
在指针我们知道传参分为传值和传址,那么我们知道形参是要压栈的,对于较小的空间影响不大,但是一个结构体可能很大,这时对栈的需求很大,就可能会导致计算机运行收到影响,所以仅凭分析我们就知道,结构体适合传址。
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;
}
如果,仅仅是打印如上而不做修改,可以使用const修饰防止被修改,如果是更改,那么只能使用传址。
ps:我们在设计和计算结构体的时候需要注意什么?
笔者认为需要注意这些:
1、首先关注是否是嵌套结构体类型,如果是,那么需要注意最大对齐数的选取是内嵌的结构体的最大对齐数和默认对齐数的较小值。
2、注意在设计结构体的时候,我们要让占用空间小的成员尽量集中在一起,以此来尽可能地节省空间。
3、如果对于它的内存不清晰,可以画图分析一下。
二.位段
2.1 位段的介绍
位段的声明和结构体是大同小异的,仅有两个地方不一样:
1、位段的成员必须是int ,unsigned int 或signed int ,char等整型家族。
2、位段的成员名后面有一个冒号和一个数字。
比如:
我们可以看到,A的大小是8个字节,64bit,而不是47bit,近似6个字节,所以说明位段有它独特的内存开辟方式。
注意:冒号后面的数字表示几个bit,而不是字节。
2.2 位段的内存分配
2、位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式开辟的
3、位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
通过上述,我们发现位段是一次一个字节,或者4个字节开辟的。
我们再看以下代码:
在这里我们首先要注意冒号后面的数字(bit)不能大于它的类型。其次我们看到S的大小是3,如果把S里面的所有bit加起来恰好是16bit,也就是两个字节,但是出现了3,说明:
说明中间浪费了一个bit位,因为按照不浪费的形式是两个bit位就够了,但是放完_b之后只剩一个bit不够_c的5个bit,所以又开辟,但是那一个bit浪费了。
2.3 位段的截断
位段的类型是可以赋值的,但是由于我们设置了bit限制,所以这里对于如何处置这些值是不清楚的。我们可以分析以下代码:
struct S
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
//冒号后面的数字不能超过类型的大小
}s;
int main()
{
s._a = 10;
s._b = 12;
s._c = 3;
s._d = 4;
return 0;
}
很明显,这里的10转化为二进制是1010,需要4个bit位,而位段限制只有3个bit可以给,所以我们要深入到内存的存储来看一下vs是如何安排的。
我们可以调试验证一下是不是不这样。
的确如我们所推的那样,会发生截断。
2.4 位段的跨平台问题
1、int位段被当成有符号数还是无符号数是不确定的。
2、位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)。
3、位段中的成员在内存中从左向右分配,还是从右向左分配标识尚未定义。
4、当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,尚不确定。跟结构体相比,位段可以达到同样的效果,可以很好的节省空间,但是跨平台的问题存在。
2.5 使用位段需要注意的问题
1、冒号后面表示的是bit
2、冒号后面的数字(bit)不能超过类型的大小
3、如果不够,会直接新开辟,而不是会利用剩下的bit
4、位段开辟是以一次一个或者4个字节开辟的5、位段会发生截断。
三.枚举
3.1 枚举的介绍
枚举,就是列举,比如我们生活中星期一到星期日可以一一列举,365天的每一天也可以一一列举。
简单的使用:
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 = 1,
Green=2,
Blue=5,
yellow=7
};
这些都是可以的。
3.2 枚举的使用
3.3 枚举的优点
我们知道#define 可以定义,那么为什么还需要枚举呢?
1.增加代码的可读性和可维护性,比如我们在用switch语句设计分支时,如果用1,2,3,等等来设计选择,那么我们可能不知道意思是怎样的,如果用枚举来替代,代码可读性大大提高。
2.和#define定义的标识符比较,枚举是一种类型,有类型检查,更加严谨。
3.防止了命名污染。
4.便于调试。
5.使用方便,一次可以定义多个常量。
四.联合(共用体)
4.1 联合类型的定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,但是特征是这些成员公用同一块空间(所以联合也叫共用体)。
这里我们发现它的大小是4,我们知道如果是按照结构体来说,大小应该是8,说明联合体的内存管理不同于结构体。
4.2联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
通过这串代码,我们发现它们的地址也是一模一样。这样我们就可以猜想,联合是共用一块空间地址也是一样的。那么联合所占空间大小如何计算呢?
4.3 联合大小的计算
1、联合的大小至少是最大成员的大小。
2、当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
比如:
在这里可能有些人可能疑惑第一个联合体Un1中char c[5]不是大小为5个字节,对齐数为5,第二哥联合体Un2中short c[7]不是大小为14个字节,对齐数为14吗?这样的话大小分别应该是5和14啊。这里我们要说一下,这里的两个数组本质上分别是5个char和7个short。所以对齐数还是1和2.
4.4 联合的应用
某度面试题:
设计一段程序,判断当前机器是大端存储还是小端存储。
普通实现:
int check_sys()
{
int m = 1;
char n = (char)m;//强制转为char类型,如果是小端则
//返回1,大端则返回0
return n;
}
int main()
{
int ret=check_sys();
if (ret == 1)
{
printf("小端存储\n");
}
else
printf("大端存储\n");
return 0;
}
联合体实现:
主要需要诸位注意的是遇到数组怎么判断对齐数。
好了,今天的分享就到这里了,感谢🌹