• 对象的生存期

    • 全局对象:程序启动时创建,程序结束时销毁
    • 局部static对象:第一次使用前创建,程序结束时销毁
    • 局部自动对象:定义时创建,离开定义所在程序块时销毁
    • 动态对象:生存期由程序控制,在显式创建时创建,显式销毁时销毁
  • 动态对象的正确释放极易出错。为安全使用动态对象,标准库定义了智能指针来管理动态对象
  • 内存空间:
    • 静态内存:局部static对象、类static数据成员、定义在任何函数之外的变量
    • 栈内存:定义在函数内的非static对象
    • 堆内存:动态对象,即运行时分配的对象
  • 静态内存和栈内存中的对象由编译器创建和销毁,堆内存中的动态对象的生存期由程序控制

12.1 动态内存与智能指针

C++通过一对运算符管理动态内存:

  • new算符在动态内存中为对象分配空间并返回指向该对象的指针,可选择对对象初始化
  • delete算符接受一个动态对象的指针,销毁该对象并释放内存
  • 确保在正确时间释放内存很难:
    • 忘记释放内存,会产生内存泄露
    • 若还有指针引用内存的情况下就将其释放,会产生引用非法内存的指针
  • C++11标准库中提供了两种智能指针来管理动态对象,负责自动释放所指向的对象,定义于memory头文件:
    • shared_ptr允许多个指针指向同一对象
    • unique_ptr指针独占所指向对象
    • weak_ptr是伴随类,是一种弱引用,指向shared_ptr管理的对象

12.1.1 shared_ptr类

  • 智能指针也是模板类,创建时必须在模板参数中给定其指向的类型
  • 默认初始化的智能指针中保存空指针,条件判断中使用智能指针是判断其是否为空
  • 解引用智能指针返回其指向的对象
shared_ptr<string> p1;
shared_ptr<list<string>> p2; if(p1 && p1->empty()) //若p1不为空,检查它是否指向一个空string
*p1="hi"; //若p1为空,解引用,将新值赋给它

make_shared 函数

  • 最安全的分配和使用动态内存的方法是调用make_shared函数,该函数定义于memory头文件中,它在动态内存中分配一个对象并初始化,返回指向它的shared_ptr
  • make_shared函数用法:
    • 是模板函数,使用时必须在模板参数中给出构造对象的类型
    • 其参数必须与构造对象的构造函数参数匹配,使用这些参数构造对象
    • 若不给实参,则对象值初始化

shared_ptr 的拷贝和赋值

  • 对shared_ptr进行拷贝/赋值时,每个shared_ptr会记录有多少个其他shared_ptr指向相同对象
  • 每个shared_ptr都有一个关联的计数器,称为引用计数
    • 一个shared_ptr的一组拷贝之间共享“引用计数管理区域”,并用原子操作保证该区域中的引用计数被互斥地访问
    • 互相独立的shared_ptr维护的引用计数也互相独立,即使指向同一对象。因此需避免互相独立的shared_ptr指向同一对象
  • 改变引用计数
    • 递增:拷贝shared_ptr时,包括:用一个shared_ptr初始化另一个shared_ptr、作为参数传入函数、作为返回值从函数传出
    • 递减:给shared_ptr赋新值、shared_ptr被销毁(例如离开作用域)
    • 一旦shared_ptr的计数器变为0,会自动释放管理的对象
  • C++标准并未要求使用计数器实现引用计数,其实现取决于标准库的实现

shared_ptr 自动销毁所管理的对象,自动释放相关联的内存

  • 指向对象的最后一个shared_ptr被销毁时,shared_ptr会通过它的析构函数完成对象的销毁
  • 析构函数控制此类型对象销毁时的操作,一般用于释放对象的资源。shared_ptr类型的析构函数被调用时递减引用计数,一旦计数为0即销毁对象。
  • 必须确保shared_ptr在不使用时及时删除。例如容器中的shared_ptr在不使用时要erase
//创建对象并返回智能指针
shared_ptr<Foo> factory(T arg){
return make_shared<Foo>(arg); //创建时计数为1,传出拷贝+1,离开作用域-1
} //使用factory创建对象,使用完后销毁
void use_factory(T arg){
shared_ptr<Foo> p=factory(arg); //创建对象,初始引用计数为1
/* 使用p */ //未传出,离开作用域时计数-1变为0,对象被销毁
} //使用factory创建对象,使用完后不销毁
shared_ptr<Foo> use_factory(T arg){
shared_ptr<Foo> p=factory(arg); //创建对象,初始引用计数为1
/* 使用p */
return p; //传出时拷贝+1,离开作用域-1,传出后计数为1
}

