【问题标题】:Why does std::strlen() work on char arrays WITHOUT terminating null characters? Is this a compiler optimization?为什么 std::strlen() 在不终止空字符的情况下对 char 数组起作用?这是编译器优化吗?
【发布时间】:2019-01-17 17:10:39
【问题描述】:

我所做的所有阅读都表明,将非空终止字符数组传递给std::strlen 是未定义的行为,可能会导致程序崩溃。但是,下面的代码(在 Cygwin 上使用 g++ 编译)运行良好。

这是怎么回事?

char test_cases[4][80] = {{'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!'}, {}, {'1'}, {'A', 'B', 'C'}};
size_t num_test_cases = std::size(test_cases); // C++17

for (size_t i = 0; i < num_test_cases; ++i) 
{
    std::cout << std::strlen(test_cases[i]) << std::endl;
}

输出:

13
0
1
3

【问题讨论】:

  • 你认为你的字符串之后有什么?
  • 迟早会遇到一个零值字符。但是您处于未定义的行为领域-它似乎只是“起作用”。
  • 您的数组初始值设定项少于 80 个字符,因此剩余的字符用 '\0' 填充。
  • @MatthieuBrucher 实际上它们是值初始化的,这导致零初始化。默认初始化会留下一个不确定的值。
  • 解释反对意见(我不一定同意):询问“为什么我的代码应该具有未定义行为的正常工作?”通常效率不高,因为未定义的行为包括“一切正常”的可能性。现在,如果您非常努力尝试让坏事发生但仍然无法让它们发生,那可能确实是一个问题的理由。但除此之外,不要假设所有可能出错的事情都会出错(无论墨菲先生会告诉你什么)。

标签: c++ c-strings strlen array-initialization


【解决方案1】:

数组的构造意味着任何未使用的插槽都可以方便地设置为零大小。

所以你写的完全合法且一致。

您是否为“Hello, world!”定义了完全正确的缓冲区大小

char test_cases[4][13]

你会得到“破碎”的答案,并触及 UB 的边缘。

还因为你声明它作为第一个缓冲区,它会运行到第二个缓冲区,所以会给出错误的答案,而不是一些致命的错误。

实际上,再看一遍,因为您将第二个字符串定义为空,您仍然不会看到错误,因为溢出数据的第一个字节可能也是零填充的!

我可能会说,因为没有值的 {} 实际上不是合法的 C。它是合法的 C++11,但我不完全确定如果 C++11 的行为是为了确保所有成员都归零调用聚合“样式”初始化程序。事实上,由于您的输出,{} 一定做了“正确”的事情。

通常内存中有很多零,以至于您的字符串通常最终会被终止!正如@John 所说,这是外星人从你的银行账户中偷钱的机会。

【讨论】:

  • 也因为你声明它作为第一个缓冲区,它会运行到第二个缓冲区,所以会给出一个错误的答案而不是一些致命的错误。虽然这通常会正式发生的事情是 UB,你不能依赖这种行为。
  • @NathanOliver 更糟糕的是,因为它是 UB,智能编译器将用无操作替换您的整个代码库。或窃取您的银行凭证并将您的钱捐赠给编译器作者的木马。或 NetHack。
  • “方便”,但并非偶然,它是完美定义的。
  • @JohnDvorak 非常有趣。尽管它在现代编译器中被定义为“未定义”——至少在 C++ 中,二维数组的布局已经“众所周知”了 50 多年。不正确,但确实意味着我可以依靠它。
  • @GemTaylor 布局的可预测性并不是重点。生活不仅仅是布局。
【解决方案2】:

您的情况是“零初始化”的常见情况。 It's perfectly defined.

从大括号括起来的列表中初始化

当使用大括号括起来的初始化器列表初始化数组时,列表中的第一个初始化器初始化索引为零处的数组元素(除非指定了指示符)(自 C99 起),并且每个后续初始化器都没有指示符 (自 C99 以来)在索引处初始化数组元素,该索引大于由前一个初始化程序初始化的索引。

由于分配了超过 13 个字符 (80),因此所有其他字符都填充为 '\0'(值为 0 的字符)。所以strlen 的工作方式与预期完全一样,因为您的空间比预期的要多。

cppreference 中完全符合您的情况的其他示例:

int x[] = {1,2,3}; // x has type int[3] and holds 1,2,3
int y[5] = {1,2,3}; // y has type int[5] and holds 1,2,3,0,0
int z[3] = {0}; // z has type int[3] and holds all zeroes

【讨论】:

  • 我明白了,所以当数组中的多余空间被零填充时,这些实际上是空字符吗?
  • @brianxk93 值为0char 实际上是一个空字符。这就是那个意思。 (无论如何,“空字符”这句话是有意义的)
  • 我对此有点困惑 - 我认为值为 '\0'(转义为零)的字符是空字符。我在这里缺少什么吗?
  • 空字符'\0' 表示值为0 的ASCII 字符。否则,我们将无法将这样的字符放入字符串中。
  • 我想我明白了:“空字符”是 0,而不是 '0',在代码中用 '\0' 表示。只需键入“0”就可以将数字零表示为一个字符,就像我在创建诸如“100”之类的数字字符串时所做的那样。这种理解正确吗?
【解决方案3】:

表示将非空终止字符数组传递给 std::strlen 是未定义的行为

正确。

但是,下面的代码可以正常工作。

所有的字符串 都以 null 结尾,因此没有未定义的行为。

无论如何,您不能假设具有未定义行为的程序不会“正常工作”。这没有什么不合常规的。

并且可能会导致程序崩溃。

期望未定义的行为“可能导致程序崩溃”并不是一个好主意。 UB完全有可能不会导致程序崩溃。

【讨论】:

  • 我明白了,这是有道理的。我也同意最后一部分 - 一个具有未定义行为且继续工作的程序确实很可怕!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-18
相关资源
最近更新 更多