C++面向对象多态的实现及原理理解


多态与递归类似,不管是书中还是老师授课,都把其讲得神乎其神,让读者一头雾水,莫名其妙。
多态实际上非常简单,学习的难点在于在接触多态之前,缺乏一个感性的认识。
多态允许将子类的对象当作基类的对象使用,某基类型的引用指向其子类型的对象,调用的方法是该子类型的方法。
这里引用和调用方法在代码编译前就已经决定了,而引用所指向的对象可以在运行期间动态绑定。再举个比较形象的例子:
比如有一个函数是叫某个人来吃饭,函数要求传递的参数是人的对象,可是来了一个美国人,你看到的可能是用刀和叉子在吃饭,
而来了一个中国人你看到的可能是用筷子在吃饭,这就体现出了同样是一个方法,可以却产生了不同的形态,这就是多态!

多态的意义:
增加了程序的可拓展性,实现了模块之间的解耦;
1. 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。//继承
2. 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 //多态的真正作用,以前需要用switch实现

多态的表现(效果):
一个调用语句可以出现/实现多种形态.
比如,在发生多态时,如果传入参数是一个子对象,那么会调用子对象的成员方法,如果传入参数是一个父对象,那么会调用父对象的成员方法.

C++中多态的实现:
C++中多态的实现需要三个条件.
1. 存在继承关系
2. 虚函数(带virtual关键字的函数);虚函数对多态的实现是必要的,遇到这个处理过程要等到执行时再确定到底调用哪个类的处理过程;
3. 子类重写基类虚函数,基类指针或引用指向子类对象.

C++面向对象三大特性分别是封装,继承,多态;
封装 把过程和数据封装起来,对数据的访问只能通过已定义的接口;封装可以隐藏实现细节,使得代码模块化;
继承 使子类可以使用基类的属性和方法,增加了代码的可复用性;
多态 使基类对象可以使用子类对象的功能,实现了解耦;使之前的代码可以执行后来人写的代码,增加了程序的可拓展性和可维护性;

C++实现多态原理:
C++编译器为含有虚函数的类对象提前布局vptr指针,生成虚函数表;当发生多态时(虚函数调用时),去虚函数表中查找调用地址(函数的入口地址);
如果用面向过程的实现就是在接口中使用typeof判断入参并使用switch语句处理不同的类型;而C++直接使用虚函数就可以实现动态绑定;

多态的分类(Java)
1)编译时多态,即方法的重载,从JVM的角度来讲,这是一种静态分派(static dispatch)
2)运行时多态,即方法的重写,从JVM的角度来讲,这是一种动态分派(dynamic dispatch)

从使用角度看多态无非就是三句话:
比如我们有一个基类 Basic 并定义了虚方法,有一个子类 SubClass 也实现了自己的方法;
1)向上转型是自动的。即Basic b = new SubClass()是自动的,不需要强转;
2)向下转型需要强转。即SubClass s = new Basic()是无法编译通过的,必须要SubClass s = (SubClass)new Basic(),让基类知道它要转成具体哪个子类;
3)基类引用指向子类对象,子类重写了基类的方法,在调用基类的方法时,实际调用的是子类重写的基类的该方法;
  即 Basic b = new SubClass(); b->toString();实际上调用的是SubClass中的toString()方法;
多态性往往用于面向对象中抽象和具体类的层次设计中,好处就在于提供系统的弹性,避免代码的僵化;
例如,可以增加一个新的子类而不需要修改原代码,避免程序中的复杂的条件分析语句;也就是面向对象中的“对扩展开放,对修改封闭”的原则;

案例
// 多态
// 多态现象:同一个调用语句 可以有多种形态 扔过来一个子类对象,执行子类的API函数 扔过来一个基类对象,执行基类API函数
// 多态成立的三个条件:继承、基类中定义了虚函数、子类重写基类的虚函数 使用时:基类指针(引用)指向子类对象
// 效果:同样一个调用语句可以有多种形态(多种调用方法)
#include <iostream>
using namespace std;
class BasicClass {
public:
    BasicClass() { cout << "Basic Class init" <<endl; }
    virtual void work() { cout << "Basic Class working" <<endl; }
    virtual ~BasicClass() { cout << "Basic Class deinit" <<endl; }
};
class SubClass: public BasicClass {
public:
    SubClass() { cout << "\tSub Class init" <<endl; }
    virtual void work() { cout << "\tSub Class working" <<endl; }
    virtual ~SubClass() { cout << "\tSub Class deinit" <<endl; }
};
class SubSubClass: public SubClass {
public:
    SubSubClass() { cout << "\t\tSub-Sub Class init" <<endl; }
    virtual void work() { cout << "\t\tSub-Sub Class working" <<endl; }
    ~SubSubClass() { cout << "\t\tSub-Sub Class deinit" <<endl; }
};

