字符串字面值和strcat

3

我不确定为什么在这种情况下 strcat 对我有效:

char* foo="foo";

printf(strcat(foo,"bar"));

这段代码成功地为我打印了“foobar”。

然而,根据stackoverflow上早期的一篇讨论主题(链接在此:I just can't figure out strcat),它说,上述代码不应该工作,因为foo被声明为字符串字面值。相反,它需要被声明为缓冲区(一个预定大小的数组,以便它可以容纳我们试图连接的另一个字符串)。

在那种情况下,为什么上述程序对我来说能够成功运行呢?


3
没有失败的保证。 - Weather Vane
1
字符串字面量是不可变的。您无法修改它。任何尝试修改字符串字面量的操作都可能导致程序的未定义行为。当出现未定义行为时,您可能会得到预期或任何意外的结果。 - haccks
出于好奇,你在使用哪个编译器/平台?旧的编译器通常只是将字符串字面量留在可写内存中(这是符合标准的,尽管相当危险,因为修改它们仍然会导致“有趣”的结果),这可能解释了这种行为。对于没有内存保护的当前平台上的现代编译器也是如此。 - Matteo Italia
即使 foo 是可写的,也不能将其连接起来。考虑尝试连接到 char foo[] = "foo";。与 char foo[42] = "foo"; 不同,没有可用的内存。 - Weather Vane
@WeatherVane请仔细阅读问题。他正在链接到另一个讨论该主题的问题。因此,这不是本问题的主题。 - JHBonarius
2个回答

5
这段代码会导致未定义行为,也就是说,你无法保证会发生什么(在此处失败)。
原因是字符串字面值是不可变的。这意味着它们是不可变的,任何尝试更改它们的操作都会导致未定义行为。
需要注意的是,由于未定义行为可能会在某些情况下正常工作(例如在您的系统中),所以可能会出现难以发现的逻辑错误。

PS:在这个实时演示中,我很幸运地遇到了一个分段错误。我说幸运是因为这个分段错误会让我调查和调试代码。

值得注意的是,GCC没有发出任何警告,而Clang的警告也不相关:

p

rog.c:7:8: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
printf(strcat(foo,"bar"));
       ^~~~~~~~~~~~~~~~~
prog.c:7:8: note: treat the string as an argument to avoid this
printf(strcat(foo,"bar"));
       ^
       "%s", 
1 warning generated.

这是因为你应该使用 char foo[255]="foo"; ;p - JHBonarius
现在的SO(Stack Overflow)变得这么严肃了吗? ;p - JHBonarius
2
@JHBonarius 但那是在另一个问题中。所以这不是这个问题的话题。;p - Weather Vane

1

字符串文字是不可变的,意味着编译器会假定您不会对它们进行更改,而不是您尝试修改时一定会出错。在法律术语中,这是"未定义的行为",因此任何事情都可能发生,并且就标准而言,这是可以的。

现代平台和现代编译器提供了额外的保护:在具有内存保护的平台上,字符串表通常放置在只读内存区域中,因此修改它将导致运行时错误。

然而,您可能会使用不提供任何运行时强制检查的编译器,这是因为您正在为没有内存保护的平台编译(例如,80386之前的x86,因此DOS的任何C编译器(如Turbo C),大多数微控制器在RAM而非闪存上操作时...),或者使用旧版编译器,其默认情况下不利用此硬件功能以与旧版本兼容(旧版VC++长时间以来),或者使用显式启用此选项的现代编译器,再次与旧代码兼容(例如,gcc使用-fwritable-strings)。在所有这些情况下,您通常不会收到任何运行时错误。

最后,还有一个特别棘手的情况:现代优化器积极利用未定义行为,即它们假设这种情况永远不会发生,并相应地修改代码。并非不可能某些特别聪明的编译器可以生成代码,只需放弃这样的写入,因为在这种情况下它可以合法地执行任何喜欢的操作。
这可以在一些简单的代码中看到,比如:
int foo() {
    char *bar = "bar";
    *bar = 'a';
    if(*bar=='b') return 1;
    return 0;
}

这里启用了优化:
  • VC++认为写操作仅用于紧随其后的条件,因此它将整个过程简化为return 0;没有内存写入,没有段错误,它“似乎工作正常”(https://godbolt.org/g/cKqYU1);
  • gcc 4.1.2“知道”文字不会改变;写操作是多余的,因此被优化掉了(因此没有段错误),整个过程变成了return 1https://godbolt.org/g/ejbqDm);
  • 任何更现代的gcc都选择了一条更分裂的路线:写操作不会被省略(因此使用默认链接器选项会导致段错误),但如果它成功了(例如,如果您手动调整了内存保护),您会得到一个return 1https://godbolt.org/g/rnUDYr)- 因此,内存已经被修改,但是随后执行的代码认为它没有被修改;这在AVR上尤其严重,因为那里没有内存保护,写操作成功。
  • clang 基本上与gcc相同
长话短说:不要试图碰运气,小心谨慎。始终将字符串文字分配给const char *(而不是普通的char *),并让类型系统帮助您避免此类问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接