阳江有哪些建站公司,国内的优秀网站,国外用wordpress,江苏建设教育网官网1.多态的概念 1.1 概念 多态的概念#xff1a;通俗来说#xff0c;就是多种形态#xff0c;具体点就是去完成某个行为#xff0c;当不同类型的对象去完成时会 产生出不同的状态。 举个例子#xff1a;比如有一个基类Animal#xff0c;它有两个子类Dog和Cat。每个…1.多态的概念 1.1 概念 多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同类型的对象去完成时会 产生出不同的状态。 举个例子比如有一个基类Animal它有两个子类Dog和Cat。每个子类都可以重写基类的方法比如make_sound()方法。当调用make_sound()方法时Dog类和Cat类会根据自己的实现发出不同的声音这就是多态的体现。 再比如买票这个行为当普通人买票时是全价买票学生买票时是半价买票军人 买票时是优先买票。 2.多态的定义以及实现 2.1多态的构成条件 多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。比如Student继承了 Person。Person对象买票全价Student对象买票半价。 在继承中要构成多态还有两个条件 1. 必须通过基类的指针或者引用调用虚函数。 2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写。 2.2 虚函数 虚函数即被virtual修饰的类成员函数称为虚函数。 class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl;}
}; 2.3虚函数的重写 虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。 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;
} 虚函数重写的两个例外 1. 协变(基类与派生类虚函数返回值类型不同) 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。 2.析构函数的重写(基类与派生类析构函数的名字不同) 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字 都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同 看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处 理编译后析构函数的名称统一处理成destructor。 2.4 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.5 重载、覆盖(重写)、隐藏(重定义)的对比 3. 抽象类 3.1 概念 在虚函数的后面写上 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.2 接口继承和实现继承 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实 现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。 4.多态的原理 4.1虚函数表 如图我们可以观察到b对象的大小为8bit正常情况下我们可能会任务为四个字节但是这是为什么呢 通过观察测试我们发现b对象是8bytes除了_b成员还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代 表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数 的地址要被放到虚函数表中虚函数表也简称虚表。 #includeiostream
using namespace std;
// 针对上面的代码我们做出以下改造
// 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;
} 通过观察和测试我们发现了以下几点问题 1. 派生类对象d中也有一个虚表指针d对象由两部分构成一部分是父类继承下来的成员虚 表指针也就是存在部分的另一部分是自己的成员。 2. 基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表 中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法覆盖是原理层的叫法。 3. 另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但是不是虚函 数所以不会放进虚表。 4. 虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。 5. 总结一下派生类的虚表生成a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 6. 这里还有一个童鞋们很容易混淆的问题虚函数存在哪的虚表存在哪的 答虚函数存在 虚表虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意 虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是 他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。那么虚表存在哪的 呢实际我们去验证一下会发现vs下是存在代码段的 4.2动态绑定与静态绑定 1. 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态 比如函数重载 2. 动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体 行为调用具体的函数也称为动态多态。 3.之前买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。 5.单继承和多继承关系的虚函数表 5.1 单继承中的虚函数表 观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这 两个函数也可以认为是他的一个小bug。那么我们如何查看d的虚表呢下面我们使用代码打印 出虚表中的函数。 思路 取出b、d对象的头4bytes就是虚表的指针前面我们说了虚函数表本质是一个存虚函数指针的指针数组这个数组最后面放了一个nullptr 1.先取b的地址强转成一个int*的指针 2.再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表的指针 3.再强转成VFPTR*因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。 4.虚表指针传递给PrintVTable进行打印虚表 5.需要说明的是这个打印虚表的代码经常会崩溃因为编译器有时对虚表的处理不干净虚表最后面没有放nullptr导致越界这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案再编译就好了。 6. 继承和多态常见的问题 1、继承方法可以使变得富有 2、动态绑定是面向对象程序设计语言的一种机制这种机制实现了方法的定义与具体对象无关而对方法的调用则可以关联与具体的对象 3、继承允许我们覆盖重写父类的实现细节父类的实现对于子类是可见的是一种静态复 用也称为白盒复用 。组合的对象不需要关心各自的实现细节之间的关系是在运行时候才确定的是一种动 态复用也称为黑盒复用。继承可以使子类能自动继承父类的接口但在设计模式中认为这是一种破坏了父类的封 装性的表现。 4、声明纯虚函数的类不能实例化对象声明纯虚函数的类是虚基类。 5、派生类必须重新定义基类的虚函数。 6、基类中有虚函数如果子类中没有重写虚函数此时子类和积累共用一张虚表。 7. 什么是多态 答多态是指相同的消息被不同类型的对象接收时产生不同的行为。 8. 什么是重载、重写(覆盖)、重定义(隐藏) 答 重载Overload同一范围内的函数名字相同但参数列表不同。编译器根据参数列表来区分不同的函数。 重写Override子类重新定义了父类中的虚函数子类的函数签名和父类的相同。重定义Hide子类重新定义了父类中的非虚函数但函数签名不同此时不会形成重写而是新定义了一个函数隐藏了父类的同名函数。 9. 多态的实现原理 答多态的实现原理是通过虚函数和动态绑定实现的。当通过基类指针或引用调用虚函数时会根据实际对象的类型来决定调用哪个版本的虚函数。 10. inline函数可以是虚函数吗 答可以不过编译器就忽略inline属性这个函数就不再是 inline因为虚函数要放到虚表中去。 11. 静态成员可以是虚函数吗 答不能因为静态成员函数没有this指针使用类型::成员函数 的调用方式无法访问虚函数表所以静态成员函数无法放进虚函数表。 12. 构造函数可以是虚函数吗 答不能因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。 13. 析构函数可以是虚函数吗什么场景下析构函数是虚函数 答可以并且最好把基类的析 构函数定义成虚函数。析构函数可以是虚函数当需要基类指针或引用指向派生类对象并使用delete释放内存时如果析构函数不是虚函数只会调用基类的析构函数而不会调用派生类的析构函数可能导致资源泄露。因此通常在基类中将析构函数声明为虚函数。 14. 对象访问普通函数快还是虚函数更快 答首先如果是普通对象是一样快的。如果是指针 对象或者是引用对象则调用的普通函数快因为构成多态运行时调用虚函数需要到虚函 数表中去查找。 15. 虚函数表是在什么阶段生成的存在哪的 答虚函数表是在编译阶段就生成的一般情况 下存在代码段(常量区)的。 16. C菱形继承的问题虚继承的原理 答菱形继承指的是通过两条不同的继承路径继承同一个基类可能会导致派生类中包含两份相同基类的成员。虚继承是通过在派生类对共同基类的继承前加上virtual关键字来解决菱形继承带来的二义性问题使得最终派生类只包含一份共同基类的成员。 17. 什么是抽象类抽象类的作用 答抽象类是包含纯虚函数的类不能直接实例化对象只能作为其他类的基类来派生出具体的子类。抽象类的作用是为了定义接口规范子类的行为。