使用了动态生存期的资源的类

  • 使用动态内存的3种情况

    • 不知道需要使用多少对象(容器)
    • 不知道所需对象的准确类型(多态)
    • 需在多个对象间共享数据
  • 若两个对象共享底层数据,则某个对象被销毁时不可单方面销毁底层数据。此时应将共享的数据做成对象,在需共享它的两个类内分别用shared_ptr访问
  • 对类对象使用默认版本的拷贝/赋值/销毁操作时,这些操作拷贝/赋值/销毁类的数据成员(包括智能指针)。
//定义StrBlob类
class StrBlob{
public:
//定义类型
using size_type=vector<string>::size_type;
//两个构造函数,默认初始化和列表初始化
StrBlob();
StrBlob(initializer_list<string> il);
//以下是对底层vector操作的封装
size_type size() const {return data->size();}
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管理底层的vector<string>数据
shared_ptr<vector<string>> data;
//检查索引i是否越界,越界时用msg抛出异常
void check(size_type i, const string &msg) const;
};
//默认构造函数,底层vector<string>默认初始化,返回shared_ptr用于初始化data
StrBlob::StrBlob():
data(make_shared<vector<string>>())
{}
//构造函数,底层vector<string>列表初始化,返回shared_ptr用于初始化data
StrBlob::StrBlob(initializer_list<string> il):
data(make_shared<vector<string>>(il))
{}
//检查下标是否越界
void StrBlob::check(size_type i, const string &msg) const {
if(i>=data->size())
throw out_of_range(msg);
}
//以下3个函数分别实现front、back、pop_back操作,用0来check索引判断是否为空
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();
}
/* 使用StrBlob */
StrBlob b1; //创建新StrBlob
{ //进入新作用域
StrBlob b2={"a","an","the"}; //初始化b2
b1=b2; //用b2初始化b1,它们共享底层数据
} //离开作用域,b2被释放,b1仍存在,共享的底层数据未丢失
while(b1.size()>0){
cout<<b1.back()<<endl;
b1.pop_back();
}

12.1.2 直接管理内存

  • 两个运算符分配/释放动态内存:

    • new分配内存,并构造对象
    • delete销毁对象,并释放内存
  • 使用new/delete管理动态内存的类不能依赖动态对象成员的拷贝/赋值/销毁的任何默认操作
  • 堆内存中分配的空间是匿名的,故new无法为其分配的对象命名,只能返回一个指向该对象的指针
  • 动态对象初始化
    • 默认情况下用默认初始化:内置类型的值未定义,类类型依赖默认构造函数

    • 直接初始化:用圆括号调用构造函数,或花括号列表初始化

    • 值初始化:类型名后跟一对空的圆括号。对于有默认构造函数的类类型而言,值初始化没有意义(都是调用默认构造函数),但对于内置类型值初始化可有良好定义的值

    • 拷贝初始化:使用圆括号里放单一对象,被分配的对象用它初始化。此时可用auto推导需分配的类型

      //默认初始化
      int *pi=new int; //未定义
      string *ps=new string; //默认初始化为空字符串
      //直接初始化
      int *pi=new int(1024); //初始化为1024
      string *ps=new stirng(10,'9'); //初始化为"9999999999"
      vector<int> *pv=new vector<int>{0,1,2,3}; //初始化为{0,1,2,3,4,5}
      //值初始化
      int *pi=new int(); //初始化为0
      string *ps=new string(); //初始化为空字符串
      //拷贝初始化
      auto p1=new auto(obj); //用obj拷贝初始化p1
      auto p2=new auto{a,b,c}; //错,只能拷贝初始化为auto

动态分配的 const 对象

  • 用new分配const对象是合法的,const对象必须初始化。
const int *pci = new const int(1024);
const string *pci = new const string;

内存耗尽

  • 若new不能分配要求的空间,则抛出名为bad_alloc的异常。
  • 可向new算符传参来阻止抛出异常,传递了参数的new叫定位new
  • 向new传入std::nothrow,则它不会抛出异常。若不能分配内存,则返回空指针。
  • bad_alloc和nothrow都定义在头文件new
int *p1 = new int; //如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int; //如果分配失败,new 返回空指针

释放动态内存

  • delete表达式将内存归还给系统,它接受一个指针,指向需要释放的对象
  • delete表达式执行两个工作:
    • 销毁指针指向的对象
    • 释放对应的内存

