首頁 > 軟體

C++的多型與虛擬函式你瞭解嗎

2022-03-27 19:00:07

多型性

多型性是物件導向程式設計的關鍵技術之一,若程式設計語言不支援多型性,不能稱為物件導向的語言,利用多型性技術,可以呼叫同一個函數名的函數,實現完全不同的功能

在C++中有兩種多型性:

  • 編譯時的多型

通過函數的過載和運運算元的過載來實現的

  • 執行時的多型性

執行時的多型性是指在程式執行前,無法根據函數名和引數來確定該呼叫哪一個函數,必須在程式執行過程中,根據執行的具體情況來動態地確定;它是通過類繼承關係public和虛擬函式來實現的,目的也是建立一種通用的程式;通用性是程式追求的主要目標之一

通過參照或指標呼叫時,才可以達到執行時的多型

虛擬函式

虛擬函式是一個類的成員函數,定義格式如下:

virtual 返回型別 函數名(參數列);

關鍵字virtual指明該成員函數為虛擬函式,virtual僅用於類定義中,如虛擬函式在類外定義,不可加virtual

我們來看下面程式碼

class Animal
{
private:
	string name;
public:
	Animal(const string& na):name(na)
	{}
public:
	virtual void eat(){}
	virtual void walk(){}
	virtual void tail(){}
	virtual void PrintInfo(){}

	string& get_name()
	{
		return name;
	}
	const string& get_name()const
	{
		return name;
	}
};

class Dog :public Animal
{
private:
	string owner;
public:
	Dog(const string& ow, const string na) :Animal(na), owner(ow)
	{}
	virtual void eat() 
	{
		cout << "Dog Eat: bone" << endl;
	}
	virtual void walk() 
	{
		cout << "Dog Walk: run" << endl;
	}
	virtual void tail() 
	{
		cout << "Dog Tail: wangwang" << endl;
	}
	virtual void PrintInfo() 
	{
		cout << "Dog owner" << owner << endl;
		cout << "Dog name:" << get_name() << endl;
	}
};
class Cat :public Animal
{
private:
	string owner;
public:
	Cat(const string& ow, const string na) :Animal(na), owner(ow)
	{}
	virtual void eat()
	{
		cout << "Cat Eat: fish" << endl;
	}
	virtual void walk()
	{
		cout << "Cat Walk: silent" << endl;
	}
	virtual void tail()
	{
		cout << "Cat Tail: miaomiao" << endl;
	}
	virtual void PrintInfo()
	{
		cout << "Cat owner: " << owner << endl;
		cout << "Cat name: " << get_name() << endl;
	}
};
 // 需要公有繼承 公有繼承代表是一個的意思
 // 需要參照或指標呼叫
void fun(Animal& animal)
{
	animal.eat(); //物件名稱.虛方法()
	animal.walk();
	animal.tail();
	animal.PrintInfo();
}

int main()
{
	Dog dog("zyq", "hashiqi"); //const string& ow = "zyq"
	Cat cat("zyq", "bosimao");
	fun(dog);
	fun(cat);
	return 0;
}

在這裡我們可以看到,當我們呼叫fun()函數時,傳入dog物件則呼叫Dog的方法,傳入cat呼叫Cat方法;這就是所謂的執行時的多型

要想達到執行時的多型(晚繫結)需要滿足:

  • 公有繼承
  • 有虛擬函式
  • 必須以指標或參照方式呼叫虛擬函式

若發生早繫結,則會呼叫Animal型別的方法

成員函數應儘可能的設定為虛擬函式,但必須注意一下幾條:

1.派生類中定義虛擬函式必須與基礎類別中的虛擬函式同名外,還必須同參數列,同返回型別;否則被認為是過載,而不是虛擬函式。如基礎類別中返回基礎類別指標,派生類中返回派生類指標是允許的,這是一個例外

2.只有類的成員函數才能說明為虛擬函式,這是因為虛擬函式僅適用於有繼承關係的類物件

3.靜態成員函數,是所有同一類物件公有,不受限於某個物件,不能作為虛擬函式(友元函數也不可以)

4.實現動態多型性時,必須使用基礎類別型別的指標變數或參照,使該指標指向該基礎類別的不同派生類的物件,並通過該指標指向虛擬函式,才能實現動態的多型性

5.行內函式每個物件一個拷貝,無對映關係,不能作為虛擬函式

