19. 智能指针与多态:基类指针管理派生类对象

多态是C++面向对象的灵魂。而智能指针,则是现代C++管理对象生命周期的利器。当这两者结合——用基类智能指针指向派生类对象——很多新手会踩坑。今天我就跟你聊聊这个话题。

为什么需要基类指针管理派生类?

说白了,就是解耦。你写一个函数,接收一个Shape*,传入CircleRectangle都能正常工作。这就是多态的魅力。但问题来了:谁负责销毁?什么时候销毁?

我早期做图形引擎时,就遇到过这种场景:一个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++标准说:通过基类指针删除派生类对象,而基类析构函数非虚,行为是未定义的。嗯,这里要注意,未定义意味着什么都有可能发生——内存泄漏只是最轻的。

警告: 基类析构函数必须声明为 virtual。这是使用多态 + 智能指针的绝对前提。

修正很简单:

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_ptrlock()返回一个shared_ptr<Base>,如果原对象已被销毁,返回空。这种模式在缓存、事件系统中非常常见。

避坑指南:数组与多态

嗯,这里要特别提醒一下。不要用unique_ptr<Base[]>来管理派生类对象数组!

// 错误做法
std::unique_ptr<Base[]> arr = std::make_unique<Derived[]>(5);  // 编译错误或未定义行为

为什么?因为数组的索引计算依赖于元素类型大小。如果BaseDerived大小不同,arr[1]会访问错误的内存位置。正确的做法是用std::vector<std::unique_ptr<Base>>

知识体系图

下面这张图总结了智能指针与多态的核心关系:

智能指针与多态:核心知识体系 Base(虚析构函数) Derived unique_ptr<Base> 独占所有权,移动语义 shared_ptr<Base> 共享所有权,引用计数 weak_ptr<Base> 弱引用,打破循环 关键操作 make_unique<Derived>() → unique_ptr<Base> | make_shared<Derived>() → shared_ptr<Base> | dynamic_pointer_cast 安全向下转型

总结一下

用智能指针管理多态对象,记住三点:

  • 基类必须有虚析构函数——这是所有多态操作的基础
  • 优先用make_unique/make_shared——异常安全,性能更好
  • 向下转型用dynamic_pointer_cast——比裸指针的dynamic_cast更安全

我在项目中见过太多因为忘记虚析构函数导致的内存泄漏。排查起来特别痛苦,因为泄漏发生在对象销毁时,而代码逻辑可能看起来完全正常。所以,养成习惯:只要类有虚函数,就把析构函数声明为virtual。哪怕它什么都不做。

核心要点: 智能指针 + 多态 = 自动生命周期管理 + 类型安全。但前提是基类析构函数是虚的。这个前提不满足,一切免谈。
专注资料整理