十二章 动态内存
静态内存
:保存局部static对象、类static数据成员和定义在任何函数之外的变量
栈内存
:保存定义在函数内的非static对象
分配在静态内存
或栈内存
中的对象由编译器自动创建和销毁
栈对象:仅在其定义的程序块运行时才存在
static对象:在使用之前分配,程序结束时销毁
堆(heap)
:自由空间(free store),存储动态分配的对象(在程序运行时分配的对象)
当动态对象不再使用时,必须显式销毁
12.1 动态内存与智能指针
12.1.1 shared_ptr类
智能指针也是模板
默认初始化的指针中保存着一个空指针
1 2 3 4 5 6 shared_ptr <string > p1;shared_ptr <list <int >> p2;if (p1 && p1->empty()){ *p1 = "hi" ; }
1. make_shared 函数
头文件:memory
最安全的分配和使用动态内存的方法
1 2 3 4 5 6 7 8 shared_ptr <int > p3 = make_shared<int >(42 );shared_ptr <string > p4 = make_shared<string >(10 ,'9' );shared_ptr <int > p5 = make_shared<int >();auto p6 = make_shared<vevtor<string >>();
2. shared_ptr的拷贝和赋值
引用计数
(reference count)
每个shared_ptr都会记录有多少个shared_ptr指向相同的对象
递增
用一个shared_ptr初始化另一个shared_ptr
将它作为参数传递给一个函数,作为函数的返回值
递减
给shared_ptr赋予一个新值
shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)
一旦计数器变为0,自动释放所管理的对象
1 2 3 4 5 6 7 8 auto p = make_shared<int >(42 ); auto q (p) ; auto r = make_shared<int >(42 ); r = q;
3. shared_ptr
自动销毁所管理的对象,自动释放相关联的内存
如果将shared_ptr
存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素。
4. 使用了动态生存期的资源的类
程序使用动态内存的原因:
程序不知道自己需要使用多少对象(比如容器类)
程序不知道所需对象的准确类型
程序需要在多个对象间共享数据
当拷贝一个vector时,原vector和副本vector中的元素相互分离:
1 2 3 4 5 6 vector <string > v1; { vector <string > v2 = {"a" , "an" , "the" }; v1 = v2; }
PS: 使用动态内存的一个常见原因:允许多个对象共享相同的状态。
5. 定义StrBlob类
类成员函数声明为const 以表明它们不修改类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class StrBlob {public : typedef std ::vector <std ::string >::size_type size_type; StrBlob(); StrBlob(std ::initializer_list <std ::string > il); size_type size () const { return data->size(); } bool empty () const { return data->empty(); } void push_back (const std ::string & t) { data->push_back(t); } void pop_back () ; std ::string & front () ; std ::string & back () ;private : std ::shared_ptr <std ::vector <std ::string >> data; void check (size_type i, const std ::string & msg) const ; };
6. Strblob构造函数
1 2 StrBlob::StrBlob() : data(make_shared<vector <string >>()){} StrBlob::StrBlob(std ::initializer_list <std ::string > il) : data(make_shared<vector <string >>(il)){}
接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数,此构造函数通过拷贝列表中的值来初始化vector的元素。
7. 元素访问成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void StrBlob::check (size_type i, const std ::string & msg) const { if (i >= data->size()) throw out_of_range(msg); }string & StrBlob::front () { check(0 , "front on empty StrBlob" ); return data->front(); }string & StrBlob::back () { check(0 , "back on empty StrBlob" ); return data->back(); }void StrBlob::pop_back () { check(0 ,"pop_back on empty StrBlob" ); data->pop_back(); }
12.1.2 直接内存管理 1. 使用new
动态分配和初始化对象
值初始化的内置类型对象有着良好定义的值
默认初始化的对象的值是未定义的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int *pi = new int ; string *ps = new string ; int *pi = new int (1024 ); string *ps = new string (10 , '9' );vector <int > *pv = new vector <int >{0 ,1 ,2 ,3 ,4 ,5 }; string *ps1 = new string ; string *ps = new string (); int *pi1 = new int ; int *pi2 = new int (); auto p1 = new auto (obj);
2. 动态分配的 const 对象
1 2 const int *pci = new const int (1024 );const string *pcs = new const string ;
3. 内存耗尽
默认情况,如果 new 不能分配所需要的内存空间,会抛出一个类型为bad_alloc
的异常
bad_alloc
和nothrow
定义在头文件new
中
1 2 3 int *p1 = new int ; int *p2 = new (nothrow) int ;
4. 释放动态内存
delete
销毁给定的指针指向的对象;释放对应的内存
5. 指针值和 delete
传递给delete的指针必须指向动态分配的内存或一个空指针
1 2 3 4 5 6 7 8 9 10 int i, *pi1 = &i, *pi2 = nullptr ;double *pd = new double (33 ), *pd2 = pd;delete i; delete pi1; delete pd; delete pd2; delete pi2; const int *pci = new const int (1024 );delete pci;
6. 动态对象的生存期直到被释放时为止
有内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在
1 2 3 4 5 6 7 8 9 Foo* factory (T arg) { return new Foo(arg); }void use_factory (T arg) { Foo *p = factory(arg); }
1 2 3 4 void use_factory (T arg) { Foo *p = factory(arg); delete p; }
当其他代码要使用use_factory所分配的对象,修改函数让它返回一个指针,指向它分配的内存
1 2 3 4 Foo* use_factory (T arg) { Foo *p = factory(arg); return p; }
6. 使用new
和delete
管理动态内存存在的三个常见问题:
忘记delete
内存,导致内存泄露
使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误
同一块内存释放两次
PS: 坚持只是用`智能指针`可以避免所有这些问题。对于一块内存,只有在设有任何智能指针指向它的情况下,智能指针才会自动释放他。
7. delete
之后重置指针值,但这只提供了有限的保护
1 2 3 4 int *p (new int (42 )) ; auto q = p; delete p; p = nullptr ;
注意:此时 q 变为了空悬指针
(dangling pointer),指向一块曾经保存数据对象但现在已经无效的内存的指针。
12.1.3 shared_ptr 和new 结合使用
接受指针参数的智能指针构造函数是 explicit
的
explicit
关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
不能将一个内置指针隐式转换为一个智能指针
1 2 shared_ptr <int > p2 (new int (1024 )) ;
一个返回shared_ptr
的函数不能在其返回语句中隐式转换一个智能指针
1 2 3 4 5 6 7 8 shared_ptr <int > clone (int p) { return shared_ptr <int >(new int (p)); }
1. 不要混合使用普通指针和智能指针
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁
2. 不要使用get
初始化另一个智能指针或为智能指针赋值
1 2 3 4 5 6 7 shared_ptr <int > p (new int (42 )) ;int *q = p.get(); { shared_ptr <int >(q); }int foo = *p;
3. 其他shared_ptr
操作
用reset
来将一个新的指针赋予一个shared_ptr
1 2 p = new int (1024 ); p.reset(new int (1024 ));
与unique
一起使用,控制多个shared_ptr
共享的对象
1 2 3 if (!p.unique()) p.reset(new string (*p)); *p += newVal;
12.1.4 智能指针和异常
1. 智能指针和哑类
为 C 和 C++ 两种语言设计的类,通常都要求用户显式地释放所使用地任何资源
1 2 3 4 5 6 7 8 9 10 struct destination ; struct connection ; connection connect (destination*) ;void disconnect (connection) ;void f (destination &d) { connection c = connect(&d); }
2. 使用我们自己的释放操作
定义一个删除器(deleter) 函数完成对shared_ptr中保存地指针进行释放操作
1 2 3 4 5 6 7 8 void end_connection (connection *p) {disconnect(*p);}void f (destination &d ) { connection c = connect(&d); shared_ptr <connection> p (&c, end_connection) ; }
3. 正确使用智能指针的基本规范
不使用相同的内置指针初始化(或 reset)多个智能指针
不 delete get()返回的指针
不使用 get() 初始化或 reset 另一个智能指针
如果你使用 get() 返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变成无效了。
如果你使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器
12.1.5 unique_ptr
1 2 3 4 5 6 7 8 9 10 11 12 13 unique_ptr <string > p1 (new string ("Stegosaurus" )) ; unique_ptr <string > p3;unique_ptr <string > p2 (p1.release()) ; unique_ptr <string > p3 (new string ("Trex" )) ; p2.reset(p3.release()); p2.release(); auto p = p2.release();
1. 传递unique_ptr
参数和返回unique_ptr
例外:可以拷贝或赋值一个将要被销毁的unique_ptr,例如从函数返回一个unique_ptr
1 2 3 4 5 6 7 8 9 10 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; }
2. 向unique_ptr
传递删除器
dectype返回一个函数类型,添加一个*来指出我们正在使用该类型的一个指针
1 2 3 4 5 6 7 8 9 10 11 12 unique_ptr<objT, delT> p(new objT, fcn);void f (destination &d ) { connection c = connect(&d); unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection); }
12.1.6 weak_ptr
指向由一个shared_ptr
管理的对象
将一个weak_ptr
绑定到一个shared_ptr
不会改变shared_ptr
的引用计数
要用一个shared_ptr来初始化一个weak_ptr
由于对象可能不存在,不能使用weak_ptr直接访问对象,必须调用lock函数
1 2 3 4 5 6 auto p = make_shared<int >(42 );weak_ptr<int > wp (p) ;if (shared_ptr <int > np = wp.lock()){ }
1. 检查指针类
weak_ptr的用途:阻止用户访问一个不再存在的vector的企图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class StrBlobPtr {public : StrBlobPtr(): curr(0 ) { } StrBlobPtr(StrBlob &a, size_t sz = 0 ) : wptr(a.data), curr(sz) {} std ::string & deref () const ; StrBlobPtr& incr () ; private : std ::shared_ptr <std ::vector <std ::string >> check (std ::size_t , const std ::string &) const ; std ::weak_ptr<std ::vector <std ::string >> wptr; std ::size_t curr; }std ::shared_ptr <std ::vector <std ::string >> check (std ::size_t i, const std ::string &msg) const { auto ret = wptr.lock(); if (!ret) throw std ::runtime_error("unbound StrBlobPtr" ); if (i >= ret->size()) throw std ::out_of_range(msg); return ret; }
2. 指针操作
定义函数deref
用来解引用StrBlobPtr
incur
递增StrBlobPtr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 std ::string & StrBlobPtr::deref () cosnt { auto p = check(curr, "dereference past end" ); return (*p)[curr]; }StrBlobPtr& StrBlobPtr::incr () { check(curr, "increment past end of StrBlobPtr" ); ++curr; return *this ; }class StrBlobPtr ;class StrBlob { friend class StrBlobPtr ; StrBlobPtr begin () { return StrBlobPtr(*this );} StrBlobPtr end () { auto ret = StrBlobPtr(*this , data->size()); return ret; } }
12.2 动态数组
大多数应用应该使用标准库容器 而不是动态分配的数组。
使用容器更为简单、更不容器出现内存管理错误并且有可能有更好的性能。
参考:StrBlob类
12.2.1 new 和数组
1 2 3 4 5 6 int *pia = new int [get_size()]; typedef int arrT[42 ]; int *p = new arrT; int *p = new int [42 ];
1. 分配一个数组会得到一个元素类型的指针
通常称new T[]分配的内存为“动态数组”
动态数组并不是数组类型
不能对动态数组调用begin或end
2. 初始化动态分配对象的数组
new 表达式失败会抛出一个类型为bad_array_new_length
的异常
头文件:new
1 2 3 4 int *pia = new int [10 ]; int *pia2 = new int [10 ](); int *pia3 = new int [10 ]{0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 };
3. 动态分配一个空数组是合法的
1 2 3 4 5 6 7 8 size_t n = get_size();int * p = new int [n];for (int * q = p; q != p+n; ++q){ }char *cp = new char [0 ];
4. 释放动态数组
1 2 3 4 5 6 delete p; delete [] pa; typedef int arrT[42 ]; int *p = new arrT; delete [] p;
5. 智能指针和动态数组
unique_ptr
不能使用点和箭头成员运算符
可以使用下标运算符来访问数组中的元素
1 2 3 4 5 unique_ptr<int[]> up(new int[10]); //up指向一个包含10个未初始化int的数组 up.release(); for (size_t i = 0 ; i != 10 ; ++i) up[i] = i;
1 2 3 4 5 shared_ptr <int > sp (new int [10 ], [](int *p){delete [] p; }) ; sp.reset();for (size_t i = 0 ; i != 10 ; ++i) *(sp.get() + i) = i;
12.2.2 allocator类
分配单个对象时,希望将内存分配和对象初始化组合在一起
分配一大块内存时,通常计划在这块内存上按需构造对象,此时希望将内存分配和对象构造分开
将内存分配和对象构造组合在一起可能会导致不必要的浪费
1 2 3 4 5 6 7 string *const p = new string [n];string s;string *q = p;while (cin >> s && q != p+n) *q++ = s;const size_t size = q-p;delete [] p;
1. allocator类
头文件:memory
帮助我们将内存分配和对象构造分离开
提供一种类型感知的内存分配方法
分配的内存是原始的、未构造的
1 2 allocator<string > alloc; auto const p = alloc.allocate(n);
2. allocator 分配未构造的内存
为了使用 allocate 返回的内存,必须用construct
构造对象
使用未构造的内存,其行为是未定义的
函数destory
接受一个指针,对指向的对象执行析构函数
释放内存通过调用deallocate
来完成
1 2 3 4 5 6 7 8 9 10 11 12 auto q = p; alloc.construct(q++); alloc.construct(q++, 10 , 'c' ); alloc.construct(q++, "hi" ); cout << *p << endl ; while (q != p) alloc.destory(--q); alloc.deallocate(p, n);
3. 拷贝和填充未初始化内存的算法
1 2 3 4 5 6 auto p = alloc.allocate(vi.size() * 2 );auto q = uninitialized_copy(vi.begin(), vi.end(), p); uninitialzed_fill_n(q, vi.size(), 42 );
12.3 使用标准库:文本查询程序
允许用户在一个给定文件中查询单词
查询结果是单词在文件中出现的次数及其所在行的列表
如果一个单词在一行中多次出现,此行只列一次
行按照升序输出
12.3.1 文本查询程序设计
数据结构
保存输入文件的类TextQuery
vector
保存输入文件的文本
map
关联每个单词和它出现的行号的set
类QueryResult
:保存查询结果,完成结果打印工作
在类之间共享数据
使用TextQuery
类
当我们设计一个类时,在真正实现成员之前先编写程序使用这个类,是一种非常有用的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void runQueries (ifstream &infile) { TextQuery tq (infile) ; while (true ){ cout << "enter word to look for ,or q to quit:" ; string s; if (!(cin >> s) || s == "q" ) break ; print(cout , tq.query(s)) << endl ; } }
12.3.2 文本查询类的定义 1 2 3 4 5 6 7 8 9 10 11 class QueryResult ; class TextQuery {public : TextQuery(std ::ifstream&); QueryResult query (const std ::string &) const ;private : std ::shared_ptr <std ::vector <std ::string >> file; std ::map < std ::string , shared_ptr <std ::set <line_no>>> wm; };
TextQuery 构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 TextQuery::TextQuery(ifstream& is) : file(new vector <string >) { string text; while (getline(is, text)) { file->push_back(text); int n = file->size() - 1 ; istringstream line (text) ; string word; while (line >> word) { auto & lines = wm[word]; if (!lines) lines.reset(new set <line_no>); lines->insert(n); } } }
QueryResult类
1 2 3 4 5 6 7 8 9 10 class QueryResult { friend std ::ostream& print (std ::ostream&, const QueryResult&) ;public : QueryResult(std ::string s, std ::shared_ptr <std ::set <line_no>>p, std ::shared_ptr <std ::vector <std ::string >> f): sought(s), lines(p),file(f){}private : std ::string sought; std ::shared_ptr <std ::set <line_no>> lines; std ::shared_ptr <std ::vector <std ::string >> file; };
query函数
1 2 3 4 5 6 7 8 9 10 11 QueryResult TextQuery::query (const string & sought) const { static shared_ptr <set <line_no>> nodata (new set <line_no>) ; auto loc = wm.find(sought); if (loc == wm.end()) return QueryResult(sought, nodata, file); else return QueryResult(sought, loc->second, file); }
打印结果
1 2 3 4 5 6 7 8 9 10 11 ostream& print (ostream& os, const QueryResult& qr) { os << qr.sought << " occurs " << qr.lines->size() << " " << make_plural(qr.lines->size(), "times" , "s" ) << endl ; for (auto num : *qr.lines) os << "\t(line " << num + 1 << " )" << *(qr.file->begin() + num) << endl ; return os; }