抽象类相关
抽象类的定义
- 抽象类 是一种特殊的类,它不能被实例化,只能作为基类来派生出具体类。
- 抽象类至少包含一个纯虚函数 。纯虚函数是在函数原型前加上
= 0
的虚函数,表示该函数没有具体实现,必须由派生类来实现。
抽象类的作用
- 提供统一接口 :抽象类定义了一组接口规范,要求所有派生类都实现这些接口。这样可以确保不同类型的对象具有相同的调用方式,使得代码更加通用和灵活。
- 避免重复代码 :通过在抽象类中定义通用的成员函数,派生类可以继承这些函数,从而避免重复实现相同的逻辑。
- 支持多态 :抽象类和纯虚函数是实现多态的关键。通过基类指针或引用,可以调用派生类的具体实现,使得程序在运行时能够根据对象的实际类型执行相应的操作。
抽象类的原理
- 纯虚函数的实现 :在
C++
中,纯虚函数是通过在编译时标记为未实现的函数来实现的。编译器会确保所有派生类都必须实现这些纯虚函数,否则派生类也会成为抽象类。 - 动态绑定 :当通过基类指针或引用来调用虚函数时,编译器会生成代码来动态决定调用哪个派生类的函数实现。这是通过虚函数表(
vtable
)和虚表指针(vpointer
)来实现的。 - 虚函数表(
vtable
) :每个类都有一个虚函数表,它是一个包含类中所有虚函数地址的数组。当创建一个派生类的对象时,其虚表指针会指向派生类的虚函数表。 - 虚表指针(
vpointer
) :每个包含虚函数的类的对象都有一个隐藏的指针,称为虚表指针,它指向该类的虚函数表。当通过基类指针调用虚函数时,编译器会通过虚表指针查找并调用相应的函数实现。
示例代码
#include <iostream>
using namespace std;class Abstract
{
public:virtual void display() = 0; // pure virtual functionAbstract(){cout << "Constructor of Abstract class" << endl;}virtual ~Abstract(){cout << "Destructor of Abstract class" << endl;}
};class Derived : public Abstract
{
public:void display(){cout << "Display function of Derived class" << endl;}Derived(){ cout << "Constructor of Derived class" << endl;}~Derived(){cout << "Destructor of Derived class" << endl;}
};void call(Abstract *obj)
{obj->display();
}int main()
{Derived d;call(&d);return 0;
}
为什么构造函数不能被声明为虚函数?
构造函数是给对象分配内存和初始化成员变量的,编译器要提前知道调用哪个构造函数,才能准确地分配内存和初始化。
就好比造汽车,必须先确定用哪个模具(构造函数),才能准备好材料和工具(分配内存)并开始组装(初始化成员变量)。
构造函数不是用来实现运行时多态的,它是为了确保对象在创建时就处于一个有效的初始状态。虚函数机制用于支持运行时多态,是通过虚函数表(vtable
)和虚表指针(vpointer
)实现的。
在C++
中,创建派生类对象时,基类构造函数先执行,接着才是派生类构造函数。假若构造函数被设为虚函数,那在基类构造函数运行时,派生类构造函数还没执行,对象的动态类型尚未确定,编译器就无法知晓该调用哪个构造函数。
另外,虚函数表的初始化是在对象构造过程中完成的。在构造函数执行前,虚函数表指针(vpointer
)尚未初始化。若构造函数是虚函数,调用时虚函数表指针未设置好,就无法确定具体调用哪个构造函数。所以,构造函数不能是虚函数。
正是因为构造函数的调用必须在编译时确定,而虚函数机制依赖于运行时的动态绑定,所以构造函数不能是虚函数。这样的设计确保了对象能够正确初始化,并且构造过程是可预测的。
虚函数表(vtable
)的初始化主要发生在哪些阶段?
1. 编译阶段,为每个包含虚函数的类生成虚函数表
虚函数表是一个包含该类所有虚函数地址的数组。
编译器会根据类的定义和继承关系来构建这个表。
对于派生类,其虚函数表会包含基类的虚函数和派生类自己的虚函数,如果派生类重写了基类的虚函数,则虚函数表中将使用派生类的实现覆盖基类的实现。
2. 对象构造阶段,为每个创建的对象分配虚表指针(vpointer
)
虚表指针(vpointer
)指向该对象对应的虚函数表。
这个指针的初始化发生在对象的构造函数执行之前。
具体来说,基类构造函数首先执行,在基类构造函数中,对象的虚表指针被初始化为基类的虚函数表。当派生类构造函数执行时,虚表指针会被更新为派生类的虚函数表。
3. 动态绑定阶段,通过虚表指针查找对应函数
在对象的生命周期中,虚表指针始终指向该对象对应的虚函数表。
当调用虚函数时,编译器会通过虚表指针查找并调用相应的函数。
如果对象的类型在运行时发生变化(例如,通过多态),虚表指针会指向不同的虚函数表,从而实现动态绑定。
4. 总结
虚函数表的初始化主要在编译阶段和对象构造阶段完成。
编译器在编译时为每个类生成虚函数表,在对象构造时为对象的虚表指针赋值。
虚函数表的初始化确保了在调用虚函数时能够正确地找到对应的函数实现,支持运行时的多态行为。
纯虚函数和虚函数的区别
纯虚函数 :在函数声明后面加上 = 0
,表示该函数没有具体实现,必须由派生类来实现。包含纯虚函数的类是抽象类,不能被实例化。
虚函数 :普通的虚函数有具体实现,可以在基类中定义,派生类可以重写也可以不重写。如果派生类不重写,将继承基类的实现。
两者都用于实现运行时多态性,但纯虚函数强制派生类必须提供实现,而虚函数允许派生类选择是否重写。
包含纯虚函数的类是抽象类,不能实例化;而包含虚函数的类可以是具体类,可以实例化。
调用 call(&d)
时,对象的创建和销毁过程
1. 对象创建过程
当执行 Derived d;
时:
- 分配内存 :为
Derived
类对象d
分配内存,包括基类Abstract
的成员和派生类Derived
的成员。 - 调用基类构造函数 :首先调用基类
Abstract
的构造函数Abstract()
,初始化基类的部分。 - 调用派生类构造函数 :然后调用派生类
Derived
的构造函数Derived()
,初始化派生类的部分。
执行顺序如下:
Constructor of Abstract class
Constructor of Derived class
2. 调用 call(&d)
当执行 call(&d);
时:
- 传递指针 :将
d
的地址传递给call
函数,函数参数obj
是一个指向Abstract
类的指针。 - 动态绑定 :在
call
函数内部,通过obj->display();
调用display()
函数。由于display()
是纯虚函数,编译器会根据obj
实际指向的对象类型(这里是Derived
类),动态绑定到Derived
类的display()
实现。 - 执行派生类函数 :调用
Derived
类的display()
函数,输出Display function of Derived class
。
3. 对象销毁过程
当 main
函数结束时,对象 d
的生命周期结束,执行以下步骤:
- 调用派生类析构函数 :首先调用派生类
Derived
的析构函数~Derived()
。 - 调用基类析构函数 :然后调用基类
Abstract
的析构函数~Abstract()
。
执行顺序如下:
Destructor of Derived class
Destructor of Abstract class
虚析构函数的定义
析构函数是用来在对象生命周期结束时释放资源的特殊成员函数。
当基类的析构函数被声明为虚函数时,它就被称为虚析构函数。
虚析构函数的作用
在C++
中,当你用基类指针去删除派生类对象时,如果没有虚析构函数,就只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类中分配的资源(比如内存、文件句柄等)没有被正确释放,从而造成内存泄漏或其他资源未释放的问题。
虚析构函数通过动态绑定机制,确保在这种情况下,程序会正确调用派生类的析构函数,然后再调用基类的析构函数。这样就保证了所有相关的资源都能被正确释放。
虚析构函数通过动态绑定机制确保在通过基类指针删除派生类对象时,会正确调用派生类的析构函数。
推荐一下
https://github.com/0voice