自定义类型:结构体

目录

1. 结构体类型的声明

1.1 结构体的使用

1.1.1 结构体的声明

1.1.2 结构体变量的初始化

1.2 结构体的特殊声明

1.3 结构体的自引用

2. 结构体的大小

2.1 结构体内存对齐

2.1.1 对齐规则

2.1.2 为什么存在内存对齐?

2.1.3 修改默认对齐数

3. 结构体传参

4. 结构体实现位段

4.1 什么是位段

4.2 位段的内存分配

4.3 位段的跨平台问题

4.4 位段使用的注意事项


1. 结构体类型的声明

1.1 结构体的使用

1.1.1 结构体的声明

结构体是利用struct关键字进行声明的

struct Stu
{
	char name[20];//姓名
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};

//为了方便后续使用,可以使用typedef进行重命名
typedef struct Stu
{
	char name[20];//姓名
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
} Stu;

1.1.2 结构体变量的初始化

初始化结构体变量可以直接按顺序初始化结构体变量,也可以通过“.操作符”进行乱序初始化

int main()
{
	
	Stu stu1 = { "zhangsan", 18, "man","2345365543" };
	Stu stu2 = { .age = 15, .sex = "woman", .id = "6545463156", .name = "lisi" };

	return 0;
}

当然,还可以使用->初始化结构体变量

1.2 结构体的特殊声明

在结构体声明时,可以不完全的声明

//匿名结构体
struct
{
	char c;
	int i;
	double d;
} s;

int main()
{
	s.c = 'c';
	s.i = 4;
	s.d = 1.1;
	printf("%c %d %lf\n", s.c, s.i, s.c);
	return 0;
}

注意:
匿名结构体基本上只能使用一次

1.3 结构体的自引用

在结构体中包含一个结构体本身的成员可行吗?

例如,定义一个链表节点:

struct Node
{
	int date;
	struct Node next;
};

很明显,这样是不合理的,若是真这么定义的话,是不是就类似于没有递归终点的递归呢,这样大小就会变成无穷大

正确的自引用方式:

struct Node
{
	int date;
	struct Node* next;
};

通过包含一个指向另一个结构体成员的指针来实现结构体的自引用,后续要操作下一个结构体可以通过指针去访问

2. 结构体的大小

先来看看猜下下面这个结构体大小是多少

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

有的人可能认为结构体大小就是所有成员变量大小之和,所以这个结构体大小是6,那么这是对的吗?

很遗憾,你猜错了。此时有人会问12是怎么计算得出来的呢?

想要计算结构体的大小,那么我们就要先知道一个概念:结构体内存对齐

2.1 结构体内存对齐

2.1.1 对齐规则

1. 结构体的第一个成员对齐和结构体变量起始位置偏移0的地址处

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

    对齐数 = 编译器默认的一个对齐数 与 该成员变量的大小的较小值

    - VS 中默认的值为 8

    - Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小

3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍

4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

//先来看看该结构体各成员变量的对齐数

struct S
{
    char c1;//对齐数为1
    int i;//对齐数为4
    char c2;//对齐数为1
} ;

//最大对齐数为4

从偏移量为0的地址开始,c1占据一个字节;而后第二个成员变量i的对齐数为4,所以它要从偏移量为4的倍数开始,储存四个字节的大小;第三个成员变量c2的对齐数为1,所以它要对齐到1的倍数就可以了,此时共占据了九个字节的大小;最后,因为结构体的大小为最大对齐数的整数倍,所以,该结构体的大小为12

再来看个嵌套结构体的大小

struct S1 //大小为16
{
	double d;//对齐数为8
	char c;//对齐数为1
	int i;//对齐数为4
};

struct S2
{
	char c1;//对齐数为1
	struct S1 s1;//对齐数为8
	double d;//对齐数为8
};

从偏移量为0的位置开始,c1占据一个字节;s1的大小为16,s1的最大对齐数为8,所以s1占据的空间为从偏移量为8的地址开始的16个字节的空间;d的对齐数为8,所以d占据的空间为偏移量为24开始的8个字节的空间;S2的大小为32个字节。

2.1.2 为什么存在内存对齐?

大部分的参考资料都是这样说的:

1. 平台原因(移植原因):

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

2. 性能原因:

为了访问未对齐的内存,处理器需要作两次访问;而对齐的内存访问仅需一次访问。假设一个处理器总是从内存中取8个字节,此时需要读取一个double类型的数据,若是数据的地址都对齐成8的整数倍,那么只需要一个内存读取操作就可以了。如果地址没有对齐成8的整数倍,那么double类型的数据可能会被分放在两个8字节内存块中,此时则需要两个内存读取操作才可以完成。

总结来说:结构体的对齐是拿空间来换取时间的做法。

在设计结构体时,为了做到既满足对齐,又节省空间,我们需要做到:

让占用空间小的成员尽量集中在一起

//例如:

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

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

此时S1和S2成员变量相同,但S1的大小为12个字节,而S2的大小为8个字节。

2.1.3 修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对齐数

#pragma pack(1)        //设置默认对齐数为1

#pragma pack()          //取消设置的对齐数,还原成默认  

3. 结构体传参

结构体传参和其他传参相似,都有传结构体和传结构体地址两种方式。
但是,需要进行结构体传参时,请尽量选择传结构体地址。

原因:

函数传参时,参数是需要压栈,会有时间和空间上的系统开销

如果传递一个对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

4. 结构体实现位段

4.1 什么是位段

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

1. 位段的成员必须是int、unsigned int 、signed int 或 char(整型家族),在C99中位段成员的类型也可以选择其他类型。

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

比如:

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

位段的其实是二进制位的位,上述位段总计47个比特位,所以位段大小为8个字节。

那么位段是用来干什么用的呢?

struct S
{
	int _a;
	int _b;
	int _c;
	int _d;
};

设想一下,若是有以上结构体,其四个成员变量分别储存0,1,2,3。这四个整数中,最大的3仅占2个比特位,但是却都消耗了一个整型(32个比特位)的大小,那么是不是会显得很浪费?此时使用位段就能节省空间了。

总的来说:位段就是用来节省空间的

4.2 位段的内存分配

1. 位段的成员可以是 int、unsigned int 、signed int 或 char 等类型

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

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

这里讲一下VS中位段的内存分配规则

1. C语言中并未规定位段的使用是从左向右还是从右向左,此时内存的使用方式就取决于编译器,VS的使用方式是从右向左

2. VS中,使用位段时会先按照所选择类型分配一块空间,若位段足够容纳下一个位段成员的大小时,位段会继续存放,直到位段不能容纳下一个位段成员的大小时,才会申请下一块空间,此时会舍弃剩余的位

4.3 位段的跨平台问题

1. int位段被当成有符号数还是无符号数是不确定的

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出现问题

3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

4.4 位段使用的注意事项

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置时没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。

所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入另一个变量,再用这个变量给位段成员进行赋值。

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值