目录

  • 多态的定义
  • 编译时多态
  • 运行时多态
  • 虚函数的实现原理
  • 虚函数表的作用机制实例分析

什么是多态

多态时C++作为面向对象(OOP)语言的三大特性之一(其他两大特性为:封装和继承)

多态性简单概括为“一个接口,多种方法”。指的是相同的对象收到不同的消息或者是不同的对象收到相同的消息时能产生不同的动作。

多态具有三个重要的组成部分:
1.相同的函数名
2.依赖于上下文
3.具有不同的实现机制

多态的好处在于:利用尽可能少的函数名来实现了不同任务的分发,利用上下文的信息来确定实现的方式。

多态的实现方式:函数重载,模板函数和虚函数

编译时多态

指的是在编译期即确定的多态,决定了同一函数名的函数如何利用参数来进行调用。包括函数重载和模板函数

  1. 函数重载:允许不同参数的函数有相同的名字,主要是通过不同的参数列表来对函数进行重命名,从而实现多态。故函数重载的函数一定是参数列表不相同。然而有一个问题在于其无法对返回值重载。此时就需要函数模板来完成多态。
  2. 模板函数:采用了自定义类型template来定义一个函数,即函数参数的参数类型是待定的,但函数的参数个数是确定的。单单采用函数模板,并不是多态,因为虽然实现了不同类型的同函数调用,但是还是使用的是同一个模板函数的定义。核心需要使用模板特化的功能来将需要进行多态的函数来进行特化,从而实现了多态性。《小技巧:模板函数可以想普通函数一样被重载,从而充分体现多态性》

运行时多态

函数是在程序执行的过程中才能确定的要真正执行的函数。

关键概念

虚函数:基类中存在的允许在派生类中的实现和基类不同。用virtual关键字来表示。

虚函数表指针:编译器为每个带虚函数的类添加的指向虚函数地址表的指针。

虚函数表:一张记录当前所继承和自定义的虚函数的地址的表。

子类型:如果类型X扩充或实现了类型Y,那么就说X是Y的子类型,注意:子类型和继承的区别,子类型用于表达接口的兼容性,当B是A的子类型时,意味着所有对A的操作都可以对B进行,继承倾向于表达实现的重用,即B重用A的操作来实现自己的操作。

向上转型:把一个子类型的对象转换为父类型的对象。向上转型中,需要注意的三点。向上转型是安全的。第二,向上转型可以自动完成。第三,向上转型的过程中会丢失子类型信息。

多态的实现

基类中声明有待virtual关键字的函数,派生类对该函数进行了重载,在使用基类指针指向派生类时,即可调用派生类所实现的虚函数内容从而实现了多态。

为什么运行时多态不能在编译期确定?

你永远也不知道运行中所提供的到底是什么类型的对象,只能知道可以使用基类指针来指向这个对象。

//比如此函数,基类指针的指向只由code来决定,而编译期是无法决定这个函数基类指针的指向的,只有在运行时,对于每个给定的code来动态的返回对应的派生类对象。
base* do_something(int code) {
    switch(code){
        case 1:
            return new A();
        case 2:
            return new B();
        default;
            return nullptr;
    }
}

为什么需要使用指针或者引用来实现运行时多态

考虑有如下函数

class b{
public:
    virtual void print(){cout<<"here is the base"<<endl;}
};
class b1: public b{
public:
    void print(){cout<<"here is the b1"<<endl;}
};
class b2:public b{
public:
    void print(){cout<<"here is the b2"<<endl;}
};
void print_result(b a){
    a.print();
}
void print_result_ref(b& a){
    a.print();
}
int main() {
    b1 a;
    b2 b;
    print_result(a);
    print_result(b);
    print_result_ref(a);
    print_result_ref(b);
}

其输出结果为:

here is the base
here is the base
here is the b1
here is the b2

转入为一个派生类对象的拷贝,得到的却是基类虚函数的内容,而明明这里派生类重新定义了基类的虚函数,应该返回派生类的内容才对,其实这里发生了子类型的向上转型,将派生类隐式转换成了基类的对象,删除了派生类多余基类的内容,从而只能调用基类的函数和其他内容。而当使用引用时,使用的是指针,C++运行使用基类指针指向派生类对象,同时根据虚函数表查表获取真正的虚函数的地址,从而得到了派生类的输出结果。

为什么析构函数需要写成虚函数?

为了防止内存泄漏,当用基类指针指向派生类对象时,销毁对象的过程只会调用基类的析构函数,故会存在值析构了基类的内容,而没有析构派生类的专属内容,只有将基类的析构函数也声明为虚函数,析构时会调用派生类的析构函数。

