动态内存
动态内存与智能指针
除了静态内存和栈内存,每个程序还拥有一个内存池被称为堆。程序用堆来存储动态分配的对象。
c++中动态内存的管理是通过new:在堆中为对象分配空间并返回一个指向该对象的指针;delete:接受一个动态对象的指针,销毁该对象并释放与之关联的内存。
动态内存的使用很容易出问题,保证其在正确的时间释放内存是及其困难的。为了更容易的使用动态内存c++11的标准库提供了智能指针类型,类似与正常的指针,但它会自动释放其所指的对象。
shared_ptr允许多个指针指向同一个对象。
uniqe_ptr则独占所指向的对象。
weak_ptr一种弱引用,指向shared_ptr所管理的对象。
这三种类型都定义在memory头文件中。
智能指针类
类似vector,智能指针也是模版。定义方式与之相似shared_ptr<string> p1;
。
默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似。解引用一个智能指针返回其指向的对象。
以下列举出了一些智能指针的通用操作:
通用操作 | 含义 |
---|---|
shared_ptr<T> sp/unique_ptr<T> up |
空智能指针,可以指向T类型的对象 |
p | 将p作为条件判断,若指向一个对象则返回true |
*p | 解引用p,返回其所指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中所保存的指针。若释放了其对象,返回指针所指向的对象也消失了。 |
swap(p,q)/p.swap(q) | 交换p与q |
shared_ptr
当shared_ptr进行拷贝或复制时,每个shared_ptr都会记录有多少个其他的shared_ptr指向这个对象。
每个shared_ptr都有一个关联的计数器,被称为引用计数。当我们拷贝一个shared_ptr或传递参数等其计数会递增,当shared_ptr被销毁赋予新值等其计数器会递减。
一旦一个shared_ptr的计数器为0时,它就会通过析构函数来释放其所管理的对象。
当动态对象不再被使用时,shared_ptr会自动释放,这一特性使得动态对象的使用变的非常容易。
以下是shared_ptr的一些操作:
操作 | 含义 |
---|---|
make_shared<T> (args) |
返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化对象 |
shared_ptr |
p是shared_ptr q的拷贝;此操作会递增q的计数器。q中的指针必须能转换成T* |
p=q | p与q都是shared_ptr,所保存的指针必须能够相互转换。会递减p的计数器,递增q的计数器。 |
p.unique() | 若p.use_count()为1,返回true;否返回false |
p.use_count() | 返回与p共享对象的智能指针数量,可能很慢 |
shared_ptr与new结合使用
如果我们不初始化一个智能指针,它就会初始化为一个空指针,我们还可以用new返回的指针来初始化智能指针。shared_ptr<int> p(new int(42));
以下是定义和改变shared_prt的操作:
操作 | 含义 |
---|---|
shared_ptr<T> p(q) |
p管理内置指针q所指向的对象;q必须指向new分配的内存,且能转换T*类型。 |
shared_ptr<T> p(u) |
p从unique_prt u那里接管了对象的所有权;将u置空。 |
shared_ptr<T> p(q,d) |
p接管了内置指针q所指向的对象的所有权。q须能够转换为T*类型,p将使用可调用对象d来代替delete |
shared_ptr<T> p(p2,d) |
p是shared_ptr p2的拷贝,p将调用d来代替delete |
p.reset()/p.reset(q)/p.reset(q,d) |
若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递可选的参数内置指针q,会令p指向q,否则会将p置空。若还传递了d,将会调用d来释放q。 |
unique_ptr
unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向给定的对象。当unique_ptr被销毁时,其所指向的对象也被销毁。
与shared_ptr不同,没有类似make_shared的标准库返回一个unique_ptr。当我们定义unique_ptr时,需要将其绑定到new返回的智能指针上。如下:unique_ptr<int> p(new int(42));
由于unique_ptr“拥有”它所指向的对象,所以不支持普通的拷贝与赋值操作,可以通过调用release或reset将指针的所有权从一个unique_ptr转移到另一个unique_ptr。unique_ptr<int> p2(p.release());
不能拷贝的规则有个例外,可以拷贝一个将要被销毁的unique_ptr如从函数返回一个unique_ptr。
1 | unique_ptr<int> clone(int p){ |
以下是unique_ptr的一些操作:
操作 | 含义 |
---|---|
unique_ptr<T> u1/unique_ptr<T, D> u2 |
空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放,u2会使用D来释放。 |
unique_ptr<T, D> u(d) |
空unique_ptr,指向T类型的对象,用D代替delete。 |
u = nullptr |
释放u所指向的对象,将u置为空。 |
u.release() |
u放弃对指针的控制权,返回指针,并将u置空。 |
u.reset()/u.reset(q)/u.reset(nullptr) |
释放u所指的对象,如果提供q,今u指向这个对象;否则将u置空。 |
weak_ptr
weak_ptr是一种不控制指向对象的生命期的智能指针,其指向一个由shared_ptr管理的对象。将一个weak_ptr绑定到shared_ptr不会改变计数器。一旦最后一个shared_ptr被销毁,即使有weal_ptr指向对象,对象还是会被释放。
以下是weak_ptr的一些操作:
操作 | 含义 |
---|---|
weak_ptr<T> w |
空weak_ptr对象,指向T类型的对象 |
weak_ptr<T> w(sp) |
与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp所指向的类型。 |
w = p |
p可以是shared_ptr或weak_ptr。赋值后w与p共享对象。 |
w.reset() |
将w置空。 |
w.use_count() |
与w共享对象的shared_ptr的数量。 |
w.expired() |
若w.use_count()为0,返回true,否返回false。 |
w.lock() |
若w.expired()是true,返回一个空shared_ptr;否返回一个指向w指向的对象的shared_ptr。 |
动态数组
new与delete一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。
为了支持这个需求C++和标准库提供了两种一次分配一个对象数组的办法。
new和数组
为了让new分配一个对象数组,要在类型名后跟一对方括号,在其中指明要分配的对象的数目。
1 | //get_size确定分配多个int |
虽然通常称new T[]分配的内存为“动态数组”,但这种叫法有些误导。当用new分配一个数组时,并未得到一个数组类型的对象,而是数组类型的指针。
因为分配的内存并不是一个数组类型,所以不能对动态数组调用degin或end。
在c11标准中还支持int *pia = new int[5]{0,1,2,3,4};
释放动态数组时,使用delete,在指针前加上[]delete [] pia;
数组中的元素按逆序销毁。
标准库提供了一个可以管理new分配的数组的unique_ptr版本。
1 | //up指向一个包含10个未初始化的int数组 |
一些unique_ptr操作动态数组的方法
指向数组的unique_ptr不支持成员访问运算符(./->)
操作 | 含义 |
---|---|
unique_ptr<T[]> u |
u可以指向一个动态分配的数组,数组元素类型为T |
unique_ptr<T[]> u(p) |
u指向p所指向的动态数组,p必须能转换成类型T* |
u[i] |
返回u拥有的数组中i处的对象,u必须指向一个数组 |
shared_ptr不支持直接管理动态数组,如果希望其管理数组需要自己定义删除器。
1 | //定义并设置删除器 |
allocator类
new有一些灵活度的上限,其将内存分配和对象构造组合到一起,类似的delete将对象的析构和内存的释放组合到一起。
标准库allocator类定义在头文件memory中,其可以帮助我们将内存分配与对象构造分离开来。
类似vector,allocator是一个模版。
1 | //可以分配string的allocator对象 |
以下是allocator的操作
操作 | 含义 |
---|---|
allocator<T> a |
定义了一个名为a的allocator对象,可以为T类型的对象分配内存 |
a.allocate(n) |
分配一段原始的、未构造的内存,保存n个类型为T的对象 |
a.deallocate(p, n) |
释放从T*指针p中地址开始的内存,其保存了n个类型为T的对象;p必须是先前由allocate返回的且n必须是p创建时所要求的大小,在调用此函数前,必须先调用destroy |
a.construct(p, args) |
p必须为T*类型的指针,指向一块原始内存;arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象 |
a.destroy(p) |
p为T*类型的指针,对其指向的对象执行析构函数 |
1 | auto q = p; |
除此之外在memory中还定义了一些伴随算法。这些算法在给定的目的创建元素,而不是由系统分配内存。
1 | //从迭代器b和e指出的输入范围中拷贝元素到b2指定的未构造内存中 |
示例:
1 | //分配比vi中元素所占空间大一倍的动态内存 |
参考资料
C++primer中文版(第五版) 电子工业出版社。