c语言自定义类型——结构体、枚举、联合
结构体
我们都知道数组是相同类型元素的集合,那有没有一种数据类型可以将不同类型的元素放在一块儿呢?
结构体类型了解一下。
结构体类型的定义
struct stu
{
char name[10];
int age;
char sex[4];
};
以上就是简单的定义了一个 struct stu
结构体类型,其中有一个10元素的字符指针,一个整型的变量,一个4元素字符指针。
那么应该怎样创建结构体变量呢?
结构体变量的创建以及初始化
struct stu student;
这里我只是创建了一个 struct stu
类型的结构体变量,但是如果想在创建变量的时候顺便初始化那就应该是这样的:
struct stu student = {"shangsan",18,male};
结构体的使用
前面讲了结构体的创建以及初始化,那么,结构体到底有什么样的使用规则呢?
- 结构体可以使用
.
操作符来访问其成员 - 结构体的每个成员都是一个单独的变量,拥有单独的空间,可以单独的去修改
- 结构体中的数组在赋值时可以进行数组的一个一个元素赋值,也可以使用
strcpy
函数进行对字符数组赋值,千万不能使用数组名=字符串
的方式。
介绍了以上结构体的使用规则,我举两个例子:
strcpy(student.name,"lisi");
student.age = 20;
strcpy(student.sex,"female");
这里我将 student
结构体变量的所有成员的值都改了一下。
然后再介绍结构体的一种访问成员的方法:
- 结构体指针可以直接通过
->
操作符来访问结构体的成员
首先得有一个结构体指针:
struct stu* pstudent; //定义一个struct stu型的指针
pstudent = &student; //将结构体student的地址放在结构体指针pstudent里面。
然后再进行指针直接访问:
strcpy(pstudent->name,"zhangsan");
pstudent->age = 18;
strcpy(pstudent->sex,"male");
这里我通过结构体指针pstudent
将结构体student
的成员又进行了修改。
结构体的内存对齐
我们都知道,是变量都要占用内存的空间,那么,结构体变量也是一样的,结构体是我们自己定义的类型,那么它所占的空间肯定是有计算方法的。
首先我们知道,结构体的成员都是有自己独立的空间的,而且成员之间互不影响,那么我们就可以得出,一个结构体所占的空间至少等于所有成员所占的空间之和。
但实际上呢,并不是这样的,因为结构体存在内存对齐:
内存对齐
内存对齐就是结构体在内存的存储时,各个成员并不是顺序存放,而是按照一定的规则进行存放,而这个规则就是内存对齐规则。
为什么存在内存对齐?
- 平台原因:并不是所有的硬件平台都可以读取内存中的任意地址的数据,一些平台上只能读取特定类型的数据,也就是它只能取特定大的内存块的数据,说白了就是只能访问某个数字倍数的地址。
- 性能原因:数据结构应该尽可能的在自然边界上对齐,因为如果访问未对齐的内存时,处理器肯能要进行两次甚至两次以上内存访问。这必然会降低处理器的效率。
内存对规则
在介绍内存对齐规则之前,我先介绍两个名词:偏移量,编译器默认对齐数
首先是偏移量,在结构体的存储中,存放结构体的第一个内存单元就是偏移量为0的地址,如图:
其次是编译器默认对齐数,linux环境下默认为4,windows环境下默认为8。
开始介绍对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认对齐数与该成员大小的较小值。
- 结构体总大小为最大对齐数(每个成员变量都有一个自己的对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
那么根据内存对齐规则我们就很准确的计算出结构体的大小了。这里我简单计算一下上面结构体 stu
的大小:
struct stu
{
char name[10];
int age;
char sex[4];
};
首先是在对齐数为0的地址处放入 name
数组,所占10个字节,放 age
的时候因为它占用 4 个字节,这里假设在windows环境下,默认对齐数为8所以取较小值为4,但是下一块内存的偏移量为10,但是我们应该放在4的倍数处,那就得浪费2个字节的内存,达到12偏移处,将 age
放进去,之后下一块的偏移量为16,而下一个成员所占的空间为4个字节,取其与编译器默认值的较小值 4 ,下一块内存刚好为16偏移处,是4的倍数,因此可以直接将 sex
数组放进去放进去之后现在我们已经用了20个字节,所有成员中最大偏移量为数组 age
,数值为4,而刚好我们就用了20个字节,是4的倍数,所以此结构体所占的内存就为20字节。 用图解的方式如下:
结构体的传参
如下结构体:
struct cont
{
int arr[1000];
char string[1000];
}
我们知道函数传参都是将参数进行了临时拷贝,但是这个结构体是非常大的,所以,我们为了性能着想,一般会传递其地址,为了防止原结构体被改变,我们可以在传地址的时候加上关键字 const
这样就可以保证原结构体不会被改变了。
位段,位段计算机大小
我们既然学了结构体的内存对齐,那么就必然知道,当结构体的成员有时只需要的一点空间(甚至不到一个字节)的时候,这必然会造成大部分的浪费(有时浪费的比使用的还多),所以就引入了位段的概念来节省内存中的浪费。
什么是位段
位段的声明和结构体是很类似的,只是成员的类型有区别:
1. 位段的成员必须是 int
, unsigned int
或 signed int
。
2. 位段的成员名后边有一个冒号和一个数字,数字代表该成员所占的比特位(bit)。
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
我不难可以计算出位段 A
的大小,首先是申请一个整形空间(4字节),然后_a
变量占2个比特位,还剩30个比特位,然后放 _b
的时候先算出它的最小对齐数为5位,所以应该浪费掉三个比特位,然后再放入 _b
占用5位,现在还剩22个比特位,放入 _c
之前计算出 _c
的最小对齐数为 10 ,现在对其的位置刚好是10,所以刚好将 _c
放入,现在还剩12个比特位,因为下一次要放入的 _d
占用30个比特位,所以只能将剩余的12个比特位浪费掉,再申请一个整形空间(4字节),在新的整形空间上放入 _d
变量,还剩2个比特位,位段是按照基本数据类型来开辟的,但是如果不够它会按照基本数据类型来增加,但是使用时按照 :
后面的数字也就是比特位数来使用的,使用的时候还是有对齐规则的,但是位段所占的空间只能是整形(包括char
)所占空间的整数倍。
枚举
枚举,字面意思就是将可能的值列出来,实际上就是这样,相当于创建一个自定义类型,用这个类型创建的变量只能取你自己设定的范围内的值,但这只是一个符号的定义,实际上都是 int
型的。
枚举类型定义
enum Day
{
sun,
mon,
tues,
wed,
thur,
fri,
sat
}
这里我创建了一个枚举类型 Day
,它创建的变量的取值范围为:
mon,tues,wed,thur,fri,sat,sun
实际上在底层,这些值知识一个符号定义,如上面的 sun
代表 0
,mon
代表1
,tues
代表 2
······
那这样做的好处是什么呢?
看以下代码:
int chose;
printf("%d",&chose);
switch (chose)
{
case 1:
Add(); //执行加法
break;
case 2:
Sub(); //执行减法
break;
case 3:
Mul(); //执行乘法
break;
case 4:
Div(); //执行除法
break;
case 0:
return 0; //退出
}
这是一个建议计算器的选择模块,如果使用 switch
语句,后面的 case
后面的数字代表不同的算法。像以上代码,如果算法增加到即使或者更多,那么,数字和算法可能会匹配乱掉,但是,如果改为用枚举变量,那么代码的可读性以及思路清晰程度会大大提高。如下:
enum shose
{
EXIT,
ADD,
SUB,
MUL,
DIV
};
int chose;
printf("%d",&chose);
switch (chose)
{
case ADD:
Add(); //执行加法
break;
case SUB:
Sub(); //执行减法
break;
case MUL:
Mul(); //执行乘法
break;
case DIV:
Div(); //执行除法
break;
case EXIT:
return 0; //退出
}
这就很清楚的体现了枚举类型的用途。
枚举的优点
- 增加代码的可读性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量。
联合体(共用体)
联合体是一种特殊自定义类型,和结构体有一点点相似。
联合类型的声明
union Un
{
char c;
int i;
};
它的类型声明,变量定义的方法和结构体是一样的。
联合的最大特点就是它的所有的成员是从同一地址处开始存放的,例如以上联合 union Un
其结构就是联合体的首地址处开始存放一个 char
型的变量c
,同时也存放一个 int
型的变量 i
,因为都是从联合体的首地址开始存储的,所以两个变量的第一个字节是重合的,也就是改了变量 c
就是改了 i
的第一个字节,修改了 i
的第一个字节就是修改了 c
变量。
这个联合体在内存中是这样存储的:
联合体大小计算
- 联合体的大小至少是最大成员的大小。
- 当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。
联合体的巧妙使用:
union ip_addr
{
unsigned long addr;
char part[4];
}
以上联合体可以将一个 long
型的ip地址转化为逆序的点分十进制形式。
有没有很神奇呢?
自定义类型就到此结束了!