S12动态内存

来源:互联网 发布:杭州java工资 编辑:程序博客网 时间:2024/06/16 07:02

S12动态内存


一、动态内存与智能指针

1、程序的内存管理
(1)静态内存用来保存局部static对象、类static数据成员、定义在任何函数之外的全局变量,栈内存用来保存定义在函数内的non-static对象,堆内存用来保存动态分配的对象,动态分配通过new/delete完成

注意:由于new/delete需要显式的操作,对内存的管理非常棘手,为了更安全更容易的使用动态内存,尽可能使用标准库提供的智能指针类型来管理动态对象,智能指针可以自动释放所指向的对象

(2)智能指针:shared_ptr/unique_ptr/weak_ptr定义在头文件memory中

2、智能指针
(1)shared_ptr允许多个指针指向同一个对象,unique_ptr独占所指向的对象,weak_ptr指向shared_ptr所指对象
(2)智能指针的操作

shared_ptr<T> sp    //空智能指针,可以指向类型为T的对象,作为条件时若nullptr则返回false,否则返回trueunique_ptr<T> up*p                  //解引用指针p,获得其指向的对象p.get()             //返回p中保存的指针,谨慎使用,因为若智能指针释放了对象,则返回的指针就是野指针swap(p, q)          //交换p和q中的指针,p/q必须同类型,不能交换unique和shared的指针p.swap(q)make_shared<T>(args)//返回一个shared_ptr指针,指向一个动态分配的类型为T的对象,使用args初始化此对象shared_ptr<T> p(q)  //p是q的拷贝,此操作会递增q中的计数器,q中的指针必须能转换为T*,不适用unique_ptrp = q               //递减p的引用计数,递增q的引用计数,若p的计数归0则将其管理的内存释放,不适用unique_ptrp.unique()          //若p.use_count()为1,则返回true,否则返回falsep.use_count()       //返回与p共享对象的智能指针数量,开销大,主要用于调试

(3)make_shared函数:最安全的分配和使用动态的内存的方法就是调用make_shared函数
(4)shared_ptr的拷贝和赋值:当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象,即关联计数器,每当拷贝或赋值时,计数器都会改变,当shared_ptr计数器归0时自动释放所管理的对象
(5)shared_ptr自动释放:只要动态对象对应的shared_ptr计数归0时,这个动态对象就会被析构函数销毁

注意:若将shared_ptr放在容器中,当容器进行一些操作后,及时使用erase销毁不需要的shared_ptr来释放内存

3、直接管理内存
(1)使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new只返回一个指向该对象的指针,默认情况该对象执行默认初始化,或者可以采用直接初始化(构造初始化、列表初始化)来显式初始化新对象

int *pi = new int;                            //pi指向一个动态分配且未初始化的intint *pi = new int();                          //值初始化,pi指向0string *ps = new string;                      //ps指向一个动态分配且默认初始化为""的stringint *pi = new int(1024);                      //构造初始化pi所指的对象为int类型的值1024string *ps = new string(2, '9');              //构造初始化ps所指的对象为string类型的"99"vector<int> *pv = new vector<int>{0,1,2,3,4}; //列表初始化pv所指的对象为vector<int>且包含0-4auto p1 = new auto(obj);                      //p1指向一个与obj对象类型相同的对象,并用obj初始化新对象auto p2 = new auto{a,b,c};                    //错误,只允许有单一初始化器

注意:当提供括号包围的单一初始化器时,可以采用auto来推断类型

(2)动态分配const对象
const对象的动态分配必须进行显式/隐式初始化,且返回的是指向const对象的指针

const string *pcs = new const string;   //使用string默认构造函数隐式初始化const int *pci = new const int(1024);   //显式初始化

(3)使用delete释放和销毁对象
delete必须指向一个动态分配的对象或是一个空指针,delete完成销毁给定的指针指向的对象并释放对应的内存,const动态对象虽然不能被修改,但是是可以被delete销毁的

注意:释放一块非new分配的内存、相同的指针多次释放都是未定义的行为

4、shared_ptrnew结合使用
(1)使用new来初始化shared_ptr,但是内置指针(new返回的指针)不能隐式转换为智能指针

shared_ptr<int> p2(new int(42));  //直接初始化,p2指向int类型的42,显式转换shared_ptr<int> p1 = new int(42); //错误,必须显式初始化,内置不隐式转换为智能

(2)shared_ptr支持的另一组操作

