考虑这个人为的例子:
#include <stddef.h>
static inline void nullify(void **ptr) {
*ptr = NULL;
}
int main() {
int i;
int *p = &i;
nullify((void **) &p);
return 0;
}
&p
(一个int **
)被铸造到void **
,然后被解除引用。这会破坏严格的别名规则吗?
根据standard:
对象的存储值只能由具有以下类型之一的左值表达式访问:
- 与对象的有效类型兼容的类型,
因此,除非void *
被认为与int *
兼容,否则这违反了严格的别名规则。
但是,这不是gcc警告所暗示的(即使它没有任何证据)。
在编译此示例时:
#include <stddef.h>
void f(int *p) {
*((float **) &p) = NULL;
}
gcc
警告严格别名:
$ gcc -c -Wstrict-aliasing -fstrict-aliasing a.c
a.c: In function ‘f’:
a.c:3:7: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
*((float **) &p) = NULL;
~^~~~~~~~~~~~~
但是,使用void **
,它不会发出警告:
#include <stddef.h>
void f(int *p) {
*((void **) &p) = NULL;
}
那么严格的别名规则是否有效?
如果不是,如何编写一个函数来取消任何指针(例如),它不会破坏严格的别名规则?
没有一般要求实现对不同的指针类型使用相同的表示。在将使用不同表示的平台上,例如一个int*
和一个char*
,没有办法支持单个指针类型void*
可以互换地作用于int*
和char*
。虽然可以互换地处理指针的实现将有助于在使用兼容表示的平台上进行低级编程,但是这种能力在所有平台上都是不可支持的。因此,标准的作者没有理由要求支持这样的功能,而不是将其视为执行质量问题。
据我所知,像icc这样适合低级编程的质量编译器,以及所有指针具有相同表示的目标平台,对于以下构造都没有任何困难:
void resizeOrFail(void **p, size_t newsize)
{
void *newAddr = realloc(*p, newsize);
if (!newAddr) fatal_error("Failure to resize");
*p = newAddr;
}
anyType *thing;
... code chunk #1 that uses thing
resizeOrFail((void**)&thing, someDesiredSize);
... code chunk #2 that uses thing
请注意,在此示例中,使用thing
的两个代码块之间明显出现了获取事物地址的行为以及所有使用结果指针的行为。因此,没有实际的别名,任何非故意盲目的编译器都会毫不费力地认识到将thing
的地址传递给reallocorFail
的行为可能会导致thing
被修改。
另一方面,如果用法类似于:
void **myptr;
anyType *thing;
myptr = &thing;
... code chunk #1 that uses thing
*myptr = realloc(*myptr, newSize);
... code chunk #2 that uses thing
那么即使质量编译器也可能没有意识到thing
可能会在使用它的两个代码块之间受到影响,因为在这两个块之间没有引用任何类型的anyType*
。在这样的编译器上,有必要将代码编写为:
myptr = &thing;
... code chunk #1 that uses thing
*(void *volatile*)myptr = realloc(*myptr, newSize);
... code chunk #2 that uses thing
让编译器知道*mtptr
上的操作正在做一些“怪异”的事情。用于低级编程的质量编译器会将此视为一种迹象,表明他们应该避免在这样的操作中缓存thing
的值,但即使是volatile
限定符也不足以实现像gcc和clang这样的优化模式旨在适用于不涉及低级编程的目的。
如果像reallocOrFail
这样的函数需要使用不适合低级编程的编译器模式,可以写成:
void resizeOrFail(void **p, size_t newsize)
{
void *newAddr;
memcpy(&newAddr, p, sizeof newAddr);
newAddr = realloc(newAddr, newsize);
if (!newAddr) fatal_error("Failure to resize");
memcpy(p, &newAddr, sizeof newAddr);
}
然而,这将要求编译器允许resizeOrFail
可能改变任何类型的任意对象的值 - 不仅仅是数据指针 - 从而不必要地损害应该有用的优化。更糟糕的是,如果有问题的指针恰好存储在堆上(并且不是void*
类型),那么仍然允许不适合低级编程的符合编译器假设第二个memcpy
不能可能会影响它。
低级编程的一个关键部分是确保选择适合该目的的实现和模式,并知道何时需要volatile
限定符来帮助他们。一些编译器供应商可能声称任何要求编译器适合其目的的代码都是“破坏”的,但是试图安抚这些供应商将导致代码的效率低于使用适合于某个目的的质量编译器所产生的代码。