第5章 繼承和多態(tài)_第1頁
第5章 繼承和多態(tài)_第2頁
第5章 繼承和多態(tài)_第3頁
第5章 繼承和多態(tài)_第4頁
第5章 繼承和多態(tài)_第5頁
已閱讀5頁,還剩50頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第5章繼承和多態(tài)本章主要內容:5.1公有繼承和is-a關系

5.2派生類對象的構造、析構

5.3同名覆蓋原則

5.4賦值兼容原則

5.5多態(tài)性和虛函數(shù)

5.6典型范例——各類物體面積求和

5.7其它繼承方式

5.8多繼承

5.9深度探索 計算機學院李衛(wèi)明面向對象的核心思想是抽象、封裝與信息隱蔽、繼承和多態(tài)。前面學習了對現(xiàn)實世界或思維世界中的實體進行抽象,用類完成封裝,完成類的接口和類的實現(xiàn)的分離,類的用戶只需了解類的接口,無需關心類實現(xiàn)的細節(jié),達到了信息隱蔽目的,同時,通過函數(shù)重載和運算符重載實現(xiàn)了靜態(tài)多態(tài)性。本章學習面向對象程序設計中重要的繼承機制和通過動態(tài)綁定實現(xiàn)動態(tài)多態(tài)性。計算機學院李衛(wèi)明5.1 公有繼承和is-a關系

5.1.1 繼承和派生

人們對現(xiàn)實世界或思維世界中的實體進行分類,形成樹形層次關系。如圖5.1所示,細分類的個體和基礎類個體之間具有特殊和一般的關系,細分類的個體也是一種基礎類的個體,稱為is-a關系,細分類個體除了具有基礎類個體的屬性和行為能力外,還具有自己特殊的屬性和行為能力。圖5.1分類關系舉例在面向對象程序設計中,用公有繼承和派生機制反映上述分類關系。用于描述基礎類個體的類稱為基類,也可稱為父類,用于描述細分類個體的類稱為派生類,也可稱為子類。計算機學院李衛(wèi)明

一個類可以直接派生出多個派生類,每個派生出的子類均繼承了基類的公有屬性和公有函數(shù)描述的功能,派生類本身也可作為基類繼續(xù)派生,形成多級派生。

一個類直接派生出的子類以及子類直接或間接派生出的所有類統(tǒng)稱為它的子孫類,一個類及它的子孫類形成一個派生關系類族,派生關系類族中從父類一直往上到最上層結點路徑上的所有類通稱為該類的祖先類,子孫類從祖先類直接或間接派生產(chǎn)生。子孫類繼承祖先類的所有公有屬性和公有函數(shù)成員描述的功能。計算機學院李衛(wèi)明聲明公有派生類的一般建議格式為:class派生類名:public基類名{public:派生類公有函數(shù)成員private:

派生類私有函數(shù)成員private:

派生類私有數(shù)據(jù)成員};與普通類聲明一樣,C++語言本身并未限制派生類成員的聲明順序。這里聲明的是公有派生,絕大部分派生是公有派生,反映is-a(是)關系,關于特殊場合使用的其它派生,請參見本章第7節(jié)。計算機學院李衛(wèi)明5.1.2 派生類定義C++中成員訪問控制修飾符有三種:private、protected、public,其中,私有成員只能在本類成員函數(shù)和友元函數(shù)里訪問,不可在派生類的成員函數(shù)里訪問,公有成員可以在任何地方訪問。protected訪問控制專用于繼承和派生,protected修飾的成員是保護成員,除了可以像私有成員在本類成員函數(shù)和友元函數(shù)里訪問外,還可在派生類的成員函數(shù)里訪問,但不可在類外訪問。

無論公有成員、私有成員還是保護成員,基類數(shù)據(jù)成員都是派生類對象的組成部分,公有繼承時,基類的公有和保護成員可訪問性在派生類中保持不變,多級派生時,子孫類成員函數(shù)也可以直接訪問祖先類的保護成員和公有成員。公有繼承時,子孫類對象繼承了祖先類的公有數(shù)據(jù)屬性和公有函數(shù)功能,程序中任何位置可訪問對象祖先類的公有成員。下節(jié)樣例程序里我們可以看到,學生類CStudent和教師類CTeacher公有繼承普通人類CPerson,學生和教師個體作為派生類對象具有基類公有函數(shù)所描述的功能,同時,派生類成員函數(shù)中可直接訪問基類中的保護成員m_strName。計算機學院李衛(wèi)明5.1.3 派生類訪問控制CPerson、CStudent、CTeacherUML圖示:

圖5.2普通人類Cperson、學生類Cstudent和教師類Cteacher具體樣例代碼見Ex5.1計算機學院李衛(wèi)明5.1.4 派生類樣例整個派生類對象狀態(tài)包含基類部分對象狀態(tài)和派生類部分數(shù)據(jù)成員構成的狀態(tài),派生類對象實際可分為兩部分:基類部分對象和派生類部分對象。基類部分數(shù)據(jù)成員,用于表示基類部分對象狀態(tài);派生類本身的所有數(shù)據(jù)成員,用于表示派生類部分對象狀態(tài)?;惒糠謹?shù)據(jù)成員包括直接基類的所有數(shù)據(jù)成員,也包括間接基類的所有數(shù)據(jù)成員,不論它們的訪問控制是公有、保護,還是私有的。

