[TOC]
# 字节对齐
在用sizeof计算结构体占用的空间的时候,不是简单把所有元素相加的,这里涉及到内存字节对齐的问题
从理论上来讲,对于任何变量都可以从任何地址访问,但是事实不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按照一定的规则排列而不是简单的顺序排列,这就是内存对齐

cpu虽然每次读取的单位是字节,但是一次是读取一个块,所以有些硬件设备不允许放在内存的奇数位上
内存对齐是操作系统为了提高访问内存的策略.操作系统在访问内存的时候,每次读取一定长度(这个长度是操作系统的对齐数,或者默认对齐数的整数倍).如果没有对齐,为了访问一个变量可能产生二次访问
* 提高存取数据的速度.比如有的平台每次都是从偶地址读取数据,对于一个int地址的变量.若从偶地址单元处存放,则需一个读取周期即可读取该变量,但是若从奇地址单元处存放,则需要2个读取周期读取变量
* 某些平台只能在特定的地址访问特定类型的数据,否则抛出硬件异常给操作系统
## 如何内存对齐
* 对于标准数据类型,它的地址只要是它的长度的整数倍
* 对于非标准类型,比如结构体,要遵循一下对齐原则
1. 数组成员对齐规则.第一个数组成员应该放在offset为0的地方,以后每个数组成员应该在offset为min(当前成员的大小, #paragama pack(n))整数倍的地方开始(比如int在32位机器为4字节, #paragam pack(2),那么从2的倍数地方开始存储)
2. 结构体总的大小,也就是sizeof的结果,必须是min(结构体内部最大成员, #paragama pack(n))的整数倍,不足要补齐
3. 结构体作为成员的对齐规则,如果一个结构体B里嵌套了另一个结构体A,还是以最大成员类型的大小对齐,但是结构体A的起点为A内部最大成员的整数倍地方(struct B里有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储),结构体A中的成员对齐规则仍满足原则1和原则2
## 手动对齐模式
~~~
#pragma pack(show)
显示当前packing alignment的字节数,以warning message的形式被显示
#pragma pack(push)
将当前指定的packing alignment数组进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n,如果n没有指定,则将当前的packing alignment数组压栈
#pragma pack(pop)
从internal compiler stack中删除最顶端的reaord,如果没有指定n,则当前栈顶record即为新的packing alignement数值.如果指定了n,则n成为新的packing alignment值
#pragma pack(n)
指定packing的数值,以字节为单位,缺省数值是8,合法数值分别是1,2,4,8,16
~~~
## 为何需要对齐
计算机内存是以字节(Byte)为单位划分的,理论上CPU可以访问任意编号的字节,但实际情况并非如此。
CPU 通过地址总线来访问内存,一次能处理几个字节的数据,就命令地址总线读取几个字节的数据。32 位的 CPU 一次可以处理4个字节的数据,那么每次就从内存读取4个字节的数据;少了浪费主频,多了没有用。64位的处理器也是这个道理,每次读取8个字节。
以32位的CPU为例,实际寻址的步长为4个字节,也就是只对编号为 4 的倍数的内存寻址,例如 0、4、8、12、1000 等,而不会对编号为 1、3、11、1001 的内存寻址。如下图所示:

样做可以以最快的速度寻址:不遗漏一个字节,也不重复对一个字节寻址。
对于程序来说,一个变量最好位于一个寻址步长的范围内,这样一次就可以读取到变量的值;如果跨步长存储,就需要读取两次,然后再拼接数据,效率显然降低了。
例如一个 int 类型的数据,如果地址为 8,那么很好办,对编号为 8 的内存寻址一次就可以。如果编号为 10,就比较麻烦,CPU需要先对编号为 8 的内存寻址,读取4个字节,得到该数据的前半部分,然后再对编号为 12 的内存寻址,读取4个字节,得到该数据的后半部分,再将这两部分拼接起来,才能取得数据的值。
将一个数据尽量放在一个步长之内,避免跨步长存储,这称为内存对齐。在32位编译模式下,默认以4字节对齐;在64位编译模式下,默认以8字节对齐。
为了提高存取效率,编译器会自动进行内存对齐
~~~
#include <stdio.h>
#include <stdlib.h>
struct{
int a;
char b;
int c;
}t={ 10, 'C', 20 };
int main(){
printf("length: %d\n", sizeof(t));
printf("&a: %X\n&b: %X\n&c: %X\n", &t.a, &t.b, &t.c);
system("pause");
return 0;
}
~~~
在32位编译模式下的运行结果:
~~~
length: 12
&a: B69030
&b: B69034
&c: B69038
~~~
如果不考虑内存对齐,结构体变量 t 所占内存应该为 4+1+4 = 9 个字节。考虑到内存对齐,虽然成员 b 只占用1个字节,但它所在的寻址步长内还剩下 3 个字节的空间,放不下一个 int 型的变量了,所以要把成员 c 放到下一个寻址步长。剩下的这3个字节,作为内存填充浪费掉了。请看下图:

编译器之所以要内存对齐,是为了更加高效的存取成员 c,而代价就是浪费了3个字节的空间。
除了结构体,变量也会进行内存对齐,请看下面的代码:
~~~
#include <stdio.h>
#include <stdlib.h>
int m;
char c;
int n;
int main(){
printf("&m: %X\n&c: %X\n&n: %X\n", &m, &c, &n);
system("pause");
return 0;
}
~~~
在VS下运行:
~~~
&m: DE3384
&c: DE338C
&n: DE3388
~~~
可见它们的地址都是4的整数倍,并相互挨着。
经过笔者测试,对于全局变量,GCC在 Debug 和 Release 模式下都会进行内存对齐,而VS只有在 Release 模式下才会进行对齐。而对于局部变量,GCC和VS都不会进行对齐,不管是Debug模式还是Release模式。
# 内存大小端对齐
我们看到上面的十六进制的63,二进制是0110 0011.
那么二进制数据是
~~~
0000 0000 0000 0000 0000 0000 0110 0011
~~~
因为int是4个字节,1个字节等于8位.
4位一组.
但是内存是16进制表示每8位对应一个16进制
~~~
00 00 00 63
~~~
内存大小端对齐
~~~
63 00 00 00
~~~
数组在内存是连续的
后面是0a,十六进制对应十进制是10
正好是下个元素
如果你想在内存地址中看到下个元素,就把那地址+4,注意是16进制表示,结果就是下个元素了
大端和小端是指数据在内存中的存储模式,它由 CPU 决定:
1. 大端模式(Big-endian)是指将数据的低位(比如 1234 中的 34 就是低位)放在内存的高地址上,而数据的高位(比如 1234 中的 12 就是高位)放在内存的低地址上。这种存储模式有点儿类似于把数据当作字符串顺序处理,地址由小到大增加,而数据从高位往低位存放。
2. 小端模式(Little-endian)是指将数据的低位放在内存的低地址上,而数据的高位放在内存的高地址上。这种存储模式将地址的高低和数据的大小结合起来,高地址存放数值较大的部分,低地址存放数值较小的部分,这和我们的思维习惯是一致,比较容易理解
## 为什么有大小端模式之分
计算机中的数据是以字节(Byte)为单位存储的,每个字节都有不同的地址。现代 CPU 的位数(可以理解为一次能处理的数据的位数)都超过了 8 位(一个字节),PC机、服务器的 CPU 基本都是 64 位的,嵌入式系统或单片机系统仍然在使用 32 位和 16 位的 CPU。
对于一次能处理多个字节的CPU,必然存在着如何安排多个字节的问题,也就是大端和小端模式。以 int 类型的 0x12345678 为例,它占用 4 个字节,如果是小端模式(Little-endian),那么在内存中的分布情况为(假设从地址 0x 4000 开始存放):

如果是大端模式(Big-endian),那么分布情况正好相反:

我们的 PC 机上使用的是 X86 结构的 CPU,它是小端模式;51 单片机是大端模式;很多 ARM、DSP 也是小端模式(部分 ARM 处理器还可以由硬件来选择是大端模式还是小端模式)。
借助共用体,我们可以检测 CPU 是大端模式还是小端模式,请看代码:
~~~
#include <stdio.h>
int main(){
union{
int n;
char ch;
} data;
data.n = 0x00000001; //也可以直接写作 data.n = 1;
if(data.ch == 1){
printf("Little-endian\n");
}else{
printf("Big-endian\n");
}
return 0;
}
~~~
在PC机上的运行结果:
Little-endian
共用体的各个成员是共用一段内存的。1 是数据的低位,如果 1 被存储在 data 的低字节,就是小端模式,这个时候 data.ch 的值也是 1。如果 1 被存储在 data 的高字节,就是大端模式,这个时候 data.ch 的值就是 0。
- c语言
- 基础知识
- 变量和常量
- 宏定义和预处理
- 随机数
- register变量
- errno全局变量
- 静态变量
- 类型
- 数组
- 类型转换
- vs中c4996错误
- 数据类型和长度
- 二进制数,八进制数和十六进制数
- 位域
- typedef定义类型
- 函数和编译
- 函数调用惯例
- 函数进栈和出栈
- 函数
- 编译
- sizeof
- main函数接收参数
- 宏函数
- 目标文件和可执行文件有什么
- 强符号和弱符号
- 什么是链接
- 符号
- 强引用和弱引用
- 字符串处理函数
- sscanf
- 查找子字符串
- 字符串指针
- qt
- MFC
- 指针
- 简介
- 指针详解
- 案例
- 指针数组
- 偏移量
- 间接赋值
- 易错点
- 二级指针
- 结构体指针
- 字节对齐
- 函数指针
- 指针例子
- main接收用户输入
- 内存布局
- 内存分区
- 空间开辟和释放
- 堆空间操作字符串
- 内存处理函数
- 内存分页
- 内存模型
- 栈
- 栈溢出攻击
- 内存泄露
- 大小端存储法
- 寄存器
- 结构体
- 共用体
- 枚举
- 文件操作
- 文件到底是什么
- 文件打开和关闭
- 文件的顺序读写
- 文件的随机读写
- 文件复制
- FILE和缓冲区
- 文件大小
- 插入,删除,更改文件内容
- typeid
- 内部链接和外部链接
- 动态库
- 调试器
- 调试的概念
- vs调试
- 多文件编程
- extern关键字
- 头文件规范
- 标准库以及标准头文件
- 头文件只包含一次
- static
- 多线程
- 简介
- 创建线程threads.h
- 创建线程pthread
- gdb
- 简介
- mac使用gdb
- setjump和longjump
- 零拷贝
- gc
- 调试器原理
- c++
- c++简介
- c++对c的扩展
- ::作用域运算符
- 名字控制
- cpp对c的增强
- const
- 变量定义数组
- 尽量以const替换#define
- 引用
- 内联函数
- 函数默认参数
- 函数占位参数
- 函数重载
- extern "C"
- 类和对象
- 类封装
- 构造和析构
- 深浅拷贝
- explicit关键字
- 动态对象创建
- 静态成员
- 对象模型
- this
- 友元
- 单例
- 继承
- 多态
- 运算符重载
- 赋值重载
- 指针运算符(*,->)重载
- 前置和后置++
- 左移<<运算符重载
- 函数调用符重载
- 总结
- bool重载
- 模板
- 简介
- 普通函数和模板函数调用
- 模板的局限性
- 类模板
- 复数的模板类
- 类模板作为参数
- 类模板继承
- 类模板类内和类外实现
- 类模板和友元函数
- 类模板实现数组
- 类型转换
- 异常
- 异常基本语法
- 异常的接口声明
- 异常的栈解旋
- 异常的多态
- 标准异常库
- 自定义异常
- io
- 流的概念和类库结构
- 标准io流
- 标准输入流
- 标准输出流
- 文件读写
- STL
- 简介
- string容器
- vector容器
- deque容器
- stack容器
- queue容器
- list容器
- set/multiset容器
- map/multimap容器
- pair对组
- 深浅拷贝问题
- 使用时机
- 常用算法
- 函数对象
- 谓词
- 内建函数对象
- 函数对象适配器
- 空间适配器
- 常用遍历算法
- 查找算法
- 排序算法
- 拷贝和替换算法
- 算术生成算法
- 集合算法
- gcc
- GDB
- makefile
- visualstudio
- VisualAssistX
- 各种插件
- utf8编码
- 制作安装项目
- 编译模式
- 内存对齐
- 快捷键
- 自动补全
- 查看c++类内存布局
- FFmpeg
- ffmpeg架构
- 命令的基本格式
- 分解与复用
- 处理原始数据
- 录屏和音
- 滤镜
- 水印
- 音视频的拼接与裁剪
- 视频图片转换
- 直播
- ffplay
- 常见问题
- 多媒体文件处理
- ffmpeg代码结构
- 日志系统
- 处理流数据
- linux
- 系统调用
- 常用IO函数
- 文件操作函数
- 文件描述符复制
- 目录相关操作
- 时间相关函数
- 进程
- valgrind
- 进程通信
- 信号
- 信号产生函数
- 信号集
- 信号捕捉
- SIGCHLD信号
- 不可重入函数和可重入函数
- 进程组
- 会话
- 守护进程
- 线程
- 线程属性
- 互斥锁
- 读写锁
- 条件变量
- 信号量
- 网络
- 分层模型
- 协议格式
- TCP协议
- socket
- socket概念
- 网络字节序
- ip地址转换函数
- sockaddr数据结构
- 网络套接字函数
- socket模型创建流程图
- socket函数
- bind函数
- listen函数
- accept函数
- connect函数
- C/S模型-TCP
- 出错处理封装函数
- 多进程并发服务器
- 多线程并发服务器
- 多路I/O复用服务器
- select
- poll
- epoll
- epoll事件
- epoll例子
- epoll反应堆思想
- udp
- socket IPC(本地套接字domain)
- 其他常用函数
- libevent
- libevent简介