派生 类 中基构造函数的控制,潜在的双重初始化

control of base constructor in derived classes, potential double-initialization

关于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(或更高/更早的 classes 的构造函数)不会阻止默认的 base 构造函数(或以其他方式)预先调用?

也就是说,上面代码片段中构造一个derived对象的顺序是:base()然后derived()然后在那个base()里面?

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

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

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

2。是否有一个用例,根据 #1,不应该 用于代替在派生的 classes 构造函数中显式调用的基构造函数?这在 C++ 中如何实现?

我知道#2 听起来很傻,毕竟你无法保证派生 class 的基础 class 部分的状态是 'ready' / 'constructed' 如果您可以推迟调用基本构造函数,直到在派生构造函数中调用任意函数。例如这个:

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

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

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

我不确定。但就观察到的效果而言,这些似乎是等价的陈述。这与我的想法背道而驰,即基本构造函数可以 forwarded (至少在某种意义上)或者更好的选择是 selected 在派生的 class 中使用 :base() 语法。实际上,该表示法要求将基础 classes 放在不同于派生 class...

的成员之前

换句话说 this answer and it's example(暂时忘记它的 C#)会调用基本构造函数两次?虽然我明白为什么会这样做,但我不明白为什么它不表现得更多 "intuitively" 和 select 基本构造函数(至少对于简单的情况)并且只调用它一次。

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

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

#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;
}

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

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

因此,基本构造函数只会被调用一次,只要您的代码没有导致内存泄漏,就不会发生任何泄漏。

but would this not leak memory? should it be expected to leak?

没有。操作顺序将是:

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

对于析构函数:

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

一新一删。完美。

不要忘记编写正确的 assignment/copy 构造函数。