目錄
一.繼承的概念及定義
1.繼承的概念
2.繼承的定義
(1)繼承的定義格式
(2)繼承基類成員訪問方式的變化
二.基類與派生類間的轉換
1.派生類對象賦值給基類的引用/指針
2. 派生類對象直接賦值給基類對象
三.繼承的作用域
四.派生類的默認成員函數
1.構造函數
2.拷貝構造函數
3.賦值重載?
4.析構函數
基類與派生類的行為總結:
?編輯實現一個不能被繼承的類
五.繼承與友元
六.繼承與靜態成員
七.多繼承與菱形繼承問題
1.繼承模型
2.虛繼承
八.繼承與組合
一.繼承的概念及定義
1.繼承的概念
????????繼承(inheritance)機制是面向對象程序設計使代碼可以復?的最重要的手段,它允許我們在保持原有類特性的基礎上進行擴展,增加方法(成員函數)和屬性(成員變量),這樣產生新的類,稱派生類。
????????繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的函數層次的復用,繼承是類設計層次的復用。
? ? ? ? 舉例:
? ? ? ? 如果一個人具有名字,性別,年齡等屬性,我們將這個集合定義為Person類,當我們還想要定義一個Student類時,由于Student與Person屬于“has-a”的關系,即除了Student自身的特性外,其也具有Person的所有特性時,我們可以將Student定義為Person的派生類,即Student類繼承Person類,在理解上,可以認為Student就是一個Person。
2.繼承的定義
(1)繼承的定義格式
????????下面我們看到Person是基類,也稱作父類。Student是派生類,也稱作子類:
? ? ? ? 舉例:
class Person
{
public:// 進入校園/圖書館/實驗室刷二維碼等身份認證void identity(){cout << "void identity()" << _name << endl;}
protected:string _name = "張三"; // 姓名string _address; // 地址string _tel; // 電話int _age = 18; // 年齡
};class Student : public Person
{
public:// 學習void study(){// ...}
protected:int _stuid; // 學號
};int main()
{Person p;Student s;return 0;
}
? ? ? ? 在監視窗口中可以看到Student類繼承了Person類的成員變量與成員函數。?
(2)繼承基類成員訪問方式的變化
? ? ? ? 三種繼承方式與三種訪問限定符對應著9種訪問方式:
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 在派生類種不可見 | 在派生類種不可見 | 在派生類種不可見 |
<1>基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
<2>基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現的。
<3>實際上面的表格我們進行一下總結會發現,基類的私有成員在子類都是不可見。基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
<4>使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過 最好顯示的寫出繼承方式。
<5>在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
二.基類與派生類間的轉換
1.派生類對象賦值給基類的引用/指針
????????public繼承的派生類對象可以賦值給基類的指針/基類的引?。這里有個形象的說法叫切片或者切割。寓意把派生類中基類那部分切出來,基類指針或引用指向的是派生類中切出來的基類那部分。
class Person
{
protected:string _name; // 姓名string _sex; // 性別int _age; // 年齡
};class Student : public Person
{
public:int _No; // 學號
};int main()
{Student sobj;// 派生類對象可以賦值給基類的指針/引用Person* pp = &sobj;Person& rp = sobj;return 0;
}
2. 派生類對象直接賦值給基類對象
? ? ? ? 這里是基類的賦值重載實現的,本質還是進行了切片,但是基類對象不能賦值給派生類對象。
Person pobj = sobj;
三.繼承的作用域
<1>在繼承體系中基類和派生類都有獨立的作用域。
<2>派生類和基類中有同名成員,派生類成員將屏蔽基類對同名成員的直接訪問,這種情況叫隱藏。(在派生類成員函數中,可以使用基類::基類成員顯式訪問)
class Person
{
protected:string _name = "?李"; // 姓名int _num = 111; // 身份證號
};class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份證號:" << Person::_num << endl; // 顯式訪問cout << " 學號:" << _num << endl; // 構成隱藏}
protected:int _num = 999; // 學號
};int main()
{Student s1;s1.Print();return 0;
};
<3>需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。
class A
{
public:void fun(){cout << "func()" << endl;}
};class B : public A
{
public:void fun(int i){cout << "func(int i)" << i << endl;}
};int main()
{B b;b.fun(10); // 調用fun(int i)//b.fun(); // 基類與派生類的函數名相同,fun構成隱藏,由于沒有傳參,直接報錯return 0;
};
<4>注意在實際中在繼承體系里面最好不要定義同名的成員。
四.派生類的默認成員函數
????????6個默認成員函數,默認的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類中,這幾個成員函數是如何生成的呢?
1.構造函數
? ? ? ? 在初始化屬于基類的那一部分成員時,可以將所屬于基類的成員整體當作一個自定義類型,派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯式調用。
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}
protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}
protected:int _num; //學號
};int main()
{Student s1("jack", 18);return 0;
}
? ? ? ? 同時在監視窗口中也可以看到派生類對象初始化先調用基類構造再調派生類構造。?
? ? ? ? 派生類對象構造順序:
基類成員 > 派生類成員
2.拷貝構造函數
????????派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}
protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s) // 切片,調用基類的拷貝構造, _num(s._num){cout << "Student(const Student& s)" << endl;}
protected:int _num; //學號
};int main()
{Student s1("jack", 18);Student s2(s1);return 0;
}
3.賦值重載?
????????派生類的operator=必須要調用基類的operator=完成基類的復制。需要注意的是派生類的 operator=隱藏了基類的operator=,所以顯式調用基類的operator=,需要指定基類作用域。
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}
protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s) // 切片,調用基類的拷貝構造, _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& s){cout << "Student& operator=(const Student& s)" << endl;if (this != &s){// 構成隱藏,所以需要顯式調用,若不顯式調用則會調用派生類的復制重載,形成死遞歸Person::operator=(s); // 賦值所屬基類的一部分_num = s._num;}return *this;}
protected:int _num; //學號
};int main()
{Student s1("jack", 18);Student s2(s1);Student s3("rose", 17);s1 = s3;return 0;
}
4.析構函數
????????派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
????????派生類對象析構清理先調用派生類析構再調基類的析構。
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){// 構成隱藏,所以需要顯?調?Person::operator =(s);_num = s._num;}return *this;}~Student(){cout << "~Student()" << endl;}// 自動調用基類的析構函數
protected:int _num; //學號
};int main()
{Student s1("jack", 18);return 0;
}
? ? ? ? 在輸出中可以看到,在派生類的析構函數執行完后會自動調用基類的析構函數,所以在派生類的析構函數中不需要再對基類的那一部分資源進行析構,否則會導致同一塊資源二次釋放,只需要釋放派生類自己申請的資源即可。?
派生類對象析構順序:
派生類成員 > 基類成員
基類與派生類的行為總結:

