系列文章目录
速通C语言系列
速通C语言第一站 一篇博客带你初识C语言 http://t.csdn.cn/N57xl
速通C语言第二站 一篇博客带你搞定分支循环 http://t.csdn.cn/Uwn7W
速通C语言第三站 一篇博客带你搞定函数 http://t.csdn.cn/bfrUM
速通C语言第四站 一篇博客带你学会数组 http://t.csdn.cn/Ol3lz
速通C语言第五站 一篇博客带你详解操作符 http://t.csdn.cn/OOUBr
速通C语言第六站 一篇博客带你掌握指针初阶 http://t.csdn.cn/7ykR0
速通C语言第七站 一篇博客带你掌握数据的存储 http://t.csdn.cn/qkerU
速通C语言第八站 一篇博客带你掌握指针进阶 http://t.csdn.cn/m95FK
速通C语言第八.五站 指针进阶题目练习 http://t.csdn.cn/wWC2x
速通C语言第九站 字符相关函数及内存函数 http://t.csdn.cn/7rCu0
感谢佬们支持!
文章目录
- 系列文章目录
- 前言
- 一、结构体
- 1 结构体的特殊声明
- 2 结构体的自引用
- 补充:数据结构
- 3 结构体的内存对齐
- 4 offsetof
- 5 位段
- 位段的内存分配
- 位段的意义
- 二、枚举
- 1 结构
- 2 枚举的优点
- 三、联合体(共用体)
- 1 初始化
- 例:判断大小端
- 2 联合体大小的计算
- 总结
前言
上篇博客我们学习了字符相关及内存函数,这篇博客给大家带来自定义类型,其中的struct对之后数据结构和C++的学习会起到接轨的作用,而剩下两个大家只要了解即可
一、结构体
1 结构体的特殊声明
struct tag
{
member_list;//成员列表
}variable_list;//变量列表
我们在声明结构体时,可以进行不完全的声明,在原来的基础上,我们可以省略tag,进行所谓匿名结构体的声明。
例:
struct
{
char c;
int i;
char ch;
double d;
}s;
但是如果你用匿名结构体创建了一个指针ps,再用ps存s的地址
struct
{
char c;
int i;
char ch;
double d;
}*ps;
int main()
{
ps=&s;
}
这个时候编译器会报一个警告
因为虽然两个结构体成员相同,编译器仍然认为这是两个不同的类型,所以认为 ps=&s 不合理。
2 结构体的自引用
结构体的成员中可以包含结构体
例:
struct A
{
int i;
char c;
};
struct B
{
char c;
struct A sa;//结构体
double d;
};
在上面这个例子中,结构体B的一个成员就是由结构体A创建的变量。
这个时候我们再思考一下,一个结构体的成员能否是自己呢?
例:
struct N
{
int d;
struct N n;
};
int main()
{
struct N sn;
}
我们不妨从结构体的大小这个角度来看
虽然我们还没学如何计算,但是我们可以大致猜测一下
N中有两个成员,一个是int类型,他的大小是4,而另一个成员是结构体N,N的两个成员一个是int,一个又是N……
显然,这波是个套娃,我们永远也无法算出N的大小,所以可以得出
结构体的成员不能是该结构体
啊但是!
结构体的成员能否是该结构体的指针呢?
我们依然从结构体的大小这个角度来看
由于任何指针的大小均为4个字节,所以我们可以算出结构体的大小
所以结构体的成员可以是该结构体的指针
那么这个时候就要引申出一个很重要的东西了
补充:数据结构
我们在内存中存数据时,通常是用数组来存储,即连续存储,下面我们引出第二种方式 - > 链式存储
相比于顺序存储,链式存储每个节点的位置是随机的,每个节点由两部分组成,一部分叫数据域,用于存放数据,另一部分叫指针域,用于存放下一个节点的地址(指针)。
以结构体来看是这样的
struct Node
{
int data;
struct Node* next;
};
3 结构体的内存对齐
现在我们来着手计算一个结构体的大小,先不急着计算,我们先来看几个例子
例;
struct s
{
int i;
char c;
};
struct s s1 = { 0 };
printf("%d\n", sizeof(s1));
i是int类型,4个字节;c是char类型,1个字节,哪s1的大小是否是5给个字节呢?
运行一波
再给一个例子
struct s2
{
char c1;
int i;
char c2;
};
struct s2 s3 = { 0 };
printf("%d\n", sizeof(s3));
运行以后
所以可以得知,结构体的大小计算不是简单的加,这波其实涉及到了一个规则
内存对齐规则
1 第一个成员在与结构体变量偏移位置为0的位置存放
解释一波偏移量
结构体变量开辟后下面的第一个空间偏移量为0,再下面的一个偏移量为1,以此类推,如图
由于 c1为char类型,一个字节,所以我们直接把c1放到偏移量为0的位置
2 其他成员要对齐到某个数字(对齐数)的整数倍地址处。
对齐数=编译器默认的一个对齐数与该成员大小的较小值
(VS默认的对齐数为8,而Linux则没有这个概念,因为它直接就把自身的大小当对齐数了)
int为4,而默认对齐数为8,显然其较小者为4,所以下个成员应从4的整数倍地址开始存
再存c2,对齐数为1,所以直接往下存就行
3 结构体的总大小为最大对齐数(每个变量的对齐数的最大值)的整数倍
c1为1,i为4,c2为1,所以其最大值为4,所以最后的大小应为4的整数倍,所以我们再浪费4个字节到12
所以最终这个结构体变量的大小为12.
我们交换一下c2和i的顺序
即
struct s2
{
char c1;
char c2;
int i;
};
struct s2 s3 = { 0 };
printf("%d\n", sizeof(s3));
运行一波
所以在设计结构体时,我们既要节省空间,又要满足对齐,如何做到?
让占用空间小的成员尽量集中在一起
4 如果有嵌套结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有的对齐数(含嵌套结构体的对齐数)的最大值的整数倍
例:
struct s4
{
double d;
char c;
int i;
};
struct s5
{
char c1;
struct s4 s;
double d;
};
struct s5 s3 = { 0 };
printf("%d\n", sizeof(s3));
运行一波
由于结构体s4中对齐数为8,所以s4存储时应对齐到8的整数倍上,由计算的出s4占16字节
接下来正好是偏移量24的位置,正好存放d,d占8个字节,8+24=32,而32正好是8的整数倍,所以最终的大小就为32
如图:
5 如果含数组,在求对齐数时以类型为标准而非元素个数
例:
char c[5]; //对齐数为1
补充:我们可以修改默认对齐数
例:比如将默认对齐数改为2
#pragma pack(2)
我们再用上面的例子测试一下
struct s2
{
char c1;
int i;
char c2;
};
struct s2 s3 = { 0 };
printf("%d\n", sizeof(s3));
大家可以自己画画图,就能得出结果
比原结果少了4个字节。
所以结构体在对齐不合适时,我们可以自己更改默认对齐数。
4 offsetof
offsetof是个宏,可以用来计算某个成员相对于首地址的偏移量,第一个参数是结构体类型,第二个是成员名。需引头文件<stddef.h>
例:我们还是用刚刚的例子
struct s2
{
char c1;
int i;
char c2;
};
struct s2 s3 = { 0 };
printf("%d\n", (int)offsetof(struct s2, c1));
printf("%d\n", (int)offsetof(struct s2, i));
printf("%d\n", (int)offsetof(struct s2,c2));
运行一波
等我们学到宏之后,我们将会模拟实现一波offsetof,期待的话请多多为我投票吧
5 位段
位段和结构体是类似的,只有2个不同
1 位段的成员必须是int、unsigned int、signed int或char
2 位段的成员名后有一个冒号和一个数字,数字用于表示该成员所占空间大小。
例:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30
};
2+5+10+30=37比特位
我们来计算一波大小
我们在给出对应的结构体再计算一波大小
struct B
{
int _a;
int _b;
int _c;
int _d;
};
struct B s2 = { 0 };
printf("%d\n", sizeof(s2));
那位段具体是如何分配大小的呢?
位段的内存分配
位段上的空间是以四个字节(int、unsigned int、signed int)和一个字节(char)来开辟的
具体如何使用?
我们以char为例再给一个例子
struct s
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
由于是char,所以我们先开一个字节
从a开始,a占3个比特位,问题来了,先占左边还是先占右边?
我们假设先占右边
下来是b,b占4个比特位,接着往下存
到c的时候就不够了,所以我们再开一个字节
现在问题又来了,是浪费掉上面那个剩下的比特位直接在这个里存还是用掉?
我们假设浪费掉
到d之后又不够了,所以我们再开一个字节,按上面的猜测,我们浪费掉第二个字节剩下的比特位
我们计算一下大小
(正好是3)
为了验证我们的猜想,我们赋一波值
struct s s1 = { 0 };
s1.a = 10;
s1.b = 12;
s1.c = 3;
s1.d = 4;
我们给a赋值为10--------》1010,由于a有3个字节,所以只能放010上去
b赋值为12--------》1100,由于b有4个字节,所以只能放1100上去
c赋值为3--------》 011, 由于c有5个字节,所以凑成00011上去
d赋值为4--------》 100,由于d有4个字节, 所以凑成0100上去
按照我们刚才的猜想,我们将值写到三个字节上,并转化为16进制显示
即为
我们再来调试一波
确实如此,所以我们的猜测是对的
因此我们得出结论
在VS下
1 一个字节内部中,先使用低地址(右边),从右到左使用
(注意:这个规则和大小端没有关系)
2 剩余空间如果不够,会浪费掉
位段的意义
位段可以根据需求定义变量有几个比特位,起到了节省空间的作用
但是由于int只有4个字节(32个比特位),所以你不能定义32以上的数
另外,位段涉及很多不确定因素,例如位段是不跨平台的,注重可移植的程序应避免使用位段
我们再讨论一下位段的跨平台问题
1 int 位段被当成有符号/无符号数是不确定的
2 位段中最大位的数目不确定,16位机器上int最大为16比特位,而32位机器上int最大为32比特位
3 位段中的成员从左向右分配/从右向左分配不确定
4 当一个结构包含两个位段,第二个位段成员较大,无法容纳第一个位段剩余的位时,使用/浪费未定义。
二、枚举
可以一一列举的东西我们可以用枚举来实现,比如月份、性别等
1、结构
enum 类型
{
//枚举类型的可能取值(常量)
member1,
member2,
……
};
其中: member1默认从0开始,依次递增
例:
星期
enum Day
{
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
Sun
};
我们可以在初始化的时候对默认开始的常量进行修改
例:
enum colour
{
RED=5,
GREEN,
BLUE
};
printf("%d %d %d", RED, GREEN, BLUE);
运行一波
1 枚举的优点
1 比 #define RED 5 这样的操作更好,有类型检查
2 增加可读性
例:
我们之前写过一个简易的计算器
在 指针进阶 的博客中(http://t.csdn.cn/YvheL) 我们采用了函数指针数组的操作,即通过下标来访问每个函数。虽然方便,但是下标的方式可读性很低,这个时候我们就可以用一波枚举
enum option
{
exit, //0
Add, //1
Sub, //2
Mul, //3
Div //4
};
这样下标就被替换为了这些具体的选择,非常奈斯
3 防止命名污染
4 方便调试
5 使用方便,一次可以定义多个变量
三、联合体(共用体)
联合也是一种特殊的自定义类型,这种类型定义的变量也包括一系列的成员,其特征是
这些成员共用同一块空间,所以也叫共用体
1 初始化
union un
{
char c;
int i;
};
union un u = { 10 };
printf("%d %d",u.c, u.i);
由于成员使用同一块空间,所以将10赋值给u时,u的每个成员都是10,所以打印之后
那如何进行独立初始化?
union un u;
u.i = 10;
u.c = 100;
因为所有成员共用一块空间,你改了一个,其他的也会相应被改。
所以独立初始化是个比较鸡肋的存在。
下面我们再来研究一下联合体时如何存储的
先来看看它的大小
printf("%d\n", sizeof(u));
答案是4,正好是一个int的大小
再来看看地址
printf("%p\n", &u);
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
均为一个地址,所以i和c确实用了相同的空间,i占了4个字节,而c占了一个字节
所以我们可以得出结论
联合体的特点:联合体的成员共用一块空间,这样一个联合体的大小至少是最大成员的大小
例:判断大小端
我们在数据存储那一节中(http://t.csdn.cn/qkerU)知道了数据在内存中的存储方式有大端和小端
之前验证大小端的方式是给一个数,将其强转为char,拿到第一个字节的值,便可验证
现在学了联合体,我们可以用联合体的性质来更轻松的验证了
union un
{
char c;
int i;
};
我们给到一个包含char和int的联合体,由于c和i占用同一块空间,所以我们看char的值便可
判断大/小端了,兄弟们,赶紧把 奈斯 打在公屏上
我们将其实现为一个函数
int check()
{
union un
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
所以,当返回1时,是小端,返回0时,是大端
2 联合体大小的计算
联合体作为一种特殊的结构体,是否有对齐规则呢?
有的,但是鉴于联合体的特点,对齐规则只有一条了
就是当最大成员不是最大对齐数的整数倍时,要对齐到最大对齐数的整数倍处
我们给到一个例子
union un
{
char a[5];
int i;
};
union un u;
printf("%d\n", sizeof(u));
显然,a数组要占5个字节,但该联合体的最大对齐数为4,所以最后要对齐到4的整数倍,也就是8
总结
做总结,今天这篇博客带大家学习了自定义类型,总体上来讲还是细碎的规则多一些,而且相比其他章节重要性也没那么高,大家只要了解即可。但是内存对齐规则大家要重点掌握,不仅考的多,而且在C++中仍然有用到。
水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。