自定义类型:结构体,枚举,联合

本文详细介绍了C语言中的结构体、枚举和联合(共用体)三种自定义类型,包括它们的创建、声明、内存对齐、枚举的使用以及联合体的特性。特别强调了结构体内存对齐的原则和计算方法,以及位段的内存分配和跨平台问题。同时,文章讨论了枚举类型的优点,并给出了联合体大小计算的规则。
摘要由CSDN通过智能技术生成

本文主要围绕,结构体,枚举,联合这三种日常代码编写过程中最常见的自定义类型进行叙述和详解。

目录

一、结构体

1.1结构体的创建与声明

1.2特殊结构体类型 

1.3结构体的自引用

1.4结构体变量的定义和初始化

​编辑

1.5结构体内存对齐

1.7修改默认对齐数

1.8结构体传参

二、位段

2.1位段

2.2位段的内存分配

2.3位段的跨平台问题

三、枚举

3.1枚举类型的定义

3.2枚举的优点

3.3枚举的使用

四、联合(共用体)

4.1联合类型的定义

4.3联合体大小的计算


一、结构体

1.1结构体的创建与声明

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体的声明:

  通过结构体我们可以联想到数组,数组是一组相同类型元素的集合,而结构体可以包含不同类型的成员变量。我们可以自定义结构体的声明,比如学生是student我们可以自定义为stu,而大括号中所包含的成员就叫做成员列表,而大括号之后的variable-list就是变量列表,可以用来创建结构体变量。

例如用结构体来描述一个学生:

如果想要在创建结构体时就顺便创建几个相关的结构体变量,我们就可以在大括号后面创建变量列表。如下图,s1,s2,s3就是struct stu的三个变量。

1.2特殊结构体类型 

在声明结构体时候,可以不完全声明。这种结构体类型就是匿名结构体类型。

 当创建一个匿名结构体类型时,我们就无法在进行例如对main函数进行编写时来为其创建变量,只能在创建匿名结构体类型时直接进行变量创建,或者是直接在匿名结构体类型后面创建一个结构体指针指向它,比如上图的*p。注意,虽然上面创建了两个一样的结构体类型,但在编译器看来,这是两个不同的匿名结构体类型。如果我们在写代码时将&x也就是x的地址赋给*p时,编译器会进行报错处理:

 不兼容就代表着它们不是一个类型,就好比将char*类型赋给int*,两种完全不同的类型,编译器肯定是会报错的。

  在这里也只是简单介绍匿名结构体类型,在日常代码编写中,匿名结构体类型的使用也是较少的,没有必要,尽量不使用。

1.3结构体的自引用

  接触过数据结构的应该都知道,数据结构描述的是数据在内存中的组织结构,有一种叫线性数据结构,线性数据结构分为顺序表和链表,顺序表是指在内存中开辟一块连续的空间进行存储。而链表则是一串数据在内存中的存放并不连续,但可以通过前一个数据找到下一个数据的具体位置,而结构体想要进行自引用找到下一个节点就需要采用这种链表的结构。

 如上所示,如果想要通过类似于链表的形式来找到下一个节点,那么结构体内部组成就要分为两部分,即数据域与指针域,数据域用来存放数据,指针域用来链接下一个结构体变量的地址。

而相比于上面,下面这种编写方式就是错误的,在日常代码编写中也是尤其需要注意的

 如果将第二个指针域直接写成结构体的变量,那么其是否可行呢?其实仔细观察就可以发现,其中是存在着很大的不合理性的,当struct node在内存中开辟空间时,int占了4个字节,往后进入struct node next,然后再开辟4个字节,再进入...是不是发现函数好像进入了类似于无限递归的形式,当我们用sizeof去计算这个结构体的大小时,我们会发现,它的大小是无法计算的。

