自定义类型详解

目录

 结构体

结构体的声明:

特殊的声明:

结构体的初始化:

 嵌套结构体时的初始化:

结构体的自引用:

typedef 类型重命名

结构体的内存对齐:

为什么会有结构体内存对齐?

修改默认对齐数

结构体传参

位段

 什么是位段?

 位段的内存分布

枚举

为什么使用枚举?

如何使用枚举

联合体

 联合体的创建:

联合体的使用

如何用联合体确认当前编译器的大小端存储方式?

联合体的大小计算


使用C语言的时候,我们往往需要一些稍微有一定自定义的变量类型来方便我们实现一些功能,比如我们想将一个人最基本的信息打包起来,每个人都是一个单独的包装,里面的信息独立存储,这个时候我们就可以使用结构体来帮助我们管理。

不过拿到了扳手不晓得用也不太行,那么如下将对结构体的一些性质以及其“家属”做一些详解。

 结构体

结构体的声明:

结构体就是一些值的集合,这些值被称为成员变量,每个成员的变量类型可以是不一样的。

相当于对一些变量类型进行了一次打包,我们打包一个“人”最基础的信息,包括他的名字和年龄。

创建方法如下:

请不要忘记它的末尾花括号要加一个分号(虽然大多数情况下在VS2019里面会自动补齐)

 这个时候,我们创建出来的结构体,也就是这个名字叫people的阿sir,他就变成了一个变量类型,也就是我们自己创建了类似于int 或者 char 这种变量,那么其实它的使用方法也差不多。

结构体创建的是一个类型,所以可以不用初始化,关于结构体的初始化将会在后面提到。

另一种创建方法:

S1S2指的是用这个结构体类型所创建出来的变量,像是打包的集合

这个变量的属性取决与创建的位置,当在main函数外创建时则是全局变量,相应的,在main函数内创建就是局部变量

当然,变量的创建方法是有多种的。

arr【20】代表创建了由20个struct people这个结构体类型所构成的数组,也就是这个数组里每个成员都是一个结构体,每个结构体里都有两个成员。一个是name【】另一个是age

而*p则代表了指向这个结构体的指针,其类型是一个结构体指针

但以上的创建并没有初始化这个结构体,初始化则有一些不同。

特殊的声明:

结构体声明的时候,可以匿名声明,也就是不加上标签:

 这样创建出来的结构体只能使用一次,即创建结构体变量的时候不给予名字,这种方法创建的结构体称为匿名结构体类型

那么我们可能会产生疑惑,失去了名字的结构体,他们会不会是一个东西呢?

匿名结构体成员一样,在编译器看来也是不同类型的结构体,所以其实是不一样的

结构体的初始化:

结构体变量的初始化也是有多种方法的,第一个箭头就是在创建结构体类型的时候直接进行一个初始化,第二个箭头则是在主函数内部创建变量并进行初始化,两种方法都是可以的

 嵌套结构体时的初始化:

当我们尝试在结构体里套结构体的时候,它的初始化是这样的

当有嵌套的情况出现时,只需要按着顺序再加一个花括号并存放入对应类型的数据即可

结构体的自引用:

错误方法:

 正确方法:

 这个变量在此时其实已经算作创建出来了,若是想要正确的访问这个当前创建好的结构体类型,使用指针是有必要的。

typedef 类型重命名

使用结构体的时候,不仅要写出创建好的名称,还要在前面加一个Struct,这着实让人头大,毕竟写这么一大串玩意儿还是很不爽的,那么我们可以借用typedef 类型重命名这个方法解决这个痛点。

类型重定义,取名为Node

pNode 等价-- > struct Node* ,对结构体指针重命名pNode,是一种指针类型

这样子重定义之后,我们使用结构体的时候就可以不用往前面加一个Struct了。

结构体的内存对齐:

结构体了解的差不多了,现在我们就应该了解一下这个类型的大小了。

