詳解C++中基類與派生類的轉換和虛基類。本站提示廣大學習愛好者:(詳解C++中基類與派生類的轉換和虛基類)文章只能為提供參考,不一定能成為您想要的結果。以下是詳解C++中基類與派生類的轉換和虛基類正文
C++基類與派生類的轉換
在公用繼續、公有繼續和掩護繼續中,只要公用繼續能較好地保存基類的特點,它保存了除結構函數和析構函數之外的基類一切成員,基類的公用或掩護成員的拜訪權限在派生類中全體都按原樣保存上去了,在派生類外可以挪用基類的公用成員函數拜訪基類的公有成員。是以,公用派生類具有基類的全體功效,一切基類可以或許完成的功效, 公用派生類都能完成。而非公用派生類(公有或掩護派生類)不克不及完成基類的全體功效(例如在派生類外不克不及挪用基類的公用成員函數拜訪基類的公有成員)。是以,只要公用派生類才是基類真實的子類型,它完全地繼續了基類的功效。
分歧類型數據之間在必定前提下可以停止類型的轉換,例如整型數據可以賦給雙精度型變量,在賦值之前,把整型數據先轉換成為雙精度型數據,然則不克不及把一個整型數據賦給指針變量。這類分歧類型數據之間的主動轉換和賦值,稱為賦值兼容。如今要評論辯論 的成績是:基類與派生類對象之間能否也有賦值兼容的關系,能否停止類型間的轉換?
答復是可以的。基類與派生類對象之間有賦值兼容關系,因為派生類中包括從基類繼續的成員,是以可以將派生類的值賦給基類對象,在用到基類對象的時刻可以用其子類對象取代。詳細表示在以下幾個方面。
1) 派生類對象可以向基類對象賦值
可以用子類(即公用派生類)對象對其基類對象賦值。如
A a1; //界說基類A對象a1 B b1; //界說類A的公用派生類B的對象b1 a1=b1; //用派生類B對象b1對基類對象a1賦值
在賦值時捨棄派生類本身的成員。也就是“年夜材小用”,如圖
現實上,所謂賦值只是對數據成員賦值,對成員函數不存在賦值成績。
請留意,賦值後不克不及妄圖經由過程對象a1去拜訪派生類對象b1的成員,由於b1的成員與a1的成員是分歧的。假定age是派生類B中增長的公用數據成員,剖析上面的用法:
a1.age=23; //毛病,a1中不包括派生類中增長的成員
b1.age=21; //准確,b1中包括派生類中增長的成員
應該留意,子類型關系是單向的、弗成逆的。B是A的子類型,不克不及說A是B的子類型。只能用子類對象對其基類對象賦值,而不克不及用基類對象對其子類對象賦值,來由是明顯的,由於基類對象不包括派生類的成員,沒法對派生類的成員賦值。同理,統一基類的分歧派生類對象之間也不克不及賦值。
2) 派生類對象可以替換基類對象向基類對象的援用停止賦值或初始化
如已界說了基類A對象a1,可以界說a1的援用變量:
A a1; //界說基類A對象a1 B b1; //界說公用派生類B對象b1 A& r=a1; //界說基類A對象的援用變量r,並用a1對其初始化
這時候,援用變量r是a1的別號,r和a1同享統一段存儲單位。也能夠用子類對象初始化援用變量r,將下面最初一行改成
A& r=b1; //界說基類A對象的援用變量r,並用派生類B對象b1對其初始化
或許保存下面第3行“A& r=a1;”,而對r從新賦值:
r=b1; //用派生類B對象b1對a1的援用變量r賦值
留意,此時r其實不是b1的別號,也不與b1同享統一段存儲單位。它只是b1中基類部門的別號,r與b1中基類部門同享統一段存儲單位,r與b1具有雷同的肇端地址。
3) 假如函數的參數是基類對象或基類對象的援用,響應的實參可以用子類對象。
若有一函數:
fun: void fun(A& r) //形參是類A的對象的援用變量 { cout<<r.num<<endl; } //輸入該援用變量的數據成員num
函數的形參是類A的對象的援用變量,原來實參應當為A類的對象。因為子類對象與派生類對象賦值兼容,派生類對象能主動轉換類型,在挪用fun函數時可以用派生類B的對象b1作實參:
fun(b1);
輸入類B的對象b1的基類數據成員num的值。
與前雷同,在fun函數中只能輸入派生類中基類成員的值。
4) 派生類對象的地址可以賦給指向基類對象的指針變量,也就是說,指向基類對象的指針變量也能夠指向派生類對象。
[例] 界說一個基類Student(先生),再界說Student類的公用派生類Graduate(研討生), 用指向基類對象的指針輸入數據。本例重要是解釋用指向基類對象的指針指向派生類對象,為了削減法式長度,在每一個類中只設很少成員。先生類只設num(學號),name(名字)和score(成就)3個數據成員,Graduate類只增長一個數據成員pay(工資)。法式以下:
#include <iostream> #include <string> using namespace std; class Student//聲明Student類 { public: Student(int, string,float); //聲明結構函數 void display( ); //聲明輸入函數 private: int num; string name; float score; }; Student::Student(int n, string nam,float s) //界說結構函數 { num=n; name=nam; score=s; } void Student::display( ) //界說輸入函數 { cout<<endl<<"num:"<<num<<endl; cout<<"name:"<<name<<endl; cout<<"score:"<<score<<endl; } class Graduate:public Student //聲明公用派生類Graduate { public: Graduate(int, string ,float,float); //聲明結構函數 void display( ); //聲明輸入函數 private: float pay; //工資 }; //界說結構函數 Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ } void Graduate::display() //界說輸入函數 { Student::display(); //挪用Student類的display函數 cout<<"pay="<<pay<<endl; } int main() { Student stud1(1001,"Li",87.5); //界說Student類對象stud1 Graduate grad1(2001,"Wang",98.5,563.5); //界說Graduate類對象grad1 Student *pt=&stud1; //界說指向Student類對象的指針並指向stud1 pt->display( ); //挪用stud1.display函數 pt=&grad1; //指針指向grad1 pt->display( ); //挪用grad1.display函數 }
上面對法式的剖析很主要,請年夜家細心浏覽和思慮。
許多讀者會以為,在派生類中有兩個同名的display成員函數,依據同名籠罩的規矩,被挪用的應該是派生類Graduate對象的display函數,在履行Graduate::display函數進程中挪用Student::display函數,輸入num,name,score,然後再輸入pay的值。
現實上這類推論是毛病的,先看看法式的輸入成果:
num:1001 name:Li score:87.5 num:2001 name:wang score:98.5
前3行是先生stud1的數據,後3行是研討生grad1的數據,並沒有輸入pay的值。
成績在於pt是指向Student類對象的指針變量,即便讓它指向了grad1,但現實上pt指向的是grad1中從基類繼續的部門。
經由過程指向基類對象的指針,只能拜訪派生類中的基類成員,而不克不及拜訪派生類增長的成員。所以pt->display()挪用的不是派生類Graduate對象所增長的display函數,而是基類的display函數,所以只輸入研討生grad1的num,name,score3個數據。
假如想經由過程指針輸入研討生grad1的pay,可以另設一個指向派生類對象的指針變量ptr,使它指向grad1,然後用ptr->display()挪用派生類對象的display函數。但這不年夜便利。
經由過程本例可以看到,用指向基類對象的指針變量指向子類對象是正當的、平安的,不會湧現編譯上的毛病。但在運用上卻不克不及完整知足人們的願望,人們有時願望經由過程應用基類指針可以或許挪用基類和子類對象的成員。假如能做到這點,法式人員會覺得便利。後續章節將會處理這個成績。方法是應用虛函數和多態性。
C++虛基類詳解
多繼續時很輕易發生定名抵觸,即便我們很當心地將一切類中的成員變量和成員函數都定名為分歧的名字,定名抵觸仍然有能夠產生,好比異常經典的菱形繼續條理。以下圖所示:
類A派生出類B和類C,類D繼續自類B和類C,這個時刻類A中的成員變量和成員函數繼續到類D中釀成了兩份,一份來自 A-->B-->D 這一路,另外一份來自 A-->C-->D 這一條路。
在一個派生類中保存直接基類的多份同名成員,固然可以在分歧的成員變量平分別寄存分歧的數據,但年夜多半情形下這是過剩的:由於保存多份成員變量不只占用較多的存儲空間,還輕易發生定名抵觸,並且很少有如許的需求。
為懂得決這個成績,C++供給了虛基類,使得在派生類中只保存直接基類的一份成員。
聲明虛基類只須要在繼續方法後面加上 virtual 症結字,請看上面的例子:
#include <iostream> using namespace std; class A{ protected: int a; public: A(int a):a(a){} }; class B: virtual public A{ //聲明虛基類 protected: int b; public: B(int a, int b):A(a),b(b){} }; class C: virtual public A{ //聲明虛基類 protected: int c; public: C(int a, int c):A(a),c(c){} }; class D: virtual public B, virtual public C{ //聲明虛基類 private: int d; public: D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){} void display(); }; void D::display(){ cout<<"a="<<a<<endl; cout<<"b="<<b<<endl; cout<<"c="<<c<<endl; cout<<"d="<<d<<endl; } int main(){ (new D(1, 2, 3, 4)) -> display(); return 0; }
運轉成果:
a=1 b=2 c=3 d=4
本例中我們應用了虛基類,在派生類D中只要一份成員變量 a 的拷貝,所以在 display() 函數中可以直接拜訪 a,而不消加類名和域解析符。
請留意派生類D的結構函數,與以往的用法有所分歧。以往,在派生類的結構函數中只需擔任對其直接基類初始化,再由其直接基類擔任對直接基類初始化。如今,因為虛基類在派生類中只要一份成員變量,所以對這份成員變量的初始化必需由派生類直接給出。假如不由最初的派生類直接對虛基類初始化,而由虛基類的直接派生類(如類B和類C)對虛基類初始化,就有能夠因為在類B和類C的結構函數中對虛基類給出分歧的初始化參數而發生抵觸。所以劃定:在最初的派生類中不只要擔任對其直接基類停止初始化,還要擔任對虛基類初始化。
有的讀者會提出:類D的結構函數經由過程初始化表調了虛基類的結構函數A,而類B和類C的結構函數也經由過程初始化表挪用了虛基類的結構函數A,如許虛基類的結構函數難道被挪用了3次?年夜家不用過慮,C++編譯體系只履行最初的派生類對虛基類的結構函數的挪用,而疏忽虛基類的其他派生類(如類B和類C)對虛基類的結構函數的挪用,這就包管了虛基類的數據成員不會被屢次初始化。
最初請留意:為了包管虛基類在派生類中只繼續一次,應該在該基類的一切直接派生類中聲明為虛基類,不然依然會湧現對基類的屢次繼續。
可以看到:應用多重繼續時要非常當心,常常會湧現二義性成績。下面的例子是簡略的,假如派生的條理再多一些,多重繼續更龐雜一些,法式員就很輕易陷人迷 魂陣,法式的編寫、調試和保護任務都邑變得加倍艱苦。是以許多法式員不倡導在法式中應用多重繼續,只要在比擬簡略和不容易湧現二義性的情形或其實需要時才應用多重繼續,能用單一繼續處理的成績就不要應用多重繼續。也正因為這個緣由,C++以後的許多面向對象的編程說話(如Java、Smalltalk、C#、PHP等)其實不支撐多重繼續。