浅谈C语言字节对齐

首先,我们得知道为什么要进行内存对齐,它的意义何在?在这儿可以先看这样一张图。(手绘请见谅!!!)

这里写图片描述
我们知道,在32位CPU下,一个读取周期可以读取四个字节。一个字符变量在内存中占一个字节,而整型为在内存中占4个字节。
那么CPU一个读写周期刚好能读取一个整形,所以如上边左图,两个整型变量分别读写一次即可;而上面中间的图,第一个读写周期读取的四个字节为一个char变量和一个整形变量的前三个字节,第二个读写周期读取的是整型变量剩余的那一个字节及其后面的三个字节的空间。这样就导致该整型变量 用了两个读写周期才读写成功,而且还要对两个读写结果的高地地址拼凑才能得出该数据。大大降低了读写效率;上边右图是中间这幅图采用内存对齐后的效果,第一个读写周期读写一个字符变量和三个字节的无内容的空间,第二个读写周期读写了一个整型变量。这样,同样是读写连续定义的一个字符变量和一个整型变量,第二次整型变量需要读写两次,第三次却只用读写一次。
因此要采用字节对齐的方式,可以理解为用空间换时间。

字节对齐是一个小知识点,但并不容易掌握,先给出字节对齐的几种基本概念:
1、 学好字节对齐需要掌握的一些基本概念:
(1)基本数据类型的自身对齐值:
1字节:char型
2字节:short型
4字节:int,float,long、指针类型
8字节:doublel类型
对齐规则是:第一个变量从程序所占所占空间偏移量为0的位置开始存放,第二个及以后的变量从上一个变量所占空间的末尾之后找到的第一个偏移量为该变量所占空间大小的整数倍处开始存放,同时满足加起来的值是4的倍数(避免一个变量需要多次读写及拼接才能读写成功)。
(2)指定对齐:编译器提供#pragma pack(n)来设定变量以n字节对齐方式。(#pragma pack(0)可以恢复默认对齐)注:指定值必须是 2 的 N 次方。
(3)自定义类型的自身对齐值:结构体或类的成员中自身对齐值最大的值。(结构体的大小必须是最大对其数的整数倍结构体的成员中把所占字节较小的成员集中存放可以节省空间,缩小结构体所占空间大小,成员存放是的对其规则满足基本数据类型的对其规则)
(4)自定义类型的有效对齐值:自定义类型的自身对齐值和指定对齐值(或边界对齐值)中较小的值。

以上几条规则基本概括了字节对齐的情况,需要掌握,据此,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。

注:VS、VC编译器的默认对齐数是8、GCC编译器的默认对齐数是4

2、几种基本概念的对应实例:
(1)基本数据类型的自身对齐值:

#include<stdio.h>int main()
{int i;char c;return 0;
}

这里写图片描述

可以看到字符变量C开辟的空间和整形 i 开辟的空间是不连续的,而是隔了中间三个字节(i 和 c 中间隔了4个字节,但C只占一个字节,还有三个字节是空着的)。为什么呢?字节对齐很容易就解释了。此时没有指定对齐,所以按自然对齐来看,即边界对齐。c 是后定义的,自身对齐值为一个字节,而要满足 c 的自身对齐值加上和 i 之间隔的空间等于 i 的自身对齐值(4个字节)且是 4 的倍数,那么c 和 i 之间要隔三个字节才行。

(2)、自定义类型的自身对齐值(结构体或类的成员中自身对齐值最大的值):
对结构体来说,要满足结构体成员中已经定义的所有成员的自身对齐值加上其后面空出来的字节数之和是下一个要定义的成员的自身对齐值的倍数,且所有成员的自身对齐值加上其后面空出来的字节数(自定义的结构体类型所占字节数)是结构体自身对齐值的倍数。

#include<stdio.h>typedef struct Test
{char c;short s;int i;double d;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}

这里写图片描述

这里结果为16,怎么来的?看下图即可:

这里写图片描述

