【笔记】《C++Primer》—— 第12章:动态内存

时间:2022-07-22
本文章向大家介绍【笔记】《C++Primer》—— 第12章:动态内存,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

这一章介绍了标准库对动态内存的管理方面,其中12.1的几个智能指针是C11引入的非常实用的类。这章对优化C++代码的编写有很大意义,值得好好理解。至此第二部分"C++标准库"就看完了,下一篇是第二部分简单的总结,然后就是第三部分了。

12.1 动态内存与智能指针

  • 静态内存/栈内存,动态内存:
    • 静态内存用来保存局部static对象,类static成员以及定义在函数之外的变量,使用前分配,程序结束时销毁
    • 栈内存也属于静态内存,用来保存函数内的非static对象,由编译器分配和销毁
    • 动态内存(自由空间,堆空间)用来储存程序运行期间分配的对象,生存期由程序控制,我们必须显式销毁它
  • 动态内存在C++中由new进行分配,由delete进行释放
  • 为了优化动态内存的管理,标准库在头文件memory中定义了两个智能指针:允许多个指针指向同个对象的shared_ptr,指针独占对象的unique_ptr,还有一个伴随的弱引用指针weak_ptr。和容器类类似,智能指针也是模板类

12.1.1-12.1.4 shared_ptr

  • 通常讲到C++的智能指针就是指shared_ptr,其操作如下
  • 智能指针的操作并不复杂,归功于C++强大的自定义能力,除了初始化之外很多时候操作与内置指针相同的。智能指针的优势在于它帮用户管理了关于动态内存对象的引用和销毁
  • 最方便的使用动态内存的方式是调用make_shared函数,它使用参数args初始化类型为T的对象并返回指向这个对象的智能指针,当我们想要用new的时候可以用这个函数来替代,这里提到了本书的一大建议:尽量少用new和delete这样低层的操作

void test() {
        // 传统的用new分配动态内存空间,返回值是对应类型的指针,构造的目标是默认初始化的
        int* p1 = new int;
        // 在类型符后加小括号后,目标会自动进行值初始化,更实用
        int* p2 = new int();
        // 用make_shared得到int的智能指针,此时会自动值初始化
        shared_ptr<int> p3 = make_shared<int>();
        // 也可以用内置指针来初始化智能指针,同样也可以直接用new的返回值
        shared_ptr<int> p4 = make_shared<int>(p2);
        // 如果用new分配动态内存的话记得在函数结尾要delete
        delete p1;
        // 如果此处没有delete p2的话,p2申请的空间将失去指向的指针,称为内存泄漏
        // 但是智能指针由于内部保存了对空间的引用次数值,且拥有自动的析构函数
        // 因此函数结束时指针的引用数会自动减少,当减少到0时会自动析构delete其内部的指针,防止了内存泄漏
}
  • 由于智能指针内有引用计数,所以可以让多个智能指针指向同个对象共享数据,并以此管理内存的释放
  • new是可以分配const对象的,且new有一定的类型推断能力,前提是初始化器只包括一个对象

// const初始化
const int* p = new const int;
// 推断类型
auto p2 = new auto(1);
  • 申请的动态内存当不用用到时一定要用delete销毁,因为动态对象的生存期是直到被delete销毁为止的,最常见的错误就是在函数里用局部指针new了一块内存后函数结束时没有delete造成内存泄漏
  • 要注意一块内存只能delete一次,多次delete是未定义的,因此delete内存后,为了防止多次delete最好将其赋值nullptr,否则称为空悬指针/野指针
  • 但是要知道记录好多个指向同块内存的指针并都赋值nullptr标记是困难的,最好的方法还是使用智能指针
  • 智能指针有几个改变的方法,且一样可以改变delete内部指针的方法,改变指向对象的方法主要是用reset函数。注意reset函数常常与unique配合,用来给那些指向唯一内存的指针重新赋值
  • 注意不要把智能指针和内置指针混用,让智能指针和内置指针都指向同一块内存容易导致引用问题,我们将无法确切得知合适这个对象应该被销毁
  • 类似的也不要用智能指针的get函数提取内部的指针出来构造别的智能指针,因为这样引用计数无法传递,get函数是用来适配一些无法传入智能指针的函数而出现的
  • 当程序跳出异常时,在delete前用new分配的内存不会自动释放,而智能指针仍然能在正确的时候释放
  • 如果要给智能指针调用新的的删除器函数,需要在构造指针时第二个参数传入一个可调用对象,且此对象的参数必须时一个该类型元素的指针

