C++ 繼承 | 對象切割、菱形繼承、虛繼承、對象組合

文章目錄

  • 繼承
    • 繼承的概念
    • 繼承方式及權限
    • using改變成員的訪問權限
    • 基類與派生類的賦值轉換
    • 回避虛函數機制
    • 派生類的默認成員函數
    • 友元與靜態成員
  • 多繼承
    • 菱形繼承
    • 虛繼承
  • 組合


繼承

繼承的概念

繼承可以使得子類具有父類的屬性和方法或者重新定義、追加屬性和方法等。

當創建一個類時,我們可以繼承一個已有類的成員和方法,并且在原有的基礎上進行提升,這個被繼承的類叫做基類,而這個繼承后新建的類叫做派生類。基類必須是已經定義而非僅僅聲明,因此,一個類不能派生它本身。

繼承這種通過生成子類的復用通常被稱為 白箱復用(white-box reuse) 。術語 白箱 是相對可視性而言:在繼承方式中,父類的內部細節對子類可見。

派生類的作用域嵌套在基類的作用域之內。

class [派生類名] : [繼承類型] [基類名]

[繼承類型] [基類名] 的組合被稱為派生列表,值得注意的是,派生列表僅出現在定義中,而不能出現在聲明中:

class A : public B; // ERROR:派生列表不能出現在聲明中

正確實現如下:

class Human
{
public:Human(string name = "張三", int age = 18) : _name(name), _age(age){}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name;int _age;
};class Student : public Human
{
public:Student(string stuNum = "123456") : Human(), _stuNum(stuNum){}void Print() // 將父類的Print函數重定向成自己的Print函數{Human::Print();cout << "stuNum:" << _stuNum << endl;}protected:string _stuNum; // 增加的成員變量
};int main()
{Human h1;Student s1;h1.Print();cout << endl;s1.Print();return 0;
}

基類和派生類都具有他們各自的作用域,那如果出現同名的成員(如上面的 Print函數 ),此時會怎么樣呢?這里就要牽扯到一個概念——隱藏。(隱藏而非 重載 ,方法名雖然相同,但處于不同的作用域中。)

隱藏:也叫做重定義,當基類和派生類中出現重名的成員時,派生類就會將基類的同名成員給隱藏起來,然后使用自己的。(但是隱藏并不意味著就無法訪問,可以通過聲明基類作用域來訪問到隱藏成員。)

因此 s1 調用 Print函數 時不會調用父類的,而是調用自己的。
在這里插入圖片描述


繼承方式及權限

繼承的方式和類的訪問限定符一樣,分為 public(公有繼承)private(私有繼承)protected(保護繼承) 三種。

關于 protected

  1. 對于類的對象來說是不可訪問的。
class Human
{
private:int pri;
protected:int pro;
public:int pub;
};

在這里插入圖片描述
在這里插入圖片描述

  1. 對于派生類的成員(數據成員or成員函數)和基類的友元來說是可以訪問的。
    在這里插入圖片描述
  2. 子類/子類的友元不能直接訪問父類的受保護成員,只能通過子類對象來訪問。 如果子類(及其友元)能直接訪問父類的受保護成員,那么 protected 提供的訪問保護也就太不安全了。
class Human
{
protected:int age;
};class Student : public Human
{int stu_id;friend void get(Human&); // 不能訪問Human::agefriend void get(Student&); // 可以訪問Student:age
};

在這里插入圖片描述

在這里插入圖片描述

基類成員的訪問說明符/派生類的繼承方式

  1. 對基類成員的訪問權限只與基類中的訪問說明符有關,與派生類的繼承方式無關。
class Human
{
private:int pri;
protected:int pro;
public:int pub;friend int f(Human h) { return h.pro; }
};class Student : public Human
{int f1() { return pri; } // ERROR:縱然是public繼承也不可以訪問private成員int f2() { return pro; } // 正確:protected成員可以被派生類訪問
};class Teacher : private Human {int f1() { return pro; } // 正確:protected成員可以被派生類訪問,即使繼承方式是private
};
  1. 繼承方式控制派生類對象(包括派生類的派生類)對于基類成員的訪問權限。

