繼承與派生——從未接觸過的船新姿勢哦

之前我們講述的是類之間的嵌套關係,下面我們要介紹一個全新的操作——繼承(派生)

簡介

繼承與派生其實是同一過程從不同的角度看
我們將保持已有類的特性而構造新類的過程稱爲繼承,簡單來說繼承的目的就是實現原來設計與代碼的重用,希望儘量利用原有的類
然而當新的問題出現,原有程序無法解決或不能完全解決時,需要對原有程序進行改造,在已有類的基礎上新增自己的特性而產生新類的過程稱爲派生

基類(或父類):被繼承的原有類
直接基類:直接參與派生出某類的基類
間接基類: 基類的基類甚至更高層的基類
派生類(或子類): 派生出的新類,包括三部分:

  • 吸收基類的成員:默認情況下派生類包含了基類除了構造函數和析構函數之外的所有成員,但是C++11規定可以用using關鍵字將構造函數也繼承過來。
  • 改造基類的成員:如果派生類聲明瞭一個和基類成員同名的新成員,這樣基類同名的成員就被覆蓋了。
  • 添加新的成員:派生類在功能上有所發展。

定義

定義派生類時需要指出繼承方式,如未顯示指出,默認爲private方式繼承

繼承方式 基類 派生類
public public 成員 public 成員
protected 成員 protected 成員
private 成員 不可直接使用
private public 成員 private 成員
protected 成員 private 成員
private 成員 不可直接使用
protected public 成員 protected 成員
protected 成員 protected 成員
private 成員 不可直接使用

上面這個表格非常清楚得顯示出了不同類型的繼承規則
可以發現,在這裏我們遇到了一個全新的類型:protected
現在我們可以來總結一下這三種類型的區別和聯繫啦

  • public是公有類型,在當前類,繼承該類的派生類,類外部都可以被訪問
  • private是私有類型,只允許被當前類的成員函數和友元訪問,不能被繼承該類的派生類訪問,也不能在類外部直接訪問
  • protected是受保護類型,只允許被當前類的成員函數和友元,與繼承該類的派生類訪問,卻不能在類外部直接訪問
    protected成員可以被基類的所有派生類使用,這一性質可以沿繼承樹無限向下傳播
    也就是說:基類中的protected類型數據成員和成員函數,在其protected類型的派生類中可以進行訪問,派生類外部不允許對其進行訪問

目前來看,在類外部如果想訪問類中的成員,只能直接使用public類型的成員
protected和private都是不能被訪問的,對於類外使用而言,這兩個類型是同權的

那麼protected類型有什麼顯而易見的作用呢?

用protected類型代替public類型,可以使派生類擁有訪問基類private成員的權限,同時又可以保證基類的private成員不會被類外部的類實例訪問

class A{
public:
	int x,y;
};
class B:public A{
public:
	void print() {cout<<x<<" "<<y<<endl;}
};

int main() 
{
	B obj;
	obj.x=233;
	return 0;
}

//------------------------------------------

class A{
protected:
	int x,y;
};
class B:public A{
public:
	void print() {cout<<x<<" "<<y<<endl;}
};

int main() 
{
	B obj;
	//obj.x=233; ERROR!
	return 0;
}

//------------------------------------------

class A{
public:
	int x,y;
};
class B:protected A{
public:
	void print() {cout<<x<<" "<<y<<endl;}
};

int main() 
{
	B obj;
	//obj.x=233; ERROR
	return 0;
}

//------------------------------------------

class A{
protected:
	int x,y;
};
class B:protected A{
public:
	void print() {cout<<x<<" "<<y<<endl;}
};

int main() 
{
	B obj;
	//obj.x=233; ERROR
	return 0;
}

訪問

派生類如何訪問基類的數據成員?
  • 在派生類內部,使用成員名就可以引用基類的public成員和protected成員
  • 當派生類重新定義了基類的成員函數時,訪問方式:base-class name:: + 成員函數
    即使是函數名相同,參數不同(函數簽名不同),派生類也不能直接調用基類的函數,必須要指定基類作用域
class A{
public:
    void f() const{cout<<"f in A is called."};
    void g() const{cout<<x<<" "<<y<<endl;}
private:
    int x,y;
};
class B:public A{
public:
    void f() const{cout<<"f in B is called."};
    void h() const{cout<<x<<" "<<y<<endl;}
private:
    int z;
};

