C++ 多态 虚函数表

简易抽象理解多态

不同的对象做同一件事产生了不同的结果。

多态的具体实现

多态实现的条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

例:

#include <iostream>

class Base {
    
    
public:
    virtual void func() {
    
    
        std::cout << "Base::func()" << std::endl;
    }
};

class Derived : public Base {
    
    
public:
	//注意此处func前虽可以不加virtaul,但为了规范最好加上
    void func() override {
    
    
        std::cout << "Derived::func()" << std::endl;
    }
};

int main() 
{
    
    
    Base* basePtr_1 = new Derived();
    Base* basePtr_2 = new Base();
    basePtr_1->func();  // "Derived::func()"
    basePtr_2->func();  //  "Base::func()"
    
}

虚函数的定义

即被virtual修饰的类成员函数称为虚函数。

虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

存在两种特殊的虚函数重写

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

  2. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

重定义(隐藏)、重载 、重写(覆盖)区别

重定义(又名隐藏) 条件:
1.两函数分别在基类和派生类中。
2.函数名相同,但不构成重写。

重载 条件 :
0.函数名称必须相同。
1.在同一作用域。
2. 函数参数类型、个数或顺序至少有一个不同。
3. 函数返回类型可以相同也可以不同。
4. 函数重载不能仅仅依靠参数的名称或者参数的默认值来区分。
5. 函数重载不能仅仅依靠函数的访问权限或者静态性质来区分。

重写(又名覆盖)条件:
1.两函数分别在基类和派生类。
2. 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同)(由于虚函数表的原因,虚函数表是个函数指针数组)

C++11 override 和 final 关键字

override 和 final 关键字主要用于规范重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写
  2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

具体使用方法:在虚函数参数列表后添加关键字即可

抽象类的定义

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。(接口包括 函数的返回类型 函数名 形参 )

接口继承是指派生类只继承了基类的接口(也就是纯虚函数),而没有继承基类的实现。这种方式使得派生类必须实现基类中的所有纯虚函数,从而使得派生类和基类的实现是分离的,实现了接口和实现的分离。这种继承方式常常用于实现抽象类和接口,强制要求派生类实现接口中的所有函数。

多态的原理:虚函数表

在C++中,虚函数是一种特殊的成员函数,它可以被子类重写,实现多态性。为了实现虚函数的动态绑定,C++编译器会为每个包含虚函数的类生成一个虚函数表(vtable),也称为虚表。

虚函数表是一个指针数组,其中每个指针指向一个虚函数。每个包含虚函数的类都有一个虚函数表,其中包含该类的所有虚函数的地址。当一个对象被创建时,它会包含一个指向其类的虚函数表的指针。当调用一个虚函数时,编译器会使用该对象的虚函数表来查找正确的函数地址。
关于虚函数表/虚表指针 的相关细节:

1.虚函数表是存储在全局数据区 。
2.虚表指针是指向虚函数表的指针。
3.virtual关键字给的是类中的虚函数,而不是虚表指针。虚表指针是由编译器自动生成的
4.虚表是在编译时由编译器生成的,通常存储在只读数据段中,因此在程序运行时是不允许修改虚表的。
5.满足多态条件以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的(通过 虚表指针-> 虚表 -> 虚函数)。不满足多态的函数调用时编译时确认好的。

单继承和多继承关系的虚函数表

在这里插入图片描述
在这里插入图片描述

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
以下是一个静态绑定的示例代码:
#include <iostream>

class Base {
    
    
public:
    void print() {
    
    
        std::cout << "Base class." << std::endl;
    }
};

class Derived : public Base {
    
    
public:
    void print() {
    
    
        std::cout << "Derived class." << std::endl;
    }
};

int main() {
    
    
    Base base;
    Derived derived;

    Base* ptr1 = &base;
    ptr1->print();  // 静态绑定,调用Base类的print函数

    Base* ptr2 = &derived;
    ptr2->print();  // 静态绑定,调用Base类的print函数

    return 0;
}

以下是一个动态绑定的示例代码:
#include <iostream>

class Base {
    
    
public:
    virtual void print() {
    
    
        std::cout << "Base class." << std::endl;
    }
};

class Derived : public Base {
    
    
public:
    void print() {
    
    
        std::cout << "Derived class." << std::endl;
    }
};

int main() {
    
    
    Base base;
    Derived derived;

    Base* ptr1 = &base;
    ptr1->print();  // 动态绑定,调用Base类的print函数

    Base* ptr2 = &derived;
    ptr2->print();  // 动态绑定,调用Derived类的print函数

    return 0;
}

猜你喜欢

转载自blog.csdn.net/WSK1454360679/article/details/132130572