前语

C++程序的内存能够分为3种,分别为静态内存、栈内存,下表简略概述:

内存类型 效果 生命周期
静态内存 用来保存部分static目标、类static数据成员以及界说在任何函数之外的变量。 由编译器主动创立和毁掉,static目标在运用之前分配,在程序完毕时毁掉。
栈内存 用来保存界说在函数内的非static目标。 由编译器主动创立和毁掉,栈目标仅在其界说的程序块运转时才存在。
堆内存(自由空间) 用来存储动态分配的目标。 动态目标的生命周期由程序来控制,也便是说,当动态目标不再运用时,咱们的代码有必要显现地毁掉他们。

在C++中,动态内存的办理是经过一对运算符来完结的:

  • new:在动态内存中为目标分配空间,而且回来一个指向该目标的指针,咱们能够挑选对目标进行初始化
  • delete:承受一个动态目标的指针毁掉该目标,而且开释与之相关的内存

动态内存的运用很简略出问题,由于保证在正确的时间开释内存是极其困难的:

  • 有时咱们会忘记开释内存,在这种状况下会产生内存走漏
  • 有时在尚有指针引证内存的状况下咱们就开释了它,这种状况下会产生引证不合法内存的指针

为了更简略一同也更安全地运用动态内存,新的标准供给了两种智能指针类型来办理动态目标,智能指针的行为相似常规指针,重要的差异是它负责主动开释所指向的目标。新标准库供给的这两种智能指针的差异在于办理底层指针的办法:

  • shared_ptr:答应多个指针指向同一个目标
  • unique_ptr:”独占“所指向的目标。

标准库一同还界说了一个名为weak_ptr的伴随类,它是一种弱引证,指向shared_ptr所办理的目标。

清晰了几种类型的内存的特色,咱们知道其难点便是堆内存的开释,也便是指向堆内存的一般指针不知道何时该开释。这时咱们能够转化一下思路,运用栈内存的特色(主动请求和开释)以及类成员变量在类毁掉时能够主动毁掉这一特性,咱们把一般指针给封装一层成类,这样一般指针目标就能够变成类类型的栈内目标,以及类类型的数据成员。

万字长文详解C++智能指针

其实这也便是标准库中的做法,运用智能指针就让咱们不用再在意动态内存的手动毁掉了。

正文

1. shared_ptr

智能指针是一个模板类,当咱们创立智能指针时,有必要供给额外信息:指针能够指向的类型,在尖括号内给出类型,之后是所界说的这种智能指针的姓名:

 std::shared_ptr<std::string> p1;    //能够指向std::string类型目标
 std::shared_ptr<std::vector<int>> p2;       //能够指向int类型的vector

默许初始化的智能指针中保存着一个空指针,所以下面写法是严重过错:

std::shared_ptr<std::string> pString;
*pString = "hi";   //对nullptr进行解引证,必定报错

为了运用习气和削减运用成本,智能指针的运用办法与一般指针相似。解引证一个智能指针能够回来它指向的目标,一般指针的->符号也能够正常运用,这是重载了相关运算符的成果:

//初始化一个智能指针,指向"hello"
std::shared_ptr<std::string> pString = std::make_shared<std::string>("hello");
//假设指针不为空,且指向的目标不为空
if (pString != nullptr && !pString->empty()) {
    *pString = "hi";
}

由于咱们在只用一般指针时,当一般指针为nullptr时,且把指针作为条件判别是为false的,所以智能指针也有相似的特色:把智能指针作为条件判别,若智能指针指向一个目标,则为true

if (pString) {  //pString指向一个目标时,为true
    *pString = "Cn";
}

关于这些用法,其实都是C++重载运算符的用法,能够极大地便利开发者。

1.1 make_shared函数

最安全的分配和运用动态内存的办法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个目标,而且初始化它回来指向该目标的shared_ptr

这个函数有2个动作,一个是请求一块内存,其次是进行初始化。当运用一般指针以及new要害字时,咱们经过结构函数来进行初始化,那么相似的make_shared函数的参数,也有必要要和初始化的目标的结构函数的参数匹配

    //指向一个值为42的int类型的shared_ptr
    std::shared_ptr<int> p3 = std::make_shared<int>(42);
    //指向一个值为10个9的string类型的shared_ptr
    std::shared_ptr<std::string> p4 = std::make_shared<std::string>(10, '9');
    //指向一个默许值初始化的int类型的shared_ptr,值为0
    std::shared_ptr<int> p5 = std::make_shared<int>();

在上述代码中,比方要创立一个指向string类型的shared_ptr,其make_shared的参数有必要符合string的某一个结构函数,而关于不传入任何参数的状况来说,目标会进行值初始化

1.2 shared_ptr的仿制和赋值

已然智能指针的原理是经过把一般指针封装一层成类类型,当作一般目标来运用,所以其仿制和赋值的操作就十分重要。

