📝前言:
這篇文章我們來講講面向對象三大特性之一——繼承
🎬個人簡介:努力學習ing
📋個人專欄:C++學習筆記
🎀CSDN主頁 愚潤求學
🌄其他專欄:C語言入門基礎,python入門基礎,python刷題專欄,Linux
文章目錄
- 一,面相對象三大特性
- 二,繼承
- 1 大白話講繼承
- 2 繼承定義格式
- 2.1 繼承方式的作用
- 2.2 繼承類模板
- 2.2.2 需指定類域
- 2.2.1 按需實例化
- 3 基類和派生類間的轉換
- 4 繼承中的作用域
- 4.1 隱藏規則
- 5 派生類的默認成員函數
- 6 實現?個不能被繼承的類
- 7 友元關系不能繼承
- 8 靜態成員的繼承
- 9 多繼承
- 10 繼承與組合
一,面相對象三大特性
面相對象編程具有三大特性,分別是封裝、繼承和多態:
- 封裝
- 概念:將數據和操作數據的方法綁定在一起,組成一個不可分割的整體,即對象。同時,對外部隱藏對象的內部實現細節,只對外提供有限的訪問接口。迭代器就是一種封裝,底層不一樣,但是卻能用相似的方法訪問
- 作用:通過封裝,可以提高代碼的安全性和可維護性。避免外部代碼直接訪問和修改對象的內部數據,防止數據被意外篡改,同時也使得代碼的結構更加清晰,各個模塊的職責更加明確。
- 繼承
- 概念:允許創建一個新的類(子類),它基于現有的類(父類)進行擴展,子類可以繼承父類的屬性和方法,并且可以在子類中添加自己特有的屬性和方法,或者重寫父類的方法。
- 作用:繼承實現了代碼的復用,避免了重復編寫相似的代碼。同時,它也體現了面向對象編程中的“is - a”關系,即子類是父類的一種特殊類型,有助于建立清晰的類層次結構,便于對問題域進行建模。
- 多態
- 概念:指同一個方法或操作在不同的對象上可以有不同的表現形式。也就是說,不同的子類對象在調用相同的方法時,可能會執行不同的代碼邏輯,產生不同的結果。
- 作用:多態提高了代碼的靈活性和可擴展性。當需要添加新的功能或修改現有功能時,不需要大量修改客戶端代碼,只需要在相應的子類中進行修改或擴展即可。它使得代碼更加易于維護和升級,同時也增強了代碼的可讀性和可理解性。
二,繼承
1 大白話講繼承
簡單來說,子類繼承父類就是指:子類可以使用父類的成員,并且也可以自己加自己的成員。我們也把父類稱為基類,子類稱為派?類。
示例(下面這個程序是沒有問題的):
class Person
{
public:string name;int age;char sex;
};class Student : public Person
{
public:int st_number;
};int main()
{Student st1;st1.sex = 'b';st1.st_number = 23;return 0;
}
在這里,Person
是父類,Student
是子類
通過監視窗口我們可以看到,st1里面繼承了父類Person的三個成員變量。
2 繼承定義格式
2.1 繼承方式的作用
我們都知道,訪問限定符有,private
,public
,和protected
。protected
就是專門為繼承設置的。
繼承方式對應也有:private
,public
,和protected
繼承類成員訪問方式的變化:
- 基類
private
成員在派?類中是不可見的。不可見是語法上限制派?類對象不管在類??還是類外?都不能去訪問它。(但是其實還是被繼承了過去) protected
成員在類外不能直接訪問,在子類中可以被訪問。- 基類的其他成員在派?類的訪問?式 == Min(成員在基類的訪問限定符,繼承方式),public > protected >private
class
默認的繼承?式是private
,struct
默認的是public
,但是建議顯式寫出繼承方式,且一般用public
如,上述代碼中父類改成:
class Person
{
public:string name;private:int age;char sex;
};
這時候子類繼承后,類外執行st1.sex = 'b';
就會報錯,因為sex
是父類的私有成員
2.2 繼承類模板
繼承類模板需要注意的是:在子類中使用父類類模板的方法時,如果參數是不確定的,要指定一下父類的類域(才能實例化)
2.2.2 需指定類域
namespace tr
{template<class T>class stack: vector<T>{public:void push(T x){push_back(x);}};
}int main()
{tr::stack<int> st;st.push(3);return 0;
}
報錯:
原因是:
stack<int>
實例化時,也實例化vector<int>
了,但是不代表push_back
實例化了,因為模板是按需實例化的- 到了
push
操作,編譯器要對其實例化,但是因為編譯器不知道push_back
是vector<T>
里的成員,從而找不到push_back
這個標識符
正確寫法:
void push(T x)
{vector<T>::push_back(x);
}
2.2.1 按需實例化
示例:
namespace tr
{template<class T>class stack: public vector<T>{public:void push(T x){push_back(x);}void print(){cout << "push_back" << endl;}};
}int main()
{tr::stack<int> st;st.print();return 0;
}
上面代碼是能正常運行的,原因是實例化stack
的時候,并不會把所有成員都實例化了,后面調用誰,才實例化誰。
3 基類和派生類間的轉換
對象傳遞:
當派生類對象傳遞給基類對象的時候,會進行切片,即:派生類對象中基類部分的數據會被復制到基類對象中,而派生類特有的成員則被 “切掉”(會丟失派生類成員的所有信息)。這時候基類對象不能訪問派生類的成員,調用父類的成員時,結果也是父類對象的。
指針/引用傳遞:
public
繼承的派生類對象的指針/引用 可以賦值給 基類的指針 / 基類的引用,叫向上傳型,也類似做切片。傳遞后,基類的指針/引用指向派生類,但是只能調用派生類中的基類成員那一部分。
如:
class Person
{
public:string name;int age;char sex;
};class Student : public Person
{
public:int st_number;
};int main()
{Student st1;// 派生類對象賦值給基類的指針/引用Person* p1 = &st1;Person& p2 = st1;// 派生類對象賦值給基類對象(實際上調用的是拷貝構造)Person p3 = st1;return 0;
}
注意:基類的不能賦值給子類(基類的指針或者引?可以通過強制類型轉換賦值給派?類的指針或者引?。但是必須是基類的指針是指向派?類對象時才是安全的。這?基類如果是多態類型,可以使?RTTI(Run-Time TypeInformation)的dynamic_cast
來進?識別后進?安全轉換。)
4 繼承中的作用域
基類和派生類都有獨立的作用域
4.1 隱藏規則
隱藏:派?類和基類中有同名成員(即同名變量或者函數,函數只要同名就算),派?類成員將屏蔽基類對同名成員的直接訪問。(也叫做重定義)
如果要訪問被隱藏的父類成員,可以指定域。基類名::成員
示例:
class Person
{
public:void print(){cout << "Person" << endl;}string name;int age = 10;char sex;
};class Student : public Person
{
public:void print(){cout << "Student" << endl;}int age = 18;int st_number;};int main()
{Student st;cout << "st.age: " << st.age << endl; // 父類的被隱藏st.Person::print(); // 指定父類的域return 0;
}
只要函數同名就會隱藏,如下也是隱藏:
5 派生類的默認成員函數
我們可以派生類中的變量看出三種類型:內置類型,自定義類型,來自父類
**在子類繼承父類的時候,父類的成員相當于是最先被聲明的,然后才到子類自己的成員。**所以調用構造的時候也是,先父類的,再子類的
如果繼承多個父類,則先繼承的先聲明。
- 派?類中基類的成員,必須調用基類的構造函數來初始化(派生類的構造函數會自動調用基類的默認構造,所以,通常,派生類中的成員又有資源申請(需要深拷貝)的時候,我們才需要自己實現構造,拷貝構造和析構也同理)。如果基類沒有默認的構造函數,則必須在派?類構造函數的初始化列表階段顯式調?(基類的構造函數)
示例(子類構造自動調用基類默認構造完成對基類成員的初始化):
class Person
{
public:Person(int age = 10) // 基類有默認構造:name("小紅"),age(age),sex('b'){}void print(){cout << "Person" << endl;}string name;int age;char sex;
};class Student : public Person
{
public:Student(int number) // 子類構造自動調用父類默認構造{st_number = number;}void print(int i){cout << "Student" << endl;}int st_number;};int main()
{Student st(23);return 0;
}
當父類沒有默認構造:
class Person
{
public:Person(int a) // 父類沒有默認構造:name("小紅"),age(a),sex('b'){}void print(){cout << "Person" << endl;}string name;int age;char sex;
};class Student : public Person
{
public:Student(int number){st_number = number;}void print(int i){cout << "Student" << endl;}int st_number;
};int main()
{Student st(23);return 0;
}
報錯:
正確寫法:
Student(int number, int a):Person(a) // 在初始化列表顯示調用父類的默認構造
{st_number = number;
}
- 派?類的拷貝構造函數必須調用基類的拷貝構造完成對基類成員的拷貝初始化(如果這個拷貝構造不是缺省的,即不是默認構造函數,也要放在初始化列表)
- 派?類的operator=必須要調用基類的operator完成基類的復制。需要注意的是派?類的
operator=隱藏了基類的operator=,所以顯?調?基類的operator=,需要指定基類作?域 - 派?類的析構函數會在被調?完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派?類對象先清理派?類成員再清理基類成員(后定義的先清理)的順序
- 因為多態中?些場景析構函數需要構成重寫,重寫的條件之?是函數名相同。那么編譯器會對析構函數名進?特殊處理,處理成
destructor()
,所以基類析構函數不加virtual
的情況下,派生類析構函數和基類析構函數構成隱藏關系。如果要顯示調用就要指定域。
示例:
class Person
{
public:Person(int a):name("小紅"),age(a){}Person(const Person& p){name = p.name;age = p.age;}Person& operator=(const Person& p){if (this != &p){name = p.name;age = p.age;}return *this;}~Person(){cout << "~Person()" << endl;}string name;int age;
};class Student : public Person
{
public:Student(int number, int a):Person(a) // 在初始化列表顯示調用父類的默認構造{cout << "Student(int number, int a)" << endl;st_number = number;}// 拷貝構造錯誤寫法:/*Student(const Student& s){Person(s);st_number = s.st_number;cout << "Student(const Student& s)" << endl;}*/// 正確寫法:Student(const Student& s): Person(s){st_number = s.st_number;cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& s){cout << "Student& operator=(const Student& s)" << endl;if (this != &s){Person::operator=(s); // 顯式調用父類的=重載來初始化父類的成員st_number = s.st_number;}return *this;}~Student(){cout << "~Student()" << endl;}int st_number;
};int main()
{Student st1(23, 18);Student st2(st1);Student st3(25, 35);st1 = st3;return 0;
}
運行結果:
6 實現?個不能被繼承的類
- ?法1:基類的構造函數私有,派?類的構成必須調?基類的構造函數,但是基類的構成函數私有化以后,派?類看不見就不能調用了,那么派生類就?法實例化出對象。
- ?法2:C++11新增了?個final關鍵字,final修改基類(表示最終類),派?類就不能繼承了。
示例:class Person final
7 友元關系不能繼承
也就是說基類友元只對基類起作用,而不能訪問派?類私有和保護成員。
8 靜態成員的繼承
基類定義了static
靜態成員,則整個繼承體系??只有?個這樣的成員。?論派?出多少個派?類,都只有?個static
成員實例,用的是用一塊內存空間的static
成員
9 多繼承
-
單繼承:?個派?類只有?個直接基類
-
多繼承:?個派?類有兩個或以上直接基類時稱這個繼承關系為多繼承,多繼承對象在內存中的模型是,先繼承的基類在前?,后?繼承的基類在后?,派?類成員在放到最后?。
-
菱形繼承:菱形繼承是多繼承的?種特殊情況。菱形繼承的問題,從下?的對象成員模型構造,可以看出菱形繼承有數據冗余和?義性的問題,在Assistant的對象中Person成員會有兩份。
虛擬菱形繼承:
在菱形繼承結構中,如果存在多層繼承關系,使得一個派生類從兩個或多個基類中繼承了相同的成員,就會產生數據冗余和二義性問題。
虛擬繼承就是為了解決這種問題而引入的,通過在繼承關系中使用virtual關鍵字,使得在最終的派生類中只保留一份共享的基類子對象。
比如,在上述Student
好Teacher
繼承時使用虛擬繼承,在可能產生數據冗余和二義性的地方添加virtual
,就不會產生兩個name
成員,在通過Assistant
訪問成員name
時,就只有一個,不會產生二義性問題:
如果不添加:
添加以后:
class Person
{
public:string _name; // 姓名
// 使?虛繼承Person類
class Student : virtual public Person
{
protected:int _num; //學號
};
// 使?虛繼承Person類
class Teacher : virtual public Person
{
protected:int _id; // 職?編號
};// 教授助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修課程
};
int main()
{
// 使?虛繼承,可以解決數據冗余和?義性Assistant a;a._name = "tr";return 0;
}
10 繼承與組合
public
繼承是?種is-a的關系。也就是說每個派?類對象都是?個基類對象。- 組合是?種has-a的關系。假設B組合了A,每個B對象中都有?個A對象。
- 繼承允許你根據基類的實現來定義派?類的實現。這種通過?成派?類的復?通常被稱為?箱復?。“白箱”:即相對可見。在繼承中,基類的內部細節對派?類可見,基類的改變,對派?類有很?的影響。派?類和基類間的依賴關系很強,耦合度?。
- 對象組合是類繼承之外的另?種復?選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接?。這種復??格被稱為?箱復?(black-box reuse),因為對象的內部細節是不可?的。組合類之間沒有很強的依賴關系,耦合度低。
- 優先使?對象組合有助于你保持每個類被封裝,優先使?組合(has - a),?不是繼承(is - 1)
🌈我的分享也就到此結束啦🌈
要是我的分享也能對你的學習起到幫助,那簡直是太酷啦!
若有不足,還請大家多多指正,我們一起學習交流!
📢公主,王子:點贊👍→收藏?→關注🔍
感謝大家的觀看和支持!祝大家都能得償所愿,天天開心!!!