/* 当调用how2Work()函数时表现多态性,根据入参不同调用不同的方法 */
/* 如果父类没有声明为virtual即没有实现多态,传入子类对象时就不会调用子类的方法 */
void how2Work(BasicClass *base)
{
    base->work();
}
int main(void)
{
#ifdef CASE1
    BasicClass *person = NULL;
    person = new BasicClass();
    person->work();
    delete person;
    person = NULL;
    person = new SubClass();
    person->work();
    delete person;
    person = NULL;
    person = new SubSubClass();
    person->work();
    delete person;
    person = NULL;
    /* 虽然都是person->work()结果却有不同的表现 */
#else
    /* 多态的使用最多的是下面这种方式 */
    BasicClass bc;
    SubClass sc;
    SubSubClass ssc;
    how2Work(&bc);
    how2Work(&sc);
    how2Work(&ssc);
#endif
    return 0;
}

C++的多态性用一句话概括就是:
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数;

1)用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。  
2)存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。  
3)多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。  
4)多态用虚函数来实现,结合动态绑定; 
5)纯虚函数是虚函数再加上 = 0;
6)抽象类是指包括至少一个纯虚函数的类;纯虚函数:virtual void fun()=0;必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容;

我们先看个例子
#include "stdafx.h"
#include <iostream> 
#include <stdlib.h>
using namespace std; 
class Basic
{
public:
    void Face()
    {
        cout << "Basic's face" << endl;
    }
    void Say()
    {
        cout << "Basic say hello" << endl;
    }
};
class Sub:public Basic
{
public:     
    void Say()
    {
        cout << "Sub say hello" << endl;
    }
};
void main()
{
    Sub sub;
    Basic *pBasic = &sub; // 隐式类型转换
    pBasic->Say();
}

输出的结果为:
Basic say hello

我们在main()函数中首先定义了一个Sub类的对象sub,接着定义了一个指向Basic类的指针变量pBasic,然后利用该变量调用pBasic->Say();
估计很多人往往将这种情况和c++的多态性搞混淆,认为sub实际上是Sub类的对象,应该是调用Sub类的Say,输出"Sub say hello",然而结果却不是;

从编译的角度来看:
c++编译器在编译的时候,要确定每个对象调用的函数(非虚函数)的地址,这称为早期绑定,
当我们将Sub类的对象sub的地址赋给pBasic时,c++编译器进行了类型转换,此时c++编译器认为变量pBasic保存的就是Basic对象的地址,
当在main函数中执行pBasic->Say(),调用的当然就是Basic对象的Say()函数

从内存角度看:
sub类所占用内存 = { Basic部分所占用内存 + Sub自身增加部分所占用内存 };
                
Sub类对象的内存模型如上图
我们构造Sub类的对象时,首先要调用Basic类的构造函数去构造Basic类的对象,然后才调用Sub类的构造函数完成自身部分的构造,
从而拼接出一个完整的Sub类对象。当我们将Sub类对象转换为Basic类型时,该对象就被认为是原对象整个内存模型的上半部分,
也就是上图中“Basic部分所占用内存”,那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法,
因此,输出“Basic Say hello”,也就顺理成章了;

在上面的代码中,我们知道pBasic实际上指向的是Sub类的对象,我们希望输出的结果是sub类的Say方法,那么想到达到这种结果,就要用到虚函数了。
前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用晚绑定,
当编译器使用晚绑定时候,就会在运行时再去确定对象的类型以及正确的调用函数,而要让编译器采用晚绑定,
就要在基类中声明函数时使用virtual关键字,这样的函数我们就称之为虚函数,一旦某个函数在基类中声明为virtual,
那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。

代码稍微改动一下,看一下运行结果

#include "stdafx.h"
#include <iostream> 
#include <stdlib.h>
using namespace std; 

