一.虚函数
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。
(一)虚表和虚基表指针
要理解这个问题,我们要引出虚表和虚基表
虚表:虚函数表的缩写,类中含有 virtual 关键字修饰的方法时,编译器会自动生成虚表,它是在编译器确定的
虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针,它是在构造函数中被初始化的
(二)纯虚函数
纯虚函数(Pure Virtual Function)是C++中的一个特殊类型的虚拟函数,它在基类中声明但没有定义。纯虚函数的声明使用virtual
关键字,并在函数声明的末尾添加= 0
来表示它是一个纯虚函数。子类(派生类)必须提供纯虚函数的实际实现,否则子类也会被标记为抽象类,无法创建对象。
class Shape {
public:
// 声明纯虚函数
virtual void draw() = 0;
// 普通成员函数
void displayInfo() {
// 这里可以包含一些通用的代码
std::cout << "This is a shape." << std::endl;
}
};
class Circle : public Shape {
public:
// 子类必须提供纯虚函数的实现
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};
class Square : public Shape {
public:
// 子类必须提供纯虚函数的实现
void draw() override {
std::cout << "Drawing a square." << std::endl;
}
};
int main() {
Circle circle;
Square square;
circle.displayInfo(); // 调用基类函数
circle.draw(); // 调用派生类函数
square.displayInfo(); // 调用基类函数
square.draw(); // 调用派生类函数
return 0;
}
在上述示例中,Shape
类包含一个纯虚函数draw()
,因此Shape
类本身是一个抽象类,不能创建它的对象。然后,Circle
和Square
类都继承自Shape
类,并必须提供对draw()
的实际实现。这种机制允许多态性(Polymorphism)的实现,允许不同的派生类以不同的方式实现相同的虚拟函数。
二.多态的实现
根据上图举例分析:
#include<iostream>
#include<vector>
using namespace std;
class A {
public:
virtual void prints() {
cout << "A::prints" << endl;
}
A() {
cout << "A:构造函数" << endl;
}
};
class B:public A {
public:
virtual void prints() {
cout << "B::prints" << endl;
}
B() {
cout << "B:构造函数" << endl;
}
};
class C :public A {
public:
};
int main() {
A *b = new B();
b->prints();
b = new C();
b->prints();
return 0;
}
子类B重写了基类A的虚函数,子类C并没有重写,从结果分析,依然是体现了多态性???
下面阐述实现多态的过程:
1.编译器在发现基类中含有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址
2.编译器会在每个对象的前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表
3.所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表指针进行初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表
4.当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面。
所以,指向派生类的基类指针在运行时,就可以根据派生类对虚函数的重写情况动态的进行调用,从而实现多态性。
三.为什么析构函数一般写成虚函数?
由于类的多态性,通常通过父类指针或引用来操作子类对象。因为多套允许我们以统一的方式处理不同的派生类对象,并且在运行时确定要调用的方法。
如果析构函数不被声明为虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样会造成派生类析构不完全,造成内存泄漏。
这种行为是为了确保资源的正确释放。由于我们只知道父类的类型,编译器无法确定指针指向的是哪个子类对象,因此只能调用父类的析构函数来释放资源。
没有虚析构:
#include<iostream>
#include<vector>
using namespace std;
class A {
public:
virtual void prints() {
cout << "A::prints" << endl;
}
A() {
cout << "A:构造函数" << endl;
}
virtual ~A() {
cout << "A:析构函数 " << endl;
}
};
class B:public A {
public:
virtual void prints() {
cout << "B::prints" << endl;
}
B() {
cout << "B:构造函数" << endl;
}
~B() {
cout << "B:析构函数 " << endl;
}
};
int main() {
A *b = new B();
b->prints();
delete b;
b = NULL;
return 0;
}
虚析构:
#include<iostream>
#include<vector>
using namespace std;
class A {
public:
virtual void prints() {
cout << "A::prints" << endl;
}
A() {
cout << "A:构造函数" << endl;
}
virtual ~A() {
cout << "A:析构函数 " << endl;
}
};
class B:public A {
public:
virtual void prints() {
cout << "B::prints" << endl;
}
B() {
cout << "B:构造函数" << endl;
}
~B() {
cout << "B:析构函数 " << endl;
}
};
int main() {
A *b = new B();
b->prints();
delete b;
b = NULL;
return 0;
}
分析:可以看到析构函数是,先从子类析构,再到父类析构