一篇文章让你由浅入深去感受四种自定义类型的魅力——结构体类型,位段,枚举类型,联合体(共用体)类型
老规矩,笔记放在目录前自取
自定义类型笔记—————欢迎自取
文章目录
- 一篇文章让你由浅入深去感受四种自定义类型的魅力——结构体类型,位段,枚举类型,联合体(共用体)类型
- 一、结构体类型
- 1. 创建结构体变量
- 1.1 创建隐藏结构体变量
- 1.2 结构体变量的重命名
- 2. 结构体的自引用
- 3. 结构体类型对齐
- 4. 改变结构体变量默认对齐数
- 5. 结构体类型每个变量的偏移量
- 6. 结构体传参
- 二、位段
- 1. 位段的内存空间的分配
- 2. 位段跨平台问题
- 三、枚举
- 1. 枚举类型的定义
- 2. 枚举的优点
- 四、联合体(共用体)
- 1. 联合体变量的定义
- 2. 联合体变量的优点
- 3. 联合体大小的计算
一、结构体类型
1. 创建结构体变量
组合类型-创建自定义类型
typedef struct Stu
{
int x;
}mine;
struct Book//内部类型定义
{
char name[30];
float price;
char id[20];
mine a;
}b1, b2;//这里可以直接写b1 = {"c语言",63.2f,"A1", {1}};
//注意分号不能漏掉
//此处b1,b2可以省略,b1,b2为全局变量
struct Book b3;//也是全局变量
int main()
{
struct Book b4={"c语言",63.2f,"A1", {1}};//内部成员初始化
printf("%d", b4.a.x);//打印1
return 0;
}
//简单访问其中成员
1.1 创建隐藏结构体变量
将变量名舍去
如果用指针指向它会怎么样
struct //直接省略变量名
{
char name[30];
float price;
char id[20];
}b1, b2;
struct
{
char name[30];
float price;
char id[20];
}*p;
int main()
{
*p = &b1;//这样的写法是错误的
//编译器会认为,上面两种是不同类型,虽然类型的内容相同
return 0;
}
1.2 结构体变量的重命名
利用typedef
typedef struct Stu { int x; }mine;//重新命名 int main() { struct Stu a; mine b; //以上两种写法均可 return 0; }
2. 结构体的自引用
先了解一下,数据结构里面的链表结构
链表中,元素是没有顺序的,但可以通过链表链接
假设:如果像函数中嵌套循环的方式,每个节点都可以传送到下一个节点,可否实现链表的引用
struct Node { int ID; struct Node n; }
但是,问题出现了,如果这样循环下去,什么时候停止,它不像普通的变量的大小可以通过条件语句停止
所以,这个假想不成立
那么,一个节点是肯定要有个东西,让我们可以访问到下一个节点,但不会死循环
这个东西,就是指针
struct Node { int ID; struct Node* n;//指向下一个节点的地址就好了 }
上面就实现了,结构体的自引用
typedef 重命名结构体 自引用
typedef struct Node { int ID; struct Node* n; }Node;
3. 结构体类型对齐
结构体变量类型的大小
struct s { int x; int y; char a; }; int main() { printf("%d", sizeof(struct s)); return 0; }
那么是否是 int(4) + int(4) + char(1) 一共九个字节的大小呢?
打印出来结果是12
显然,不是简单的组成结构体中各类型相加
那么,具体的规则到底是什么?
结构体变量对齐规则
第一个成员在与结构体变量偏移量为0的地址处
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数 与 该成员大小中 较小值
- VS 默认的值为8
- Linux 没有默认值(成员自身大小就是对齐数)
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
如果嵌套了结构体的情况,
嵌套的结构体对齐到自己的最大对齐数的整数倍处
结构体的整体大小(字节数)就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
图解:
为什么要内存对齐?
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 有些硬件只能在某种规律下访问,所以数据最好在特定的位置上
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
- 一次访问4个字节,如果不对齐,则需要访问两次(比如一个char和一个int类型连在一起)
- 而对齐则只需要访问一次,且是完整的数据
结构体类型为了对齐,浪费那么多空间怎么办
- 我们只能将小的变量放在一起
- 这样就能尽可能节省空间
4. 改变结构体变量默认对齐数
利用 #pragma pack进行修改和恢复
#pragma pack(1) //相当于没有对齐,空间小,但效率可能没那么高了 struct stu { int x; int y; char a; }; #pragma pack() //用完结构体后,恢复默认对齐数 int main() { struct stu a; printf("%d", sizeof(a));//打印9 }
5. 结构体类型每个变量的偏移量
利用 offsetof 这个函数
offsetof
头文件:<stddef.h>
size_t offsetof( structName, memberName );
参数:结构体类型名,结构体成员名
返回值:返回指定成员起始位置的字节偏移量
#include <stdio.h> #include <stddef.h> struct s { int x; int y; char a; }; int main() { printf("%d", offsetof(struct s, x));//0 printf("%d", offsetof(struct s, y));//4 printf("%d", offsetof(struct s, a));//8 return 0; }
6. 结构体传参
引用符号 ->
访问符号 .
struct Book { char name[30]; float price; char id[20]; }; void Print(struct Book* b) { printf("书名:%s\n",(*b).name); //结构体指针 -> 成员名/x->name与上面等价 printf("价格:%f\n",(*b).price); printf("书名:%s\n",(*b).id); } int main() { struct Book b1 = { "c语言",63.2f,"A1" };//内部成员初始化 Print(&b1); }
为了节省函数创建临时变量的空间大小,创建一个相同大小的结构体接受值也是可以的,但为了节省空间,我们将指针传进去,更好的节省空间大小,我们操作通过指针去操作
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
二、位段
位段是什么:
位段和结构体类似
但又有区别:
位段的成员必须是int、unsigned int 或signed int 和 char(char属于整型家族的)
位段的成员名后边有一个冒号和一个数字, _ 不是必要的
位段可以节省空间
位段的位 —— 二进制位(1bit)
struct s { int _a:2;//_a分配2个bit位 int _b:4;//_b分配4个bit位 int _c:10;//_c分配10个bit位 }; int main() { printf("%d", sizeof(s));//16个bit位--4个字节 }
但事实上,有时不是正好是整字节,位段其实还是会浪费一点空间
所以位段到底是怎么分配内存空间的?
1. 位段的内存空间的分配
位段空间的需要,是按照char(1个字节)或者 int(4个字节)形式去开辟的
位段涉及很多不确定因素,位段是不跨平台的,不同编译器编译出来的结果可能不同,注重可移植的程序应该避免使用位段
但c语言标准里面没有说明,开辟的字节中,从低位到高位存储,还是从高位到低位存储
我们首先假设一个方向,每个字节从高位向地位存储
通过一个例子来看:
struct stu
{
char a:6;//分配6bit
char b:5;//分配5bit
char c:4;//分配4bit
char d:2;//分配2bit
};
int main()
{
struct stu s = { 0 };//初始化结构体
s.a = 10;
s.b = 15;
s.c = 20;
s.d = 3;
return 0;
}
分析:
a : 因为只分配了6个bit位,所以10的二进制存储时发生了截断,只存储了001010这几个数
其他变量分析同a,详细见图
最后我们运行一下,看是否如同我们假设一样
调试起来,等变量赋值之后,查看内存,和我们分析的一样,是0a0f34
但再次强调,每个编译器不同,不同的编译器可能会得出不同的结果,此结果只支持VS2019
2. 位段跨平台问题
- 创建int位段时候,有无符号是未知
- 位段中最大数目不确定(32位机器最大32,写成40,会出问题)
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,未知
在网络传送信息包装的时候,用位段节省空间,可以使得效率变高
三、枚举
描述生活中可以列举的东西
1. 枚举类型的定义
枚举类型的取值只能是正数,枚举类型的大小也是4个字节
//定义一个性别枚举常量
enum sex
{
//默认从0开始,向下递增,但也可以字节定义数字
male,
female,
secret
};
int main()
{
enum sex a = male;
//错误操作
//enum sex a = 4;//整型和枚举类型不匹配
//male = 4; //枚举常量是常量不能赋值,但可以在enum sex中赋值
printf("%d\n", male);//printf打印出0
printf("%d\n", female);//printf打印出1
printf("%d\n", secret);//printf打印出2
return 0;
}
2. 枚举的优点
枚举和define相似,但效果截然不同,枚举的优点有以下几点
- 增加代码的可读性和可维护性,比如某些情境下选择选项的数字,换成字符,更容易理解
- 和#define定义的标识符比较枚举有类型检查,更加严谨
- 防止了命名污染(封装),相比较define全局变量要好
- 便于调试,调试器在检验枚举变量时,可以显示符号值
- 使用方便,一次可以定义多个常量,而#define宏一次只能定义一个
举个栗子吧
只是个大概的模型,并不是完整代码,简单感受一下
enum menu
{
START,
SAVE,
EXIT
};
void print_menu()
{
printf("1.开始游戏 2.保存游戏 0.退出游戏");
}
int main()
{
int input = 0;
scanf("%d", &input);
//switch(input)
//{
//case 1 : printf("开始游戏");break;
//case 2 : printf("保存游戏");break;
//case 0 : printf("退出游戏");break;
//}
//当我们选择0,1,2的时候,还需要看一下每个选项对应的数字
switch(input)
{
case START: printf("开始游戏");break;
case SAVE: printf("保存游戏");break;
case EXIT: printf("退出游戏");break;
}
//但此时我们就不需要了,直接输入我们想要选择的选项就行
}
四、联合体(共用体)
1. 联合体变量的定义
先用一串代码感受一下,为什么它叫共用体
union U
{
char a;
int b;
};
int main()
{
union U u = { 0 };//初始化
printf("%p", &u);
printf("%p", &(u.a));
printf("%p", &(u.b));
//让我们看看联合体类型变量内存的分配是如何的?
return 0;
}
结果是
居然是相同的地址!到底是怎么实现的?
a和b共同利用这块空间,所以联合体变量大小至少是成员中最大成员的大小
2. 联合体变量的优点
使用联合体巧妙使用联合体计算计算机大小端存储
我们先用之前的知识实现一下
int main()
{
int a = 1;
char* p = (char*)a;
if(*p == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
我们还可以使用结构体来实现此功能
这样我们就可以不用指针了
union U
{
char a;
int b;
};
int main()
{
union U u = { 0 };
u.b = 1;
if(u.b == 1)
{
printf("小端");
}
else
{
printf("大端");
}
}
3. 联合体大小的计算
前面说,联合体类型大小至少是成员中最大成员的大小
但还有一个要求,就是当联合体成员中最大成员大小不是对齐数的整数倍时,
对齐到对齐数的整数倍
举个栗子
union U
{
char a[3];
int b;
};
union UU
{
char a[13];
int b;
};
int main()
{
printf("%d\n", sizeof(union U));//4
printf("%d\n", sizeof(union UU));//16
return 0;
}
我们来分析:
关于union U
关于union UU