专业网站设计推荐,自有品牌如何推广,wordpress页面模板文件,网页升级紧急通知域名前言#xff1a;多态的概念#xff0c;通俗地来讲就是多种形态。当我们要完成某个行为的时候#xff0c;不同的对象去完成时会产生不同的状态#xff0c;这就叫做多态。具体点就是去完成某个行为#xff0c;当不同的对象去完成时会 产生出不同的状态。多态在C的类和对象中… 前言多态的概念通俗地来讲就是多种形态。当我们要完成某个行为的时候不同的对象去完成时会产生不同的状态这就叫做多态。具体点就是去完成某个行为当不同的对象去完成时会 产生出不同的状态。多态在C的类和对象中具有十分重要的作用大部分学校对于C中多态的讲解并不透彻下面我们就一起来深入了解多态...... 注意我们在本次的讲解中用到的代码环境均为vs2022的x86环境32位的环境下指针是4字节需要注意不同的运行环境下代码可能会有不同。 目录
1.多态的定义及其实现
多态的构成条件
虚函数的重写
两个例外
1. 协变(基类与派生类虚函数返回值类型不同)
2. 析构函数的重写(基类与派生类析构函数的名字不同)
什么情况下需要析构函数重写呢
C11 override 和 final
1. final修饰虚函数表示该虚函数不能再被重写
2. override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。
重载、覆盖(重写)、隐藏(重定义)的对比
2.抽象类
接口继承和实现继承
典例解析
3.多态的原理
虚函数表
为什么多态实现不能是传对象类型而必须是引用或指针
虚函数的地址一定会被放进虚函数表吗
监视窗口不可信情况
最可信的大招-打印虚表
动态绑定与静态绑定
多继承中的虚函数表
结论
一些细节问题
4.菱形继承、菱形虚拟继承了解
菱形继承
菱形虚拟继承
5.多态经典题
都看到这里了休息一下叭~ 1.多态的定义及其实现
多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。
多态的构成条件
在继承中要构成多态有两个条件 1. 必须通过基类的指针或者引用调用虚函数 2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 虚函数的重写 虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。
下面一段代码实现了一个最简单的多态
class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }/*注意在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议这样使用*//*void BuyTicket() { cout 买票-半价 endl; }*/
};
void Func(Person p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
} 用相同的func函数实现了传入不同对象返回了不同的功能这就是最简单的多态。
两个例外
1. 协变(基类与派生类虚函数返回值类型不同) 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。
我们可以将上述的代码进行简单的修改
class Person {
public:virtual Person* BuyTicket() { cout 买票-全价 endl; return new Person; }
};
class Student : public Person {
public:virtual Student* BuyTicket() { cout 买票-半价 endl; return new Student; }
};
void Func(Person p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
} 此时我们发现其也能实现多态的功能这种特殊情况了解即可。
2. 析构函数的重写(基类与派生类析构函数的名字不同) 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字 都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同 看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor。
class Person {
public:virtual ~Person() { cout ~Person() endl; }
};
class Student : public Person {
public:virtual ~Student() { cout ~Student() endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数下面的delete对象调用析构函数才能构成多态才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{Person* p1 new Person;Person* p2 new Student;delete p1;delete p2;return 0;
}什么情况下需要析构函数重写呢 普通的情况下析构函数没问题。Student先析构子类析构函数不需要显式调用父类的析构函数会自动调用父类析构函数来完成析构达成子类对象先析构父类对象后析构的目的。 但是当父类对象尝试new一个子类对象后也想要实现达成多态的条件之一时如果不把析构函数写成虚函数便会出问题。 这种情况下只是释放了父类对象的空间而实际上父类的指针指向的其实是一个子类的对象正常调用应该是先释放子类对象然后在释放父类对象只调用父类的析构函数将会导致内存泄漏的问题。
C11 override 和 final C对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数 名字母次序写反而无法构成重载而这种错误在编译期间是不会报出的只有在程序运行时没有得到预期结果才来debug会得不偿失因此C11提供了override和final两个关键字可以帮 助用户检测是否重写。
1. final修饰虚函数表示该虚函数不能再被重写
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout Benz-舒适 endl;}
};2. override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。
class Car{
public:virtual void Drive(){}
};
class Benz :public Car {
public:virtual void Drive() override {cout Benz-舒适 endl;}
};
重载、覆盖(重写)、隐藏(重定义)的对比 2.抽象类 在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数只要有纯虚函数的类叫做抽象类也叫接口 类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生 类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。这也是多态的另外一种表达形式由一对一的父子继承关系变成了一对多的一父多子的继承关系各个子类重写的是同一个父类的纯虚函数。
示例
class Car{
public:virtual void Drive() 0;
};
class Benz :public Car{
public:virtual void Drive(){cout Benz-舒适 endl;}
};
class BMW :public Car{
public:virtual void Drive(){cout BMW-操控 endl;}
};
void Test()
{Car* pBenz new Benz;pBenz-Drive();Car* pBMW new BMW;pBMW-Drive();
}
接口继承和实现继承 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成 多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。
典例解析
请移步至多态经典题目第一小题。 3.多态的原理
虚函数表
我们来看下面一段含有虚函数的代码及其输出结果 我们发现类的大小比我们预想的四个字节正好又多了四个字节下面我们来进行调试查看这个Base类内部的成员变量 我们发现在类对象的内部除了私有成员变量 _b,还有一个指针变量_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针每一个虚表指针都是一个函数指针可以用来保存若干虚函数的函数地址因为虚函数 的地址要被放到虚函数表中虚函数表也简称虚表。 有时候监视窗口并不能很好地反应变量在内存中存储的内容我们可以通过内存调试将该对象的内存中存储的内容显示出来 那么派生类中这个表放了些什么呢我们针对上面的代码做出修改使之符合派生类继承和重写的要求
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的内部成员信息 由于上面基类对象b的结构即内存分布在上面已经给出这里重点介绍基类在派生类中的内存分布并且给出多态的实现的一般性原理 为什么多态实现不能是传对象类型而必须是引用或指针 一般的我们将子类对象通过赋值兼容规则赋值给父类对象的时候编译器只会拷贝子类对象中父类的数据但是却不会拷贝虚函数表指针给父类但是一旦采取了赋值的方式拷贝了虚函数表指针给父类父类对象就不能保证父类对象的需要是自己的也有可能是子类的虚表拷贝过来的这就无法实现父类和子类的虚函数各自保存在自己的虚表中以通过子类重写以实现多态的目的。
虚函数的地址一定会被放进虚函数表吗 正确的回答是是的每个类的所有虚函数的地址一定会被放进虚函数表中但是有一些特殊情况容易导致我们误认为虚函数没有被放进虚函数表我们来分析一下这些情况
监视窗口不可信情况
我们给出一段多继承代码如下尝试查看对象的虚函数表的情况
class Base {
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() { cout Derive::func4 endl; }void func5() { cout Derive::func5 endl; }
private:int b;
};
class X :public Derive {
public:virtual void func3() { cout X::func3 endl; }
};
int main()
{Base b;Derive d;X x;Derive* p d;p-func3();p x;p-func3();return 0;
}
调出监视窗口我们发现事实并不像我们想的那样 监视窗口并没有显示出虚函数表的全部函数从上面我们可以看出明明上面都写着虚函数表的大小是5也就是保存了四个虚函数地址最终却只显示出来了2个虚函数所以当前的监视窗口变得不可信了因为它是经过开发人员优化过的结果最终目的是为了让用户更加容易理解。 监视窗口会骗我们但是内存窗口不会所以我们调出内存窗口继续观察这三个对象的虚表指针指向的位置 最可信的大招-打印虚表
我们可以通过打印虚表的方式来获取对象虚表中的全部内容具体可以参考以下代码
class Base {
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() { cout Derive::func4 endl; }void func5() { cout Derive::func5 endl; }
private:int b;
};
class X :public Derive {
public:virtual void func3() { cout X::func3 endl; }
};//虚表是一个函数指针数组
typedef void (*VFUNC)();//函数指针重命名
void PrintVFT(VFUNC a[])
{for (size_t i 0; a[i] ! 0; i) //vs下虚表中的最后一个位置是nullptr但是Linux环境不行只能固定写多少个这一点需要注意{printf([%d]:%p-, i, a[i]);VFUNC f a[i];f();//调用一下函数根据输出辨别是哪一个函数//(*f)();}printf(\n);
}
int main()
{void (*f)();//普通函数指针的变量定义Base b;Derive d;X x;PrintVFT((VFUNC*)(*((int*)b)));//int* 表示取前四个字节还需要注意类型转换直接打印就是int*型和函数指针类型不匹配所以需要再强转为函数指针型关于数据强转通常情况下内存和类型相关或相似的才能够互相强转PrintVFT((VFUNC*)(*((int*)d)));PrintVFT((VFUNC*)(*((int*)x)));return 0;
}这里需要注意这个代码使我们直接访问内存所以可能会出现异常访问的情况这个时候我们需要重新生成一下解决方案就可以了。 这个时候各个类中的虚函数就一目了然了后面我们还会继续使用这种方法来显示类对象的所有虚函数由于这种方式是直接访问内存所以其结果也是非常可靠的。
动态绑定与静态绑定 1. 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态 比如函数重载 2. 动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体 行为调用具体的函数也称为动态多态。
多继承中的虚函数表
我们给出如下的多继承示例代码
class Base1 {
public:virtual void func1() { cout Base1::func1 endl; }virtual void func2() { cout Base1::func2 endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout Base2::func1 endl; }virtual void func2() { cout Base2::func2 endl; }
private: int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }
private:int d1;
};int main()
{Derive d;return 0;
}
我们考虑查看出d的虚表结构进行观察下面是监视结果。 d对象的结构也是按照我们监视出来的成员从上到下的顺序分布的所以我们可以大致看出d的成员变量分布如下 为了确定Base1和Base2中虚表的信息和d对象本身将自己的虚表保存在何处我们需要打印出虚表的详细信息这里有细节需要注意就是我们如何获取Base2的前四个字节这里给出其中一种方式我们可以将d对象整体强转为一字节地址然后将其加上Base1地址空间就是Base2的首地址了接着再进行强转即可。
typedef void (*VFUNC)();//函数指针重命名
void PrintVFT(VFUNC a[])
{for (size_t i 0; a[i] ! 0; i){printf([%d]:%p-, i, a[i]);VFUNC f a[i];f();//调用一下函数根据输出辨别是哪一个函数//(*f)();}printf(\n);
}
int main()
{Derive d;VFUNC* vTableb1 (VFUNC*)(*(int*)d);PrintVFT(vTableb1);VFUNC* vTableb2 (VFUNC*)(*(int*)((char*)d sizeof(Base1)));//当前我们需要取Base2的虚表指针由结构图可知其距离d对象首地址的偏移量为一个Base1的大小PrintVFT(vTableb2);return 0;
} 哎~等等你确定它实现了多态吗为什么在上面显示的结果中派生类中的func1函数明明已经被重写了而在派生类func1函数只有一个但是打印出虚表之后却是有两个不同的地址呢 话不多说实践一下我们给出一个多态的环境看看它能否实现多态不就行了吗下面是测试代码和运行结果 我们发现它确实能实现多态但是这不合理啊一个函数居然有两个地址吗 这里要是演示起来需要利用汇编进行调试比较费时费力所以这里我们直接给出结论我们知道Base1和Base2指针在派生类对象中的位置不一样当父类指针调用派生类虚函数的时候需要用到派生类的this指针所以Base1和Base2指针需要在调用派生类函数时将自己的指针修正为派生类的指针此处的修正并不是简单的强转而是需要找到派生类指针存在的合理的位置进行相同结构的强转只有这样才能按照派生类的模式正常的调用派生类的函数而Base1指针的位置恰好与派生类对象指针重合只要进行强转即可不需要偏移然而Base2指针与派生类指针相距8个4字节的距离也就是相距一个Base1的地址所以在修正Base2时需要将其向低地址偏移8个4字节从而做到以统一的方式调用派生类中的函数。 这个偏移8个4字节我们可以通过上面的地址直接验证出来具体的计算如下 从输出结果我们可以看出d对象将自己未重写的func3函数放在了Base1的虚表中此时我们不难联想到派生类的未重写的虚函数将会放在其继承的第一个基类对象的虚表中为了验证这个结论我们可以将派生类的继承基类进行互换再次打印虚表我们将两次打印的结果进行对比可以得出结论。 所以在多继承中我们可以得出如下结论
结论 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中而将重写的虚函数放在继承下来的基类对象的虚表中。 一些细节问题 1.虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。 2.同一个类的对象的虚表相同理论上而言一个类只有一个虚表当然多继承就会不同这个后面讲解 3.总结一下派生类的虚表生成 a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函 数对于派生类不重写的虚函数派生类在自己的虚表中保存一份与基类相同的虚函数 地址。 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 4.虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段常量区的只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。 针对第4点我们来浅浅验证一下我们给出如下代码具体细节在注释中讲解
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;
};int main()
{Base b1;Base b2;static int a 0;int b 0;int* p1 new int;const char* p2 hello world;printf(静态区%p\n, a);printf(栈区%p\n, b);printf(堆区%p\n, p1);printf(代码段(常量区)%p\n, p2);printf(虚表%p\n, *((int*)b1) ); //虚表保存在类对象的前四个字节中,取对象的地址然后使用int*将其强转为前四个字节因为此处保存的是虚表指针我们需要对前四个字节进行解引用得到指针所指向的地址printf(虚函数地址%p\n, Base::Func1);//类成员函数在获取地址时需要加上普通成员函数则不需要return 0;
} 从运行结果可以看出虚表和虚函数地址与代码段的区域地址较为接近所以他们都保存在代码段。
4.菱形继承、菱形虚拟继承了解
菱形继承 菱形继承其实就是多继承的一种派生类也是继承两张虚表分别是两个父类的虚表只是需要注意多进程的细节即可比如在派生类中添加虚函数则该虚函数会被加入到派生类第一个声明的继承类中。
class A
{
public:virtual void func1() { cout A::func1 endl;}
public:int _a;
};class B : public A
//class B : virtual public A
{
public:virtual void func1(){cout B::func1 endl;}virtual void func3(){cout B::func3 endl;}
public:int _b;
};class C : public A
//class C : virtual public A
{
public:virtual void func1(){cout C::func1 endl;}virtual void func5(){cout C::func5 endl;}
public:int _c;
};class D : public B, public C
{
public:virtual void func1(){cout D::func1 endl;}virtual void func2(){cout D::func2 endl;}
public:int _d 1;
};int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;return 0;
} 菱形继承和本质上和多继承一样派生类会继承两张虚表分别来自其两个父亲每一个父亲都会自己的虚表和从共同的父类上继承的虚表其监视图如下 菱形虚拟继承 菱形虚继承的情况就要复杂得多我们将上面的代码中B和C类设为虚继承采用内存的方式查看派生类对应的虚表结构因为菱形虚继承比我们想象的要复杂得多这里只时简单的了解一下即可实际中也要尽可能的避免用到菱形虚继承。 5.多态经典题
1.如下程序的输出结果是什么
class A
{
public:virtual void func(int val 1) { std::cout A- val std::endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(int val 0) { std::cout B- val std::endl; }
};int main(int argc, char* argv[])
{B* p new B;p-test();return 0;
} 乍一看p是一个派生类B的指针p调用继承自A的test函数func函数形成多态所以答案其实就是 B- 0了。嘿嘿搞定~ 等等我还有话要说咱先看看这个 这怎么可能呢你快说啊别卖关子了~ 子类的指针去调用test因为test被public继承下来了。但是即使被继承下来的this指针还是A*的this这里又涉及了赋值兼容的转换因为B的指针要调用成员函数得把B的指针传给A*的this。子类传给父类发生了切片相当于test里面的this指向了new出来的B对象。
所以这里也符合多态的两个条件参数是相同的因为只看类型不看缺省值。子类继承重写父类虚函数是接口继承会把缺省值也继承下来。重写的是函数的实现。 2.inline函数可不可以是虚函数 内联函数本质上就是在编译阶段将代码展开内联函数作为虚函数在不同的情况下具有不同的效果由于篇幅实在是太大了所以这里直接给出结论内联函数可以是虚函数只是在多态调用下内联就会失去内联的作用因为虚函数表和虚函数指针等结构不能在展开内联属性只对普通调用生效。 3.静态成员函数可不可以是虚函数 不可以因为静态成员函数没有this指针静态本质是全局函数只不过它受类域的限制。 4.构造函数能不能是虚函数呢 不可以编译报错因为对象中的虚表指针是构造函数阶段才初始化的注意此时虚函数表已经生成了如果还没有构造虚函数的多态调用需要到虚表中查找但是此时虚表指针还没有初始化。
4.多态调用和普通调用的效率问题 一般情况下多态调用要比普通调用慢因为要设涉及到虚表指针的问题到虚表中查找的问题效率要低下一些。
5.虚函数是在什么阶段生成的
虚函数表在编译时就已经生成了虚函数表指针构造时才初始化给对象的。 都看到这里了休息一下叭~ 无论你起多早总有人比你早一步无论你多努力总有人比你更拼命。我希望屏幕前的你好好生活好好努力也要好好休息。从现在起屏蔽所有不相关的信息静下心来好好爱自己。