C++ 智能指针

316 阅读7分钟

C++智能指针是一种用于管理动态分配的对象内存的工具,其作用是自动化内存管理,防止内存泄漏和悬挂指针等问题。智能指针通过在对象生命周期结束时自动释放其所占用的内存,减少手动管理内存的繁琐和出错的可能性,提高程序的安全性和可靠性。常见的C++智能指针有unique_ptr、shared_ptr和weak_ptr等。auto_ptr在C++11中已经废弃。

  • 本文摘录《C++ Primer Plus》第六版中关于"智能指针模板类"的介绍。
  • 提供一个简单的shared_ptr的实现
  • 介绍weak_ptr的用途
  • 介绍std::enable_shared_from_this的作用

智能指针背后的设计思想

先看一个简单的例子:

void remodel(std::string & str)
{
    std::string * ps = new std::string("hello world");
    ...
    str = *ps;
    return;
}

上面代码存在的问题:每当调用时,该函数在堆中分配内存,但从来不回收,从而导致内存泄漏。解决之道是别忘了在return之前加入语句delete ps。然而,但凡涉及"别忘了"的解决方法,很少是最佳的。

remodel()这样的函数终止,本地变量都将从栈内存中删除——因此指针ps占据的内存将被释放。如果ps指向的内存也被自动释放,那该多好。如果它是对象,则可以在它过期时,让它的析构函数删除指向的内存。这正是智能指针背后的设计思想。

使用智能指针

要创建智能指针,必须包含头文件memory。类auto_ptr包含如下构造函数

template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr { // non-copyable pointer to an object
public:
    explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t{}, _Ptr) {}
};

定义一个智能指针如下:

auto_ptr<double> pd(new double);
unique_ptr<double> pdu(new double);
shared_ptr<string> pss(new string);

所有智能指针都有一个explicit构造函数,该构造函数将普通指针作为参数。因此不允许自动将指针转换为智能指针对象。

shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg                   // not allowd(implicit conversion)

智能指针的注意事项

不能指向栈内存的地址

string vacation("hello world");
shared_ptr<string> pvac(&vacation);  // NO!!

pvac过期时,程序将把delete运算符作用于非堆内存,这是错误的。

C++11废弃auto_ptr的原因

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; 
vocaticn = ps;

上述赋值语句将引入什么后果呢?序将试图删除同一个对象两次——一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种:

  • 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
  • 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更严格。
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main() {
  auto_ptr<string> films[5] =
 {
  auto_ptr<string> (new string("Fowl Balls")),
  auto_ptr<string> (new string("Duck Walks")),
  auto_ptr<string> (new string("Chicken Runs")),
  auto_ptr<string> (new string("Turkey Errors")),
  auto_ptr<string> (new string("Goose Eggs"))
 };
 auto_ptr<string> pwin;
 pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针

 cout << "The nominees for best avian baseballl film are\n";
 for(int i = 0; i < 5; ++i)
  cout << *films[i] << endl;
 cout << "The winner is " << *pwin << endl;
 cin.get();

 return 0;
}

运行上述代码发现程序崩溃了,原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:

  • 使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。
  • 使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译时指出代码行出现错误。

unique_ptr还有更聪明的地方

unique_ptr<string> demo(const char * s)
{
    unique_ptr<string> temp (new string (s))return temp;
}


