重學C++ (十一) OOP面向對象編程(2)
轉換與繼承
本節主要須要區分的是:
基類和派生類的轉換;
引用(指針)的轉換和對象的轉換。
1.每一個派生類對象包括一個基類部分。因此。能夠像使用基類對象一樣在派生類對象上執行操作。
基於這一點,能夠將派生類對象的引用(指針)轉換為基類子對象的引用(指針),且存在自己主動轉換。
反之,基類到派生類的自己主動轉換是不存在的,因此基類不包括派生類型的成員。另外,將基類指針或引用綁定到派生類對象時也存在限制,由於編譯器編譯時無法知道該轉換是安全的(編譯器確定轉換是否合法,僅僅看指針或引用的靜態類型)。假設我們確定基類到派生類的轉換是安全的,能夠使用static_cast強制編譯器進行轉換,也能夠用dynamic_cast申請在執行時進行檢查。
2.引用轉換不同於對象轉換。
在引用轉換中。對象本身未被復制,轉換不會在不論什麽方面改變派生類型對象。
在對象轉換中(不是引用或指針),形參類型是固定的。將派生類對象傳給基類對象形參時,該派生類對象的基類部分被【復制】到形參,這裏是對基類對象進行初始化或賦值:初始化時調用構造函數,賦值時調用賦值操作符。(其實,一般基類的復制構造函數和賦值操作符的形參是基類類型的const引用,由於存在從派生類引用到基類引用的轉換,故這兩個成員函數可用於從派生類對象到基類對象進行初始化或賦值)。
構造函數和復制控制
構造函數
樣例1:
class Bulk_item: public Item_base
{
public :
Bulk_item():min_qty(0), discount(0.0) {}
};
該構造函數隱式調用基類的默認構造函數初始化對象的基類部分:首先使用Item_base的默認構造函數初始化Item_base部分,之後再初始化Bulk_item部分的成員,並執行構造函數的函數體。
樣例2:
class Bulk_item: public Item_base
{
public:
Bulk_item(const std::string& book, double sales_price, std::size_t qty=0, double disc_rate=0.0):
Item_base(book, sales_price),min_qty(qty), discount(disc_rate) {}
};
該構造函數使用有兩個形參的Item_base構造函數初始化基類子對象。
*首先初始化基類。然後依據【聲明次序】初始化派生類的成員。
*一個類僅僅能初始化自己的【直接】基類。
復制控制和繼承
*僅僅包括類類型或內置類型數據成員,不含指針的類一般能夠使用合成操作。復制、復制或撤銷這種成員不須要特殊控制。
假設派生類定義了自己的復制構造函數,該復制構造函數一般應【顯式】使用基類復制構造函數初始化對象的基類部分:
class Base {/* …… */};
class Derived: public Base
{
public:
//Base::Base(const Base&) not invoked automatically
Derived(const Dervied& d):
Base(d) /* other member initalization*/
{/* …… */}
};
Base(d)將派生類對象d轉換為它的基類部分的引用,並調用基類復制構造函數。
*假設忽略Base(d)。則將執行Base的【默認】構造函數初始化對象的基類部分——並不符合我們【復制】的本意。
*對於復制操作符。必須防止自身賦值。
Derived &Derived::oprator=(const Derived &rhs)
{
if (this != &rhs)
{
Base::operator=(rhs); //assign the base part
//assign the derived part
}
return *this;
}
派生類析構函數不負責撤銷基類對象的成員。【編譯器】總是【顯式】調用派生類對象基類部分的析構函數。每一個析構函數僅僅負責清楚自己的成員:
class Derived: public Base
{
public:
//Base::~Base() invoked automatically
~Derived() {/* 清理派生類部分的成員 */}
};
*對象的撤銷順序與構造順序相反:先執行派生類析構函數。然後按繼承層次依次向上調用個基類析構函數。
虛析構函數
要保證執行適當的析構函數。基類中的析構函數必須為虛函數:
class Item_base
{
virtual ~Item_base() {}
};
Item_base *itemP = new Item_base;
delete itemP; //調用Item_base的析構函數
itemP = new Bulk_item; //指針的靜態類型和動態類型不同
delete itemP; //調用Bulk_item的析構函數
在上述情況中。假設我們不定義虛析構函數。則第二個delete調用的將是Item_base的析構函數,這對於派生類型是沒有定義的行為。
*構造函數不能定義為虛函數。賦值操作符設為虛函數則可能會令人混淆,並且不會有什麽用處。
構造函數和析構函數中的虛函數
首先須要明白:
構造派生類對象時先執行基類構造函數,此時對象的派生類部分是未初始化的。
撤銷派生類對象時先撤銷派生類部分。
在這兩種情況下,對象都是不完整的。
因此,在基類構造函數或析構函數中,編譯器將派生類對象【以基類類型對象對待】。
假設在構造函數或析構函數中調用虛函數。執行的是構造函數或析構函數自身類型定義的版本號。
理由:
假設從基類構造函數中調用虛函數的派生類版本號,則派生類版本號可能會調用派生類成員。而此時對象的派生部分成員還未初始化!
該訪問將可能導致程序崩潰。
繼承情況下的類作用域
在繼承情況下,派生類的作用域嵌套在基類作用域中。
假設不能在派生類作用域中確定名字,就在外圍基類作用域中查找該名字的定義。
與基類成員同名的派生類成員將屏蔽對基類成員的直接訪問(可通過作用域操作符訪問被屏蔽的基類成員)。
註意。對於函數。即使函數原型不同。僅僅要名字相同。基類成員就會被屏蔽。
局部作用域中聲明的函數不會重載全局作用域中定義的函數,相同,派生類中定義的函數也不重載基類中定義的成員。
因此,假設基類有下面函數:
int memfcn();
而派生類有下面函數:
int memfcn(int);
則對派生類調用memfcn()將報錯。因此不存在對基類函數的重載,基類中的memfcn()被屏蔽。
這也解釋了虛函數為什麽必須在基類和派生類中擁有同一原型。
看下面代碼:
class Base
{
public:
virtual int fcn();
};
class D1: public Base
{
public:
int fcn(int); //屏蔽基類的fcn()
};
class D2: public D1
{
public:
int fcn(int); //非虛函數,屏蔽D1的fcn(int)
int fcn(); //虛函數,重寫基類的fcn()
};
Base bobj;
D1 d1obj;
D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //call Base::fcn at run time
bp2->fcn(); //call Base::fcn at run time
bp3->fcn(); //call D2::fcn at run time
三個指針都是基類類型的指針,故通過在Base中查找fcn確定這三個調用。
由於fcn是虛函數,所以編譯器會生成代碼,在執行時基於引用或指針所綁定的實際類型進行調用。
純虛函數
在函數形參表後面寫上=0指定純虛函數:
double fcn(std::size_t) const = 0;
含有一個或多個純虛函數的類是抽象基類,除了作為抽象基類的派生類的對象的組成部分。不能創建抽象類型的對象。
容器與繼承
假設一個容器存放的是基類類型的對象,當插入一個派生類對象時,會將派生類對象的基類部分【復制】到基類對象並保存在容器中(因此派生類部分將被切掉。也就是說容器裏的僅僅能是基類對象,而不是派生類對象)。
一個解決方式是,使用容器保存對象的指針。
重學C++ (十一) OOP面向對象編程(2)