[C++11新特性]智能指针详解

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

[C++11新特性]智能指针详解
C++ 程序设计中使⽤堆内存是⾮常频繁的操作,堆内存的申请和释放都由程序员⾃⼰管理。

但使⽤普通指针,容易造成内存泄露(忘记释放)、⼆次释放、程序发⽣异常时内存泄露等问题等。

所有 C++11 就引⼊了智能指针。

⼀、原始指针容易发⽣内存泄漏
C 语⾔中最常使⽤的是函数分配内存,函数释放内存,⽽ C++ 中对应的是、关键字。

只是分配了内存,⽽则更进⼀步,不仅分配了内存,还调⽤了构造函数进⾏初始化。

使⽤⽰例:
malloc()free()new delete malloc()new int main()
{
// malloc 返回值是 void* int* argC = (int*)malloc(sizeof(int)); free(argC);
char *age = new int(25); // 做了两件事情 1.分配内存 2.初始化
delete age;
}
和必须成对出现,有时候是不⼩⼼忘记了,有时候则是很难判断在这个地⽅⾃⼰是不是该,这个和资源的⽣命周期有关,这个资源是属于我这个类管理的还是由另外⼀个类管理的(其它类可能要使⽤),如果是别⼈管理的就由别⼈。

new delete delete delete delete 如果需要⾃⼰管理内存的话,最好显⽰的将⾃⼰的资源传递进去,这样的话,就能知道是该资源确实应该由⾃⼰来管理。

char *getName(char* v, size_t bufferSize) {
//do something
return v;
}
上⾯还是⼩问题,⾃⼰⼩⼼⼀点,再仔细看看⽂档,还是有机会避免这些情况的。

但是在 C++ 引⼊异常的概念之后,程序的控制流就发⽣了根本性的改变,在写了 delete 的时候还是有可能发⽣内存泄漏。

如下例:
void badThing(){
throw 1;// 抛出⼀个异常
}
void test() { char* a = new char[1000];
badThing();
// do something
delete[] a;}int main() { try { test();
}
catch (int i){
cout << "error happened " << i << endl;
}
}
上⾯的和是成对出现的,但是程序在中间的时候抛出了异常,由于没有⽴即捕获,程序从这⾥退出了,并没有执⾏到,内存泄漏还是发⽣了。

new delete delete ⼆、使⽤构造函数和析构函数解决内存泄漏C++ 中的构造函数和析构函数⼗分强⼤,可以使⽤构造和析构解决上⾯的内存泄漏问题,⽐如:
class SafeIntPointer {
public: explicit SafeIntPointer(int v) : m_value(new int(v)) { }
~SafeIntPointer() { delete m_value;
cout << "~SafeIntPointer" << endl; } int get() { return *m_value; }
private:
int* m_value;
};
void badThing(){
throw 1;// 抛出⼀个异常
}
void test() {
SafeIntPointer a(5);
badThing();
}
int main() {
try {
test();
}
catch (int i){ cout << "error happened " << i << endl;
}
}
// 结果
// ~SafeIntPointer
// error happened 1
可以看到,就算发⽣了异常,也能够保证析构函数成功执⾏!要等到所有⼈都不再使⽤的时候才能释放掉,对于这种问题,就需要对上⾯的增加⼀个引⽤计数,如下:
这⾥的例⼦是这个资源只有⼀个⼈使⽤,我不⽤了就将它释放掉。

