1. 动态内存和智能指针
C++提供了两种智能指针管理动态对象,负责自动释放所指对象。
类似vector,智能指针也是模板。创建一个只能指针时,需要给出指针可以指向的类型。
1.1 shared_ptr
make_shared定义在头文件memory内。
类似于顺序容器的emplace成员,make_shared使用其参数构造所指向的对象。如果不传递任何参数,对象就会进行值初始化。
1.1.1 shared_ptr计数器
每个shared_ptr会有一个与之关联的计数器,记录着和它指向同一对象的shared_ptr数目。
拷贝shared_ptr时,计数会递增。如 当使用一个shared_ptr初始化另一个shared_ptr,将一个shared_ptr作为参数传递给一个函数,将shared_ptr作为函数返回值。
改变shared_ptr指向对象,一个shared_ptr被销毁,计数器递减。具体的实现为,shared_ptr的析构函数会递减所指对象的引用计数。
当shared_ptr计数器减为0,shared_ptr的析构函数就会自动释放所指的对象。
1.1.2 使用动态生存期的资源的类
程序使用动态内存出于三个原因:
对于第三种原因,可使用shared_ptr实现数据共享: 需要共享数据(动态内存分配),但是多个共享者的生命周期又不同。希望分配的资源和共享者具有独立的生命周期(shared_ptr计数器机制)。用了shared_ptr,只要有共享者仍然指向该对象,该对象就不会被销毁
如上所示,用make_shared在堆区分配内存(共享),然后把用shared_ptr存放数据的地址(独立的生命周期)。在拷贝对象时,StrBlob的数据成员data也会被拷贝,当拷贝的某个对象被释放,只要还有对象存在,data的计数就不会为0,这块堆空间也就一直没被释放。
1.2 直接管理内存
1.2.1 动态分配内存new,delete
new:创建对象,返回指针。 delete:销毁指针所指对象,释放对应内存。
1.2.1.1 new
默认情况下,动态分配的对象是默认初始化的。也就是内置类型的对象的值将是没定义的。 也可以用圆括号进行值初始化,或者使用花括号进行列表初始化: 补充:对于定义了构造函数的类而言,值初始化(给出括号但不给初始值-见类那一章)是没有意义的,对象实际使用的是默认初始化。而对于内置类型,默认初始化得到未定义值,值初始化会得到一个良好定义的值。
如果使用new时,提供了一个圆括号包裹的初始化器,那么可以使用auto 自动推导要分配的类型。使用这种方法要求只能用一个初始化器 可以用new分配const对象,除非对象定义了默认构造函数,可以隐式初始化,否则必须显式初始化 ,new返回的是一个指向常量的指针: 当程序所分配的空间耗尽时,new会失败,抛出bad_alloc异常,可以改变使用new的方式阻止抛出异常: 第二中形式的new称为定位new,允许向new传递诸如上文的nothrow 的额外参数。
1.2.1.2 delete
传递给delete的指针必须是指向动态分配的内存的指针或者是空指针。 尝试释放一块并非new的内存,或者释放同一空间两次(多个指针指向同一空间),都会产生未定义的行为。
可以向释放普通指针一样释放指向常量的指针。
使用常规指针的动态对象的生命周期到被显式释放时为止。(智能指针会随着对象销毁而释放内存) 而一旦常规指针自身生命周期结束,他所申请的动态对象将无法再被释放。所以一定要delete。
delete之后的指针就称为空悬指针,即指向曾经保存数据对象但现在已经无效的内存的指针。空悬指针具有野指针(未初始化的指针)的所有缺点。
如何处理空悬指针?在指针所指向内存释放后将其赋值为nullptr;但是此方法很有限,当有多个指针指向同一内存时,重置一个指针,其他指针还会出错。
1.3 shared_ptr结合new
shared_ptr具有接收普通指针的构造函数。但是该构造函数是explicit的,即不能隐式将普通指针转化为智能指针,需要使用直接初始化来显式初始化智能指针。 也可以使用reset完成指针类型转换:
用来初始化智能指针的普通指针必须指向动态内存,因为当智能指针最后会默认使用delete 释放它所关联的对象。如果使用不指向动态内存的指针初始化智能指针,需要定义自己的释放操作来替代delete。
1.3.1 智能指针注意事项
当把shared_ptr绑定到普通指针时,就将管理内存的责任交给了shared_ptr,不要再通过普通指针访问对象。如下: 不要使用get的返回值初始化另一个智能指针或为智能指针赋值。get返回一个指向智能指针所指对象的内置指针。
1.4 智能指针和异常
智能指针在发生异常时也能完成内存的释放。
而如果是直接管理内存的方式,则不能释放内存: 为C和C++两种语言设计的类,通常要求用户显式释放所使用的任何资源,即分配了资源,但是没有析构函数,如果忘记释放,也会造成资源泄露。 可以使用改造版的shared_ptr帮忙自动释放资源。默认情况下,shared_ptr假定指向动态内存,当计数清0时,对所管理的对象执行delete操作。只要写一个针对性的资源释放函数(称为删除器函数)来代替delete,就能使用shared_ptr自动释放资源。 比如对如下C和C++都可使用的网络库操作:
定义删除器,用于关闭网络释放资源: 创建一个改造后的shared_ptr指向connection的对象:
1.5 unique_ptr
1.5.1 unique_ptr基础
unique_ptr只能绑定到内置指针上。 unique_ptr拥有他所指向的对象,所以不支持普通的拷贝和赋值操作。一个时刻只能有一个unique_ptr指向一个给定对象,当该unique_ptr被销毁时,所指向的对象也被销毁。 虽然unique_ptr不支持普通的赋值和拷贝操作,但可以使用release 或者reset 将指针所有权从一个unique_ptr转移到另一个: 注意release以后一定要接收结果,或是重新绑定至智能指针,或是直接管理返回的内置指针。否则将导致指针和它所指的对象丢失。
可以拷贝和赋值一个将要被销毁的unique_ptr对象。 return临时对象: 或者return局部遍历:
1.5.2 重载默认删除器
默认情况下,unique_ptr以delete作为默认删除器。 和shared_ptr不同,如果重载了unique_ptr的删除器,在构造该类型的对象时需要在尖括号内unique_ptr指向类型之后提供删除器类型(类似重载关联函数的比较操作)。如下:
1.6 weak_ptr
weak_ptr不控制所指对象的生存周期,指向由shared_ptr管理的对象,不会增加引用计数。 由于weak_ptr所指对象可能不存在(被释放),所以不能直接用weak_ptr访问对象,需要调用lock。
weak_ptr不会影响对象生命周期,也可以处理对象为空的情况。
2. 动态数组
2.1 new
前面提到用new动态创建对象,现在使用new动态创建一个对象的数组。 使用[] 在new时指定分配的对象数目: new 返回的是一个指向元素的指针,而不是数组。所以不能对返回值调用begin 和end ,同样的也不能使用范围for循环。
2.1.1 初始化动态数组
值初始化: 列表初始化,当初始值数目小于元素数目,剩下的元素将被值初始化,如果初始值数目大于元素数目,将抛出bad_array_new_length 异常: 由于不能用括号包裹初始化器进行初始化,所以不能用auto分配数组。
当试图将动态数组元素数量设置为0时,代码能正常工作,会得到一个合法的非空指针,可以用该指针加上0,或者减去自身。但无法访问。
2.1.2 释放动态数组
释放动态数组和释放单个元素不同,要加上方括号: 先销毁pa的元素,再释放内存,元素按逆序销毁。 使用数组的类型别名创建动态数组时,new表达式不需要[] 。但是删除时,无论如何都要带上[] 。
2.1.3 智能指针和动态数组
标准库提供了可以管理new分配数组的unique_ptr版本,定义该unique_ptr时,在类型名后跟上[] 。 由于up指向数组,所以当unique_ptr被释放时,会调用delete[] 。 和unique_ptr不同,shared_ptr不支持直接管理动态数组,如果一定要使用shared_ptr管理动态数组,需要提供自己定义的删除器,如果不提供删除器,默认会使用delete去销毁一个动态数组,这是未定义的行为: 即使用改变了删除器的版本去管理动态数组,也会有问题:
2.2 allocator类
使用new可能会造成空间浪费。new将分配内存和构造对象组合在一起,可能创建了一些永远也用不到的对象,还会导致没有默认构造函数的类无法动态分配数组。 如上,分配了n个string对象的空间,但可能只用了前几个,就被delete[]了。
allocator分配的内存是原始的、未构造的。 allocate只是分配了这么大的空间,里面什么都没有: 没有构造的原始内存是不能使用的。需要调用construct构造后才能使用: 用完对象后需要对每个对象调用destrory完成对象销毁。 销毁完对象的空间可以再次使用,或者释放内存(全部释放,需要把所有构造的对象destroy): allocator可以拷贝和填充未被初始化的内存(但是需要先被allocate分配): uninitialized_copy返回最后一个构造的元素之后的元素。
|