struct S1
{
    char c1;
    int i;
    char c2;
};

思考一下,这个结构体的大小是多少?1+4+1,6?

答案是12

为啥?

在这里我们不能直接想当然的求取整个结构体的大小,过于简单的规则拎出来讲就很逊了。

 内存对齐规则:

1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。

VS中默认的对齐数值为8 ;Linxu环境下没有默认对齐数,没有默认对齐数时,自身大小就是对齐数
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。


再回到刚才的那个结构体上,它在内存中的占用如图所示:

 我们照着内存对齐的规则来看整个结构体。

1. 第一个成员在与结构体变量偏移量为0的地址处:我们可以看到c1确实处在了偏移量为0的位置。

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS的默认对齐数为:8,8与4比取小,也就是4,此处的int就需要修正到偏移量为4的地方存放,也就是如图的绿色区块所示。

这个时候只剩下c2还没放进去了,不过只剩它一个了,直接放进去就好,但是这有个疑问,那我这横竖都求的是9啊?这也凑不到12啊?

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍:这个结构体的最大对齐数是4,我们求出来的9显然不符合条件,那么我们就需要按规则修正到4的整数倍,也就是12.

这样,我们就求出来了这个结构体的大小。


那么再来几个:

struct S2
{
    char c1;
    char c2;
    int i;
};

我们还是就着规则来分析,先把图放这了。

1. 第一个成员在与结构体变量偏移量为0的地址处:c1存放在了偏移量为0的位置

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。对于c2来讲在这里,VS默认为8,c2为1,8 >1 取1,对齐到1的倍数上而对于i:VS默认为8 i 为 4 ,8 > 4 取4,对齐到4的倍数上,往偏移量为4的位置放置。

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍:这里直接凑到了8,满足条件,无需修正。

结果就是8


 大的要来了,当出现了结构体嵌套的情况又该怎么处理和计算呢?

struct S3
{
    double d;
    char c;
    int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

1. 第一个成员在与结构体变量偏移量为0的地址处:c1存放在了偏移量为0的位置

接下来的则是嵌套的结构体,我们使用规则4对敌:

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
什么意思呢?简单来讲就是先把当前嵌套的结构体里头最大的对齐数求出来,在这里我们可以很简单明了的看出来,S3里头最大的对齐数是由double所提供的对齐数8,那么我们为了在内存里对齐这个结构体,我们就需要寻找8的倍数的偏移量进行存放。

目前我们只是找到了我们需要存放这个结构体的位置,这个结构体究竟占了多少内存空间还是未知数,由于我们已经找到了对齐位置接下来直接先把当前结构体的第一个变量存进去就好。

我们接下来处理S3内部的C,

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。   8 > 1 我们直接找到1的倍数直接存就好。

存完了C,还有一个int i

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。8>4  我们选择4作为对齐数,16已经被c占用了,打不过就跑,修正到20

那么只剩个d了,我们发现嘿!这个d很听话的直接呆在了8的倍数的位置,不需要修正,我们直接存就好。

 那么这就是高频考点结构体的内存对齐的知识了,更多的像是套公式,其中画图还是非常重要的。

那么这么麻烦的玩意到底为什么存在?直接老老实实加不香吗?

为什么会有结构体内存对齐?

其实这个问题还是非常简单的,假如你买了一盒月饼,你肯定希望里头所有的月饼至少在包装以及大小上面是统一的,大大小小的往一盒里塞不仅不美观分类起来也困难。

回到程序上来:结构体的内存对齐是拿空间来换取时间的做法。

1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到: 让占用空间小的成员尽量集中在一起。

所以,S1会次于S2

struct S1
{
    char c1;
    int i;
    char c2;
};

struct S2
{
    char c1;
    char c2;
    int i;
};

修改默认对齐数

之前我们见过了 #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));
}

结论:
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

结构体传参

结构体传参的方法有两种,一种为传值,一种为传地址

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;
}

哪一种好呢?

答案是传地址。

