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

122

这是我的示例代码:

#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;

但是当我运行我的程序时,我遇到了一个异常错误:

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

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


4
在神的恩典下,gcc上运作完美,见https://ideone.com/YCf4OI。 - v78
1
虽然这个问题已经有了答案,但我认为这是一个足够复杂的话题,因此用不同的措辞给出第二个答案是有价值的。我已经投票支持恢复您优秀的回答。 - Lightness Races in Orbit
5
他们实际上是一样的。也就是说,“#include<iostream>”和“<string>”可能都包含了“<common/stringimpl.h>”。 - MSalters
3
在Visual Studio 2015中,如果运行以下命令cl /EHsc main.cpp /Fetest.exe,则会出现警告信息 ...\main.cpp(23) : warning C4717: 'operator<<': recursive on all control paths, function will cause runtime stack overflow - CroCo
@CroCo 在VS 2010中也是一样的,从/W1开始。 - cbuchart
显示剩余4条评论
2个回答

161

确实,非常有趣的行为。

请问当我注释掉 #include <string> 后为什么会出现运行时错误?

使用 MS VC++ 编译器时,如果不包含 #include <string>,则 std::stringoperator<< 没有定义,就会出现错误。

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

 start:
     operator<<(MyClass) -> 
         MyClass::MyClass(MyClass::getName()) -> 
             operator<<(MyClass) -> ... goto start;
为了避免错误,您需要#include <string>确保定义了std::stringoperator<<。此外,您应该使您的MyClass构造函数显式以避免这种意外转换。 智慧之规则:如果构造函数只接受一个参数,则使其显式以避免隐式转换。
class MyClass
{
    string figName;
public:
    explicit MyClass(const string& s) // <<-- avoid implicit conversion
    {
        figName = s;
    }

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

看起来在使用 MS 编译器时,只有在包含 <string> 时才定义了 std::stringoperator<<,因此一切编译都通过了,但是你会得到一些意料之外的行为,因为在 MyClass 中递归调用了 operator<< 而不是调用 std::stringoperator<<

这是否意味着通过 #include <iostream> 只部分地包含了 string?

不,string 是完全被包含进来的,否则你就无法使用它了。


19
这并不是“Visual C++特有的问题”,而是当你没有包含正确的头文件时可能发生的情况。如果在没有包含#include<string>的情况下使用std::string,可能会发生各种各样的问题,不仅限于编译时错误。调用错误的函数或运算符显然是另一种选项。 - Bo Persson
15
好的,这并不是"调用了错误的函数或运算符";编译器正在按照你告诉它要做的事情去做。你只是不知道你在告诉它做这件事 ;) - Lightness Races in Orbit
18
在不包含相应头文件的情况下使用类型是一个 bug。毫无疑问。实现是否可以使 bug 更容易被发现?当然可以。但这不是实现的“问题”,而是你编写的代码的问题。 - Cody Gray
4
标准库可以在自己内部包含已经定义在标准库中其他地方的令牌,并且如果它们定义了一个令牌,则不需要包含整个标头。 - Yakk - Adam Nevraumont
5
看到一堆 C++ 程序员争论编译器和/或标准库应该更努力地帮他们解决问题,有些令人发笑。正如已经多次指出的那样,根据标准,实现在这里是完全有权利的。可以使用“奇技淫巧”让程序员更容易理解吗?当然可以,但我们也可以用 Java 编写代码并完全避免这个问题。为什么 MSVC 要公开其内部帮助程序?为什么一个头文件要引入一堆不必要的依赖关系?这违反了整个语言的精神! - Cody Gray
显示剩余17条评论

35
问题在于你的代码出现了无限递归。对于std::stringstd::ostream& operator<<(std::ostream&, const std::string&))的流运算符声明在<string>头文件中,尽管std::string本身是在其他头文件中声明的(由<iostream><string>都包含)。
当你不包含<string>时,编译器试图找到一种方式来编译ausgabe << f.getName();
恰好你定义了MyClass的流运算符和一个可以接受std::string的构造函数,所以编译器使用它(通过隐式构造),创建了一个递归调用。
如果您在构造函数中声明explicitexplicit MyClass(const std::string& s)),那么您的代码将无法编译,因为没有办法使用std::string调用流操作符,并且您将被迫包含<string>头文件。 编辑 我的测试环境是VS 2010,在警告级别1(/W1)下,它会警告您有问题:

warning C4717: 'operator<<' : recursive on all control paths, function will cause runtime stack overflow


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