天天看點

C++:43---派生類向基類轉換、靜态/動态的類變量

一、繼承中類的類型轉換規則

  • 我們普通的程式設計規則規定,如果我們想把引用或指針綁定到一個對象上,則引用或指針的類型必須與所綁定的對象的類型一緻或者對象的類型含有一種可接受的const類型轉換規則。但是繼承關系中的類比較例外,其規則如下:
  • ①我們可以将基類的指針或引用綁定到派生對象上
  1. #include <iostream>
  2. class A {};
  3. class B:public A{};
  4. int main()
  5. {
  6. A *a;
  7. B b;
  8. a = &b;
  9. return 0;
  10. }
  • ②即使不是指針/引用類型,我們也可以将派生類轉換為基類
  1. #include <iostream>
  2. class A {};
  3. class B:public A{};
  4. int main()
  5. {
  6. A a;
  7. B b;
  8. a = b;
  9. return 0;
  10. }
  • ②不能将基類對象綁定到派生類的指針/引用上
  1. A a;
  2. B *b;
  3. b = &a; //程式錯誤,不能将基類對象轉換為派生類對象

二、轉換的本質

  • 派生類可以轉換為基類的本質是:
  • ①為什麼派生類可以轉換為基類:派生類從基類而來,是以派生類中包含了基類的方法和成員。此時基類可以通過指針或引用指向派生類(相當于将派生類從基類中繼承的那部分方法和成員綁定到基類上了,相當于派生類被截斷了),然後基類就可以将派生類假裝是一個基類對象來使用(調用其中的成員/方法)
  • ②為什麼基類不能轉換為派生類:因為派生類可能會定義自己新的成員/方法,但是這些成員/方法是基類中所沒有的。如果将一個基類對象綁定到派生類的指針/引用上,此時派生類通過指針/引用通路自己新定義的成員/方法時,發現找不到(是以不能将基類轉換為派生類)
  • 例如:下面B繼承于A,子類繼承于父類,同時為父類的成員開辟了空間。将子類對象指派給父類對象,相當于将子類中的父類成員變量指派給父類
C++:43---派生類向基類轉換、靜态/動态的類變量

三、繼承方式對類型轉換的影響

遵循下面3個規則:

假設B繼承于A

  • ①隻有當B公有地繼承A時,使用者代碼才能使用派生類向基類轉換;如果B是受保護的/私有的繼承于A,則不能使用派生類向基類轉換
  • 因為保護或者私有繼承,基類的成員/方法在子類中都變為保護或者私有的了,是以轉換之後基類也無法通過指針通路
class A{};
class B :public A{};
class C :protected A{};
int main()
{
A *a;
B b;
C c;
a = &b; //正确
a = &c; //錯誤
return 0;
}      
  • ②B不論以什麼方式繼承于A,B的成員函數和友元中可以将派生類對象向基類轉換
class A{};
class B :protected A
{
void func() {
A *a;
B b;
a = &b;   //正确,成員函數内可以
a = this; //正确,成員函數内可以
}
friend void func2(); //友元函數
};
void func2()
{
A *a;
B b;
a = &b; //正确,友元函數内可以
}
int main()
{
A *a;
B b;
a = &b; //錯誤,因為為保護繼承
return 0;
}      
  • ③如果B繼承于A的方式是公有的或者受保護的,則B的派生類的成員和友元可以使用B向A的類型轉換;如果B繼承于A的方式是私有的,則不能
class A{};
class B :protected A{};
class C :public B {
void func1() {
A *a;
B b;
a = &b;   //正确
a = this; //正确
}
};      

四、一種出錯的情景

  • 下面案例我們先将派生類轉換為基類,然後再将基類轉換為派生類,這樣是錯的
//假設B公有繼承于A
A *a;
B b;
a = &b;   //将派生類轉換為基類,正确
B *p = a; //将基類再轉換為派生類,錯誤      

五、類靜态類型/類動态類型

  • 在上面我們介紹過,基類的指針或引用可以指向于基類對象也可以指向于派生類對象,是以一個類可以分為是動态類型的還是靜态類型的:
  • 靜态類型的類變量:在編譯時就已經知道是什麼類型的了
  • 動态類型的類變量:自己所指的類型不明确,直到運作時才知道
  • 如果表達式既不是引用也不是指針,那麼其就沒有靜态類型和動态類型的概念,因為其隻能與自己類型一緻的對象綁定到一起

示範案例

  • 當我們使用基類的引用(或指針)時,我們并不清楚該引用(或指針)所綁定的對象的真實類型,該對象可能是基類的對象,也可能是派生類的對象。隻有在程式運作的時候我們才知道所綁定的對象的真實類型
class A {};
class B:public A{};
int main()
{
A a;  //靜态類型
B b;  //靜态類型
A *p; //動态類型
p = &a; //p指向了a
p = &b; //p又指向了b
return 0;
}
class A {
protected:
int a;
public:
void setA(int num) { a = num; }
void cout_getA() { cout << "A" << a << endl; }
};
class B :public A {
public:
void setA(int num) { a = num; }
void cout_getA() { cout << "B" << a << endl; }
};
int main()
{
A a;
B b;
a.setA(10);
b.setA(20);
A *p1, *p2;
p1 = &a;
p2 = &b;
p1->cout_getA();
p2->cout_getA();
return 0;
}      
  • 結果解析:
  • A 10:這個比較簡單,因為a的類型為A,且指針也為A,是以調用A的getA()函數
  • A 20:雖然p2指針指向的類類型為B,但是通路規則隻與指針/引用的類類型有關,而與指針/引用指向的類型無關。是以b已經被視為一個A對象來看了。此處p2指針的類型為A,是以調用A的getA()函數。又因為b對象使用setA()函數将整個繼承體系中的a改為了20,是以列印出來的a為20
