使用引用指针作为函数参数的危险

Posted

技术标签:

【中文标题】使用引用指针作为函数参数的危险【英文标题】:Dangers of using reference-to-pointer as function argument 【发布时间】:2020-05-06 17:06:35 【问题描述】:

谁能告诉我这是否安全,因为我认为它不安全:

class A

public:
    A(int*& i) : m_i(i)
    

    int*& m_i;
;

class B

public:
    B(int* const& i) : m_i(i)
    

    int* const & m_i;
;


int main()

    int i = 1;
    int *j = &i;

    A a1(j);   // this works (1)
    A a2(&i);  // compiler error (2)
    B b(&i);   // this works again (3)

我明白为什么 (1) 有效。我们传递一个指针,函数接受它作为引用。

但为什么 (2) 不起作用?从我的角度来看,我们传递的是同一个指针,只是没有先将它分配给指针变量。我的猜测是 &i 是一个右值并且没有自己的记忆,所以引用无效。我可以接受这个解释(如果是真的)。​​

但是为什么 (3) 会编译呢?这是否意味着我们允许无效引用,所以b.m_i 本质上是未定义的?

我的工作原理完全错了吗?我问是因为我得到奇怪的单元测试失败,我只能通过指针变得无效来解释。它们只发生在某些编译器上,所以我假设这一定是标准之外的东西。

所以我的核心问题基本上是:在函数参数中使用int* const & 本身就很危险并且应该避免,因为毫无戒心的调用者可能总是使用&i 调用它,就像使用常规指针参数一样?

附录: 正如@franji1 所指出的,以下是理解这里发生的事情的有趣想法。我修改了 main() 以更改内部指针,然后打印成员m_i

int main()

    int i = 1;
    int *j = &i;  // j points to 1

    A a1(j);
    B b(&i);

    int re = 2;
    j = &re;  // j now points to 2

  std::cout << *a1.m_i << "\n";  // output: 2
  std::cout << *b.m_i << "\n";  // output: 1

所以,显然 a1 按预期工作。 但是,由于 b 无法知道 j 已被修改,它似乎持有对“个人”指针的引用,但我担心它在标准中没有很好地定义,所以可能会有编译器针对这个“个人”指针未定义。谁能证实这一点?

【问题讨论】:

在 a1 中,m_i 引用是 j。如果再往下,你重新分配 j 指向,比如说 &k,那么 a1.m_i 将指向 k。 a2 引用的具体指针是什么? @franji1 我认为您试图指出为什么(2)不起作用,并且解释很容易理解。谢谢。但它仍然没有解释为什么(3)有效。我认为这是我问题的核心。我的假设是,根据您的解释,a2.m_i 并没有真正引用任何东西,或者至少是未定义的东西,这就是为什么我想确认这个构造本质上是危险的。 B b(&i);不比 A a1(j) 更危险;如果 i 和 j 的生命周期小于 a1 和 b,则 (1) 和 (3) 都可能变得悬空。 (2) 和 (3) 的区别在于 &amp;i 不是对 int* 的有效引用,但它是对 int* 的有效 const 引用,仅此而已。 【参考方案1】:

A 的构造函数采用 非常量引用int* 指针。 A a1(j); 有效,因为jint* 变量,所以满足引用。并且ja1 寿命更长,因此A::m_i 成员在a1 的生命周期内可以安全使用。

A a2(&amp;i); 无法编译,因为虽然&amp;iint*,但operator&amp; 返回一个临时 值,该值不能绑定到非常量引用。

B b(&amp;i); 可以编译,因为B 的构造函数采用 reference 到 const int*,它可以绑定到临时变量。临时对象的生命周期将通过绑定到构造函数的i 参数来延长,但是一旦构造函数退出就会过期,因此B::m_i 成员将是一个悬空引用,使用起来不安全在构造函数退出之后。

【讨论】:

这正是我的想法。感谢您提供术语并对其进行清理。您能否阐明使用 int* const&amp; 的函数参数是否本质上是不好的(并且仅在语言中为完整性而接受)或者是否有实际的实际用途?因为我认为它要求调用者相当多地假设使用&amp;i 调用函数是一个坏主意,而如果函数只接受没有引用的指针则不会有问题。 使用int* const &amp; 参数本身并没有什么坏处(不过,真正的问题是,为什么要使用指针作为开头,而不是int&amp;?)。但是您通过尝试保存引用所指的内容更进一步,当所引用的内容是临时值时,这是危险的。引用不知道它指的是什么。从编译器的角度来看,保存另一个引用的引用是合法的,但取决于上下文是有问题的。 我使用的架构迫使我提供对稍后填充的结构的引用。由于其中一个结构实际上是指针,因此我需要传递对指针的引用(尽管指向指针的指针也可以工作)。我不得不承认,我对指针的 const-ness 有点不了解,所以我尝试了不同的方法,直到它编译完成。我猜这就是我在使用它之前没有完全理解机制所得到的。 @RiadBaghbanli 由于我和 Remy Lebeau 所表明的原因,您是不正确的:来自B b(&amp;i);&amp;i 是一个prvalue - 一个临时的。这个临时结束在构造函数端。 m_i 现在留下一个悬空引用。 @ceno 为什么不把任务放入队列,然后启动第一个任务,让它在退出前启动第二个任务,然后将其完成的数据(即选定的指针)作为输入传递时间?然后第二个任务可以启动第三个任务,根据需要给它输入。等等。您无需将各个任务相互关联,也无需引用某些外部共享数据。只需将相关数据沿链向下传递,一次一项任务。【参考方案2】:

