超级详细的结构体内存对齐讲解

403 阅读8分钟

Cover prompts:Above the city at the foot of the mountain, the sky is full of stars.picasso.


我承认阁下内存管理很强,但如果,我是说如果,我定义一个位段结构体,作为一个成员放到另一个被我malloc结构体内,并让阁下输出它的内存对齐数和大小,阁下要如何应对?

代码(VS环境下):

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#pragma pack(8)

// ① 定义一个名为bitSegmentStruct的位段结构体
//一次性开辟好,一个字节内部,从低位向高位
typedef struct {
    int bit_fi : 3;                 
    int bit_se : 3;                 
} bitSegmentStruct;// 对齐数4

// 定义一个名为customStruct的结构体,用于满足要求的各种数据类型
typedef struct {
    int int_number;                   // int类型的数字    4<8 取4
    double double_number;             // double类型的数字  取8
    char char_array[3];               // 大小为8的char类型数组   取1,相当于3个char
    bitSegmentStruct bit_segment;     // bitSegmentStruct类型的结构体成员 
} customStruct;

int main() {
    // ② 使用malloc函数构建一个customStruct类型的结构体
    customStruct* ptr = (customStruct*)malloc(sizeof(customStruct));
    if (ptr == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }
    *ptr = { 0 };//将堆上这一部分的值初始化

    // 为结构体成员赋值(根据需要自行修改)
    ptr->bit_segment.bit_fi = 6;//结构体指针找成员 使用-> 结构体引出成员使用.
    ptr->bit_segment.bit_se = 5;
    strncpy(ptr->char_array, "xx\0", 3);

    // ③ 输出默认对齐数为8的情况下的对齐数字
    printf("默认对齐数为8的情况下:\n");
    printf("\n");
    printf("int_number的对齐数字: %zu\n", offsetof(customStruct, int_number));
    printf("\n");
    printf("double_number的对齐数字: %zu\n", offsetof(customStruct, double_number));
    printf("\n");
    printf("char_array的对齐数字: %zu\n", offsetof(customStruct, char_array));
    printf("\n");
    printf("bit_segment的对齐数字: %zu\n", offsetof(customStruct, bit_segment));
    printf("\n");
    printf("bit_fi的实际存储的数字:%d\n", ptr->bit_segment.bit_fi);
    printf("\n");
    printf("bit_se的实际存储的数字:%d\n", ptr->bit_segment.bit_se);
    printf("\n");
    printf("整个ptr结构的大小是:%zd", sizeof(*ptr));

    // 释放分配的内存
    free(ptr);

    return 0;
}

运行结果及图片分析

(↑内存对齐↑)

(↑对位段式结构体的分析↑)

:为什么结果是-2,-3,因为发生了截断


剖析说明:

一些常用的内存管理函数:

malloc和realloc是C语言标准库(stdlib.h)中用于动态内存分配的两个函数

malloc

void *malloc(size_t size);

功能:malloc(内存分配)用于在堆内存中分配指定大小的内存空间。

用法:malloc函数接受一个参数,即所需分配的内存空间的大小。例如,int *ptr = (int *) malloc(10 * sizeof(int));,表示分配一个可以容纳10个整数的内存空间。而realloc函数接受两个参数,第一个是指向已分配内存空间的指针,第二个是新的内存空间大小。例如,ptr = (int *) realloc(ptr, 20 * sizeof(int));,表示将原来的内存空间扩展为可以容纳20个整数。

工作原理:malloc在堆中分配一块新的内存空间,分配成功后返回指向该内存空间的指针。如果分配失败,返回空指针(NULL)。

realloc

void *realloc(void *ptr, size_t size);

功能:realloc(重新分配内存)的作用是调整已经分配的内存空间的大小,可以增加或减少空间。

工作原理:realloc尝试在原有内存空间的基础上进行调整。如果有足够的连续内存空间,realloc将直接调整原有空间的大小。如果没有足够的连续内存空间,realloc会分配一块新的内存空间,并将原有内存空间的数据拷贝到新的内存空间,然后释放原有内存空间。最后,realloc返回指向新的内存空间的指针。如果调整失败,返回空指针(NULL)。

以上两者的优缺点:

realloc和malloc相比,各有优缺点:

  1. 效率问题:realloc在调整内存空间大小时可能需要进行数据拷贝。如果原有内存空间附近没有足够的连续内存,realloc会在堆上分配一块新的内存空间,并将原有内存空间的数据拷贝到新的内存空间,然后释放原有内存。这个过程可能会导致效率降低。
  2. 内存碎片化:频繁使用realloc调整内存空间大小可能会导致内存碎片化,从而影响程序的性能。特别是当程序需要大量连续内存时,内存碎片化可能会导致分配失败。
  3. 额外注意事项:使用realloc时需要注意,如果调整失败,它会返回空指针(NULL),但不会释放原有内存。因此,在使用realloc时,建议将结果赋值给另一个指针变量,以便在调整失败时仍能访问并释放原有内存。

