精华内容
下载资源
问答
  • 2018-05-20 01:54:00

    引言

           多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是面向对象编程的核心思想之一,因此我们有必要深入探索一下它的实现原理。理解了原理才能更好的使用。

    前置条件

            现有代码如下所示,非常简单的例子。通过基类的引用调用recv函数来触发多态。接下来的分析涉及汇编知识,如果还没熟悉汇编,可以看另外一篇文章<深入理解c++函数调用的参数传递与局部变量申请>

    #include <stdio.h>
    #include <string>
    
    #define trace(fmt, ...) printf("[trace] %s:%s:%d " fmt, __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__)
    
    class IClient
    {
    	public:
    		IClient(){};
    		virtual ~IClient(){};
    		virtual ssize_t recv(char *buff, size_t len) = 0;
    };
    class CStreamClient: public IClient
    {
    	public:
    		CStreamClient(){};
    		~CStreamClient(){};
    		ssize_t recv(char *buff, size_t len)
    		{
    			trace("recv %d bytes in %p\n", len, buff);
    			return len;
    		}
    };
    int main(int argc, char **argv)
    {
    	CStreamClient streamclient;
    	IClient &client = streamclient;
    	client.recv(NULL, 0);
    	return 0;
    }

    分析

            我们都知道拥有虚函数的类都有属于自己的虚表存放在.text段中,实例化后对象拥有一个内建变量_vptr虚表指针,它指向了实际对象的虚表。那么这个_vptr在哪里初始化呢?当然是构造函数。如下图所示,CStreamClient与IClient都会在自己的构造函数中初始化_vptr为自己的虚表地址。不过父类构造先于子类构造函数执行,因此CStreamClient对象指向的是自己的虚表。


            接下来我们对main反汇编代码分析如下所示,client对象处于栈中。首先计算出client对象的地址。接下来解引用得到_vptr的值。值得注意的是_vptr并非指向虚表起始地址,而是+0X08。起始4字节猜测是保留,毕竟全是0x00;接下来4字节存放的是对象的类型信息,反汇编typeid()函数你就会发现它就是在读取这个地址指向的typeinfo。

            得到_vptr地址后+0x08的偏移得到虚表存放recv地址的地址,再解引用就得到实际绑定对象的recv函数地址了。接下来是各个参数入栈,再跳转执行。


            那么,这就结束了吗?眼尖的你肯定发现了,为什么会有两个的析构函数?函数声明还是一个模子出来的。那么我们对这两个析构反汇编分析对比一下有什么不同。有注意到什么不同吗?

            左侧的析构函数赋值eax为零,eax分为[16位ax|8位ah|8位al]。那么test指令将会置位ZF标志位,因此接下来的je指令将会跳转,也就是不进行free的操作。右侧的析构函数在调用左侧析构的基础上进行了释放内存。那么就是说它们分别是为栈上和堆上的对象准备的。


    更多相关内容
  • 4. 多态用虚函数来实现,结合动态绑定。5. 纯虚函数是虚函数再加上= 0。6. 抽象类是指包括至少一个纯虚函数的类。 纯虚函数:virtual void breathe()=0;即抽象类!必须在子类实现这个函数!即先有名称,没内容,在...
  • c++ 多态实现原理

    2022-05-13 19:12:40
    动态多态就是通过继承重写基类的虚函数实现多态,因为是在运行时决议确定,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。 c++的多态性用一句话概括:在基类的函数前加上virtual关键字,在派生类中...

    1. 什么是多态?

            多态就是多种形态,C++的多态分为静态多态动态多态。静态多态就是重载,因为在编译期决议确定,所以称为静态多态。动态多态就是通过继承重写基类的虚函数实现的多态,因为是在运行时决议确定,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。

            c++的多态性用一句话概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

    2. 如何实现多态?

    例子1:

    #include <stdlib.h>
    #include <iostream>
    using namespace std;
    
    class Father
    {
    public:
    	void Face()
    	{
    		cout << "Father's face." << endl;
    	}
    
    	void Say()
    	{
    		cout << "Father say hello." << endl;
    	}
    };
    
    class Son : public Father
    {
    public:
    	void Say()
    	{
    		cout << "Son say hello." << endl;
    	}
    };
    
    int main(int argc, char** argv)
    {
    	Son son;
    
    	Father* pFather = &son;
    	pFather->Say();
    
    	return 0;
    }

    输出结果:

     分析:我们在main()函数中首先定义了一个Son类的对象son,接着定义了一个指向Father类的指针变量pFather,然后利用该变量调用pFather->Say().估计很多人往往把这种情况和c++的多态搞混淆,认为son实际上是Son类的对象,应该调用Son类的Say,输出“Son say hello”,然而结果确不是。

             从编译的角度来看:

    c++编译器在编译的时候,要确定每个对象调用的函数(非虚函数)的地址,这成为早期绑定,当我们将Son类的对象son的地址赋给pFather时,c++编译器进行了类型转换,此时c++编译器认为变量pFather保存的就是Father对象的地址,当在main函数中执行pFather->Say(),调用的当然就是Father对象的San函数

            从内存的角度看:

    Son类对象的内存模型如上图,我们构造Son类的对象时,首先要调用Father类的构造函数去构造Father类的对象,然后才调用Son类的构造函数完成自身部分的构造,从而拼接出一个完整的Son类对象。当我们将Son类对象转换为Father类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是图中“Father的对象所占内存”,那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法。因此,输出“Father Say hello.”。

     例子2:

    #include <stdlib.h>
    #include <iostream>
    using namespace std;
    
    class Father
    {
    public:
    	void Face()
    	{
    		cout << "Father's face." << endl;
    	}
    
    	virtual void Say()
    	{
    		cout << "Father say hello." << endl;
    	}
    };
    
    class Son : public Father
    {
    public:
    	void Say()
    	{
    		cout << "Son say hello." << endl;
    	}
    };
    
    int main(int argc, char** argv)
    {
    	Son son;
    
    	Father* pFather = &son;
    	pFather->Say();
    
    	return 0;
    }

    输出结果: 

    备注:代码稍微改动一下,只是在Father类的Son函数前添加virtual。

    分析: 

            我们知道pFather实际上指向的是Son类的对象,我们希望输出的结果是Son类的Say方法,那么想要达到这种结果,就要用到虚函数了。

            前面输出的结果因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用晚绑定,当编译器使用晚绑定的时候,就会在运行时再去确定对象的类型以及正确的调用函数,而要让编译器采用晚绑定,就要在基类中声明函数时使用virtual关键字,这样的函数我们就称之为虚函数,一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式的声明为virtual。

            编译器在编译的时候,发现Father类中有虚函数,此时编译器会为每一个包含虚函数的类创建一个虚表(即vtable),该表是一个一维数组,在这个数组中存放每一个虚函数的地址。

            

     那么如何定位虚表呢?编译器另外还为每个对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表,在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数的时候,能够找到正确的函数。对于第二段代码程序,由于pFather实际指向的对象类型是Son,因此vptr指向的Son类的vtable,当调用pFather->Son()时,根据虚表中的函数地址找到的就是Son类的Say()函数。

            正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要,换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数,那么虚表指针是在什么时候,或者什么地方初始化呢?

            答案就是在构造函数中进行虚表的创建和虚表指针的初始化,在构造子类对象时,要先调用父类的构造函数,此时编译器只看到了父类,并不知道后面是否还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表,当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。

            当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

    3. 多态的实现原理

            1. 用virtual关键字声明的函数叫做虚函数,虚函数肯定是的成员函数。

            2.  存在虚函数的类都有一个一维的虚函数表叫做虚表。当类总声明虚函数时,编译器会在类中生成一个虚函数表。

            3. 类的对象有一个指向虚表开始的虚指针,使调用虚函数时,能够找到正确函数虚表和类是对应的,虚表指针和对象是对应的。

            补充:虚函数表是一个存储类成员函数指针的数据结构,是由编译器自动生成和维护的。 当存在虚函数时,每个对象中都有一个指向虚函数的指针vptr,vptr一般作为类对象的第一个成员。

      

    展开全文
  • 文章目录前言有虚函数的类对象模型派生类继承有虚函数的基类的...【传送门】:谈谈C++多态的基本使用和总结 有虚函数的类对象模型 class Base { public: Base(){ cout << "Base::Base()的构造函数调用" <

    前言

    要理解C++的多态,必须先学会使用多态:
    多态的基本使用,我总结一篇文章:有需要可以看看
    【传送门】:谈谈C++多态的基本使用和总结

    测试平台:vs2013 32位;

    并且都是单继承,没有分析多继承,原因是:多继承较为复杂,多继承还有虚继承再加虚表,对象模型比较复杂;实际工程运用可能相对比较少,由于笔者精力不够充足,所以不打算分析多继承体系下有虚函数的对象模型了;

    有兴趣可以看看陈硕大佬的两篇文章,都分析了多继承体系下的对象模型;并且总结的也非常棒;

    【传送门】C++ 对象的内存布局
    【传送门】C++ 虚函数表解析


    以下的分析是我观察调试程序观察的结果!谈谈我对多态的理解


    1. 有虚函数的类对象模型

    class Base {
    public:
    	Base(){
    		cout << "Base::Base()的构造函数调用" << endl;
    	}
    	virtual void fun() { 
    		cout << "Base::fun()被调用" << endl;
    	}
    private:
    	int _b;
    };
    int main(){
    	Base b;
    	return 0;
    }
    

    通过监视窗口我们发现:有了虚函数,会在类的对象增加多一个指针,该指针就是虚函数指针_vfptr;
    并且该虚函数指针的位置,是在类的最开始位置;
    该虚函数指向一个虚表,虚表本质就是函数指针数组,虚表里面存放着该对象的虚函数的地址
    目前该对象 b只有一个虚函数fun,所以说,虚表的第一个元素位置就是fun函数的地址
    在这里插入图片描述


    我们可以通过vs2013的开发人员命令行工具可以看得更清楚它的对象模型:
    在这里插入图片描述


    2. 派生类继承有虚函数的基类的对象模型

    2.1. 无虚函数派生类继承有虚函数基类的对象模型

    我们首先看一个派生类,没有任何成员,也就是不重写基类虚函数,并且派生类自己没有添加自己的虚函数,而是直接继承有虚函数的基类,看看其对象模型的样子:

    class Base {
    public:
    	Base(){
    		cout << "Base::Base()的构造函数调用" << endl;
    	}
    	virtual void fun() { 
    		cout << "Base::fun()被调用" << endl;
    	}
    private:
    	int _b;
    };
    class Derive :public Base{
    
    };
    int main(){
    	Base b;
    	Derive d;
    	return 0;
    }
    
    

    观察b的对象模型;
    在这里插入图片描述


    通过观察子类的对象模型:
    我们首先得知道,子类对象有父类虚函数指针,和成员我们可以理解,因为这是继承体系中,本来就会把父类的东西继承到子类中;
    但我们有两个迫切想知道的问题:
    第一:子类中的虚函数指针,是否和父类的虚函数指针是一样的地址呢?
    第二:子类的虚函数表的内容是否和父类虚函数表的内容一致的呢?

    为了回答第一个问题:我们观察父类的对象模型和子类的对象模型进行对比:


    在这里插入图片描述


    通过观察:我们得知:子类虽然继承了父类,但是但是子类的虚表指针,却和父类的虚表指针值不一样!!! 这和我们平时的认知有点偏差,因为我们平时知道,子类继承父类,是完完全全的照搬父类的成员到子类中的,而如今却得知,子类继承父类时候,不可以把父类的虚表指针以同样的值继承下来,而只是拷贝了一份父类的虚表指针给子类;

    所以很重要的结论是:
    子类继承父类时候,只是继承父类的虚表指针的拷贝,并不是继承父类虚表指针一样的值;


    第二问题:子类的虚函数表的内容是否和父类虚函数表的内容一致的呢?

    通过观察,我们发现,子类的虚函数表里面存放的虚函数,是和父类的虚函数一样的;(这也是在子类没有重写父类虚函数的前提下)因为子类会完完全全的照搬父类的虚函数表到子类中虚函数表中(注意这里我说是照搬到子类虚函数表中,意思是父类虚函数表和子类虚函数表本身就是两个不一样的表,子类虚函数表可以有其他的虚函数,但是它一定会包含父类的虚函数,所以说:子类虚函数表和父类虚函数表关系可以说是包含关系:子类包含父类)

    所以在子类没有重写父类的虚函数时候,子类的虚函数表和父类是完全一样的(这是在子类没有自己的虚函数情况下)


    2.2. 有虚函数派生类继承有虚函数的基类对象模型

    上面说的是派生类没有重写任何父类虚函数,并且派生类没有自己虚函数的派生类对象模型;

    这次我们试着在派生类重写父类虚函数,并且派生类有自己的虚函数,看看对象模型又是什么模样;

    class Base {
    public:
    	Base(){
    		cout << "Base::Base()的构造函数调用" << endl;
    	}
    	virtual void fun() { 
    		cout << "Base::fun()被调用" << endl;
    	}
    private:
    	int _b;
    };
    class Derive :public Base{
    public:
    	virtual void fun(){
    		cout << "Derive::fun()被调用" << endl;
    	}
    	virtual void fun1(){
    		cout << "Derive::fun1()被调用" << endl;
    
    	}
    };
    int main(){
    	Base b;
    	Derive d;
    	return 0;
    }
    

    在这里插入图片描述
    通过调试观察父类和子类的对象模型,我们发现:
    在派生类中的对象模型里:
    虚表发生了变化,里面的虚表不再是和父类虚表的内容一致了,当子类有了重写父类的虚函数,子类在自己的虚表中用重写的虚函数去覆盖原来父类的虚函数;
    并且假如子类有自己的虚函数(这个虚函数不是重写父类的虚函数),子类的虚函数也会加入到子类虚表中的后面的位置;


    总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 ;
    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 ;
    c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后;


    注意:在vs2013的监视窗口,可能会优化,把派生类自己的虚函数地址,没有放到虚表中:如下图,
    实际上是有的只不过被vs2013的窗口不显示
    在这里插入图片描述


    3. 多态原理剖析

    有了上面的对象模型知识的储配,我们就来开始剖析多态的原理;
    我们知道要构成多态必定要满足两个条件:
    在继承体系中:
    对于使用者来说:父类指针或引用调用虚函数;
    对于设计类的人来说:子类必须重写父类的虚函数;


    好了我们就按照这个多态条件,来写一段多态的代码:

    class Person {
    public:
    	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    };
    
    class Student : public Person {
    public:
    	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    };
    void Func(Person& p)
    {
    	p.BuyTicket();
    }
    int main()
    {
    	Person Mike;
    	Func(Mike);
    	
    	Student Johnson;
    	Func(Johnson);
    	
    	return 0;
    }
    

    这段代码我们都知道结果:运行时候,发生多态,根据传入的对象是什么,去调用谁的虚函数;
    我们从对象模型原理去看看这个问题:
    在这里插入图片描述
    父类引用调用虚函数BuyTicket()的过程:
    当我传递给父类引用Person& p 是 父类对象Mike时候,那么p.BuyTicket()就是去父类Person的对象Mike中找到虚函数指针,通过虚函数指针,找到虚函数的地址,然后就调用成功;

    当我传递给父类引用Person& p 是 子类对象Johnson时候,那么p.BuyTicket()就是去子类Student的对象Johnson中找到虚函数指针,通过虚函数指针,找到虚函数的地址,然后就调用成功;


    你凭什么说这是发生了多态调用,而不是编译时候就确定了地址,直接调用这虚函数的呢?
    我们可以通过汇编观察:
    在汇编代码中,我们发现,调用这p.BuyTicket()这句代码时候,是call eax,这说明什么意思:因为eax寄存器是变量,变量只有在运行时候才会分配地址,所以说call eax只有在运行时候才会发生;这个说明多态铁定是运行时候才确定要调用哪个函数的;
    在这里插入图片描述


    假如你还是不信,我可以给你看不发生多态时候,是如何调用函数的
    在这里插入图片描述
    父类对象直接调用虚函数函数,不是父类指针或者引用调用虚函数,这肯定不发生多态对吧!
    在不发生多态时候,调用函数,也就是直接把地址写死了,根本不需要在运行时候,就知道函数的地址了,编译时候就确定要调用谁了;


    总结:一句话:
    多态的原理:基类的指针或者引用指向谁就去谁的虚函数表中找到对应的虚函数进行调用;


    4. 汇编代码分析多态的过程

    分析汇编代码中,多态时候是如何找到对应的虚函数的;

    // 以下汇编代码中跟你这个问题不相关的都被去掉了
    void Func(Person* p)
    {
    ...
    p->BuyTicket();
    // p中存的是Mike对象的指针,将p移动到eax中
    001940DE mov eax,dword ptr [p]
    // [eax]就是取eax值指向的内容,这里相当于把Mike对象头4个字节(虚表指针)移动到了edx
    001940E1 mov edx,dword ptr [eax]
    // [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
    00B823EE mov eax,dword ptr [edx]
    // call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
    001940EA call eax
    001940EC cmp esi,esp
    }
    

    分析没发生多态时候,调用虚构函数的汇编代码:

    int main()
    {
    ...
    // 首先BuyTicket虽然是虚函数,但是Mike是对象,不满足多态的条件,
    //所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
    Mike.BuyTicket();
    00195182 lea ecx,[mike]
    00195185 call Person::BuyTicket (01914F6h)
    ...
    }
    

    5. 有关多态的常见几个问题

    1. 问题1:同一个类的不同对象的虚函数指针是否一样?虚函数表是否一样?;

    是一样的。它们的对象的虚函数指针都指向同一个虚表;虚函数指针都是一样的

    这都是可以验证的,只要我们多创建几个同一个类不同对象,观察它们对象的模型就可以知道:
    在这里插入图片描述


    1. 虚表在哪个阶段生成虚表存放在哪?

    虚函数我们知道存放在虚表,但是虚表存放在哪呢?其实存放在字符常量区(vs验证得到的结果);
    虚表在编译阶段就形成了;

    我们依旧可以通过代码验证:只要打印出代码区,字符常量区,数据段区,栈区,堆区,我们在打印虚函数的地址,就可以观察,虚函数地址靠近哪个区就在哪个区存放着虚表了;
    我就不验证了有兴趣可以试一试;


    1. 在发生多态时候,虚函数被private修饰了,还可以被调用吗?

    答案是可以的,当该函数成为虚函数时候,该虚函数地址是被放入到了虚表中,当发生多态时候,是去虚函数表中找到该虚函数的,并不受访问限定符private限制;
    假如没有发生多态,也就是不是父类指针引用调用该虚函数时候,是对象调用该虚函数时候,那么就会编译失败,因为此时会受访问限定符的限定;

    验证也是可以的:
    在这里插入图片描述


    1. 虚表指针什么时候被初始化?

    虚表指针在构造函数的初始化列表初始化!

    我们也可以验证,只要我们在有虚函数的类定义一个构造函数,然后再创建一个该类的对象,通过vs2013的窗口监视,调试可以看到虚函数指针,只有构造函数被调用时候,才会被初始化。(过于简单,我就不验证了,你们可以试一试)


    1. 静态成员函数可以为虚函数嘛?

    不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。


    1. 构造函数可以是虚函数吗?

    不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始 化的


    6. 打印虚表的内容

    我们可以再vs2013尝试打印虚表的内容,由于vs2013编译器会给每个类的最后存放一个nullptr,所以我们可以通过遍历虚表的方式,打印虚表的内容;


    打印虚表的思路:

    1. 拿出虚表指针
    2. 遍历虚表指针的内容

    class Base {
    public :
    
    	virtual void func1() { cout<<"Base::func1" <<endl;}
    	virtual void func2() {cout<<"Base::func2" <<endl;}
    private :
    	int a;
    };
    class Derive :public Base {
    public :
    	virtual void func1() {cout<<"Derive::func1" <<endl;}
    	virtual void func3() {cout<<"Derive::func3" <<endl;}
    	virtual void func4() {cout<<"Derive::func4" <<endl;}
    private :
    	int b;
    };
    	int main(){
    		Base b;
    		Derive d;	
    		return 0;
    	}
    

    我们通过vs2013的监视窗口查看一下 基类和派生类的对象模型:
    在这里插入图片描述


    观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,
    也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。


    typedef void(*VFPTR) (); //定义一个函数指针,这里是本类中的虚函数指针类型
    void PrintVTable(VFPTR vTable[])
    {
    // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
    	printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
    	//由于该地址是虚函数的地址,那么通过函数指针f去调用一定能够成功
    	//进一步验证这是虚函数表的虚函数指针
    	VFPTR f = vTable[i];
    	f();
    }
    	cout << endl;
    }
    int main()
    {
    	Base b;
    	Derive d;
    // 思路:取出b、d对象的头4bytes,就是虚表的指针,
    //前面我们说了虚函数表本质是一个存虚函数指针的
    //指针数组,这个数组最后面放了一个nullptr
    // 1.先取b的地址,强转成一个void**的指针
    // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
    // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
    // 4.虚表指针传递给PrintVTable进行打印虚表
    // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有
    //放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
    	VFPTR* vTableb = (VFPTR*)(*(void**)&b);
    	PrintVTable(vTableb);
    	VFPTR* vTabled = (VFPTR*)(*(void**)&d);
    	PrintVTable(vTabled);
    	return 0;
    }
    

    结果如下:

    在这里插入图片描述

    展开全文
  • 1 实现多态的三步骤 1、 有继承关系的类 2、父类有虚函数,子类重写父类的虚函数 3、子类的指针或应用赋值给父类 2 虚函数注意点 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。...

    1 实现多态的三步骤

    1、 有继承关系的类
    2、父类有虚函数,子类重写父类的虚函数
    3、子类的指针或应用赋值给父类

    2 虚函数注意点

    1. 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是重载,而不是虚函数。但是基类中虚函数的返回值返回基类指针,派生类中对应的虚函数返回值返回派生类指针是允许的,这是一个例外

    2. 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。即全局函数不能说明为虚函数

    3. 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。

    4. 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。

    5. 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。如delete一个父类指针时,这个指针是new一个子类赋值给父类指针的,如果子类中有动态分配的内存空间,那么需要将父类的析构函数声明为虚函数,否则子类的析构函数在delete父类指针时不会被调用造成内存泄漏。

    6. 如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。即virtual关键字只能出现在函数的声明或定义处,不能同时出现

    3 纯虚函数与抽象类

    定义纯虚函数的一般格式为:
    virtual 返回类型 函数名(参数表)=0;
    纯虚函数(pure virtual function)是指被标明为不具体实现的虚拟成员函数。它用于这样的情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。
    含有纯虚函数的基类为抽象类纯虚函数没有实现部分,不能产生对象,因此抽象类是不能用来实例化对象的,但是可以定义抽象类指针或引用来实现多态。

    4 多态实现原理

    1、基类中的虚函数
    类中若定义了虚函数,那么类会多出一个成员指针,称为虚函数指针,虚函数指针指向一张虚函数表,虚函数表中存放的是类中的每一个虚函数地址,注意非虚函数的成员函数不会放入虚函数表中
    2、继承
    子类继承父类,那么子类会继承除了构造函数和析构函数的所有成员,当然也会继承父类的虚函数指针和虚函数表,可以看做子类的构成中有一个父类
    3、重写子类的虚函数 以及子类指针或引用赋值给父类
    在重写子类的虚函数后,子类的虚函数地址与父类对应的虚函数地址会有不同,在子类指针或引用赋值给父类时,虚函数表中原先父类的虚函数地址会被重写的子类的虚函数地址覆盖掉,从而完成不同的子类指针或引用赋值给同一父类指针或引用时,在调用父类的虚函数时回去调用对应子类的虚函数,最终实现多态。注意若在子类定义了父类不具有的虚函数,这个虚函数也会加入子类继承而来的虚函数表中,但是不能通过父类指针或引用去调用这个父类不具有的虚函数,即子类指针或引用赋值给父类后,父类指针或引用的作用范围不能超过父类的成员范围

    5 虚函数表图解

    5.1 一般继承(无虚函数覆盖)

    在这里插入图片描述
    请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,对于实例Derive d 的虚函数表如下所示:
    在这里插入图片描述
    我们可以看到下面几点:
    1)虚函数按照其声明顺序放于表中。
    2)父类的虚函数在子类的虚函数前面

    5.2 一般继承(有虚函数覆盖)

    覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。如果子类中有虚函数重载了父类的虚函数,我们有下面这样的一个继承关系
    在这里插入图片描述
    在这个类的设计中,只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
    在这里插入图片描述
    我们从表中可以看到下面几点,
    1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
    2)没有被覆盖的函数依旧。
    这样,我们就可以看到对于下面这样的程序,
    Base *b = new Derive();
    b->f();
    由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

    5.3 多重继承(无虚函数覆盖)

    下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数
    在这里插入图片描述
    对于子类实例中的虚函数表,是下面这个样子:
    在这里插入图片描述
    我们可以看到:
    1) 每个父类都有自己的虚表。
    2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
    这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

    5.4 多重继承(有虚函数覆盖)

    下面我们再来看看,如果发生虚函数覆盖的情况。 下图中,我们在子类中覆盖了父类的f()函数。
    在这里插入图片描述
    下面是对于子类实例中的虚函数表的图:
    在这里插入图片描述
    我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
    Derive d;
    Base1 *b1 = &d;
    Base2 *b2 = &d;
    Base3 *b3 = &d;
    b1->f(); //Derive::f()
    b2->f(); //Derive::f()
    b3->f(); //Derive::f()
    b1->g(); //Base1::g()
    b2->g(); //Base2::g()
    b3->g(); //Base3::g()

    展开全文
  • C++多态实现原理

    2020-10-24 16:20:02
    多态就是多种形态,C++多态分为静态多态与动态多态。 静态多态就是重载,因为在编译期决议确定,所以称为静态多态。在编译时就可以确定函数地址。 动态多态就是通过继承重写基类的虚函数实现多态,因为实在...
  • 3.多态实现原理 4.如何证明vptr指针? 三.总结 一.背景介绍 虚函数重写:子类重新定义父类中有相同函数名,返回值和参数的虚函数 非虚函数重写:子类重新定义父类中有相同名称和参数的非虚函数 继承中的类型...
  • C++多态原理实现举例

    千次阅读 2019-08-29 15:39:00
    2、C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。 3、多态的分类: 多态可以分为静态多态和动态多态。静态多态里包括函数重载和泛型编程;动态多态主要体现在虚函数...
  • C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。... 4:多态用虚函数来实现,结合动态绑定.   5:纯虚函数是虚函数再加上
  • C++实现多态原理

    万次阅读 多人点赞 2018-04-15 22:21:48
    C++多态是面向对象编程的核心,那么C++多态是怎么来实现的?今天我们就来探讨一下。 我们先来看下面程序和它的运行结果 #include &lt;iostream&gt; using namespace std; class Father { public: ...
  • C++多态实现原理

    2016-09-13 16:59:44
    当类中声明虚函数时,编译器会在类中生成一个虚函数表  虚函数表是一个存储类成员函数指针的数据结构  虚函数表是由编译器自动生成与维护的  virtual 成员函数会被编译器放入虚函数表中  存在虚函数时,每个...
  • C++ 多态实现原理分析

    千次阅读 多人点赞 2018-08-28 11:31:15
    二、C++ 多态实现原理 1. 实现原理 当类中存在虚函数时,编译器会在类中自动生成一个虚函数表 虚函数表是一个存储类成员函数指针的数据结构 虚函数表由编译器自动生成和维护 virtual 修饰的成员函数会被...
  • c++多态原理

    2020-11-08 16:46:10
    C++多态问题 这是一篇关于我多态的理解,用的都是我自己通俗易懂的语言,没有那么书面化,大家可以参考一下!也欢迎大家来讨论 首先就是多态的概念多态有静态多态和动态多态 静态多态:函数重载 和 运算符重载属于...
  • 根据我的已有知识,如果要实现C++多态,那么,基类中相应的函数必须被声明为虚函数(或纯虚函数)。举个例子: class Point { public: Point(float x = 0.0, float y = 0.0) : _x(x), _y(y) { } virtual ...
  • C++多态实现原理

    2021-03-10 17:23:09
    多态是指在不同的条件下表现出不同的状态,C++中通过重载函数的方法可以在编译期间实现多态。 在编译时编译器会根据参数列表的不同寻找合适的函数。 int Add(int left, int right) { return left + right; } ...
  • virtual C++多态实现原理

    千次阅读 2014-04-30 11:39:09
    1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。 2. 存在虚函数的类都有一个一维的虚函数表叫做虚...4. 多态用虚函数来实现,结合动态绑定。 5. 纯虚函数是虚函数再加上= 0。 6. 抽象类是指包
  • 探究C++多态实现原理

    2018-09-24 15:37:55
    探究C++多态实现原理 在之前的博客中讲到了如何使用C++多态的性质,实现面对对象的程序设计。 在这一篇文章中将会从多态的实现原理,_vfptr 以及多态类的内存构建模型上来探究C++多态的实现过程。 从以下代码来讨论...
  • C++多态的底层原理

    千次阅读 多人点赞 2022-04-24 22:09:26
    要了解C++多态的底层原理需要我们对C指针有着深入的了解,这个在打印虚表的时候就可以见功底,理解了多态的本质我们才能记忆的更牢,使用起来更加得心应手。
  • 分享学习C++多态的笔记,并将多态底层原理进行分析
  • c++多态实现原理

    2017-08-17 23:09:23
    c++多态实现,依靠的是虚函数表。先来看看代码:#include using namespace std;class TestCls { public: int a; void func() { cout ; } };int main(void) { cout (Te
  • 自上一个帖子之间跳过了一篇总结性的帖子,之后再发,今天主要研究了c++语言当中虚函数对多态实现,感叹于c++设计者的精妙绝伦 c++中虚函数表的作用主要是实现多态的机制。首先先解释一下多态的概念,多态c++...
  • C++多态原理

    2016-09-24 16:34:31
    本文档是我自己整理并且参考各个博客,加工整理集合在一起的,绝对全
  • C++多态三种实现方式

    千次阅读 2020-10-14 19:52:01
    下面我们来谈一谈他们各自的实现方式和实现原理。 重载 实现方式 重载是在同一作用域内(不管是模块内还是类内,只要是在同一作用域内),具有相同函数名,不同的形参个数或者形参类型。返回值可以相同也可以不同...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 30,636
精华内容 12,254
关键字:

c++多态的实现原理

c++ 订阅