严格的别名和内存位置
严格别名会阻止我们使用不兼容的类型访问相同的内存位置。
int* i = malloc( sizeof( int ) ) ; //assuming sizeof( int ) >= sizeof( float ) *i = 123 ; float* f = ( float* )i ; *f = 3.14f ;
根据C标准,这将是非法的,因为编译器“知道” float
左值不能访问int
。
如果我使用该指针指向正确的内存,如下所示:
int* i = malloc( sizeof( int ) + sizeof( float ) + MAX_PAD ) ; *i = 456 ;
首先我为int
, float
分配内存,最后一部分是内存,它允许float
存储在对齐的地址上。 float
需要在4的倍数上对齐MAX_PAD
通常是16个字节中的8个,具体取决于系统。 在任何情况下, MAX_PAD
都足够大,因此可以正确对齐float
。
然后我写了一个int
到i
,到目前为止一直很好。
float* f = ( float* )( ( char* )i + sizeof( int ) + PaddingBytesFloat( (char*)i ) ) ; *f= 2.71f ;
我使用指针i
,用int
的大小递增它并使用PaddingBytesFloat()
函数正确对齐它,它返回给定地址时对齐float
所需的字节数。 然后我写了一个浮点数。
在这种情况下, f
指向不重叠的不同存储位置; 它有不同的类型。
以下是标准(ISO / IEC 9899:201x)6.5中的一些部分,我在编写此示例时依赖于此。
别名是指多个左值指向同一个内存位置。 标准要求这些左值具有与对象的有效类型兼容的类型。
什么是有效类型,引自标准:
用于访问其存储值的对象的有效类型是对象的声明类型(如果有).87)如果通过具有非字符类型的类型的左值将值存储到没有声明类型的对象中,然后左值的类型成为该访问的对象的有效类型以及不修改存储值的后续访问。 如果使用memcpy或memmove将值复制到没有声明类型的对象中,或者将其复制为字符类型数组,则该访问的修改对象的有效类型以及不修改该值的后续访问的有效类型是复制值的对象的有效类型(如果有)。 对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。
87)分配的对象没有声明的类型。
我正在尝试连接各个部分并弄清楚是否允许这样做。 在我的解释中,分配对象的有效类型可以根据该内存上使用的左值的类型进行更改,因为这部分: 对于没有声明类型的对象的所有其他访问,对象的有效类型就是用于访问的左值的类型。
这合法吗? 如果没有,如果我在第二个例子中使用void指针作为lvalue而不是int指针i
怎么办? 如果即使这样也行不通,如果我将第二个例子中指定给浮点指针的地址作为memcopied值,并且该地址以前从未用作左值,该怎么办呢?
我认为是的,这是合法的。
为了说明我的观点,让我们看看这段代码:
struct S { int i; float f; }; char *p = malloc(sizeof(struct S)); int *i = p + offsetof(struct S, i); //this offset is 0 by definition *i = 456; float *f = p + offsetof(struct S, f); *f= 2.71f;
对于适当的PaddingBytesFloat()
和MAX_PAD
值,这个代码,IMO,显然是合法的,从编译器的角度来看,它等同于你的MAX_PAD
。
请注意,我的代码不使用struct S
类型的任何l值,它仅用于简化paddings的计算。
当我阅读标准时,在malloc的内存中没有声明的类型,直到写入那里。 然后声明的类型是写入的内容。 因此,可以随时更改此类内存的声明类型,使用不同类型的值覆盖内存,非常类似于union。
TL; DR:我的结论是,对于动态内存,只要您使用与上次写入该内存相同的类型(或兼容的类型)读取内存,就可以安全地使用严格别名。
是的,这是合法的。 要了解原因,您甚至不需要考虑严格的别名规则,因为它不适用于这种情况。
根据C99标准,当你这样做时:
int* i = malloc( sizeof( int ) + sizeof( float ) + MAX_PAD ) ; *i = 456 ;
malloc
将返回一个指向内存块的指针,该内存块足够大以容纳sizeof(int)+sizeof(float)+MAX_PAD
。 但请注意,您只使用这么大的一小块; 特别是,您只使用第一个sizeof(int)
字节。 因此,您将留下一些可用于存储其他对象的可用空间,只要将它们存储为不相交的偏移量(即,在第一个sizeof(int)
字节之后)。 这与对象究竟是什么的定义密切相关。 从C99第3.14节:
对象:执行环境中的数据存储区域,其内容可以表示值
i
指向的对象内容的精确含义是值456
; 这意味着整数对象本身只占用您分配的内存块的一小部分。 标准中没有任何内容阻止您在前面几个字节存储任何类型的新的不同对象。
这段代码:
float* f = ( float* )( ( char* )i + sizeof( int ) + PaddingBytesFloat( (char*)i ) ) ; *f= 2.71f ;
将另一个对象有效地附加到已分配内存的子块。 只要f
的结果内存位置与i
内存位置不重叠,并且还有足够的空间来存储float
,您将始终是安全的。 严格别名规则在这里甚至不适用,因为指针指向不重叠的对象 – 内存位置不同。
我认为这里的关键点是要理解你正在有效地操纵两个不同的对象,有两个不同的指针。 恰好两个指针指向同一个malloc()
‘d块,但它们彼此足够远,所以这不是问题。
您可以查看此相关问题: 哪些对齐问题限制了malloc创建的内存块的使用? 阅读Eric Postpischil的好答案: https ://stackoverflow.com/a/21141161/2793118 – 毕竟,如果你可以在同一个malloc()
块中存储不同类型的数组,为什么不存储int
和a float
? 您甚至可以将代码视为这些数组是单元素数组的特殊情况。
只要您处理对齐问题,代码就完美无缺,100%便携。
更新(后续行动,阅读以下评论) :
我相信你对标准没有对malloc()
‘d对象强制执行严格别名的推理是错误的。 确实,动态分配的对象的有效类型可以改变,正如标准所传达的那样(这是使用具有不同类型的左值表达式来存储新值的问题),但请注意,一旦你做了那么,确保没有其他类型的其他左值表达式访问对象值是你的工作。 这由第6.5节的规则7强制执行,您在问题中引用了它:
对象的存储值只能由具有以下类型之一的左值表达式访问: – 与对象的有效类型兼容的类型;
因此,当您更改对象的有效类型时,您隐含地向编译器承诺,您将不使用具有不兼容类型的旧指针访问此对象(与新的有效类型相比)。 对于严格别名规则而言,这应该足够了。
我发现了一个很好的类比。 您可能还会发现它很有用。 引自ISO/IEC 9899:TC2 Committee Draft — May 6, 2005 WG14/N1124
6.7.2.1结构和联合说明符
[16]作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型; 这称为灵活的arrays成员。 在大多数情况下,忽略灵活的数组成员。 特别地,结构的尺寸好像省略了柔性arrays构件,除了它可以具有比遗漏所暗示的更多的拖尾填充。 但是,当一个。 (或 – >)运算符有一个左操作数,它是一个带有灵活数组成员的结构(指针),右操作数命名该成员,它的行为就好像该成员被最长的数组替换(具有相同的元素类型) )不会使结构大于被访问的对象; 数组的偏移量应保持为灵活数组成员的偏移量,即使这与替换数组的偏移量不同。 如果此数组没有元素,则其行为就好像它有一个元素,但如果任何尝试访问该元素或生成一个经过它的指针,则行为是未定义的。
[17]示例声明后:
struct s {int n; double d []; };结构struct具有灵活的数组成员d。 使用它的典型方法是:
int m = / *某个值* /; struct s * p = malloc(sizeof(struct s)+ sizeof(double [m]));并且假设对malloc的调用成功,p指向的对象在大多数情况下表现得好像p已被声明为:
struct {int n; 双d [m]; }> * p;(在某些情况下,这种等同性被破坏;特别是,成员d的偏移量可能不同)。
使用如下示例会更公平:
struct ss { double da; int ia[]; }; // sizeof(double) >= sizeof(int)
在上面引用的例子中, struct s
大小与int
(+ padding)相同,然后是double。 (或其他一些类型, float
在你的情况下)
在struct start之后访问内存sizeof(int) + PADDING
字节为double
(使用语法糖 )看起来很好,按照这个例子,所以我相信你的例子是合法的C.
严格的别名规则允许更积极的编译器优化,特别是能够重新排序对不同类型的访问,而不必担心它们是否指向相同的位置。 因此,例如,在您的第一个示例中,编译器将写入重新排序为i
和f
是完全合法的,因此您的代码是未定义行为(UB)的示例。
此规则有一个例外,您可以从标准中获得相关报价
具有不是字符类型的类型
你的第二段代码是完全安全的。 存储区域不重叠,因此如果跨越该边界重新排序存储器访问则无关紧要。 实际上,两段代码的行为完全不同。 第一个将int放在内存区域中,然后浮点放入同一个内存区域,而第二个放置一个int到一个内存区域,一个浮点放到它旁边的一些内存中。 即使重新排序这些访问,您的代码也会产生相同的效果。 完美,合法。
我觉得我在这里错过了真正的问题。
如果你真的想要在你的第一个程序中行为,那么摆弄低级内存的最安全的方法是(a)联合或(b) char *
。 在很多C代码中使用char *
然后转换为正确的类型,例如:在这个pcap教程中 (向下滚动到“对于那些坚持指针无用的新C程序员,我会打击你。”