C++繼承詳解

廢話不多說直接上代碼

class 派生類名:[繼承方式] 基類名{
??? 派生類新增加的成員
};

繼承方式限定了基類成員在派生類中的訪問權限,包括 public(公有的)、private(私有的)和 protected(受保護的)。此項是可選項,如果不寫,默認為 private(成員變量和成員函數默認也是 private)。

現在我們知道,public、protected、private 三個關鍵字除了可以修飾類的成員,還可以指定繼承方式。

繼承方式

不同的繼承方式會影響基類成員在派生類中的訪問權限。

1) public繼承方式

  • 基類中所有 public 成員在派生類中為 public 屬性;
  • 基類中所有 protected 成員在派生類中為 protected 屬性;
  • 基類中所有 private 成員在派生類中不能使用。

2) protected繼承方式

  • 基類中的所有 public 成員在派生類中為 protected 屬性;
  • 基類中的所有 protected 成員在派生類中為 protected 屬性;
  • 基類中的所有 private 成員在派生類中不能使用。

3) private繼承方式

  • 基類中的所有 public 成員在派生類中均為 private 屬性;
  • 基類中的所有 protected 成員在派生類中均為 private 屬性;
  • 基類中的所有 private 成員在派生類中不能使用。


通過上面的分析可以發現:
1)?基類成員在派生類中的訪問權限不得高于繼承方式中指定的權限。例如,當繼承方式為 protected 時,那么基類成員在派生類中的訪問權限最高也為 protected,高于 protected 的會降級為 protected
2) 基類中的 private 成員在派生類中始終不能使用(不能在派生類的成員函數中訪問或調用)。
3) 如果希望基類的成員能夠被派生類繼承并且毫無障礙地使用,那么這些成員只能聲明為 public 或 protected;只有那些不希望在派生類中使用的成員才聲明為 private。
4) 如果希望基類的成員既不向外暴露(不能通過對象訪問),還能在派生類中使用,那么只能聲明為 protected。
注意,我們這里說的是基類的 private 成員不能在派生類中使用,并沒有說基類的 private 成員不能被繼承。實際上,基類的 private 成員是能夠被繼承的,并且(成員變量)會占用派生類對象的內存,它只是在派生類中不可見,導致無法使用罷了。

改變訪問權限

使用 using 關鍵字可以改變基類成員在派生類中的訪問權限,例如將 public 改為 private、將 protected 改為 public。

注意:using 只能改變基類中 public 和 protected 成員的訪問權限,不能改變 private 成員的訪問權限,因為基類中 private 成員在派生類中是不可見的,根本不能使用,所以基類中的 private 成員在派生類中無論如何都不能訪問。

#include<iostream>
using namespace std;//基類People
class People {
public:void show();
protected:char *m_name;int m_age;
};
void People::show() {cout << m_name << "的年齡是" << m_age << endl;
}//派生類Student
class Student : public People {
public:void learning();
public:using People::m_name;  //將protected改為publicusing People::m_age;  //將protected改為publicfloat m_score;
private:using People::show;  //將public改為private
};
void Student::learning() {cout << "我是" << m_name << ",今年" << m_age << "歲,這次考了" << m_score << "分!" << endl;
}int main() {Student stu;stu.m_name = "小明";stu.m_age = 16;stu.m_score = 99.5f;stu.show();  //compile errorstu.learning();return 0;
}

代碼中首先定義了基類 People,它包含兩個 protected 屬性的成員變量和一個 public 屬性的成員函數。定義 Student 類時采用 public 繼承方式,People 類中的成員在 Student 類中的訪問權限默認是不變的。

不過,我們使用 using 改變了它們的默認訪問權限,如代碼第 21~25 行所示,將 show() 函數修改為 private 屬性的,是降低訪問權限,將 name、age 變量修改為 public 屬性的,是提高訪問權限。

因為 show() 函數是 private 屬性的,所以代碼第 36 行會報錯。把該行注釋掉,程序輸出結果為:
我是小明,今年16歲,這次考了99.5分!

繼承時的名字遮蔽問題

如果派生類中的成員(包括成員變量和成員函數)和基類中的成員重名,那么就會遮蔽從基類繼承過來的成員。所謂遮蔽,就是在派生類中使用該成員(包括在定義派生類時使用,也包括通過派生類對象訪問該成員)時,實際上使用的是派生類新增的成員,而不是從基類繼承來的。
?

基類和派生類的構造函數

類的構造函數不能被繼承。因為即使繼承了,它的名字和派生類的名字也不一樣,不能成為派生類的構造函數。

在設計派生類時,對繼承過來的成員變量的初始化工作也要由派生類的構造函數完成,但是大部分基類都有 private 屬性的成員變量,它們在派生類中無法訪問,更不能使用派生類的構造函數來初始化。