1.4结构体变量的定义和初始化

  与其他类型一样,也可以通过直接在结构体后面加变量名称来进行变量的定义:

 而对结构体成员进行赋值时,可以采用默认顺序或者通过 . 操作符来进行赋值操作。

 当然结构体变量中也可以包含结构体类型(需要用大括号将其括起来):

以下就是几种结构体变量定义和初始化的方式总结:

struct Point
{
 int x;
 int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2


//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化

struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

1.5结构体内存对齐

标准规定:

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

 结构体内存对齐,直接来讲就是计算结构体大小。观察以下这一段代码:

 按照正常的方式来计算的话,int占4个字节,char占1个字节,站在一般角度来看,s1和s2应该都是6个字节才对,但当程序运行起来,后我们却得到了两个截然不同的结果:

 而这种结果正是由结构体内存对其方式决定的。我们可以通过引用#include <stddef.h>来使用offsetof,offsetof可以查找结构体成员与起始位置的偏移量。(注意offsetof是一个宏,不是函数)

 它的第一个参数是type(类型)第二个参数是member(成员)。

 拿刚刚的s1距离,我们可以看到三个变量c1 i c2 的偏移量分别是0 4 8,根据偏移量我们大概可以得出结构体类型在内存中的存储方式如下:

 如果仅仅根据偏移量来看,应该也只有九个字节,可是刚刚打印出s1的大小却是12,所以在8的下面还有三个字节。

 通过上面现象分析,我们发现结构成员不是按照顺序在内存中连续存放的,而是有一定对其规则的。

通过查看上面的标准规定,所以s1在存储时,c1存放在0的位置char占一个字节,和默认对齐数8比,1小所以放在0的位置占一个字节。i作为int型占4个字节,和默认对齐数8比4小,所以找4的倍数,所以从4开始往后存4个字节。c2作为char占一个字节任何一个整数都是1的倍数,所以直接放在8的位置,而因为结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍,s1中最大对齐数为4,而现在一共才9个字节,不是4的倍数,所以要凑够4的倍数,而往后最小的4的倍数就是12,所以再往后开辟3个字节。(注意:开辟时看倍数的是下标,最后根据最大对齐数来看倍数决定总大小时看的是此时内存所占空间)。

 

 同样的s2第一个成员类型是int,所以从0开始往下开辟四个字节,c1占一个放在4的位置,c2放在5的位置,此时总共占了6个字节,此时最大对齐数是4,往后最小的4的倍数就是8,所以此时只需要8个字节就可以存放s2,所以再往后开辟两个字节的空间到7,此时刚好8个字节。

 不同的情况,结构体开辟空间的大小就不一样,如果有double类型那最大对齐数和最终大小都会不一样:

 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。如下图:

 为什么会存在内存对齐?

大部分的参考资料都是如是说的:
1. 平台原因 ( 移植原因 )
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特

 定类型的数据,否则抛出硬件异常。

2. 性能原因

数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
问。
总体来说:
结构体的内存对齐是拿 空间来换取 时间的做法。
而通过对内存对其的了解,今后在代码编写的过程中,我们可以合理的利用不同类型数据内存对齐的方式来达到减少浪费空间、节约内存提高效率的目的。

1.7修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
#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;
}

结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。 例:当用pargma将对齐数设置成1时,就意味着没有对齐了。

1.8结构体传参

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函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。(具体内容可查看: 函数栈帧的创建与销毁)。

二、位段

2.1位段

位段的声明和结构是类似的,有两个不同:

1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。

举例:

struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

此时,运行程序,得到的结果是8个字节。

