网站制作公司多少钱一年,全国物流信息网,安徽省铜陵市建设银行网站,做公众号的模版的网站【C进阶】继承、多态的详解#xff08;多态篇#xff09; 目录 【C进阶】继承、多态的详解#xff08;多态篇#xff09;多态的概念多态的定义及实现多态的构成条件#xff08;重点#xff09;虚函数虚函数的重写#xff08;覆盖、一种接口继承#xff09;C11 override…【C进阶】继承、多态的详解多态篇 目录 【C进阶】继承、多态的详解多态篇多态的概念多态的定义及实现多态的构成条件重点虚函数虚函数的重写覆盖、一种接口继承C11 override 和 final重载、覆盖重写、隐藏重定义的对比 抽象类概念动态绑定与静态绑定 单继承和多继承关系的虚函数表菱形继承、菱形虚拟继承 常见的题目附加问题 作者爱写代码的刚子 时间2023.8.16 前言本篇博客主要介绍C中多态有关的知识是C中的一大难点刚子将带你深入C多态的知识。该博客涉及到的代码是在x86的环境下如果是在x86_64环境下指针的大小可能需要变成8bytes 多态的概念
多态的概念:通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同 的状态。
多态的定义及实现
多态的构成条件重点 必须通过基类的指针或者引用调用虚函数 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写派生类的重写虚函数可以不加virtual
示例
多态中不同对象传过去调用不同的函数多态调用指针或引用看的指向的对象普通调用看当前的类型。
指定调用不满足多态
虚函数
虚函数即被virtual修饰的类成员函数称为虚函数
虚函数的重写覆盖、一种接口继承
虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。
被重写的函数必须是虚函数 重写的条件虚函数 三同但是有些例外 虚函数重写的两个例外
协变(基类与派生类虚函数返回值类型不同)派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。(不常用) 协变返回值可以不同但是要求返回值必须是父子关系指针和引用(同时为指针或同时为引用) 析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的 析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规 则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处 理成destructor。
【问题】析构函数加virtual是不是虚函数重写 是因为类析构函数都被处理成destructor这个统一的名字因为要让他们构成重写。 【问题】将析构函数变为虚函数和普通的析构函数有什么区别呢 答在特殊场景下会不同 上面的场景很可能会造成内存泄漏这里我们期待p-destructor()是一个多态调用而不是普通调用。多态调用看指向的内容普通调用看对象的类型上图中的p指针是Person类型且普通调用。要想实现多态调用需要加上virtual构成重写
C11 override 和 final
从上面可以看出C对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数名字母次序 写反而无法构成重载而这种错误在编译期间是不会报出的只有在程序运行时没有得到预期结果才来debug会得不偿失因此C11提供了override和final两个关键字可以帮助用户检测是否重写。 final修饰虚函数表示该虚函数不能再被继承和重写 override检查派生类虚函数是否重写了基类某个函数如果没有重写编译报错。 final和override关键字顺序可以改变
【附】如果一个类不想被继承有几种方法
C98基类构造函数私有化通过public静态成员函数执行构造函数。如果不是静态成员函数不创建类就不能调用函数类似先有鸡还是先有蛋的问题还可以将析构函数私有化再创建一个静态的destructor成员函数C11类名后面加final关键字
重载、覆盖重写、隐藏重定义的对比 抽象类
概念
在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。
通过观察和测试我们发现了以下几点问题:
派生类对象d中也有一个虚表指针d对象由两部分构成一部分是父类继承下来的成员虚表指针也就 是存在部分的另一部分是自己的成员。基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表中存的是重 写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的 叫法覆盖是原理层的叫法。另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但是不是虚函数所以不会 放进虚表。虚函数表本质是一个存虚函数指针的指针数组这个数组最后面放了一个nullptr。总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基 类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表虚表存在对象中。注意上面的回答的错的。注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是他的指针又存到了虚表中。另外 对象中存的不是虚表存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的Linux g下呢
动态绑定与静态绑定
静态绑定又称为前期绑定(早绑定编译时)在程序编译期间确定了程序的行为也称为静态多态比如:函数重载动态绑定又称后期绑定(晚绑定运行时)是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态。
单继承和多继承关系的虚函数表
需要注意的是在单继承和多继承关系中下面我们去关注的是派生类对象的虚表模型因为基类的虚表模型前面我们已经看过了没什么需要特别研究的
单继承中的虚函数表多继承中的虚函数表
class Base1 {
public:virtual void func1() {cout Base1::func1 endl;}virtual void func2() {cout Base1::func2 endl;}
private:int b13;
};
class Base2 {
public:virtual void func1() {cout Base2::func1 endl;}virtual void func2() {cout Base2::func2 endl;}
private:int b24;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() {cout Derive::func1 endl;}virtual void func3() {cout Derive::func3 endl;}
private:int d15;
};int main() {Derive d;printf(%d,sizeof(d));return 0;
}多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
这里有一个需要注意的地方虽然重写的都是func1()但是他们地址不一样而调用的是同一个函数为什么 可以查看汇编代码发现第一个Base1中的func1()的地址就是正常的地址。第二个Base2中调用func1()时在ecx寄存器中发生了this指针减8指向了Derive对象的首地址保证this指针的正确因为是派生类调用而不是基类调用。所以本质就是修正this指针不同编译器处理方法不同也可以在存相同的地址在调用前让ecx中的this指针减8。
菱形继承、菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承一方面太复杂容易出问题另一方面这样的模型访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了一般我们也不需要研究清楚因为实际中很少用。如果要深入研究的话可以参考以下文章
C虚函数表解析 C对象的内存分布
常见的题目
输出结果
class A
{
public:virtual void func(int val 1){ std::coutA- val std::endl;}virtual void test(){ func();}
};
class B : public A
{
public:void func(int val0){ std::coutB- val std::endl; }
};
int main(int argc ,char* argv[])
{B*p new B;p-test();return 0;
}答案是B-1本题可能误认为是B-0
重要的是理解test()是由谁调用这里p-test()实际上是由p传给父类的A* this指针由父类指针去调用func()同时func()调用符合多态1. 父类指针 2. 虚函数重写注意虽然this指针类型是A*但是this指针是由B* p转化而来的实际上指向的对象区域还是派生类所以多态调用调用的是派生类的func();还有很坑的一点虚函数重写的是实现缺省参数还是使用的是基类的
附加问题
【问题1】多态的条件中为什么不能子类指针或者引用为什么不能是父类的对象 答1. 因为只有父类的指针才能既能指向父类又能指向子类 2. 对象的切片和指针或引用的切片是有一些不同的。子类赋值给父类对象切片不会拷贝虚表如果拷贝虚表那么父类对象虚表中是父类虚函数还是子类就不确定了。所以对象的切片不拷贝虚表就已经不满足多态了。如果是指针或引用的切片父类的切片对应父类的虚表子类的切片对应子类的虚表。重写后子类会将从父类拷贝来的虚表进行修改用重写的函数来覆盖来实现多态。
【问题2】 inline函数可以是虚函数吗? 答:不能因为inline函数没有地址无法把地址放到虚函数表中。
【问题3】静态成员可以是虚函数吗? 答:不能因为静态成员函数没有this指针使用类型::成员函数的调用方式无法访问虚函数表所以静态成员函数无法放进虚函数表。
【问题4】构造函数可以是虚函数吗? 答:不能因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
【问题5】析构函数可以是虚函数吗?什么场景下析构函数是虚函数? 答:可以并且最好把基类的析构函数定义成虚函数。
【问题6】对象访问普通函数快还是虚函数更快? 答:首先如果是普通对象是一样快的。如果是指针对象或者是引用对象则调用的普通函数快因为构成多态运行时调用虚函数需要到虚函数表中去查找。
【问题7】虚函数表是在什么阶段生成的存在哪的? 答:虚函数表是在编译阶段就生成的一般情况下存在代码段(常量区)的。
【问题8】什么是抽象类?抽象类的作用? 答:抽象类强制重写了虚函数另外抽象类体现出了接口继承关系。 【附】 不是虚函数不会进入虚表通常虚表最后会加个0但是不同的编译器处理不同VS下会给g没给派生类如果自己加虚函数编译器会将该虚函数放在虚表后面但监视窗口很可能不会显示。同类型的对象共用虚表虚表存在哪打印地址进行比较可发现虚表存在代码段(常量区) 打印虚函数表
typedef void(*FUNC_PTR)();
void PrintVFT(FUNC_PTR* table)
{for(size_t i0;table[i]!nullptr;i)//vs下linux下要写死范围{printf([%d]:%p\n,i,table[i]);}
}
int main()
{Person ps;int vft *((int*)ps);PrintVFT((FUNC_PTR*)vft);return 0;
}有虚函数表就可以拿到里面的函数无论是不是私有还是公有拿到地址就可以使用里面的函数