C++智能指针之shared_ptr

  • Post author:
  • Post category:其他


shared_ptr类

智能指针也是模板,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型:

shared_ptr<string> p1;    //可以指向string的 shared_ptr
shared_ptr<list<int>> p2; //可以指向list<int>的 shared_ptr

默认初始化的智能指针中保存着一个空指针。

智能指针的使用方式类似于普通指针。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:

shared_ptr支持的操作
shared_ptr<T> sp 空智能指针,可以指向类型为T的对象
p 将p用作一个判断条件,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象
p->mem 等价于(*p).mem
p.get() 返回p保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
p.swap(q) 交换p和q的指针
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化对象
shared_ptr<T> p(q) p是shared_ptr q的拷贝。此操作会递增q中的计数器,q中的指针必须能转换为T*
p = q p和q都是shared_ptr,所保存的指针必须能互相转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理内存释放
p.unique() 若p.use_count()为1,返回true,否则返回false
p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于调试

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。该函数也定义在头文件memory中。

//指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);

//p4指向一个值为'9999999999'的string
shared_ptr<string> p4 = make_shared<string>(10, '9');

//指向一个值初始化的Int,默认为0
shared_ptr<int> p5 = make_shared<int>();

make_shared用其参数来构造给定类型的对象。通常用auto来保存make_shared的结果:

//p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();

shared_ptr的赋值和拷贝

当进行赋值或拷贝操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

auto p = make_shared<int>(42);  //p指向的对象只有p一个引用者
auto q(p);  //p和q指向相同对象,此对象有两个引用者

我们可以认为每个shared_ptr都有一个关联的引用计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增,当我们给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会释放自己所管理的对象。

auto r = make_shared<int>(42); //r指向的int只有一个引用者
r = q; //给r赋值,令它指向另一个地址
       //递增q指向的对象的引用计数
       //递减r原来指向的对象的引用计数
       //r原来指向的对象已经没有引用者,会被释放掉

shared_ptr销毁管理

shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数会销毁对象,并释放它占用的内存。

当动态对象不再被使用时,shared_ptr类会自动释放动态对象,这一特性使动态内存的使用变得非常容易:

// factory 返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg)
{
    //恰当处理arg
    //shared_ptr负责释放内存
    return make_shared<Foo>(arg);
}

由于factory返回一个shared_ptr,所以我们可以确保它分配的对象会在恰当的时刻被释放。

void use_factory(T arg)
{
    shared_ptr<Foo> p = factory(arg);
    //使用p
    return p;   //当我们返回p时,引用计数进行了递增操作
} //p离开了作用域,但它指向的内存不会释放掉

如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得erase删除不再需要的那些元素。

使用了动态生存期的资源的类

程序使用动态内存出于以下三种原因之一:

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

实现数据共享的示例(shared_ptr的成员记录有多少个对象共享相同的vector):

//SharedPtr.h
#pragma once
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <initializer_list>

class SharedPtr
{
public:
	typedef std::vector<std::string>::size_type size_type;
	SharedPtr();
	SharedPtr(std::initializer_list<std::string> il);
	size_type size() const { return data->size(); }

	bool empty() const { return data->empty(); }
	void push_back(const std::string &t) { data->push_back(t); }
	void pop_back();
	std::string &front();
	std::string &back();
private:
	std::shared_ptr<std::vector<std::string> > data;
	void check(size_type i, const std::string &msg) const;
};
#include "SharedPtr.h"

SharedPtr::SharedPtr():data(std::make_shared<std::vector<std::string>>())
{

}

SharedPtr::SharedPtr(std::initializer_list<std::string> il):data(std::make_shared<std::vector<std::string>>(il))
{

}

void SharedPtr::pop_back()
{
	check(0, "pop back on empty vector");
	data->pop_back();
}

std::string & SharedPtr::front()
{
	check(0, "front on empty vector");
	return data->front();
}

