持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天点击查看活动详情
虚函数
虚函数的作用
1.方便继承 2.(动态联编)可以通过基类指针或者引用指向子类 来调用子类中的函数
实现原理
编译器会为每个包含虚函数的类创建一个虚表(vtable) 编译器会为每一个包含虚函数类的对象创建一个虚表指针(vptr)指向虚函数表
虚表(vtable)
虚表是一个一维数组,在这个数组中存放每个虚函数的地址 每个类可以有很多个对象 每个对象都有一个虚表指针vptr 但是一定只有一张虚函数表 在编译时虚表就已经确定了 在只读端不能更改
虚表指针(vptr)
在创建子类对象的时候会先调用父类的构造函数 此时会创建一个子类的虚表指针 但是此时的虚表指针是指向父类的虚表 然后再调用子类的构造函数 此时子类的虚表指针才是指向子类的虚表 虚表指针指向了虚表 虚表指针的32字节的大小是4 所以有虚函数的类都会多一个虚表指针 4字节 在程序运行时根据对象类型去初始化虚表指针 这样虚表指针就能正确指向虚表中存储的函数 例如创建了一个父类指针 将该指针初始化指向子类对象 则该指针指向对象的虚表指针就指向子类对象的虚表
纯虚函数(抽象函数)
语法
virtual 返回值类型 函数名 (参数列表) = 0 例如 virtual void func() = 0;
纯虚函数有什么用
一般就是不实现他 单纯的方便子类继承
抽象类
什么是抽象类
当类中有了纯虚函数(也包括纯虚析构函数) 这个类也称为抽象类
抽象类特点
无法实例化对象 子类必须重写抽象类中的纯虚函数 否则也属于抽象类
虚析构和纯虚析构
虚析构和纯虚析构共性
1.可以解决父类指针释放子类对象(多态使用时 如果子类中有属性开辟到堆区 那么父类指针在释放时无法调用到子类的析构代码) 2.都需要有具体的函数实现
虚析构和纯虚析构区别
如果时纯虚析构 该类属于抽象类 无法实例化对象
虚析构语法
virtual ~类名(){}
纯虚析构语法
virtual ~类名() = 0;
用法
虚析构
只需要父类的析构函数加上virtual 父类指针在被释放时 子类析构函数就能被调用了
class A
{
public:
int a;
virtual ~A()
{
cout << "A out" << endl;
}
};
class B :public A
{
public:
int b;
virtual ~B()
{
cout << "B out" << endl;
}
};
void main()
{
A *a = new B;
delete(a);
}
若不加virtual 则
使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数为什么不是虚函数
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存
构造函数为什么不能为虚函数
1.如果A类的构造函数是虚函数 B类是A类的派生类 此时如果创建 B b = new B 就会发现直接调用了B类的构造函数 没有调用A类的构造函数(动态联编) 但是正常情况应该是在创建b时 先调用A类构造函数 再调用B类构造函数 2.如果构造函数时虚函数的话 由于是要确定该对象类型之后才能确定虚函数指针的指向 但是在调用构造函数之前是没办法确定这个对象的类型 所以就会很冲突 你需要执行构造函数才能确定虚函数指针 而需要虚函数指针才能调用构造函数
析构函数为什么尽量不要调用虚函数
在析构的时候会首先调用子类的析构函数,析构掉对象中的子类部分,然后在调用基类的析构函数析构基类部分,如果在基类的析构函数里面调用虚函数,会导致其调用已经析构了的子类对象里面的函数,这是非常危险的。
纯虚析构
不仅要声明还要实现 也就是在父类中需要virtual ~类名() = 0; 在类外还需要再实现一下 要不然会报错 在类外只需要 ~类名::类名(){} 因为在类外 所以要作用域
虚函数多继承
当类A和类B都有虚函数 且类C同时继承了这两个类时 有多少个基类就有多少个虚函数表指针,前提是基类要有虚函数才算上这个基类 1.子类虚函数会覆盖每一个父类的每一个同名虚函数。 2.父类中没有的虚函数而子类有,填入第一个虚函数表中,且用父类指针是不能调用。 3.父类中有的虚函数而子类没有,则不覆盖。仅子类和该父类指针能调用。 这篇很详细