memmove

void *memmove(void *dest, const void *src, size_t count);

它将src指针所指向的内存区域的count个字节复制到dest指针所指向的内存区域。memmove函数处理源内存区域和目标内存区域重叠的情况。如果内存区域重叠,memmove会确保复制的内容始终保持正确,不会出现意外的覆盖。

memcpy

void *memcpy(void *dest, const void *src, size_t count);

memcpy功能类似于memmove,也是将src指针所指向的内存区域的count个字节复制到dest指针所指向的内存区域。但是,memcpy不处理源内存区域和目标内存区域重叠的情况。如果内存区域重叠,memcpy的行为是未定义的,可能导致意外的覆盖或数据丢失。

两者区别:

memmove和memcpy是C语言标准库(string.h)中提供的两个内存操作函数,它们分别用于将一块内存区域的内容复制到另一块内存区域。

在实际编程中,如果确定源内存区域和目标内存区域不会重叠,可以使用memcpy,因为它可能会提供更好的性能。否则,为了安全起见,建议使用memmove。

free

void free(void *ptr);

ptr参数是一个指向要释放的内存区域的指针。当不再需要之前分配的内存时,使用free函数可以将这块内存归还给操作系统,以便其他程序或系统本身使用。这样可以避免内存泄漏——即程序占用的内存持续增加,导致系统资源耗尽。

需要注意的是:

  1. 当使用free释放内存后,原来的指针变量变为悬空指针(dangling pointer)。继续使用该指针可能导致未定义行为。为了避免这种情况,在释放内存后,可以将指针设置为NULL。
  2. 不要尝试释放未经malloc、calloc或realloc分配的内存,这可能导致未定义行为。
  3. 不要多次释放同一块内存,这可能导致未定义行为。为避免这种情况,释放内存后,可以将指针设置为NULL,这样即使多次调用free,也不会产生问题,因为释放NULL指针是安全的。

内存对齐:

  1. 结构体对齐:结构体中的每个成员都遵循自然对齐规则。结构体本身的对齐要求通常是其成员中对齐要求最大的那个———可以理解为,结构体大小需要是其成员中最大对齐数的倍数。编译器会在结构体的成员之间添加填充(padding),以确保每个成员都遵循自己的对齐规则——成员的偏移量,需要是自身和默认对齐数中,较小哪一位的倍数。拿开头中的题当作例子,double_number的对齐数是8,但存储完int_number后,此时地址的偏移量是3,需要填充到double_number的对齐数的倍数处,这里就是8本身(8/8==1)。因此,结构体的实际大小可能大于其成员大小之和。
  2. 联合体对齐(篇幅原因,没有对这种情况讨论):联合体(union)的对齐要求是其成员中对齐要求最大的那个。不过,由于联合体中的所有成员共享同一块内存,因此联合体的大小等于其最大成员的大小。
  3. 数组对齐:数组中的每个元素都遵循自然对齐规则。数组本身的对齐要求等于其元素类型的对齐要求。

对齐数的计算:

1.第一个成员在与结构体变量偏移量为0的地址处。

2.其他成员变量要对齐到对齐数的整数倍的地址处——对齐数 = 编译器默认的对齐数(如VS2019下是8)与 该成员大小的较小值。

3.结构体总大小为成员中最大对齐数的整数倍(如标题中24%8==0)

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处。

位段式的结构体:

位段(bit-field)是C语言中一种特殊的结构体成员类型,它允许您使用比字节更小的单位来存储数据。位段在结构体中定义时,需要指定一个整数类型(通常为unsigned int或int),并在冒号后面指明位数。位段式结构体通常用于表示具有多个独立标志位或具有较小范围值的数据,从而节省内存空间。

例如:

typedef struct {
    unsigned int flagA : 1; // 分配1位来存储flagA
    unsigned int flagB : 1; // 分配1位来存储flagB
    unsigned int field : 6; // 分配6位来存储field
} BitFieldStruct;
//总的一共占1个字节

对于位段结构(bit-field structure),计算其对齐的方式与普通结构体略有不同。位段结构的对齐取决于其内部所定义的最宽的数据类型成员的对齐要求。

再去试试最开始的题目吧!


如需补充改正,欢迎大家留言

感谢支持