shared_ptr<T> p(u)      //p从unique_ptr u那里接管了对象的所有权,并将u置为空shared_ptr<T> p(q, d)   //p管理q所指向的对象,q必须是new分配的且能转为T*,销毁时p将调用可调用对象d来代替deleteshared_ptr<T> p(p2, d)  //p是智能指针p2的拷贝,销毁时p将调用可调用对象d来代替deletep.reset()               //若p是指向某对象的唯一shared_ptr,reset会释放此对象p.reset(q)              //若传递了可选参数内置指针q则将p指向qp.reset(q, d)           //若传递了可选参数q和d,则在以后释放q是调用d来代替deletep = new int(1024);      //错误,不能将内置指针赋值给智能指针p.reset(new int(1024)); //正确

(3)不要混合使用智能指针和内置指针,当将一个智能指针绑定到一个内置指针时,此后就不应该再用过内置指针来访问对象,且不应该用get函数来初始化另一个智能指针或为智能指针赋值

5、智能指针和异常
(1)当使用内置指针,异常发生时可能会造成内存泄漏,而智能指针不会,参考《Effective C++》item13 RAII对象

int *p = new int(42);//throw an exceptiondelete p;   //由于此前发生异常,造成函数等直接退出销毁临时变量,指针p在delete前就被销毁,若指向42的只有p,则p销毁后发生内存泄漏shared_ptr<int> sp(new int(42));//throw an exception//当发生异常造成函数退出时,sp是智能指针,当发现只有sp指向42时,sp在销毁时会同时销毁42,没有内存泄漏发生

(2)智能指针使用规范

  • 不使用相同的内置指针初始化或reset多个智能指针
  • 不对get返回的指针使用delete
  • 不使用get返回的指针初始化或reset另一个智能指针(即不允许p1.reset(p2.get())
  • 若使用了get返回的指针,切记当最后一个shared_ptr被销毁时,此返回的指针所指内容就已经不存在了
  • 若使用智能指针来管理的资源不是new得到的(即不能用默认的delete释放)需要传递一个删除器
shared_ptr<int> p(new int(42));  int *q = p.get();                 //利用get获得42的内置指针{    shared_ptr<int> p1(q);        //利用内置指针创建另一个智能指针指向同一个42}                                 //由于两个智能指针是单独建立的,虽然指向同一块内存,但是计数器各自为1,离开作用域后,p1连带42一起被销毁int foo = *p;                     //此时p是空悬指针,内容已被销毁,赋值会出现未定义行为

6、unique_ptr
unique_ptr与所指对象的绑定是唯一的,当unique_ptr被销毁时,对象也被销毁,因此没有类似make_shared函数且不能拷贝或赋值,定义时需要绑定到内置指针上
(1)unique_ptr支持的操作

unique_ptr<T> u1           //空unique_ptr,可以指向T类型的对象,u1默认采用delete来释放对象unique_ptr<T, D> u2(q, d)  //类型为D的可调对象d来释放对象u = nullptr                //释放u所指的对象,同时u置为空u.release()                //不释放u所指对象只返回对象的指针,同时u放弃对该对象的“拥有”并被置为空u.reset()                  //释放u所指的对象,如果提供了内置指针q则u指向q,否则将u置空u.reset(q)u.reset(nullptr)unique_ptr<string> p1(new string("abc"));unique_ptr<string> p2(p1.release());  //release将p1=nullptr即置空,并使p2指向原p1指的"abc"unique_ptr<string> p3(new string("def"));p2.reset(p3.release());               //reset使"abc"被释放,并让p2指向p3的"def",且p3=nullptr

注意:unique_ptr不支持拷贝或赋值,但可以通过reset和release实现对象转移

注意:release返回的指针必须有接受者,否则会导致内存并未释放且丢失了指针,造成内存泄漏

(2)向unique_ptr传递删除器
默认情况下,shared_ptrunique_ptr都使用delete来释放资源,,向unique_ptr传递删除器需要重载其默认删除器,这是因为两者对删除器的管理不同:通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销;通过在运行时绑定删除器,shared_ptr使用户重载删除器更为方便

shared_ptr<T> p(new obj, deleter);unique_ptr<T, D> u(new obj, func);unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);

7、weak_ptr
weak_ptr是一种不控制所指对象生存期的智能指针,可以指向shared_ptr但不增加引用计数,且对象被销毁时无视是否有weak_ptr指向它
(1)weak_ptr支持的操作

