C++智能指针

scorlw 发布于

C++智能指针

c++

本文介绍c++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用。

为什么要使用智能指针:我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。下面我们逐个介绍。

auto_ptr

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <memory> //auto_ptr的头文件
using namespace std;
class Test
{
public:
Test(string s)
{
str = s;
cout << "Test creat\n";
}
~Test()
{
cout << "Test delete:" << str << endl;
}
string &getStr()
{
return str;
}
void setStr(string s)
{
str = s;
}
void print()
{
cout << str << endl;
}

private:
string str;
};

int main()
{
auto_ptr<Test> ptest(new Test("123")); //调用构造函数输出Test creat
ptest->setStr("hello "); //修改成员变量的值
ptest->print(); //输出hello
ptest.get()->print(); //输出hello
ptest->getStr() += "world !"; //成员函数get()返回一个原始的指针
(*ptest).print(); //输出hello world
ptest.reset(new Test("123")); //成员函数reset()重新绑定指向的对象,而原来的对象则会被释放,所以这里会调用一次构造函数,还有调用一次析构函数释放掉之前的对象
ptest->print(); //输出123
system("pause");
return 0;
}

输出:

1
2
3
4
5
6
7
Test creat
hello
hello
hello world !
Test creat
Test delete:hello world !
123

如上面的代码:智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。我们也可用声明一个空智能指针

auto_ptr<Test>ptest();

当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL),如下代码:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
auto_ptr<Test> ptest(new Test("123")); //调用构造函数输出Test creat
auto_ptr<Test> ptest2(new Test("456")); //调用构造函数输出Test creat
ptest2 = ptest; //调用ptest的析构函数输出Test delete:456
ptest2->print(); //调用print()函数,发现此时ptest2已经是123了
if(ptest.get() == NULL)
cout << "ptest = NULL\n"; //此时ptest已经为空。
system("pause");
return 0;
}

输出:

1
2
3
4
5
Test creat
Test creat
Test delete:456
123
ptest = NULL

还有一个值得我们注意的成员函数是release,这个函数只是把智能指针赋值为空,但是它原来指向的内存并没有被释放,相当于它只是释放了对资源的所有权,从下面的代码执行结果可以看出,析构函数没有被调用。

1
2
3
4
5
6
int main()
{
auto_ptr<Test> ptest(new Test("123"));
ptest.release();
return 0;
}

输出:

1
Test creat

那么当我们想要在中途释放资源,而不是等到智能指针被析构时才释放,我们可以使用ptest.reset() 语句。

1
2
3
4
5
6
7
int main()
{
auto_ptr<Test> ptest(new Test("123")); //调用构造函数输出Test creat
ptest.reset();
system("pause");
return 0;
}

输出:

1
2
Test creat
Test delete:123

auto_ptr的弊端

  1. “=”运算符的重载
1
2
my_auto2 = my_auto1
my_auto1->PringSomething(); //崩溃

autp_ptr对赋值运算符重载的实现是reset(Myptr.release()),即reset和release函数的组合。release会释放所有权,reset则是用于接受所有权。my_auto1被release之后已经被置0(内部实现),所以调用函数当然会出错。

  1. 临时对象
1
2
3
4
5
6
7
8
9
10
11
void Fun(auto_ptr <Test> ap)
{
*ap;
}

void TestAutoPtr3()
{
auto_ptr<Test> my_auto(new Test(1));
Fun(my_auto);
my_auto->PrintSomething(); //崩溃
}

这个错误非常隐蔽,基本很难发现。可以看到在调用Fun函数之后,my_auto竟然又被置空了,所以导致调用PrintSomething方法崩溃。

说到底,还是因为我们在Fun函数的参数列表中使用的是值传递。值传递的情况下在调用Fun函数时,由于不会对本身进行修改,所以会产生一个临时对象来接受参数。是不是熟悉的问题?又出现了赋值运算符,因为auto_ptr对赋值运算符重载的关系原指针就被置空了。只不过这次Fun函数结束后临时对象也被抛弃,my_auto也置空,保存的那块内存就彻底丢失了,也造成了内存泄漏,这可是真正上的危机。

解决的方法也很简单,Fun函数参数改为引用传递就可以了。

  1. 直接调用release就会导致内存泄漏
1
2
3
4
5
6
7
8
9
void TestAutoPtr4()
{
auto_ptr <Test> my_auto(new Test(1));
if(my_auto.get())
{
//my_auto.release(); //内存泄漏
my_auto.reset();
}
}

直接调用release就会导致内存泄漏。在不了解release和reset的实现时,经常会出现不知道使用哪个而导致内存泄漏的问题。在此我们就对这两个函数进行分析和区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
_Ty *release() _THROW0()
{ // return wrapped pointer and give up ownership
_Ty *_Tmp = _Myptr;
_Myptr = 0;
return (_Tmp);
}

