《c++ primer》 第12章 动态内存 学习笔记

来源:互联网 发布:照片大小修剪软件 编辑:程序博客网 时间:2024/05/22 01:35

概述

静态内存:保存static变量和全局变量。它在程序结束后自行销毁。
栈内存:保存局部非static变量,它在程序块接受后自行销毁。
堆内存:不会自动释放,需要手动释放。

12.1动态内存与智能指针

c++中动态内存的管理是通过一对运算符来完成的:
new:在动态内存中对对象分配空间并返回一个指向该对象的指针,我们可以对该对象进行初始化。
delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
有时我们会忘记释放它,造成内存泄漏。
有时还在使用这个动态对象指针时就释放了它,造成引用非法内存的指针。

C++11新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,区别是它自动释放所指向的内存。

它们定义在头文件#include <memory>

两种智能指针:

shared_ptr:允许多个指针指向同一个对象。

unique_ptr:独占所指向的对象。

伴随类weak_ptr:弱引用,指向share_ptr所管理的对象。

12.1.1 share_ptr类

shared_ptr和unique_ptr都支持的操作 
shared_ptr<string>sp; //空智能指针。可以指向string类型的对象 unique_ptr<string>up;  sp         //sp可以作为条件判断sp是否指向一个对象  *sp        //解引用sp,获得它指向的对象  sp->mem    //等价于(*sp).mem  sp.get()   //返回sp中所报存的指针。要小心使用,所智能指针释放了对象,则返回的指针所指向的对象也不存在了。  swap(sp,sq)  sp.swap(sq)  //交换sp和sq中的指针    shared_ptr支持的操作  make_shared<T>(args)  //返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化对象  shared_ptr<T>p(q)     //p是shared_ptr q的拷贝,此操作会递增q中的记数器,q中的指针必须能转换成T*  p = q                 //都是shared_ptr,保存的指针必须能相互转换,操作会递减p的引用计数,增加q的引用计数,p引用计数为0时会释放其管理的内存  p.use_count()         //返回与p共享智能指针的数量,可能很慢主要用于调试  p.unique()            //当p.use_count()为1时,返回ture,否则返回false。  

初始化(构造函数)

shared_ptr<string>sp;//默认构造函数,空指针
shared_ptr<T>p(q)     //p是shared_ptr q的拷贝,此操作会递增q中的记数器,q中的指针必须能转换成T* 
make_shared<T>(args)  //返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化对象  
**注意:make_shared<T>(args)中      T类类型时:args必须与T的某个构造函数相匹配。 T为内置类型时:args必须能用来初始化一个T

赋值拷贝p = q ;

swap(sp,sq);
p= make_shared<T>(args);

#include <iostream>#include <string>#include <vector>#include <memory>using namespace std;int main(){shared_ptr<string> sp;//cout << sp.use_count;//提示出错。因为没有指向任何对象make_shared<string>(); //动态分配内存默认初始化,调用默认构造函数必须要有括号make_shared<string>("a"); //动态分配内存值初始化shared_ptr<string>sp2 = make_shared<string>();//初始化智能指针shared_ptr<string>sp3 = make_shared<string>("b");//初始化智能指针cout << sp3.use_count() << endl;auto sp4 = sp3;//拷贝一次观察计数器变化cout << sp3.use_count() << endl;sp4 = sp2;//被其他赋值,观察计数器变化cout << sp3.use_count() << endl;getchar();}

程序使用动态内存出于以下三种原因

1.程序不知道自己需要使用多少对象

2.程序不知道所需对象的准确类型

3.程序需要在多个对象间共享数据。

使用动态内存的一个常见的原因是允许多个对象共享相同的状态。

例子:我们希望定义一个Blob类,保存一组元素,与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。既当我们拷贝一个Blob时,

原Blob对象及其拷贝应该引用相同的底层元素。

定义一个管理string的类,命名为StrBlob。

