基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

C++的虚函数和纯虚函数

知识点图片

在C++中,虚函数(Virtual Function)纯虚函数(Pure Virtual Function)是实现面向对象多态性(Polymorphism)的核心机制。特别是“动态多态”(运行时多态),完全依赖于这两个概念。

下面我将为你详细、通俗地解析这两个概念及其区别、底层原理和使用注意事项。


一、 虚函数(Virtual Function)

1. 什么是虚函数?
虚函数是在基类中使用 virtual 关键字声明的成员函数。它的主要作用是允许在派生类中重写(Override)该函数,从而实现通过基类指针或引用调用派生类的同名函数。

2. 核心特性:动态绑定
普通的函数调用在编译时就已经决定了(静态绑定)。而虚函数是在运行时根据指针(或引用)实际指向的对象类型来决定调用哪个类的方法(动态绑定)。

3. 代码示例:

cpp
#include <iostream>

class Animal {
public:
    // 声明虚函数
    virtual void speak() {
        std::cout << "Animal makes a sound." << std::endl;
    }
};

class Dog : public Animal {
public:
    // 重写基类的虚函数
    void speak() override { // override是C++11引入的关键字,用于显式标明重写,防止拼写错误
        std::cout << "Dog barks: Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        std::cout << "Cat meows: Meow!" << std::endl;
    }
};

int main() {
    Animal* ptr1 = new Dog();
    Animal* ptr2 = new Cat();
    Animal* ptr3 = new Animal();

    ptr1->speak(); // 输出: Dog barks: Woof! (根据实际对象调用Dog的speak)
    ptr2->speak(); // 输出: Cat meows: Meow! (根据实际对象调用Cat的speak)
    ptr3->speak(); // 输出: Animal makes a sound.

    delete ptr1; delete ptr2; delete ptr3;
    return 0;
}

二、 纯虚函数(Pure Virtual Function)

1. 什么是纯虚函数?
纯虚函数是一种特殊的虚函数,它在基类中没有具体实现(虽然语法上允许有,但极少这么用),并要求任何非抽象的派生类必须提供该函数的实现。
声明方式是在虚函数声明的末尾加上 = 0

2. 核心特性:抽象类(Abstract Class)

  • 包含至少一个纯虚函数的类被称为“抽象类”。
  • 抽象类不能被实例化(不能直接 new 一个抽象类对象)。
  • 抽象类的主要作用是定义接口(规范派生类必须具备哪些功能)。
  • 如果派生类没有重写基类的纯虚函数,那么该派生类也会变成抽象类,同样无法实例化。

3. 代码示例:

cpp
#include <iostream>

// Shape是一个抽象类,因为它包含纯虚函数
class Shape {
public:
    // 纯虚函数:只定义接口,不提供具体实现
    virtual double getArea() = 0; 
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    // 必须实现纯虚函数,否则Circle也是抽象类
    double getArea() override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    // Shape s; // 错误!不能实例化抽象类
    
    Shape* myShape = new Circle(5.0);
    std::cout << "Area: " << myShape->getArea() << std::endl; // 输出: Area: 78.5397
    
    delete myShape;
    return 0;
}

三、 虚函数 vs 纯虚函数 核心区别总结

特性 虚函数 (virtual) 纯虚函数 (virtual ... = 0)
声明语法 virtual void func(); virtual void func() = 0;
是否有实现 必须有具体的函数实现。 可以没有实现(只定义接口)。
派生类重写 可选的。派生类可以重写,也可以直接继承基类的实现。 强制的。派生类必须重写,否则派生类也无法实例化。
对类实例化的影响 包含虚函数的类可以被实例化 包含纯虚函数的类是抽象类,绝对不能被实例化
设计目的 提供默认功能,允许子类自定义修改。 定义统一接口规范,强制子类去实现。

四、 底层原理解析:虚函数表(vtable)与虚表指针(vptr)

面试中常问:C++是怎么实现动态绑定的?
答案是:虚函数表(Virtual Table, vtable)虚表指针(Virtual Pointer, vptr)

  1. 虚函数表(vtable):
    当一个类包含虚函数时,编译器会为该类生成一个虚函数表。这是一个隐藏的数组,里面存储了该类所有虚函数的函数指针。
  2. 虚表指针(vptr):
    编译器还会为该类的每一个对象实例隐式地添加一个指针(通常在对象的内存起始位置),这个指针指向该对象所属类的虚函数表。
  3. 调用过程:
    当通过基类指针调用虚函数时,程序在运行时:
    • 通过指针找到实际对象。
    • 通过对象内部的 vptr 找到该类的 vtable
    • vtable 中查找对应的函数指针,然后调用真正的函数。

(这也是为什么含有虚函数的对象体积通常会大一个指针的大小,比如64位系统下大8字节)。


五、 极度重要的注意事项:虚析构函数

在使用多态时,基类的析构函数必须声明为虚函数!
如果你通过基类指针去删除一个派生类对象,且基类的析构函数不是虚函数,那么只会调用基类的析构函数,派生类的析构函数不会被执行,从而导致严重的内存泄漏。

cpp
class Base {
public:
    Base() { std::cout << "Base Constructor" << std::endl; }
    // 必须加上 virtual
    virtual ~Base() { std::cout << "Base Destructor" << std::endl; } 
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived Constructor" << std::endl; }
    ~Derived() override { std::cout << "Derived Destructor" << std::endl; }
};

int main() {
    Base* ptr = new Derived();
    delete ptr; 
    // 如果基类析构是虚函数,输出:Derived Destructor -> Base Destructor (正确)
    // 如果基类析构不是虚函数,只会输出:Base Destructor (内存泄漏!)
    return 0;
}

补充知识:构造函数可以是虚函数吗?

绝对不能。
因为虚函数的调用依赖于对象的 vptr(虚表指针),而 vptr 是在构造函数执行期间才被初始化的。如果构造函数是虚函数,此时 vptr 还没有构建完毕,根本无法实现多态。

00:00
00:00