首页 > 系统相关 >C语言结构体内存对齐

C语言结构体内存对齐

时间:2024-08-15 10:28:32浏览次数:10  
标签:变量 int C语言 char 内存 对齐 体内 结构

结构体或许小伙伴们都知道,或许也能够做到熟悉的去运用结构体,但你们有没有想过:整型数组存放的数据都是整型,字符数组存放的数据都是字符,它们类型相同,所以也都能够做到在内存中紧密的存储而结构体中存放的数据各种各样,它们的存储是否能做到在内存中紧密排列呢?又或者说,结构体的内存应该怎样去计算呢?今天让我们来一起探讨一下这个问题~

首先,在了解结构体内存如何计算之前,我们先来了解一下存址的相关知识,高地址和低地址以及数据的高字节和低字节。

一、高地址与低地址

当我们把数据对应的指针存储在计算机给定空间时,为了方便查找会给地址进行编号,我们可以把它们看作一本书:第一页是0x00000001,第二页是0x00000002,第三页是0x00000003......一直到最后一页0x00000100.(这里是随便给的数,方便理解,并没有实际意义)。我们看书肯定是从第一页开始看,那么0x00000001就是低地址,相对的最后一页0x00000100就是高地址。

二、高字节与低字节

当我们小学学数学的时候,我们知道数字有个位,十位,百位。它们对应的值都成比例的越来越大,因此它们也分为高位和低位:一个三位数里百位就是高位,个位就是低位,这是十进制中的算法。当然相对的,二进制,十六进制也都分高位和低位:

而对应的这个二进制数中高位的0001就是高字节,1111就是低字节。也就是说高位就对应着高字节,低位对应着低字节

三、大端与小端存储模式

上面我们已经了解了高地址低地址,高字节低字节,那么字节存放在地址中应该以何种顺序存放呢?这就引出了我们的大端与小端存储模式。

大端(存储)模式: 是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。

小端(存储)模式: 是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。

(计算机读址的顺序是从低地址开始读向高地址)

我们假设一个16进制数字num为0x01 02 03 04,那么大端模式下将它存储,存放顺序就应该为:

而在小端模式下将它存储,存放顺序应该为:

大端存储和小端存储各有优缺点

大端存储的优缺点:大端存储在直观性和顺序性方面具有优势,但在传输效率和空间利用率方面略逊于小端存储。

小端存储的优缺点:小端存储虽然在节约空间和提高读写速度方面有优势,但违背了人类的直观认识,并可能引发平台依赖性问题。

四、结构体内存对齐

① 结构体内存对齐的规则

前面花费一些功夫把高低地址,高低字节以及大小端存储问题给大家讲解清楚了,终于可以引出我们这篇文章的主要内容:结构体内存对齐了。回想一下我们最开始提出的问题:结构体中存放的数据各种各样,它们的存储是否能做到在内存中紧密排列呢?又或者说,结构体的内存应该怎样去计算呢?让我们来举个例子:

struct Stu1 {
	int age;
	char name;
	int id;
	char sex;
};
struct Stu2 {
	char sex;
	char name;
	int age;
	int id;
};
int main()
{
	int num1 = sizeof(struct Stu1);
	int num2 = sizeof(struct Stu2);
	printf("num1长度为:%d\nnum2长度为:%d\n", num1, num2);
	return 0;
}

如果按照之前计算整型数组和字符数组大小的常规思路来判断,这两个结构体的大小应该是相等的,一个int型变量占4个字节,一个char型变量占1个字节,那么Stu1有两个int型变量和两个char型变量,大小理应为4+4+1+1=10,同样的Stu2也应为10。那让我们将代码运行一下看看是不是这样的:欸?奇怪了,不仅两个结构体的大小不为10,甚至两个结构体的大小都不相等,这是怎么一回事呢?其实结构体Stu1在内存中的存址形式是这样的:为什么会是这样的存储形式呢?这就是结构体内存对齐所造成的

结构体内存对齐的规则

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

其他成员变量要对齐一个叫"对齐数"的数字的整倍数的地址。

(对齐数的概念)

  • 在不同的编译器中,默认的对齐数也有所不同,Visual Studio Code的编译器的对齐数是8,Linux的编译器的对齐数是4。
  • 在结构体存址时,对齐数取(编译器默认的对齐数)和(该成员变量大小)中较小的数。

结构体所占内存大小等于最大对齐数(结构体中每个成员变量都有一个对齐数各成员变量中最大的对齐数)的整数倍

