C++相关:动态内存和智能指针

前言

在C++中,动态内存的管理是通过运算符newdelete来完成的。但使用动态内存很容易出现问题,因为确保在正确的时间释放内存是及其困难的。有时候我们会忘记内存的的释放,这种情况下就会产生内存泄露;有时候又会在尚有指针引用的情况下就用delete释放了内存,这样又会产生引用非法内存的指针(野指针)。因此,为了更容易地使用动态内存,C++标准库提供了两种智能指针,shared_ptrunique_ptr。shared_ptr允许多个指针指向同一个对象,unique_ptr则独占指向的对象。另外,还有一种叫weak_ptr的伴随类,他是一种弱引用,指向shared_ptr所管理的对象。三者定义于memory头文件中。

shared_ptr类

声明方式类似vector,属于模板类,如下

shared_ptr<string> p1;     //声明了一个指向string的智能指针,默认空

解引用等使用的方式类似普通指针

if( p1 && p1->empty())
  *p1 = "hi!"; //如果p1指向一个空string,解引用并赋一个新值

两种智能指针公用的操作

shared_ptr<T> sp;
unique_ptr<T> up;

//假设声明了两个名为p、q的智能指针
p->mem;  //等价于(*p).mem
p.get();     //返回p中存放的指针

//交换二者保存的指针
swap(p,q);
p.swap(q);

shared_ptr独有的操作

p.unique();  //是否独有
p.use_count; //返回p共享对象的智能指针数量
p = q;  //该操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放
make_shared<T>(args);//该方法返回一个shared_ptr,指向一个T类型的对象,并使用args初始化该对象,具体见下文 

make_shared函数

最安全的分配和使用动态内存的方法。

shared_ptr<int> p3 = make_shared<int>(42);//指向值为42的int的智能指针。
//或者也可以
auto p3 = make_shared<int>(42);

每一个shared_ptr都有关联的计数器,称为引用计数。当用一个shared_ptr ——p去初始化另个一个q时,或者将p作为参数传递给函数,或者作为函数返回值时,它关联的对象的引用计数就会递增;而如果给它赋一个新值或者是shared_ptr被销毁时,之前关联的计数器就减1,当一个shared_ptr的计数器为0时,他就会自动释放管理的对象的内存。

动态分配的const对象

const int *pci = new int(1024);
const string *pcs = new string;
/*const修饰的对象必须初始化
虽然对象的值不能被修改,但是本身可以销毁的*/
delete pcs;//这是可以的

PS:用delete释放一个空指针总是没有错误的。

内存耗尽

如果内存耗尽的情况下,使用new会分配失败并抛出std::alloc异常。

可以使用

int *p2 = new (nothrow) int;

的形式来向new传递额外的参数,从而告诉它不能抛出异常,这种称为定位new。

 动态对象的生存周期直到被释放为止

Foo* factory(T arg)
{
   return new Foo(arg);
}

void use_factory(T arg)
{
   Foo *p = factory(arg);
}

上述的代码,虽然p在离开作用域以后被销毁了,但他所指向的动态内存并没有被释放,不注意的话很可能内存泄漏!

所以,正确的做法如下:

void use_factory(T arg)
{
   Foo *p = factory(arg);
   //这块内存不再使用了就释放
   delete p;
}

概括来说,由内置指针(不是智能指针)管理的动态内存在被显式地释放前会一直存在,直到手动释放或者程序结束时才会被回收。因此,智能指针的使用能够避免很多忘记释放内存等失误带来的麻烦。

另外,delete之后,虽然指针已经无效,但是它依然保存着释放的内存的地址,因此为了避免误操作最好将指针置空。

int *p(new int(42));
auto q = p;
delete p;
p = nullptr;

但是这样提供的保护还是有限的,如上述代码虽然将p置空,但是q仍然指向那块内存,仍然存在隐患。

 shared_ptr和new结合使用

//错误的方式,智能指针的构造函数由explicit修饰,不支持将内置指针隐式转换为智能指针
shared_ptr<int> p1= new int(1024);
//正确方式
shared_ptr<int> p2(new int(1024));

p2.reset(); //若p2是唯一指向,则释放其指向的内存并置空
p2.reset(q) //令p2指向q,否则置空

//同样的,返回值如果时内置指针也会报错
shared_ptr<int> clone(int p)
{
    return new int(p);  //错误,无法隐式转换为智能指针
}

  

 智能指针和普通指针最好不要混合使用

