基本问题
在解决此任务之前,您必须处理一个基本问题:构造数组需要具有固定且已知大小的元素。
这是因为数组元素 i 是通过将元素大小的 i 倍添加到数组的基地址来定位的。仅当元素的大小存在(元素具有固定大小)并且您知道它(大小已知)时,才能执行该计算。
虽然您定义Block 包含一个大小为零的成员(Buf 是一个具有零元素的数组),但您打算将该成员用作16 个字节(一个16 个char 的数组)。但是,无法告诉编译器您将分配和使用的 Block 对象实际上是具有 16 个额外字节的 Block 对象。你当然可以为它们分配空间,我会告诉你怎么做,但是你打算如何使用它们呢?如果x 是Ring 对象,而您编写x.blk[i],编译器将生成代码,将i 乘以它认为Block 的大小,这将是错误的,因为编译器认为Block 的 Buf 的字节数为零,但您的 Block 对象更大。
标准 C 与扩展
将结构成员声明为具有零元素的数组是一种扩展(特别是在 GCC 中可用)。 1999 年的 C 标准引入了一个类似的特性,称为灵活的数组成员。在标准 C 中,一个灵活的数组成员被声明为没有维度,而不是零维度。
灵活的数组成员是不完整类型 (C 2018 6.7.2.1 18)。换句话说,类型没有完全指定。数组的成员数是未知的,所以数组的总大小是未知的。
然后,在定义Ring 时,我们不能将blk 成员定义为Block 数组的灵活数组成员,因为标准C 要求数组的元素类型是完整类型( C 2018 6.7.6.2 1,“元素类型不得为不完整或函数类型”)。
因此,这段代码不能被制作成标准 C。这实际上是一个优势:C 标准防止你犯上面的根本错误,即创建一个无法工作的数组,因为它的元素大小未知。
奇怪的是,用于 x86-64 的 GCC 8.1 无法对此进行诊断。它应该给出违反约束的诊断。 Apple LLVM 版本 9.1.0 (clang-902.0.39.2) 确实会发出诊断信息。
但是,我们将继续考虑您编写的代码,使用语言扩展。
元素有多大?
当 C 实现布局结构时,它必须确保结构中的每个成员都正确对齐。 (正确的对齐方式由实现定义,因此它们会有所不同。但是,无论它们是什么,编译器都必须相应地布局结构。)由于结构可以用作数组的元素,因此布局结构的大小必须是这样的,当数组中一个结构跟随另一个结构时,后面结构中的所有成员也正确对齐。
满足此约束要求结构的大小是所有成员的对齐要求的倍数。例如,如果有成员的对齐要求为 4 字节和 8 字节,则结构的大小必须是 8 字节的倍数,因为这是 4 字节和 8 字节的最小公倍数。事实上,所有对齐要求都是 2 的幂,因此所有对齐要求的最小公倍数就是最大(最严格)对齐要求。
这意味着,在为 Block 对象的数组分配空间时,不能简单地为额外的 Buf 元素使用任意数量的字节。您必须确保每个 Block 对象的总大小是其成员对齐要求的倍数。
C 提供了一种了解结构对齐要求的方法。表达式_Alignof(Block) 是对齐要求。因此,如果您希望每个Block 在Buf 中有x 元素,则每个Block 所需的大小是基本结构(sizeof(Block))的大小加上实际数组所需的大小元素 (x * sizeof(char)) 加上足够的填充以将总数四舍五入为对齐要求的倍数。您可以通过以下方式计算:
// Calculate desired space.
size_t S = sizeof(Block) + x * sizeof(char);
// Note the alignment requirement.
static const size_t A = _Alignof(Block);
// Round up to multiple of alignment requirement.
S = (S-1) / A * A + 1;
(这是一个众所周知的表达式,用于四舍五入到 A 的倍数。您可以修改一些示例来了解它的工作原理。)
一旦您使用上面的代码计算了一个Block 所需的空间(x 需要 16 个),您就可以使用包含 32 个 Block 的数组为一个 Ring 分配空间:
Ring *R = malloc(sizeof(Ring) + 32 * S);
访问数组元素
现在您有了空间,您如何访问blk 的成员?如上所述,编译器不知道如何执行此操作。不幸的是,C 不提供任何帮助。您将不得不手动计算地址。由于您知道每个 Block 对象的大小,S,您可以计算索引为 i 的块的地址:
Block *B = (Block *) ((char *) R->blk + S*i);
讨论
这既麻烦又容易出错。地址计算可以包装到一个辅助函数中以使其更好一些。但是,使用这样的复杂代码通常不是一个好主意。您应该考虑替代解决方案。