IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++深入理解多态(一) -> 正文阅读

[C++知识库]C++深入理解多态(一)

多态

一、 多态概念

多态(polymorphism)是面向对象(OOP)的核心思想,按照字面意思去理解,就是多种形态。

是对于同一种指令,针对不同的对象,产生不一样的行为。简短来说就是一个接口,多个方法

1 如何去理解多态

这里我们用一个例子去步步引入

  1. 假如我们定义了一个Animal的动物基类,该基类有两个成员函数,一个是eat()吃,一个是run()跑,并且还有一个数据成员name名字,这是基本上每个动物都会有的特征。
  2. 此时有两个具体的动物类Dog和Cat来继承我们的Animal动物基类。
  3. 此时我们的目的是通过基类的方式去创建不同的动物,并使用每个动物实现的方法。
#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;

class Animal {
public:
    void eat() {
        cout << "Animal eat()" << endl;
    }
    void run() {
        cout << "Animal run()" << endl;
    }
protected:
    string name;
};

//Dog类继承了Animal类
class Dog : public Animal{
public:
    void eat() {
        cout << "Dog eat()" << endl;
    }
    void run() {
        cout << "Dog run()" << endl;
    }
};
//Cat类继承了Animal类
class Cat : public Animal{
public:
    void eat() {
        cout << "Cat eat()" << endl;
    }
    void run() {
        cout << "Cat run()" << endl;
    }
};

int main()
{ 
    Animal animal = Dog();
    animal.eat();
    animal.run();
    
    animal = Cat();
    animal.eat();
    animal.run();
    return 0;
}

  1. 正如第三条所说,我们希望通过一个接口就能得到不同动物的不同方法的调用,这正是我们多态实现的意义所在。

2 如何去实现多态

然而我们上面的写的形式在C++语言中真的能够实现吗?

我们可能想的太过简单。当我们用上述代码去实现时会得到一下结果。

Animal eat()
Animal run()
Animal eat()
Animal run()
Animal eat()

我们发现,这并不是想要得到的效果,应该是创建Dog的对象,调用Dog的函数才对。这里调用的还是Animal,为了解决这个问题,需要引入一些概念。

2.1 C++的多态性(静态联编和动态联编)

C++支持两种多态性:编译时多态和运行时多态。

  • 编译时多态:也称为静态多态,C++编译器根据传递给函数的参数和函数名决定要具体使用哪一个函数,又称为先期联编(early binding)或静态联编(static binding)。
  • 运行时多态:有时,编译器无法在编译过程中完成联编,必须在程序运行时完成,因此编译器提供了一套“动态联编(dynamic binding)”的机制或晚期联编(late binding)。

而我们想要的多态和上面代码表现出来的不一样,我们一般想要的多态是运行是多态。

2.2 虚函数

我们发现上述例子显然实现的是静态多态,在这里需要介绍一种(表示还有其他方式实现动态多态)实现动态多态的方式:虚函数。

2.2.1 虚函数的定义

虚函数在基类中被声明为virtual的函数,并在其派生类中被重新定义的成员函数。

class Base {
  	virtual 返回类型 函数名(参数列表) { ... }  
};
//或者是在类中声明之后在类外实现
virtual 返回类型 类名::函数名(参数列表) { ... }
2.2.2 派生类继承虚函数的使用事项:
  1. 如果一个基类的成员函数定义为了虚函数,那么它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字,也仍然是虚函数。

  2. 派生类要对继承来的虚函数进行重写(覆盖),但有要求:

    1. 与基类的虚函数有相同的参数个数及类型;
    2. 与基类的虚函数有相同的返回值类型;
    3. 与基类的虚函数有相同的函数名;

    简单来说,除了函数体不同,其他都相同

让我们根据这些概念来完善上述代码。

#include <iostream>
#include <string>
using std::endl;
using std::cout;
using std::string;

class Animal {
public:
    //这里加了virtual
    virtual void eat() {
        cout << "Animal eat()" << endl;
    }
    virtual void run() {
        cout << "Animal run()" << endl;
    }
protected:
    string name;
};

//Dog类继承了Animal类
class Dog : public Animal{
public:
    void eat() {
        cout << "Dog eat()" << endl;
    }
    void run() {
        cout << "Dog run()" << endl;
    }
};
//Cat类继承了Animal类
class Cat : public Animal{
public:
    void eat() {
        cout << "Cat eat()" << endl;
    }
    void run() {
        cout << "Cat run()" << endl;
    }
};

int main()
{ 
    Animal animal = Dog();
    animal.eat();
    animal.run();
    
    animal = Cat();
    animal.eat();
    animal.run();
    return 0;
}
2.2.3 虚函数的访问

此时如果再次运行代码,我们能得到什么结果?

Animal eat()
Animal run()
Animal eat()
Animal run()

发现还是与我们想的不一样,这究竟是为什么?

我们发现,左边声明的是Animal类对象,右边初始化的是其派生类对象,按理说,应该调用的是派生类的函数才对。但为什么调用的还是Animal基类中的函数?

