在 c/c++ 中經常會發生資料型別的轉換,例如將 int 型別的資料賦值給 float 型別的變數時,編譯器會先把 int 型別的資料轉換為 float 型別再賦值;反過來,float 型別的資料在經過型別轉換後也可以賦值給 int 型別的變數;
資料型別轉換的前提是,編譯器知道如何對資料進行取捨;大的變小,直接丟棄就可以,小的變大的多出來的記憶體不知道如何填充
類其實也是一種資料型別,也可以發生資料型別轉換,不過這種轉換只有在基類和派生類之間才有意義,並且只能將派生類賦值給基類,包括將派生類物件賦值給基類物件、將派生類指標賦值給基類指標、將派生類引用賦值給基類引用,這在 c++ 中稱為向上轉型(upcasting)。相應地,將基類賦值給派生類稱為向下轉型(downcasting);
向上轉型非常安全,可以由編譯器自動完成;
向下轉型有風險,需要程式設計師手動干預。本節只介紹向上轉型,向下轉型將在後續章節介紹;
將派生類物件賦值給基類物件
#include
using
namespace std;
classa;
a::a
(int a)
:m_a
(a)void a::
print()
const
classb:
public a
;b::b(
int a,
int b):a
(a),
m_b(b)
void b::
print()
const
intmain()
執行結果:
class
a: m_a =
1class
b: m_a =
11, m_b =
22--
----
----
----
--class
a: m_a =
11class
b: m_a =
11, m_b =
22
賦值的本質是將現有的資料寫入已分配好的記憶體中,物件的記憶體只包含了成員變數,所以物件之間的賦值是成員變數的賦值,成員函式不存在賦值問題。執行結果也有力地證明了這一點,雖然有a = b;這樣的賦值過程,但是a.print() 始終呼叫的都是 a 類的 print() 函式。換句話說,物件之間的賦值不會影響成員函式,也不會影響 this 指標;
將派生類物件賦值給基類物件時,會捨棄派生類新增的成員,也就是「大材小用」;
這種轉換關係是不可逆的,只能用派生類物件給基類物件賦值,而不能用基類物件給派生類物件賦值。理由很簡單,基類不包含派生類的成員變數,無法對派生類的成員變數賦值。同理,同一基類的不同派生類物件之間也不能賦值。
要理解這個問題,還得從賦值的本質入手。
賦值實際上是向記憶體填充資料,當資料較多時很好處理,捨棄即可;本例中將 b 賦值給 a 時(執行a = b;語句),成員 m_b 是多餘的,會被直接丟掉,所以不會發生賦值錯誤。
但當資料較少時,問題就很棘手,編譯器不知道如何填充剩下的記憶體;如果本例中有b = a;這樣的語句,編譯器就不知道該如何給變數 m_b 賦值,所以會發生錯誤;
除了可以將派生類物件賦值給基類物件(物件變數之間的賦值),還可以將派生類指標賦值給基類指標(物件指標之間的賦值);
我們先來看乙個多繼承的例子:基類 a 擁有成員 m_a;中間派生類 b 繼承於 a,並新增了新成員 m_b;基類 c 擁有成員 m_c;最終派生類 d 繼承 b 和 c;
#include
using
namespace std;
classa;
a::a
(int a)
:m_a
(a)void a::
print()
const
classb:
public a
;b::b(
int a,
int b):a
(a),
m_b(b)
void b::
print()
const
classc;
c::c
(int c)
:m_c
(c)void c::
print()
const
classd:
public b,
public c
;d::d(
int a,
int b,
int c,
int d):b
(a, b),c
(c),
m_d(d)
void d::
print()
const
intmain()
執行結果:
class
a: m_a =
1class
b: m_a =
11, m_b =
22class
c: m_c =
3class
d: m_a =
110, m_b =
114, m_c =
119, m_d =
120--
----
----
----
----
----
----
class
a: m_a =
110class
b: m_a =
110, m_b =
114class
c: m_c =
119--
----
----
----
----
----
----
-pa =
0xb61aee4090
pb =
0xb61aee4090
pc =
0xb61aee4098
pd =
0xb61aee4090
本例中定義了多個物件指標,並嘗試將派生類指標賦值給基類指標。與物件變數之間的賦值不同的是,物件指標之間的賦值並沒有拷貝物件的成員,也沒有修改物件本身的資料,僅僅是改變了指標的指向;
通過基類指標訪問派生類的成員
將派生類指標 pd 賦值給了基類指標 pa,從執行結果可以看出,呼叫 print() 函式時雖然使用了派生類的成員變數,但是 print() 函式本身卻是基類的;也就是說,將派生類指標賦值給基類指標時,通過基類指標只能使用派生類的成員變數,但不能使用派生類的成員函式
,pb、pc 也是一樣的情況;
這是因為,呼叫哪個類的函式不是由指標指向的資料為判斷標準,而是決定於指標的型別
,如果指標的型別是a *,那麼就呼叫類 a 的函式,如果是d *,就呼叫類 d 的函式;
概括起來說就是:編譯器通過指標來訪問成員變數,指標指向哪個物件就使用哪個物件的資料;編譯器通過指標的型別來訪問成員函式,指標屬於哪個類的型別就使用哪個類的函式。
賦值後值不一致的情況
本例中我們將最終派生類的指標 pd 分別賦值給了基類指標 pa、pb、pc,按理說它們的值應該相等,都指向同一塊記憶體,但是執行結果卻有力地反駁了這種推論,只有 pa、pb、pd 三個指標的值相等,pc 的值比它們都大。也就是說,執行pc = pd;語句後,pc 和 pd 的值並不相等,這是因為在賦值前,編譯器會進行某些處理,就如同將 float 型別的 3.14 賦值給 int 型別的變數時,編譯器會直接捨棄小數點後的值,最終變為 3;物件指標之間的賦值也是這個道理;因為a->b,b->d,c->d,b->d,所以a,b,d理所應當同一塊記憶體,c與a肯定存在偏移,我們通過繼承類的物件記憶體模型可以推導。
引用在本質上是通過指標的方式實現的,既然基類的指標可以指向派生類的物件,那麼我們就有理由推斷:基類的引用也可以指向派生類的物件,並且它的表現和指標是類似的;
最後需要注意的是,向上轉型後通過基類的物件、指標、引用只能訪問從基類繼承過去的成員(包括成員變數和成員函式),不能訪問派生類新增的成員;
通過上節最後乙個例子我們發現,將派生類的指標賦值給基類的指標後,它們的值有可能相等,也有可能不相等;
我們通常認為,賦值就是將乙個變數的值交給另外乙個變數,這種想法雖然沒錯,但是有一點要注意,就是賦值以前編譯器可能會對現有的值進行處理;
例如將 double 型別的值賦給 int 型別的變數,編譯器會直接抹掉小數部分,導致賦值運算子兩邊變數的值不相等;
將派生類的指標賦值給基類的指標時也是類似的道理,編譯器也可能會在賦值前進行處理;
要明確的一點是,物件的指標必須要指向物件的起始位置;這也是第二個**例子裡c的位址與其他不一樣的原因。
對於 a 類和 b 類來說,它們的子物件的起始位址和 d 類物件一樣,所以將 pd 賦值給 pa、pb 時不需要做任何調整,直接傳遞現有的值即可;
而 c 類子物件距離 d 類物件的開頭有一定的偏移,將 pd 賦值給 pa 時要加上這個偏移,這樣 pc 才能指向 c 類子物件的起始位置;
也就是說,執行pc = pd;語句時編譯器對 pd 的值進行了調整,才導致 pc、pd 的值不同;
派生類賦值操作符
賦值操作符通常與複製建構函式類似 如果派生類定義了自己的賦值操作符,則該操作符必須對基類部分進行顯式賦值。base operator const base not invoked automatically derived derived operator const derived rhs 賦值操...
C 派生類與基類的賦值
class a class b a void main 可以把派生類賦值給基類。我們知道賦值,是呼叫了類的賦值運算子。所以當派生類給基類賦值時,呼叫了基類的複製運算子函式,該函式的引數是基類物件的const 引用,那麼 a b,實際就是用基類引用派生類,然後將派生類中基類部分賦值給對應的基類成員。而...
c 派生類物件賦值給基類物件
基類物件和派生類物件之間的賦值關係具體是指 基類的物件可不可以賦值給子類物件或者子類物件可不可以賦值給基類物件。一般來說,只有派生類的物件可以賦值給基類的物件,反之,則不可以。例如 cpp view plain copy father a 基類物件 son b 派生類物件 a b 可以 b a 不可...