C++学习之多态的使用详解
目录
- 前言
- 多态
- 向上转型
- 向下转型
- 作用域
前言 最近为了完成数据库系统的实验,又复习起了《C++ Primer》,上一次看这本巨著也是大二下的六月份,那时看面向对象程序编程这一章还云里雾里的,没有领会多态的奥妙,学完 Java 之后回头再看这一章发现对多态有了更好的理解。好记性不如烂笔头,本篇博客将会对 C++ 的多态机制做一个不太详细的总结,希望下一次不需要从头再看一遍《C++ Primer》了 _(:з」∠)_。
多态 多态离不开继承,首先来定义一个基类
Animal
,里面有一个虚函数 speak()
:class Animal {public:Animal() = default; Animal(string name) : m_name(name) {}virtual ~Animal() = default; virtual void speak() const { cout << "Animal speak" << endl; }string name() const { return m_name; }private:string m_name; };
接着定义子类
Dog
,并重写虚函数,由于构造函数无法继承,所以使用 using
来 “继承” 父类的构造函数。和父类相比,Dog
还多了一个 bark()
方法。class Dog : public Animal {public:using Animal::Animal; // 可加上 override 声明要重写虚函数,函数签名必须和基类相同(除非返回类自身的指针或引用)void speak() const override { cout << "Dog bark" << endl; }void bark() const { cout << "lololo" << endl; }};
向上转型 我们在堆上创建一个
Dog
对象,并将地址赋给一个 Animal
类型的指针。由于指针指向的是个 Dog
对象,调用 speak()
方法时,实际上调用的是底层狗狗重写之后的 speak()
方法,而不是基类 Animal
的 speak()
。也就是说编译时不会直接确定要调用的是哪个 speak()
,要在运行时绑定。Animal* pa = new Dog("二哈"); pa->speak(); // 调用的是 Dog::speakpa->Animal::speak(); // 强制调用基类的 speak
利用运行时绑定这一特点,我们将基类的析构函数定义为虚函数,这样子类对象在析构的时候就能调用自己的虚函数了。
虽然
pa
指向的是一个 Dog
对象,但是不能使用 bark()
方法。因为 pa
是一个 Animal
类型的指针,在编译时编译器会跳过 Dog
而直接在 Animal
的作用域中寻找 bark
成员,结果发现并不存在此成员而报错。要实现向上转型不止能用指针,引用同样可以实现。但是如果写成以下这种形式,实质上是调用了拷贝构造函数,会用
Dog
的基类部分来初始化 Animal
对象,和向上转型没有任何关系,之后调用的就是底层 Animal
对象的 speak()
方法:Dog dog("二哈"); Animal animal = dog; animal.speak(); // 调用的是 Animal::speak
向下转型 【C++学习之多态的使用详解】要想调用底层
Dog
对象的 bark()
方法,我们需要将 pa
强转为 Dog
类型的指针。一种方法是使用 static_cast
进行静态转换,另一种这是使用 dynamic_cast
进行运行时转换。相比于前者,dynamic_cast
转换失败的时候会返回空指针,而 dynamic_cast
则会报 bad_cast
错误,因此更加安全。Dog* pd_ = static_cast(pa); pd_->bark(); if (Dog* pd = dynamic_cast (pa)) {pd->bark(); } else {cout << "转换失败" << endl; }
作用域 子类的作用域是嵌套在父类里面的,在子类的对象上查找一个成员时,会现在子类中查找,如果没找到才回去父类中寻找。由于作用域的嵌套,会导致子类隐藏掉父类中的同名成员。比如下述代码:
class Animal {public:virtual void speak() const{cout << "Animal speak" << endl; }}; class Dog : public Animal {public:// void speak() const override { cout << "Dog speak" << endl; }void speak(string word) const{cout << "Dog bark: " + word << endl; }}; int main(int argc, char const* argv[]){Animal* pa = new Dog(); Dog* pd = new Dog(); // pd->speak(); 报错pd->speak("666"); // Dog::speak 隐藏了 Animal::speakreturn 0; }
我们在父类中定义了一个虚函数
void speak()
,子类中没有重写它,而是定义了另一个同名但是参数不同的函数 void speak(string word)
。这时候子类中的同名函数会隐藏掉父类的虚函数,如果写成 pd->speak()
,编译器会先在子类作用域中寻找名字为 speak
的成员,由于存在 speak(string word)
,它就不会接着去父类中寻找了,接着进行类型检查,发现参数列表对不上,会直接报错。如果用了 VSCode 的 C/C++ 插件,可以看到参数列表确实只有一个,没有提示有重载的同名函数。文章图片
要想通过调用基类的
speak()
方法,有两种方法:- 向上转型,使用基类的指针
pa
来调用pa->speak()
,由于子类没有重写虚函数,所以在动态绑定时会调用父类的虚函数; - 使用作用域符强制调用父类的虚函数:
pd->Animal::speak()
理解函数调用的解析过程对于理解 C++ 的继承至关重要,假定我们调用
p->mem()
(或者 obj.mem()
),则依次执行以下4个步骤:1.首先确定
p
(或 obj
) 的静态类型。因为我们调用的是一个成员,所以该类型必 然是类类型。2.在
p
(或 obj
) 的静态类型对应的类中查找 mem
。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。3.一旦找到了
mem
,就进行常规的类型检查以确认对于当前找到的 mem
,本次调用是否合法。4.假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
- 如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代 码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型。
- 反之,如果
mem
不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用。
推荐阅读
- C++多态的全面讲解
- C++知识总结|【C++】类与对象(一)—— 类的引入
- CPP|详细介绍C++中的类对象内存模型
- C++|C++中的类与对象
- #|李宏毅机器学习笔记1--线性回归
- 深度学习|李宏毅深度学习笔记 ---- 简介
- 机器学习|李宏毅机器学习GAN的笔记
- 机器学习实战|机器学习实战 | 机器学习特征工程最全解读
- C++ 中的 Lambda 表达式
- 蓝桥杯|【蓝桥杯】第十二届蓝桥杯C++B 组真题