在這里插入圖片描述

總結來講父類成員的訪問權限決定了子類是否能訪問該成員,而繼承方式決定了父類成員在子類中的新權限是怎樣的:

  • public:繼承自父類的成員在父類中是什么權限,子類中就是什么權限。
  • protected:繼承自父類的成員其訪問權限都變成 protected
  • private:繼承自父類的成員其訪問權限都變成 private

派生類向基類轉換的可訪問性

假設 D 繼承自 B

  • 只有當 D 公有地繼承 B 時,派生類對象才能使用派生類向基類的轉換;如果繼承方式是受保護的或者私有的,則不能使用該轉換。
  • 不論 D 以什么方式繼承 BD 的成員函數和友元都能使用派生類向基類的轉換。換言之,派生類向其直接基類的類型轉換對于派生類的成員和友元來說永遠是可訪問的。
  • 如果繼承方式是公有的或者受保護的,則D的子類(第二點說的是D本身)的成員和友元可以使用 DB 的類型轉換;反之,如果繼承方式是私有的,則不能使用。

默認的繼承方式

默認情況下:

  • 使用 class 關鍵字定義的派生類是私有繼承的
  • 使用 struct 關鍵字定義的派生類是公有繼承的

using改變成員的訪問權限

我們說繼承方式決定了派生類的對象及派生類的子類對繼承來的成員的訪問權限,但這不是絕對的,我們可以通過 using 改變成員的訪問權限,但只能改變派生類能訪問的成員,即基類中的 protectedpublic 成員。

class Human
{
private:int pri;
protected:int pro;
public:int pub;
};class Teacher : private Human { // 私有繼承
private:// 只能被類的成員or友元訪問
public:using Human::pri; // 錯誤:using只能為派生類可訪問的成員提供聲明using Human::pro; // Teacher的對象、成員、友元、子類都可以訪問
protected:using Human::pro; // Teacher的對象、成員、友元可以訪問using Human::pub;
};

基類與派生類的賦值轉換

我們在 四種強制轉換類型中的 dynamic_cast 部分 提到過父類與子類的賦值轉換

派生類可以賦值給基類的對象、指針或者引用,這樣的賦值也叫做 對象切割

在這里插入圖片描述
當把派生類賦值給基類時,可以通過切割掉多出來的成員如 _stuNum 來完成賦值。
但是 基類對象 不可以賦值給 派生類 ,因為他不能憑空多一個 _stuNum 成員出來。

但是 基類的指針卻可以通過強制類型轉換賦值給派生類對象 , 如:

int main()
{Human h1;Student s1;Human* hPtrs = &s1; // 指向派生類對象Human* hPtrh = &h1; // 指向基類對象// 傳統方法Student* pPtr = (Student*)hPtrs; // 沒問題Student* pPtr = (Student*)hPtrh; // 有時候沒有問題,但是會存在越界風險// 如果父類之中包含虛函數,可以使用dynamic_castStudent* pPtr = dynamic_cast<Student*>(hPtrh);// 如果確認基類向派生類的轉換是安全的,可以使用static_castStudent* pPtr = static_cast<Student*>(hPtrs);return 0;
}

總結來講:

  • 派生類可以賦值給基類的對象、指針或者引用
  • 基類對象不能賦值給派生類對象
  • 基類的指針可以通過強制類型轉換賦值給派生類的指針。但是必須是基類的指針是指向派生類對象時才是安全的,否則會存在越界的風險。但基類如果是多態類型(父類之中包含虛函數),則可以使用 RTTIdynamic_cast 來實現 指向基類的基類指針派生類對象 的安全轉換。

回避虛函數機制

我們說多態是為了實現子類對于同一操作的不同結果,但有時候,派生類需要調用其父類的虛函數版本,而非自己的虛函數版本:

int main()
{Human h1;Student s1;Human* hPtrs = &s1; // 指向派生類對象hPtrs->print(); // 由于hPtrs指向子類對象,因此調用子類的虛函數hPtrs->Human::print(); // 強行調用Human中的虛函數,而不在意hPtrs的動態類型return 0;
}

