C++ Primer 动态内存

动态内存与智能指针

  1. 静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量
  2. 栈内存用来保存定义在函数内的非static对象
  3. 除了静态内存和栈内存,程序还有一个内存池称为自由空间或者堆(heap),用来存储动态分配的对象,这些对象的生存期由程序控制,必须显式地销毁
  4. 在C++中,管理动态内存使用一对运算符:new和delete,分别进行分配空间和销毁对象
  5. 动态内存使用很容易出现问题,即忘记释放动态内存,而导致内存泄漏,因此为了更加安全地使用动态内存,新的标准库使用两种 智能指针(smart pointer) 类型shared_ptr和unique_ptr来管理动态对象,负责自动释放所指向的对象,定义在头文件memory
  6. 两种智能指针管理底层的方式不同,shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象

shared_ptr类

  1. 智能指针类似于vector也是模板,创建智能指针时必须提供指针可以指向的类型shared_ptr<string> p1;//shared_ptr,可以指向string

  2. 用法和普通指针一样,解引用指针得到所指向的对象,默认初始化为空指针

  3. 最安全的分配和使用动态内存是调用make_shared标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr,定义在memory头文件中

    //指向一个值为42的int的shared_ptr
    shared_ptr<int> p3 = make_shared<int>(42);
    //指向一个值为“9999999999”的string
    shared_ptr<string> p4 = make_shared<string>(10,'9');
    
  4. 类似于顺序容器的emplace成员,make_shared也用其参数来构造给定类型的对象,例如调用make_shared时传递的参数必须与string容器中的某个构造函数相匹配,若不传递任何参数(),则进行值初始化

  5. 也可也用auto来定义一个对象保存make_shared的结果

  6. 可以认为每个shared_ptr都有一个关联的计数器,称其为引用计数,无论何时拷贝/赋值一个shared_ptr计数器都会增加,当给shared_ptr赋新值或者被销毁,则计数器减少

  7. 一旦一个shared_ptr的计数器变为0,它就会自动释放自己多管理的对象

  8. shared_ptr通过析构函数来完成销毁工作,控制此类型的对象销毁时做什么操作,当计数器为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存

  9. 如果唯一的智能指针被销毁,那么它指向的那块动态分配的内存也会被释放,如果还有其他的shared_ptr指向这块内存,那么它就不会被释放。例如局部对象离开作用域,该对象会被销毁,唯一指向的动态内存会被释放,但是如果return p;即返回一个p的拷贝,那么shared_ptr的计数器会增加,,当p离开作用域被销毁时,p所指向的动态内存不会被释放

  10. 如果你将shared_ptr存放在一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得erase删除其中不再需要的那些元素

  11. 使用动态内存的一个常见原因时允许多个对象共享相同的状态。例如书中的例子,我们为了实现所希望的数据共享,为每一个类设置一个shared_ptr来管理动态分配的vector,该shared_ptr的成员将记录有多少个类共享相同的vector,并在vector的最后一个使用者被销毁时释放vector

  12. 相对于使用智能指针,使用newdelete运算符管理内存容易出错

  13. 在自由空间即堆上分配的内存是无名的,因此new返回的是一个指向该无名对象的指针,默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型的对象将使用默认构造函数进行初始化

  14. 我们也可使用直接初始化(使用圆括号)int *p = new int(1024);或者列表初始化来初始化一个动态分配的对象

  15. 也可以对动态分配的对象进行值初始化。只需要在类型后面加一对空括号int *p = new int();//值初始化为0

  16. 值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值是未定义的

  17. 也可以使用auto来推断单一初始化器的类型auto p = new auto(obj);//p指向一个与obj类型相同的对象

  18. 动态分配的const对象必须初始化,定义了默认构造函数的类则会隐式初始化const string *p = new const string;//分配并默认初始化一个const的空string

  19. 当内存耗尽时,程序会抛出bad_alloc的异常,使用int *p = new (nothrow) int;//如果分配失败,new返回一个空指针来阻止它抛出异常

  20. bad_allocnothrow都在new头文件

  21. delete表达式需要两个步骤:先销毁给定指针所指向的对象;释放对应的内存

  22. 传递给delete的指针必须是指向动态分配的内存,或者是一个空指针,如果指向一块非new分配的内存,或者将相同的指针值释放多次,会导致未定义的行为

  23. const对象不能被改变,但是可以被销毁

  24. 由内置指针(而非智能指针)管理的动态内存在被显式释放之前一直都会存在

  25. delete一个指针后,该指针可能仍然保存着(已经释放了的)动态内存的地址,这个指针就变成了空悬指针(dangling pointer),即指向一块曾经保存数据对象但现在已经无效的内存的指针

  26. 为了避免空悬指针问题,在delete之后将nullptr赋值给该指针,清楚地指出指针不指向任何对象

  27. 函数定义的返回值类型如果和实际的返回类型不匹配,导致的类型转换可能会导致内存泄漏,例如指针类型被转换成bool类型,那么该指针再也无法被delete(delete只接受指针类型),造成内存泄漏

  28. 接受指针参数的智能指针构造函数是explicit的,因此不能将一个内置指针隐式转换成一个智能指针,必须使用直接转换(使用圆括号)

    shared_ptr<int> p1 = new int(1024);//错误:必须使用直接初始化的方式
    shared_ptr<int> p2(new int(1024));//正确:使用直接初始化的方式
    

    同样,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针

    shared_ptr<int> clone(int p)
    {
    	return  new int(p);//错误:无法隐式转换为shared_ptr
    }
    shared_ptr<int> clone(int p)
    {
    	//正确,显式地使用int *创建shared_ptr
    	return  shared_ptr<int>(new int(p));
    }
    
  29. 不要混用普通指针和智能指针,当使用pass by value的方式传递shared_ptr参数时,会进行拷贝,即增加该智能指针的引用次数,当函数调用结束后,该指针的引用次数减1,但是不会为0,因此所指向的内存不会被释放

  30. 如果给函数传递一个由内置指针 p 显示构造的shared_ptr临时对象,那么在传值时引用计数为1,函数结束后,引用计数递减为0,所指向的内存会被释放,该内置指针p继续指向(已经释放了的)内存,变成了空悬指针,无法再使用该指针

  31. 智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象,get用来将指针的访问权限传递给代码,只有在确定代码不会delete指针的情况下,才能使用get,不然这个内置指针被销毁后,它指向的内存也会被释放,原来的智能指针变成了空悬指针,或者当这个智能指针被释放的时候,则这块内存被释放了两次

  32. 如果在资源分配和释放之间发生了异常,且该异常没有被捕获,则使用内置指针管理的内存永远也不会被释放了,若使用的是智能指针,那么当函数结束后,该指针会自动释放内存

  33. shared_ptr<T> p(q,d);//p接管了内置指针q所指向的对象,q必须能转换成T *类型,p将使用可调用对象d来代替delete

  34. p.reset(q,d);//将p释放,置为空,若还传递了参数d,将会调用d而不是delete来释放p

  35. 为了使shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete,这个删除器deleter)函数必须能够完成对shared_ptr中保存的指针进行释放的操作

    shared_ptr<connection> p(&c,end_connection);
    

    p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection

  36. 智能指针的陷阱:
    (1)不使用相同的内置指针值初始化(或reset)多个智能指针
    (2)不delete get()返回的指针
    (3)不使用get()初始化或reset另一个智能指针
    (4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
    (5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它的一个删除器

unique_ptr类

  1. 不同于shared_ptr,定义unique_ptr需要将其绑定到一个new返回的指针上,类似地,初始化unique_ptr 必须采用直接初始化的方式unique_ptr<double> p2(new int(42));

  2. 由于unique_ptr拥有(霸占)它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作(()、= )

  3. unique_ptr虽然不支持拷贝或赋值调用release或reset将指针(非const)所有权转移给另一个unique_ptr

    u.release()//u放弃对指针的控制权,返回指针,并将u置为空
    u.reset()//释放u所指向的对象
    u.reset(q)//如果提供了内置指针q,令u指向这个对象;
    u.reset(nullptr)//否则将u置为空
    
  4. unique_ptr<string> p2(p1.release());//release将p1置为空,并将所有权转移给p2

  5. p2.reset(p3.release());//reset释放了原来指向的内存,reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针,如果unique_ptr不为空,则原来指向的对象被释放,然后,将p3对指针的所有权转移给p2,并将p3置为空

  6. 使用release成员函数必须要有一个指针来保存返回的指针,否则

    p2.release();//p2不会释放内存,而且我们也丢失了指针
    
  7. 不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr,最常见的就是从函数返回一个unique_ptr,可以由参数创建的,也可以是返回一个局部对象unique_ptr的拷贝

  8. 具体例子说明向unique_ptr传递删除器

    void f(destination &d /*其他需要的参数*/)
    {
    	connection c = connect(&d);//打开链接
    	//当p被销毁时,连接会关闭
    	unique_ptr<connection, decltype(end_connection)*>
    		p(&c, end_connection);
    	//使用连接
    	//当f退出时(即使是由于异常而退出),connection会被正确关闭
    }
    

    decltype(end_connection)* 用于提供 删除器 的类型,由于decltype只能返回函数类型,所以要加一个*来指明我们使用的是该类型的一个指针(指明函数类型时使用这个格式,需要加 * )
    end_connection为删除器

  9. IntP p5 (p2.get()); // 不合法,使用get初始化一个智能指针,p2和p5指向同一块内存,当指针非法,智能指针会自动delete,此时这块内存会被二次delete

  10. weak_ptr是一种不控制所指向对象生存期的智能指针

  11. 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,这是一种弱共享,因此weak_ptr所指向的对象可能被释放,所以用lock()函数来检查指向的对象是否还存在

    if(shared_ptr<int> np = wp.lock()){ //如果np不为空则条件成立
    	//在if中,np与p共享对象
    }
    
  12. 作为weak_ptr的一个用途,用一个伴随指针类保存weak_ptr,来核查伴随指针类指向的对象是否被释放

动态数组

  1. 使用容器的类可以使用默认版本的拷贝构造、拷贝赋值和析构函数,而分配动态数组的类必须要定义自己版本的拷贝构造、拷贝赋值和析构函数

  2. 记住,我们所说的动态数组new T[]不是数组类型,返回的是一个元素类型的指针,因此不能对动态数组调用begin或end等int *p = new int[10];

  3. 初始化动态分配对象的数组,在大小后面加一对圆括号进行值初始化

    int *pia = new int[10];//10个未初始化的int
    int *pia2 = new int[10]();//10个值初始化为0的int
    

    提供初始化器的花括号列表

    //10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
    string *pia3 = new string[10]{"a","an","the",string(3,'x')};
    

    如果初始化器数目大于元素数目,则new表达式会失败,不会分配任何内存

  4. 虽然可以用空括号对数组进行值初始化,但是不能在括号中添加初始化器

  5. new可以分配一个大小为0的动态数组(静态数组的大小不能为0),返回一个合法的非空指针

  6. 释放动态数组delete [ ] pa,pa必须指向一个动态分配的数组或为空

  7. 标准库提供了可以管理new分配的数组的unique_ptr版本unique_ptr<int []> up(new int[10]);

  8. 当上述的up销毁(release)它管理的指针时,它会自动使用delete []

  9. 当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符,我们可以使用下标运算符来访问数组中的元素up[]

  10. unique_ptr 不同,shared_ptr不直接支持管理动态数组,如果有需要,则必须要自己定义删除器:

    //提供一个删除器
    shared_ptr<int> sp(new int[10],[](int *p) {delete[] p; });
    sp.reset();//使用我们提供的lambda释放数组,它使用delete[]
    

    注意尖括号中指定类型与 unique_ptr 的不同

  11. 但是shared_ptr没有定义下标运算符,而且智能指针不支持指针的算术运算,所以为了访问数组中的元素shared_ptr需要调用**get()**来获取一个内置指针进行指针的算术运算,来访问数组的元素

    //shared_ptr未定义下标运算符
    for (size_t i = 0;i != 10; ++i)
    	*(sp.get() + i) = i;//使用get获得一个内置指针
    

allocator类

  1. 标准库allocator类定义在头文件memory中,帮助我们将内存分配和对象构造分离开来

  2. allocator类提供一种类型感知的内存分配方法,分配的内存是原始的、未构造的

  3. 当定义一个allocator对象必须指明对象类型

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

    这些由allocator分配的内存是 未构造 的,我们按照需要在此内存中构造对象

  4. 利用成员函数construct构造,该成员函数接受一个指针和若干个额外参数,额外参数必须是与构造的对象类型相匹配的初始化器alloc.construct(q++,10,'c');//*q为cccccccccc,这里的q指向最后构造的元素之后的位置

  5. 还未构造对象的情况下就使用原始内存是错误的,想要使用allocator分配的内存,必须使用construct构造对象

  6. 使用完对象后,必须对每个构造的元素调用destroy来销毁(只能对真正构造了的元素进行该操作),该函数接受一个指针,对指向的对象调用析构函数

    while(q != p)
    {
    	alloc.destroy(--q);//释放我们真正构造的string
    }
    

    由于之前的q指向的是最后构造的元素之后的位置,所以先对它进行递减操作,则q指向了最后一个元素

  7. destroy操作是销毁构造的元素,如果要将该内存归还给系统,则需要调用deallocate来完成alloc.deallocate(p,n);我们传递的p不能为空,必须指向由allocator分配的内存,且n必须和创建时的大小一样

  8. allocator类的伴随算法,用于在未初始化的内存中创建对象

    uninitialized_copy(b,e,b2);//b,e为迭代器,从b2指定的未初始化内存开始
    uninitialized_copy_n(b,n,b2);//从迭代器b指向位置开始的n个元素
    uninitialized_fill(b,e,t);//全部用t填充
    uninitialized_fill_n(b,n,t);//从迭代器b指向位置开始的n个元素
    

    举例:

    auto p = alloc.allocate(vi.size() * 2);
    auto q = uninitialized_copy(vi.begin(),vi.end(),p);
    uninitialized_fill_n(q,vi.size(),42);
    

    uninitialized_copy调用返回一个指针,指向最后一个构造的元素之后的位置

猜你喜欢

转载自blog.csdn.net/qq_41423750/article/details/103157534