《C++ Primer》读书笔记—第十二章 动态内存
声明:
- 文中内容收集整理自《C++ Primer 中文版 (第5版)》,版权归原书所有。
- 学习一门程序设计语言最好的方法就是练习编程
这一章是难点。
之前程序所使用的对象都是有严格定义的生存期,全局对象在程序启动时分配,在程序结束时销毁。对于局部对象,当我们进入其定义所在的程序块时被创建,在离开时销毁。局部static对象在第一次使用前分配,程序结束时销毁。动态分配的对象的生存期与他们在哪里创建是无关的,只有显式的被释放,这些对象才能被销毁。
一、动态内存与智能指针
1、动态内存的管理通过一对运算符完成,new,在动态内存中为对象分配空间并返回一个指向该对象的指针。可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放相关联的内存。
2、两种智能指针管理动态对象,负责释放所指向的对象。这两者区别在于管理底层指针的方式。
shared_ptr:允许多个指针指向同一个对象
unique_ptr:独占所指向的对象。
weak_ptr伴随类,是一种弱引用,指向shared_ptr所管理的对象。
三者都定义在memory头文件中。
3、shared_ptr类初始化
- 默认初始化
- make_shared函数
- 使用new运算符进行初始化
默认初始化的智能指针中保存着一个空指针。
1 shared_ptr<string> p1; 2 shared_ptr<list<int>> p2;
最安全的方式是使用make_shared函数。
和顺序容器的emplace成员函数类似,make_shared函数使用传递的参数来构造给定类型的对象
1 shared_ptr<int> p3 = make_shared<int>(42); 2 shared_ptr<string> p4 = make_shared<string>(10, '9'); 3 shared_ptr<int> p5 = make_shared<int>();
如果我们不传递任何参数,对象就会进行值初始化。注意区分值初始化和默认初始化的区别。(默认初始化得到的是空指针)
4、为什么使用智能指针而不是new和delete?
可以认为每个shared_ptr对象都有一个关联的计数器,通常称为引用计数。
- 无论何时我们拷贝一个shared_ptr,计数器都会递增,例如
- 一个shared_ptr初始化另一个shared_ptr
- 作为函数参数
- 作为函数返回值
-
当我们给shared_ptr赋一个新值或是shared_ptr被销毁时,计数器就会递减
-
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象
这里要明白一点,shared_ptr对象是一个类类型的对象,它有着自己的生存期
1 { 2 shared_ptr<int> p = make_shared<int>(10); 3 }
make_shared函数在堆上开辟一块动态内存,并将指向该内存的指针包装成智能指针返回给p,这块内存的引用计数为1。(引用计数是对内存而言的)
p是一个局部自动对象,当程序离开块时,p被销毁,引用计数减1。引用计数为0,动态内存被释放。
5、对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,就不会被释放。
如果将shared_ptr存放在一个容器中,而后不需要全部元素,只要使用其中的一部分,记得用erase删除不需要的元素。
6、什么时候需要使用动态内存?
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
这里重点介绍第3种情况
1 vector<string> v1; 2 { 3 vector<string> v2 = {"a", "an", "the"}; 4 v1 = v2; //v2被销毁,v1存放v2中原来的三个元素 5 }
当v2被销毁时,v2中的元素也都被销毁。拷贝vector时,原vector和副本vector中的元素是相互分离的。
与容器不同,如果我们希望对象的不同拷贝之间共享相同的元素,则销毁某个对象时,我们不能单方面地销毁底层数据。
这个时候把需要共享的数据成员保存在动态内存中是一个明智的选择。
1 Blob<string> v1; 2 { 3 Blob<string> v2 = {"a", "an", "the"}; 4 v1 = v2;//v2被销毁了,但v2中的元素还存在着,v1指向最初由v2创建的元素 5 }
7、实现一个新的集合类型的最简单方法就是使用某个标准库容器来管理元素。可以借助标准库类型来管理元素所使用的内存空间。
8、P404定义StrBlob类???
9、自己直接管理类内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。
10、在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针
1 int *pi = new int;//pi指向一个动态分配的,未初始化的无名对象
默认情况下,动态分配的对象是默认初始化的。
- 内置类型或组合类型的对象的值将是未定义的
- 类类型对象将用默认构造函数进行初始化
可以使用直接初始化方式来初始化一个动态分配的对象
1 int *pi = new int(2104); //pi指向对象的值是1024 2 string *ps = new string(10, '9'); //9999999999 3 //10个元素,从0-9 4 vector<int> *pv = new vector<int>{0,1,2,3,4,5,6};
也可以使用值初始化,在类型名后加一对空括号即可
1 string *ps1 = new string; //默认初始化为空string 2 string *ps2 = new string(); //值初始化为空string 3 int *pi1 = new int; //默认初始化,pi1值未定义 4 int *pi2 = new int();//值初始化为0,pi2值为0
string值初始化为空的string,int值初始化为0,
和变量一样,最好对动态分配的对象进行初始化,
可以使用auto来进行类型推断。
1 auto p1 = new auto(obj);
但是对于提供了多个初始值的情况不适用,以下是错误的。
1 auto p2 = new auto{a,b,c};//错误,只能有单个初始化器
11、动态分配const是合法的
1 const int *pci = new const int(1024); //分配并初始化一个const int 2 const string *pcs = new const string; //分配并默认初始化一个const的空string
12、如果new不能分配所要求的内存空间,则会抛出一个类型为bad_alloc的异常。可以改变new的方式阻止抛出异常。
1 int *p1 = new int;//分配失败,new抛出std::bad_alloc 2 int *p2 = new (nothrow) int;//如果分配失败,new返回一个空指针
称为定位new。允许我们向new传递额外的参数。
13、delete操作执行两个动作:销毁给定的指针指向的对象,释放对应的内存。
释放一块非new分配的内存,或者将相同的指针释放多次,都会产生未定义的行为。
const的值不能被改变,但它本身可以被销毁。
14、返回指向动态内存的指针的函数,调用者必须记得释放内存。由内置指针管理的动态内存在被显式释放前一直都会存在。
15、使用new和delete管理动态内存存在三个问题:
1、忘记delete内存,导致内存泄露。不容易被发现,真正内存耗尽后才能检测到错误。
2、使用已经释放掉的内存。
3、同一块内存被释放两次。两个指针指向同一个动态分配对象,如果一个指针进行了delete,对象的内存就归还给自由空间了,如果随后delete第二个指针,就会破坏自由空间。
16、空悬指针:指向一块曾经保存数据对象但现在已经无效的内存的指针。因此,我们应当在delete指针后,将nullptr赋给指针。清楚的指出指针不再指向任何对象。
17、
1 int *p(new int(42)); //p指向动态指针 2 auto q = p; //p和q指向相同的内存 3 delete p; //p和q均变为无效 4 p = nullptr;//指出p 不再绑定到任何对象
虽然我们明确指出p不再指向任何对象,但是释放p指向的内存后,q也变成了空悬指针。虽然在这个程序里我们可以很容易的把q也置为nullptr,但是在实际系统中,找出类似q的这种指针本身就是异常困难的。
18、使用智能指针的另外一个好处是,当有异常发生时,申请的动态内存也会被自动释放
1 void f() 2 { 3 int *p = new int(42);//如果这里发生异常,delete将不被执行 4 delete ip; 5 }
智能指针可避免类似情况。
19、不单是动态内存,一些其他资源,如IO、网络连接等也可以使用智能指针来管理。
比如我们在建立一个网络连接,在使用完毕后需要确保断开连接,否则会造成资源泄漏。
同样可以使用智能指针来管理:
1 void end_connection(connection *p) 2 { 3 disconnect(*p); 4 } 5 6 void f(destination &d) 7 { 8 connection c = connect(&d); 9 shared_ptr<connection> p(&c, end_connection); 10 }
当f退出时,connection会被正确关闭。
20、一个unique_ptr “独享”它所指向的对象,当unique_ptr被销毁时,它所指向的对象也被销毁。
初始化unique_ptr必须采用直接初始化形式:
1 unique_ptr<double> p1; 2 unique_ptr<int> p2(new int(42));
由于一个unique_ptr独享它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。
但是可以拷贝或赋值一个将要被销毁的unique_ptr
1 unique_ptr<int> clone(int p) 2 { 3 return unique_ptr<int>(new int(p)); 4 } 5 6 unique_ptr<int> clone(int p) 7 { 8 unique_ptr<int> ret(new int(p)); 9 return ret; 10 }
还有一种方法可以将指针的所有权转移到另外一个unique_ptr:
1 unique_ptr<string> p2(p1.release());
21、weak_ptr指向由一个shared_ptr管理的对象,但是不会影响它的引用计数。
当创建一个weak_ptr时,要用一个shraed_ptr来初始化它
1 auto p = make_shared<int>(42); 2 weak_ptr<int> wp(p);
由于weak_ptr不影响引用计数,它指向的对象可能不存在,所以不能直接使用weak_ptr访问对象。
一种安全的做法是使用lock函数,如果weak_ptr指向的对象存在,lock函数返回一个指向共享对象的shared_ptr:
1 if(shared_ptr<int> np = wp.lock()) 2 { 3 4 }//见P421-P422
二、动态数组
1、大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。
2、当使用new分配一个数组时,并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。
3、new 和数组:
1 int *pia = new int[get_size()];
- 类型名后跟一对方括号,指明要分配的对象的数目,方括号中的大小必须是整型,但不必是常量。
- 返回指向第一个元素的指针
也可以使用类型别名:
1 typedef int arrT[42]; 2 int *p = new arrT;
要注意,new T[] 返回的是指向首元素的指针而不是数组,这意味着我们无法使用begin、end之类的操作。
初始化:
- 默认初始化
- 值初始化(方括号后加一对空括号)
- 列表初始化(花括号列表)
1 int *pia2 = new int[10](); 2 int *pia3 = new int[10]{1,2,3,4};
如果:
- 初始化器数目小于元素数目,剩余元素将进行值初始化。
- 初始化器数目大于元素数目,new表达式失败,不分配任何内存,抛出一个bad_array_new_length异常。
4、动态分配一个长度为0的数组是合法的:
1 char arr[0];//错误 2 char *cp = new char[10];//正确
此时返回一个合法的非空指针:
1 size_t n = get_size(); 2 int *p = new int[n]; 3 for(int *q = p; q != p+n; ++q) 4 { 5 6 }
如果get_size返回0,上面的代码仍能正确执行。
5、智能指针和动态数组:
1 unique_ptr<int []> up(new int[10]);
必须在尖括号中类型名后加一对空的方括号:
1 up.release();
自动调用delete [] 销毁其指针。
和普通unique_ptr的区别:
- 不能使用点和箭头成员运算符
- 可以使用下标来访问数组中的元素。
1 for(size_t i = 0; i != 10; ++i) 2 { 3 up[i] = i; 4 }
shared_ptr不直接支持管理动态数组。
6、allocator类:
定义在memory头文件中。
将内存分配和对象构造分离开来。
1 allocator<string> alloc; 2 auto const p = alloc.allocate(n);
为n个string分配了内存,分配的内存是未构造的。
可以使用construct函数来构造。
1 allocator<T> a; 2 a.construct(p, args);
类似于make_shared,args传递给构造函数来创建对象。
1 alloc.construct(p++); 2 alloc.construct(p++, 10, 'c');
实际上,应当p进行缓存,以便访问已经构造的元素。
1 auto q = p; 2 alloc.construct(q++); 3 alloc.construct(q++, 10, 'c');
p指向起始位置,q指向下一个待构造的位置。
用完后,必须对每个构造的元素调用destroy来销毁它们。
1 while(q != p) 2 { 3 alloc.destory(--q); 4 }
销毁后,可以重新使用这部分内存,也可以释放:
1 alloc.deallocate(p, n);
n必须与分配内存时提供的大小一样。
三、使用标准库:文本查询程序
1、P430-P435 实现一个文本查询程序,类似于notepad++中的查询功能,是对标准库文件的一个综合性的应用,未看完。
2、参考https://zhuanlan.zhihu.com/p/25258749
3、
3.25周六帮越越做网易的机试题,题目不难,但由于不够熟练,还是做得不是很流畅。跟杨哥实验室的玩狼人杀,屠城一局,好久没玩屠城的局了,太顺。5局3局被首验,几乎完全是裸着打。再次表示新手光环太强大刚不动,做隐狼再合适不过。周末打球。假日总是过得飞快。清明不能回家,还是出去逛逛吧。
3.27一个月时间,只看完435页,离预算的进度有点慢了。打算两个月内看完,现在看来得三个月。之后的内容越来越难,进度越来越慢。有些生涩的概念性的东西看完也不一定懂,还得翻回来再看。有空找越越讲吧。
十二章暂时结束。且随疾风前行。