asp網(wǎng)站開發(fā)環(huán)境同仁seo排名優(yōu)化培訓(xùn)
文章目錄
目錄
一、繼承的概念及定義
1.1繼承的概念
1.2 繼承定義
1.2.1定義格式
1.2.2繼承關(guān)系和訪問限定符
?1.2.3繼承基類成員訪問方式的變化
二、基類和派生類對象賦值轉(zhuǎn)換
三、繼承中的作用域
四、派生類的默認(rèn)成員函數(shù)
一、繼承的概念及定義
1.1繼承的概念
繼承(inheritance)機(jī)制是面向?qū)ο蟪绦蛟O(shè)計使代碼可以復(fù)用的最重要的手段,它允許程序員在保持原有類特性的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,這樣產(chǎn)生新的類,稱派生類。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計的層次結(jié)構(gòu),體現(xiàn)了由簡單到復(fù)雜的認(rèn)知過程。以前我們接觸的復(fù)用都是函數(shù)復(fù)用,繼承是類設(shè)計層次的復(fù)用。
我們把需要共用的數(shù)據(jù)和方法提取到一個類中,我們就把這個類叫做父類或基類;我們把繼承父類的所有屬性和行為,并且可以根據(jù)自己需要定義自己的屬性和行為的類叫做子類或派生類。
#include <iostream>
#include <string>using namespace std;class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}string _name = "peter"; // 姓名int _age = 18; // 年齡
};// 繼承后父類的Person的成員(成員函數(shù)+成員變量)都會變成子類的一部分。
// 這里體現(xiàn)出了Student和Teacher復(fù)用了Person的成員。
// 我們使用監(jiān)視窗口查看Student和Teacher對象,可以看到變量的復(fù)用。
// 調(diào)用Print可以看到成員函數(shù)的復(fù)用。
class Student : public Person
{
protected:int _stuid; // 學(xué)號
};class Teacher : public Person
{
protected:int _jobid; // 工號
};int main()
{Student s;s._name = "張三"; s._age = 18;s.Print();Teacher t;t._name = "王老師";t._age = 33;t.Print();return 0;
}
運(yùn)行結(jié)果:
1.2 繼承定義
1.2.1定義格式
下面我們看到Person是父類,也稱作基類。Student是子類,也稱作派生類,冒號后面的public就是繼承方式。
1.2.2繼承關(guān)系和訪問限定符
?1.2.3繼承基類成員訪問方式的變化
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
基類的public成員 | 派生類的public成員 | 派生類的protected 成員 | 派生類的private 成員 |
基類的protected 成員 | 派生類的protected 成員 | 派生類的protected 成員 | 派生類的private 成員 |
基類的private成 員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
總結(jié):
- 基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
- 基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected??梢钥闯霰Wo(hù)成員限定符是因繼承才出現(xiàn)的。
- 實際上面的表格我們進(jìn)行一下總結(jié)會發(fā)現(xiàn),基類的私有成員在子類都是不可見?;惖钠渌蓡T子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public>protected>private。
- 使用關(guān)鍵字class時默認(rèn)的繼承方式是private,使用struct時默認(rèn)的繼承方式是public,不過最好顯示的寫出繼承方式。
- 在實際運(yùn)用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里面使用,實際中擴(kuò)展維護(hù)性不強(qiáng)。
特別注意:protected/private成員 表示在類外面不能訪問,在類里面可以訪問;而不可見 表示影身,在類的里面和外面都不能訪問。
// 實例演示三種繼承關(guān)系下基類成員的各類型成員訪問關(guān)系的變化
class Person
{
public:void Print(){cout << _name << endl;}
protected:string _name = "張三"; // 姓名
private:int _age = 18; // 年齡
};//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:int _stunum; // 學(xué)號
};int main()
{Student s;s._name = "李四"; // 保護(hù)成員遇到共有繼承,繼承后依然是保護(hù)(報錯)s._age = 20; // 私有成員遇到共有繼承,繼承后依然是私有(報錯)s.Print(); // 共有成員遇到共有繼承,繼承后是共有(不報錯)return 0;
}
編譯器報錯內(nèi)容:
二、基類和派生類對象賦值轉(zhuǎn)換
- 派生類對象?可以賦值給?基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割(不能當(dāng)成隱式類型轉(zhuǎn)換)。寓意把派生類中父類那部分切來賦值過去。
- 基類對象不能賦值給派生類對象。
- 基類的指針或者引用可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。這里基類如果是多態(tài)類型,可以使用RTTI(RunTime Type Information)的?dynamic_cast 來進(jìn)行識別后進(jìn)行安全轉(zhuǎn)換。
小技巧:
在公有繼承中,子類和父類的關(guān)系可以看成?is a?的關(guān)系,每一個子類對象都是一個特殊的父類對象。
代碼示例:
class Person
{
protected:string _name; // 姓名string _sex; // 性別int _age; // 年齡
};class Student : public Person
{
public:int _No; // 學(xué)號
};int main()
{Student sobj;// 1.子類對象可以賦值給父類對象/指針/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;// 2.基類對象不能賦值給派生類對象sobj = pobj; // 報錯// 3.基類的指針可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針pp = &sobj;Student * ps1 = (Student*)pp; // 這種情況轉(zhuǎn)換時可以的。ps1->_No = 10;pp = &pobj;Student* ps2 = (Student*)pp; // 這種情況轉(zhuǎn)換時雖然可以,但是會存在越界訪問的問題ps2->_No = 10;return 0;
}
三、繼承中的作用域
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數(shù)中,可以使用 基類::基類成員 顯示訪問)
- 需要注意的是如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構(gòu)成隱藏。
- 注意在實際中在繼承體系里面最好不要定義同名的成員。
例一
// Student的_num和Person的_num構(gòu)成隱藏關(guān)系,可以看出這樣代碼雖然能跑,但是非常容易混淆
class Person
{
protected:string _name = "小黑子"; // 姓名int _num = 111; // 身份證號
};
class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl; // 打印Person的_namecout << " 身份證號:" << Person::_num << endl; // 打印Person的_numcout << " 學(xué)號:" << _num << endl; // 打印Student的_num}
protected:int _num = 999; // 學(xué)號
};int main()
{Student s1;s1.Print();return 0;
}
運(yùn)行結(jié)果:
?例二(易錯題)
// 提問:A和B的fun函數(shù)構(gòu)成什么關(guān)系class A
{
public:void fun(){cout << "func()" << endl;}
};class B : public A
{
public:void fun(int i){A::fun();cout << "func(int i)->" << i << endl;}
};int main()
{B b;b.fun(10); // 默認(rèn)訪問B中的fun函數(shù),A中的fun函數(shù)在B中被隱藏了cout << "-------------" << endl;b.A::fun(); // 想要訪問A中的fun函數(shù),就需要指定作用域return 0;
}// 答案:
// B中的fun和A中的fun不是構(gòu)成重載,因為不是在同一作用域
// B中的fun和A中的fun構(gòu)成隱藏,成員函數(shù)滿足函數(shù)名相同就構(gòu)成隱藏。
運(yùn)行結(jié)果:
四、派生類的默認(rèn)成員函數(shù)
6個默認(rèn)成員函數(shù),“默認(rèn)”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類中,這幾個成員函數(shù)是如何生成的呢?
- 派生類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員。如果基類沒有默認(rèn)的構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
- 派生類的拷貝構(gòu)造函數(shù)必須調(diào)用基類的拷貝構(gòu)造完成基類的拷貝初始化。
- 派生類的operator=必須要調(diào)用基類的operator=完成基類的復(fù)制。
- 派生類的析構(gòu)函數(shù)會在被調(diào)用完成后自動調(diào)用基類的析構(gòu)函數(shù)清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
- 派生類對象初始化先調(diào)用基類構(gòu)造再調(diào)派生類構(gòu)造。
- 派生類對象析構(gòu)清理先調(diào)用派生類析構(gòu)再調(diào)基類的析構(gòu)。
- 因為后續(xù)一些場景析構(gòu)函數(shù)需要構(gòu)成重寫,重寫的條件之一是函數(shù)名相同。那么編譯器會對析構(gòu)函數(shù)名進(jìn)行特殊處理,處理成destrutor(),所以父類析構(gòu)函數(shù)不加virtual的情況下,子類析構(gòu)函數(shù)和父類析構(gòu)函數(shù)構(gòu)成隱藏關(guān)系。
?調(diào)用順序:
代碼示例:
// 父類
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) // 父類沒有默認(rèn)構(gòu)造,就只能在初始化列表階段顯示調(diào)用, _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s) // 賦值轉(zhuǎn)換,子類通過切片傳給父類, _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); // 調(diào)用父類的賦值重載時,需要聲明作用域,因為父類的賦值重載被隱藏了_num = s._num;}return *this;}~Student(){cout << "~Student()" << endl;}protected:int _num; //學(xué)號
};int main()
{Student s1("jack", 18);Student s2(s1);Student s3("rose", 17);s1 = s3;return 0;
}
運(yùn)行結(jié)果:
特別注意1:
子類的析構(gòu)函數(shù)跟父類的析構(gòu)函數(shù)會構(gòu)成隱藏。為什么呢?
由于后面多態(tài)的需要,析構(gòu)函數(shù)名會統(tǒng)一處理成destructtor(),這樣就導(dǎo)致子類和父類的析構(gòu)函數(shù)名相同,所以就構(gòu)成了隱藏。
特別注意2:
如果我們把上面子類的析構(gòu)函數(shù)代碼改成,顯示調(diào)用父類析構(gòu)函數(shù),這時會發(fā)生什么呢?
~Student()
{Person::~Person(); // 顯示調(diào)用父類的析構(gòu)函數(shù)cout << "~Student()" << endl;
}
運(yùn)行結(jié)果:
?這時我們就會發(fā)現(xiàn),明明只創(chuàng)建了3個對象,卻調(diào)用了6次父類的析構(gòu)函數(shù),這是為什么呢?
這是因為,在每個子類析構(gòu)函數(shù)后面,編譯器會自動調(diào)用父類的析構(gòu)函數(shù),不需要自己顯示調(diào)用。這樣才能保證先析構(gòu)子類,再析構(gòu)父類(具體看“特別注意3”)
特別注意3:先定義的對象會后析構(gòu),后定義的對象會先析構(gòu),因為這些對象是存在棧上的。所以上面的代碼是先析構(gòu)s3再析構(gòu)s2最后析構(gòu)s1。所以就有這樣的一個規(guī)定:子類繼承的父類,父類的部分先構(gòu)造,子類的部分再構(gòu)造;子類的部分先析構(gòu),父類的部分再析構(gòu)。(這也就是不讓你顯示調(diào)用父類析構(gòu)函數(shù)的原因,因為這樣就無法保證析構(gòu)的順序了)
總結(jié):其他的父類默認(rèn)成員函數(shù)可以顯示的調(diào)用,只有父類的析構(gòu)函數(shù)不需要顯示調(diào)用。