【问题标题】:control of base constructor in derived classes, potential double-initialization派生类中基构造函数的控制,潜在的双重初始化
【发布时间】:2017-01-05 18:39:59
【问题描述】:

关于this question and the answer to it 似乎确实有一个例外,但它对我提出的问题比回答它们要多。考虑一下:

#include <iostream>
using namespace std;
struct base {
  virtual void test() {cout << "base::test" << endl;}
  base() {test();}
  virtual ~base() {}
};
struct derived : base {
  virtual void test() {cout << "derived::test" << endl;}
  derived() : base() {}
  ~derived() {}
};
int main() {
  derived d;
  return 0;
}

我天真地认为这只会打印两条消息中的一个。它实际上打印 both - 首先是基本版本,然后是派生的。这在 -O0-O3 设置上的行为相同,因此据我所知,这不是优化或缺乏优化。

我是否理解在 derived 构造函数中调用 base(或更高/更早类的构造函数)不会阻止预先调用默认的 base 构造函数(或其他方式)?

也就是说,上面sn-p中构造derived对象时的顺序是:base()然后derived()再在base()之内?

我知道仅仅为了调用base::base()而修改vtable是没有意义的,回到调用derived::derived()之前的状态,只是为了调用不同的构造函数。我只能猜测与 vtable 相关的东西被硬编码到构造函数链中,并且调用以前的构造函数被字面解释为正确的方法调用(直到到目前为止在链中构造的最派生的对象)?

抛开这些小问题,它提出了两个重要的问题:

1.在派生构造函数中调用基构造函数是否总是会在首先调用派生构造函数之前调用默认基构造函数?这不是低效吗?

2。是否存在一个用例,即每个 #1,不应该使用默认基本构造函数来代替在派生类的构造函数中显式调用的基本构造函数?这如何在 C++ 中实现?

我知道#2 听起来很傻,毕竟如果你可以推迟调用基构造函数直到任意函数调用,你就不能保证派生类的基类部分的状态是“就绪”/“已构造”在派生的构造函数中。比如这个:

derived::derived() { base::base(); }

... 我希望以相同的方式运行并调用基本构造函数两次。但是,编译器似乎将其视为与此相同的情况是有原因的吗?

derived::derived() : base() { }

我不确定。但就观察到的效果而言,这些似乎是等效的陈述。这与我的想法背道而驰,即基础构造函数可以转发(至少在某种意义上),或者更好的单词选择可能会在使用:base() 语法的派生类。实际上,这种表示法要求将基类放在与派生类不同的成员之前...

换句话说this answer and it's example(暂时忘记它的C#)会调用基础构造函数两次?虽然我理解它为什么会这样做,但我不明白为什么它没有表现得更“直观”并选择基本构造函数(至少对于简单的情况)并只调用一次。

这不是双重初始化对象的风险吗?还是在编写构造函数代码时假设对象未初始化的一部分?最坏的情况是我现在必须假设每个类成员都可能被初始化两次并防止这种情况发生吗?

我将以一个可怕的例子结束 - 但这不会泄漏内存吗?应该会泄漏吗?

#include <iostream>
using namespace std;
struct base2 {
  int * member;
  base2() : member(new int) {}
  base2(int*m) : member(m) {}
  ~base2() {if (member) delete member;}
};
struct derived2 : base2 {
  derived2() : base2(new int) {
    // is `member` leaking?
    // should it be with this syntax?
  }
};
int main() {
  derived2 d;
  return 0;
}

【问题讨论】:

  • 第一个例子是无效代码,使用“隐式int”返回类型。函数test 需要指定的返回类型。请修复并重新检查。
  • 编辑后的程序没有出现您描述的行为。见cpp.sh/6rviu
  • 使用void 返回类型代码编译,但我无法重现隐含的编译器错误。无论如何,对于编译器错误,编译器和版本以及完整的调用,逐字逐句,是相关的。
  • 注意,谢谢。我知道这没有表现出问题,我需要向它添加更多的工作代码(不认为它涉及)
  • 现在必须关闭,我的示例中到处都是宏。 TL;DR base 也继承自一个类,该类具有为跟踪目的而定义的每个构造函数和运算符(非平凡构造函数)。基本上我需要重新措辞整个问题,示例不适合它,对不起...

标签: c++ c++11 constructor virtual-functions


【解决方案1】:

但这不会泄漏内存吗?应该会泄漏吗?

没有。操作顺序为:

derived2::derived2()
  auto p = new int
  base2::base2(p)
   base2::member = p

对于析构函数:

derived2::~derived2() (implied)
 base2::~base2()
  if (base2::member) { delete base2::member; }

一个新的,一个删除的。完美。

不要忘记编写正确的赋值/复制构造函数。

【讨论】:

    【解决方案2】:

    要构造派生类对象编译器需要构造它的基础部分。您可以通过derived2() : base2(new int) 指定应该使用哪个基类构造函数编译器。

    如果您缺少这样的规范,编译器将使用基本默认构造函数。

    所以,基本构造函数只会被调用一次,只要你的代码没有导致内存泄漏,就不会有任何一个。

    【讨论】:

    • 谢谢,这是我认为应该发生的事情,但我以某种方式设法打破了这一点。当我弄清楚那是什么,如果有什么,而不是我的愚蠢,我会再次具体询问。
    • 关于derived::derived() { base::base(); }...MSVS 确实调用了两次base()。这看起来像短枪......
    • Clang 的工作方式与 MSVS 相同,并且 gcc 无法编译并显示消息“无法直接调用构造函数 'D2::B2' [-fpermissive]”
    猜你喜欢
    • 2023-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-16
    • 2011-09-09
    相关资源
    最近更新 更多