咱们能够以为每个shared_ptr目标都有一个相关的计数器,被称为引证计数,用来记录有多少个shared_ptr一同指向所办理的内存目标,这句话的要害是多少个shared_ptr指向所办理的内存目标,而非一切指针,比方下面代码:

    //i1是一个一般指针
    int* i1 = new int(10);
    //运用i1初始化智能指针si1
    std::shared_ptr<int> si1(i1);
    //仿制si1会添加引证计数
    std::shared_ptr<int> si2(si1);
    //只会统计共同指向目标的shared_ptr数量
    std::cout << si1.use_count();

这儿的use_count()办法便是回来其计数器,这儿能够发现虽然有1个一般指针和2个shared_ptr都指向同一个目标,可是这儿计数器回来值是2,这儿也就引入一个根本准则:不要混用智能指针和一般指针,由于智能指针会毁掉所办理的目标,假设再运用一般指针,会呈现不合法引证的状况,后面会详细阐明。

当仿制一个shared_ptr时,关于被仿制的shared_ptr所指向的目标来说,其引证计数会添加,一般来说有3种常见的状况:

  • 运用一个shared_ptr去初始化另一个shared_ptr,会仿制参数的shared_ptr目标。
  • 将它作为参数,传递给一个函数时。
  • 将它作为回来值,也会产生仿制。

这3种状况咱们需求常常留意,而有哪些状况会削减其相关目标的引证计数呢?一般有两种状况:

  • shared_ptr毁掉时,比方脱离其效果域,会触发其析构函数,这时所办理目标的引证计数会减一。
  • 当给shared_ptr赋予一个新值时,其本来所指向的目标的引证计数会减一
    //r指向的int目标只有一个引证者
    auto r = std::make_shared<int>(42);
    r = si1;    //给r赋值,让其指向别的目标
               //递加si1指向的目标的引证计数
                //递减r本来指向的目标的引证计数
                //r本来指向的目标现已没有引证者,会主动开释

这儿要清楚引证计数记住是什么,记的是其指向目标有多少个同享的引证者,当被赋予新值时,这个shared_ptr会指向新的目标,而本来目标的引证者就会减一。

1.3 shared_ptr主动毁掉动态目标

前面说了shared_ptr是类类型,而且有所指向目标的引证计数,所以其办理所指向目标是经过这两者结合来完结的:

  1. shared_ptr析构函数会递减它所指向的目标的引证计数,一同给一个shared_ptr赋予新值时也会递减。

  2. 当引证计数变为0时,shared_ptr会毁掉所指向的目标,一同开释内存。留意这儿一般是析构函数来做的,可是也不完满是,比方前一段的实例代码中,r所指向的目标42,当r指向其他目标时,这个42就再也没有shared_ptr指向它了,所以这时仍是由r会去毁掉42以及开释其内存。

所以结合这2点,以及递加和递减引证计数的原理,在平时运用时咱们直接运用shared_ptr类型进行值传递,这样就能够完结主动开释内存的功用了。

1.4 运用动态资源的场景

在程序开发中,常常有如下3种状况会运用动态内存:

  1. 程序不知道自己需求运用多少目标。比方容器类,在编译阶段并不知道真实运用时会有多少个元素,所以运用动态内存创立和办理数组是十分便利的。
  2. 程序不知道所需目标的准确类型。这种状况涉及到多态,即父类指针能够指向子类目标,这种状况多用于接口编程。
  3. 程序需求在多个目标之间同享数据。比方在处理流数据的体系中,一个比较大的数据流,在不同函数之间和不同目标之间传递时,必定不能运用值仿制,这样太费功能了,最好的办法是运用动态内存,多个目标同享数据。

1.5 运用shared_ptr同享底层数据

现在咱们就来实践一下,咱们准备界说一个StrBlob类,该类存储着string列表,一同需求在各个函数之间传递和处理其string列表,这就要求该数据能在不同StrBlod类之间同享,不能进行值仿制,所以直接运用std::vector<std::string>作为数据成员的计划就不能够了。

处理计划便是运用指针,而且是运用智能指针,这儿保存数据的容器仍是运用标准容器,所以StrBlob.h的界说如下:

class StrBlob {
public:
	//运用typedef简化代码
	typedef std::vector<std::string>::size_type size_type;
	StrBlob();
	//用来初始化vector数据
	StrBlob(std::initializer_list<std::string> il);
	//由于智能指针的重载运算符,所以能够把data当成std::vector<>*一般指针来运用
	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;
	//假设data[i]不合法,抛出反常
	void check(size_type i, const std::string &msg) const;
};

代码中要害当地都有注释,核心便是不再运用裸指针,以及能够像运用裸指针办法相同运用智能指针。接下来便是几个办法的完结,也是十分简略:

//运用初始化列表,初始化一个空vector
StrBlob::StrBlob(): data(std::make_shared<std::vector<std::string>>()) { }
//运用初始化列表,由于il是vector中的结构函数之一的参数,所以这儿make_shared的参数
//便是il
StrBlob::StrBlob(std::initializer_list<std::string> il) :
	data(std::make_shared<std::vector<std::string>>(il)){ }
