C++虚函数小记

本文介绍了C++中的虚函数,包括虚函数的概念、纯虚函数的应用,以及虚函数的底层实现机制,如虚表、虚表指针和虚函数调用过程。通过实例解析了如何利用虚函数实现多态性,并探讨了虚函数表的构造和虚函数调用的细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、虚函数入门–“小明打猎”

1、什么是虚函数?

小明有一天去打猎,出门前正在思考带上什么装备去打猎。
假设我们在基类中定义了一种方法,那就是小明的“手”,随后在派生类中分别定义了三种打猎方法:石头、枪。
显然小明想带上枪去打猎,他调用了派生类中使用枪的方法(注意,此时他没有在基类中加Virtual关键字),于是乎,他调用后发现自己的手上并没有枪,小明非常的苦恼。最终他在基类调用方法前面加上了Virtual关键字,再次调用后,发现自己成功拿到了枪。

class Hunt {
public:
    Hunt(int a = 0, int b = 0)
    {

    }
    int measure() //这个函数前添加Virtual关键字,便可以实现子类的访问
    {
        cout << "I will use my hand" << endl;
        return 0;
    }
};
class Stick : public Hunt {
public:
	Stick(int a = 0, int b = 0) :Hunt(a, b) { }
    int measure()
    {
        cout << "I will use stick" << endl;
    }
};

class Gun : public Hunt {
public:
    Gun(int a = 0, int b = 0) :Hunt(a, b) { }
    int measure()
    {
        cout << "I will use Gun" << endl;
    }
};
int main()
{
    Hunt* hunt;
    Gun  gun;

    hunt = &gun;
    hunt->measure();
    return 0;
}

上述例子中,小明刚开始的失败是由于他并没有给出实现多态的方法,上述程序在没加Virtual之前之所以会报出“I will use my hand”的原因是,在调用打猎方法之前,基类中的方法函数已经被编译器准备好了。也就是所谓的静态链接。
由此我们引出了虚函数的概念,虚函数就是为了实现多态性(另一种方法是函数重载或者运算符重载),用Virtual关键字对成员函数进行修饰,这就是虚函数。

2、纯虚函数

接着上一个打猎的问题,小明准备好枪后,又在基类中写了一个打猎目标的函数,需要写出这些动物的所在地、出没时间等等,但是小明突然脑子短路,突然想不起来具体信息了,所以他想了一个好的办法。将基类中的虚函数后面直接赋值0,把具体的打猎信息在派生类中更好的实现。

class Hunt {
public:
	Hunt(int a = 0, int b = 0){
    }
	virtual int Animal() = 0;//纯虚函数
};

这便是纯虚函数,在基类中定义虚函数,为了在派生类中重新定义该函数更好地适用于对象,但是在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

二、虚函数底层实现

1、虚表&虚表指针

了解了虚函数之后,我们不禁会想,虚函数的底层原理是怎么实现的呢,编译器处理虚函数的方法是,为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(Virtual Function Table, vtbl),每个类使用一个虚函数表,每个类对象用一个虚表指针。
我们对一个类写入虚函数可以发现,类的大小增加了4,并且无论增加几个虚函数,大小始终增加4,这个大小便是一个指针的大小,也就是所谓的虚表指针。虚表就是保存虚函数地址的一个数组。下面来看一个实验:

#include <iostream>
#include <windows.h>
using namespace std;
class A {
	int k;
};
class B {
public:
	int m;
	virtual void print1()
	{
		cout << "bprint1\n";
	}
	virtual void print2()
	{
		cout << "bprint2\n";
	}
};
class C : public B {
public:
	int n;
	virtual void print2()
	{
		cout << "cprint2\n";
	}
};

int main()
{
	cout << "类A的大小 = " << sizeof(A) << endl;
	cout << "类B的大小 = " << sizeof(B) << endl;
	cout << "类C的大小 = " << sizeof(C) << endl;
	
	B* b = new C; //实例化
	b->print2();
	printf("C的虚表地址 0x%X\n", *(long*)b);
	free(b);
	b = new B;
	b->print2();
	printf("B的虚表地址 0x%X\n", *(long*)b);
	while (1);
}

计算三个类的大小我们会发现,类A只有一个k变量,大小为4;B有一个m变量,还有一个虚表指针,大小为8;C是B的继承,所以有m和n变量,还有一个虚表指针,所以大小是12。
在后续的输出虚表地址实验中,
在这里插入图片描述
在这里插入图片描述
可以看出他们的虚函数地址是一样的。并且可以知道,虚表指针在类对象起始地址最开始的4字节或者8字节处!
在这里插入图片描述

2、虚函数表构造过程

从编译器的角度来说,B的虚函数表很好构造,C的虚函数表构造过程相对复杂。下面给出了构造C的虚函数表的一种方式:
1)如上图所示,首先拷贝基类B的虚表;
2)替换已重写的虚函数指针,也就是子类实现多态的虚函数指针;
3)追加子类自己的虚函数指针,完成构造。

3、虚函数的调用过程

我们在上述例子中调用了b->print2();能够很好的实现功能的原因是,我们声明了b是B类的指针,但是到目前为止,编译器只知道b是B类型的指针,并不知道它指向的具体对象类型。所以我们让他指向了C类,此时就可以完成C类中函数的调用。
要注意的是,在B类和C类的虚函数表中,print2函数在其表中的相对偏移是一致的,所以如果b指向B的对象,可以获取到B对象的vptr,加上偏移值8((char
)vptr + 8),可以找到B::print2。如果b指向C的对象,可以获取到C对象的vptr,加上偏移值8((char*)vptr + 8) ,可以找到C::print2。
另外,虚函数指针虚函数成员指针ptr部分内容为虚函数对应的函数指针在虚函数表中的偏移地址加1(之所以加1是为了用0表示空指针),而adj部分为调节this指针的偏移字节数。所以C::print2是一个虚函数指针,他的ptr部分是9(8+1).
在这里插入图片描述(图片来源于博客https://www.cnblogs.com/malecrab/p/5572119.html)
函数指针的size是普通指针的两倍,倘若在上述例子中调用C::print1;那么adj部分将会是4。具体参考上述链接博客中对指针的讲解。

三、参考文献

在撰写本文的过程中,参阅了不少网络资料和牛人博客,感激不尽。参考文章的链接:
https://www.cnblogs.com/Allen-rg/p/6927319.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

剑桥艺术生

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

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

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

打赏作者

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

抵扣说明:

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

余额充值