C语言常见的自定义数据类型(1)—— 结构体

目录

1、结构体

1.1 结构体的定义

1.2 结构体的自引用

1.3 结构体类型的重命名

1.4 结构体的嵌套

2、结构体大小的计算

2.1 结构体内存对齐

2.2 嵌套结构体大小的计算

2.3 offsetof函数

2.4 修改默认对齐数

2.5 结构体内存对齐的意义

3、结构体传参

①结构体传参

②结构体地址传参

4、位段

4.1 什么是位段

4.2 位段的内存分配

4.3 位段的跨平台问题

4.4 总结:


在C语言中,常见的数据类型有整型int、浮点型float、字符类型char等,而仅仅用这些简单的数据类型来描述现实世界是远远不够的,如描述一本书时,要想通过定义一个变量涵盖这本书的名字,出版社,作者等信息,只用简单的数据类型是不能实现的,因此,C语言还规定了几个常见的自定义类型:结构体、枚举、联合体。

1、结构体

1.1 结构体的定义

C语言中,可以使用结构体来实现存放一组不同类型的数据。结构体也可以认为是一些值的集合,这些值称为成员变量,结构体的每个成员可以是不同类型的变量。

struct tag
{
	member list;//成员列表
}variable list;//变量列表

如对一个学生的姓名、年龄、成绩进行描述时定义的结构体:

#include<stdio.h>
struct Stu
{
	char name[20];//成员变量
	int age;
	double score;
};

int main()
{
	struct Stu s1;//定义一个结构体变量s1
return 0;
}

1.2 结构体的自引用

如果有五个节点分别为1 ,2 ,3, 4 ,5 ...... 如果要像顺序表一样,按照一个线性的关系把它存起来,即通过1可以找到2,通过2找到3 ...... ,以此类推。可以把2节点的地址存到1节点,把3节点的地址存到2节点 ...... ,以此类推,在最后一个节点处存放空指针。如下定义了一个链表式的结构体:

#include<stdio.h>
struct Node
{
	int data;//数据域
	struct Node* next;//指针
};
int main()
{
	struct Node n;

return 0;
}

1.3 结构体类型的重命名

与其他数据类型一样,结构体类型的重命名同样可以用typedef来实现:

#include<stdio.h>
typedef struct Node
{
	int data;//数据域
	struct Node* next;//指针
}Node;

int main()
{
	struct Node n1;
	Node n2;
	//n2和n1的类型是一样的

return 0;
}

1.4 结构体的嵌套

如下列代码,在结构体 struct Node 里又嵌套了结构体 struct Book:

#include<stdio.h>
struct Book
{
	char name[20];
	float price;
	char id[12];
}s={"加油",55.5f,"haha001"};

struct Node
{
	struct Book b;
	struct Node* next;
};

int main()
{
	//struct Book s2={"努力",55.3f,"yeah002"};//也可以这样创建结构体变量
	struct Node n={{"gogogo",89.2f,"lyd"},NULL};

return 0;
}

2、结构体大小的计算

有如下两个结构体 S1、S2 :

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

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

它们的成员列表相同,而排列顺序不同,由简单数据类型的大小可知,char 类型占一个字节,int 类型占4个字节,这样我们可以简单估算两个结构体的大小为6个字节,实际上真的会是这样吗?下面利用 sizeof 来进行检验计算:

#include<stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	struct S1 s;
	struct S2 s2;
	printf("%d\n",sizeof(s)); //12
	printf("%d\n",sizeof(s2)); //8

return 0;
}

可以发现,在内存中S1结构体占12个字节大小,S2结构体占8个字节的大小,那么是什么原因导致这样的结果呢?

2.1 结构体内存对齐

结构体对齐规则:

1、第一个成员在与结构体变量偏移量为0的地址处。

2、其他成员变量要对齐到对齐数的整数倍的地址处。

(对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(VS默认的值为8))

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

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

这样,我们再看上面的S1和S2结构体:

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