我们知道,调用函数的时候会创建栈帧,如果我们直接传入结构体,会额外开一个形参的空间,结构体本身还是比较大的,这样子会浪费空间,所以我们在传参的时候传递结构体的地址为最佳。

位段

 啥位段?这个我熟!我黑铁!

 什么是位段?

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

1.位段的成员必须是 int、unsigned int 或signed int 。

2.位段的成员名后边有一个冒号和一个数字

这个冒号后面的数字是啥意思?数字代表占bit位个数

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

 那么这个位段的大小是多少?

 位段的内存分布

1. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

空间不够开辟1/4个字节空间,不够继续开辟1/4个字节空间

2. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段,或者写出跨平台代码。

3. 位段的成员可以是 int unsigned int signed int 或者是 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. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

枚举

枚举类型,故名思意,相当于对一种类别的情况一一列举,比如一周的每天称呼周一到周天等。

本质来讲是列举。

创建方法如下:

enum Day//星期
{
    Mon,
    Tues,
    Wed,
    Thur,
    Fri,
    Sat,
    Sun
};
enum Sex//性别
{
    MALE,
    FEMALE,
    SECRET
};

枚举类型的每个变量隔开需要使用逗号。

上面定义的 Day , Sex 都是枚举类型,其花括号内部为这个类型的可能取值。

对于这些可能取值来说,他们是有值的,默认从0开始,依次递增1.

当然我们也可以直接为其赋值。

enum Color//颜色
{
    RED=1,
    GREEN=2,
    BLUE=4
};

为什么使用枚举?

 我们可能会有一个疑问,可以用#define定义的数字,为什么偏要用枚举?

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

如何使用枚举

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

那么这段代码会不会出现问题?

clr = 5; //ok??

5的变量类型是int 而clr的变量类型是enum ,这段代码是错误的。

联合体

 联合体这个变量类型比较有 “包容性”  联合体变量类型内部的成员是共享同一块存储空间的。

 联合体的创建:

 联合体的成员变量之间用分号隔开。

union MyUnion
{
	int i;
	char n;
};

联合体的使用

 访问联合体内部的成员变量需要使用.


int main()
{
	union MyUnion Un;

	printf("%d\n", sizeof(Un));
	printf("%p\n", &Un);
	printf("%p\n", &(Un.i));
	printf("%p\n", &(Un.n));

}

 我们可以发现,对于i和n来讲,他们的地址是同一个起点开始的。

 当我们改变i时,c的值也会被改变

    u.c = 0x55;
    u.i = 0;

 但是当我们去使用内部的成员变量的时候,并不会出现重合的问题,同一时间只会使用一个变量。

如何用联合体确认当前编译器的大小端存储方式?

这是一道非常有意思的题目,我们借用联合体的性质可以很简单的解决这个问题。

因为联合体的内部存储是共享的,我们只需要监视其第一个字节的变化即可,如果我们更改了其中的变量,字节内部的存储也发生了变化那就是小端存储,反之则大端存储

int check_sys()
{
	union Un
	{
		char c;
		int i;
	}u;
	u.i = 1;
 
	return u.c;
}
 
int main()
{
	if (1 == check_sys())
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

联合体的大小计算

 联合的大小至少是最大成员的大小,因为联合至少得有能力保存最大的那个成员。

当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

union Un1
{
    char c[5]; //5
    int i; // 4
};
union Un2
{
    short c[7];
    int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));// 5个字节大小的Char可以容得下int,但是
                                  //存在内存对齐,联合体的内存对齐于其最大对齐数的整数倍
                                  //也就是int的整数倍数,所以结果是8

printf("%d\n", sizeof(union Un2));//Un2也是同理,short c的大小是14 ,可以容下int,short的
                                  //对齐数是2,int是4,取4,最大大小14,向4的倍数对齐,取16

 至此结束,感谢阅读!希望对你有点帮助!

部分图片来源于@北方留意尘

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值