首页 / 资讯中心 / 文章详情

C++ 多态(三):虚函数、函数重写与动态绑定

C++ 多态(三):虚函数、函数重写与动态绑定 ★ FEATURED ARTICLE
目录一、为什么需要动态多态二、虚函数与 virtual 关键字1. 什么是虚函数2. 使用 override 检查重写三、函数重载、重写与隐藏1. 函数重载2. 函数重写3. 函数隐藏4. 三者的区别四、基类指针与基类引用1. 基类指针指向派生类对象2. 基类引用绑定派生类对象3. 基类指针指向不同的对象五、静态绑定与动态绑定1. 静态绑定2. 动态绑定3. 直接通过派生类对象调用六、成员函数末尾的 const 与重写1. const 的作用2. 派生类必须匹配 const3. 如果不写 override 呢4. 返回类型中的 const 不一样七、协变返回类型1. 什么是协变返回类型2. 协变返回类型的常见规则3. 协变返回类型的意义八、动态多态的实际应用九、综合示例与常见错误1. 虚函数与普通成员函数的区别2. 常见易错点总结十、本篇总结一、为什么需要动态多态在学习 C 继承时我们知道派生类可以继承基类的成员也可以重新定义基类中的同名函数。但是当我们使用基类指针指向派生类对象时调用函数的结果可能和直接使用派生类对象不同。先看下面的代码#include iostream using namespace std; class Animal { public: void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak() { cout Dog endl; } }; int main() { Dog d; Animal* p d; d.speak(); p-speak(); return 0; }运行结果Dog Animal为什么两次调用的结果不同d.speak()通过Dog对象直接调用因此执行Dog::speak()。p-speak()虽然p指向的是Dog对象但p的静态类型是Animal*而speak()不是虚函数所以调用Animal::speak()。体现了非虚函数的静态绑定直接原因输出animal也体现了函数隐藏这个知识点。如果我们希望通过基类指针调用函数时能够根据实际指向的对象类型选择对应的函数就需要用到虚函数和动态绑定。二、虚函数与 virtual 关键字1. 什么是虚函数在 C 中可以使用virtual关键字将基类中的成员函数声明为虚函数。虚函数的一个重要作用是当通过基类指针或基类引用调用该函数时如果派生类正确重写了这个虚函数程序就可以根据对象的实际类型调用相应的函数实现。例如#include iostream using namespace std; class Animal { public: virtual void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak() { cout Dog endl; } }; int main() { Dog d; Animal* p d; d.speak(); p-speak(); return 0; }运行结果Dog Dog与前面的代码相比我们只在基类的speak()前面增加了virtual第二次调用的结果就发生了变化。这是因为基类的speak()是虚函数。派生类的speak()正确重写了基类的虚函数。p实际指向一个Dog对象。通过基类指针调用虚函数时程序根据对象的实际类型选择函数实现。需要注意的是派生类重写基类虚函数时不需要再次写virtual重写后的函数仍然是虚函数。不过为了让编译器检查重写是否正确建议使用override。2. 使用 override 检查重写class Animal { public: virtual void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak() override { cout Dog endl; } };override的作用是告诉编译器我希望这个函数重写基类中的虚函数请检查是否真的重写成功。例如如果把派生类函数改成void speak(int x) override { cout Dog endl; }由于参数列表发生变化它没有重写基类的speak()因此编译器会报错。易错点override不会让函数自动变成重写函数它只是检查重写是否成功。如果去掉override的话相当于是函数隐藏。三、函数重载、重写与隐藏这三个概念容易混淆需要分别理解。1. 函数重载函数重载是指在同一作用域内函数名称相同但参数列表不同。class Animal { public: void speak() { cout Animal endl; } void speak(int x) { cout Animal x endl; } };这里的两个speak()构成函数重载。调用时编译器根据传入的参数选择对应的函数Animal a; a.speak(); a.speak(10);2. 函数重写函数重写发生在继承关系中。派生类重新实现基类的虚函数并满足虚函数重写的要求。class Animal { public: virtual void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak() override { cout Dog endl; } };这里Dog::speak()重写了Animal::speak()。3. 函数隐藏发生在基类与派生类之间当派生类声明了与基类同名的成员函数时派生类中的同名函数会影响基类同名函数的名字查找。例如class Animal { public: void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak(int x) { cout Dog endl; } };此时Dog d; // d.speak(); // 编译错误基类同名函数被隐藏 d.speak(10); // 调用 Dog::speak(int)虽然基类中存在speak()但是派生类声明了同名函数speak(int)普通的名字查找不会自动把基类的同名函数重载集合合并进来。如果希望基类的同名函数也参与名字查找可以使用usingclass Dog : public Animal { public: using Animal::speak; void speak(int x) { cout Dog endl; } };这样Dog d; d.speak(); // 调用 Animal::speak() d.speak(10); // 调用 Dog::speak(int)需要注意using Animal::speak;只是让基类同名函数重新参与名字查找不会让speak(int)重写基类的speak()也不会因此产生动态多态。4. 三者的区别对比项函数重载函数重写函数隐藏是否需要继承不一定需要通常发生在继承关系中函数名称相同相同相同参数列表必须有区别必须符合重写规则可以不同是否要求基类函数为虚函数不要求要求不要求主要作用根据参数选择函数实现动态多态影响名字查找四、基类指针与基类引用1. 基类指针指向派生类对象在继承关系中基类指针可以指向派生类对象。Dog d; Animal* p d;当基类中存在虚函数并且派生类正确重写了该函数时通过p调用虚函数就可以执行派生类的实现。p-speak();输出Dog2. 基类引用绑定派生类对象除了基类指针基类引用也可以绑定派生类对象。Dog d; Animal r d; r.speak();如果speak()是虚函数并且派生类正确重写了它那么这次调用同样会执行Dog::speak()。因此基类指针和基类引用都可以用于实现动态多态。3. 基类指针指向不同的对象class Animal { public: virtual void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak() override { cout Dog endl; } }; class Cat : public Animal { public: void speak() override { cout Cat endl; } }; int main() { Dog d; Cat c; Animal a; Animal* p d; p-speak(); p c; p-speak(); p a; p-speak(); return 0; }运行结果Dog Cat Animal虽然指针p的类型始终是Animal*但它可以指向不同类型的对象。每次通过它调用虚函数时程序都会根据当前指向对象的实际类型选择对应的函数实现。这说明动态绑定关注的是对象的实际类型而不只是指针的静态类型。五、静态绑定与动态绑定1. 静态绑定对于普通非虚成员函数通过基类指针调用时调用哪个函数主要由指针的静态类型决定。class Animal { public: void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak() { cout Dog endl; } }; int main() { Dog d; Animal* p d; p-speak(); return 0; }输出Animal因为speak()不是虚函数调用p-speak()时使用的是Animal::speak()。2. 动态绑定将基类函数声明为虚函数并让派生类正确重写class Animal { public: virtual void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak() override { cout Dog endl; } };此时Dog d; Animal* p d; p-speak();输出Dog通过基类指针调用虚函数时程序会根据实际对象类型选择函数实现这就是动态绑定。3.直接通过派生类对象调用Dog d; d.speak();无论speak()是否为虚函数这里都会调用Dog::speak()因为调用表达式直接使用Dog对象。因此不能简单地认为“只有虚函数才能调用派生类函数”。虚函数的关键作用是支持通过基类指针或引用进行动态分派。六、成员函数末尾的 const 与重写1. const 的作用成员函数末尾的const表示该成员函数不会通过当前对象修改普通的非mutable数据成员并且可以通过常量对象调用。例如class Animal { public: virtual void speak() const { cout Animal endl; } };这里的const是成员函数声明的一部分会影响虚函数重写是否匹配。2. 派生类必须匹配 const下面的代码会导致override检查失败class Animal { public: virtual void speak() const { cout Animal endl; } }; class Dog : public Animal { public: void speak() override { cout Dog endl; } };原因是基类声明的是speak() const而派生类声明的是speak()两者不匹配。即使派生类函数体中没有修改任何成员变量也不能因此视为正确重写。函数是否修改数据与函数声明中是否带有const是两个不同的问题。正确写法是class Dog : public Animal { public: void speak() const override { cout Dog endl; } };3. 如果不写 override 呢如果派生类仍然声明void speak() { cout Dog endl; }但没有写override代码通常可以编译。不过派生类的speak()并没有重写基类的speak() const。此时又相当于是函数的隐藏Dog d; Animal* p d; d.speak(); // Dog p-speak(); // Animal输出Dog Animal因此建议重写虚函数时尽量使用override让编译器及时发现声明不匹配的问题。4. 返回类型中的 const 不一样下面的声明virtual const char* speak();其中const修饰的是字符表示不能通过返回的指针修改相应字符。它与下面的声明不同virtual void speak() const;后者的const修饰成员函数本身的对象限定会影响重写匹配。这两种const不要混淆。七、协变返回类型1. 什么是协变返回类型通常情况下派生类重写虚函数时返回类型需要满足相应的重写规则。C 还允许一种特殊情况基类虚函数返回某个类的指针或引用派生类重写时可以返回其派生类的指针或引用。这种机制称为协变返回类型。例如class Animal { public: virtual Animal* clone() { return this; } }; class Dog : public Animal { public: Dog* clone() override { return this; } };基类返回Animal*派生类返回Dog*。由于Dog继承自Animal这里满足协变返回类型的要求。2. 协变返回类型的常见规则基类返回类型派生类返回类型是否允许Animal*Animal*允许Animal*Dog*满足条件时允许AnimalDog满足条件时允许Animal*Dog不允许Animal*int不允许Animal*无关类的Cat*不允许这里的协变只适用于符合要求的类指针或类引用并不是只要返回类型可以转换就一定能够重写成功。3. 协变返回类型的意义协变返回类型可以让派生类在重写函数时返回更具体的类型。例如通过Dog对象直接调用Dog::clone()返回值类型可以是Dog*这样调用者就能够直接获得更具体的类型信息而不必立即将结果转换回Animal*。八、动态多态的实际应用前面介绍了很多语法规则下面来看动态多态在实际程序中的用途。假设我们需要处理不同动物的叫声#include iostream using namespace std; class Animal { public: virtual void speak() { cout Animal endl; } }; class Dog : public Animal { public: void speak() override { cout 汪汪 endl; } }; class Cat : public Animal { public: void speak() override { cout 喵喵 endl; } }; void makeSound(Animal* p) { p-speak(); } int main() { Dog d; Cat c; makeSound(d); makeSound(c); return 0; }输出汪汪 喵喵这里的makeSound()只需要接收Animal*不需要专门为狗和猫分别编写两个函数。如果以后新增一个Bird类只要它继承Animal并正确重写speak()原来的makeSound()就不需要修改。这体现了动态多态的优势统一接口通过基类指针或引用调用不同派生类的实现。减少重复代码相同的调用逻辑不需要为每种派生类重复编写。方便扩展新增派生类时可以复用已有的通用处理函数。需要注意的是使用动态多态时基类指针或引用必须指向有效对象如果传入空指针直接调用成员函数会导致未定义行为。九、综合示例与常见错误1. 虚函数与普通成员函数的区别#include iostream using namespace std; class Animal { public: virtual void speak() const { cout Animal endl; } void eat() { cout Animal eat endl; } }; class Dog : public Animal { public: void speak() const override { cout Dog endl; } void eat() { cout Dog eat endl; } }; int main() { Dog d; Animal* p d; p-speak(); p-eat(); d.eat(); return 0; }输出Dog Animal eat Dog eat分析p-speak()speak()是虚函数Dog正确重写了它因此调用Dog::speak()。p-eat()eat()是普通成员函数通过Animal*调用时执行Animal::eat()。d.eat()直接通过Dog对象调用因此执行Dog::eat()。这道题体现了虚函数与普通成员函数在调用机制上的区别。2. 常见易错点总结错误一认为派生类重写函数必须再次写 virtual。实际上基类声明为虚函数后派生类正确重写时不需要重复写virtual。建议使用override。错误二认为函数体没有修改成员变量就可以省略 const。是否修改成员变量与成员函数声明中的const是两个不同的问题。重写时必须匹配相应的对象限定。错误三认为 override 会让函数自动重写成功。override只负责检查。如果函数名、参数或其他重写条件不匹配编译器会报错。错误四认为基类指针指向派生类对象就一定调用派生类函数。只有通过基类指针或引用调用符合条件的虚函数才能根据对象实际类型进行动态分派。普通非虚函数不会因此自动进行动态绑定。错误五混淆重载、重写和隐藏。三者虽然都可能涉及同名函数但发生条件和作用不同。尤其要注意派生类声明同名函数后基类同名函数可能被隐藏。错误六认为协变返回类型允许任意不同的返回类型。协变只允许符合要求的类指针或类引用之间的特定变化不能把返回类型随意改成int、void*或无关类指针。十、本篇总结本篇主要学习了 C 动态多态的基础知识使用virtual声明基类虚函数。使用override检查派生类是否正确重写虚函数。理解函数重载、重写和隐藏的区别。理解基类指针与基类引用如何参与动态多态。区分静态绑定和动态绑定。理解成员函数末尾的const对重写的影响。了解协变返回类型的基本规则。理解动态多态如何统一接口、减少重复代码并方便扩展。
阅读完成 · 觉得有帮助?
咨询建站