class Basic
{
public:
    void Face()
    {
        cout << "Basic's face" << endl;
    }

    /* say()方法声明为virtual方法 */
    virtual void Say()
    {
        cout << "Basic say hello" << endl;
    }
};


class Sub:public Basic
{
public:     
    void Say()
    {
        cout << "Sub say hello" << endl;
    }
};

void main()
{
    Sub sub;
    Basic *pBasic=&sub; // 隐式类型转换
    pBasic->Say();
}
输出的结果为:
Sub say hello

我们发现结果是"Sub say hello"也就是根据对象的类型调用了正确的函数,那么当我们将Say()声明为virtual时,背后发生了什么。

编译器在编译的时候,发现Basic类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即 vtable),该表是一个一维数组,
在这个数组中存放每个虚函数的地址,
Basic类和Sub类的虚表:
Basic类的vtable: &Basic::say() --> Basic::say();
Sub类的vtable: &Sub::say() --> Sub::say();


那么如何定位虚表呢?编译器另外还为每个对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表,在程序运行时,
根据对象的类型去初始化vptr,从而让vptr正确的指向了所属类的虚表,从而在调用虚函数的时候,能够找到正确的函数,
对于第二段代码程序,由于pBasic实际指向的对象类型是Sub,因此vptr指向的Sub类的vtable,当调用pBasic->Sub()时,
根据虚表中的函数地址找到的就是Sub类的Say()函数.

正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的,换句话说,
在虚表指针没有正确初始化之前,我们不能够去调用虚函数,那么虚表指针是在什么时候,或者什么地方初始化呢?

答案是在构造函数中进行虚表的创建和虚表指针的初始化,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,
并不知道后面是否还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表,当执行子类的构造函数时,
子类对象的虚表指针被初始化,指向自身的虚表。

总结(基类有虚函数的):
1)每一个类都有虚表;
2)虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现,
  如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,
  那么虚表中的地址就会改变,指向自身的虚函数实现,如果派生类有自己的虚函数,那么虚表中就会添加该项;
3)派生类的虚表中虚地址的排列顺序和基类的虚表中虚函数地址排列顺序相同;

这就是c++中的多态性,当c++编译器在编译的时候,发现Basic类的Say()函数是虚函数,这个时候c++就会采用晚绑定技术,
也就是编译时并不确定具体调用的函数,而是在运行时依据对象的类型来确认调用的是哪一个函数,这种能力就叫做c++的多态性;
我们没有在Say()函数前加virtual关键字时,c++编译器就确定了哪个函数被调用,这叫做早期绑定;

c++的多态性就是通过晚绑定技术来实现的;
c++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,
如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数;

虚函数是在基类中定义的,目的是不确定它的派生类的具体行为,例如:
定义一个基类class Animal //动物,它的函数为breathe()
再定义一个类class Fish //鱼。它的函数也为breathe()
再定义一个类class Sheep //羊,它的函数也为breathe()
将Fish,Sheep定义成Animal的派生类,然而Fish与Sheep的breathe不一样,一个是在水中通过水来呼吸,一个是直接呼吸;所以基类不能确定该如何定义breathe();
所以在基类中只定义了一个virtual breathe()=0,它是一个空的虚函数,具体的函数在子类中分别定义;程序运行时找到类,如果它有基类,再找到它的基类,
最后运行的是基类中的函数,这时它在基类中找到的是virtual标识的函数,它就会再回到子类中找同名函数;这就是虚函数的产生,和类的多态性的体现;
这里的多态性是指类的多态性。

函数的多态性(重载)是指一个函数被定义成多个不同参数的函数。当你调用这个函数时,就会调用不同的同名函数。
一般情况下(不涉及虚函数),当我们用一个指针/引用调用一个函数的时候,被调用的函数是取决于这个指针/引用的类型。

当涉及到多态性的时候,采用了虚函数和动态绑定,此时的调用就不会在编译时候确定而是在运行时确定。
不再单独考虑指针/引用的类型而是看指针/引用的对象的类型来判断函数的调用,根据对象中虚指针指向的虚表中的函数的地址来确定调用哪个函数;

参考资料
https://blog.csdn.net/u011616739/article/details/75142636
https://www.cnblogs.com/cxq0017/p/6074247.html
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值