对结构体S1:假设这个结构体在一个位置处开辟空间要存储,第一个成员c1在与结构体变量偏移量为0的地址处,即在0处;对第二个成员变量,对齐到对齐数的整数倍的地址处,4与8比较4较小,4是对齐数,所以i放到(4的倍数)与结构体变量偏移量为4的地址处,占4个字节,c2为1个字节,1与8比较1较小,1是对齐数,所以c2放到与结构体变量偏移量为8的地址处。又因为结构体总大小为最大对齐数的整数倍,4与1相比,4为最大对齐数,所以现在总共占了9个字节的大小,不是4的倍数,所以还要继续开辟3个字节的空间,即12个字节的空间,所以这个结构体的大小为12个字节。

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

同理对结构体S2:假设这个结构体在一个位置处开辟空间要存储,第一个成员c1在与结构体变量偏移量为0的地址处,即在0处;对第二个成员变量,对齐到对齐数的整数倍的地址处,1与8比较1较小,1是对齐数,所以c2放到(1的倍数)与结构体变量偏移量为1的地址处,占1个字节,i为4个字节,4与8比较4较小,4是对齐数,所以i放到与结构体变量偏移量为4的地址处。又因为结构体总大小为最大对齐数的整数倍,4与1相比,4为最大对齐数,现在总共占了8个字节的大小,是4的倍数,所以这个结构体的大小为8个字节。

2.2 嵌套结构体大小的计算

看下面代码,在结构体S4中嵌套了结构体S3,求S4结构体的大小:

#include<stdio.h>
#include<stddef.h>
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%d\n",sizeof(struct S4));//32

return 0;
}

根据结构体对齐规则第四条,如果有嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

c1大小为1字节,放到与结构体变量偏移量为0的地址处,S3可知为16字节大小,对齐到8(嵌套的结构体对齐到自己的最大对齐数的整数倍处)地址处,d大小为8字节,对齐到24地址处,此时总共占了32字节,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍,32是8的整数倍,所以S4大小即为32字节。

2.3 offsetof函数

——计算结构体成员相对于起始位置的偏移量

size_t offsetof( structName, memberName );

该函数的两个参数分别为结构体名和成员名,头文件为:<stddef.h>。

#include<stdio.h>
#include<stddef.h>
struct S3
{
	double d;
	char c;
	int i;
};

struct S2
{
	char c1;//1
	char c2;//1
	int i;///4
};

int main()
{
	printf("%u\n",offsetof(struct S3,d)); //0
	printf("%u\n",offsetof(struct S3,c)); //8
	printf("%u\n",offsetof(struct S3,i)); //12
	//printf("%d\n",sizeof(struct S3)); //16

	printf("%u\n",offsetof(struct S2,c1)); //0
	printf("%u\n",offsetof(struct S2,c2)); //1
	printf("%u\n",offsetof(struct S2,i)); //4
return 0;
}

2.4 修改默认对齐数

上面介绍到,在VS编译器中默认对齐数为8(linux环境下没有默认对齐数,此时自身的大小就是其对齐数),在对齐方式不合适的时候,我们可以更改默认的对齐数。

利用#pragma pack( )修改默认对齐数:

#include<stdio.h>

#pragma pack(4) //设置
//#pragma pack(1)----9    改成1时实际上就是按照不对齐方式存储的
struct S
{
	char c;
	double d;
};

#pragma pack() //取消

int main()
{
	struct S s;
	printf("%d\n",sizeof(s));//12

return 0;
}

可知,当修改默认对齐数为1时,结构体大小变为9字节,此时就是按照不对齐方式存储的;当修改默认对齐数为4时,此时结构体大小变为12字节。

2.5 结构体内存对齐的意义

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

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

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

总的来说,结构体内存对齐是拿空间来换取时间的做法。在上面的代码中,改了下成员的顺序,开辟的空间就不一样了。所以,在设计结构体的时候,如果考虑既要满足对齐,又要节省空间,可以让占用空间小的成员尽量集中在一起(这样在一定程度上浪费的空间就会更少)。

3、结构体传参

比较下列结构体传参的两种方式:

①结构体传参

#include<stdio.h>
struct S
{
	int data[1000];
	int num;
};

