簡體   English   中英

在Visual Studio 2008監視窗口中調試C ++虛擬多重繼承

[英]Debugging C++ virtual multiple inheritance in Visual Studio 2008 watch window

我在使用指向具有虛擬多重繼承的對象的指針調試Visual Studio C ++ 2008中的項目時遇到問題。 如果指針是基類型,我無法檢查派生類中的字段。

我做的一個簡單的測試用例:

class A
{
    public:
        A() { a = 3; };
        virtual ~A() {}
        int a;
};

class B : virtual public A
{
    public:
        B() { b = 6; }
        int b;
};

class C : virtual public A
{
    public:
        C() { c = 9; }
        int c;      
};

class D : virtual public B, virtual public C
{
    public:
        D() { d = 12; }
        int d;
};

int main(int argc, char **argv)
{
    D *pD = new D();
    B *pB = dynamic_cast<B*>(pD);

    return(0);
}

在“return(0)”上放置一個斷點,並將pD和pB放在監視窗口中。 我無法想辦法在觀察窗口的pB中看到“d”。 調試器不接受C樣式轉換或dynamic_cast。 擴展到v表顯示調試器知道它實際指向D析構函數,但無法查看“d”。

從基類定義中刪除“虛擬”(因此D有2個A),調試器將讓我擴展pB並看到它真的是一個可以擴展的D *對象。 這也是我想在虛擬案例中看到的。

有沒有辦法讓這項工作? 我是否需要弄清楚對象布局的實際偏移量才能找到它? 或者是時候說我對虛擬多重繼承和重新設計不夠聰明,因為實際項目要復雜得多,如果我不能調試,我應該讓它更簡單:)

仔細查看pB和pD的實際指針值。 使指針調整正確很難,需要編譯器。

在我看來,需要多次和虛擬繼承的時間非常少,甚至可能有更好的方法來建模域。 繼承本身會在基類和派生類之間產生緊密耦合,因此在菱形樹中添加會創建一堆緊密耦合的類,最終會形成脊狀設計。

除此之外。 我在vs2003和vs2005中編譯了你的代碼,它們都在監視窗口中顯示了以下內容。

pD               
 + B   { b=6 }
 + C   { c=9 }
   d   12

好吧,我終於可以使用指針算術了,所以我會回答我自己的問題。 宣布全球:

D d;

現在我可以將它放在調試器中,我可以看到包含pB指向的B的D對象的內容:

(D*)((char *) pB + (((char *)&d.d) - ((char *)&d.b)))

所以基本上,我只需要定義一個僅調試D實例,我可以使用它來查找指針偏移量。

奇怪的是,調試器似乎正在使用運行時類型識別來確定&d.d和&d.b的地址偏移量。 如果我嘗試一個沒有指向D實例的內存地址,調試器會給出錯誤的答案! 這個:

&((D *)(void *) pB)->b
&((D *)(void *) pB)->d

實際上顯示兩個值的相同地址! 完全奇怪!

解決方案並不漂亮,但它的工作原理。 我可以創建僅調試全局變量來使用。 似乎調試器應該能夠自動獲取此信息,但事實並非如此。 那好吧!

鏈接還指示調試符號引擎存在使用虛擬基類進行多重繼承的問題。

但是如果你只是想要幫助調試,為什么不在類A上添加一個輔助函數來獲得一個D指針(如果可用)。 你可以看pB-> GetMyD()。

class D;

class A 
{
    ...
    D* GetMyD();
    ...
}

class D...

D* A::GetMyD()
{
   return dynamic_cast<D*>(this);
}

這將把指針算術留給編譯器。

事實上,就我所知,沒有安全的恢復方法。
如果你查看pB和pD的內存地址,你會發現它們不一樣。

D *pD = new D(); // points at 0x00999720

B *pB = dynamic_cast<B*>(pD); // points at 0x00999730, 
// hence inside the memory segment of pD

由於您不再擁有原始起始地址,因此無法恢復。 即使是reinterpret_cast也會默默地失敗。 它會給你一個D *但是有錯誤的值,因為它將從0x00999730而不是0x00999720開始。 (reinterpret_cast在監視窗口中不起作用)

