C++ 智能指针

对于指针,优先使用make_unique,make_shared,而不是直接使用new。

智能指针

先介绍智能指针:

auto_ptr

auto_ptr主要是用来解决资源自动释放的问题,比如如下代码:

1
2
3
4
5
6
7
8
void Function()
{
Obj*p = new Obj(20);
...
if (error occor)
throw ... 或者 retrun;
delete p;
}

在函数遇到错误之后,一般会抛异常,或者返回,但是这时很可能遗漏之前申请的资源,及时是很有经验的程序员也有可能出现这种错误,
而使用auto_ptr会在自己的析够函数中进行资源释放。也就是所说的RAII

使用auto_ptr代码如下

1
2
3
4
5
6
7
void Function()
{
auto_ptr<Obj> ptr( new Obj(20) );
...
if (error occur)
throw exception...
}

这样无论函数是否发生异常,在何处返回,资源都会自动释放。
需要提一下的是这是一个被c++11标准废弃的一个智能指针,为什么会被废弃,先看一下下面的代码:

1
2
3
4
5
auto_ptr<Obj> ptr1( new Obj() );
ptr1->FuncA();
auto_ptr<Obj> ptr2 = ptr1;
ptr2->FuncA();
ptr1->FuncA(); // 这句话会异常

为什么在把ptr1赋值给ptr2之后ptr1再使用就异常了呢?
这也正是他被抛弃的主要原因。
因为auto_ptr赋值构造函数中把真实引用的内存指针进行的转移,也就是从ptr1转移给了ptr2,此时,ptr2引用了Obj内存地址,而ptr1引用的内存地址为空。参考std::move()
此时再使用ptr1就异常了。

unique_ptr

unique_ptr可以看成是auto_ptr的替代品。因为他对对象的所有权比较专一。

无法进行拷贝构造和赋值操作

auto_ptr与unique_ptr的对比:

1
2
3
4
5
6
auto_ptr<Obj> ap(new Obj() );
auto_ptr<Obj> one (ap) ; // ok
auto_ptr<Obj> two = one; //ok
unique_ptr<Obj> ap(new Obj() );
unique_ptr<Obj> one (ap) ; // 会出错
unique_ptr<Obj> two = one; //会出错

也就是说unique_ptr对对象的引用比较专一,不允许随随便便的进行转移

可以进行移动构造和移动赋值操作

1
2
3
4
5
6
unique_ptr<Obj> GetObj()
{
unique<Obj> ptr( new Obj() );
return ptr;
}
unique<Obj> ptr = GetObj(); //GetObj()返回右值

上面的代码可以顺利执行!
那么如果万一我就是需要把一个unique_ptr智能指针赋值给另外一个怎么办呢?
可以使用移动函数!如下:

1
2
unique<Obj> ptr1( new Obj() );
unique<Obj> ptr2( std::move(ptr1) );

这个效果和auto_ptr直接赋值是一样的,就是ptr1不再拥有Obj对象了,所以ptr1不能再用来操作内存中的Obj对象,因为这个是手动操作的,
所以程序员自己也会更加小心。

shared_ptr

auto_ptr和unique_ptr都只能一个智能指针引用对象,而shared_ptr则是可以多个智能指针同时拥有一个对象。
shared_ptr实现方式就是使用引用计数。这一技术在COM中是用来管理COM对象生命周期的一个方式。
引用计数的原理是,多个智能指针同时引用一个对象,每当引用一次,引用计数加一,每当智能指针销毁了,引用计数就减一,
当引用计数减少到0的时候就释放引用的对象。这种引用计数的增减发生在智能指针的构造函数,复制构造函数,赋值操作符,析构函数中。
这种方式使得多个智能指针同时对所引用的对象有拥有权,同时在引用计数减到0之后也会自动释放内存,也实现了auto_ptr和unique_ptr的资源释放的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Function()
{
shared_ptr<Obj> ptr1( new Obj() ); // 引用计数为1
{
shared_ptr<Obj> ptr2( ptr1 ); // 引用计数为2
{
shared_ptr<Obj> ptr3 = ptr2; // 引用计数为3
int e = 0
}
//引用计数为2
}
//引用计数为1
}
//函数返回之后引用计数为0,new 出来的Obj内存已经释放了

