C++相关:动态内存和智能指针
前言
在C++中,动态内存的管理是通过运算符new和delete来完成的。但使用动态内存很容易出现问题,因为确保在正确的时间释放内存是及其困难的。有时候我们会忘记内存的的释放,这种情况下就会产生内存泄露;有时候又会在尚有指针引用的情况下就用delete释放了内存,这样又会产生引用非法内存的指针(野指针)。因此,为了更容易地使用动态内存,C++标准库提供了两种智能指针,shared_ptr和unique_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