返回

C++ 中智能指针的基本用法

简介

智能指针主要用来实现对资源的自动管理。按其用途有 std::unqiue_ptrstd::shared_ptrstd::weak_ptr

unique_ptr

unique_ptr 主要用来实现对某个资源对象的独占管理,该资源的生命周期会在 unique_ptr 声明周期结束时或被指向另一资源时结束。unique_ptr 可以通过 std::move 将自己管理的资源转移到另一个 unqiue_ptr 上。

以下是一些简单的例子:

#include <iostream>
#include <memory>

class Base {
public:
    Base(int id) : id(id), count(new int(1000)) { std::cout << "Base::Base() for " << id << std::endl; }

    ~Base() {
        delete count;
        std::cout << "Base::~Base() for " << id << std::endl;
    }

private:
    int id = 0;
    int* count = nullptr;
};

int main() {
    // unqiue_ptr would dispose resources when going out of scope
    std::cout << "-------------\n";
    { std::unique_ptr<Base> p2 = std::make_unique<Base>(1); }
    std::cout << "-------------\n";

    // unqiue_ptr can pass resource to another smart pointers
    std::cout << "-------------\n";
    {
        std::unique_ptr<Base> p2 = nullptr;
        std::cout << "================\n";
        {
            std::unique_ptr<Base> p3 = std::make_unique<Base>(2);
            p2 = std::move(p3);
        }
        std::cout << "==================\n";
    }
    std::cout << "-------------\n";

    return 0;
}

运行结果为:

-------------
Base::Base() for 1
Base::~Base() for 1
-------------
-------------
================
Base::Base() for 2
==================
Base::~Base() for 2
-------------

shared_ptr

在某些情况下,我们需要共享某个资源的所有权(操作权),例如一个图结构的节点和边,每个边也许两个节点,而每个节点可以和多个边相连,此时假如我们有一个边结构,那么涉及一个节点的每条边都需要拥有对该节点的所有权。此时 unqiue_ptr 不能满足需求,因此需要引入 shared_ptr 来实现对节点的共同管理。shared_ptr 内部维护一个引用计数,只有当所有 shared_ptr 的对象都超出生命周期时(引用计数降为 0)才会对资源对象进行析构,如下所示:

#include <iostream>
#include <memory>

class Base {
public:
    Base(int id) : id(id), count(new int(1000)) { std::cout << "Base::Base() for " << id << std::endl; }

    ~Base() {
        delete count;
        std::cout << "Base::~Base() for " << id << std::endl;
    }

private:
    int id = 0;
    int* count = nullptr;
};

int main() {
    // shared_ptr can be used to shared management for resource
    std::cout << "-------------\n";
    {
        std::shared_ptr<Base> p1 = std::make_shared<Base>(1);
        std::cout << "Use count for 1: " << p1.use_count() << std::endl;
        std::cout << "================\n";
        {
            std::shared_ptr<Base> p2 = p1;
            std::cout << "Use count for 1: " << p1.use_count() << std::endl;
        }
        std::cout << "================\n";
        std::cout << "Use count for 1: " << p1.use_count() << std::endl;
    }
    std::cout << "-------------\n";

    return 0;
}

运行结果如下所示:

-------------
Base::Base() for 1
Use count for 1: 1
================
Use count for 1: 2
================
Use count for 1: 1
Base::~Base() for 1
-------------

weak_ptr

有时候滥用 shared_ptr 可能会引起循环引用的问题。例如在上一部分中提到的图中的节点和边的问题。假如我们有一个节点类 Node 和边类 Edge。假如我们让 Node 使用 shared_ptr 指向一个 Edge 对象,又让该对象使用 shared_ptr 指向 Node 对象。即一对 NodeEdge 对象互相包含一个 shared_ptr 指向对方,此时,如果节点对象要释放,必须先让边对象析构释放触发其中的智能指针释放节点,对边对象也是一样的,从而发生引用循环,在这两个对象出了他们的 scope 后,他们指向的对象还存在于内存中且无法再次获取,会导致内存泄漏,如下所示:

#include <iostream>
#include <memory>

class Node;

class Edge {
public:
    Edge() { std::cout << "Edge Constructed.\n"; }
    ~Edge() { std::cout << "Edge Destroyed.\n"; }

    void setNode(std::shared_ptr<Node>& node) { node_ = node; }

private:
    std::shared_ptr<Node> node_;
};

class Node {
public:
    Node() { std::cout << "Node Constructed.\n"; }
    ~Node() { std::cout << "Edge Destroyed.\n"; }

    void setEdge(std::shared_ptr<Edge>& edge) { edge_ = edge; }

private:
    std::shared_ptr<Edge> edge_;
};

int main() {
    // shared_ptr can be used to shared management for resource
    std::cout << "-------------\n";
    {
        std::shared_ptr<Edge> edge = std::make_shared<Edge>();
        std::shared_ptr<Node> node = std::make_shared<Node>();

        edge->setNode(node);
        node->setEdge(edge);
    }
    std::cout << "-------------\n";

    return 0;
}

运行结果如下所示:

-------------
Edge Constructed.
Node Constructed.
-------------