2.2位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型。
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
在得知位段是如何分配后,我们可以回顾上一个代码,首先开辟四个字节也就是32个bit位来用于数据的存储a占2个,b占5个,c占10个,d占30个。加起来一共47个bit位,而一个整形4个字节32个bit位,因为类型是int,所以再次开辟4个字节来存放,所以最终的结果就是8个字节。
而为了更好更直观的看到位段的内存分配,我们采用char类型来当作样例,一次只开辟一个字节,更加方便观察和分析。
//一个例子
struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的?

 所以先开辟1个字节即8个bit'位来存放,a有3个bit位的空间,10的二进制表示形式位01010,存三位进去存的就是010。b为12,二进制位为01100,存4位进去就是1100。

  因为先使用高地址再使用低地址,此时开辟的第一个字节的8个bit位已经用了7个还有1个已经不够存放之后的数据了,所以此时需要再开辟一个字节,来存放后面的数据。

  c为3,二进制位为011,有5个bit位的空间,不够的就用0补齐,所以存放进去就是00011,此时存放d,又不够了就再开辟一个字节,b的二进制位为00100,存放4个进去就是0100。

  而数据在进行存放时,因为大多数电脑的存储方式都是小端存储,所以数据的低位就存放在低地址处,高位存放在高地址处,a最先创建是struct S的低位所以先在低地址处为其开辟1个字节即8个bit位的空间用来存储,而将a存放进这8个bit时,数据在使用内存时,先使用高位,再使用低位,然后将a放在这8个字节的高位,随后存放b,此时虽然ab所在空间还有1个字节,但是因为其处在低位,而后面开辟的空间则是在整个ab所在字节的更高位,所以这1个字节就无法进行使用了,然后不够后按照此步骤继续开辟空间进行存放(中间环节如有不懂可参考数据在内存中的存储(一))因为在查看内存时一般转换成16进制数字(每4个二进制位可转换成1个16进制位),所以最终struct S在内存中的布局大概就是这样:

 我们在编译器中进行内存查看:

 和我们推演出来的结果一样。

此测试以VS平台进行测试,综合测试数据如下:

2.3位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。

三、枚举

3.1枚举类型的定义

枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举。
enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};

enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
};

enum Color//颜色
{
 RED,
 GREEN,
 BLUE
};
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
在{}中的内容是枚举类型的可能取值,也叫枚举常量。
这些可能取值都是有值的,默认从0开始,依次递增1,在声名枚举类型的时候也可以赋初值。
例如:
enum Color//颜色
{
 RED=1,
 GREEN=2,
 BLUE=4
};

如果只给其中一个赋值,那么前面的值依然是默认的值,后面的常量会在此基础上继续递增,例如:

enum Color//颜色
{
 RED,
 GREEN=5,
 BLUE
};

那么RED就是0,而BLUE就是6。

3.2枚举的优点

在对常量进行定义时,我们往往第一个想到的是使用#define来定义常量,为什么非要使用枚举。

枚举的优点:
1. 增加代码的可读性和可维护性。
2. #define 定义的标识符比较枚举有类型检查,更加严谨。
3. 便于调试。
4. 使用方便,一次可以定义多个常量。

3.3枚举的使用

enum Color//颜色
{
 RED=1,
 GREEN=2,
 BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5;               //ok??

四、联合(共用体)

4.1联合类型的定义

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
比如:
//联合类型的声明
union Un
{
 char c;
 int i;
};

//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));

4.2 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是 最大成员的大小(因为联
合至少得有能力保存最大的那个成员)。如下图所示:
union Un
{
 int i;
 char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));

程序运行之后,可以看到,显示的地址是一样的,由此可见,联合的成员是共用同一块内存空间的。

4.3联合体大小的计算

1.联合的大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
 char c[5];//5个字节
 int i;//4个字节
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));

 虽然,char[5]所占的内存是最大的,是五个字节,char类型最大对齐数是1,int类型最大对齐数是4,而char[5]作为最大成员占5个字节,但整个联合的最大对齐数是4,5不是4的整数倍所以要对齐到最大整数倍,往后4最小的倍数就是8。所以最后结果应该是8。

  本章内容就到此结束了,每一篇文章都是博主的精心打磨,耐心编排。更多好文关注博CSDN。一键三连不迷路。

  • 30
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C+五条

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值