hello hello~ ,這里是大耳朵土土垚~💖💖 ,歡迎大家點贊🥳🥳關注💥💥收藏🌹🌹🌹
💥個人主頁:大耳朵土土垚的博客
💥 所屬專欄:C++入門至進階
這里將會不定期更新有關C++的內容,希望大家多多點贊關注收藏💖💖
目錄
- 1.繼承的概念
- 2.繼承定義
- 2.1定義格式
- 2.2訪問限定符
- 2.3繼承方式
- 3.基類和派生類對象賦值轉換
- 4.繼承中的重定義(隱藏)
- 5.派生類的默認成員函數
- ?構造函數
- ?拷貝構造
- ?賦值運算符重載
- ?析構函數
- 6.結語
1.繼承的概念
繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類或子類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的復用都是函數復用,繼承是類設計層次的復用。
2.繼承定義
2.1定義格式
下面我們看到Person是父類,也稱作基類。Student是子類,也稱作派生類。
//基類或父類
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter";// 姓名int _age = 18; //年齡
};//派生類或子類
class Student : public Person
{
protected:int _stuid; // 學號int _major; //專業
};
2.2訪問限定符
C++類的訪問限定符用于控制類的成員(包括成員變量和成員函數)在類的外部的可訪問性。C++中有以下三種訪問限定符:
-
public: 公共訪問限定符,任何地方都可以訪問公共成員。可以在類的外部使用對象名和成員名直接訪問公共成員。
-
private: 私有訪問限定符,只有類內部的其他成員函數可以訪問私有成員。類的外部無法直接訪問私有成員,但可以通過公共成員函數間接訪問私有成員。
-
protected: 保護訪問限定符,只有類內部的其他成員函數和派生類的成員函數可以訪問保護成員。類的外部無法直接訪問保護成員,但可以通過公共成員函數或派生類的成員函數間接訪問保護成員。
需要注意的是,訪問限定符只在類的內部起作用,在類的外部沒有直接的影響。同時,訪問限定符可以用于類的成員變量和成員函數的聲明中,默認情況下,成員變量和成員函數的訪問限定符是private。
2.3繼承方式
C++類的繼承方式有以下幾種:
- 公有繼承(public inheritance):使用關鍵字"public"表示的繼承方式。在公有繼承中,基類的公有成員和保護成員都可以在派生類中訪問,私有成員不能在派生類中直接訪問。
class Base {
public:// 公有成員
protected:// 保護成員
private:// 私有成員
};class Derived : public Base {// 公有繼承
};
- 保護繼承(protected inheritance):使用關鍵字"protected"表示的繼承方式。在保護繼承中,基類的公有成員和保護成員在派生類中都變為保護成員,私有成員不能在派生類中直接訪問。
class Base {
public:// 公有成員
protected:// 保護成員
private:// 私有成員
};class Derived : protected Base {// 保護繼承
};
- 私有繼承(private inheritance):使用關鍵字"private"表示的繼承方式。在私有繼承中,基類的公有成員和保護成員在派生類中都變為私有成員,私有成員不能在派生類中直接訪問。
class Base {
public:// 公有成員
protected:// 保護成員
private:// 私有成員
};class Derived : private Base {// 私有繼承
};
總結如下:
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
①基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private
這些繼承方式可以根據具體的需求選擇合適的方式
②基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
例如:
//基類或父類
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}//保護成員
protected:string _name = "tutu";// 姓名int _age = 20; //年齡//私有成員
private:string _tele = "123456";};//派生類或子類
class Student : public Person
{
public:void sPrint(){Person::Print(); //可以使用父類的公有成員}
protected:int _stuid; // 學號string _sname = _name;//可以訪問父類的保護成員_namestring _stele = _tele; //不可以訪問父類的私有成員_tele
};
結果如下:
上述父類Person中成員有三種訪問限定分別是public、protected、private,而子類Student使用public繼承父類,那么對于父類的公有成員在子類中的訪問方式還是public,protected成員訪問方式選擇繼承方式public和protected中較小的protected,同理父類的private成員繼承到子類中也是選擇private方式,在子類中不可訪問
對于私有成員也是被繼承到子類中,只是不可訪問:
③基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現的。
④ 使用關鍵字class
時默認的繼承方式是private
,使用struct
時默認的繼承方式是public
,不過最好顯示的寫出繼承方式。
⑤在實際運用中一般使用都是public
繼承,幾乎很少使用protetced/private繼承,也不提倡使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
3.基類和派生類對象賦值轉換
- 派生類對象可以賦值給基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
如下圖所示:
- 基類對象不能賦值給派生類對象
例如下面代碼:
//父類
class Person
{
protected:string _name; // 姓名string _sex; //性別int _age; // 年齡
};
//子類
class Student : public Person
{
public:int _No; // 學號
};
void Test()
{Student sobj;// 1.子類對象可以賦值給父類對象/指針/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;//2.基類對象不能賦值給派生類對象sobj = pobj;//error
}
4.繼承中的重定義(隱藏)
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員將屏蔽對父類同名成員的直接訪問,這種情況叫隱藏,也叫重定義。
當一個類繼承另一個類時,它可以重定義繼承的成員函數或者成員變量。
需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。
- 如果要訪問被隱藏的父類的同名成員,可以在子類成員函數中,使用
父類::父類成員
來顯示訪問
注意在實際中在繼承體系里面最好不要定義同名的成員。
例如:
// Student的_num和Person的_num構成隱藏關系,可以看出這樣代碼雖然能跑,但是非常容易混淆
//父類
class Person
{
protected:string _name = "胡土土"; // 姓名int _num = 1234; // 身份證號
};//子類
class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份證號:" << Person::_num << endl;cout << " 學號:" << _num << endl;}
protected:int _num = 999; // 學號,與父類的_num重名構成隱藏
};void Test()
{Student s1;s1.Print();
};
結果如下:
我們發現當子類與父類有隱藏關系時,對于同名變量_num的調用,除非顯示使用
Person::_num
調用的是父類的成員變量,其他情況_num表示的都得子類中定義的變量,這是因為它們有不同的作用域,在子類中調用變量都是先從子類這個作用域中尋找。
再看下面的例子:
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;}
};void Test()
{B b;b.fun(10);
}
這里 B中的fun和A中的fun不是構成重載,因為不是在同一作用域
B中的fun和A中的fun構成隱藏,成員函數滿足函數名相同就構成隱藏。
結果如下:
如果Test函數中:
void Test()
{B b;b.fun();//這里沒有給參數
}
結果如下:
使用對象b調用fun()沒有給參數,這樣編譯是不通過的,因為這樣調用是調用的類B中的成員函數fun是需要傳參的,如果要調用基類中的fun函數就必須顯示調用,代碼如下:
void Test()
{B b;b.A::fun();//顯示調用A中的fun函數
}
結果如下:
5.派生類的默認成員函數
6個默認成員函數,“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類中,(先不考慮取地址重載)這幾個成員函數是如何生成的呢?
例如如下父類:
//有如下Person父類
class Person
{
public:Person(const char* name = "tutu"): _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; // 姓名
};
?構造函數
- 派生類的構造函數必須調用基類的默認構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用基類的構造函數。
//基于上面Person的派生類Student
class Student : public Person
{
protected:int _num; //學號
};
int main()
{Student s;return 0;
}
結果如下:
我們發現對于父類中的成員它會自動調用父類Person的默認構造函數與析構函數
- 如果父類Person沒有默認構造函數,那么我們就需要在初始化列表里顯示調用父類的構造函數
例如:
當我們將基類的默認構造函數中的缺省值"tutu",去掉,它就不再是默認構造函數,那么在創建子類Student對象時就不會自動調用默認構造函數,會保錯,那么這時我們就需要在初始化列表里顯示調用
代碼如下:
class Student : public Person
{
public:Student(const char* name, int num):Person(name) //顯示調用父類構造函數, _num(num){}
protected:int _num; //學號
};int main()
{Student s("tutu", 111);;return 0;
}
結果如下:
還有一種顯示調用情況:
這種情況是不可取的,這是因為規定在初始化列表中是不可以使用父類的成員的
?拷貝構造
- 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
默認生成拷貝構造一般情況下夠用,只有當子類成員涉及深拷貝時就必須自己實現拷貝構造
這里也可以自己顯示實現一下拷貝構造:
class Student : public Person
{
public://構造函數Student(const char* name, int num):Person(name) //顯示調用父類構造函數, _num(num){} //拷貝構造Student(const Student& st):Person(st) //利用前面學習的基類與派生類的賦值轉換,_num(st._num){}
protected:int _num; //學號
};
注意這里
Person(st)
中調用Person中的拷貝構造實現賦值兼容
?賦值運算符重載
- 派生類的
operator=
必須要調用基類的operator=
完成基類的復制。
?析構函數
- 派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
如果自己顯示寫析構函數:
//析構函數
~Student()
{~Person();//這樣寫是錯誤的
}
因為多態的原因,析構函數的名字會被統一處理為destructor(),所以這里調用會構成隱藏,會循環調用,所以要指定作用域:
//析構函數
~Student()
{Person::~Person();cout << "~Student()" << endl;
}
但是我們發現Person的析構函數居然調用了兩次:
這是因為析構函數具有特殊性,在子類析構函數調用完之后會自動調用父類的析構函數,所以即便是自己顯示實現了子類的析構函數也不需要自己主動調用父類的析構函數
所以不需要自己主動調用父類的析構函數,否則會報錯
其核心原因在于初始化時先構造父類再構造子類,而析構時先析構子類再析構父類,因為子類析構時是可能用到父類成員的,先父后子可能會出錯
所以為了保證先析構子類再析構父類,編譯器會在析構了子類后自動調用父類的析構函數
總結如下:
默認成員函數\子類成員 | 內置成員 | 自定義成員 | 子類中的父類成員(整體) |
---|---|---|---|
默認生成的構造 | 不做處理 | 調用自定義類型的默認構造 | 調用父類的默認構造 |
默認生成的拷貝構造 | 值拷貝 | 調用自定義類型的拷貝構造 | 調用父類的拷貝構造 |
默認生成的賦值重載 | 直接賦值 | 調用自定義類型的賦值重載 | 調用父類的賦值重載 |
默認生成的析構函數 | 不做處理 | 調用自定義類型的析構函數 | 自動調用父類的析構函數 |
對于構造和析構:
派生類對象初始化先調用基類構造再調派生類構造。
派生類對象析構清理先調用派生類析構再調基類的析構
6.結語
繼承可以分為公有繼承(public inheritance)、保護繼承(protected inheritance)和私有繼承(private inheritance)。繼承在C++中的應用非常廣泛,可以用于構建復雜的類層次結構,提供代碼的復用性和靈活性。但是,在使用繼承時也需要注意避免多層次的繼承導致的類關系復雜性增加,以及合理設計基類和派生類之間的關系。以上就是今天的所以內容啦~ 完結撒花~ 🥳🎉🎉