std::shared_ptr为何能正确释放子类指针

本文简述了std::shared_ptr可以通过在父类析构函数非虚的情况下,通过父类指针正确释放子类的特点,以及一个简单实现。

现象

今天在知乎上看到了这个问题,就记录一下:

首先,对于两个类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Parent {
public:
    ~Parent() {
        std::cout << "parent" << std::endl;
    }
};

class Child: public Parent {
public:
    ~Child() {
        std::cout << "child" << std::endl;
    }
};

如果父类析构函数不是虚函数,那么使用父类指针指向子类并析构不会调用子类的析构函数(因为没有虚函数表,找不到子类的析构函数):

1
2
3
4
Parent* p = new Child{};
delete p;

// 输出 parent

但是std::shared_ptr是可以做到正确释放的:

1
2
std::shared_ptr<Parent> p = std::make_shared<Child>(new Child{});
p.reset();

因为std::shared_ptr内部存储了子类的信息,可以正确释放。

简单实现

我们自己要实现std::shared_ptr的话可以将指针从父类转换成子类,然后调用子类的析构函数来正确析构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template <typename T>
class DestructWithBases final {
public:
    template <typename U>
    struct traits {
        static void destruct(T* ptr) {
            delete (static_cast<U*>(ptr));
        }
    };

    template <typename U>
    DestructWithBases(U* u): value_(u), destruct_(traits<U>::destruct) { }

    ~DestructWithBases() {
        destruct_(value_);
    }

    T* operator->() {
        return value_;
    }

private:
    T* value_; 
    void(*destruct_)(T*);
};

这里traits::destruct会将父类指针转换为对应子类,然后析构。在构造函数时将对应函数记录下来即可(拷贝和移动构造也要记录,但是我懒得写了)

updatedupdated2023-10-232023-10-23