这里需要对虚函数的访问做一些解释:

  • 1 对象访问:即Animal animal = Dog();
    • 如上述main函数中的使用,我们通过对象的方式去访问虚函数,此时编译器采用的是静态联编
    • 通过对象名访问虚函数时,调用哪个类的函数取决于定义对象的类型
    • 对象类型是基类型,那么就调用基类的函数;对象类型是派生类型,那么就调用派生类的函数。
  • 2 指针访问:即Animal *pAnimal = new Animal();
    • 使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对象类型。(可以看出这点和对象访问一致)
    • 使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关。
  • 3 引用访问:即Dog dog; Animal &animal = dog;
    • 使用引用访问虚函数,与使用指针访问虚函数类似,都表现出动态多态的特性。
    • 不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。(这点还是符合了引用的特性:引用就是初始化时对象的别名)。
    • 因此虽然在使用上有一定限制,但是在一定程度上提供了代码的安全性,特别体现在函数参数传递等场合中。
  • 4 成员函数中访问:在类的成员函数中访问该类层次中的虚函数,采用动态联编方式。
  • 5 构造函数和析构函数中访问:构造函数和析构函数是特殊的成员函数
    • 在其中访问虚函数时,C++采用静态联编,即在构造函数或析构函数内,即使是使用"this->虚函数名"的形式来调用,编译器仍将其解释为静态联编的”本类名::虚函数名“。
    • 即它们所调用的虚函数是自己类中定义的函数,如果在自己类中没有实现该函数,则调用的是基类中的虚函数。但绝对不会调用任何在派生类中重写的虚函数。
2.2.4 访问格式的验证
  1. 对象访问:上述代码以及验证过,这里不多加赘述。
  2. 指针访问:发现此时的却可以访问到对应派生类的函数,指针所指对象的类型绝对要调用哪个函数。
int main() {
	Animal *pAnimal = new Dog();
	pAnimal->eat();
	pAnimal->run();
         
	delete pAnimal;
           
	pAnimal = new Cat();
	pAnimal->eat();
	pAnimal->run();
}
/*
Dog eat()
Dog run()
Cat eat()
Cat run()
*/
  1. 引用访问:此时我们发现这也能访问到对应派生类的函数,但是重新改变指向,指为cat时却还是原来的dog类型。(从逻辑出发,对于引用来说,这种操作本身就是错的。但在可执行程序的生成过程中却没有报错,访问的时候与预期值不符合,这样行为称之为未定义行为。在C++中未定义行为有很多,你还知道哪些?)
int main() {
    Dog dog;
    Cat cat;
    Animal &animal = dog;
    animal.eat();
    animal.run();
    
    cout << endl;
    
    animal = cat;
    animal.eat();
    animal.run();
}
/*
Dog eat()
Dog run()

Dog eat()
Dog run()
*/
  1. 成员函数的访问:在Animal定义一个bark()函数,其中调用了一个虚函数run()
class Animal {

public:
    virtual void eat() {
        cout << "Animal eat()" << endl;
    }
    virtual void run() {
        cout << "Animal run()" << endl;
    }
    
    void bark() {
        cout << "There is a animal barking now" << endl;
        run();
    }
    
protected:
    string name;

};

class Dog : public Animal{
public:
    void eat() {
        cout << "Dog eat()" << endl;
    }
    void run() {
        cout << "Dog run()" << endl;
    }
    
    /*
    virtual void drink() {
        cout << "the dog want to drink" << endl;
    }
    */ 
};

int main() {
    Animal *pAnimal = new Dog();
    pAnimal->bark();
    
	return 0;
}

/*
从结果发现,的确实动态联编,调用的还是Dog类中的run函数
There is a animal barking now
Dog run()
*/
  1. 构造函数和析构函数中访问:在Dog和Cat的构造函数中都调用eat()函数,其中Dog类重写了继承过来的eat()函数,Cat类没有重写eat()函数
class Animal {
public:
    virtual void eat() {
        cout << "Animal eat()" << endl;
    }
    virtual void run() {
        cout << "Animal run()" << endl;
    }  
protected:
    string name;
};

//重写了eat
class Dog : public Animal{
public:
    Dog() {
        this->eat();
        cout << "Dog()" << endl;
    }
    
    void eat() {
        cout << "Dog eat()" << endl;
    }
};

//没有重写eat()
class Cat : public Animal{
public:
    
    Cat() {
        eat();
        cout << "Cat()" << endl;
    }

};

int main() {
    Animal *pAnimal = new Dog();
    delete pAnimal;

    pAnimal = new Cat();
    delete pAnimal;
}
/*
从结果中可以发现,若将继承过来的虚函数重写了那么会调用该类中重写过的函数,若没有重写,则调用基类中对应的虚函数。
Dog eat()
Dog()
Animal eat()
Cat()
*/

2.3 常见的多态类型(欢迎补充)

对于多态性我们可以做个小总结,对哪些属于动态多态,哪些属于静态多态进行分类。

2.3.1 静态多态

函数重载,运算符重载,类类型对象,模板等发生的时机都在编译的时候。

2.3.2 动态多态

类类型指针,类类型引用,成员函数调用虚函数等,发生的时机在运行的时候,通过虚函数体现。

3 如何去使用动态多态

  1. 基类要定义或声明虚函数
  2. 派生类要重写该虚函数
  3. 创建派生类对象
  4. 用基类的指针或引用指向派生类的对象
  5. 基类的指针或去调用虚函数
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-10 13:14:33  更:2021-08-10 13:15:32 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/18 17:38:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码