C++虚函数详解

1.虚函数的使用?

1.1虚函数的定义

在实现c++多态时会用到虚函数。虚函数使用的其核心目的是通过基类访问派生类定义的函数。所谓虚函数就是在基类定义一个未实现的函数名,为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。一般格式:

class base
{
public:
 base();
 virtual void test(); //定义的一个虚函数
private:
 char *basePStr;
};

上述代码在基类中定义了一个test的虚函数,所有可以在其子类重新定义父类的做法这种行为成为覆盖(override),或者为重写。

常见用法:声明基类指针,利用指针指向任意一个子类对象,调用相关的虚函数,动态绑定,由于编写代码时不能确定被调用的是基类函数还是那个派生类函数,所以被称为“”虚“”函数。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。


#include<iostream>  
using namespace std;  
  
class A  
{  
public:  
    void foo()  
    {  
        printf("1\n");  
    }  
    virtual void fun()  
    {  
        printf("2\n");  
    }  
};  
class B : public A  
{  
public:  
    void foo()  //隐藏:派生类的函数屏蔽了与其同名的基类函数
    {  
        printf("3\n");  
    }  
    void fun()  //多态、覆盖
    {  
        printf("4\n");  
    }  
};  
int main(void)  
{  
    A a;  
    B b;  
    A *p = &a;  
    p->foo();  //输出1
    p->fun();  //输出2
    p = &b;  
    p->foo();  //取决于指针类型,输出1
    p->fun();  //取决于对象类型,输出4,体现了多态
    return 0;  
}

 2.虚函数的常见错误-override和final

虚函数的两个常见错误:无意的重写、虚函数签名不匹配。

2.1.1、无意的重写

无意的重写 示例如下,在派生类中声明了一个与基类的某个虚函数具有相同的签名的成员函数,不小心重写了这个虚函数。

class Base {
public:
    virtual void Show(); // 虚函数
};

class Derived : public Base {
public:
    void Show(); // 无意的重写
};

2.1.2、虚函数签名不匹配

函数的签名包括:函数名,参数列表,const属性。

虚函数签名不匹配的错误通常是因为 函数名、参数列表 或 const 属性不一样,导致意外创建了一个新的虚函数,而不是重写一个已存在的虚函数。

class Base {
public:
    virtual void Show(int x); // 虚函数
};

class Derived : public Base {
public:
    virtual void Sh0w(int x); // o 写成了 0,新的虚函数 
    virtual void Show(double x); // 参数列表不一样,新的虚函数 
    virtual void Show(int x) const; // const 属性不一样,新的虚函数 
};

上述三种写法,编译器并不会报错,因为它不知道你的目的是重写虚函数,而是把它当成了新的虚函数。

2.2如何避免这些错误?

针对上述情况,C++ 11 增加了两个继承控制关键字:override 和 final,两者的作用分别为:

  • override:保证在派生类中声明的重载函数,与基类的虚函数有相同的签名;
  • final:阻止类的进一步派生 和 虚函数的进一步重写。

2.2.1、override

比如下面的代码,加了override,明确表示派生类的这个虚函数是重写基类的,如果派生类与基类虚函数的签名不一致,编译器就会报错。

class Base {
public:
    virtual void Show(int x); // 虚函数
};

class Derived : public Base {
public:
    virtual void Show(int x) const override; // const 属性不一样,新的虚函数 
};

报错信息如下:

因此,为了减少程序运行时的错误,重写的虚函数都建议加上 override。

2.2.2、final

如果不希望某个类被继承,或不希望某个虚函数被重写,则可以在类名和虚函数后加上 final 关键字,加上 final 关键字后,再被继承或重写,编译器就会报错。

class Base {
public:
    virtual void Show(int x) final; // 虚函数
};

class Derived : public Base {
public:
    virtual void Show(int x) override; // 重写提示错误  
};

报错信息如下:

因此,一旦一个虚函数被声明为final,则派生类不能再重写它。

3.虚函数之vptr(虚指针)和vtbl(虚表)

类在继承中的内存是如何实现管理的?

在类的继承中,每一个class产生一堆指向virtual function的指针,放在vtbl(虚表)中。对于每一个class object 被添加了一个指针,指向相关的virtual table,这里指针称为vptr(虚指针)。

这里写图片描述

下面我们结合上图对vptr和vtbl做进一步的讲解:

图中定义了三个类,分别为class A、class B、class C;其中class A class B的基类,class Bclass C的基类。

class A 的 公共成员中有四个函数接口,分别为虚函数 virtual void vfunc1()virtual void vfunc2();普通成员函数void func1()void func2()。class A中有两个私有成员,分别为m_data1m_data2
class B 继承了class A ,但是class B中的虚函数virtual void vfunc1()覆盖了基类的同名虚函数;此外class B的公共成员中还有一个属于自己的函数 void func2(),注意,这里面的函数func2虽然与class A中的函数func2同名,但是他们是互补相关的两个函数,也不存在谁覆盖谁,因为他们并没有将该同名函数申明为virture function(虚函数),所以class B的对象在调用void func2()的时候,它只能调用到class B自身公共成员函数中的void func2()函数,而无法调用到class A 中的void func2()函数。 对由于class B中的私有成员只有一个m_data3
class C继承了class B,但是但是class C中的虚函数virtual void vfunc1()覆盖了基类class B中的同名虚函数;此外存在一个公共成员函数:void func2(),其调用原理,参考上一段文字(在此处就不做详细说明了)。