派生類構造函數(shù)一般形式如下:派生類名(形參表):基類構造函數(shù)名(基類構造函數(shù)參數(shù)表),數(shù)據(jù)成員1(數(shù)據(jù)成員參數(shù)表1),數(shù)據(jù)成員2(數(shù)據(jù)成員參數(shù)表2),…{……//函數(shù)體}對象建立時數(shù)據(jù)成員初始化由構造函數(shù)完成,對象撤銷時掃尾處理由析構函數(shù)完成?;悩嬙旌瘮?shù)只能完成基類部分對象的初始化,基類的析構函數(shù)只能完成基類部分對象的掃尾處理,從這個意義來說,基類的構造函數(shù)和析構函數(shù)不可繼承。計算機學院李衛(wèi)明5.2 派生類對象的構造、析構派生類構造函數(shù)會先調用基類構造函數(shù)完成基類部分數(shù)據(jù)成員的初始化,再完成派生類部分數(shù)據(jù)成員的初始化,析構時,次序正好相反,派生類析構函數(shù)會先執(zhí)行派生類析構函數(shù)體,完成派生類部分對象的掃尾處理,再隱式調用基類析構函數(shù)完成基類部分對象的掃尾處理。

樣例Ex5.1中基類、派生類均未定義構造函數(shù)和析構函數(shù),基類、派生類構造函數(shù)和析構函數(shù)由編譯器合成。編譯器合成的CPerson調用string類構造函數(shù)完成名字成員m_strName初始化為空字符串,年齡成員m_iAge未初始化,派生類CStudent構造函數(shù)先調用基類無參構造函數(shù)完成基類部分對象的初始化,再完成數(shù)據(jù)成員子對象選修課表向量m_strVec初始化為空向量,派生類CTeacher構造函數(shù)先調用基類無參構造函數(shù)完成基類部分對象的初始化,再完成數(shù)據(jù)成員子對象講授課表向量m_strVec初始化為空向量。

計算機學院李衛(wèi)明如果需要在建立CStudent類對象時直接指定學生的名字和年齡,在建立CTeacher類對象時直接指定教師的名字和年齡,需要為基類CPerson類增加下述公有構造函數(shù):

CPerson(stringstrName,intiAge):m_strName(strName),m_iAge(iAge){}

CStudent類和CTeacher類也需要增加下述公有構造函數(shù):

CStudent(stringstrName,intiAge):CPerson(strName,iAge){}CTeacher(stringstrName,intiAge):CPerson(strName,iAge){}

計算機學院李衛(wèi)明上述派生類新增構造函數(shù)在初始化列表里顯式調用基類的構造函數(shù)。如果派生類構造函數(shù)初始化列表里沒有顯式調用基類的構造函數(shù),編譯器會隱式調用基類的缺省無參構造函數(shù),當基類有其它構造函數(shù)而無缺省無參構造函數(shù)時,編譯器會報錯。

計算機學院李衛(wèi)明一般情況下,派生類繼承了基類公有的數(shù)據(jù)成員和公有函數(shù)成員。如果派生類新增的數(shù)據(jù)成員或函數(shù)成員與基類中成員同名,則基類中所有同名成員名字被覆蓋,派生類成員函數(shù)中或外部不可通過派生類對象名直接訪問基類同名成員,這一原則稱為同名覆蓋原則。如我們?yōu)樯瞎?jié)樣例中CPerson類增加顯示人員信息的成員函數(shù)Show后:voidShow()const{cout<<m_strName<<","<<m_iAge<<"yearsold"<<endl;}再為CStudent類和CTeacher類增加顯示學生信息和教師信息的成員函數(shù)Show后,基類CPerson中同名成員函數(shù)已被覆蓋。CStudent類、CTeacher類的對象或它們的派生類對象調用的Show函數(shù)都是覆蓋后的派生類成員函數(shù)Show。