指针值和 delete

  • 递给delete表达式的指针必须指向动态内存,或是空指针
  • 用delete释放非new分配的内存,或者将同一指针释放多次,都是未定义
  • 编译器无法知道一个指针是否指向动态内存,也无法知道一个指针指向的内存是否已被释放,故这些错误不会被编译器发现
  • const对象的值不可改变,但可被销毁
int i, *pil = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; //错误,i不是指针
delete pil; //未定义,pil指向一个局部变量
delete pd; //正确
delete pd2; //未定义,pd2指向的内存已经被释放
delete pi2; //正确,释放一个空指针总是没有错误的 const int *pci=new const int(1024);
delete pci;

动态对象的生存期直到被释放时为止

  • 内置指针管理的动态对象,在被显式释放之前一直存在
  • 返回指向动态内存的指针的函数给其调用者增加了一个额外负担——调用者必须记得释放
Foo* factory(T arg){
return new Foo(arg);
} void use_factory(T arg){
Foo *p = factory(arg);
//使用p 但不delete它
} //p离开了它的作用域,但它所指向的内存没有被释放
  • 内置类型的对象被销毁时什么都不会发生(与类类型不一样)。特别是,内置指针被销毁时不影响其指向的对象。若这个内置指针指向动态对象,则空间不会被释放
void use_factory(T arg){
Foo *p = factory(arg);
//使用p
delete p;
} Foo* use_factory(T arg){
Foo *p = factory(arg);
//使用p
return p; //调用者必须释放内存
}

delete 之后重置指针值

  • delete之后,指针变为空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存指针。避免空悬指针:

    • 尽量在指针即将离开作用域时释放其管理的动态内存
    • 也可在delete后立即将指针置为nullptr
  • delete内存后将指针置nullptr的做法只对单个指针有效,若还有其他指针指向该对象则它们变为空悬指针。由于很难知道有哪些指针指向这个对象,故很难用new和delete管理动态内存

12.1.3 shared_ptr和new结合使用

  • 可用new返回的内置指针初始化智能指针,如果不对智能指针初始化,就被初始化为空指针
  • 接受内置指针的智能指针构造函数是explicit的,即不能将内置指针隐式转换为智能指针,必须直接初始化
  • 用于初始化智能指针的内置指针必须指向动态内存,因为智能指针默认使用delete释放其指向的对象。静态内存和栈内存不需要也不能使用智能指针
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> clone(int p){
return shared_ptr<int>(new int(p)); //对,可以直接构造
}

不要混合使用普通指针和智能指针

  • shared_ptr用于自动管理对象释放的功能,只限于其自身的一组拷贝之间,互相独立的shared_ptr其引用计数也互相独立内置指针不参与引用计数
  • 推荐使用make_shared而不用new的内置指针初始化shared_ptr,因为make_shared可保证分配对象的同时和shared_ptr绑定,避免将一块内存绑定到多个互相独立的shared_ptr
  • 使用内置指针构造智能指针时必须立即构造,禁止混合使用两种指针,禁止传参时构造
  • 将一个shared_ptr绑定到一个内置指针时,内存管理的责任被交给shared_ptr,不应该再用该内置指针访问内存
void process(shared_ptr<int> ptr){  //传入时copy,计数+1
/* 使用ptr */
} //离开作用域,计数-1
//以下为正确用法:
shared_ptr<int> p(new int(42)); //新建一个智能指针
process(p); //处理后引用计数为1
int i=*p; //以下为错误用法:
int *x(new int(1024));
process(x); //错,不可将内置指针隐式转换为智能指针
process(shared_ptr<int>(x)); //该智能指针的生存期只在这个函数中,离开时智能指针被释放,对象也被释放
int j=*x; //x是空悬指针

不要使用 get 初始化另一个智能指针或为智能指针赋值

  • shared_ptr定义了get成员函数,它返回内置指针,指向shared_ptr管理的对象。用于不兼容shared_ptr的情形。
  • get使用风险
    • 不可将get返回的内置指针dedete,因为原来的shared_ptr变为空悬

