【c++ Prime 学习笔记】第12章 动态内存
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变为空悬
作者:砥才人
出处:https://www.cnblogs.com/shiroe
本系列文章为笔者整理原创,只发表在博客园上,欢迎分享本文链接,如需转载,请注明出处!