【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为空,解引用,将新值赋给它

image

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结合使用

image

  • 可用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变为空悬
posted @ 2021-04-22 16:34  砥才人  阅读(275)  评论(0编辑  收藏  举报