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
繼承方法來說明:
- 如果父類中某成員爲
public
,子類通過public
方式將其繼承,該成員的訪問權限不變依然爲public
。 - 如果父類中某成員爲
protected
,子類通過public
方式將其繼承,該成員的訪問權限變爲protected
。 - 如果父類中某成員爲
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;
}
存儲結構:
-
如果將子類對象 初始化/賦值 給父類:
子類中屬性的數據會賦值給父類的屬性,父類沒有的屬性,會被截斷(即丟失),只賦值父類有的屬性的值(也就是從父類繼承來的那些,非子類獨有的部分)。
對於父類來說,它只能接收自己擁有的數據成員。
// 初始化: Soldier so; Person p = so; // 賦值: Soldier so; Person p; p = so;
[外鏈圖片轉存失敗(img-JOKQMFNu-1563352548404)(./images/muke_C++/003.png)]
-
如果用父類指針來訪問子類對象,那麼也只能訪問到父類所擁有的數據成員,而無法訪問到子類擁有的數據成員和成員函數。
[外鏈圖片轉存失敗(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()
====
*/
這裏來看下輸出:
Person()
和Solidier()
是因爲so
這個對象 的創建,在調用子類的構造函數前會先調用父類的構造函數。- 之後的
Person()
和Copy Solidier()
是因爲so1
的創建,但是這裏Person這個類並沒有調用拷貝構造函數,而是調用了構造函數,也就是在這裏賦上了初始值。Solidier類則是調用了拷貝構造函數。 m_strName = Person001
和m_iAge = 0
,因爲我們手寫了Solidier類的拷貝構造函數,其中又沒有賦值操作,所以這裏m_strName
的值是調用Person構造函數時的初始值,而m_iAge
是Solidier類中的屬性,所以沒有被賦值,輸出就是0
。
如果這裏用自動生成的拷貝構造函數(沒有手寫拷貝構造函數的時候就會自動生成),則so對象中屬性的值是會賦值到so1對象的屬性中的。
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
本篇爲視頻教程筆記,視頻如下: