服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C/C++ - C++学习之多态的使用详解

C++学习之多态的使用详解

2022-12-26 16:17之一Yo C/C++

这篇文章主要为大家详细介绍了C++中多态的机制以及使用,文中的示例代码讲解详细,对我们学习C++有一定的帮助,感兴趣的可以了解一下

前言

最近为了完成数据库系统的实验,又复习起了《C++ Primer》,上一次看这本巨著也是大二下的六月份,那时看面向对象程序编程这一章还云里雾里的,没有领会多态的奥妙,学完 Java 之后回头再看这一章发现对多态有了更好的理解。好记性不如烂笔头,本篇博客将会对 C++ 的多态机制做一个不太详细的总结,希望下一次不需要从头再看一遍《C++ Primer》了 _(:з」∠)_。

多态

多态离不开继承,首先来定义一个基类 Animal,里面有一个虚函数 speak()

?
1
2
3
4
5
6
7
8
9
10
11
12
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() 方法。

?
1
2
3
4
5
6
7
8
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() ,要在运行时绑定。

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

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

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

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

?
1
2
3
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 错误,因此更加安全。

?
1
2
3
4
5
6
7
8
Dog* pd_ = static_cast<Dog *>(pa);
pd_->bark();
 
if (Dog* pd = dynamic_cast<Dog*>(pa)) {
    pd->bark();
} else {
    cout << "转换失败" << endl;
}

作用域

子类的作用域是嵌套在父类里面的,在子类的对象上查找一个成员时,会现在子类中查找,如果没找到才回去父类中寻找。由于作用域的嵌套,会导致子类隐藏掉父类中的同名成员。比如下述代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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++ 插件,可以看到参数列表确实只有一个,没有提示有重载的同名函数。

C++学习之多态的使用详解

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

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

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

理解函数调用的解析过程对于理解 C++ 的继承至关重要,假定我们调用 p->mem() (或者 obj.mem()),则依次执行以下4个步骤:

1.首先确定 p (或 obj) 的静态类型。因为我们调用的是一个成员,所以该类型必 然是类类型。

2.在 p (或 obj ) 的静态类型对应的类中查找 mem。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。

3.一旦找到了 mem,就进行常规的类型检查以确认对于当前找到的 mem,本次调用是否合法。

4.假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:

  • 如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代 码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型。
  • 反之,如果 mem 不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用。

以上就是C++学习之多态的使用详解的详细内容,更多关于C++多态的资料请关注服务器之家其它相关文章!

原文链接:https://www.cnblogs.com/zhiyiYo/p/16387116.html

延伸 · 阅读

精彩推荐
  • C/C++C++实现模板中的非类型参数的方法

    C++实现模板中的非类型参数的方法

    这篇文章主要介绍了C++实现模板中的非类型参数的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友...

    taojYang9992021-10-26
  • C/C++DSP中浮点转定点运算--定点数模拟浮点数运算及常见的策略

    DSP中浮点转定点运算--定点数模拟浮点数运算及常见的策略

    本文主要讲解DSP中定点数模拟浮点数运算及常见的策略,具有参考价值,需要的朋友可以参考一下。 ...

    ganxingming8792021-04-07
  • C/C++C/C++ Qt TreeWidget 单层树形组件应用小结

    C/C++ Qt TreeWidget 单层树形组件应用小结

    TreeWidget 目录树组件,该组件适用于创建和管理目录树结构,在开发中我们经常会把它当作一个升级版的ListView组件使用,本文将通过TreeWidget实现多字段显...

    lyshark9112022-03-06
  • C/C++EasyC++模板重载

    EasyC++模板重载

    这篇文章主要介绍了C++模板重载,重载的模板的函数特征,也就是入参的数量和类型必须有所不同,下面我们讲举例说明此内容,具有一定的参考价值,需...

    梁唐6322022-07-14
  • C/C++详解原码、反码与补码存储与大小

    详解原码、反码与补码存储与大小

    这篇文章主要介绍了详解原码、反码与补码存储与大小的相关资料,需要的朋友可以参考下...

    魏尔肖6002021-05-18
  • C/C++C++ const的使用及this指针常方法(面试最爱问的this指针)

    C++ const的使用及this指针常方法(面试最爱问的this指针)

    这篇文章主要介绍了C++ const的使用,this指针,常方法(面试最爱问的this指针),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值...

    双鱼2118222021-10-29
  • C/C++深入浅析C++中的#,##,和"

    深入浅析C++中的#,##,和"

    这篇文章主要介绍了C++中的#,##,和"的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧...

    chinabinlang4302021-09-27
  • C/C++C++踩坑实战之构造和析构函数

    C++踩坑实战之构造和析构函数

    不论是构造函数,还是析构函数,都是C++、C#语言相对于其他语言而言特殊的地方,它是为了方便类中对象的初始化,这篇文章主要给大家介绍了关于C++踩坑实战...

    Just for Life.8432021-12-03