C++ 單繼承 多重繼承的記憶體佈局
單繼承
#include <iostream> #define P(x) std::cout<<x<<std::endl; class A { public: A(){} ~A(){} virtual void func(){ P("A func call"); } virtual void funcA(){ P("A funcA call"); } int a = 10; }; class B : public A { public: B(){} ~B(){} virtual void func(){ P("B func call"); } virtual void funcB(){ P("B funcB call"); } int b = 20; }; void main() { A* t1 = new A(); A* t2 = new B(); t1->func(); t2->func(); }
根據多型的性質我們知道此時輸出是:
A func call
B func call
先看看A物件的記憶體佈局(本例中的測試程式都是32位的)
根據這個記憶體佈局和指標t1來呼叫到func和funcA以及如何訪問成員變數a(這裡強調的是不是通過指標的->操作符來呼叫,而是通過物件的基地址來獲取到對應函式和成員變數的地址來訪問)
先看一下獲取成員變數a。a的地址是物件的基地址下移了一個虛擬函式表地址的大小,也就是4個位元組。改一下main函式,看下面的程式碼:
void main() { A* t1 = new A(); A* t2 = new B(); t1->func(); t2->func(); int A_a = *((int*)t1 + 1); P(A_a); }
上面的A_a就是t1的成員變數a的值,大家看一下輸出就知道了。將指標t1強轉成int*型別,加一後相當於偏移了4個位元組,也就是偏移一個虛表地址的大小,這時候指向的是成員變數a的地址,取地址內容就是成員變數a的值。
接下來看一下通過t1指標呼叫func和funcA兩個虛擬函式。我們先上程式碼
void main() { A* t1 = new A(); A* t2 = new B(); typedef void(*FunPtr)(); auto A_func = FunPtr(*((int*)(*(int*)t1))); A_func(); auto A_funcA = FunPtr(*((int*)(*(int*)t1) + 1)); A_funcA(); }
A_func和A_funcA分別呼叫的是A::func和A::funcA。這裡具體說一下怎麼獲取到的:
auto A_func = FunPtr(*((int*)(*(int*)t1)));
(*(int*)t1) //獲取虛擬函式表的地址值。
((int*)(*(int*)t1)) //取虛擬函式表中第一個元素的地址
*((int*)(*(int*)t1)) //獲取虛擬函式表中第一個元素的值,也就是A::func的函式地址,並強轉成函式地址並
呼叫
auto A_funcA = FunPtr(*((int*)(*(int*)t1) + 1));
//跟上面相比只是這裡是獲取虛擬函式表第二個元素的地址,並去第二個元素的值也就是A::funcA的函式地址,並
最終呼叫到A::funcA。結合著記憶體分佈應該很容易理解
下面繼續講B記憶體分佈。我們先看B物件的記憶體佈局
B的物件的虛擬函式表中,其中func函式是被過載了的,所以地址被替換成的B::func的地址,其他的兩個虛擬函式位置不變。根據前面的將的規則很容易通過t2指標訪問物件的虛擬函式和成員變數,下面給出程式碼,分析如上所示:
void main()
{
A* t1 = new A();
A* t2 = new B();
typedef void(*FunPtr)();
auto B_func = FunPtr(*((int*)(*(int*)t2)));
B_func();
auto B_funcA = FunPtr(*((int*)(*(int*)t2) + 1));
B_funcA();
auto B_funcB = FunPtr(*((int*)(*(int*)t2) + 2));
B_funcB();
auto B_a = *(((int*)t2) + 1);
P(B_a);
auto B_b = *(((int*)t2) + 2); //嚴格意義上說這裡的加2是特殊處理的。改成auto B_b = *((int*)((char*)t2 + sizeof(A)))會更嚴謹
P(B_b);
}
//輸出
B func call
A funcA call
B funcB call
10
20
這裡額外說一個知識點。在類的建構函式中呼叫虛擬函式是不會有多型的效果。具體的分析請看這篇部落格
多重繼承
其實單繼承是多重繼承的一個特例。我們看下面多重繼承的程式碼:
#include <iostream>
#define P(x) std::cout<<x<<std::endl;
class A
{
public:
A(){}
~A(){}
virtual void func(){ P("A func call"); }
virtual void funcA(){ P("A funcA call"); }
int a = 10;
};
class B
{
public:
B(){}
~B(){}
virtual void func(){ P("B func call"); }
virtual void funcB(){ P("B funcB call"); }
int b = 20;
};
class C : public A, public B
{
public:
C(){}
~C(){}
virtual void func(){ P("C func call"); }
virtual void funcC(){ P("C funcC call"); }
int c = 30;
};
void main()
{
C* t1 = new C();
A* t2 = t1;
B* t3 = t1;
}
下面我們直接來說一下C的記憶體佈局:
看看記憶體佈局就知道怎麼獲取物件的虛擬函式和成員變數,下面給出具體的程式碼:
void main()
{
C* t1 = new C();
typedef void(*FunPtr)();
auto C_func = FunPtr(*((int*)(*(int*)t1)));
C_func();
auto C_funcA = FunPtr(*((int*)(*(int*)t1) + 1));
C_funcA();
auto C_funcC = FunPtr(*((int*)(*(int*)t1) + 2));
C_funcC();
int* b_addr = (int*)((char*)t1 + sizeof(A));
auto BC_func = FunPtr(*((int*)(*b_addr)));
BC_func();
auto BC_funcB = FunPtr(*((int*)(*b_addr) + 1));
BC_funcB();
int C_a = *((int*)t1 + 1);
P(C_a);
int C_b = *((int*)((char*)t1 + sizeof(A) + 4));
P(C_b);
int C_c = *((int*)((char*)t1 + sizeof(A) + sizeof(B)));
P(C_c);
}
//輸出
C func call
A funcA call
C funcC call
C func call
B funcB call
10
20
30
多重繼承第一個父類的虛擬函式表的地址就成了整個物件的虛擬函式表的地址(同樣適用於單繼承),後續的每個父類都有自己的虛擬函式表地址,子類過載的虛擬函式會沖掉父類中對應的虛擬函式地址。
這裡需要注意的第一個虛擬函式表中的包含哪些虛擬函式的地址。它包含A類中的所有虛擬函式地址(當然被C過載的虛擬函式地址要替換掉C類對應的虛擬函式地址)加上C類中未過載任何父類的虛擬函式地址之和。針對這個例子就是包含A中的A::func和A::funcA兩個虛擬函式和C中C::funcC這個未過載任何父類的虛擬函式。而A::func是被C過載的,所以需要替換成C::func。這樣就形成了第一個虛擬函式表。
如果我們把C::funcC換成C::funcB。如下程式碼:
#include <iostream>
#define P(x) std::cout<<x<<std::endl;
class A
{
public:
A(){}
~A(){}
virtual void func(){ P("A func call"); }
virtual void funcA(){ P("A funcA call"); }
int a = 10;
};
class B
{
public:
B(){}
~B(){}
virtual void func(){ P("B func call"); }
virtual void funcB(){ P("B funcB call"); }
int b = 20;
};
class C : public A, public B
{
public:
C(){}
~C(){}
virtual void func(){ P("C func call"); }
virtual void funcB(){ P("C funcB call"); }
int c = 30;
};
這時候的第一個虛擬函式表中就不再包含C::funcB,因為它過載了父類B中funcB,會出現B類對應的虛擬函式表中。而不會出現在第一個虛擬函式中。
整個物件模型如下所示