"" + something in C++

62

我的代码出现了一些非常奇怪的问题。我相信我已经追踪到了标记为“here”的部分(当然,代码已经简化):

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

在我尝试cout这个函数的结果时,各种问题都会发生。我甚至认为自己已经获取了底层C++文档的一些片段,以及许多分段错误. 对我来说,很明显这在C++中行不通(现在我已经转而使用stringstream将其转换为string),但我想知道原因。由于我长时间使用C#而没有学习C ++,这给我带来了许多痛苦。


44
数组会退化为指针。字符会被提升为整型。指针算术运算随之发生。 - chris
6
这段代码的含义是 const char *str=""; return &(str[(int)c]);。除非 C 是 0,否则会导致未定义行为。我的翻译保持了原文的意思并使其更易懂,但没有解释或提供额外的信息。 - hyde
7
你可能会对 C++14 中的 s 字符串字面值感兴趣。 - Hiura
3
你可以使用return std::string(1, c);来返回一个包含单个字符cstd::string对象。 - GingerPlusPlus
12
“我甚至成功地获取了底层C++文档的一些片段”,这是一个有趣的 未定义行为 的例子。 - usr
显示剩余9条评论
3个回答

94
  • ""是一个字符串字面值。它们的类型为N const char的数组。这个特定的字符串字面值是一个1 const char的数组,其唯一的元素是空终止符。

  • 数组很容易衰变成指向它们第一个元素的指针,例如在需要指针的表达式中。

  • lhs + rhs对于数组和整数不被定义为lhsrhs。但是对于指针作为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*` 参数的 `std::string` 构造函数期望的是指向以 null 结尾的字符数组的指针。
如果 `c != 0`,那么表达式 `"" + c` 将导致未定义行为:
- 对于 `c > 1`,指针运算会产生未定义行为。指针运算只在数组上被定义,并且如果结果是同一数组的元素,则是安全的。 - 如果 `char` 是带符号类型,则对于相同的原因,`c < 0` 会产生未定义行为。 - 对于 `c == 1`,指针运算并不会产生未定义行为。这是一个特殊情况;指向数组最后一个元素之外的位置是允许的(尽管不能使用它所指向的内容)。尽管如此,仍会导致未定义行为,因为在这里调用的 `std::string` 构造函数要求其参数是指向有效数组(和以 null 结尾的字符串)的指针。而超出数组范围的那个元素并不属于该数组本身。违反这个要求同样导致未定义行为。
现在可能发生的情况是,`std::string` 的构造函数试图通过搜索与 '\0' 相等的数组中(第一个)字符来确定你传递给它的 null 结尾字符串的大小:
string(char const* p)
{
    // simplified
    char const* end = p;
    while(*end != '\0') ++end;
    //...
}

这将会导致访问冲突,或者所创建的字符串包含“垃圾”。

同时,编译器可能会认为这种未定义行为永远不会发生,并进行一些有趣的优化,导致出现奇怪的行为。

顺便说一下,clang++3.5对此片段发出了一个不错的警告

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

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

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


3
基本上就是一堆隐式转换。好的,感谢您的反馈! - wlyles
1
对于 c > 1"" + c 只是未定义行为,因为 "" 的大小为 1,标准明确允许指针超出分配内存的末尾一个元素以实现迭代。否则,即使是简单的 for 循环遍历整个数组也会有 UB。不过,在 std::string 构造函数中解引用该指针是 UB。 - Simon Lehmann
2
@SimonLehmann 是的,在 c == 1 的情况下,UB 来自于使用不是指向以 null 结尾的字符串(这意味着指向有效数组的指针)的东西调用 std::string 的构造函数。也就是说,对于 c > 1,存在比构造函数调用更早的 UB 源,但无论如何,对于 c > 0 都存在 UB。 - dyp
1
@Simon:针对 c < 0 的情况,因为 char 可能是有符号类型。 - user1084944
1
@boycy 我认为运行时间太晚了。这个奇怪的表达式可以被静态分析器发现,实际上被clang++3.5捕获(请参见编辑后的答案)。 - dyp
显示剩余2条评论

27

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

你似乎期望从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类型)。

操作符重载本质上是函数,遵循相同的规则。


@dyp:谢谢,已修复。我可能在考虑std中重载函数和运算符的规则......在那里,标准类型可以满足“类或类引用”的要求,而自定义类型的指针可以满足与自定义类型相关的要求。 - Ben Voigt
或者是 ADL 规则,其中也包括指针。不确定您所指的规则是哪个,但在命名空间 std 中重载函数是被禁止的,除非明确允许。 - dyp
@dyp:我猜是17.6.4.2.1/1,这并不是很具体...只是“如果声明依赖于用户定义的类型”。 - Ben Voigt

9
这个返回语句
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之外就没有其他用途了。 - Jim Balter

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