面向对象三大特性之一 多态

首先,我们看一下百科的解释:

多态(Polymorphism)按字面的意思就是"多种状态"。在面向对象语言中,接口的多种不同的实现方式即为多态。

通俗的讲,就是同一个东西表现出多种状态,在面向对象的描述中就是同一个函数接口,实现多种不同的表现方式。

这里写图片描述

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

【动态多态】
动态绑定:在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方
法。
使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,派生类需要重新实现,编译器将实’现动态绑定。

【动态绑定条件】
1、必须是虚函数,派生类中必须重写虚函数。
2、通过基类类型的引用或者指针调用虚函数

小结:通过基类的指针或引用调用虚函数()派生类中对该虚函数进行重写),调用基类还是派生类的虚函数,要在运行时根据指针/引用的类型确定。

1、必须是虚函数,派生类中必须重写虚函数。
示例1:

#include<iostream>

using namespace std;


//实现多态

class Base
{
public:
    Base()
        :_b(1)
    {
        cout << "Base::Base()" << endl;
    }
     void Show()
    {
        cout << _b<<endl;
    }

private:
    int _b;
};


class Derived:public Base
{
public:
    Derived()
        :_d(2)
    {
        cout << "Dereived::Derived()" << endl;
    }
    void Show()
    {
        cout << _d;
    }

private:
    int _d;
};

void Print(Base* p)
{
    p->Show();
}


void Test()
{
    Base b;//1
    Derived d;//2

    Print(&b);
    Print(&d);

}
int main()
{
    Test();

    return 0;
}

我们期待的运行结果是: 1 2
真实的是:
这里写图片描述
我们再来看一下构造函数的调用顺序是:
首先,看一下汇编代码
这里写图片描述
我们实例化对象d时,首先是调用派生类的构造函数,但是打印的时候却是先打印基类的构造函数呢?

这是因为,实例化对象时,该对象只能调用自己类型的构造函数,所以汇编代码先调用派生类自己的,为什么先打印基类的呢,因为我们构造派生类时, 在初始化列表中,先构造出基类的。才进入派生类构造函数的定义体中。

我们再把问题聚焦到上面的多态为什么没有实现?这是因为我们调用的函数不是虚函数。
我们改为虚函数后,结果就会是我们期望的了。
这里写图片描述

那除了要加virtual关键字,我们还需要什么要注意的呢?,我们整理一下在继承关系中容易混淆的几个点。
这里写图片描述

另外补充一点:可以发生协变,协变是指:基类函数返回基类的指针或者引用,派生类返回派生类的指针或引用。

示例2:

#include<iostream>

using namespace std;


//实现多态

class Base
{
public:
    Base()
        :_b(1)
    {
        cout << "Base::Base()" << endl;
    }
    virtual Base* Show()
    {
        cout << _b<<endl;
        return this;
    }

private:
    int _b;
};


class Derived:public Base
{
public:
    Derived()
        :_d(2)
    {
        cout << "Dereived::Derived()" << endl;

    }
    virtual Derived* Show()
    {
        cout << _d;
        return this;
    }

private:
    int _d;
};

void Print(Base* p)
{
    p->Show();
}


void Test()
{
    Base b;//1
    Derived d;//2

    Print(&b);
    Print(&d);

}
int main()
{
    Test();

    return 0;
}

我们运行一下,发现也是可以。

这里写图片描述


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


我们接下来对几点需要特别注意的地方讨论一下:
2、基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。(继承虚表)怎么理解?
也就是说,加入派生类中没有对虚函数进行重写,那在派生类的对象模型中,也继承了基类的虚函数,我们看代码:


class Base
{
public:
    Base()
        :_b(1)
    {
        cout << "Base::Base()" << endl;
    }
    virtual Base* Show()
    {
        cout << _b<<endl;
        return this;
    }

private:
    int _b;
};


class Derived:public Base
{
public:
    Derived()
        :_d(2)
    {
        cout << "Dereived::Derived()" << endl;

    }
    //virtual Derived* Show()
    //{
    //  cout << _d;
    //  return this;
    //}

private:
    int _d;
};
void Test()
{
    Base b;//1
    Derived d;//2

    Print(&b);
    Print(&d);

}

我们取对象的地址,查看一下内存
这里写图片描述
发现了当我们在派生类中没有重写虚函数时,对于基类的虚函数我们继承了下来,同样也为虚函数。

3、只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数。

//类的静态成员函数没有this指针,加入静态成员函数我们定义为虚函数,当我们去调用该函数时,需要拿到虚表指针,进而找到虚表,拿到虚函数的入口地址。但是,我们没有this,显然我们拿不到该虚表指针

5、构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但最好不要这么做,使用时容易混淆