這種矛盾在C++繼承中是普遍存在的,解決這個問題的思路是:在派生類的構造函數中調用基類的構造函數。
?

Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }

People(name, age)就是調用基類的構造函數,并將 name 和 age 作為實參傳遞給它,m_score(score)是派生類的參數初始化表,它們之間以逗號,隔開。

也可以將基類構造函數的調用放在參數初始化表后面:

 

Student::Student(char *name, int age, float score): m_score(score), People(name, age){ }

但是不管它們的順序如何,派生類構造函數總是先調用基類構造函數再執行其他代碼(包括參數初始化表以及函數體中的代碼),總體上看和下面的形式類似:

Student::Student(char *name, int age, float score){

? ? People(name, age);

? ? m_score = score;

}

當然這段代碼只是為了方便大家理解,實際上這樣寫是錯誤的,因為基類構造函數不會被繼承,不能當做普通的成員函數來調用。換句話說,只能將基類構造函數的調用放在函數頭部,不能放在函數體中。

另外,函數頭部是對基類構造函數的調用,而不是聲明,所以括號里的參數是實參,它們不但可以是派生類構造函數參數列表中的參數,還可以是局部變量、常量等,例如:

Student::Student(char *name, int age, float score): People("小明", 16), m_score(score){ }

構造函數的調用順序

從上面的分析中可以看出,基類構造函數總是被優先調用,這說明創建派生類對象時,會先調用基類構造函數,再調用派生類構造函數,如果繼承關系有好幾層的話,例如:

A --> B --> C

那么創建 C 類對象時構造函數的執行順序為:

A類構造函數 --> B類構造函數 --> C類構造函數

構造函數的調用順序是按照繼承的層次自頂向下、從基類再到派生類的。

還有一點要注意,派生類構造函數中只能調用直接基類的構造函數,不能調用間接基類的。以上面的 A、B、C 類為例,C 是最終的派生類,B 就是 C 的直接基類,A 就是 C 的間接基類。

C++ 這樣規定是有道理的,因為我們在 C 中調用了 B 的構造函數,B 又調用了 A 的構造函數,相當于 C 間接地(或者說隱式地)調用了 A 的構造函數,如果再在 C 中顯式地調用 A 的構造函數,那么 A 的構造函數就被調用了兩次,相應地,初始化工作也做了兩次,這不僅是多余的,還會浪費CPU時間以及內存,毫無益處,所以 C++ 禁止在 C 中顯式地調用 A 的構造函數。?

調用規則:事實上,通過派生類創建對象時必須要調用基類的構造函數,這是語法規定。換句話說,定義派生類構造函數時最好指明基類構造函數;如果不指明,就調用基類的默認構造函數;如果沒有默認構造函數,那么編譯失敗。

?

基類和派生類的析構函數

和構造函數類似,析構函數也不能被繼承。與構造函數不同的是,在派生類的析構函數中不用顯式地調用基類的析構函數,因為每個類只有一個析構函數,編譯器知道如何選擇,無需程序員干涉。

另外析構函數的執行順序和構造函數的執行順序也剛好相反:

  • 創建派生類對象時,構造函數的執行順序和繼承順序相同,即先執行基類構造函數,再執行派生類構造函數。
  • 而銷毀派生類對象時,析構函數的執行順序和繼承順序相反,即先執行派生類析構函數,再執行基類析構函數。

?

多繼承

容易讓代碼邏輯復雜、思路混亂,一直備受爭議,中小型項目中較少使用,后來的 Java、C#、PHP 等干脆取消了多繼承。
多繼承的語法也很簡單,將多個基類用逗號隔開即可。例如已聲明了類A、類B和類C,那么可以這樣來聲明派生類D:
class D: public A, private B, protected C{
? ? //類D新增加的成員
}

D 是多繼承形式的派生類,它以公有的方式繼承 A 類,以私有的方式繼承 B 類,以保護的方式繼承 C 類。D 根據不同的繼承方式獲取 A、B、C 中的成員,確定它們在派生類中的訪問權限。

多繼承下的構造函數

多繼承形式下的構造函數和單繼承形式基本相同,只是要在派生類的構造函數中調用多個基類的構造函數。以上面的 A、B、C、D 類為例,D 類構造函數的寫法為:

D(形參列表): A(實參列表), B(實參列表), C(實參列表){
? ? //其他操作
}

基類構造函數的調用順序和和它們在派生類構造函數中出現的順序無關,而是和聲明派生類時基類出現的順序相同。仍然以上面的 A、B、C、D 類為例,即使將 D 類構造函數寫作下面的形式:

D(形參列表): B(實參列表), C(實參列表), A(實參列表){
? ? //其他操作
}

