【C++ | 虚函数】虚函数详解 及 例子代码演示(包含虚函数使用、动态绑定、虚函数表、虚表指针)

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍C++的虚函数 🍭
😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你——泰戈尔🍭
⏰发布时间⏰:2024-07-13 14:33:55

本文未经允许,不得转发!!!



在这里插入图片描述

🎄一、什么是虚函数?为什么需要虚函数?

定义:虚函数就是在函数声明时使用关键字virtual修饰的成员函数。其格式一般如下:

class CAnimal{
	virtual void eat();	// 声明了虚函数 eat()
}

为什么需要虚函数?

在C++中使用虚函数的主要目的是实现多态。多态是多种形态的意思,也就是同一个方法在派生类和基类中的行为是不同的。

C++怎样通过虚函数实现多态的呢?一般情况下,通过指针或引用调用一个类成员函数时,程序会 根据引用类型或指针类型 选择对应类的成员函数,如果被调用的成员函数是虚函数的话,则程序将根据 引用或指针指向的对象 的类型来选择方法。

下面用代码演示基类指针调用 正常函数 和 虚函数 的区别:

// g++ 23_Virtual.cpp 
#include <iostream>

using namespace std;

class CAnimal{
public:
	CAnimal(){
		cout << "Calling CAnimal(): this=" << this << endl;
	}
	void eat(){
		cout << "Animal eat" << endl;
	}
	virtual void run(){
		cout << "Animal run" << endl;
	}
	
private:
};


class CDog : public CAnimal{
public:
	CDog(){
		cout << "Calling CDog(), this=" << this << endl;
	}
	void eat(){		// 重写了基类的eat(),基类的会隐藏
		cout << "Dog eat" << endl;
	}
	void run(){
		cout << "Dog run" << endl;
	}
};

int main ()
{
	CAnimal animal;
	CDog dog;
	
	CAnimal* pAnimal = &animal;	// 基类指针指向基类对象
	pAnimal->eat();	// 调用非虚函数,按照指针类型调用,打印 Animal eat
	pAnimal->run();	// 调用虚函数,按照对象类型调用,打印 Animal run
	cout << endl;
	
	pAnimal = &dog;	// 基类指针指向派生类对象
	pAnimal->eat();	// 调用非虚函数,按照指针类型调用,Animal eat
	pAnimal->run();	// 调用虚函数,按照对象类型调用,打印 Dog run
	cout << endl;
	
	return 0;
}

运行结果如下:
在这里插入图片描述


在这里插入图片描述

🎄二、静态绑定、动态绑定

绑定(binding):编译器将源代码中的函数调用解释为执行特定的函数代码块被称为函数名绑定(binding)

在 C 语言中,函数名绑定非常简单,因为每个函数名都对应一个不同的函数。 在 C++ 中,由于重载函数的出现,函数名绑定变得复杂,编译器必须査看函数参数以及函数名才能确定使用哪个函数。编译器可以在编译期间完成这样的函数名绑定。但是虚函数的出现使编译器无法在编译期间知道调用的是哪个函数,必须在运行时才完成函数名绑定。

静态绑定(binding):在编译期间就可以完成的函数名绑定。
动态绑定(binding):在运行期间才可以完成的函数名绑定。

当 我 们 使 用 基 类 的 引 用 或 指 针 调 用 基 类 中 定 义 的 一 个 函 数 时 , 我 们 并 不 知 道 该 函 数 真 正 作 用 的 对 象 是 什 么 类 型 , 因 为 它 可 能 是 一 个 基 类 的 对 象 也 可 能 是 一 个 派 生 类 的 对 象。 如 果 该 函 数 是 虚 函 数, 则 直 到 运 行 时 才 会 决 定 到 底 执 行 哪 个 版 本, 判 断 的 依 据 是 引 用 或 指 针 所 綁 定 的 对 象 的 真 实 类 型

当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

