C++智能指针

2024-05-30

如果在程序中使用new从堆(自由存储区)分配内存,等到不再需要时,应使用delete将其释放。C++引入了智能指针auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL是)表明,需要有更加精致的机制。基于程序员的编程体验和BOOST库提供的解决方案,C++11摒弃了auto_ptr,并新增了三种智能指针:unique_ptr, shared_ptr和weak_ptr。所有新增的智能指针都能与STL容器和移动语义协同工作。

智能指针是行为类似于指针的类对象,以下面函数为例:

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

这段程序存在缺陷,每当调用时,该函数都分配堆中的内存,但是从不回收,从而导致内存泄漏。这个问题实际上很简单,只要别忘了在return之前添加下面语句,释放分配的内存即可:

delete ps;

但是即便没有忘记释放分配内存,也存在问题,看下面这段程序:

void remodel(std::string &str)
{
    std::string *ps = new std::string();
    ...
    if (weird_thing()) {
		throw exception();
    }
    str = *ps;
    delete ps;
    return;
}

当出现异常时,delete将不被执行,因此也将导致内存泄漏。

问题分析

当remodel()函数终止时(不论正常终止还是异常终止),本地变量都将从栈内存中删除——因此指针ps所占据的内存将被释放。我们希望同时ps指向的内存也被释放,如果ps有一个析构函数,该析构函数将在ps过期时释放它指向的内存。

所以关键问题在于,ps只是一个常规指针,不是有析构函数的类对象。如果他是对象,则可以在对象过期时,让它的析构函数释放指向的内存。这正是auto_ptr、unique_ptr和shared_ptr背后的思想。模板auto_ptr是C++98提供的解决方案,C++11已经将其摒弃。

使用智能指针

这三个智能指针模板(auto_ptr、unique_ptr和shared_ptr)都定义了类似指针的对象,可以将new获得的地址赋值给这种对象,当智能指针过期时,其析构函数将使用delete来释放内存。因此,如果将new返回的地址赋给这些对象,则无需记住稍后释放这些内存,在智能指针过期时,这些内存将自动释放。

要创建智能指针,必须包含头文件memory,该文件模板定义。然后使用通常的模板语法来实例化所需类型的指针。

请求X类型的auto_ptr将获得一个指向X类型的auto_ptr:

auto_ptr<double> pd(new double);
auto_ptr<string> ps(new string);

其他两种智能指针使用相同的语法:

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

接下来对上面的remodel()函数进行转换,应该按下面3个步骤进行:

  1. 包含头文件memory;
  2. 将指向string的指针替换为指向string的智能指针对象;
  3. 删除delete语句。
#include <memory>
void remodel(std::string &str)
{
    std::auto_ptr<std::string> ps (new std::string(str));
    ...
    if (weird_thing()) {
        throw exception();
    }
    str = *ps;
    // delete ps;
    return;
}

可以看出智能指针模板位于名称空间std中,下面程序演示如何使用全部的三种智能指针。每个智能指针都放在一个代码段中,这样在离开代码块时,指针将过期。

#incldue <iostream>
#include <string>
#include <memory>

class Report
{
private:
    std::string str;
public:
	Report(const std::string s) : str(s) {
        std::cout << "Object created!\n";
    }
    ~Report() {
		std::cout << "Object deleted!\n";
    }
    void comment() const {
        std::cout << str << "\n";
    }
}

int main()
{
    {
        std::auto_ptr<Report> ps(new Report("using auto_ptr"));
        ps->comment();
    }
    {
        std::unique_ptr<Report> psu(new Report("using unique_ptr"));
        psu->comment();
    }
    {
        std::shared_ptr<Report> pss(new Report("using shared_ptr"));
        pss->comment();
    }
	return 0;
}

程序输出如下:

Object created!
using auto_ptr
Object deleted!
Object created!
using unique_ptr
Object deleted!
Object created!
using shared_ptr
Object deleted!

所有智能指针类都有一个explicit构造函数,该构造函数将指针作为参数,因此不需要自动将指针转换为智能指针对象:

shared_ptr<double> pd;
double *p = new double;
pd = p;							// not allow (implicit conversion)
pd = shared_ptr<double>(p);		// allow (explicit conversion)
shared_ptr<double> pshared = p; // not allow (implicit conversion)
shared_ptr<double> pshared(p);	// allow (explicit conversion)

对于三种智能指针,应该避免下面这种方式:

string str("I wandered lonely as a cloud.");
shared_ptr<string> pstr(&str);		// NO!

pstr过期时,程序将把delete运算符用于非堆内存,会发生段错误。

为何摒弃auto_ptr

为何摒弃auto_ptr呢?先来看下面的赋值语句:

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

如果执行了上述语句,两个指针指向了同一个对象,那么程序会试图删除同一个对象两次,ps过期时一次,vocation过期时一次。要避免这种问题,方法有多种:

  • 重载赋值运算符,执行深复制。这样两个指针指向不同的对象;
  • 建立所有权(ownership)概念,用于特定对象,只有一个智能指针可以拥有它,这样只有拥有对象的智能指针的构造函数会删除该对象。然后,让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr的策略,unique_ptr策略更加严格;
  • 创建更加智能的指针,跟踪引用特定对象的智能指针数。这称为引用计数(reference counting)。例如,赋值时,计数器将加1,而指针过期时,计数器将减1。仅当最后一个指针过期时,才调用delete。这是shared_ptr的策略。

