C++面向對象筆記(4):繼承篇

C++繼承篇

1.繼承的基本概念

被繼承的類叫做基類也叫做父類/超類,從其他類繼承而來的類叫做派生類也叫做子類。

子類中不僅繼承了父類的中的數據成員,也繼承了父類的成員函數。

實例化子類時,構造和析構函數的調用:

在子類實例化對象的時候,會先隱式的調用父類的構造函數;在子類對象析構之後,也會隱式的調用父類的析構函數。

舉個例子,假如有父類A,子類B,在其構造函數和析構函數中都輸出一個標記語句,在這種情況下,如果我們創建一個子類B的對象,那麼可以得到以下輸出:

// 創建子類B的對象,構造函數和析構函數的調用順序
A(),This is the constructor
B(),This is the constructor

~B(),This is the destructor
~A(),This is the destructor

2.繼承方式-訪問限定符

繼承時一定別忘了寫訪問限定符!如果不寫,默認是private私有繼承。

繼承的使用:

分類 寫法
公有繼承 class A:public B
保護繼承 class A:protected B
私有繼承 class A:private B

繼承方式和對應的訪問屬性(權限):

繼承方式 基(父)類成員訪問屬性 派生類訪問屬性
public public public
protected protected
private 無法訪問
protected public protected
protected protected
private 無法訪問
private public private
protected private
private 無法訪問

這個表格我們每一行從左往右看,拿public繼承方法來說明:

  1. 如果父類中某成員爲public,子類通過public方式將其繼承,該成員的訪問權限不變依然爲 public
  2. 如果父類中某成員爲protected,子類通過public方式將其繼承,該成員的訪問權限變爲 protected
  3. 如果父類中某成員爲private,子類通過public方式將其繼承,該成員的訪問權限變爲 不可訪問。

小總結:

B類從A類派生,那麼B類是A類的子類,A類是B類的超類。

B類從A類派生,那麼B類中含有A類的所有數據成員

B類從A類公共派生,那麼可以通過B類的對象調用到A類的 所有成員函數 public及protected限定符下的成員函數

B類從A類公共派生,那麼可以在B類中直接使用A的public及protected的數據成員

B類從A類公共派生,那麼A類的公共成員函數成爲B類的公共成員函數。

B類從A類公共派生,那麼A類的保護成員函數成爲B類的保護成員函數。

B類從A類公共派生,那麼A類的私有成員函數 成爲B類的私有成員函數 不能被B類繼承並使用

B類從A類私有派生,那麼A類的公共成員函數成爲B類的 公共成員函數 私有成員函數

B類從A類保護派生,那麼A類的公共成員函數成爲B類的保護成員函數。

B類從A類保護派生,那麼A類的保護成員函數成爲B類的保護成員函數。

#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;

/**
 * 定義人的類: Person
 * 數據成員姓名: m_strName
 * 成員函數: eat()
 */
class Person
{
public:
    string m_strName;
	void eat()
	{
		cout << "eat" << endl;
	}
};

/**
 * 定義士兵類: Soldier
 * 士兵類公有繼承人類: public
 * 數據成員編號: m_strCode
 * 成員函數: attack()
 */
class Soldier:public Person
{
public:
	string m_strCode;
	void attack()
	{
		cout << "fire!!!" << endl;
	}
};

int main(void)
{
    // 創建Soldier對象
	Soldier so;
    // 給對象屬性賦值
    so.m_strName = "Jim";
	so.m_strCode = "592";
    // 打印對象屬性值
	cout << so.m_strName << endl;
	cout << so.m_strCode << endl;
    // 調用對象方法
	so.eat();
	so.attack();

	return 0;
}

3.繼承中同名成員的隱藏(父類與子類的繼承關係中)

當父類和子類中出現同名成員時,使用子類的對象調用這個同名成員,你會發現只調用了子類中的,而父類中的同名成員,彷彿被隱藏了一樣,你無法直接調用,這就是隱藏。

