C++多态知多少?

一、什么是多态?

用一句话表述多态的话,那么应该是:同样的调用语句有不同的表现形态。
要了解和实现多态必须先要明确几个相关知识:

相关知识
1)赋值兼容(多态实现的前提)

赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。 赋值兼容是一种默认行为,不需要任何的显示的转化步骤。赋值兼容规则中所指的替代包括以下的情况:
1,派生类的对象可以赋值给基类对象。
2,派生类的对象可以初始化基类的引用。
3,派生类对象的地址可以赋给指向基类的指针。
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。

2)静态联编和动态联编

理论:
1、联编是指一个程序模块、代码之间互相关联的过程。
2、静态联编,是程序的匹配、连接在编译阶段实现,也称为早期匹配。
3、动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。
实际实现:
1、 C++与 C 相同,是静态编译型语言
2、在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
3、由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编
如图:这里写图片描述
静态多态:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
动态多态:动态绑定:在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

3)函数重写

函数重写
1,必须发生于父类与子类之间
2,并且父类与子类中的函数必须有完全相同的原型
3,使用 virtual 声明之后能够产生多态(如果不使用 virtual,那叫重定义)
重载、重写、重定向区别:
这里写图片描述

二,如何实现多态

必备条件:
1,父类中有虚函数。
2,子类 override(覆写)父类中的虚函数。
3,通过己被子类对象赋值的父类指针或引用,调用共用接口。
代码实现:(未实现多态,利用普通函数来调用)

#include<iostream>
using namespace  std;
class Base
{
public:
     void show()
    {
        cout << "Base:: show()" << endl;
    }
     void print_num()
    {
        cout << "this is a Base num" << endl;
    }
     void Add_print()
    {
        cout << "this is a Add fun in Base" << endl;
    }
    int ba;
};
class Dri:public Base
{
public:
     void show()
    {
        cout << "Dri::show()" << endl;
    }
     void print_num()
    {
        cout << "this is a Dri:: num" << endl;
    }
    void Add_print()
    {
        cout << "this is a Add fun in Dri::" << endl;
    }
int dr;
};
void Fun_call(Base &b)//实现基类引用调用
{
    b.show();
    b.print_num();
    b.Add_print();
}
int main()
{
    Base base;
    Dri dri;
    base.ba = 1;
    dri.ba = 2;
    dri.dr =3;
    Fun_call(base);
    Fun_call(dri);
    cin.get();
    return 0;
}

查看内存与输出!
这里写图片描述
可以看到,父类引用不管引用的是派生类还是父类,输出结果都是父类的函数,这种情况就是 重定向,并为构成重写。
修改代码:(实现了多态,)

#include<iostream>
using namespace  std;
class Base
{
public:
    virtual void show()
    {
        cout << "Base:: show()" << endl;
    }
    virtual void print_num()
    {
        cout << "this is a Base num" << endl;
    }
    /*virtual*/ void Add_print()
    {
        cout << "this is a Add fun in Base" << endl;
    }
    int ba;
};

class Dri:public Base
{
public:
    virtual void show()
    {
        cout << "Dri::show()" << endl;
    }
    virtual void print_num()
    {
        cout << "this is a Dri:: num" << endl;
    }
    void Add_print()
    {
        cout << "this is a Add fun in Dri::" << endl;
    }

int dr;
};
int main()
{
    Base base;
    Dri dri;
    base.ba = 1;
    dri.ba = 2;
    dri.dr =3;

    Fun_call(base);
    cout << endl;
    cout << endl;
    Fun_call(dri);
    cin.get();
    return 0;
}

这里写图片描述
从图中输出和内存布局可以看出。
在基类和派生类开头多了一个地址,查看这个地址,可以看到内存2和内存3窗口的内容,那么这个是什么呢?
这个就是虚函数表,也正是通过这个虚函数表来实现的多态。

三、虚函数表

C++的多态是通过一张虚函数表(Virtual Table)来实现的,简称为 V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆写的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

C++的编译器应该保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
编程取到虚函数表及其里面的虚函数,并且输出。


typedef void (*pfun)();

int main()
{
    Base base;
    Dri dri;
    base.ba = 1;
    dri.ba = 2;
    dri.dr =3;
    Base &b = base;
    pfun * fun = (pfun *)(*(int *)&base);
    //&base取到对象地址
    //转换为int *保证后续可以取到对象首地址四个字节内容(虚表指//针)
    //利用函数指针来遍历虚函数表
    while (*fun)
    {
        (*fun)();
        fun++;
    }
    cin.get();
    return 0;
}

这里写图片描述

图解说明(截图摘自http://coolshell.cn/articles/12165.html 这是一个很牛掰的技术控!!!建议前往看看,哈)

一般继承:(无虚函数的)
这里写图片描述
这里写图片描述
带虚函数的继承:
这里写图片描述

四、最后说一些关于多态的问题
1、构造函数中调用虚函数能实现多态吗?为什么?

1). 对象在创建的时,由编译器对VPTR指针进行初始化
2).只有当对象的构造完全结束后VPTR的指向才最终确定:
父类对象的VPTR指向父类虚函数表
子类对象的VPTR指向子类虚函数表
结论:子类对象构建时,在父类调用虚函数,不会产生多态。

2、什么函数不能声明为虚函数?

1):只有类的成员函数才能说明为虚函数
多态是依托于类的,要声明的多态的函数前提必须是虚函数。
2):静态成员函数不能是虚函数;
<1>从技术层面上说,静态函数的调用不需要传递this指针。但是虚函数的调用需要this指针,来找到虚函数表。相互矛盾
<2>从存在的意义上说,静态函数的存在时为了让所有类共享。可以在对象产生之前执行一些操作。与虚函数的作用不是一路的。
3):内联函数不能为虚函数;
内联函数属于静态联编,即内联函数是在编译期间直接展开,可以 减少函数调用的花销,即是编译阶段就确定调用哪个函数了。但是虚函数是属于动态联编,即是在运行时才确定调用哪一个函数。显然这两个是冲突的。
4):构造函数不能是虚函数;
1.每个析构函数(不加 virtual) 只负责清除自己的成员。
2.可能有基类指针,指向的确是派生类成员的情况。(这是很正常的),那么当析构一个指向派生类成员的基类指针时,程序就不知道怎么办了。 所以要保证运行适当的析构函数,基类中的析构函数必须为虚析构。
所以—————– 基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值