请考虑以下代码段。
#include <stdio.h>
typedef struct s {
int _;
char str[];
} s;
s first = { 0, "abcd" };
int main(int argc, const char **argv) {
s second = first;
printf("%s\n%s\n", first.str, second.str);
}
当我用GCC 7.2编译它时,我得到:
$ gcc-7 -o tmp tmp.c && ./tmp
abcd
abcd
但是当我用Clang(Apple LLVM版本8.0.0(clang-800.0.42.1))编译它时,我得到以下内容:
$ clang -o tmp tmp.c && ./tmp
abcd
# Nothing here
为什么编译器之间的输出不同?我希望字符串不被复制,因为它是一个灵活的数组成员(类似于this question)。为什么GCC实际上会复制它?
修改
一些评论和答案表明这可能是由于优化。 GCC可能会second
为first
的别名,因此更新second
应该禁止GCC进行优化。我添加了一行:
second._ = 1;
但这不会改变输出。
答案 0 :(得分:2)
以下是gcc正在进行的真实答案。正如您所期望的那样,second
被分配在堆栈中。它不是first
的别名。通过打印地址可以轻松验证这一点。
此外,声明s second = first;
正在破坏堆栈,因为(a)gcc正在为second
分配最小存储量,但(b)它正在复制所有 first
成为第二个,破坏了堆栈。
以下是原始代码的修改版本,其中显示了:
#include <stdio.h>
typedef struct s {
int _;
char str[];
} s;
s first = { 0, "abcdefgh" };
int main(int argc, const char **argv) {
char v[] = "xxxxxxxx";
s second = first;
printf("%p %p %p\n", (void *) v, (void *) &first, (void *) &second);
printf("<%s> <%s> <%s>\n", v, first.str, second.str);
}
在我的32位Linux机器上,使用gcc,我得到以下输出:
0xbf89a303 0x804a020 0xbf89a2fc
<defgh> <abcdefgh> <abcdefgh>
正如您从地址中看到的那样,v
和second
位于堆栈中,first
位于数据部分。此外,很明显second
的初始化已经覆盖了堆栈上的v
,结果是代替预期的<xxxxxxxx>
,而是显示<defgh>
。
这对我来说似乎是一个gcc bug。至少,它应该警告second
的初始化会破坏堆栈,因为它显然有足够的信息在编译时知道这一点。
编辑:我对此进行了更多测试,并通过将second
的声明拆分为:
s second;
second = first;
真正的问题是作业。它复制了first
的所有,而不是结构类型的最小公共部分,这是我认为它应该做的。实际上,如果将first
的静态初始化移动到单独的文件中,则赋值执行它应该执行的操作,v
正确打印,second.str
是未定义的垃圾。这是gcc应该产生的行为,无论first
的初始化是否在同一个编译单元中可见。
答案 1 :(得分:-1)
因此,对于答案,两个编译器都表现正常,但您得到的答案是未定义的行为。
GCC
因为您永远不会修改second
GCC只是在其查找表中创建second
和first
的别名。修改第二个,GCC无法进行优化,你会得到与Clang相同的答案/崩溃。
锵
看来,Clang并没有自动应用相同的优化。因此,当它复制结构时,它会正确地执行:它复制单个int
而不复制任何其他内容。
您很幸运,在您的本地second
变量后,堆栈上的值为零,从而终止您的未知字符串。基本上,您使用的是未初始化的指针。如果没有零,你可能会得到很多垃圾和内存故障。
这个东西的目的是做一些低级的东西,比如通过向你的结构中添加一些内存来实现一个内存管理器等。编译器没有义务理解你在做什么;它只是有义务表现得好像你知道自己在做什么。如果您未能将结构类型转换为实际上具有该类型数据的内存,则所有投注均已关闭。
修改强>
所以,使用godbolt.org并查看程序集:
.LC0:
.string "%s\n%s\n"
main:
sub rsp, 24
mov eax, DWORD PTR first[rip]
mov esi, OFFSET FLAT:first+4
lea rdx, [rsp+16]
mov edi, OFFSET FLAT:.LC0
mov DWORD PTR [rsp+12], eax
xor eax, eax
call printf
xor eax, eax
add rsp, 24
ret
first:
.long 0
.string "abcd"
我们看到GCC实际上与OP的原始代码完全相同:将second
视为first
的别名。
Tom Karzes 对代码进行了重大修改,因此遇到了不同的问题。他报告的内容确实是一个错误;我没时间在ATM上弄清楚他的堆栈腐败任务究竟发生了什么。