weak_ptr<T> w1     //空weak_ptr,可以指向T类型的对象weak_ptr<T> w2(p)  //w2与p指向相同对象,T必须能转换成p的对象类型,p可以是shared_ptr或weak_ptrw = p              //p可以是shared_ptr或weak_ptr,赋值后共享对象w.reset()          //w=nullptr置空w.use_count()      //与w共享对象的shared_ptr数量w.expired()        //若w.use_count()为0则返回true,否则返回falsew.lock()           //若w.expired()为true,则返回一个空shared_ptr,否则返回一个指向w对象的shared_ptr//由于weak_ptr所指的对象可能不存在,因此是不能直接通过weak_ptr访问对象,而要使用w.lock()if(shared_ptr<int> np = w.lock())        //通过lock来确认weak_ptr所指对象是否存在{    do something with np;                //若存在,通过lock返回的shared_ptr来访问对象}

二、动态数组

1、new和数组
(1)通常称new T[]分配的内存为动态数组,但是实际上new返回的是数组元素类型的指针而不是一个数组类型的对象,因此动态数组不是数组类型

int a[10];            //a是一个数组,类型是int[10]begin(a);             //正确,a是一个数组auto b = new int[5];  //new返回int*而不是数组begin(b);             //错误,对指针不能使用begin,b是动态数组

(2)初始化动态数组

int *pia = new int[10];                //默认初始化int *pia2 = new int[10]();             //值初始化为0int *pia3 = new int[10]{0, 1, 2, 3};   //列表初始化,未提供的进行值初始化为0

注意:动态分配一个空数组是合法的,返回的指针类似尾后指针,因此不能解引用

(3)释放动态数组

delete p;     //p必须指向动态分配的对象delete [] p;   //p必须指向动态数组

注意:释放动态数组时从最后一个元素开始逆序释放,如果忽略[]或是对非动态数组的动态对象释放时加上[]是未定义的行为

(4)智能指针和动态数组
标准库提供了能管理动态数组的unique_ptr,在使用时在元素类型后加上[]

unique_ptr<int[]> up(new int[10]);up[2] = 10;            //动态数组的unique_ptr允许使用下标操作而没有解引用操作

注意:shared_ptr一般不能直接管理动态数组,若一定要管理将有诸多繁琐的操作,参考《C++primer 5th》p.426

2、allocator
allocator类定义在头文件memory中,将内存分配和对象构造分离(new的内存分配和对象构造是整合的,delete的内存释放和对象析构是整合的),它提供一种类型感知的内存分配方法,所分配的内存是原始未构造的
(1)allocator操作

allocator<T> a       //定义名为a的allocator对象,它可以用来为类型为T的对象分配内存a.allocate(n)        //分配一段原始未构造的内存,可保存n个类型为T的对象a.deallocate(p, n)   //p是T*类型的指针,释放从p开始的内存,n必须是p创建时要求的大小                     //p必须是allocate返回的指针,且在deallocate前用户必须对区域内每个对象调用destroya.construct(p, args) //p是T*类型的指针指向原始内存,args被传递给类型为T的构造函数用来在p指向的内存中构造对象a.destroy(p)         //p是T*类型的指针,对p指向的对象执行析构函数

(2)allocator分配未构造的内存
allocator分配的是未构造的内存,我们需要按需进行构造,construct函数接受一个指针和额外的参数用来初始化构造对象

allocator<string> alloc;          //可以分配string的allocator对象auto const p = alloc.allocate(n); //分配n个未初始化的stringauto q = p;                       //定义q后q一直指向最后一个已构造对象的后面alloc.construct(q++);             //构造一个空串存放在p指向的位置alloc.construct(q++, 2, 'c');     //构造一个"cc"存放在p+1指向的位置while(q != p){    alloc.destroy(--q);           //destroy对指向的对象析构,此后这块内存又可以重新使用construct}alloc.deallocate(p, n);           //所有对象都destroy后,可以归还内存给系统

注意:使用未经过construct构造的allocator返回的原始内存,行为是未定义的

(3)allocator算法

uninitialized_copy(b, e, b2)    //将迭代器b/e之间的元素或是b开头的连续n个元素拷贝到b2指定的未构造的原始内存中uninitialized_copy_n(b, n, b2)  //b2指向的内存必须足够大,返回一个指针指向最后一个构造元素之后的位置uninitialized_fill(b, e, t)     //在迭代器b/e指定的或是b开头的连续n个大小的原始内存中创建填充对象t的拷贝uninitialized_fill_n(b, n, t)  //原始内存必须足够大容纳n个对象,返回void