如果结构体有嵌套,那么嵌套的结构体存储在自己成员的最大对齐数的整数倍地址处,嵌套结构体大小为所有成员(包括嵌套的结构体的成员)的最大对齐数的整数倍

② 结构体内存对齐数的运算

通过这一段定义大家应该能够大概了解结构体内存对齐的基本概念了,但过于笼统的概念读起来或许会让大家有点难以理解,对于其中的对齐数,或许大家还带有一些疑虑和困惑,那么接下来我给大家具体的讲解一下对齐数到底应该如何计算,和结构体存储到底怎么样才能避免浪费空间

比如此时我们创建一个结构体变量:

struct Stu1 {
	char name;
	int id;
	char sex;
};

然后让我们来分析一下将这个结构体大小该如何计算:

我们先将结构体变量中定义的第一个成员变量存放,存放后样式应该是这样的。

在这一步还看不出什么,那么我们紧接着将第二个成员变量也计算进去,看看现在的样式:

我们发现结构体的大小一下子从1变成了8,并且其中出现了三个浪费的空间,那么造成这种情况的原因是什么呢?让我们来一起计算一下它现在的对齐数

我们知道,结构体中每一个成员变量都有对应的对齐数当结构体Stu1只存放一个char name时char name的对齐数为1,VS编译器默认的对齐数为8,而结构体存址时,对齐数取(编译器默认的对齐数)和(该成员变量大小)中较小的数也就是说此时结构体存址,对齐数取1

而结构体大小又要是各成员变量对应对齐数的最大对齐数的整数倍也就是说此时结构体大小应该为1的整数倍。所以此时我们计算结构体的大小时,打印出的是1。而之后我们的结构体中又多了一个int id的元素,当int id作为第二个成员变量进入结构体时,需要对int id再进行一次对齐数的计算那么此时int型变量对齐数为4同样取(编译器默认的对齐数)和(该成员变量大小)中较小的数,此时结构体存址,对齐数取4。

如果按照正常运算,在这里应该大小为4+1=5。但结构体大小必须为各成员变量对应对齐数的最大对齐数的整数倍,此时结构体大小必须为4的整数倍。所以我们只能浪费三个空间,最终占用8个空间。接下来我们再将char sex加入结构体中,让我们一起猜一下现在结构体的大小会是多大呢

没错,现在结构体的大小就是12啦,因为现在对齐数已经变为4了,所以还需要再浪费3个空间,让大小变为对齐数的整数倍~

那么接下来让我们来探究一下结构体发生嵌套时,大小应该如何计算吧~(上面的对齐规则中提到过)

struct Stu1 {
	char name;
	char sex;
	int age;
};
struct Stu2
{
	char name1;
	struct Stu1 s1;
	char age;
};

我们先来计算一下结构体变量Stu1的大小:因为前两个char型变量的存入不会改变各成员变量中最大对齐数所以可以连续存放而等存入int型变量时,各成员变量中最大对齐数就变成了4于是结构体大小就必须要被4整除,所以浪费两个空间,结构体大小为8。

然后我们再来探究一下结构体嵌套情况下,结构体的大小Stu2的计算由于计算嵌套结构体中最大对齐数时,需要将被嵌套的结构体中的成员变量也列入其中所以虽然Stu2中除了结构体Stu1 s1以外只有大小为1的char型变量但最大对齐数仍需要算上Stu1 s1中的int型,对齐数从而变成了4使得两个char型变量都浪费了3个空间

③ 结构体内存对齐存在原因

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

性能原因:原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问就足够了。或许光这样说,大家还是会听的云里雾里,不明白为什么有时候需要两次内存访问,而有时候仅访问一次就够了。那么接下来我用作图的方式为大家详细的讲解一下:

假设我们在结构体中创建一个char a和int b两种变量,分别用结构体内存未对齐结构体内存对齐的两种存放地址的方法,来查看处理器访问内存的情况

假如我们的处理器现在一次会读取四个字节

当处理器进行读取时,读取a后,对于b的读取只读取了一半,如果想将a完全读取,就需要再读取一次,于是就进行了两次访问。(因为假设一个处理器总是从内存中取4个字节,如果我们没有将所有数据对齐存放,我们可能需要执型两次内存访问,因为对象可能被分放在两个4字节内存块中。)