下面2個語句分別調用CStudent類、CTeacher類的成員函數(shù)Show:studZhang.Show();//studZhang是一個CStudent類對象teacherLiu.Show();//teacherLiu是一個CTeacher類對象計算機學院李衛(wèi)明5.3 同名覆蓋原則如果需要調用基類中被覆蓋的同名成員函數(shù),調用時可在成員函數(shù)名前添加基類名::。如下述CStudent類和CTeacher類成員函數(shù)Show實現(xiàn)時,用CPerson::Show()調用基類Show成員函數(shù):voidCStudent::Show()const{CPerson::Show();cout<<"isastudent"<<endl;}voidCTeacher::Show()const{CPerson::Show();cout<<"isateacher"<<endl;}同名覆蓋后,CStudent類和CTeacher類對象,可用下述方式調用基類Show成員函數(shù):studZhang.CPerson::Show();//studZhang是一個CStudent類對象teacherLiu.CPerson::Show();//teacherLiu是一個CTeacher類對象計算機學院李衛(wèi)明公有繼承下,派生類或子孫類對象也是一種特殊的基類對象,具有基類對象的屬性和公有函數(shù)描述的功能,因此,在需要基類對象的場合,派生類對象或子孫類對象也都可以滿足需要,這稱為賦值兼容原則。具體如下:首先,考慮對象指針類型。指針類型變量用于存放對象的地址,指向對象。指針類型變量可以指向本類對象,也允許基類指針變量指向派生類對象或子孫類對象,但不可以指向祖先類對象;基類指針變量作為形參時,實參可以是指向本類對象的指針、指向派生類對象或子孫類對象的指針,不可以是指向祖先類對象的指針;函數(shù)返回對象指針類型時,函數(shù)可以實際返回本類對象的指針、派生類對象或子孫類對象的指針,不可以返回祖先類對象的指針。賦值時,可以給本類對象指針類型的變量賦值本類對象指針、派生類對象指針或子孫類對象指針,不可以將祖先類對象的指針賦值給基類類型的指針變量。計算機學院李衛(wèi)明5.4 賦值兼容原則函數(shù)OlderOne返回候選者中選出年齡較大者的指針:CPerson*OlderOne(CPserson*first,CPserson*second);可以調用函數(shù)OlderOne從兩位學生中選出年齡大的一位,也可以調用函數(shù)OlderOne從學生和教師中選出年齡大的一位:CPerson*pOlderOne;pOlderOne=OlderOne(&studZhang,&studWang);CPerson*pOlderPerson=OlderOne(&zhang,&teacherLiu);但不可以將基類指針變量賦值給派生類指針變量,如:CStudent*pOlderStudent=pOlderOne;//錯誤函數(shù)BestStudent可以從兩位學生中選出選課多的人,返回學生指針:CStudent*BestStudent(CStudent*first,CStudent*second);可以調用函數(shù)BestStudent從兩位學生中選出選課多的一位:CStudent*pBestStudent=BestStudent(&studZhang,&studWang);但不可以調用函數(shù)BestStudent從學生和教師中選出選課多的一位,也不可以從普通人和學生中選出選課多的一位:BestStudent(&studZhang,&teacherLiu);//錯誤BestStudent(&studZhang,&someOne);//錯誤計算機學院李衛(wèi)明我們再來考慮引用。引用具有指針類似道理。引用是對象的別名,引用對象與被引用對象是同一個對象。引用變量可以引用本類對象,C++允許基類引用變量引用派生類對象或子孫類對象,但不可以引用祖先類對象;基類引用變量作為形參時,實參可以是本類對象、派生類對象或子孫類對象,實參不可以是祖先類對象;函數(shù)返回對象引用時,函數(shù)可以實際返回本類對象、派生類對象或子孫類對象,但不可以返回祖先類對象。定義引用變量初始化時,可以初始化為本類對象、派生類對象或子孫類對象,不可以初始化為祖先類對象。計算機學院李衛(wèi)明函數(shù)OlderOne2返回候選者中年齡最大者的引用,原型如下:constCPerson&OlderOne2(constCPserson&first,constCPserson&second);可以調用函數(shù)OlderOne2從兩位學生中選出年齡大的一位,也可以從學生和教師中選出年齡大的一位:CPerson&olderOne=OlderOne2(studZhang,studWang);CPerson&olderPerson=OlderOne2(studZhang,teacherLiu);不可以定義派生類引用變量時將其初始化為基類對象:CStudent&olderStudent=olderOne;//錯誤函數(shù)BestStudent返回選課多學生的引用:constCStudent&BestStudent2(constCStudent&first,constCStudent&second);可以調用函數(shù)BestStudent從兩位學生中選出選課多的一位:constCStudent&bestStudent=BestStudent2(studZhang,studWang);不可以調用函數(shù)BestStudent2從學生和教師中選出選課多的一位,也不可以從普通人和學生中選出選課多的一位:BestStudent2(studZhang,teacherLiu);//錯誤BestStudent2(studZhang,someOne);//錯誤,someOne不是學生對象計算機學院李衛(wèi)明考慮類類型普通變量和函數(shù)參數(shù)傳值、函數(shù)返回普通對象類型情況。一個類類型變量,可以存放一個同類對象,不能存放其它類類型對象。C++可以將對象賦值給同類對象變量,同類對象賦值時實際執(zhí)行的是對象的復制賦值或轉移賦值。C++不可以將對象賦值給不相關類對象變量或派生類對象變量。C++允許將對象賦值給基類對象變量,不會編譯報錯、甚至不會警告,但實際復制的只是基類部分子對象,不是派生類對象的全部,這個現(xiàn)象稱為切片現(xiàn)象,一般場合下存在切片現(xiàn)象的賦值是不合適的。參數(shù)采用傳值方式時,基類變量作為形參,實參可以是本類對象、不可以是祖先類對象,C++允許派生類對象或子孫類對象作實參,但復制給形參的只是基類部分對象,也存在切片現(xiàn)象,并不合適;

