程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++對象布局及多態實現之動態和強制轉換

C++對象布局及多態實現之動態和強制轉換

編輯:關於C++

為了驗證前面提到過的類型動態轉換(即dynamic_cast轉換),以及對象類型的強制轉換。我們利用前面定義的C041、C042及C082類來進行驗證。

運行下列代碼:

c082.C041::c_ = 0x05;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_DETAIL(C041, ((C041)c082))
PRINT_VTABLE_ITEM(((C041)c082), 0, 0)
PRINT_VTABLE_ITEM(c082, 5, 0)
C042 * pt = dynamic_cast<C042*>(&c082);
PRINT_VTABLE_ITEM(*pt, 0, 0)

第2行和第5行是為了對照方便而打印原對象中的信息。第3、4行把C082對象類型進行強制轉換並分別打印轉換後的對象內存信息及虛表信息。第6行我們用dynamic_cast進行了一次動態類型轉換,從子類的指針轉型為右父類的指針,再把指針指向的對象的信息打印出來。

結果為:

c041 : objadr:0012FA74 vpadr:0012FA74 vtadr:0045B364 vtival(0):0041DF1E
The detail of C041 is 64 b3 45 00 05
((C041)c082) : objadr:0012F2A3 vpadr:0012F2A3 vtadr:0045B364 vtival(0):0041DF1E
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483
*pt : objadr:0012FA55 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483

首先我們比較最後兩行,從objadr列我們可以知道pt指向的並不是c082對象的起始地址,而是指向了c082的第2個虛表指針的所在地址(因為最後一行的objadr值等於倒數第2行的vpadr的值)。倒數第二行的vpadr值是c082對象的第2個虛表指針(我們在輸出時指定了偏移值5)。而這個地址正是c082對象中屬於從C042類繼承而來的部分,即在進行動態類型轉換時,除了改變類型信息,編譯器還調整了指針的位置,以確保轉換語義的正確性。所以我們可以知道,對指向有復雜繼承結構的類對象的指針進行類型轉換(一般在繼承樹中向上或向下轉換)時,必須使用dynamic_cast,它會正確的處理指針位置的調整,如果轉換是非法的,它會返回一個NULL指針。使用dynamic_cast時記得要做這個檢查,文中為了簡略把這些檢查省去了。這種檢查可以通過宏來定義,以便於在release版中去掉,提高效率。

再將((C041)c082)和c082兩行的輸出進行對照,可以發現對對象進行向上的類型強制轉換實際上編譯器生成了一個新的臨時對象,因為它們的objadr列不一樣了,這表明它們已經不是同一個對象。再觀察 c041、((C041)c082)及c082三行的vtadr和vtival(0),前兩行相比是一樣的,而後兩行相比就不一樣了。這也說明編譯器在處理強制轉換時,實際上是new了一個新的C041對象出來。因為對象的強制類型轉換不象指針的動態類型轉換,指針的動態類型轉換同時要確保多態的語義,所以只需要調整指針位置。而對象強制類型轉換,還要調整虛表中的條目值,因為對象類型轉換不需要多態的行為。c082類的第一個虛表的第一個條目中存放的是 C082::foo()函數的地址,做了對象類型轉換後,應該調整為C041::foo()才對,做這種調整過於復雜,所以編譯器干脆new了一個新的 C041的臨時對象出來。對比這三行的最後二列即知。我不知道這是否是C++標准規范中定義的行為,改天查到我再更新上來。

在new出新對象的同時,編譯器還將原對象中屬於父類部分的數據成員的值拷貝了過來。注意代碼的第1行,c082.C041::c_ = 0x05;,我們先把c082對象中從C041類繼承過來的數據成員的值改寫為0x05,原來是的值是0x01,由C041的構造函數初始化。我們觀察輸出的第2行,上面說了這個被打印的對象並非c082而是編譯器new出的來的臨時對象,可以注意到對象的最後一字節為0x05,即數據成員的值。所以我們知道編譯器除了new出新的臨時對象外,還把原對象中相應的數據成員的值也復制了過來。

這和我以前的認識有點偏差,直觀上我一直以為這種轉換不會產生新的對象,不過仔細想想編譯器的這種作法也是對的,如果不產生新的對象,就意味著它要象前述的那樣動態的改變虛表中條目的值。但new出臨時對象,也意味著使用下列的語句調用,可能產生意想不到的結果。

((C041)c082).somefun();

如果somefun函數會改變對象的狀態,那麼上邊的代碼執行後,c082的狀態並不會被改變。因為somefun實際改變的是臨時對象,在執行完後該臨時對象就扔掉了。這和直觀的認識有所差異,一般會認為這個調用會作用於c082對象上。為了驗證我們聲明以下兩個類。

struct C010
{
C010() : c_(0x01) {}
void foo() { c_ = 0x02; }
char c_;
};
struct C013 : public C010
{
C013() : c1_(0x01) {}
void foo() { c1_ = 0x02; }
char c1_;
};

兩個類為繼承關系,各有一個同名的普通成員函數,該函數改寫類的相應成員變量。我們做以下的調用:

C013 obj;
obj.foo();
((C010)obj).foo();

第1個foo調用,改變的是c1_值,最後一行的調用改變的是c_的值。直觀上容易認為上述代碼執行後obj.c_和obj.c1_的值均為0x02。但我們在調試環境的局部變量窗口中把obj對象展開可以發現obj.c1_為0x02,但obj.c_為0x01。原因就是前面所說的((C010) obj)實際產生了一個臨時對象,所以最後一行的調用沒有作用到obj對象上。

更進一步的想想,如果我們在一個類上運用了單件 (singleton)模式,而這個類又有一個繼承結構,當在程序中想利用對對象進行向上轉型來調用父類的方法時,應該會出現編譯時錯誤,因為父類臨時對象無法構造。在這裡有個前提,父類的構造函數應該用protected進行保護,而不是用private,否則子類根本無法構造。這種我沒有驗證了,因為用這種方法調用實在是比較蠢的作法,但不排除這種可能性。向上例中最後一行正確的調用方法應該是:

obj.C010::foo();

這樣就可以調用到父類中被覆蓋的函數,而且也是作用在正確的對象上。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved