文章目錄
- 繼承
- 1. 繼承的概念及定義
- 1.1 繼承的概念
- 1.2 繼承的定義
- 1.2.1 定義格式
- 1.2.2 繼承方式和訪問限定符
- 1.2.3 繼承基類成員訪問方式的變化
- 1.2.3.1 基類成員訪問方式的變化規則
- 1.2.3.2 默認繼承方式
- 1.3 繼承類模版
- 2. 基類和派生類的轉化
- 3. 繼承中的作用域
- 3.1 隱藏
- 3.2 經典面試題
- 4. 派生的默認成員函數
- 4.1 普通類的默認成員函數
- 4.2 派生類的默認成員函數
- 4.2.1 派生類的構造函數
- 4.2.2 派生類的拷貝構造函數
- 4.2.3 派生類的賦值重載函數
- 4.2.4 派生類的析構函數
- 4.2.5 派生類的4個成員函數的總結
- 4.3 面試題:實現一個不能被繼承的類
- 5. 繼承和友元
- 6. 繼承和靜態成員
- 7. 多繼承和菱形繼承問題
- 7.1 繼承模型
- 7.2 虛繼承
- 7.3 虛擬繼承的原理
- 7.4 多繼承面試題
- 8. 繼承和組合
繼承
1. 繼承的概念及定義
1.1 繼承的概念
繼承(inheritance)機制是面向對象程序設計使代碼可以復用的重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱為派生類。
繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前接觸的復用都是函數復用,而繼承便是類設計層次的復用。
具體示例:
下面在沒有學習到繼承之前設計了兩個類 Student
和 Teacher
,Student
和 Teacher
都有姓名、年齡等成員變量,都有 Print
這個成員函數,然而這些內容在這兩個類中是重復出現的,設計到兩個類里面就是冗余的。
當讓這兩個類中也有一些不同的成員變量,比如 Teacher
中獨有的成員變量是工號,Student
中獨有的成員變量是學號。
//Student類
class Student
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "張三"; //姓名int _age = 18; //年齡int _stuid; //學號
};//Teacher類
class Teacher
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "張三"; //姓名int _age = 18; //年齡int _jobid; //工號
};
下面是使用了繼承,將公有的成員都放到了 Person
中,Student
和 Teacher
都繼承Person
,就可以復?這些成員,就 不需要重復定義了,省去了很多麻煩。
繼承后,父類 Person
的成員,包括成員函數和成員變量,都會變成子類的一部分,也就是說,子類 Student
和 Teacher
復用了父類 Person
的成員。
//父類
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "張三"; //姓名int _age = 18; //年齡
};//子類
class Student : public Person
{
protected:int _stuid; //學號
};//子類
class Teacher : public Person
{
protected:int _jobid; //工號
};
1.2 繼承的定義
1.2.1 定義格式
下面看到 Person
是基類,也稱作父類。Student
是派生類,也稱作子類。(因為翻譯的原因,所以既叫基類/派生類,也叫父類/子類)
1.2.2 繼承方式和訪問限定符
訪問限定符有以下三種:
- public訪問
- protected訪問
- private訪問
繼承的方式也有類似的三種:
- public繼承
- protected繼承
- private繼承
1.2.3 繼承基類成員訪問方式的變化
基類當中被不同訪問限定符修飾的成員,以不同的繼承方式繼承到派生類當中后,該成員最終在派生類當中的訪問方式將會發生變化。
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
**總結:**可以認為三種訪問限定符的權限大小為:public
> protected
> private
,可以以這個為基準去理解表中的結果。
1.2.3.1 基類成員訪問方式的變化規則
- 在基類當中的訪問方式為
public
或protected
的成員,在派生類當中的訪問方式變為:Min(成員在基類的訪問方式,繼承方式)
。 - 在基類當中的訪問方式為
private
的成員,在派生類當中都是不可見的。
如何去理解基類的private成員在派生類當中不可見?
這句話的意思是,無法在派生類當中訪問基類的
private
成員。例如,雖然
Student
類繼承了Person
類,但是無法在Student
類當中訪問Person
類當中的private
成員 _name。//基類 class Person { private:string _name = "張三"; //姓名 }; //派生類 class Student : public Person { public:void Print(){//在派生類當中訪問基類的private成員,報錯!cout << _name << endl; } protected:int _stuid; //學號 };
也就是說,基類的
private
成員無論以什么方式繼承,在派生類中都是不可見的,這里的不可見是指基類的私有成員雖然被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
- 因為規則2中規定基類的private成員在派生類中是不能被訪問的,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就需要定義為
protected
,由此可以看出,protected
限定符是因繼承才出現的。
**注意:**在實際運用中一般使用的都是 public
繼承,幾乎很少使用 protected
和 private
繼承,也不提倡使用 protected
和 private
繼承,因為使用 protected
和 private
繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
1.2.3.2 默認繼承方式
在使用繼承的時候也可以不指定繼承方式,使用關鍵字 class
時默認的繼承方式是 private
,使用 struct
時默認的繼承方式是 public
。
在關鍵字為class的派生類當中,所繼承的基類成員_name的訪問方式變為private。
//基類
class Person
{
public:string _name = "張三"; //姓名
};//派生類
class Student : Person //默認為private繼承
{
protected:int _stuid; //學號
};
在關鍵字為struct的派生類當中,所繼承的基類成員_name的訪問方式仍為public。
//基類
class Person
{
public:string _name = "張三"; //姓名
};//派生類
struct Student : Person //默認為public繼承
{
protected:int _stuid; //學號
};
注意: 雖然繼承時可以不指定繼承方式而采用默認的繼承方式,但還是最好顯示的寫出繼承方式。
1.3 繼承類模版
下面是利用繼承,通過 vector<int>
作為基類繼承給了 stack
從而達到快速開發的目的。
namespace bit
{//template<class T>//class vector//{};// stack和vector的關系,既符合is-a,也符合has-a template<class T>class stack : public std::vector<T>{public:void push(const T& x){// 基類是類模板時,需要指定?下類域, // 否則編譯報錯:error C3861: “push_back”: 找不到標識符 // 因為stack<int>實例化時,也實例化vector<int>了 // 但是模版是按需實例化,push_back等成員函數未實例化,所以找不到 vector<T>::push_back(x);//push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}int main()
{bit::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}
**補充:**第18行,使用了類模版中還未實例化的成員函數一定要指定類域才可以調用。
2. 基類和派生類的轉化
在繼承關系中,派生類對象可以直接賦值給基類的對象/基類的指針/基類的引用,而不產生類型轉換。這個賦值的過程也被形象的叫做切片或者切割,寓意把派生類中父類那部分切來賦值過去。
如圖所示:派生類對象賦值給基類對象時是直接將派生類中屬于基類那一部分切割給基類。引用和指針也是一樣,基類的引用是派生類中屬于基類那一部分成員的別名,基類的指針指向派生類中屬于基類的那一部分。
具體示例:
//基類
class Person
{
protected:string _name; //姓名string _sex; //性別int _age; //年齡
};//派生類
class Student : public Person
{
protected:int _stuid; //學號
};int main()
{Student s;Person p = s; //派生類對象賦值給基類對象,可以Person* ptr = &s; //派生類對象賦值給基類指針,可以Person& ref = s; //派生類對象賦值給基類引用,可以s = p; //基類對象不可以賦值給派生類,這里會編譯錯誤return 0;
}
派生類對象賦值給基類指針圖示:
派生類對象賦值給基類引用圖示:
**注意:**基類對象不能賦值給派生類對象,基類的指針可以通過強制類型轉換賦值給派生類的指針,但是此時基類的指針必須是指向派生類的對象才是安全的。
3. 繼承中的作用域
3.1 隱藏
在繼承體系中的基類和派生類都有獨立的作用域。若派生類和基類中有同名成員(成員變量、成員函數),派生類成員將屏蔽基類對自己作用域中同名成員的直接訪問,這種情況叫隱藏。
具體示例:
#include <iostream>
#include <string>
using namespace std;//父類
class Person
{
protected:int _num = 111;
};//子類
class Student : public Person
{
public:void fun(){cout << _num << endl;}
protected:int _num = 999;
};int main()
{Student s;s.fun(); return 0;
}
//運行結果:999
對于以上代碼,訪問成員函數 fun
會打印子類中的成員變量 _num
,但是父類和子類中均有成員變量 _num
,但是這里會訪問子類中的 _num
,也就是打印 999
。
補充:
-
若此時就是要訪問父類當中的
_num
成員,可以使用作用域限定符進行指定訪問。void fun() {cout << Person::_num << endl; //指定訪問父類當中的_num成員 }
3.2 經典面試題
題目描述:
**問題1:**下面兩個 func 是什么關系?A. 重載 B. 重寫 C.沒關系
**問題2:**下面這段程序編譯運行的結果是什么?A. 編譯報錯 B. 運行報錯 C.正常運行
class A
{
public:void func(){cout << "func()" << endl;}
};class B : public A
{
public:void func(int i){cout << "func(int i)" <<i<<endl;}
};int main()
{B b;b.fun();return 0;
}
問題1:
雖然 A 類中的
func
函數和 B 類中的func
函數同名且參數不同,但是它們不構成重載,因為它們的作用域不同,重載函數一定是在同一個作用域中的。并且根據隱藏的規則,成員函數的隱藏,只需要函數名相同就構成隱藏,可知,這兩個函數構成隱藏,選擇A。問題2:
所以兩個
func
的關系是隱藏,因為B
繼承自A
,這里通過實例化B
的對象b
來調用fun
函數,但是B
中的fun
函數需要參數,所以這里的語法出現問題,會編譯報錯。選擇A。如果這里想調用父類中的
fun
函數,需要在指定父類作用域。
總結:
- 針對成員變量,派生類和基類中有同名成員,派生類成員將屏蔽基類對同名成員的直接訪問,這種情況叫隱藏。(在派生類成員函數中,可以使用基類
::
基類成員顯示訪問) - 如果是成員函數的隱藏,只需要函數名相同就構成隱藏。
- 注意在實際中在繼承體系里面最好不要定義同名的成員。
4. 派生的默認成員函數
4.1 普通類的默認成員函數
在學習派生類的默認成員函數之前,先來回顧一下普通類的默認成員函數:C++中成員變量的類型一共可以分為兩類:內置類型和自定義類型,各個默認成員函數對它們的處理可以用下面兩個圖片概括:
**注意:**由于取地址重載和 const 取地址重載這兩個默認成員函數一般使用編譯器自動生成的即可,所以在這里不考慮它們。
4.2 派生類的默認成員函數
和普通類的默認成員函數一樣,這里只討論構造函數、析構函數、拷貝構造函數和賦值重載函數這四個成員函數。
4.2.1 派生類的構造函數
在均使用默認構造的前提下對于派生類的成員變量中的內置類型(有缺省值就用,沒有就由編譯器初始化)、自定義類型(使用默認構造)和父類成員(調用父類默認構造)。
若要自行實現構造函數,如果基類有默認的構造函數,派生類的構造函數無須調用基類的構造函數直接初始化子類的成員變量即可。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用基類的構造函數,再對剩余派生類中的成員進行構造。
//父類
class Person
{
public://父類構造函數Person(const char* name)//非默認構造: _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; //學號
}
補充:上述代碼中的父類中沒有構造函數,所以其子類 Student
的構造函數就必須先顯示調用父類 Person
的構造函數再初始化子類中的成員變量。如果父類 Person
中的構造函數有默認構造那么子類的構造函數在默認情況下就無需顯式調用父類的構造函數,只需要對子類的成員函數進行初始化即可。
4.2.2 派生類的拷貝構造函數
在均使用默認拷貝構造的前提下對于派生類的成員變量中的內置類型(淺拷貝)、自定義類型(此類型的拷貝構造)和父類成員(調用父類的拷貝構造)。
如果要自行實現拷貝構造函數,派生類的拷貝構造函數必須先調用基類的拷貝構造完成基類的拷貝初始化。再對子類中的成員變量進行拷貝構造。
//父類
class Person
{
public://父類拷貝構造函數Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}
protected:string _name; // 姓名
}//子類
class Student : public Person
{
public://自行實現的子類拷貝構造函數Student(const Student& s): Person(s) //顯示調用父類拷貝構造, _num(s._num){cout << "Student(const Student& s)" << endl;}protected:int _num; //學號
}
**補充:**針對拷貝構造無論父類如何,子類若要自行實現拷貝構造函數必須在初始化列表中顯示調用父類的拷貝構造函數。
并且這里調用父類的拷貝構造時傳遞的參數直接傳遞子類的變量名即可,因為以指針的形式接收,父類的拷貝構造函數接受后會對其進行切割,這里涉及基類和派生類對象的賦值中的知識點。
4.2.3 派生類的賦值重載函數
在均使用默認賦值重載構造的前提下對于派生類的成員變量中的內置類型(淺拷貝)、自定義類型(此類型的拷貝構造)和父類成員(調用父類的拷貝構造)。與拷貝構造相同。
如果要自行實現賦值重載函數,其要求也與自行實現拷貝構造函數相同,必須要調用基類的operator=
完成基類的復制。
//父類
class Person
{
public://父類賦值重載函數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& 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; //學號
}
**補充:**針對賦值重載函數無論父類如何,子類若要自行實現賦值重載函數必須在初始化列表中顯示調用父類的賦值重載函數。
同樣這里的賦值重載函數也需要使用傳子類的變量名作為參數給父類的賦值重載函數,因為因為以指針的形式接收,父類的賦值重載函數接受后會對其進行切割,使其變成子類對象中父類那部分的別名。
并且這里調用 operator =
時需要指定父類類域,因為如果不指定這里的 operator =
由于子類和父類中的函數名相同構成隱藏,就會一直調用子類的 operator =
最后造成棧溢出。
4.2.4 派生類的析構函數
//父類
class Person
{
public://父類析構函數~Person(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
}//子類
class Student : public Person
{
public://自行實現的子類賦值重載函數~Student() {cout << "~Student()" << endl;}
protected:int _num; //學號
}
補充:派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。所以在自行實現子類析構函時不需要顯示調用父類的析構函數。
如果在平時代碼中需要在子類中調用父類的析構函數需要使用類域指定析構函數,Person : ~Person()
。這是因為派生類的析構和基類的析構構成隱藏關系。(由于多態關系需求,所有的析構函數的函數名都會被編譯器處理為 destructor
,因為函數名相同所以構成隱藏。)
4.2.5 派生類的4個成員函數的總結
-
派生類的成員變量分為三類:內置類型、自定義類型以及父類成員變量。其中派生類成員函數對內置類型和自定義類型的處理和普通類的成員函數一樣,但是父類成員變量必須由父類成員函數來處理。
-
派生類的析構函數非常特殊,它不需要我們顯式調用父類的析構函數,而是會在子類析構函數調用完畢后自動調用父類的析構函數,這樣做是為了保證子類成員先被析構,父類成員后被析構 (如果我們顯式調用父類析構,那么父類成員變量一定先于子類成員變量析構)。同時,子類析構和父類析構構成隱藏。
-
派生類對象初始化先調用基類構造再調派生類構造,派生類對象析構清理先調用派生類析構再調基類的析構。
并且派生類對象的析構函數在被調用完之后會自動調用基類的析構函數清理基類成員。因為只有這樣才能保證派生類對象先清理派生類成員在清理基類成員的順序。
4.3 面試題:實現一個不能被繼承的類
**方法1:**基類的構造函數私有,派生類的構成函數必須調用基類的構造函數,但是基類的構成函數私有化以后,派生類看不見就不能調用了,那么派生類就無法實例化出對象。
class Base
{
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;
}
上面這種是 C++98 給出的做法,它雖然阻止了子類創建對象,但是構造私有化也使得它本身也不能創建對象,因為創建對象需要調用構造函數。所以 C++11 提供了另外一種方式。
**方法2:**C++11新增了一個 final
關鍵字,final
修改基類,派生類就不能繼承了。
//C++11的方法
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:
};class Derive :public Base
{void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{Base b;Derive d;return 0;
}
5. 繼承和友元
友元關系不能被繼承,基類友元不能訪問派生類私有和保護成員。
**簡記:**你父親的朋友并不是你的朋友。
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;
}
補充:這里的 Dispaly
函數分別調用了基類和派生類中的成員變量,如果需要訪問的話需要再基類和派生類中都加上友元聲明。
6. 繼承和靜態成員
在 類和對象介紹了類的靜態成員變量具有如下特性:
- 靜態成員為所有類對象所共享,不屬于某個具體的對象,存放在靜態區;
- 靜態成員變量必須在類外定義,定義時不添加 static 關鍵字,類中只是聲明;
- 靜態成員變量的訪問受類域與訪問限定符的約束。
在繼承中,如果父類定義了 static 靜態成員,則該靜態成員也屬于所有派生類及其對象,即整個繼承體系里面只有一個這樣的成員,并且無論派生出多少個子類,都只有一個 static 成員實例。繼承下來的靜態成員變量都是指向同一塊空間的。
class Person
{
public:string _name;static int _count; //靜態成員類內聲明
};
int Person::_count = 0; //靜態成員類外定義class Student : public Person
{
protected:int _stuNum;
};
7. 多繼承和菱形繼承問題
7.1 繼承模型
**單繼承:**一個子類只有一個直接父類時稱這個繼承關系為單繼承。
**多繼承:**一個子類有兩個或兩個以上直接父類時稱這個繼承關系為多繼承。
**菱形繼承:**菱形繼承是多繼承的一種特殊情況。
從菱形繼承的模型構造就可以看出,菱形繼承的繼承方式存在數據冗余和二義性的問題。
例如,對于以上菱形繼承的模型,當實例化出一個 Assistant
對象后,訪問成員時就會出現二義性問題。
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()
{Assistant a;a._name = "peter"; //二義性:無法明確知道要訪問哪一個_name,產生報錯return 0;
}
補充: Assistant
對象是多繼承的 Student
和 Teacher
,而 Student
和 Teacher
當中都繼承了 Person
,因此 Student
和 Teacher
當中都有 _name
成員,若是直接訪問 Assistant
對象的 _name
成員會出現訪問不明確的報錯。
如果想要訪問 _name
中的數據,可以具體指定是哪個類域的 _name
。
//顯示指定訪問哪個父類的成員
a.Student::_name = "張同學";
a.Teacher::_name = "張老師";
雖然該方法可以解決二義性的問題,但仍然不能解決數據冗余的問題。因為在 Assistant
的對象在 Person
成員始終會存在兩份。
7.2 虛繼承
為了解決菱形繼承的二義性和數據冗余問題,出現了虛擬繼承。如前面說到的菱形繼承關系,在Student和Teacher繼承Person是使用虛擬繼承,即可解決問題
虛擬繼承代碼如下:
class Person
{
public:string _name; //姓名
};class Student : virtual public Person //虛擬繼承
{
protected:int _num; //學號
};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;
}
此時就可以直接訪問 Assistant
對象的 _name
成員了,并且之后就算指定訪問 Assistant
的 Student
父類和 Teacher
父類的 _name
成員,訪問到的都是同一個結果,解決了二義性的問題。而打印 Assistant
的 Student
父類和 Teacher
父類的 _name
成員的地址時,顯示的也是同一個地址,解決了數據冗余的問題。
cout << a.Student::_name << endl; //運行結果:peter
cout << a.Teacher::_name << endl; //運行結果:petercout << &a.Student::_name << endl; //運行結果:0136F74C
cout << &a.Teacher::_name << endl; //運行結果:0136F74C
7.3 虛擬繼承的原理
7.4 多繼承面試題
多繼承中指針偏移問題?下面說法正確的是()
A: p1 == p2 == p3
B: p1 < p2 < p3
C: p1 == p3 != p2
D: p1 != p2 != p3
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;
}
解答:
首先創建一個 Derive
類的對象 d
,因為C++中規定在內存中先繼承的存儲在前面,所以這里的 Base1
和 Base2
呈上圖存儲方式排列。
所以 p3
理所當然指向這塊空間的起始地址,然而對于 p1
和 p2
因為其類型為 Derive
的父類,所以在賦值的時候,需要進行切片,指向子類中父類那一塊所屬的空間。
故 p1
指向 Base1
的起始地址也就是和 p3
指向的空間一樣,但是需要注意 p1
和 p3
的含義并不一樣,如果對 p3
解引用其空間包含 d
的整塊空間,如果對 p1
解引用其空間則只包含 Base1
那一塊。p2
與以上類似,指向 Base2
的起始地址。
最后根據圖示的地址大小可以得出答案為C。
8. 繼承和組合
public
繼承是一種 is-a
的關系。也就是說每個派生類對象都是一個基類對象,本質是子類對象是一種特殊的父類對象
而組合是一種 has-a
的關系,假設 B 組合了 A,則每個 B 對象中都有一個 A 對象。
繼承允許你根據基類的實現來定義派生類的實現,這種通過生成派生類的復用通常被稱為白箱復用 (white-box reuse)。術語 “白箱” 是相對可視性而言:在繼承方式中,基類的內部細節對子類可見,即派生類可以訪問基類的 protected 成員 。所以繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響,派生類和基類間的依賴關系很強,耦合度高。
而對象組合是類繼承之外的另一種復用選擇,新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口,這種復用風格被稱為黑箱復用 (black-box reuse),因為對象的內部細節是不可見的,即組合只能訪問對象的共有成員,對象只以 “黑箱” 的形式出現。組合類之間沒有很強的依賴關系,耦合度低,優先使用對象組合有助于保持每個類被封裝。
所以如果既能用繼承,也能用組合,優先使用組合,因為組合耦合度低,代碼維護性好。對于繼承來說,父類的任何一個非私有成員修改都可能會影響子類,而對于組合,只有公有成員修改才可能會影響;但在實際開發中基本上不會出現全部都是公有成員的類,所以優先使用組合。
不過繼承也有用武之地的,有些關系就適合繼承那就用繼承,另外要實現多態也必須要繼承,只是說當類之間的關系即可以用繼承,可以用組合時,優先使用組合。
**eg1:**車類和寶馬類就是is-a的關系,它們之間適合使用繼承。
class Car
{
protected:string _colour; //顏色string _num; //車牌號
};class BMW : public Car //BWM是車,繼承關系
{
public:void Drive(){cout << "this is BMW" << endl;}
};
**eg2:**車和輪胎之間就是has-a的關系,它們之間則適合使用組合。
class Tire
{
protected:string _brand; //品牌size_t _size; //尺寸
};class Car //車有輪胎,組合關系
{
protected:string _colour; //顏色string _num; //車牌號Tire _t; //輪胎
};