這將導致相同的事情:

(D*)(void*)pB

在監視窗口中工作,但會顯示錯誤的值,因為指出的內存實際上從0x00999730而不是原始0x00999720開始。

在您的示例中,reinterpret_cast將導致:

D* pD2 = reinterpret_cast<D*>(pB); // or "(D*)(void*)pD" in the watch window
pD2
 + B {b=6}
   + A {a=3}
 + C {c=6}
   + A {a=3}    
 d=6

顯然是錯的,應該是:

 + B {b=6}
   + A {a=3}
 + C {c=9}
   + A {a=3}    
 d=12

因此,原始的dynamic_cast會混淆事物。

編輯(附加注意事項):
令人困惑的是你認為pB實際上仍然是D,而事實並非如此。 由於虛擬繼承,pB實際上僅在從D *轉換時指向B。
這是由於內部如何表示類。
正常繼承可以被認為是導致像這樣的內存結構:

struct A
{
    int a;
}
struct B
{
    A base
    int b;
}

而虛擬繼承導致類似這樣的事情:

struct A
{
    int a;
}
struct B
{
    A* base
    int b;
}

這是因為虛擬繼承旨在防止重復,它使用指針進行復制。 如果你有:

class A
class B: virtual public A
class C: virtual public A
class D: virtual public B, virtual public C

D可以被認為是這樣的:

struct D
{
    B* base1;
    C* base2;
    int d;
}

其中B和C的A *基指向A的相同實例。因此,當您將D轉換為B時,pB將指向D的base2,而不是具有與正常單繼承相同的內存起始點。

與非虛擬多重繼承相同的東西。

class A
class B
class C: public A, public B

將導致一個可以被認為是的內存結構:

struct C
{
    A base1;
    B base2;
    int c;
}

所以,如果你這樣做:

{
    C *pC = new C();
    B *pB = dynamic_cast<B*>(pC);
    C *pC2 = reinterpret_cast<C*>(pB);
}

它會失敗,因為pB實際指向base2,它與pC不在同一個內存地址,與base1相同

免責聲明!
上述表示可能不完全正確。 這是一個簡化的心理模型,它在大多數時間里都適用於我。 可能存在此模型不正確的情況。

結論:多重繼承和任何類型的虛擬繼承都會以安全的方式阻止reinterpret_cast回到子類型。
MS VC ++(Visual Studio中使用的C ++編譯器)實現非虛擬多重繼承的方式可以從超類列表中的第一個基類型轉回到子類。 不知道這是否符合C ++規范或其他編譯器的規定。

我剛剛將其添加為C ++的“最奇怪的語言功能”。 你建議編譯器壞了,這是可信的。 為什么煩惱? 不要使用虛擬MI。

添加“AProxy”(由傳遞給A ref構造)並且具有像D這樣的“具體”類包含單個A成員,將其傳遞給基礎B和C.

AProxy為A提供了一個接口,而不是真正的A - 它委托給構造中的A綁定。 這很難看,但鑽石MI也是如此。

 struct AProxy { const A& a_; AProxy(const A& a) : a_(a) { } } struct B : public AProxy ... B(const A& a) : AProxy(a) { } struct C : public AProxy ... struct D : public B, public C { A a_; D() : a_(), B(a_) C(a_) { } } 

你可以在Visual中做另一個丑陋的事情,這可能會幫助你了解幕后發生的事情。 打開其中一個內存窗口,輸入變量名稱作為地址,然后打開“自動重新評估”選項。 還要將列寬設置為4個字節,以便成員很好地對齊。

對其他變量執行相同操作,並與監視窗口一起查看對象的內容,並顯示子類型如何堆疊在一起並組成派生類型。

您應該最終得到一些指向各種vf表和整數成員的指針。 vf表指針很有趣,因為它們告訴你對象的實際類型。 但是,您需要在每個派生類中重新聲明至少一個虛方法,以便每個類獲得一個新的vf表。 重新聲明析構函數應該可以解決問題。

希望這能說明那里發生的事情。 干杯。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM