用自己构造对象作为参考?

时间:2010-12-06 16:06:53

标签: c++ copy-constructor

我刚刚意识到这个程序编译并运行(gcc版本4.4.5 / Ubuntu):

#include <iostream>
using namespace std;

class Test
{
public:
  // copyconstructor
  Test(const Test& other);
};

Test::Test(const Test& other)
{
  if (this == &other)
    cout << "copying myself" << endl;
  else
    cout << "copying something else" << endl;
}

int main(int argv, char** argc)
{
  Test a(a);              // compiles, runs and prints "copying myself"
  Test *b = new Test(*b); // compiles, runs and prints "copying something else"
}

我想知道为什么在地球上这甚至会编译。我假设(就像在Java中)参数在调用方法/构造函数之前被计算,所以我怀疑这个案例必须由语言规范中的某些“特殊情况”覆盖?

问题:

  1. 有人可以解释一下(最好是参考说明书)吗?
  2. 允许这样做的理由是什么?
  3. 是标准C ++还是gcc特定的?

  4. 编辑1:我刚刚意识到我甚至可以写int i = i;

    编辑2:即使使用-Wall-pedantic,编译器也不会抱怨Test a(a);

    编辑3:如果我添加方法

    Test method(Test& t)
    {
      cout << "in some" << endl;
      return t;
    }
    

    我甚至可以在没有任何警告的情况下Test a(method(a));

6 个答案:

答案 0 :(得分:8)

这个“被允许”的原因是因为规则说标识符范围在标识符之后立即开始。在案件中

int i = i;

RHS我是“在LHS之后”,所以我在范围内。这并不总是坏事:

void *p = (void*)&p; // p contains its own address

因为可以在不使用其值的情况下解决变量。在OP的复制构造函数的情况下,不容易给出错误,因为绑定对变量的引用不需要初始化变量:它等同于获取变量的地址。合法的构造函数可以是:

struct List { List *next; List(List &n) { next = &n; } };

如果您看到参数仅被解决,则不使用其值。在这种情况下,自引用实际上可能有意义:列表的尾部由自引用给出。实际上,如果你将“next”的类型更改为引用,那么几乎没有选择,因为你不能像指针那样容易地使用NULL。

像往常一样,问题是倒退。问题不在于为什么变量的初始化可以引用自身,问题是为什么它不能引用它。 [在菲利克斯,这是可能的]。特别是,对于与变量相对的类型,转发引用的能力的缺乏是非​​常破坏的,因为它阻止了除了使用不完整类型之外定义递归类型,这在C中已经足够了,但是由于存在模板。

答案 1 :(得分:3)

我不知道这与规范有什么关系,但我就是这样看的:

执行Test a(a);时,它会在堆栈上为a分配空间。因此,编译器在a开始时就知道main在内存中的位置。当调用构造函数时(当然在此之前分配内存),正确的this指针会传递给它,因为它已知。

执行Test *b = new Test(*b);时,您需要将其视为两个步骤。首先分配和构造对象,然后将指向它的指针分配给b。你得到消息的原因是你实际上是在传递一个未初始化的指向构造函数的指针,并将它与对象的实际this指针进行比较(最终将它分配给{{1} },但不是在构造函数退出之前。)

答案 2 :(得分:3)

您使用new的第二个实际上更容易理解;你在那里调用的内容完全相同:

Test *b;
b = new Test(*b);

并且您实际上正在执行无效的解除引用。尝试在构造函数中的<< &other <<行添加cout,并将其设为

Test *b = (Test *)0xFOOD1E44BADD1E5;

看到你正在通过堆栈上的指针给出的任何值。如果没有明确初始化,那就是未定义的。但即使你没有使用某种(in)sane默认值初始化它,它也会与你发现的new的返回值不同。

首先,将其视为就地newTest a是一个局部变量而不是指针,它存在于堆栈中,因此它的内存位置总是很好定义 - 这与指针Test *b非常不同,除非明确初始化为某个有效位置,将是悬空。

如果您编写第一个实例,如:

Test a(*(&a));

你在那里调用它会变得更清楚。

我不知道如何通过复制构造函数使编译器不允许(甚至警告)这种自我初始化。

答案 3 :(得分:3)

第一个案例(可能)由3.8 / 6覆盖:

  

在对象的生命周期之前   开始但是在存储之后   该对象将占据一直   分配或,在一生的后期   对象已经结束,之前已经结束   对象占用的存储空间   重复使用或释放​​,任何左值   指原始对象可能   使用但仅限于有限的方式。这样的   左值是指分配的存储空间   (3.7.3.2),并使用的属性   不依赖于它的左值   价值定义明确。

由于您在生命周期开始之前使用a(和other绑定a)的所有内容都是地址,我认为您很好:阅读该段的其余部分以了解详细规则。

请注意,虽然8.3.2 / 4说,“引用应初始化以引用有效的对象或功能。”有一些问题(作为标准的缺陷报告)在这种情况下“有效”意味着什么,所以你可能无法将参数other绑定到未构造的(因此,“无效”?){{ 1}}。

所以,我不确定标准实际上在这里说了什么 - 我可以使用左值,但不能将它绑定到引用,也许,在这种情况下a 不是好的,只要将指针传递给a,只要它只以3.8 / 5允许的方式使用。

a的情况下,您在初始化之前使用了该值(因为您取消引用它,并且因为即使您到达那么远,b也是{{1}的值1}})。这显然不太好。

与C ++一样,它编译是因为它不是违反语言限制,并且标准没有明确要求诊断。想象一下,当一个对象在其自己的初始化中被无效地使用时,为了强制执行诊断,规范必须经历的扭曲,并想象编译器可能必须做的数据流分析来识别复杂的情况(甚至可能不是如果指针是通过外部定义的函数走私的话,那么在编译时是可能的。更容易将其保留为未定义的行为,除非任何人对新规范语言有任何非常好的建议; - )

答案 4 :(得分:2)

如果您提高警告级别,编译器可能会警告您使用未初始化的内容。 UB不需要诊断,很多“明显”错误的东西都可以编译。

答案 5 :(得分:0)

我不知道规范引用,但我知道访问未初始化的指针总是会导致未定义的行为。

当我在Visual C ++中编译代码时,我得到:

  

test.cpp(20):警告C4700:   未初始化的局部变量'b'使用