void print1(struct S s)
{
	printf("%d\n",s.num);
}

int main()
{
	print1(s);//传结构体

return 0;
}

②结构体地址传参

#include<stdio.h>
struct S
{
	int data[1000];
	int num;
};

void print2(struct S* ps)
{
	printf("%d\n",ps->num);
	printf("%d\n",(*ps).num);
}

int main()
{
	print1(&s);//传结构体地址

return 0;
}

以上两种方式都可以实现对结构体成员信息的打印。不过,函数在传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。第一种方式把整个结构体传参,如果结构体过大,参数压栈的时候系统开销比较大,会导致系统性能的下降;而第二种方式只需要传一个指针,而指针的大小是固定的,4字节(32位平台)或者8字节(64位平台),所以结构体传参的时候,尽量传结构体的地址。

4、位段

了解了结构体,就不得不提结构体实现位段的能力了。

4.1 什么是位段

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

1. 位段的成员必须是 int 、unsigned int 或 signed int (char 其实也可以)

2. 位段的成员名后边有一个冒号和一个数字,数字代表占用 bit 位的数量

比较结构体和位段的不同:

结构体:

//结构体
struct A
{
	int _a;
	int _b;
	int _c;
	int _d;
};

位段:

//位段
struct B
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

对于结构体A,如果不考虑对齐,结构体A需要4个整型即16个字节的内存;对于位段B,_a这个成员只需要占2个bit位,_b这个成员占5个bit位,_c这个成员占10个bit位,_d这个成员占30个bit位,总共只要47个bit位,所以只用两个整型空间即可存下:

#include<stdio.h>
//结构体
struct A
{
	int _a; //一个整型的取值范围为INT_MIN ~ INT_MAX
	int _b;
	int _c;
	int _d;
};

//位段
struct B
{
	int _a : 2; //2个比特位 只能放 00 01 10 11 四个数, 如果有一个变量正好只有这几个取值,可以考虑用位段
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%d\n",sizeof(struct A)); // 16   
	printf("%d\n",sizeof(struct B)); // 8    ---- 确实只需要两个整型的空间
return 0;
}

注意:

结构体A中的整型成员_a,取值范围为INT_MIN ~ INT_MAX;

位段B中的成员_a由于只占两个bit位的大小,意味着只能放 00 ,01 ,10, 11 四个数。

所以在定义结构体的时候,如果发现某些成员是不需要那么大的空间的,只需要少量的bit位就够了,这个时候使用位段在一定程序上节省了空间。

4.2 位段的内存分配

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

对这个位段(均为int),在内存存储的时候,先开辟了一个int类型的空间,即4个字节,先存_a,占了2个bit位,还有30个,再存_b,又占了5个bit位,还剩25个bit位,再存_c,还剩15个bit位,再存_d的时候,空间不够了,考虑再开辟一个int类型的空间,就能放下_d了。

此时考虑一个问题:放_d的时候是接着后面放还是在新开辟的那个int类型里的空间去放?

假设存储的时候从低位开始,空间不够了在新开辟的空间去存,不够的空间浪费掉,最后推算的应该是需要3个字节,下面用一个例子去验证:

#include<stdio.h>
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = {0};
	printf("%d\n",sizeof(s)); // 3

return 0;
}

由打印结果可知,我们的推断是正确的,确实是3字节。

4.3 位段的跨平台问题

在使用位段的时候,同时要注意:

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

2. 位段中最大位的数目也是不确定的 (16 位机器最大16,32 位机器最大 32,如果写成27,在16位机器会出问题);

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

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

由于以上的不确定性,造成了位段有跨平台的问题。

4.4 总结:

1. 位段的成员可以是int 、 unsigned int 、  signed int 或是char(属于整型家族)类型
2. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的

3. 在VS环境下:位段的在内存中的空间开辟与存储规则是:当存不下的时候开辟空间,放的时候直接在新开辟的空间里去存,之前放不下的空间就浪费掉了

4. 与结构相比,位段可以很好的节省空间,但是有跨平台的问题所在,注重可移植的程序应该避免使用位段

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值