详解C语言内存对齐

 

在C语言里有一个机制是内存对齐,当然不止C语言,包括其他的编程语言都会有内存对齐机制,否则编译出来的软件无法正常运行,至于为什么呢?众所周知,在内存中,所有的数据都是按字节为最小单位存储的,存储单位称为存储单元,所以也叫内存是由很多存储单元组成的,这些存储单元(字节)都有固定的地址标示着(这里说的非虚拟模式下),在我们程序员眼里内存就是一个一个字节组成的,这些字节对应的地址都是连续并排好的,但是在CPU中不是,在CPU看来内存是一段一段的,每段大小取决于CPU的寻址位宽!

比如在C语言里申请了short和两个char变量:

Shor a;

Char b;

Char c;

在内存中对应的起始地址为:0x01(a),0x03(b),0x04(c)

在我们程序员眼里在内存中的存储是这样的:

 

 

 

但是在一个寻址位宽为32位的CPU上是这样的:

 

 

 

Short占用16个bit位char占用8个bit位所以16+8+8=32 刚好组成一段!

但是如果在c语言中我们这样定义了三个变量:

Short a;(起始地址:0x01)

Char b;(起始地址:0x03)

Short c;(起始地址:0x04)

那么如果C语言编译器不为我们自动对齐在内存中就会这样:

 

 

 

可以看到最后一个short的地址被分两个段存储了,也就是说如果CPU要读取变量c那么必须先将第一个段里的数据全部读取出来送到寄存器里(这里假定通用寄存器为32位),然后将第一个寄存器里的0x01到0x03的数据剔除掉,将0x04上的bit位数据挪移到寄存器的低位上,然后在去读取第二个段上的地址数据也送到寄存器里,然后将0x06到0x08上的数据剔除掉,这样就将额外的数据剔除掉了,但是这样就会读取两次,浪费更多的时间1,为什么不直接从0x04开始读取?注意CPU不可以从某段的段偏移上读取只能从某段的起始地址开始读取!这是虚拟内存中的概念(详细参见虚拟内存的映射关系),不然的话就会造成读取时出现数据越界的情况!

假如0x05地址是栈的尾地址,如: 0x04 0x05 0x06 0x07

那么在一个32位的CPU下寻址时就会发生越界的情况,访问到其他进程下的内存则会被操作系统里内核中断代码捕捉会被视为恶意代码会立即被中断,

 

当然部分CPU也并不会全部都给你按上面的方式来读取,假如遇到了上图那样的地址存储方式,CPU会直接报硬件中断,直接罢工不读取,然后由操作系统捕捉这个硬件中断直接咔擦掉你的程序!在调试情况下调试器捕捉这个消息时会中断!

所以为了解决这一问题,编译器使用了一种叫做空间交换法的方式来对齐内存,也就是说增加额外的内存,换取时间上的效率,同时也避免部分CPU的硬件中断!

下面是C语言内存对齐后的地址:

 

 

 

可以看到char为了对齐short增加了一个字节,而short为了对齐32位寻址增加了两个字节,这就是空间上的时间交换法,这样的话CPU读取时只需要一次就可以读取完!注意增加的字节只是为了对齐寻址,额外的字节是不允许被赋值的,虽然说可以赋值,但是当我们赋值是编译器不会允许我们向额外的地址传递数据!注意以上内存对齐是发生在结构体当中的,不光是结构体在函数体里所有的变量包括代码段内存全部都会被内存对齐,函数体里的对齐方式不同于结构体,博主也没有去深刻的去查函数体里的对齐方式,这里也不必要做过多的了解因为编译器都会帮我们做好!只是在个别面试时会面试到关于内存对齐的基础题!

这里来做一个演示:

下面是一个结构体

Struct st{

       Inta;

       Charb;

       Intc;

}

Sizeof的大小是多少?通过上面的说明应该可以断定大小为:12,为什么不是9?上面也说过了内存对齐!a刚好为32位的寻址宽,而b则不满足32位寻址宽,所以它需要与a进行一个内存对齐的情况,所以就额外递增了3个字节给b满足32位的寻址宽,而c则不需要对齐因为它刚好满足32位的寻址宽!

假如还有一个结构体:

Struct st{

       Shorta;

       Charb;

       Intc;

}

Sizeof的大小是多少?答:8,应该可以很容易的算出来,首先short a的占用字节为2(16bit)不满足32位寻址,而charb(8bit)占用一字节,这个时候呢编译器会给b填充一个字节让其与a对齐,然后将a与b看成一段!这样a与b都是16位bit相加在一起就是32位寻址,刚好满足位宽为32位的CPU寻址!

这种方法就是空间上的时间转换,牺牲额外的空间换取时间上的效率,毕竟在这个内存越来越大,价格越来越便宜的年代,牺牲额外的内存换取效率也是一件可行之事!

每个编译器里都有一个内存对齐的模数,这个摸数取决于你的CPU位宽!我们可以通过# #pragma pack(n)来强制改变它,n的可取值范围是:n=1,2,4,8,16(字节),当然这里也不是特别建议去改变它的默认寻址宽度,因为超出的话CPU没有办法一次性读取完还是会分段裁剪来读,过小的话CPU也是要去分两次或者更多的次数然后裁剪的读取,所以建议为默

  • 17
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

17岁boy想当攻城狮

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值