目录

1、前言

智能指针是代理模式的具体应用,它使用 RAII 技术代理了裸指针,能够自动释放内存, 无需程序员干预,所以被称为“智能指针”。

智能指针不是指针,而是一个对象,所以不要对其调用delete,它会自动管理初始化时的指针,在离开作用域时析构释放内存。

智能指针也没有定义加减运算,不能随意移动指针地址,这样避免了指针越界操作。

在使用上:

如果指针是“独占”使用,就应该选择 unique_ptr,它为裸指针添加了很多限制,更加安全 。

如果指针是“共享”使用,就应该选择 shared_ptr,它的功能非常完善,用法几乎与原始指针一样

使用智能指针要加头文件#include <memory>

工厂函数make_unique()、make_shared()不只是返回智能指针对象,其内部也有优化。

scope_ptr

scoped_ptr是一个智能指针的类模板,它在C++中用于管理动态分配的对象。scoped_ptr提供了一种简单的方式来确保在离开作用域时自动释放内存,并避免内存泄漏。

以下是使用scoped_ptr的基本方法:

  1. 包含头文件:首先,需要包含 <memory> 头文件,因为 scoped_ptr 是位于该头文件中的。
#include <memory>
  1. 创建和初始化 scoped_ptr:可以使用 scoped_ptr 的构造函数来创建并初始化一个指向对象的指针。
std::scoped_ptr<int> ptr(new int(42));

上述代码示例创建了一个 scoped_ptr 对象 ptr,它指向一个动态分配的整数对象并初始化为 42。

  1. 使用指针:可以通过 scoped_ptr 对象访问指向的对象。可以使用解引用操作符 * 来获取指针所指向的值,也可以使用箭头操作符 -> 来调用指针所指向对象的成员函数。
int value = *ptr;
ptr->someFunction();

上述代码示例展示了如何通过 scoped_ptr 访问指向的对象。可以将指针解引用以获取值,并使用箭头操作符调用对象的成员函数。

  1. 释放内存:scoped_ptr 将在其作用域结束时自动释放内存。不需要显式调用 delete 来释放内存,scoped_ptr 析构函数会自动处理。
{
    std::scoped_ptr<int> ptr(new int(42));
    // 在作用域结束时自动释放内存
} // 内存被自动释放

上述代码示例中,在 scoped_ptr 的作用域结束时,指向的对象会自动被释放,无需手动调用 delete。

需要注意的是,scoped_ptr 是 C++11 之前的标准库提供的智能指针,从 C++11 开始已经被废弃,推荐使用 std::unique_ptr 代替。std::unique_ptr 提供了更丰富的功能和更好的语义。在新的代码中,建议使用 std::unique_ptr 或其他智能指针类来管理动态分配的对象。

希望这些解释对你有所帮助!

unique_ptr

unique_ptr需要手动初始化,声明的时候必须用模板参数指定类型:

unique_ptr<int> ptr1(new int(10)); // int智能指针
assert(*ptr1 = 10); // 可以使用*取内容
assert(ptr1 != nullptr); // 可以判断是否为空指针
unique_ptr<string> ptr2(new string("hello")); // string智能指针
assert(*ptr2 == "hello"); // 可以使用*取内容
assert(ptr2->size() == 5); // 可以使用->调用成员函数

也可以调用工厂函数,强制创建智能指针的时候必须初始化:

auto ptr3 = make_unique<int>(42); // 工厂函数创建智能指针
assert(ptr3 && *ptr3 == 42);
auto ptr4 = make_unique<string>("god of war"); // 工厂函数创建智能指针
assert(!ptr4->empty());

unique_ptr表示该智能指针的所有权是唯一的,不允许共享,任何时候只能有一个人持有。

它禁止拷贝赋值,但是可以使用std::move()显式地声明所有权转移:

auto ptr1 = make_unique<int>(42); // 工厂函数创建智能指针
assert(ptr1 && *ptr1 == 42); // 此时智能指针有效
auto ptr2 = ptr1; // 编译有问题
auto ptr2 = std::move(ptr1); // 使用move()转移所有权
assert(ptr2 && *ptr2 == 42);
assert(ptr1); // 此时智能指针无效.会报错

指针的所有权就被转走了,原来的 unique_ptr 变成了空指针,新的 unique_ptr 接替了管理权,保证所有权的唯一性

shared_ptr

