文章目錄
- 🦜1. 什么是繼承
- 🐊1.1 概念
- 🐊1.2 格式
- 🐊1.3 繼承方式 & 訪問限定符
- 🐦2. 派生類和基類的賦值問題
- 🦩3. 派生類和基類同名成員問題
- 🐓4.派生類默認成員函數
- 🐉4.1 構造函數
- 🐉4.2 拷貝構造
- 🐉4.3 賦值運算符重載
- 🐉4.4 析構函數
- 🐥5. 友元和靜態成員
- 🐧6. 多繼承
🦜1. 什么是繼承
🐊1.1 概念
在現實生活中,談起繼承,就會聯想到繼承家業、家產。
而在編程世界中,繼承也是如此,一個類(稱子類或者派生類),可以繼承另一個類(稱父類或基類)的屬性和行為。
//定義一個人的屬性 基類
class Person
{
public:Person(string name = "Kangkang", string gender = "male", int age = 18):_name(name),_gender(gender),_age(age){cout << "Person()" << endl;}void Print(){cout << "name:" << _name << endl;cout << "gender:" << _gender << endl;cout << "age:" << _age << endl;}
protected:string _name; // 姓名string _gender; // 姓別int _age; //年齡
};
//定義一個學生類,繼承人的屬性 子類
class Student :public Person
{
public:Student(string name = "Lihua", string gender = "female", int age = 20, int id=111):Person(name,gender,age),_stuId(id){};
protected:int _stuId; //學號
};
int main()
{Person p;Student stu("Lisa","female",20,20230812);p.Print();stu.Print();return 0;
}
🐊1.2 格式
class 子類 : 繼承方式 基類
{};
🐊1.3 繼承方式 & 訪問限定符
繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
父類public成員 | 子類的public成員 | 子類的protected成員 | 子類的private成員 |
父類的protected成員 | 子類的protected成員 | 子類的protected成員 | 子類的private成員 |
父類的private成員 | 子類不可見 | 子類不可見 | 子類不可見 |
這里其實很好分辨,我們只需要取權限小的即可:public>protected>private
對于public
成員,我們可以直接在類的外面訪問調用,而對于protected
成員,可在類里面通過this
指針訪問,而private
成員,雖然繼承到了派生類對象中,但無法訪問到,也可以理解為將父類的成員設為private
就是不想讓其他類繼承
但是在實際應用過程中,一般都是采用的public
繼承方式
Tips:
關鍵字
class
不指定繼承方式時,默認繼承方式為private
而使用
struct
關鍵字時,默認繼承方式為public
但這里還是建議,每次都顯示繼承方式
class A
{
public:void func1() { cout << "func1()" << endl; }
protected:void func2(){ cout << "func2()" << endl; }
private:void func3(){ cout << "func3()" << endl; }int _a = 0;
};
class B :public A
{
public:void Print(){this->func1();this->func2();}int _b = 1;};
int main()
{B b;b.Print();
}
🐦2. 派生類和基類的賦值問題
派生類和基類之間的賦值操作涉及到對象切片的問題。派生類的對象可以賦值給基類對象/基類指針/基類引用 ,但反過來(將基類對象賦值給派生類對象)是不合法的,因為這可能導致對象切片,即派生類對象的額外成員信息丟失
這就好比,學習C++,C++是在C語言的基礎上衍生出來的,可以理解問C++繼承了C語言的衣缽,C++的代碼可以兼容C的代碼;反之,C的代碼卻不能卻不能兼容C++。
🦩3. 派生類和基類同名成員問題
class A
{
public:int _x=1;int _y=2;void Print(){cout << "A()" << endl;}
};
class B :public A
{
public:int _x = 3;int _y = 4;void Print(){cout << "B()" << endl;}
};
int main()
{B b;cout << b._x << endl; // 3cout << b._y << endl; // 4b.Print(); // B()
}
這段代碼基類A和派生類B,成員名都是相同的,但我們輸出發現,輸出的是派生類的成員,那這里是否繼承了A的這些成員呢?
通過監視窗口發現,這里A是被B繼承了,但是由于成員名相同,A被B給隱藏了,這也叫重定義。
如果要訪問基類的成員,可使用基類:基類成員顯示訪問,這也可理解問他們都有著獨立的作用域
🐓4.派生類默認成員函數
🐉4.1 構造函數
派生類的構造函數必須調用基類的構造函數來初始化繼承下來的那部分成員;如果基類沒有默認構造,那在派生類構造函數的初始化列表顯示調用
class Person
{
public://全缺省,默認構造Person(string name = "Kangkang"):_name(name){}
protected:string _name;
};
class Student : public Person
{
public:Student(string name, int id):Person(name),_id(id){}void Print(){cout << "name:" << _name << endl;cout << "id:" << _id << endl;}
protected:int _id;
};
int main()
{Student stu("Lisa",2023);stu.Print();return 0;
}
🐉4.2 拷貝構造
派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化,但我們可以直接傳子類對象,因為調用父類的拷貝構造時,父類會自動切片拿到父類中的對象
父類拷貝構造
Person(const Person& p):_name(p._name)
{}
子類拷貝構造
Student(const Student&stu):Person(stu._name),_id(stu._id)
{}
調用
Student stu("Lisa", 2023);
stu.Print();
Student stu2(stu);
stu2.Print();
🐉4.3 賦值運算符重載
子類的operator=
必須要調用父類的operator=
完成基類的復制;但是因為賦值運算符重載了=
,那么子類和父類的名字都是一樣,這樣就造成了子類隱藏了父類的operator=
。所以需要顯示調用父類的operator=
//operator=
Person& operator=(const Person& p)
{if (this != &p){_name = p._name;}return *this;
}
Student& operator=(Student& stu)
{if (this != &stu){//指定調用父類Person::operator =(stu);_id = stu._id;}return *this;
}
🐉4.4 析構函數
子類的析構函數會在被調用完成后自動調用父類的析構函數清理基類成員;因為這樣才能保證子類對象先清理子類成員再清理父類成員的順序。
Tips:
切記,這里是自動調用父類的析構,所以我們不需要在子類的析構函數中調用父類的析構函數
如果這里有指針,同一塊區域釋放兩次,會造成未定義行為
🐥5. 友元和靜態成員
在繼承中,友元關系是不可以被繼承的,就好比咱們朋友的朋友,不一定是咱們的朋友。
對于靜態成員,這里繼承的是它的使用權,就比如家里有三個孩子,一個大哥哥,兩個小弟弟,這個哥哥是他兩“共用的”,并不會說2個弟弟必須有2個哥哥。
class A
{
public:static int _sa;int _a;
};
int A::_sa = 1;
class B :public A
{
public:int _b;
};int main()
{A a;B b;cout <<"a._a:" << &a._a << endl;cout <<"b._a:" << &b._a << endl;cout <<"a._sa:" << &A::_sa << endl;cout <<"b._sa:" << &B::_sa<< endl;
}
這里也可以驗證,對于靜態成員,父類和子類是共用的(可用于計算父類有多少個派生類)。
🐧6. 多繼承
對于一個子類只有一個直接父類,這種關系稱為單繼承
//單繼承
class A
{};
class B:public A
{};
class C :public B
{};
而對于一個子類有多個直接父類,這種關系稱為多繼承
//多繼承
class A
{};
class B
{};
class C :public A, public B
{};
多繼承會引發一個很麻煩的問題——菱形繼承
我們先來上代碼
class A
{
public:int _a;
};
class B:public A
{
public:int _b;
};
class C :public A
{
public:int _c;
};
class D :public B, public C
{
public:int _d;
};
int main()
{D d;d._a = 1; //errord._b = 2;return 0;
}
這段代碼,直接報錯,_a
的指定不明確,因為D類繼承了B類和C類,編譯器不知道這個_a
是屬于繼承的哪個類,從而產生二義性的問題。
當然,前面也提到過,可以通過指定類域,來明確告訴編譯器,這屬于哪個類
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
這樣雖然解決了二義性的問題,但是這樣看的數據十分冗余,很容易分不清哪個是哪個
為了填補這個坑,推出了一種名為虛擬繼承的繼承方式(僅限菱形繼承使用,其他地方不要使用)
class A
{
public:int _a;
};
class B:virtual public A
{
public:int _b;
};
class C :virtual public A
{
public:int _c;
};
class D :public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;d._a = 6;return 0;
}
使用虛擬繼承之后,我們發現這里的_a
,只有一份了,而且我們查看內存發現,數據并不是連在一起,多了一些地址
這叫做虛基表,用來尋找基類偏移量的表,虛擬繼承的派生類里面就包含了這個虛表,這個虛表記錄著距離基類的偏移量,如果要用到基類的數據,加上這個距離就能找到,這樣就解決了數據的二義性和數據冗余的問題。
但是在實際過程中,這個模型十分雞肋且復雜,所以一般都不會采用這種繼承方式。
多繼承就屬于C++語法復雜的一個體現,而繼承雖然可以復用,但是繼承的耦合度十分高,代碼直接的依賴關系很強,這樣就造成了代碼的不便于維護。但又涉及到多態必須使用繼承,所以在實際之中,代碼要復用的話,我們得分場景。
那本期的方向就到這咯,我們下期再見,如果有下期的話。