隱藏不僅適用於成員函數,還適用於其它對象成員。

當然,也是有辦法可以訪問父類的同名成員的,那就是使用父類名::,看下面的例子。

例1,訪問同名函數的方法:

// Person爲Soldier的父類
int main(){
    
    Soldier so;
    so.play();			// 調用子類中的play()
    so.Person::play();	// 調用父類中的play(),Person是父類的類名
    
    return 0;
}

例2,訪問同名屬性的方法:

class Persion{
public:
    void play();
protected:
    string code;	// 父類code變量
};

class Soldier:public Person{
public:
    void play();
    void work();
protected:
    int code;		// 子類code變量
}

void Soldier::work(){
    code = 1234;			// 調用子類的code(就是當前的這個類)
    Person::code = "5678";	// 調用父類的code(被當前類繼承的類)
}

隱藏與重載:

如果需要調用父類中的同名成員,一定要使用父類名::。當然,你會覺得這很像重載,所以可能會嘗試將父子類中的同名函數利用參數進行區分。然而這當然是不行的,隱藏和重載是不同的概念,父子類中的同名成員函數是無法通過函數的參數進行區分的,如果不使用父類名::,就無法調用父類中的同名成員函數。

4.isA語法-將子類賦值給父類

**子類(派生類)是可以賦值給父類(基類)**的。

如果這個時候對

// 人是父類,士兵類是子類
int main(){
    // 正確
    Soldier s1;
    Person p1 = s1;
    Person *p2 = &s1;
    // 錯誤
    s1 = p1;
    Soldier *s2 = &p1;
    
    return 0;
}

存儲結構:

  1. 如果將子類對象 初始化/賦值 給父類:

    子類中屬性的數據會賦值給父類的屬性,父類沒有的屬性,會被截斷(即丟失),只賦值父類有的屬性的值(也就是從父類繼承來的那些,非子類獨有的部分)。

    對於父類來說,它只能接收自己擁有的數據成員。

    // 初始化:
    Soldier so;
    Person p = so;
    // 賦值:
    Soldier so;
    Person p;
    p = so;
    

    [外鏈圖片轉存失敗(img-JOKQMFNu-1563352548404)(./images/muke_C++/003.png)]

  2. 如果用父類指針來訪問子類對象,那麼也只能訪問到父類所擁有的數據成員,而無法訪問到子類擁有的數據成員和成員函數。

    [外鏈圖片轉存失敗(img-ZBHD3rs6-1563352548405)(images/muke_C++/004.png)]

isA的常見用法:

既然子類可以賦給父類,那麼我們就可以用父類作爲形參來接收子類。

void fun1(Person *p){// 指針
    ...
}
void fun2(Person &p){// 引用
    ...
}

int main(){
    Person p1;
    Soldier s1;
    
    fun1(&p1); fun2(p1);
    fun1(&s1); fun2(s1);
    
    return 0;
}

需要使用 虛析構函數 的情況:

當存在繼承關係,我們使用父類的指針,去指向堆中的子類的對象,並且我們還想使用父類的指針去釋放這塊內存,這個時候我們就需要 虛析構函數

爲什麼?因爲在這種情況下之後調用父類的析構函數,而子類的析構函數不會被釋放,這樣就會造成內容泄露的問題(在return 0;前打斷點才能看出,因爲程序一旦return退出,所有的內存都會釋放)。

這種情況的具體表現請看下面的代碼:

類聲明:

// Person類聲明
class Person {
public:
    Person(string name = "Person001");// 構造函數賦初值
    ~Person();// 析構函數
    void play();
protected:
    string m_strName;
};

// Soldier類聲明
class Soldier: public Person {
public:
    Soldier(string name = "soldier001",int age = 20);// 構造函數賦初值
    ~Soldier();// 析構函數
    void work();
protected:
    int m_iAge;
};

類實現:

