{"id":"58954469","type":"page","status":"current","title":"08.多态","version":{"by":{"type":"known","username":"ethanliu","userKey":"2c90931b6cfb654f016d5c69becc0001","profilePicture":{"path":"/images/icons/profilepics/default.svg","width":48,"height":48,"isDefault":true},"displayName":"刘浩","_links":{"self":"https://wiki.suncaper.net/rest/api/user?key=2c90931b6cfb654f016d5c69becc0001"},"_expandable":{"status":""}},"when":"2022-07-09T01:51:24.000Z","message":"","number":1,"minorEdit":false,"hidden":false,"_links":{"self":"https://wiki.suncaper.net/rest/experimental/content/58954469/version/1"},"_expandable":{"content":"/rest/api/content/58954469"}},"body":{"storage":{"value":"

4.8 多态

4.8.1 多态基本概念

多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。

多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。

c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。

静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)

cppmA = a;\n\t}\n\tvoid setB(int b){\n\t\tthis->mB = b;\n\t}\n\tvoid setOperator(string oper){\n\t\tthis->mOperator = oper;\n\t}\n\tint getResult(){\n\t\t\n\t\tif (this->mOperator == \"+\"){\n\t\t\treturn mA + mB;\n\t\t}\n\t\telse if (this->mOperator == \"-\"){\n\t\t\treturn mA - mB;\n\t\t}\n\t\telse if (this->mOperator == \"*\"){\n\t\t\treturn mA * mB;\n\t\t}\n\t\telse if (this->mOperator == \"/\"){\n\t\t\treturn mA / mB;\n\t\t}\n\t}\nprivate:\n\tint mA;\n\tint mB;\n\tstring mOperator;\n};\n\n//这种程序不利于扩展,维护困难,如果修改功能或者扩展功能需要在源代码基础上修改\n//面向对象程序设计一个基本原则:开闭原则(对修改关闭,对扩展开放)\n\n//抽象基类\nclass AbstractCaculator{\npublic:\n\tvoid setA(int a){\n\t\tthis->mA = a;\n\t}\n\tvirtual void setB(int b){\n\t\tthis->mB = b;\n\t}\n\tvirtual int getResult() = 0;\nprotected:\n\tint mA;\n\tint mB;\n\tstring mOperator;\n};\n\n//加法计算器\nclass PlusCaculator : public AbstractCaculator{\npublic:\n\tvirtual int getResult(){\n\t\treturn mA + mB;\n\t}\n};\n\n//减法计算器\nclass MinusCaculator : public AbstractCaculator{\npublic:\n\tvirtual int getResult(){\n\t\treturn mA - mB;\n\t}\n};\n\n//乘法计算器\nclass MultipliesCaculator : public AbstractCaculator{\npublic:\n\tvirtual int getResult(){\n\t\treturn mA * mB;\n\t}\n};\n\nvoid DoBussiness(AbstractCaculator* caculator){\n\tint a = 10;\n\tint b = 20;\n\tcaculator->setA(a);\n\tcaculator->setB(b);\n\tcout << \"计算结果:\" << caculator->getResult() << endl;\n\tdelete caculator;\n}]]>

4.8.2 向上类型转换及问题

4.8.2.1 问题抛出

对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。

也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。

cpp

 

运行结果: 动物在唱歌

问题抛出: 我们给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是Dog::speak

 

4.8.2.1 问题解决思路

解决这个问题,我们需要了解下绑定(捆绑,binding)概念。

把函数体与函数调用相联系称为绑定(捆绑,binding)

 

当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。

上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speakAnimal::speak的,而不是真正传入的对象Dog::speak

解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。

C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。对于一种编译语言,编译器并不知道实际的对象类型(编译器并不知道Animal类型的指针或引用指向的实际的对象类型)。

4.8.2.1 问题解决方案(虚函数,vitual function)

C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。

对于特定的函数进行动态绑定,c++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用.

  • 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.
  • 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual.
  • 在派生类中virtual函数的重定义称为重写(override).
  • Virtual关键字只能修饰成员函数.
  • 构造函数不能为虚函数

 

注意: 仅需要在基类中声明一个函数为virtual.调用所有匹配基类声明行为的派生类函数都将使用虚机制。虽然可以在派生类声明前使用关键字virtual(这也是无害的),但这个样会使得程序显得冗余和杂乱。(我建议写上)

cpp

4.8.3 C++如何实现动态绑定

动态绑定什么时候发生?所有的工作都是由编译器在幕后完成。当我们告诉通过创建一个virtual函数来告诉编译器要进行动态绑定,那么编译器就会根据动态绑定机制来实现我们的要求, 不会再执行早绑定。


问题:C++的动态捆绑机制是怎么样的?

首先,我们看看编译器如何处理虚函数。当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。

验证对象中的虚指针:

cpp

在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。

起初,子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的虚函数表。

  • 396px当子类无重写基类虚函数时:

 

过程分析:

     Animal* animal = new Dog;

     animal->fun1();

     当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是基类的func1.

执行结果: 我是基类的func1

测试结论: 无重写基类的虚函数,无意义

  • 当子类重写基类虚函数时:

 

过程分析:

     Animal* animal = new Dog;

     animal->fun1();

     当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,由于子类重写基类的func1函数,所以调用func1时,调用的是子类的func1.

执行结果: 我是子类的func1

测试结论: 无重写基类的虚函数,无意义

 

多态的成立条件:

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

4.8.4 抽象基类和纯虚函数(pure virtual function)

在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。

 

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

  • 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
    • 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
    • Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。

 

   建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类.

4.8.5 纯虚函数和多继承

多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。

绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,c++中没有接口的概念,但是可以通过纯虚函数实现接口。

 

    接口类中只有函数原型定义,没有任何数据定义。

 

多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。

注意:除了析构函数外,其他声明都是纯虚函数。

        

4.8.6 虚析构函数

4.8.6.1 虚析构函数作用

虚析构函数是为了解决基类指针指向派生类对象,并用基类的指针删除派生类对象。

cpp~People();\n}]]>

4.8.6.2 纯虚析构函数

纯虚析构函数在c++中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体。

那么问题是:如果给虚析构函数提供函数体了,那怎么还能称作纯虚析构函数呢?

纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象。

cpp

如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数。

4.8.7 重写 重载 重定义

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


cpp


","representation":"storage","_expandable":{"content":"/rest/api/content/58954469"}},"_expandable":{"editor":"","view":"","export_view":"","styled_view":"","anonymous_export_view":""}},"extensions":{"position":"none"},"_links":{"webui":"/pages/viewpage.action?pageId=58954469","edit":"/pages/resumedraft.action?draftId=58954469","tinyui":"/x/5ZKDAw","collection":"/rest/api/content","base":"https://wiki.suncaper.net","context":"","self":"https://wiki.suncaper.net/rest/api/content/58954469"},"_expandable":{"container":"/rest/api/space/2CT","metadata":"","operations":"","children":"/rest/api/content/58954469/child","restrictions":"/rest/api/content/58954469/restriction/byOperation","history":"/rest/api/content/58954469/history","ancestors":"","descendants":"/rest/api/content/58954469/descendant","space":"/rest/api/space/2CT"}}