std::string & SharedPtr::back()
{
	check(0, "back on empty vector");
	return data->back();
}

void SharedPtr::check(size_type i, const std::string &msg) const
{
	if (i >= data->size())
		throw std::out_of_range(msg);
}

该类使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作,当我们拷贝、赋值或者销毁一个SharedPtr对象时,它的shared_ptr成员会被拷贝、赋值或销毁。

shared_ptr和new结合使用

接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换成一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

shared_ptr<int> p1 = new int(1024);  //错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));   //正确:使用了直接初始化形式

相同原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换成一个普通指针

shared_ptr<int> clone(int p)
{
    return new int(p);  //错误。隐式转换为shared_ptr<int>
}

shared_ptr<int> clone(int p)
{
    return shared_ptr<int>(new int(p));  //正确
}
定义和改变shared_ptr的 其他方法
shared_ptr<T> p(q) p管理内置指针q所指向的对象。q必须指向new分配的内存,且能转换为T* 类型
shared_ptr<T> p(u) p从unique_ptr u那里接管了对象的所有权,将u置为空
shared_ptr<T> p(q, d) p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
p.reset() 若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递可选参数内置指针q,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete 来释放q
p.reset(q)
p.reset(q, d)

不要混合使用普通指针和智能指针

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝之间。这也是为什么我们推荐使用make_shared而不是new的原因。这样我们就能在分配对象的同时将shared_ptr与之绑定,从未避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

void process(shared_ptr<int> ptr)
{
    //使用ptr
} //ptr离开作用域,被销毁

正确使用此函数的方法是传递一个shared_ptr:

shared_ptr<int> p(new int(42)); //引用计数为1
process(p); //拷贝p会递增它的引用计数;在process中引用计数为2
int i = *p;

但是这样做会导致错误:

int *x(new int(1024)); //危险,x是一个普通指针,不是一个智能指针
process(x); //错误,不能将int*转换为shared_ptr<int>
process(shared_ptr<int>(x));//合法的,但内存会被释放。表达式结束时,临时对象销毁,它所指向的内存被释放!
int j = *x; //未定义,x是个悬空指针

当一个shared_ptr绑定到了一个普通指针时,我们将管理内存的责任就交给了这个shared_ptr,一旦这样做,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了

不要使用get初始化另一个智能指针或为智能指针赋值

智能指针类型定义了一个get()函数,它返回一个内置指针,指向智能指针管理的对象,此函数是为了这样一种情况设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。

错误示例:

shared_ptr<int> p(new int(42)); //引用计数为1
int *q = p.get();  //正确,但使用q时注意,不要让它管理的指针被释放
{//新程序块
     shared_ptr<int> (q);
}//程序块结束,q被销毁,它指向的内存被释放

int foo = *p; //未定义:p指向的内存已经被释放了

get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get.特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

智能指针reset操作

我们可以用reset来将一个新的指针赋予一个shared_ptr:

p = new int(1024); //错误,不能将一个指针赋予shared_ptr
p.reset(new int(1024)); //正确,p指向一个新对象

reset会更新引用计数,如果需要的话,会释放p指向的对象。reset经常与unique一起使用,来控制多个shared_ptr共享的对象。

if(!p.unique()){
    p.reset(new string(*p)); //我们知道自己不是唯一用户,分配新的拷贝
} 
*p += newVal; //现在我们知道自己是唯一的用户,可以改变对象的值

智能指针和异常

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不在需要时将其释放:

void f()
{
    shared_ptr sp(new int(42)); //分配一个新对象
    //这段代码抛出一个异常,且在f中未被捕获
} // 在函数结束时shared_ptr自动释放内存

使用智能指针规范

  1. 不适用相同的内置指针初始化(或reset)多个智能指针
  2. 不delete get()返回的指针
  3. 不使用get()初始化或reset另外一个智能指针
  4. 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  5. 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器



版权声明:本文为u012069234原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。