何小豆儿在此 发表于 2023-5-22 11:37:23

【重学C++】03 | 手撸C++智能指针实战教程

文章首发

【重学C++】03 | 手撸C++智能指针实战教程
前言

大家好,今天是【重学C++】的第三讲,书接上回,第二讲《02 脱离指针陷阱:深入浅出 C++ 智能指针》介绍了C++智能指针的一些使用方法和基本原理。今天,我们自己动手,从0到1实现一下自己的unique_ptr和shared_ptr。
回顾

智能指针的基本原理是基于RAII设计理论,自动回收内存资源,从根本上避免内存泄漏。在第一讲《01 C++ 如何进行内存资源管理?》介绍RAII的时候,就已经给了一个用于封装int类型指针,实现自动回收资源的代码实例:
class AutoIntPtr {
public:
    AutoIntPtr(int* p = nullptr) : ptr(p) {}
    ~AutoIntPtr() { delete ptr; }

    int& operator*() const { return *ptr; }
    int* operator->() const { return ptr; }

private:
    int* ptr;
};我们从这个示例出发,一步步完善我们自己的智能指针。
模版化

这个类有个明显的问题:只能适用于int类指针。所以我们第一步要做的,就是把它改造成一个类模版,让这个类适用于任何类型的指针资源。
code show time
template <typename T>
class smart_ptr {
public:
        explicit smart_ptr(T* ptr = nullptr): ptr_(ptr) {}
        ~smart_ptr() {
                delete ptr_;
        }
        T& operator*() const { return *ptr_; }
        T* operator->() const { return ptr_; }
private:
        T* ptr_;
}我给我们的智能指针类用了一个更抽象,更切合的类名:smart_ptr。
和AutoIntPtr相比,我们把smart_ptr设计成一个类模版,原来代码中的int改成模版参数T,非常简单。使用时也只要把AutoIntPtr(new int(9)) 改成smart_ptr(new int(9))即可。
另外,有一点值得注意,smart_ptr的构造函数使用了explicit, explicit关键字主要用于防止隐式的类型转换。代码中,如果原生指针隐式地转换为智能指针类型可能会导致一些潜在的问题。至于会有什么问题,你那聪明的小脑瓜看完下面的代码肯定能理解了:
void foo(smart_ptr<int> int_ptr) {
    // ...
}

int main() {
    int* raw_ptr = new int(42);
    foo(raw_ptr);// 隐式转换为 smart_ptr<int>
    std::cout << *raw_ptr << std::endl;   // error: raw_ptr已经被回收了
    // ...
}自定义移动构造函数。在移动构造函数中,我们先是接管了other.ptr_指向的资源对象,然后把other的ptr_置为nullptr,这样在other析构时就不会错误释放资源内存。
同时,根据C++的规则,手动提供移动构造函数后,就会自动禁用拷贝构造函数。也就是我们能得到以下效果:
smart_ptr<int> ptr1{new int(10)};
smart_ptr<int> ptr2 = ptr1;自定义赋值函数。在赋值函数中,我们使用std::swap交换了 rhs.ptr_和this->ptr_,注意,这里不能简单的将rhs.ptr_设置为nullptr,因为this->ptr_可能有指向一个堆对象,该对象需要转给rhs,在赋值函数调用结束,rhs析构时顺便释放掉。避免内存泄漏。
注意赋值函数的入参rhs的类型是unique_smart_ptr而不是unique_smart_ptr&&,这样创建rhs使用移动构造函数还是拷贝构造函数完全取决于unique_smart_ptr的定义。因为unique_smart_ptr当前只保留了移动构造函数,所以rhs是通过移动构造函数创建的。
https://huiwan-images-1253247883.cos.ap-guangzhou.myqcloud.com/cpp/Pasted%20image%2020230426083308.png
多个智能指针共享对象 - shared_smart_ptr

