转载 | C++ Primer

Posted 创智俱乐部ISA

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了转载 | C++ Primer相关的知识,希望对你有一定的参考价值。

点击蓝字关注,创智助你长姿势



动态内存


我们这一章来关注内存,我们来看一段代码:

int main() {  int a = 1;  return 0; }


这段代码几乎什么都没干,就是定义了一个 int 类型的对象,我们主要是看这个 a 在内存中的变化,你注意到这个事情就行。其实我们有很好的总结:


1.全局对象在程序启动时分配内存,在程序结束时销毁


2.对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁


3.局部 static 对象在第一次使用前分配,在程序结束时销毁 这些呢,都是 C++ 编译器帮我们管理的,对于这么爱赋予程序员自由的语言,C++ 也允许我们自己来决定,就是动态分配对象:动态分配的对象的生存期与它们在哪里创建无关,只有显式地被释放时,这些对象才会销毁。


你可能觉得这个还比较简单,其实这个非常难,尤其在程序比较复杂的时候,所以啊,C++ 又给我们提供了一些帮助:标准库定义了两个智能指针类型来帮助我们管理动态分配的对象,怎么帮助呢?当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。


通过不同方式定义的变量类型在内存中的存储位置也不同,分以下三种情况:


1.静态内存:保存局部 static 对象、类 static 数据成员、定义在任何函数之外的变量


2.栈内存:保存定义在函数内的非 static 对象


3.堆:存储动态分配的对象,当动态对象不再使用时,我们必须显式地销毁它们。


动态内存与智能指针


在 C++ 中,动态内存的管理是通过一对运算符来完成:


1.new,在动态内存中为对象分配空间,并且返回一个指向该对象的指针,可以选择对对象进行初始化


2.delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存


这样我们在使用的时候很容易出问题:


1.忘记释放内存,会产生内存泄漏


2.在尚有指针引用内存的情况下,释放了它,会产生引用非法内存的指针(非法指针)


好了,说了这么多,终于引出了智能指针:智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象,它管理底层指针的方式不同:


1.shared_ptr 允许多个指针指向同一个对象


2.unique_ptr 独占所指向的对象


3.weak_ptr,弱引用,指向 shared_ptr 所管理的对象


以上三种类型都定义在 memory 头文件中。


shared_ptr 类


类似 vector,智能指针也是模板:

shared_ptr p1; //shared_ptr, 指向 stringshared_ptr> p2; //shared_ptr, 指向 list


默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似,解引用一个智能指针返回它指向的对象:

if(p1 && p1->empty()) // 如果指针本身不为空(有所指对象),且指针所指对象为空{ *p1 = "hi"; }


好好看下面这张图: 


转载 | C++ Primer


make_shared 函数


最安全的分配和使用动态内存的方法是调用一个 make_shared 的标准库函数:此函数在动态内存中分配一个对象并初始化它,并且,返回指向此对象的 shared_ptr:

shared_ptr p0 = make_shared(); // 指向值初始化的 int0 的 share_ptrshared_ptr p1 = make_shared(42); // 指向 int42 的 share_ptrshared_ptr p2 = make_shared(5,'9'); // 指向"99999"的 share_ptr


这样定义看着长,我们可以用神器 auto:


auto p3 = make_shared>();


shared_ptr 的拷贝和赋值


当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象:

auto p = make_shared(42); //p 指向的对象只有 p 一个引用者auto q(p); //p 和 q 指向相同对象,此对象有两个引用者


我们可以认为每个 shared_ptr 都有一个关联的计数器 - 引用计数:无论何时我们拷贝一个 shared_ptr,都会递增计数器,举些不那么明显的拷贝例子


1. 用一个 shared_ptr 初始化另一个 shared_ptr


2. 将它作为参数传递给一个函数


3. 作为函数的返回值


递减计数器:


1. 给 shared_ptr 赋予一个新值


2. shared_ptr 被销毁(例如一个局部的 shared_ptr 离开其作用域)


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

auto r = make_shared(42); //r 指向的 int 只有一个引用者r = q; //1:给 r 赋新值,让它指向新地址;2:递增 q 指向对象的引用计数;//3:递减 r 原来指向的对象的引用计数;4:r 原来指向的对象已没有引用者,会自动释放


shared_ptr 自动销毁所管理的对象


如何销毁呢?智能指针通过它另一个特殊的成员函数-析构函数。类似构造函数,每个类都有一个析构函数,用于控制此类型的对象销毁时做什么。

shared_ptr的析构函数会递减它所指的对象的引用计数,如果计数为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。


看看智能指针有多智能


接下来,我会写一段代码,并分析内存情况,只分析这一次让大家知道内存的具体情况,其实这些工作以后都不用管,有内存指针来负责:

// 返回类型为智能指针的函数,所以我们会确保它分配的对象在恰当的时刻释放 shared_ptr factory(T arg) { return make_shared(arg); } void use_factory(T arg) { shared_ptr p = factory(arg); // 使用 p }//p 离开了作用域,它指向的内存被自动释放掉(在这释放是最好的了)


程序使用动态内存出于以下三种原因之一:


1. 程序不知道自己要使用多少对象(比如容器类)


2. 程序不知道所需对象的准确类型(后面再学)


3. 程序需要在多个对象间共享数据(接下来介绍)


到目前为止,我们使用的每个对象都拥有自己的元素:

vector v1; // 空的 { // 强行作用域 vector v2 = {"a"};  v1 = v2; // 拷贝一份给 v1 } // 到了这儿,v2 已经被销毁了,但是 v1 还是有元素的,因为是拷贝过来的,独一份,没影响


我们接下来要做一个事情,来改善上面的情况。什么事情呢?我们希望在拷贝的时候,不去拷贝元素,而是引用内存中同一块地方,v2 出了作用域之后,v2 本身不能用,但 v2 对应的元素还是保留,我们把将要实现的模板类称为 Blob,来代替 vector,具体实现后的效果应该是这样的:

Blob b1; // 空的 { // 强行作用域 vector b2 = {"a"}; b1 = b2; //b1 和 b2 共享相同的元素 } //b2 被销毁了,但 b2 中的元素还在 //b1 还是指向最初由 b2 创建的元素。


定义 StrBlob 类


我们最终会将 Blob 类实现为一个模板,但是我们要到第十六章才会学习模板,所以,在这里,我们就简化一下,先定义一个管理 string 的类 StrBlob。


实现一个新的集合类型最简单的方法就是借助标准库容器,这会省很多事,在这里,我们借助 vector 来保存元素。


我们当然不能直接用 vector,这样还是达不到我们的目的(别忘了我们为什么要提出这个),我们要把 vector 保存在动态内存中,这样它的生存期就完全由我们(借助智能指针)来掌控。


策略:为了实现我们希望的数据共享,我们为每个 StrBlob 设置一个 shared_ptr 来管理动态分配的 vector,这个 shared_ptr 会记录有多少个 StrBlob 共享相同的 vector,并在 vector 的最后一个使用者被销毁时释放 vector(这样是不是很棒啊)


为了简化问题的复杂度,我们的 StrBlob 只会实现 vector 的一小部分操作,下面代码是我们正儿八经开始自定义一个类了,要仔细看啊:

class StrBlob { public: // 类型别名  typedef vector::size_type size_type; // 构造函数 StrBlob(); // 别以为这样就好了啊,这就是个函数声明啊 StrBlob(initializer_list il); // 就是列表初始化的意思{"a", "b"} // 其他成员函数 size_type size() const {return data->size();} // 别忘了这里的 const 是为了让 const 对象也可以访问 bool empty() const {return data->empty();} // 添加和删除元素 void push_back(const string &t){data->push_back(t);} void pop_back(); // 元素访问 string& front(); string& back(); private: shared_ptr> data; //data 是一个指向 vector 的智能指针 // 如果 data[i]不合法,抛出异常 void check(size_type i, const string &msg) const;};


下面依次来实现类中没实现的函数:


StrBlob 构造函数


两个构造函数都使用初始化列表来初始化 data 成员,让它指向一个动态分配的 vector,默认构造函数分配的是空 vector:

StrBlob::StrBlob() : data(make_shared>()) {}// 这里的:是初始化列表那个标志,别看不出来啊StrBlob::StrBlob(initializer_list il) : data(make_shared>(il) {}


元素访问成员函数


先检查给定索引是否合法,实现 check 函数:

void StrBlob::check(size_type i, const string &msg) const{ if(i >= data->size()) {throw out_of_range(msg);}}


pop_back 和元素访问成员函数首先调用 check,如果 check 成功再继续利用底层 vector 的操作来完成工作:

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(); }


front 和 back 还应该对 const 重载


StrBlob 的拷贝、赋值和销毁


到这里,其实我们已经实现了最初的目标,我们来分析看看:我们的 StrBlob 类只有一个数据成员,是智能指针类型的,因此,当我们拷贝、赋值或销毁一个 StrBlob 对象时,它的 shared_ptr 成员会被拷贝、赋值或销毁,这不就已经实现了吗?


转载 | C++ Primer


                       

往期回顾




  转载 | C++ Primer

创智俱乐部

微信:sziitlSA



转载 | C++ Primer

一个让你涨姿势的社团

长按二维码关注


你的“分享、点赞和在看”

让我们之间的距离又近了一步

以上是关于转载 | C++ Primer的主要内容,如果未能解决你的问题,请参考以下文章

C++ Primer笔记16---chapter13 代码实例

C++ Primer笔记16---chapter13 代码实例

转载C++ IO库

《C++ Primer Plus》学习笔记——C++程序创建到运行的整个过程

转载C++如何进阶学习

C++ Primer Plus学习:第十章