#include <iostream>#include <string>#include <memory>             //智能指针和动态分配内存#include <vector>#include <initializer_list>   //初始值列表#include <stdexcept>class StrBlob{public:typedef std::vector<std::string>::size_type size_type;StrBlob();StrBlob(std::initializer_list<std::string>il);//初始化列表构造函数size_type size()const{ return data->size(); }bool empty() { return data->empty(); }//添加删除元素void push_back(const std::string &s){ data->push_back(s); }void pop_back();//访问元素std::string& front();std::string& back();const std::string& front()const;const std::string& back() const;private:std::shared_ptr<std::vector<std::string>> data;//智能指针,指向string的vector//private 检查函数,当我们访问容器时检查下标是否越界void check(size_type i, const std::string &msg)const;};//构造函数StrBlob::StrBlob() :data(std::make_shared<std::vector<std::string>>()) { }StrBlob::StrBlob(std::initializer_list<std::string>il) :data(std::make_shared<std::vector<std::string>>(il)) { }//检查函数void StrBlob::check(size_type i, const std::string &msg)const{if (i >= data->size())//不在范围内throw std::out_of_range(msg);}//重载函数:const和non_const是因为有时候有const StrBlob对象,此时需要用const函数//调用const版本时对象是const,所以this指针也是const,通过转换this指针才能调用const版本,std::string& StrBlob::back(){check(0, "back on empty StrBlob"); return data->back();}std::string& StrBlob::front(){check(0, "front on empty StrBlob");return data->front();}//const版本的,当const StrBlob A时,数据成员也是const的const std::string& StrBlob::back() const{check(0, "back on empty StrBlob");return data->back();}const std::string& StrBlob::front() const{check(0, "front on empty StrBlob");return data->front();}void StrBlob::pop_back(){check(0, "pop_back on empty StrBlob");data->pop_back();}int main(){std::shared_ptr<StrBlob> sp;//空指针std::cout << sp << std::endl;StrBlob s({ "zhang", "yang"});StrBlob s2(s);//共享s内的数据std::string st = "abcd";s2.push_back(st);std::cout << s2.front() << std::endl;std::cout << s2.back() << std::endl;}


12.1.2直接管理内存

默认情况下,动态分配的内存是默认初始化的,这意味着内置组合或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。

int *p=new int;

直接初始化方式:int *p=new int(1024);

值初始化方式:int *p=new int();

delete p;//p必须指向一个动态分配的对象或是一个空指针

动态内存的管理非常容易出错

1,忘记delete内存

2,使用已经释放掉的对象

3,同一块内存释放两次

坚持只使用智能指针,就可以避免所有的问题,对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会释放它。

delete之后重置指针

delete p;之后,指针值变得无效,但是指针仍然保存着动态内存的地址,此时的指针叫做空悬指针。可以在delete之后将nullptr赋予给指针。

12.1.3shared_ptr和new结合使用

如果我们不初始化一个智能指针,它就会被初始化为一个空指针。

我们可以用new返回的指针来初始化智能指针。接受指针参数的智能指针构造函数是explicit(避免隐式转换)的,我们不能将一个内置指针隐式转换为一个智能指针。

shared_ptr<int>p(new int(10)); //正确,调用了构造函数来初始化的 
shared_ptr<int>p=new int(10);  //错误,int *隐式转换为share_ptr类型的(构造函数接受int *),然后在初始化


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

当将一个share_ptr绑定到一个普通指针时,我们将内存的管理责任交给了这个share_ptr。一旦这样做了,我们就不应该在使用内置指针来访问share_ptr所指向的内存了。

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

智能指针定义了一个名为get的函数,返回一个普通类型的指针。eg:p.get() 返回一个int *

目的:向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针。

将另一个智能指针绑定到get返回的指针也是错误的。

永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

普通指针不能自动转化为智能指针。


p.get()       //返回p中的指针,为内置类型

p.reset()    //p是唯一指向其对象的share_ptr,reset会释放此对象,置空p

p.reset(q)  //若q为内置类型,令p指向q


所以为了正确使用智能指针,我们必须坚持一些基本规范:

1,不使用相同的内置指针初始化(或reset)多个智能指针。

2,不delete get()返回的指针

3,不使用get()初始化或reset另一个智能指针

4,如果使用了get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变得无效了。

5,如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。


12.1.5unique_ptr

一个unique_ptr 拥有它所指向的对象,和shared_ptr不同,某个时刻只能有一个unique_ptr 指向一个给定对象,当unique_ptr 被销毁时,对象也被销毁。

初始化:只支持new内置类型的初始化

 unique_ptr<int>q(new int(p));  //正确

 unique_ptr<int>q;//正确

 unique_ptr<int>q=new int(p); 错误,不支持隐式初始化  

unique没有类似make_shared,必须手动new,将其绑定。由于unique_ptr独占它所指向的对象,因此他不支持普通的拷贝和赋值

但是有种特殊的拷贝可以支持:我们可以拷贝或赋值一个即将要被销毁的unique_ptr。

虽然我们不能拷贝或赋值unique_ptr,但是可以通过调用release()或reset()来将指针的所有权从一个unique_ptr转移给另一个unique:

u.release();//u放弃了对指针的控制权,返回内置指针,并将u清空

u.reset();//释放u指向的对象,u不需要担心,因为程序结束u自动清空。

