20、虚函数与内存管理:虚函数与智能指针、std::shared_ptr与多态、std::unique_ptr与多态、自定义删除器

说到虚函数和内存管理,这其实是个老生常谈但又特别容易翻车的话题。我见过不少项目,代码写得挺漂亮,一跑起来就崩,追根溯源,十有八九是虚函数和内存管理没配合好。

你想想看,虚函数本身是个好东西,它让多态变得优雅。但一旦涉及到动态内存分配,事情就复杂了。谁负责释放?什么时候释放?如果基类指针指向派生类对象,delete 的时候会不会只析构了基类部分?

嗯,这些问题,智能指针能帮我们解决一大半。

裸指针与虚函数:一个危险的组合

先看个反面教材。我以前接手过一个遗留系统,里面全是这种写法:

class Base {
public:
    virtual ~Base() {}
    virtual void doWork() { std::cout << "Base work\n"; }
};

class Derived : public Base {
public:
    ~Derived() { std::cout << "Derived destroyed\n"; }
    void doWork() override { std::cout << "Derived work\n"; }
};

void process() {
    Base* p = new Derived();
    p->doWork();
    delete p;  // 如果基类析构函数不是虚函数,这里就出大事了
}

这段代码能跑,但前提是基类析构函数必须是虚函数。我曾经在一个项目中看到有人忘了加 virtual,结果内存泄漏了整整一个季度才发现。排查过程极其痛苦——每次重启服务内存就涨一点,但又不是立刻崩溃。

⚠️ 核心原则:只要类中有虚函数,析构函数就应该是虚函数。这不是可选项,是必须项。

std::shared_ptr 与多态:引用计数的艺术

shared_ptr 是我个人最常用的智能指针。它通过引用计数来管理生命周期,非常适合多态场景。

#include <memory>
#include <iostream>

class Animal {
public:
    virtual ~Animal() = default;
    virtual void speak() const = 0;
};

class Dog : public Animal {
public:
    void speak() const override {
        std::cout << "Woof!\n";
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        std::cout << "Meow!\n";
    }
};

void makeSound(std::shared_ptr<Animal> animal) {
    animal->speak();
}

int main() {
    auto dog = std::make_shared<Dog>();
    auto cat = std::make_shared<Cat>();
    
    makeSound(dog);  // Woof!
    makeSound(cat);  // Meow!
    
    // 引用计数自动管理,无需手动 delete
    return 0;
}

这里有个细节要注意:std::make_shared 比直接 new 再传给 shared_ptr 更高效。它只分配一次内存,把控制块和对象放在一起。我在做性能优化时,发现这个改动能让内存分配次数减少一半。

💡 小技巧:尽量用 std::make_shared 而不是 std::shared_ptr<T>(new T(...))。前者更安全,不会因为异常导致内存泄漏。

std::unique_ptr 与多态:零开销的抽象

unique_ptr 是我在大多数场景下的首选。它没有引用计数的开销,语义清晰——所有权唯一。

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override {
        return 3.14159 * radius_ * radius_;
    }
};

class Rectangle : public Shape {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double area() const override {
        return width_ * height_;
    }
};

std::unique_ptr<Shape> createShape(const std::string& type) {
    if (type == "circle") {
        return std::make_unique<Circle>(5.0);
    } else if (type == "rectangle") {
        return std::make_unique<Rectangle>(4.0, 6.0);
    }
    return nullptr;
}

int main() {
    auto shape = createShape("circle");
    if (shape) {
        std::cout << "Area: " << shape->area() << "\n";
    }
    // unique_ptr 离开作用域时自动释放
    return 0;
}

unique_ptr 支持多态的关键在于:它的析构函数会在编译期确定要调用哪个析构函数。只要基类析构函数是虚函数,一切就自动正确。

📌 关键点:unique_ptr 的默认删除器会调用 delete,而 delete 会通过虚函数表找到正确的析构函数。所以基类析构函数必须是虚函数。

自定义删除器:掌控资源释放的细节

有时候,默认的 delete 不够用。比如你从 C 库拿到一个指针,需要用 free() 释放;或者你打开了一个文件句柄,需要 fclose()。这时候自定义删除器就派上用场了。

// 自定义删除器:释放 C 风格的内存
struct FreeDeleter {
    void operator()(void* p) const {
        std::cout << "Custom deleter: calling free()\n";
        free(p);
    }
};

// 自定义删除器:关闭文件
struct FileCloser {
    void operator()(FILE* fp) const {
        if (fp) {
            std::cout << "Custom deleter: closing file\n";
            fclose(fp);
        }
    }
};

int main() {
    // 使用自定义删除器的 unique_ptr
    std::unique_ptr<FILE, FileCloser> file(fopen("test.txt", "w"));
    if (file) {
        fprintf(file.get(), "Hello, world!\n");
    }
    // 离开作用域时自动调用 FileCloser
    
    // 使用自定义删除器的 shared_ptr
    std::shared_ptr<void> memory(malloc(1024), FreeDeleter());
    // 离开作用域时自动调用 FreeDeleter
    
    return 0;
}

我在项目中遇到过一种情况:某个第三方库返回的对象需要用特定的 release() 函数释放,而不是 delete。自定义删除器完美解决了这个问题,而且代码非常干净。

⚠️ 注意:shared_ptr 的自定义删除器不会影响引用计数的行为。删除器只是指定了「如何释放」,而「何时释放」仍然由引用计数决定。

知识体系总览

下面这张图把虚函数和智能指针的关系梳理清楚了。我建议你把它存下来,写代码时对照着看。

虚函数与智能指针知识体系 虚函数 + 智能指针 裸指针 + 虚函数 析构函数必须为虚函数 手动 delete 易遗漏 异常安全差 std::shared_ptr 引用计数管理 make_shared 优化 支持自定义删除器 std::unique_ptr 零开销抽象 所有权唯一 编译期确定析构 核心:基类析构函数必须是虚函数

实际项目中的选择策略

说了这么多,到底该用哪个?我总结了一个简单的决策表:

场景 推荐方案 理由
对象所有权明确,单一所有者 std::unique_ptr 零开销,语义清晰
多个对象共享所有权 std::shared_ptr 引用计数自动管理
需要自定义资源释放方式 自定义删除器 + unique_ptr/shared_ptr 灵活控制释放逻辑
工厂函数返回多态对象 std::unique_ptr 可转换为 shared_ptr,反之不行
容器中存储多态对象 std::vector<std::unique_ptr<Base>> 性能好,内存连续
💡 我的习惯:能用 unique_ptr 就不用 shared_ptr。只有在确实需要共享所有权时,才考虑 shared_ptr。这能避免很多循环引用的问题。

最后说一句,智能指针不是银弹。它解决的是资源管理问题,但虚函数本身的正确性(比如基类析构函数是不是虚函数)还是要靠你自己保证。工具再好,用错了地方也是白搭。

公众号:蓝海资料掘金营,微信deep3321