void reset(_Ty *_Ptr = 0)
{ // destroy designated object and store new pointer
if (_Ptr != _Myptr)
delete _Myptr;
_Myptr = _Ptr;
}

以上是memory头文件中的release和reset源码。

先看release函数。定义一个指针来接受myptr,然后将myptr置空,最后return 临时指针。如果直接调用该方法而不去使用别的指针进行接受的话,就会引起内存泄漏。这个函数意在将调用该函数的智能指针的所有权转移,如 ptr = my_auto2.release();就是将my_auto2的所有权转给ptr。(这个代码只是伪代码,有助于理解release函数,实际上需要考虑赋值运算符重载的问题)。

当然你也可以直接my_auto.release();而左边不用任何东西去接收,只不过这样就会导致内存泄漏。

再来看reset函数。它有一个参数,默认下是空。首先进行myptr和参数的比较。如果此时没有参数(即为默认的0)且此时myptr非空,就是真正的reset,将myptr delete掉,然后将myptr置空,完成了释放内存的操作。另一种情况就是有参数的情况,只要myptr和参数不相等,就直接delete myptr,然后把参数再赋给myptr。删除旧对象,保存了一个新的对象。

最后看一下赋值运算符重载的实现,是release和reset的组合。

1
2
3
4
5
_Myt& operator=(_Myt& _Right) _THROW0()
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}

a = b,其中b即为参数Right。首先将b release,b被置空,绑定的对象返回作为reset的参数,this指针调用reset即将this指针中的内容delete,然后接收release返回的对象,就完成了赋值运算符的模拟。

unique_ptr

unique_ptr 是用于取代c++98 auto_ptr 的产物,在c++98的时候还没有移动语义 (move semantics) 的支持,因此对于auto_ptr 的控制权转移的实现没有核心元素的支持,但还是实现了auto_ptr 的移动语义,这样带来的一些问题是拷贝构造函数和复制操作重载函数不够完美,具体体现就是把 auto_ptr 作为函数参数传进去的时候控制权转移到函数参数,当函数返回的时候并没有一个控制权移交的过程,所以过了函数调用则原先的 auto_ptr 已经失效了。

在c++11当中有了移动语义,使用 move() 把unique_ptr 传入函数,这样你就知道原先的 unique_ptr 已经失效了。再一个 auto_ptr 不支持传入deleter,所以只能支持单对象(delete object),而 unique_ptr 对数组类型有偏特化重载,并且还做了相应的优化,比如用 [] 访问相应元素等。

unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

  • 拥有它指向的对象
  • 无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr 指向同一个对象。但是可以进行移动构造和移动赋值操作
  • 保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