但还有种情况,⼀份
资源被很多⼈共同使⽤,SafeIntPointer class SafeIntPointer {
public: explicit SafeIntPointer(int v) : m_value(new int(v)), m_used(new int(1)) { }
~SafeIntPointer() {
cout << "~SafeIntPointer" << endl;
(*m_used) --; // 引⽤计数减1
if(*m_used <= 0){
delete m_used;
delete m_value;
cout << "real delete resources" << endl; }
}
SafeIntPointer(const SafeIntPointer& other) {
m_used = other.m_used;
m_value = other.m_value;
(*m_used)++; // 引⽤计数加1 }
SafeIntPointer& operator= (const SafeIntPointer& other) {
if (this == &other) // 避免⾃我赋值!!
return *this;
m_used = other.m_used;
m_value = other.m_value;
(*m_used)++; // 引⽤计数加1 return *this;
}
int get() { return *m_value; }
int getRefCount() {
return *m_used;
}
private:
int* m_used; // 引⽤计数
int* m_value;
};
int main() {
SafeIntPointer a(5);
cout << "ref count = " << a.getRefCount() << endl;
SafeIntPointer b = a;
cout << "ref count = " << a.getRefCount() << endl;
SafeIntPointer c = b;
cout << "ref count = " << a.getRefCount() << endl;}/*ref count = 1ref count = 2ref count = 3~SafeIntPointer
~SafeIntPointer
~SafeIntPointer
real delete resources
*/
可以看到每⼀次赋值,引⽤计数都加⼀,最后每次析构⼀次后引⽤计数减⼀,知道引⽤计数为 0,才真正释放资源。

要写出⼀个正确的管理资源的包
装类还是蛮难的,⽐如上⾯那个例⼦就不是线程安全的,只能属于⼀个玩具,在实际⼯程中简直没法⽤。

所以 C++11 中引⼊了智能指针(Smart Pointer ),这使得智能指针实质是⼀个对象,⾏为表现的却像⼀个指针。

它利⽤了⼀种叫做 RAII (资源获取即初始化)的技术将普通的指针封装为⼀个栈对象。

当栈对象
的⽣存周期结束后,会在析构函数中释放掉申请的内存,从⽽防⽌内存泄漏。

智能指针主要分为、和三种,使⽤时需要引⽤头⽂件。

C++98 中还有,基本被淘汰了,不推荐使
⽤。

⽽ C++11 中和都是参考库实现的。

shared_ptr unique_ptr weak_ptr <memory>auto_ptr shared_ptr weak_ptr boost 三、shared_ptr 共享的智能指针
3.1 shared_ptr 的初始化与智能指针⼀样,make_shared 也定义在头⽂件 memory 中。

最安全的分配和使⽤动态内存的⽅法是调⽤⼀个名为 make_shared 的标准库函数。

此函数在动态内存中分配⼀个对象并初始化它,返回指向此对象
的 shared_ptr 。

// 指向⼀个值为42的int 的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4 指向⼀个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
// p5指向⼀个值初始化的int
shared_ptr<int> p5 = make_shared<int>();
我们还可以⽤ new 返回的指针来初始化智能指针,不过接受指针参数的智能指针构造函数是 explicit 的。

因此,我们不能将⼀个内置指针隐式转换为
⼀个智能指针,必须使⽤直接初始化形式来初始化⼀个智能指针:
shared_ptr<int> pi = 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>
}
3.2 shared_ptr 的基本使⽤
的基本使⽤很简单,看⼏个例⼦就明⽩了:
std::shared_ptr #include <memory>
#include <iostream>class Test
{
public:
Test()
{
std::cout << "Test()" << std::endl;
}
~Test()
{
std::cout << "~Test()" << std::endl;
}};
int main()
{
std::shared_ptr<Test> p1 = std::make_shared<Test>();
std::cout << "1 ref:" << e_count() << std::endl; {
std::shared_ptr<Test> p2 = p1;
std::cout << "2 ref:" << e_count() << std::endl;
}
std::cout << "3 ref:" << e_count() << std::endl;
return 0;
}
输出如下:
Test()
1 ref:1
2 ref:2
3 ref:1
~Test()针对代码解读如下:
⾥⾯调⽤了 new 操作符分配内存;
std::make_shared 第⼆个之所以显⽰为 2,是因为增加了引⽤对象 p2,⽽随着⼤括号的结束,p2 的作⽤域结束,所以 p1 的引⽤计数变回 1,⽽随
着 main 函数的结束,p1 的作⽤域结束,此时检测到计数为 1,那就会在销毁 p1 的同时,调⽤ p1 的析构函数 delete 掉之前分配的内存空间;
e_count()3.3 shared_ptr 常⽤操作下⾯列出了独有的操作:
shared_ptr 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() // 若e_count()为1,返回true ;否则返回false
e_count() // 返回与p 共享对象的智能指针数量;可能很慢,主要⽤于调试
下⾯介绍⼀些改变的其他⽅法:
shared_ptr p.reset () //若p 是唯⼀指向其对象的shared_ptr ,reset 会释放此对象。