同样的策略也适用于复制构造函数。

每个方法都有其用途。看下面这段程序:

#include <iostream>
#include <memory>
#include <string>

int main()
{
    using namespace std;
    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 Runs"));
        auto_ptr<string> (new string("Goose Egges"));
    };
    auto_ptr<string> pwin;
    pwin = films[2];		// 所有权转移
    for (int i = 0;i < 5;i++) {
		cout << *films[i] << endl;
    }
    return 0;
}

程序输出如下:

Fowl Balls
Duck Walks
Segmentation fault

这里的问题在于,下面的语句将所有权从films[2]转让给pwin:

pwin = films[2];

这就导致了films[2]不再引用该字符串,所以程序打印films[2]时发现这是一个空指针,发生内存错误。

如果在程序中使用shared_ptr代替auto_ptr,则程序将正常运行:

Fowl Balls
Duck Walks
Chicken Runs
Turkey Runs
Goose Egges

如果使用unique_ptr,结果与auto_ptr一样,unique_ptr也采用所有权模型。但使用unique_ptr时,在编译时由于这一句就会发生错误:

pwin = films[2];

那么为何unique_ptr要优于auto_ptr,看下面的语句:

auto_ptr<string> p1(new string("auto"));
auto_ptr<string> p2;
p2 = p1;

第三句p2接管p1的所有权之后,可以有效防止p2和p1删除同一个对象,但如果程序随后试图使用p1,由于p1不在指向有效的数据。但是如果使用unique_ptr,编译器会认为第三句是非法语句,避免了p2失去所有权后不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全(编译阶段的错误比程序运行中的潜在错误更安全)。

相比于auto_ptr,unique_ptr有另一个优点,它有一个可用于数组的变体。模板auto_ptr使用delete而不是delete[],因此只能与new而不能与new[]一起使用:

std::unique_ptr<double[]> pda(new double(5)); // 过期时使用delete []

选择智能指针

如果程序要使用多个指向同一个对象的指针,应该选择shared_ptr;如果程序不需要多个指向同一个对象的指针,可使用unique_ptr。

第四种智能指针

参考【C++】weak_ptr弱引用智能指针详解_weak ptr 原理-CSDN博客

实际上智能指针有4种,还有一种weak_ptr。weak_ptr在C++11种被引入,是为了弥补shared_ptr的缺陷。shared_ptr虽然解决了指针独占的问题,但是带来了引用成环的问题,这种问题它自己是没办法解决的,所以将weak_ptr引入标准库用来解决引用成环的问题。

循环引用:在shared_ptr使用过程中,当强引用计数器为0时,就会释放所指向的堆内存。但是如果和死锁一样,两个shared_ptr互相引用,他们就无法被释放了。

#include <iosteam>
#include <string>
#include <memory>
using namespace std;

class B;
class A {
private: 
	shared_ptr<B> b_ptr;
public:
	A() {
		cout << "A" << endl;
	}
	~A() {
		cout << "~A" << endl;
	}
	void set_ptr(shared_ptr<B> & ptr) {
		b_ptr = ptr;
	}
};
class B {
private:
	shared_ptr<A> a_ptr;
public:
	B() {
		cout << "B" << endl;
	}
	~B() {
		cout << "~B" << endl;
	}
	void set_ptr(shared_ptr<A> & ptr) {
		a_ptr = ptr;
	}
};

int main()
{
	shared_ptr<A> ptr_a(new A());
    shared_ptr<B> ptr_b(new B());
    ptr_a->set(ptr_b);
    ptr_b->set(ptr_a);
    cout << ptr_a.use_count() << " " << ptr_b.use_count() << endl;
    return 0;
}

运行结果如下:

A
B
2 2

可以看到析构函数并没有调用,这说明两个指针的计数器都不是0,运行结果也证明了这一点。

为了解决循环引用问题,解决办法就是将其中一个成员变量改为weak_ptr对象:

class B {
private:
	weak_ptr<A> a_ptr;
public:
	B() {
		cout << "B" << endl;
	}
	~B() {
		cout << "~B" << endl;
	}
	void set_ptr(shared_ptr<A> & ptr) {
		a_ptr = ptr;
	}
};

运行结果如下:

A
B
1 2
~A
~B

通过这次结果可以看到,两个对象都被正常析构了。

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。

weak_ptr模板类没有重载*和->运算符,因此weak_ptr类型指针只能访问某一shared_ptr指向的堆内存空间,无法对其进行修改。

C++11 weak_ptr智能指针(一看即懂) (biancheng.net)

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    std::shared_ptr<int> sp1(new int(10));
    std::shared_ptr<int> sp2(sp1);
    std::weak_ptr<int> wp(sp2);
    //输出和 wp 同指向的 shared_ptr 类型指针的数量
    cout << wp.use_count() << endl;
    //释放 sp2
    sp2.reset();
    cout << wp.use_count() << endl;
    //借助 lock() 函数,返回一个和 wp 同指向的 shared_ptr 类型指针,获取其存储的数据
    cout << *(wp.lock()) << endl;
    return 0;
}

运行结果如下:

2
1
10