【C++學習筆記】----詳解虛函數相關問題(多態,覆蓋(重寫),隱藏(重定義),切片,虛函數表)

1.常見問題

1.什麼是多態?

多態是不同對象調用同一個函數,產生不同的結果(狀態),
例如買票函數,大人->全價,小孩->半價。

2.多態實現的原理?
基類的引用或者指針調用虛函數。
被調用的函數必須是虛函數,派生類的虛函數必須進行重寫(覆蓋)。

3.構造函數可以是虛函數嗎?
構造函數不可以是虛函數,因爲對象還沒有產生,
沒有虛函數表指針,對象初始化才產生虛函數表指針。

4.析構函數可以是虛函數嗎?
析構函數可以是虛函數(非必須),在實現多態的時候必須是虛函數,
編譯器將析構函數優化成同名的(destructor),如果不寫成虛函數,
就會造成內存泄漏,調用是根據對象類型調用,調用的是父類指針。

5.靜態成員函數可以是虛函數嗎?
靜態成員函數沒有this指針,是所有對象共有,
而多態就是爲了體現對象不同狀態不同,所以靜態成員函數不能是虛函數。

6.虛函數在什麼時候生成,存在哪個位置。
虛函數在編譯階段生成,通過反彙編查看到,存在代碼段的常量區。
同類對象共用一個虛函數表。

7.什麼是抽象類?
抽象類只聲明瞭純虛函數的類,不能實例化出對象,作爲接口繼承。

8.對象訪問普通函數和虛函數哪個快?
如果直接通過對象訪問,普通函數和虛函數一樣都是直接通過地址訪問。在這裏插入圖片描述
如果通過基類對象的引用或者指針訪問的話(多態),就會先去虛函數指針表中找對應的虛函數。普通函數快。
在這裏插入圖片描述

2.代碼展示

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
/*class A {};
class B :public A {};*/
class person
{
public:
	 person()
	{
		cout << "person()" << endl;
	 }
	   ~person()
	{
		cout << "~person()" << endl;
	}
	  virtual void buyticket()//final虛函數不能被子類重寫
	{
		cout << "全價" << endl;
		cout << _name << endl;
		
	}
	/* virtual person& buyticket()
	 {
		 cout << "全價" << endl;
		 cout << _name << endl;
		 return*this;
	 }*/
	  /*virtual A* buyticket()
	 {
		 cout << "全價" << endl;
		 cout << _name << endl;
		 return new A;
	 }*/
	string _name;
};

class stu: public person
{
public:
	stu()
	{
		cout << "stu()" << endl;
	}
	 ~stu()
	{
		cout << "~stu()" << endl;
	}
	 void buyticket()//override檢查是否重寫基類的虛函數
	{
		cout << "半價" << endl;
		cout << _name << endl;
		cout << _num << endl;
		cout << person::_name << endl;
	}
	/*virtual void buyticket(int)//虛函數重寫,返回值,函數名,參數都相同時,成爲子類重寫了基類的虛函數
	{
		cout << "半價" << endl;
		cout << _name << endl;
		cout << _num << endl;
	}*/
	/*virtual stu& buyticket()//虛函數重寫,函數名,參數都相同時,成爲子類重寫了基類的虛函數(協變引用都必須是引用,指針都指針)
	{
		cout << "半價" << endl;
		cout << _name << endl;
		cout << _num << endl;
		return *this;
	}*/
	/*virtual B* buyticket()//只要返回值滿足父類對父類,子類對子類的引用或者指針,都可以是協變。其他類繼承也可以。
	{
		cout << "半價" << endl;
		cout << _name << endl;
		cout << _num << endl;
		return new B;
	}*/
	int _num;
	string _name;
};
void test1()
{
	//同名隱藏 - 》重定義
	person p;
	p._name = "xff";
	stu s;
	s._name = "cf";
	s._num = 1;
	s.person::_name = "name";
	//p.buyticket();
	//s.buyticket();
	//person*pa = new stu;
	//cout << pa->_name << endl;
	//delete pa;
	//父類調用子類對象出現切片
	//person* pp = &s;
	//person& ps = s;
	//ps.buyticket();
	//pp->buyticket();//調用指針對象(person)類型的,數據是指向(stu)類型的。
	//stu*pps = &p;//子類調用不合法
	//stu* ps = (stu*)&p; //強轉
	//ps->buyticket();    //強轉成stu類型,調用的函數是stu對象指向的,數據是指向(person)類型的,有越界訪問。
}
void func(person p)
{
	p.buyticket();
}
void func1(person& p)//父類引用
{
	p.buyticket();
}
void func2(person* p)//父類指針
{
	p->buyticket();
}
void test2()
{
	person p;
	p._name = "xff";
	stu s;
	s._name = "cf";
	s._num = 1;
	//func(p);
	//func(s);
	func1(p);
	func1(s);
	//func2(&p);
	//func2(&s);
}

