浅谈C语言结构体以及内存对齐规则


前言

结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂类型.学会正确使用结构体是很有十分重要的。本文将围绕结构体的进行相关的介绍。


1、结构体简单介绍

1.结构体的定义

结构体是C语言中一种重要的数据类型,该数据类型由一组称为成员(或称为域,或称为元素)的不同数据组成,其中每个成员可以具有不同的类型。 结构体通常用来表示类型不同但是又相关的若干数据。

这是百度百科给出的定义,总的来说结构体是一种数据类型。结构体相当一个集合,但是集合中元素类型可以各不相同。结构体就好像一个超市,超里可以有日用品,服饰,食品等等各种类型不同的商品。结构体的成员可以是整型 ,浮点型,等等

2为什么要有结构体

我们知道C语言是提供了很多种数据类型的,比如整型,浮点型,字符型等等。但是在生活中是有很多复杂类型的,当我们用上述这些单一的数据去描述这些复杂对象时,这些单一的数据类型根本无法做到。就比如,怎么描述一个人。用任意一个单一的数据类型都好像无法准确的描述。一个人要有姓名,性别,年龄,身份证号码。上述这些数据结合在一起基本上就可以表述出某个人。但是这些数据怎么结合在一起,用数组也不合适啊,数组只能存放相同类型的数据元素。姓名应该用字符串表示,年龄应该用整型表示,身份证号码也是整型表示,性别应该用字符型表示。为了将这么多不同的数据的类型集中在一起表示,于是结构体就诞生了。所以才说结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂类型

2、结构体的使用

1.声明结构体类型

结构体声明要使用到C语言关键字struct,同时在后面接着这个结构体的标签,所谓标签就是相当于给结构体类型起个名字用于区分其他的结构体,我们知道指针用 * 表示,int * 就是整型指针,结构体也是如此,比如struct s s1,s1的数据类型就是struct s,结构体内部都是成员,成员的类型可以相同也可不同。结构体是使用者自己创建的一种数据类型,所以要先声明,再使用。如果不事先声明,编译器就无法识别

代码示例

#include <stdio.h>
struct person
{
    int id;
    char name[10];
    char sex[6];
    int age;
};
int main()
{
    struct person s1 = { 2022,"zhangsan","nan",12 };
  
    
}

我们首先声明了一个结构体类型 struct person.这结构体类型有4个成员,结构体的标签是person,定义并初始化话一个结构体类型变量s,s的初始化要和结构成员的顺序保存一致。同时结构在声明之后可以接着直接定义结构体变量
在这里插入图片描述
刚才提到结构体标签是用来区分其他标签的,也可以省略不写,不写就是匿名结构体,匿名结构体只能使用一次也就是如上图所示一样在声明结构体后直接定义结构体变量

在这里插入图片描述
这个结构体类型就是没有名字的匿名结构体类型,只能使用一次也就是在声明这个结构体体类型的时候,同时如果需要初始化只能紧接着初始化,在实际上编程过程中基本上都不会使用这样的写法,因为没有什么意义。

如果创建两个了匿名结构体,就算结构体的成员一样,编译器还是会认为是两种类型的结构体

在这里插入图片描述
在这里插入图片描述
编译器会显示类型不兼容。

2.结构体的嵌套

1.结构体是允许嵌套的使用的
给出如下代码示例
在这里插入图片描述
在结构体类型b中有一个成员的数据类型是是结构体类型a,这是允许的
但是不能嵌套自己,因为在计算结构体变数据类型大小的时候,会无限套娃,算不出来

在这里插入图片描述
如果要算结构体b类型的大小,肯定要计算成员s的大小,但是成员s是结构体b类型,又要算结构体b类型,陷入一种死循环的状态

如果结构体变量想找到相同类型的结构体,将相同类型的结构变量关联起来怎么做呢?
这里简单提下链表,链表就是把在逻辑上相邻的元素,物理存储位置不同的元素按顺序像链条一样串起来。

在这里插入图片描述
这些1 2 3 4 5像一个个节点一样在内存中随机排布,为了实现链表这种数据结构经常使用的一种方法就是定义一个结构体类型表示这些节点,其成员分别是用来存储元素的数据域和找到下一个节点的地址域。地址域通常是指针变量,指针变量的数据类型就是节点结构体类型指针。
在这里插入图片描述

