是否在 C 中定义了通过指向 char 的指针访问联合空间"extra"?



C标准允许通过指向字符类型的指针访问对象(§6.3):

对象的存储值只能由具有以下内容之一的左值访问类型:[…]

  • 字符类型

这允许像memcpy()fwrite()这样的函数工作。

假设我有一个用于变体类型的并集类型(也称为标记并集):

union var_uint
{
uint8_t  n1;
uint32_t n4;
};
enum kind_t
{
KIND_1,
KIND_4
};
struct tagged_uint
{
enum  kind_t   kind;
union var_uint value;
};

C标准还说:

  • 并集的大小是其最大成员的大小,并集的末尾可能有未命名的填充
  • 类型punning(通过作为char数组的另一个成员访问联合成员)是实现定义的(我认为通过不是上述成员的char数组访问它也应该是合法的?)

是否定义了通过字符指针访问联合类型的完整大小的行为,即使您实际上没有任何依赖于填充字节值的逻辑?例如:

union var_uint number;
number.n1 = 127;
struct tagged_uint tagged_number = { KIND_1, number };
fwrite(&tagged_number, sizeof (union var_uint), 1, my_stream);
// Later.
struct other_tagged_number;
fread(&other_tagged_number, sizeof (union var_uint), 1, my_reopened_stream);

在这里,必须访问填充字节才能将其写入流,即使这对稍后的代码逻辑没有影响(假设它在访问var_uint成员之前检查kind字段)。

我现在只有C90标准,但我对其他标准也很感兴趣。

(我实际上并不是用这种方式将数据串行化到磁盘上。)

C99规范的6.2.6.1有两个相关段落:

当值存储在结构或并集类型的对象中时,包括在成员中对象,对象表示形式中与任何填充字节对应的字节未指定的值。填充字节的值不应影响这样的对象是陷阱表示。结构或并集对象中在与位字段成员相同的字节中,但不是该成员的一部分,同样不应影响此类对象的值是否为陷阱表示。

当一个值存储在联合类型对象的成员中时,该对象的字节与该成员不对应但与其他成员对应的表示取未指定的值,但并集对象的值不应因此成为陷阱代表。

因此,访问填充并不是未定义的行为,只要您使用的类型(如char)不能有任何陷阱值。

C89标准作为一个整体,就像它所描述的语言一样,是基于一个抽象模型的,在这个抽象模型中,适合存储区域的每个T类型的可寻址对象都将根据其地址处(T)字节大小的内容,持有其类型的值或陷阱表示。当对象包含一个值时,尝试读取该对象将产生该值;当对象持有陷阱表示时,尝试读取该对象的后果不在标准的管辖范围内。注意,在这个模型下,关于在并集中写入一个对象和读取另一个对象的行为的任何说法都同样适用于代码,例如,用另一种类型的指针写入对象,将指针转换为不同的类型,然后使用该类型读取对象。

许多关于结构和并集的模糊性是为了适应这样的实现,即写入较大内存区域可能比写入较小内存区域更快。例如,如果一个小端序平台支持8位和32位读写,但不支持16位,并且其结构类型为:

struct foo {
uint16_t a,b,c; // Followed by 16 bits of padding
uint32_t d;
} *p;

通过将23存储到保持p->c的32位字来处理p->c = 23;可能比通过使用一对8位写入更快。然而,如果还有一种结构类型:

struct bar {
uint16_t a,b,c,d;
} my_bar;

并且一个是在p指向my_bar时执行p->c = 23;,这样的操作将破坏my_bar.d的值。尽管在某些情况下,能够读取和写入常见的初始序列成员是有用的,但允许CIS成员写入和读取会使处理p->c = 23;的编译器有必要使用一对字节存储而不是字存储,这可能会导致很大的速度损失。《标准》的作者可能认为,没有理由以这种方式处理写入的实现可以并且将通过以避免损坏附近对象的方式处理CIS写入来扩展语言,而不管《标准》是否要求他们这样做,因此没有必要让《标准》处理CIS写入的行为。

尽管该标准似乎暗示,对于每一个类型为T的对象,都存在一个占用相同存储的char[sizeof (T)]对象,并且可以使用后一个对象执行操作,以访问与前一个对象相关联的存储,其语义由原始抽象模型隐含,但它实际上并没有说存在这样的对象。给定,例如

union foo {
unsigned char x[2];
int y;
} it;

标准从不明确指针表达式(unsigned char*)it是否产生指向隐式覆盖itchar[sizeof (union foo)]对象的第一元素的指针而不是例如指向char[2]对象u.x的第一元素(其当然将共享相同地址)的指针。

请注意,这种模糊性不会造成在某些情况下一段代码可能有两种不同的定义行为的情况。在几乎所有这样的情况下,很明显,行为要么定义明确,要么根本没有定义。如果编译器在合理的情况下将构造视为具有明确定义的行为,无论标准是否要求它们这样做,那么标准是否留下了是否定义行为的模糊问题也就无关紧要了。不幸的是,编译器编写者和程序员经常对什么时候是"编译"产生分歧;明智的";将可能只有一个可能定义含义的动作视为UB。

我不认为今天的任何编译器在你描述的情况下都会做出荒谬的行为,但我不认为《标准》会禁止这种行为。相反,它依赖于编译器作者运用常识,而在这种特殊的情况下,据我所知,他们并没有把常识抛到九霄云外。

最新更新