p.reset(q) //若传递了可选的参数内置指针q ,会令P 指向q ,否则会将P 置为空。

p.reset(q, d) //若还传递了参数d,将会调⽤d ⽽不是delete 来释放q
四、weak_ptr 弱引⽤的智能指针
4.1 shared_ptr 相互引⽤会有什么后果?的⼀个最⼤的陷阱是循环引⽤,循环引⽤会导致堆内存⽆法正确释放,导致内存泄漏。

看下⾯的例⼦:shared_ptr #include <iostream>
#include <memory>
class Parent; // Parent 类的前置声明class Child {
public: Child() { std::cout << "hello child" << std::endl; }
~Child() { std::cout << "bye child" << std::endl; }
std::shared_ptr<Parent> father;
};class Parent {
public: Parent() { std::cout << "hello Parent" << std::endl; } ~Parent() { std::cout << "bye parent" << std::endl; } std::shared_ptr<Child> son;
};
void testParentAndChild() {
}int main() {
std::shared_ptr<Parent> parent(new Parent()); // 1 资源A
std::shared_ptr<Child> child(new Child()); // 2 资源B
parent->son = child; // 3 e_count() == 2 and e_count() == 1
child->father = parent; // 4 e_count() == 2 and e_count() == 2 return 0;
}
/*
输出:hello Parent
hello child
*/
很惊讶的发现,⽤了管理资源,没有调⽤ Parent 和 Child 的析构函数,表⽰资源最后还是没有释放!内存泄漏还是发⽣了。

shared_ptr 分析:执⾏编号的语句时,构造了⼀个共享智能指针,称呼它管理的资源叫做资源(产⽣的对象)吧, 语句构造了⼀个共享智能指针,管理资源(产⽣的对象),此时资源和的引⽤计数都是,因为只有个智能指针管理它们,执⾏到了语句的时候,是⼀个智能指针的赋值操作,资源的引⽤计数变为了,同理,执⾏完语句,资源的引⽤计数也变成了。

1p A new Parent()2c B new Child()A B 113B 24A 2出了函数作⽤域时,由于析构和构造的顺序是相反的,会先析构共享智能指针,资源的引⽤计数就变成了;接下来继续析构共享智能指针,资源的引⽤计数也变成了。

由于资源和的引⽤计数都不为,说明还有共享智能指针在使⽤着它们,所以不会调⽤资源的析构函数!c B 1p A 1A B 1这种情况就是个死循环,如果资源的引⽤计数想变成,则必须资源先析构掉(从⽽析构掉内部管理资源的共享智能指针),资源的引⽤计数想变为,⼜得依赖资源的析构,这样就陷⼊了⼀个死循环。

A 0B A B 0A 4.2 weak_ptr 如何解决相互引⽤的问题
要想解决上⾯循环引⽤的问题,只能引⼊新的智能指针。

有什么特点呢?与最⼤的差别是在赋值的时候,不会引起智能指针计数增加。

std::weak_ptr std::weak_ptr std::shared_ptr 被设计为与共同⼯作,可以从⼀个或者另⼀个对象构造,获得资源的观测权。

但没有共享资源,它的构造不会引起指针引⽤计数的增加。

