【c++ Prime 学习笔记】第12章 动态内存
- 对象的
生存期
:全局对象
:程序启动时创建,程序结束时销毁局部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章 动态内存的更多相关文章
- 《C++ Primer》笔记 第12章 动态内存
shared_ptr和unique_ptr都支持的操作 解释 shared_ptr sp或unique_ptr up 空智能指针,可以指向类型为T的对象 p 将p用作一个条件判断,若p指向一个对象,则 ...
- CSS3秘笈第三版涵盖HTML5学习笔记9~12章
第9章,装饰网站导航 限制访问,处于隐私方面考虑,浏览器已经开始限制可以对伪类:visited应用哪些CSS属性了.其中包括对已访问过的链接定义color.background-color.borde ...
- C++ Primer 5th 第12章 动态内存
练习12.1:在此代码的结尾,b1 和 b2 各包含多少个元素? StrBlob b1; { StrBlob b2 = {"a", "an", "th ...
- 《C和指针》 读书笔记 -- 第11章 动态内存分配
1.C函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放,这些函数维护一个可用内存池. void *malloc(size_t size);//返回指向分配的内存块起始位置的 ...
- [C++ Primer] : 第12章: 动态内存
动态内存与只能指针 静态内存用来保存局部static对象, 类static数据成员以及定义在任何函数之外的变量. 栈内存用来保存定义在函数内的非static对象. 分配在静态或栈内存中的对象由编译器自 ...
- 《DOM Scripting》学习笔记-——第七章 动态创建html内容
本章内容: 1.动态创建html内容的“老”技巧:document.write()和innerHTML属性 2.DOM方法:createElement(),creatTextNode(),append ...
- <<Python基础教程>>学习笔记 | 第12章 | 图形用户界面
Python支持的工具包非常多.但没有一个被觉得标准的工具包.用户选择的自由度大些.本章主要介绍最成熟的跨平台工具包wxPython.官方文档: http://wxpython.org/ ------ ...
- 《Jave并发编程的艺术》学习笔记(1-2章)
Jave并发的艺术 并发编程的挑战 上下文切换 CPU通过时间片分配算法来循环执行任务,当前时间片执行完之后会切换到下一个任务.但是,切换会保存上一个任务的状态,一遍下次切换回这个任务时,可以再次加载 ...
- 【c++ Prime 学习笔记】目录索引
第1章 开始 第Ⅰ部分 C++基础 第2章 变量和基本类型 第3章 字符串.向量和数组 第4章 表达式 第5章 语句 第6章 函数 第7章 类 第 Ⅱ 部分 C++标准库 第8章 IO库 第9章 顺序 ...
随机推荐
- 异步处理方式之信号(三):kill、raise、alarm、pause函数简介
文章目录 6. 函数kill和raise 7. 函数alarm和pause 7.1 alarm() 7.2 pause() 6. 函数kill和raise kill函数用来将信号发送给进程或者进程组. ...
- 学习反射例子,调用DLL窗体及方法
创建类库,并添加新窗体,加入以下方法 public static string setText(string str) { return str; } 编译后把生成的DLL文件放入新项目的bin目录, ...
- Centos6.5时间服务器NTP搭建
NTP时间服务器安装与配置 第1章 Server端的安装与配置 1.1 查看系统是否已经安装ntp服务组件 rpm -qa | grep "ntp" #<==查看是否已经安装 ...
- Linux find命令实例教程 15个find命令用法
除了在一个目录结构下查找文件这种基本的操作,你还可以用find命令实现一些实用的操作,使你的命令行之旅更加简易.本文将介绍15种无论是于新手还是老鸟都非常有用的Linux find命令.首先,在你的h ...
- 获取发布版SHA1和调试版SHA1
总结 调试版: 常见问题 | 高德地图API (amap.com) 发布版: 首先需要生成签名 Android Studio生成签名文件,自动签名,以及获取SHA1和MD5值_donkor_的博客-C ...
- Kubernetes-Pod介绍(三)-Pod调度
前言 本篇是Kubernetes第六篇,大家一定要把环境搭建起来,看是解决不了问题的,必须实战. Kubernetes系列文章: Kubernetes介绍 Kubernetes环境搭建 Kuberne ...
- 我爬取交通学博士付费的GIS资源,每年被动收入2w很简单?
目录 1.背景介绍 2.技术路线 3.数据结果 4.数据分析 5.总结 6.后记 1.背景介绍 某周末闲来无事,顺手打开了CSDN,看到了一个人发布的收费GIS资源,售价是¥19.9,POI数据也有人 ...
- ecshop调用商品的购买次数方法
这时候我们修改一下 写成一个函数放到lib_goods.php 这样就可以随便调用了 --------------------------------------------------------- ...
- Feign超时不生效问题
使用Feign作为RPC调用组件,可以配置连接超时和读取超时两个参数 使用Feign配置超时需要注意:Feign内部使用了负载均衡组件Ribbon,而Ribbon本身也有连接超时和读取超时相关配置一. ...
- 华为云计算IE面试笔记-桌面云用户登录连接流程及故障处理?
1-10:桌面与系统验证成功 http协议 11-19:桌面list(VM列表)获取,选择 http协议 20-30: ...