派生類的默認成員函數

之前有寫過 類的默認六個成員函數

class Human
{
public:Human(){cout << "Human 構造函數" << endl;}~Human(){cout << "Human 析構函數" << endl;}protected:string _name;int _age;
};class Student : public Human
{
public:Student(){cout << "Student 構造函數" << _name << endl;}~Student(){//~Human(); 不需要手動調用父類的析構函數,編譯器會在子類析構函數結束后自動調用。cout << "Student 析構函數" << endl;}
protected:string _stuNum;
};int main()
{Student s1;return 0;
}

在這里插入圖片描述
可以看到,調用派生類的默認成員函數時都會調用基類的默認構造函數。

  • 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
  • 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
  • 派生類的 operator= 必須要調用基類的 operator= 完成基類的復制。
  • 派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。

在派生類的析構函數中,基類的析構函數會被隱藏,為了實現多態,它們都會被編譯器重命名為 destructor


友元與靜態成員

友元

友元關系是不會繼承的(友元關系不具有傳遞性),可以這樣理解,你長輩的朋友并不是你的朋友。

  • 基類的友元能訪問基類的私有/保護成員,但不能訪問子類的私有/保護成員。(當然基類本身也無法訪問子類的私有成員。)
  • 子類訪問父類友元的私有成員就更不用想了:
    1. 一是友元關系并不對稱,A 是 B 的友元,B 不一定是 A 的友元,也就是說父類本身都不一定能訪問父類友元的私有成員(父類不一定是其友元的友元),何況子類;
    2. 二是就算父類是其友元的友元,但友元關系不具有傳遞性,子類不一定是父類友元的友元。
  • 子類的友元無法訪問父類的私有/保護成員。

靜態成員

無論繼承了多少次,派生了多少子類,靜態成員在這整個繼承體系中有且只有一個。靜態成員不再單獨屬于某一個類亦或者是某一個對象,而是屬于這一整個繼承體系。


多繼承

如果一個子類同時繼承兩個或以上的父類時,此時就是多繼承。

多繼承雖然能很好的繼承多個父類的特性,達到復用代碼的效果,但是他也有著很多的隱患,例如菱形繼承的問題,這也就是為什么后期的一些語言如 java 把多繼承去掉的原因。


菱形繼承

class Human
{
public:int _age;
};class Student : public Human
{
public:int _stuNum;
};class Teacher : public Human
{
public:int _teaNum;
};

這里有著人類、學生類、老師類。在學校中,還存在著同時具有老師和學生這兩個屬性的人,也就是助教。所以我們可以讓他同時繼承 teacher類student類

在這里插入圖片描述

class Assistant : public Teacher, public Student
{
};

按照道理來說,各個類的大小應該是這樣的。Human 類4個字節,TeacherStudent 都是8個字節,而 Assistant 是12個字節。但是實際上 Assistant 卻是16字節。
在這里插入圖片描述
這就是菱形繼承的 數據冗余二義性 問題的體現。

這里的 TeacherStudent 都從 Human 中繼承了相同的成員 _age 。但是 Assistant 再從 TeacherStudent 繼承時,就分別把這兩個 _age 都給繼承了過來。

在這里插入圖片描述
這就是數據冗余問題。

倘若我們想要給那個 _age 賦值:

在這里插入圖片描述
在這里插入圖片描述

因為里面存在兩個一樣的 _age ,因此需要指定作用域:
在這里插入圖片描述
在這里插入圖片描述

這也就是二義性問題。


虛繼承

想解決二義性很簡單,當多個類繼承同一個類時,就在繼承這個類時,為其添加一個虛擬繼承的屬性。

class Student : virtual public Human
{
public:int _stuNum;
};class Teacher : virtual public Human
{
public:int _teaNum;
};

在這里插入圖片描述

這時就可以看到,它只繼承了一次。

接下來看看大小:
在這里插入圖片描述
按照道理來說,a 應該是 12字節ts 應該是 8字節 啊?這里就牽扯到了C++的對象模型,先推薦一篇博客: C++中的虛函數(表)實現機制以及用C語言對其進行的模擬實現

這里多出來的 8個字節,其實是兩個虛基表指針(vbptr)。同理,st 多出來的 4字節,是 一個vbptr

因為這里 Human 中的 _ageteacherstudent 共有的,所以為了能夠方便處理,在內存中分布的時候,就會把這個共有成員 _age 放到對象組成的最末尾的位置。然后在建立一個虛基表,這個表記錄了各個虛繼承的類在找到這個共有的元素時,在內存中偏移量的大小,而虛基表指針則指向了各自的偏移量。

這里打個比方:
在這里插入圖片描述
通過這個偏移量,他們能夠找到自己的 _age 的位置。

為什么需要這個偏移量呢?

int main()
{Assistant a;Teacher t = a; Student s = a;return 0;
}

如上,當把對象 a 賦值給 ts 的時候,因為他們互相沒有對方的 _stuNum_teaNum,所以他們需要進行對象的切割,但是又因為 _age 存放在對象的最尾部,所以只有知道了自己的偏移量,才能夠成功的在切割了沒有的元素時,還能找到自己的 _age


組合

那除了繼承還有什么好的代碼復用方式嗎?那答案肯定是有的,就是組合。組合就是將多個類組合在一起,實現代碼復用。

繼承和組合又有什么區別呢?

  • 繼承是一種 is a 的關系,基類是一個大類,而派生類則是這個大類中細分出來的一個子類,但是他們本質上其實是一種東西。正如:學生也是人,所以他可以很好的繼承人的所有屬性,并增加學生獨有的屬性。
  • 組合是一種 has a 的關系,就是一種包含關系。對象a對象b 中的一部分,對象b 包含 對象a

組合這種通過對方開放接口來實現的復用被稱為 黑箱復用(black-box reuse),因為對象的內部細節是不可見的。對象只以 黑箱 的形式出現。

class Study
{
public:void ToStudy(){cout << "Study" << endl;}
};class Student : public Human
{
public:Study _s;int _stuNum;
};

這里的 Student類 中包含了一個 Study類 ,學習是學生日常生活中不可缺少的一部分。

比較組合和繼承:

  • 組合的依賴關系弱,耦合度低。保證了代碼具有良好的封裝性和可維護性。 在組合中,幾個類的關聯不大,我只需要用到你那部分的某個功能,我并不需要了解你的實現細節,只需要你開放對應的接口即可,并且如果我要修改,只修改那一部分功能即可。
  • 繼承的依賴關系就非常的強,耦合度非常高。 因為你要想在子類中修改和增加某些功能,就必須要了解父類的某些細節,并且有時候甚至會修改到父類,父類的內部細節在子類中也一覽無余,嚴重的破壞了封裝性。并且一旦基類發生變化時,牽一發而動全身,所有的派生類都會有影響,這樣的代碼維護性會非常的差。一個可用的解決方法就是只繼承抽象類,因為抽象類通常提供較少的實現。

但是大部分場景下,如果繼承和組合都可以選擇,那么 優先使用對象組合,而不是類繼承

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/443761.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/443761.shtml
英文地址,請注明出處:http://en.pswp.cn/news/443761.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

博弈論 | 博弈論簡談、常見的博弈定律、巴什博弈

文章目錄博弈論什么是博弈論&#xff1f;博弈的前提博弈的要素博弈的分類非合作博弈——有限兩人博弈囚徒困境合作博弈——無限多人博弈囚徒困境常見的博弈定律零和博弈重復博弈智豬博弈斗雞博弈獵鹿博弈蜈蚣博弈酒吧博弈槍手博弈警匪博弈海盜分金巴什博弈博弈論 什么是博弈論…

MySQL命令(二)| 表的增刪查改、聚合函數(復合函數)、聯合查詢

文章目錄新增 (Create)全列插入指定列插入查詢 (Retrieve)全列查詢指定列查詢條件查詢關系元素運算符模糊查詢分頁查詢去重&#xff1a;DISTINCT別名&#xff1a;AS升序 or 降序更新 (Update)刪除 (Delete)分組&#xff08;GROUP BY&#xff09;聯合查詢內連接&#xff08;inne…

Spring3.1+Quertz1.8實現多個計劃任務

1.主要是配置文件&#xff1a;如下&#xff1a;(這里說明一下主要是看紅色部分的配置&#xff0c;其他的可以根據自己的實際情況修改&#xff0c;這里只是個思路。) <?xml version"1.0"?> <beans xmlns"http://www.springframework.org/schema/beans…

MySQL | 數據庫的六種約束、表的關系、三大范式

文章目錄數據庫約束NOT NULL&#xff08;非空約束&#xff09;UNIQUE&#xff08;唯一約束&#xff09;DEFAULT&#xff08;缺省約束&#xff09;PRIMARY KEY&#xff08;主鍵約束&#xff09;AUTO_INCREMENT 自增FOREIGN KEY&#xff08;外鍵約束&#xff09;CHECK&#xff08…

哈希 :哈希沖突、負載因子、哈希函數、哈希表、哈希桶

文章目錄哈希哈希&#xff08;散列&#xff09;函數常見的哈希函數字符串哈希函數哈希沖突閉散列&#xff08;開放地址法&#xff09;開散列&#xff08;鏈地址法/拉鏈法&#xff09;負載因子以及增容對于閉散列對于開散列結構具體實現哈希表&#xff08;閉散列&#xff09;創建…

C++ 泛型編程(一):模板基礎:函數模板、類模板、模板推演成函數的機制、模板實例化、模板匹配規則

文章目錄泛型編程函數模板函數模板實例化隱式實例化顯式實例化函數模板的匹配規則類模板類模板的實例化泛型編程 泛型編程旨在削減重復工作&#xff0c;如&#xff1a; 將一個函數多次重載不如將他寫成泛型。 void Swap(int& left, int& right) {int temp left;lef…

你真的了解靜態變量、常量的存儲位置嗎?

文章目錄引言C對內存的劃分如何落實在Linux上自由存儲區和堆之間的問題棧常量區靜態存儲區靜態局部變量靜態局部變量、靜態全局變量、全局變量的異同macOS系統的測試結果總結引言 在動態內存的博客中&#xff0c;我提到&#xff1a; 在Linux 內存管理的博客中&#xff0c;我提…

C++ 泛型編程(二):非類型模板參數,模板特化,模板的分離編譯

文章目錄非類型模板參數函數模板的特化類模板的特化全特化偏特化部分參數特化參數修飾特化模板分離編譯解決方法非類型模板參數 模板的參數分為兩種&#xff1a; 類型參數&#xff1a; 則是我們通常使用的方式&#xff0c;就是在模板的參數列表中在 class 后面加上參數的類型…

Java操作——獲取文件擴展名,去掉文件擴展名

昨天收郵件&#xff0c;得知要參加一個產品部的會議&#xff0c;猜想&#xff0c;也許是因為我做的這個產品demo問題。于是昨天忙活到凌晨3點半&#xff0c;結果早上一來才知道又被調戲了。發郵件的MM把郵件誤發給我了。悲催啊有木有&#xff0c;困啊有木有&#xff01;自己還是…

數據結構 | B樹、B+樹、B*樹

文章目錄搜索結構B樹B樹的插入B樹的遍歷B樹的性能B樹B樹的插入B樹的遍歷B*樹B*樹的插入總結搜索結構 如果我們有大量的數據需要永久存儲&#xff0c;就需要存儲到硬盤之中。但是硬盤的訪問速度遠遠小于內存&#xff0c;并且由于數據量過大&#xff0c;無法一次性加載到內存中。…

MySQL 索引 :哈希索引、B+樹索引、全文索引

文章目錄索引引言常見的索引哈希索引自適應哈希索引B樹索引聚集索引非聚集索引使用方法聯合索引最左前綴匹配規則覆蓋索引全文索引使用方法索引 引言 為什么需要索引&#xff1f; 倘若不使用索引&#xff0c;查找數據時&#xff0c;MySQL必須遍歷整個表。而表越大&#xff0c;…

服裝店怎么引流和吸引顧客 服裝店鋪收銀系統來配合

實體店的同城引流和經營是實體經濟的一個重要的一環&#xff0c;今天我們來分享服裝行業的實體店鋪怎么引流和吸引、留住顧客&#xff0c;并實現復購。大家點個收藏&#xff0c;不然劃走就再也找不到了&#xff0c;另外可以點個關注&#xff0c;下次有新的更好的招&#xff0c;…

約瑟夫環(丟手絹問題)

文章目錄問題描述思路代碼實現問題描述 有 1~N 個數字&#xff0c;從 1~m 依次報數&#xff0c;數到 m 的數字要被刪掉&#xff0c;求最后剩下的數字是&#xff1f; 思路 第一次報數第二次報數1n-m12n-m2……m-2n-2m-1n-1m被刪掉了m11m22……n-1n-1-mnn-m 通過上面的表格&…

MySQL 鎖的相關知識 | lock與latch、鎖的類型、簡談MVCC、鎖算法、死鎖、鎖升級

文章目錄lock與latch鎖的類型MVCC一致性非鎖定讀&#xff08;快照讀&#xff09;一致性鎖定讀&#xff08;當前讀&#xff09;鎖算法死鎖鎖升級lock與latch 在了解數據庫鎖之前&#xff0c;首先就要區分開 lock 和 latch。在數據庫中&#xff0c;lock 和 latch 雖然都是鎖&…

Hibernate使用原生SQL適應復雜數據查詢

HQL盡管容易使用&#xff0c;但是在一些復雜的數據操作上功能有限。特別是在實現復雜的報表統計與計算&#xff0c;以及多表連接查詢上往往無能為力&#xff0c;這時可以使用SQL&#xff08;Native SQL&#xff09;實現HQL無法完成的任務。 1、使用SQL查詢 使用SQL查詢可以通過…

MySQL 存儲引擎 | MyISAM 與 InnoDB

文章目錄概念innodb引擎的4大特性索引結構InnoDBMyISAM區別表級鎖和行級鎖概念 MyISAM 是 MySQL 的默認數據庫引擎&#xff08;5.5版之前&#xff09;&#xff0c;但因為不支持事務處理而被 InnoDB 替代。 然而事物都是有兩面性的&#xff0c;InnoDB 支持事務處理也會帶來一些…

MySQL 事務 | ACID、四種隔離級別、并發帶來的隔離問題、事務的使用與實現

文章目錄事務ACID并發帶來的隔離問題幻讀&#xff08;虛讀&#xff09;不可重復讀臟讀丟失更新隔離級別Read Uncommitted (讀未提交)Read Committed (讀已提交)Repeatable Read (可重復讀)Serializable (可串行化)事務的使用事務的實現Redoundo事務 事務指邏輯上的一組操作。 …

MySQL 備份與主從復制

文章目錄備份主從復制主從復制的作用備份 根據備份方法的不同&#xff0c;備份可劃分為以下幾種類型&#xff1a; 熱備(Hot Backup) &#xff1a; 熱備指的是在數據庫運行的時候直接備份&#xff0c;并且對正在運行的數據庫毫無影響&#xff0c;這種方法在 MySQL 官方手冊中又…

C++ 流的操作 | 初識IO類、文件流、string流的使用

文章目錄前言IO頭文件iostreamfstreamsstream流的使用不能拷貝或對 IO對象 賦值條件狀態與 iostate 類型輸出緩沖區文件流fstream類型文件模式文件光標函數tellg() / tellp()seekg() / seekp()向文件存儲內容/讀取文件內容string流istringstreamostringstream前言 我們在使用 …

Hibernate 更新部分更改的字段 hibernate update

Hibernate 中如果直接使用 Session.update(Object o);或則是Session.updateOrUpdate(Object o); 會把這個表中的所有字段更新一遍。 如&#xff1a; ExperClass4k e new ExperClass4k(); e.setTime(time); e.setQ_num(q_num); e.setK(k); if (str "finch_fix")…