// 自定义的删除器函数,常常用来处理那些由工厂产生的对象,如各种connection
void newDeleteFun(int* inp) {
        // do sth
        delete inp;
}

int main() {
        // 第二个参数传入此可调用对象
        auto p = make_shared<int>(2, newDeleteFun);
        return 0;
}
  • 小总结智能指针的几个规范如下:

12.1.5 unique_ptr

  • unique_ptr的特点是它拥有指向的内存,引用计数保持最高为1,因此同一时刻目标内存只能由一个unique_ptr指向
  • unique_ptr的一个特点是没有make_shared函数之类的函数可以使用,我们必须用内置指针来初始化它
// 进行内置指针的初始化,初始化的动态int值为2
unique_ptr<int> p(new int(2));
  • 我们不可以对unique_ptr进行拷贝和赋值,但是我们可以用release和reset函数来转移它的所有权,release会放弃当前指针的所有权并返回其内部的指针,reset则和智能指针一样类似于赋值
  • 尽管我们不能拷贝unique_ptr但是我们可以拷贝和赋值一个即将销毁的unique_ptr,最常见的是在函数返回时使用
  • 我们同样可以像shared_ptr那样自定义指针的删除器,但是我们必须类似指定关联容器的比较器一样在模板尖括号中指出删除器的类型

// 需要指明删除器的类型
unique_ptr<int, decltype(newDeleteFun)*> p(new int(2), newDeleteFun);

12.1.6 weak_ptr

  • 弱指针的是一种不会影响对象生存期的指针,一般用来引用和标识,特点就是对对象的weak_ptr指向不会增加shared_ptr的引用计数
  • 弱指针必须用shared_ptr来赋值或初始化,且使用时必须使用lock函数的返回值来解引用
  • 由于是弱共享,当对象的shared_ptr都被释放weak_ptr也可能不会被释放,这就是lock,use_count,expired等函数存在的意义

12.2 动态数组

  • 我们都知道用new和方括号可以申请一大块连续内存用于初始化一个对象数组,返回值是指向这个数组第一个元素的指针
  • 注意由于返回的终究是个指针所以我们不能对其使用begin等用在数组上的迭代器操作,也无法使用范围for语句
  • 同样使用结尾小括号的方式我们可以对整个数组中的值进行值初始化,也可以带花括号进行列表初始化
  • 尽管我们可以用小括号初始化数组但我们不能在此输入构造器,因此我们不能用auto来推断类型
  • 尽管我们不能定义长度为0的静态数组,但我们可以申请长度为0的动态数组,但是用途非常有限,相当于一个尾后迭代器
  • 为了释放动态数组我们要用delete[]的形式,但是注意方括号形式的delete应只用在动态数组首指针,用在其他的指针上都是未定义的
  • 动态数组一样可以由unique_ptr来管理,我们也一样可用下标访问其中元素
  • 但是shared_ptr不直接支持管理动态数组,当用shared_ptr管理时我们需要提供自己的删除器且不能用下标访问元素而是需要用get得到内置指针来访问
  • 处于灵活性的考虑,有时候我们希望能得到一块连续内存但先不初始化它,此时我们可以用allocator类来处理,而且大多数时候我们用它分配动态数组可以得到更高的效率并更好管理
  • allocator分配的内存是未构造的,因此我们需要用construct函数来构造其中的元素,用destroy来析构元素
  • 当需要批量构造元素到这段内存中时,我们可以使用uninitialized系列算法来填充,使用起来类似于copy函数。其中的uninitialized_copy函数会返回指向最后一个构造的元素的指针