【c++ Prime 学习笔记】第12章 动态内存的更多相关文章

  1. 《C++ Primer》笔记 第12章 动态内存

    shared_ptr和unique_ptr都支持的操作 解释 shared_ptr sp或unique_ptr up 空智能指针,可以指向类型为T的对象 p 将p用作一个条件判断,若p指向一个对象,则 ...

  2. CSS3秘笈第三版涵盖HTML5学习笔记9~12章

    第9章,装饰网站导航 限制访问,处于隐私方面考虑,浏览器已经开始限制可以对伪类:visited应用哪些CSS属性了.其中包括对已访问过的链接定义color.background-color.borde ...

  3. C++ Primer 5th 第12章 动态内存

    练习12.1:在此代码的结尾,b1 和 b2 各包含多少个元素? StrBlob b1; { StrBlob b2 = {"a", "an", "th ...

  4. 《C和指针》 读书笔记 -- 第11章 动态内存分配

    1.C函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放,这些函数维护一个可用内存池. void *malloc(size_t size);//返回指向分配的内存块起始位置的 ...

  5. [C++ Primer] : 第12章: 动态内存

    动态内存与只能指针 静态内存用来保存局部static对象, 类static数据成员以及定义在任何函数之外的变量. 栈内存用来保存定义在函数内的非static对象. 分配在静态或栈内存中的对象由编译器自 ...

  6. 《DOM Scripting》学习笔记-——第七章 动态创建html内容

    本章内容: 1.动态创建html内容的“老”技巧:document.write()和innerHTML属性 2.DOM方法:createElement(),creatTextNode(),append ...

  7. &lt;&lt;Python基础教程&gt;&gt;学习笔记 | 第12章 | 图形用户界面

    Python支持的工具包非常多.但没有一个被觉得标准的工具包.用户选择的自由度大些.本章主要介绍最成熟的跨平台工具包wxPython.官方文档: http://wxpython.org/ ------ ...

  8. 《Jave并发编程的艺术》学习笔记(1-2章)

    Jave并发的艺术 并发编程的挑战 上下文切换 CPU通过时间片分配算法来循环执行任务,当前时间片执行完之后会切换到下一个任务.但是,切换会保存上一个任务的状态,一遍下次切换回这个任务时,可以再次加载 ...

  9. 【c++ Prime 学习笔记】目录索引

    第1章 开始 第Ⅰ部分 C++基础 第2章 变量和基本类型 第3章 字符串.向量和数组 第4章 表达式 第5章 语句 第6章 函数 第7章 类 第 Ⅱ 部分 C++标准库 第8章 IO库 第9章 顺序 ...

随机推荐

  1. 异步处理方式之信号(三):kill、raise、alarm、pause函数简介

    文章目录 6. 函数kill和raise 7. 函数alarm和pause 7.1 alarm() 7.2 pause() 6. 函数kill和raise kill函数用来将信号发送给进程或者进程组. ...

  2. 学习反射例子,调用DLL窗体及方法

    创建类库,并添加新窗体,加入以下方法 public static string setText(string str) { return str; } 编译后把生成的DLL文件放入新项目的bin目录, ...

  3. Centos6.5时间服务器NTP搭建

    NTP时间服务器安装与配置 第1章 Server端的安装与配置 1.1 查看系统是否已经安装ntp服务组件 rpm -qa | grep "ntp" #<==查看是否已经安装 ...

  4. Linux find命令实例教程 15个find命令用法

    除了在一个目录结构下查找文件这种基本的操作,你还可以用find命令实现一些实用的操作,使你的命令行之旅更加简易.本文将介绍15种无论是于新手还是老鸟都非常有用的Linux find命令.首先,在你的h ...

  5. 获取发布版SHA1和调试版SHA1

    总结 调试版: 常见问题 | 高德地图API (amap.com) 发布版: 首先需要生成签名 Android Studio生成签名文件,自动签名,以及获取SHA1和MD5值_donkor_的博客-C ...

  6. Kubernetes-Pod介绍(三)-Pod调度

    前言 本篇是Kubernetes第六篇,大家一定要把环境搭建起来,看是解决不了问题的,必须实战. Kubernetes系列文章: Kubernetes介绍 Kubernetes环境搭建 Kuberne ...

  7. 我爬取交通学博士付费的GIS资源,每年被动收入2w很简单?

    目录 1.背景介绍 2.技术路线 3.数据结果 4.数据分析 5.总结 6.后记 1.背景介绍 某周末闲来无事,顺手打开了CSDN,看到了一个人发布的收费GIS资源,售价是¥19.9,POI数据也有人 ...

  8. ecshop调用商品的购买次数方法

    这时候我们修改一下 写成一个函数放到lib_goods.php 这样就可以随便调用了 --------------------------------------------------------- ...

  9. Feign超时不生效问题

    使用Feign作为RPC调用组件,可以配置连接超时和读取超时两个参数 使用Feign配置超时需要注意:Feign内部使用了负载均衡组件Ribbon,而Ribbon本身也有连接超时和读取超时相关配置一. ...

  10. 华为云计算IE面试笔记-桌面云用户登录连接流程及故障处理?

    1-10:桌面与系统验证成功                           http协议 11-19:桌面list(VM列表)获取,选择                http协议 20-30: ...