簡體   English   中英

C ++如果我將派生對象向上轉換為其基類型,虛函數是否仍然有用

[英]C++ Will virtual functions still work if I upcast a derived object to its base type

考慮以下:

  • B繼承自A並覆蓋打印功能。
  • A有一個靜態函數,它取一個void *,將它轉換為A並調用虛擬打印函數。
  • 如果void *最初是B,它會調用A :: print還是B :: print?

     #include <iostream> class A { public: static void w(void *p) { A *a = reinterpret_cast<A*>(p); a->print(); } virtual void print() { std::cout << "A" << std::endl; } }; class B : public A { public: void print() { std::cout << "B" << std::endl; } }; int main () { B b; A::w(&b); } 

這為我打印B。

似乎已經轉換為A的void *仍然知道B被覆蓋的打印功能。 原因不是立即清楚。

有人可以向我解釋這是否是我可以依賴的行為,或者它是否只是一些有效的因素,因為它是一個小例子(比如返回對局部變量的引用並不總是在小程序中段錯誤)。

您的代碼具有未定義的行為

§5.2.10重新解釋演員

7將“指向T1的指針”類型的prvalue轉換為“指向T2的指針”類型(其中T1和T2是對象類型,T2的對齊要求不比T1更嚴格)並返回其原始類型會產生原始指針值。 未指定任何其他此類指針轉換的結果。

虛函數通常由隱式vtable解析。 這基本上是類層次結構中每個虛函數的函數指針數組。 編譯器將其添加為您的類的“隱藏成員”。 調用虛函數時,調用vtable中的相應條目。

現在,當您創建類型B的類時,它隱式地將B-vtable存儲在對象中。 強制轉換不會影響此表。

因此,當你將void *A ,會出現原始的vtable( B類),它指向B::print

請注意,這是實現定義的行為,標准不保證這一點。 但大多數編譯器都會這樣做

首先,你的reinterpret_cast是未定義的。 如果你將A*傳遞給w ,它將被定義。

A * p = new B;
A::w(p);
delete p;

我建議使用static_cast<A*>(p) ,如果w總是使用A*調用。

如果你有一個定義的強制轉換為void*並且返回內存地址保持不變。 所以, a你的內部w將是一個有效的A*如果你通過一個有效的A*w第一。

程序知道如何處理調用的問題與稱為“虛擬表”的機制有關。

注意:對於不同的編譯器,這可能有所不同。 我將談談Visual Studio如何處理它。

為簡單的繼承提供一些粗略的想法:

編譯器將在您的代碼中編譯2個print函數: A::print即地址X )和B::print即地址Y )。

包含虛函數的類的實際內存占用量(即

struct A
{
  void print (void);
  size_t x;
};
struct B : A
{
  void print (void);
  size_t y;
};

)會有點像

struct Real_A
{
  void * vtable;
  size_t x;
};
struct Real_B : Real_A
{
  size_t y;
};

此外,將有兩個所謂的虛擬表,每個類包含一個虛函數或一個具有虛函數的基類。

您可以將vtable視為包含每個函數的“真實”地址的結構。

在編譯時,編譯器將為每個類( AB )創建Vtables:A的每個實例將具有vtable = <Address of A Vtable>B每個實例將具有vtable = <Address of B Vtable>

在運行時,如果調用虛函數,程序將從Vtable中查找函數的“實際”地址,該地址存儲在作為AB每個對象的第一個元素的地址處。

以下代碼是非標准的,並不是理智的 ...但它可能會給你一個想法...

#include <iostream>
struct A 
{
  virtual void print (void) { std::cout << "A called." << std::endl; }
  size_t x;
};

struct B : A 
{
  void print (void) { std::cout << "B called." << std::endl; }
};
// "Real" memory layout of A
struct Real_A
{
  void * vtable;
  size_t x_value;
};
// "Real" memory layout of B
struct Real_B : Real_A
{
  size_t y_value;
};
// "Pseudo virtual table structure for classes with 1 virtual function"
struct VT
{
  void * func_addr;
};

int main (void) 
{
  A * pa = new A;
  pa->x = 15;
  B * pb = new B;
  pb->x = 20;
  A * pa_b = new B;
  pa_b->x = 25;
  // reinterpret addrress of A and B objects as Real_A and Real_B
  Real_A& ra(*(Real_A*)pa);
  Real_B& rb(*(Real_B*)pb);
  // reinterpret addrress of B object through pointer to A as Real_B
  Real_B& rb_a(*(Real_B*)pa_b);
  // Print x_values to know whether we meet the class layout
  std::cout << "Value of ra.x_value = " << ra.x_value << std::endl;
  std::cout << "Value of rb.x_value = " << rb.x_value << std::endl;
  std::cout << "Value of rb.x_value = " << rb_a.x_value << std::endl;
  // Print vtable addresses
  std::cout << "VT of A through A*: " << ra.vtable << std::endl;
  std::cout << "VT of B through B*: " << rb.vtable << std::endl;
  std::cout << "VT of B through A*: " << rb_a.vtable << std::endl;
  // Reinterpret memory pointed to by the vtable address as VT objects
  VT& va(*(VT*)ra.vtable);
  VT& vb(*(VT*)rb.vtable);
  VT& vb_a(*(VT*)rb_a.vtable);
  // Print addresses of functions in the vtable
  std::cout << "FA of A through A*: " << va.func_addr << std::endl;
  std::cout << "FA of B through B*: " << vb.func_addr << std::endl;
  std::cout << "FA of B through A*: " << vb_a.func_addr << std::endl;

  delete pa;
  delete pb;
  delete pa_b;

  return 0;
}

Visual Studio 2013輸出:

Value of ra.x_value = 15
Value of rb.x_value = 20
Value of rb.x_value = 25
VT of A through A*: 00D9DC80
VT of B through B*: 00D9DCA0
VT of B through A*: 00D9DCA0
FA of A through A*: 00D914B0
FA of B through B*: 00D914AB
FA of B through A*: 00D914AB

gcc-4.8.1輸出:

Value of ra.x_value = 15
Value of rb.x_value = 20
Value of rb.x_value = 25
VT of A through A*: 0x8048f38
VT of B through B*: 0x8048f48
VT of B through A*: 0x8048f48
FA of A through A*: 0x8048d40
FA of B through B*: 0x8048cc0
FA of B through A*: 0x8048cc0

https://ideone.com/iKyBB3

注意:無論您是通過A*還是B*訪問B對象,您都會先找到相同的vtable地址,並且您也會在vtable中找到相同的地址。

如果考慮在C ++中如何實現多重繼承,則reinterpret_cast無法工作。

基本上,當在相關類型之間進行轉換時,具有多重繼承,轉換可能涉及添加偏移量。 因此,在不知道源和目標類型的情況下,編譯器無法發出正確的指令。

因此,對於至少此用例,重新解釋強制轉換是可撤消的,因此它們被定義為未定義。

這里的危險部分,即使你不進行多重繼承,現代編譯器也開始將這種“未定義”行為解釋為意味着它們可以優化事物,它包含塊等等。 這幾乎肯定是有效的C ++標准(未定義意味着什么,一切都很好),但對開發人員來說可能是一個驚喜,開發人員傾向於將“未定義”理解為“代碼輸出可能無法正常工作”。

暫無
暫無

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

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