为什么#include <string>在这里防止堆栈溢出错误?


121

这是我的示例代码:

#include <iostream>
#include <string>
using namespace std;

class MyClass
{
    string figName;
public:
    MyClass(const string& s)
    {
        figName = s;
    }

    const string& getName() const
    {
        return figName;
    }
};

ostream& operator<<(ostream& ausgabe, const MyClass& f)
{
    ausgabe << f.getName();
    return ausgabe;
}

int main()
{
    MyClass f1("Hello");
    cout << f1;
    return 0;
}

如果我注释掉#include <string>我没有得到任何编译器错误,我想是因为它是通过包含的#include <iostream>。如果我在Microsoft VS中“右键单击->转到定义”,它们都指向xstring文件中的同一行:

typedef basic_string<char, char_traits<char>, allocator<char> >
    string;

但是,当我运行程序时,出现异常错误:

OperatorString.exe中的0x77846B6E(ntdll.dll):0xC00000FD:堆栈溢出(参数:0x00000001,0x01202FC4)

知道为什么在注释掉时会出现运行时错误#include <string>吗?我正在使用VS 2013 Express。


4
有了上帝的恩典。在gcc上可以正常工作,请参见ideone.com/YCf4OI
v78

您是否尝试过使用Visual C ++的Visual Studio并注释掉include <string>?
空降

1
@cbuchart:尽管该问题已经回答,但我认为这是一个足够复杂的话题,因此用不同的词来回答第二个问题很有价值。我已投票决定不删除您的好答案。
轨道轻盈赛

5
@Ruslan:有效。也就是说,#include<iostream><string>可能都包括<common/stringimpl.h>
MSalters

3
在Visual Studio 2015中,...\main.cpp(23) : warning C4717: 'operator<<': recursive on all control paths, function will cause runtime stack overflow运行此行会产生警告cl /EHsc main.cpp /Fetest.exe
-CroCo

Answers:


161

确实,这是非常有趣的行为。

知道为什么我注释掉时会出现运行时错误 #include <string>

使用MS VC ++编译器会发生错误,因为如果不这样做#include <string>,则不会operator<<为定义std::string

编译器尝试进行编译时ausgabe << f.getName();,会为查找operator<<定义std::string。由于未定义,因此编译器会寻找替代方案。有一个operator<<for 的定义,MyClass编译器尝试使用它,并且要使用它必须将其转换std::string为它MyClass,这正是由于MyClass具有非显式构造函数而发生的事情!因此,编译器最终会创建您的新实例,MyClass并尝试再次将其流式传输到您的输出流。这导致无限递归:

 start:
     operator<<(MyClass) -> 
         MyClass::MyClass(MyClass::getName()) -> 
             operator<<(MyClass) -> ... goto start;

为避免错误,您需要#include <string>确保为operator<<定义了一个std::string。另外,您应该使MyClass构造函数明确,以避免这种意外的转换。明智的原则:如果仅采用一个参数来避免隐式转换,则使构造函数显式:

class MyClass
{
    string figName;
public:
    explicit MyClass(const string& s) // <<-- avoid implicit conversion
    {
        figName = s;
    }

    const string& getName() const
    {
        return figName;
    }
};

看起来operator<<for std::string仅在<string>包含时才定义(与MS编译器一起使用),因此,所有内容都会编译,但是您会得到一些意外的行为,因为operator<<递归调用for MyClass而不是operator<<for std::string

这是否意味着#include <iostream>仅部分包含了通串?

不,字符串已完全包含在内,否则您将无法使用它。


19
@airborne-这不是“ Visual C ++特定的问题”,但是如果您没有包含正确的标头,将会发生什么。当std::string不使用任何#include<string>东西时都可能发生,而不仅限于编译时错误。调用错误的函数或运算符显然是另一种选择。
Bo Persson

15
好吧,这不是“调用错误的函数或运算符”;而是 编译器完全按照您的指示执行。您只是不知道您要告诉它这样做;)
轨道轻轨赛

18
使用一种类型而不包含其对应的头文件是一个错误。期。实施是否可以使错误更容易发现?当然。但这不是实现的“问题”,而是您编写的代码的问题。
科迪·格雷

4
标准库可以自由地在其内部的std中其他位置定义定义的令牌,并且如果它们定义一个令牌,则不需要包括整个标头。
Yakk-亚当·内夫罗蒙特

5
看到一群C ++程序员争辩说编译器和/或标准库应该做更多的工作来帮助他们,这有点幽默。正如多次指出的那样,根据该标准,实施完全在其权限之内。可以使用“诡计”使程序员更清楚吗?当然可以,但是我们也可以用Java编写代码,从而完全避免出现此问题。MSVC为什么要使其内部助手可见?为什么标头会拖入一堆实际上不需要的依赖项?这违反了语言的整体精神!
科迪·格雷

35

问题在于您的代码正在执行无限递归。std::stringstd::ostream& operator<<(std::ostream&, const std::string&))的流运算符在<string>头文件中声明,尽管std::string它本身在其他头文件中声明(由<iostream>和包含<string>)。

当您不包括在内时<string>,编译器会尝试寻找一种编译方法ausgabe << f.getName();

碰巧您已经为定义了流运算符MyClass和允许使用的构造函数std::string,因此编译器使用它(通过隐式构造),从而创建了递归调用。

如果您声明explicit了构造函数(explicit MyClass(const std::string& s)),则代码将不再编译,因为无法使用调用流运算符std::string,并且您将不得不包含<string>标头。

编辑

我的测试环境是VS 2010,从警告级别1(/W1)开始,它会警告您有关该问题的信息:

警告C4717:'operator <<':在所有控制路径上递归,该函数将导致运行时堆栈溢出

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.