由于shared_ptr支持拷贝构造,所以他可以作为标准库容器中的元素

1
2
3
4
5
6
vector<shared_ptr<Obj>> vect;
for (int i = 0; i < 10; ++i)
{
vect.push_back( shared_ptr<Obj>( new Obj() ) );
}
vector<shared_ptr<Obj>> vect2 = vect;

这些操作是auto_ptr和unique_ptr不能实现的。
注意,智能指针默认使用delete来释放资源,如果资源是FILE*怎么办?释放的时候就需要用fclose了。
如何实现呢?
shared_ptr构造函数可以传递一个删除器。

1
2
FILE* pStm = fopen(...);
shared_ptr<FILE> fileRes(pStm, &fclose);

或者使用一个仿函数

1
2
3
4
5
6
7
8
9
10
11
class FileCloser {
public:
void operator()(FILE* file)
{
std::cout << "The FileCloser has been called with a FILE*, "
"which will now be closed.\n";
if (file!=0)
fclose(file);
}
};
shared_ptr<FILE> fileRes(pStm, FileCloser);

关于这点看一参考这篇文章: http://www.cnblogs.com/learn-my-life/p/3817151.html

weak_ptr

shared_ptr是一种强引用的关系,智能指针直接引用对象。那么这个会代码一个隐含的问题,就是循环引用,从而造成内存泄漏,即便是java语言有自己的
垃圾回收器,对这种内存泄漏也没有办法,所以循环引用对java程序员来说也是一个很值得注意的问题。首先来看一个循环引用的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent
{
public:
shared_ptr<Child> child;
};
class Child
{
public:
shared_ptr<Parent> parent;
};
void Function()
{
shared_ptr<Parent> pA(new Parent);
shared_ptr<Child> pB(new Child);
pA->child = pB;
pB->parent = pA;
}

现在来分析一下Function函数的执行过程:

  • 第一条语句使得pA引用了Parent一个指针,Parent引用计数为1
  • 第二条语句使得pB引用了Child一个指针,Child引用计数为1
  • 第三条语句,调用了shared_ptr类的赋值操作符,使得Child引用计数变为2
  • 第四条语句,调用了shared_ptr类的赋值操作符,使得Parent引用计数变为2
  • 函数返回之前调用了shared_ptr和shared_ptr类的析够函数,使得Child引用计数变为1,Parent引用计数变为1

函数执行完之后new出来的Parent和Child并没有释放,所以出现了内存泄漏。
出现泄漏的原因就是pA和pB相互引用了,导致两者所引用对象的引用计数不能减少到0,造成泄漏。
如果把第三条语句或者第四条语句任意删除一个,就不会有泄漏了。
这就是强引用所带来的问题。
weak_ptr从字面意思上可以看出是一个弱指针,不是说明这个指针的能力比较弱,而是说他对他所引用的对象的所有权比较弱,
说得更直接一点儿就是他并不拥有所引用对象的所有权,而且他还不能直接使用他所引用的对象。

在stl中,weak_ptr是和shared_ptr配合使用的,在实现shared_ptr的时候也就考虑了weak_ptr的因素。
weak_ptr是shared_ptr的观察者,它不会干扰shared_ptr所共享对象的所有权,
当一个weak_ptr所观察的shared_ptr要释放它的资源时,它会把相关的weak_ptr的指针设置为空,防止weak_ptr持有悬空的指针。
注意:weak_ptr并不拥有资源的所有权,所以不能直接使用资源。
可以从一个weak_ptr构造一个shared_ptr以取得共享资源的所有权,weak_ptr的大致用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Function()
{
shared_ptr<int> sp( new Obj() );
assert(sp.use_count() == 1);
weak_ptr<int> wp(sp); //从shared_ptr创建weak_ptr
assert(wp.use_count() == 1);
if (!wp.expired())//判断weak_ptr观察的对象是否失效
{
shared_ptr<int> sp2 = wp.lock();//获得一个shared_ptr
*sp2 = 100;
assert(wp.use_count() == 2);
}
assert(wp.use_count() == 1);
return 0;
}

