关于后端:重学C03-手撸C智能指针实战教程

36次阅读

共计 6069 个字符,预计需要花费 16 分钟才能阅读完成。

文章首发

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

前言

大家好,明天是【重学 C ++】的第三讲,书接上回,第二讲《02 脱离指针陷阱:深入浅出 C++ 智能指针》介绍了 C ++ 智能指针的一些应用办法和基本原理。明天,咱们本人入手,从 0 到 1 实现一下本人的 unique_ptrshared_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<int>(new int(9)) 即可。

另外,有一点值得注意,smart_ptr的构造函数应用了 explicitexplicit 关键字次要用于避免隐式的类型转换。代码中,如果原生指针隐式地转换为智能指针类型可能会导致一些潜在的问题。至于会有什么问题,你那聪慧的小脑瓜看完上面的代码必定能了解了:

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 曾经被回收了
    // ...
}

假如咱们没有为 smart_ptr 构造函数加上 explicit,原生指针raw_ptr 在传给 foo 函数后,会被隐形转换为 smart_ptr<int>foo 函数调用完结后,栖构入参的 smart_ptr<int> 时会把 raw_ptr 给回收掉了,所以后续对 raw_ptr 的调用都会失败。

拷贝还是挪动?

以后咱们没有为 smart_ptr 自定义拷贝构造函数 / 挪动构造函数,C++ 会为 smart_ptr 生成默认的拷贝 / 挪动构造函数。默认的拷贝 / 挪动构造函数逻辑很简略:把每个成员变量拷贝 / 挪动到指标对象中。

按以后 smart_ptr 的实现,咱们假如有以下代码:

smart_ptr<int> ptr1{new int(10)};
smart_ptr<int> ptr2 = ptr1;

这段代码在编译时不会出错,问题在运行时才会裸露进去:第二行将 ptr1 治理的指针复制给了ptr2,所以会反复开释内存,导致程序奔溃。

为了防止同一块内存被反复开释。解决办法也很简略:

  1. 独占资源所有权,每时每刻一个内存对象 (资源) 只能有一个 smart_ptr 占有它。
  2. 一个内存对象 (资源) 只有在最初一个领有它的 smart_ptr 析构时才会进行资源回收。

独占所有权 – unique_smart_ptr

独占资源的所有权,并不是指禁用掉 smart_ptr 的拷贝 / 挪动函数(当然这也是一种简略的防止反复开释内存的办法)。而是 smart_ptr 在拷贝时,代表资源对象的指针不是复制到另外一个 smart_ptr,而是 ” 挪动 ” 到新smart_ptr。挪动后,原来的smart_ptr.ptr_ == nullptr, 这样就实现了资源所有权的转移。
这也是 C ++ unique_ptr的根本行为。咱们在这里先把它命名为unique_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_;
};

自定义挪动构造函数 。在挪动构造函数中,咱们先是接管了other.ptr_ 指向的资源对象,而后把 otherptr_置为 nullptr,这样在other 析构时就不会谬误开释资源内存。

同时,依据 C ++ 的规定,手动提供挪动构造函数后,就会主动禁用拷贝构造函数。也就是咱们能失去以下成果:

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

自定义赋值函数 。在赋值函数中,咱们应用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 是通过挪动构造函数创立的。

多个智能指针共享对象 – shared_smart_ptr

学过第二讲的shared_ptr,咱们晓得它是利用计数援用的形式,实现了多个智能指针共享同一个对象。当最初一个持有对象的智能指针析构时,计数器减为 0,这个时候才会回收资源对象。

咱们先给出 shared_smart_ptr 的类定义

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_;
}

临时不思考多线程并发平安的问题,咱们简略在堆上创立一个 int 类型的计数器count_。上面具体开展各个函数的实现。

为了防止对 count_ 的反复删除,咱们放弃:只有当 ptr_ != nullptr 时,才对 count_ 进行赋值。

构造函数

同样的,应用 explicit 防止隐式转换。除了赋值ptr_,还须要在堆上创立一个计数器。

explicit shared_smart_ptr(T* ptr = nullptr){
    ptr_ = ptr;
    if (ptr_) {count_ = new int(1);
    }
}

析构函数

在析构函数中,须要依据计数器的援用数判断是否须要回收对象。

