【问题标题】:Inconsistent C style string output between different operating systems / compilers不同操作系统/编译器之间的 C 风格字符串输出不一致
【发布时间】:2021-01-11 07:27:29
【问题描述】:

我有一个 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 style string复制到另一个。

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

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

埃加德! 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.

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

这是 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;dr C++ 程序在我的桌面上输出 C 样式字符串的方式与任何其他计算机不同。为什么?

【问题讨论】:

  • char bar[] = ""; 不是一个很大的字符串。将任何内容复制到其中可能会导致缓冲区溢出以及您的朋友和我的Undefined Behaviour。由于具有未定义行为的程序的行为是未定义的,因此推理不一致的行为是一种值得怀疑的时间消耗。..
  • 旁注:您可能会发现while (*baz) { *bar++ = *baz++; } 对大脑更容易一些。虽然baz 处有一个非零值,但获取baz 处的值,然后将baz 推进到下一个字符。将值存入bar处的值,然后前进bar
  • 您可能希望在[] 之间指定bar 的长度。
  • @user4581301:这个(尽管是规范的)循环的唯一问题是你必须在循环之后传输\0
  • Sleep, do/while 在这里工作得更好一些,因为 paxdiablo 的评论指出了一些原因。如果你测试空终止符并在复制空终止符后退出,你不必担心。

标签: c++ c++11 g++


【解决方案1】:
char bar[] = "";

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

因此它不足以存储字符串"Hello",这需要六个字节。例如,C++20 [expr.add] 对此进行了介绍,并特别强调:

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

如果您想确保 this 代码 sn-p 中有足够的空间,您只需将声明更改为:

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

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


虽然未定义的行为意味着任何事情都可能发生,但在您的错误情况下,最可能发生的情况与堆栈上的以下内存布局有关。您将字符从baz 一个接一个地复制到bar$ 代表\0 字符),从而产生以下前后快照:

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

因此,您可以看到超出bar 末尾的写入如何影响堆栈上的其他内容,例如baz。如果堆栈布局不同,效果很可能也会不同。

例如,如果barbaz其他 顺序在堆栈上,那么bar影响baz。它几乎肯定会影响堆栈上的某些东西else,导致奇怪的行为,特别是如果其他东西恰好是调用函数的返回地址:-)

底线是,未定义的行为完全意味着 - 您不能依赖任何按预期工作的东西。

【讨论】:

    猜你喜欢
    • 2018-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-14
    • 1970-01-01
    相关资源
    最近更新 更多