c语言自定义类型——结构体、枚举、联合

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的成员又进行了修改。

结构体的内存对齐

我们都知道,是变量都要占用内存的空间,那么,结构体变量也是一样的,结构体是我们自己定义的类型,那么它所占的空间肯定是有计算方法的。

首先我们知道,结构体的成员都是有自己独立的空间的,而且成员之间互不影响,那么我们就可以得出,一个结构体所占的空间至少等于所有成员所占的空间之和。
但实际上呢,并不是这样的,因为结构体存在内存对齐:

内存对齐

内存对齐就是结构体在内存的存储时,各个成员并不是顺序存放,而是按照一定的规则进行存放,而这个规则就是内存对齐规则。

为什么存在内存对齐?

  1. 平台原因:并不是所有的硬件平台都可以读取内存中的任意地址的数据,一些平台上只能读取特定类型的数据,也就是它只能取特定大的内存块的数据,说白了就是只能访问某个数字倍数的地址。
  2. 性能原因:数据结构应该尽可能的在自然边界上对齐,因为如果访问未对齐的内存时,处理器肯能要进行两次甚至两次以上内存访问。这必然会降低处理器的效率。

内存对规则

在介绍内存对齐规则之前,我先介绍两个名词:偏移量,编译器默认对齐数

首先是偏移量,在结构体的存储中,存放结构体的第一个内存单元就是偏移量为0的地址,如图:
结构体内存对齐

其次是编译器默认对齐数,linux环境下默认为4,windows环境下默认为8。

开始介绍对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认对齐数与该成员大小的较小值。
  3. 结构体总大小为最大对齐数(每个成员变量都有一个自己的对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

那么根据内存对齐规则我们就很准确的计算出结构体的大小了。这里我简单计算一下上面结构体 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 intsigned 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 代表 0mon代表1tues 代表 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;           //退出
}

这就很清楚的体现了枚举类型的用途。

枚举的优点

  1. 增加代码的可读性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 防止了命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量。

联合体(共用体)

联合体是一种特殊自定义类型,和结构体有一点点相似。

联合类型的声明

union Un
{
    char c;
    int i;
};

它的类型声明,变量定义的方法和结构体是一样的。

联合的最大特点就是它的所有的成员是从同一地址处开始存放的,例如以上联合 union Un 其结构就是联合体的首地址处开始存放一个 char 型的变量c,同时也存放一个 int型的变量 i,因为都是从联合体的首地址开始存储的,所以两个变量的第一个字节是重合的,也就是改了变量 c就是改了 i 的第一个字节,修改了 i 的第一个字节就是修改了 c 变量。

这个联合体在内存中是这样存储的:
联合体在内存中的存储

联合体大小计算

  1. 联合体的大小至少是最大成员的大小。
  2. 当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。

联合体的巧妙使用:

union ip_addr
{
    unsigned long addr;
    char part[4];
}

以上联合体可以将一个 long型的ip地址转化为逆序的点分十进制形式。

有没有很神奇呢?

自定义类型就到此结束了!

猜你喜欢

转载自blog.csdn.net/qq_38590948/article/details/80456395