微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

是否可以在不违反严格别名的情况下使用字符数组作为内存池?

如何解决是否可以在不违反严格别名的情况下使用字符数组作为内存池?

我有一个静态分配的字符数组。我可以在不违反严格的别名规则的情况下重用这个数组来存储不同的类型吗?我不太了解严格的别名,但这里有一个代码示例,可以完成我想做的事情:

#include <stdio.h>

static char memory_pool[256 * 1024];

struct m1
{
    int f1;
    int f2;
};

struct m2
{
    long f1;
    long f2;
};

struct m3
{
    float f1;
    float f2;
    float f3;
};

int main()
{
    void *at;
    struct m1 *m1;
    struct m2 *m2;
    struct m3 *m3;

    at = &memory_pool[0];
    
    m1 = (struct m1 *)at;
    m1->f1 = 10;
    m1->f2 = 20;

    printf("m1->f1 = %d,m1->f2 = %d;\n",m1->f1,m1->f2);

    m2 = (struct m2 *)at;
    m2->f1 = 30L;
    m2->f2 = 40L;

    printf("m2->f1 = %ld,m2->f2 = %ld;\n",m2->f1,m2->f2);

    m3 = (struct m3 *)at;
    m3->f1 = 5.0;
    m3->f2 = 6.0;
    m3->f3 = 7.0;

    printf("m3->f1 = %f,m3->f2 = %f,m3->f3 = %f;\n",m3->f1,m3->f2,m3->f3);

    return 0;
}

我使用带有 -Wstrict-aliasing=3 -fstrict-aliasing 的 gcc 编译了这段代码,它按预期工作:

m1->f1 = 10,m1->f2 = 20;
m2->f1 = 30,m2->f2 = 40;
m3->f1 = 5.000000,m3->f2 = 6.000000,m3->f3 = 7.000000;

那个代码安全吗?假设 memory_pool 总是足够大。

解决方法

是否可以在不违反严格别名的情况下使用字符数组作为内存池?

没有。 C 2018 6.5 7 中的规则说,定义为 char 数组的对象可以通过以下方式访问:

  1. char 数组兼容的类型,
  2. char 数组兼容的类型的限定版本,
  3. 对应于char数组的有符号或无符号类型,
  4. 对应于char数组的有符号或无符号类型,
  5. 在其成员中包含 char 数组的聚合或联合类型,或
  6. 一种字符类型。

3 和 4 对于 char 数组是不可能的;它们仅在原始类型为整数类型时适用。在您的各种结构示例中,这些结构的类型与 char 数组不兼容(也不是它们的成员),排除了 1 和 2。它们的成员中不包括 char 数组,规则out 5. 它们不是字符类型,排除了 6。

我已经使用 gcc 和 -Wstrict-aliasing=3 -fstrict-aliasing 编译了这段代码,它按预期工作:

示例输出显示代码在一次测试中产生了所需的输出。这并不等同于显示它按预期工作。

那个代码安全吗?

没有。在某些情况下可以使代码安全。首先,以适当的对齐方式声明它,例如static _Alignas(max_align_t) memory_pool[256 * 1024];。 (max_align_t<stddef.h> 中定义。)这使得指针转换部分定义。

其次,如果你使用GCC或者Clang,请求-fno-strict-aliasing,编译器提供了C语言的扩展,放宽了C 2018 6.5 7。或者,在某些情况下,也可以从知识推导编译器和链接器设计,即使违反了 6.5 7,您的程序也能正常工作:如果程序在单独的翻译单元中编译,并且目标模块不包含类型信息或未使用花哨的链接时优化,并且没有别名违规发生在实现内存池的翻译单元中,那么违反 6.5 7 不会产生不利后果,因为 C 实现没有办法将违反 6.5 7 的代码与不违反内存池的代码区分开来。此外,您必须知道指针转换按预期工作,它们有效地生成指向相同地址的指针(而不仅仅是可以转换回原始指针值但不能直接用作指向同一内存的指针的中间数据) .

没有不良后果的推论是脆弱的,应谨慎使用。例如,很容易在实现内存池的翻译单元中意外违反 6.5 7,例如将指针存储在已释放的内存块中或将大小信息存储在已分配块之前的隐藏头中。

,

标准特意避免要求所有实现都适合低级编程,但允许旨在用于低级编程的实现扩展语言以支持此类使用,方法是在比标准规定的更多情况下指定它们的行为。然而,即使使用专为低级编程设计的编译器,使用字符数组作为内存池通常也不是一个好主意。然而,为了与最广泛的编译器和平台兼容,应该将内存池对象声明为具有最宽对齐类型的数组,或者包含具有最宽对齐类型的字符数组 long 的联合,例如

 static uint64_t my_memory_pool_allocation[(MY_MEMORY_POOL_SIZE+7)/8];
 void *my_memory_pool_start = my_memory_pool_allocation;

 union
 {
   unsigned char bytes[MY_MEMORY_POOL_SIZE];
   double alignment_force;
 } my_memory_pool_allocation;
 void *my_memory_pool_start = my_memory_pool_allocation.bytes;

请注意,clang 和 gcc 可以配置为通过使用 -fno-strict-aliasing 标志以适合低级编程的方式扩展语言,并且商业编译器通常可以支持低级概念,例如内存池,即使使用基于类型的别名,因为它们将指针类型转换视为可能错误的基于类型的别名假设的障碍。

如果 void* 被初始化为一个静态对象的地址,该对象的符号没有在其他上下文中使用,我认为任何普通编译器都不会关心用于初始化的类型。跳过箍在这里跟随标准是一个傻瓜的差事。当不使用 -fno-strict-aliasing 时,clang 和 gcc 都不会处理标准规定的所有极端情况,并且使用 -fno-strict-aliasing,它们将扩展语言的语义以允许使用内存池无论标准是否要求,都方便。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。