函數(shù)返回對象時,函數(shù)可以實際返回本類對象,不可以返回祖先類對象,C++允許返回派生類對象或子孫類對象,但返回的只是基類部分對象,存在切片現(xiàn)象,也并不合適。此外,大對象在函數(shù)間傳遞時,如果參數(shù)采用傳值方式,存在對象的復制,效率會較低,一般應該盡量避免大對象的傳值方式傳遞或傳值方式返回,同時,要保證不可返回臨時對象的引用或指針,因為臨時對象在函數(shù)返回后已析構。計算機學院李衛(wèi)明面向對象程序設計中,作用在對象上的函數(shù)或運算符的調用可以看作向對象發(fā)出某個消息,對象接收消息后做出某些動作。不同類的對象接收同一個消息后可以調用不同的函數(shù)體,做出不同的動作,這個現(xiàn)象稱為多態(tài)性。抽象、封裝、繼承和多態(tài)性是面向對象程序設計的主要特征。C++面向對象程序設計中多態(tài)性可根據(jù)決定調用函數(shù)時機分為靜態(tài)多態(tài)性和動態(tài)動態(tài)性。如果在編譯期間確定實際調用的函數(shù),也就是采用靜態(tài)綁定(binding),這樣的多態(tài)性稱為靜態(tài)多態(tài)性;如果編譯期間不能確定實際調用的函數(shù),需要程序執(zhí)行到具體函數(shù)調用語句時才能確定實際調用的函數(shù),也就是需要采用動態(tài)綁定(binding),這樣的多態(tài)性稱為動態(tài)多態(tài)性。前面介紹的普通函數(shù)重載和成員函數(shù)重載都是在編譯階段確定實際調用的函數(shù),因此,函數(shù)重載是一種靜態(tài)多態(tài)性。下一章介紹的函數(shù)模板和類模板也是在編譯階段確定實際調用的函數(shù)和實際使用的類及相應成員函數(shù),這樣的多態(tài)性也是一種靜態(tài)多態(tài)性。動態(tài)多態(tài)性是一種面向對象程序設計中非常重要的多態(tài)性。例如:同樣調用run方法,飛鳥調用時動作是飛,馬調用時動作是奔跑。計算機學院李衛(wèi)明5.5 多態(tài)性和虛函數(shù)5.5.1 多態(tài)性面向對象程序設計中動態(tài)多態(tài)性的實現(xiàn)受到繼承性的支持,通過在派生類中重定義基類虛函數(shù)來實現(xiàn)多態(tài)性,利用類繼承的層次關系,把具有通用功能的協(xié)議存放在類層次中盡可能高的地方,而將實現(xiàn)這一功能的不同方法置于較低層次,在這些低層次上生成的對象就能給通用消息以不同的響應。同一類族的對象可以統(tǒng)一管理,利用多態(tài)性用戶可發(fā)送一個通用的信息給同一類族的對象,而將所有的實現(xiàn)細節(jié)都留給接收消息的對象自行決定,不同類型的對象即可調用不同的函數(shù)響應同一消息,簡化了程序。計算機學院李衛(wèi)明樣例Ex5.2在Ex51基礎上,綜合了前面各節(jié)情況,各類增加了構造函數(shù)、增加了成員函數(shù)Show,增加了三個普通函數(shù)ShowByPointer、ShowByReference、ShowByValue,參數(shù)分別以對象指針、對象引用、對象傳值方式傳遞,根據(jù)賦值兼容原則,調用這三個函數(shù)時形參實際是派生類對象指針(語句S112~S114)、派生類對象引用(語句S117~S119)、派生類對象的切片(語句S122~S124)。從運行結果我們看到,無論何種情況,樣例程序調用的都是基類的Show函數(shù),我們需要的動態(tài)多態(tài)性并未發(fā)生。由于C++采用的是靜態(tài)綁定,這些情況下,普通函數(shù)ShowByPointer、ShowByReference、ShowByValue三個函數(shù)中參數(shù)類型分別為基類對象指針、基類對象引用、基類對象,綁定的都是基類的Show成員函數(shù)。

計算機學院李衛(wèi)明5.5.2 虛函數(shù)C++動態(tài)多態(tài)性如何實現(xiàn)呢?如果需要動態(tài)多態(tài)性,我們需要在基類成員函數(shù)聲明或定義前加入關鍵字virtual,聲明這個成員函數(shù)是虛函數(shù),也就是上述樣例語句S13函數(shù)定義前加關鍵字virtual,聲明Show成員函數(shù)是虛函數(shù),派生類成員函數(shù)Show前是否加關鍵字virtual沒有實際影響,C++對虛函數(shù)采用動態(tài)綁定。修改后運行結果中,通過對象指針和引用調用虛函數(shù)都體現(xiàn)出動態(tài)多態(tài)性,通過傳值調用時,形參得到的實際是派生類對象的切片,已完全是基類對象,因此無動態(tài)多態(tài)性。C++規(guī)定構造函數(shù)不可以是虛函數(shù),析構函數(shù)可以是虛函數(shù)。類成員函數(shù)中直接調用虛函數(shù)或通過this調用虛函數(shù)都是當前實際對象類型自己定義或繼承的虛函數(shù),具有動態(tài)多態(tài)性。構造函數(shù)里調用的虛函數(shù)時,由于對象派生類部分尚未構造完成,調用的是構造函數(shù)所在類或繼承的虛函數(shù),不是派生類中定義的虛函數(shù)。計算機學院李衛(wèi)明