学过第二讲的shared_ptr, 我们知道它是利用计数引用的方式,实现了多个智能指针共享同一个对象。当最后一个持有对象的智能指针析构时,计数器减为0,这个时候才会回收资源对象。
https://huiwan-images-1253247883.cos.ap-guangzhou.myqcloud.com/cpp/20230401145831.png
我们先给出shared_smart_ptr的类定义
template <typename T>
class unique_smart_ptr {
public:
        explicit unique_smart_ptr(T* ptr = nullptr): ptr_(ptr) {}

        ~unique_smart_ptr() {
                delete ptr_;
        }

        // 1. 自定义移动构造函数
        unique_smart_ptr(unique_smart_ptr&& other) {
                // 1.1 把other.ptr_ 赋值到this->ptr_
                ptr_ = other.ptr_;
                // 1.2 把other.ptr_指为nullptr,other不再拥有资源指针
                other.ptr_ = nullptr;
        }

        // 2. 自定义赋值行为
        unique_smart_ptr& operator = (unique_smart_ptr rhs) {
                // 2.1 交换rhs.ptr_和this->ptr_
                std::swap(rhs.ptr_, this->ptr_);
                return *this;
        }

T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }

private:
        T* ptr_;
};暂时不考虑多线程并发安全的问题,我们简单在堆上创建一个int类型的计数器count_。下面详细展开各个函数的实现。
为了避免对count_的重复删除,我们保持:只有当ptr_ != nullptr时,才对count_进行赋值。
构造函数

同样的,使用explicit避免隐式转换。除了赋值ptr_, 还需要在堆上创建一个计数器。
unique_smart_ptr<int> ptr1{new int(10)};
unique_smart_ptr<int> ptr2 = ptr1; // error
unique_smart_ptr<int> ptr3 = std::move(ptr1); // ok

unique_smart_ptr<int> ptr4{ptr1} // error
unique_smart_ptr<int> ptr5{std::move(ptr1)} // ok析构函数

在析构函数中,需要根据计数器的引用数判断是否需要回收对象。
template <typename T>
class shared_smart_ptr {
public:
        // 构造函数
        explicit shared_smart_ptr(T* ptr = nullptr)
        // 析构函数
        ~shared_smart_ptr()
        // 移动构造函数
        shared_smart_ptr(shared_smart_ptr&& other)
        // 拷贝构造函数
        shared_smart_ptr(const shared_smart_ptr& other)
        // 赋值函数
        shared_smart_ptr& operator = (shared_smart_ptr rhs)
        // 返回当前引用次数
        int use_count() const { return *count_; }

        T& operator*() const { return *ptr_; }
        T* operator->() const { return ptr_; }

private:
        T* ptr_;
        int* count_;
}移动构造函数

添加对count_的处理
explicit shared_smart_ptr(T* ptr = nullptr){
        ptr_ = ptr;
        if (ptr_) {
                count_ = new int(1);
        }
}赋值构造函数

添加交换count_
~shared_smart_ptr() {
        // ptr_为nullptr,不需要做任何处理
        if (ptr_) {
                return;
        }
        // 计数器减一
        --(*count_);
        // 计数器减为0,回收对象
        if (*count_ == 0) {
                delete ptr_;
                delete count_;
                return;
        }

}拷贝构造函数

对于shared_smart_ptr,我们需要手动支持拷贝构造函数。主要处理逻辑是赋值ptr_和增加计数器的引用数。
shared_smart_ptr(shared_smart_ptr&& other) {
        ptr_ = other.ptr_;
        count_ = other.count_;

        other.ptr_ = nullptr;
        other.count_ = nullptr;
}这样,我们就实现了一个自己的共享智能指针,贴一下完整代码
shared_smart_ptr& operator = (shared_smart_ptr rhs) {
        std::swap(rhs.ptr_, this->ptr_);
        std::swap(rhs.count_, this->count_);
        return *this;
}使用下面代码进行验证:
int main(int argc, const char** argv) {        shared_smart_ptr ptr1(new int(1));        std::cout
页: [1]
查看完整版本: 【重学C++】03 | 手撸C++智能指针实战教程