weak_ptr shared_ptr shared_ptr weak_ptr weak_ptr 同样,在析构时也不会导致引⽤计数的减少,它只是⼀个静静地观察者。

没有重载和,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。

weak_ptr weak_ptr operator*->如要操作资源,则必须使⽤⼀个⾮常重要的成员函数从被观测的获得⼀个可⽤的对象,从⽽操作资源。

lock()shared_ptr shared_ptr 当我们创建⼀个时,要⽤⼀个来初始化它:
weak_ptr shared_ptr auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp 弱共享p; p 的引⽤计数未改变
我们在上⾯的代码基础上使⽤进⾏修改,如下:
std::weak_ptr #include <iostream>
#include <memory>
class Parent; // Parent 类的前置声明
class Child {
public:
Child() { std::cout << "hello child" << std::endl; }
~Child() { std::cout << "bye child" << std::endl; }
// 测试函数
void testWork()
{
std::cout << "testWork()" << std::endl;
}
std::weak_ptr<Parent> father;
};
class Parent {
public:
Parent() { std::cout << "hello Parent" << std::endl; }
~Parent() { std::cout << "bye parent" << std::endl; }
std::weak_ptr<Child> son;
};
void testParentAndChild() {
}
int main() {
std::shared_ptr<Parent> parent(new Parent());
std::shared_ptr<Child> child(new Child());
parent->son = child;
child->father = parent;
std::cout << "parent_ref:" << e_count() << std::endl;
std::cout << "child_ref:" << e_count() << std::endl;
// 把std::weak_ptr类型转换成std::shared_ptr类型,以调⽤内部成员函数
std::shared_ptr<Child> tmp = parent.get()->son.lock();
tmp->testWork();
std::cout << "tmp_ref:" << e_count() << std::endl;
return 0;
}
/*
输出:
hello Parent
hello child
parent_ref:1
child_ref:1
testWork()
tmp_ref:2
bye child
bye parent
*/
由以上代码运⾏结果我们可以看到:
所有的对象最后都能正常释放,不会存在上⼀个例⼦中的内存没有释放的问题;
std::weak_ptr
parent 和 child 在 main 函数中退出前,引⽤计数均为 1,也就是说,对的相互引⽤,不会导致计数的增加。

4.3 weak_ptr常⽤操作
weak_ptr<T> w; // 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(shared_ptr p); // 与p指向相同对象的weak_ptr, T必须能转换为sp指向的类型
w = p; // p可以是shared_ptr或者weak_ptr,赋值后w和p共享对象
w.reset(); // weak_ptr置为空
e_count(); // 与w共享对象的shared_ptr的计数
w.expired(); // e_count()为0则返回true,否则返回false
w.lock(); // w.expired()为true,返回空的shared_ptr;否则返回指向w的shared_ptr
五、unique_ptr独占的智能指针
5.1 unique_ptr的基本使⽤
unique_ptr shared_ptr
相对于其他两个智能指针更加简单,它和使⽤差不多,但是功能更为单⼀,它是⼀个独占型的智能指针,不允许其他的智能指针共享其内部的指针,更像原⽣的指针(但更为安全,能够⾃⼰释放内存)。

不允许赋值和拷贝操作,只能够移动。

std::unique_ptr<int> ptr1(new int(0));
std::unique_ptr<int> ptr2 = ptr1; // 错误,不能复制
std::unique_ptr<int> ptr3 = std::move(ptr1); // 可以移动
std::make_shared std::unique_ptr std::make_unique
在 C++11 中,没有类似的初始化⽅法,但是在 C++14 中,对于引⼊了⽅法进⾏初始化。

#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<std::string> ptr1(new std::string("unique_ptr")); std::cout << "ptr1 is " << *ptr1 << std::endl;
std::unique_ptr<std::string> ptr2 = std::make_unique<std::string>("make_unique init!");
std::cout << "ptr2 is " << *ptr2 << std::endl; return 0;}
/*
输出:
ptr1 is unique_ptr
ptr2 is make_unique init!
*/
5.2 unique_ptr 常⽤操作下⾯列出了特有的操作。