C++規(guī)定構造函數(shù)不可以是虛函數(shù),析構函數(shù)可以是虛函數(shù)。那么,如何精確復制一個基類指針所指的派生類對象呢?例如,我們有哺乳動物類CMammal,從哺乳動物類CMammal派生出狗類CDog和馬類CHorse,指針向量zoo定義如下:vector<CMammal*>zoo;程序運行過程中,指針向量zoo已存放若干動物對象指針,每個指針分別指向狗類CDog對象或馬類CHorse對象,如何復制出原指針向量zoo里管理的動物,讓動物加倍呢?我們可以采用如下克隆技術,就是在哺乳動物類CMammal里增加純虛函數(shù)(在下節(jié)介紹)克隆Clone,在狗類CDog和馬類具體實現(xiàn)Clone函數(shù):計算機學院李衛(wèi)明5.5.3 派生類對象的克隆classCMammal{public:virtualCMammal*Clone()const=0;...//其它部分省略};classCDog:publicCMammal{public:CMammal*Clone()const{returnnewCDog(*this);//調用拷貝構造動態(tài)生成一只狗}...//其它部分省略};計算機學院李衛(wèi)明classCHorse:publicCMammal{public:CMammal*Clone()const{returnnewCHorse(*this);//調用拷貝構造動態(tài)生成一匹馬}...//其它部分省略};下列代碼執(zhí)行后將zoo中原有動物逐個克隆,zoo中動物個數(shù)多了一倍。

size_tn=zoo.size();//原動物數(shù)量

for(size_ti=0;i<n;++i){zoo.push_back(zoo[i]->Clone());//克隆原動物,放入zoo}計算機學院李衛(wèi)明考慮類族統(tǒng)一接口的需要,有時在基類中將某一成員函數(shù)聲明為虛函數(shù),只是在基類中預留了一個接口函數(shù)名,具體功能在基類中并無實現(xiàn),而是在派生類中根據(jù)需要去實現(xiàn),正如上節(jié)動物園例子中所述。這時應當將基類的這個成員函數(shù)聲明為純虛函數(shù),聲明純虛函數(shù)的一般形式是:virtual返回值類型函數(shù)名(形參表)=0;純虛函數(shù)沒有函數(shù)定義,不能被調用,包含純虛函數(shù)的類稱為抽象類或抽象基類。抽象類無法建立對象,只作為一種用作繼承時的基類。如果抽象類派生出的新類還有未定義的純虛函數(shù),新派生類仍然是抽象類。只有所派生出的新類中已定義了基類的所有純虛函數(shù),這時所有虛函數(shù)就可以被調用,這樣的派生類就不再是抽象類,可以用來建立具體的對象。雖然不能建立抽象類的對象,但可以定義抽象類的指針變量,用指針變量指向派生類對象,然后通過該指針調用虛函數(shù),實現(xiàn)多態(tài)性的操作。下節(jié)講述使用了抽象類的典型案例。計算機學院李衛(wèi)明5.5.4 純虛函數(shù)和抽象類程序設計中經(jīng)常需要求各類形狀物體的面積。樣例Ex5.3定義了抽象形狀基類CShape,具有求面積的純虛函數(shù)GetArea,由CShape類派生出3個類:圓類CCircle、長方形類CRectangle和三角形類CTriangle。程序中定義了求面積和函數(shù)doubleTotalArea(constvector<CShape*>&),該函數(shù)用虛函數(shù)計算向量里各基類指針所指物體的面積并求和。樣例程序動態(tài)生成各類對象,將對象指針保存在指針向量容器里,需要時可以調用求面積函數(shù)求容器里指針指向各類對象的正確面積,實現(xiàn)了動態(tài)多態(tài)性,達到了對象創(chuàng)建和對象使用的分離,即使將來需要增加新類型,如橢圓形,TotalArea函數(shù)也無需修改。程序執(zhí)行結束時,指針向量容器vecShapes正常析構,容器對象析構時先執(zhí)行容器里每個元素對象的析構,再釋放本容器直接動態(tài)分配的內存空間。本例中容器里元素類型為普通指針,析構時并不會主動刪除所指對象,需要顯式遍歷刪除所指對象,由于基類析構函數(shù)是虛函數(shù),所以可以動態(tài)調用各對象相應類的析構函數(shù),完成資源的釋放。如果配合第7章智能指針,本例中各對象的刪除可以自動完成。具體代碼參見樣例。計算機學院李衛(wèi)明5.6 典型范例——各類物體面積求和前面討論都是公有繼承,反映派生類對象也是一種基類對象,繼承了基類對象的特征。除了公有繼承,C++允許私有繼承和保護繼承,語法上只需將公有繼承關鍵字改為private或protected即可,形式如下:class派生類名:private基類名{...};class派生類名:protected基類名{...};私有繼承后,基類公有成員和保護成員在派生類里成為私有成員,也就是派生類對象不再具有基類對象所具有的功能,派生類對象不再是一種基類對象,不符合賦值兼容原則。保護繼承后,基類公有成員和保護成員在派生類里成為保護成員,派生類對象不再具有基類對象所具有的功能,派生類對象也不再是一種基類對象,也不符合賦值兼容原則。計算機學院李衛(wèi)明5.7 其它繼承方式程序設計中,絕大部分繼承是公有繼承,私有繼承和保護繼承很少使用。C++規(guī)定,沒有聲明繼承方式時,默認繼承方式是私有繼承。

Ex5.4是私有繼承的樣例,它通過私有繼承整形雙鏈表類list<int>實現(xiàn)了棧類,利用雙鏈表類功能很方便地實現(xiàn)了棧類功能。注意,棧類對象不再是一個雙鏈表對象,不具有雙鏈表對象具有的功能。

具體代碼參見樣例EX5.4。計算機學院李衛(wèi)明在第2章中,我們知道類的數(shù)據(jù)成員可以是其它類類型的對象,具有其它類類型,這個類類型和數(shù)據(jù)成員的類類型之間形成一種組合關系,數(shù)據(jù)成員對象是類類型主對象的一部分,也可以稱為子對象,主對象和子對象具有整體和局部的關系,是一種有(has-a)關系,主對象功能可以通過子對象功能實現(xiàn)。本章討論的公有繼承關系,反映一種是(is-a)關系,派生類對象也是一種基類對象,具有基類對象的功能,派生類對象也具有基類對象部分。所以,反映是(is-a)關系時采用公有繼承,反映有(has-a)關系時采用組合。私有繼承、保護繼承和組合間建議優(yōu)先采用組合。計算機學院李衛(wèi)明5.7.2 繼承與組合如果一個派生類有兩個或更多個基類,這種行為稱為多繼承。C++允許多繼承,具有多繼承的派生類的一般聲明格式如下:class派生類名:繼承方式1基類名1,繼承方式2基類名2……{……//派生類新增的數(shù)據(jù)成員和成員函數(shù)};例如已聲明了類A和類B,可按如下方式聲明多繼承的派生類C:classC:publicA,privateB{……//類C新增的數(shù)據(jù)成員和成員函數(shù)}派生類對象除具有派生類里描述的公有屬性和公有函數(shù)描述的功能外,還繼承了公有繼承的各基類的公有屬性和公有函數(shù)功能。例如,鳥類具有飛功能,馬類具有奔跑的功能,飛馬類公有繼承了鳥類和馬類,飛馬類對象既具有飛的功能,又具有奔跑的功能。

計算機學院李衛(wèi)明5.8 多繼承繼承時,派生類對象包含各基類部分對象和派生類數(shù)據(jù)成員子對象。建立派生類對象時,各基類部分對象和數(shù)據(jù)成員子對象的初始化通過派生類構造函數(shù)完成。多繼承派生類的構造函數(shù)在參數(shù)初始化表中可包含多個基類構造函數(shù),一般定義格式如下:派生類名(形參表):基類名1(基類構造函數(shù)參數(shù)表1),基類名2(基類構造函數(shù)參數(shù)表2),…,數(shù)據(jù)成員1(數(shù)據(jù)成員參數(shù)表),數(shù)據(jù)成員2(數(shù)據(jù)成員參數(shù)表2),…{……//函數(shù)體}構造函數(shù)中各基類構造順序應該與聲明派生類時繼承順序一致,派生類構造函數(shù)的執(zhí)行順序為:先調用各基類的構造函數(shù),再執(zhí)行各數(shù)據(jù)成員的構造或初始化,最后執(zhí)行派生類構造函數(shù)的函數(shù)體。派生類對象撤銷時會執(zhí)行派生類析構函數(shù),完成數(shù)據(jù)成員子對象和基類部分對象的掃尾處理。析構函數(shù)的執(zhí)行順序與構造函數(shù)的執(zhí)行順序正好相反。多繼承在實際應用中并不普遍。計算機學院李衛(wèi)明多繼承在帶來便利性的同時,也可能會帶來一些問題。其中一個問題是二義性問題,比如,兩個類都具有同名的公有成員函數(shù),派生類對象從兩個類都繼承了這功能,調用時就會有二義性。解決二義性的辦法是通過派生類對象調用時在函數(shù)名前加類名::指定調用某個類的成員函數(shù),如樣例Ex5.5所示,或在派生類中定義同名成員函數(shù)覆蓋基類同名成員函數(shù),在派生類同名成員函數(shù)里通過類名::指明調用哪個基類成員函數(shù)。

具體參見樣例代碼Ex5.5。計算機學院李衛(wèi)明5.8.2 二義性問題解決辦法多繼承可能帶來的另一個問題是信息冗余和不一致問題。比如,兩個類從同一個類派生,而這兩個類又共同派生出了一個新類,形成了一個菱形的繼承關系。B類和C類都從A類派生,而D類通過多繼承由B與C共同派生。如果類A有成員函數(shù)Show(),通過D的對象去訪問A類的成員函數(shù)Show(),這時由于類B和類C都有繼承來自于類A的Show()的副本,編譯器無法確定使用哪個副本,將報錯。同時,D的對象中存在2份A類對象部分,即A類數(shù)據(jù)成員有2份,造成信息冗余和信息不一致問題。計算機學院李衛(wèi)明5.8.3 *虛繼承虛繼承是解決多義性問題的一種簡便而有效的方法。虛繼承由關鍵字virtual標識,一般語法格式如下:class派生類名:virtual繼承方式基類名虛繼承時的基類稱為虛基類。虛基類不是在基類定義時聲明,而是在聲明派生類時,在繼承方式前加關鍵字vitual聲明。虛基類的最終子孫類對象只建立一份虛基類部分對象,虛基類部分對象構造方法在最終建立對象的子孫類構造函數(shù)中指明,忽略中間類構造函數(shù)里虛基類構造部分,中間類部分對象構造時將不再構造虛基類部分對象。這樣將不會導致信息冗余和不一致問題,也不會出現(xiàn)多義性問題。如果在虛基類中定義有帶參數(shù)的構造函數(shù),并且參數(shù)沒有默認值,而且沒有定義無參構造函數(shù),則在虛基類的直接派生類或間接派生類的構造函數(shù)的初始化表中都要對虛基類進行初始化,均應加上:虛基類構造函數(shù)名(參數(shù)表)計算機學院李衛(wèi)明樣例Ex5.6中描述了普通人類CPerson、研究生類CGraduate、教師類CTeacher、在職研究生CGraduateTeacher類,各類之間的繼承關系如圖5.3所示。CPerson具有姓名、年齡數(shù)據(jù)成員和分別設置、獲取姓名、年齡的公有函數(shù)成員,CGraduate類具有學習某課程能力和顯示已學課程列表功能,CTeacher類具有教授某課程和顯示所有教授課程能力,CGraduate類和CTeacher類繼承了CPerson類公有成員。CGraduateTeacher類共有繼承CGraduate類、CTeacher類,CGraduateTeacher類對象研究生助教可以認為既是研究生,也是教師,具有研究生和教師的雙重功能。具體代碼參見Ex5.6。圖5.3研究生助教類CGraduateTeacher菱形繼承關系圖計算機學院李衛(wèi)明前面講述了利用繼承和多態(tài)性進行C++程序設計。本節(jié)深度探索派生類對象的內存分布、虛函數(shù)機制實現(xiàn)原理和特殊場合使用的運行時類型識別和動態(tài)類型轉換。C++標準并未規(guī)定這些機制如何實現(xiàn),探索這些機制的實現(xiàn)原理,有利于加深對C++程序執(zhí)行機制的理解,設計出結構良好、高效的程序。計算機學院李衛(wèi)明5.9 深度探索無論是公有繼承、保護繼承還是私有繼承,派生類對象內部都包含了基類部分對象的數(shù)據(jù)成員和派生類新增數(shù)據(jù)成員,基類部分對象的數(shù)據(jù)成員本身也可能由祖先類部分對象的數(shù)據(jù)成員和基類本身數(shù)據(jù)成員組成。根據(jù)賦值兼容原則,基類指針可以指向派生類對象。下面探討派生類對象的內存分布。計算機學院李衛(wèi)明5.9.1 派生類對象的內存分布//Ex5.7A完整代碼見樣例classCBase{//成員省略};classCDerived:publicCbase{//函數(shù)省略};intmain(){CDerivedd;CBase*pb;CDerived*pd;pd=&d;pb=pd;cout<<reinterpret_cast<int>(pd)<<endl;//輸出指針值

cout<<reinterpret_cast<int>(pb)<<endl;}計算機學院李衛(wèi)明5.9.1.1 單繼承派生類對象內存分布如圖5.4所示,樣例Ex5.7A派生類對象d內存包含基類部分對象的數(shù)據(jù)成員和派生類部分新增的數(shù)據(jù)成員,派生類指針變量pd指向派生類對象d,根據(jù)賦值兼容原則,可以將派生類對象指針pd賦值給基類對象指針pb,單繼承時,2個指針變量值相同。圖5.4單繼承CDerived對象內存分布圖計算機學院李衛(wèi)明//Ex5.7B完整代碼見樣例classCBase1{//成員省略};classCBase2{//成員省略};classCDerived:publicCBase1,publicCBase2{//成員省略};intmain(){CDerivedd;CBase1*pb1;CBase2*pb2;CDerived*pd;pd=&d;pb1=pd;pb2=pd;cout<<reinterpret_cast<int>(pd)<<endl;//輸出指針值

cout<<reinterpret_cast<int>(pb1)<<endl;cout<<reinterpret_cast<int>(pb2)<<endl;}計算機學院李衛(wèi)明5.9.1.2 *多繼承派生類對象內存分布如圖5.5所示,派生類對象d內存包含2個基類部分對象的數(shù)據(jù)成員和派生類部分新增的數(shù)據(jù)成員,派生類指針變量pd指向派生類對象d,根據(jù)賦值兼容原則,可以將派生類對象指針pd賦值給基類對象指針pb1和基類對象指針pb2,多繼承時,pb1、pd指針變量值相同,pb2指針變量值不同,比pb1、pd指針變量值大CBase1部分對象內存大?。?字節(jié)。這里,我們看到指針變量賦值不僅僅是數(shù)值的復制。注意,當派生類對象指針變量pd值為nullptr時,基類對象指針pb1和基類對象指針pb2值應該也是nullptr。圖5.5多繼承CDerived對象內存分布圖計算機學院李衛(wèi)明//Ex5.7C完整代碼見樣例classCCommonBase{//成員省略};classCBase1:virtualpublicCCommonBase{//成員省略};classCBase2:virtualpublicCCommonBase{//成員省略};classCDerived:publicCBase1,publicCBase2{//成員省略};intmain(){CDerivedd;CCommonBase*pcb;CBase1*pb1;CBase2*pb2;CDerived*pd;pd=&d;pb1=pd;pb2=pd;pcb=pd;cout<<reinterpret_cast<int>(pd)<<endl;//輸出指針值

cout<<reinterpret_cast<int>(pcb)<<endl;cout<<reinterpret_cast<int>(pb1)<<endl;cout<<reinterpret_cast<int>(pb2)<<endl;}

計算機學院李衛(wèi)明5.9.1.3 *虛繼承派生類對象內存分布如圖5.6所示,派生類對象d內存包含1份CCommonBase部分對象、2個基類CBase1、CBase2部分對象和派生類部分新增的數(shù)據(jù)成員。pd指向派生類對象d,根據(jù)賦值兼容原則,可以將pd賦值給pb1、pb2、pcb,pb1、pb2也需要能夠訪問CCommonBase部分對象,CBase1、CBase2基類部分對象包含公共基類部分對象的偏移或地址,pb1、pd指針變量值相同,pb2指針變量值不同,比pb1、pd指針變量值大CBase1部分對象內存大小:數(shù)據(jù)成員和偏移量共8字節(jié),pcb指針變量值,比pb1、pd指針變量值大CBase1部分對象內存大小8字節(jié)、CBase2部分對象內存大小8字節(jié)和CDerived類新增數(shù)據(jù)成員大小4字節(jié):共20字節(jié)。這里展示了指針變量賦值不僅僅是數(shù)值復制的更復雜案例。注意,當pd值為nullptr時,pb1、pb2、pcb值應該也是nullptr。圖5.6虛繼承CDerived對象內存分布圖計算機學院李衛(wèi)明C++中動態(tài)多態(tài)性通過虛函數(shù)體現(xiàn),編譯器一般是通過為每個具有虛函數(shù)的類建立虛函數(shù)表實現(xiàn)虛函數(shù)的。計算機學院李衛(wèi)明5.9.2 虛函數(shù)實現(xiàn)原理虛函數(shù)體現(xiàn)了多態(tài)性.classCDog:classCAnimal{…};CAnimal*p=newCDog;p->Move();虛函數(shù)工作原理.V-表(VirtualTable)中包含虛函數(shù)地址.基類數(shù)據(jù)成員派生類數(shù)據(jù)成員對象內存分布圖V-表如圖5.7所示,基類CBase和派生類CDerived都具有虛函數(shù)。CBase類虛函數(shù)表包含2個指針項,指向CBase類f1、f2函數(shù),CDerived類虛函數(shù)表包含3個指針項,指向CDerived類改寫后的f1函數(shù)、繼承CBase類的f2函數(shù)、CDerived類新增的f3函數(shù),每個具有含虛函數(shù)類的對象都含有一個指針vptr,指向各自類的虛函數(shù)表,調用虛函數(shù)動態(tài)綁定時,根據(jù)對象里保存的vptr指針,查找所指虛函數(shù)表中對應函數(shù)項,執(zhí)行所指函數(shù)即可實現(xiàn)動態(tài)多態(tài)性。虛函數(shù)表不含指向普通函數(shù)的指針。圖5.7虛函數(shù)實現(xiàn)原理計算機學院李衛(wèi)明完整代碼見樣例Ex5.8。

樣例中基類CBase具有4個成員函數(shù):普通成員函數(shù)g1、g2和虛成員函數(shù)f1、f2,派生類CDerived公有繼承基類CBase,也有4個成員函數(shù):普通成員函數(shù)g1、g3和虛成員函數(shù)f1、f3,其中g1、f1覆蓋基類同名成員函數(shù),g3、f3是派生類新增成員函數(shù),樣例中通過實際指向派生類對象的基類指針調用g1、g2和f1、f2時,g1、g2是普通成員函數(shù),根據(jù)調用時指針類型靜態(tài)綁定執(zhí)行基類函數(shù),f1、f2是虛成員函數(shù),根據(jù)調用時指針所指實際對象動態(tài)綁定CDerived類的相應函數(shù),CDerived類f1虛成員函數(shù)已覆蓋基類f1虛成員函數(shù),執(zhí)行CDerived類f1虛成員函數(shù),CDerived類繼承了基類f2函數(shù),執(zhí)行CBase類f2函數(shù)。

計算機學院李衛(wèi)明根據(jù)賦值兼容原則,派生類對象指針可以賦值給基類對象指針,內部所需轉換是在編譯時進行的,是靜態(tài)轉換。這樣,同一類族的對象可以統(tǒng)一管理,再配合動態(tài)多態(tài)性統(tǒng)一使用。反過來,有時需要發(fā)揮其中某些派生類對象的派生類特有功能,這時,一般不可以使用編譯時靜態(tài)轉換static_cast將基類對象指針轉換成派生類對象指針,因為基類對象指針實際可能并非指向指定的派生類對象,這樣的static_cast編譯時靜態(tài)轉換不安全。計算機學院李衛(wèi)明5.9.3 *運行時類型識別RTTI和動態(tài)類型轉換dynamic_cast下面討論建立在上節(jié)樣例基礎上。CBase*pb,baseObj;CDerived*pd,derivedObj;//CDerived公有繼承baseObjpb=&baseObj;pd=static_cast<CDerived*>(pb);//不安全if(pd!=nullptr)pd->g3();pb=&derivedObjj;pd=static_cast<CDerived*>(pb);//可行if(pd!=nullptr)pd->g3();計算機學院李衛(wèi)明C++提供了運行時類型識別RTTI(RunTimeTypeIdentification)和動態(tài)類型轉換dynamic_cast機制用于解決這一特定需求。C++將含虛函數(shù)的類稱為多態(tài)性類,不含虛函數(shù)的類稱為非多態(tài)性類。每個多態(tài)性類具有像虛函數(shù)表一樣的類型信息,編譯器一般在多態(tài)性類的虛函數(shù)表中保存類型信息對象,每個多態(tài)性類的對象有指針指向虛函數(shù)表,因而可獲得的對象的類型信息。C++程序可以通過通過下述2種語句獲得類型信息,獲得的類型信息為type_info類型對象的常引用,type_info類型對象可以進行相等比較和通過成員函數(shù)返回類型信息文字描述,使用時需包含頭文件<typeinfo>:consttype_info&tiObj=typeid(表達式);consttype_info&tiObj=typeid(類型);當表達式結果為非多態(tài)性類時,編譯器編譯時就可以獲取類型信息,一般也無此必要;當表達式結果為多態(tài)性類對象引用或指針時,上述語句返回相應多態(tài)性類的虛函數(shù)表中類型信息對象的常引用,為多態(tài)性類提供了運行時類型識別RTTI支持。計算機學院李衛(wèi)明C++通過運行時類型識別RTTI支持,進一步支持運行時動態(tài)類型轉換dynamic_cast:程序運行過程中,遇到dynamic_cast基類指針轉換時,查詢基類指針所指對象的實際類型信息,基類指針如果實際指向派生類對象,就可以通過dynamic_cast安全轉換為派生類指針,基類指針如果實際指向的不是派生類對象,dynamic_cast轉換返回空指針。下述語句段可以正常獲得預期結果:CBase*pb,baseObj;CDerived*pd,derivedObj;//CDerived公有繼承baseObjpb=&baseObj;pd=dynamic_cast<CDerived*>(pb);//安全,實際返回空指針if(pd!=nullptr)pd->g3();pb=&derivedObjj;pd=dynamic_cast<CDerived*>(pb);//可行,實際返回派生類對象指針if(pd!=nullptr)pd->g3();計算機學院李衛(wèi)明動態(tài)類型轉換dynamic_cast同樣適用于基類對象引用。程序運行過程中,遇到dynamic_cast基類引用轉換時,查詢基類引用對象的實際類型信息,基類引用的實際是派生類對象,就可以通過dynamic_cast安全轉換為派生類對象引用,

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經(jīng)權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論