🌟前情提要 🌟
本章节我们讲C语言中的自定义类型:结构体
、枚举
和联合
。
文章目录
前言
为什么C语言中会有自定义类型呢?
目前学到现在我们知道了C语言类型有:
int
char
float
short
…
但如果C语言中只有这几种简单的类型其实是无法满足我们的日常编程需求的:
比如你想描述一个学生,学生包含学号年龄性别,这样一个学生中具有多种信息,你要怎么描述呢?
这个时候我们就需要用到
🏠结构体类型
🍺结构体类型
就可以把这些基础类型组合起来
。
结构体本质是一种自定义类型。
❓那自定义类型还有哪些呢?
在我们的C语言中还涉及了枚举
、联合体
,枚举和联合体也是一种自定义类型。
💚于是,让我们先从结构体开始讲起:
🧃1. 结构体的声明
🥗1. 1 结构体的基础知识
结构是一些值的集合,这些值称为
成员变量
。结构的每个成员可以是不同类型的变量。
🥗1. 2 结构的声明
struct tag
{
member-list;
}variable-list;
🍍例如描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1,s2,s3; //分号不能丢
🍸struct Stu
内部放的是成员列表,结构体外部放的就是变量列表(定义三个结构体变量s1,s2,s3)。
❗千万要注意结构体后面有一个分号要写!
🥗1. 3 特殊的声明
❓什么叫特殊的声明?
在声明结构的时候,可以不完全的声明。 |
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了:
//在上面代码的基础上,下面的代码合法吗?
p = &x;
✨答案:
编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的。
💥注意事项:
匿名结构体
后面必须加上变量列表!匿名结构体变量
只能用一次!(因为你连名字都没有怎么创建下一个结构体变量啊?)匿名结构体的成员
如果一样,在编译器看来也是不同类型的结构体。
这里再给大家讲一下 ⚪typedef |
typedef
就是类型重命名。
比如:
typedef struct Node
{
int data;//数据
struct Node* next;//指针
}Node;
int main()
{
struct Node n1;
Node n2;
return 0;
}
🍇这里就是把struct Node
这个结构体类型重命名
为了Node
,主函数里面的n1
和n2
是完全相同的结构体类型。
这就是重命名。
🥗1. 4 结构体变量的定义和初始化以及结构体嵌套
🍡 有了结构体类型,那如何定义变量,其实很简单。
struct Book
{
char name[20];
float price;
char id[12];
}s = {"哒哒的C语言教程", 55.5f, "PGC001"};
struct Node
{
struct Book b;//结构体嵌套
struct Node* next;
};
int main()
{
struct Book s2 = { "哒哒的数据结构", 66.6f, "HG001" };
struct Node n = { {"哒哒的Java", 66.8, "TG001"}, NULL};
return 0;
}
🚦1. 5 结构体内存对齐(非常重要)
我们选择已经掌握了结构体的基本使用了。
🚃现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点: 结构体内存对齐
看例子:
❓咦?这里s
的大小为什么是12个字节呢?正常如果把S1
里面的各个成员的字节加起来不一共才6个字节吗?
💦这是怎么回事呢?
这个一会再给大家去讲,我们先再来看一个例子:
我们再创建一个结构体S2
这里我们的S2和S1的成员是一模一样的,只是位置有所改变。
❓咦?这里s2
的大小为什么又改成了8个字节?为什么与S1又不同了?
这就是我们今天要研究的问题:结构体内存对齐
💫如何计算?
首先得掌握结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况:嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
🧡以struct S1 s
举例:
我们先按照规则中的第一条将c1
从内存中放进去:
- 第一个成员在与结构体变量偏移量为0的地址处。
💛接下来我们按照第二条规则把i放进去
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
💌这里i是整型,大小为4个字节,小于vs默认的对齐数8,所以i的对齐数就为4
于是就把i放在4的倍数的地址处,4是4的倍数,于是把i放在地址为4的位置,中间什么都不放。
💚再把c2放进去
并按照第三条规则得到结构体总大小
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
🔰🔰这里
c1
的对齐数为1,i
的对齐数为4,c2
的对齐数为1,所以结构体的总大小就为最大对齐数4的整数倍,由于目前已经用了9个字节,结构体的总大小不可能为4或8,综上所述结构体的总大小为 3*4 = 12。
知道了原理我想大家也应该会算struct S2 s2
了吧?
🔊道理是一样的哦!
❔ 那为什么存在内存对齐?
🎁大部分的参考资料都是如是说的:
🍉 1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(比如平台只能在4的倍数的地址处进行数据的读取)
🍉 2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
🍸举个例子:
🌮在32位机器里,当我们读取一次的时候可以一次操作32个比特位(4个字节),如果我们对s
进行操作,想把i
拿出来,在不考虑对齐时需要读取两次才可以把i
拿出来,而考虑对齐时只需要直接访问4的倍数处的地址就可以直接把i
一次性拿出来了。
🥫总体来说:
结构体的内存对齐其实就是拿空间来换取时间的做法。
⭕那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
🍻S1
和S2
类型的成员一模一样,但是S2
就比S1
占的空间小,因为把空余的地方占上了。
🥗1.6 修改默认对齐数
我们使用
#pragma
这个预处理指令,可以改变我们的默认对齐数。
🌼上代码:
#pragma pack(1)
struct S
{
char c;//1
double d;//8
};
#pragma pack()
int main()
{
struct S s;
printf("%d\n", sizeof(s));
return 0;
}
🥝通过计算出的结构体s
的大小就可以看出,这时默认对齐数8
已经被修改成了1
,结构体变成了不考虑对齐
的储存方法。
🍹结论:
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。 |
🥗1.7 结构体传参
直接上代码:
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;
}
❓上面的 print1
和 print2
函数哪个好些?
✅答案是:首选print2
函数。
🤔这是为什么??
原因是:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
🌞就是如果传参的时候会建立一个临时变量,传一个结构体就会建立一个跟结构体一样大的临时变量,而传地址进去只需要建立一个指针变量就可以啦!并且也可以找到结构体!这样会大大节省时间和空间上的开销~
结论:
结构体传参的时候,最好传结构体的地址。 |
🧃2. 位段
结构体讲完就得讲讲结构体实现 位段
的能力。
⛺2.1 什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是
int
、unsigned int
或signed int
。
2.位段的成员名后边有一个冒号和一个数字。
🛺比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A
就是一个位段类型
。
❔那位段A
的大小是多少?
是不是不懂了?
🍜 其实真的很简单:
int _a:2
的意思就是a这个成员现在只在内存中占2个比特位,int _d:30
的意思就是d这个成员现在在内存中占30个比特位。
❗但是我们要非常注意!位段不可以想怎么用就怎么用!
正常一个整型int
可以放32个比特位
,最大的数可以放到2147483647
,而int _a:2
只有2个比特位,如果你说你有一个数据只有0、1、2、3这四种情况才可以写成int _a:2
这种形式,不然是放不下的!
🟡好处就是可以很大限度的节省空间~
❔ 那总共加起来才47个比特位啊!是不是才占6个字节呢?
让我们打印出来看一看:
printf("%d\n", sizeof(struct A));
✅答案是:8
⛺2.2 位段的内存分配
- 位段是不对齐的!(如果位段要是对齐的话那不就与节省空间的主旨相违背了吗?)
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
这里的内存是怎么开辟的呢?
我们以struct A
来举例
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
💛首先由第3点:
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
🚀我们可以知道这里位段上的空间是以4个字节 4个字节的方式来开辟的,而这里47个比特位大于4个字节,所以需要开辟8个字节:
1️⃣先把a
放进去,还剩30个
比特位,再把b
放进去,还剩25个
比特位,再把c
放进去,还剩15个
比特位。这时我们发现,剩下的空间不足以放下d了 2️⃣于是我们放到下一个字节:
💙这时问题来了,d是接着上次开辟的空间放30个比特位呢?还是在下面的空间直接放30个比特位呢?
💚这里就引出了第4点:
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
💥在不同的平台,d如何放,在内存中是不一样的,所以位段不可以跨平台使用。
🌍到这里我们也知道了为什么上一题打印出来的结果是8,因为位段这里按照需要,一次开辟4个字节,一共开辟8个字节,而不是开辟6个字节。
❓那到底数据存储是接着上次开辟的空间储存呢?还是在新开的空间直接储存呢?
💧我们再举一个例子来说一下:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
⛪这是给
结构体S
分配的3个字节,假设s.a,s.b,s.c,s.c
从低位往高位开始放
⛪这个时候
s.c
放不下了,往下一个字节放
⛪s.d
放不下了,再往下一个字节放
❓我们放的对不对呢?真的是这样放的吗?
这个时候这个数字转成16进制是0x620304
,真的是这样吗?我们来调试看一下:
真的是这样的欸!
🌈那我们就验证了位段在内存中是从低位放到高位,也就不存在接着上一个字节的比特位向下存储的问题了~因为结构体的成员是从高地址放到低地址的(像数组一样)。
⛺2.3 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
🍹总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。 |
⛺2.4 位段的应用
🧃3.枚举
💎枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
- 一周的星期一到星期日是有限的7天,可以一一列举。
- 性别有:男、女、保密,也可以一一列举。
- 月份有12个月,也可以一一列举
这里就可以使用枚举了。
💸3.1 枚举类型的定义
enum Sex
{
MALE,
FEMALE,
SECRET
};
在这个地方我们就是定义了一个枚举类型.
int main()
{
enum Sex s = MALE;
enum Sex s2 = FEMALE;
return 0;
}
🎎以上定义的 enum Sex
就是枚举类型。
{ }中的内容是枚举类型的可能取值,也叫 枚举常量
。
1️⃣这些可能取值都是有值的,默认从0开始,一次递增1,
2️⃣当然在定义的时候也可以赋初值(初始化)。
例如:
3️⃣如果你这样写:
enum Sex
{
MALE=2,
FEMALE,
SECRET
};
那么FEMALE
的值就是3,SECRET
的值就是4,依次递增
💥注意!这里的FEMALE
,SECRET
等是枚举常量,是常量,除了一开始在结构体里面赋初值之外是不可以改变的!
如果你想这样写:
int main()
{
enum Sex s = MALE;
enum Sex s2 = FEMALE;
MALE = 6;
return 0;
}
对不起,这是不被允许的!
💸3.2 枚举的优点
❓为什么使用枚举?
我们可以使用 #define
定义常量,为什么非要使用枚举?
枚举的优点:
增加代码的可读性和可维护性
和
#define
定义的标识符比较枚举有类型检查,更加严谨。防止了命名污染(封装)
💜限制了范围,比如我定义一个枚举类型的RED,只能在枚举类型里面使用,而用#define
定义则全局都可以使用便于调试
💜我们在预编译这个地方完成了#define
定义符号的替换
比如:
#define RED 5
就是一旦预编译完成了之后,RED
就不再是RED
了,而是5,而这句#define RED 5
代码也就删掉了,而我们调试的代码是预编译之后产生的代码,我们肉眼看到的和真实执行的代码是不同的,就不便于调试.而枚举不会改变使用方便,一次可以定义多个常量
🧃4. 联合(共用体)
🌊3.1 枚举类型的定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。 |
🏸联合体(共用体)的关键字:union
看代码🔽
union Un 联合类型的声明
{
char c;//1
int i;//4
};
int main()
{
union Un u; 联合变量的定义
u.c = 0x55;
u.i = 0;
printf("%d\n", sizeof(u)); 计算联合变量的大小
return 0;
}
🌊4.2 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。 |
🔹那我们来验证一下,把u
和u.c
和u.i
的地址分别打印出来:
哦~我们发现地址是相同的!
实际上呢,是这样子的:i
有一个字节与c
共用了一块区域!
所以联合体也叫共用体.
💥所以这个联合体里面的用户是不可以同时使用的!
💦这个特性可以使我们判断当前机器是大端还是小端:
int check_sys()
{
union Un
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
int main()
{
if (1 == check_sys())
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
✅结构体成员是从低地址处放到高地址处
,所以绿色的为u.c
,橙色+绿色为u.i
小端存储是把低位放在低地址处,高位放在高地址处,
如果是小端存储,则01
刚好放在了u.c
的位置,这个时候我们判断u.c是否为1
就能知道当前机器是小端存储还是大端存储了.
🌊4.3 联合大小的计算
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
例子:
union Un
{
short arr[7];//14 2
int i;//4 4
};
🧊请大家最后再算一下这个联合体Un
的大小~
✅答案是:16个字节
💌解:
Un
的最大成员大小是14,对齐数是2,i
的对齐数是4,最大成员大小不是最大对齐数的整数倍,所以Un
需要对齐到最大对齐数的整数倍,也就是4*4=16
总结
以上就是C语言进阶の自定义类型:结构体,枚举,联合
的全部内容,本文仅仅介绍了这几种自定义类型以及的它们的使用,而想让自身取得提升还需要大家多方面的练习~那我就在这里祝大家越来越牛,年入百万!