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
赋予新值或销毁,例如离开作用域,计数器就会递减。当计数器为 ,就会自动释放自己管理的对象。
1 | auto p = make_shared<int>(1), r = make_shared<int>(2); |
动态内存常用于一下三种情况:
- 程序不知道自己需要使用多少对象
- 程序不知道对象的准确类型
- 程序需要在多个对象之间共享数据
shared_ptr
中,分配的资源与对象的生存期不一致。这与通常的容器类不同。
直接管理内存
C++定义了两个运算符来分配和释放内存。new
分配内存,delete
释放内存。
new
返回指向对象的指针。new
初始化对象可以用多种初始化方式。
1 | string *ps = new string; |
若分配失败,new
抛出std::bad_alloc
。可以使用new (nothrow)
语法使分配失败时不抛出异常,而是返回空指针。
通过delete
释放动态内存。释放非new
分配的内存和释放多次都是未定义行为。
一旦在指针的生存期内没有释放其指向的内存,就会造成内存泄漏。
delete
释放后的指针被称为空悬指针。其仍然指向一块内存,但内存数据已被释放。应当在delete
后主动为其赋值nullptr
来明确指出指针不指向任何对象。
动态内存的一个基本问题是多个指针可能指向一个相同内存。释放一个内存可能造成多个空悬指针。
结合使用shared_ptr
和new
可以使用new
返回的指针来显示地初始化智能指针(不允许隐式转换)。
1 | shared_ptr<int> p(new int(42)); |
要在非初始化时使用new
为智能指针赋值,需要使用reset
函数。
混用new
和shared_ptr
可能导致严重后果。shared_ptr
的引用计数仅限于自身的拷贝,不包括原始指针。当
shared_ptr`销毁对象时。原始指针将成为空悬指针,而这是无法检测的。
1 | void process(shared_ptr<int> ptr); |
不难发现,在process
函数返回时引用计数清零,销毁内存数据,此时p
成为空悬指针。
智能指针具有get
方法,返回一个内置指针。这一函数的作用是适配需要转递内置指针的代码。不应当使用其初始另一个智能指针,或释放该内置指针。
unique
函数指示该指针是否是唯一指向该对象的指针。
unique_ptr
一个unique_ptr
“拥有”它所指向的对象。某个时刻只能有一个unique_ptr
指向一个指定对象。
unique_ptr
没有类似make_shared
的函数,必须绑定到一个new
返回的指针上。unique_ptr
不支持普通的拷贝或赋值操作。
通过调用release
或reset
可以将指针的所有权从一个(非const
)unique_ptr
转移给另一个。
1 | unique_ptr<int> p1(new int(10)); |
如果不用智能指针来保存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 | if (share_ptr<int> np = wp.lock()) |
动态数组
new
和delete
一次分配/释放一个对象。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
必须与分配时的值一致。