void StrBlob::pop_back() {
	check(0, "pop_back on empty StrBlob!");
	return data->pop_back();
}
std::string& StrBlob::front() {
	check(0, "front on empty StrBlob!");
	return data->front();
}
std::string& StrBlob::back() {
	check(0, "back on empty StrBlob!");
	return data->back();
}
//查看是否越界
void StrBlob::check(size_type i, const std::string& msg) const {
	if (i >= data->size()) {
		throw std::out_of_range(msg);
	}
}

上述代码完结起来十分简略,咱们能够像运用裸指针相同来运用智能指针,最重要的是再也不用在析构函数里开释内存了,其data数据还能够同享,是一个十分常见的运用场景。

2. newdelete

虽然现在C++11后就引荐运用智能指针,可是大量的老项目以及各种状况,让开发者有时不得不运用一般指针,还有便是了解一般指针的各种用法,有利于了解智能指针的完结以及处理一些常见过错。

2.1 内存耗尽

简略的怎么运用newdelete就不说了,这儿先说一种内存耗尽的状况。当程序呈现内存走漏,有限的堆内存就有或许被耗尽,这时现已没有足够的空间来分配动态目标了,这时new表达式就会失利,一同抛出一个类型为bad_alloc的反常。

可是咱们能够改动运用new的办法来阻挠它抛出反常:

    //假设分配失利,new抛出std::bad_alloc反常
    int* px = new int;
    //假设分配失利,new回来一个空指针
    int* px1 = new (std::nothrow) int;

这种形式的new被称为定位new(placement new),这时咱们想一下这有什么用呢?便是能够合作判别new的指针是否为空,来看new操作是否成功了,如下:

    //假设分配失利,new回来一个空指针
    int* px1 = new (std::nothrow) int;
    if (!px1) {
        //new成功了
    }

而关于一般指针,许多开发者也喜爱这样写,判别一下new的指针是否为空,可是一旦为空,就会抛出反常,后面的判别句子根本履行不了,是无效判别。 所以在请求一些大的内存数据时,仍是主张运用new (std::nothrow)合作判空,来判别内存是否耗尽。

2.2 delete留意事项

C++中运用delete来开释new请求的内存,经过delete能够将动态内存归还给体系。delete会履行2个动作:毁掉给定的指针指向的目标以及开释对应的内存

delete的用法就不说了,现在来说几点需求特别留意的。

  1. delete接纳的指针有必要是指向动态分配的内存或许是一个空指针
  2. 开释一块非new分配的内存,其行为是未界说的。
    int i = 0;
	//pi1是指向栈内存的指针
	int* pi1 = &i;
	//pi2是空指针
	int* pi2 = nullptr;
	//delete一个指向非动态内存的指针是严重过错!!!
	delete pi1;
	//能够delete一个空指针
	delete pi2;

在上面代码中,pi1是指向非动态内存的指针,编译器是无法判别传递给delete的指针详细指向的是什么,所以能够编译经过,运转会报错,这种潜在的问题要时间留意。

  1. 相同的指针值开释屡次,其行为是未界说的。
    int* pi3 = new int(20);
	delete pi3;
	//delete一个指针屡次,是严重过错!!
	delete pi3;

上面这种状况相同编译器无法辨认,只会在运转时才报错,需求时间留意。这儿咱们能够想一下,已然delete一个空指针是能够的,可是为什么不能delete一个指针2次呢?这是由于delete后的指针值是一个无效值,但不是nullptr

  1. delete之后的指针被称为空悬指针(dangling pointer),即指向一块曾经保存数据目标但现在现已无效的内存的指针,对空悬指针再次调用delete是严重过错。
  2. 假设有事务需求,需求再次运用被delete后的指针,这时咱们能够在delete之后对指针赋予nullptr,这样在下次再运用时不论是误操作delete仍是判空再赋予新值,都能够正常运用。
  3. 关于多个一般指针指向同一块内存的状况,操作delete要十分留意,防止呈现一个指针现已开释了内存,其他指针再次运用的状况,导致引证不合法内存。处理这种状况最好的办法便是运用shared_ptr

2.3 shared_ptrnew结合运用

前面说了创立shared_ptr目标最佳计划是运用make_shared办法,可是更多的时分咱们不得不好一般指针打交道,标准库也供给了shared_ptrnew结合运用的各种场景。

  1. 能够运用new回来的指针来初始化智能指针,接纳指针参数的智能指针结构函数是explicit,即是显现的,所以无法将一个内置指针隐式转化为一个智能指针
    int* i = new int(1024);
	//过错,有必要运用直接初始化的办法
	std::shared_ptr<int> si = i;
	//正确的办法
	std::shared_ptr<int> si1(i);

这种直接运用的场景十分简略辨认,难的是许多旧函数库中的参数和回来值是指针的状况,这时咱们就要特别留意,后面咱们会说这种状况。

和前面delete留意事项相同,由于shared_ptr默许会调用指针的delete函数来开释内存,所以传递给shared_ptr的一般指针也有必要是指向动态内存的

  1. 已然智能指针是指针的封装,所以它必定不能一直和一个指针绑定,所以标准库还界说了一些改动shared_ptr指向目标的其他办法,都是十分有用的。

值得关注的是reset()办法,它表示”重置”的意思,能够把一个shared_ptr包括的指针重置为空指针或许其他指针,在重置的过程中,咱们就应该明晰的认识到相相关的目标的引证数的变化

