摘要:例如,在關鍵字為的派生類當中,所繼承的基類成員的訪問方式變為。繼承中的作用域在繼承體系中的基類和派生類都有獨立的作用域。為了避免類似問題,實際在繼承體系當中最好不要定義同名的成員。
繼承(inheritance)機制是面向對象程序設計使代碼可以復用的重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱為派生類。
繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的復用都是函數復用,而繼承便是類設計層次的復用。
例如,以下代碼中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; //工號};
繼承后,父類Person的成員,包括成員函數和成員變量,都會變成子類的一部分,也就是說,子類Student和Teacher復用了父類Person的成員。
繼承的定義格式如下:
說明: 在繼承當中,父類也稱為基類,子類是由基類派生而來的,所以子類又稱為派生類。
我們知道,訪問限定符有以下三種:
而繼承的方式也有類似的三種:
基類當中被不同訪問限定符修飾的成員,以不同的繼承方式繼承到派生類當中后,該成員最終在派生類當中的訪問方式將會發生變化。
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
稍作觀察,實際上基類成員訪問方式的變化規則也不是無跡可尋的,我們可以認為三種訪問限定符的權限大小為:public > protected > private,基類成員訪問方式的變化規則如下:
基類的private成員在派生類當中不可見是什么意思?
這句話的意思是,我們無法在派生類當中訪問基類的private成員。例如,雖然Student類繼承了Person類,但是我們無法在Student類當中訪問Person類當中的private成員_name。
//基類class Person{private: string _name = "張三"; //姓名};//派生類class Student : public Person{public: void Print() { //在派生類當中訪問基類的private成員,error! cout << _name << endl; }protected: int _stuid; //學號};
也就是說,基類的private成員無論以什么方式繼承,在派生類中都是不可見的,這里的不可見是指基類的私有成員雖然被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
因此,基類的private成員在派生類中是不能被訪問的,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就需要定義為protected,由此可以看出,protected限定符是因繼承才出現的。
注意: 在實際運用中一般使用的都是public繼承,幾乎很少使用protected和private繼承,也不提倡使用protected和private繼承,因為使用protected和private繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
在使用繼承的時候也可以不指定繼承方式,使用關鍵字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; //學號};
注意: 雖然繼承時可以不指定繼承方式而采用默認的繼承方式,但還是最好顯示的寫出繼承方式。
派生類對象可以賦值給基類的對象、基類的指針以及基類的引用,因為在這個過程中,會發生基類和派生類對象之間的賦值轉換。
例如,對于以下基類及其派生類。
//基類class Person{protected: string _name; //姓名 string _sex; //性別 int _age; //年齡};//派生類class Student : public Person{protected: int _stuid; //學號};
代碼當中可以出現以下邏輯:
Student s;Person p = s; //派生類對象賦值給基類對象Person* ptr = &s; //派生類對象賦值給基類指針Person& ref = s; //派生類對象賦值給基類引用
對于這種做法,有個形象的說法叫做切片/切割,寓意把派生類中基類那部分切來賦值過去。
派生類對象賦值給基類對象圖示:
派生類對象賦值給基類指針圖示:
派生類對象賦值給基類引用圖示:
注意: 基類對象不能賦值給派生類對象,基類的指針可以通過強制類型轉換賦值給派生類的指針,但是此時基類的指針必須是指向派生類的對象才是安全的。
在繼承體系中的基類和派生類都有獨立的作用域。若子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。
例如,對于以下代碼,訪問成員_num時將訪問到子類當中的_num。
#include #include 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(); //999 return 0;}
若此時我們就是要訪問父類當中的_num成員,我們可以使用作用域限定符進行指定訪問。
void fun(){ cout << Person::_num << endl; //指定訪問父類當中的_num成員}
需要注意的是,如果是成員函數的隱藏,只需要函數名相同就構成隱藏。
例如,對于以下代碼,調用成員函數fun時將直接調用子類當中的fun,若想調用父類當中的fun,則需使用作用域限定符指定類域。
#include #include using namespace std;//父類class Person{public: void fun(int x) { cout << x << endl; }};//子類class Student : public Person{public: void fun(double x) { cout << x << endl; }};int main(){ Student s; s.fun(3.14); //直接調用子類當中的成員函數fun s.Person::fun(20); //指定調用父類當中的成員函數fun return 0;}
特別注意: 代碼當中,父類中的fun和子類中的fun不是構成函數重載,因為函數重載要求兩個函數在同一作用域,而此時這兩個fun函數并不在同一作用域。為了避免類似問題,實際在繼承體系當中最好不要定義同名的成員。
默認成員函數,即我們不寫編譯器會自動生成的函數,類當中的默認成員函數有以下六個:
下面我們看看派生類當中的默認成員函數,與普通類的默認成員函數的不同之處。
例如,我們以下面這個Person類為基類。
//基類class Person{public: //構造函數 Person(const string& 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; }private: string _name; //姓名};
我們用該基類派生出Student類,Student類當中的默認成員函數的基本邏輯如下:
//派生類class Student : public Person{public: //構造函數 Student(const string& name, int id) :Person(name) //調用基類的構造函數初始化基類的那一部分成員 , _id(id) //初始化派生類的成員 { cout << "Student()" << endl; } //拷貝構造函數 Student(const Student& s) :Person(s) //調用基類的拷貝構造函數完成基類成員的拷貝構造 , _id(s._id) //拷貝構造派生類的成員 { cout << "Student(const Student& s)" << endl; } //賦值運算符重載函數 Student& operator=(const Student& s) { cout << "Student& operator=(const Student& s)" << endl; if (this != &s) { Person::operator=(s); //調用基類的operator=完成基類成員的賦值 _id = s._id; //完成派生類成員的賦值 } return *this; } //析構函數 ~Student() { cout << "~Student()" << endl; //派生類的析構函數會在被調用完成后自動調用基類的析構函數 }private: int _id; //學號};
派生類與普通類的默認成員函數的不同之處概括為以下幾點:
在編寫派生類的默認成員函數時,需要注意以下幾點:
destructor();
。因此,派生類和基類的析構函數也會因為函數名相同構成隱藏,若是我們需要在某處調用基類的析構函數,那么就要使用作用域限定符進行指定調用。operator=
當中調用基類的拷貝構造函數和operator=
的傳參方式是一個切片行為,都是將派生類對象直接賦值給基類的引用。說明一下:
基類的構造函數、拷貝構造函數、賦值運算符重載函數我們都可以在派生類當中自行進行調用,而基類的析構函數是當派生類的析構函數被調用后由編譯器自動調用的,我們若是自行調用基類的構造函數就會導致基類被析構多次的問題。
我們知道,創建派生類對象時是先創建的基類成員再創建的派生類成員,編譯器為了保證析構時先析構派生類成員再析構基類成員的順序析構,所以編譯器會在派生類的析構函數被調用后自動調用基類的析構函數。
友元關系不能繼承,也就是說基類的友元可以訪問基類的私有和保護成員,但是不能訪問派生類的私有和保護成員。
例如,以下代碼中Display函數是基類Person的友元,當時Display函數不是派生類Student的友元,即Display函數無法訪問派生類Student當中的私有和保護成員。
#include #include using namespace std;class Student;class Person{public: //聲明Display是Person的友元 friend void Display(const Person& p, const Student& s);protected: string _name; //姓名};class Student : public Person{protected: int _id; //學號};void Display(const Person& p, const Student& s){ cout << p._name << endl; //可以訪問 cout << s._id << endl; //無法訪問}int main(){ Person p; Student s; Display(p, s); return 0;}
若想讓Display函數也能夠訪問派生類Student的私有和保護成員,只能在派生類Student當中進行友元聲明。
class Student : public Person{public: //聲明Display是Student的友元 friend void Display(const Person& p, const Student& s);protected: int _id; //學號};
若基類當中定義了一個static靜態成員變量,則在整個繼承體系里面只有一個該靜態成員。無論派生出多少個子類,都只有一個static成員實例。
例如,在基類Person當中定義了靜態成員變量_count,盡管Person又繼承了派生類Student和Graduate,但在整個繼承體系里面只有一個該靜態成員。
我們若是在基類Person的構造函數和拷貝構造函數當中設置_count進行自增,那么我們就可以隨時通過_count來獲取該時刻已經實例化的Person、Student以及Graduate對象的總個數。
#include #include using namespace std;//基類class Person{public: Person() { _count++; } Person(const Person& p) { _count++; }protected: string _name; //姓名public: static int _count; //統計人的個數。};int Person::_count = 0; //靜態成員變量在類外進行初始化//派生類class Student : public Person{protected: int _stuNum; //學號};//派生類class Graduate : public Person{protected: string _seminarCourse; //研究科目};int main(){ Student s1; Student s2(s1); Student s3; Graduate s4; cout << Person::_count << endl; //4 cout << Student::_count << endl; //4 return 0;}
此時我們也可以通過打印Person類和Student類當中靜態成員_count的地址來證明它們就是同一個變量。
cout << &Person::_count << endl; //00F1F320cout << &Student::_count << endl; //00F1F320
單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承。
多繼承:一個子類有兩個或兩個以上直接父類時稱這個繼承關系為多繼承。
菱形繼承:菱形繼承是多繼承的一種特殊情況。
從菱形繼承的模型構造就可以看出,菱形繼承的繼承方式存在數據冗余和二義性的問題。
例如,對于以上菱形繼承的模型,當我們實例化出一個Assistant對象后,訪問成員時就會出現二義性問題。
#include #include using namespace std;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成員會出現訪問不明確的報錯。
對于此,我們可以顯示指定訪問Assistant哪個父類的_name成員。
//顯示指定訪問哪個父類的成員a.Student::_name = "張同學";a.Teacher::_name = "張老師";
雖然該方法可以解決二義性的問題,但仍然不能解決數據冗余的問題。因為在Assistant的對象在Person成員始終會存在兩份。
為了解決菱形繼承的二義性和數據冗余問題,出現了虛擬繼承。如前面說到的菱形繼承關系,在Student和Teacher繼承Person是使用虛擬繼承,即可解決問題。
虛擬繼承代碼如下:
#include #include using namespace std;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成員,訪問到的都是同一個結果,解決了二義性的問題。
cout << a.Student::_name << endl; //petercout << a.Teacher::_name << endl; //peter
而我們打印Assistant的Student父類和Teacher父類的_name成員的地址時,顯示的也是同一個地址,解決了數據冗余的問題。
cout << &a.Student::_name << endl; //0136F74Ccout << &a.Teacher::_name << endl; //0136F74C
在此之前,我們先看看不使用菱形虛擬繼承時,以下菱形繼承當中D類對象的各個成員在內存當中的分布情況。
測試代碼如下:
#include using namespace std;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;
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/122420.html
摘要:繼承繼承,就是子類繼承父親的特征和行為,使得子類具有父類的成員變量和方法。此時,被繼承的類稱為父類或基類,而繼承的類稱為子類或派生類。,如果存在繼承關系的時候,和就不一樣了基類中的成員可以在派生類中使用,但是基類中的成員不能再派生類中使用。 ...
摘要:博主在公眾號后臺設置了關鍵字回復,回復下面的里面的內容,可免費獲得學習視頻和資料。 博主在公眾號后臺設置了關鍵字回復, 回復下面的【】里面的內容, 可免費獲得C++學習視頻和資料。 如回復:C++基礎 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? 【C++】 【1】...
摘要:類的定義假如要定義一個類,表示二維的坐標點最最基本的就是方法,相當于的構造函數。嚴格來講,并不支持多態。靜態類型的缺失,讓很難實現那樣嚴格的多態檢查機制。有時候,需要在子類中調用父類的方法。 類的定義 假如要定義一個類 Point,表示二維的坐標點: # point.py class Point: def __init__(self, x=0, y=0): se...
閱讀 1241·2021-11-08 13:25
閱讀 1440·2021-10-13 09:40
閱讀 2773·2021-09-28 09:35
閱讀 735·2021-09-23 11:54
閱讀 1123·2021-09-02 15:11
閱讀 2430·2019-08-30 13:18
閱讀 1668·2019-08-30 12:51
閱讀 2685·2019-08-29 18:39