虚函数

虚函数虚函数的定义

虚函数是一个类的成员函数,它的定义语法如下:
在这里插入图片描述
特别注意:

  1. 当一个类的一个成员函数被定义为虚函数时,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征
  2. 当在派生类中重新定义虚函数时,不必加关键字 virtual。但重新定义时不仅要求函数同名,而且要求函数的参数列表与返回值类型也必须和基类中的虚函数相同,否则编译器会报错
  3. 虚函数可以在先在类内进行声明,然后在类外定义。但在类内声明时需要在返回值类型之前加上关键字 virtual,在类外定义时则不需要在添加关键字 virtual

虚函数使用的注意事项

  • 派生类中重定义虚函数时,虚函数的函数名必须与其基类中的虚函数的函数名相同,除此之外要求参数列表和函数的返回值类型也必须相同
    • 当基类中的虚函数的返回值类型是基类类型的指针时,允许在派生类中重定义该虚函数时将返回值类型改写为派生类类型的指针
#include<iostream>
using namespace std;

class Animal{
public:
    int value;
    Animal():value(0){}
    Animal(int v):value(v){}
    virtual Animal* show(){ //返回值类型是Animal类型的指针 
        cout<<"Animal类中的value值是:"<<value<<endl;
        return this;
    } 
};

class Person:public Animal{ 
public:
    int value;
    Person():value(0){}
    Person(int v):value(v){}
    Person* show(){ //返回值类型是Person类型的指针  
        cout<<"Person类中的value值是:"<<value<<endl;
        return this; 
    }
};

int main(){
    Animal animal(10);
    Person person(20);
    animal.show();
    cout<<"------------分界线----------------"<<endl;
    person.show();
    return 0;
}

在这里插入图片描述

  • 只有类中的成员函数才有资格成为虚函数。这是因为虚函数仅适用于有继承关系的类对象(建议成员函数尽可能地设置为虚函数)
  • 类中的静态成员函数是该类所有对象所共有的,不受限于某个对象个体,因此不能作为虚函数
  • 内联 (inline) 函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的
  • 类中的析构函数可以作为虚函数,但构造函数不能作为虚函数。这是因为在调用构造函数时对象还没有完成实例化,而调用析构函数时对象已经完成了实例化
    • 在基类中及其派生类中都动态分配内存空间时,必须把析构函数定义为虚函数,从而实现 “销毁” 对象时的多态性。例如在 C++ 中用 new 运算符建立临时对象时,若基类中有析构函数并且同时定义了一个指向该基类的指针变量,但指针变量指向的对象却是该基类的派生类对象,那么在程序执行 delete 操作时,系统只会执行基类中的析构函数,而不会执行派生类中的析构函数,从而造成内存泄漏
    • 这是因为 这里的指针本质上指向的其实是派生类对象中隐藏包含的基类子对象 。将基类的析构函数定义为虚函数可以解决这个问题。
      • 当基类中的析构函数为虚函数时,无论指针指向的是同一类族中的哪一个对象,系统都会采用动态关联,调用相应的析构函数,对该对象进行清理工作。因此最好将基类中的析构函数声明为虚函数,这将使该基类的所有派生类的析构函数自动成为虚函数
      • 总结来说,基类需要虚析构函数的原因是:防止内存泄漏。想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象。假如没有虚析构函数,释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏
#include<iostream>
using namespace std;

class Father{
public:
    Father()=default;
    ~Father(){
        cout<<"调用Father类的析构函数"<<endl;
    }
};

class Son:public Father{
public:
    Son()=default;
    ~Son(){
        cout<<"调用Son类的析构函数"<<endl;
    }
};

int main(){
    Father* ptr=new Son;
    delete ptr;
    return 0;
}

运行结果

#include<iostream>
using namespace std;

class Father{
public:
    Father()=default;
    virtual ~Father(){
        cout<<"调用Father类的析构函数"<<endl;
    }
};

class Son:public Father{
public:
    Son()=default;
    ~Son(){
        cout<<"调用Son类的析构函数"<<endl;
    }
};

int main(){
    Father* ptr=new Son;
    delete ptr;
    return 0;
}

在这里插入图片描述

  • 使用虚函数会使程序的运行速度降低,这是因为为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现,但程序的通用性会变得更高
  • 如果虚函数的定义放在类外,virtual 关键字只加在函数的声明的前面,不能再添加在函数定义的前面。正确的定义必须不包括关键字 virtual

虚表

什么是虚表

在 C++ 中,虚函数是通过虚表(Virtual Table)来实现的。虚表本质上是一个类的虚函数的地址表,它解决了继承、覆盖的问题,保证其容量真实反映实际的函数。而 对虚表的利用,往往需要通过指向虚表的指针来实现在 C++ 中,编译器必须保证虚表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量),这样我们便可以通过对象实例的地址得到虚表,然后利用指向虚表的指针遍历虚表中的函数指针,并调用相应的函数