虚函数的实现原理

利用函数表指针指向一个虚函数表的起始位置,虚函数表存放着该类中的虚函数指针,调用时可以找到虚函数表指针,通过虚函数表指针找到虚函数表,在利用虚函数表的偏移找到函数的入口,从而找到要使用的虚函数
当实例化一个基类的子类对象时,若子类中没有定义虚函数,而继承了父类的虚函数,子类对象也会具有一个虚函数表,虚函数表里的函数地址与父类是一样的。如果子类定义了从父类继承的虚函数,子类的就会覆盖掉父类的函数指针

虚函数表作用机制的分析

//基类
class base{
public:
    base(long m1 = 1, long m2 = 2):m1(m1),m2(m2){};
    virtual void virtualbase1() {
        std::cout<<"this is the base1 vitual funciton"<<endl;
    }
    virtual void virtualbase2() {
        std::cout<<"this is the base2 vitual funciton"<<endl;
    }
    virtual void virtualbase4() {
        std::cout<<"this is the base3 vitual funciton"<<endl;
    }
private:
    long m1;
    long m2;
};
//派生类
class base1 : public base{
    void virtualbase2() {
        std::cout<<"this is the base from subclass virtual function"<<endl;
    }
};

基类中包含两个变量m1,m2和三个虚函数
派生类中包含一个基类虚函数的重新实现

int main(){

    base b;

    long * bAddress = (long *)&b;

    //输出base类的地址
    cout<<"bAddress: "<<bAddress<<endl;
    //输出虚拟表指针的地址
    long * vtptr = (long *)*(bAddress + 0);
    cout<<"\t vtptr:"<<vtptr<<endl;
    //输出虚拟表中第一个虚拟函数的地址
    long *pfunc1 = (long *)*(vtptr + 0);
    cout<<"\t vfunc1:"<<pfunc1<<endl;
    //输出虚拟表中第二虚拟函数的地址
    long *pfunc2 = (long *)*(vtptr + 1);
    cout<<"\t vfunc2:"<<pfunc2<<endl;
    //输出虚拟表中第三个虚拟函数的地址
    long *pfunc3 = (long *)*(vtptr + 2);
    cout<<"\t vfunc3:"<<pfunc3<<endl;
    //利用对象的地址偏移获取私有对象m1的内容
    long m1 = (long)*(bAddress + 1);
    cout<<"m1: "<<m1<<endl;
    //同理获取私有对象m2的内容
    long m2 = (long)*(bAddress + 2);
    cout<<"m2: "<<m2<<endl;

    //将指针转换为函数指针
    void (* pfunc1_fun)() = (void (*)()) pfunc1;
    //获取函数的结果
    pfunc1_fun();

    //新建派生类对象b1
    base1 b1;
    //获取对象的地址
    long * b1Address = (long *) &b1;
    cout<<"b1Address: "<<b1Address<<endl;
    //获取虚拟表指针的地址
    long * b1vtptr = (long *)*(b1Address + 0);
    cout<<"\t b1vtptr: "<<b1vtptr<<endl;
    //获取虚拟表中函数的地址
    long * b1pfunc1 = (long *)*(b1vtptr + 0);
    cout<<"\t b1pfunc1: "<<b1pfunc1<<endl;
    //获取虚拟表中函数的地址
    long * b2pfunc2 = (long *)*(b1vtptr + 1);
    cout<<"\t b1pfunc2: "<<b2pfunc2<<endl;
    //获取虚拟表中函数的地址
    long * b3pfunc3 = (long *)*(b1vtptr + 2);
    cout<<"\t b1pfunc3: "<<b3pfunc3<<endl;
    //将指针转换为函数指针
    void(* b2pfunc2_fun)() = (void(*)()) b2pfunc2;
    //获取函数的结果
    b2pfunc2_fun();
    return 0;
}

运行以上函数可以得到结果:

bAddress: 0x7ffeeb90f9f0
     vtptr:0x1042f7528
     vfunc1:0x1042f3a50
     vfunc2:0x1042f3a90
     vfunc3:0x1042f3ad0
m1: 1
m2: 2
this is the base1 vitual funciton
b1Address: 0x7ffeeb90f998
     b1vtptr: 0x1042f7560
     b1pfunc1: 0x1042f3a50
     b1pfunc2: 0x1042f3b60
     b1pfunc3: 0x1042f3ad0
this is the base from subclass virtual function

通过结果可以看出,当派生类重新定义了基类的函数后其虚函数表中的指针发生了覆盖,而没有重新定义的地方则维持了基类虚函数的地址。


zer0like
1 声望0 粉丝

no bug no life


« 上一篇
单词翻转