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 支持多态的关键在于:它的析构函数会在编译期确定要调用哪个析构函数。只要基类析构函数是虚函数,一切就自动正确。
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。自定义删除器完美解决了这个问题,而且代码非常干净。
知识体系总览
下面这张图把虚函数和智能指针的关系梳理清楚了。我建议你把它存下来,写代码时对照着看。
实际项目中的选择策略
说了这么多,到底该用哪个?我总结了一个简单的决策表:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 对象所有权明确,单一所有者 | std::unique_ptr | 零开销,语义清晰 |
| 多个对象共享所有权 | std::shared_ptr | 引用计数自动管理 |
| 需要自定义资源释放方式 | 自定义删除器 + unique_ptr/shared_ptr | 灵活控制释放逻辑 |
| 工厂函数返回多态对象 | std::unique_ptr | 可转换为 shared_ptr,反之不行 |
| 容器中存储多态对象 | std::vector<std::unique_ptr<Base>> | 性能好,内存连续 |
最后说一句,智能指针不是银弹。它解决的是资源管理问题,但虚函数本身的正确性(比如基类析构函数是不是虚函数)还是要靠你自己保证。工具再好,用错了地方也是白搭。