概览

  • 什么是虚函数,有什么作用?
  • 纯虚函数,为什么需要纯虚函数?
  • 为什么需要虚析构函数,什么时候不需要?
  • 内联函数、构造函数、静态成员函数可以是虚函数吗?
  • 构造函数中可以调用虚函数吗?
  • 为什么需要虚继承?虚继承实现原理解析
  • 虚函数是针对类还是针对对象的?
  • 同一个类的两个对象的虚函数表是如何维护的?

在C++中,虚函数(virtual function)是面向对象编程的一个核心概念,它允许通过基类指针或引用来调用派生类的重写方法,实现动态多态(dynamic polymorphism)。虚函数的背后涉及虚函数表(vtable)、指针机制以及一些运行时的操作。

虚函数的基本概念与作用

虚函数是基类中声明为virtual的成员函数,它允许派生类重写该函数,并支持通过基类的指针或引用调用派生类的重写函数。

作用

  • 虚函数实现了动态多态,允许基类指针或引用在运行时调用派生类的重写函数,而不是基类的版本。
#include <iostream>

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

class Derived : public Base {
public:
    void show() override {  // 重写虚函数
        std::cout << "Derived class show() called" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();  // 基类指针指向派生类对象
    ptr->show();  // 调用派生类的show(),而不是Base的show()
    // 输出 Derived class show() called
    delete ptr;
}

在这个例子中,Base类的show()是虚函数,当通过基类指针ptr调用时,实际调用的是派生类Derived的show(),这就是虚函数的作用——动态分派。

纯虚函数与抽象类

纯虚函数(pure virtual function)是指在基类中只声明而不定义的虚函数,形式是virtual void func() = 0;。一个包含纯虚函数的类称为抽象类,抽象类不能实例化,只能被继承。

class Base {
public:
    virtual void show() = 0;  // 纯虚函数
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived class show() called" << std::endl;
    }
};

int main() {
    // Base obj;  // 错误!抽象类不能实例化
    Base* ptr = new Derived();
    ptr->show();  // 调用派生类的show()
    delete ptr;
}

虚析构函数的必要性

如果一个类中存在虚函数,通常也需要将析构函数声明为虚函数。这是为了确保通过基类指针或引用删除对象时,能够正确调用派生类的析构函数,防止资源泄漏。

为什么需要虚析构函数: 如果基类的析构函数不是虚函数,那么通过基类指针删除派生类对象时,只会调用基类的析构函数,导致派生类的资源没有正确释放。

class Base {
public:
    virtual ~Base() {
        std::cout << "Base destructor called" << std::endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor called" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();  // 基类指针指向派生类对象
    delete ptr;  // 调用虚析构函数,确保派生类析构函数被调用
   // output
   // Derived destructor called
   // Base destructor called
}

如果没有虚析构函数,Derived类的析构函数将不会被调用,导致资源泄漏。

什么时候不需要虚析构函数

当类不打算用于继承,或者不打算通过基类指针删除派生类对象时,虚析构函数可以省略。普通的类(非多态类)不需要虚析构函数。

构造函数中可以调用虚函数吗?

不可以。在构造函数中调用虚函数时,虚函数的动态分派机制不会工作。因为在构造基类对象时,派生类部分还未构造完成,此时即便调用的是虚函数,调用的也是基类的版本。

class Base {
public:
    Base() { show(); }  // 构造函数中调用虚函数
    virtual void show() {
        std::cout << "Base show() called" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived show() called" << std::endl;
    }
};

int main() {
    Derived obj;  // 调用Derived的构造函数
    // output
    // Base show() called
}

即使派生类Derived重写了show(),构造函数中调用的仍然是基类的show()。

哪些函数不能是虚函数