weak_ptr并没有重载-> 和 * 操作符,所以我们不能通过他来直接使用资源,我们可以通过lock来获得一个shared_ptr对象
来对资源进行使用,如果引用的资源已经释放,lock()函数将返回一个存储空指针的shared_ptr。 expired函数用来判断资源是否失效。

使用weak_ptr并不会增加资源的引用计数。所以对资源的引用是弱引用,利用这个特性可以解决前面所说的循环依赖问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent
{
public:
weak_ptr<Child> child;
};
class Child
{
public:
weak_ptr<Parent> parent;
};
void Function()
{
shared_ptr<Parent> pA(new Parent);
shared_ptr<Child> pB(new Child);
pA->child = pB;
pB->parent = pA;
}

这个时候第三和第四条语句的执行并没有增加引用计数,从而在函数执行完成只有能自动释放内存。
从上面的分析可以看出,weak_ptr是一种辅助shared_ptr的一种智能指针,一般不单独使用,而是结合
shared_ptr一起使用。

总结:

  • 尽量使用unique_ptr而不要使用auto_ptr
  • 一般来说shared_ptr能够满足我们大部分的需求
  • weak_ptr可以避免递归的依赖关系

智能指针的优势

常规指针操作,如果发生异常,会使得后续代码不能正常执行,后续的delete无法执行,导致内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Dog{
public:
string m_name;
Dog(){cout <<"nameless dog created."<<endl; m_name="nameless";}
Dog(string name){cout<<"dog is created:"<<name<<endl; m_name = name;}
void bark(){cout<<"dog"<<m_name<<"rules"<<endl;}
~Dog(){cout<<"dog is destroyed:"<<m_name<<endl;}
}
void test()
{
Dog* pd = new Dog("gunner");
pd->bark(); //如果此处出现异常,导致后续的delete失败,内存泄漏
delete pd;
}
void test1()
{
unique_ptr<Dog> pd(new Dog("gunner"));
pd->bark();
//Dog* p = pd.release(); //没有调用析构函数
//pd.reset(new Dog("smokey"));//gunner destroyed ;dog is created:smokey
//pd.reset();//等效于pd = nullptr,调用了析构函数
if(!pd)
cout<<"pd is empty"<<endl;
else cout<<"pd is not empty"<<endl;
}
void main()
{
test();
}

对于new操作而言

1
std::shared_ptr<Investment> ptr(new Investment)

两次动态内存分配,第一次是new Investment,第二次是为控制块分配空间。

make_shared

1
auto pln = std::make_shared<Investment>();

一次动态内存分配。make_shared会为Investment对象和控制块一次性分配一大块内存,效率比new高。

make的劣势

1、需要遵循make函数的资源释放规则。reset…release…etc
2、部分初始化方式不支持。
3、内存释放不灵活。

  • 在new的方式中,有两块独立的堆内存,一块存放资源对象,一块存放控制块,当资源对象引用计数为0的时候,资源对象会被销毁,所占用的内存也随之销毁。
  • make函数的方式中,一次性为资源对象和控制块分配了一大块内存,当资源对象的引用计数为0时,对象被销毁,但是资源对象占用的内存却不会被释放,只有当控制块占用的内存被销毁时才会将资源对象所占内存一并释放,即当weak count的值为0时,控制块才会被释放,当资源对象非常庞大时,weak_ptr比shared_ptr长寿,使用make函数的方式将造成不小的资源浪费。峰值会比较大。

unique_ptr 不允许拷贝构造,拷贝赋值,只能传引用或者指针。