虛函數在C++主要用於通過父類指針調用子類對象方法,從而達到實現多態機制。虛函數聲明在類中,用virtual關鍵字修飾,虛函數在類的定義時就被放在了內存代碼段,虛函數不在對象內存佈局中。
1.虛函數表與虛函數指針
虛函數表可以看作一段內存裏面面放着類裏面的所有虛函數指針,當需要調用虛函數就從這裏面找。在生成對象時,編譯器會產生一個虛函數指針來指向虛函數表,虛函數指針在32位系統佔4字節,64位系統佔8字節。下面代碼實例演示:
#include "stdafx.h"
#include <stdio.h>
#include <iostream>
using namespace std;
class A {
public:
virtual void fun1(){
cout << "this is A virtual fun1" << endl;
}
virtual void fun2() {
cout << "this is A virtual fun2" << endl;
}
virtual void fun3() {
cout << "this is A virtual fun3" << endl;
}
};
int main()
{
A a1;
printf("######### 對象a1 佔用內存大小 %d\n", sizeof(a1));
printf("######### 通過對象調用虛函數\n");
a1.fun1();
a1.fun2();
a1.fun3();
typedef void(*myfun)();
//拿到虛函數表首地址 vptr就是一個有三個元素的函數指針數組
long *vptr =(long *)*((long *)(&a1));
myfun vfun1 = (myfun)vptr[0];
myfun vfun2 = (myfun)vptr[1];
myfun vfun3 = (myfun)vptr[2];
printf("######### 通過虛函數指針 手工調用虛函數\n");
vfun1();
vfun2();
vfun3();
return 0;
}
運行結果如下:
因爲類A沒有構造方法,有三個虛函數,且虛函數不佔用對象內存空間。只有一個虛函數指針,所以時4字節。通過手工調用說明了虛函數表指針地址與類對象首地址相同,且虛函數表指針指向的內存存放着虛函數地址,這段內存就叫虛函數表。
2.繼承模式下的虛函數與虛函數指針
1.單繼承
#include <iostream>
using namespace std;
using namespace std;
class A {
public:
virtual void fun1() {
cout << "this is A virtual fun1" << endl;
}
virtual void fun2() {
cout << "this is A virtual fun2" << endl;
}
virtual void fun3() {
cout << "this is A virtual fun3" << endl;
}
};
class B :public A {
public:
virtual void fun3() {
cout << "this is B virtual fun3" << endl;
}
virtual void fun4() {
cout << "this is B virtual fun4" << endl;
}
};
int main()
{
B b1;
printf("######### 對象b1 佔用內存大小 %d\n", sizeof(b1));
printf("######### 通過對象調用虛函數\n");
b1.fun1();
b1.fun2();
b1.fun3();
b1.fun4();
typedef void(*myfun)();
//拿到虛函數表首地址 vptr就是一個有三個元素的函數指針數組
long *vptr = (long *)*((long *)(&b1));
myfun vfun1 = (myfun)vptr[0];
myfun vfun2 = (myfun)vptr[1];
myfun vfun3 = (myfun)vptr[2];
myfun vfun4 = (myfun)vptr[3];
printf("######### 通過虛函數指針 手工調用虛函數\n");
vfun1();
vfun2();
vfun3();
vfun4();
return 0;
}
運行結果爲:
子類會繼承所有父類的虛函數,如果子類有相同名字參數的虛函數則會覆蓋父類虛函數。對於父類虛函數fun3,即使子類的fun3不加virtual關鍵字,編譯器也會認爲fun3是虛函數,這點需要注意下。在這裏父類和子類都只有一個虛函數表,父類和子類的虛函數指針分別指向自己的虛函數表。在這裏子類的虛函數表比父類的虛函數表多一個fun4,fun3覆蓋父類的fun3。如果子類沒有虛函數,那麼子類的虛函數表內容與父類的虛函數表內容相同,但虛函數指針不同,指向的虛函數表首地址也不同,僅僅是虛函數表的內容相同而已。虛函數表在類的定義時產生,虛函數指針在對象的構造時產生。
2.多繼承
#include <iostream>
using namespace std;
class base1 {
public:
virtual void fun1() {
cout << "this is base1 virtual fun1" << endl;
}
virtual void fun2() {
cout << "this is base1 virtual fun2" << endl;
}
};
class base2 {
public:
virtual void fun3() {
cout << "this is base2 virtual fun3" << endl;
}
virtual void fun4() {
cout << "this is base2 virtual fun4" << endl;
}
};
class B :public base1, public base2{
public:
void fun2() { //覆蓋base1虛函數
cout << "this is B virtual fun2" << endl;
}
virtual void fun3() { //覆蓋base2虛函數
cout << "this is B virtual fun3" << endl;
}
virtual void fun5() {
cout << "this is B virtual fun5" << endl;
}
};
int main()
{
B b1;
printf("######### 對象b1 佔用內存大小 %d\n", sizeof(b1));
printf("######### 通過對象調用虛函數\n");
b1.fun1();
b1.fun2();
b1.fun3();
b1.fun4();
b1.fun5();
typedef void(*myfun)();
//子類與第一個基類公用一個虛函數指針vptr ,其他的基類有自己的虛函數指針
long *vptr1 = (long *)*((long *)(&b1));
myfun vfun1 = (myfun)vptr1[0];
myfun vfun2 = (myfun)vptr1[1];
myfun vfun3 = (myfun)vptr1[2];
//myfun vfun4 = (myfun)vptr1[3];
//myfun vfun5 = (myfun)vptr[4];
printf("######### 通過虛函數指針 手工調用虛函數\n");
vfun1();
vfun2();
vfun3();
//vfun4();
long *base2_vptr2 = (long *)*(long *)((long *)&b1 + 1);
myfun base2_vfun1 = (myfun)base2_vptr2[0];
myfun base2_vfun2 = (myfun)base2_vptr2[1];
base2_vfun1();
base2_vfun2();
return 0;
運行結果如下:
這裏對象b1的內存大小是8,說明對象b1有兩個虛函數指針,繼承自兩個基類。其中子類的虛函數指針與子類第一個繼承的父類共用,這裏子類沒有其他數據成員,因此對象b1的兩個虛函數指針連續存儲。由於子類覆蓋了父類兩個虛函數,因此子類對象一共有5個虛函數。由於繼承兩個基類,且這兩個基類都有虛函數,所以子類有兩個虛函數表,分別由兩個虛函數指針指向。在子類定義的時候,對於虛函數表,子類會覆蓋父類的同名虛函數。
3.虛函數指針位置分析
證明虛函數指針位置代碼如下:
#include <iostream>
using namespace std;
class A {
public:
int m_a;
virtual void fun1() {
cout << "this is A virtual fun1" << endl;
}
};
int main()
{
A a;
long * a_pos = (long *)&a.m_a;
printf("對象大小 %d, 對象首地址 %p, 成員a的地址%p\n", sizeof(a), &a, a_pos);
return 0;
}
運行結果爲
對象有一個int的數據成員和一個虛函數指針,所以對象大小爲8字節,且數據成員的地址比對象首地址要偏移4字節,說明虛函數指針就在對象內存的開始位置。
這張圖比較形象的說明了多繼承虛函數及虛函數表的佈局
圖轉載自https://blog.csdn.net/qq_36359022/article/details/81870219