【问题标题】:"" + something in C++"" + C++ 中的一些东西
【发布时间】:2014-11-06 21:01:44
【问题描述】:

我的代码中发生了非常奇怪的事情。我相信我已经追踪到标有“这里”的部分(代码当然是简化的):

std::string func() {
    char c;
    // Do stuff that will assign to c
    return "" + c; // Here
}

当我尝试cout 这个函数的结果时会发生各种各样的事情。我想我什至设法获得了一些底层 C++ 文档,以及许多 segmentation fault。我很清楚,这在 C++ 中不起作用(我现在使用stringstream 来转换为string),但我想知道为什么。在使用了很多 C# 并且没有使用 C++ 之后,这给我带来了很多痛苦。

【问题讨论】:

  • 数组衰减为指针。 chars 晋升为 ints。指针运算随之而来。
  • 该代码相当于const char *str=""; return &(str[(int)c]);。未定义的行为(除非 C 为 0)。
  • 您可能对 C++14 感兴趣 s literal
  • “我什至设法获得了一些底层 C++ 文档”,这是 Undefined Behavior 的一个有趣示例。

标签: c++ string


【解决方案1】:
  • "" 是字符串文字。它们的类型为 数组 N const char。这个特定的字符串字面量是一个 由 1 个const char 组成的数组,其中一个元素是空终止符。

  • 数组很容易衰减为指向其第一个元素的指针,例如在需要指针的表达式中。

  • lhs + rhs 没有为数组定义为lhs,整数为rhs。但它被定义为指针作为 lhs 和整数作为 rhs,使用通常的指针算法。

  • char 是 C++ 核心语言中的整数数据类型(即被视为整数)。

==> 字符串文字+ 字符 因此被解释为指针+ 整数

表达式"" + c大致等价于:

static char const lit[1] = {'\0'};
char const* p = &lit[0];
p + c // "" + c is roughly equivalent to this expression

您返回一个std::string。表达式"" + c 产生一个指向const char 的指针。期望 const char*std::string 的构造函数期望它是指向以 null 结尾的字符数组的指针。

如果c != 0,则表达式"" + c 导致未定义行为:

  • 对于c > 1,指针运算会产生未定义的行为。指针算法只定义在数组上,如果结果是同一个数组的元素。

  • 如果char 已签名,则c < 0 出于相同原因产生未定义行为。

  • 对于c == 1,指针运算不会产生未定义的行为。这是一个特例;允许指向数组最后一个元素之后的一个元素(但不允许使用它所指向的元素)。它仍然会导致未定义行为,因为此处调用的 std::string 构造函数要求其参数是指向有效数组的指针(以及以 null 结尾的字符串)。最后一个元素不是数组本身的一部分。违反此要求也会导致 UB。


现在可能发生的情况是std::string 的构造函数试图通过在数组中搜索等于'\0' 的(第一个)字符来确定您传递给它的以空字符结尾的字符串的大小:

string(char const* p)
{
    // simplified
    char const* end = p;
    while(*end != '\0') ++end;
    //...
}

这将产生访问冲突,或者它创建的字符串包含“垃圾”。 编译器也有可能假设这种未定义行为永远不会发生,并进行一些有趣的优化,从而导致奇怪的行为。


顺便说一句,clang++3.5 emits a nice warning 这个 sn-p:

警告:将 'char' 添加到字符串不会追加到字符串 [-Wstring-plus-int]

return "" + c; // Here
       ~~~^~~

注意:使用数组索引来消除此警告

【讨论】:

  • 那么本质上就是一堆隐式转换。好的,感谢您的意见!
  • "" + c 只是c > 1 的未定义行为,因为"" 的大小为1,并且标准明确允许指针在分配的内存结束后一个元素,以使迭代成为可能。否则,即使是整个数组长度的简单 for 循环也会有 UB。尽管如此,取消引用该指针是 UB,这是在 std::string 的构造函数中发生的。
  • @SimonLehmann 是的,c == 1 案例中的 UB 来自于调用 std::string 的 ctor 时使用的不是指向以 null 结尾的字符串的指针(这意味着指向有效数组的指针)。也就是说,对于c > 1,在 ctor 调用之前还有另一个 UB 来源,但无论如何,c > 0 都有 UB。
  • @Simon: 也适用于c < 0,因为char 可能是有符号类型。
  • @boycy 我认为运行时间为时已晚。这个奇怪的表达式可以被静态分析器发现,实际上被 clang++3.5 捕获(见编辑的答案)。
【解决方案2】:

关于编译器如何解释这段代码有很多解释,但你可能想知道的是你做错了什么。

您似乎期待来自std::string+ 行为。问题是两个操作数实际上都不是std::string。 C++ 查看操作数的类型,而不是表达式的最终类型(这里是返回类型,std::string)来解决重载问题。如果没有看到std::string,它将不会选择std::string+ 版本。

如果你对一个操作符有特殊的行为(或者你写了它,或者有一个提供它的库),那么这种行为只适用于至少一个操作数具有类类型(或引用类类型,并且用户-定义的枚举也计数)。

如果你写了

std::string("") + c

std::string() + c

""s + c // requires C++14

那么你会得到运算符 + 的 std::string 行为。

(请注意,这些实际上都不是好的解决方案,因为它们都会产生短暂的std::string 实例,而std::string(1, c) 可以避免)

函数也是如此。这是一个例子:

std::complex<double> ipi = std::log(-1.0);

您将收到运行时错误,而不是预期的虚数。那是因为编译器不知道它应该在这里使用复数对数。重载只看参数,参数是一个实数(实际上是double)。

运算符重载 ARE 函数并遵守相同的规则。

【讨论】:

  • @dyp:谢谢,已修复。我可能正在考虑 std... 中重载函数和运算符的规则,其中标准类型可以满足“类或对类的引用”要求,而指向自定义类型的指针可以满足与自定义类型有关的要求。
  • 或 ADL 规则,其中也包括指针。不确定您指的是哪些规则,除非明确允许,否则禁止重载命名空间 std 中的函数。
  • @dyp:我猜是 17.6.4.2.1/1,这不是很具体……只是“如果声明依赖于用户定义的类型”
【解决方案3】:

这个返回语句

return "" + c;

有效。使用了所谓的指针算法。字符串字面量 "" 被转换为指向其第一个字符的指针(在这种情况下指向其终止零),并将存储在 c 中的整数值添加到指针中。 所以表达式的结果

"" + c

有类型const char *

类 std::string 具有接受 const char * 类型参数的转换构造函数。问题是这个指针可以指向字符串文字之外的东西。因此该函数具有未定义的行为。

我认为使用这个表达式没有任何意义。如果你想基于一个字符构建一个字符串,你可以写例如

return std::string( 1, c );

C++ 和 C# 之间的区别在于,在 C# 中,字符串文字具有 System.String 类型,该类型具有重载的运算符 + 用于字符串和字符(在 C# 中是 unicode 字符)。在 C++ 中,字符串文字是常量字符数组,运算符 + 用于数组和整数的语义不同。数组被转换为指向它们的第一个元素的指针,并使用指针算法。

标准类 std::string 具有重载运算符 + 用于字符。 C++ 中的字符串文字不是 std::string 类型的此类的对象。

【讨论】:

  • "此返回语句return "" + c; 有效。" -- 如果 c > 1 则不是。如果 c == 1,则返回值不可用,只能从中减去 1。
猜你喜欢
  • 1970-01-01
  • 2019-07-13
  • 1970-01-01
  • 2012-03-29
  • 1970-01-01
  • 2014-05-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多