unique_ptr<string> ps;
ps = demo('Uniquely special");

demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值,这正是unique_ptr更聪明的地方。

总之,党程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做, 例如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

当然,您可能确实想执行类似于#1的操作,仅当以非智能的方式使用摒弃的智能指针时(如解除引用时),这种赋值才不安全。要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。下面是一个使用前述demo()函数的例子,该函数返回一个unique_ptr对象:
使用move后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

设计一个简单的shared_ptr

该类的实现使用了引用计数的技术,即将一个整数计数器与指向动态分配对象的指针相关联。每当有一个新的shared_ptr指向该对象时,计数器就会加1;每当一个shared_ptr被销毁时,计数器就会减1。当计数器为0时,表示没有任何shared_ptr指向该对象,此时就可以安全地释放该对象的内存。

template<typename T>
class shared_ptr {
public:
    explicit shared_ptr(T* ptr = nullptr) : ptr_(ptr), ref_count_(new size_t(1)) {}

    shared_ptr(const shared_ptr<T>& other) : ptr_(other.ptr_), ref_count_(other.ref_count_) {
        ++(*ref_count_);
    }

    ~shared_ptr() {
        release();
    }

    shared_ptr<T>& operator=(const shared_ptr<T>& other) {
        if (this != &other) {
            release();
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            ++(*ref_count_);
        }
        return *this;
    }

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

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

    size_t use_count() const {
        return *ref_count_;
    }

    T* get() const {
        return ptr_;
    }

    bool unique() const {
        return *ref_count_ == 1;
    }

private:
    void release() {
        --(*ref_count_);
        if (*ref_count_ == 0) {
            delete ptr_;
            delete ref_count_;
            ptr_ = nullptr;
            ref_count_ = nullptr;
        }
    }

    T* ptr_;
    size_t* ref_count_;
};

weak_ptr 指针

std::weak_ptr是一种弱引用智能指针,它可以用于解决共享式智能指针(shared_ptr)的循环引用问题。它不会增加引用计数,只是提供一种非拥有性的观察机制,可以查询所指向的对象是否已经被释放,如果没有被释放则可以通过std::weak_ptr对象构造出一个std::shared_ptr对象来访问所指向的对象。

下面是一个使用std::weak_ptr的例子:

#include <iostream>
#include <memory>

class B; // 前置声明

class A {
public:
    std::weak_ptr<B> b_ptr; // 弱引用智能指针
    void do_something() {
        std::cout << "A::do_something()" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> a_ptr; // 共享式智能指针
    void do_something() {
        std::cout << "B::do_something()" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
    if (auto shared_b = a->b_ptr.lock()) {
        shared_b->do_something();
    } else {
        std::cout << "B has been deleted." << std::endl;
    }
    return 0;
}    

在这个例子中,类A和类B相互引用,如果使用std::shared_ptr来管理内存,会出现循环引用的问题,导致内存泄漏。为了解决这个问题,我们可以将B类中的std::shared_ptr成员改为std::weak_ptr,并在主函数中使用std::weak_ptr来访问B对象,从而避免了循环引用的问题。

在主函数中,我们首先创建了一个std::shared_ptr对象a和一个std::shared_ptr对象b,并分别将它们互相引用。然后我们使用a->b_ptr.lock()来获取B对象的std::shared_ptr,如果B对象还存在,则调用B对象的do_something()函数,否则输出一条消息表示B对象已经被删除。

std::enable_shared_from_this 作用

假设我们有两个类A和B,其中类B需要引用类A的对象,如下所示

class A {
public:
    void do_something() {
        std::cout << "A::do_something()" << std::endl;
    }
};

class B {
public:
    B(A* a) : a_(a) {}
    void do_something() {
        if (a_) {
            a_->do_something();
        }
    }
private:
    A* a_;
};

在类B的构造函数中,我们使用一个裸指针A*来引用类A的对象。虽然这种方式可以实现类B对类A的引用,但由于裸指针不会自动地管理对象的生命周期,存在以下潜在问题:

  1. 如果在程序的其他地方释放了类A对象的内存,那么类B引用的指针a_将会变成一个悬空指针,此时调用a_->do_something()同样会导致程序崩溃。

std::enable_shared_from_this是C++11中提供的一个模板类,它的作用是让一个对象能够安全地生成一个指向自身的std::shared_ptr,即实现一个对象的自身智能指针。

当一个类继承自std::enable_shared_from_this时,该类的对象就可以调用shared_from_this()函数来获得一个指向自身的std::shared_ptr。这个指针是安全的,因为它与对象原来的std::shared_ptr共享同一块内存,可以保证对象在std::shared_ptr引用计数为0时被正确释放,避免了使用裸指针的潜在风险。

使用std::enable_shared_from_this的步骤如下:

  1. 在类中继承std::enable_shared_from_this,其中T是类名;
  2. 在类中定义一个私有构造函数,用于禁止直接创建对象;
  3. 在类中使用shared_from_this()函数来获取一个指向自身的std::shared_ptr。

下面是一个使用std::enable_shared_from_this的例子:

#include <memory>
#include <iostream>

class A : public std::enable_shared_from_this<A> {
public:
    static std::shared_ptr<A> create(int value) {
        return std::shared_ptr<A>(new A(value));
    }
    void do_something() {
        std::cout << "A::do_something() value=" << value_ << std::endl;
    }
private:
    A(int value) : value_(value) {}
    int value_;
};

int main() {
    std::shared_ptr<A> a1 = A::create(123);
    std::shared_ptr<A> a2 = a1->shared_from_this();
    a1->do_something();
    a2->do_something();
    return 0;
}    

在上面的示例代码中,我们使用一个静态成员函数create来创建类A的实例,该函数接受一个int类型的参数value并返回一个std::shared_ptr对象。在create函数中,我们使用new关键字来分配类A的内存,并返回一个std::shared_ptr对象,该对象将自动地管理类A的生命周期。这样,我们就可以通过std::shared_ptr来管理类A的生命周期,避免了使用裸指针的潜在风险。