C++核心编程(三)
4.6 繼承
繼承是面向對象三大特性之一
子類擁有父類的一些共性
利用繼承的技術可以減少重復的代碼
4.6.1 繼承方式
語法: class 子類 : 繼承方式 父類{}
子類 也被稱為
派生類
父類 也被稱為基類
示例:
// 繼承
class Animal
{
public:string name; // 動物名稱int age; // 動物年齡
};class Dog
{
public:string leg; // 狗腿
};
**總結:**
繼承的好處:`可以減少重復代碼`
派生類中的成員,包含兩大部分- 一類是從基類繼承過來的,一類是自己的成員
- 從基類繼承繼承過來表現其共性,而新增的成員體現了個性。
4.6.2 繼承的方式
繼承的方式一共有三種
- 公共繼承
- 保護繼承
- 私有繼承
4.6.3 繼承中的對象模型
問題:從父類繼承過來的成員,哪些屬于子類對象中?
示例:
#include<iostream>
using namespace std;
#include <string>// 繼承中的對象模型
class Base
{
public:int m_A;protected:int m_B;private:int m_C;
};class Son : public Base
{
public:int m_D;};// 利用VS提供的開發人員命令提示工具查看對象模型
// 在文件路徑下操作:cl /d1 reportSingleClassLayout class_namevoid test01()
{// 最終結果是 16cout << "sizeof of Son = " << sizeof(Son) << endl;// 結論:父類中所有的非靜態的成員屬性都會被子類繼承下去// 父類中私有成員屬性 是被編譯器給隱藏了,因此訪問不到,但是確實被繼承了
}int main()
{test01();return 0;
}
結論: 父類中所有的非靜態的成員屬性都會被子類繼承下去,父類中私有成員屬性是被編譯器給隱藏了,因此訪問不到,但是確實被繼承了
4.6.4 繼承中的構造和析構順序
子類繼承父類后,當創建子類對象,也會調用父類的構造函數
問題: 父類和子類中的構造函數和析構函數順序呢?
示例:
#include<iostream>
using namespace std;
#include <string>// 繼承中的對象模型
class Base
{public:Base(){cout << "Base 構造函數執行了" << endl;}~Base(){cout << "Base 析構函數執行了" << endl;}
public:int m_A;};class Son : public Base
{
public:Son(){cout << "Son 構造函數執行了" << endl;}~Son(){cout << "Son 析構函數執行了" << endl;}};void test01()
{Son son;
}int main()
{test01();return 0;
}
運行結果:
結論: 先構造父類,然后構造兒子,析構則是相反。
4.6.5 繼承中同名成員處理方式
問題:當子類與父類出現同名的成員,如何通過子類對象,訪問到子類或者父類中的同名數據呢?
- 訪問子類同名成員 直接訪問即可
- 訪問父類同名成員 需要加作用域
示例:
#include<iostream>
using namespace std;
#include <string>// 繼承中同名成員處理
class Base
{public:Base(){m_A = 100;}void func(){cout << "父類中的函數調用" << endl;}void pr(){cout << "父類中的pr()函數調用" << endl;}
public:int m_A;};class Son : public Base
{
public:Son(){m_A = 200;}void func(){cout << "子類中的函數調用" << endl;}public:int m_A;
};// 同名的成員屬性
void test01()
{Son son;cout << "子類中 m_A = " << son.m_A << endl;cout << "父類中 m_A = " << son.Base::m_A << endl;
}// 同名的成員函數處理
void test02()
{Son son;son.func();son.pr();son.Base::func();// 如果子類中出現和父類同名的成員函數,子類的同名成員會影藏掉父類中所有同名的成員函數:重載的也被影藏,仍然要添加作用域
}int main()
{test01();test02();return 0;
}
運行結果:
總結:
- 子類對象可以直接訪問到子類中的同名成員
- 子類對象加作用域可以訪問到父類中的同名成員,示例:
son.Base::func(); - 當子類與父類擁有同名的成員函數,子類會隱藏父類中的同名成員函數,加作用域可以訪問到父類中同名函數
4.6.6 繼承同名靜態成員處理方式
問題:繼承中同名的靜態成員在子類對象如何進行訪問?
靜態成員和非靜態成員出現同名,處理方式一致
- 訪問子類同名成員 直接訪問即可
- 訪問父類同名成員 需要加作用域
示例:
#include<iostream>
using namespace std;
#include <string>// 繼承中同名靜態成員處理
class Base
{public:static void func(){cout << "父類中 func() 的調用" << endl;}public:static int m_A;};
// 靜態成員初始化
int Base::m_A = 100;class Son : public Base
{
public:static void func(){cout << "子類中 func() 的調用" << endl;}public:static int m_A;
};int Son::m_A = 200;// 同名的靜態成員屬性
void test01()
{Son son;cout << "通過對象訪問:" << endl;cout << "子類中 m_A = " << son.m_A << endl;cout << "父類中 m_A = " << son.Base::m_A<< endl;cout << "通過類名訪問:" << endl;cout << "子類中 m_A = " << Son::m_A << endl; // 此處的雙冒號代表通過類名訪問cout << "父類中 m_A = " << Son::Base::m_A << endl; // 此處雙冒號代表父類作用域下
}// 同名的靜態成員函數
void test02()
{Son son;cout << "通過對象訪問:" << endl;son.func();son.Base::func();cout << "通過類名訪問:" << endl;Son::func();Son::Base::func();
}int main()
{test01();test02();return 0;
}
總結: 同名靜態成員處理方式和非靜態處理方式一樣,只不過有兩種訪問的方式(通過對象和通過類),此外,如果函數重載,會隱藏父類中的同名函數,需要添加作用域才可以訪問。
4.6.7 多繼承語法
C++允許一個類繼承多個類
語法:class 子類: 繼承方式 父類1, 繼承方式 父類二 ...{}
多繼承可能會引發父類中有同名成員出現,需要加作用域區分
C++實際開發中不建議使用多繼承
示例:
#include<iostream>
using namespace std;
#include <string>class Base1
{
public:int m_A = 100;
};class Base2
{
public:int m_A = 200;
};class Son : public Base1, public Base2
{public:int m_B = 300;
};void test01()
{Son son;cout << "子類中 m_B = " << son.m_B << endl;cout << "父類Base1中 m_A = " << son.Base1::m_A<< endl;cout << "父類Base2中 m_A = " << son.Base2::m_A << endl;
}int main()
{test01();return 0;
}
運行結果:
總結: 多繼承中如果父類中出現了同名的情況,訪問需要使用作用域區別
4.6.8 菱形繼承
概念:
? 兩個派生類繼承同一個基類
? 又有某個類同時繼承兩個派生類
? 這種繼承被稱為菱形繼承,或者鉆石繼承
典型的菱形繼承案例:
菱形繼承問題:
- 羊繼承了動物的數據,駝同樣繼承了動物的數據,當草泥馬使用數據就會產生二義性
- 草泥馬繼承自動物的數據繼承了兩份,但是這個數據只需要一份就可以。
問題解決:使用虛繼承
示例:
#include<iostream>
using namespace std;
#include <string>// 動物類
class Animal
{public:public:int age = 100;};// 利用虛繼承 解決菱形繼承的問題
// 繼承之前 加上關鍵字 virtual 變為虛繼承
// Animal 被稱為 虛基類
// 羊類
class Sheep: virtual public Animal
{};// 駝類
class Camel : virtual public Animal
{};// 羊駝類 alpaca
class Alpaca: public Sheep, public Camel
{};void test01()
{Alpaca alpaca;alpaca.Sheep::age = 18;alpaca.Camel::age = 20;// 當出現菱形繼承的時候,兩個父類擁有相同的數據,需要加以作用域區分cout << "alpaca.Sheep::age = " << alpaca.Sheep::age << endl;cout << "alpaca.Camel::age = " << alpaca.Camel::age << endl;cout << "alpaca.age = " << alpaca.age << endl;cout << "sizeof Sheep = " << sizeof(Sheep) << endl;cout << "sizeof Camel = " << sizeof(Camel) << endl;cout << "sizeof Alpaca = " << sizeof(Alpaca) << endl;}int main()
{test01();return 0;
}
運行結果:
4.7 多態
4.7.1 多態的基本概念
多態是C++面向對象三大特性之一
多態分為兩類
- 靜態多態:函數重載,和運算符重載屬于靜態多態,復用函數名
- 動態多態:派生類和虛函數實現運行時多態
靜態多態和動態多態區別:
- 靜態多態的函數地址早綁定 - 編譯階段確定函數地址
- 動態多態的函數地址晚綁定 - 運行階段確定函數地址
下面通過案例進行講解多態:
#include<iostream>
using namespace std;
#include <string>// 動物類
class Animal
{public:// 關鍵字 virtual 使函數地址在運行時綁定virtual void speak(){cout << "父類對應為虛函數:動物在說話" << endl;}void bark(){cout << "父類對應為普通:動物在咬" << endl;}};class Cat: public Animal
{
public:void speak(){cout << "父類對應為虛函數:小貓在說話" << endl;}void bark(){cout << "父類對應為普通:函數小貓在咬" << endl;}
};// 執行說話的函數:父類對應函數為虛函數,
// 函數地址晚綁定, 在運行階段確定函數地址
void doSpeak(Animal &animal) // Animal& animal = cat
{animal.speak();
}// 執行說話的函數:父類對應函數為普通函數
// 地址早幫定,在編譯階段確定函數地址
void doBark(Animal &animal)
{animal.bark();
}void test01()
{Cat cat;doSpeak(cat);doBark(cat);
}int main()
{test01();return 0;
}
執行結果:
動態多態滿足條件
- 有繼承關系
- 子類重寫父類的虛函數
動態多態的使用:
父類的指針或者引用 執行子類對象。
Animal& animal = cat;
animal.speak()
重寫: 函數返回值類型、函數名、參數列表完全一致稱為重寫
虛函數底層:
4.7.2 多態案例–計算器
案例描述:
分別利用普通寫法和多態技術,設計實現兩個操作數進行運算的計算器類
多態的優點:
- 代碼組織結構清晰
- 可讀性強
- 利于前期和后期的擴展以及維護
傳統方法示例:
// 普通的寫法
class Calculator
{
public:int a;int b;
public:int getResult(string oper){if (oper == "+"){return a + b;}else if (oper == "-"){return a - b;}else if (oper == "*"){return a * b;}else if (oper == "/"){return a / b;}// 如果想擴展新的功能,需要修改源碼// 在真實的開發中,提供開閉原則// 開閉原則:對擴展進行開放,對修改進行關閉}
};void test01()
{// 創建一個Calculate對象Calculator c;c.a = 10;c.b = 20;cout << c.a << "+" << c.b << " = " << c.getResult("+") << endl;cout << c.a << "-" << c.b << " = " << c.getResult("-") << endl;cout << c.a << "*" << c.b << " = " << c.getResult("*") << endl;cout << c.a << "/" << c.b << " = " << c.getResult("/") << endl;
}int main()
{test01();return 0;
}
運行結果:
利用多態的示例:
// 利用多態實現計算器
class AbstractCaluclator
{
public:virtual int getResult(){return 0;}int a;int b;
};// 設計一個加法計算器類
class AddCalculator : public AbstractCaluclator
{
public:int getResult(){return a + b;}
};// 設計一個減法計算器類
class SubCalculator : public AbstractCaluclator
{
public:int getResult(){return a - b;}
};// 設計一個乘法計算器類
class MulCalculator : public AbstractCaluclator
{
public:int getResult(){return a * b;}
};// 設計一個除法計算器類
class DivideCalculator : public AbstractCaluclator
{
public:int getResult(){return a / b;}
};void test02()
{// 加法AbstractCaluclator* c = new AddCalculator();c->a = 10;c->b = 20;cout << c->a << "+" <<c->b << " = " << c->getResult() << endl;// 使用完畢,記得銷毀delete c; // 銷毀的只是指正指向的堆區的數據// 減法c = new SubCalculator;c->a = 10;c->b = 20;cout << c->a << "-" << c->b << " = " << c->getResult() << endl;// 使用完畢,記得銷毀delete c; // 銷毀的只是指正指向的堆區的數據// 乘法c = new MulCalculator;c->a = 10;c->b = 20;cout << c->a << "*" << c->b << " = " << c->getResult() << endl;// 使用完畢,記得銷毀delete c; // 銷毀的只是指正指向的堆區的數據
}int main()
{test02();return 0;
}
運行結果:
利用多態的好處總結:
- 組織結構清晰
- 可讀性強
- 對于前期和后期的可擴展性以及可維護性高
4.7.3 純虛函數和抽象類
在多態中,通常父類中虛函數的實現是毫無意義的,主要都是調用子類重寫的內容,因此可以將虛函數改為純虛函數
純虛函數語法:virtual 返回值類型 函數名 ( 參數列表) = 0;
當類中有了純虛函數,這個類也被稱為抽象類
抽象類特點:
- 無法實例化對象
- 子類必須重寫抽象類中的純虛函數,否則也屬于抽象類
示例:
// 純虛函數和抽象類
class Base
{
public:// 純虛函數:本類只要有一個純虛函數,此類就是抽象類,無法實例化virtual void func() = 0;
};class Son :public Base
{
public:// 對父類中純虛函數進行重寫:否則無法實例化對象 virtual 關鍵字可以不寫void func(){cout << "哈哈哈哈" << endl;}
};void test01()
{Son s;s.func();
}int main()
{test01();return 0;
}
4.7.4 多態案例二
案例:煮茶和煮咖啡
示例:
class AbstractDrinking
{
public:// 煮水virtual void boil() = 0;// 沖泡virtual void brew() = 0;// 倒入杯中virtual void pourInCup() = 0;// 加入輔料virtual void putSomething() = 0;// 制作飲品void makeDrink(){boil();brew();pourInCup();putSomething();}};// 制作咖啡
class Coffee : public AbstractDrinking
{// 煮水virtual void boil(){cout << "煮水" << endl;}// 沖泡virtual void brew(){cout << "沖泡咖啡" << endl;}// 倒入杯中virtual void pourInCup(){cout << "倒入杯中" << endl;}// 加入輔料virtual void putSomething(){cout << "倒入糖" << endl;}
};// 泡茶
class Tea : public AbstractDrinking
{// 煮水virtual void boil(){cout << "煮水" << endl;}// 沖泡virtual void brew(){cout << "沖泡咖啡" << endl;}// 倒入杯中virtual void pourInCup(){cout << "倒入杯中" << endl;}// 加入輔料virtual void putSomething(){cout << "倒入枸杞" << endl;}
};// 制作函數
void doWork(AbstractDrinking* abs)
{abs->makeDrink();delete abs; // 釋放
}void test01()
{cout << "泡咖啡****************" << endl;// 制作咖啡doWork(new Coffee);cout << endl << "泡茶****************" << endl;// 泡茶doWork(new Tea);
}int main()
{test01();return 0;
}
運行結果
4.7.5 虛析構和純虛構
多態使用時,如果子類中有屬性開辟到堆區,那么父類指針在釋放時無法調用到子類的析構代碼
解決方式:將父類中的析構函數改為虛析構或者純虛構
虛析構和純虛析構共性:
- 可以解決父類指針釋放子類對象
- 都需要有具體的函數實現
虛析構和純析構區別
- 如果是純虛析構,該類屬于抽象類,無法實例化對象
虛析構語法:virtual ~類名(){}
純虛析構語法:virtual ~類名() = 0
示例:
#include<iostream>
using namespace std;
#include <string>// 虛析構和純虛析構
class Animal
{
public:Animal(){cout << "Animal 構造函數調用" << endl;}// 利用虛析構解決子類對象釋放不干凈的問題/*virtual ~Animal(){cout << "Animal 析構函數調用" << endl;}*/// 純虛析構 也需要代碼實現,但并不是在子類中實現// 有純虛析構,則是抽象類,不可實例化virtual ~Animal() = 0;// 純虛函數virtual void speak() = 0;
};// Animal 中純虛析構實現
Animal::~Animal()
{cout << "Animal中純虛析構函數調用" << endl;
}class Cat : public Animal
{
public:string *name;public:Cat(string name){cout << "Cat的構造函數調用" << endl;this->name = new string(name);}~Cat(){if (name != NULL){cout << "Cat 析構函數調用" << endl;delete name;name = NULL;}}virtual void speak(){cout << *this->name << "小貓在說話" << endl;}
};void test01()
{Animal* animal = new Cat("Tom");animal->speak();// 父類指針在析構的時候,不會調用子類中析構函數,導致子類堆區數據有內存泄露情況delete animal;
}int main()
{test01();return 0;
}
運行結果:
總結:
- 虛析構活純虛析構就是用來解決通過父類指針釋放子類對象的問題
- 如果子類中沒有堆區數據,就可以不寫虛析構或純虛析構
- 擁有純虛析構,此類為抽象類
總結
以上是生活随笔為你收集整理的C++核心编程(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++核心编程(一)
- 下一篇: C++核心编程(四)--文件操作