C++ 悬空引用奇怪的行为

C++ dangling reference strange behaviour

int*& f(int*& x, int* y){
 int** z = &y;
 *z = x;
 return *z;
}

大家好,我在一次考试中得到了这段代码,但我遇到了一些问题。

我的理解是,给定对指针 (x) 的引用和在函数主体中构造的指针副本 (y),正在创建局部双指针 (z) 并使用 y 左值进行初始化,然后取消引用 1 次,因此正在访问 y,并且 y noe 中包含的地址成为 x 中包含的地址。之后 *z 被 return 编辑为我假设为 y 的指针引用。

如果我之前的部分是正确的,我没有解释为什么 returning y who gets deallocated on the function (since a temporary parameter) does not create any problems in the program, indeed 考试答案是函数是return悬挂参考我同意,但是,复制粘贴代码,并且“使用 returned 变量播放”,甚至在参数被 returned 之后做一些随机的事情,以便“编辑堆栈”,其中释放的 y 仍然存在并且“准备好被覆盖”(如果我还是对的)不存在程序的任何未定义行为。

我唯一的解释是 return 只复制 y 中包含的右值,或者可能是 returns 左值和右值(因为 returns 是参考指向指针)但是当关联到“调用”函数 y 的外部指针时,函数 y 没有得到正确的释放,或者以某种方式获取值的指针取代了被释放的 y 指针。

在底部您可以找到用于测试函数 int*& f(int*&, int*) 的代码。

我的问题是:这是一个适当的悬挂引用还是可以在程序中使用这种东西的临界情况?

#include <iostream>
using namespace std;
int a = 65;

int*& f(int*& x, int* y)
{
    cout<<"indirizzo di y: "<<&y<<endl;
    cout<<"indirizzo di x: "<<&x<<endl;

    int** z = &y;
    cout<<"indirizzo di *z prima: "<<*z<<endl;
    *z=x;
    cout<<"indirizzo di *z dopo: "<<*z<<endl;
    cout<<"y punta a: "<<y<<endl;
     cout<<"z dopo: "<<z<<endl;
    return *z;
}

int*& crashaFisso() //function that crashes every time with a "proper" dangling reference
{
    int a=10;
    int* x =&a;
    return x;
}

int main()
{
    system("CLS");
  
    int b = 20;
    int codicerandom=0;
    
    int* i = &a;
    cout<<"indirizzo di i: "<<i<<endl;
    int* u = &b;
    cout<<"indirizzo funzione: "<<&f(i,u)<<endl;
    int* aux = f(i,u);

    int* crash = crashaFisso();

    cout<<"crash: "<<*crash<<endl;
    cout<<"aux: "<<*aux<<endl;

    for(int i=0;i<100;i++)
        codicerandom +=i;
    
    for(int i=0;i<100;i++)
        codicerandom +=i;
    for(int ji=100;ji>0;ji--)
    {
        codicerandom +=ji;
        for(int x=100;x>0;x--)
        {
            codicerandom -= x*2;
        }
    }
    
    cout<<codicerandom<<endl;


    cout<<"crash: "<<*crash<<endl;
    cout<<"aux: "<<*aux<<endl;

    cout<<"crash: "<<*crash<<endl;
    cout<<"aux: "<<*aux<<endl;

    a=32;

    cout<<"aux: "<<*aux<<endl;
    return 0;
}

the exam answer was that the function was returning a dangling reference

正确。

but (...) does not present any undefined behaviour of the program.

是什么让你这么认为?未定义的行为并不意味着“程序无法正常工作”或“程序崩溃”。未定义行为的意思正是它所说的:行为没有被标准定义。事实上,它可能“正确”工作(无论那意味着什么),标准并不禁止它。这就是为什么它如此危险。因为可能在您的测试中它工作正常,因为硬件、OS、特定的编译器以及发生的其他一些假设。但问题是不能保证它能正常工作。如果你改变机器,OS,一个编译器(甚至切换优化设置),一个代码稍微或什至在两天后编译它可能会以(ekhm)未定义的方式表现得很奇怪。

一般来说,如果存在 UB,则无法知道程序是否正确运行。您试图通过考虑 l 值、r 值、分配等来分析情况,而现实是当 UB 存在时 the entire program is meaningless。你只是在浪费时间。

不要写UB代码。不管它看起来是否有效。

does not present any undefined behaviour of the program.

Undefined behavior表示对程序行为有NO保证。在这种情况下,允许看似总是运行的程序。但也允许下次你用新的编译器版本编译程序时(甚至只是再次运行同一个程序),它可能突然不能工作了。