那么也是先調用 A 類的構造函數,再調用 B 類構造函數,最后調用 C 類構造函數。

命名沖突:當兩個或多個基類中有同名的成員時,如果直接訪問該成員,就會產生命名沖突,編譯器不知道使用哪個基類的成員。這個時候需要在成員名字前面加上類名和域解析符::,以顯式地指明到底使用哪個類的成員,消除二義性。

虛繼承

多繼承是指從多個直接基類中產生派生類的能力,多繼承的派生類繼承了所有父類的成員。盡管概念上非常簡單,但是多個基類的相互交織可能會帶來錯綜復雜的設計問題,命名沖突就是不可回避的一個。

類 A 派生出類 B 和類 C,類 D 繼承自類 B 和類 C,這個時候類 A 中的成員變量和成員函數繼承到類 D 中變成了兩份,一份來自 A-->B-->D 這條路徑,另一份來自 A-->C-->D 這條路徑。

在一個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變量中分別存放不同的數據,但大多數情況下這是多余的

//間接基類A
class A{
protected:int m_a;
};//直接基類B
class B: public A{
protected:int m_b;
};//直接基類C
class C: public A{
protected:int m_c;
};//派生類D
class D: public B, public C{
public:void seta(int a){ m_a = a; }  //命名沖突
private:int m_d;
};

為了消除歧義,我們可以在 m_a 的前面指明它具體來自哪個類:

void seta(int a){ B::m_a = a; }

虛繼承

虛繼承使得在派生類中只保留一份間接基類的成員。
在繼承方式前面加上?virtual?關鍵字就是虛繼承。

//間接基類A
class A{
protected:int m_a;
};//直接基類B
class B: virtual public A{  //虛繼承
protected:int m_b;
};//直接基類C
class C: virtual public A{  //虛繼承
protected:int m_c;
};//派生類D
class D: public B, public C{
public:void seta(int a){ m_a = a; }  //正確void setb(int b){ m_b = b; }  //正確void setc(int c){ m_c = c; }  //正確void setd(int d){ m_d = d; }  //正確
private:int m_d;
};int main(){D d;return 0;
}

虛繼承的目的是讓某個類做出聲明,承諾愿意共享它的基類。其中,這個被共享的基類就稱為虛基類(Virtual Base Class),本例中的 A 就是一個虛基類。在這種機制下,不論虛基類在繼承體系中出現了多少次,在派生類中都只包含一份虛基類的成員。

我們會發現虛繼承的一個不太直觀的特征:必須在虛派生的真實需求出現前就已經完成虛派生的操作。在上圖中,當定義 D 類時才出現了對虛派生的需求,但是如果 B 類和 C 類不是從 A 類虛派生得到的,那么 D 類還是會保留 A 類的兩份成員。

換個角度講,虛派生只影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身。

在實際開發中,位于中間層次的基類將其繼承聲明為虛繼承一般不會帶來什么問題。通常情況下,使用虛繼承的類層次是由一個人或者一個項目組一次性設計完成的。對于一個獨立開發的類來說,很少需要基類中的某一個類是虛基類,況且新類的開發者也無法改變已經存在的類體系。

使用多繼承經常會出現二義性問題,必須十分小心。上面的例子是簡單的,如果繼承的層次再多一些,關系更復雜一些,程序員就很容易陷人迷魂陣,程序的編寫、調試和維護工作都會變得更加困難,因此不提倡在程序中使用多繼承

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

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

相關文章

串【數據結構F】

先來講解一下串結構的概念性質的東西&#xff0c;以及我們需要注意的一些問題 串結構簡單的ADT以及一些基本的操作 最小操作函數&#xff1a;就是功能已經達到了最小的功能實現了&#xff0c;不能繼續執行更大的功能&#xff0c;類似于我們在家蓋房子一樣&#xff0c;水泥的…

C++ STL與迭代器

將容器類模板實例化時&#xff0c;會指明容器中存放的元素是什么類型的&#xff1a;可以存放基本類型的變量&#xff0c;也可以存放對象。 對象或基本類型的變量被插入容器中時&#xff0c;實際插入的是對象或變量的一個復制品。 STL 中的許多算法&#xff08;即函數模板&…

在JSP頁面中輸出JSON格式數據

JSON-taglib是一套使在JSP頁面中輸出JSON格式數據的標簽庫。 JSON-taglib主頁&#xff1a; http://json-taglib.sourceforge.net/index.html JAR包下載地址&#xff1a; http://sourceforge.net/projects/json-taglib/files/latest/download 使用方法&#xff1a; 1、下載js…

git/github使用完整教程(1)基礎

