Eva*_*ran 109 c c89 undefined-behavior
我要问的是众所周知的"结构的最后一个成员有可变长度"的技巧.它是这样的:
struct T {
int len;
char s[1];
};
struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");
Run Code Online (Sandbox Code Playgroud)
由于结构在内存中的布局方式,我们可以将结构覆盖在一个大于必要的块上,并将最后一个成员视为大于1 char指定的大小.
所以问题是:这种技术在技术上是不确定的行为吗?.我希望它是,但很好奇标准对此有何看法.
PS:我知道C99的方法,我希望答案专门针对上面列出的技巧版本.
Car*_*rum 51
正如C FAQ所说:
目前尚不清楚它是合法的还是便携的,但它很受欢迎.
和:
......官方解释认为它并不严格符合C标准,尽管它似乎在所有已知的实施中都有效.(仔细检查数组边界的编译器可能会发出警告.)
背后的"严格符合"位的基本原理是在规范中,部分J.2未定义行为,其中包括未定义行为的列表:
- 数组下标超出范围,即使某个对象显然可以使用给定的下标访问(如
a[1][7]声明中给出的左值表达式int a[4][5])(6.5.6).
第6.5.6节Additive运算符的第8段另外提到超出定义的数组边界的访问是未定义的:
如果指针操作数和结果指向相同的数组对象,或一个过去的数组对象的最后一个元素的元素两者,所述评估也不得产生溢出; 否则,行为未定义.
Jer*_*fin 34
我认为技术上它是未定义的行为.该标准(可论证)并未直接解决,因此它属于"或遗漏任何明确的行为定义".条款(C99第4/2条,C89第3.16/2条)表示它是未定义的行为.
上面的"可论述"取决于数组下标运算符的定义.具体来说,它说:"后缀表达式后跟方括号[]中的表达式是数组对象的下标." (C89,§6.3.2.1/ 2).
您可以认为这里违反了"数组对象"(因为您在数组对象的定义范围之外进行了预订),在这种情况下,行为(更多一点)明确地未定义,而不仅仅是未定义礼貌没有完全定义它.
从理论上讲,我可以想象一个编译器执行数组边界检查,并且(例如)在/如果您尝试使用超出范围的下标时将中止该程序.事实上,我不知道存在这样的事情,并且考虑到这种代码风格的流行,即使编译器试图在某些情况下强制执行下标,也很难想象有人会忍受这样做这个情况.
oua*_*uah 12
是的,这是未定义的行为.
C语言缺陷报告#051给出了这个问题的明确答案:
这个成语虽然很常见,但并不严格遵守
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html
在C99理由文件中,C委员会补充说:
这种结构的有效性一直是值得怀疑的.在对一个缺陷报告的回复中,委员会认为它是未定义的行为,因为数组p->项目只包含一个项目,无论该空间是否存在.
Chu*_*uck 11
这种特殊的做法并没有在任何C标准中明确定义,但C99确实包含了"struct hack"作为语言的一部分.在C99中,结构的最后一个成员可以是"灵活的数组成员",声明为char foo[](用你想要的任何类型代替char).
它不是未定义的行为,无论官方或其他人说什么,因为它是由标准定义的.p->s,除了用作左值时,计算与指针相同的指针(char *)p + offsetof(struct T, s).特别是,这是charmalloc'd对象内部的有效指针,并且紧跟其后的连续地址有100个(或更多,取决于对齐注意事项),这些地址也作为char分配对象内的对象有效.该指针是通过使用得到的事实->,而不是明确地将偏移到返回的指针malloc,转换为char *,是无关紧要的.
从技术上讲,p->s[0]是char结构内部数组的单个元素,接下来的几个元素(例如,p->s[1]通过p->s[3])可能是结构内部的填充字节,如果你作为一个整体执行对结构的赋值可能会被破坏,但如果你只是访问个体则不会成员,其余元素是分配对象中的额外空间,只要您遵守对齐要求(并且char没有对齐要求),您可以随意使用它们.
如果您担心结构中与填充字节重叠的可能性可能会以某种方式调用鼻子恶魔,您可以通过将1in 替换为[1]一个值来避免这种情况,该值确保结构末尾没有填充.一个简单但浪费的方法是创建一个具有相同成员的结构,但最后没有数组,并s[sizeof struct that_other_struct];用于数组.然后,p->s[i]明确定义为结构中数组的元素,i<sizeof struct that_other_struct并作为结构结尾后的地址的char对象i>=sizeof struct that_other_struct.
编辑:实际上,在上面获得正确大小的技巧中,你可能还需要在数组之前放置一个包含每个简单类型的联合,以确保数组本身以最大对齐开始,而不是在其他元素的填充中间.再一次,我不相信任何这一点是必要的,但我会为那里最偏执的语言律师提供它.
编辑2:由于标准的另一部分,与填充字节的重叠绝对不是问题.C要求如果两个结构在其元素的初始子序列中一致,则可以通过指向任一类型的指针来访问公共初始元素.因此,如果相同的结构struct T,但具有较大的阵列天线的最终被宣布,该元件s[0]将不得不与元件重合s[0]在struct T,并且这些附加的元件的存在不能影响或通过访问较大结构的共同要素的影响使用指针struct T.
是的,这是技术上未定义的行为.
注意,至少有三种方法可以实现"struct hack":
(1)声明大小为0的尾随数组(遗留代码中最"流行"的方式).这显然是UB,因为零大小的数组声明在C中始终是非法的.即使它确实编译,该语言也不保证任何违反约束的代码的行为.
(2)声明具有最小法定大小的数组 - 1(您的情况).在这种情况下,任何尝试获取指针p->s[0]并将其用于超出指针运算的p->s[1]是未定义的行为.例如,允许调试实现生成带有嵌入范围信息的特殊指针,每次尝试创建指针时都会捕获该信息p->s[1].
(3)例如,声明像"非常大"的大小的数组,例如10000.我们的想法是,声明的大小应该大于实际操作中可能需要的大小.关于阵列访问范围,该方法没有UB.然而,在实践中,当然,我们总是会分配更少的内存(只有真正需要的内存).我不确定这是否合法,即我想知道为对象分配的内存比声明的对象大小更合法(假设我们从不访问"未分配"成员).
小智 5
标准非常明确,您无法访问数组末尾之外的内容。(并且通过指针没有帮助,因为您甚至不允许在数组结束后将指针增加到超过一)。
以及“在实践中工作”。我见过 gcc/g++ 优化器使用标准的这一部分,因此在遇到这个无效的 C 时会生成错误的代码。