在您的程序中,fcrashaFisso return 都是对类型 int* 的悬空引用。这本身并不是未定义的行为。您可以 return 悬挂引用和指针。但是,这样的 return 值是无用的,因为它们不能以任何实际方式使用。

在你的代码中

&f(i,u)

是第一个问题。您在此处获取悬挂引用的地址。这很可能本身就是未定义的行为,我现在还不能完全确定。如果不是,则将生成的 无效指针值 传递给输出该值的函数至多具有实现定义的行为, 可能 没问题。

 int* aux = f(i,u);

绝对是未定义的行为。您正在尝试从悬挂引用在它被销毁之前引用的对象中获取值以初始化新的 int* 指针。那绝对是未定义的行为。

Undefined behavior means anything1 can happen including but not limited to the program giving your expected output. But never rely(or make conclusions based) on the output of a program that has undefined behavior.

所以您看到的输出是未定义行为的结果。正如我所说,不要依赖具有 UB 的程序的输出。

因此,使程序正确的第一步是删除 UB。 然后并且只有那时你可以开始对程序的输出进行推理。


1有关未定义行为的更准确的技术定义,请参阅 this 其中提到:没有对程序行为的限制.

你的测试并不像它应该的那样敏锐:为了表明引用是悬空的,你实际上应该存储引用而不是值的副本它所指的已故物体。

为了理解为什么这会更有趣,让我们分析一下函数。

  • int** z = &y; 使 z 指向 y*z 现在是 y 的别名
  • *z=x;x 引用的指针所包含的地址值进行 复制,并将其分配给称为 y 的实体或 *z那个地址是完全有效的f()是用main的a的地址调用的)。
  • return *z; returns 对左值 *z 又名 y 的左值引用(即,您可以在语法上分配给它的引用)。该左值是指向 int 的指针类型,包含 main a 的有效地址。代码的问题在于,一旦函数返回,所引用的内容即 y 就会被销毁,因此通过它读取 cout<<"indirizzo funzione: "<<&f(i,u) 中的值是未定义的行为,编译器会发出警告它。

程序没有崩溃的原因是紧接着freturns,它原来的局部变量的内存还完好无损。当然,访问它是非法的,但如果你查看内存,它就在那里。因此,int* aux = f(i,u); 只需读取存储在最近去世的 y 中的(有效)地址,然后 将其作为副本存储在 aux 中。 您现在可以随心所欲地写在堆栈上:aux 将包含一个有效值。

这就是为什么您尝试写入堆栈以覆盖它但没有成功的原因。

如果你 将返回的引用存储到 *z 又名 y 你将引用已故对象本身,它不可避免地会被未来覆盖堆栈操作或编译器以其他方式使用。

这是一个使用引用而不是副本的英语化最小示例(注意变量的定义 dangling_ref)。我编译并 运行 它两次,标准优化和最大优化。简单地更改编译器选项会更改输出(并且,我假设是一个错误,确定是否输出警告!)。这是 msys2 上的示例会话。

$ cat dangling-ref.cpp
#include <iostream>
using namespace std;

int*& dangling_ref_ret(int*& x, int* y)
{
    int** z = &y;
    *z = x;
    cout << "ret addr " << *z << " (should be == " << y << ")" << ", val = " << **z << endl;
    return *z;
}

int main()
{
    int b = 1;
    int* pb = &b;
    int c = 2;
    int*& dangling_ref = dangling_ref_ret(pb, &c);
    cout << "val of dangling_ref " << dangling_ref << " is " << *dangling_ref << endl;
}

$ gcc --version
gcc (GCC) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ g++ -Wall -o dangling-ref dangling-ref.cpp && ./dangling-ref.exe
ret addr 0xffffcc34 (should be == 0xffffcc34), val = 1
val of dangling_ref 0xffffcc34 is 1
$ g++ -Wall -O3 -o dangling-ref dangling-ref.cpp && ./dangling-ref.exe
dangling-ref.cpp: In function ‘int*& dangling_ref_ret(int*&, int*)’:
dangling-ref.cpp:9:13: warning: function may return address of local variable [-Wreturn-local-addr]
    9 |     return *z;
      |             ^
dangling-ref.cpp:4:38: note: declared here
    4 | int*& dangling_ref_ret(int*& x, int* y)
      |                                 ~~~~~^
ret addr 0xffffcc10 (should be == 0xffffcc10), val = 1
val of dangling_ref 0xffffcc14 is 2

Visual Studio 在调试和发布模式之间的行为也不同。

您可以在 godbolt 上尝试不同的编译器和选项。

Bruh,它不可能永远不会崩溃,因为引用是堆栈中的一个变量,并且在您退出函数时不会超出范围,所以它不会被释放,因此不会悬空。