智能指针


  • C++ Primer 笔记之动态内存
  • Author: Jokey_Chan

文章目录

  • 动态内存
    • 动态内存与智能指针
      • shared_ptr 类
        • make_shared
        • shared_ptr的拷贝与赋值
        • shared_ptr 自动销毁所管理的对象
        • 共享内存
          • 定义 StrBlob 类
          • StrBlob 构造函数
          • 元素访问成员函数
      • 直接管理内存
      • 普通指针与智能指针

动态内存

  • 动态分配的对象生存期与他们在哪里创建的无关,只有当显示的释放时,这些对象才会被销毁。
  • 动态对象在编程中极容易出错,标准库定义了两个智能指针类型管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

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

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

自由空间或堆:存储动态分配的对象,即程序运行时分配的对象。

分配在静态或栈内存中的对象有编译器自动创建和销毁,而动态对象的生存期由程序来控制,当动态对象不再使用时,代码必须显式地销毁它们。

动态内存与智能指针

头文件 #include

  • new delete : 这对运算符用于对象的空间分配与销毁
  • 两种智能指针:shared_ptr , unique_ptr。都能自动释放所指向的对象。区别在于,前者允许多个指针指向同一个对象,后者则“独占”所指的对象。
  • weak_ptr:伴随类,一种弱引用,指向 shared_ptr 所管理的对象。

shared_ptr 类

智能指针默认初始化时保存一个空指针(nullptr),用法与普通指针类似,解引用一个智能指针返回它所指向的对象。

shared_ptrp1
shared_ptr>p2
//如果p1不为空,检查它是否指向一个空的string
if(p1 && p1->empty())//p1指针为空,if判断为false,不会进入
{
    //如果p1指向空的string,解引用p1,将一个新值赋予string
    *p1 = "hello,world!";
}

表1:shared_ptr 和 unique_ptr都支持的操作

操作 含义
shared_ptrsp 空智能指针,可以指向类型为T的对象
unique_ptrup
p 将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象
p->mem 与(*p).mem等价
p.get() 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p,q)与p.swap(q) 交换p与q中的指针

表2:shared_ptr 独有的操作

操作 含义
make_shared(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。
shared_ptrp(q) p是shared_ptr q 的拷贝;此操作会递增q中的计数器,q中的指针必须能转换为T*
p=q p和q都是shared_ptr,所保存的指针必须能够相互转换,此操作会递减p的引用计数,递增q的引用计数;若p的计数为零,则将其管理的原内存释放
p.unique() 若p.use_count()为1,返回true;否则返回false
p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于调试

make_shared

调用make_shared分配和使用动态内存是最安全的,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。代码如下:

//指向一个值为42的int的shared_ptr
shared_ptrp3 = make_shared(42);
//p4指向一个值为“9999999999”的字符串
shared_ptrp4 = make_shared(10,9);
//p5指向一个值初始化的int,即值为0
shared_ptrp5 = make_shared();
//也可以用auto保存make_shared结果
auto p6 = make_shared>();

shared_ptr的拷贝与赋值

shared_ptr的自动释放通过引用计数来实现,当引用计数为0时,智能指针自动释放。

  • 当拷贝一个shared_ptr时,计数器都会递增。如将一个shared_ptr初始化另一个shared_ptr,将一个shared_ptr作为参数传递给一个函数或作为函数的返回值。
  • 当给shared_ptr赋一个新值时,其计数器递减。如,局部的shared_ptr离开其作用域。
auto p = make_shared(42);//p指向的对象只有p一个对象
auto q(p);//p和q指向相同的对象,此时该对象有p和q两个引用者
          //注意初始化和等号(=)的区别
auto r = make_shared(42);//r指向的int只有一个引用者
r = q;//给r赋值,另其指向另一个地址
      //递增q指向的对象的引用计数
      //递减r原来的指向的对象的引用计数
      //r原来指向的对象已没有引用者,自动释放

shared_ptr 自动销毁所管理的对象

看代码:

//factory 返回一个shared_ptr,指向一个动态分配的对象
shared_ptr factory(T arg)
{
    //恰当的处理arg
    //shared_ptr负责释放内存
    retrun make_ptr(arg);
}

factory返回一个shared_ptr,我们可以确保它分配的对象会在恰当的时候被释放。如:

void use_factory(T arg)
{
    shared_ptr p = factory(arg);
    //使用p;
}//p离开作用域,其指向的内存被自动释放

如果有其他shared_ptr也指向这块内存,则其指向的内存不会被释放。如:

void use_factory(T arg)
{
    shared_ptr p = factory(arg);
    //使用p;
    return p;//当返回p时,引用计数会递增
}//p离开作用域,其指向的内存不会被释放

我们应时刻保证无用的 shared_ptr 不会保留,如果忘记销毁无用的 shared_ptr,程序仍然可以正确执行,但会浪费内存。

注意:某种情况下,将 shared_ptr 保存至容器中,重排了容器后,不再需要某些元素,这时应该用erase删除那些不再需要的 shared_ptr 元素。

共享内存

到目前为止,使用过的类中分配的资源都与对应对象生存期一致。如:拷贝数据时,原数据和数据副本是相互分离的,即在内存中分别占了一块内存。

vector v1; //空vector
{
    //新作用域
    vector v2 = {"a","an","the"};
    v1 = v2;//从v2拷贝元素至v1
}//v2离开作用域即被销毁,v1中的元素为原v2中元素的拷贝

但某些类分配的资源具有与原对象相独立的生存期。也就是,多个对象共享底层的数据,某个对象被销毁时,不能单方面的销毁底层数据:

//我们实现的 Blob 类应该如下
Blob b1;// 空 Blob
{
    //新作用域
    Blob b2 = {"a","an","the"};
    b1 = b2; // b1、b2 共享底层数据
}// b2 被销毁但 b2 中的元素不能销毁

定义 StrBlob 类

为实现共享内存,在类中使用 shared_ptr 来管理动态分配的 vector。shared_ptr 的成员会记录有多少个类共享 vector,在最后一个使用者被销毁时自动释放 vector。

class StrBlob
{
public:
    typedef std::vector::size_type size_type;
    StrBlob();
    StrBlob(std::initializer_list i1);
    size_type size() const {return data->size();}
    bool empty() const {return data->empty();}
    //添加删除元素
    void push_back(const std::string &t){data->push_back(t);}
    void pop_back();
    //元素访问
    std::string& front();
    std::string& back();
private:
    std::shared_ptr>data;
    //如果 data[i] 不合法,抛出异常
    void check(size_type i,const std::string &msg)const;
};
StrBlob 构造函数

两个构造函数,其中第二个构造函数接受一个 initializer_list 的参数,此构造函数将拷贝列表中的值来初始化 vector 的元素。

StrBlob::StrBlob():data(make_shared>){}
StrBlob::StrBlob():data(initializer_listi1):data(make_shared>(i1)){}


Note: initializer_list 形参

如果参数的实参数量未知但是全部实参的类型相同,则可以使用 initializer_list 类型的形参。initializer_list 是一种标准库类型,用于表示某种特定类型的值数组。头文件 #include

表3 initializer_list 提供的操作

操作 含义
initializer_listlst; 默认初始化;T类型元素的空列表
initializer_listlst{a,b,c…}; lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst);或lst2=lst 拷贝列表,但不会拷贝原列表中的元素,即拷贝后数据在内存中是共享的
lst.size() 列表中的元素数量
lst.begin() 返回指向lst中首元素的指针
lst.end() 返回指向lst中尾元素下一位置的指针

与vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中的元素值。
使用如下:

void error_msg(initializer_listi1)
{
    for(auto beg = i1.begin();beg != i1.end();++beg)
    {
        cout<< *beg << " ";
    }
    cout<

元素访问成员函数
void StrBlob::check(size_type i,const string &msg)const
{
    if(i>=data->size())
    {
        throw out_of_range(msg);
    }
}

string& StrBlob::front()
{
    //如果vector为空,check会抛出一个异常
    check(0,"front on empty strBlob");
    rturn 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();
}

直接管理内存

C++定义了两个运算符分配和释放动态内存。运算符new分配内存,delete释放由new分配的内存。

  • new 的用法
int *pi = new int; // pi指向动态分配,未初始化的无名对象
int *pi = new int();//值初始化为0
string *ps = new string; //初始化为空string
int *pi = new int(1024); //直接初始化
string *ps = new string(3,9); // *ps值为999
//vector有十个元素,值依次从0-9
vector *pv = new vector{0,1,2,3,4,5,6,7,8,9};

//p1指向一个与obj类型相同的对象,并用obj进行初始化
auto p1 = new auto(obj);

//也可以动态分配const对象
const int *pci = new const int(1024);
const string *pcs = new const string;
  • delete
    通过new分配的内存在不用时应delete,否则容易造成内存泄漏。但delete后指针变成空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存指针。为了避免空悬指针,可以在delete之后将nullptr赋予指针,说明指针不指向任何对象了。但有其局限性:当多个指针指向同一片内存时,delete之后重置指针只对该指针有效。
int *p(new int(42));
auto q = p; //p和q指向同一片内存
delete p;//p和q指向的内存释放掉了
p = nullptr;//只对p有效

普通指针与智能指针

  1. shared_ptr和new可以结合使用,可以将new返回的普通指针初始化一个智能指针,需要注意的是接受参数的智能指针构造函数是explicit的,因此,不能将内置指针隐式转换成一个智能指针,必须使用直接初始化的形式。
shared_ptr p1 = new int(1024);//错误,不能隐式转化
shared_ptr p2(new int(1024)); //正确:直接初始化

shared_ptr clone(int p)
{
    return new int(p);////错误,不能隐式转化
}

shared_ptr clone(int p)
{
    return shared_ptr(new int(p));//正确:显示用int*创建shared_ptr
}
  1. 混合使用普通指针和智能指针很容易发生错误,因为不知道智能指针何时释放内存,如果普通指针和智能智能指向同一块内存,智能指针一旦释放,普通指针则变成未定义的。
void process(shared_ptrptr)
{
    //使用ptr
}//ptr离开作用域,被销毁

int *x(new int(1024));//
process(x);//错误,不能隐式转换
//合法,但参数是临时的智能指针,函数调用结束会释放其指向的内存,导致x也被释放,成为空悬指针
process(shared_ptr(x));
int j = *x;//未定义,x指向的内存已被智能指针释放

  1. 不要用get函数获得的指针初始化另一个智能指针,或者为智能指针赋值。因为若这样做,两个智能指针是相互独立创建的,虽然指向同一片内存,但引用计数各自为一,当释放其中一个智能指针时,虽然另一个智能指针计数不为0,也会被释放。另外,用get返回的指针传递给普通指针后,该普通指针不能被delete,因为智能指针会自动释放内存。
  2. shared_ptr可以用reset来将一个新的指针赋予一个shared_ptr.
p = new int(1024);//错误,不能隐式转换
p.reset(new int(1024)); // 正确:p指向一个新对象

if(!p.unique())
{
    p.reset(new string(*p));//我们不是唯一用户;分配新的拷贝
}
*p += newVal;//现在知道自己是唯一用户,可以改变对象的值

你可能感兴趣的:(C++,学习与进阶)