~shared_smart_ptr() {
    // ptr_为 nullptr,不须要做任何解决
    if (ptr_) {return;}
    // 计数器减一
    --(*count_);
    // 计数器减为 0,回收对象
    if (*count_ == 0) {
        delete ptr_;
        delete count_;
        return;
    }

}

挪动构造函数

增加对 count_ 的解决

shared_smart_ptr(shared_smart_ptr&& other) {
    ptr_ = other.ptr_;
    count_ = other.count_;

    other.ptr_ = nullptr;
    other.count_ = nullptr;
}

赋值构造函数

增加替换count_

shared_smart_ptr& operator = (shared_smart_ptr rhs) {std::swap(rhs.ptr_, this->ptr_);
    std::swap(rhs.count_, this->count_);
    return *this;
}

拷贝构造函数

对于 shared_smart_ptr,咱们须要手动反对拷贝构造函数。次要解决逻辑是赋值ptr_ 和减少计数器的援用数。

shared_smart_ptr(const shared_smart_ptr& other) {
    ptr_ = other.ptr_;
    count_ = other.count_;

    if (ptr_) {(*count_)++;
    }
}

这样,咱们就实现了一个本人的共享智能指针,贴一下残缺代码

template <typename T>
class shared_smart_ptr {
public:
    explicit shared_smart_ptr(T* ptr = nullptr){
        ptr_ = ptr;
        if (ptr_) {count_ = new int(1);
        }
    }

    ~shared_smart_ptr() {
        // ptr_为 nullptr,不须要做任何解决
        if (ptr_ == nullptr) {return;}

        // 计数器减一
        --(*count_);

        // 计数器减为 0,回收对象
        if (*count_ == 0) {
            delete ptr_;
            delete count_;
        }
    }

    shared_smart_ptr(shared_smart_ptr&& other) {
        ptr_ = other.ptr_;
        count_ = other.count_;
        
        other.ptr_ = nullptr;
        other.count_ = nullptr;
    }

    shared_smart_ptr(const shared_smart_ptr& other) {
        ptr_ = other.ptr_;
        count_ = other.count_;
        if (ptr_) {(*count_)++;
        }
    }

    shared_smart_ptr& operator = (shared_smart_ptr rhs) {std::swap(rhs.ptr_, this->ptr_);
        std::swap(rhs.count_, this->count_);
        return *this;
    }

    int use_count() const { return *count_;};

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

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

private:
    T* ptr_;
    int* count_;
};

应用上面代码进行验证:

int main(int argc, const char** argv) {shared_smart_ptr<int> ptr1(new int(1));
    std::cout << "[初始化 ptr1] use count of ptr1:" << ptr1.use_count() << std::endl;
    {
        // 赋值应用拷贝构造函数
        shared_smart_ptr<int> ptr2 = ptr1;
        std::cout << "[应用拷贝构造函数将 ptr1 赋值给 ptr2] use count of ptr1:" << ptr1.use_count() << std::endl;

        // 赋值应用挪动构造函数
        shared_smart_ptr<int> ptr3 = std::move(ptr2);
        std::cout << "[应用挪动构造函数将 ptr2 赋值给 ptr3] use count of ptr1:" << ptr1.use_count() << std::endl;}
    std::cout << "[ptr2 和 ptr3 析构后] use count of ptr1:" << ptr1.use_count() << std::endl;}

运行后果:

[初始化 ptr1] use count of ptr1: 1
[应用拷贝构造函数将 ptr1 赋值给 ptr2] use count of ptr1: 2
[应用挪动构造函数将 ptr2 赋值给 ptr3] use count of ptr1: 2
[ptr2 和 ptr3 析构后] use count of ptr1: 1

总结

这一讲咱们从 AutoIntPtr 登程,先是将类进行模版化,使其可能治理任何类型的指针对象,并给该类起了一个更形象、更贴切的名称——smart_ptr

接着围绕着「如何正确开释资源对象指针」的问题,一步步手撸了两个智能指针 ——unique_smart_ptrshared_smart_ptr。置信大家当初对智能指针有一个较为深刻的了解了。

<center>- END -</center>
【往期举荐】

01 C++ 如何进行内存资源管理

02 脱离指针陷阱:深入浅出 C++ 智能指针

正文完
 0