现在我们知道了A、B、C这三个类的继承关系,以及各自所拥有的公有成员、私有成员以及各自的virtual function 。那么我们接下来就来谈谈,各自对象的虚指针和虚表。

从图的左上角可以看到,当class中有虚函数时,那么他所创建的对象将会多出一个指针(如图中的黑点所示为一个指针),这也是为什么类中有虚函数比类中没有虚函数在进行对象所占字节的测试时,会多出4个字节,而这多出的四个字节其实就是vptr(虚指针);在图中虚指针对应的地址为0x409004,对于对象a的成分除了包括一个vptr(虚指针)外,还包函两个数据成员m_data1、m_data2.&EMSP;对象a通过虚指针指向虚表(A vtbl),表中放的都是函数指针,指向虚函数。从class A中可以看出class A有两个虚函数,所以vtbl中有两个虚指针,分别指向对应的虚函数。(注意同种相同颜色的框框)。

此处补充一下:对于图中给出的三个类(class A,class B,class C)他们一共有8个函数。对应的四个普通成员函数四个虚函数,必须要明白的一点是,如果基类中有虚函数,那么子类中一定有虚函数,因为子类继承了基类的成份。并且虚指针只能调用虚函数,而不能调用普通成员函数。

 对于B的对象b,因为class B继承了class A,而class A中有虚函数,那么刚才我们说了继承时,由于子类继承了基类的成分,所以对象b一定也有一个虚指针,指向对应的虚函数。而对象b中由于是继承了基类A,所以对象b的成分按照顺序将会包含:一个虚指针,对象Am_data1,m_data2(基基类的成份),然后才到自身的m_data3

对于C的对象c,他的成分同样是按顺序包括:一个虚指针,类Am_data1,m_data2,类Bm_data3,和自身的m_data1、m_data4.

由于是虚指针,各自的虚指针(vptr)只会指向各自对象对应的虚表( vtbl ) ,对于class A的对象a的虚指针指向的虚表有两个虚函数分别为A::vfunc1()A::vfunc2()。这个比较好理解。

那么对于class B的对象b,它的虚指针呢。因为class Bclass A的子类,子类将会继承基类的成分,而基类有两个虚函数vfunc1()vfunc2(),这两个虚函数都属于基类的成分,所以继承时都需要继承,但是class B的虚函数virtual void vfunc1()将基类的同名虚函数覆盖掉了,那么实际上对象b只有两个虚函数,分别为自身的虚函数vfunc1(),和来自基类继承的虚函数vfun2()

   对于class C的对象c而言,其原理可以参考对象b,但是需要说明的一点是,由于class C继承了class B,而class B 又继承了class A,虽然class B中没有写出虚函数vfunc2(),但是实际上class B中时包含了class A的的虚函数vfunc2()的成分的。又因为class C,继承了class B,这时候我们从上面的图中左下角可以看到,class C的对象中其实上是包含了class B从class A中继承过来的成分。所以此时对于class C的对象c来说,它也是包含了两个虚函数的,分别为自身的虚函数vfunc1(),和A::vfunc2()

对于每个类对应的对象的vptr通过vtbl调用的虚函数如图中第二列和第三列之间的箭头所示(在同种可以通过图中相同的颜色进行区分)。

对于图中的(*(P->vptr)n)在图的左下角,有一个P,那么这行代码的实际意思是:通过指针找出它的虚指针,再找到它的虚表,第n个,把他当成函数指针来调用,由于是通过P来调用,所以P就是this point,所以括号里面的P就是this point


#include<iostream>
using namespace std;
 
class A {
public:
	virtual void vfunc1() { cout << "A::vfunc1()" << endl; };
	virtual void vfunc2() { cout << "A::vfunc2()" << endl; };
	void func1() { cout << "A::func1()" << endl; };
	void func2() { cout << "A::func2()" << endl; };
private:
	int data1_;
	int data2_;
};
 
class B :public A {
public:
	virtual void vfunc1() override { cout << "B::vfunc1()" << endl; };
	void func2() { cout << "B::func2()" << endl; };
private:
	int data3_;
};
 
class C :public B {
public:
	virtual void vfunc1() override { cout << "C::vfunc1()" << endl; };
	void func2() { cout << "C::func2()" << endl; };
private:
	int data1_, data4_;
};
 
//演示了手动调用虚函数的过程
int main() {
	B a;
	typedef void(*Fun)(void);
	Fun pFun = nullptr;
	cout << "虚函数表地址:" << (int*)(&a) << endl;
	cout << "虚函数表第1个函数地址:"<<(int*)*(int*)(&a) << endl;
	cout << "虚函数表第2个函数地址:" << (int*)*(int*)(&a) + 1 << endl;
	pFun = (Fun)*((int*)*(int*)(&a));
	pFun();
	pFun = (Fun)*((int*)*(int*)(&a) + 1);
	pFun();
	return 0;
}

4.虚函数与纯虚函数

首先:强调一个概念

定义一个函数为虚函数,不代表函数为不被实现的函数。

定义他为虚函数是为了允许用基类的指针来调用子类的这个函数

定义一个函数为纯虚函数,才代表函数没有被实现

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

4.1纯虚函数

4.1.1定义

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0:

virtual void funtion1()=0

4.1.2引入原因

在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。

定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

4.2抽象类

包含纯虚函数的类称为抽象类。

抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

4.2.1抽象类的作用

 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

4.2.2使用抽象类时注意:

  • 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
  • 抽象类是不能定义对象的。
  • 455
    点赞
  • 2084
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值