C++:43---派生類向基類轉換、靜态/動态的類變量

六、轉換之後資料與方法的通路規則

  • 當我們使用一個指針或引用通路類資料與方法的時候,實際上取決于這個指針或引用的類類型,而不是指針所指向或引用的類型(這是在編譯階段決定的)
  • 當然,如果是普通類型下将派生類轉換為子類的話,那麼調用的時候也取決于左邊的類型
  • 轉換之後,基類隻能通過派生類通路屬于自己(基類)的那一部分,而不能通路屬于派生類的資料成員(見下面示範案例③)
  • 虛函數的調用是個例外:虛函數的調用是取決于指針或引用所指向的類型,并且多态隻能發生在指針/引用指向于派生類的情況下,普通類型之間的轉換不會發生。 

示範案例①

class A
{
public:
int a = 10;
void show1()const { cout << "A:show1\n"; }
virtual void show2()const { cout << "A:show2\n"; }
};
class B :public A
{
public:
int a = 15;
void show1()const { cout << "B:show1\n"; }
virtual void show2()const { cout << "B:show2\n"; }
};
int main()
{
A a;
B b;
A *pa = &b;
cout << pa->a << endl;
pa->show1();
pa->show2();
return 0;
}      
  • 結果分析:
  • 列印10:因為B繼承于A,将b轉換為A類對象的指針,通路是跟指針的類型有關,而與指針所指的類對象類型無關,是以通路A的a,列印10
  • 列印“A:show1”:因為show1()不是虛函數,是以通路時跟指針的類型有關,此處指針的類型為A,是以通路A的show1函數
  • 列印“B:show2”:因為show2()函數為虛函數,是以根據虛函數的性質,使用基類的指針通路子類時,通路虛函數跟指針所指的類對象類型有關,此處指針所指的類類型為B,是以通路B的show2()函數
C++:43---派生類向基類轉換、靜态/動态的類變量

示範案例②

  • 我們修改示範案例①,上面是将基類的指針指向于派生類。但是這個示範案例中是将派生類對象指派給基類對象(而不是指針形式)
class A
{
public:
int a = 10;
void show1()const { cout << "A:show1\n"; }
virtual void show2()const { cout << "A:show2\n"; }
};
class B :public A
{
public:
int a = 15;
void show1()const { cout << "B:show1\n"; }
virtual void show2()const { cout << "B:show2\n"; }
};
int main()
{
A pa;
B pb;
pa = pb;
cout << pa.a << endl;
pa.show1();
pa.show2();
return 0;
}      
  • 結果分析:
  • 列印10:因為B繼承于A,将B指派給A,相當于把B中屬于A的内容指派給A,是以通路到A中的a,為10
  • 列印“A:show1”:因為show1()不是虛函數,是以通路時跟左邊的類型有關,此時為A,就通路A中的show1()函數
  • 列印“A:show2”:雖然show2()函數為虛函數,但是多态隻有發生在基類指針/引用指向于派生類的情況下才會發生,此處基類是普通對象,而不是引用/指針,是以通路的還是A中的show2()函數

示範案例③

class A {};
class B :public A {
public:
int num = 10;
};
int main()
{
B b;


A a = b; //基類指向與派生類
a.num;   //錯誤,num屬于B,而A内不含有此成員
B *pa = new B;
A *pb = new B; //基類指向與派生類
pa->num;       //正确
pb->num;       //錯誤,num屬于B,而A内不含有此成員
return 0;
}      

七、其他情境下的類型轉換

  • 當我們用一個派生類對象為一個基類對象初始化或指派時,隻有該派生類對象中的基類部分會被拷貝、移動或指派,它的派生類部分會被忽略掉(截斷了)

拷貝時的類型轉換

class A {
public:
int a;
public:
A(int num) :a(num) {};
A(A const &other); //拷貝構造
};
A::A(A const &other)
{
this->a = other.a;
}
class B :public A {
public:
int b;
public:
B(int num) :A(num) {};
};
int main()
{
A a1(10); //定義A類對象
B b(20);  //定義B類對象
A a2(a1); //拷貝構造,使用與A類類型a1對象
A a3(b);  //拷貝構造,使用B類類型的b對象,b對象的内容被截斷
return 0;
}      

指派運算符時的類型轉換

#include <iostream>
using namespace::std;
class A {
public:
int a;
public:
A(int num) :a(num) {};
public:
A& operator=(A const& other); //指派運算符
};
A& A::operator=(A const& other)
{
this->a = other.a;
}
class B:public A{
public:
int b;
public:
B(int num) :A(num) {};
};
int main()
{
A a1(10); //定義A類對象
B b(20);  //定義B類對象
A a2(30);
a1 = b;   //将b對象指派給a1,b對象的内容被截斷
a1 = a2;  //将a2對象指派給a1
return 0;
}      

繼續閱讀