void test3()
{
	//person p;
	//stu s;
	person *pp = new stu;//不是多態會內存泄漏
	//person *pp = new person;
	delete pp;
}
//抽象類(接口類) 純虛函數的類
class A 
{
public :
	virtual void  fun1()
	{
		cout << "A::fun1()" << endl;
	}
	virtual void  fun2()
	{
		cout << "A::fun2()" << endl;
	}
	 /* void  fun3()
	{
		cout << "A()" << endl;
	}*/
	//virtual void  fun1() = 0;
	int _num1;
	
};
class B 
{
public:
	virtual void  fun1() 
	{
		cout << "B::fun1()" << endl;
	}
	/*virtual void  fun()//不重寫純虛函數子類就不能實例化出對象
	{

	}*/
	void  fun3()
	{
		cout << "B()" << endl;
	}
	virtual void  fun2()
	{
		cout << "B::fun2()" << endl;
	}
	int _age;
};
class C :public B,public A
{
public:
	virtual void  fun1()
	{
		cout << "C::fun1()" << endl;
	}
	virtual void  fun3()
	{
		cout << "C::fun3()" << endl;
	}
};

typedef void(*VFPTR)();
void PrintVTable(VFPTR* vTable)
{
	// 依次取虛表中的虛函數指針打印並調用。調用就可以看出存的是哪個函數
	cout << " 虛表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d個虛函數地址 :0X%p,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
void test4()
{
	A a;//抽象類不能實例化出對象
	B b;
	C c1;
	C c2;
	//A*pa = new B;
	//pa->fun1();
	//printf("%p\n", *((int*)&a));
	//printf("%p\n", *((int*)&b));
	VFPTR* va = (VFPTR*)(*(int*)&a);
	PrintVTable(va);
	VFPTR* vb = (VFPTR*)(*(int*)&b);
	PrintVTable(vb);
	VFPTR* vc1 = (VFPTR*)(*(int*)&c1);
	PrintVTable(vc1);
	VFPTR* vc2 = (VFPTR*)(*(int*)&c2);
	PrintVTable(vc2);
	//a.fun3();
	//b.fun3();
 	//cout << sizeof(a) << endl;//虛函數表指針32位-4字節大小
	//cout << sizeof(b) << endl;

}
int main()
{
	//test1();//隱藏重定義名字相同
	//test2();//多態重重寫
	//test3();
	//test4();
 	system("pause");
	return 0;
}

3.結果展示

1.test1()

通過對象調用,函數與調用對象有關。
在這裏插入圖片描述
同名隱藏,隱藏父類。
在這裏插入圖片描述
基類指針調用虛函數,子類重寫虛函數。
在這裏插入圖片描述
基類對象的引用,調用子類的重寫的虛函數。
在這裏插入圖片描述
子類調用父類的不合法
在這裏插入圖片描述
強轉成子類對象,可以實現多態,但是存在越界訪問。
父類對象裏面沒有子類的_num
在這裏插入圖片描述
在這裏插入圖片描述
父類的引用或者指針直接訪問的話,會出現切片,也就是隻能訪問到子類對象繼承父類的部分。
在這裏插入圖片描述
在這裏插入圖片描述

2.test2()

父類的指針或者引用,虛函數重寫實現多態。
第一個沒有傳這兩個類型,所以沒有構成多態,函數是通過對象調用,
所以我們可以看出來,傳遞的時候切片了,子類顯示的_name是繼承父類對象裏面的_name.
有很好的展示了隱藏(重定義)
在這裏插入圖片描述

3.test3()

析構函數沒寫virtual,只析構了基類對象的。
在這裏插入圖片描述
析構寫成虛函數,構成多態。
在這裏插入圖片描述
基類的不影響。
在這裏插入圖片描述

4.test4()

在這裏插入圖片描述
在這裏插入圖片描述
我們可以看出,多繼承中虛函數的繼承以及重寫,C類中的A B類的對象重寫了fun1(),fun2()直接繼承了之前的,fun3()是沒有重寫的虛函數,它放在第一繼承基類的虛函數最後面,我們改一下第一繼承類,發現未重寫的虛函數,會放到它後面。

相同類對象共用一個虛函數表。

在這裏插入圖片描述

4.總結

只有滿足基類的引用或者指針調用派生類虛函數重寫,才能構成多態。
對象直接調用的話,和對象類型有關,基類調子類會切片。
同一類不同對象共用一個虛函數表,不同類對象不共用,多繼承的時候,一個對象不止只有一個虛函數表。
多繼承中,派生類未重寫的虛函數放在第一繼承類的後面。
協變是虛函數重寫中,返回類型可以不同,但必須是基類與派生類的關係,不能調換順序。

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