深入了解C++多态的原理及实现方式

前言

需要深入了解C/C++语言的基础之上再看此文章。

关于多态

具有多种形态,调用同一个方法会随上下文不同而产生不同的结果,多态有静态多态与动态多态两种。

函数承载方式

函数重载是让同一个函数(函数名字是相同的)可以根据参数不同从而实现不同的处理,之所以称之为函数重载,是因为它有多个同名的函数,而编译器进行了重载,编译器根据实参与形参的类型及个数,自动确定调用那一个函数。这是通过函数重载的方式实现多态,这种实现的方式是静态的多态,因为在编译阶段就已经知道了的。

比如以下代码,通过重载Base类中的func方法,达到不同的参数产生不同的结果:

#include <iostream>

class Base {
public:
    Base(void){};
    ~Base(){};

    void func() { 
        std::cout << "Base func()" << std::endl;
    };
    void func(int a) {
        std::cout << "Base func(int a)" << std::endl;
    };
};

int main(void)
{
    Base base;
    base.func();      
    base.func(2);  

    return 0;
}

// 运行输出

Base func()
Base func(int a)

当然,也可以在子类中重载基类的函数方法,比如在基类Bash中派生出一个子类Subclass,重载func函数:

class Subclass : public Base {
public:
    Subclass(void) {};
    ~Subclass() {};

    using Base::func;       // 将基类的func所有重载实例到包含到Subclass中
    void func(std::string s) { 
        std::cout << "Subclass func(string s)" << std::endl;
    };
};

这样子在子类中也可以直接通过重载使用基类的函数:

#include <iostream>

class Base {
public:
    Base(void) {};
    ~Base() {};

    void func() { 
        std::cout << "Base func()" << std::endl;
    };
    void func(int a) {
        std::cout << "Base func(int a)" << std::endl;
    };
};

class Subclass : public Base {
public:
    Subclass(void) {};
    ~Subclass() {};

    using Base::func;
    void func(std::string s) { 
        std::cout << "Subclass func(string s)" << std::endl;
    };
};

int main(void)
{
    Base base;
    Subclass sub;
    
    sub.func();
    sub.func(2);
    sub.func("666");

    return 0;
}

// 运行结果
Base func()
Base func(int a)
Subclass func(string & s)

虚函数方式

当类中存在虚函数时,编译器会在类中自动生成一个虚函数表,虚函数表是一个存储类成员函数指针的数据结构,编译器会为对象自动生成一个指向虚函数表的指针(通常称之为 vptr 指针),它由编译器自动生成和维护,通过C++关键字 virtual 修饰的成员函数会被编译器放入虚函数表中。虚函数表是和类对应的,虚函数表指针是和对象对应的,而对象在运行过程中会被赋值或者是强转,因此这种实现的方式是动态的,因为虚函数表指针是可以被修改的,当父类中的虚函数表的指针被子类的虚函数表指针替换掉,那么在调用这个类的方法(虚函数)就会指向实际子类实现的方法,因此实现了多态。

那么如何定位虚函数表呢?编译器另外还为每个对象提供了一个虚函数表指针(即vptr),这个指针指向了对象所属类的虚函数表,在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向了所属类的虚函数表,从而在调用虚函数的时候,能够找到正确的函数。

正是由于每个对象调用的虚函数都是通过虚函数表指针来索引的,也就决定了虚函数表指针的正确初始化是非常重要的,换句话说,在虚函数表指针没有正确初始化之前,我们不能够去调用虚函数,那么虚函数表指针是在什么时候,或者什么地方初始化呢?这其实是在构造函数中进行虚函数表的创建和虚函数表指针的初始化,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承者,它初始化父类对象的虚函数表指针,该虚函数表指针指向父类的虚函数表,当执行子类的构造函数时,子类对象的虚函数表指针被初始化,指向自身的虚函数表

总的来说:虚函数表可以继承,如果子类没有重写虚函数,那么子类虚函数表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现,如果基类有3个虚函数,那么基类的虚函数表中就有3项(虚函数地址),派生类也会有虚函数表,至少有3项,如果重写了相应的虚函数,那么虚函数表中的地址就会改变,指向自身的虚函数实现,如果派生类有自己的虚函数,那么虚函数表中就会添加该项。

比如以下的代码,创建一个父类,父类中包含一个虚函数Say(),同时再定义一个子类Son,子类中实现了Say()函数方法:

#include <iostream>

class Father {
public:
    Father(void) {};
    ~Father() {};
    
    virtual void Say() {
        std::cout << "father say hello!" << std::endl;
    };
};

class Son : public Father {
public:
    Son(void) {};
    ~Son() {};
    
    void Say(void) {
        std::cout << "son say hello!" << std::endl;
    };
};

int main(void)
{
    using namespace std;

    Father father;
    Son son;
    Father *father1 = &son; // 强制转换类型

    father.Say();
    son.Say();

    father1->Say();
}

// 运行结果

father say hello!
son say hello!
son say hello!

通过这个结果,我们发现是"son say hello",也就说它其实已经根据对象的类型调用了正确的函数,这就是虚函数方式实现了C++的多态性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章