C++(继承):17---继承中的构造函数、析构函数、拷贝控制一系列规则

一、继承中的构造函数

  • 根据构造函数的执行流程我们知道:
    • 派生类定义时,先执行基类的构造函数,再执行派生类的构造函数
    • 拷贝构造函数与上面是相同的原理

二、继承中的析构函数

  • 根据析构函数的执行流程我们知道:
    • 派生类释放时,先执行派生类的析构函数,再执行基类的析构函数

二、继承中被删除的函数的语法

  • 基类或派生类可以将其构造函数或者拷贝控制成员定义为删除的。此外,某些定义基类的方式也可能导致有的派生类成员成为被删除的函数。规则如下:
    • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符、或析构函数是被删除的或者是不可访问的,则派生类中对应的成员将是删除的,原因是编译器不能使用基类成员来执行派生类对象中属于基类的部分操作
    • 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分
    • 编译器不会合成一个删除掉的移动操作。当我们使用=default请求一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除或不可访问的,则派生类的移动构造函数也将是被删除的

演示案例

class B {
public:
    B() { cout << "B" << endl; }
    B(const B&) = delete; //拷贝构造函数被定义为删除的
    //其他成员,不包含移动构造函数
};

class D :public B {
    //没有声明任何构造函数
};

D d;                //正确,使用D的合成默认构造函数
D d2(d);            //错误,D的合成构造函数是被删除的
D d3(std::move(d));//错误,隐式地使用D的被删除的拷贝构造函数