所以用指针就可以将相同类型的结构体变量关联起来,而不是错误的的定义一个相同结构体类型的成员变量,因为不管任何类型的指针的大小都是8或者4

3.访问结构体成员

结构体成员的成员的访问就是变量名+一个点+成员变量名
代码示例如下
在这里插入图片描述
上述提到指针,除了这样的访问方式外,还可以利用指针访问。
在这里插入图片描述
指针访问有两种方式,一个是解引用访问,还有一个就是利用->操作符访问

4,结构体重命名

在使用结构体数据类型的时候,要经常写struct关键字,有没有什么方法直接使用结构体标签来表示结构数据类型,确实有,就是使用typedef对结构类型进行重命名。

在这里插入图片描述
当成员变量是有类似于上述链表中的地址域的指针类型时,在声明结构体时不可以省略struct,因为typedef的重命名还未生效
在这里插入图片描述

3.结构体的内存对齐

1.内存对齐规则

结构体的大小怎么计算,看下面代码的,结构体a ,结构体b,两种类型的结构体成员除了顺序不一样,其他的基本上没差别。这两个结构的体大小是多少,是一样的吗?想搞懂这个问题就得先了解结构体内存对齐规则

typedef struct a
{
    char i;
    char s;
    int id;
}a;

typedef struct b
{
    char s;
    int id;
    char i;
  
}b;

结构体内存对齐规则一共有4条
1.第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 为编译器默认的一个对齐数与该成员大小中的较小值。
vs中默认值是8
3.结构体总大小为最大对齐数的整数倍。(每个成员变量都有自己的对齐数)
4.如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。

按照上述规则描述我们来分析一下开头的代码中两个结构体类型的大小

在这里插入图片描述
在这里插入图片描述
前面两个例子都没有结构体嵌套的情况,我们加上结构体嵌套的情况,在分析一次。

typedef struct a
{
    char i;
    char s;
    int id;
}a;

typedef struct b
{
    char s;
    int id;
    char i;
    a s1;
}b;

在这里插入图片描述
以上的结论都是分析出来的,还需要验证一下,我们将代码vs上运行,查看结果

在这里插入图片描述在这里插入图片描述

结果和我们推测的一样,就是8 ,12,20

2.为什么要有内存对齐规则

1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

在这里插入图片描述
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。

4.设置默认对齐数和查看结构体成员起始位置相较于结构体起始位置的偏移量

利用#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()//取消设置的默认对齐数,还原为默认

结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
利用宏offsetof可以查看成员的起始位置相较于结构体起始位置的偏移量
给出如下代码示例
在这里插入图片描述

#include<stdio.h>
#include<stddef.h>//包含头文件使用
typedef struct a
{
    char i;
    char s;
    int id;
}a;

int main()
{    printf("%d\n", offsetof(a,s));//第一个参数是结构体类型,第二个参数是成员
     printf("%d\n", offsetof(a,i));//对struct a,进行了重命名,所以直接写a
     printf("%d\n", offsetof(struct a, id));//没有重命名就写struct a
     return 0;


}

4.结构体传参

结构体传参,既可以传对应类型的结构体,还可以使用指针传参

#include<stdio.h>
struct S
{
	int num;
};
//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);
	
}
//结构体地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	struct S s = {  1000 };
	print1(s); //传结构体
	print2(&s); //传地址
	return 0;
}

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。结构体传参的时候,要传结构体的地址

总结

1.结构类使得C语言有了描述复杂对象的能力,结构体是一中数据类型,不同的结构体标签,代表了不同的结构体数据类型。结构体的声明和成员都由由使用者自己决定创建。
2.结构体的大小要使用内存对齐规则计算
3使用宏offsetof可以查看结构体成员的起始位置相较于结构体起始位置的偏移量。
4使用#pragma pack可以修改vs默认对齐数。
4,结构体传参最好使用地址传参。
5.以上是我对于结构体的简单介绍。如有问题,欢迎指正。

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值