继承构造函数
在 C++ 中,由于派生类会隐藏掉基类的构造函数,如果想让派生类的构造方式和基类相同,就必须在派生类中重新定义一套和基类一摸一样的构造函数。例如:
struct A
{
A() { }
A(int i) { }
// ...
};
struct B : public A
{
// ...
};
int main()
{
B b(1); // Error,没有匹配的构造函数
}
这里想通过一个 int 值构造 B 的方式就行不通,因为派生类 B 把基类 A 的构造函数隐藏了,所以 B 必须这样定义才可以:
struct B : public A
{
B() : A() { }
B(int i) : A(i) { }
};
但如果基类有多个不同的构造函数,这种限制就会让代码写起来十分麻烦。例如:
struct A
{
A() { }
A(int i) { }
A(double d) { }
A(const char* s) { }
// ...
};
struct B : public A
{
B() : A() { }
B(int i) : A(i) { }
B(double d) : A(d) { }
B(const char* s) : A(s) { }
void foo() { }
};
派生类 B 只是新增了一个功能函数接口 foo,并没有扩展任何的数据成员,但是仍然需要重新写一遍与基类 A 完全相同形式的构造函数,才能继承和使用 A 的构造方式,这显然十分不合理。
为了解决这种问题,C++11 允许使用 using 语句,使派生类直接继承基类的构造函数,下面例子是使用 C++11 新标准的实现:
struct A
{
A() { }
A(int i) { }
A(double d) { }
A(const char* s) { }
// ...
};
struct B : public A
{
using A::A; // 可以直接使用A的构造函数
void foo() { }
};
int main()
{
B b1(1);
B b2(2.1);
B b3("abc");
return 0;
}
在派生类 B 的定义中,using A::A 使得它可以直接使用基类 A 的构造函数,这样一来就大大简化了我们的代码编写。
实际上这个 using 的语法在 C++11 之前就存在,用于让派生类能够使用被隐藏掉的基类的重名函数,例如:
struct A
{
void foo();
};
struct B : public A
{
void foo(int i);
using A::foo; // 使派生类能够使用A中无参数版本的foo函数
};
但这种机制之前不能用于构造函数,C++11 使扩展了这种 using 的使用范围,允许用于构造函数。
委派构造函数
当成员变量比较多,或者初始化逻辑比较复杂时,如果存在多个构造函数,在每个构造函数中都执行一遍初始化逻辑就会比较麻烦,也显得重复冗余。C++11 允许在一个构造函数中调用另一个构造函数,从而可以在一些情况下简化一些初始化的代码,例如:
struct Foo
{
Foo(int i, char c, double d) { /*其它一些初始化的逻辑*/ } // 1
Foo(int i) : Foo(i, 'a', 3.0) { } // 2
Foo(char c) : Foo(0, c, 3.0) { } // 3
Foo(double d) : Foo(0, 'a', d) { } // 4
};
在这个例子中,第一个构造函数接受三个参数,并包含一些其他的初始化逻辑,可以认为这是一个最为通用的构造函数。以此为“基准”,后面又定义了三个构造函数,它们又调用了“基准”的构造函数,这样的构造函数就是 C++11 中的委派构造函数。
委派构造函数是在原本初始化列表的位置(冒号后面),调用了另外一个构造函数,以达到委派一部分初始化任务的目的,有时也称为委托构造函数。就像上面的例子一样,在使用委派构造函数时,一般需要先定义一个比较通用的构造函数作为基准,然后其他的构造函数就可以通过这个基准进行委派。委派构造函数也可以包含其它一些逻辑,这些代码会在被委派的“基准”构造函数之后再执行。
注意,在使用委派构造函数时,不允许再同时使用构造函数中的初始化列表。例如下面的例子就是错误的:
struct Foo
{
Foo(int i, char c, double d) { /*其它一些初始化的逻辑*/ }
Foo(int i) : Foo(i, 'a', 2.0), name("good") { } // Error,委派构造和初始化列表不可同时使用
std::string name;
};