这不应该给我悬而未决的参考错误吗?

Shouldn't this give me dangling reference errors?

提问人:Jason 提问时间:7/24/2023 最后编辑:Jason 更新时间:7/25/2023 访问量:75

问:

在使用了大约十年的其他编程语言之后,我将回到 C++,所以请耐心等待。以下程序在 CLion 的 C++20 项目中为我编译:

#include <iostream>

using namespace std;

class MyClass {
private:
public:
    MyClass() {
        cout << "MyClass constructor" << endl;
    }
    MyClass(const MyClass& myClass) {
        cout << "MyClass copy constructor" << endl;
    }

    MyClass& operator=(const MyClass& myClass) {
        cout << "MyClass operator=" << endl;
        return *this;
    }
    friend std::ostream& operator<< (std::ostream& os, const MyClass &myClass){
        return os << "MyClass stringifier";
    }
    ~MyClass(){
        cout << "MyClass destructor" << endl;
    }
};

MyClass& f(){
    cout << "f() entered" << endl;
    MyClass x;
    return x;
}

MyClass* g(){
    cout << "g() entered" << endl;
    MyClass x;
    return &x;
}

int main() {
    cout << "main() entered" << endl;
    MyClass& a = f();
    cout << "f() exited" << endl;
    cout << a << endl;
    MyClass* b= g();
    cout << "g() exited" << endl;
    cout << *b << endl;
}

输出为:

main() entered
f() entered
MyClass constructor
MyClass destructor
f() exited
MyClass stringifier
g() entered
MyClass constructor
MyClass destructor
g() exited
MyClass stringifier

现在,当我将鼠标悬停在语句上时,CLion 确实给了我一些警告:return

Address of stack memory warning

Reference to stack memory warning

有趣的是,这篇现在相当古老的文章提到函数生成的场景是“无效的”C++。然而,我的编译器似乎不同意。f()

令我惊讶的是,这个程序编译和运行没有问题。我不应该在尝试打印和运行时收到“悬空引用”错误吗?我不认为这属于返回值优化/复制省略,因为我没有返回任何地方的新实例:复制构造函数显然没有在任何地方调用,因为我们看不到它的打印副作用。a*bMyClass

编辑:2011 年发布的这篇 SO 帖子的公认答案似乎也表明这里所做的不应该起作用。f

C++ 悬空指针

评论

6赞 UnholySheep 7/24/2023
我不应该在运行时收到”悬空引用“错误” - 你得到未定义的行为,其中还可能包括程序看起来按预期工作(直到它突然没有)
3赞 Yunnosch 7/24/2023
我认为您忽略了一点,即缺乏编译器错误并不意味着“有效的 C++”。至少,在我们的共同理解中,“有效”意味着,除其他外,“没有未定义的行为”。有很多方法可以用 C++ 拍摄自己的脚,编译器(以及通常的运行时行为)会默默地让你这样做( ...当你陷入那个陷阱时,也许会听到嘲笑——但这可能只是我的想象......
4赞 user17732522 7/24/2023
"大约十年后,我将回到 C++“:从未存在任何”悬空引用“错误。访问悬空的引用/指针会导致未定义的行为,这意味着您绝对无法保证会发生什么。在 C++ 中一直都是这样(在 C 中也是如此)。
1赞 Pepijn Kramer 7/24/2023
顺便说一句,编译器可以为您的问题生成警告,请参阅 godbolt.org/z/o7WbnTd1v。喜欢。因此,请务必尽可能提高警告级别。<source>:30:12: warning: reference to local variable 'x' returned [-Wreturn-local-addr]
1赞 Jason 7/24/2023
@PepijnKramer 谢谢你的建议。在阅读了这篇 SO 帖子后,我继续添加到我的文章中,现在我收到了明确的编译时警告。基于到目前为止的讨论,我对“未定义的行为”与“无编译器警告/无运行时错误”有了清晰的理解。set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") CMakeLists.txt

答:

0赞 selbie 7/25/2023 #1

下面是一个简单的示例,使用您提供的相同 MyClass:

int main() {
    MyClass* ptr = g();
    std::ostream << *ptr << std::endl;
}

从技术上讲,取消引用这样的错误指针是未定义的行为。但正如你所指出的,它只是碰巧起作用。就此而言,甚至可以返回 NULL,并且该程序仍然可能有效。但这就是为什么它恰好起作用的原因:g()

归根结底,您定义的那些 C++ 方法在逻辑上被编译为函数,就像 C 函数一样 - 除了考虑了“this”指针和重载的名称修改。因此,编译器(无名称修改)生成了一个如下函数:

std::ostream* MyClass_operator_ostream(std::ostream* os, MyClass* this) {
    os->write("MyClass stringifier", 19);
}

所以你的主要工作基本上是这样做的:

    MyClass* ptr = g();
    MyClass_operator_ostream(&cout, ptr);

但是,流运算符实现实际上并不使用“this”指针。因此,实际上没有生成会触及该错误指针的代码。因此,没有悬而未决的引用实际上被击中。

现在,假设您扩展了 MyClass 以包含成员变量,然后您的流运算符重载引用了该变量。

class MyClass {

    int value;   // gets assigned by constructor

    ...

    friend std::ostream& operator<< (std::ostream& os, const MyClass &myClass){
        return os << "MyClass stringifier.  value = " << value;
    }

    ...

}

因此,编译器将再次生成如下代码:

std::ostream* MyClass_operator_ostream(std::ostream* os, MyClass* this) {
    os->write("MyClass stringifier.  value = ", 30);

    const char* tmp = some_internal_code_to_convert_int_to_string(this->x);
    
    os->write(tmp, strlen(tmp));

}

哎呀,现在已经习惯了。如果为 null,它肯定会崩溃。如果指向来自先前调用的函数的堆栈内存,它可能会打印出预期值。此时,它更有可能打印出堆栈上的任何内存。如果在 和 之间调用了另一个函数,则尤其如此。this->xthisthisg()cout << *g() << endl

我上面所说的一切都与返回指针的函数有关。但功能是一样的。编译器将引用视为后台的指针。g()f()

但从技术上讲,这是未定义的行为,你不能依赖我在整个答案中所说的任何事情来成立。但这是最可能的解释。