C/C++学习之路: 多态


目录

  1. 多态基本概念
  2. 向上类型转换及问题
  3. 如何实现动态绑定
  4. 抽象基类和纯虚函数
  5. 纯虚函数和多继承
  6. 虚析构函数
  7. 重写,重载,重定义

1. 多态基本概念

  1. 多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
  2. 多态性(polymorphism)提供接口与具体实现之间的另一层隔离。
  3. 多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。
  4. c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
  5. 静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。
    1. 如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。
    2. 而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,就属于晚绑定(动态多态,运行时多态)。
//计算器
class Caculator {public:void setA(int a) {this->mA = a;}void setB(int b) {this->mB = b;}void setOperator(string oper) {this->mOperator = oper;}int getResult() {if (this->mOperator == "+") {return mA + mB;} else if (this->mOperator == "-") {return mA - mB;} else if (this->mOperator == "*") {return mA * mB;} else if (this->mOperator == "/") {return mA / mB;}}private:int mA;int mB;string mOperator;
};//这种程序不利于扩展,维护困难,如果修改功能或者扩展功能需要在源代码基础上修改
//面向对象程序设计一个基本原则:开闭原则(对修改关闭,对扩展开放)//抽象基类
class AbstractCaculator {public:void setA(int a) {this->mA = a;}virtual void setB(int b) {this->mB = b;}virtual int getResult() = 0;protected:int mA;int mB;
};//加法计算器
class PlusCaculator : public AbstractCaculator {public:virtual int getResult() {return mA + mB;}
};//减法计算器
class MinusCaculator : public AbstractCaculator {public:virtual int getResult() {return mA - mB;}
};//乘法计算器
class MultipliesCaculator : public AbstractCaculator {public:virtual int getResult() {return mA * mB;}
};void DoBussiness(AbstractCaculator *caculator) {int a = 10;int b = 20;caculator->setA(a);caculator->setB(b);cout << "计算结果:" << caculator->getResult() << endl;delete caculator;
}

2. 向上类型转换及问题

1. 问题

  1. 对象可以作为自己的类或者作为它的基类的对象来使用,还能通过基类的地址来操作它。
  2. 取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。
  3. 父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。
class Animal {public:void speak() {cout << "动物在唱歌..." << endl;}
};class Dog : public Animal {public:void speak() {cout << "小狗在唱歌..." << endl;}
};void DoBussiness(Animal &animal) {animal.speak();
}void test3() {Dog dog;DoBussiness(dog);
}
  1. 我们给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是Dog::speak。

2. 问题解决(虚函数,virtual function)

  1. 把函数体与函数调用相联系称为绑定(捆绑,binding)
  2. 当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding),C语言中只有一种函数调用方式,就是早绑定。
  3. 上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。
  4. 编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak。
  5. 解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。
  6. C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。
  7. C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。
  8. 对于特定的函数进行动态绑定,c++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用.
  9. 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.
  10. 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.
  11. 在派生类中virtual函数的重定义称为重写(override),Virtual关键字只能修饰成员函数,构造函数不能为虚函数
  12. 注意: 仅需要在基类中声明一个函数为virtual.调用所有匹配基类声明行为的派生类函数都将使用虚机制。
class Animal {public:virtual void speak() {cout << "动物在唱歌..." << endl;}
};class Dog : public Animal {public:virtual void speak() {cout << "小狗在唱歌..." << endl;}
};void DoBussiness(Animal &animal) {animal.speak();
}void test3() {Dog dog;DoBussiness(dog);
}

3. 如何实现动态绑定

  1. 当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是虚表指针(缩写vptr),这个指针是指向对象的虚函数表。
  2. 在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。
  3. 在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),
  4. 在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。
  5. 如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。
  6. 子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的虚函数表。
  7. 当子类无重写基类虚函数时:

  Animal* animal = new Dog;animal->fun1();当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是基类的func1.
  1. 当子类重写基类虚函数时:

     Animal* animal = new Dog;animal->fun1();当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,由于子类重写基类的func1函数,所以调用func1时,调用的是子类的func1.
  1. 多态的成立条件:

    1. 有继承
    2. 子类重写父类虚函数函数:返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外),子类中virtual关键字可写可不写,建议写
    3. 类型兼容,父类指针,父类引用 指向 子类对象

