请考虑以下人工示例:
#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;
}
那么严格的别名规则是否有效?
如果不是,如何编写一个函数来使任何指针(例如不破坏严格别名规则的指针)无效?
答案 0 :(得分:1)
没有一般的要求,即实现对不同的指针类型使用相同的表示形式。在将使用不同表示形式的平台上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
限定符来帮助他们。一些编译器供应商可能声称,要求编译器适合其用途的任何代码都是“破损的”,但试图安抚此类供应商将导致其代码效率低于使用适合自己目的的高质量编译器所产生的代码。