构造函数与拷贝控制
2024年1月6日 2024年1月7日
虚析构函数
考虑如下场景:
- 基类指针指向派生类对象
- 使用delete销毁基类指针所指对象
指针的静态类型与被销毁对象的动态类型不符, 我们需要调用派生类的析构函数
将基类的析构函数定义为虚函数, 派生类的析构函数继承了虚属性, 因此, 可以实现析构时的动态绑定
如果基类的析构函数不是虚函数, delete一个指向派生类对象的基类指针将产生未定义行为
基类的析构函数
基类需要一个虚析构函数
之前有讲到, 如果一个类需要析构函数, 它同时也需要拷贝和赋值操作. 这一准则不适用基类的析构函数
如果一个类定义了析构函数, 编译器不会为这个类合成移动操作
个人理解
- 如果一个类需要自定义析构函数, 如管理动态内存, 通常还需要自定义拷贝操作(拷贝构造函数和拷贝赋值运算符)和移动操作(移动构造函数和移动赋值运算符)
三/五法则
- 拷贝操作的必要性高于移动操作, 当我们没有自定义移动操作时, 编译器可以使用拷贝操作代替
- 编译器主动提供拷贝操作合成版本的必要条件, 和析构函数有关的, 在于能够销毁一个对象
- 编译器主动提供移动操作合成版本的必要条件之一是类未定义析构函数, 个人认为与移动操作的次要性和三/五法则有关
虚析构函数将阻止合成移动操作 :
- 将析构函数声明为虚函数, 通常会显式要求编译器提供合成版本
显式要求编译器提供合成版本, 等价于定义了该操作 - 如果一个类定义了析构函数, 编译器不会为这个类合成移动操作
拷贝控制与继承
继承关系中的类的拷贝控制
继承关系中的类(基类或派生类)的拷贝控制成员:
- 对类本身的成员进行初始化, 赋值和销毁操作
- 负责使用直接基类中的对应操作对直接基类部分进行初始化, 赋值和销毁
要求直接基类相应的成员可访问且非删除
示例
-
当我们构造一个Bulk_quote对象时
- Bulk_quote默认构造函数先调用Disc_quote默认构造函数意图初始化Disc_quote子对象; 而Disc_quote默认构造函数会先调用Quote默认构造函数意图初始化Quote子对象
- Quote默认构造函数最先被执行: 默认初始化bookNo, 使用类内初始值初始化price
- 之后执行Disc_quote默认构造函数: 使用类内初始值初始化quantity和discount
- 最后执行Bulk_quote默认构造函数
- Quote默认构造函数最先被执行: 默认初始化bookNo, 使用类内初始值初始化price
Quote, Disc_quote和Bulk_quote的默认构造函数均为显式default
- Bulk_quote默认构造函数先调用Disc_quote默认构造函数意图初始化Disc_quote子对象; 而Disc_quote默认构造函数会先调用Quote默认构造函数意图初始化Quote子对象
-
合成的Bulk_quote拷贝构造函数使用Disc_quote拷贝构造函数, 后者又使用Quote拷贝构造函数
Quote, Disc_quote和Bulk_quote的拷贝构造函数均为隐式default -
所有类都使用合成的析构函数
- 基类的析构函数显式
default
且为虚函数 - 派生类的析构函数隐式
default
- 析构函数的隐式析构部分负责销毁类的成员
派生类的析构函数除了销毁自己的成员外, 还负责销毁派生类的直接基类; 直接基类往上销毁直至继承链的顶端
- 基类的析构函数显式
-
Quote没有合成的移动操作, 其派生类也没有
1class Quote 2{ 3public: 4 Quote() = default; 5 Quote(const string &book, double sales_price) : bookNo(book), price(sales_price) {} 6 string isbn() const { return bookNo; } 7 virtual double net_price(size_t n) const { return n * prices; } 8 virtual ~Quote() = default; 9 10private: 11 string bookNo; 12 13protected: 14 double price = 0.0; 15}; 16 17class Disc_quote : public Quote 18{ 19public: 20 Disc_quote() = default; 21 Disco_quote(const string &book, double price, size_t qty, double disc) : Quote(book, price), quantity(qty), discount(disc) {} 22 double net_price(size_t) const = 0; 23 24protected: 25 size_t quantity = 0; 26 double discount = 0.0; 27}; 28 29class Bulk_quote : public Disc_quote 30{ 31public: 32 Bulk_quote() = default; 33 Bulk_quote(const string &book, double price, size_t qty, double disc) : Disc_quote(book, price, qty, disc) {} 34 35 // Bulk_quote调用Disc_quote, Disc_quote调用Quote 36 // 先初始化Quote子对象, 再初始化Disc_quote子对象, 最后运行Bulk_quote的构造函数 37 38 double net_price(size_t) const override; 39};
继承关系中构造函数和析构函数的调用顺序
继承关系中, 从上往下调用构造函数的函数体, 从下往上调用析构函数的函数体
1#include <iostream> 2 3using namespace std; 4 5class FooBase 6{ 7public: 8 FooBase(int aa) : a(aa) {} 9 FooBase() : FooBase(0) { cout << "this is default constructor of FooBase.\n"; } 10 ~FooBase() { cout << "this is destructor of FooBase.\n"; } 11 12private: 13 int a; 14}; 15 16class Foo : protected FooBase 17{ 18public: 19 Foo() { cout << "this is default constructor of Foo.\n"; } 20 ~Foo() { cout << "this is destructor of Foo.\n"; } 21}; 22 23class FooDerived : public Foo 24{ 25public: 26 FooDerived() { cout << "this is default constructor of FooDerived.\n"; } 27 ~FooDerived() { cout << "this is destructor of FooDerived.\n"; } 28}; 29 30int main() 31{ 32 FooDerived fd; 33 34 cout << "main finished.\n"; 35 return 0; 36}
输出
this is default constructor of FooBase. this is default constructor of Foo. this is default constructor of FooDerived. main finished. this is destructor of FooDerived. this is destructor of Foo. this is destructor of FooBase.
继承与默认构造函数和拷贝控制成员的合成版本
基类可以导致派生类的某些操作被定义为隐式删除
- 基类的默认构造函数/拷贝构造函数/拷贝赋值运算符/析构函数若为删除或不可访问, 派生类的对应成员为隐式删除
编译器无法使用基类成员来执行派生类对象基类部分的构造/赋值/销毁操作 - 基类的析构函数不可访问或删除, 派生类中的默认构造函数, 拷贝构造函数和移动构造函数为隐式删除
编译器无法销毁派生类对象的基类部分 - 基类没有移动操作时
- 如果派生类显式要求编译器提供移动操作的合成版本
=default
, 派生类的移动操作为显式删除 - 如果派生类未定义移动操作, 派生类不会有移动操作
编译器只在一种情况提供隐式删除的移动操作, 那就是基类的析构函数不可访问或删除
其他情况, 编译器不会提供隐式删除的移动操作
- 如果派生类显式要求编译器提供移动操作的合成版本
示例
1class B 2{ 3public: 4 B(); // 自定义默认构造函数 5 B(const B &) = delete; // B的拷贝构造函数被定义为删除, 编译器不会提供合成版本为default的移动构造函数 6}; 7 8class D : public B 9{ 10 // 有合成版本为default的默认构造函数 11 // 有合成版本为delete的拷贝构造函数 12 // 无合成版本的移动构造函数, 请求移动构造时, 使用拷贝构造代替 13}; 14 15D d; // 正确: B和D都有默认构造函数 16D d2(d); // 错误: B和D的拷贝构造函数都为删除 17D d3(std::move(d)); // 错误: 无法使用拷贝构造代替移动构造
移动操作与继承
大多数基类会定义一个虚析构函数, 因此, 基类不会有编译器提供的隐式 default
的移动操作
派生类也不会有编译器提供的隐式 default
的移动操作
如果我们需要移动操作, 需要在基类中定义移动操作:
- 自定义移动操作
- 显式要求编译器提供
default
的合成版本
除非派生类中含有排斥移动的成员, 派生类也将自动获得合成的移动操作
示例
1class Quote 2{ 3public: 4 Quote() =default; 5 Quote(const Quote &) = default; 6 Quote(Quote &&) = default; 7 8 Quote &operator=(const Quote &) = default; 9 Quote &operator=(Quote &&) = default; 10 11 virtual ~Quote() = default; 12};
派生类的拷贝控制成员
分3类:
-
构造函数
派生类构造函数在其初始化阶段除了需要初始化派生类自己的成员, 还负责初始化派生类对象的基类部分派生类的拷贝/移动构造函数在拷贝/移动自有成员的同时, 还要拷贝/移动基类部分的成员
-
赋值运算符
派生类的拷贝/移动赋值运算符除了为自有成员赋值, 还需为基类部分的成员赋值 -
析构函数
析构函数只负责销毁派生类自己分配的资源
定义派生类的拷贝/移动构造函数
在构造函数初始值列表中, 将基类部分的初始化委托给基类的拷贝/移动构造函数
1class Base { /*...*/ }; 2class D : public Base 3{ 4public: 5 D(const D &d) : Base(d) { /*...*/ } // Base(d)匹配B的拷贝构造函数 6 D(D &&d) : Base(std::move(d)) { /*...*/ } // Base(std::move(d))匹配B的移动构造函数 7};
否则, 对派生类对象的基类部分执行默认初始化. 可以察觉, 此时派生类的拷贝/移动构造函数不符合拷贝/移动的定义
默认情况下, 基类默认构造函数负责初始化派生类对象的基类部分. 如果我们希望拷贝/移动基类部分, 则需要在派生类的构造函数初始值列表中显式地使用基类的拷贝/移动构造函数
定义派生类赋值运算符
需显式为基类部分赋值
-
派生类的拷贝赋值运算符
1D &D::operator=(const D &rhs) 2{ 3 Base::operator=(rhs); 4 // ... 5 return *this; 6}
-
派生类的移动赋值运算符
1D &D::operator=(D &&rhs) 2{ 3 Base::operator=(std::move(rhs)); 4 // ... 5 return *this; 6}
定义派生类析构函数
只负责销毁派生类自己分配的资源
对象销毁的顺序与其创建的顺序相反: 先执行派生类析构函数, 然后是基类的析构函数, 沿着继承体系的反方向直至根节点
在构造函数和析构函数中调用虚函数
对象的类型在构造/析构的过程中, 与构造函数/析构函数的类型一致 :
- 正向构造
构造基类部分时, 派生类部分还未初始化; 基类构造函数调用虚函数, 使用基类的虚函数版本 - 逆向析构
析构派生类部分时, 基类部分已被销毁; 派生类析构函数调用虚函数, 使用派生类的虚函数版本
在构造/析构函数中调用另一个函数时, 作用域与构造/析构函数类型的作用域一致
如果构造/析构函数调用了某个虚函数, 则我们应该执行与构造/析构函数所属类型相对应的虚函数版本
派生类继承直接基类的构造函数
- 派生类负责初始化它的直接基类: 派生类可以在构造函数初始值列表中使用直接基类的构造函数, 但这不被视作继承
- 派生类可以继承直接基类的构造函数, 不包括默认/拷贝/移动构造函数
使用using声明
1class Bulk_quote : public Disc_quote 2{ 3public: 4 using Disc_quote::Disc_quote; 5};
对于基类的每个构造函数, 编译器会生成与之对应的派生类构造函数
编译器生成的派生类构造函数拥有与基类对应的构造函数相同的形参列表, 编译器在构造函数初始值列表中将参数传递给直接基类的构造函数
1derived(params) : base(args) {}
示例
1Bulk_quote(const string &book, double price, size_t qty, double disc) : Disc_quote(book, price, qty, disc) { }
编译器生成的派生类构造函数, 默认初始化派生类部分数据成员
编译器生成的派生类构造函数的访问属性, 与基类对应的构造函数相同
和普通成员的using声明不一样, 构造函数的using声明不会改变构造函数的访问级别
如果基类的构造函数使用explicit/constexpr标记, 编译器生成的派生类构造函数具有相同的属性
如果基类构造函数拥有默认实参, 编译器会生成多个派生类构造函数, 这些构造函数匹配形参个数
1class Base 2{ 3public: 4 Base(int aa = 1, double bb = 0.0) : a(aa), b(bb) { } 5 Base() = default; 6private: 7 int a; 8 double b; 9}; 10 11class Derived : public Base 12{ 13 using Base::Base; 14 // => 15 // Derived(int a, double b) : Base(a, b) {} 16 // Derived(double b) : Base(1, b) {} 17 // Derived() : Base(1, 0.0) {} 18};
派生类不会继承的基类构造函数
- 如果派生类定义的构造函数与基类构造函数拥有相同的形参列表, 编译器不会生成基类构造函数对应的派生类构造函数
- 派生类不会继承基类的默认/拷贝/移动构造函数
这些构造函数按正常规则被合成
继承的构造函数不被视作用户定义的构造函数
如果一个类只含有继承的构造函数, 编译器会尝试为它合成 default
的默认构造函数