而内存对齐时,两个数据处于对齐的状态,都均匀的分布在各自的4字节内存块中而处理器每次进行访问都访问四个字节,所以访问时并不需要对b分次访问两次,而是访问一次就足够了

总而言之结构体的内存对齐是拿空间来换取时间。 

好啦,那么关于上篇讲解内存函数的文章中所提到的结构体内存对齐的知识就为大家分享到这里啦,如果讲解上有什么问题,或者哪里出现了错误,还请大家多多指出,我也会虚心学习的!我们下一期再见啦ヾ( ̄▽ ̄)Bye~Bye~

标签:变量,int,C语言,char,内存,对齐,体内,结构
From: https://blog.csdn.net/ixiaotang_/article/details/141157237

相关文章

  • 【C语言】sizeof 和 strlen
    sizeof和strlen的对比sizeof在学习操作符的时候,我们学习了sizeof,sizeof计算变量所占内存内存空间大小的,单位是字节。如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。sizeof只关注占用内存空间的大小,不在乎内存中存放什么数据。比如:#include......
  • C语言发展史
    在线书籍:54笨鸟1.C语言发展史任何一种新事物的出现都不是来自于偶然,而是时代所驱使的必然结果。1.1C语言有多伟大如果你问我:C语言有多伟大。那么,我可能会想一下,说:多伟大我不知道,但是我知道很伟大。这里,我想说一句可能有点片面的话,就是:如今这世界上,凡是带电的地方,可能......
  • 初学者如何学c语言
    你对学习如何编程感兴趣吗?如果是的话,你从C语言编程开始是不会错的。C语言是一种通用的、强大的、广泛使用的编程语言,可用于构建操作系统、开发视频游戏,甚至创造机器人。然而,对于初学者来说,学习C语言编程可能是令人生畏的。这就是为什么我们把这个初学者指南放在一起,让你开......
  • C语言中水平制表符 \t 与退格键 \b 的使用方法探索
    经个人实践,C语言中使用转义序列码(\t)会输出一个8个格数的组合,当\t之前的内容达到8*n格时,后续内容出现在8*(n+1)+1格。(式中n>=1,且为整数)探索过程如下:第一次在看到某大佬的科普中提到:\b将输出位置左移一位\t表示一个tab的距离即1个大空格,相当于4个小空格......
  • C语言函数(上)
    前言与概述笔者打算通过两篇文章详细介绍C语言函数的相关知识,本文章将会介绍C语言函数定义、C语言函数分类、函数参数、函数调用。下一篇文章将会介绍函数声明、链式访问,并详细介绍函数递归。笔者不才,如有错误,欢迎各位编程大佬在评论区批评指正。C语言函数定义维基百科中将C......
  • 关于C语言学习的第二天
    今天学习的是C语言,本次接着上次的内容继续开始,主要是说C语言中数据类型的内容,如有疑问可以翻找我上一篇关于C语言的内容。首先是冯诺依曼机,它规定指令和数据都是存储在内存中的,都是以二进制形式存储的。那为什么是使用二进制呢?因为这在电器元件中易实现,且双稳态元件较多。而二......
  • C语言基础11指针
    指针的引入为函数修改实参提供支持。为动态内存管理提供支持。为动态数据结构提供支持。为内存访问提供另一种途径。指针概述内存地址:系统为了内存管理的方便,将内存划分为一个个的内存单元(1个内存单元占1个字节),并为每一个内存单元进行了编号,内存单元的编号称为该......
  • c语言中输入问题,scanf遇到空格就会停止输入
    一.问题描述:在c语言当中使用scanf进行输入字符串时,遇到空格会停止输入,如下面的例子。#include<stdio.h>intmain(){ chars[30]; scanf("%s",s); printf("%s",s); return0;}如下图可看出当输入“Helloworld!”时,从输出可以看出只能读入“Hello”。二.原因:在C......
  • c语言转换char字符数组为大写小写
    #include<string.h>#include<stdlib.h>#include<stdio.h>#include<time.h>#include<ctype.h>#include<sys/stat.h>voidgetdate(char*datestr,char*format){ time_tnnowtime=time(NULL); structtm*ptmTemp=loc......
  • c语言替换字符串 Replace the first ‘oldstr‘ with ‘newstr‘ in ‘srcstr‘
    #include<string.h>#include<stdlib.h>#include<stdio.h>#include<time.h>#include<ctype.h>#include<sys/stat.h>voidgetdate(char*datestr,char*format){ time_tnnowtime=time(NULL); structtm*ptmTemp=loc......