4. 抽象基类和纯虚函数

  1. 在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。

  2. 同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。

  3. 做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class).

    1. 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
    2. 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
    3. Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。
  4. 建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现),可以创建一个公共类。

//抽象制作饮品
class AbstractDrinking{public://烧水virtual void Boil() = 0;//冲泡virtual void Brew() = 0;//倒入杯中virtual void PourInCup() = 0;//加入辅料virtual void PutSomething() = 0;//规定流程void MakeDrink(){Boil();Brew();PourInCup();PutSomething();}
};//制作咖啡
class Coffee : public AbstractDrinking{public://烧水virtual void Boil(){cout << "煮农夫山泉!" << endl;}//冲泡virtual void Brew(){cout << "冲泡咖啡!" << endl;}//倒入杯中virtual void PourInCup(){cout << "将咖啡倒入杯中!" << endl;}//加入辅料virtual void PutSomething(){cout << "加入牛奶!" << endl;}
};//制作茶水
class Tea : public AbstractDrinking{public://烧水virtual void Boil(){cout << "煮自来水!" << endl;}//冲泡virtual void Brew(){cout << "冲泡茶叶!" << endl;}//倒入杯中virtual void PourInCup(){cout << "将茶水倒入杯中!" << endl;}//加入辅料virtual void PutSomething(){cout << "加入食盐!" << endl;}
};//业务函数
void DoBussiness(AbstractDrinking* drink){drink->MakeDrink();delete drink;
}void test(){DoBussiness(new Coffee);cout << "--------------" << endl;DoBussiness(new Tea);
}

5. 纯虚函数和多继承

  1. 绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,c++中没有接口的概念,但是可以通过纯虚函数实现接口。
  2. 接口类中只有函数原型定义,没有任何数据定义。
  3. 多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。
  4. 注意:除了析构函数外,其他声明都是纯虚函数。

6. 虚析构函数

1. 虚析构函数作用

  1. 虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
class People{public:People(){cout << "构造函数 People!" << endl;}virtual void showName() = 0;virtual ~People(){cout << "析构函数 People!" << endl;}
};class Worker : public People{public:Worker(){cout << "构造函数 Worker!" << endl;pName = new char[10];}virtual void showName(){cout << "打印子类的名字!" << endl;}~Worker(){cout << "析构函数 Worker!" << endl;if (pName != NULL){delete pName;}}
private:char* pName;
};void test(){People* people = new Worker;people->~People();
}

2. 纯虚析构函数

  1. 纯虚析构函数在c++中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体。
  2. 那么问题是:如果给虚析构函数提供函数体了,那怎么还能称作纯虚析构函数呢?
  3. 纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象。
  4. 如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数。
//非纯虚析构函数
class A{public:virtual ~A();
};A::~A(){}//纯析构函数
class B{public:virtual ~B() = 0;
};B::~B(){}void test(){A a; //A类不是抽象类,可以实例化对象B b; //err,B类是抽象类,不可以实例化对象
}

7. 重写,重载,重定义

  1. 重载,同一作用域的同名函数

    1. 同一个作用域
    2. 参数个数,参数顺序,参数类型不同
    3. 和函数返回值,没有关系
    4. const也可以作为重载条件 //do(const Teacher& t){} do(Teacher& t)
  2. 重定义(隐藏)
    1. 有继承
    2. 子类(派生类)重新定义父类(基类)的同名成员(非virtual函数)
  3. 重写(覆盖)
    1. 有继承
    2. 子类(派生类)重写父类(基类)的virtual函数
    3. 函数返回值,函数名字,函数参数,必须和基类中的虚函数一致

