不同操作系统/编译器之间的C风格字符串输出不一致

Inconsistent C style string output between different operating systems / compilers

本文关键字:风格 字符串 输出 不一致 之间 操作系统 编译器      更新时间:2024-04-28

我有一个C++程序:

#include <iostream>
char * foo (char * bar, const char * baz) {
int i = -1;
do {
i++;    
*(bar + i) = *(baz + i);
} while (*(baz + i));
return bar;
}
int main (int argc, char *argv[]) {
char bar[] = "";
char baz[] = "Hello";
foo(bar, baz);
std::cout << "bar: " << bar << std::endl;
std::cout << "baz: " << baz << std::endl;
}

这并不是重要的部分,但这个程序的要求是使用指针将一个C风格的字符串复制到另一个。

当我在Ubuntu 16.04桌面上编译和执行二进制文件时,我看到的是:

$ g++ -std=c++11 test.cpp -o test && ./test
bar: Hello
baz: ello

Egad!baz的初始'H'已经被删除,但我根本看不到我的foo函数是如何更改baz的。嗯…

我的Ubuntu桌面上的g++版本是这样的:

$ g++ --version
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

我认为这是我的代码中的一个错误或bug(现在可能仍然如此(,但我发现当我在任何其他操作系统上编译和运行时,我会得到不同的行为。

以下是macOS上的输出:

$ g++ -std=c++11 test.cpp -o test && ./test
bar: Hello
baz: Hello

这是macOS笔记本电脑上的g++版本:

$ g++ --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 12.0.0 (clang-1200.0.32.2)
Target: x86_64-apple-darwin19.5.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

当在其他Linux盒子、Windows等上测试时,它具有正确的、预期的barbaz输出,两者都是Hello

发生了什么事!?

tl;drC++程序在我的桌面上输出的C风格字符串与其他任何计算机都不同。为什么?

char bar[] = "";

这保证了创建一个一字节长的内存区域(基本上刚好足够容纳''(。实现可能会给你更多,但你不能依赖它。

因此,它不够大,无法存储需要六个字节的字符串"Hello"。例如,C++20 [expr.add]中介绍了这一点,我还强调了一点:

如果表达式P指向具有n元素的数组对象x的元素x[i],则表达式P + JJ + P(其中J具有值j(如果0 <= i + j <= n则指向(可能假设的(元素x[i + j]否则,行为未定义

如果您想确保这个代码段中有足够的空间,您可以将声明更改为:

char baz[] = "Hello";
char bar[sizeof(baz)];  // bar will be same size as baz

对于其他场景,有不同的方法来保证这个大小,但一般规则仍然相同:确保目标数组足够大,这样你就不会写超出它的末尾。


尽管未定义的行为意味着任何事情都可能发生,但在错误情况下最有可能发生的是堆栈上的以下内存布局。将字符从baz一个接一个地复制到bar(其中$表示字符(,生成以下快照前后:

bar
V
+---+---+---+---+---+---+---+
| $ | H | e | l | l | o | $ |  (before)
+---+---+---+---+---+---+---+
| H | e | l | l | o | $ | $ |  (after)
+---+---+---+---+---+---+---+
^
baz

因此,您可以看到在bar结束之后的写入会如何影响堆栈上的其他内容,如baz。如果堆栈布局不同,效果也很可能不同。

例如,如果barbaz其他顺序在堆栈上,则bar影响baz。它几乎肯定会影响堆栈上的其他东西,从而导致奇怪的行为,尤其是如果其他东西恰好是调用函数的返回地址:-(

底线是,未定义的行为正意味着——你不能依赖任何预期的工作。