u.reset(q);//若q为内置指针,令u指向这个对象

#include<iostream>#include<memory>using namespace std;int main(){unique_ptr<int> u1(new int(1));cout << *u1 << endl;//不能直接赋值,只能转移值//前提是要先释放,然后给另一个指针赋值//用到u.release()和u.reset()函数unique_ptr<int> u2(u1.release());//u1释放自己,赋值给u2cout << *u2 << endl;unique_ptr<int> u3;u3.reset(u2.release());//u2释放自己,赋值给u3cout << *u3 << endl;}

注意: p.release()返回一个指针,并将p置空,若没有为对象赋值,则出现错误。p.release( );               //错误:p被置空,并返回了一个内置指针,但是内存泄漏了auto q = p.release( );      //q 是int * 类型, 记得delete释放q

12.1.6weak_ptr


weak_ptr 是一种不控制对象生存期的智能指针,它指向由一个shared_ptr 管理的对象。所以当我们创建一个weak_ptr 必须用一个 shared_ptr 初始化。

引入lock和expired是防止在weak_ptr 不知情的情况下,shared_ptr 被释放掉

weak_ptr 不会更改shared_ptr 的引用计数。

std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr

std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。

此外,std::weak_ptr 还可以用来避免 std::shared_ptr 的循环引用。

/* *避免拷贝,多个指针共用一个vector<string> *使用weak_ptr访问共享的对象 * */#include <iostream>#include <vector>#include <string>#include <initializer_list>#include <memory>#include <stdexcept>#include <fstream>#include <sstream>class StrBlob;class StrBlobPtr;class StrBlob{public:friend class StrBlobPtr;typedef std::vector<std::string>::size_type size_type;StrBlob(); //默认构造函数StrBlob(std::initializer_list<std::string>il); //拷贝构造函数size_type size() { return data->size(); }      //对data进行解引用就是对vector<string>操作std::string& front();std::string& back();const std::string& front()const;const std::string& back()const;void push_back(const std::string &s) { data->push_back(s); }void pop_back();StrBlobPtr begin();StrBlobPtr end() ;private:void check(size_type sz, std::string msg)const;std::shared_ptr<std::vector<std::string>>data;};std::string& StrBlob::front(){check(0, "front on empty vector");return data->front();}std::string& StrBlob::back(){check(0, "back on empty vector");return data->back();}//const 版本const std::string& StrBlob::front()const{check(0, "front on empty vector");return data->front();}const std::string& StrBlob::back()const{check(0, "back on empty vector");return data->back();}void StrBlob::check(size_type sz, std::string msg)const{if (sz >= data->size())throw std::out_of_range(msg);}StrBlob::StrBlob() :data(std::make_shared<std::vector<std::string>>()) { }StrBlob::StrBlob(std::initializer_list<std::string> il) :data(std::make_shared<std::vector<std::string>>(il)) { }/* --------------------------------------------------------------------------------- *///必须定义在StrBlobPtr的后面class StrBlobPtr{public:friend StrBlob;StrBlobPtr() :curr(0){ }StrBlobPtr(StrBlob &s, std::size_t sz = 0) :wptr(s.data), curr(sz){ }std::string& deref()const;//返回当前stringStrBlobPtr& incr(); //递增private:std::shared_ptr<std::vector<std::string>> check(std::size_t i, const std::string &msg)const;std::weak_ptr<std::vector<std::string>> wptr;std::size_t curr;  //当前下标};StrBlobPtr& StrBlobPtr::incr(){check(curr, "increment past end of StrBlobPtr");++curr; //推进当前位置。  return *this;//为什么要return *this, 如果再次自加可以重复,举个例子就像赋值一样 a = b = c;  //如果不返回对象不能继续赋值。} //return *this是一份拷贝。 return this是地址。std::string& StrBlobPtr::deref()const{auto p = check(curr, "dereference past end"); //shared_ptr引用计数会增加,但是作用域结束后,引用计数又会减1return (*p)[curr];//p是所指的vector,(*p)指向vector第一个元素,与int (*p)[10]一个道理}//check检查是否存在shared_ptr和大小std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg)const{auto ret = wptr.lock(); //检查是否存在,存在返回shared_ptr,不存在返回空的shared_ptr.if (!ret)throw std::runtime_error("unbound StrBlobPtr");if (i >= ret->size())throw std::out_of_range(msg);return ret;}StrBlobPtr StrBlob:: begin() { return StrBlobPtr(*this); }StrBlobPtr StrBlob::end() { auto ret = StrBlobPtr(*this, data->size());  return ret; }int main(int argc, char*argv[]){std::fstream is(argv[1]);std::string s;StrBlob S;while (std::getline(is, s)){std::string temp;std::istringstream ist(s);while (!ist.eof()){ist >> temp;S.push_back(temp);}}std::cout << "size:" << S.size() << std::endl;StrBlobPtr sp(S);for (auto it =0 ;it!= S.size(); ++it){std::cout << sp.deref() << std::endl;sp.incr();}}


