在zwol对Is it legal to implement inheritance in C by casting pointers between one struct that is a subset of another rather than first member?的回答中,他举例说明了为什么在类似结构之间进行简单类型转换并不安全,并且在注释中有一个示例环境,其行为异常:在gcc上编译以下内容-O2使其打印“ x = 1.000000 some = 2.000000”
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
struct base
{
double some;
char space_for_subclasses[];
};
struct derived
{
double some;
int value;
};
double test(struct base *a, struct derived *b)
{
a->some = 1.0;
b->some = 2.0;
return a->some;
}
int main(void)
{
size_t bufsz = sizeof(struct base);
if (bufsz < sizeof(struct derived)) bufsz = sizeof(struct derived);
void *block = malloc(bufsz);
double x = test(block, block);
printf("x=%f some=%f\n", x, *(double *)block);
return 0;
}
我正在鬼混代码以更好地准确理解它的行为,因为我需要做类似的事情,并且注意到将a
标记为volatile
足以防止其打印不同的值。这符合我对出问题的期望-gcc假设a->some
不受写入b->some
的影响。但是,我本以为gcc仅在a
或b
被标记为restrict的情况下才可以假设。
我误解了这里发生的事情和/或限制限定符的含义吗?如果不是,gcc是否可以自由地做出这个假设,因为{em} a
和b
是不同类型的?最后,将a
和b
都标记为volatile
是否使此代码符合标准,或者至少防止未定义的行为允许gcc做出上述假设?
答案 0 :(得分:3)
如果仅使用volatile
限定的左值访问存储区域,则编译器将不得不走得很远,不要将每次写入都转换为将值转换为位模式并存储它,并且每次读取都是从内存中读取位模式并将其转换为值。该标准实际上并未强制要求这种行为,并且在理论上提供了以下编译器:
long long volatile foo;
...
int test(void)
{
return *((short volatile*)(&foo));
}
可以假设任何可以调用test
的代码分支都将永远不会执行,但是我还不知道任何以这种极端方式运行的编译器。
另一方面,给出如下功能:
void zero_aligned_pair_of_shorts(uint16_t *p)
{
*((uint32_t void volatile*)&p) = 0;
}
像gcc
和clang
这样的编译器将无法可靠地识别出它可能会对使用类型uint16_t
的不合格左值访问的对象的存储值产生某些影响。诸如icc之类的某些编译器将volatile
访问视为同步已获取其地址的所有寄存器缓存对象的指示符,因为这样做是一种编译器坚持标准中所述的C精神的廉价而简便的方法。宪章和基本原理文件,因为“不要阻止程序员做需要做的事情”,而无需特殊的语法。但是,其他编译器(例如gcc和clang)要求程序员要么使用gcc / clang特定的内在函数,要么使用命令行选项来全局阻止大多数形式的寄存器缓存。
答案 1 :(得分:0)
此特定问题和zwol's answer的问题在于它们将类型调整和严格别名混为一谈。 Zwol的答案对于该特定用例是正确的,因为它用于初始化结构。但在一般情况下,也不是wrt。像struct sockaddr
那样的POSIX类型可能会暗示答案。
对于在具有公共初始成员的结构类型之间进行类型修剪的情况,您要做的就是声明(不使用!)这些结构的并集,并且可以通过任何结构类型的指针安全地访问公共成员。 。自ANSI C 3.3.2.3起,这是明确允许的行为,包括C11 6.5.2.3p6(链接到n1570草稿)。
如果一个实现包含用户空间应用程序可见的所有struct sockaddr_
结构的并集,那么我认为zwol's answer OP链接是误导的,如果有人读到它暗示该struct sockaddr
结构支持需要编译器提供的一些非标准内容。 (如果定义_GNU_SOURCE
,则glibc会将这样的联合定义为struct __SOCKADDR_ARG
,其中包含所有此类类型的匿名联合。但是,glibc设计为使用GCC进行编译,因此可能会有其他问题。)< / p>
Strict aliasing是一个函数的参数不能引用相同的存储(内存)的要求。例如,如果您有
int i = 0;
char *iptr = (char *)(&i);
int modify(int *iptr, char *cptr)
{
*cptr = 1;
return *iptr;
}
然后调用modify(&i, iptr)
是严格的别名冲突。在iptr
的定义中修剪类型是偶然的,并且实际上是允许的(因为允许您使用char
类型来检查任何类型的存储表示; C11 6.2.6.1p4)。 / p>
这里是类型校正的正确示例,避免了严格的别名问题:
struct item {
struct item *next;
int type;
};
struct item_int {
struct item *next;
int type; /* == ITEMTYPE_INT */
int value;
};
struct item_double {
struct item *next;
int type; /* == ITEMTYPE_DOUBLE */
double value;
};
struct item_string {
struct item *next;
int type; /* == ITEMTYPE_STRING */
size_t length; /* Excluding the '\0' */
char value[]; /* Always has a terminating '\0' */
};
enum {
ITEMTYPE_UNKNOWN = 0,
ITEMTYPE_INT,
ITEMTYPE_DOUBLE,
ITEMTYPE_STRING,
};
现在,如果在相同的范围内可以看到以下联合,则可以在指向上述结构类型的指针之间进行类型双打,并完全安全地访问next
和type
成员:>
union item_types {
struct item any;
struct item_int i;
struct item_double d;
struct item_string s;
};
对于其他(非常见)成员,我们必须使用与初始化结构相同的结构类型。这就是type
字段存在的原因。
作为这种完全安全的用法的示例,请考虑以下函数,该函数将打印项目列表中的值:
void print_items(const struct item *list, FILE *out)
{
const char *separator = NULL;
fputs("{", out);
while (list) {
if (separator)
fputs(separator, out);
else
separator = ",";
if (list->type == ITEMTYPE_INT)
fprintf(out, " %d", ((const struct item_int *)list)->value);
else
if (list->type == ITEMTYPE_DOUBLE)
fprintf(out, " %f", ((const struct item_double *)list)->value);
else
if (list->type == ITEMTYPE_STRING)
fprintf(out, " \"%s\"", ((const struct item_string *)list)->value);
else
fprintf(out, " (invalid)");
list = list->next;
}
fputs(" }\n", out);
}
请注意,我为值字段使用了相同的名称value
,只是因为我没有想到更好的名称。它们不必相同。
类型绑定发生在fprintf()
语句中,并且仅在以下情况下有效:1)使用与type
字段匹配的结构初始化结构,并且2)union item_types
初始化结构在当前范围内可见。
我尝试过的当前C编译器都没有遇到以上代码的任何问题,即使在极端优化级别上也破坏了标准行为的某些方面。 (我没有检查过MSVC,但是那确实是一个C ++编译器,它也可以编译大多数C代码。但是,如果上面的代码有任何问题,我会感到惊讶。)