前言

最近为了完成数据库系统的实验,又复习起了《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() 方法,而不是基类 Animalspeak()。也就是说编译时不会直接确定要调用的是哪个 speak() ,要在运行时绑定。

Animal* pa = new Dog("二哈");
pa->speak(); // 调用的是 Dog::speak
pa->Animal::speak(); // 强制调用基类的 speak

利用运行时绑定这一特点,我们将基类的析构函数定义为虚函数,这样子类对象在析构的时候就能调用自己的虚函数了。

虽然 pa 指向的是一个 Dog 对象,但是不能使用 bark() 方法。因为 pa 是一个 Animal 类型的指针,在编译时编译器会跳过 Dog 而直接在 Animal 的作用域中寻找 bark 成员,结果发现并不存在此成员而报错。

要实现向上转型不止能用指针,引用同样可以实现。但是如果写成以下这种形式,实质上是调用了拷贝构造函数,会用 Dog 的基类部分来初始化 Animal 对象,和向上转型没有任何关系,之后调用的就是底层 Animal 对象的 speak() 方法:

Dog dog("二哈");
Animal animal = dog;
animal.speak(); // 调用的是 Animal::speak

向下转型

要想调用底层 Dog 对象的 bark() 方法,我们需要将 pa 强转为 Dog 类型的指针。一种方法是使用 static_cast 进行静态转换,另一种这是使用 dynamic_cast 进行运行时转换。相比于前者,dynamic_cast<type *> 转换失败的时候会返回空指针,而 dynamic_cast<type &> 则会报 bad_cast 错误,因此更加安全。

Dog* pd_ = static_cast<Dog *>(pa);
pd_->bark(); if (Dog* pd = dynamic_cast<Dog*>(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::speak
return 0;
}

我们在父类中定义了一个虚函数 void speak(),子类中没有重写它,而是定义了另一个同名但是参数不同的函数 void speak(string word)。这时候子类中的同名函数会隐藏掉父类的虚函数,如果写成 pd->speak(),编译器会先在子类作用域中寻找名字为 speak 的成员,由于存在 speak(string word),它就不会接着去父类中寻找了,接着进行类型检查,发现参数列表对不上,会直接报错。如果用了 VSCode 的 C/C++ 插件,可以看到参数列表确实只有一个,没有提示有重载的同名函数。

要想通过调用基类的 speak() 方法,有两种方法:

  • 向上转型,使用基类的指针 pa 来调用 pa->speak(),由于子类没有重写虚函数,所以在动态绑定时会调用父类的虚函数;
  • 使用作用域符强制调用父类的虚函数:pd->Animal::speak()

《C++ Primer》对名字查找做了一个非常好的总结:

总结

在 《C++ Primer》中对于上述内容有更加详细的讲解,这里不过是拾其牙慧罢了。看了这一章之后最大的感受就是:

你看 100 遍她的视频,她不是你的,你看 100 遍 《C++ Primer》,知识就是你的

以上~~

最新文章

  1. Atitit.log日志技术的最佳实践attilax总结
  2. guava
  3. Android 启动白屏或者黑屏闪现解决
  4. Visio 2007/2010 左侧&quot;形状&quot;窗口管理
  5. hdu1588 矩阵快速幂
  6. [转]一些实用的图表Chart制作工具
  7. leetcode problem (5) Longest Palindromic Substring
  8. bzoj1664 [Usaco2006 Open]County Fair Events 参加节日庆祝
  9. 达内TTS6.0课件oop_day03
  10. [vc]如何对radio按钮分组
  11. bzoj4830 hnoi2017 抛硬币
  12. python12--字符串的比较 函数的默认值的细节 三元表达式 函数对象 名称空间 作用域 列表与字典的推导式 四则运算 函数的嵌套
  13. SpringBoot 配置文件 中文乱码
  14. C#轻量级日志监控器EasyLogMonitor
  15. UWP-开发一个具有闹钟,天气预报,翻译,语音功能的Demo
  16. android APP上线前,应该准备的东西
  17. 【ES6】改变 JS 内置行为的代理与反射
  18. mysql source 恢复 sql数据time_zone报错 已解决
  19. Linux内核project导论——网络:路由:路由原理
  20. VS2008让Release配置也能调试起来~

热门文章

  1. 01 | 堆、栈、RAII:C++里该如何管理资源?(极客时间笔记)
  2. VsCode 常用插件清单
  3. 联想Y7000安装 debian10 wifi 不好使解决方法
  4. html_学习所有标签使用
  5. 手写useState与useEffect
  6. JS 中 对象 基础认识
  7. 1.7 Linux系统的优缺点
  8. p2p-tunnel 打洞内网穿透系列(一)客户端配置及打洞
  9. 羽夏 MakeFile 简明教程
  10. WPF中的依赖属性