(3)自定义类型的有效对齐值(自定义类型的自身对齐值和指定对齐值中较小的值):
对结构体来说,要满足每一个成员的自身对齐值加上其后面空出来的字节数是有效对齐值的倍数,且所有成员的自身对齐值加上其后面空出来的字节数(自定义的结构体类型所占字节数)是结构体有效对齐值的倍数。

#include<stdio.h>#pragma pack (4)typedef struct Test
{char c;double d;int i;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}

这里写图片描述

按上一题的理解,这儿应该是24,但怎么又是16呢?

关键在于———–#pragma pack (4)

看图说话:

这里写图片描述

(4)关于自定义类型的自身对齐值再来看两个例子:

#include<stdio.h>#pragma pack (4)typedef struct Test
{short s;struct{int i;double d;char c;};long l;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}

结果如下图:

这里写图片描述

#include<stdio.h>typedef struct Test
{short s;struct A{int i;double d;char c;};long l;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}

结果如下图:

这里写图片描述

以上两段代码看着一样,但为什么结果却不一样呢?细心就会发现第二段代码在内嵌的struct后面加了一个 A。
当内嵌的struct后面没有 A 时,它是一个结构体
当内嵌的结果提后面有 A 是,它是一个类型,因为类型肯定是可以定义变量的,那么这个定义出来的变量肯定是有大小的,那样才能存储数据,这个大小就是又类型决定的,所以类型肯定是有大小的,而此时不知道为它分配多大空间合适,大了,浪费,小了,不够用,所以编译器就为其分配了程序一般变量的最小单位–>>一个字节。

这里写图片描述

注:空结构体的所占内存空间大小是一个字节。因为它是一个类型,而想要成为类型,那么必须要能定义变量,那变量就要占空间,而这个空间大了就太浪费了,因为不知道这个类型要定要什么类型的变量,所以编译器就自动为它分配了一个变量存储的最小单位(一个字节)。

(5)自定义类型中联合体的对齐方式:
因为联合体的对齐方式与联合体有不同,所以在单独提一下。
联合体的字节对齐也分自身对齐值与有效对齐值,联合体自身对齐值为其成员中自身对齐值最小的,有效对齐值为联合体的自身对齐值与边界对齐值(编译器决定,VC6.0默认为8字节,其值可在 工程·–>设置–>C/C++–>Code Generation–>Struct member Alignment 处修改)、指定对齐值中最小的,用数学表达式可以表示为:
有效对齐值 = min(min(成员1,成员2…成员n),边界对齐值,指定对齐值)
最后要满足,联合体成员中自身对齐值的最大值经过字节对齐后(N = 最大值+n,,所加的n最小且能使N成为联合体有效对齐值的倍数),就是自定义的联合体类型所占的字节数。

下面以几个例子来说明:
①有效对齐值为联合体成员中自身对齐值的最大值

#include<stdio.h>#pragma  pack (6)typedef union Test
{char c;int i;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}

结果如下图:

这里写图片描述

解析如下图:

这里写图片描述

②有效对齐值为边界对齐值

#include<stdio.h>#pragma  pack (6)typedef union Test
{char c[13];int i;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}

表面看与第一段代码没区别,细心就会发现联合体中的字符变量变成了字符数组

结果如下图:

这里写图片描述

解析如下图:

这里写图片描述

③有效对齐值为指定对齐值

#include<stdio.h>#pragma  pack (2)typedef union Test
{char c[13];short i;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}

结果为:

这里写图片描述

解析如下图:

这里写图片描述

(6)自定义类型相互嵌套的字节对齐
①联合体嵌套结构体:

#include<stdio.h>#pragma pack(2)typedef union Test
{char c;int i;struct{short s;int i1;double d;char c1;};
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;

这里写图片描述

解析如图:

这里写图片描述

结构体自嵌套(上面已给出(4))、联合体自嵌套、结构体嵌套联合体 ……….与此例道理都差不多,这里不给出示例。
究其根本,不管怎么嵌套,只要掌握了基本的自定义类型的对齐方式,都能很简单的做出来,所以,自定义类型的字节对齐方式才是本文的重难点。

(7)位域
所谓“位域”是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
位域在内存中的存储遵循以下原则:
①一个位域必须存储在同一个字节中,不能跨字节,同样不能垮类型。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
  struct bs
  {unsigned a:4unsigned :0 /空域/unsigned b:4 /从下一单元开始存放/unsigned c:4}
  在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
②位域的长度不能大于指定类型固有长度,比如说int的位域长度不能超过32,bool的位域长度不能超过8。
③位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
  struct k
  {int a:1int :2 /该2位不能使用/int b:3int c:2};
  从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。
④位域的内存分配仍然遵循字节对齐

用一张图来说明位域这个概念:

这里写图片描述

⑤位域的字节对齐

#include<stdio.h>typedef struct Test
{char a:2;char b:4;char c:3;int d:2;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}

结果为:

这里写图片描述

解析如下图:

这里写图片描述

常见的字节对齐基本就这些,当然其中的变化情况会很多很多。字节对齐是一个小而难的知识点,初学很不易掌握,写下这篇博客的过程中自己也学到了很多。希望读者与博主互勉共进,不吝纠正博客中的错误。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/526214.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言中函数调用中的传值与传址

首先介绍一下函数中传值与传址的概念&#xff1a; 传值&#xff1a;传值&#xff0c;实际是把实参的值赋值给行参&#xff0c;相当于copy。那么对行参的修改&#xff0c;不会影响实参的值 。传址&#xff1a; 实际是传值的一种特殊方式&#xff0c;只是他传递的是地址&#xf…

C语言交换两个变量数值的几种方法

因为经常见到这类题目&#xff0c;就自己总结了以下几种办法 1. 创建中间变量 这是最快也是最简单的办法&#xff0c;例如&#xff1a; #include<stdio.h>int main() {int a10;int b20;int temp;printf("交换前a,b的值为:\n");printf("a%d\n",a);…

vim中自动添加文件的作者、时间信息、版本等

1、linux系统版本&#xff1a;ubuntu-10.10 2、打开&#xff1a;vim ~/.vimrc 在文件末尾添加如下内容&#xff0c;如图一 &#xff08;图一&#xff09; 3、新建文件后直接按“F4”可插入作者文件信息&#xff0c;如图二所示 &#xff08;图二&#xff09;

数组的下标越界与内存溢出

很相似的两个概念&#xff0c;一不小心就会混淆 首先&#xff0c;对两个名词做一个大概的解释&#xff1a; 下标越界 在引用数组元素时&#xff0c;使用的下标超过了该数组下标的应有范围&#xff0c;但应注意的是&#xff1a; C/C不对数组做边界检查。 可以重写数组的每一…

二分查找(折半查找)

二分查找(折半查找)&#xff1a; 从有序序列中找到给出的要查询的数字。 原理是&#xff1a;首先把一个有序序列中间位置的值与要查找的数比较&#xff0c;若相等则找到了有序序列中的此数&#xff1b;否则比较两者的大小&#xff0c;若前者大&#xff0c;则把有序序列的中间…

C语言实现用星号在屏幕上打印菱形

很多人第一感觉肯定都是&#xff1a;很简单啊&#xff0c;不就是多写几个printf 语句嘛 像这样&#xff1a; #define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>int main() {printf(" *\n");printf(" ***\n");printf(" *****\n");p…

多维数组元素的下标引用与指针访问

在这儿值介绍二维数组元素的访问方式&#xff0c;三维、四维……的与之类似。 先用一维数组的相关知识来抛砖 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>//1、一维数组元素的下标引用与指针访问 int main() {int arr[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }…

scanf()函数错误把输入缓存里的回车作为一次字符输入

有时我们会需要连续的从标准输入中多次读入数据时&#xff0c;那么就可能存在连续使用多次scanf()函数的情况。然而在连续使用scanf()函数时可能会出现一些难以预料&#xff0c;不易发现的坑 #include <stdio.h>int main() {char str[20] { 0 };char c 0;scanf("…

C语言注释与C++注释的相互转换

做此项目的经历主要的收获是熟悉了状态机这一方法的使用&#xff0c;还有就是对每实现一个功能就尽量封装一个函数这一概念把握的更为精到。 状态机&#xff1a;关于状态机的一个极度确切的描述是它是一个有向图形&#xff0c;由一组节点和一组相应的转移函数组成。状态机通过…

运算符优先级与结合性

在编程中经常会有遇到运算符的地方&#xff0c;而每个运算符都有不同的优先级&#xff0c;错误的使用运算符&#xff0c;可能会发生难以预料而又不易发现的错误&#xff0c;因此我们需要掌握运算符的优先级。 优先级的序号越小&#xff0c;优先级越高。

switch()语句块的出口:break;

switch()语句块里的case:相当于一个开关&#xff0c;只要满足case的条件&#xff0c;开关就打开&#xff0c;从而执行case语句块的内容&#xff0c;而break就相当于一个出口&#xff0c;只要碰到了break&#xff0c;就跳出switch语句块&#xff0c;否则就继续执行下一条语句&am…

函数声明

对C缺陷与陷阱里第二章第一节里面的函数声明的一些段落的理解

字符变量存放多个字符

在我们的认知中&#xff0c;不会有字符变量中存放多个字符的情况&#xff08;不要说\n、\t等&#xff0c;这些本来就被C语言标准定义为是一个字符&#xff09;&#xff0c;因为一个字符变量就只占1个字节空间&#xff0c;只能存放一个字符&#xff0c;但往下看&#xff0c;你会…

不同类型的变量与零值比较的方法

在if()、while()等语句块中&#xff0c;我们经常会遇到变量与零值比较的情况&#xff0c;然而不同的类型的变量与零值的比较方法是有一定标准的&#xff0c;编程时最好遵守这些标准&#xff0c;否则会发生一些难以预料的错误。

测试机器大小端的方法

首先&#xff0c;给出一些大小端相关概念。 大小端的由来&#xff1a; 在乔纳森斯威夫特的著名讽刺小说《格列夫游记》中&#xff0c;小人国内部分裂成Big-endian和Little-endian两派&#xff0c;区别在于一派要求从鸡蛋的大头把鸡蛋打破&#xff0c;另一派要求从鸡蛋的小头把…

main()函数参数

很多人学了很长时间的C语言&#xff0c;可能还不知道mian()函数也是有参数的&#xff0c;甚至会说&#xff0c;main()函数哪来的参数&#xff0c;我从来都没见到过&#xff0c;更没有使用过&#xff0c;然而&#xff0c;不得不说&#xff0c;main()函数确实是有参数的。 main函…

运算符求值顺序

&&与||两个运算符的求值顺序是非常重要的&#xff0c;因为很多判断语句都是基于它们拥有一定的求值顺序才能正确进行的。 eg: if((num1 ! 0) && (num2 / num1 \ > num2 - num1)) 上面的if()判断语句合理的运用了&&运算符&#xff0c;使之不会出现…

CentOS设置

我的CentOS是在VMware Workstation里面装的虚拟机&#xff0c;但操作都是一样的 1、设置系统语言 许多人在安装CentOS系统时&#xff0c;可能在开始阶段选择区域及语言时选择了非中文&#xff0c;而却想在中文环境下使用系统&#xff0c;却苦于不知道如何切换到中文系统&…

printf函数的格式修饰符

本文介绍printf函数的一些特殊的格式控制修饰符 1、域宽修饰符 –> 数字 对所有格式控制符都有效&#xff0c;即便是%%也不例外 宽度修饰符出现在%和格式控制符之间的数字&#xff0c;使数据在固定区域打印。如果待打印的数值不能填满位置&#xff0c;它的左侧会被天上空…

线性表之顺序表与单链表的区别及优缺点

这里比较的是基于C语言实现的顺序表与单链表&#xff0c;与其他语言的实现可能会有差异&#xff0c;但我相信语言是相通的&#xff0c;它们的实现机制应该也差不多。 1、What 什么是顺序表和单链表 ①顺序表&#xff1a; 顺序表是在计算机内存中以数组的形式保存的线性表&a…