Fork me on GitHub

构造函数和拷贝控制

构造函数和拷贝控制

虚析构函数

继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。

原因: 当我们delete一个动态分配的对象的指针时将执行析构函数。如果该指针指向继承体系中的某个类型,则有可能出现指针的静态类型与被删除对象的动态类型不符的情况。例如,如果我们delete一个Quote*类型(基类)的指针,则该指针有可能实际指向了一个Bulk_quote(派生类)类型的对象。如果这样的话,编译器就必须清楚它应该执行的是Bulk_quote的析构函数。和其他函数一样,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。

1
2
3
4
5
class Quote{
public:
//如果我们删除的是一个指向派生类对象的基类指针,则需要虚析构函数
virtual ~Quote() = default; //动态绑定析构函数
};
1
2
3
4
Quote *itemP = new Quote; //静态类型与动态类型一致
delete itemP; //调用Quote的析构函数
itemP = new Bulk_quote; //静态类型与动态类型不一致
delete itemP; //调用Bulk_quote的析构函数

警告: 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为。

虚析构函数将阻止合成移动操作

基类需要一个虚析构函数这一事实还会对基类和派生类的定义产生另外一个间接的影响:如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。

合成的拷贝控制与继承

基类或派生类的合成构造函数控制成员的行为与其他合成的构造函数、赋值运算符或析构函数类似:它们对类本身的成员依次进行初始化、赋值或销毁的操作。此外,这些合成的成员还负责使用直接基类中对应的操作对一个对象的直接基类部分进行初始化、赋值或销毁的操作

对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该直接基类又销毁它自己的直接基类,以此类推直至继承链的顶端。

派生类中删除的拷贝控制与基类的关系 (略)

移动操作与继承

如前所述,大多数基类都会定义一个虚析构函数。(如果一个类需要析构函数,那么它同样需要拷贝和控制操作,基类的析构函数并不遵循上诉准则),因此在默认情况下,基类通常不含有合成的移动操作,而且它的派生类中也没有合成的移动操作(如果我们定义了拷贝构造函数,则编译器将不会为类合成一个移动构造函数)。

因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确实需要执行移动操作时应该首先在基类中进行定义。我们的Quote可以使用合成的版本,不过前提是Quote必须显式地定义这些成员。一旦Quote定义了自己的移动操作,那么它必须同时显式地定义拷贝操作

1
2
3
4
5
6
7
8
9
10
class Quote{
public:
Quote() = default; //对成员依次进行默认初始化
Quote(const Quote&) = default; //对成员依次拷贝
Quote(Quote&&) = default; //对成员依次拷贝
Quote& operator= (const Quote&) = default; //拷贝赋值
Quote& operator=(Quote&&) = default; //移动赋值
virtual ~Quote() = default;
//其他成员与之前的版本一致
}

派生类的拷贝控制成员

派生类构造函数在其初始化阶段中不但要初始化派生类自己的成员,还负责初始化派生类对象的基类部分。因此,派生类的拷贝和移动构造函数在拷贝和移动自有成员的同时,也要拷贝和移动基类部分的成员。类似的,派生类赋值运算符也必须为其基类部分的成员赋值。

和构造函数及赋值运算符不同的是,析构函数只负责销毁派生类自己分配的资源。如前所述,对象的成员是被隐式销毁的;类似的,派生类对象的基类部分也是自动销毁的。

警告: 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。

定义派生类的拷贝或移动构造函数

当为派生类定义拷贝或移动构造函数时,我们通常使用对应的基类构造函数初始化对象的基类部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base{
public:
Base() = default;
Base(int value):val(value) {}
Base(const Base &b) { this->val = b.val;} //拷贝构造函数
void show() {cout<<"Base::"<<val<<endl;}
private:
int val;
};
class D:public Base{
public:
D() = default;
D(int dval,int bval):Base(bval),Dval(dval) {}
D(const D &d):Base(d),Dval(d.Dval) {} //派生类的拷贝构造函数
void show() {cout<<"D::"<<Dval<<endl;}
private:
int Dval;
};
1
2
3
4
5
6
//D的这个拷贝构造函数很可能是不正确的定义
//基类部分被默认初始化,而非拷贝
D(const D &d) /*成员初始值,但是没有提供基类初始值*/
{
/* .... */
}

在上面的例子中,Base的默认构造函数将被用来初始化D对象的基类部分。假定D的构造函数从d中拷贝了派生类成员,则这个新构建的对象的配置将非常奇怪:它的Base成员被赋予了默认值,而D成员的值则是从其他对象拷贝得来的。

警告: 默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。

派生类赋值运算符

与拷贝和移动构造函数一样,派生类的赋值运算符也必须显式地为其基类部分赋值:

1
2
3
4
5
6
7
//Base::operator=(const Base&) 不会被自动调用
D &D::operator=(const D &rhs){
Base::operator=(rhs); //为基类部分赋值
//按照过去的方式为派生类的成员赋值
//酌情处理自赋值及释放已有资源等情况
return *this;
}

值得注意的是,无论基类的构造函数或赋值运算符是自定义的版本还是合成的版本,派生类的对应操作都能使用他们。例如,对于Base::operator=的调用语句将执行Base的拷贝赋值运算符,至于该运算符是由Base显式定义的还是由编译器合成的无关紧要。

派生类析构函数

如前所述,在析构函数体执行完成后,对象的成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源:

1
2
3
4
5
class D:public Base{
public:
//Base::~Base被自动调用执行
~D() { /*该处由用户定义清除派生类成员的操作*/ }
};

对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直到最后。

在构造函数和析构函数中调用虚函数

如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。

继承的构造函数

在C++新标准中,派生类能够重用其直接基类定义的构造函数。尽管如我们所知,这些构造函数并非以常规的方式继承而来,但是为了方便,我们不妨姑且称其为“继承”的。一个类只初始化它的直接基类,出于同样的原因,一个类也只继承其直接基类的构造函数。类不能继承默认、拷贝和移动构造函数。如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。

派生类继承基类构造函数的方式是提供一条注明了(直接)基类名的using声明语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base{
public:
Base() = default;
Base(int value):val(value) {} //构造函数
void show() {cout<<"Base::"<<val<<endl;}
private:
int val;
};
class D:public Base{
public:
D() = default;
using Base::Base; //继承基类的构造函数
void show() {cout<<"D::"<<Dval<<endl;}
private:
int Dval;
};

通常情况下,using声明语句只是令某个名字在当前作用域内可见。而当作用于构造函数时,using声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。换句话说,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。

这些编译器生成的构造函数形如:

1
derived(parms):base(args) {}

其中,derived是派生类的名字,base是基类的名字,parms是构造函数的形参列表,args将派生类构造函数的形参传递给基类的构造函数。上述继承的构造函数等价于:

1
D(int value):Base(value) {}

如果派生类含有自己的数据成员,则这些成员将被默认初始化。

继承构造函数的特点

  • 一个构造函数的using声明不会改变该构造函数的访问级别。例如,不管using声明出现在哪儿,基类的私有构造函数在派生类中还是一个私有构造函数;受保护的构造函数和公有构造函数也是同样的规则。
  • 如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承。定义在派生类中的构造函数将替换继承而来的构造函数。
  • 默认、拷贝和移动构造函数不会被继承。这些构造函数按照正常规则被合成。继承的构造函数不会被作为用户定义的构造函数来使用,因此,如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。
-------------本文结束感谢您的阅读-------------

本文地址:http://www.wangxinri.cn/2017/10/26/构造函数和拷贝控制/
转载请注明出处,谢谢!

梦想夹带眼泪,咸咸的汗水!