實現一個不能被繼承的類
方法1:基類的構造函數私有,派生類的構成必須調用基類的構造函數,但是基類的構成函數私有化以后,派生類看不見就不能調用了,那么派生類就無法實例化出對象。
方法2:C++11新增了?個final關鍵字,final修改基類,派生類就不能繼承了。
// C++11的方法
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:// C++98的方法/*Base(){}*/
};class Derive :public Base
{void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{Base b;Derive d;return 0;
}
五.繼承與友元
????????友元關系不能繼承,也就是說基類友元不能訪問派生類私有和保護成員 。
class Student; // 前置聲明class Person
{
public:friend void Display(const Person& p, const Student& s);
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;
}
六.繼承與靜態成員
????????基類定義了static靜態成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個派生類,都只有一個static成員實例。
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;
}
七.多繼承與菱形繼承問題
1.繼承模型
<1>單繼承:一個派生類只有一個直接基類時稱這個繼承關系為單繼承。
<2>多繼承:一個派生類有兩個或以上直接基類時稱這個繼承關系為多繼承,多繼承對象在內存中的模型是,先繼承的基類在前面,后面繼承的基類在后面,派生類成員在放到最后面。
<3>菱形繼承:菱形繼承是多繼承的?種特殊情況。菱形繼承的問題,從下面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題,在Assistant的對象中Person成員會有兩份。支持多繼承就一定會有菱形繼承,像Java就直接不支持多繼承,規避掉了這里的問題,所以實踐中也是不建議設計出菱形繼承這樣的模型的。
?
class Person
{
public:string _name; // 姓名
};class Student : public Person
{
protected:int _num; //學號
};class Teacher : 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;
}
?? ? ? ? 那么如何讓解決菱形繼承中存在的數據冗余二義性的問題呢,這里可以通過虛繼承來進行解決。
2.虛繼承
? ? ? ? 實現虛繼承的方式就是在派生類的繼承方式前加一個關鍵字virtual。
class Person
{
public:string _name; // 姓名/*int _tel;int _age;string _gender;string _address;*/// ...
};// 使用虛繼承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 = "peter";return 0;
}
八.繼承與組合
<1>?public繼承是一種is-a的關系。也就是說每個派生類對象都是一個基類對象。
<2>組合是一種has-a的關系。假設B組合了A,每個B對象中都有一個A對象。
<3>繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的復用通常被稱為白箱復用?(white-box reuse)。術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節對派生類可見?。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關系很強,耦合度高。
<4>對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse),因為對象的內部細節是不可見的。對象只以“黑箱”的形式出現。 組合類之間沒有很強的依賴關系,耦合度低。優先使用對象組合有助于你保持每個類被封裝。
<5>優先使用組合,而不是繼承。實際盡量多去用組合,組合的耦合度低,代碼維護性好。不過也不太那么絕對,類之間的關系就適合繼承(is-a)那就用繼承,另外要實現多態,也必須要繼承。類之間的關系既適合用繼承(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 _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 T>
class vector
{};// stack和vector的關系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};template<class T>
class stack
{
public:vector<T> _v;
};int main()
{return 0;
}