C++將派生類賦值給基類(向上轉型)

在 C/C++ 中經常會發生數據類型的轉換,例如將 int 類型的數據賦值給 float 類型的變量時,編譯器會先把 int 類型的數據轉換爲 float 類型再賦值;反過來,float 類型的數據在經過類型轉換後也可以賦值給 int 類型的變量。

數據類型轉換的前提是,編譯器知道如何對數據進行取捨。例如:

 
  1. int a = 10.9;
  2. printf("%d\n", a);

輸出結果爲 10,編譯器會將小數部分直接丟掉(不是四捨五入)。再如:

 
  1. float b = 10;
  2. printf("%f\n", b);

輸出結果爲 10.000000,編譯器會自動添加小數部分。

類其實也是一種數據類型,也可以發生數據類型轉換,不過這種轉換隻有在基類和派生類之間纔有意義,並且只能將派生類賦值給基類,包括將派生類對象賦值給基類對象、將派生類指針賦值給基類指針、將派生類引用賦值給基類引用,這在 C++ 中稱爲向上轉型(Upcasting)。相應地,將基類賦值給派生類稱爲向下轉型(Downcasting)。

向上轉型非常安全,可以由編譯器自動完成;向下轉型有風險,需要程序員手動干預。本節只介紹向上轉型,向下轉型將在後續章節介紹。

向上轉型和向下轉型是面向對象編程的一種通用概念,它們也存在於   JavaC#  等編程語言中。

將派生類對象賦值給基類對象

下面的例子演示瞭如何將派生類對象賦值給基類對象:

 
  1. #include <iostream>
  2. using namespace std;
  3.  
  4. //基類
  5. class A{
  6. public:
  7. A(int a);
  8. public:
  9. void display();
  10. public:
  11. int m_a;
  12. };
  13. A::A(int a): m_a(a){ }
  14. void A::display(){
  15. cout<<"Class A: m_a="<<m_a<<endl;
  16. }
  17.  
  18. //派生類
  19. class B: public A{
  20. public:
  21. B(int a, int b);
  22. public:
  23. void display();
  24. public:
  25. int m_b;
  26. };
  27. B::B(int a, int b): A(a), m_b(b){ }
  28. void B::display(){
  29. cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
  30. }
  31.  
  32.  
  33. int main(){
  34. A a(10);
  35. B b(66, 99);
  36. //賦值前
  37. a.display();
  38. b.display();
  39. cout<<"--------------"<<endl;
  40. //賦值後
  41. a = b;
  42. a.display();
  43. b.display();
  44.  
  45. return 0;
  46. }

運行結果:
Class A: m_a=10
Class B: m_a=66, m_b=99
----------------------------
Class A: m_a=66
Class B: m_a=66, m_b=99

本例中 A 是基類, B 是派生類,a、b 分別是它們的對象,由於派生類 B 包含了從基類 A 繼承來的成員,因此可以將派生類對象 b 賦值給基類對象 a。通過運行結果也可以發現,賦值後 a 所包含的成員變量的值已經發生了變化。

賦值的本質是將現有的數據寫入已分配好的內存中,對象的內存只包含了成員變量,所以對象之間的賦值是成員變量的賦值,成員函數不存在賦值問題。運行結果也有力地證明了這一點,雖然有a=b;這樣的賦值過程,但是 a.display() 始終調用的都是 A 類的 display() 函數。換句話說,對象之間的賦值不會影響成員函數,也不會影響 this 指針。

將派生類對象賦值給基類對象時,會捨棄派生類新增的成員,也就是“大材小用”,如下圖所示:


可以發現,即使將派生類對象賦值給基類對象,基類對象也不會包含派生類的成員,所以依然不同通過基類對象來訪問派生類的成員。對於上面的例子,a.m_a 是正確的,但 a.m_b 就是錯誤的,因爲 a 不包含成員 m_b。

這種轉換關係是不可逆的,只能用派生類對象給基類對象賦值,而不能用基類對象給派生類對象賦值。理由很簡單,基類不包含派生類的成員變量,無法對派生類的成員變量賦值。同理,同一基類的不同派生類對象之間也不能賦值。

要理解這個問題,還得從賦值的本質入手。賦值實際上是向內存填充數據,當數據較多時很好處理,捨棄即可;本例中將 b 賦值給 a 時(執行a=b;語句),成員 m_b 是多餘的,會被直接丟掉,所以不會發生賦值錯誤。但當數據較少時,問題就很棘手,編譯器不知道如何填充剩下的內存;如果本例中有b= a;這樣的語句,編譯器就不知道該如何給變量 m_b 賦值,所以會發生錯誤。

將派生類指針賦值給基類指針

除了可以將派生類對象賦值給基類對象(對象變量之間的賦值),還可以將派生類指針賦值給基類指針(對象指針之間的賦值)。我們先來看一個多繼承的例子,繼承關係爲:


