前语
C++程序的内存能够分为3种,分别为静态内存、栈内存和堆,下表简略概述:
内存类型 | 效果 | 生命周期 |
---|---|---|
静态内存 | 用来保存部分static 目标、类static 数据成员以及界说在任何函数之外的变量。 |
由编译器主动创立和毁掉,static 目标在运用之前分配,在程序完毕时毁掉。 |
栈内存 | 用来保存界说在函数内的非static 目标。 |
由编译器主动创立和毁掉,栈目标仅在其界说的程序块运转时才存在。 |
堆内存(自由空间) | 用来存储动态分配的目标。 | 动态目标的生命周期由程序来控制,也便是说,当动态目标不再运用时,咱们的代码有必要显现地毁掉他们。 |
在C++中,动态内存的办理是经过一对运算符来完结的:
-
new
:在动态内存中为目标分配空间,而且回来一个指向该目标的指针,咱们能够挑选对目标进行初始化; -
delete
:承受一个动态目标的指针,毁掉该目标,而且开释与之相关的内存。
动态内存的运用很简略出问题,由于保证在正确的时间开释内存是极其困难的:
- 有时咱们会忘记开释内存,在这种状况下会产生内存走漏;
- 有时在尚有指针引证内存的状况下咱们就开释了它,这种状况下会产生引证不合法内存的指针。
为了更简略一同也更安全地运用动态内存,新的标准供给了两种智能指针类型来办理动态目标,智能指针的行为相似常规指针,重要的差异是它负责主动开释所指向的目标。新标准库供给的这两种智能指针的差异在于办理底层指针的办法:
-
shared_ptr
:答应多个指针指向同一个目标; -
unique_ptr
:”独占“所指向的目标。
标准库一同还界说了一个名为weak_ptr
的伴随类,它是一种弱引证,指向shared_ptr
所办理的目标。
清晰了几种类型的内存的特色,咱们知道其难点便是堆内存的开释,也便是指向堆内存的一般指针不知道何时该开释。这时咱们能够转化一下思路,运用栈内存的特色(主动请求和开释)以及类成员变量在类毁掉时能够主动毁掉这一特性,咱们把一般指针给封装一层成类,这样一般指针目标就能够变成类类型的栈内目标,以及类类型的数据成员。
其实这也便是标准库中的做法,运用智能指针就让咱们不用再在意动态内存的手动毁掉了。
正文
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
是类类型,而且有所指向目标的引证计数,所以其办理所指向目标是经过这两者结合来完结的:
-
shared_ptr
的析构函数会递减它所指向的目标的引证计数,一同给一个shared_ptr
赋予新值时也会递减。 -
当引证计数变为0时,
shared_ptr
会毁掉所指向的目标,一同开释内存。留意这儿一般是析构函数来做的,可是也不完满是,比方前一段的实例代码中,r
所指向的目标42
,当r
指向其他目标时,这个42
就再也没有shared_ptr
指向它了,所以这时仍是由r
会去毁掉42
以及开释其内存。
所以结合这2点,以及递加和递减引证计数的原理,在平时运用时咱们直接运用shared_ptr
类型进行值传递,这样就能够完结主动开释内存的功用了。
1.4 运用动态资源的场景
在程序开发中,常常有如下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. new
和delete
虽然现在C++11后就引荐运用智能指针,可是大量的老项目以及各种状况,让开发者有时不得不运用一般指针,还有便是了解一般指针的各种用法,有利于了解智能指针的完结以及处理一些常见过错。
2.1 内存耗尽
简略的怎么运用new
和delete
就不说了,这儿先说一种内存耗尽的状况。当程序呈现内存走漏,有限的堆内存就有或许被耗尽,这时现已没有足够的空间来分配动态目标了,这时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
的用法就不说了,现在来说几点需求特别留意的。
-
delete
接纳的指针有必要是指向动态分配的内存或许是一个空指针。 -
开释一块非
new
分配的内存,其行为是未界说的。
int i = 0;
//pi1是指向栈内存的指针
int* pi1 = &i;
//pi2是空指针
int* pi2 = nullptr;
//delete一个指向非动态内存的指针是严重过错!!!
delete pi1;
//能够delete一个空指针
delete pi2;
在上面代码中,pi1
是指向非动态内存的指针,编译器是无法判别传递给delete
的指针详细指向的是什么,所以能够编译经过,运转会报错,这种潜在的问题要时间留意。
- 将相同的指针值开释屡次,其行为是未界说的。
int* pi3 = new int(20);
delete pi3;
//delete一个指针屡次,是严重过错!!
delete pi3;
上面这种状况相同编译器无法辨认,只会在运转时才报错,需求时间留意。这儿咱们能够想一下,已然delete
一个空指针是能够的,可是为什么不能delete
一个指针2次呢?这是由于delete
后的指针值是一个无效值,但不是nullptr
。
- 被
delete
之后的指针被称为空悬指针(dangling pointer
),即指向一块曾经保存数据目标但现在现已无效的内存的指针,对空悬指针再次调用delete
是严重过错。 - 假设有事务需求,需求再次运用被
delete
后的指针,这时咱们能够在delete
之后对指针赋予nullptr
,这样在下次再运用时不论是误操作delete
仍是判空再赋予新值,都能够正常运用。 - 关于多个一般指针指向同一块内存的状况,操作
delete
要十分留意,防止呈现一个指针现已开释了内存,其他指针再次运用的状况,导致引证不合法内存。处理这种状况最好的办法便是运用shared_ptr
。
2.3 shared_ptr
和new
结合运用
前面说了创立shared_ptr
目标最佳计划是运用make_shared
办法,可是更多的时分咱们不得不好一般指针打交道,标准库也供给了shared_ptr
和new
结合运用的各种场景。
- 能够运用
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
的一般指针也有必要是指向动态内存的。
- 已然智能指针是指针的封装,所以它必定不能一直和一个指针绑定,所以标准库还界说了一些改动
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时,即便有一般指针还在指向它,也会被开释。
- 不要混用一般指针和智能指针,这一点十分要害,也是在日常开发中十分简略犯错的当地。
//事务处理,当参数十分大时,咱们想运用指针来防止值仿制
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
后,就不要再运用一般指针了。
- 不得不混用的状况还有一种,便是有些库函数要求传入一般指针,可是程序中运用的却是智能指针,这时能够运用
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 运用智能指针保证资源安全开释
一个简略保证资源被开释的办法便是运用智能指针,关于在办法中运用动态内存的场景,咱们要求在办法完毕前能开释动态内存,假设不运用智能指针,当在new
和delete
之间呈现反常时,程序就会履行到反常处理分支,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的调试器,发现程序的内存一直在增长:
阐明当呈现反常时,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
是栈内存目标,它必定会在办法履行完退出栈,一同开释内存,所以不会导致内存走漏:
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 ,即w 和p 都指向同一个目标。 |
w = p |
p 能够是一个shared_ptr 或许weak_ptr 目标,赋值后,w 和p 同享一个目标。 |
w.reset() |
将w 置空 |
w.use_count() |
与w 同享目标的shared_ptr 的数量。 |
w.expired() |
expired 为过期、失效、不再有用的意思,即阐明这个weak_ptr 是否失效了,当use_count() 为0时回来true ,不然回来false 。 |
w.lock |
假设expired 为true ,即现已失效,回来一个空的shared_ptr ,不然回来一个指向w 的shared_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_ptr
和unique_ptr
的差异便是对资源的一切权不相同,前者能够有多个引证者同享,而后者是独享。所以,当要运用指针时,应该先清晰需求,这个指针所指向的目标是否需求同享,即先要清晰资源的一切权。
假设资源具有者要把一个目标借给他人用一下,用完就归还,办法大致如下:
void borrow(??? res);
这儿怎么界说办法参数类型,有3种挑选:
- 首先是
const shared_ptr<T>
,这种必定是先pass
的,由于从需求上来说,没有同享的必要,运用shared_ptr
只会耗费功能。 - 其次运用
const unique_ptr<T>&
,依据需求,这儿有必要运用引证,而不能运用const unique_ptr<T>
,原因在前面说过。关于运用const unique_ptr<T>&
的状况,这种是可行的,也是比较主张的,或许呈现的问题便是假设这是一个第三方函数,需求给他人用,他人或许是运用的是一般指针,这种状况就需求转化。 - 最终便是直接运用
T*
,由于从资源一切权的视点来说,这个办法只是运用一下资源罢了,它并不需求独占或许同享资源,而且界说为T*
也愈加便利他人运用。
上述观念,也是个人愚见,没有肯定之分,也是许多人的观念,可是不变的思想是:需求决议技术细节,先看资源的一切权,再决议运用什么智能指针。假设只是简略调用,能够不用智能指针。
6.2 少运用指针
网上有个段子,便是越是厉害的C++大佬,越少运用C++。其实关于指针也是相似,作为一把双刃剑,用好了功能大大地进步,用不好不只会导致内存走漏还有添加代码复杂度。所以,有个观念是少用指针。
- 优先考虑引证和值,特别是函数之间,根本上没有必要传指针。
- 再考虑运用
unique_ptr
,代替类的成员不能运用引证的状况下运用指针。 -
一切资源都应该运用RAII来办理,尽量一个类办理资源。关于一个类办理不了的状况,再运用
shared_ptr
和weak_ptr
。
经过少运用指针以及合理地运用指针,能够削减绝大多数指针的运用场景。
6.3 假设或许,尽或许运用智能指针
这个标题或许和前面说的有收支,可是这是我从实践项目中踩坑得到的观念。不要滥用智能指针,假设用,就清晰用法,做到全部运用,根绝new
和delete
,运用make_shared/unique
代替。理由如下:
- 关于多线程编程来说,资源同享运用手动开释十分费事,主张运用智能指针。
- 反常处理愈加便利,程序可靠性更好。
- 运用智能指针能够让开发者愈加清晰资源的一切权。
- 削减忘记
delete
或许不合时宜的delete
形成的问题。
总的来说,要理性看待C++的智能指针,在项目中要尽或许少运用指针,假设运用则有必要清晰资源的一切权,再采纳合适的智能指针,进行合理地运用。
总结
智能指针不是C++处理动态内存问题的万能钥匙,清晰资源所属权,合理且正确地运用智能指针才是根本意图。
参考:<<C++ Primer>>、<<Effective C++>>。