安裝git 在Linux上安裝Git 首先輸入git&#xff0c;看看系統有沒有安裝Git&#xff1a; $ git The program git is currently not installed. You can install it by typing: sudo apt-get install git像上面的命令&#xff0c;有很多Linux會友好地告訴你Git沒有安裝&#x…

git/github使用完整教程(2)分支

分支 首先&#xff0c;我們創建dev分支&#xff0c;然后切換到dev分支&#xff1a; $ git checkout -b dev Switched to a new branch devgit checkout命令加上-b參數表示創建并切換&#xff0c;相當于以下兩條命令&#xff1a; $ git branch dev $ git checkout dev Switch…

數組【數據結構】

前提 數組的定義以及數組的延伸 這種不好進行理解&#xff0c;那么我們下面以二維數組進行解釋 多維數組的數據特點 存儲數組結構的兩種方式 問題抽象總結

Kafka深度解析

原創文章&#xff0c;轉載請務必將下面這段話置于文章開頭處&#xff08;保留超鏈接&#xff09;。 本文轉發自技術世界&#xff0c;原文鏈接 http://www.jasongj.com/2015/01/02/Kafka深度解析 背景介紹 Kafka簡介 Kafka是一種分布式的&#xff0c;基于發布/訂閱的消息系統。…

數據的存儲特殊矩陣壓縮存儲【數據結構F】

以行為主序 以列為主序 矩陣的前提分類 三角矩陣

C++ 多態和虛函數

虛函數實現多態 #include <iostream> using namespace std;//基類People class People{ public:virtual void display(); //聲明為虛函數 }; void People::display(){cout<<"無業游民。"<<endl; }//派生類Teacher class Teacher: public People{…

圖的基本概念【數據結構】

序言 1對1的線性結構&#xff0c;一對多的樹二叉樹以及森林&#xff0c;第3種就是多對多的結構&#xff0c;也就是我們所要講到的圖的結構&#xff0c;圖形結構是數據結構當中最復雜的一種結構&#xff0c;圖形結構的特點就是在這個圖當中任意兩點之間都會有關系&#xff0c;這…

go語言一天入門(上)

第一個go程序 package mainimport "fmt"func main() {/* 這是我的第一個簡單的程序 */fmt.Println("Hello, World!") } 第一行代碼 package main 定義了包名。你必須在源文件中非注釋的第一行指明這個文件屬于哪個包&#xff0c;如&#xff1a;package m…

Jquery 全選,反選

<script src"../js/jquery-1.6.2.min.js" type"text/javascript"></script><script language"javascript" type"text/javascript">$(function(){$("#selectAll").click(function(){//全選$("#playLi…

go語言一天入門(下)

結構體 和c一樣 package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func main() {// 創建一個新的結構體fmt.Println(Books{"Go 語言", "www.runoob.com", "Go 語言教程", 6495407}…

圖的遍歷算法【數據結構F】

圖的遍歷算法有哪兩種&#xff1f; 深度優先調度算法---------將圖結構看成是樹形結構&#xff0c;樹形結構的子圖直接是沒有交叉的&#xff0c;但是對于圖結構的樹形結構之間是有交叉的&#xff0c;類比于樹形結構的二叉樹&#xff0c;左指數和右指數都會相應的經歷三次&#…

go語言快速刷《程序員面試金典》(1)

實現一個算法&#xff0c;確定一個字符串 s 的所有字符是否全都不同。 一個數組統計是否有 func isUnique(astr string) bool {var arr[26] int;for _,ch:range astr{num:ch-aif(arr[num]1){return false}arr[num]}return true } 給定兩個字符串 s1 和 s2&#xff0c;請編寫一…

最小生成樹【數據結構】

前提 【1】網的最小生成樹&#xff0c;涉及到生成樹了那么就會有最小的權值在里面了 【2】對于一個圖來說生成樹是由多個的&#xff0c;并不是唯一的 【3】&#xff1a;廣度優先算法的遍歷是可以得到生成樹的&#xff0c;深度優先算法也是可以得到生成樹的 任意的一個聯通網&am…

go語言快速刷《程序員面試金典》(2)

字符串輪轉。給定兩個字符串s1和s2&#xff0c;請編寫代碼檢查s2是否為s1旋轉而成&#xff08;比如&#xff0c;waterbottle是erbottlewat旋轉后的字符串&#xff09;。 示例1 輸入&#xff1a;s1 "waterbottle", s2 "erbottlewat" 輸出&#xff1a;T…

廣義表的基本概念【數據結構】

實名廣義表與匿名廣義表的區別&#xff1a;對于匿名的廣義表的表示方法我們認為一對括號就是一個廣義表&#xff0c;里面的數據可以是廣義表也可以是 原子&#xff0c;對于有名字的廣義表&#xff0c;也就是大寫的字母我們可以直接認為大寫的就是廣義表的表示方法小練習----廣義…