//测验类,打印了析构函数
class Test{
public:
	Test(int i);
	~Test(){
		std::cout << "析构 k = " << k << std::endl;
	}
	int getValue() { return k; }
private:
	int k;
};
int main()
{
	std::shared_ptr<Test> sp1 = std::make_shared<Test>(100);
	//重置为空,sp1本来指向的目标就没有引证者了,会被析构
	sp1.reset();
	if (!sp1){
		std::cout << "sp1 is nullptr!" << std::endl;
	}
	Test* p2 = new Test(1024);
	//sp1指向了新的目标,这时值为1024的Test目标,有一个一般指针指向和智能
	//指针指向
	sp1.reset(p2);
	std::cout << "sp1 = " << sp1->getValue() << std::endl;
	//办法履行完,由于1024目标只有一个shared_ptr指向,当sp1毁掉时,引证变为0,
	//它会去毁掉目标。
	return 0;
}
//运转成果
析构 k = 100
sp1 is nullptr!
sp1 = 1024
析构 k = 1024

上述代码咱们用了一个Test类来愈加明晰地分辨目标析构的机遇,在每一步reset()时,咱们都应该留意该shared_ptr本来所指向的目标以及新指向的目标的引证计数,一同还验证了:目标的引证计数是其shared_ptr的个数,当一个同享目标的shared_ptr为0时,即便有一般指针还在指向它,也会被开释

  1. 不要混用一般指针和智能指针,这一点十分要害,也是在日常开发中十分简略犯错的当地。
//事务处理,当参数十分大时,咱们想运用指针来防止值仿制
void process(std::shared_ptr<Test> ptr) {
	//进行事务处理
	std::cout << "process ptr.Value=" << ptr->getValue() << std::endl;
	//...
}//脱离效果域时,ptr会被毁掉
int main()
{
	Test* p = new Test(100);
	//由于要传递指针指针类型,所以创立了一个暂时变量
	process(std::shared_ptr<Test>(p));
	//指向的目标现已被delete,这是一个空悬指针,比空指针更可怕
	std::cout << "after process p.Value=" << p->getValue() << std::endl;
	return 0;
}
//运转成果
process ptr.Value=100
析构 k = 100
after process p.Value=-572662307

能够发现经过process处理后,内存会被开释,这时p是指向了一个被delete了的内存,也便是空悬指针,这种状况十分可怕,由于代码在运转时都不会报错,排查起来也困难。

为了根绝这种状况,咱们应该定一个准则:假设接收了一般指针的一切权,就应该全权交由智能指针来办理,不该该再运用一般指针来拜访shared_ptr所指向的内存。所以正确运用如下:

int main()
{
	Test* p = new Test(100);
	//运用智能指针接收
	std::shared_ptr<Test> sp(p);
	//会产生仿制
	process(sp);
	//全权运用智能指针
	std::cout << "after process p.Value=" << sp->getValue() << std::endl;
	return 0;
}
//运转成果
process ptr.Value=100
after process p.Value=100
析构 k = 100

总的来说,这儿的不要混用的意思不是不能运用一般指针,而是能够运用一般指针来初始化智能指针,可是一旦内存一切权交由shared_ptr后,就不要再运用一般指针了。

  1. 不得不混用的状况还有一种,便是有些库函数要求传入一般指针,可是程序中运用的却是智能指针,这时能够运用get函数回来一个一般指针,指向智能指针办理的目标。

好像前面说的准则,尽量不要运用get()获取智能指针中的一般指针,假设非要运用的话,需求了解一个准则:不要运用运用get初始化另一个智能指针或为智能指针赋值

咱们先来看几种状况,需求把智能指针中的一般指针获取出来,然后传递给函数:

void process(std::shared_ptr<Test> ptr) {
	//进行事务处理
	std::cout << "process ptr.Value=" << ptr->getValue() << std::endl;
}//脱离效果域时,ptr会被毁掉
void process1(Test* ptr) {
	//进行事务处理
	ptr->addOne();
	std::cout << "process1 ptr.Value=" << ptr->getValue() << std::endl;
}//脱离效果域时,ptr不会毁掉
void process2(Test* ptr) {
	//进行事务处理
	ptr->addOne();
	std::cout << "process2 ptr.Value=" << ptr->getValue() << std::endl;
	//需求对ptr进行毁掉
	delete ptr;
}
int main()
{
	std::shared_ptr<Test> p1(new Test(100));
	Test* p2 = p1.get();
	//传递一般指针,且办法里不进行毁掉
	process1(p2);
	std::cout << "after process1 p1.Value=" << p1->getValue() << std::endl;
	//传递一般指针,且办法里进行毁掉
	process2(p2);
	//在办法process2中对内存目标进行了毁掉和开释,此时p1将指向delete了的内存
	//即空悬指针
	std::cout << "after process2 p1.Value=" << p1->getValue() << std::endl;
	return 0;
}

