仅写入动态分配的内存块的一部分是否会设置整个块的有效类型?

Does writing to only part of a dynamically allocated memory block set the effective type of the whole block?

编辑:C11 Standard - §6.5 Expressions (p6,7) 中提到了我所说的 "effective type"。 (感谢 David C. Rankin 在您的评论中给出此 link。)

看了一些书后,我没有完全理解 C 语言中关于有效类型和严格别名的规则。我在下面的代码中评论了我认为有效类型正在发生的事情。为了这个例子,请假设 int 和 float 的大小相同。

void *memory = malloc(sizeof(int) + sizeof(float));

int *x = memory;    // x points to an "object" with no effective type.
*x = 1;             // x points to an object with effective type int.
float *y = memory;  // y points to an object with effective type int.
++y;                // y points to an "object" with effective type ???

最后,y指向还没有写入的内存。因此,如果 y 指向没有有效类型的 "object" 对我来说是有意义的。

另一方面,在动态分配的 "object," 中写入了一个整数,因此此 "object" 可能会被解释为一个整数数组。从这个角度来看,如果 y 指向一个有效类型为 int 的对象,对我来说是有意义的。

考虑另一个例子:

void *memory = malloc(sizeof(short) + sizeof(float));

short *x = memory;  // x points to an "object" with no effective type.
*x = 1;             // x points to an object with effective type short.
++x;                // x points to an "object" with effective type ???

这里,由于内存对齐的原因,将x指向的东西想象成浮点数似乎是不合理的。由于像这样的对齐问题,我可以理解为什么写入内存块的一部分可能会设置整个块的有效类型。

如果这总是正确的,如果我理解的话,从技术上讲,分配一个巨大的内存块然后在它的两端访问不同的数据类型是未定义的行为。

这确实是促使我研究有效类型的核心问题。我一直在使用自己的内存空间,但我无法弄清楚分配大块内存并将它们解释为连续打包的不同结构是否 技术上 错误。它在实践中一直有效。否则,在动态分配的内存块中实现多种类型存储的有效方法是什么(除了将它们全部放在结构或联合中)?

我仍然在为你的问题的基础而苦苦挣扎,但无论如何..

首先 sizeof(int) + sizeof(float) 是问题开始的地方。这是一个你不能信守的承诺!如果你声明说:

struct t
{
    int x;
    float y;
};

你能确定一直sizeof(struct t) == sizeof(int) + sizeof(float)吗? 你显然不能,因为这完全取决于你所在的特定平台的对齐限制。您可能没有为两者分配足够的存储空间。因此,如果您想连续存储一个 int 和一个 float 并通过各自类型的指针直接访问它们,那么您需要将它们包装在一个结构中并使用该类型的大小。

在你的情况下,因为你知道尺寸是相同的,所以你总是可以争辩说它有效,但标准不能保证这一点。

其次,

float *y = memory;  // y points to an object with effective type int.
++y;                // y points to an "object" with effective type ???

显然是未定义的行为(在您尝试取消引用 y 的那一刻),因为您无法确定 x 和 y 没有别名。除非你知道你有一个相同类型的同类数组,否则你不能进行指针运算。例外可能是 char*,您可以使用它直接查看属于任何类型的内存。同样,如果大小和对齐方式相同,这可能有效,但语言不允许。

Otherwise, what is a valid way to implement the storage of more than one type within a dynamically allocated block of memory (besides putting them all in a struct or union)?

将内存块视为 char 数组并一次读取或写入单一类型,使用 memcpy 与具有正确有效类型的临时变量进行复制。将该变量用于任何算术运算。否则,您将依赖于可能仅在您的硬件上可用的特殊仙尘!

任何想了解 C 标准的人都应该阅读已发布的基本原理文档(例如 http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf,这是“C99 基本原理”的第一个 google 命中)。

支持使 C 语言独一无二的各种有趣结构的能力一直是实施质量问题,超出了标准的管辖范围。相反,QoI 问题旨在留给市场解决。由于其客户需要某些构造的编译器编写者可能会在不考虑标准是否需要的情况下寻求满足客户的需求,因此不需要标准强制支持某些程序需要但其他程序不需要的构造,也没有任何理由担心编写会明确解决所有极端情况的规则。

您询问的案例是标准的作者似乎没有考虑的众多案例之一。因此,最合理的解释是,虽然标准不禁止实现无意义地处理此类构造,但它无意特别邀请此类行为,并且质量实现更关心构造是否有用而不是它是否有用授权应该支持。

有效类型规则基于对缺陷报告 028 的一个写得不好的响应,该响应旨在解决编译器是否给出如下内容的问题:

float test(float *p1, unsigned *p2)
{
  *p1 = 1.0f;
  *p2 = 0;
  return *p1;
}

应该要求允许它可能被以下函数调用的可能性:

float test2(void)
{
  union { float f, unsigned u} uf;
  return test2(&uf.f, &uf.u);
}

响应正确地指出,不应要求编译器允许这种可能性,但引用了荒谬的推理:因为将 unsigned 写入联合对象并读取 float 是实现定义的行为,因此通过指针访问此类对象的行为是未定义的行为。没有理由说使用指针不应产生与直接使用对象相同的实现定义的行为。这里的含义是没有完全定义的联合行为的动作将调用 UB。

事实上,对 DR #028 的正确回应应该是说没有 general 权限使用成员类型的指针访问联合(甚至结构)成员,但是出于类型访问规则的目的,应将通过指针或左值进行的访问视为从可用于访问对象的不同类型之一派生的,应将其视为通过原始类型进行的访问。编译器通常已经适应了代码派生和使用指针的最常见模式,但这种适应背后的实际机制各不相同。因此,编译器何时应容纳派生左值的问题被留作实施质量问题。

有效类型规则试图通过编纂对 DR #028 的响应来“澄清”规则,但没有注意到它将实现定义的行为视为未定义的行为,而没有引用任何这样做的依据,而且它也完全失败了考虑许多重要的极端情况。因此,虽然这些规则本应是为了“澄清”事情,但实际上却起到了相反的作用。

从实际的角度来看,clang 和 gcc 应该被视为处理 C 的一种方言,它不允许通过任何特定的非字符类型访问过的任何存储区域像任何其他类型一样可靠地访问,即使在标准允许此类访问的情况下。相反,其他编译器(如 icc)将认识到,在它可以看到用于形成另一种类型指针的指针或左值的情况下,对该指针的操作可能会影响原始对象,而不管标准是否要求它们注意这些事情。如果 malloc 块中存储的特定部分在块的生命周期内从未通过一种以上的类型访问,即使是 clang 和 gcc 也可能允许使用不同类型访问块的不相交部分。然而,无论是 clang 还是 gcc 都不能可靠地处理有时使用一种类型访问存储区域有时使用另一种类型的情况,即使将用于形成对象地址的唯一指针已从旧类型转换为新类型新类型。