天天看點

C++:51---繼承中的構造函數、析構函數、拷貝控制一系列規則

一、繼承中的構造函數

  • 根據構造函數的執行流程我們知道:
  • 派生類定義時,先執行基類的構造函數,再執行派生類的構造函數
  • 拷貝構造函數與上面是相同的原理

二、繼承中的析構函數

  • 根據析構函數的執行流程我們知道:
  • 派生類釋放時,先執行派生類的析構函數,再執行基類的析構函數

二、繼承中被删除的函數的文法

  • 基類或派生類可以将其構造函數或者拷貝控制成員定義為删除的。此外,某些定義基類的方式也可能導緻有的派生類成員成為被删除的函數。規則如下:
  • 如果基類中的預設構造函數、拷貝構造函數、拷貝指派運算符、或析構函數是被删除的或者是不可通路的,則派生類中對應的成員将是删除的,原因是編譯器不能使用基類成員來執行派生類對象中屬于基類的部分操作
  • 如果在基類中有一個不可通路或删除掉的析構函數,則派生類中合成的預設和拷貝構造函數将是被删除的,因為編譯器無法銷毀派生類對象的基類部分
  • 編譯器不會合成一個删除掉的移動操作。當我們使用=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的被删除的拷貝構造函數      

三、移動操作與繼承

  • 在預設情況下,基類通常不含有合成的移動操作,而且在它的派生類中也沒有合成的移動操作
  • 因為基類缺少移動操作會阻止派生類擁有自己的合成移動操作,是以當我們确實需要執行移動操作時應該首先在基類中進行定義
  • 一旦定義了自己的移動操作,那麼它必須同時顯式地定義拷貝操作(因為如果不定義,合成的拷貝操作會被删除,詳情見對象移動:例如:下面是一個基類,其中顯式地定義了移動操作。現在可以對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;
}      
  • 示範結果如下:
C++:51---繼承中的構造函數、析構函數、拷貝控制一系列規則
  • 我們在Bulk_quote類中使用using繼承了Disc_quote的所有構造函數。對于基類的每個構造函數,編譯器會在派生類中生成一個與之對應的派生類構造函數。格式如下:
  • 例如在本代碼中我們的 using Disc_quote::Disc_quote;語句将在派生類構造函數中生成這樣的代碼(僞代碼,編譯器會自動生成的):
  1. Bulk_quote(const std::string& book, double price, std::size_t qty, double disc)
  2. :Disc_quote(book, price, qty, disc)
  3. {
  4. }
  • 使用了基類的構造函數之後,派生類中的成員将被預設初始化

從基類中繼承的構造函數的特點

  • 規則①:和普通成員的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.預設、拷貝和移動構造函數不會被繼承。這些構造函數按照正正常則被合成。繼承的構造函數不會被作為使用者定義的構造函數來使用。是以,如果一個類隻含有繼承的構造函數,則它也将擁有一個合成的預設構造函數

繼續閱讀