自定义类型:结构体(内存对齐),枚举,联合
Posted 捕获一只小肚皮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义类型:结构体(内存对齐),枚举,联合相关的知识,希望对你有一定的参考价值。
文章目录
自定义类型:结构体(内存对齐),枚举,联合
前言:
在c语言中,类型有两种:
- 内置类型(比如
char
,short
,int
,long
等) - 自定义类型(结构体, 枚举, 联合体)
区别: 内置类型是c语言自带的,我们不能进行修改. 自定义类型是我们自己根据实际需要进行自定义
内置类型,博主在此不再赘述,本章的重点是 自定义类型
1.结构体
1.1结构体类型的声明
1.1.1结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
举个例子,比如:
-
人就是一个结构,而人具有 手,脚,五官,毛发,内脏,这些散的称为成员变量
-
学生就是一个结构,学生具有 学号,性别,姓名,身高,年龄, 后面的东西就是 成员变量
1.1.2结构的声明语法
struct tag
{
member-list;
}variable-list;
tag:
结构名,(自己取)
member-list:
成员变量
variable-list:
结构体变量名,(这里一般不在这里声明)
例如 描述一个学生.
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不能丢
1.1.3特殊的声明(匿名结构体)
在声明结构的时候,可以不完全的声明(在上面我们说variable-list
一般不在后面声明,但是这种特殊声明必须声明变量)
例如:
//匿名结构体x
struct
{
int a;
char b;
float c;
}x;
//匿名结构体数组a,和匿名结构体指针p
struct
{
int a;
char b;
float c;
}a[20], *p;
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
对于这种类型的结构体,只能使用一次(指的是声明变量,不是x,a只能用一次),在下面的结构体的变量的定义和初始化会讲到
问题: 匿名结构体中上式子这样写会怎样? p = &x;
会报警告,因为该结构是匿名的,就相当于不知道类型,而实际情况也是 编译器会把
p
与&x
看做两个完全不一样的东西
1.2结构的自引用
顾名思义: 就是类似函数递归,结构体自引用就是在自己内部创建同类型的成员变量
比如下面这样创建:
struct Node
{
int data;
struct Node pp;
};
大家仔细想想,这样创建真的可以行得通吗?
答案是否定的,最好的打脸问句--------sizeof(struct Node)
的大小是多少???是不是发现根本计算不出来?
那么,正确的在自己内部创建自己类型的成员变量怎么弄呢?大家先想想前面用过的知识再往下面看哦
答案是 指针.我们可以利用指针进行创建自己类型的成员变量.为什么呢??大家以后会学习数据结构的链表,而链表就是利用结构体实现的
,而链表中每一个小块(结点),都被前后一条链给连接.比如有下面的这样一个学生结构体:
struct student
{
char name[20]; //姓名
int age;//年龄
char number[11]; //电话
struct Node *next; //注意,这里是指针哦~
};
然后我用图来绘了一下 abcde
5个学生
会发现,
abcde
五个学生都是一样的信息,且都是结构体类型,但是想要讲这五个人的信息一一连接,只能用结构体指针,学生a有自己的 姓名,年龄,电话,存储好后,a里面还有个指针指向下一个学生b
学生b有自己的 姓名,年龄,电话,存储好后,a里面还有个指针指向下一个学生c
学生c有自己的…
结论: 结构体的自引用需要用结构体指针
1.2.1 typedef
控制结构体命名
typedef
可以为类型改名比如:
typedef int Element
那么声明整型变量时候可以有这种写法Element b = 10;
typedef float good
那么声明整型变量时候可以有这种写法good c = 10.123;
在上面介绍 结构体自引用时候,我们说到要用结构体指针进行,没错.但是我们可以为了方便,改改类型名,比如:
typedef struct node
{
int data;
NODE* next;
}NODE;
这样修改后,我们就不需要声明变量时候写的很多(比如:
struct node * next
而是NODE* next
)
那如果匿名结构体这样行吗?
typedef struct
{
int data;
Node* next;
}Node;
答案: NO!
, 为何?因为匿名结构体他的类型名是没有的,且匿名结构体只可以声明一次,那么修改为 Node后,在里面的Node*
到底谁先执行?
就好比是 先有鸡还是先有蛋 所以匿名结构体不可以这样写
1.3结构体变量的定义和初始化
有了结构体类型,那如何定义变量? 其实很简单,有两种方法.
1.3.1结构体变量的定义
方法一: 在创建结构体时候就声明变量
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1 此时的p1是全局变量
方法二: 先创建好结构体,然后哪里需要就在哪里声明结构体
struct Point
{
int x;
int y;
};
int main()
{
struct Point p1; //在这里需要创建,就在这里声明 此时的p1是局部变量
return 0;
}
1.3.2结构体变量的初始化
初始化:定义变量的同时赋初值。不然等声明好了变量后,在给值会报错.
struct Student
{
char name[15];
int age;
} xiaoming = {"小明",15}; //全局变量初始化
int main()
{
struct Student xiaohua = {"小花",18}; //局部变量初始化
return 0;
}
错误初始化示例:
struct Student
{
char name[15];
int age;
};
int main()
{
struct Student xiaohua;
int a = 0;
xiaohua = {"小花",18};
return 0;
}
运行截图:
1.4结构体内存对齐
我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点: 结构体内存对齐
1.4.1结构体内存对齐规则
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该 成员大小的较小值。
VS中默认的值为8
结构体总大小为该结构体最大对齐数(每个成员变量都有一个对齐数)的整数倍。
最大对齐数 = 结构体中所有成员中的最大对齐数
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
说了这么文绉绉和枯燥的文字,到底什么意思呢??我们一条一条语句的解释
第一条: 偏移量
我们知道,在计算机中内存被划分为一个个字节作为基本单元空间.而一个变量的存储地址是不确定的.比如有变量a,b,那么他们的地址可能会像下面这样:
他们的地址是不确定.而结构体变量同样,因此我为了后面方便叙说第一条什么意思,便假设某结构体变量在某个位置
现在有下面这样一个结构体变量:
struct S1
{
char c1;
int i;
char c2;
} p1;
然后我们用图解释偏移量
第二条:对齐数
对齐数,即一个数字,按照对齐数规则 对齐数 = 编译器默认的一个对齐数 与 该 成员大小的较小值。
还是按照刚才那个结构体例子,且我使用的编译器是
vusial studio
,即默认对齐数是8:struct S1 { char c1; //大小为1个字节 int i; //大小为4个字节 char c2; //大小为1个字节 } p1;
c1
的大小是 1,小于8,所以c1
的 对齐数 是 1,但是其偏移量为0.
i
的大小是 4,小于8,所以i
的 对齐数 是 4.其偏移量必须是4的整数倍
c2
的大小是 1,小于8,所以c2
的对齐数是 1.其偏移量必须是1的整数倍
第二条中的对齐规则图例
第三条:结构体总大小
1.4.2结构体对齐题型训练:
第一题
struct S1
{
char c1;
char c2;
int i;
}p1;
解析图:
第二题
struct S2
{
double d;
char c;
int i;
}p1;
解析图:
1.4.3第四条:嵌套结构体对齐规则
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
比如有两个结构体:
struct S1
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S1 s;
double d;
};
结构体中 嵌套的结构体s,按照上述叙述,s的对齐数就是8. 且s的大小前面我们已经知道了,是16字节.因此我们有下面的图解
图解:
1.4.4为什么会有结构体对齐?
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能
在某些地址处取某些特定类型的数据,否则抛出硬件异常。
比如一些单片机,他们访问地址遵循着一定的规则,不像我们的C语言编译器,可以任意访问内存.如果结构体不内存对齐,就会发生硬件异常
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的
内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
什么意思呢?不急,我们先弄清楚一个读取概念.这里以32位机器为例:
- 32位机器 ,即代表着有32根地址线
- 32位机器 ,即代表着有32根数据线
- 32位机器 ,即代表着有一次性读取或传输32位空间大小的能力,即一次性可以传输 4个字节
- 而我们的CPU寄存器大小刚刚也是32位,即4个字节
所以,我们的电脑在读取数据时,也是常常一次性读取4个字节的
现在我们有以下一个结构体:
struct S
{
char c;
int i;
};
我们会发现:如果未对齐,当第一次,一次性读取4个字节后,只能读取c,想要读取i,就必须返回进行第二次读取,需要访问2次
我们会发现: 如果对齐,在第一次访问4个字节后,想要在读取i的内容,只需要再往后移动就行. 只需要访问内存一次
这就是结构体对齐的意义:
结构体的内存对齐是拿空间来换取时间的做法。
1.4.5 怎么设计结构体?
让占用空间小的成员尽量集中在一起。
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
1.4.6 修改编译器默认对齐数
不同的编译器默认设置的对齐数不一样,甚至有的编译器根本不会设置默认对齐数,比如
linux
下的gcc
,在gcc
编译器下是默认成员变量自己大小为对齐数. VS编译器的默认大小是8.那么我们会想,能不能修改,默认对齐数呢?答案是肯定的,可以修改.使用下面的语句
#pragma pack(num) structure #pragma pack()
#pragma pack(num)
num参数就是代表着需要修改为的默认对齐数
structure
这个位置写结构体的相关内容
#pragma pack()
与第一条语句搭配使用,代表结束
示例:
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
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; }
答案是什么呢???我就留给大家了.
1.5结构体传参
假设有下面这样一个结构体:
struct S { int data[1000]; int num;};
然后我们需要设计一个初始化函数 Init()
,用来初始化结构体S创建的对象.怎么设计了呢? 下面给出了两种方法
方法一:
void Init(struct S struc_name){ struc_name.data = {0}; struc_name.num = 100;}
方法二:
void Init(struct S* struc_name){ struc_name->data = {0}; struc_name->num = 100;}
大家觉得哪种设计会更合理??? 答案显而易见 ---------- 第二种
为什么这样说呢? 我们在前面的函数章节讲过,
- 第一种方法是什么传参?----------------------> 传值
- 第二种方法是什么传参?----------------------> 传址
区别: 传值调用时,形参只是实参的一份临时拷贝
传址调用时,形参可以控制改变实参
优势对比: 传值调用时,如果实参的空间很大时,那么函数接收时候,就会接收这么大一个东西
传址调用时,无论实参的空间多大,都只会接收四个字节.
看下面的一段代码:
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
函数。 原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
1.6结构体实现位段(位段的填充&可移植性)
讲解完毕结构体后怎么能够少得了位段呢?
什么是位段?
位,就是比特位.具体请往后看.
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是
int、unsigned int
或signed int
等整型2.位段的成员名后边有一个冒号和一个数字。数字就代表是几个比特位.
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
那么,sizeof(struct A)
的大小是多少呢? 答案:8字节.
因为成员后面的数字代表位,所以 2+5+10+30 = 47位, 是不是接近8字节(64位)??
但是明明是47位大小,为何却占用了8字节呢?原来位段也有自己开辟空间的规则.(每次开辟都是按照需要每次4个字节4个字节的开辟或者一个字节一个字节的开辟)
,比如上面的开辟,我画图解释:
发现最终就是利用了8个字节.
再次测试一个题:
struct S { char a:3; char b:4; char c:5; char d:4;};
sizeof(struct S)
大小是?
发现最终开辟了3个字节,所以最终就是3字节大小.
位段与大小端存储,整型截断的综合运用题:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
struct S s = { 0 };
int main()
{
s.a = 10;
s.b = 12;
s.c = 19;
s.d = 4;
printf("%d\\n", s.a);
printf("%d\\n", s.b);
printf("%d\\n", s.c);
printf("%d\\n", s.d);
return 0;
}
问题一: 此时打印a,b,c,d的值分别是多少???
问题二:此时s的内存中的存储情况是??请用16进制表示出该3字节中的存储内容
希望大家一定要思考后再看下面的答案哦~~
答案:
10,12,19,4是正数, 二进制表示分别是 1010(10) 1100(12) 10011(19) 100(4)
但是a只有3个比特位,所以整型10会发生截断,最后只有末尾3位存进去了,也就是010放进a了
b有4个比特位,所以整型12末尾4个数字刚刚存进去,也就是1100放进b了
c有 5个比特位,所以整型3完全够存进去,也就是10011放进c了
d有4个比特位,所以整型4完全足够放进去,也就是100放进d了
所以此时abc
在内存中的情况如图:
可以清晰看到,a,b,c,d的位范围,而printf
要求是%d
格式输出,即有符号形式
- a有3位,最高位是0,所以当成正数输出,10的十进制是2,所以输出2.
- b有4位,最高位是1,所以当成负数输出,1100负数补码还原回十进制是**-4**,所以输出-4
- c有5位,最高位是1,所以当成负数输出,10011负数补码还原回十进制是**-13**,所以输出-13
- d有4位,最高位是0,所以当成正数输出,0100的十进制是4,所以输出4
而根据上面三个字节的内容,又知每4位二进制对应一位16进制,所以下面开始转换
0110 0010 0001 0011 0000 0100 对应下面16进制
62 13 04
图一:
图二:
是不是非常正确??,值分别为2 -4 -13 4 内存中的内容就是62 13 04
位段的跨平台问题
-
int 位段被当成有符号数还是无符号数是不确定的。
-
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 -------最大位的数目是指位段成员冒号后的最大位数
-
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
-
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在
位段的应用
我们平时在网络上,电话上,社交平台上发送与接收消息都是一对一的,而不是某个人发个消息,全世界人的都知道.这归功于对信息的加密.
而网络加密常用的有 比如源IP地址(32位) 目的IP地址(32)位 网络协议(8位) 偏移(13位) 等,会发现各个东西需要的位是不一样的,如果我们只能每次一个字节或者4个字节的开劈方式,将会浪费极大空间,而位段就完美解决了.需要多少,就用多少.
下面是网络加密需要用的一些不同位需求的表:
2.枚举
仔细品味这个词语,枚举,什么是枚举呢?顾名思义,就是列举
把可能的所有值,一一列举出来:
比如:
性别,只有两种: 男生 女生
月份,只有12种:1月 2月 3月 4月 5月…11月 12月
星期,只有7天:1天 2天 3天…7天
十进制一位数数字,只有10个 :0 1 2 3 … 8 9
这里就可以使用枚举了
2.1枚举类型的语法定义
enum tag
{
list1, //末尾是逗号
list2,
list3...
};
例如 星期
enum weeks
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
例如 颜色
enum Color//颜色
{
RED,
GREEN,
BLUE
};
例如 性别
enum Sex
{
MALE,
FEMALE
};
以上定义的 enum weeks
, enum Color
, enum Sex
都是枚举类型, {}中的内容是枚举类型的可能取
值,也叫 枚举常量 。他们的值是按照顺序从0开始默认递增的.
比如下面:
enum weeks
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
printf("%d %d %d %d %d %d %d", Mon,Tues,Wed,Thur,Fri,Sat,Sun);
return 0;
}
但是我们也可以修改值,但是还是按照一定顺序;
enum weeks
{
Mon = 100,
Tues,
Wed,
Thur = 300,
Fri,
Sat,
Sun
};
int main()
{
printf("%d %d %d %d %d %d %d", Mon,Tues,Wed,Thur,Fri,Sat,Sun);
return 0;
}
2.2枚举的优点
为什么使用枚举?
我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:
-
增加代码的可读性和可维护性
-
和
#define
定义的标识符比较,枚举有类型检查,更加严谨。 -
防止了命名污染(封装)
-
便于调试
-
使用方便,一次可以定义多个常量
第二条信息请看2.3枚举
2.3枚举的使用
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
只有以上的这种写法才是正确的.而如果直接赋值,会有类型差异,比如:
clr = 100
,这种写法不合法
3.联合(共用体)
3.1联合类型的定义
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块
空间(所以联合也叫共用体)。
联合的声明,变量定义:
//联合类型的声明结构
union tag
{
member1;
member2;
member3;
...
};
比如:
union Un
{
char c;
int i;
};
//联合变量的定义
union tag name;
比如:
union Un func;
以上是关于自定义类型:结构体(内存对齐),枚举,联合的主要内容,如果未能解决你的问题,请参考以下文章