虚函数定义与作用:
virtual关键字声明虚函数,虚函数可被派生类override(保证返回类型与参数列表,名字均相同),从而通过基类指针调用时,实现多态的功能
virtual关键字:
将函数声明为虚函数
override关键字:
告诉编译器该函数为重写的虚函数,若重写失败,报错,防止出现疏忽导致虚函数未重写的情况
final关键字:
声明给虚函数时,表明该虚函数不可再被派生类重写
声明给类时,表明该类不可再被继承
class Base{
public:virtual void print() const{cout << "Base" << endl;}
};class Derived final : public Base{//注意此处final的位置
public:void print() const override final{ //此处const override final的顺序不可调换cout << "Derived" << endl;}
};
指针的动态类型与静态类型:
指针/引用的定义类型为其静态类型
指针/引用指向的对象类型为其动态类型
当调用非虚函数时,函数的匹配取决于指针/引用的静态类型
当调用虚函数时,函数的匹配取决于指针/引用的动态类型
例子:
class Base{
public:virtual void print(){cout << "Base" << endl;}
};class Derived:public Base{
public:void print()override{cout << "Derived" << endl;}
};int main(){Base *p_base{new Derived};p_base->print(); //调用Derived::print()Base obj_base = *p_base;obj_base.print();//调用Base::print()return 0;
}
输出:
Derived
Base
特殊的虚函数重写:协变返回类型
当虚函数的返回类型为一系列的基类/派生类的指针/引用时,重写的虚函数返回类型可以不一样
例子:
class A{
public:void print(){cout << "A" << endl;}
};
class B:public A{
public:void print(){cout << "B" << endl;}
};class Base{
public:virtual A& get(){cout << "return A" << endl;return *(new A{});}
};class Derived:public Base{
public:B& get() override{cout << "return B" << endl;return *(new B{});}
};int main(){Base *p_base{new Derived};p_base->get();Base obj_base = *p_base;obj_base.get();return 0;
}
输出:
return B
return A
综合的虚函数调用例子:
#include <iostream>
using namespace std;class A{
public:void print(){cout << "A" << endl;}virtual void vprint(){cout << "A" << endl;}
};
class B:public A{
public:void print(){cout << "B" << endl;}void vprint()override{cout << "B" << endl;}
};class C{
private:A m_a{};
public:virtual A& get(){return m_a;}
};class D:public C{
private:B m_b{};
public:B& get() override{return m_b;}
};int main(){C *p_C{new D{}};p_C->get().print(); //静态调用print(),所以调用的是p_C的静态类型对应的get(),返回A&p_C->get().vprint(); //动态调用vprint(), 所以调用的是p_C的动态类型对应的get(),返回B&return 0;
}
输出:
A
B
虚析构函数:
在类的析构函数前加上virtual关键字,可将其变为虚析构函数,此后的派生类写自己的析构函数时,相当于重写基类的虚函数,派生类的析构函数默认成为虚函数
如果一个类会被继承的话,那么应当将其析构函数写成虚析构函数,以避免内存泄漏
不使用虚析构函数的继承:
class Base{
public:~Base(){cout << "~Base()" << endl;}
};class Derived:public Base{
public:~Derived(){cout << "~Derived()" << endl;}
};int main(){Base* p{new Derived{}};delete p;return 0;
}
输出:
~Base()
可以看到delete只调用了Base的析构函数,从而导致Derived部分分配的内存未被清空,发生内存泄漏
使用虚析构函数的继承:
class Base{
public:virtual ~Base(){cout << "~Base()" << endl;}
};class Derived:public Base{
public:~Derived() override{cout << "~Derived()" << endl;}
};int main(){Base* p{new Derived{}};delete p;return 0;
}
输出:
~Derived()
~Base()
指针正确调用了Derived的虚析构函数,而该析构函数又调用了~Base(),从而正确的清空了分配的内存
纯虚函数:
在虚函数声明后面加上=0,使其成为纯虚函数
class A{
public:virtual void func() = 0;void func2(){}
};
纯虚函数也可以有定义,如写在类外面的定义:
void A::func(){cout << "I'm a pure virtual function" << endl;
}
抽象类:
只要包含纯虚函数的类就称为抽象类(如上述的A类),抽象类不可被实例化
继承自抽象类的派生类需要重写其所有纯虚函数,否则该派生类也是抽象类
接口类:
不包含任何属性和成员函数,只包含纯虚函数的类称为接口类
利用虚函数修改派生类的operator<<
为了使用ostream,我们通常将operator<<写成友元函数,但友元函数不能是虚函数,因此无法被派生类重写
但我们又不想每定义一个派生类就新添加一个友元operator<<
因此,我们可以定义一个辅助print()虚函数,然后用operator<<来调用这个虚函数,从而达到多态的目的
class Base{
public:virtual ostream& print(ostream& out){out << "Base" << endl;}friend ostream& operator<<(ostream& out,Base& obj){return obj.print(out);}
};class Derived:public Base{
public:ostream& print(ostream& out) override{out << "Derived" << endl;}
};int main(){Derived d{};cout << d << endl;return 0;
}
输出:
Derived
在cout<<d的时候,由于没有与Derived匹配的<<运算符,因此编译器将d隐式转换为Base,然后传入operator<<(ostream& out,Base& obj)里,从而通过Base&调用Derived对应的虚函数print(),实现多态的目的
虚函数的实现原理:
结构
当一个类内包含虚函数,那么编译器就会为这个类分配一个数组,数组里存了若干个指针(虚函数指针,vfptr),每个指针指向对应的虚函数的地址,我们把这个数组称作虚函数表(__vtable)。
当该类实例化为对象时,编译器会在该对象头部插入一个指针,该指针指向虚函数表,我们把这个指针称作虚函数表指针(__vptr),虚函数表指针的初始化在构造函数之前。
结构如图所示:
运行:
当我们通过类指针调用虚函数的时候,编译器并不会在编译期就根据函数签名来确定调用的函数的地址(静态绑定),而是在运行期让类指针通过__vptr找到vtable,并通过调用的函数签名,确定在vtable的偏移量,从而找到对应的vfptr,通过vfptr找到需要调用的函数的地址,进而调用该函数,此谓动态绑定
继承规则:
当我们发生继承、虚函数重写、添加虚函数时,
那么vtable以及__vptr的分配规则如下:
每个含有虚函数的基类的子对象的首地址都会有对应的__vptr
重写虚函数:修改对应__vptr所指向的vtable的对应位置(下标)内的vfptr,
新添虚函数: 在首个继承的基类的vtable后添加vfptr
例子:
当我们有如下代码的继承关系时:
class Base1{
public:virtual void func1(){}virtual void print1(){}void test1(){}
};class Base2{
public:virtual void func2(){}virtual void print2(){}void test2(){}
};class Derived:public Base1,public Base2{
public:void func1() override{}void print2() override{}virtual void f_derived(){}
};
Base1,Base2各有两个虚函数和一个非虚函数,Derived各重写了基类的一个虚函数,以及新添加了一个自己的虚函数
结构如图所示:
可以看到
在Derived obj内,
虚函数表内vfptr_func1_b和vfptr_print2_b对应的位置被替换成了vfptr_func1_d和vfptr_print2_d
新添加的f_derived对应的vfptr_f_d被添加到了__vptr1指向的vtable的末尾
值得注意的是
Derived的vtable和Base1,Base2的vtable是独立开来的,因此这里总共有三个虚函数表
如果Derived没有重写任何虚函数,其仍会生成一个独立的vtable
多态实现原理:
当我们用基类指针指向不同的派生类,调用其相同的虚函数时,若其虚函数被重写过,则会产生不同的效果,此谓多态
那么多态是如何实现的呢:
用上述的Base1,Base2,Derived举例
调用重写的虚函数:
比如我们现在有
Base2* p{new Derived{}};
p->print2();
由于我们使用了Base2指针指向Derived对象,那么该指针会指向Derived中的Base2子对象的首地址,也就是__vptr2所在处,当我们调用print2()时,指针会通过__vptr2找到vtable,然后由print2()的函数签名,编译器会让指针偏移一个指针偏移量,从而找到vfptr_print2_d,进而调用Derived::print2(),这也就是为什么要修改对应位置的vfptr的原因,因为vtable就是根据位置来找对应的虚函数的
调用新添的虚函数:
当我们使用Base1指针指向Derived对象时,也是同样的道理:
Base1* p2{new Derived{}};
p2->f_derived();
当我们调用f_derived()时,编译器是用__vptr1去找该函数的,这也就是为什么vfptr_f_d会加到__vptr1的末尾的原因,也就是说Derived与其Base1的子对象是共用一个vtable的,也因此,p2无法调用在Base2定义的虚函数
指向不同对象时的调用:
当我们用Base1指针指向Base1对象时,其__vptr指向的是Base1的vtable(图右上角),因此调用时只会调用Base1定义的虚函数
总结:
多态的根本原理是不同类的__vtpr指向的vtable不同,从而在运行时索引的时候找到不同的函数并运行
this指针调整:
编译器在运行时动态调整this指针的功能,具体实现原理这里不再赘述
例子1:
在上述例子中,我们再加入一个派生类:
class Derived2:public Derived{
public:
};int main(){Derived* p{new Derived2{}};p->print2();return 0;
}
上述代码肯定是可运行的,但按上述运行原理,Derived*所用的__vptr是不可能找到print2()的,也就是说其使用了__vptr2,那么编译器是怎么实现的呢?
在这里,编译器用到了一种叫做this指针调整的功能,在调用虚函数表指针之前,编译器先让p向下偏移到了Base2子对象首地址的位置,进而使用__vptr2正确调用print2()
例子2:
若Derived有虚析构函数,那么该虚析构函数只会放在__vptr1所指向的vtable内,此时,若用Base2指针指向Derived对象,在调用虚析构函数时,编译器会先将this指针偏移到Base1子对象的首地址处,进而使用__vptr1正确地调用虚析构函数