目錄
一 繼承的概念與定義
1 繼承的概念
2 示例
3繼承定義
(1)定義格式
(2)繼承方式與訪問限定
(3)繼承基類成員訪問方式的變化
4 繼承類模板
二 基類和派生類之間的轉換
1 基礎類型轉換
2 基類和派生類轉換
3 示例
三 繼承中的作用域
1 隱藏規則
2 考察繼承作用域相關選擇題
四 派生類的默認成員函數
1 四個常見默認成員函數
2 核心生成機制
3 構造函數與析構函數
一 繼承的概念與定義
1 繼承的概念
繼承機制是面向對象程序設計使代碼可以複用的最重要的手段,它允許我們在保持原有類特性的基礎上進行擴展,增加方法(成員函數)和屬性(成員變量),這樣產生新的類,稱派生類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到複雜的認知過程。以前我們接觸的函數層次的複用,繼承是類設計層次的復⽤。
繼承的本質是類層次的複用
2 示例
假如我們需要寫學生管理系統或者門禁管理系統,我們就需要定義多個類:如學生,老師,食堂阿姨等等。
我們發現類中間有些成員是重複的,可以理解為是公用的。我們把這部分公共類叫做父類/基類。
把每個部分獨有的叫做子類/派生類
公共的特性,抽取出來,放到一個公共類(父類/基類)裏面——
我們看到沒有繼承之前我們設計了兩個類Student和Teacher,Student和Teacher都有姓名/地址/電話/年齡等成員變量,都有identity⾝份認證的成員函數,設計到兩個類裏面就是冗餘的。當然他們也有⼀些不同的成員變量和函數,比如老師獨有成員變量是職稱,學生的獨有成員變量是學號;學生的獨有成員函數是學習,老師的獨有成員函數是授課。
我們來實現一下:
class Student
{
public:
// 進⼊校園/圖書館/實驗室刷⼆維碼等⾝份認證
void identity()
{
// ...
}
// 學習
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 電話
int _age = 18; // 年齡
int _stuid; // 學號
};
class Teacher
{
public:
// 進⼊校園/圖書館/實驗室刷⼆維碼等⾝份認證
void identity()
{
// ...
}
// 授課
void teaching()
{
//...
}
protected:
string _name = "張三"; // 姓名
int _age = 18; // 年齡
string _address; // 地址
string _tel; // 電話
string _title; // 職稱
};
int main()
{
return 0;
}
我們把公共的類放到person中去,Student和teacher都繼承Person,就可以複用這些成員,就
不需要重複定義了,省去了很多麻煩
class Person
{
public:
// 進⼊校園/圖書館/實驗室刷⼆維碼等⾝份認證
void identity()
{
cout << "void identity()" <<_name<< endl;
}
protected:
string _name = "張三"; // 姓名
string _address; // 地址
string _tel; // 電話
int _age = 18; // 年齡
};
class Student : public Person
1{
public:
// 學習
void study()
{
// ...
}
protected:
int _stuid; // 學號
};
class Teacher : public Person
{
public:
// 授課
void teaching()
{
//...
}
protected:
string title; // 職稱
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
3繼承定義
(1)定義格式
下⾯我們看到Person是基類,也稱作⽗類。Student是派⽣類,也稱作⼦類。(因為翻譯的原因,所以既叫基類/派⽣類,也叫⽗類/⼦類)
(2)繼承方式與訪問限定
(3)繼承基類成員訪問方式的變化
1. 基類的private成員在派生類中,無論採用何種繼承方式,都是不可見的。這裏的“不可見”意味着:基類的私有成員雖然會被繼承到派生類對象中,但從語法層面,無論是在派生類內部還是外部,都不允許訪問該成員。
2. 由於基類的private成員在派生類中無法訪問,若希望某成員不在類外被直接訪問,卻能在派生類中被訪問,可將其定義為protected。由此可見,保護成員限定符是因繼承需求而產生的。
3. 對上述內容總結後可知:基類的私有成員在派生類中始終不可見;基類的其他成員在派生類中的訪問方式,等於“成員在基類的訪問限定符”與“繼承方式”二者中的較小值(訪問權限等級:public > protected > private)。
4. 使用class關鍵字時,默認的繼承方式為private;使用struct時,默認的繼承方式為public。不過,為清晰起見,最好顯式寫出繼承方式。
5. 在實際應用中,通常採用public繼承,很少且不提倡使用protected或private繼承。因為通過protected/private繼承得到的成員,僅能在派生類內部使用,會導致實際擴展和維護性較差。
4 繼承類模板
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;
}
模版是按需實例化的,調用了哪個成員函數,就實例化哪個。
像這裏,構造/析構/push_back會實例化,其他成員函數就不會實例化
二 基類和派生類之間的轉換
1 基礎類型轉換
通常情況下我們把一個類型的對象賦值給另一個類型的指針或者引用時,存在類型轉換,中間會產生臨時對象,所以需要加const,如:
int a=l;
const double&d = a;
2 基類和派生類轉換
public繼承中,是一個例外,派生類對象可以賦值給基類的指針/基類的引用,而不需要加const,這裏的指針和引用綁定的是派生類對象中的基類部分。也就意味着一個基類的指針或者引用,可能指向基類對象,也可能指向派生類對象。
public 繼承的派生類對象可以賦值給基類的指針或基類的引用。這裏有個形象的説法叫切片或者切割,意思是把派生類中屬於基類的那部分切出來,基類指針或引用指向的就是派生類中切出來的基類部分。
- 基類對象不能賦值給派生類對象。
- 基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用,但必須保證基類的指針指向的是派生類對象時,這種轉換才是安全的。如果基類是多態類型,可以使用 RTTI(Run-Time Type Information,運行時類型信息)的 dynamic_cast 來進行識別後再進行安全轉換。(注:這部分內容我們會在後面的類型轉換章節單獨專門講解,這裏先簡單提及)
3 示例
class Person
{
protected :
string _name; // 姓名
string _sex; // 性別
int _age; // 年齡
};
class Student : public Person
{
public :
int _No ; // 學號
};
int main()
{
Student sobj ;
// 1.派⽣類對象可以賦值給基類的指針/引⽤
Person* pp = &sobj;
Person& rp = sobj;
// 派⽣類對象可以賦值給基類的對象是通過調⽤後⾯會講解的基類的拷⻉構造完成的
Person pobj = sobj;
//2.基類對象不能賦值給派⽣類對象,這⾥會編譯報錯
sobj = pobj;
return 0;
}
三 繼承中的作用域
1 隱藏規則
1. 在繼承體系中,基類和派生類各自擁有獨立的作用域。
2. 當派生類與基類存在同名成員時,派生類的成員會屏蔽基類中同名成員的直接訪問,這種情況稱為隱藏。(在派生類的成員函數中,可通過“基類::基類成員”的方式顯式訪問基類的同名成員)
3. 需要注意的是,對於成員函數的隱藏,只要函數名稱相同就會構成隱藏,與參數列表等無關。
4. 實際應用中,在繼承體系裏最好不要定義同名成員,以避免混淆和錯誤。
示例:
// Student的_num和Person的_num構成隱藏關係,可以看出這樣代碼雖然能跑,但是⾮常容易混淆
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;
};
2 考察繼承作用域相關選擇題
1、A和B類中的兩個func構成什麼關係()
A. 重載 B. 隱藏 C. 沒關係
2、下面程序的編譯運行結果是什麼()
A. 編譯報錯 B. 運行報錯 C. 正常運行
答案是:B A
解析:第一題很有可能會判斷成函數重載,但是注意!:函數重載要求在同一作用域。但是顯然,基類和派生類不在同一作用域。
第二題:b對應的是class B ,當發現參數不匹配的時候,會直接編譯報錯,而不是去基類中找
四 派生類的默認成員函數
1 四個常見默認成員函數
在派生類中,這幾個函數怎麼生成呢?
2 核心生成機制
1、派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
2、派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
3、派生類的operator=必須要調用基類的operator=完成基類的複製。需要注意的是派生類的
operator=隱藏了基類的operator=,所以顯示調用基類的operator=,需要指定基類作用域4、派生類的析構函數會在被調用完成後自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
5、派生類對象初始化先調用基類構造再調派生類構造。
6、派生類對象析構清理先調用派生類析構再調基類的析構。
7、因為多態中一些場景析構函數需要構成重寫,重寫的條件之一是函數名相同(這個我們多態章節會講解)。那麼編譯器會對析構函數名進行特殊處理,處理成destructor(),所以基類析構函數不加virtual的情況下,派生類析構函數和基類析構函數構成隱藏關係。
3 構造函數與析構函數
class Person
{
public:
Person(const char* name)
:_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; // 姓名
};
1、我們不寫,默認生成的函數行為是什麼?是否符合需求
2、不符合,我們要自己實現,如何實現?
本質:可以把派生類當做多一個的自定義類型成員變量(基類)的普通類——
class Student : public Person
{
public:
Student(const char* name = "張三", int num = 18, const char* address = "武漢")
:Person(name)
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
// 本質:可以把派生類當做多一個的自定義類型成員變量(基類)的普通類
Student(const Student& s)
:Person(s)
, _num(s._num)
, _address(s._address)
{
// 涉及到深拷貝,需要自己實現
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
_address = s._address;
}
return *this;
// 涉及到深拷貝,需要自己實現
}
~Student()
{
//// 不用顯示調用基類析構,編譯器會在派生類析構結束之後自動調用析構
//Person::~Person();
//// ...
// 隱式原因:為了安全性
cout << "~Student()" << endl;
}
protected:
int _num; // 學號
string _address; // 地址
//int* _ptr;
};
1 繼承的基類成員變量(整體對象)+ 自己的成員變量(遵循普通的規則,跟類和對象部分一樣)
2 默認生成的構造,派生類自己的成員,內置類型不確定,自定義類型調用默認構造,基類部分調用默認構造
3 本質上可以把派生類當做多了自定義類型成員變量(基類)的普通類總,跟普通類原則一樣。
4 派生類一般要自己的實現構造,不顯示寫析構、拷貝析構、賦值重載,除非派生類有深拷貝的資源。
析構構成了隱藏,需要指定類域(見處理機制第七點)---->為了補多態的坑
構造先父後子 析構先子後父
構造先父後子是因為:初始化列表的時候,是按照放在內存中的順序放的。
析構先子後父是因為:如果先父的話,析構完了父就會銷燬,但是子還能訪問父,就會造成野指針