内存对齐浅析1

内存对齐可以用一句话来概括:

“数据项只能存储在地址是数据项大小的整数倍的内存位置上”(有对齐系数就取他们比较后的较小值,Linux默认是4,windows默认是8.)

 


下面重点说的是结构体的内存对齐:

规则:

1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack(n)指定的数值n和这个数据成员自身长度^{x_{i}}中,比较小的那个进行。

2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack(n)指定的数值n和结构(或联合)最大数据成员长度max(^{x_{i}})中,比较小的那个进行。

3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。


总结:

1.数据成员对齐(对象:数据成员长度;关键:成员对齐系数=min(n,^{x_{i}}))和整体对齐(对象:整体长度——圆整;关键:整体对齐 系数=min(n,max(^{x_{i}})),整体大小(size)=(成员总大小) 按 (整体对齐系数) 圆整 = 13   /*13(成员总大小)%1(整体对齐系数)=0*/ )。

2.结构体的一个成员开辟空间之前,预开辟空间的首地址相对于结构体首地址的偏移是成员对齐系数的整数倍。

3.编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数(每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”)。

遗留问题:

结构体首地址是最宽的基本数据类型的整数倍,我觉得这里的最宽的基本数据类型不准确,应该换成min(n,最宽数据类型) 有什么好的验证方法。


测试代码:

#pragma pack(1) /* n = 1, 2, 4, 8, 16 */
/*默认的对齐系数为4(Linux)*/

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

struct test_t
{
    char   d[9];
    int    a;
    char   b;
    short  c;
    
    struct tmp
    {
        short y;
        double x;
        char arr[8];
    }arr_t;
    
    //long double e;
}ttt;

void print_hex_data(char *info, char *data, int len)
{
    int i;
    printf("%s:size = %d\n", info, len);
    for(i = 0; i < len; i++)
    {
        printf("%02x ", (unsigned char)data[i]);
    }
    printf("\n");

    for(i = 0; i < len; i++)
    {
        printf("%p ", data + i);
    }
    printf("\n");
}

int main()
{
    ttt.a = 0x1a2a3a4a;
    ttt.b = 0x1b;
    ttt.c = 0x1c2c;
    char *s = "123456789"; //1的hex进制为0x31
    memcpy(ttt.d, s, 9);
    
    for(int i = 0; i < sizeof(ttt.arr_t.arr); i++)
    {
        ttt.arr_t.arr[i] = 'a' + i; // a的hex进制为0x61
    }
    ttt.arr_t.x =0x8899aabbccddeeff;
    ttt.arr_t.y =0x1122;
    
    //printf("long double 的大小:%ld\n", sizeof(long double));
    //ttt.e = 0x0e1e2e3e4e5e6e7e8e9eaebecedeeefe;
    //printf("结构体里元素最大长度为:%ld\n", sizeof(ttt.arr_t));
    print_hex_data("struct_data", (char *)&ttt, sizeof(struct test_t));
    return 0;
}

注意:

(1)内存不对齐的坏处不是浪费内存,因为即使我写一个随便在不同位置放置不同大小的数据结构时,只要告诉编译器说必须按照一字节对齐,编译器编译时肯定按照我的意愿不浪费一个字节的内存。编译器默认按照自然边界对齐,是因为它要求效率,保证程序的正常运行(因为非对齐访问可能导致进程退出)。我们对结构体的组织的调整是为了节约内存,而调整的规则就是按照内存对齐来安插数据。

(2)如果结构体内 存在 长度大于处理器位数(64位 8个字节) 的元素,那么就以处理器的倍数为对齐单位;否则,如果结构体内的元素的长度都小于处理器的倍数的时候,便以结构体里面最长的数据元素为对齐单位。

(3)结构体内类型相同的连续元素将在连续的空间内,和数组一样。


测试:

#pragma pack(1)

#pragma pack(2)

#pragma pack(4)

#pragma pack(8)

#pragma pack(16)

之所以用内存对齐:

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

2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。


三个问题:

1.结构体嵌套结构体

如果结构体中嵌套结构体,那么嵌套的结构体的偏移量必须是它(min(n,最大成员的字节数))的整数倍。

结构体的总偏移量必须是它(min(n,最大成员的字节数))最大成员字节数的整数倍(包括嵌套的结构体中的最大成员)。

2.位段元素

以下是位段的一些规则:

(1)第一个位段成员的偏移满足内存对齐的原则。

(2)相邻的位段结构成员的类型相同。若它们的位宽之和不大于该类型的存储单元的位宽,则后面的成员紧接在前一个成员后;若否,则为后面的成员开辟一个新的存储单元。新的存储单元的偏移满足内存对齐的原则。

