c++虚函数微微的深度理解

Posted 超级无敌陈大佬的跟班

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++虚函数微微的深度理解相关的知识,希望对你有一定的参考价值。

5.1 ★什么是虚函数?什么是纯虚函数?

面试高频指数:★★★★★

1、虚函数:被 virtual 关键字修饰的成员函数,就是虚函数。

2、纯虚函数:

  • 纯虚函数在类中声明时,加上 = 0;
  • 含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;
  • 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。

说明:

  • 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。
  • 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
  • 可以声明抽象类指针,可以声明抽象类的引用;

3、虚函数和纯虚函数的区别

  • 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类)
  • 使用方式不同虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用
  • 定义形式不同虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 = 0;
  • 虚函数必须实现,否则编译器会报错;
  • 对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;
  • 析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。

5.2 ★虚函数的实现机制

面试高频指数:★★★★★

实现机制:

  • 虚函数通过虚函数表来实现
  • 虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表
  • 虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。

虚函数表相关知识点:

  • 虚函数表存放的内容类的虚函数的地址
  • 虚函数表建立的时间编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
  • 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。

注:虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。

实例:

无虚函数覆盖的情况:

#include <iostream>
using namespace std;

class Base
{public:
    virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
    virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base{
public:
    virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main(){
    Base *p = new Derive();
    p->B_fun1();     // Base::B_fun1() 父类

    Derive *d = new Derive();
    d->B_fun1();    // Base::B_fun1()    父类
    d->D_fun1();    // Derive::D_fun1()    子类
    return 0;
}

基类和派生类的继承关系:

基类的虚函数表:

派生类的虚函数表:

  • 虚函数表中包含基类和派生类自身的虚函数

主函数中基类的指针 p 指向了派生类的对象,当调用函数 B_fun1() 时,通过派生类的虚函数表找到该函数的地址,从而完成调用。

5.3 单继承和多继承的虚函数表结构

面试高频指数:★★★★☆

编译器处理虚函数表:

  • 编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。
  • 如果派生类没有重新定义基类的虚函数 A,则派生类的虚函数表中保存的是基类的虚函数 A 的地址,也就是说基类和派生类的虚函数 A 的地址是一样的。
  • 如果派生类重写了基类的某个虚函数 B,则派生的虚函数表中保存的是重写后的虚函数 B 的地址,也就是说虚函数 B 有两个版本,分别存放在基类和派生类的虚函数表中。
  • 如果派生类重新定义了新的虚函数 C,派生类的虚函数表保存新的虚函数 C 的地址。

1、单继承无虚函数覆盖的情况

#include <iostream>
using namespace std;

class Base{
public:
    virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
    virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base{
public:
    virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main(){
    Base *p = new Derive();
    p->B_fun1(); // Base::B_fun1()
    return 0;
}

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/efd81s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

基类和派生类的继承关系:

基类的虚函数表:

派生类的虚函数表:

2、单继承有虚函数覆盖的情况:

#include <iostream>
using namespace std;

class Base{
public:
    virtual void fun1() { cout << "Base::fun1()" << endl; }
    virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base{
public:
    # fun1覆盖基类
    virtual void fun1() { cout << "Derive::fun1()" << endl; }  
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main(){
    Base *p = new Derive();
    p->fun1(); // Derive::fun1()
    return 0;
}

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/efd81s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

派生类的虚函数表:

  • 此时虚函数表中已经没有基类的fun1()函数了

3、多继承无虚函数覆盖的情况:

#include <iostream>
using namespace std;

class Base1{
public:
    virtual void B1_fun1() { cout << "Base1::B1_fun1()" << endl; }
    virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }
    virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }
};
class Base2{
public:
    virtual void B2_fun1() { cout << "Base2::B2_fun1()" << endl; }
    virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }
    virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }
};
class Base3{
public:
    virtual void B3_fun1() { cout << "Base3::B3_fun1()" << endl; }
    virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }
    virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }
};

class Derive : public Base1, public Base2, public Base3{
public:
    virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};

int main(){
    Base1 *p = new Derive();
    p->B1_fun1(); // Base1::B1_fun1()
    return 0;
}

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/efd81s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

基类和派生类的关系:

派生类的虚函数表:

  • 继承了多少个基类,就有多少个虚函数表。
  • 基类的顺序和声明的顺序一致

4、多继承有虚函数覆盖的情况:

#include <iostream>
using namespace std;

class Base1{
public:
    virtual void fun1() { cout << "Base1::fun1()" << endl; }
    virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }
    virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }
};
class Base2{
public:
    virtual void fun1() { cout << "Base2::fun1()" << endl; }
    virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }
    virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }
};
class Base3{
public:
    virtual void fun1() { cout << "Base3::fun1()" << endl; }
    virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }
    virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }
};

class Derive : public Base1, public Base2, public Base3{
public:
    ///=== 覆盖fun1()
    virtual void fun1() { cout << "Derive::fun1()" << endl; }    
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};

int main(){
    Base1 *p1 = new Derive();
    Base2 *p2 = new Derive();
    Base3 *p3 = new Derive();
    p1->fun1(); // Derive::fun1()
    p2->fun1(); // Derive::fun1()
    p3->fun1(); // Derive::fun1()
    return 0;
}

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/efd81s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

派生类的虚函数表:

 

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/efd81s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

以上是关于c++虚函数微微的深度理解的主要内容,如果未能解决你的问题,请参考以下文章

深入理解C++ 虚函数表

c++中的纯虚函数机制如何从DLL中暴露函数

图解C++虚函数

c++ 深入理解虚函数

C++ 内存布局:深入理解C++内存布局

C++ 虚函数