之前总是不理解一点,我在定义基类指针或引用时,明明已经指明的对象了,为什么它需要在运行时才知道呢?那是因为考虑少了。如果当基类指针或引用作为函数参数使用,没运行时是不清楚传入的是基类对象或子类对象的。又或者从输入根据条件去创建对象,也是无法事先知道基类指针或引用指向哪个对象的。下面代码演示C++的动态绑定:

// g++ 23_Dynamic_binding.cpp -std=c++11
#include <iostream>
using namespace std;

class CAnimal{
public:
	virtual void run(){
		cout << "Animal run" << endl;
	}
};

class CDog : public CAnimal{
public:
	virtual void run() override{
		cout << "Dog run" << endl;
	}
};

int main ()
{
	const int OBJ_NUM = 4;
	CAnimal *pAnimal[OBJ_NUM];	// 基类指针数组,用于管理对象
	
	// 程序运行后,根据输入类型创建对象
	char kind;		// 用于获取类型
	for(int i=0; i<OBJ_NUM; i++)
	{
		cout << "请输入要创建的对象:1表示CAnimal, 2表示CDog" << endl;
		while(cin >> kind && (kind != '1' && kind != '2'))
			cout << "请输入1或2" << endl;
		if(kind == '1')
			pAnimal[i] = new CAnimal();
		else
			pAnimal[i] = new CDog();
	}
	cout << endl;
	
	// 按照实际的对象打印
	for(int i=0; i<OBJ_NUM; i++)
	{
		pAnimal[i]->run();
	}
	
	for(int i=0; i<OBJ_NUM; i++)
	{
		delete pAnimal[i];	// 释放对象
	}
	return 0;
}

运行结果如下:
在这里插入图片描述


在这里插入图片描述

🎄三、虚函数的使用

虚函数一定是在 继承的前提下 才有用的,如果定义了一个类,它不会被继承,那么这个类定义虚函数也没用。
虚函数一定是在 基类的指针或引用 去调用时,才会按照实际对象类型去动态绑定的。

✨3.1 虚函数在基类的定义

在基类定义虚函数时,需要注意下面几点:

  1. 所有虚函数都必须有定义。因为直到运行时才知道调用哪个虚函数,这样做避免出现问题。
  2. 如果某个函数的行为在派生类会存在不同实现,则可以考虑设置成虚函数,例如:CAnimal类的eat方法就可能与其派生类CDog的 eat方法不同实现。
  3. 如果不需要在派生类中重新定义基类的函数,则不应该将其设置为虚函数,节省开销。例如下面例子的setName函数。
  4. 一旦某个函数在基类被声明成虚函数,则在所有派生类中它都是虚函数。
  5. 构造函数不能声明为虚函数。
  6. 基类的析构函数一般声明为虚函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。如果析构函数不是虚的, 则将只调用对应于指针类型的析构函数。如果析构函数是虚的,将调用相应对象类型的析构函数。
  7. 关键字virtual只用于类声明的函数原型中,不出现在定义的时候。

🌰举例子:

// g++ 23_Virtual_usage.cpp 
#include <iostream>
#include <string.h>

using namespace std;

class CAnimal{
public:
	//virtual CAnimal(){ 	// 5、构造函数不能设置虚函数
	CAnimal(){
		cout << "Calling CAnimal(): this=" << this << endl;
	}
	virtual void eat(){	// 2、派生类的eat()可能不同实现,设置成虚函数
		cout << "Animal eat" << endl;
	}
	void setName(char *name)	// 3、不需要在派生类重写的函数,不设置成虚函数
	{
		strncpy(m_name, name, sizeof(m_name));
	}
	virtual ~CAnimal(){	// 6、基类的析构函数声明为虚函数
		cout << "~CAnimal" << endl;
	}
private:
	char m_name[64];
};