  • 内联函数(inline function):理论上内联函数可以是虚函数,但实际中,虚函数需要动态分派,因此通常不会被内联。编译器无法在运行时决定调用哪个版本的函数,所以虚函数的内联机会较少。
  • 构造函数:构造函数不能是虚函数,因为在对象构造时无法进行动态分派。构造函数的职责是创建对象,虚函数的作用是在对象创建完成后进行动态分派。
  • 静态成员函数:静态成员函数不能是虚函数,因为它们与具体的对象无关。虚函数依赖于对象的动态类型,而静态函数属于类本身,不依赖于对象实例。

为什么需要虚继承?

虚继承用于解决多重继承中的菱形继承问题,即当多个派生类从同一个基类继承时,可能导致基类被多次继承,从而引发二义性或资源冗余。虚继承通过让所有派生类共享基类的唯一实例来解决这个问题。

class Base {
public:
    int value;
};

class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};

class Final : public Derived1, public Derived2 {};

int main() {
    Final obj;
    obj.value = 10;  // 没有二义性,只有一个Base子对象
}

在上面的例子中,如果不使用虚继承,Final类将有两个Base的副本,导致value的二义性。

虚继承的实现原理

虚继承通过引入**虚基类指针(virtual base pointer, vptr)**来跟踪共享的基类实例。派生类在内存布局中包含一个指向虚基类实例的指针,所有派生类都指向同一个基类实例,避免重复继承。

虚函数表(vtable)的维护

每个具有虚函数的类在编译时会生成一个虚函数表(vtable),表中存储了该类的虚函数指针。每个对象会有一个虚函数表指针(vptr),指向该类的虚函数表。

同一个类的两个对象的虚函数表

对于同一个类的多个对象,它们共享相同的虚函数表(vtable),但每个对象都有自己的虚函数表指针(vptr),指向相同的vtable。

虚函数是针对类还是对象的

虚函数是针对类的。虚函数表是类级别的,所有对象共享同一个虚函数表,但调用虚函数时,是根据对象的动态类型来选择合适的函数。这就是动态多态的核心机制。

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

class Derived : public Base {
public:
    void show() override { std::cout << "Derived show() called" << std::endl; }
};

int main() {
    Base b;
    Derived d;
    Base* ptr1 = &b;
    Base* ptr2 = &d;
    ptr1->show();  // 调用Base的show()
    ptr2->show();  // 调用Derived的show()
    // output
    // Base show() called
    // Derived show() called
}

在这个例子中,Base和Derived类都有各自的虚函数表,ptr1指向基类对象,调用基类的show(),ptr2指向派生类对象,调用派生类的show()。

如果拿到虚函数表的储存地址,是否可以改写虚函数表的内容?

虽然可以修改虚函数表的内容,但这种操作是非常危险的,如果将虚函数表的某个条目改为无效地址,调用虚函数时会导致崩溃。改写虚函数表后,程序行为变得不可预测,可能导致奇怪的结果、错误调用、数据损坏等问题。

#include <iostream>
#include <cstring>

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

class Derived : public Base {
public:
    void func1() override {
        std::cout << "Derived func1" << std::endl;
    }
    void func2() override {
        std::cout << "Derived func2" << std::endl;
    }
};

// 一个假的函数,伪装成虚函数
void hacked_func() {
    std::cout << "Hacked function called!" << std::endl;
}

int main() {
    Derived obj;

    // 获取虚函数表指针
    // vptr通常是对象的前8个字节(或者32位系统的前4个字节)
    long long* vptr = *(long long**)(&obj);

    // 打印虚函数表内容
    std::cout << "Before hacking:" << std::endl;
    typedef void(*FuncPtr)();
    FuncPtr original_func1 = (FuncPtr)vptr[0];
    original_func1();  // 调用虚函数表的第一个函数,应该调用Derived::func1

    // 修改虚函数表的第一个函数指针
    vptr[0] = (long long)(&hacked_func);

    // 调用虚函数表第一个函数,应该调用伪造的hacked_func
    std::cout << "After hacking:" << std::endl;
    original_func1 = (FuncPtr)vptr[0];
    original_func1();  // 调用hacked_func

    // output
    // Before hacking:
    // Derived func1
    // After hacking:
    // Hacked function called!
    return 0;
}