基類和派生類的定義以及虛函數
基類Quote的定義:
classs Quote {
public:
Quote() = default;
Quote(cosnt std::string& book, double sales_price) : bookNo(book), price(sales_price) {}
std::string isbn() const { return bookNo; }
virtual double net_price(size_t n) const { return n * price; }
virtual ~Quote() = default;
private:
std::string bookNo;
protected:
double price = 0.0;
};
PS: 基類通常都應該定義一個虛析構函數,儘管這個析構函數不執行任何實際的操作。
派生類可以直接繼承基類的成員。基類應該講它的成員函數分爲兩種:一種是基類希望它的派生類重寫覆蓋的函數,類似net_price()成員函數;另一種是基類希望派生類直接繼承而不要改變的成員函數,類似isbn() 。 對於基類的成員函數,派生類如果需要覆蓋,則將其聲明爲虛函數(virtual)的,它只能出現在成員函數的聲明的最前面。
基類可以將任意非static成員函數(除了構造函數之外)定義爲virtual函數。
如果成員函數被定義爲虛函數,則其解析過程發生在運行階段,如果未被定義爲虛函數,解析過程發生在編譯階段。
定義派生類:
class Bulk_quote : public Quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, size_t, double);
// override : 覆蓋基類的net_price版本
double net_price(size_t) const override;
private:
size_t min_qty = 0;
double discount = 0.0;
};
一個派生類不總是需要覆蓋它的基類的虛函數,如果派生類沒有覆蓋掉基類的虛函數,則這個虛函數和普通函數沒什麼區別,派生類會直接繼承基類中的版本;
如果派生類需要覆蓋基類的虛函數版本來重寫自己的版本,就需要用到C++11關鍵字override。
override允許派生類顯示的註明它使用某個成員函數覆蓋了它繼承的基類的虛函數,具體的做法是:
在虛函數的參數列表後面添加override關鍵字,如果這個虛函數是一個const成員函數,則override寫在const之後,如果這個虛函數還是一個引用成員函數,則override跟在引用限定符&或&&之後。
因爲派生類對象中含有其基類對應的組成部分,因此一個派生類對象可以被當做一個基類來使用,也能把一個基類指針或引用綁定到派生類對象的基類部分上。
Quote item;
Bulk_quote bulk;
Quote* p = &item;
p = &bulk;
Quote& r = bulk;
這種轉換稱爲“派生類向基類的轉換”,在派生類中含有基類的組成部分,這是繼承的關鍵所在。
派生類的構造函數
派生類並不直接構造基類的成員,而是使用基類的構造函數來構造,通過向基類的構造函數傳遞對應的參數來完成:
Bulk_quote(const std:string& book, double p, size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) {}
改構造函數構造順序是:先進行基類Quote的構造,待Quote的函數體執行完畢後,按派生類Bulk_quote的成員的聲明順序來初始化它自己的成員,最後執行它的函數體。
首先進行基類Quote的構造,再按照聲明的順序依次初始化派生類的成員。
繼承與靜態成員
如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義。不論派生類有多少個,這個靜態成員只有這一個,同時, 靜態成員遵循訪問控制規則。
void Derived::f(const Derived& derived)
{
Base::statmem();
Derived::statmem();
derived.statmem(); // 通過derived對象訪問
statmem(); // 通過this對象訪問
}
一個類如果被用做基類,則這個類必須是已經定義的類,而非僅僅聲明。因此,一個類不能做自己的基類。
防止一個類被繼承的方法是,在這個類的類名後跟關鍵字final。
class NoDerived final {};
class NoDerived1 : public NoDerived {}; // 錯誤, NoDerived是final的, 不能被繼承
類型轉換和繼承
靜態類型與動態類型
可以將一個基類的指針或引用綁定到派生類的對象上,但是實際上並不知道該指針或引用所綁定對象的真實類型,可能是基類對象,也可能是派生類對象。
PS: 和內置指針一樣,智能指針也支持派生類向基類的轉換,於是我們就可以將派生類對象存儲在一個基類的智能指針內。
在使用繼承關係的類型時,需要將靜態類型和動態類型區分開。 靜態類型在編譯時是已知的,它是變量聲明時的類型或表達式生成的類型;而動態類型則是變量表示內存中的對象的類型,直到運行時纔可知。
如果表達式既不是指針也不是引用,那麼它的動態類型永遠和靜態類型一致。
void print_total(Quote& q){
q.net_price(1);
}
q的靜態類型爲Quote。
print_total在調用net_price時, 動態類型依賴於q綁定的實參,如果傳遞的是Quote對象,則它的靜態類型和動態類型相同,如果傳遞的是一個Bulk_quote對象,則它的動態類型和靜態類型不一樣。
派生類對象可以當做基類使用,但是不存在從基類向派生類的轉換,即使一個基類指針或引用綁定在一個派生類對象上,也不能執行基類向派生類的轉換。
Bulk_quote bulk;
Quote* itemp = &bulk;
Bulk_quote* bulkp = itemp; // 錯誤,不能將基類轉換成派生類
編譯器在編譯時無法確定某個轉換在運行時是否安全,因爲編譯器只能通過檢查指針或引用的靜態類型來推斷該轉換是否合法。
如果我們已知從一個基類轉換到派生類是安全的,我們可以使用static_cast來強制覆蓋掉編譯器的檢查工作;如果基類中有虛函數,我們可以使用dynamic_cast請求一個類型轉換,該轉換的安全檢查在運行時執行。
如果只是普通的基類對象和派生類對象,則不存在上述這些轉換, 因爲把一個派生類賦值給一個基類對象時,實際上執行的是基類的構造函數, 基類構造函數只負責將派生類中基類的部分賦值給自己,剩下的派生類的部分就被切掉了。
虛函數
對虛函數的調用在運行時被解析
void print_total(Quote& q){
q.net_price(1);
}
在print_total的調用中,q的靜態類型爲Quote,通過q來調用虛函數net_price(), 對虛函數的調用時,在運行時纔會決定q的類型到底是Quote還是Bulk_quote。
當且僅當對通過指針或引用調用虛函數時,纔會在運行時進行解析。也只有這種情況下,對象的動態類型纔可能與其靜態類型不同。
派生類中的虛函數如果覆蓋了基類的虛函數,則它的形參類型必須和它所覆蓋的虛函數的形參類型完全一致。 同樣,派生類中重寫的虛函數的返回類型也必須和覆蓋了的函數的返回類型一致,但是有個例外,如果該返回類型是類本身的指針或引用時,該規則無效。
如果D繼承自B, 則基類B中的虛函數如果返回B*或B&, 則D中的該虛函數可以返回D*或D&, 前提是從D到B的類型轉換時可訪問的。
final和override
override說明符可以避免虛函數覆蓋中的一些問題:
派生類如果定義了一個函數名和基類中虛函數名相同的函數但是形參列表不同,則派生類定義的這個函數則認爲是一個獨立的函數,而這時,派生類沒有覆蓋掉基類的虛函數,對於實際的編程習慣而言,這有可能會引發錯誤。
可以通過override來幫助我們發現一些錯誤,如果override標記了一個函數,但該函數並沒有覆蓋掉已存在的虛函數,此時編譯器將報錯:
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B {
void f1(int) const override; // 正確,覆蓋了基類的f1
void f2(int) override; // 錯誤,基類沒有f2(int)的函數
void f3() override; // 錯誤,f3()不是虛函數
void f4() override; // 錯誤,基類中沒有f4()函數
};
還可以將某個函數指定爲final,將基類中一個虛函數指定爲final,則它的派生類中試圖覆蓋該虛函數的操作都將發生錯誤:
struct B {
virtual void f1(int) const;
virtual void f2();
void f3();
virtual void func_final() final;
};
struct D2 : B {
void func_final() /*override*/; // 錯誤, func_final函數是final的
};
虛函數與默認實參
虛函數可以擁有默認實參,如果在某次的調用中使用了虛函數的默認實參,則該實參值由本次調用的靜態類型決定。
也就是說,如果我們通過基類的指針或引用調用函數, 則使用基類中該虛函數給的默認實參, 即使動態類型時派生類也是如此。
迴避虛函數的機制
如果派生類的虛函數需要調用它的基類的該虛函數的版本,需要基類的作用域符來強制要求,否則在運行時調用的是派生類的虛函數版本, 這樣會導致無窮遞歸!
class Base {
public:
string name() { return basename; }
virtual void print(ostream& os) { os << basename; }
private:
string basename;
};
class derived : public Base {
public:
void print(ostream& os) { print(os); os << " " << i; } // 錯誤,會導致無窮遞歸
private:
int i;
};
在derived類中,print函數的目的是調用基類Base的print函數, 但在實際運行print函數中, 派生類的print函數相當於this->print, this綁定的是derived類,這樣就會導致無限遞歸,修改的方法是使用Base的作用域:
void print(ostream& os) { Base::print(os); os << " " << i; }