class CDog : public CAnimal{
public:
	CDog(){
		cout << "Calling CDog(), this=" << this << endl;
	}
	virtual void eat(){		// 重写了基类的eat()
		cout << "Dog eat bones" << endl;
	}
	~CDog(){
		cout << "~CDog" << endl;
	}
};

int main ()
{
	CAnimal* pAnimal = new CAnimal();	// 基类指针指向基类对象
	pAnimal->eat();
	delete pAnimal;
	pAnimal = NULL;
	cout << endl;
	
	pAnimal = new CDog();	// 基类指针指向派生类对象
	pAnimal->eat();
	delete pAnimal;
	pAnimal = NULL;
	cout << endl;
	
	return 0;
}

运行结果如下,下面运行了两次,第一次是基类析构函数没声明为虚函数的。
在这里插入图片描述


✨3.2 虚函数在派生类的使用

基类定义的虚函数,如果派生类有不同的实现,需要重写。

派生类使用虚函数注意几点:

  1. 一旦某个函数在基类被声明成虚函数,则在所有派生类中它都是虚函数。
  2. 如果派生类重新定义基类的虚函数,最好加上关键字virtual,这样使代码更容易读。不加virtual的话,也是虚函数。
  3. 重写基类的虚函数时,需要保证 函数名,参数列表,const属性 都与基类的虚函数声明一致。
    class CAnimal{
    public:
        virtual void eat(int x) const; // 虚函数
    };
     
    class CDog: public CAnimal{
    public:
        virtual void Eat(int x); // e 写成 E,函数名不一致,新的虚函数 
        virtual void eat(short x); // 参数列表不一样,新的虚函数 
        virtual void eat(int x); // const 属性不一样,新的虚函数 
        virtual void eat(int x) const; // 函数名,参数列表,const属性都一致,重写了基类的虚函数 
    }
    
  4. 重写基类虚函数时,返回值类型也需要一致,否则会编译报错。下面这个情况除外,如果基类虚函数返回值类型是基类指针或引用时,在派生类重写时返回值类型可以在派生类指针或引用。
    class CAnimal{
    public:
       virtual CAnimal *run(int a) const;
    };
    
    class CDog: public CAnimal{
    public:
       virtual CDog *run(int a) const;	// 重写了基类的 run ,但返回值为 CDog*
    }
    

🌰完整例子:

// g++ 23_Virtual_usage2.cpp 
#include <iostream>
#include <string.h>

using namespace std;

class CAnimal{
public:
	CAnimal(){
		cout << "Calling CAnimal(): this=" << this << endl;
	}
	virtual void eat(int a) const{
		cout << "Animal eat" << endl;
	}
	virtual CAnimal *run(int a) const{
		cout << "Animal run" << endl;
		return new CAnimal();
	}
	virtual ~CAnimal(){
		cout << "~CAnimal" << endl;
	}

};


class CDog : public CAnimal{
public:
	CDog(){
		cout << "Calling CDog(), this=" << this << endl;
	}
	
	virtual void Eat(int x){};		// e 写成 E,函数名不一致,新的虚函数 
	virtual void eat(short x){};	// 参数列表不一样,新的虚函数 
	virtual void eat(int x){};		// const 属性不一样,新的虚函数 
	virtual void eat(int a) const{		// 重写了基类的eat()
		cout << "Dog eat bones" << endl;
	}
	virtual CDog *run(int a) const{	// 重写了基类的 run ,但返回值为 CDog*
		cout << "CDog run" << endl;
		return new CDog();
	}
	~CDog(){
		cout << "~CDog" << endl;
	}
};

int main ()
{
	CAnimal* pAnimal = new CAnimal();	// 基类指针指向基类对象
	CAnimal* pRun = pAnimal->run(1);
	pAnimal->eat(1);
	delete pAnimal;
	pAnimal = NULL;
	cout << endl;
	delete pRun;
	pRun = NULL;
	cout << endl;
	
	pAnimal = new CDog();	// 基类指针指向派生类对象
	pRun = pAnimal->run(1);
	pAnimal->eat(1);
	delete pAnimal;
	pAnimal = NULL;
	cout << endl;
	delete pRun;
	pRun = NULL;
	cout << endl;
	
	return 0;
}

