一、虚函数
(一)虚函数定义和用途
- 定义:虚函数,是指被virtual关键字修饰的成员函数。
- 用途:在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。
(二)重写
在派生类中对基类中的虚函数重新实现,返回类型不变、函数名不变、参数不变,重写也叫覆盖。
(三)协变
基类虚函数返回类型是基类的指针或者引用,派生类中重写的虚函数返回类型是派生类的指针或者引用,也就是返回类型依然是继承关系,依然可以构成多态。这种情况叫做协变。
(四)虚函数的原理
在基类中,通过virtual关键字修饰的函数叫做虚函数,具有虚函数的对象在储存空间的开始处会开辟一个指向虚函数表(vftable)的指针(_vfptr)。这个虚函数表顺序存放每个虚函数的指针,本质上就是一个指针数组。对_vfptr取值,得到数组首地址,再取值得到数组第一个元素,也就是第一个虚函数的地址。
-
void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 }; const void** __vfptr = __fun[0];
以下代码中存在强制类型转换和不同系统下指针变量所占空间的不同,且是难点,我结合自己理解总结如下:
- 指针变量在32位设备上占4个字节,在64位设备上占8个字节。(简单理解32位系统有32根线,每根线有0和1两种可能,共有2^32种可能,就是CPU单次提取数据最大量为4个字节(32个位),同理,64位设备就有2^64种可能,也就是8个字节);32位的地址范围就是:0——2^32-1,64位的地址范围就是:0——2^64-1。
- 指针变量占4个字节(以32位系统为例),刚好和一个整型变量所占空间一样,为了方便计算将指针变量强转为整形指针处理。此处我的理解是:在对象储存空间开始处取得4个字节的指针变量,这个指针变量就是指向虚函数表的指针。int *vftable=(int*)&base;当在64位系统上,需要将int换成long,c/c++标准库中通常用intptr_t来代表指针类型的变量。为了各系统的兼容,通常写作:intptr_t *vftable=(intptr_t*)&base。
- 对虚函数表的指针取值,就得到了虚函数表(指针数组)的地址;再次取值就得到了第一个元素的值,也就是第一个虚函数的地址。
#include<iostream>
using namespace std;
class Base {
private:
int num;
public:
Base() { num = 10; }
~Base(){}
virtual void virFunc1() { //基类虚函数1
cout << "Base中虚函数1" << endl;
}
virtual void virFunc2() {
cout << "Base中虚函数2" << endl; //基类虚函数2
}
virtual void virFunc3() {
cout << "Base中虚函数3" << endl; //基类虚函数3
}
};
class Son :public Base {
public:
virtual void virFunc2() { //派生类重写虚函数2
cout << "Son中虚函数2" << endl;
}
};
void test01() {
Base base;
intptr_t *vfptr = (intptr_t*)&base; //在对象储存空间开始处取出一个指针变量,这个指针指向虚函数表(也就是存放虚函数地址的数组)
intptr_t *vftable = (intptr_t*)*vfptr; //对虚函数表的指针取值,取得虚函数表,可理解为数组名
void(*func)() = (void(*)())*vftable; //对虚函数表的数组名取值,得到第一个元素,也就是第一个虚函数的地址
func();
func = (void(*)())*(vftable+1); //数组名+1就是第二个元素的地址,
func();
}
int main(int argc, char const **argv) {
test01();
return 0;
}
二、纯虚函数
纯虚函数:一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
纯虚函数也可以叫抽象函数,一般来说它只有函数名、参数和返回值类型,不需要函数体。这意味着它没有函数的实现,需要让派生类去实现。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
抽象类:含有纯虚函数的类叫做抽象类,抽象类不可实例化。在派生类中必须重写基类的纯虚函数,否则派生类也是抽象类,无法实例化。例如:
class Base{
public:
virtual void func()=0;
}
class Son:public Base{
public:
void func(){
cout<<"对基类纯虚函数重写"<<endl;
}
}
此时Base类为抽象类,无法实例化,Son类通过对纯虚函数重写,可以实例化。
三、虚析构
当派生类中包含指向堆中空间的指针时,因派生类未不继承基类构造和析构函数,需要使用虚析构函数实现用基类指针删除派生类释放派生类中空间的多态技术。当基类中析构函数不是虚函数时,利用基类指针删除派生类对象时不会调用派生类的析构函数,这就造成了派生类内部new来的内存泄漏。例如:
#include<iostream>
using namespace std;
class Base {
public:
Base() { cout << "基类的构造函数" << endl; }
virtual ~Base() { cout << "基类的析构函数" << endl; }
};
class Derived :public Base {
public:
int *m_Num;
Derived() { m_Num = new int(10); cout << "派生类的构造函数" << endl; }
~Derived() { cout << "派生类的析构函数" << endl; delete m_Num; }
};
void test01() {
Base *ptr = new Derived();
delete ptr;
}
int main(int argc, char const **argv) {
test01();
return 0;
}
运行结果:
- 基类的构造函数
- 派生类的构造函数
- 派生类的析构函数
- 基类的析构函数
四、纯虚析构
在某些类里声明纯虚析构函数是非常方便的,声明了纯虚函数的类也叫做抽象类,一般作为基类使用。基类中的析构函数一般需要声明为虚函数,用于方便调用每层派生类的析构函数,避免造成内存泄漏。那么在类中声明一个纯虚析构函数构造一个抽象类作为基类最为方便。
纯虚析构函数在类内声明,必须在类外定义(可以是空函数)。因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要调用析构函数,所以要保证为它提供函数体。例如:
#include<iostream>
using namespace std;
#include<string>
class Base {
public:
virtual void print() { cout << "基类构造函数" << endl; };//测试抽象类是否实例化
virtual ~Base() = 0;
};
Base::~Base() { cout << "基类析构函数" << endl; } //测试抽象类是否调用析构函数
class Derived :public Base{
public:
int *ptr_Age;
string *ptr_Name;
Derived(int age, string name) { //初始化派生类中的指针成员属性
ptr_Age = new int(age);
ptr_Name = new string(name);
cout << "派生类构造函数" << endl; //测试派生类构造函数调用时机
}
~Derived() { //释放派生类成员中申请的内存
delete ptr_Age;
delete ptr_Name;
cout << "派生类析构函数" << endl; //测试派生类析构函数调用时机
}
void print() {
cout << "MyName:" << *ptr_Name << endl;
cout << "MyAge:" << *ptr_Age << endl;
}
};
void test01() {
Base *ptrBase = new Derived(20,"zhangsan");
ptrBase->print();
delete ptrBase;
}
int main(int argc, char const **argv) {
test01();
return 0;
}
运行结果:
- 派生类构造函数
- MyName:zhangsan
- MyAge:20
- 派生类析构函数
- 基类析构函数