下面的代碼實現了這種繼承關係:

 
  1. #include <iostream>
  2. using namespace std;
  3.  
  4. //基類A
  5. class A{
  6. public:
  7. A(int a);
  8. public:
  9. void display();
  10. protected:
  11. int m_a;
  12. };
  13. A::A(int a): m_a(a){ }
  14. void A::display(){
  15. cout<<"Class A: m_a="<<m_a<<endl;
  16. }
  17.  
  18. //中間派生類B
  19. class B: public A{
  20. public:
  21. B(int a, int b);
  22. public:
  23. void display();
  24. protected:
  25. int m_b;
  26. };
  27. B::B(int a, int b): A(a), m_b(b){ }
  28. void B::display(){
  29. cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
  30. }
  31.  
  32. //基類C
  33. class C{
  34. public:
  35. C(int c);
  36. public:
  37. void display();
  38. protected:
  39. int m_c;
  40. };
  41. C::C(int c): m_c(c){ }
  42. void C::display(){
  43. cout<<"Class C: m_c="<<m_c<<endl;
  44. }
  45.  
  46. //最終派生類D
  47. class D: public B, public C{
  48. public:
  49. D(int a, int b, int c, int d);
  50. public:
  51. void display();
  52. private:
  53. int m_d;
  54. };
  55. D::D(int a, int b, int c, int d): B(a, b), C(c), m_d(d){ }
  56. void D::display(){
  57. cout<<"Class D: m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
  58. }
  59.  
  60.  
  61. int main(){
  62. A *pa = new A(1);
  63. B *pb = new B(2, 20);
  64. C *pc = new C(3);
  65. D *pd = new D(4, 40, 400, 4000);
  66.  
  67. pa = pd;
  68. pa -> display();
  69.  
  70. pb = pd;
  71. pb -> display();
  72.  
  73. pc = pd;
  74. pc -> display();
  75.  
  76. cout<<"-----------------------"<<endl;
  77. cout<<"pa="<<pa<<endl;
  78. cout<<"pb="<<pb<<endl;
  79. cout<<"pc="<<pc<<endl;
  80. cout<<"pd="<<pd<<endl;
  81.  
  82. return 0;
  83. }

運行結果:
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400
-----------------------
pa=0x9b17f8
pb=0x9b17f8
pc=0x9b1800
pd=0x9b17f8

本例中定義了多個對象指針,並嘗試將派生類指針賦值給基類指針。與對象變量之間的賦值不同的是,對象指針之間的賦值並沒有拷貝對象的成員,也沒有修改對象本身的數據,僅僅是改變了指針的指向。

1) 通過基類指針訪問派生類的成員

請讀者先關注第 68 行代碼,我們將派生類指針 pd 賦值給了基類指針 pa,從運行結果可以看出,調用 display() 函數時雖然使用了派生類的成員變量,但是 display() 函數本身卻是基類的。也就是說,將派生類指針賦值給基類指針時,通過基類指針只能使用派生類的成員變量,但不能使用派生類的成員函數,這看起來有點不倫不類,究竟是爲什麼呢?第 71、74 行代碼也是類似的情況。

pa 本來是基類 A 的指針,現在指向了派生類 D 的對象,這使得隱式指針 this 發生了變化,也指向了 D 類的對象,所以最終在 display() 內部使用的是 D 類對象的成員變量,相信這一點不難理解。

編譯器雖然通過指針的指向來訪問成員變量,但是卻不通過指針的指向來訪問成員函數:編譯器通過指針的類型來訪問成員函數。對於 pa,它的類型是 A,不管它指向哪個對象,使用的都是 A 類的成員函數,具體原因已在《C++函數編譯原理和成員函數的實現》中做了詳細講解。

概括起來說就是:編譯器通過指針來訪問成員變量,指針指向哪個對象就使用哪個對象的數據;編譯器通過指針的類型來訪問成員函數,指針屬於哪個類的類型就使用哪個類的函數。

2) 賦值後值不一致的情況

本例中我們將最終派生類的指針 pd 分別賦值給了基類指針 pa、pb、pc,按理說它們的值應該相等,都指向同一塊內存,但是運行結果卻有力地反駁了這種推論,只有 pa、pb、pd 三個指針的值相等,pc 的值比它們都大。也就是說,執行pc = pd;語句後,pc 和 pd 的值並不相等。

這非常出乎我們的意料,按照我們通常的理解,賦值就是將一個變量的值交給另外一個變量,不會出現不相等的情況,究竟是什麼導致了 pc 和 pd 不相等呢?我們將在《將派生類指針賦值給基類指針時到底發生了什麼?》一節中解開謎底。

將派生類引用賦值給基類引用

引用在本質上是通過指針的方式實現的,這一點已在《引用在本質上是什麼,它和指針到底有什麼區別》中進行了講解,既然基類的指針可以指向派生類的對象,那麼我們就有理由推斷:基類的引用也可以指向派生類的對象,並且它的表現和指針是類似的。

修改上例中 main() 函數內部的代碼,用引用取代指針:

 
  1. int main(){
  2. D d(4, 40, 400, 4000);
  3.  
  4. A &ra = d;
  5. B &rb = d;
  6. C &rc = d;
  7.  
  8. ra.display();
  9. rb.display();
  10. rc.display();
  11.  
  12. return 0;
  13. }

運行結果:
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400

ra、rb、rc 是基類的引用,它們都引用了派生類對象 d,並調用了 display() 函數,從運行結果可以發現,雖然使用了派生類對象的成員變量,但是卻沒有使用派生類的成員函數,這和指針的表現是一樣的。

引用和指針的表現之所以如此類似,是因爲引用和指針並沒有本質上的區別,引用僅僅是對指針進行了簡單封裝,讀者可以猛擊《引用在本質上是什麼,它和指針到底有什麼區別》一文深入瞭解。

最後需要注意的是,向上轉型後通過基類的對象、指針、引用只能訪問從基類繼承過去的成員(包括成員變量和成員函數),不能訪問派生類新增的成員。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章