int main()
{
    B obj;
    obj.f();
    obj.A::f();
    return 0;
}

// Output
f in B is called.
f in A is called.

初始化

Review:構造函數初始化列表
只能使用構造函數初始化列表進行初始化的數據成員:

  • const data member 常數據成員 (特例:const static integer)
  • reference data member 引用類型的數據成員
  • member objects 數據成員是其他類(而且未提供缺省構造函數)的對象
  • base class 繼承類的基類(未提供缺省構造函數)

對於派生類來說,ta的部分數據成員繼承於基類,在派生類內部並沒有顯式定義
因此在初始化這些繼承而來的數據成員時,應該使用基類的構造函數

PlusCommissionEmployee::PlusCommissionEmployee(const string &first,const string &last,double sales,double rate,double salary):CommissionEmployee(first,last,sales,rate)
{setBaseSalary(salary);}

注意
分文件定義派生類時,需要使用#include包含基類的頭文件:

  • 告知編譯器基類的存在
  • 讓編譯器根據類的定義確定對象的大小,派生類的對象大小取決於派生類顯示定義的數據成員 和繼承自基類的數據成員
  • 讓編譯器能夠判斷派生類是否正確使用了基類的成員

基類與派生類之間的聯繫

派生類可以直接訪問基類的protected數據成員,這就存在一定的缺點:

  • 影響數據的有效性檢查
  • 派生類依賴於基類的實現
    • 基類的數據成員發生改變有可能影響派生類的實現
    • 軟件 " 易碎 "

而且我們在擴展派生類時,可能會定義與基類成員函數名相同的函數,這時我們就將其視爲對基類成員函數的重定義

class CommissionEmployee {
public:
    CommissionEmployee(const string &,const string &,double,double);
    double getBaseSalary() const{...};
    double earnings() const;
    void print() const();
protected:
    string firstName;
    string lastName;
    double grossSales;
    double commissionRate;
};

class PlusCommissionEmployee:public CommissionEmployee {
public:
    PlusCommissionEmployee(const string &first,const string &last,double sales,double rate,double salary):CommissionEmployee(first,last,sales,rate)
    {setBaseSalary(salary);}
    //重定義
    double earnings() const {
        return getBaseSalary()+CommissionEmployee::earnings();
    }
    //重定義
    void print() const() {
        cout<<"base salary: ";
        CommissionEmployee::print();
    }
private:
    double baseSalary;
};

注意:

  • 我們可以通過調用基類的public成員函數,在派生類中訪問基類的私有數據成員
  • 當功能相同時,儘量調用成員函數,以避免代碼拷貝
  • 重定義基類成員函數時,如果牽扯到調用基類成員函數,就必須使用 :: 限定,否則會引起無線遞歸
  • 符合軟件工程的要求:使用繼承,通過調用成員函數隱藏了數據,保證了數據的一致性

構造與析構

構造順序

  • 建立派生類的對象時,必須先調用基類的構造函數初始化派生類對象的繼承成員
  • 派生類的構造函數既可以隱式調用基類的構造函數(缺省構造函數),也可以在派生類的構造函數初始化列表中顯示調用(提供初始值)

析構順序

  • 析構函數地調用順序和構造函數的順序相反,因此派生類的析構函數在基類析構函數之前被調用

Review:不同存儲類別的對象析構函數的調用順序

  • 全局對象:在任何函數執行前構造,在程序結束時析構
  • 局部變量:
    • 自動變量:對象定義時構造,塊結束時析構
    • 靜態變量:首次定義時構造,程序結束時析構
  • 全局和靜態對象(均爲靜態存儲類別)析構順序恰好與構造順序相反

特殊情況一:調用 exit 函數退出程序執行時,除了析構靜態存儲類別對象,不調用剩餘對象的析構函數
特殊情況二:調用 abort 函數退出程序執行時,不調用任何剩餘對象的析構函數

int main()
{
    { // begin new scope
        CommissionEmployee employee1("Bob","Lewis",5000,0.2);
    } // end scope
    cout<<endl;
    PlusCommissionEmployee employee2("Lisa","Jones",7000,0.4);
    cout<<endl;
    PlusCommissionEmployee employee3("Mark","Sands",8000,0.3);
    cout<<endl;
    return 0;
}