void process(shared_ptr<int> ptr)
{

}
/*ptr离开作用域被销毁
-----------------
如果使用普通指针*/
int *x(new int(1024));
process(x);//出错,无法转换
process(shared_ptr<int>(x));//合法,但是x指向的内存在内部会被释放掉!!
int j = *x; //错误,未定义,x是一个空悬指针

上述代码中,shared_ptr通过x拷贝构造了一个智能指针ptr传递进process,这个时候的引用计数为1,而离开作用域后ptr被销毁,其指向的对象不再被引用,因此内存被回收,指针x因此无家可指变为野指针。

另外,也尽量避免使用get初始化另一个智能指针,也不要delete get()返回的内置指针。

使用自定义的释放操作

struct destination;       //连接目的地
struct connection;       //连接信息
connection connect(destination *);  //打开连接
void disconnect(connection);          //关闭指定连接
void end_connection(connection *p)
{
     disconnect(*p);
}

void f(destination &d /*其他参数*/)
{
    connection c = connect(&d);
    shared_ptr<connection> p(&c,end_connection);
    //使用连接
   //当f退出时(即使为异常退出),connection也会被正确关闭
}

上述代码模拟的一个网络库的代码使用。

当p被销毁时,她不会对保存的的指针delete,而是调用end_connection,接下来end_connection会调用disconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分,如果发生了异常,p同样被销毁,连接从而被关闭。

unique_ptr类

顾名思义,独一无二的指针,与shared_ptr不同,某个时刻只能由一个unique_ptr指向一个给定对象。声明以及初始化如下

unique_ptr<int> p2(new int(42));

由于unique_ptr独享其对象,所以它不支持普通的拷贝和赋值操作

unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);  //错误:不支持拷贝
unique_ptr<string> p3'
p3 = p2;                             //错误,不支持赋值

unique_ptr的相关操作

unique_ptr<T> u1; //空unique_ptr指针
unique_ptr<T,D> u2; //使用D对象来代替delete释放
unique_ptr<T,D> u(new class());

u = nullptr; //释放u指向的对象并置空
u.release(); //u会放弃对该对象的控制权(内存不会释放),返回一个指向对象的指针,并置空自己
u.reset();   //释放u所指对象
u,reset(q);//如果提供了内置指针q,则指向q所指对象;否则u置空

当unique_ptr将要被销毁时,可以“特殊地”被拷贝或者赋值,比如下面这种情况

unique_ptr<int> clone(int p)
{
    return unique_ptr<int>(new int(p));  //正确
}

//或者
unique_ptr<int> clone(int p)
{
   unique_ptr<int> ret(new int(p));
   //......
   return ret; //正确
}

weak_ptr类

weak_ptr是一种不控制所指向对像生命周期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,当最后一个指向对象的shared_ptr被销毁时,对象会被释放(即使有weak_ptr指向)。

weak_ptr的操作

weak_ptr<T>w;
weak_ptr<T>w(sp);//使用一个shared_ptr初始化

w = p;               //p可以是一个sp也可是一个wp。赋值后w,p共享对象

w.reset();//置空
w.use_count();       //同w共享对象的shared_ptr的数量
w.expired();        //w.use_count()为0返回true,否则返回false
w.lock();            //expired为true,返回一个空的shared_ptr;否则返回一个指向w的对象的shared_ptr

allocator类

定义在memory中,它提供了一种类型感知的内存分配方式,将内存分配和对象构造分离开来。它分配的内存是原始的、未构造的。基本用法如下:

allocator<string> alloc;  //分配string的allocator对象
auto const p = alloc,allocate(n); //分配n个未初始化的string

allocator的操作

allocator<T> a;
a.allocate(n);

a.deallocate(p,n); /*释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是
一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之后,用户必须对
每个在这块内存中创建的对象调用destroy*/

a.construct(p,args);/*p必须是一个类型为T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象*/

a.destroy(p); //p为T*类型的指针,此算法对p所指向的对象执行析构函数

allocator分配的内存是未构造的,所以我们必须用construct构造对象,并且只能对构造了的对象执行destroy操作。

销毁的参考代码如下:

while(q != p)
   alloc.destroy(--q);

 一旦所有元素被销毁后,就可以重新使用这部分内存来保存其他的string,也可以使用 alloc.deallocate(p,n)来释放内存。

参考资料

《C++ Primer 第5版》 电子工业出版社    作者:【美】  Stanley B. Lippman  && Josee Lajoie && Barbara E.Moo

原文地址:https://www.cnblogs.com/0kk470/p/7903779.html