在上述代码中,咱们运用new创立了一个指针,给智能指针p1赋值,然后获取其办理的指针赋值给p2,在process1()办法中,咱们对目标进行加一操作,可是在process2()办法中,咱们对传入的指针进行了delete,这时所指向的内存就被开释了,这时智能指针p1也是一个空悬指针,获取其值是未界说的。

而关于空悬指针的值,不同编译器有不同处理计划,比方我运用msvc2017进行编译时,空悬指针的内容会是被delete之前的值,上述代码运转如下:

process1 ptr.Value=101
after process1 p1.Value=101
process2 ptr.Value=102
~Test k = 102
after process2 p1.Value=102

然后换成了gcc编译套件,空悬指针的内容是不确定的未知数,运转如下:

process1 ptr.Value=101
after process1 p1.Value=101
process2 ptr.Value=102
~Test k = 102
after process2 p1.Value=15105512

所以空悬指针的bug仍是很难发现的,因此在写代码时要时间留意。

然后咱们再了解为什么get()的指针不能用来初始化其他智能指针就很简略了解了,由于当多个独立的shared_ptr指向同一个目标时,引证计数是分开计数的,当其中一类的shared_ptr的引证计数为0时,就会开释目标内存,这时其他shared_ptr便是空悬指针了。

void process4(Test* ptr) {
	//创立智能指针
	std::shared_ptr<Test> p(ptr);
}//脱离效果域时,p会开释ptr指向的内存
int main()
{
	std::shared_ptr<Test> p1(new Test(100));
	Test* p2 = p1.get();
	process4(p2);
	//p1会变成空悬指针
	std::cout << "after process4 p1.Value=" << p1->getValue() << std::endl;
	return 0;
}
//msvc2017运转成果
~Test k = 100
after process4 p1.Value=100
~Test k = 100
//gcc运转成果
~Test k = 100
after process4 p1.Value=14777832

尤其是没有标准好的项目,很简略呈现这种问题,空悬指针排查还比较费事,所以切勿混用。

3. 智能指针和反常

反常处理程序在现代编程项目中十分常见,意图便是为了程序能在产生反常时流程能够持续履行,这就要求在产生反常时资源能够被正确地开释

3.1 运用智能指针保证资源安全开释

一个简略保证资源被开释的办法便是运用智能指针,关于在办法中运用动态内存的场景,咱们要求在办法完毕前能开释动态内存,假设不运用智能指针,当在newdelete之间呈现反常时,程序就会履行到反常处理分支,delete将永久不会履行

咱们仍是写个测验程序验证一下:

void testException() {
	try {
		//请求动态内存
		int* p = new int[1024 * 1024];
		//呈现反常
		throw std::runtime_error("error");
		//开释内存
		delete[] p;
	}
	catch (const std::exception&) {
		std::cout << "occur exception!" << std::endl;
	}
}
int main()
{
	while (true) {
		// 让程序休眠2秒
		std::this_thread::sleep_for(std::chrono::seconds(2));
		testException();
	}
	return 0;
}

由VS的调试器,发现程序的内存一直在增长:

万字长文详解C++智能指针
阐明当呈现反常时,delete无法被调用,咱们再运用智能指针版本测验一下:

void testException() {
	try {
		//运用智能指针
		std::unique_ptr<int[]> up = std::make_unique<int[]>(1024 * 1024);
		//呈现反常
		throw std::runtime_error("error");
		//无需开释动态内存
	}
	catch (const std::exception&) {
		std::cout << "occur exception!" << std::endl;
	}
}

把请求动态内存的操作改成unique_ptr智能指针时,当呈现反常,由于up栈内存目标,它必定会在办法履行完退出栈,一同开释内存,所以不会导致内存走漏:

万字长文详解C++智能指针

4. unique_ptr

大致搞清楚了shared_ptr的运用,unique_ptr运用就十分简略了,它和shared_ptr的差异便是办理所指向目标的办法,shared_ptr答应多个shared_ptr指向同一个目标,而unique_ptr独占目标,即只答应一个unique_ptr指向目标

这时就会有一个很值得考虑的问题,假设内存中有一个目标X,这时我能够界说多个一般指针和shared_ptr指向它,在函数和类之间传递来传递去处理事务。可是现在你说只能有一个指针指向目标,这显然是底层逻辑上无法约束的,由于彻底能够把X的地址赋值给多个智能指针目标,比方下面代码:

    int* p = new int(100);
    //多个unique_ptr指向同一个目标
    std::unique_ptr<int> up1(p);
    std::unique_ptr<int> up2(p);

所以这儿其实是一个资源所属权的规划问题,假设在需求上,这个目标不需求同享,则运用unique_ptr,不然运用shared_ptr。留意,是需求决议运用哪种智能指针,一旦挑选了某种智能指针,就需求恪守相关规矩。

4.1 根本介绍

关于unique_ptr的相似shared_ptr的运用就不细说了,这儿简略过一遍:

  • 相似make_shared()函数,能够运用make_unique()来创立unique_ptr目标,需求传入的参数符合模板类型的结构函数之一。
  • 能够结合new来创立unique_ptr目标,即接收目标。
  • 不存在引证计数了,由于是独占目标,所以在unique_ptr目标毁掉时,就会开释所指向的内存,默许也是调用delete函数。