C/C++学习之路: 多态相关推荐

  1. C/C++学习之路: 模板和异常

    C/C++学习之路: 模板和异常 目录 模板 类型转换 异常 1. 模板 1. 模板概述 c++提供了函数模板(function template),函数模板实际上是建立一个通用函数,其函数类型和形参 ...

  2. F#学习之路(2) 深刻理解函数(上)

    函数在函数式编程语言中是一等公民,是函数式语言中最重要的基本组成元素,也是其名称的由来. F# 中的函数之如C#中的类,是组织程序结构的最基本单元.是命令式编程语言中函数或OO编程语言中方法的超集.超 ...

  3. c gui qt 4编程第二版_我的QT5学习之路(一)——浅谈QT的安装和配置

    一.前言 说到Qt,不能不说到C++,这门伟大的语言.因为其面向对象的编程思想和陡峭的学习曲线,一开始学习起来很是吃力.Qt从QT4开始基本封装了很多C++的工具库和界面库,而且支持跨平台,这是它最大 ...

  4. java学习之路目录(已完结)

    java学习之路目录(持续更新中-) 第一阶段 javaSE(完结) 序号 标题 内容 001 java初识 java语言特点.体系结构.运行机制 002 java SE基础语法 注释.关键字.变量. ...

  5. Python学习之路9☞面向对象的程序设计

    Python学习之路9☞面向对象的程序设计 一 面向对象的程序设计的由来 见概述:http://www.cnblogs.com/linhaifeng/articles/6428835.html 二 什 ...

  6. java qt gui_工控编程,Qt 学习之路

    原标题:工控编程,Qt 学习之路 Qt 是一个著名的 C++ 库--或许并不能说这只是一个 GUI 库,因为 Qt 十分庞大,并不仅仅是 GUI.使用 Qt,在一定程序上你获得的是一个"一站 ...

  7. typescript学习之路(三) —— ts定义类的方法(包含es5以及es6的定义类)

    提起类,不得不说一下,强类型编程语言,如php,java,c++等都有类的概念.而js作为一门弱类型语言,是没有类这个概念的,虽然也能模拟类的实现,但总归不是类.so,ts也只是模拟类而已,使得更贴切 ...

  8. 送你九年经验,我的Java学习之路你也可以复制

    这篇文章写的非常认真,足足花了两周时间,不是简单的资料聚合,我将多年的工作和学习经验写下来了,相信看完后你能有一种豁然开朗的感觉,这就是我要达到的目的,希望不要被打脸. 最近身边很多人在问:Java ...

  9. 一个程序员的Java和C++学习之路(整理)

    转载:http://blog.csdn.net/ajian005/article/details/8003655 Java学习之路 一直有这么个想法,列一下我个人认为在学习和使用Java过程中可以推荐 ...

最新文章

  1. 从代理机制到Spring AOP
  2. 快速上手RaphaelJS-Instant RaphaelJS Starter翻译(一)
  3. oracle定时器每天下午6点_周五下午6点到8点 万盛经开区党工委书记、管委会主任袁光灿直播带货...
  4. CentOS - 修改主机名教程(将 localhost.localdomain 改成其它名字)
  5. python第三方库介绍和安装
  6. 第二次力扣周赛:排名149 / 2046;在完赛边缘打转(总结了5点,实力还不够)
  7. go代码--数据结构
  8. 慢就是快的人生哲理_非常精辟的人生哲理句子,句句经典睿智,不管多忙都要看看!...
  9. php蘑菇街商城源码,php源码:dedecms精仿蘑菇街(mogujie.com)源码,时尚购物社区源码...
  10. css不能控制文字属性有什么,巧用CSS动态控制文本的属性_css
  11. MIME Types MIME 类型
  12. css显示苹方字体,苹方字体合集
  13. Ubuntu Server 20.04 LTS 安装配置 PostgreSQL
  14. Excel一键删除工作簿中所有表格中的条件格式
  15. [转帖]解决CE6和CE5在PB的Connectivity Options上的冲突
  16. WordPress限制登录次数防破解插件Limit Login Attempts Reloaded
  17. image-compressors前端图片压缩工具
  18. 使用Pandas对数据进行筛选和排序
  19. 自回归模型 java_随机天气模型及其JAVA实现-电子学报.PDF
  20. 音乐制作软件中文版-Cubase Elements 8.0.35 WiN

热门文章

  1. Rails测试《十一》添加邮件发送程序及测试邮件发送程序
  2. Notepad++ JSON关键字自动提示
  3. 开机慢 不换SSD如何提升Windows 10开机速度
  4. 微服务开发的12项要素
  5. [sh]uniq-sort-awk
  6. pybot --help
  7. C++11 并发指南一(C++11 多线程初探)
  8. OD使用教程3(中) - 调试篇03|解密系列
  9. XWork ParameterInterceptor类绕过安全限制漏洞-解决1
  10. 3COM小型企业有线局域网方案(三、四、五)