C++多态(一)对象类型与虚函数

来源:互联网 发布:美食句子 知乎 编辑:程序博客网 时间:2024/04/29 17:38

多态适合继承有密不可分的关系的,我在前面写了几篇关于继承的博文:
C++继承(一) 常用的继承方式;
C++继承(二)派生类的默认函数;
C++继承(三)通过菱形继承看virtual继承;
如果在看多态时,关于继承的问题可以返回来看看;

一、对象的类型

在C++prime中写道>当我们使用存在继承关系的类型时,必须讲一个变量或其他表达式的静态类型与该表达式表示的动态类型区分开来。表达式的静态类型在编译时总是已知的,它是变量声明的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象模型,动态类型直到运行时才知道。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
这里写图片描述

class Base{};class Derived1:public Base {};class Derived2:public Base{};int main(){    Derived1* pD1 = new Derived1;    Base* pBase = pD1;    Derived2* pD2 = new Derived2;    pBase = pD2;    return 0;}

对各个变量的分析:
这里写图片描述
上面的动态类型能变是因为他们进行了赋值运算;

二、多态

什么是多态:顾名思义,多态的意思是一个事物有多种形态。
在面向对象方法中一般是这样表述多态的:向不同的对象发送同一个消息,不同的对象在接受时会产生不同的行为(方法)
这里写图片描述
多态分为静态多态和动态多态;

1、静态多态(早绑定)

:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用函数,否则出现编译错误。

2、动态多态(晚绑定)

:在函数执行期间(非编译期间)判断所引用对象的实际类型,根据其实际类型调用相应的方法;
动态绑定的条件
1、在基类中必须是虚函数,并且在派生类中重写基类的虚函数;
2、通过基类类型的引用或者指针调用虚函数

class Base{public:    virtual void FunTest()//基类中的虚函数    {        cout << "Base::FunTest()" << endl;    }    int _pub;};class Derived :public Base{public:    virtual void FunTest()//在派生类中的重写    {        cout << "Derived::FunTest()" << endl;    }};int main(){    Base b;    return 0;}
(1)虚函数

在动态多态中最最重要的就是虚函数:在基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数。
表示方法:
virtual 函数返回类型 函数名(参数表){函数体};

实现多态性。实质上是指通过指向派生类的基类指针或引用,访问派生类中同名覆盖函数。如果基类中没有关键字virtual,程序将根据引用类型或指针类型选择方法。

class Base{public:    void FunTest()    {        cout << "Base::FunTest()" << endl;    }};class Derived:public Base{public:    void FunTest()    {        cout << "Derived::FunTest()" << endl;    }};int main(){    Base b;    Derived d;    Base *pb = &d;    pb->FunTest();//父类的指针或者引用可以直接访问子类的对象    return 0;}

这里写图片描述
在上面程序中Base中没有virtual关键字,所以程序是根据指针类型选择的方法,在上面代码中pb是Base类的,所以访问的是Base类中的函数
当我们加上virtual关键字时:

class Base{public:    virtual void FunTest()    {        cout << "Base::FunTest()" << endl;    }};class Derived:public Base{public:    void FunTest()    {        cout << "Derived::FunTest()" << endl;    }};int main(){    Base b;    Derived d;    Base *pb = &d;    pb->FunTest();//父类的指针或者引用可以直接访问子类的对象    return 0;}

这里写图片描述
当我在基类的FunTest()函数上加virtual关键字时,Base类型的指针变量pb,指向了Derived的对象,程序在运行的时候输出的是派生类的函数;
总结一下
1、如果在基类中定义了虚函数,那么派生类中的同名函数将自动变为虚函数,但是我们可以在派生类同名函数前也加上virtual关键字(可以不加,建议加上),这样可以增加代码的可读性。
2、如果要在派生类中重新定义基类的方法,通常应将基类方法声明为virtual的。这样程序将根据对象类型而不是引用或指针的类型来选择方法也就是函数的版本;
3、必须是指针或引用调用方法的时候用虚函数

(2)虚函数的工作原理

通常,编译器处理虚函数的方法是:
1、给每个对象添加一个隐藏成员;
2、隐藏成员中保存了一个指向函数地址数组的指针(虚函数表);
3、虚函数表中存储了为类对象进行声明的虚函数的地址;

class Base{public:    int b;    virtual void FunTest1()    {        cout << "Base::FunTest1()" << endl;    }    virtual void FunTest2()    {        cout << "Base::FunTest2()" << endl;    }};class Derived :public Base{public:    int d;};int main(){    Base b;    b.b = 1;    Derived d;    d.b = 2;    d.d = 3;    return 0;}

我们来通过这个例子认识一下虚函数的构造:

Base b;

的内存分布:

这里写图片描述
在内存1中我们可以看到的是:
第一行是一段地址(虚函数的地址);
第二行是01,是Base 中b的值为1;
第三行不属于Base类

在内存2中可以看到2行的地址和一段全是0的,其中最后一行全为0表示:
虚表已经结束(其实编译器给每个虚表最后都会加4个字节的0,用来表示虚表结束)

所以Base类的模型如下图:
这里写图片描述

Derived d;

这里写图片描述
在Derived的内存表中我们可以看到和Base同样的东西,可以看到的是在虚表中2个函数的地址是完全一样的;

这里写图片描述

(3)虚函数的注意事项

1、构造函数不能为虚函数;
2、静态成员函数不能为虚函数(没有this指针,没有对象)
3、友元函数不能为虚函数(没有this指针,没有对象)
4、赋值运算符的重载可以是虚函数(不建议,但也没错)
5、如果基类中有虚函数,则析构函数一定给虚函数(不然内存泄漏)

(4)纯虚函数

在成员函数(必须为虚函数)的形参列表后面写上=0,则成员函数为虚函数。包含纯虚函数的类叫做抽象类(也是接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例出处对象。

class Bsae{    virtual void FunTest() = 0;//纯虚函数};
(5)虚函数的总结

1、派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值必须完全相同。(协变除外(协变:基类/派生类中的返回值类型是基类/派生类的指针或引用));
2、基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性;
3、只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数;
4、如果类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加;
5、构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但是最好不要这么做,使用时容易混淆;
6、不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为;
7、最好将基类的析构函数声明为虚函数,(析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里编译器做了特殊处理)
8、虚表是所有类对象实例共用的;

1 0
原创粉丝点击