运行结果:
在这里插入图片描述


✨3.3 finaloverride 说明符

由于派生类重写虚函数时需要 函数名,参数列表,const属性 都一致才行,这样容易导致写错。C++11提供了两个说明符 finaloverride 来帮助我们更好地使用虚函数。

  • final 说明符:如果基类已经把函数定义成 final 了, 则之后任何尝试覆盖该函数的操作都将引发错误:
    class CAnimal{
    public:
    	virtual void finlaFun(int a) const final; // 指定为final,不允许被重写
    };
    class CDog : public CAnimal{
    public:
    	virtual void finlaFun(int a) const{}	// 基类指定为final了,重写会报错
    };
    
  • override 说明符:可以告诉编译器,我们写的这个函数是为了重写基类的虚函数,如果 函数名,参数列表,const属性,返回值 这些不一致,就给我报错。在3.2的例子中,我们声明的虚函数如果和基类不一致会成为新的虚函数,加上override之后,如果不构成重写就会报错
    class CAnimal{
    public:
        virtual void eat(int x) const; // 虚函数
    };
     
    class CDog: public CAnimal{
    public:
        virtual void Eat(int x)override{};		// e 写成 E,函数名不一致,报错
    	virtual void eat(short x)override{};	// 参数列表不一样,报错
    	virtual void eat(int x)override{};		// const 属性不一样,报错
    	virtual void eat(int a) const override{		// 重写了基类的eat()
    		cout << "Dog eat bones" << endl;
    	}
    }
    

🌰完整例子:

// g++ 23_Virtual_usage3.cpp -std=c++11
#include <iostream>
#include <string.h>

using namespace std;
#define DEBUG 1
class CAnimal{
public:
	CAnimal(){
		cout << "Calling CAnimal(): this=" << this << endl;
	}
	virtual void finlaFun(int a) const final{	// 指定为final,不允许被重写
		cout << "Animal finlaFun" << endl;
	}
	virtual void eat(int a) const{
		cout << "Animal eat" << endl;
	}
	virtual CAnimal *run(int a) const{
		cout << "Animal run" << endl;
		return new CAnimal();
	}
	virtual ~CAnimal(){
		cout << "~CAnimal" << endl;
	}
};


class CDog : public CAnimal{
public:
	CDog(){
		cout << "Calling CDog(), this=" << this << endl;
	}
#if DEBUG
	virtual void finlaFun(int a) const{}	// 基类指定为final了,重写会报错
	
	virtual void Eat(int x)override{};		// e 写成 E,函数名不一致,报错
	virtual void eat(short x)override{};	// 参数列表不一样,报错
	virtual void eat(int x)override{};		// const 属性不一样,报错
#endif
	virtual void eat(int a) const override{		// 重写了基类的eat()
		cout << "Dog eat bones" << endl;
	}
	virtual CAnimal *run(int a) const override{// 重写了基类的 run ,但返回值为 CDog*
		cout << "CDog run" << endl;
		return new CDog();
	}
	~CDog(){
		cout << "~CDog" << endl;
	}
};

int main ()
{
	CAnimal* pAnimal = new CAnimal();	// 基类指针指向基类对象
	delete pAnimal;
	pAnimal = NULL;
	cout << endl;
	
	pAnimal = new CDog();	// 基类指针指向派生类对象
	delete pAnimal;
	pAnimal = NULL;
	cout << endl;
	
	return 0;
}

编译上面例子时,需要添加 -std=c++11 让编译器支持C++11标准,编译报错结果如下,想编译通过需要把宏DEBUG的值改为0:
在这里插入图片描述


在这里插入图片描述