unique_ptr 可以实现如下功能:

  • 为动态申请的内存提供异常安全
  • 讲动态申请的内存所有权传递给某函数
  • 从某个函数返回动态申请内存的所有权
  • 在容器中保存指针
  • auto_ptr 应该具有的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unique_ptr<Test> fun()
{
return unique_ptr<Test>(new Test("789"));//调用了构造函数,输出Test creat
}
int main()
{
unique_ptr<Test> ptest(new Test("123"));//调用构造函数,输出Test creat
unique_ptr<Test> ptest2(new Test("456"));//调用构造函数,输出Test creat
unique_ptr<Test> ptest3(new Test("abc"));//调用构造函数,输出Test creat
unique_ptr<Test> ptest4(new Test("def"));//调用构造函数,输出Test creat
ptest->print();//输出123
ptest2 = std::move(ptest);//不能直接ptest2 = ptest,调用了move后ptest2原本的对象会被释放,ptest2对象指向原本ptest对象的内存,输出Test delete: 456
if(ptest == NULL)cout<<"ptest = NULL\n";//因为两个unique_ptr不能指向同一内存地址,所以经过前面move后ptest会被赋值NULL,输出ptest=NULL
Test* p = ptest2.release();//release成员函数把ptest2指针赋为空,但是并没有释放指针指向的内存,所以此时p指针指向原本ptest2指向的内存
p->print();//输出123
ptest.reset(p);//重新绑定对象,原来的对象会被释放掉,但是ptest对象本来就释放过了,所以这里就不会再调用析构函数了
ptest3.reset(p);//重新绑定对象,原来的对象会被释放掉,输出Test delete:abc
ptest->print();//输出123
ptest2 = fun(); //这里可以用=,因为使用了移动构造函数,函数返回一个unique_ptr会自动调用移动构造函数
ptest2->print();//输出789
system("pause");
return 0;//此时程序中还有4个对象,调用4次析构函数释放对象,但由于ptest与ptest3都指向p,析构函数调用顺序与构造函数相反,所以ptest3释放了p的内存,ptest再次释放的时候就会造成内存泄漏,报错
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Test creat
Test creat
Test creat
Test creat
123
Test delete:456
ptest = NULL
123
Test delete:abc
123
Test creat
789
请按任意键继续. . .
Test delete:def
Test delete:123
Test delete:789
Test delete:

unique_ptr 和 auto_ptr用法很相似,不过不能使用两个智能指针赋值操作,应该使用std::move;而且它可以直接用 if(ptest == NULL) 来判断是否空指针;release、get、reset 等用法也和 auto_ptr 一致,使用函数的返回值赋值时,可以直接使用 =,这里使用c++11 的移动语义特性。另外注意的是当把它当做参数传递给函数时(使用值传递,应用传递时不用这样),传实参时也要使用 std::move,比如foo(std::move(ptest))。它还增加了一个成员函数swap用于交换两个智能指针的值

注意使用reset的时候不要多个智能指针绑定同一普通指针。

share_ptr

shared_ptr 多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化:智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如 std::shared_ptr<int> p4 = new int(1)的写法是错误的
  • 拷贝和赋值:拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
  • get函数获取原始指针
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{
shared_ptr<Test> ptest(new Test("123"));//调用构造函数输出Test create
//shared_ptr<Test> ptest = make_shared<Test>("123");//调用构造函数输出Test create
shared_ptr<Test> ptest2(new Test("456"));//调用构造函数输出 Test creat
cout<<ptest2->getStr()<<endl;//输出456
cout<<ptest2.use_count()<<endl;//显示此时资源被几个指针共享,输出1
ptest = ptest2;//"456"引用次数加1,“123”销毁,输出Test delete:123
ptest->print();//输出456
cout<<ptest2.use_count()<<endl;//该指针指向的资源被几个指针共享,输出2
cout<<ptest.use_count()<<endl;//2
ptest.reset();//重新绑定对象,绑定一个空对象,此时指针指向的原对象还有其他指针能指向,就不会释放该对象的内存空间
ptest2.reset();//此时“456”销毁,此时指针指向的内存空间上的指针为0,就释放了该内存,输出Test delete:456
cout<<"done !\n";
system("pause");
return 0;
}

输出:

1
2
3
4
5
6
7
8
9
10
Test creat
Test creat
456
1
Test delete:123
456
2
2
Test delete:456
done !

weak_ptr

shared_ptr依然存在着资源无法释放的问题。看下面这个例子:

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
30
31
32
33
34
35
36
37
38
39
class A;
class B;

class A {
public:
shared_ptr<B> pointer;
A(){
cout << "A creat\n";
}
~A() {
cout << "A 被销毁" << endl;
}
};
class B {
public:
shared_ptr<A> pointer;
B(){
cout << "B creat\n";
}
~B() {
cout << "B 被销毁" << endl;
}
};
int fun()
{
shared_ptr<A> a = make_shared<A>();
// shared_ptr<A> a(new A());
shared_ptr<B> b = make_shared<B>();
a->pointer = b;
b->pointer = a;
cout<<a.use_count()<<endl;
cout<<b.use_count()<<endl;
return 0;
}
int main() {
fun();
system("pause");
return 0;
}

​ 输出:

1
2
3
4
5
A creat
B creat
2
2
请按任意键继续. . .

运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。

解决这个问题的办法就是使用弱引用指针 std::weak_ptrstd::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加.

std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它的唯一作用就是用于检查 std::shared_ptr是否存在,expired() 方法在资源未被释放时,会返回 true,否则返回 false

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
30
31
32
33
34
35
36
37
38
39
40
class A;
class B;

class A {
public:
// A 或 B 中至少有一个使用 weak_ptr
weak_ptr<B> pointer;//只有这里修改了
A(){
cout << "A creat\n";
}
~A() {
cout << "A 被销毁" << endl;
}
};
class B {
public:
shared_ptr<A> pointer;
B(){
cout << "B creat\n";
}
~B() {
cout << "B 被销毁" << endl;
}
};
int fun()
{
shared_ptr<A> a = make_shared<A>();
// shared_ptr<A> a(new A());
shared_ptr<B> b = make_shared<B>();
a->pointer = b;
b->pointer = a;
cout<<a.use_count()<<endl;
cout<<b.use_count()<<endl;
return 0;
}
int main() {
fun();
system("pause");
return 0;
}

输出:

1
2
3
4
5
6
7
A creat
B creat
2
1
B 被销毁
A 被销毁
请按任意键继续. . .

原文地址:https://blog.csdn.net/weixin_40721097/article/details/102999973

https://blog.csdn.net/icandoit_2014/article/details/56666277?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-6.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-6.channel_param

https://blog.csdn.net/czc1997/article/details/84026887