我最近一直试图理解严格别名的一个特定方面,我想我已经制作了尽可能小的有趣代码。 (对我来说很有意思!)
更新:根据目前为止的答案,我很清楚我需要澄清这个问题。这里的第一个列表是"显然"从某个角度来定义行为。真正的问题是遵循这个逻辑到自定义分配器和自定义内存池。如果我在开始时malloc
有一大块内存,然后编写我自己使用该单个大块的my_malloc
和my_free
,那么就是UB,理由是它没有&# 39;使用官方free
?
我会坚持使用C,有点随意。我得到的印象是更容易谈论,C标准更清晰。
int main() {
uint32_t *p32 = malloc(4);
*p32 = 0;
free(p32);
uint16_t *p16 = malloc(4);
p16[0] = 7;
p16[1] = 7;
free(p16);
}
第二个malloc
可能会返回与第一个malloc
相同的地址(因为它之间是free
d)。这意味着它正在访问具有两种不同类型的相同内存,这违反了严格的别名。那么上面肯定是未定义的行为(UB)?
(为简单起见,让我们假设malloc
总是成功。我可以添加malloc
的返回值的检查,但这会使问题变得混乱)
如果不是UB,为什么?标准中是否有明确的例外情况,表示允许malloc
和free
(以及calloc
/ realloc
/ ...)"删除&# 34;与特定地址相关联的类型,允许进一步访问" imprint"地址上的新类型?
如果malloc
/ free
是特殊的,那么这是否意味着我不能合法地编写我自己的分配器来克隆malloc
的行为?我确定有很多项目都有自定义分配器 - 它们都是UB吗?
因此,如果我们决定这样的自定义分配器必须定义行为,那么这意味着严格别名规则本质上是"不正确"。我会更新它,说可以通过不同(' new')类型的指针写(不是读),只要你不再使用旧类型的指针。如果确认所有编译器基本上都遵守了这条新规则,那么这个措辞可能会悄然改变。
我的印象是gcc
和clang
基本上尊重我的(积极的)重新解释。如果是这样,也许应该相应地编辑标准?我的证据'关于gcc
和clang
很难描述,它使用memmove
具有相同的源和目标(因此被优化出来),以便它阻止任何不需要的优化,因为它告诉将来通过目标指针读取的编译器将对通过源指针先前写入的位模式进行别名。我能够相应地阻止不受欢迎的解释。但我想这不是真正的证据,也许我只是幸运。 UB显然意味着编译器也被允许给我误导性的结果!
(...除非,当然,还有另一条规则使memcpy
和memmove
与malloc
特殊的方式相同。他们可以改变目标指针类型的类型。这符合我的证据&#39 ;.)
无论如何,我在漫无边际。我想一个非常简短的答案是:"是的,malloc
(和朋友)是特别的。自定义分配器不是特殊的,因此是UB,除非它们为每种类型维护单独的内存池。而且,进一步看一个极端代码的示例X,其中编译器Y确实做了不需要的东西,因为编译器Y在这方面非常严格,并且与这种重新解释相矛盾。"
跟进:非malloc
内存怎么样?是否同样适用。 (局部变量,静态变量,......)
答案 0 :(得分:4)
注意:这只回答了初始问题,而不是关于自定义分配器的部分。
不,它不是UB,因为p16
现在拥有不同的对象而前者在您调用free(p32)
之后就消失了。
请注意,malloc()
返回为每个对象预先对齐的指针,因此这避免了在实际条件下破坏严格别名。来自C11(N1570)7.22.3 / p1 内存管理功能(强调我的):
如果分配成功,则返回指针 它可以被赋值给任何类型的对象的指针 基本对齐要求然后用来访问这样的 对象或分配的空间中的此类对象的数组(直到 空间被明确释放)。已分配对象的生存期 从分配延伸到解除分配。
答案 1 :(得分:3)
以下是C99严格的别名规则(我希望是)他们的全部:
6.5
(6)访问其存储值的对象的有效类型是对象的声明类型(如果有)。如果通过具有非字符类型的左值的值将值存储到没有声明类型的对象中,则左值的类型将成为该访问的对象的有效类型以及不修改该值的后续访问的有效类型储值。如果将值复制到没有使用声明类型的对象中 memcpy或memmove,或者被复制为字符类型数组,然后是有效类型 用于该访问的修改对象以及不修改该访问的后续访问 value是从中复制值的对象的有效类型(如果有)。对于 对没有声明类型的对象的所有其他访问,对象的有效类型是 只是用于访问的左值的类型。(7)对象的存储值只能由具有其中一个的左值表达式访问 以下类型:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,
- 与对象的有效类型对应的有符号或无符号类型的类型,
- 对应于对象有效类型的限定版本的有符号或无符号类型的类型,
- 在其成员中包含上述类型之一的聚合或联合类型(包括递归地,子聚合或包含联合的成员),或者
- 字符类型。
这两个子句一起禁止一个特定情况,通过类型X的左值存储值,然后通过与X不兼容的Y类左值检索值。
因此,当我阅读标准时,即使这种用法也完全正常(假设4个字节足以存储uint32_t
或两个uint16_t
)。
int main() {
uint32_t *p32 = malloc(4);
*p32 = 0;
/* do not do this: free(p32); */
/* do not do this: uint16_t *p16 = malloc(4); */
/* do this instead: */
uint16_t *p16 = (uint16_t *)p32;
p16[0] = 7;
p16[1] = 7;
free(p16);
}
没有规定禁止存储uint32_t
然后将uint16_t
存储在同一地址,因此我们完全可以。
因此,没有什么能阻止编写完全兼容的池分配器。
答案 2 :(得分:1)
您的代码是正确的C并且不会调用未定义的行为(除了您不测试malloc返回值),因为:
未定义的是p16
是否会获得与p32
在不同时间 相同的值
什么是未定义的行为,即使值相同,也可以在释放后访问p32
。示例:
int main() {
uint32_t *p32 = malloc(4);
*p32 = 0;
free(p32);
uint16_t *p16 = malloc(4);
p16[0] = 7;
p16[1] = 7;
if (p16 == p32) { // whether p16 and p32 are equal is undefined
uint32_t x = *p32; // accessing *p32 is explicitely UB
}
free(p16);
}
它是UB,因为您在释放后尝试访问内存块。即使它确实指向内存块,该内存块也已初始化为uint16_t
数组,使用它作为指向另一种类型的指针是正式未定义的行为。
自定义分配(假设符合C99的编译器):
所以你有很大的内存,想要编写自定义的free和malloc函数而不需要UB。有可能的。在这里,我不会深入探讨分配和自由集团管理的难点,只是给出提示。
您必须使您的内存池从与系统对齐兼容的地址开始:
intptr_t orig_addr = chunk;
int delta = orig_addr % alignment;
char *pool = chunk + alignement - delta; /* pool in now aligned */
您现在只需从您自己的池集合地址返回pool + n * alignement
并转换为void *
:6.3.2.3§1说:指向void的指针可能会被转换为或者从指向任何不完整或对象类型的指针。指向任何不完整或对象类型的指针可能会转换为指向void的指针并再次返回;结果应该等于原始指针。
使用C11会更干净,因为C11明确地添加了_Alignas
和alignof
关键字来明确处理它,它会比当前的黑客更好。但它应该可以工作
限制:
我必须承认,我对6.3.2.3§7的解释是指向正确对齐的char数组的指针可以转换为另一种类型的指针并不是很整洁。有人可能会说,所说的只是如果它最初指向另一种类型,它可以用作char指针。但是当我从一个char指针开始时,它没有明确允许。这是真的,但它是可以做到的最好的,它没有被明确地标记为未定义的行为......而且它正是malloc所做的。
由于对象明确依赖于实现,因此无法创建可用于任何实现的通用库。
答案 3 :(得分:0)
关于混叠的实际规则在标准6.5节第7段中列出。注意措辞:
对象的存储值只能由具有以下类型之一的左值表达式访问:
(强调我的)
别名包括对象的概念,而不仅仅是一般的记忆。要使malloc
在第二次使用时返回相同的地址,则需要取消分配原始对象。即使它具有相同的地址,也不会被视为相同的对象。由于完全不同的原因,在free
之后剩余的指针通过悬空指针访问第一个对象的任何尝试都是UB,所以没有别名,因为任何继续使用第一个指针p32
无论如何都是无效的。 / p>