C++中的动态内存

内容包括:智能指针与allocator

动态内存与智能指针

除了静态内存和栈内存,每个程序还有一个内存池用于存储动态分配(dynamic allocate) 的对象。动态对象的生存期由程序控制。

为了更容易且安全的使用动态内存,C++11标准库提供了智能指针(smart pointer) 来管理动态对象。shared_ptr允许多个指针指向同一个对象,unique_ptr独占所指向的对象。weak_ptr是弱引用,指向shared_ptr管理的对象。

shared_ptr

使用模板指定指针可以指向的类型。

1
shared_ptr<int> p;

智能指针的行为与基本指针一致。

使用make_shared函数安全地分配和使用动态内存。由于make_shared是一个模板函数,我们同样可以使用auto关键字保存返回的智能指针。

1
auto p = make_shared<string>(10, '9');

每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。拷贝一个shared_ptr,计数器将递增,例如拷贝初始化、作为参数传递给函数或作为函数的返回值。当我们给shared_ptr赋予新值或销毁,例如离开作用域,计数器就会递减。当计数器为 00,就会自动释放自己管理的对象。

1
2
3
4
auto p = make_shared<int>(1), r = make_shared<int>(2);
p = r; // 递增 r 对象的计数器
// 递减 p 对象的计数器
// p 计数器为 0,释放为其分配的内存

动态内存常用于一下三种情况:

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道对象的准确类型
  3. 程序需要在多个对象之间共享数据

shared_ptr中,分配的资源与对象的生存期不一致。这与通常的容器类不同。

直接管理内存

C++定义了两个运算符来分配和释放内存。new分配内存,delete释放内存。

new返回指向对象的指针。new初始化对象可以用多种初始化方式。

1
2
3
4
5
6
7
8
string *ps = new string;
int *pi = new(0);
auto p = new auto(obj);

// 可以分配const的数据
const int* pci = new const int(0);
// (*pci)是只读的,不能修改
// new const int 中的 const 可以省略

若分配失败,new抛出std::bad_alloc。可以使用new (nothrow)语法使分配失败时不抛出异常,而是返回空指针。

通过delete释放动态内存。释放非new分配的内存和释放多次都是未定义行为。

一旦在指针的生存期内没有释放其指向的内存,就会造成内存泄漏。

delete释放后的指针被称为空悬指针。其仍然指向一块内存,但内存数据已被释放。应当在delete后主动为其赋值nullptr来明确指出指针不指向任何对象。

动态内存的一个基本问题是多个指针可能指向一个相同内存。释放一个内存可能造成多个空悬指针。

结合使用shared_ptrnew

可以使用new返回的指针来显示地初始化智能指针(不允许隐式转换)。

1
shared_ptr<int> p(new int(42));

要在非初始化时使用new为智能指针赋值,需要使用reset函数。

混用newshared_ptr可能导致严重后果。shared_ptr的引用计数仅限于自身的拷贝,不包括原始指针。shared_ptr`销毁对象时。原始指针将成为空悬指针,而这是无法检测的。

1
2
3
void process(shared_ptr<int> ptr);
int *p = new int;
process(shared_ptr<int>(p));

不难发现,在process函数返回时引用计数清零,销毁内存数据,此时p成为空悬指针。

智能指针具有get方法,返回一个内置指针。这一函数的作用是适配需要转递内置指针的代码。不应当使用其初始另一个智能指针,或释放该内置指针。

unique函数指示该指针是否是唯一指向该对象的指针。

unique_ptr

一个unique_ptr“拥有”它所指向的对象。某个时刻只能有一个unique_ptr指向一个指定对象。

unique_ptr没有类似make_shared的函数,必须绑定到一个new返回的指针上。unique_ptr不支持普通的拷贝或赋值操作。

通过调用releasereset可以将指针的所有权从一个(非constunique_ptr转移给另一个。

1
2
3
4
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2(p1.release());
unique_ptr<int> p3;
p3.reset(p2.release());

如果不用智能指针来保存release返回的指针,就必须负责该指针的释放。

不能拷贝unique_ptr的例外是,可以从函数中返回一个unique_ptr

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象。将一个weak_ptr绑定到shared_ptr不会影响其引用计数。当shared_ptr释放对象时,可能有weak_ptr指向该对象。

创建weak_ptr需要用一个shared_ptr来初始化。由于对象可能不存在,我们不能用weak_ptr直接访问对象。lock函数返回一个指向共享对象的shared_ptr,例如

1
2
3
4
if (share_ptr<int> np = wp.lock())
{
// ...
}

动态数组

newdelete一次分配/释放一个对象。C++语言和标准库提供了一次分配多个对象的方法:new[]allocator类。通常若需要使用可变数量的对象,应当优先考虑vector

new和数组

new type[size]语法用于分配一个对象数组。方括号中的大小必须是整型,但不必是常量。

动态数组不是数组类型,返回值是指向首地址的指针,因此不能使用范围for语句。

默认状态下,new分配的对象默认初始化。可以在方括号后使用圆括号进行值初始化(初始化为零值),或使用花括号进行列表初始化。

可以分配长度为0的数组,但指针不能解引用。

释放动态数组时,在指针前加上空方括号。

标准库提供了一个可以管理new分配的数组的unique_ptr。对象类型后面需要跟一对方括号。

1
unique_ptr<int[]> up(new int[10]);

<int[]>表明up指向一个int数组而不是int。当up销毁它管理的指针时,会自动使用delete[]

unique_ptr指向一个数组时,无法使用点和箭头运算符,而是使用下标运算符访问数组中的元素。

shared_ptr不支持管理动态数组。如果希望使用,则需要提供自己定义的删除器。

1
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; })

如果不提供删除器, shared_ptr将使用delete销毁指定的对象,产生UB。

shared_ptr访问数组中的元素,只能通过访问内置指针的方式。

allocator类

当分配一大块内存时,有时我们希望在内存上按需构造对象。我们希望将内存分配和对象构造分离。allocator类提供了这一功能。

allocator是模板类。allocator<T>对象可以为类型T的对象分配内存。使用allocate(size)函数分配可以保存size个对象的内存,内存是原始的,未构造的。函数返回指向内存开头的指针。

使用construct(p,args)函数构造对象,p为指向内存位置的指针,args是可变参数,使用这些参数构造对象。当我们使用完对象之后,可以使用destroy(p)函数销毁它。

释放内存通过调用deallocate(p,n)完成,p必须指向分配的内存,n必须与分配时的值一致。