Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

动态内存

动态内存与智能指针

除了静态内存和栈内存,每个程序还拥有一个内存池被称为。程序用堆来存储动态分配的对象。
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(q) 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
2
3
4
unique_ptr<int> clone(int p){
...
return unique_ptr<int>(new 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
2
//get_size确定分配多个int
int *pia = new int[get_site()];

虽然通常称new T[]分配的内存为“动态数组”,但这种叫法有些误导。当用new分配一个数组时,并未得到一个数组类型的对象,而是数组类型的指针。
因为分配的内存并不是一个数组类型,所以不能对动态数组调用degin或end。
在c11标准中还支持
int *pia = new int[5]{0,1,2,3,4};
释放动态数组时,使用delete,在指针前加上[]
delete [] pia;
数组中的元素按逆序销毁。
标准库提供了一个可以管理new分配的数组的unique_ptr版本。

1
2
3
4
//up指向一个包含10个未初始化的int数组
unique_ptr<int[]> up(new int[10]);
//自动用delete[]销毁
up.release();

一些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
2
3
4
//定义并设置删除器
shared_ptr<int> sp(new int[10],[](int *p){delete[] p;});
//将使用定义的删除器
sp.reset();

allocator类

new有一些灵活度的上限,其将内存分配和对象构造组合到一起,类似的delete将对象的析构和内存的释放组合到一起。
标准库allocator类定义在头文件memory中,其可以帮助我们将内存分配与对象构造分离开来。
类似vector,allocator是一个模版。

1
2
3
4
//可以分配string的allocator对象
allocator<string> alloc;
//分配n个未初始化的string
auto const p =alloc.allocate(n);

以下是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
2
3
4
5
6
7
8
9
10
auto q = p;
//等价与alloc.construst(q++,"cccc");
alloc.construst(q++,4,'c');
//此时q所指向的为未构造内存,p为第一个地址
while(q!=p){
//销毁元素,可以用该内存存储其他string
dlloc.destroy(--q);
}
//释放内存
alloc.deallocate(p, 4);

除此之外在memory中还定义了一些伴随算法。这些算法在给定的目的创建元素,而不是由系统分配内存。

1
2
3
4
5
6
7
8
9
//从迭代器b和e指出的输入范围中拷贝元素到b2指定的未构造内存中
//b2必须可以容纳输入序列中的元素拷贝
uninitialized_copy(b,e,b2);
//从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中。
uninitialized_copy_n(b,e,b2);
//在迭代器b,e指定的原始内存中创建对象,对象的值为t的拷贝
uninitialized_fill(b,e,t);
//从迭代器b指向的内存开始创建n个对象。b必须能够容纳给定的对象
uninitialized_fill_n(b,n,t);

示例:

1
2
3
4
5
6
//分配比vi中元素所占空间大一倍的动态内存
auto p = alloc.allocate(vi.size() * 2);
//拷贝vi中的元素来构造从p开始的元素
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
//将其余元素初始化为42
uninitialized_fill_n(q, vi.suze(), 42);

参考资料

C++primer中文版(第五版) 电子工业出版社。

评论