#include<iostream>
using namespace std;

class Father{
public:
    virtual void show(){ //虚函数1 
        cout<<"调用Father类的成员方法show()"<<endl;
    }
    virtual void func(){ //虚函数2 
        cout<<"调用Father类的成员方法func()"<<endl; 
    }
    void print(){ //普通成员函数 
        cout<<"调用Father类的成员方法print()"<<endl;
    } 
};

class Son:public Father{ 
public:
    void show(){ //虚函数1 
        cout<<"调用Son类的成员方法show()"<<endl;
    }
    void func(){ //虚函数2 
        cout<<"调用Son类的成员方法func()"<<endl; 
    }
    virtual void list(){ //虚函数3 
        cout<<"调用Son类的成员方法list()"<<endl;
    }
};

int main(){
    Father father;
    father.show();
    father.func();
    father.print();
    cout<<"-----------分界线------------"<<endl;
    Son son;
    son.show();
    son.func();
    son.print();
    son.list();
    return 0;
}

在这里插入图片描述

图解说明

当 Father 类创建对象 father 后,其内存分布大致如下:
在这里插入图片描述
Father 类中的成员函数 print () 是普通成员函数,因此其不再虚表中。
当 Son 类创建对象 son 后,其内存分布大致如下:
在这里插入图片描述

虚表的四种情况

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

在这里插入图片描述
如上图所示,在这个继承关系中,子类 Derive 没有重定义任何父类 Base 的虚函数,而是在其继承父类 Base 的基础上添加了三个新的虚函数,其代码主要如下(摘要):

class Base{//基类
public:
    virtual void f();
    virtual void g();
    virtual void h(); 
}; 

class Derive{ //子类
public:
    virtual void f1();
    virtual void g1();
    virtual void h1(); 
};

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:
在这里插入图片描述
从中我们可以看出:

  • 虚函数按照其声明顺序放于虚表中
  • 父类的虚函数在子类的虚函数的前面

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

在这里插入图片描述
如上图所示,在这个继承关系中,子类 Derive 重定义了部分父类 Base 的虚函数,其代码主要如下(摘要):

class Base{//基类
public:
    virtual void f();
    virtual void g();
    virtual void h(); 
}; 

class Derive{ //子类
public:
    void f(); //重定义基类中的虚函数
    virtual void g1();
    virtual void h1(); 
};

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:
在这里插入图片描述
从中我们可以看出:

  • 派生类中重定义的虚函数(如 void f ())会被放到虚表中原来父类虚函数的位置
  • 没有被重定义的虚函数保持原样

正因如此,当程序执行语句:

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

由于虚表中 Base::f () 的位置已经被 Derive::f () 函数地址所取代,因此指针 b 此时调用的函数 f () 是 Derive::f (),而不是 Base::f ()

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

在这里插入图片描述
如上图所示,在这个继承关系中,子类 Derive 没有重定义任何父类中的虚函数,而是在其继承所有父类的基础上添加了两个新的虚函数,其代码主要如下(摘要):

class Base1{//基类1
public:
    virtual void f();
    virtual void g();
    virtual void h(); 
}; 

class Base2{//基类2
public:
    virtual void f();
    virtual void g();
    virtual void h(); 
};
 
class Base3{//基类3
public:
    virtual void f();
    virtual void g();
    virtual void h(); 
}; 

class Derive{ //子类
public:
    virtual void f1();
    virtual void g1();
};

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:
在这里插入图片描述
从中我们可以看出:

  • 每一个父类都有自己的虚表
  • 子类中新增加的虚函数会被添加在第一个父类的虚表的后面(所谓的第一个父类时按照声明的顺序来判断的),这样做的目的是为了使不同父类类型的指针在指向同一个子类的实例时都能够调用到实际的函数

多重继承(由虚函数覆盖)

在这里插入图片描述
如上图所示,在这个继承关系中,子类 Derive 重定义了部分父类中的虚函数,其代码主要如下(摘要):

class Base1{//基类1
public:
    virtual void f();
    virtual void g();
    virtual void h(); 
}; 

class Base2{//基类2
public:
    virtual void f();
    virtual void g();
    virtual void h(); 
};
 
class Base3{//基类3
public:
    virtual void f();
    virtual void g();
    virtual void h(); 
}; 

class Derive{ //子类
public:
    void f();
    virtual void g1();
};

此时对于子类的实例(Derive d;)来说,其虚表结构大致如下:
在这里插入图片描述
从中我们可以看到:

  • 派生类中重定义虚函数(如 void f ())时,其所有父类的虚表中的相应位置都会被替换
  • 没有被重定义的虚函数保持原样
  • 11
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值