C语言自定义类型详讲---结构体、联合、枚举

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) // 设置默认对齐数为 8
struct 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.

 

面试题:利用联合判断当前计算机的大小端存储。

猜你喜欢

转载自blog.csdn.net/qq_47406941/article/details/109648333