虚函数及多态

虚函数及多态

非虚函数(non-virtual):你不希望派生类(derived class)重新定义(override,重写)它。

虚函数(virtual):你希望派生类(derived class)重新定义(override,重写)它,且你对它已有默认定义。

纯虚函数(pure virtual):你希望派生类(derived class)一定要重新定义(override,重写)它,你对它没有默认定义。

1. 虚函数的详细知识:

  1. 虚函数的定义和声明

    在类的成员函数前加上virtual关键字就可以将其定义为虚函数。虚函数在基类中定义时,可以在派生类中进行重写,从而实现多态性。需要注意的是,虚函数只能是类的成员函数,不能是静态成员函数和全局函数

  2. 虚函数的调用

    通过基类指针或引用访问派生类对象时,会根据对象的实际类型来确定调用哪个版本的函数,即实现动态绑定。C++中使用的是后期绑定(late binding)的机制,即在运行时才确定调用哪个版本的函数。

  3. 虚函数的多态性

    虚函数通过定义为基类的成员函数,实现了多态性。多态性是指同一个函数名可以有不同的实现方式,通过基类指针或引用可以在不同的对象上调用不同的实现方式。多态性可以提高代码的灵活性和可扩展性,使得程序更易于维护和扩展。

  4. 纯虚函数

    **纯虚函数是一种特殊的虚函数,没有实现体,只有函数声明,即只有函数原型,没有函数实现。**纯虚函数可以被用来定义抽象类,抽象类不能被实例化,只能作为基类来使用。派生类必须实现基类中的所有纯虚函数,否则它也将成为一个抽象类。

  5. 虚析构函数

    虚析构函数是一种特殊的虚函数,用于在释放派生类对象时调用派生类析构函数。如果不使用虚析构函数,当基类指针指向派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,从而导致内存泄漏等问题。

  6. 虚函数的性能

    虚函数的实现会带来额外的开销,因为需要在运行时通过虚函数表来查找实际调用的函数。但是,对于大多数应用来说,这个开销是可以接受的。如果需要优化程序的性能,可以通过将虚函数转换为非虚函数、使用内联函数等方式来实现。

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

override错误

override是C++11引入的一个关键字,用于指示派生类中的函数是基类中虚函数的重写。在使用override时,编译器会检查函数声明是否与基类中的虚函数匹配,如果匹配则编译通过,否则会报错。但是,如果程序员在派生类中没有正确地重写基类中的虚函数,而仍然使用了override,就会出现错误。

例如:

class Base {
public:
    virtual void foo() { cout << "Base::foo()" << endl; }
};
class Derived : public Base {
public:
    void fooo() override { cout << "Derived::foo()" << endl; } // 错误,没有正确重写基类中的虚函数
};

在上面的例子中,Derived类中的fooo函数并没有重写Base类中的虚函数foo,而是定义了一个新的函数。由于使用了override关键字,编译器会认为这是一个重写,但实际上并没有实现多态性。这种错误可以通过仔细检查派生类中的函数声明和基类中的虚函数声明来避免。

函数签名不匹配

函数的签名包括:函数名,参数列表,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 属性不一样,新的虚函数 
};

final错误

final也是C++11引入的一个关键字,用于指示派生类不能再重写某个虚函数。在使用final时,编译器会检查派生类中是否重写了该虚函数,如果有则会报错。但是,如果程序员在基类中使用了final,就会出现错误。

例如:

class Base {
public:
    virtual void foo() final { cout << "Base::foo()" << endl; }
};
class Derived : public Base {
public:
    void foo() { cout << "Derived::foo()" << endl; } // 错误,不能再重写基类中已经使用final关键字的虚函数
};

在上面的例子中,Base类中的虚函数foo已经使用了final关键字,表示派生类不能再重写它。但是,在Derived类中仍然尝试重写它,就会出现错误。这种错误可以通过仔细检查基类中的虚函数声明和使用final关键字的位置来避免。

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

虚指针是一个指向虚表的指针,它是每个包含虚函数的类的一个隐藏成员变量。当对象被创建时,虚指针被初始化为指向该类的虚表。

虚表是一个指针数组,其中每个元素指向一个虚函数的地址。每个包含虚函数的类都有一个对应的虚表。虚表是在编译时创建的,对于每个类只有一个虚表,虚表中的元素按照虚函数在类中声明的顺序排列。

当调用一个虚函数时,实际上是通过对象的虚指针找到该对象的虚表,然后根据函数在虚表中的位置找到虚函数的地址,最终调用该函数。

虚函数及多态_第1张图片

图中定义了三个类,分别为class A、class B、class C;其中class Aclass 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 。那么我们接下来就来谈谈,各自对象的虚指针和虚表。

虚函数及多态_第2张图片

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

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


虚函数及多态_第3张图片

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

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

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

那么对于class B的对象b,它的虚指针呢。因为class B是class 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
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. 虚函数与纯虚函数

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

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

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

纯虚函数的定义

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

virtual void funtion1()=0

引入原因

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

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

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

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

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

抽象类

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

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

抽象类的作用

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

使用抽象类时注意:

  • 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
    类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

使用抽象类时注意:

  • 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
  • 抽象类是不能定义对象的。

你可能感兴趣的:(C++,c++,笔记)