4.2 开释所指向目标

正常来说,一个unique_ptr目标被毁掉时,其所指向的目标也就主动开释了,可是还能够经过其他办法来开释目标,如下:

  • up = nullptr,把一个类类型的目标置为nullptr,这阐明unique_ptr里面重载了=操作符,在这种状况下,会开释up指向的目标。
//memory.h源码
    unique_ptr& operator=(nullptr_t) noexcept
		{	// assign a null pointer
		reset();
		return (*this);
		}

这儿调用了reset()办法,即重置,在shared_ptr中有相似的用法,当没有传递参数时,即重置为空,能够了解为智能指针抛弃对一般指针的办理权,一同会开释所指向目标。

//示例代码
int main()
{
	std::unique_ptr<Test> up = std::make_unique<Test>(100);
	up = nullptr;
	std::cout << "up is nullptr";
	return 0;
}
//运转成果
~Test k = 100  //先调用析构,阐明不是由于up出效果域导致的析构
up is nullptr
  • up.reset(),把一个智能指针重置,能够重置为空指针或许其他目标,相似调用shared_ptr目标的重置办法,会削减本来指向目标的引证计数,添加新指向目标的引证计数,调用unique_ptr目标的重置办法,也会开释本来指向的目标,从头指向新的目标。
  • up.release()抛弃指针的控制权,回来裸指针,而且将up置为空。这儿和reset()的最大差异便是,回来的裸指针能够持续用,并不会开释所指向的目标

4.3 unique_ptr不支持一般的仿制和赋值

由于unique_ptr独占的特色,所以不答应进行一般的仿制和赋值,这儿的完结也十分简略,咱们只需求对其仿制结构函数和仿制赋值运算符进行约束即可:

//memory.h源码
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

这儿仍是阐明一点,是先有独占的需求,再挑选unique_ptr,然后再遵从它的规矩。

4.4 移动unique_ptr的目标

已然同一时间只能有一个unique_ptr独享目标,所以就有必要要有一种状况便是搬运控制权,把一个目标的控制权由一个unique_ptr搬运给另一个。有2种办法能够完结:

  • 运用release(),由于release()的职责便是抛弃对指针的控制权,所以咱们能够运用release()回来的裸指针来初始化另一个unique_ptr,从而完结控制权搬运:
int main()
{
	std::unique_ptr<Test> up = std::make_unique<Test>(100);
	std::unique_ptr<Test> up1(up.release());
	std::cout << "up1.Value=" << up1->getValue() << std::endl;
	return 0;
}
  • 运用move()move语义是C++11中新界说的一种操作,能够将一个目标的资源从一个目标移动到另一个目标中,从而防止不用要的仿制和从头资源分配,运用如下:
int main()
{
	std::unique_ptr<Test> up = std::make_unique<Test>(100);
	std::unique_ptr<Test> up1(std::move(up));
	std::cout << "up1.Value=" << up1->getValue() << std::endl;
	return 0;
}

4.5 unique_ptr在函数中运用

由于unique_ptr不能被仿制,所以把unique_ptr作为参数类型必定是报错的:

void testUniquePtr(std::unique_ptr<Test> ptr) {
	ptr->addOne();
}
int main(){
	std::unique_ptr<Test> up = std::make_unique<Test>(100);
	//直接编译不过,提示仿制结构函数是delete的
	testUniquePtr(up);
	return 0;
}

这儿能够把运用的函数的参数改成引证类型,则能够正常运用:

void testUniquePtr(std::unique_ptr<Test> &ptr) {
	ptr->addOne();
}

假设testUniquePtr的参数不是引证类型,而且假设外部不再需求运用该指针,彻底能够把该目标的控制权搬运,将其交由调用的函数办理,这时能够运用move()或许release():

void testUniquePtr(std::unique_ptr<Test> ptr) {
	ptr->addOne();
} //效果域完毕,将会开释ptr所指向的目标
int main(){
	std::unique_ptr<Test> up = std::make_unique<Test>(100);
	//将目标的唯一控制权交给了函数
	//函数完毕后,目标被开释
	testUniquePtr(std::unique_ptr<Test>(std::move(up)));
	//不能持续运用up了
	//std::cout << up->getValue();
	return 0;
}

这儿时间要记住一个unique_ptr所独占的目标,同一时间只有一个引证者。

当把unique_ptr作为参数回来时,其实是调用的其move()办法,一同还能够把回来的unique_ptr转化为shared_ptr运用:

//运用move操作,而非仿制结构函数
std::unique_ptr<Test> test(int i) {
	return std::make_unique<Test>(i);
}
int main(){
	//运用move给up赋值,而非仿制结构函数
	std::unique_ptr<Test> up = test(100);
	//能够把一个目标的控制权交由shared_ptr来办理
	std::shared_ptr<Test> sp = test(100);
	return 0;
}

至于为什么这儿能够用=来进行初始化,由于并非调用仿制结构函数,而是调用移动结构函数。

4.6 为什么优先选用unique_ptr

