《C++ Primer》学习笔记(十二):动态内存 妖狐艹你老母 2023-02-09 14:12 76阅读 0赞 ### 《C++ Primer》学习笔记(十二):动态内存 ### * 动态内存与智能指针 * * shared\_ptr * 内存耗尽 * shared\_ptr与new结合使用 * 智能指针和异常 * unique\_ptr * weak\_ptr * 动态数组 * * allocator类 * 使用标准库设计文本查询程序 * 练习 程序用堆来存储动态分配的对象,当动态对象不再使用时,我们的代码必须显式地销毁它们。 # 动态内存与智能指针 # C++中动态内存的管理是通过`new`和`delete`来完成的。为了更安全地使用动态内存,提供了两种**智能指针**类型来管理动态对象,均定义在`memory`头文件中。 * `shared_ptr`:允许多个指针指向同一个对象; * `unique_ptr`: 独占所指向地对象; * `weak_ptr`:是一种指向`shared_ptr`所管理对象的弱引用。 ## shared\_ptr ## ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70] ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 1] //指向一个值为42的int的shared_ptr shared_ptr<int> p3 = make_shared<int>(42); //p4指向一个值为"9999999999"的string shared_ptr<string> p4 = make_shared<string>(10,'9'); //p5指向一个值初始化的int,即值为0 shared_ptr<int> p5 = make_shared<int>(); auto p = make_shared<int>(42);//p指向的对象只有p一个引用者 auto q(p);//p和q指向相同对象,此对象有两个引用者 每个`shared_ptr`都有一个关联的计数器,称为引用计数。一旦引用计数变为0,它就会自动释放自己所管理的对象。 void use_factory(T arg) { shared_ptr<Foo> p = factory(arg); //使用p }//p离开了作用域,它指向的内存会被自动释放掉 **注意**:如果你将`shared_ptr`存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用`erase`删除不再需要的那些元素。 使用动态内存的一个常见原因是允许多个对象共享相同的状态。 class StrBlob{ public: typedef std::vector<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; //如果data[i]不合法,抛出一个异常 void check(size_type i, const std::string &msg) const; }; StrBlob::StrBlob():data(make_shared<vector<string>>()) { } StrBlob::StrBlob(std::initializer_list<string> il): data(make_shared<vector<string>>(il)) { } void StrBlob::check(size_type i, const std::string &msg) const { if(i >= data->size()){ throw out_of_range(msg); } } string& StrBlob::front() { //如果vector为空,check会抛出一个异常 check(0, "front on empty StrBlob"); return data->front(); } string& StrBlob::back() { //如果vector为空,check会抛出一个异常 check(0, "back on empty StrBlob"); return data->back(); } void StrBlob::pop_back() { //如果vector为空,check会抛出一个异常 check(0, "pop_back on empty StrBlob"); data->pop_back(); } `StrBlob`类只有一个`shared_ptr`类型的数据成员,因此当我们拷贝、赋值或销毁一个`StrBlob`对象时,它的`shared_ptr`成员会被拷贝、赋值或销毁。 拷贝一个`shared_ptr`会递增其引用计数;将一个`shared_ptr`赋予另一个`shared_ptr`会递增赋值号右侧的`shared_ptr`的引用计数,而递减左侧`shared_ptr`的引用计数。 ## 内存耗尽 ## 如果`new`不能分配所要求的内存空间,会抛出一个`bad_alloc`类型的异常。我们可以改变使用`new`的方式来阻止它抛出异常: int *p1 = new int(10); //如果分配失败,抛出`bad_alloc`类型的异常 int *p2 = new (nothrow) int(10); //如果分配失败,`new`返回一个空指针 ## shared\_ptr与new结合使用 ## 可以用`new`返回的指针来初始化智能指针,但是要注意接受指针参数的智能指针的构造函数是`explicit`的,因此必须使用直接初始化形式。出于相同的原因,一个返回`shared_ptr`的函数不能在其返回语句中隐式转换一个普通指针。 shared_ptr<int> p1 = new int(1024);//错误:必须使用直接初始化形式 shared_ptr<int> p2(new int(1024));//正确:使用了直接初始化形式 shared_ptr<int> clone(int p) { return new int(p); //错误:不能隐式转换为shared_ptr<int> } shared_ptr<int> clone(int p) { return shared_ptr<int>(new int(p)); //正确:显式的用int*创建shared_ptr<int> } ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 2] ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 3] **注意:当我们将一个`shared_ptr`绑定到一个普通指针后,我们就不应该再使用内置指针来访问`shared_ptr`所指向的内存了,否则可能发生意想不到的错误。** **注意:`get`用来将指针的访问权限传递给代码,只有在确定代码不会`delete`该指针的情况下,才能使用`get`。特别的,永远不要用`get`获得的指针来初始化另一个智能指针。** shared_ptr<int> p(new int(42)); //引用计数为1 int *q = p.get(); // 正确:但是使用q时要注意,不要让它管理的指针被释放 { //新程序块 //未定义:两个独立的shared_ptr指向相同的内存 shared_ptr<int>(q); }//程序块结束,引用计数减1,q被销毁,它指向的内存被释放 int foo = *p; //未定义:p指向的内存已经被释放了 ## 智能指针和异常 ## 包括所有标准库在内的很多C++类都定义了析构函数,负责清理对象使用的资源。但不是所有的类都是这样良好定义的。特别是那些为C和C++两种语言设计的类,通常都需要用户显式地释放所使用的任何资源,称这种类为**哑类**。 对于**哑类**,同样可以使用`shared_ptr`来管理,这是我们需要首先定义一个函数来代替`delete`,这个删除器(deleter)函数必须能够完成对`shared_ptr`中保存的指针进行释放的操作。例如对于一个`connection`类: void end_connection(connection *p) { disconnect(*p);} void f(destination &d)//其他参数 { connection c = connect(&d); shared_ptr<connection> p(&c, end_connection); //使用连接 //当f退出时(即使是由于异常退出),connection会正确关闭 } **为了正确使用智能指针,需要遵守一些基本规范:** * 不使用相同的内置指针初始化(或`reset`)多个智能指针。 * 不`delete get()`返回的指针。 * 不使用`get()`初始化或`reset`另一个智能指针 * 如果你使用了`get()`返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。 * 如果你使用智能指针来管理的资源不是`new`分配的资源,记住传递给它一个删除器(deleter)。 ## unique\_ptr ## 与`shared_ptr`不同,某个时刻只能有一个`unique_ptr`指向一个给定对象。由于一个`unique_ptr`独占它指向的对象,因此`unique_ptr`不支持普通的拷贝和赋值操作。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 4] 不能拷贝`unique_ptr`的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的`unique_ptr`。 unique_ptr<int> clone(int p) { //正确:从int*创建一个unique_ptr<int> return unique_ptr<int>(new int(p)); } ## weak\_ptr ## `weak_ptr`是一种不控制所指向对象生存期的智能指针,它指向由一个`shared_ptr`管理的对象。将一个`weak_prt`绑定到一个`shared_ptr`不会改变`shared_ptr`的引用计数。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 5] 当我们创建一个`weak_ptr`时,要用一个`shared_ptr`来初始化它: auto p = make_shared<int>(42); weak_ptr<int> wp(p);//wp弱共享p;p的引用计数未改变 我们不能使用`weak_ptr`直接访问对象,而必须调用`lock`。 if(shared_ptr<int> np = wp.lock()) //如果np不为空则条件成立 { //在if中,np与p共享对象 } `shared_ptr`是采用引用计数的智能指针,多个`shared_ptr`实例可以指向同一个动态对象,并维护了一个共享的引用计数器。对于引用计数法实现的计数,总是避免不了**循环引用**(或环形引用)的问题,`shared_ptr`也不例外。为了解决类似这样的问题,C++11引入了`weak_ptr`,来**打破这种循环引用**。 举个例子: #include <iostream> #include <memory> #include <vector> using namespace std; class ClassB; class ClassA { public: ClassA() { cout << "ClassA Constructor..." << endl; } ~ClassA() { cout << "ClassA Destructor..." << endl; } shared_ptr<ClassB> pb; // 在A中引用B }; class ClassB { public: ClassB() { cout << "ClassB Constructor..." << endl; } ~ClassB() { cout << "ClassB Destructor..." << endl; } shared_ptr<ClassA> pa; // 在B中引用A }; int main() { shared_ptr<ClassA> spa = make_shared<ClassA>(); shared_ptr<ClassB> spb = make_shared<ClassB>(); spa->pb = spb; spb->pa = spa; // 函数结束,思考一下:spa和spb会释放资源么? } 上面代码的输出如下: ClassA Constructor... ClassB Constructor... Program ended with exit code: 0 从上面代码中,ClassA和ClassB间存在着循环引用,从运行结果中我们可以看到:当`main`函数运行结束后,`spa`和`spb`管理的动态资源并没有得到释放,产生了内存泄露。 使用`weak_ptr`来改造前面的代码,可以打破循环引用问题。 class ClassB; class ClassA { public: ClassA() { cout << "ClassA Constructor..." << endl; } ~ClassA() { cout << "ClassA Destructor..." << endl; } weak_ptr<ClassB> pb; // 在A中引用B }; class ClassB { public: ClassB() { cout << "ClassB Constructor..." << endl; } ~ClassB() { cout << "ClassB Destructor..." << endl; } weak_ptr<ClassA> pa; // 在B中引用A }; int main() { shared_ptr<ClassA> spa = make_shared<ClassA>(); shared_ptr<ClassB> spb = make_shared<ClassB>(); spa->pb = spb; spb->pa = spa; // 函数结束,思考一下:spa和spb会释放资源么? } 输出结果如下: ClassA Constructor... ClassB Constructor... ClassA Destructor... ClassB Destructor... Program ended with exit code: 0 # 动态数组 # 大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更加简单,更不容易出错,且一般有更好的性能。 int *pia = new int[get_size()];//pia指向第一个int delete[] pia; 注意:动态数组并不是数组类型 标准库提供了一个可以管理`new`分配的数组的`unique_ptr`版本。 //up指向一个包含10个未初始化int的数组 unique_ptr<int[]> up(new int[10]); up.release(); //自动调用delete[]销毁其指针 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 6] ## allocator类 ## 标准库`allocator`类定义在`memory`头文件中,帮助我们将内存分配与对象构造分离开来。**它分配的内存是原始的、未构造的**。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 7] allocator<string> alloc;//可以分配string的allocator对象 auto const p = alloc.allocate(n);//分配n个未初始化的string auto q = p;//q指向最后构造的元素之后的位置 alloc.construct(q++); //*q为空字符串 alloc.construct(q++, 10, 'c'); //*q为cccccccccc alloc.construct(q++, "hi"); //*q为hi **注意:为了使用`allocate`返回的内存,我们必须用`construct`构造对象。使用未构造的内存,其行为是未定义的。** 当我们用完对象后,必须对每个构造的元素调用`destroy`来销毁它们。但要注意只能对真正构造了的元素进行`destroy`操作。 while( q != p) alloc.destroy(--q); //释放我们真正构造的string 一旦元素被销毁后,就可以重新使用这部分内存来保存其他`string`,也可以通过调用`deallocate`来释放内存,归还给系统。 alloc.deallocate(p, n); ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 8] //分配比vi中的元素所占用空间大一倍的动态内存 auto p = alloc.allocate(vi.size() * 2); //通过拷贝vi中的元素来构造从p开始的元素 auto q = uninitialized_copy(vi.begin(), vi.end(), p); //将剩余元素初始化为42 uninitialized_fill_n(q, vi.size(), 42); 传递给`uninitialized_copy`的目的位置迭代器必须指向未构造的内存。与`copy`不同,`uninitalized_copy`在给定目的位置构造元素。`uninitalized_copy`的返回值是一个指针,指向最后一个构造元素之后的位置。 # 使用标准库设计文本查询程序 # 当我们设计一个类时,在真正实现成员之前先编写程序使用这个类,可以帮助我们看到类是否有我们需要的操作。例如下面的函数接受一个指向要处理文件的`ifstream`,并与用户交互,打印给定单词的查询结果。 void runQueries(ifstream &infile) { TextQuery tq(infile);//保存文件并建立查询map //与用户交互:提示用户输入要查询的单词,完成查询并打印结果 while(true) { cout << "Enter word to look for, or q to quit:"; string s; //若遇到文件尾或用户输入了`q`时循环终止 if( !(cin >> s) || s=="q") break; //指向查询并打印结果 print(cout, tq.query(s)) << endl; } } class QueryResult;//为了定义函数query的返回类型,这个定义是必须的 class TextQuery { public: using line_no = std::vector<std::string>::size_type; TextQuery(std::ifstream&); QueryResult query(const std::string&) const; private: std::shared_ptr<std::vector<std::string>> file; //输入文件 //每个单词到它所在的行号的集合的映射 std::map<std::string, std::shared_ptr<std::set<line_no>>> wm; }; 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){ //对行中每个单词 //如果单词不在wm中,以之为下标在wm中添加一项 auto &lines = wm[word]; //lines是一个shared_prt if(!lines) //在我们第一次遇到这个单词时,此指针为空 lines.reset(new set<line_no>); // 分配一个新的set lines->insert(n); //将此行号插入set中 } } } QueryResult TextQuery::query(const string &sought) const { //如果未找到sought,我们就返回下面这个局部static对象 static shared_ptr<set<line_no>> nodata(new set<line_no>); //使用find而不是下标运算符来查找单词,避免将单词添加到wm中 auto loc = wm.find(sought); if(loc == wm.end()) return QueryResult(sought, nodata, file); //未找到 else return QueryResult(sought, loc->second, file); } class QueryResult { friend std::ostream& QueryResultPrint(std::ostream&, const QueryResult&); public: using line_no = std::vector<std::string>::size_type; QueryResult(std::string s, std::shared_ptr<std::set<line_no>> p, std::shared_ptr<std::vector<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; //输入文件 }; std::ostream &QueryResultPrint(std::ostream &os, const QueryResult &qr) { //如果找到了单词,打印出现的次数和所有出现位置 os << qr.sought << " occurs " << qr.lines->size() << " " << ( qr.lines->size() > 1 ? "times": "time") << endl; //打印单词出现的每一行 for(auto num : *qr.lines) //避免行号从0开始给用户带来疑惑 os << "\t(line "<< num+1 <<") " << *(qr.file->begin() + num) << endl; return os; } # 练习 # 1. **如果你试图拷贝或赋值`unique_ptr`,编译器并不总能给出易于理解的错误信息。编写包含这种错误的程序,观察编译器如何诊断这种错误。** error C2248: “std::unique_ptr<_Ty>::unique_ptr”: 无法访问 private 成员(在“std::unique_ptr<_Ty>”类中声明) 1. **下面的`unique_ptr`的声明中,哪些是合法的。哪些可能导致后续的程序错误?** int ix = 1024, *pi = &ix, *pi2 = new int(2018); typedef unique_ptr<int> IntP; (a) IntP p0(ix); (b) IntP p1(pi); (c) IntP p2(pi2); (d) IntP p3(&ix); (e) IntP p4(new int(2048)); (f) IntP p5(p2.get()); (a):不合法,`ix`不是`new`返回的指针 (b):同上 ©:合法,`unique_ptr`必须采用直接初始化 (d):不合法,同(a) (e):合法 (f):不合法,必须使用`new`返回的指针进行初始化,赋值和拷贝的操作也不包含`get()`方法。 1. **编写一个程序,连接两个字符串字面常量,将结果保存在一个动态分配的`char`数组中。重写这个程序,连接两个标准库`string`对象。** #include<iostream> #include <string> #include <cstring> using namespace std; int main(int argc, char**argv) { char *s1 = "abc"; char *s2 = "efg";//字符串字面常量,字符串末尾有空格 string si = "a"; string sl = "b";//标准库string对象 char *p = new char[strlen(s1)+strlen(s2)+1];//必须指明要分配对象的个数 strcpy(p,s1);//复制 strcat(p,s2);//拼接并覆盖p中已有的terminating null character cout<<p<<endl; strcpy(p,(si+sl).c_str());//必须转换为c类型字符串(c中无string类型) cout<<p<<endl; delete [] p; system("pause"); return 0; } ![在这里插入图片描述][20200520164549328.png] [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70]: https://img-blog.csdnimg.cn/20200519112508873.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 1]: https://img-blog.csdnimg.cn/20200519112618844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 2]: https://img-blog.csdnimg.cn/20200519161740980.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 3]: https://img-blog.csdnimg.cn/20200519161810665.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 4]: https://img-blog.csdnimg.cn/20200519171838310.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 5]: https://img-blog.csdnimg.cn/20200519173008822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 6]: https://img-blog.csdnimg.cn/20200519181702508.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 7]: https://img-blog.csdnimg.cn/20200520094246601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx_size_16_color_FFFFFF_t_70 8]: https://img-blog.csdnimg.cn/20200520102545612.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI1ODAwMzEx,size_16,color_FFFFFF,t_70 [20200520164549328.png]: https://img-blog.csdnimg.cn/20200520164549328.png
还没有评论,来说两句吧...