c++之多态篇
一、多态的概念多态Polymorphism 的字面意思是“多种形态”。在面向对象编程中它指的是同一个行为方法在不同的对象上会表现出不同的实现方式。举个例子对于移动这个词汽车的移动方式是行驶飞机的移动方式是飞行人的移动方式是走路虽然我们都称之为“移动”这个行为但不同对象执行它的具体方式完全不同。这就是多态的思想。在编程中多态意味着我们可以使用一个统一的接口父类引用来操作不同的对象子类实例而具体执行哪个对象的代码则由其实际类型在运行时决定。二、多态的定义2.1构成多态要三个条件1.存在一个继承 hierarchy层级结构即父类和子类子类对父类中定义的某些方法提供了自己的、不同的实现即方法重写2.必须通过基类的指针或者引用调用虚函数因为只有基类的指针或引用才能既指向基类 对象又指向派⽣类对象继承文章有提到3.被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写2.2虚函数虚函数即被virtual修饰的类成员函数称为虚函数。class Animal { public: // 使用 virtual 关键字声明虚函数 virtual void makeSound() { cout 动物发出某种叫声... endl; } };2.3虚函数重写虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。注意继承的时候有一个重定义的概念但是重写和重定义是两回事重定义是没有virtual关键字的并且重定义只要求函数名一样参数以及返回值根本不在乎。注意这里的重写可以理解为覆盖但是只是覆盖了定义的内容声明不变也就是相当于把接口里的方法变了但是传入的接口名缺省参数的初始值不变 class Animal { public: // 使用 virtual 关键字声明虚函数 virtual void makeSound() { cout 动物发出某种叫声... endl; } }; // 派生类狗 class Dog : public Animal { public: virtual void makeSound() { cout 汪汪汪 endl; } }; // 派生类猫 class Cat : public Animal { public: virtual void makeSound() { cout 喵喵喵 endl; } };2.4虚函数重写的两个例外1. 协变(基类与派生类虚函数返回值类型不同)了解即可派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。了解所以说虚函数重写并非要满足前面3个条件。class A{}; class B : public A {}; class Person { public: virtual A* f() {return new A;} }; class Student : public Person { public: virtual B* f() {return new B;} };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* p1 new Student; delete p1; delete p2; return 0; }2.5与虚函数相关的关键字override和finalfinal关键字作用修饰虚函数表示该虚函数不能再被重写class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() {cout Benz-舒适 endl;} //会报错因为基类有的Drive()函数有final关键字修饰子类不能重写Drive()虚函数 };override关键字作用检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。class Car{ public: virtual void Drive(){} }; class Benz :public Car { public: virtual void Drive() override {cout Benz-舒适 endl;} };override是一个不错的有利于纠正我们一些错误的关键字比如我们派生类想重写的虚函数名错误此时override就发力了编译器会报错。三、抽象类3.1概念在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。#include iostream using namespace std; // 抽象基类动物 class Animal { public: virtual void makeSound() const 0; // 纯虚函数 virtual ~Animal() {} // 虚析构函数 }; // 具体派生类狗 class Dog : public Animal { public: void makeSound() const override {//这里派生类用不用都构成重写只要基类有virtual关键字就行 cout 汪汪! endl; } }; // 具体派生类猫 class Cat : public Animal { public: void makeSound() const override { cout 喵喵! endl; } }; int main() { // Animal animal; // 错误不能创建抽象类的对象 Dog dog; Cat cat; // 多态调用 Animal* animal1 dog; Animal* animal2 cat; animal1-makeSound(); // 输出: 汪汪! animal2-makeSound(); // 输出: 喵喵! return 0; }3.2接口继承和实现继承普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。纯虚函数的继承是一种接口继承派生类继承的是基类纯虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。举个例子接口继承#include iostream using namespace std; // 抽象基类 - 主要提供接口继承 class Animal { public: // 纯虚函数 - 只继承接口不继承实现 // 派生类 MUST 提供自己的实现 virtual void makeSound() const 0; // 虚析构函数是必须的 virtual ~Animal() {} }; class Dog : public Animal { public: // 重写纯虚函数提供具体实现 void makeSound() const override { cout 汪汪汪 endl; } }; class Cat : public Animal { public: void makeSound() const override { cout 喵喵喵 endl; } };Animal类规定了所有动物都必须有一个makeSound()行为接口。但它没有提供makeSound()的具体实现0。Dog和Cat继承了这个接口必须实现makeSound并提供了各自的实现。实现继承// 基类 - 提供接口和默认实现 class AnimalWithDefault { public: // 非纯虚函数 - 既继承接口也继承默认实现 // 派生类可以使用默认实现也可以重写它 virtual void sleep() const { cout Zzz... (动物在睡觉) endl; } }; class LazyDog : public AnimalWithDefault { // 没有重写 sleep(), 直接继承并使用基类的默认实现 };LazyDog类继承了基类已经实现的函数。四、多态的原理4.1虚指针表我们先来看下面一个代码class Base { public: virtual void Func1() { cout Func1() endl; } protected: int _b 1; char _ch x; }; int main() { Base b; cout sizeof(b) endl; return 0; }上述代码运行结果是16字节但是int是4字节char是1字节就算内存对齐也是8字节为什么最后结果是16字节呢注意这里的16字节是在64位系统下如果32位系统下则运行结果是12我们调试了一下发现诶怎么多了一个void指针的指针类型。对象中的这个指针我们叫做虚函数表指针(v代表virtualf代 表function)注意可能不同的编译器名称不同有的是vptr。一个含有虚函数的类中都至少都有⼀个虚函数表指针这个虚函数表指针指向虚函数表的地址一个类所有虚函数的地址要被放到这个类对象的虚函数表中虚函数表也简称虚表(vtable)。4.2多态的原理满足多态条件后底层不再是编译时通过调用对象确定函数的地址而是运行时到指向的对象的虚表中确定对应的虚函数的地址这样就实现了指针或引用指向基类就调用基类的虚函数指向派生类就调用派生类对应的虚函数。4.3动态绑定和静态绑定1.对不满足多态条件(指针或者引用调用虚函数)的函数调用是在编译时绑定也就是编译时确定调用函数的地址叫做静态绑定。2.满足多态条件的函数调用是在运行时绑定也就是在运行时到指向对象的虚函数表中找到调用函数的地址也就做动态绑定。扩展我们想要将我们写的Cpp程序代码进行运行我们首先得将Cpp的代码通过编译转成二进制编码然后才可以进行运行我们编译期分为四部预处理#include头文件插入、#define宏替换等等、语法检查、生成汇编代码、将所有汇编代码文件(.o)进行合并也就是链接的过程(就生成了可执行文件exe)然后我们再通过gcc命令运行可执行文件exe我们由于虚函数我们在编译器无法确认对象指向的是哪一个所以我们并不能在编译器就把函数调用的地址直接绑定例如下面这段代码class Base { public: virtual void func() { cout Base\n; } }; class Derived1 : public Base { public: void func() override { cout Derived1\n; } }; class Derived2 : public Base { public: void func() override { cout Derived2\n; } }; // 甚至可以后续新增 Derived3、Derived4编译原代码时根本不知道存在 void foo(Base* p) { p-func(); // 关键调用 } int main() { int x; cin x; Base* p; if(x 1) p new Derived1; else if(x 2) p new Derived2; else p new Base; foo(p); return 0; }这段代码很明显我们是不能通过编译器直到它对应的函数地址也就是它具体要调用的是哪一个作用域的函数因为他要根据x输入的值来进行转化如果foo函数不是多态的那么就说明子类和派生类用的这个函数的实现都是一样的所以可以直接绑定函数地址在编译器的时候但是如果它是多肽的话我们就不能在编译期绑定函数地址我们必须在运行时根据虚函数函数表来绑定它的函数地址。4.4虚函数表基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表不同类型的对象各自有独立的虚表所以基类和派生类有各自独立的虚表同类型的对象共用一张虚表这也说明了为什么一个对象只有虚函数表指针而不是虚函数表目的是为了更加高效节省空间。派生类由两部分构成继承下来的基类和自己的成员一般情况下继承下来的基类中有虚函数表指针自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针指向的地址和基类对象的虚函数表指针指向的地址不是同一个。派生类中重写的基类的虚函数派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。派生类的虚函数表中包含(1)基类的虚函数地址(2)派生类重写的虚函数地址完成覆盖派生类自己的虚函数地址两个部分。虚函数存在哪的虚函数和普通函数⼀样的编译好后是一段指令都是存在代码段的只是虚函数的地址又存到了虚表中。虚函数表存在哪的这个问题严格说并没有标准答案C标准并没有规定vs下是存在代码段(常量区)