C语言中除了有基本数据类型外还有一些自定义类型,前面我们对基本数据类型进行了详细解释说明(基本数据类型博客链接:https://mp.csdn.net/editor/html/109584674),这次我们来对自定义类型进行详细讲解。
一、结构体
1.结构体的声明
结构体是一些值的集合,这些值称为结构体的成员变量。这些成员变量以是任意类型(整型、字符型、指针、结构体等)。结构体的声明需要用到关键字struct,具体声明如下:
一般声明
struct structName
{
MemberList;//成员列表
}VariableList;//变量列表,这里的分号不能缺失
匿名声明
struct
{
MemberList;
}VariableList;
注:两个匿名声明的结构体,即使其参数列表完全相同,编译器也会将其声明为两个完全不同的结构体。
问题:结构体声明时,如果其成员变量是该结构体类型,该如何声明?
这个就是结构体的自引用问题。结构体成员变量为结构体类型,无非两种方法---结构体类型、结构体指针类型。但是,这两种方法都可行吗?
如果结构体成员变量声明为结构体类型,那么该结构体就成了一个无限“递归”的结构体(成员列表里的结构体类型中又会有一个为结构体类型的成员,这样就无限“递归”),编译器将无法为其分配空间。因此,结构体成员变量需要声明为结构体类型时,我们需要将其声明为结构体指针类型。具体声明如下:
struct stu
{
char name;
int age;
struct stu * p;
};
2.结构体变量的定义和初始化
结构体变量的定义有以下三种方式:
方法1:声明结构体的同时定义结构体变量
strcut point
{
int x;
int y;
}point1;//point1为定义的结构体变量
方法2:结构体关键字+变量名定义
struct point point1;//point1为定义的结构体类型
方法3:用结构体定义
typedef strcut point
{
int x;
int y;
}point1;
point1 point2;//point2为定义的一个结构体变量
结构体的初始化
定义变量的同时进行初始化:struct Point p3 = {x, y};struct Node{int data;struct Point p;struct Node* next;}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
3.结构体内存对齐(结构体大小计算)
在结构体中有一个重要的知识,就是结构体内存对齐,也是很多公司笔试常考的一个知识点。只有掌握结构体内存对齐,才能学会计算结构体大小。结构体类型不同于其他类型,结构体类型是一种重要的自定义类型,同时结构体类型大小计算也不同于其他类型,结构体类型大小不是直接将结构体成员变量类型之和,在计算结构体大小时最重要的一个知识就是内存对其。
什么是内存对齐?结构体内存对齐的规则是什么?
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(VS中默认的值为8)。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举个栗子:
为什么存在内存对齐?
大部分的参考资料都是如是说的:
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的 内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
利用上面的内存对其知识计算下面两个结构体的大小,你发现了什么问题?
struct S1{char c1 ;int i ;char c2 ;};struct S2{char c1 ;char c2 ;int i ;};
经过计算我们会发现,两个结构体的大小不一样,结构体S1的大小为12,结构体2的大小为8,仔细观察我们会发现两个结构体的成员变量完全相同只是顺序不同。这也就说明,结构体大小不仅跟成员变量类型有关也跟成员变量的顺序有关。其实,归根结底都是内存对其所导致的问题。
修改默认对齐参数
前面我们说过,不同编译器的对齐参数可能不同,但对齐参数我们可以根据实际需要进行修改(#pragma pack()进行修改,括号内可以是任意想要修改的对齐数的整数值),具体修改如下:
#include <stdio.h>#pragma pack(8) // 设置默认对齐数为 8struct S1{char c1 ;int i ;char c2 ;};
4.结构体传参
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。 因此结构体传参的时候,要传结构体的地址。相应的形参也就为结构体指针来接受实参传的结构体的地址。
再来举个栗子
struct S{int data [ 1000 ];int num ;};struct S s = { { 1 , 2 , 3 , 4 }, 1000 }; //定义结构体变量并初始化// 结构体地址传参void print2 ( struct S * ps ){printf ( "%d\n" , ps -> num );}int main (){print2 ( & s ); // 传地址return 0 ;}
5.位段
什么是位段?位段又是如何分配内存的?
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int(signed int )、unsigned int。
2.位段的成员名后边有一个冒号和一个数字。
例如:
struct A{int _a : 2 ;int _b : 5 ;int _c : 10 ;int _d : 30 ;};注意:位段跟结构体的一样都需要使用关键字struct而且位段的结尾的分号也是不可缺少的,位段后边的数字代表分配的位数
位段的内存分配
我们知道,一个字节占8个比特位,而位段分配内存就是按比特位来分配的。有以下规则:
1.第一个成员变量直接分配类型个大小,例如,上面位段中先分配4个字节,这4个字节的前两个比特位用来存放a的值。
2.如果前一个开辟的空间没有放满,且可以存下将要存放的字符,则不再开辟新的空间,直接在原有空间继续存储。如,第一次开辟了4个字节的空间(32个比特位)而只使用了2个还剩30个,b和c一共占15个比特位,该空间可以存储下,则b,c接着a继续存储,三个成员变量公用这四个字节的前17个比特位。
3.如果前一次开辟的空间已满或者剩余空间不足以存储当前成员变量,继续根据当前成员变量类型大小开辟新的空间。例如,d占30个字节,而上次开辟的4个字节的空间已使用17个还剩15个,不足以存储30个比特位的数据,所以系统会新开辟4个字节的空间来存储d。
即,段位A的大小为8个字节。
下面,画图演示一下段位在内存中的存储:
内存中结构如下:
位段的跨平台问题:
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
二、枚举
枚举就是将可能的取值一一列举。枚举的定义需要使用关键字enum,枚举的定义如下:
例如:一周有7天,将这些全部列举出来就可以表示为下面的形式。enum Day // 星期{Mon ,Tues ,Wed ,Thur ,Fri ,Sat ,Sun};//这里的分号也是必不可少的
枚举中的常量也是有一定取值的,如果不写则默认从0开始依次递加。如果定义了取值,则从定义位置开始依次按照所给的值进行递加,前边没定义的还是从0开始。
例如:
1.第一个开始定义enum Day // 星期{Mon = 1 ,Tues = 2 ,Wed = 3 ,Thur = 4 ,Fri = 5 ,Sat = 6 ,Sun = 7};//只要第一个写了,后边的即使不写也是上边定义的值2.从某一位置开始enum Day // 星期{Mon ,Tues ,Wed = 5 ,Thur ,Fri ,Sat ,Sun};//向上边这种,Mon = 0,Tues = 1,从wed开始从5依次递加。
枚举的作用(优点)
我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#defifine定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,依次可以定义多个常量
三、联合
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块 空间(所以联合也叫共用体)。联合的定义要使用关键字nuion
联合的声明和定义:
//联合类型的声明union Un{char c;int i;};//联合变量的定义union Un un;//计算连个变量的大小printf("%d\n", sizeof(un));
联合类型的特点:
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为 联合至少得有能力保存最大的那个成员)。
联合大小的计算
1.联合的大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
计算下面程序的运行结果:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
根据联合大小的计算我们知道,Un1的最大对齐数是int而Un1的最大成员的大小是1*5 = 5,5不是4的整数倍。所以,Un1的大小是8.Un2中最大对齐数是int而Un2的最大成员大小为2*7=14个字节,不是最大对齐数的整数倍,所以,Un2的大小为16.
面试题:利用联合判断当前计算机的大小端存储。