// Output
CommissionEmployee constructor.
CommissionEmployee destructor.

CommissionEmployee constructor.
PlusCommissionEmployee constructor.

CommissionEmployee constructor.
PlusCommissionEmployee constructor.

PlusCommissionEmployee constructor.
CommissionEmployee constructor.
PlusCommissionEmployee constructor.
CommissionEmployee constructor.

包含成員對象的基類與派生類

若基類和派生類都包含其他類的對象:

  • 在建立派生類的對象時,首先執行基類成員對象的構造函數,接着執行基類的構造函數,然後執行派生類的成員對象的構造函數,最後執行派生類的構造函數
  • 析構函數的調用次序與調用構造函數的次序相反。
  • 建立成員對象的順序是對象在類定義中的聲明順序。成員初始化值的順序不影響建立對象的順序。

這就牽扯出來一個很重要的問題:Review 對象創建的步驟

對象創建的步驟:

  • 分配內存,數據成員初始化
    • 按照類定義的順序,爲數據成員分配內存空間,並初始化每個數據成員
    • 如果初始化列表中未指定初始化,則使用系統提供的缺省構造函數進行初始化
  • 執行constructor函數體內的語句
class Obj{
public:
    Obj(int x) {cout<<"Obj constructor.\n"; num=x;}
    Obj() {cout<<"Obj default constructor.\n";}
    ~Obj() {cout<<"Obj destructor.\n";}
private:
    int num;
};

class BassClass{
public:
    //構造函數,顯式調用Obj的構造函數
    BaseClass(int x,int y):object(x) 
    {cout<<"BaseClass constructor.\n"; num=y;}
    //缺省構造函數,隱式調用Obj的缺省構造函數
    BaseClass() {cout<<"BaseClass default constructor.\n";}
    ~BaseClass() {cout<<"BaseClass destructor.\n";}
private:
    Obj object;
    int num;
};

class DerivingClass:public BaseClass{
public:
    //構造函數,隱式調用BaseClass的缺省構造函數,顯式調用Obj的構造函數
    DerivingClass(int x,int y):_object(x)
    {cout<<"DerivingClass constructor.\n"; num=y;}
    //缺省構造函數,隱式調用BaseClass的缺省構造函數,隱式調用Obj的缺省構造函數
    DerivingClass() {cout<<"DerivingClass default constructor.\n";}
    ~DerivingClass() {cout<<"DerivingClass destructor.\n";}
private:
    Obj _object;
    int num;
};

int main()
{
    DerivingClass A(10,20);
    return 0;
}
// Output
Obj default constructor.
BaseClass default constructor.
Obj constructor.
DerivingClass constructor.
DerivingClass destructor.
Obj destructor.
BaseClass destructor.
Obj destructor.

修改一下:

class Obj{
public:
    Obj(int x) {cout<<"Obj constructor.\n"; num=x;}
    Obj() {cout<<"Obj default constructor.\n";}
    ~Obj() {cout<<"Obj destructor.\n";}
private:
    int num;
};

class BassClass{
public:
    //構造函數,顯式調用Obj的構造函數
    BaseClass(int x,int y):object(x) 
    {cout<<"BaseClass constructor.\n"; num=y;}
    //缺省構造函數,隱式調用Obj的缺省構造函數
    BaseClass() {cout<<"BaseClass default constructor.\n";}
    ~BaseClass() {cout<<"BaseClass destructor.\n";}
private:
    Obj object;
    int num;
};

class DerivingClass:public BaseClass{
public:
    //構造函數,顯式調用BaseClass的構造函數,顯式調用Obj的構造函數
    DerivingClass(int x,int y):_object(x),BaseClass(x,y)
    {cout<<"DrivingClass constructor.\n"; num=y;}
    //缺省構造函數,隱式調用BaseClass的缺省構造函數,隱式調用Obj的缺省構造函數
    DerivingClass() {cout<<"DerivingClass default constructor.\n";}
    ~DerivingClass() {cout<<"DerivingClass destructor.\n";}
private:
    Obj _object;
    int num;
};

int main()
{
    DerivingClass A(10,20);
    return 0;
}
// Output
Obj constructor.
BaseClass constructor.
Obj constructor.
DerivingClass constructor.
DerivingClass destructor.
Obj destructor.
BaseClass destructor.
Obj destructor.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章