6.解構函式可定義為虛擬函式,建構函式不可以定義為虛擬函式,因為在呼叫建構函式時物件還沒有完成範例化;在基礎類別中及其派生類中都動態分配的記憶體空間時,必須把解構函式定義為虛擬函式,實現復原物件時的多型性

7.函數執行速度要稍慢一些,為了實現多型性,每一個派生類中均要儲存相應虛擬函式的入口地址表,函數的呼叫機制也是間接實現;所以多型性總要付出一定代價,但通用性是一個更高的目標

8.如果定義放在類外,virtual只能加在函數宣告前面,不能載入函數定義前面;正確的定義必須不包括virtual

虛擬函式是覆蓋,同名函數是隱藏

虛擬函式編譯過程

class Object
{
private:
	int value;
public:
	Object(int x = 0) :value(x)
	{}
	virtual void add()
	{
		cout << "Object::add" << endl;
	}
	virtual void fun()
	{
		cout << "Object::fun" << endl;
	}
	virtual void print()const
	{
		cout << "Object::print" << endl;
	}
};
class Base:public Object
{
private:
	int sum;
public:
	Base(int x = 0) :Object(x+10),sum(x)
	{}
	virtual void add()
	{
		cout << "Base::add" << endl;
	}
	virtual void fun()
	{
		cout << "Base::fun" << endl;
	}
	virtual void print()const
	{
		cout << "Base::print" << endl;
	}
};

int main()
{	
}

此處虛擬函式表中進行的是同名覆蓋,而不像繼承關係中,同名成員進行隱藏,就近處理;虛函表僅有一份,存在資料區

在主函數建立物件

int main()
{
	Base base(10);
	Object* op = &base;
}

可以看到base的大小為12位元組,因為其中基礎類別物件Object,新增了虛表變為了8位元組,且在構建過程,首先構建Object基礎類別,此時虛表指標指向Object的虛表,而接著構建Base類的時候,會將虛表指標修改為指向Base的虛表

也就是,當有虛擬函式時,建構函式除了構建物件初始化物件的資料成員外,還會將虛表的地址給到虛表指標;同時這也是建構函式不可以作為虛擬函式的原因

int main()
{
	Base base(10);
	Object* op = NULL;
	Object obj(0);

	op = &base;
	op->add(); //指標或參照調動,則採用執行時多型
	op->fun();
	op->print();

	obj = base;
	obj.add(); //物件直接調動,則採用編譯時多型
	obj.fun();
	obj.print();
}

也就是我們通過,物件名.方法 的方式呼叫虛擬函式,則通過編譯時多型的方式

執行時的多型,是通過查詢虛表進行呼叫;下面通過組合進一步檢視

只有進行以指標呼叫或參照呼叫的時候才會對虛表進行查詢

三層繼承

class Object
{
private:
	int value;
public:
	Object(int x = 0) :value(x)
	{}
	virtual void add()
	{
		cout << "Object::add" << endl;
	}
	virtual void fun()
	{
		cout << "Object::fun" << endl;
	}
	virtual void print()const
	{
		cout << "Object::print" << endl;
	}

	void fn_a()
	{
		fun();	
	}
};
class Base:public Object
{
private:
	int sum;
public:
	Base(int x = 0) :Object(x+10),sum(x)
	{}
	virtual void add()
	{
		cout << "Base::add" << endl;
	}
	virtual void fun()
	{
		cout << "Base::fun" << endl;
	}
	virtual void show()
	{
		cout << "Base::show" << endl;
	}
};
class Test :public Base
{
private:
	int num;
public:
	Test(int x = 0) :Base(x + 10)
	{}
	virtual void add()
	{
		cout << "Test::add" << endl;
	}
	virtual void print() const
	{
		cout << "Test::print" << endl;
	}
	virtual void show()
	{
		cout << "Test::show" << endl;
	}
};

我們可以看到虛擬函式表,當我們構建派生類,會複製基礎類別的虛擬函式表,將虛表指標指向新的虛擬函式表,並且將同名的虛擬函式進行覆蓋

依舊使用上面程式碼

/*
	void fn_a()
	{
		fun();	//this->fun(); 屬於動態繫結!
	}
*/
int main()
{
	Test t1;
	Base base;
	Object obj;

	t1.fn_a(); //fn_a(&t1);
	base.fun_a();
	obj.fn_a();
	return 0;
}

這裡依然屬於動態繫結,所以呼叫虛表指標指向的相對應類的虛表

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容! 


IT145.com E-mail:sddin#qq.com