// Person類實現
Person::Person(string name){
    m_strName = name;
    cout << "Person()" << endl;
}
Person::~Person(){
    cout << "~Person()" << endl;
}
void Person::play(){
    cout << "Person -- play()" << endl;
    cout << "m_strName = " << m_strName << endl;
}

// Soldier類實現
Soldier::Soldier(string name, int age){
    // 將初始值賦值給對象
    m_strName = name;
    m_iAge = age;
    cout << "Solidier()" << endl;
}
Soldier::~Soldier(){
    cout << "~Solidier()" << endl;
}
void Soldier::work(){
    cout << "m_strName = " << m_strName << endl;
    cout << "m_iAge = " << m_iAge << endl;
    cout << "Soldier -- work()" << endl;
}

mian.c:

int main() {

    Soldier so;
    Person *p = &so;

    cout<< "==="<< endl;
    p->play();
    cout<< "==="<< endl;

    delete p;
    p = nullptr;

    return 0;
}
/** 輸出:
Person()
Solidier()
===
Person -- play()
m_strName = soldier001
===
~Person()
*/

這裏我們使用指針(堆,需要手動釋放)來進行資源的釋放,可以觀察到,最後並沒有調用~Solidier(),這樣會有內存泄露的問題。

如何解決這個問題,就是使用虛析構函數,這裏要使用修飾符virtual

虛析構函數是可以繼承的,父類中寫了,即使子類中的不寫,也是虛析構函數。但是建議全寫上,起提示作用,這很重要!

// 拿本例來說明
// 在Person和Solidier類的聲明中,在析構函數前面加virtual
// 在實現部分不需要加virtual

轉載自:virtual 在哪些情況要用?

個人總結,virtual當前出現的三種地方:

虛析構函數:當父類指針指向子類對象時,釋放內存時,若不定義virtual,則僅釋放父類內存。

虛繼承:防止多繼承和多重繼承時,一個父類被繼承多次,造成內存空間的浪費。

虛函數:當父類指針指向子類對象時,父類指針可以指向子類方法。

附加例子,在實驗時發現的,關於子類對象相互賦值與拷貝構造函數的調用:

如果在子類寫一個沒有賦值操作的拷貝構造函數,然後用子類對象01給子類對象02賦值,會發現對象02中的屬性如果是繼承於父類的,屬性的值就是父類中賦的初始值,如果是子類中添加的,則值爲空。代碼如下:

關於拷貝構造函數的內容,在筆記第2篇“封裝篇(上)”。

類聲明:

// Person類
class Person {
public:
    // 構造函數賦初值
    Person(string name = "Person001");
    Person(const Person& pe);
protected:
    string m_strName;
};

// Soldier類
class Soldier: public Person {
public:
    // 構造函數賦初值
    Soldier(string name = "soldier001",int age = 20);
    Soldier(const Soldier& soso);
protected:
    int m_iAge;
};

類實現:

// Person類
Person::Person(string name){
    m_strName = name;
    cout << "Person()" << endl;
}
Person::Person(const Person& pe){
    cout << "Copy Person()" << endl;
}

// Soldier類
Soldier::Soldier(string name, int age){
    // 將初始值賦值給對象
    m_strName = name;
    m_iAge = age;
    cout << "Solidier()" << endl;
}
Soldier::Soldier(const Soldier& soso){
    cout << "Copy Solidier()" << endl;
}
void Soldier::work(){
    cout << "m_strName = " << m_strName << endl;
    cout << "m_iAge = " << m_iAge << endl;
    cout << "Soldier -- work()" << endl;
}

main.c

int main() {
    Soldier so;
    Soldier so1 = so;
    cout << "====" << endl;
    so1.work();
    cout << "====" << endl;
    return 0;
}
/* 輸出:
Person()
Solidier()
Person()
Copy Solidier()
====
m_strName = Person001
m_iAge = 0
Soldier -- work()
====
*/