从前面剖析咱们可知,当资源不需求有多个所属权时能够运用unique_ptr来代替裸指针,这儿给出2个根本原因:

  • 更安全,防止内存走漏。最常见的运用场景便是前面的反常部分,运用栈内存的特功能够保证资源被收回。
  • 相比于shared_ptr,防止更大的开支。由于它没有引证计数和原子操作等,所以和运用裸指针所耗费的资源几乎是相同的。许多开发者为了便利,都直接运用shared_ptr,这是不可取的。

5. weak_ptr

weak_ptr是一种特殊的智能指针类型,熟悉Java开发的同学应该知道,Java的垃圾收回机制在很早期的时分运用的是引证计数法,可是后来迅速被筛选而选用可达性剖析法,被筛选的原因便是无法处理循环引证。而C++的shared_ptr其实便是小型的垃圾收回机制,其所运用的引证计数法也存在相同问题,所以就引入了weak_ptr来处理该问题。

5.1 简略介绍

weak_ptr是一种不控制所指向目标生存期的智能指针,它指向由一个shared_ptr办理的目标。所以初始化一个weak_ptr有必要需求一个shared_ptr,而且将一个weak_ptr绑定到一个shared_ptr不会改动shared_ptr的引证计数

weak_ptr的最大特色是:一旦最终一个指向目标的shared_ptr被毁掉,该目标就会被毁掉,即便有weak_ptr指向该目标

这儿也就会呈现一种状况,即weak_ptr还指向着目标,可是该目标现已被毁掉了,所以经过weak_ptr拜访其所指向的目标,不能直接拜访

如下几个API便是上述描述的完结:

API 效果
weak_ptr<T> w 能够指向类型为T的空的weak_ptr,和其他智能指针相同,在创立目标时,需求类型模板参数。
weak_ptr<T> w(sp) 运用shared_ptr目标来初始化w,即wp都指向同一个目标。
w = p p能够是一个shared_ptr或许weak_ptr目标,赋值后,wp同享一个目标。
w.reset() w置空
w.use_count() w同享目标的shared_ptr的数量。
w.expired() expired为过期、失效、不再有用的意思,即阐明这个weak_ptr是否失效了,当use_count()为0时回来true,不然回来false
w.lock 假设expiredtrue,即现已失效,回来一个空的shared_ptr,不然回来一个指向wshared_ptr

其实了解起来十分简略,下面是简略运用示例:

int main(){
	std::shared_ptr<Test> sp = std::make_shared<Test>(100);
	//w和sp指向一个目标
	std::weak_ptr<Test> w(sp);
	std::cout << "共有" << w.use_count() << "个shared_ptr指向同享目标" << std::endl;
	//运用weak_ptr拜访目标
	if (std::shared_ptr<Test> sp1 = w.lock()) {
		//当shared_ptr不指向空时,能够作为if判别条件,回来true
		std::cout << "w.Value=" << sp1->getValue() << std::endl;
	}
	return 0;
}

在上述代码中,咱们直接判别w.loack()回来的shared_ptr示例,这是前面咱们说过的shared_ptr的特性,重载了=操作符。

5.2 处理循环引证

话不多说,weak_ptr的真实效果是处理shared_ptr的循环引证问题,测验代码如下:

//界说AClass,具有BClass类型指针
class AClass {
public:
	std::shared_ptr<BClass> spb;
	~AClass() {
		std::cout << "~AClass" << std::endl;
	}
};
//界说BClass,具有AClass类型的指针
class BClass {
public:
	std::shared_ptr<AClass> spa;
	~BClass() {
		std::cout << "~BClass" << std::endl;
	}
};
void testCircularRef() {
        //部分变量pa和pb
	std::shared_ptr<AClass> pa = std::make_shared<AClass>();
	std::shared_ptr<BClass> pb = std::make_shared<BClass>();
        //pa内部指向pb
	pa->spb = pb;
        //pb内部履行pa
	pb->spa = pa;
} 
//1. 当办法完毕时,部分变量的毁掉是按照其创立次序的相反次序毁掉,即pb先毁掉,再pa毁掉。
//2. pb毁掉时会调用pb的析构函数,即shared_ptr的析构函数,它会检测到它所指向的目标有2个引证者,
//即pb自己和pa所指向目标的数据成员spb。
//依据shared_ptr的规矩,pb毁掉时,不会去开释pb所指向的内存。
//3. 这时轮到pa被毁掉,相同是调用shared_ptr的析构函数,它会检测到它所指向的目标也有2个引证者,
//即pa自己和刚刚没有被开释掉的pb所指向的目标的数据成员spa。
//相同依据规矩,pa自己被毁掉,可是其所指向的目标不会被开释
int main(){
        //会导致循环引证,2个堆内存目标无法被开释
	testCircularRef();
	return 0;
}

上述代码中的注释需求细心琢磨,不难了解为什么循环引证时,会导致内存无法开释。

而处理上述问题的的办法便是运用weak_ptr,由于互相引证都是运用的强引证,假设把其中一个换成弱引证即可处理,比方把AClass中的数据成员进行修改:

//界说AClass,具有BClass类型的weak_ptr指针
class AClass {
public:
    //改动在这儿
	std::weak_ptr<BClass> wb;
	~AClass() {
		std::cout << "~AClass" << std::endl;
	}
};
//界说BClass,具有AClass类型的指针
class BClass {
public:
	std::shared_ptr<AClass> spa;
	~BClass() {
		std::cout << "~BClass" << std::endl;
	}
};
void testCircularRef() {
	std::shared_ptr<AClass> pa = std::make_shared<AClass>();
	std::shared_ptr<BClass> pb = std::make_shared<BClass>();
        //pa内部的weak_ptr指向pb
	pa->wb = pb;
        //pb内部的依旧是强引证
	pb->spa = pa;
}
//1. 依据部分变量毁掉次序,pb先毁掉,pa再毁掉。
//2. pb毁掉时,调用pb即shared_ptr的析构函数,会发现pb所指向的目标有2个引证者,一个是shared_ptr类型即自己,另一个是weak_ptr类型。
//依据shared_ptr和weak_ptr的特色,这时会开释pb所指向的目标。
//3. pa毁掉时,调用pa即shared_ptr的析构函数,会发现pa所指向的目标有1个引证者,即shared_ptr类型的自己,
//本来pb所指向的目标中的spa也指向它,可是pb所指向的目标现已被开释,故不存在。
//依据shared_ptr的特色,会正常开释pa所指向的目标。
int main(){
    //2个堆内存目标能够正常被开释
	testCircularRef();
	return 0;
}

6. 是否应该运用智能指针彻底替换裸指针?

许多开发者都有一个这样的观念,已然C++11开端供给了智能指针,那么就应该彻底运用智能指针,把项目中的裸指针都改形成智能指针,这样就能够防止内存走漏了,真的是这样吗?

先说根本观念,即便全部运用智能指针也防止不了内存走漏,比方运用shared_ptr,就有或许在不用要的当地比方大局目标中持有了一个目标的shared_ptr引证,这个目标就无法被收回,这是运用上简略犯的过错。

其次,关于智能指针是否彻底能够替换裸指针这个问题,我的观念是:能够,但不满是。

6.1 清晰资源的一切权(ownership)

在文章刚开端就说了,shared_ptrunique_ptr的差异便是对资源的一切权不相同,前者能够有多个引证者同享,而后者是独享。所以,当要运用指针时,应该先清晰需求,这个指针所指向的目标是否需求同享,即先要清晰资源的一切权

假设资源具有者要把一个目标借给他人用一下,用完就归还,办法大致如下:

void borrow(??? res);

这儿怎么界说办法参数类型,有3种挑选:

  1. 首先是const shared_ptr<T>,这种必定是先pass的,由于从需求上来说,没有同享的必要,运用shared_ptr只会耗费功能。
  2. 其次运用const unique_ptr<T>&,依据需求,这儿有必要运用引证,而不能运用const unique_ptr<T>,原因在前面说过。关于运用const unique_ptr<T>&的状况,这种是可行的,也是比较主张的,或许呈现的问题便是假设这是一个第三方函数,需求给他人用,他人或许是运用的是一般指针,这种状况就需求转化。
  3. 最终便是直接运用T*,由于从资源一切权的视点来说,这个办法只是运用一下资源罢了,它并不需求独占或许同享资源,而且界说为T*也愈加便利他人运用。

上述观念,也是个人愚见,没有肯定之分,也是许多人的观念,可是不变的思想是:需求决议技术细节先看资源的一切权,再决议运用什么智能指针。假设只是简略调用,能够不用智能指针

6.2 少运用指针

网上有个段子,便是越是厉害的C++大佬,越少运用C++。其实关于指针也是相似,作为一把双刃剑,用好了功能大大地进步,用不好不只会导致内存走漏还有添加代码复杂度。所以,有个观念是少用指针。

  1. 优先考虑引证和值,特别是函数之间,根本上没有必要传指针。
  2. 再考虑运用unique_ptr代替类的成员不能运用引证的状况下运用指针。
  3. 一切资源都应该运用RAII来办理尽量一个类办理资源。关于一个类办理不了的状况,再运用shared_ptrweak_ptr

经过少运用指针以及合理地运用指针,能够削减绝大多数指针的运用场景。

6.3 假设或许,尽或许运用智能指针

这个标题或许和前面说的有收支,可是这是我从实践项目中踩坑得到的观念。不要滥用智能指针,假设用,就清晰用法,做到全部运用,根绝newdelete,运用make_shared/unique代替。理由如下:

  • 关于线程编程来说,资源同享运用手动开释十分费事,主张运用智能指针
  • 反常处理愈加便利,程序可靠性更好。
  • 运用智能指针能够让开发者愈加清晰资源的一切权
  • 削减忘记delete或许不合时宜的delete形成的问题。

总的来说,要理性看待C++的智能指针,在项目中要尽或许少运用指针,假设运用则有必要清晰资源的一切权,再采纳合适的智能指针,进行合理地运用。

总结

智能指针不是C++处理动态内存问题的万能钥匙,清晰资源所属权,合理且正确地运用智能指针才是根本意图。

参考:<<C++ Primer>>、<<Effective C++>>。