12.2动态数组

new和delete一次只能分配和释放一个对象,但有时我们需要一次为很多对象分配内存的功能

C++和标准库引入了两种方法,另一种new 和 allocator。

使用allocator 通常会提供更好的性能和更灵活的内存管理能力。

12.2.1new和数组

动态数组不是数组类型的。所以不能调用begin和end,也不能调用范围for语句。而是用数组维度来返回指向首元素和尾后元素的指针。


#include <iostream>  #include <memory>    using namespace std;    typedef int arr[10];  //声明arr为一个int的10元素数组  int main()  {      int *p = new int[10];      int *p2 = new arr;  //两个等价,返回指向第一个int的指针    for(int i = 0; i < 10; i++)      {          p[i] = i;      }      for(int i = 0; i < 10; i++)          cout << p[i] << " ";      cout << endl;      //for(const int i : p);         //error:动态分配数组返回的不是数组类型,而是数组元素的指针。所以不能用范围for        /*---------------------------------- */      //初始化动态数组      int *pi = new int[10];          //默认初始化    int *pi2 = new int[10]();       //初始化为0,且有括号必须为空 +括号即可     string *ps = new string[10];       //10个空string      string *ps2 = new string[10]();    //10个空string      //可以使用列表初始化      int *pi3 = new int[10]{1,2,3,4,5,6,7,8,9,0};//初始值列表里的值不能多于容量,否则new失败,不会分配内存      string *ps3 = new string[10]{"a","b","c","d","e","f","g","h","i",string(3,'x')};      //释放动态数组      delete []pi3; //必须要加[]括号,且释放动态数字时是逆序释放。如果delete动态数组不加[],行为是未定义的。    /*----------------------------------- */      //智能指针和动态数组,标准库定义了特别的unique_ptr来管理,当uo销毁它管理的指针时,会自动调用delete [];      int *p5 = new int[10];      //unique_ptr<int[]> up;         unique_ptr<int[]> up(p5);        for(int i = 0; i < 10; ++i)          cout << up[i] << " ";      cout << endl;      //如果使用shared_ptr的话我们必须自己定义delete函数      shared_ptr<int>sp(new int[10], [](int *p) { delete []p;});      //智能指针不支持算数类型,如果要访问数组中的元素,必须使用get函数返回一个内置指针。      cout << (*sp.get()) << endl;    }  


12.2.2allocator类

引入allocator的原因是new类上的缺陷。我们分配单个对象时,希望将内存分配和对象初始化组合在一起。但是分配大块内存时,只有在需要时才真正执行对象创建操作。

new它将内存分配和对象构造结合到了一起,delete将对象析构和内存释放组合一起。就造成了不必要的浪费。

比如:string *p = new string;

new是现在找一块内存分配,不够继续malloc,在分配内存的地址上调用构造函数,delete也一样,在释放内存的时候也会调用析构函数。

内置类型要指定初值。

但是如果我们希望指定它的初值,不让它调用默认构造函数new就不可行了,而且本身调用了一次构造函数,然后我们赋值了一次。

更重要的是,没有默认构造函数的就不能动态分配内存了。


allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它分配的内存是原始的、未构造的。
 allocator<string> alloc;   auto const p = alloc.allocate(ivec.size()*4);//返回迭代器,指向第一个内存的位置。 


allocator<T> a 
a.allocate(n)       //分配一段原始的未分配的内存,返回迭代器,指向第一个内存位置a.deallocate(p,n)   //释放指针p开始的内存a.construct(p,args) //在p指向的内存中构造一个对象a.destroy(p)        //对p指向的对象指向析构函数


标准库还为allocator定义了两个伴随算法

在未初始化的内存中创建对象,都定义在头文件memory

uninitialized_copy(b,e,b2)      //b,2是输入容器的迭代器,b2是内存的起始地址,要保证空间足够  uninitialized_copy_n(b,n,b2)    //b是输入容器的起始迭代器,复制n个,复制到以b2为起始地址的动态内存中  uninitialized_fill(b,e,t)       //b,e是动态内存的起始和终止位置,t是要fill的元素  uninitialized_fill_n(b,n,t)     //b是动态内存的起始,fill n个,t是要fill的元素 



                                             
1 0
原创粉丝点击