基本使用方式与unique_ptr并无不同:

shared_ptr<int> ptr1(new int(10)); // int智能指针
assert(*ptr1 = 10); // 可以使用*取内容
shared_ptr<string> ptr2(new string("hello")); // string智能指针
assert(*ptr2 == "hello"); // 可以使用*取内容
auto ptr3 = make_shared<int>(42); // 工厂函数创建智能指针
assert(ptr3 && *ptr3 == 42); // 可以判断是否为空指针
auto ptr4 = make_shared<string>("zelda"); // 工厂函数创建智能指针
assert(!ptr4->empty()); // 可以使用->调用成员函数

不过它的所有权可以被安全共享,支持拷贝赋值

auto ptr1 = make_shared<int>(42); // 工厂函数创建智能指针
assert(ptr1 && ptr1.unique() ); // 此时智能指针有效且唯一
auto ptr2 = ptr1; // 直接拷贝赋值,不需要使用move()
assert(ptr1 && ptr2); // 此时两个智能指针均有效
assert(ptr1 == ptr2); // shared_ptr可以直接比较
// 两个智能指针均不唯一,且引用计数为2
assert(!ptr1.unique() && ptr1.use_count() == 2);
assert(!ptr2.unique() && ptr2.use_count() == 2);

其内部使用引用计数,所以具有完整的”值语义“,可以在任何场合下代替原始指针。

不过维护引用计数的存储和管理都是成本,过度使用会降低运行效率。其引用计数也会带来循环引用,下面是简化后的典型例子:

#include <iostream>
#include <memory>
#include <assert.h>
using namespace std;
class Node final {
public:
    using this_type = Node;
    using shared_type = std::shared_ptr<this_type>;
    shared_type next;   // 使用只能指针来指向下一个节点
};
int main() {
    auto n1 = make_shared<Node>();  // 工厂函数创建智能指针
    auto n2 = make_shared<Node>();

    // 此时引用计数均为1
    assert(n1.use_count() == 1);
    assert(n2.use_count() == 1);

    // 产生循环引用
    n1->next = n2;
    n2->next = n1;

    // 此时引用计数均为2,且无法减到0,内存泄露
    assert(n1.use_count() == 2);
    assert(n2.use_count() == 2);
}

这个例子很简单,你一下子就能看出存在循环引用。但在实际开发中,指针的关系可不像例 子那么清晰,很有可能会不知不觉形成一个链条很长的循环引用,复杂到你根本无法识别, 想要找出来基本上是不可能的。 想要从根本上杜绝循环引用,光靠 shared_ptr 是不行了,必须要用到weak_ptr。

weak_ptr

它专门为打破循环引用而设计,只观察指针,不会增 加引用计数(弱引用),但在需要的时候,可以调用成员函数 lock(),获取 shared_ptr(强引用) 。用法如下

#include <iostream>
#include <memory>
#include <assert.h>
using namespace std;
class Node final {
public:
    using this_type = Node;
    // 改用weak_ptr
    using shared_type = std::weak_ptr<this_type>;
    shared_type next;   // 使用只能指针来指向下一个节点
};
int main() {
    auto n1 = make_shared<Node>();  // 工厂函数创建智能指针
    auto n2 = make_shared<Node>();

    // 此时引用计数均为1
    assert(n1.use_count() == 1);
    assert(n2.use_count() == 1);

    // 产生循环引用
    n1->next = n2;
    n2->next = n1;

    // 因为使用了weak_ptr,引用计数为1
    assert(n1.use_count() == 1);
    assert(n2.use_count() == 1);
    if (!n1->next.expired()) {  // 检查指针是否有效
        auto ptr = n1->next.lock(); // lock()获取shared_ptr
        assert(ptr == n2);
    }
}

使用建议

在上面的代码中需要注意,我们使用std::make_shared代替new的方式创建shared_ptr。

2、在类内部调用其他类的方法

不可以在A内部定义B、C的shared_ptr,而是应该使用weak_ptr

class A {
private:
    std::weak_ptr<B> b_;
    std::weak_ptr<C> c_;
public:
    void do_something();
    void set_b_c(const std::shared_ptr<B>& b, const std::shared_ptr<C>& c) {
        b_ = b;
        c_ = c;
    }
};

#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class A;

class D {
private:
    std::shared_ptr<A> a_;
public:
    D(std::shared_ptr<A>& a): a_(a) {}
};