要解决这个问题,有几个思路:

  • 首先思考有没有必要在所有类中都包含其他类,例如在这个例子下,作为一个节点有必要知道它对应的信息吗?如果没有必要,我们可以直接将节点中的边变量去除
  • 如果我们需要在节点和边都保留对方的相关信息以便做出某种操作,一种或许更好的方法是新建一个图类 Graph 来统一对边和节点的管理,而在 NodeEdge 只保留相关变量的 id,在有需要时从图中获取即可
  • 如果为了减少函数调用或者其他原因我们不想在外部对这些变量统一管理,那么可以使用 weak_ptr 来降低某个变量对另一个变量的权限。

weak_ptr 可以用来作为对一个 shared_ptr 管理的对象的弱引用,相当于 shared_ptr 的配套组合。在使用上,weak_ptr 不能直接对资源进行操作,在使用前需要通过 expired() 来检查资源是否合法(存在),在操作时必须临时转为 shared_ptr 才可以使用。这样能够保证 shared_ptr 的生命周期不会过长。在上面的例子中,我们可以将节点类中的边改为 weak_ptr

#include <iostream>
#include <memory>

class Node;

class Edge {
public:
    Edge() { std::cout << "Edge Constructed.\n"; }
    ~Edge() { std::cout << "Edge Destroyed.\n"; }

    void setNode(std::shared_ptr<Node>& node) { node_ = node; }

private:
    std::weak_ptr<Node> node_;
};

class Node {
public:
    Node() { std::cout << "Node Constructed.\n"; }
    ~Node() { std::cout << "Node Destroyed.\n"; }

    void setEdge(std::shared_ptr<Edge>& edge) { edge_ = edge; }

private:
    std::shared_ptr<Edge> edge_;
};

int main() {
    // shared_ptr can be used to shared management for resource
    std::cout << "-------------\n";
    {
        std::shared_ptr<Edge> edge = std::make_shared<Edge>();
        std::shared_ptr<Node> node = std::make_shared<Node>();

        edge->setNode(node);
        node->setEdge(edge);
    }
    std::cout << "-------------\n";

    return 0;
}
-------------
Edge Constructed.
Node Constructed.
Edge Destroyed.
Edge Destroyed.
-------------

智能指针的简单实现

在了解了智能指针的原理后,我们可以进行智能指针的简单实现。

Unique_ptr

Unique_ptr 的要点在于限制资源资源的拷贝构造和赋值,且保证移动构造和赋值成立,如下所示:

template <typename T>
class Unique_ptr {
public:
    /******************* Construct and Assign ********************/
    Unique_ptr(T* res = nullptr) : res_(res) {}
    Unique_ptr& operator=(T* res) {
        delete res_;
        res_ = res;
        return *this;
    }

    Unique_ptr(const Unique_ptr& rhs) = delete;
    Unique_ptr& operator=(const Unique_ptr& rhs) = delete;

    Unique_ptr(Unique_ptr&& rhs) {
        if (rhs.res_ == res_) return;  // do no change
        if (res_) delete res_;

        res_ = rhs.res_;
        rhs.res_ = nullptr;  // make sure that make rhs own nullptr
    }
    Unique_ptr& operator=(Unique_ptr&& rhs) {
        if (rhs.res_ == res_) return *this;  // do no change
        if (res_) delete res_;

        res_ = rhs.res_;
        rhs.res_ = nullptr;  // make sure that make rhs own nullptr

        return *this;
    }
    /******************* Construct and Assign ********************/

    /******************* Usage ************************/
    explicit operator bool() const { return res_; }
    T* operator*() const { return res_; }
    T& operator->() const { return *res_; }
    /******************* Usage ************************/

    ~Unique_ptr() {
        if (res_) delete res_;
    }

private:
    T* res_ = nullptr;
};

Shared_ptr

Shared_ptr 的要点在于维护资源的引用技术,在重新赋值或者析构时更新引用计数,当引用计数为 0 时释放资源:

template <typename T>
class Shared_ptr {
public:
    /******************* Construct and Assign ********************/
    Shared_ptr(T* res = nullptr) : res_(res), count_(new int) {
        if (res_) *count_ = 1;
    }
    Shared_ptr& operator=(T* res) {
        if (res == res_) return *this;

        (*count_)--;
        if (*count_ == 0) delete res_;

        res_ = res;
        if (res_) *count_ = 1;
        return *this;
    }

    Shared_ptr(const Shared_ptr& rhs) : res_(rhs.res_), count_(rhs.count_) { (*count_)++; }
    Shared_ptr& operator=(const Shared_ptr& rhs) {
        if (rhs.res_ == res_) return *this;

        (*count_)--;
        if (*count_ == 0) delete res_;

        res_ = rhs.res;
        count_ = rhs.count_;
        *count_++;

        return *this;
    }
    /******************* Construct and Assign ********************/

    /******************* Usage ************************/
    explicit operator bool() const { return res_; }
    T* operator*() const { return res_; }
    T& operator->() const { return *res_; }
    int use_count() const { return *count_; }
    /******************* Usage ************************/

    ~Shared_ptr() {
        (*count_)--;
        if (*count_ == 0) {
            if (res_) delete res_;
            delete count_;
        }
    }

private:
    T* res_ = nullptr;
    int* count_ = nullptr;
};
Built with Hugo
Theme Stack designed by Jimmy