动态内存与智能指针

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态内存与智能指针相关的知识,希望对你有一定的参考价值。

一、shared_ptr

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

1 shared_ptr<int> p3 = make_shared<int>(42);
2 shared_ptr<string> p4 = make_shared<string>(10, 9);
3 shared_ptr<int> p5 = make_shared<int>();

如果我们不传递参数,对象就会进行值初始化。

shared_ptr内部有一个引用计数变量,记录有多少个其他shared_ptr指向相同的对象。计数器变为0,会自动释放自己所管理的对象。

shared_ptr在无用之后一定要销毁,否则程序仍会正确执行,但会浪费内存。shared_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。

在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。比如unique算法,重排输入序列,覆盖相邻的重复元素,返回一个指向不重复元素之后位置的迭代器。

 

二、new与delete

new

默认情况下,动态分配的对象是默认初始化。在类型名之后跟一对空括号进行值初始化。

对于类类型来说,不管采用什么形式,对象都会通过默认构造函数来初始化。

Tip:出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。

1 auto p1 = new auto(obj);          // 正确
2 auto p2 = new auto(a,b,c);       // 错误

由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才能使用auto。

默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常。

1 int *p1 = new int;                       // 如果分配失败,new抛出std::bad_alloc
2 int *p2 = new  (nothrow) int;      // 如果分配失败,new返回一个空指针

这种形式的new是定位new(placement new)。

 

delete

delete p;     // p必须指向一个动态分配的对象或是一个空指针

通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。

虽然一个const对象的值不能被改变,但它本身是可以被销毁的。想要释放一个const动态对象,只要delete指向它的指针即可:

1 const int *pci = new const int(1024);
2 delete pci;       // 正确

Tip:内置指针指向的动态内存在销毁内置指针之前必须显示释放。

使用new和delete管理动态内存存在三个常见问题:

1.忘记delete内存。

2.使用已经释放掉的对象。

3.同一块内存释放两次。

Tip:坚持只使用智能指针,就可以避免所有这些问题。

当我们delete一个指针后,指针变为无效,但在很多机器上指针仍然保存着动态内存的地址。

空悬指针是指向一块曾经保存数据对象但现在已经无效的内存的指针,本质上和未初始化指针类似。

有一种方法可以避免空悬指针的问题:释放内存后马上销毁指针。

如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

 

三、shared_ptr和new结合使用

如果我们不初始化一个智能指针,它就会被初始化为一个空指针。

接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

1 shared_ptr<int> p1 = new int(1024);     // 错误
2 shared_ptr<int> p2(new int(1024));      // 正确

出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

1 shared_ptr<int> clone(int p)
2 {
3   return new int(p);      // 错误
4 }

我们必须将shared_ptr显示绑定到一个想要返回的指针上:

1 shared_ptr<int> clone(int p)
2 {
3   return shared_ptr<int>(new int(p));      // 正确
4 }

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。

我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来代替delete。

Cautions:

1.不要混合使用普通指针和智能指针

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。这也是为什么我们推荐使用make_shared而不是new的原因。

这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

 1 void process(shared_ptr<int> ptr)
 2 {
 3     // 使用ptr
 4 }
 5 
 6 shared_ptr<int> p(new int(42));
 7 process(p);
 8 int i = *p;     // 正确
 9   
10 int *x(new int(1024));
11 process(x);     //  错误
12 process(shared_ptr<int>(x));      // 合法的
13 int j = *x;     // 未定义的:x是一个空悬指针!

在上面的调用中,我们将一个临时shared_ptr传递给process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。

销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。

当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。

一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。

2.不要使用get初始化另一个智能指针或为智能指针赋值

 智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。

此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。

使用get返回的指针的代码不能delete此指针。

虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get返回的指针上是错误的:

1 shared_ptr<int> p(new int (42));
2 int *q = p.get();
3 {
4   shared_ptr<int>(q);      // 未定义:两个独立的shared_ptr指向相同的内存
5 }
6 int foo = *p;     // 未定义:p指向的内存已经被释放了

Tip:get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。

特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

3.其他shared_ptr操作

我们可以将一个新的指针赋予一个shared_ptr:

1 p = new int(1024);            // 错误
2 p.reset(new int(1024));     // 正确

reset会更新引用计数。reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。

在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

1 if(!p.unique())
2   p.reset(new string(*p));      // 我们不是唯一用户;分配新的拷贝
3 *p += newVal;      // 我们是唯一用户,可以改变对象的值

 

四、删除器(deleter)

与管理动态内存类似,我们通常可以使用类似的技术来管理不具有良好定义的析构函数的类。

例如,假定我们正在使用一个C和C++都使用的网络库,使用这个库的代码可能是这样的:

 1 struct destination;                              // 表示我们正在连接什么
 2 struct connection;                              // 使用连接所需的信息
 3 connection connect(destination*);       // 打开连接
 4 void disconnect(connection);              // 关闭给定连接
 5 void f(destination &d /* 其他参数*/)
 6 {
 7     // 获得一个连接;记住使用完后要关闭它
 8     connection c = connect(&d);
 9     // 使用连接
10     // 如果我们在f退出前忘记调用disconnect,就无法关闭c了
11 }

 

以上是关于动态内存与智能指针的主要内容,如果未能解决你的问题,请参考以下文章

动态内存与智能指针

12.1 动态内存与智能指针

第十二章 动态内存与智能指针

动态内存——动态内存与智能指针

动态内存1(动态内存与智能指针)

c++动态内存管理与智能指针