🎄四、虚函数表、虚表指针

这个小节介绍虚函数的工作原理。

✨4.1 虚表指针

通常, 编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这个函数地址数组被成为虚函数表,而这个隐藏成员被成为虚表指针(在32位系统占用4个字节,64位系统占用8个字节)。所以,当一个类存在虚函数,那么它就会多占用一个指针大小的内存。这个指针会存在类对象的最前面。下面例子可以证明对象中存在一个虚表指针:

// g++ 23_Virtual_Point.cpp
#include <iostream>
using namespace std;

class CAnimal_NULL{	// 空类
};

class CAnimal{		// 基类,有一个虚函数
public:
	virtual void eat(){}
};

class CDog : public CAnimal{// 派生类,继承了基类的虚函数
};

int main ()
{
	CAnimal_NULL animal_null;
	CAnimal animal;
	CDog dog;
	cout << sizeof(animal_null) << endl;
	cout << sizeof(animal) << endl;
	cout << sizeof(dog) << endl;
}

运行结果,空类的大小是1,只定义了一个虚函数,类大小就变成8了,刚好是一个64位指针大小。
在这里插入图片描述


✨4.2 虚函数表

当基类定义了虚函数,基类对象就会将这些虚函数地址都存放到一个函数地址数组中(虚函数表),然后将这个数组的地址存放到对象的开始位置。

派生类对象的开始位置也会有一个虚表指针,指向一个独立的虚函数表,如果派生类重写(重新定义)了基类的虚函数,则会将重写的函数地址替换掉从基类继承的虚函数地址。

下面用代码演示类对象的虚函数表,甚至可以直接通过函数地址调用类的虚函数:

// g++ 23_Virtual_FunTable.cpp -std=c++11
#include <iostream>
using namespace std;

class CAnimal{
public:
	virtual void eat(){
		cout << "Animal eat" << endl;
	}
	virtual void run(){
		cout << "Animal run" << endl;
	}
private:
	char m_name[64];
};

class CDog : public CAnimal{
public:
	virtual void run() override{	// 重写,会使用新的虚函数地址
		cout << "Dog run" << endl;
	}
	virtual void jump(){
		cout << "Dog jump" << endl;
	}
private:
	int hair_color;
};
 
typedef void(*PFUN)(void);	// 定义函数指针类型
int main ()
{
	CAnimal animal;
	CDog dog;
	
	// 获取虚表指针,类对象第一个指针大小的内存里的值
	unsigned long* vptr_animal = (unsigned long*)(*((unsigned long*)(&animal)));
	unsigned long* vptr_dog = (unsigned long*)*(unsigned long*)&dog;
	
	// 打印虚函数表各个虚函数地址
	cout << "虚表指针:" << vptr_animal << endl;
	for(int i=0; i<2; i++)
	{
		cout << "animal虚函数表-第 " << i+1 << " 个虚函数地址:" << *vptr_animal << endl;
		PFUN pfun = (PFUN)*vptr_animal;
		pfun();	// 使用函数地址调用类函数
		vptr_animal++;
	}
	
	cout << endl;
	
	cout << "虚表指针:" << vptr_dog << endl;
	for(int i=0; i<3; i++)
	{
		cout << "dog虚函数表-第 " << i+1 << " 个虚函数地址:" << *vptr_dog << endl;
		PFUN pfun = (PFUN)*vptr_dog;
		pfun();	// 使用函数地址调用类函数
		vptr_dog++;
	}
}

运行结果如下:
在这里插入图片描述
可以结合下图去理解代码:

在这里插入图片描述


在这里插入图片描述

🎄五、总结

👉本文介绍了C++的虚函数,包括虚函数的定义、重写,动态绑定、虚函数表、虚表指针等内容。

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

参考:
《C++ primer plus》
《C++ primer》
C++虚函数详解

  • 29
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 25
    评论
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wkd_007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值