目錄
前言:
一、派生類的默認成員函數專題
1.1實現一個不可繼承類實現
1.1.1 間接實現:【C++98】構造函數私有的類不能被繼承
1.1.2 直接實現:final關鍵字修改基類
1.1.3 代碼實現
4.4.4 final關鍵字
二、繼承體系中的友元關係
2.1 友元與繼承的關係特性
2.2 解決方案
2.3 實戰
2.3.1 正確代碼演示
2.3.2 前置聲明的必要性
2.3.3 友元關係不能繼承
三、靜態成員在繼承中的特性
3.1 靜態成員共享機制:父子共用同一份
3.2 靜態與非靜態成員對比
3.2.1 非靜態成員:實例獨立
3.2.2 靜態成員:類間共享
3.3 實踐出真知:靜態成員繼承實踐案例
四、單繼承 vs 多繼承(以及菱形繼承問題詳解)
4.1 單繼承 vs 多繼承
4.1.1 概念對比
4.1.2 實戰
4.2 菱形繼承問題詳解
4.2.1 菱形繼承的概念
4.2.2 菱形繼承的數據冗餘與二義性問題
4.2.3 虛繼承解決方案
4.2.4 虛繼承機制與virtual關鍵字
4.2.5 菱形繼承的問題
4.2.6 實戰
4.2.7 可以設計出多繼承,不建議設計出菱形繼承
4.3 IO庫中的菱形虛擬繼承
4.4 多繼承中的指針偏移問題
4.4.1 題目
4.4.2 答案解析
五、繼承與組合設計模式對比
5.1 基本概念:is-a vs has-a
5.2 繼承與組合關係對比
5.3 實踐
5.4 繼承 vs 組合
5.4.1 白盒複用與黑盒複用
8.4.2 軟件設計中的選擇策略
8.4.3 模塊
8.4.4繼承和組合哪個更好?
完整代碼演示
結尾
前言:
在面向對象編程的世界裏,“避免重複” 與 “靈活擴展” 是開發者始終追求的目標,而 C++ 的繼承機制正是實現這兩個目標的核心工具。它讓我們能夠從已有的類(基類)中 “繼承” 成熟的成員變量與成員函數,無需重新編寫重複代碼;同時又能在新類(派生類)中添加專屬成員、重寫原有函數,讓類的功能隨需求自然延伸。無論是模擬現實世界中 “動物與貓、狗” 的層級關係,還是開發中 “基礎組件與定製組件” 的複用場景,繼承都為代碼的組織與維護提供了清晰的邏輯框架。理解繼承,便是掌握 C++ 面向對象編程的關鍵一步
一、派生類的默認成員函數專題
1.1實現一個不可繼承類實現
1.1.1 間接實現:【C++98】構造函數私有的類不能被繼承
基類的構造函數私有,派生類的構成必須調用基類的構造函數,但是基類的構成函數私有化以後,派生類看不見就不能調用了,那麼派生類就無法實例化出對象
運行結果:
這裏必須調用基類的構造,但是基類這裏是私有的,看不見,所以就不能再調用了。
1.1.2 直接實現:final關鍵字修改基類
C++11新增了一個final關鍵字,final修改基類,派生類就不能繼承了
1.1.3 代碼實現
//設計一個不能被繼承的類
//class Base
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
//構造函數私有的類不能被繼承
Base()
{}
};
class Derive :Base
{
};
int main()
{
Derive d;
return 0;
}
4.4.4 final關鍵字
在本文博主不展開講,下篇博客,博主會介紹C++進階中又一個重要的模塊——【多態】,在【多態】中,博主會介紹兩個涉及到【多態】中的重寫相關知識點的關鍵字:override和final。
也就是説,final充當了兩個作用
(1)直接實現一個不能被繼承的類(【繼承】篇涉及知識點);
(2)不讓重寫基類虛函數(【多態】(下一篇博客)篇即將涉及的知識點)。
二、繼承體系中的友元關係
2.1 友元與繼承的關係特性
友元關係不能繼承。
也就是説基類友元不能訪問派生類私有和保護成員
2.2 解決方案
把派生類也變成基類的友元的友元即可
2.3 實戰
2.3.1 正確代碼演示
class Student;
class Person
{
public:
//友元關係不能被子類繼承
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; // 學號
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
// 編譯報錯:error C2248: “Student::_stuNum”: ⽆法訪問 protected 成員
// 解決⽅案:Display也變成Student 的友元即可
Display(p, s);
return 0;
}
運行結果:
這段代碼是能順利運行的,但是,我們看下面這段代碼
class Person
{
// 友元關係不能被子類繼承
friend void Display(const Person& p, const Student& s);
public:
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 學號
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
// 編譯報錯:error C2248:“Student::_stuNum”:無法訪問 protected 成員
// 解決方案:Display也變成Student 的友元即可
Display(p, s);
return 0;
}
2.3.2 前置聲明的必要性
不加前置聲明會報下面的錯
2.3.3 友元關係不能繼承
因為友元關係不能繼承,因此我們要給派生類也變成基類友元的友元
三、靜態成員在繼承中的特性
3.1 靜態成員共享機制:父子共用同一份
基類定義了static靜態成員,則整個繼承體系裏面只有一個這樣的成員。無論派生出多少個派生類,都只有一個static成員實例
3.2 靜態與非靜態成員對比
3.2.1 非靜態成員:實例獨立
非靜態成員的繼承是父類和子類各一份,地址不一樣
3.2.2 靜態成員:類間共享
靜態成員的繼承是父類和子類共用同一份,地址也一樣
3.3 實踐出真知:靜態成員繼承實踐案例
class Person
{
public:
string _name;
static int _count;//存放在靜態區
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
// 這⾥的運⾏結果可以看到⾮靜態成員_name的地址是不⼀樣的
// 説明派⽣類繼承下來了,⽗派⽣類對象各有⼀份
cout << &p._name << endl;
cout << &s._name << endl;
// 這⾥的運⾏結果可以看到靜態成員_count的地址是⼀樣的
// 説明派⽣類和基類共⽤同⼀份靜態成員
cout << &p._count << endl;
cout << &s._count << endl;
// 公有的情況下,⽗派⽣類指定類域都可以訪問靜態成員
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
運行結果:
四、單繼承 vs 多繼承(以及菱形繼承問題詳解)
事先説明:多繼承是個大坑!!!
4.1 單繼承 vs 多繼承
4.1.1 概念對比
4.1.2 實戰
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person //virtual虛擬繼承在腰部
{
protected:
int _num; //學號
};
class Teacher : virtual public Person
{
protected:
int _id; // 職⼯編號
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修課程
};
4.2 菱形繼承問題詳解
4.2.1 菱形繼承的概念
4.2.2 菱形繼承的數據冗餘與二義性問題
基類數據越多,這兩個問題越嚴重
數據冗餘:如下圖所示,Person有兩個
二義性:訪問不明確~>指定類域勉強解決
4.2.3 虛繼承解決方案
菱形繼承——多繼承延伸的坑
多繼承不是問題,多繼承實現的菱形繼承才是問題
因此設計了“菱形虛擬繼承”來解決,下面我們會介紹虛繼承
4.2.4 虛繼承機制與virtual關鍵字
關鍵詞virtual加在腰部位置,如下圖所示
都加上virtual可不可以?——當然不行。
換個説法,藥能多吃嗎?會影響底層的空間模型,能編譯通過但底層空間會亂
虛繼承太複雜了,無論是使用還是底層,都太複雜
不要玩菱形繼承!!!當然,菱形繼承也是有應用的,庫裏面的IO庫就是搞成菱形繼承的,IO庫的使用會專門在IO庫講
4.2.5 菱形繼承的問題
4.2.6 實戰
class Person
{
public:
Person(const char* name)
:_name(name)
{ }
string _name; // 姓名
};
class Student : virtual public Person
{
public:
Student(const char* name, int num)
:Person(name)
, _num(num)
{
}
protected:
int _num; //學號
};
class Teacher : virtual public Person
{
public:
Teacher(const char* name, int id)
:Person(name)
, _id(id)
{
}
protected:
int _id; // 職⼯編號
};
// 不要去玩菱形繼承
class Assistant : public Student, public Teacher
{
public:
Assistant(const char* name1, const char* name2, const char* name3)
:Person(name3)
,Student(name1, 1)
,Teacher(name2, 2)
{
}
protected:
string _majorCourse; // 主修課程
};
int main()
{
// 思考⼀下這⾥a對象中_name是"張三", "李四", "王五"中的哪⼀個?
Assistant a("張三", "李四", "王五");
return 0;
}
運行結果:
4.2.7 可以設計出多繼承,不建議設計出菱形繼承
我們可以設計出多繼承,但是不建議設計出菱形繼承,因為菱形虛擬繼承以後,無論是使用還是底層都會複雜很多。當然有多繼承語法支持,就一定存在會設計出菱形繼承,像Java是不支持多繼承的,就避開了菱形繼承
4.3 IO庫中的菱形虛擬繼承
4.4 多繼承中的指針偏移問題
4.4.1 題目
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
下面説法正確的是( )
A. p1 == p2 == p3
B. p1 < p2 < p3
C. p1 == p3 != p2
D. p1 != p2 != p3
4.4.2 答案解析
正確答案:C
五、繼承與組合設計模式對比
5.1 基本概念:is-a vs has-a
- public繼承是一種is-a的關係。也就是説每個派生類對象都是一個基類對象
- 組合是一種has-a的關係。假設B組合了A,每個B對象中都有一個A對象
- 繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的複用通常被稱為白箱複用(white-boxreuse)。術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節對派生類可見。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關係很強,耦合度高。
- 對象組合是類繼承之外的另一種複用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種複用風格被稱為黑箱複用(black-boxreuse),因為對象的內部細節是不可見的。對象只以“黑箱”的形式出現。組合類之間沒有很強的依賴關係,耦合度低。優先使用對象組合有助於你保持每個類被封裝
- 先使用組合,而不是繼承。實際儘量多去用組合,組合的耦合度低,代碼維護性好。不過也不太那麼絕對,類之間的關係就適合繼承(is-a)那就用繼承,另外要實現多態,也必須要繼承。類之間的關係既適合用繼承(is-a)也適合組合(has-a),就用組合
5.2 繼承與組合關係對比
5.3 實踐
// 繼承和組合
// Tire(輪胎)和Car(⻋)更符合has-a的關係
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car {
protected:
string _colour = "白色"; // 顏色
string _num = "陝ABIT00"; // 車牌號
Tire _t1; // 輪胎
Tire _t2; // 輪胎
Tire _t3; // 輪胎
Tire _t4; // 輪胎
};
class BMW : public Car {
public:
void Drive() { cout << "好開-操控" << endl; }
};
// Car和BMW/Benz更符合is-a的關係
class Benz : public Car {
public:
void Drive() { cout << "好坐-舒適" << endl; }
};
template
class vector
{};
// stack和vector的關係,既符合is-a,也符合has-a
template
class stack : public vector
{};
template
class stack
{
public:
vector _v;
};
int main()
{
return 0;
}
is-a用繼承,has-a用組合
// 繼承和組合
// Tire(輪胎)和Car(⻋)更符合has-a的關係
// 輪胎類
class Tire {
protected:
string _brand = "Michelin";
size_t _size = 17;
};
// 汽車基類
class Car {
protected:
string _colour = "白色";
string _num = "陝ABIT00";
Tire _tires[4]; // 使用數組更合適
};
// 派生類
class BMW : public Car {
public:
void Drive() { cout << "好開-操控" << endl; }
};
class Benz : public Car {
public:
void Drive() { cout << "好坐-舒適" << endl; }
};
// 正確的stack實現
template
class Stack {
private:
vector _v; // 組合關係
public:
void push(const T& x) { _v.push_back(x); }
void pop() {
if (!_v.empty())
_v.pop_back();
}
T& top() {
if (!_v.empty())
return _v.back();
throw std::out_of_range("Stack is empty");
}
bool empty() const { return _v.empty(); }
size_t size() const { return _v.size(); }
};
int main() {
BMW bmw;
bmw.Drive();
Stack s;
s.push(1);
s.push(2);
cout << s.top() << endl; // 輸出2
return 0;
}
5.4 繼承 vs 組合
5.4.1 白盒複用與黑盒複用
白盒測試:更加難,一般由研發人員寫並且測試,看得見、透明——保護、私有都可使用;
黑盒測試:看不見,不透明;
白盒 / 黑盒好壞的依據是從軟件設計角度出發的
8.4.2 軟件設計中的選擇策略
高內聚,低耦合——可維護性(其中一個修改,另一個不受影響)
8.4.3 模塊
打成一個個模塊,哪個出問題改哪個,不受影響
組件:靜態庫、動態庫——不可執行的二進制文件
1、編譯時間降低;
2、看不到源碼(二進制編譯)
8.4.4繼承和組合哪個更好?
實踐的角度:優先使用組合;既符合繼承也符合組合,我們使用組合;但是要注意:是“優先使用組合”,不是必須使用,但是像多態這些需要繼承的地方還是要用繼承
完整代碼演示
#include
using namespace std;
//class Student;
//
//class Person
//{
//public:
// //友元關係不能被子類繼承
// friend void Display(const Person& p, const Student& s);
//protected:
// string _name; // 姓名
//};
//
//class Student : public Person
//{
// friend void Display(const Person& p, const Student& s);
//protected:
// int _stuNum; // 學號
//};
//void Display(const Person& p, const Student& s)
//{
// cout << p._name << endl;
// cout << s._stuNum << endl;
//}
//
//int main()
//{
// Person p;
// Student s;
// // 編譯報錯:error C2248: “Student::_stuNum”: ⽆法訪問 protected 成員
// // 解決⽅案:Display也變成Student 的友元即可
// Display(p, s);
//
// return 0;
//}
//class Person
//{
//public:
// string _name;
// static int _count;//存放在靜態區
//};
//
//int Person::_count = 0;
//
//class Student : public Person
//{
//protected:
// int _stuNum;
//};
//
//int main()
//{
// Person p;
// Student s;
// // 這⾥的運⾏結果可以看到⾮靜態成員_name的地址是不⼀樣的
// // 説明派⽣類繼承下來了,⽗派⽣類對象各有⼀份
// cout << &p._name << endl;
// cout << &s._name << endl;
// // 這⾥的運⾏結果可以看到靜態成員_count的地址是⼀樣的
// // 説明派⽣類和基類共⽤同⼀份靜態成員
// cout << &p._count << endl;
// cout << &s._count << endl;
// // 公有的情況下,⽗派⽣類指定類域都可以訪問靜態成員
// cout << Person::_count << endl;
// cout << Student::_count << endl;
//
// return 0;
//}
//菱形繼承
//菱形繼承是多繼承的⼀種特殊情況。菱形繼承的問題,從下⾯的對象成員模型構造,可以
//看出菱形繼承有數據冗餘和⼆義性的問題,在Assistant的對象中Person成員會有兩份。⽀持多繼承就
//⼀定會有菱形繼承,像Java就直接不⽀持多繼承,規避掉了這⾥的問題,所以實踐中我們也是不建議
//設計出菱形繼承這樣的模型的
//class Person
//{
//public:
// string _name; // 姓名
//};
////
//class Student : virtual public Person //virtual虛擬繼承在腰部
//{
//protected:
// int _num; //學號
//};
//class Teacher : virtual public Person
//{
//protected:
// int _id; // 職⼯編號
//};
//
//class Assistant : public Student, public Teacher
//{
//protected:
// string _majorCourse; // 主修課程
//};
//
//int main()
//{
// // 編譯報錯:error C2385: 對“_name”的訪問不明確
// Assistant a;
// a._name = "peter";
// // 需要顯⽰指定訪問哪個基類的成員可以解決⼆義性問題,但是數據冗餘問題⽆法解決
// a.Student::_name = "xxx";
// a.Teacher::_name = "yyy";
//
// return 0;
//}
//class Person
//{
//public:
// Person(const char* name)
// :_name(name)
// { }
// string _name; // 姓名
//};
//
//class Student : virtual public Person
//{
//public:
// Student(const char* name, int num)
// :Person(name)
// , _num(num)
// {
// }
//protected:
// int _num; //學號
//};
//
//class Teacher : virtual public Person
//{
//public:
// Teacher(const char* name, int id)
// :Person(name)
// , _id(id)
// {
// }
//protected:
// int _id; // 職⼯編號
//};
//
//// 不要去玩菱形繼承
//class Assistant : public Student, public Teacher
//{
//public:
// Assistant(const char* name1, const char* name2, const char* name3)
// :Person(name3)
// ,Student(name1, 1)
// ,Teacher(name2, 2)
// {
// }
//protected:
// string _majorCourse; // 主修課程
//};
//
//int main()
//{
// // 思考⼀下這⾥a對象中_name是"張三", "李四", "王五"中的哪⼀個?
// Assistant a("張三", "李四", "王五");
// return 0;
//}
// 繼承和組合
// Tire(輪胎)和Car(⻋)更符合has-a的關係
// 輪胎類
//class Tire {
//protected:
// string _brand = "Michelin";
// size_t _size = 17;
//};
//
//// 汽車基類
//class Car {
//protected:
// string _colour = "白色";
// string _num = "陝ABIT00";
// Tire _tires[4]; // 使用數組更合適
//};
//
//// 派生類
//class BMW : public Car {
//public:
// void Drive() { cout << "好開-操控" << endl; }
//};
//
//class Benz : public Car {
//public:
// void Drive() { cout << "好坐-舒適" << endl; }
//};
//
//// 正確的stack實現
//template
//class Stack {
//private:
// vector _v; // 組合關係
//public:
// void push(const T& x) { _v.push_back(x); }
// void pop() {
// if (!_v.empty())
// _v.pop_back();
// }
// T& top() {
// if (!_v.empty())
// return _v.back();
// throw std::out_of_range("Stack is empty");
// }
// bool empty() const { return _v.empty(); }
// size_t size() const { return _v.size(); }
//};
//
//int main() {
// BMW bmw;
// bmw.Drive();
//
// Stack s;
// s.push(1);
// s.push(2);
// cout << s.top() << endl; // 輸出2
//
// return 0;
//}
//實現多態的兩個重要條件
// 必須是基類的指針或者引⽤調⽤虛函數
// 被調⽤的函數必須是虛函數,並且完成了虛函數重寫 / 覆蓋。
// 虛函數的重寫/覆蓋:派⽣類中有⼀個跟基類完全相同的虛函數
// (即派⽣類虛函數與基類虛函數的返回值類型、函數名字、參數列表完全相同),
// 稱派⽣類的虛函數重寫了基類的虛函數
//class Person
//{
//public:
// virtual void BuyTicket()//虛函數
// {
// cout << "買票-全價" << endl;
// }
//};
//
//class Student :public Person
//{
//public:
// virtual void BuyTicket()
// {
// cout << "買票-打折" << endl;
// }
//};
//
//void Func(Person* ptr)
//{
// // 這⾥可以看到雖然都是Person指針Ptr在調⽤BuyTicket
// // 但是跟ptr沒關係,⽽是由ptr指向的對象決定的。
// ptr->BuyTicket();
//}
//int main()
//{
// Person ps;
// Student st;
//
// Func(&ps);
// Func(&st);
//
// return 0;
//}
//class Animal
//{
//public:
// virtual void talk() const
// { }
//};
//
//class Dog : public Animal
//{
//public:
// //重寫實現,可以不加virtual
// virtual void talk() const
// {
// std::cout << "汪汪" << std::endl;
// }
//};
//
//class Cat : public Animal
//{
//public:
// virtual void talk() const
// {
// std::cout << "(>^ω^<)喵" << std::endl;
// }
//};
//
////必須是指針或者引用
//void letsHear(const Animal& animal)
//{
// animal.talk();
//}
//
//int main()
//{
// Cat cat;
// Dog dog;
//
// letsHear(cat);
// letsHear(dog);
//
// return 0;
//}
//class A {};
//class B : public A {};
//
//class Person {
//public:
// //協變(瞭解)
// // 派⽣類重寫基類虛函數時,與基類虛函數返回值類型不同。即基類虛函數返回基類對象的指針或者引⽤,
// // 派⽣類虛函數返回派⽣類對象的指針或者引⽤時,稱為協變
// virtual A* BuyTicket()
// {
// cout << "買票-全價" << endl;
// return nullptr;
// }
//};
//
//class Student : public Person {
//public:
// virtual B* BuyTicket()
// {
// cout << "買票-打折" << endl;
// return nullptr;
// }
//};
//
//void Func(Person* ptr)
//{
// ptr->BuyTicket();
//}
//
//int main()
//{
// Person ps;
// Student st;
//
// Func(&ps);
// Func(&st);
//
// return 0;
//}
//class A
//{
//public:
// virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
// virtual void test() { func(); }
//};
//class B : public A
//{
//public:
// //多態是:不加virtual重寫是重寫虛函數的實現部分
// //相當於是基類的函數聲明部分+派生類的函數實現部分
// //即 virtual void func(int val = 1)+{ std::cout << "B->" << val << std::endl; }
// void func(int val = 0) { std::cout << "B->" << val << std::endl; }
//};
//int main(int argc, char* argv[])
//{
// B* p = new B;
// //多態調用
// p->test();
// //普通調用
// p->func();
//
// return 0;
//}
//class A
//{
//public:
// virtual ~A()
// {
// cout << "~A()" << endl;
// }
//};
//
//class B : public A {
//public:
// //建議加上virtual
// //virtual ~B()
// ~B()
// {
// cout << "~B()->delete:" << _p << endl;
// delete _p;
// }
//protected:
// int* _p = new int[10];
//};
//
//// 只有派⽣類Student的析構函數重寫了Person的析構函數,下⾯的delete對象調⽤析構函數,才能
////構成多態,才能保證p1和p2指向的對象正確的調⽤析構函數。
//
////基類只要保證析構函數是虛函數,下面這下些場景就不會存在內存泄露
//int main()
//{
// A* ptr1 = new B;
// delete ptr1;
//
// A* ptr2 = new A;
// delete ptr2;
//
// return 0;
//}
//override檢查虛函數
//class Car {
//public:
// //virtual void Dirve()//函數名寫錯、參數寫錯等導致⽆法構成重寫
// virtual void Drive()
// { }
// //不想讓派⽣類重寫這個虛函數,那麼可以⽤final去修飾
// virtual void Drive() final
// { }
//};
//class Benz :public Car {
//public:
// virtual void Drive() override { cout << "Benz-舒適" << endl; }
//};
//int main()
//{
// return 0;
//}
//設計一個不能被繼承的類
//class Base
//class Base final
//{
//public:
// void func5() { cout << "Base::func5" << endl; }
//protected:
// int a = 1;
//private:
// //構造函數私有的類不能被繼承
// Base()
// {}
//};
//
//class Derive :Base
//{
//
//};
//int main()
//{
// Derive d;
//
// return 0;
//}