C++ 學習 對象模型之虛函數

虛函數在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
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章