unique_ptr unique_ptr<T> u1 // 空unique_ptr ,可以指向类型为T 的对象。

u1会使⽤delete 来释放它的指针unique_ptr<T, D> u2 // u2会使⽤⼀个类型为D 的可调⽤对象来释放它的指针
unique_ptr<T, D> u(d) // 空unique_ptr ,指向类型为T 的对象,⽤类型为D 的对象d 替代delete
u = nullptr // 释放u 指向的对象,将u 置为空
u.release() // u 放弃对指针的控制权,返回指针,并将u 置为空
u.reset() // 释放u 指向的对象
u.reset(q) // 如果提供了内置指针q ,另u 指向这个对象;否则将u 置为空
u.reset(nullptr)
虽然我们不能拷贝或赋值,但可以通过调⽤ release 或 reset 将指针的所有权从⼀个(⾮const )转移给另⼀个:unique_ptr unique_ptr unique_ptr unique_ptr<string> p1(new string("Stegosaurus"));
// 将所有权从pl (指向string Stegosaurus)转移给p2
unique_ptr<string> p2(p1, release()); // release 将 p1 置为空 unique_ptr<string> p3(new string("Trex"));
// 将所有权从p3转移给p2
p2.reset(p3.release()); // reset 释放了 p2 原来指向的内存
调⽤ release 会切断和它原来管理的对象间的联系,如果我们不⽤另⼀个智能指针来保存 release 返回的指针,我们的程序就要负责资源的释放:unique_ptr p2.release(); // 错误:p2不会释放内存,⽽且我们丢失了指针
auto p = p2.release(); // 正确,但我们必须记得 delete(p)
delete(p);
5.3 传递unique_ptr 参数和返回unique_ptr
最常见的例⼦是从函数返回⼀个:不能拷贝 unique_ptr 的规则有⼀个例外:我们可以拷贝或赋值⼀个将要被销毁的 unique_ptr 。

unique_ptr unique_ptr<int> clone (int p)
{
unique_ptr<int> ret(new int (p));
// ...
return ret;
}
对于上⾯这段代码,编译器都知道要返回的对象将要被销毁。

在此情况下,编译器执⾏⼀种特殊的“拷贝”,在《C++ Primer 》13.6.2节(第473页)中有介绍。

六、性能与安全的权衡
使⽤智能指针虽然能够解决内存泄漏问题,但是也付出了⼀定的代价。

以举例:
shared_ptr 的⼤⼩是原始指针的两倍,因为它的内部有⼀个原始指针指向资源,同时有个指针指向引⽤计数。

shared_ptr 引⽤计数的内存必须动态分配。

虽然⼀点可以使⽤来避免,但也存在⼀些情况下不能够使⽤。

make_shared()make_shared()增加和减⼩引⽤计数必须是原⼦操作,因为可能会有读写操作在不同的线程中同时发⽣。

⽐如在⼀个线程⾥有⼀个指向⼀块资源的可能调⽤了析构(因此所指向的资源的引⽤计数减⼀),同时,在另⼀线程⾥,指向相同对象的⼀个可能执⾏了拷贝操作(因此,引⽤计数加⼀)。

原⼦操作⼀般会⽐⾮原⼦操作慢。

但是为了线程安全,⼜不得不这么做,这就给单线程使⽤环境带来了不必要的困扰。

shared_ptr shared_ptr 我觉得还是分场合吧,看应⽤场景来进⾏权衡,我也没啥经验,但我感觉安全更重要,现在硬件已经⾜够快了,其他例如这种⽀持垃圾回收的语⾔不还是⽤的很好吗。

java 参考:
《C++ Primer 第5版》。

相关文档
最新文档