C++primer第五版第12章笔记
动态内存
前面章节中介绍了静态内存区,用来保存static对象、类static数据成员、以及定义在任何函数之外的变量,在创建前分配,在程序结束时销毁;栈内存区,用来保存局部自动对象,仅在程序运行时存在;本章介绍动态内存区(堆区)创建对象,我们需要显示的销毁对象并回收内存,一般可以用智能指针来管理对象。
12.1动态内存和智能指针
我们需要一对操作来对动态内存对象进行管理,使用new创建新对象,使用delete销毁对象;有时我们没有释放内存会造成内存泄露,或者销毁了有指针引用的内存,会造成引用非法内存的指针。
12.1.1shared_ptr类
使用智能指针时必须在尖括号中表明元素类型,默认初始化则为空指针,解引用可以得到指向的对象,如果将指针放在条件中,是判空的意思。
最安全的分配动态内存的方法是调用make_shared函数(保存于头文件memory),需要在尖括号中指明要创建元素的类型,与顺序容器的emplace相似可以添加参数来使用元素类型的构造函数。
shared_ptr<int> p=make_shared<int>(5);(我们可以不传值采用默认初始化,或者使用auto替换智能指针声明)
每个shared_ptr都有一个关联的计数器,当我们拷贝或将其当做参数传递或者作为返回值返回时都会使计数器加1,注意即便在临时块中创建,若临时块结束智能指针计数不为0也不销毁,shared_ptr计数器减为0时,会自动调用析构函数销毁对象,并释放内存。一般使用动态内存有三个原因:1程序不知道需要多少对象,2程序不知道对象的准确类型,3多个对象之间需要共享数据。
12.1.2直接管理内存
C++使用new和delete可以直接管理动态内存区域,对于某些内置类型和没有默认构造函数的类类型,我们若进行默认初始化,值将是未定义的,需要采用值初始化。
int *p=new int;(对内置类型进行默认初始化将产生未定义的值)
int *p=new int();(进行值初始化,值为0)
const int *p=new const int(1);(创建指向const的指针)
若内存耗尽,继续new会抛出bad_alloc异常,可以在new后加上(nothrow)创建空指针不分配内存,注意动态内存对象生存周期为直到对象被显示的释放,我们可能使用多个指针指向同一个相同的内存,但当我们delete一个之后,另一个可能变成空悬指针,找出这些指针往往比较困难。
12.1.3shared_ptr和结合new一起使用
我们如果不对智能指针赋值,那么将会生成一个空指针,注意由于智能指针的构造函数是explicit的,所以不能用=将new的普通指针隐式转化为智能指针,需要用值初始化的方式显示的赋值给智能指针。
shared_ptr<int>p=new int();(错误,将new返回的普通指针隐式转化为智能指针)
shared_ptr<int>p(new int());(值初始化的方式创建一个指向0的智能指针)
可以将指针绑定到指向其他类型资源的指针,但是我们需要自定义释放资源方法替代delete
shared_ptr<t>p(q);(p管理q指针,qnew的对象类型必须能转化为t)
shared_ptr<t>p(q,d);(q同上,p使用d替代delete)
p.reset(q);(使用p管理q,若p原指内容的计数器减为0则进行删除操作)
p.reset(q,d);(新指针采用d来delete)
我们最好不要使用普通指针和智能指针混用,因为可能在某个块中用智能指针管理普通指针,块中结束时智能指针减为0,普通指针所指内存区域被释放,普通指针就悬空了,也不要使用get方法返回的指针来重新建立智能指针,若这样做可能造成两个不同的智能指针指向同一块内存,如下:
shared_ptr<int>p(new int(42));
int *q=p.get();
{
shared_ptr<int>(q);
}(使用get获取普通指针,在块内用q再生成一个智能指针,导致块结束时,指针回收内存,致使p悬空)
int foo=*p;
12.1.4智能指针异常
我们使用智能指针可以避免异常导致程序未正常关闭而使资源无法释放,可以采用智能指针来管理非new的对象,但如果该对象未定义析构函数,我们需要自定义删除函数并在指针创建时声明。
shared_ptr<connection>p(&c,end_connection);(我们自定义end_cinnection,这里自动生成指针来调用函数,我们也可以传递&来调用)
12.1.5unique_ptr
unique_ptr只能有一个指向一个给定对象,我们定义unique_ptr时需要显示的用new返回的指针,由于唯一性,所以我们不能拷贝或者赋值,但可以在函数返回时,返回局部创建的unique_ptr指针,也可通过relese或者reset将所有权转交给别的智能指针,unique_ptr也可以传递给其删除器。
u.release();(若不用其他智能指针管理返回的指针,将会使管理的指针丢失,从而无法回收内存)
u.reset(q);(使q管理的指针交由u,并且释放q)
unique_ptr<connection,decltype(end_connnection)*>p(&c,end_connection);
12.1.6weak_ptr
我们使用weak_ptr的目的主要有两个:1空悬指针问题,如果我们将两个指针同指一个区域,若一个指针被释放,另一个将变成空悬指针,使用weak_ptr可知道是否被释放,并通过lock函数变成shared_ptr;2循环引用问题,有时我们将shared_ptr指针循环引用导致资源不被释放,但如果我们使用weak_ptr来管理循环引用就能避免。weak_ptr指针绑定shared_ptr指针,不会使计数器增加,是一种弱引用。
weak_ptr<t>w(sp);(与shared_ptr sp指向相同对象,t必须能转化成sp指向类型)
w.reset();(将指针置空)
w.lock();(若shared_ptr存在返回shared_ptr指针,否则返回一个空shared_ptr指针)
12.2动态数组
有时我们需要一次定义多个对象,这时我们可以选择动态数组来分配对象。C++提供两种方法:1使用另一种形式的new表达式,可以分配并初始化一个对象数组;2使用allocator类,允许将分配和初始化分离。一般我们不会直接访问动态内存数组,但当需要可变数量的对象时,使用标准库容器,若使用动态数组我们需自定义拷贝、赋值、销毁时对应的内存区域。
12.2.1new和数组
使用new分配一个动态数组,我们需要在类型名后加一对方括号,指明要分配的对象数目,new会返回指向第一个元素类型(第一个对象)的指针,由于返回的不是数组类型,所以我们不能使用begin或end来获取首和尾指针。我们可以在方括号后加小括号进行值初始化,或者加列表进行初始化。可以动态分配一个大小为0的数组,返回指针相当于一个尾指针,但不能对其使用解引用。释放动态内存数组需要使用delete[]。可以使用智能指针来管理动态内存数组,如下:
unique<int[]>p(new int[5]());(unique必须在类型中加[]表明是动态数组类型)
p.release();(自动调用delete[]销毁指针)
p[0];(表示数组对象第一个值,可以使用下标访问)
shared_ptr<int>q(new int[5],[](int *p){
delete[] q;
});(使用shared_ptr管理必须自定义删除器)
q.reset();(调用上述lambda表达式)
注意由于shared_ptr不能使用下标访问,通常遍历元素如下:
for(size_t i=0;i!=10;i++){
*(sp.get()+i)=i; //使用get返回的指针
}
12.2.2allocator类
new将内存分配和对象构造组合在了一起,delete将对象析构和释放内存组合在了一起。我们需要先分配大量内存,再按照需要构造对象或将不必要的内存释放,如果采用new将造成大量内存在分配时就构造了对象,降低了效率。我们可以使用allocator类来先分配内存,此类保存与头文件memory中。
allocator<string> allo;(创建一个名为allo的allocator对象,将为类型为string的对象分配内存)
allo.allocate(n);(分配n个大小类型为string的内存,返回指向第一个元素类型的指针)
allo.construct(p,args);(p是某个类型的指针,args为参数列表构造对象)
allo.destroy(p);(p为某个类型指针,此算法调用指针类型对象的析构函数)
allo.deallocate(p,n);(p为allocate返回的指针,从p开始释放n个指针所指类型的内存,必须在n个对象都调用destroy析构之后才能调用)
以下为拷贝填充未初始化内存的算法
uninitialized_copy(b,e,b2);(从b和e迭代器范围拷贝到b2开始的位置,b2所指内存必须足够大能存下,返回指针指向拷贝完最后一个元素的后一个位置)
uninitialized_copy_n(b,n,b2);(从b开始拷贝n个元素到b2)
uninitialized_fill(b,e,t);(从b到e的范围填充值为t的元素)
uninitialized_fill_n(b,n,t);(填充n个)