(3)相邻的位段结构成员的类型不同,前一成员类型的位宽大于后一成员类型的位宽。若前一成员所在的存储单元还有空间容纳后一成员,则把后一成员紧接在前一成员后;若否,则为后一成员开辟新的存储单元。新的存储单元的偏移满足内存对齐的原则。

(4) 相邻的位段结构成员的类型不同,前一成员类型的位宽小于后一成员类型的位宽。若把前一成员所在的存储单元的位宽扩展为后一成员类型的位宽的大小后,能把后一成员容纳下的,则把对前一成员的存储单元进行位宽扩展,并把后一成员紧接在前一成员后;若否,则为后一成员开辟新的存储空间,其存储空间的偏移满足内存对齐的原则。(存储单元的位宽扩展的原则:若前一成员所在的字节单元的偏移不为后一成员大小的整数倍,则先向前兼并字节单元扩展,直到向前找到偏移为后一成员大小的整数倍的字节单元,此时判断扩展的位数是否足够,如果不够则从后开辟新字节单元进行扩展)

(5)可以通过定义长度为0的位段的方式使下一位段从下一存储单元开始。
(6)可以定义无名位段。 
(7)定义位段时其长度不能大于存储单元的长度。 
(5)位段无地址,不能对位段进行取地址运算。 
(6)位段可以以%d,%o,%x格式输出。 
(7)位段若出现在表达式中,将被系统自动转换成整数。

(8)位段的最大取值范围不要超出二进制位数定的范围,否则超出部分会丢弃。

#include<stdio.h>
typedef unsigned char  uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int   uint32_t;

struct s1_stru
{ 
    uint8_t  ch1:6;
    uint8_t  ch2:1;
    uint16_t sh:2;
}t1;

struct s2_stru
{
    uint8_t  ch:7;
    uint16_t sh:9;
}t2;

int main()
{
    t1.ch1 = 0b111111;
    t1.ch2 = 0b1;
    t1.sh  = 0b11;

    t2.ch  = 0b1111111;
    t2.sh  = 0b111111111;
    
    printf("sizeof(t1)=%d\n", sizeof(t1));
    printf("sizeof(t2)=%d\n", sizeof(t2));
    return 0;
}

这是位段数据三次赋值操作的过程,遵循存储单元的位宽扩展的原则。

3.联合体

联合体(union)的内存对齐规则

1.联合体也是一个结构,联和体是共享内存的。

2.所以的联合体的内部成员起始地址都是一样的,都是联合体的首地址。

3.它的对齐方式要适应所有成员。

4.该空间必须足够容纳最宽成员。

5.联合体的对齐数为最大成员的对齐数。


拓展:

动态申请空间的首地址为对齐系数的整数倍。

#include<stdio.h>
#include <stdlib.h>
#define  NUM  4  //设置对齐数 
void* my_malloc(size_t size) 
{    
    if (size == 0) 
    {        
        return NULL;
    }
    size_t real_size = size + NUM;
    void* reall_addr = malloc(real_size);
    if (reall_addr == NULL)
    {
        return NULL;
    }
    void* return_addr = reall_addr + (NUM - ((size_t)reall_addr % NUM));//返回偏移后的地址
    unsigned char *record = ((unsigned char*)return_addr-1);
    *record = (unsigned char)(NUM - ((size_t)reall_addr % NUM));//保存偏移量
    printf("reall_addr: 0x%x\n",reall_addr);
    printf("return_addr: 0x%x\n",return_addr);
    printf("record: %u\n",*record);
    return return_addr;
    }
void my_free(void *addr)
{
    if (addr == NULL)
    {
        return ;
    }
    unsigned char record = *((unsigned char*)addr-1);
    printf("record: %u\n",record);
    void* reall_addr = (void*)((unsigned char*)addr-record);
    printf("reall_addr: 0x%x\n",reall_addr);
    free(reall_addr);
    } 
#define SIZE 17
int main()
{
    void *p = my_malloc(SIZE);
    unsigned char* arr = (unsigned char*)p;
    unsigned char i=0;
    for (; i<SIZE; i++)
    {
      arr[i] = i;
      printf("arr[%u]=%u ",i,arr[i]);
    }
    printf("\n");
    my_free(p);
    return 0;
}

---------------------

本文来自 kai8wei 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/kai8wei/article/details/78701531?utm_source=copy 

参考文章:

https://www.linuxidc.com/Linux/2016-08/134052.htm

https://baike.baidu.com/item/%E5%86%85%E5%AD%98%E5%AF%B9%E9%BD%90/9537460?fr=aladdin

https://blog.csdn.net/pfgmylove/article/details/7027906

https://blog.csdn.net/kai8wei/article/details/78701531

 

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值