虚函数 订阅
在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。 展开全文
在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。
信息
关    键
用指向基类的指针或引用操作对象
形象解释
求同存异
定    义
被virtual关键字修饰的成员函数
作    用
实现多态性
中文名
虚函数
声    明
virtual
外文名
virtual function
虚函数定义
简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。下面来看一段简单的代码。
收起全文
精华内容
下载资源
问答
  • 虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容...
  • 本文重点:应该为多态基类声明虚析构器。一旦一个类包含虚函数,它就应该包含一个虚析构器。如果一个类不用作基类或者不需具有多态性,便不应该为它声明虚析构器。
  • 在面向对象的C++语言中,虚函数(virtual function)是一个非常重要的概念。因为它充分体现 了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广。比如在微软的MFC类库中,你会发现很多函数都有virtual...
  • 简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例: 其中: B的虚函数表中存放着B::foo和B::bar两个函数指针。 D...
  • 常见的不不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。 1.为什么C++不支持普通函数为虚函数? 普通函数(非成员函数)只能被overload,不能被override,声明为...
  • 构造函数不能声明为虚函数,析构函数可以声明为虚函数
  • 本文分两部分即继承和虚函数与多态性,本文第一部分详细讲解了继承时的构造函数和析构函数的问题,父类与子类的 同名变量和函数问题,最后介绍了多重继承与虚基类。本文第二部分重点介绍了虚函数与多态性的问题,...
  • C++虚函数表测试源码

    2018-10-12 20:52:42
    C++虚函数表的测试代码,用于学习C++虚函数的调用关系。
  • 高质量的C++多态讲解,详细讲解虚函数虚函数表,虚函数继承,虚函数继承下的内存分配等
  • C++虚函数虚函数表解析,内容详细,分析清晰,推荐给大家。
  • C++虚函数实现原理

    2015-10-24 21:11:09
    虚函数表中虚函数的分布情况;其中包括发生继承的情况下虚函数表中虚函数的分布情况;
  • 虚函数

    千次阅读 2018-05-13 11:22:22
    本篇目录************************************* 什么是虚函数 ** 虚函数指针以及虚函数表 ** 成为虚函数的条件 ** 虚函数的生存周期 ** 为什么要使用虚析构函数 ...

    本篇目录

    ************************************

    *     什么是虚函数                           *

    *     虚函数指针以及虚函数表          *

    *     虚函数表重写的问题                *

    *     成为虚函数的条件                    *

    *     虚函数的生存周期                    *

    *     为什么要使用虚析构函数          *

    *     虚函数与访问限定                    *

    ************************************

    一、什么是虚函数?

    虚函数就是在普通函数前面加了virtual关键字。

    声明时要加virtual,定义时不用加。

    虚函数的作用==》用指针/引用调用时会发生动态的绑定。

    二、虚函数指针

    (1)有了虚函数,会产生一个虚函数指针。

    (2)一个类中有无数多个虚函数都只会产生一个虚函数指针。


    由上面例子可以看出,多了一个虚函数指针,导致sizeof(Base)=8;尽管有两个虚函数,但是sizeof(Base)=8.

    (3)虚函数指针的优先级最高,在内存布局中在最前面。

    (4)虚函数指针指向的是虚函数表。


    查看内存布局可以验证。

    三、虚函数表

    (1)虚函数表中存放的信息。


    (2)虚函数表与对象类型对应,一个类型只有一张虚函数表。

    比如定义了Base b1(10),Base b2(20),Base b3(30)……,但仍然只有一张虚函数表。一个类型对应一个虚函数表!!!

    (3)虚函数表在编译期间产生,存放在只读数据段,生存周期从程序开始到程序结束。

    四、虚函数表重写的问题

    class Base{
    public:
    	Base(int a):ma(a){
    		cout<<"Base(int)"<<endl;
    		clear();//调用了clear函数
    	}
    	void clear(){
    		memset(this,0,sizeof(Base));//将对象清零
    	}
    	virtual ~Base(){
    		cout<<"~Base()"<<endl;
    	}
    	virtual void show(){
    		cout<<"Base::ma="<<ma<<endl;//打印一下ma的值
    	}
    protected:
    	int ma;
    };
    
    class Derive:public Base{
    public:
    	Derive(int b):Base(b),mb(b){
    		cout<<"Derive(int)"<<endl;
    	}
    	~Derive(){
    		cout<<"~Derive()"<<endl;
    	}
    	void show(){
    		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;//打印ma和mb的值
    	}
    private:
    	int mb;
    };

    (1)在堆上开辟一个基类的对象,进行打印:


    程序崩溃,原因在于p1调用show函数时,因为是虚函数,所以要去虚函数表中去找,但是在构造对象时,已经将对象进行清0,0地址访问函数出错。

    (2)用基类指针指向派生类对象时,会发生什么情况呢?


    程序运行正确,你可能会想,在派生类构造之前需要构造基类部分,因此在派生类生成的过程中一定调用了基类的构造函数,但为什么程序正常呢?

    原因就是在继承过程中,每个类型对应一个虚函数表,每一层的构造刚开始之前都需要把虚函数表的地址往虚函数指针写一遍,因为指向什么对象,指针就应该存谁的虚函数表。

    顺一下思路:生成派生类对象时,先构造基类部分,再构造派生类部分。在基类部分构造的时候,clear将对象清零,基类虚函数指针为0了;在构造派生类的时候,派生类中也应该有一张虚函数表,虚表重写,虚函数指针不为0,只是在打印继承过来的ma时,为0.

    五、生成虚函数的条件

    因为虚函数表中存放的时虚函数的地址  ==》  因此虚函数一定要能取地址

    因为虚函数指针存在于对象中 ==》  因此虚函数要用对象来调用

    上面两条必不可少,综上,我们来判断一下,下面哪些函数能成为虚函数?

    1)构造函数      =》不能成为虚函数,因为对象还未生成。

    2)析构函数      =》可以成为虚函数,满足上面两条。

    3)静态成员方法   =》不可以成为虚函数,因为不依赖于对象的调用。

    4)内联函数       =》不可以成为虚函数,因为不能取地址。

    六、虚函数的生存周

    我们写出以下代码:派生类继承了基类,并且在基类和派生类中都有一个虚函数void show(){}

    class Base{
    public:
    	Base(int a):ma(a)
    	{
    		cout<<"Base(int)"<<endl;
    		//show();//*1
    	}
    	virtual ~Base()
    	{
    		//show();//*2
    		cout<<"~Base()"<<endl;
    	}
    	virtual void show()
    	{
    		cout<<"Base::ma="<<ma<<endl;
    	}
    protected:
    	int ma;
    };
    
    class Derive:public Base
    {
    public:
    	Derive(int b):Base(b),mb(b)
    	{
    		cout<<"Derive(int)"<<endl;
    		//show();//*1
    	}
    	~Derive()
    	{
    		//show();//*2
    		cout<<"~Derive()"<<endl;
    	}
    	void show()
    	{
    		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;
    	}
    private:
    	int mb;
    };

    我们分别在构造函数和析构函数中调用show()方法

    int main()
    {
    	Derive* p=new Derive(20);
    	p->show();
    	delete p;
    
    	return 0;
    }

    (1)去掉代码中 *1 的注射,查看反汇编

    Base下,构造函数中调用虚函数show:


    Derive下,构造函数中调用虚函数show:


    可以看出在构造函数中虚函数的调用不是动态的绑定,而是静态的绑定。因为在构造函数中对象正在生成,因此没有发生多态。

    (2)去掉代码中的 *2 注释,查看反汇编

    Base下析构函数中的调用虚函数show


    Derive下析构函数中的调用虚函数show


    可以看出在析构函数中虚函数 的调用步数动态的绑定,而是静态的绑定。因为在析构函数中对象正在死亡,逻辑意义上对象已经不完整,因此调用虚函数没发生多态。

    综上:虚函数的生存周期:从构造函数开始,到析构结束。

    七、为什么要使用虚析构函数?

    (1)如果一个基类指针指向派生类时,并且派生类对象在堆上构造时,调用delete时,只看指针的类型,那么就会造成指针只会析构基类的对象,而派生类的对象并未析构,将会导致内存泄漏。

    class Base{
    public:
    	Base(int a):ma(a){
    		cout<<"Base(int)"<<endl;
    	}
    	~Base(){
    		cout<<"~Base()"<<endl;
    	}
    	void operator delete(void *p){
    		cout<<"free start addr:"<<p<<endl;
    		free(p);
    	}
    private:
    	int ma;
    };
    class Derive:public Base
    {
    public:
    	Derive(int b):Base(b),mb(b){
    		cout<<"Object start addr:"<<this<<endl;
    		cout<<"Derive(int)"<<endl;
    	}
    	~Derive(){
    		cout<<"~Derive()"<<endl;
    	}
    	void* operator new(size_t size){
    		void* p=malloc(size);
    		cout<<"malloc start addr:"<<p<<endl;
    		return p;
    	}
    private:
    	int mb;
    };
    int main(){
    	Base* pb=new Derive(20);
    	delete pb;
    	return 0;
    }

    打印结果:


    可以看出:代码运行正常,但是对于析构只是析构了基类的一部分,并没有调用派生类的析构,造成内存泄漏。

    (2)接着,当派生类中有虚函数时,调用delete会使程序奔溃。因为生成对象时,指针永远保存的是基类的起始地址。
    class Base{
    public:
    	Base(int a):ma(a){
    		cout<<"Base(int)"<<endl;
    	}
    	~Base(){
    		cout<<"~Base()"<<endl;
    	}
    	void operator delete(void *p){
    		cout<<"free start addr:"<<p<<endl;
    		free(p);
    	}
    private:
    	int ma;
    };
    class Derive:public Base
    {
    public:
    	Derive(int b):Base(b),mb(b){
    		cout<<"Object start addr:"<<this<<endl;
    		cout<<"Derive(int)"<<endl;
    	}
    	~Derive(){
    		cout<<"~Derive()"<<endl;
    	}
    	virtual void show(){
    		cout<<"mb="<<mb<<endl;
    	}//只有派生类有虚函数
    	void* operator new(size_t size){
    		void* p=malloc(size);
    		cout<<"malloc start addr:"<<p<<endl;
    		return p;
    	}
    private:
    	int mb;
    };
    int main(){
    	Base* pb=new Derive(20);
    	delete pb;//程序会崩溃
    	return 0;
    }


    程序出现奔溃,从打印的地址上可以看出,派生类在生成时,是从0x01344560处开始开辟,因此指针也是从该地址处指向,但是在delete指针时,只是看指针的类型,而指针的类型是Base*,所以它就会调用基类的析构函数,基类部分和对象开辟的起始位置不一样,所以程序奔溃。

    派生类的内存布局:


    (3)写成虚析构

    class Base{
    public:
    	Base(int a):ma(a){
    		cout<<"Base(int)"<<endl;
    	}
    	virtual ~Base(){
    		cout<<"~Base()"<<endl;
    	}
    	void operator delete(void *p){
    		cout<<"free start addr:"<<p<<endl;
    		free(p);
    	}
    private:
    	int ma;
    };
    class Derive:public Base
    {
    public:
    	Derive(int b):Base(b),mb(b){
    		cout<<"Object start addr:"<<this<<endl;
    		cout<<"Derive(int)"<<endl;
    	}
    	~Derive(){
    		cout<<"~Derive()"<<endl;
    	}
    	void* operator new(size_t size){
    		void* p=malloc(size);
    		cout<<"malloc start addr:"<<p<<endl;
    		return p;
    	}
    private:
    	int mb;
    };
    int main(){
    	Base* pb=new Derive(20);
    	delete pb;
    	return 0;
    }

    运行成功并且没有内存泄漏:



    总结:将析构函数写成虚析构的前提条件是什么?===》在堆上生成对象时。

    八、虚函数与访问限定

    写出如下代码:

    class Base{
    public:
    	Base(int a):ma(a){}
    	virtual ~Base(){}
    	//virtual void show(){
    	//	cout<<"Base::ma="<<ma<<endl;
    	//}
    	//*1
    protected:
    	//virtual void show(){
    	//	cout<<"Base::ma="<<ma<<endl;
    	//}
    	//*2
    	int ma;
    };
    
    class Derive:public Base{
    public:
    	Derive(int b):Base(b),mb(b){}
    	~Derive(){}
    private:
    	void show()
    	{
    		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;
    	}
    	int mb;
    };
    int main()
    {
    	Base* p=new Derive(20);
    	p->show();//动态的绑定
    	delete p;
    
    	return 0;
    }

    在上述代码中,

    1)派生类中的虚函数不论写在private/protected/public下,只要基类的虚函数写成private/protected下时,会出现编译错误。

    2)派生类中的虚函数不论写在private/protected/public下,只要基类的虚函数写在public下时,运行成功。

    原因:

    1)访问限定符只在编译期间起作用。

    2)p为Base*类型(基类指针),编译期间调用基类的show方法(放在private/protected下时,编译错误),发现show方法为虚函数,动态的绑定,p指向的是derive类型的对象,运行的时候从派生的的虚表中找虚函数,运行时访问限定不起作用。

    展开全文
  • 本篇文章主要介绍了C++中的普通成员函数、虚函数以及纯虚函数,非常的详细,有需要的朋友可以参考下
  • 主要介绍了C++中的虚函数,在C++中,虚函数联系到多态、多态联系到继承,因而虚函数是C++中的一大重要特性,需要的朋友可以参考下
  • C++虚函数指针虚函数

    千次阅读 多人点赞 2017-07-10 22:25:57
    函数重载和运算符重载实现的多态属于静态多态,而通过虚函数可以实现动态多态。实现函数的动态联编其本质核心则是虚表指针与虚函数表。   1. 虚函数与纯虚函数区别 1)虚函数在子类里面也可以不重载的;但纯虚必须...

    C++的多态可以分为静态多态和动态多态。函数重载和运算符重载实现的多态属于静态多态,而通过虚函数可以实现动态多态。实现函数的动态联编其本质核心则是虚表指针与虚函数表。

     

    1. 虚函数与纯虚函数区别

    1)虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现

    2)带纯虚函数的类叫虚基类也叫抽象类,这种基类不能直接生成对象,只能被继承,重写虚函数后才能使用,运行时动态动态绑定!

     

    2.子类继承父类(子类和父类无同名虚函数的时候)

    如果父类有实现名为虚函数fucnA ,而子类没有实现虚函数funcA,那么子类将继承该虚函数,该虚函数将存在子类的虚函数表中。

     

    3.子类继承父类(子类覆盖父类的同名虚函数时)

    子类继承父类,并且子类的虚函数覆盖父类的同名虚函数的时候,子类的虚函数表将存放着子类的虚函数及没有被覆盖的同名虚函数。

     

    4.为什么构造函数不能是虚函数

    虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。



    虚函数表和虚指针


    虚函数表的地址vptr

    虚指针函数在vtable里面






    从上图可以看出,Derived继承Base,子类Derived实现的虚函数将覆盖父类Base的同名虚函数,如start,stop,另外子类Derived将会继承没有实现的Base虚函数,如stop2

    从图中可以看出,虚函数表是实现多态的核心,即就算使用基类指针指向派生类对象,调用的虚函数跟通过虚函数表来查找。


    下面的代码将显示出虚函数的地址

    #include <iostream>
    
    class Base
    {
    public:
    	virtual int start()
    	{
    		std::cout<<"Base start"<<std::endl;
    	}
    	
    	virtual int stop()
    	{
    		std::cout<<"Base stop"<<std::endl;
    	}
    
    	virtual int stop2()
    	{
    		std::cout<<"Base stop2"<<std::endl;
    	}
    	int func()
    	{
    
    	}
    private:
    	int m_b;
    };
    
    class Derived : public Base
    {
    public:
        virtual	int start()
    	{
    		std::cout<<"Derived start"<<std::endl;
    	}
    
    	virtual int middle()
    	{
    		std::cout<<"Derived middle"<<std::endl;
    	}
    	 
        virtual	int stop()
    	{
    		std::cout<<"Derived stop"<<std::endl;
    	}
    	
    private:
    	int m_d;
    };
    
    typedef int (*pfun)();
    int main()
    {
    	Base base;
    	Derived derived;
    
    	Base *pBase  = new Derived();
    	pfun pf = NULL;
    	
    	pf = (pfun) ((int **)(*(int *)pBase))[0];
    	pf();
    	pf = (pfun)((int **)(*(int *)pBase))[1];
    	pf();
    	pf = (pfun)((int **)(*(int *)pBase))[2];
    	pf();
    	pf = (pfun)((int **)(*(int *)pBase))[3];
    	pf();
    
    	 //虚函数表的地址
    	std::cout<<"  virtual  table  addr " << (int *) pBase<<std::endl;
    	
    	std::cout<<((int **)(*(int *)pBase))[0]<<std::endl;
        std::cout<<((int **)(*(int *)pBase))[1]<<std::endl;
        std:: cout<<((int **)(*(int *)pBase))[2]<<std::endl;
        std::cout<<((int **)(*(int *)pBase))[3]<<std::endl;
    
        return 0;
    }
    






    常见虚函数面试题


    45. 虚函数,虚函数表里面内存如何分配?


    编译时若基类中有虚函数,编译器为该的类创建一个一维数组的虚表,存放是每个虚函数的地址。基类和派生类都包含虚函数时,这两个类都建立一个虚表。构造函数中进行虚表的创建和虚表指针的初始化。在构造子类对象时,要先调用父类的构造函数,初始化父类对象的虚表指针,该虚表指针指向父类的虚表。执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。每一个类都有虚表。虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。当用一个指针/引用调用一个函数的时候,被调用的函数是取决于这个指针/引用的类型。即如果这个指针/引用是基类对象的指针/引用就调用基类的方法;如果指针/引用是派生类对象的指针/引用就调用派生类的方法,当然如果派生类中没有此方法,就会向上到基类里面去寻找相应的方法。这些调用在编译阶段就确定了。当涉及到多态性的时候,采用了虚函数和动态绑定,此时的调用就不会在编译时候确定而是在运行时确定。不在单独考虑指针/引用的类型而是看指针/引用的对象的类型来判断函数的调用,根据对象中虚指针指向的虚表中的函数的地址来确定调用哪个函数。


    46. 纯虚函数如何定义?含有纯虚函数的类称为什么?为什么析构函数要定义成虚函数?


    纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。纯虚函数是虚函数再加上= 0。virtual void fun ()=0。含有纯虚函数的类称为抽象类在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。如果析构函数不是虚函数,那么释放内存时候,编译器会使用静态联编,认为p就是一个基类指针,调用基类析构函数,这样子类对象的内存没有释放,造成内存泄漏。定义成虚函数以后,就会动态联编,先调用子类析构函数,再基类。
    47. C++ 中哪些不能是虚函数?


    1)普通函数只能重载,不能被重写,因此编译器会在编译时绑定函数。
    2)构造函数是知道全部信息才能创建对象,然而虚函数允许只知道部分信息。
    3)内联函数在编译时被展开,虚函数在运行时才能动态绑定函数。
    4)友元函数 因为不可以被继承。
    5)静态成员函数 只有一个实体,不能被继承。父类和子类共有。





    展开全文
  • C++虚函数及其纯虚函数学习。 虚函数:子类可以实现具体函数,如果子类没有实现基类回自动补上。 纯虚函数:子类必须实现,不实现编译 不能通过。 虚析构:基类必须用虚析构函数这样子类析构(消失灭亡)时候才会...
  • 虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型(也...
  • 虚函数表工作原理

    2012-11-02 13:22:11
    虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容...
  • 一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。
  • 虚函数 2 之虚函数的定义

    千次阅读 2019-01-24 23:20:34
    1、虚函数的定义 虚函数就是在基类中被关键字 virtual 说明,并在派生类中重新定义的函数。 虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数...

    1、虚函数的定义

    • 虚函数就是在基类中被关键字 virtual 说明,并在派生类中重新定义的函数。
    • 虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
    • 虚函数的定义是在基类中进行的,它是在基类中在那些需要定义为虚函数的成员函数的声明中冠以关键字 virtual 。定义虚函数的方法如下:
    virtual 函数类型 函数名(形参表){
    	      函数体;
    }
    

      在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。在派生类中重新定义时,其函数原型,包括函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。

    例 1:虚函数的使用

    #include<iostream>
    using namespace std;
    class B0{
    	public:
    		virtual void print(char *p){	//定义虚函数 print 
    			cout<<p<<"print()"<<endl;
    		}
    };
    class B1:public B0{
    	public:
    		virtual void print(char *p){	//重新定义虚函数 print 
    			cout<<p<<"print()"<<endl;
    		}
    };
    class B2:public B1{
    	public:
    		virtual void print(char *p){	//重新定义虚函数 print 
    			cout<<p<<"print()"<<endl;
    		}
    };
    int main(){
    	B0 ob0,*op;	//定义基类对象 ob0 和对象指针 op
    	op=&ob0; 
    	op->print("B0::");    //调用基类 B0 的 print 
    	B1 ob1;  //定义派生类 B1 的对象 
    	op=&ob1;
    	op->print("B1::");  //调用派生类 B1 的 print 
    	B2 ob2;
    	op=&ob2;
    	op->print("B2::");
    	return 0;
    }
    

    执行结果:
    在这里插入图片描述
    说明:
    (1)若在基类中,只声明虚函数原型(需加上 virtual),而在类外定义虚函数时,则不必再加 virtual。例如:

    class B0{
    	public:
    			virtual void print(char *p);     //声明虚函数原型,需加上 virtual
    };
    

    在类外,定义虚函数时,不要加 virtual:

    void B0::print(char *p){
    		cout<<p<<"print()"<<endl;
    }
    

    (2)在派生类中,虚函数被重新定义时,其函数的原型与基类中的函数原型(即包括函数类型、函数名、参数个数、参数类型的顺序)都必须完全相同。
    (3)C++ 规定,当一个成员函数被定义为虚函数后,其派生类中符合重新定义虚函数要求的同名函数都自动称为虚函数。因此,在派生类中重新定义该虚函数时,关键字 virtual 可以不写。 但是,为了使程序更加清晰最好在每一层派生类中定义该函数时都加上关键字 virtual。
    (4)如果在派生类中没有对基类的虚函数重新定义,则公有派生类继承其直接基类的虚函数。一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。 例如:

    class B0{
    	···
    	public:
    		virtual void show();	//在基类定义 show 为虚函数
    };
    class B1:public B0{
    	···
    };
    

    若在公有派生类 B1 中没有重新定义虚函数 show ,则函数 show 在派生类中被继承,仍是虚函数。
    (5)虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
    (6)使用对象名和点运算符的方式调用虚函数是在编译时进行的,是静态联编,没有利用虚函数的特性。只有通过基类指针访问虚函数时才能获得运行时的多态性。

    例 2:使用对象名和点运算符的方式调用虚函数

    #include<iostream>
    using namespace std;
    class B0{
    	public:
    	   virtual void print(char *p){	//定义虚函数 print 
    			cout<<p<<"print()"<<endl;
    		}
    };
    class B1:public B0{
    	public:
    	  virtual void print(char *p){
    			cout<<p<<"print()"<<endl;
    		}
    };
    class B2:public B1{
    	public:
    	  virtual void print(char *p){
    			cout<<p<<"print()"<<endl;
    		}	
    };
    int main(){
    	B0 ob0;
    	ob0.print("B0::");
    	B1 ob1;
    	ob1.print("B1::");
    	B2 ob2;
    	ob2.print("B2::");
    	return 0;
    }
    

    2、虚析构函数

      在 C++ 中,不能声明虚构造函数,但是可以声明虚析构函数。
      https://blog.csdn.net/aaqian1/article/details/84915540 中介绍了先执行派生类的析构函数,再执行基类的析构函数。

    例 3:虚析构函数的引例 1:

    #include<iostream>
    using namespace std;
    class B{
    	public:
    		~B(){
    			cout<<"调用基类 B 的析构函数\n";
    		}
    };
    class D:public B{
    	public:
    		~D(){
    			cout<<"调用派生类 D 的析构函数\n";
    		}
    };
    int main(){
    	D obj;
    	return 0;
    }
    

      本程序运行结果符合预想,即先执行派生类的析构函数,再执行基类的析构函数。但是,如果在主函数中用 new 运算符建立一个派生类的无名对象和定义了一个基类的对象指针,并将无名对象的地址赋给这个对象指针。当用 delete 运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。

    例 4:虚析构函数的引例2

    #include<iostream>
    using namespace std;
    class B{
    	public:
    		~B(){
    			cout<<"调用基类 B 的析构函数\n";
    		}
    };
    class D:public B{
    	public:
    		~D(){
    			cout<<"调用派生类 D 的析构函数\n";
    		}
    };
    int main(){
    	B *p;	//定义指向基类 B 的指针变量 p
    	p=new D;	
    //用运算符 new 为派生类的无名对象动态地分配了一个存储空间,并将地址赋给对象指针 p
    	delete p;
    //用 delete 撤销无名对象,释放动态存储空间
    	return 0; 
    }
    

    执行结果:
    在这里插入图片描述
      当撤销指针 P 所指的派生类的无名对象,而调用析构函数时,采用了静态联编方式,只调用了基类 B 的析构函数。
      如果希望程序执行动态联编方式,在用 delete 运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。

    例 5:虚析构函数的使用

    #include<iostream>
    using namespace std;
    class B{
    	public:
    		virtual ~B(){
    			cout<<"调用基类 B 的析构函数\n";
    		}
    };
    class D:public B{
    	public:
    		virtual ~D(){
    			cout<<"调用派生类 D 的析构函数\n";
    		}
    };
    int main(){
    	B *p;	//定义指向基类 B 的指针变量 p
    	p=new D;	
    //用运算符 new 为派生类的无名对象动态地分配了一个存储空间,并将地址赋给对象指针 p
    	delete p;
    //用 delete 撤销无名对象,释放动态存储空间
    	return 0; 
    }
    
    

    在这里插入图片描述  由于使用了虚析构函数,程序执行了动态联编,实现了运行的动态性。虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数。

    3、虚函数与重载函数的关系

      在一个派生类中重新定义基类的虚函数是函数重载的另一种形式,但它不同于一般的函数重载。
      当普通的函数重载时,其函数的 参数参数类型 有所不同,函数的 返回类型 也可以不同。但是,当重载一个虚函数时,即在派生类中重新定义虚函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。①如果仅仅返回类型不同,其余均相同,系统会给出错误信息;②若仅仅函数名相同,而参数的个数,类型或顺序不同,系统将它作为普通的函数重载,这时虚函数的特性将丢失。

    例 6:虚函数与重载函数的关系

    #include<iostream>
    using namespace std;
    class Base{
    	public:
    		virtual void fun1();
    		virtual void fun2();
    		virtual void fun3();
    		void fun4();
    };
    class Derived:public Base{
    	public:
    	    virtual void fun1();	//fun1 是虚函数,这里可不写 virtual 
    		void fun2(int x);		//与基类中的 fun2 作为普通函数重载,虚特性消失
    	//	char fun3();			//错误,因为与基类只有返回类型不同,应删去
    		void fun4(); 
    };
    void Base::fun1(){
    	cout<<"---Base fun1---"<<endl;	
    }
    void Base::fun2(){
    	cout<<"---Base fun2---"<<endl;	
    }
    void Base::fun3(){
    	cout<<"---Base fun3---"<<endl;	
    }
    void Base::fun4(){
    	cout<<"---Base fun4---"<<endl;	
    }
    void Derived::fun1(){
    	cout<<"---Derived fun1---"<<endl;
    } 
    void Derived::fun2(int x){
    	cout<<"---Derived fun2---"<<endl;
    }
    /*
    void Derived::fun3(){
    	cout<<"---Derived fun3---"<<endl;
    }*/
    void Derived::fun4(){
    	cout<<"---Derived fun4---"<<endl;
    }
    int main(){
    	Base d1,*bp;
    	Derived d2;
    	bp=&d2;
    	bp->fun1();
    	bp->fun2();
    	bp->fun4();
    	return 0;
    }
    

    执行结果:
    在这里插入图片描述

    4、多重继承与虚函数

      多重继承可以视为多个单继承的组合。因此,多重继承情况下的虚函数调用与单继承情况下的虚函数调用有相似之处。

    例 7:多重继承与虚函数的例子

    #include<iostream>
    using namespace std;
    class Base1{
    	public:
    		virtual void fun(){		//定义 fun 是虚函数 
    			cout<<"--Base1--\n";
    		}
    };
    class Base2{
    	public:
    		void fun(){		//定义 fun 是普通的成员函数 
    			cout<<"--Base2--\n";
    		}
    };
    class Derived:public Base1,public Base2{
    	public:
    		void fun(){
    			cout<<"--Derived--\n";
    		} 
    };
    int main(){
    	Base1 *ptr1;	//定义指向基类 Base1 的对象指针 ptr1
    	Base2 *ptr2;	//定义指向基类 Base2 的对象指针 ptr2
    	Derived obj3;	//定义派生类 Derived 的对象 obj3 
    	ptr1=&obj3;		
    	ptr1->fun();	
    //此处的 fun为虚函数,因此调用派生类 Derived 的虚函数 fun
    	ptr2=&obj3;
    	ptr2->fun();	
    //此处的 fun为非虚函数,而 ptr2 为类 Base2 的对象指针,因此调用基类 Base2 的函数 fun
    	return 0;
    }
    

    执行结果:
    在这里插入图片描述  相对于 Base1 的派生路径,由于 Base1 中的 fun 是虚函数,当声明为指向 Base1 的指针指向派生类 Derived 的对象 obj3 时,函数 fun 呈现出虚特性。
      相对于 Base2 的派生路径,由于 Base2 中的 fun 是
    一般成员函数,所以此时它只能是一个普通的重载函数,当声明为指向 Base2 的指针指向 Derived 的对象 obj3 时,函数 fun 只呈现普通函数的重载特性。

    5、虚函数举例

    例 8:应用 C++ 的多态性,计算三角形、矩形和圆的面积。

    #include<iostream>
    using namespace std;
    class Figure{	//定义一个公共基类 
    	protected:
    		double x,y;
    	public:
    		Figure(double a,double b){
    			x=a;
    			y=b;
    		}
    		virtual void area(){	//定义一个虚函数,作为界面接口 
    			cout<<"在基类中定义的虚函数,";
    			cout<<"为派生类提供一个公共接口,";
    			cout<<"以便派生类根据需要重新定义虚函数。";
    		}
    };
    class Triangle:public Figure{	//定义三角形派生类 
    	public:
    		Triangle(double a,double b):Figure(a,b){ 	//构造函数 
    		}
    		void area(){	//虚函数重新定义,用作求三角形的面积 
    			cout<<"三角形的高是:"<<x<<",底是:"<<y;
    			cout<<",面积是:"<<0.5*x*y<<endl;
    		}
    };
    class Square:public Figure{
    	public:
    		Square(double a,double b):Figure(a,b){
    		}
    		void area(){	//虚函数重新定义,用作求矩形的面积 
    			cout<<"矩形的长是:"<<x<<",宽是:"<<y<<",面积是:"<<x*y<<endl; 
    		}
    };
    class Circle:public Figure{		//定义圆派生类
     	public:
     		Circle(double a):Figure(a,a){
    		 }
    		 void area(){
    		 	cout<<"圆的半径是:"<<x<<",面积是:"<<3.1416*x*x<<endl;
    		 }
    };
    int main(){
    	Figure *p;
    	Triangle t(10.0,6.0);
    	Square s(10.0,6.0);
    	Circle c(10.0);
    	p=&t;
    	p->area();
    	p=&s;
    	p->area();
    	p=&c;
    	p->area();
    	return 0;
    }
    

    运行结果:
    在这里插入图片描述

    展开全文
  • 虚函数详解

    千次阅读 多人点赞 2019-03-09 20:05:26
    文章目录一、虚函数实例二、虚函数的实现(内存布局)1、无继承情况2、单继承情况(无虚函数覆盖)3、单继承情况(有虚函数覆盖)4、多重继承情况(无虚函数覆盖)5、多重继承情况(有虚函数覆盖)三、虚函数的相关...

    一、多态与重载

    1、多态的概念

      面向对象的语言有三大特性:继承、封装、多态。虚函数作为多态的实现方式,重要性毋庸置疑。

      多态意指相同的消息给予不同的对象会引发不同的动作(一个接口,多种方法)。其实更简单地来说,就是“在用父类指针调用函数时,实际调用的是指针指向的实际类型(子类)的成员函数。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的

    2、重载—编译期多态的体现

      重载,是指在一个类中的同名不同参数的函数调用,这样的方法调用是在编译期间确定的

    3、虚函数—运行期多态的体现

      运行期多态发生的三个条件:继承关系、虚函数覆盖、父类指针或引用指向子类对象

    二、虚函数实例

    在这里插入图片描述
      在上述例子中,我们首先定义了一个基类base,基类有一个名为vir_func的虚函数,和一个名为func的普通成员函数。而类A,B都是由类base派生的子类,并且都对成员函数进行了重载。然后我们定义三个base类型的指针Base、a、b分别指向类base、A、B。可以看到,当使用这三个指针调用func函数时,调用的都是基类base的函数而使用这三个指针调用虚函数vir_func时,调用的是指针指向的实际类型的函数。最后,我们将指针b做强制类型转换,转换为A类型指针,然后分别调用func和vir_func函数,发现普通函数调用的是类A的函数,而虚函数调用的是类B的函数。

      以上,我们可以得出结论当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定

      虚函数的实现过程:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。

    三、虚函数的实现(内存布局)

      虚函数表中只存有一个虚函数的指针地址,不存放普通函数或是构造函数的指针地址。只要有虚函数,C++类都会存在这样的一张虚函数表,不管是普通虚函数亦或是纯虚函数,亦或是派生类中隐式声明的这些虚函数都会生成这张虚函数表。

      虚函数表创建的时间:在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置。虚函数表其实就是函数指针的地址。函数调用的时候,通过函数指针所指向的函数来调用函数。

    1、无继承情况

    #include <iostream>
    using namespace std;
     
    class Base
    {
    public:
        Base(){cout<<"Base construct"<<endl;}
        virtual void f() {cout<<"Base::f()"<<endl;}
        virtual void g() {cout<<"Base::g()"<<endl;}
        virtual void h() {cout<<"Base::h()"<<endl;}
        virtual ~Base(){}
    };
     
    int main()
    {
        typedef void (*Fun)();  //定义一个函数指针类型变量类型 Fun
        Base *b = new Base();
        //虚函数表存储在对象最开始的位置
        //将对象的首地址输出
        cout<<"首地址:"<<*(int*)(&b)<<endl;
     
        Fun funf = (Fun)(*(int*)*(int*)b);
        Fun fung = (Fun)(*((int*)*(int*)b+1));//地址内的值 即为函数指针的地址,将函数指针的地址存储在了虚函数表中了
        Fun funh = (Fun)(*((int *)*(int *)b+2));
     
        funf();
        fung();
        funh();
     
        cout<<(Fun)(*((int*)*(int*)b+4))<<endl; //最后一个位置为0 表明虚函数表结束 +4是因为定义了一个 虚析构函数
     
        delete b;
        return 0;
    }
    

    在这里插入图片描述

    2、单继承情况(无虚函数覆盖)

      假设有如下所示的一个继承关系:
    在这里插入图片描述
      请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
    在这里插入图片描述
    【Note】:

    • 虚函数按照其声明顺序放于表中

    • 父类的虚函数在子类的虚函数前面

    3、单继承情况(有虚函数覆盖)

      覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
    在这里插入图片描述
      为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
    在这里插入图片描述
    【Note】:

    • 覆盖的f()函数被放到了虚表中原来父类虚函数的位置

    • 没有被覆盖的函数依旧在原来的位置

    这样,我们就可以看到对于下面这样的程序,

    Base *b = new Derive();
    b->f();
    

    由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

    4、多重继承情况(无虚函数覆盖)

      下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
    在这里插入图片描述
    对于子类实例中的虚函数表,是下面这个样子:

    在这里插入图片描述
    【Note】:

    • 每个父类都有自己的虚表(有几个基类就有几个虚函数表)

    • 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)。

    5、多重继承情况(有虚函数覆盖)

      下面我们再来看看,如果发生虚函数覆盖的情况。下图中,我们在子类中覆盖了父类的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()
    

    四、虚函数的相关问题

    1、构造函数为什么不能定义为虚函数

      构造函数不能是虚函数。

      首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区。

    2、析构函数为什么要定义为虚函数?

      析构函数可以是虚函数且推荐最好设置为虚函数。

    class B
    {
    public:
        B() { printf("B()\n"); }
        virtual ~B() { printf("~B()\n"); }
    private:
        int m_b;
    };
     
    class D : public B
    {
    public:
        D() { printf("D()\n"); }
        ~D() { printf("~D()\n"); }
    private:
        int m_d;
    };
     
    int main()
    {
        B* pB = new D();
        delete pB;
        return 0;
    }
    

    在这里插入图片描述
      C++中有这样的约束:执行子类构造函数之前一定会执行父类的构造函数;同理,执行子类的析构函数后,一定会执行父类的析构函数,这也是为什么我们一直建议类的析构函数写成虚函数的原因。

    3、如何去验证虚函数表的存在

    typedef void(*Fun)(void);
    // 取类的一个实例
    Base b;
    Fun pFun = NULL;
    // 把&b转成int ,取得虚函数表的地址
    cout << "虚函数表地址:" << (int*)(&b) << endl;
    // 再次取址就可以得到第一个虚函数的地址了
    cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
    pFun = (Fun)*((int*)*(int*)(&b));
    pFun();
    

    参考:https://mp.weixin.qq.com/s?__biz=MzIzNjk2NjUxOQ==&mid=2247483655&idx=1&sn=5b29918a121006d14a09e75d2dcb0a8b&chksm=e8ce861fdfb90f09aaa9a5f3c3bbf38b342f73fdfbe37c111c39937e7baa931fc0ac73cf8074#rd

    展开全文
  • 只有用virtual声明类的成员函数,使之成为虚函数,不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。所以虚函数只能用于类的继承层次结构中。  一个成员函数被声明为...
  • C++ 虚函数专题

    2020-12-17 11:46:58
    虚函数 基类中使用virtual关键字声明的函数,称为虚函数虚函数的实现,通过虚函数表来实现的。即V-table 这个表中有一个类,用于储存虚函数的地址。解决其继承,覆盖的问题,用于保证其真实反映的函数。这样有...
  • C++中重载与重写函数区别及虚函数
  • 要成为虚函数必须满足两点,一就是这个函数依赖于对象调用,因为虚函数就是依赖于对象调用,因为虚函数是存在于虚函数表中,有一个虚函数指针指向这个虚表,所以要调用虚函数,必须通过虚函数指针,而虚函数指针是...
  • 1.子类重写父类“虚函数”,子类也写“虚函数” #include<iostream> using namespace std; class parent{ public: virtual void s(){ cout<<"parent"<<endl; } }; class son : ...
  • C++虚函数的实现

    千次阅读 2019-05-09 16:41:17
    http://blog.kongfy.com/2015/08/探索c虚函数在g中的实现/?utm_source=tuicool&utm_medium=referral ...一、虚函数表解析 前言 虚函数表 一般继承(无虚函数覆盖) 一般继承(有虚函数覆盖) 多重继承(无...
  • C++虚函数虚函数表原理

    万次阅读 多人点赞 2018-07-26 19:49:54
    虚函数的地址存放于虚函数表之中。运行期多态就是通过虚函数虚函数表实现的。 类的对象内部会有指向类内部的虚表地址的指针。通过这个指针调用虚函数虚函数的调用会被编译器转换为对虚函数表的访问: ptr-...
  • 虚函数和纯虚函数的理解区别,最重要的是知道虚函数和纯虚函数的用法
  • C++虚函数表详解

    千次阅读 多人点赞 2019-01-05 09:51:57
    C++的虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖(override)的问题,保证其能真实的反应实际的函数。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 228,471
精华内容 91,388
关键字:

虚函数