//首先看一下,构造函数不能定义为虚函数,有两点:
①我们使用虚函数的目的是,实现多态,那我们构造基类时,到底是构造出基类还是派生类呢?
②调用虚函数需要虚表指针,而我们的对象还没有创建创建成功,哪来的虚表指针

虽然operator=可以声明为虚函数,最好不要这样做

//可能会导致错误(由于和赋值兼容规则所矛盾)
①派生类对象就可以直接赋值给基类对象,但基类对象却不能给派生类对象赋值
②实现多态的接口是基类的指针或引用,如果当前绑定的是派生对象,即使如此,我也无法通过这样对派生类成员赋值。

6.友元函数不可以声明为虚函数

//友元函数成员函数,当我们调用该虚函数找不到虚表指针

7.在类的六个默认成员函数中,只有析构函数需要定义为虚函数。
首先。我们看一下这种情况:

#include<iostream>
using namespace std;

class Base
{
public:
    Base()
        :_b(3)
    {
        cout << "Base::Base()" << endl;
    }
     ~Base()
    {
        cout << "Base::~Base()" << endl;
    }

private:
    int _b;
};

class Derived:public Base

{
public:
    Derived()
        :_d(6)
    {
        cout << "Derived::Derived()" << endl;
    }
    ~Derived()
    {
        cout << "Derived::~Derived()" << endl;
    }

private:
    int _d;
};

void Test2()
{   
    Base* pBase = new Derived;
    delete pBase;

}
int main()
{
    Test2();
    return 0;
}

调用结果为:
这里写图片描述

我们用基类指针指向申请的派生类对象,那我们应该释放派生类对象那么大的空间,很明显我们这样内存泄漏了。
当我们把基类的析构函数声明为虚函数时

#include<iostream>
using namespace std;

class Base
{
public:
    Base()
        :_b(3)
    {
        cout << "Base::Base()" << endl;
    }
    virtual ~Base()
    {
        cout << "Base::~Base()" << endl;
    }

private:
    int _b;
};

class Derived:public Base

{
public:
    Derived()
        :_d(6)
    {
        cout << "Derived::Derived()" << endl;
    }
    ~Derived()
    {
        cout << "Derived::~Derived()" << endl;
    }

private:
    int _d;
};

void Test2()
{   
    Base* pBase = new Derived;
    delete pBase;

}
int main()
{
    Test2();
    return 0;
}

这里写图片描述
内存泄露的问题我们得到了有效的解决。

多态的调用
1)如果不是虚函数——>直接调用类中函数;
2)如果是虚函数:
①取虚表指针;
②取该虚函数在虚表中的偏移量;
③定位虚函数。

接下来,
探索多态实现的机制:
回顾一下,C++空类的大小是多少?为1(对不同的对象起区分作用)
我们知道类的大小,成员函数不计算在内,那我们定义一个虚函数呢?

#include<iostream>
using namespace std;

class Base
{
public:
    Base()
        :_b(3)
    {
        cout << "Base::Base()" << endl;
    }
    virtual void  FunTest1()
    {
        cout<<"FunTest()1"<<endl;
    }

     ~Base()
    {
        cout << "Base::~Base()" << endl;
    }

private:
    int _b;
};

class Derived:public Base

{
public:
    Derived()
        :_d(6)
    {
        cout << "Derived::Derived()" << endl;
    }
    ~Derived()
    {
        cout << "Derived::~Derived()" << endl;
    }

private:
    int _d;
};

;

void Test1()
{
    cout << sizeof(Base) << endl;
}

int main()
{
    Test1();

    return 0;
}

加了虚函数我们发现类大小在原有的基础上+4了。
那如果我们定义两个虚函数,类大小会发生改变吗?我们发现大小依旧为8。(一个非静态成员_b在加一个虚表指针)如果细心一点的话,会发现上面的内存布局我们其实已经知道了对象模型了

那既然我们已经知道当前对象的前四个字节放着虚表指针,那我们当前有两个虚函数,那虚表中的布局又是如何呢?

这里写图片描述
从上图我们可以看出虚表中虚函数的入口地址下的四个字节值为000000;
我们用函数指针就可以调用为:

#include<iostream>
using namespace std;

class Base
{
public:
    Base()
        :_b(3)
    {
        cout << "Base::Base()" << endl;
    }
    virtual void  FunTest1()
    {
        cout << "FunTest1()" << endl;
    }
    virtual void  FunTest2()
    {
        cout << "FunTest2()" << endl;
    }

     ~Base()
    {
        cout << "Base::~Base()" << endl;
    }

private:
    int _b;
};
typedef void(*pFun)();

