19. 智能指针与多态:基类指针管理派生类对象
多态是C++面向对象的灵魂。而智能指针,则是现代C++管理对象生命周期的利器。当这两者结合——用基类智能指针指向派生类对象——很多新手会踩坑。今天我就跟你聊聊这个话题。
为什么需要基类指针管理派生类?
说白了,就是解耦。你写一个函数,接收一个Shape*,传入Circle、Rectangle都能正常工作。这就是多态的魅力。但问题来了:谁负责销毁?什么时候销毁?
我早期做图形引擎时,就遇到过这种场景:一个Scene类持有大量Shape*,每个指针指向堆上创建的派生类对象。手动管理这些对象的生命周期,简直是一场噩梦。后来换成智能指针,世界清净了。
核心原则:基类必须有虚析构函数
这是前提,没有商量余地。你想想看:
class Base {
public:
~Base() { std::cout << "Base destructor\n"; }
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() { delete[] data; std::cout << "Derived destructor\n"; }
};
int main() {
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
// 问题:只调用了Base的析构函数,Derived的data泄漏了!
}
为什么会这样?因为unique_ptr在析构时,调用的是Base::~Base(),而不是Derived::~Derived()。C++标准说:通过基类指针删除派生类对象,而基类析构函数非虚,行为是未定义的。嗯,这里要注意,未定义意味着什么都有可能发生——内存泄漏只是最轻的。
修正很简单:
class Base {
public:
virtual ~Base() = default; // 虚析构函数
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() override { delete[] data; }
};
unique_ptr 与多态
std::unique_ptr天生支持多态。你可以这样写:
std::unique_ptr<Base> createShape(const std::string& type) {
if (type == "circle") return std::make_unique<Circle>();
if (type == "rect") return std::make_unique<Rectangle>();
return nullptr;
}
void drawAll(const std::vector<std::unique_ptr<Base>>& shapes) {
for (const auto& s : shapes) {
s->draw(); // 多态调用
}
}
我个人习惯用std::make_unique创建派生类对象,然后直接赋值给基类unique_ptr。注意,这里有个隐式转换:unique_ptr<Derived>可以隐式转换为unique_ptr<Base>。这是标准库特意设计的。
unique_ptr<Derived>移动到unique_ptr<Base>,直接用std::move即可。但反过来不行——你不能把unique_ptr<Base>转成unique_ptr<Derived>,除非你知道它实际指向的是派生类对象。
shared_ptr 与多态
shared_ptr也支持多态,但有个细节要注意:控制块的处理。
std::shared_ptr<Base> sp = std::make_shared<Derived>();
这里make_shared会为Derived对象分配内存,同时控制块也放在同一块内存中。当shared_ptr引用计数归零时,它会正确调用Derived的析构函数——前提是基类有虚析构函数。
我曾经踩过一个坑:用shared_ptr<Base>管理派生类对象,然后通过std::dynamic_pointer_cast向下转型:
std::shared_ptr<Base> sp = std::make_shared<Derived>();
auto dp = std::dynamic_pointer_cast<Derived>(sp);
if (dp) {
// 安全使用dp
}
这个dynamic_pointer_cast是标准库提供的,它会检查类型信息,如果转型失败返回空shared_ptr。比裸指针的dynamic_cast更安全,因为不会出现悬空指针问题。
weak_ptr 与多态
weak_ptr也支持多态。比如你有一个观察者模式,基类指针观察派生类对象:
class Observer {
std::weak_ptr<Base> observed;
public:
void observe(const std::shared_ptr<Base>& obj) {
observed = obj; // weak_ptr<Base> 可以接收 shared_ptr<Derived>
}
void check() {
if (auto sp = observed.lock()) {
sp->doSomething(); // 多态调用
}
}
};
这里weak_ptr的lock()返回一个shared_ptr<Base>,如果原对象已被销毁,返回空。这种模式在缓存、事件系统中非常常见。
避坑指南:数组与多态
嗯,这里要特别提醒一下。不要用unique_ptr<Base[]>来管理派生类对象数组!
// 错误做法
std::unique_ptr<Base[]> arr = std::make_unique<Derived[]>(5); // 编译错误或未定义行为
为什么?因为数组的索引计算依赖于元素类型大小。如果Base和Derived大小不同,arr[1]会访问错误的内存位置。正确的做法是用std::vector<std::unique_ptr<Base>>。
知识体系图
下面这张图总结了智能指针与多态的核心关系:
总结一下
用智能指针管理多态对象,记住三点:
- 基类必须有虚析构函数——这是所有多态操作的基础
- 优先用
make_unique/make_shared——异常安全,性能更好 - 向下转型用
dynamic_pointer_cast——比裸指针的dynamic_cast更安全
我在项目中见过太多因为忘记虚析构函数导致的内存泄漏。排查起来特别痛苦,因为泄漏发生在对象销毁时,而代码逻辑可能看起来完全正常。所以,养成习惯:只要类有虚函数,就把析构函数声明为virtual。哪怕它什么都不做。