class A : public std::enable_shared_from_this<A>{
public:
    void new_D()
    {
        // 错误方式,用this指针重新构造shared_ptr,将导致二次释放当前对象,重新创建的shared_ptr与当前对象的shared_ptr没有关系,没有增加当前对象的引用计数
        // 关于这个问题,可以详细地看一下这篇博客:https://www.cnblogs.com/jo3yzhu/p/11358400.html
//        std::shared_ptr<A> this_shared_ptr1(this);
//        std::unique_ptr<D> d1(new D(this_shared_ptr1));
        // 正确方式 对象继承了enable_shared_from_this后,可以通过shared_from_this()获取当前对象的shared_ptr指针。
        std::shared_ptr<A> this_shared_ptr2 = shared_from_this();
        std::unique_ptr<D> d2(new D(this_shared_ptr2));
    }
};

int main() {
    std::shared_ptr<A> a(new A());
    a->new_D();
}

4、智能指针只能管理堆对象,不能管理栈上对象

栈上对象本身在出栈的时候就会被自动销毁,如果将其指针交给智能指针,会造成对象的二次销毁。

5、不能将this指针直接托管给智能指针

如果对一个类初始化为智能指针,那么在类内部函数用this指针重新构造shared_ptr,不会增加引用计数,会导致当前对象的二次释放。

class A {
public:
    void function() {
        // 错误方式
        std::shared_ptr<A> this_shared_ptr(this);
    }
};
std::shared_ptr<A> e = std::make_shared<E>();

shared_ptr是管理共享资源的利器,需要注意避免循环引用,通常的做法是owner持有指向child的shared_ptr,child持有指向owner的weak_ptr。

这种做法可以解决循环引用的问题。当 owner 不再需要 child 时,owner 的 shared_ptr 被释放,同时减少 child 的引用计数。如果没有其他 shared_ptr 持有 child,则 child 也会被销毁。

7、访问原始指针采用*而非.get()

  • request.get(): 这是 boost::shared_ptr 类的成员函数,用于获取指向的原始裸指针。使用 get() 函数可以获得指向被 shared_ptr 管理的对象的裸指针,但请注意,当您使用裸指针时,将失去 shared_ptr 提供的管理功能,这可能导致潜在的内存泄漏或悬空指针问题。因此,除非特定情况下需要裸指针,否则最好避免使用 get() 函数。
  • *request: 这是对 boost::shared_ptr 的解引用操作符。这将返回由 shared_ptr 所管理的对象的引用。通过解引用操作符,您可以直接访问由 shared_ptr 管理的对象。这种方式更安全,因为它维持了 shared_ptr 提供的所有权和引用计数的机制。

因此,总体而言,应当优先选择使用 *request 而不是 request.get(),以便利用 shared_ptr 提供的安全管理功能。

以下是一个简单的示例,演示了 request.get() 和 *request 之间的区别:

#include <iostream>
#include <boost/shared_ptr.hpp>

class MyClass {
public:
    void display() {
        std::cout << "Hello from MyClass!" << std::endl;
    }
};

int main() {
    // 创建一个 MyClass 对象的 shared_ptr
    boost::shared_ptr<MyClass> ptr(new MyClass());

    // 使用 shared_ptr 的构造函数创建另一个 shared_ptr
    boost::shared_ptr<MyClass> anotherPtr(ptr);

    // 使用 request.get() 获取裸指针
    MyClass* rawPtr = ptr.get();

    // 使用 *request 解引用
    (*ptr).display();
    anotherPtr->display();

    // 修改原始指针会影响到共享的智能指针
    rawPtr->display();

    // 此时删除原始指针,但不影响 anotherPtr
    delete rawPtr;

    // 通过智能指针访问已经被删除的内存,可能导致未定义行为
    // anotherPtr->display(); // 这里将导致未定义行为

    return 0;
}

在这个示例中,我们创建了一个 MyClass 类的 shared_ptr 对象。然后使用 get() 函数获得了指向原始对象的裸指针,并且通过解引用操作符 *ptr 和箭头操作符 -> 来访问该对象的成员函数。随后删除了这个裸指针,但由于还有另一个 shared_ptr 持有相同的对象,这可能导致未定义行为。这个示例展示了在使用裸指针时要格外小心,而智能指针则提供了更安全的内存管理方式。