void Test1()
{
    Base b;

    //思路:取到虚表指针,通过虚表指针访问到相应的函数

    //pFun pFun1 = *((pFun*)(&b));这样的调用崩溃了
    pFun* pFun1 = (pFun*)(*((int*)(&b)));//取地址b的前四个字节(虚表指针),所以我们转为(int *)在解引用拿到地址的值

    while (*pFun1)
    {
        (*pFun1)();
        //++pFun1;这样的调用不可以
        pFun1 = (pFun*)((int*)(pFun1)+1);
    }


    //pFun pFun1 = *((pFun*)(*((int *)&b)));

    //while (pFun1)
    //{
    //  pFun1();
    //  pFun1 = (pFun)((int*)pFun1 + 1);//这样程序会崩溃,因为你这是在pFun1的值上加4,但其实不是这样,是在pFun*的基础上偏移4个字节
    //}


    cout << sizeof(Base) << endl;
}

int main()
{
    Test1();

    return 0;
}

这里写图片描述

接下来我们看一下:
1->单继承
2->多重继承
3->菱形继承
中虚函数,虚表的变化情况

1->单继承:

class Base
{
public:
    Base()
        :_b(1)
    {
        cout << "Base::Base()" << endl;
    }
    virtual Base* FunTest1()
    {
        return this;
    }

private:
    int _b;
};


class Derived :public Base
{
public:
    Derived()
        :_d(2)
    {
        cout << "Dereived::Derived()" << endl;

    }
    virtual Derived* FunTest1()
    {
        return this;
    }

private:
    int _d;
};



int main()
{
    Derived d;

    return 0;
}

在创建派生类的对象时,都会执行哪些操作?(分析下图)

调用派生类的构造函数(在初始化列表调基类的构造函数—–>在对象的前四个字节填充虚表指针),在基类构造函数调用完成后会返回到派生类的初始化列表中,然后执行派生类的构造函数(会将对象的前4个字节修改为派生类的虚表指针)。

从这里可以看出:类中如果存在虚函数,那么它的造函数的主要功能就是填充虚表指针,(若派生类没有显示给出构造函数,编译器也会自动合成,因为它公有继承基类并且需要填充基类的虚表指针)

这里还应该明白虚函数表在程序编译结束时已经形成了。(基类虚表不可以改,修改了虚表指针,1.把基类的虚表拷贝过来,2.若重写了基类虚函数,则进行修改3.若派生类自己写了新的虚函数,则追加到修改后的虚表后面)
虚表指针发生了变化:

这里写图片描述

2->多重继承

读者可以自行验证,我们学习它背后思想:
从继承的经验我们基本可以推断出,多重继承的对象模型。

3.->菱形继承

菱形继承的对象模型我们也很清楚,菱形继承
,那问题就来了?菱形继承中既有偏移量指针,又有虚表指针,那它新的对象模型会是怎样呢?

class Base
{
public:
    Base()
        :_b(1)
    {
        cout << "Base::Base()" << endl;
    }
    virtual void FunTest1()
    {
        return ;
    }

private:
    int _b;
};


class Derived1 :virtual public Base
{
public:
    Derived1()
        :_d1(2)
    {
        cout << "Dereived::Derived()" << endl;

    }
    virtual void FunTest1()
    {
        cout << "D1::FunTest1()" << endl;

    }
    virtual void FunTest2()
    {
        cout << "D1::FunTest2()" << endl;
    }
private:
    int _d1;
};
class Derived2 :virtual public Base
{
public:
    Derived2()
        :_d2(3)
    {
        cout << "Dereived::Derived()" << endl;

    }
    virtual void FunTest1()
    {
        cout << "FunTest3()" << endl;

    }
    virtual void FunTest3()
    {
        cout << "FunTest3()" << endl;
    }

private:
    int _d2;
};

class DD :public Derived1, public Derived2
{
public:
    DD()
        :_dd(4)
    {
        cout << "Dereived::Derived()" << endl;

    }
    virtual void FunTest1()
    {
        cout << "DD::FunTest1()" << endl;

    }
    virtual void FunTest2()
    {
        cout << "DD::FunTest2()" << endl;

    }
    virtual void FunTest3()
    {
        cout << "DD::FunTest3()" << endl;
    }
    virtual void  FunTest4()
    {
        cout << "DD::FunTest4()" << endl;
    }

private:
    int _dd;


};

int main()
{
    DD d;

    return 0;
}

这里写图片描述
同理,D2类的对象模型也类似与D1类。看得出来先是虚表指针,再接着是偏移量指针。

另外,细心的同学会发现我们的对象模型中多了4个字节,为00000
我们改变去掉派生类的构造函数看一下内存布局:
这里写图片描述
变少了四个字节,经过尝试当我们在菱形继承中的下三角派生类中,添加构造函数或者析构函数,对象模型会增大四个字节,也就是说我们上面增加的4个字节跟下三角派生类中,添加构造函数或者析构函数有很大关系。

最后,我再来看一下多态的缺点:
一、通过父类型的指针访问子类自己的虚函数

我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

Base1 *b1 = new Derive();
b1->FunTest2(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

二、访问non-public的虚函数

另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值