织梦网站装修公司源码,东莞企业网站建设费用,重庆seo博客,优秀高端网站建设全文目录 虚函数虚函数的重写接口继承和实现继承重载、重写#xff08;覆盖#xff09;、隐藏#xff08;重定义#xff09;C11 override 和 final抽象类 多态的概念多态原理虚函数表 单继承和多继承的虚函数表打印虚函数表单继承的虚函数表多继承的虚函数表 常见面试问答题… 全文目录 虚函数虚函数的重写接口继承和实现继承重载、重写覆盖、隐藏重定义C11 override 和 final抽象类 多态的概念多态原理虚函数表 单继承和多继承的虚函数表打印虚函数表单继承的虚函数表多继承的虚函数表 常见面试问答题 虚函数
通过virtual修饰的成员函数
class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl;}
};虚函数的重写
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。
如果构成重写子类可以不加virtual。
class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout 买票-半价 endl; }/*注意在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议
这样使用*/
/*void BuyTicket() { cout 买票-半价 endl; }*/};特例
协变
派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。
// f()依旧构成重写
class Person
{
public:virtual A* f() {return new A;}
};class Student : public Person
{
public:virtual B* f() {return new B;}
};析构函数的重写
如果基类的析构函数为虚函数只要子类的析构函数定义就构成重写。因为析构函数编译器会统一特殊处理成destructor()天然构成重写条件。
// 析构函数底层是同名函数天然构成重写
class Person
{
public:virtual ~Person() {cout ~Person() endl;}
};class Student : public Person
{
public:virtual ~Student() { cout ~Student() endl; }
};在继承体系中建议将析构函数定义成虚函数保证析构函数的正确调用父类指针指向父类对象调用父类析构父类指针指向子类对象调用子类析构
Person* p1 new Person;
Person* p2 new Student;
delete p1;
delete p2; // 如果重写虚构函数会只调用父类的析构导致子类资源没有释放接口继承和实现继承
普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数普通函数继承是一种实现继承。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口也就是说重写的函数的接口是父类的接口参数名缺省参数按照父类为准目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。
重载、重写覆盖、隐藏重定义 C11 override 和 final
final修饰虚函数表示该虚函数不能再被重写
virtual void Drive() final {}override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。
class Car{
public:virtual void Drive(){}
};class Benz :public Car {
public:virtual void Drive() override {cout Benz-舒适 endl;}
};抽象类
在虚函数的后面写上 0 则这个函数为纯虚函数。
class Car
{
public:virtual void Drive() 0;
};包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。
多态的概念
计算机程序运行时相同的消息可能会送给多个不同的类别之对象而系统可依据对象所属类别引发对应类别的方法而有不同的行为。简单来说所谓多态意指相同的消息给予不同的对象会引发不同的动作。 在C继承体系中多态构成有两个条件
必须通过基类的指针或者引用调用虚函数被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 多态原理
虚函数表
先看代码
class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数 的地址要被放到虚函数表中虚函数表也简称虚表。 深入研究改造一下代码
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:virtual void Func1(){cout Base::Func1() endl;}virtual void Func2(){cout Base::Func2() endl;}void Func3(){cout Base::Func3() endl;}
private:int _b 1;
};class Derive : public Base
{
public:virtual void Func1(){cout Derive::Func1() endl;}
private:int _d 2;
};int main()
{Base b;Derive d;return 0;
}同样的通过调试观察对象 总结
基类b对象和派生类d对象虚表是不一样的这里我们发现 Func1完成了重写所以d的虚表中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。只有虚函数是才能进入虚表。虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr派生类虚表的生成 先将基类中的虚表内容拷贝一份到派生类虚表中如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 虚表存放在对象中虚函数存放在代码断中。 当构成多态时父类指针或引用指向父类对象时在父类的虚表中找函数地址父类指针或引用指向子类对象时在子类的虚表中找函数地址。 单继承和多继承的虚函数表
打印虚函数表
通过将对象地址强转int*类型后解引用取到虚表的地址根据虚表后面的nullptr判断结束
typedef void(*VFPTR)(); // void ()类型的函数指针void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout 虚表地址 vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}cout endl;
}int main()
{Base b;Derive d;// 思路取出b、d对象的头4bytes就是虚表的指针前面我们说了虚函// 数表本质是一个存虚函数指针的指针数组这个数组最后面放了一// 个nullptr// 1.先取b的地址强转成一个int*的指针// 2.再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表// 的指针// 3.再强转成VFPTR*因为虚表就是一个存VFPTR类型(虚函数指针类型)// 的数组。// 4.虚表指针传递给PrintVTable进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃因为编译器有时对虚// 表的处理不干净虚表最后面没有放nullptr导致越界这是编译器// 的问题。我们只需要点目录栏的-生成-清理解决方案再编译即可。VFPTR* vTableb (VFPTR*)(*(int*)b);PrintVTable(vTableb);VFPTR* vTabled (VFPTR*)(*(int*)d);PrintVTable(vTabled);return 0;
}单继承的虚函数表
class Base
{
public :virtual void func1() { coutBase::func1 endl;}virtual void func2() {coutBase::func2 endl;}
private :int a;
};class Derive :public Base
{
public :virtual void func1() {coutDerive::func1 endl;}virtual void func3() {coutDerive::func3 endl;}virtual void func4() {coutDerive::func4 endl;}
private :int b;
};其对象的虚表 在监视窗口看不到fun3() 和 fun4()可能是vs的小bug但是通过打印虚表可以推测出其存放的位置 多继承的虚函数表
单继承好理解毕竟只有一个嘛多继承就有点迷糊多出来的虚函数放在哪里呢
同样通过打印虚函数表可以看出多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中 如果派生类中重写了父类中的同一个虚函数虽然指向的函数时同一个函数但是两个函数的地址是不一样的。 如果构成第一个基类的多态直接在第一张虚表里面找 跳转到函数执行的位置。
如果构成第二个基类的多态先找到第一张虚表的地址通过第一张虚表来进行调用。
常见面试问答题 inline函数可以是虚函数吗 答可以不过编译器就忽略inline属性这个函数就不再是inline因为虚函数要放到虚表中去。 静态成员可以是虚函数吗 答不能因为静态成员函数没有this指针使用类型::成员函数的调用方式无法访问虚函数表所以静态成员函数无法放进虚函数表。 构造函数可以是虚函数吗 答不能因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 析构函数可以是虚函数吗什么场景下析构函数是虚函数 答可以并且最好把基类的析构函数定义成虚函数。 对象访问普通函数快还是虚函数更快 答首先如果是普通对象是一样快的。如果是指针对象或者是引用对象则调用的普通函数快因为构成多态运行时调用虚函数需要到虚函 数表中去查找。 虚函数表是在什么阶段生成的存在哪的 答虚函数表是在编译阶段就生成的一般情况下存在代码段(常量区)的不要与虚函数指针搞混了。 什么是抽象类抽象类的作用 答参考抽象类。抽象类强制重写了虚函数另外抽象类体现出了接口继承关系。