三、移动操作与继承

  • 在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作
  • 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确实需要执行移动操作时应该首先在基类中进行定义
  • 一旦定义了自己的移动操作,那么它必须同时显式地定义拷贝操作(因为如果不定义,合成的拷贝操作会被删除,详情见对象移动:https://blog.csdn.net/qq_41453285/article/details/104419356
  • 例如:下面是一个基类,其中显式地定义了移动操作。现在可以对Quote的对象进行拷贝、移动、赋值、销毁操作了。除非Quote的派生类含有排斥移动的成员,构造派生类自动获得合成的移动操作
class Quote {
public:
    Quote() = default;                        //构造函数
    Quote(const Quote&) = default;            //拷贝构造
    Quote(Quote&&) = default;                 //移动拷贝构造
    Quote& operator=(const Quote&) = default; //拷贝赋值运算符
    Quote& operator=(Quote&&) = default;      //移动赋值运算符
	virtual ~Quote() = default;               //虚析构函数
};

四、派生类的拷贝控制成员

  • 派生类在执行拷贝构造函数/移动拷贝构造函数,或拷贝赋值运算符/移动赋值运算符时,不仅需要拷贝自己的成员,而需要拷贝基类的成员

拷贝构造函数/移动构造函数

  • 当派生类定义拷贝或移动构造函数时,不仅需要构造自己的成员,还需要构造属于基类的成员
  • 这与构造函数不同:
    • 如果基类有构造函数,派生类必须在构造函数的初始化列表构造继承(这是强制的)
    • 而拷贝构造函数/移动构造函数不是强制的,因此如果你没有拷贝/移动属于基类的部分,那么可能会导致基类部分的数据不明确(这是建议性的)
  • 例如:
class Base {
    //基类成员
};

class D :public Base {
public:
    D(const D& d) :Base(d)  //别忘记构造基类成员
    {
        //在函数体内拷贝构造本类成员
    }
    D(D &&d) :Base(std::move(d)) //别忘记移动基类成员
    {
        //在函数体内移动本类成员
    }
};

派生类赋值运算符

  • 与拷贝和移动构造函数一样,派生类的赋值运算符页必须显式地为其基类部分赋值:
  • 例如:
class Base {
    //基类成员
};

class D :public Base {
public:
    D& operator=(const D& rhs) {
        Base::operator=(rhs); //为基类执行赋值运算符
        //然后再执行本类的部分
        return *this;
    }
};

五、特别注意:在构造函数和析构函数中调用虚函数

  • 根据构造函数,析构函数我们知道:
    • 派生类构造时,先构造基类部分,然后再构造派生类部分
    • 派生类析构时,先析构派生类部分,然后再析构基类部分
  • 因此:
    • 在基类构造函数执行的时候,派生类的部分是未定义状态
    • 在基类析构函数执行的时候,派生类的部分已经被释放了
  • 所以在基类的构造函数或析构函数中调用虚函数是不建议的,因为:
    • 虚函数在执行的时候可能会调用到属于派生类的成员,而此时派生类可能还未构造/或者已经被释放了,因此程序可能会崩溃
  • 所以建议:
    • 如果构造函数或析构函数调用了某个虚函数,则应该执行与构造函数或析构函数所属类型相同的虚函数版本(同属于一个类)

六、继承/重用基类构造函数

  • C++11标准中,派生类能够“继承/重用”其直接基类定义的构造函数
  • 使用规则:
    • 使用using声明(见下面的演示案例)

演示案例

class Disc_quote {
public:
    Disc_quote(const std::string& book, double price, std::size_t qty, double disc)
        :book(book), price(price), qty(qty), disc(disc)
    {
        cout << "A" << endl;
    }
public:
    std::string book;
    double price;
    std::size_t qty;
    double disc;
};

class Bulk_quote :public Disc_quote {
public:
    //虽然我们使用了using声明从基类中接收来了一个构造函数,但是
    //我们还必须显式地给出一个构造函数,且为基类进行构造,否则不能创建Bulk_quote对象
    Bulk_quote() :Disc_quote("Word", 1, 2, 3) {
        cout << "B" << endl;
    }
    using Disc_quote::Disc_quote; //继承基类中的所有构造函数
};

int main()
{
    Bulk_quote *a = new Bulk_quote(); //此处调用Bulk_quote的构造函数
    cout << "********" << endl;
    Bulk_quote *b = new Bulk_quote("Hello", 1, 2, 3); //此处调用从Disc_quote中的构造函数
    return 0;
}
  • 演示结果如下:

  • 我们在Bulk_quote类中使用using继承了Disc_quote的所有构造函数。对于基类的每个构造函数,编译器会在派生类中生成一个与之对应的派生类构造函数。格式如下:

  • 例如在本代码中我们的 using Disc_quote::Disc_quote;语句将在派生类构造函数中生成这样的代码(伪代码,编译器会自动生成的):
Bulk_quote(const std::string& book, double price, std::size_t qty, double disc)
    :Disc_quote(book, price, qty, disc)
{
}
  • 使用了基类的构造函数之后,派生类中的成员将被默认初始化

从基类中继承的构造函数的特点

  • 规则①:和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别。例如,基类的public构造函数在派生类中被using声明,不论该using声明在派生类的哪一访问级别下(public/protected/private)都是public的
  • 规则②:一个using声明不能指定explicit或constexpr。如果基类的构造函数是explicit或者constexpr的。这些属性在派生类中继续存在
  • 规则③:当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数
    • 例如:基类有一个接受两个形参的构造函数,其中第二个形参含有默认实参,则派生类将获得两个构造函数(一个构造函数接受两个形参(都没有实参),另一个构造函数只接受一个形参)
    • 见下面的演示案例
class A
{
public:
    A(int a, int b = 10) :a(a) {}
private:
    int a;
    int b;
};

class B:public A {
public:
    B() :A(1) {}
    using A::A;
    //此using语句相当于在B中定义了这样两个构造函数
    /*
        B(参数)::A(参数1, 参数2) {} //其中两个参数都要给出
	    B(参数)::A(参数1) {}        //其中只要给出第一个参数
    */
};
  • 规则④:如果基类含有几个构造函数,除了两个例外情况,否则派生类将继承基类的所有构造函数
    • 1.如果派生类定义了一个构造函数与基类的构造函数具有相同的参数列表,则在用这个构造函数创建派生类时,执行的是派生类的那个,因为基类的那个没有被继承(也可以被理解为覆盖了
    • 2.默认、拷贝和移动构造函数不会被继承。这些构造函数按照正常规则被合成。继承的构造函数不会被作为用户定义的构造函数来使用。因此,如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数
发布了1481 篇原创文章 · 获赞 1026 · 访问量 38万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104435826