這裏來看下輸出:

  1. Person()Solidier()是因爲 so這個對象 的創建,在調用子類的構造函數前會先調用父類的構造函數。
  2. 之後的Person()Copy Solidier()是因爲 so1的創建,但是這裏Person這個類並沒有調用拷貝構造函數,而是調用了構造函數,也就是在這裏賦上了初始值。Solidier類則是調用了拷貝構造函數。
  3. m_strName = Person001m_iAge = 0,因爲我們手寫了Solidier類的拷貝構造函數,其中又沒有賦值操作,所以這裏m_strName的值是調用Person構造函數時的初始值,而m_iAge是Solidier類中的屬性,所以沒有被賦值,輸出就是0

如果這裏用自動生成的拷貝構造函數(沒有手寫拷貝構造函數的時候就會自動生成),則so對象中屬性的值是會賦值到so1對象的屬性中的。

第一種方法直接初始化的爲啥比第二種少一個Person()構造函數

用soldier賦值給p的這種操作有什麼實際意義?

5.多重繼承和多繼承

多重繼承

當B類從A類派生,C類從B類派生,形成一條繼承鏈,此時成爲多重繼承。

舉個多重繼承的例子:

步兵類繼承士兵類,士兵類繼承人類。

[外鏈圖片轉存失敗(img-PFOCt8QK-1563352548407)(images/muke_C++/005.png)]

class Person{  
};
class Soldier:public Person{
};
class Infantryman:public Soldier{
};

多繼承

多繼承是指一個子類繼承多個父類。多繼承對父類的個數沒有限制,繼承方式可以是公共繼承、保護繼承和私有繼承。

再舉個多繼承的例子:

農民工同時繼承工人類和農民類,工人類和農民類是平行關係。

[外鏈圖片轉存失敗(img-nE3H2F6A-1563352548408)(images/muke_C++/006.png)]

[外鏈圖片轉存失敗(img-F3mlAChm-1563352548410)(images/muke_C++/007.png)]

class Worker{
};
class Farmer{
};
class MigrantWorker:public Worker,public Farmer{  
};

多繼承 下的調用初始化列表使用:

這裏的繼承關係同上,這裏只列出構造函數的代碼:

// 類的實現部分代碼,這裏分別是Farmer、Worker和MigrantWorker的構造函數

Farmer::Farmer(string name){
    m_strName = name;
}

Worker::Worker(string code){
   m_strName  = code;
}

// 通過MigrantWorker的構造函數,調用Farmer和Worker的構造函數,同時將name和code的值傳遞過去
MigrantWorker::MigrantWorker(string name,string code):Farmer(name),Worker(code){
    
}

6.虛繼承與菱形繼承(環狀繼承)

虛繼承的使用:

在繼承的訪問限定符之前添加virtual修飾符。

菱形繼承

在菱形繼承中,類A是類B的父類,類A是類C的父類,類B和類C都是類D的父類,is-A的關係如下:

[外鏈圖片轉存失敗(img-BddrsI6o-1563352548411)(images/muke_C++/008.png)]

可見,菱形繼承中是存在 多重繼承多繼承 的。

(多重繼承:類D繼承類B,類B繼承類A;多繼承:類D繼承類B,也繼承類C)

菱形繼承的數據冗餘問題:

這樣的繼承中會存在類D中存在有2份類A的數據,我們需要使用虛繼承來解決,看下面的例子:

[外鏈圖片轉存失敗(img-hFtdMrvw-1563352548412)(images/muke_C++/009.png)]

// 在類的聲明中:
// 之後會被農民工繼承,所以這個類是虛基類(父類)
class Worker:virtual public Person{
}
class Farmer:virtual public Person{
}

// 繼承上面2個虛基類
class MigrantWorker:public Worker, public Farmer{   
}

解決類的重定義問題:

在菱形繼承中,類的重定義是一定會發生的,我們可以在發生重定義的內部寫一段來避免重定義。

#ifndef XXX
#define XXX

// 在這裏寫:類的聲明

#endif

本篇爲視頻教程筆記,視頻如下:

C++遠征之繼承篇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章