j 是一个左值,因此它可以绑定到非 const 左值引用。

&amp;i 是纯右值,不能绑定到非 const 左值引用。这就是 (2) 无法编译的原因

&amp;i 是一个纯右值(临时),它可以绑定到一个 const 左值引用。将纯右值绑定到引用会将临时对象的生命周期延长到引用的生命周期。在这种情况下,这个临时生命周期被延长到构造函数参数i 的生命周期。然后将引用m_i 初始化为i(构造函数参数)(这是对临时的引用),但因为i 是左值,临时的生命周期不会延长。最后你会得到一个引用成员m_i 绑定到一个不存在的对象。您有一个悬空引用。从现在开始(在构造函数完成后)访问m_i 是未定义的行为。


引用可以绑定到什么的简单表格:C++11 rvalue reference vs const reference

【讨论】:

感谢非常好的解释。这可以解释为什么在函数中使用对 const 指针的引用可能没有问题,但是这种情况失败了,因为我之后将它复制到了一个成员。那么是否有可能某些编译器将构造函数参数的生命周期延长到成员的生命周期?因为我只在一个特定情况下遇到问题。例如,在 C++ shell 中,这是可行的,所以我假设他们的编译器是根据我最初的直觉来实现的(这通常被证明是错误的)。 @Cerno 没有。未定义的行为是……嗯……未定义。任何事情都有可能发生。它看起来可以工作,它可以崩溃它可以做任何疯狂的事情。 "那么是否有可能某些编译器将构造函数参数的生命周期延长到成员的生命周期?" - 不,因为生命周期扩展不是这样定义的工作。 @RemyLebeau 谢谢,所以,我认为这只是巧合,因为某些已释放的内存部分可能仍包含临时指针地址,因此我仍然可以访问它,但不能保证,或者其他什么类似的? @Cerno 不,这不是巧合。 (2) 和 (3) 之间的区别在于 &i 不是对 int* 的有效引用,但它是对 int* 的有效 const 引用。这个答案不准确。【参考方案3】:

指针是一个内存地址。为简单起见,将指针视为uint64_t 变量,其中包含一个代表内存地址的数字。引用只是一些变量的别名。

在示例 (1) 中,您将指针传递给构造函数,期望引用指针。它按预期工作,因为编译器获取存储指针值的内存地址并将其传递给构造函数。构造函数获取该数字并创建一个别名指针。结果,您将获得 j 的别名。如果您修改 j 以指向其他内容,则 m_i 也将被修改。您也可以修改 m_i 以指向其他内容。

在示例 (2) 中,您将一个数字值传递给期望指针引用的构造函数。因此,构造函数获取的不是地址的地址,而是地址,编译器无法满足构造函数的签名。

在示例 (3) 中,您将一个数字值传递给构造函数,期望指针的常量引用。常量引用是一个固定的数字,只是一个内存地址。在这种情况下,编译器理解意图并提供要在构造函数中设置的内存地址。结果,您将获得i 的固定别名。

编辑(为清楚起见):(2)和(3)之间的区别在于&amp;i不是对int*的有效引用,但它是对@987654329的有效const引用@。

【讨论】:

我不确定这是否正确。如果 (3) 有效,那么为什么我的单元测试会失败?为什么只针对某些编译器?这对我来说闻起来像是未定义的行为。 您的单元测试展示了 C++ 规范所期望的确切行为。 b.m_i 是对 i 的引用,而不是 j。修改j 不会影响b.m_i。如果要修改i', then b.m_i`的值也会被修改。 你是说 (3) 不会 以悬空引用结束吗? (3) 并不比 (1) 更悬空。 b.m_i 引用 i,而不是临时变量。它将常量值&amp;i 作为常量引用传递。在调试器中自己检查一下。 您在情况 (3) 中回答:“但它是对 int* 的有效 const 引用”不,不是。它是对在 B b(&amp;i); 上创建的 &amp;i 临时的引用。当b 的构造函数结束并且b.m_i 现在是对过期临时的引用时,此临时结束。一个悬空的裁判。

以上是关于使用引用指针作为函数参数的危险的主要内容,如果未能解决你的问题,请参考以下文章

指针作为函数参数传递的注意事项

Linux c 中引用可以做函数参数吗?

将取消引用的指针作为函数参数传递给结构

c++ 中啥样的指针是裸指针,参数可以是智能指针的引用吗,求高手举例指教

C++ 把引用作为函数参数

使用变量与指向变量的指针作为函数的参数