除了自动和 static
对象之外,C++ 还支持动态分配对象。动态分配对象的生命周期与创建位置无关,必须显式释放才会销毁。动态对象的释放极易出错,为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配对象。
静态内存 用来保存局部 static
对象、类 static
数据成员、定义在所有函数外的变量。栈内存 用来保存定义在函数内的非 static
对象。静态内存和栈内存中的对象由编译器自动创建和销毁:
- 栈对象在定义它的程序块运行时才存在;
static
对象在使用前分配,在程序结束时销毁。
除此之外,每个程序还有一个内存池,称为自由空间 (free store)或堆 (heap),用来存储动态分配(dynamically allocate)对象------程序运行时分配的对象。动态对象的生命周期由程序控制,必须显式销毁。
动态内存与智能指针
C++ 动态内存管理通过一对运算符完成:
new
,在动态内存中分配空间并返回指向该对象的指针。delete
,接受一个动态对象指针,销毁该对象并释放与之关联的内存。
不管是忘记释放内存导致内存泄漏,还是使用内存已释放的指针,都会出问题。标准库的智能指针 (smart pointer)类型可以更容易地管理动态对象。与常规指针相比,智能指针负责自动释放所指对象。头文件 memory
中定义了三种智能指针类型:
shared_ptr
,允许多个指针指向同一对象。unique_ptr
,独占所指对象。- 伴随类
weak_ptr
是一种弱引用,指向shared_ptr
所管理的对象。
shared_ptr
类
创建智能指针时,必须在模板中提供指针所指类型。
cpp
shared_ptr<string> p1;
shared_ptr<list<int>> p2;
if (p1 && p1->empty()) {
*p1 = "hi";
}
标准库函数 make_shared
定义在头文件 memory
中,它在动态内存中分配一个给定类型的对象并以所接受参数初始化,返回指向该对象的 shared_ptr
。如果没有参数,则执行值初始化。
cpp
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10, '9');
shared_ptr<int> p5 = make_shared<int>();
auto p6 = make_shared<vector<string>>();
每个 shared_ptr
都有一个关联的计数器,称为引用计数 (reference count)。当拷贝或赋值时,引用计数会同步更新。拷贝 shared_ptr
时,比如初始化、函数传参、函数返回,计数器会递增;给 shared_ptr
赋新值或销毁时,比如退出作用域,计数器会递减;计数器变为 0 时,会自动释放所管理的对象。
采用计数器还是其它数据结构由标准库具体实现决定。
cpp
auto p = make_shared<int>(42);
auto q(p);
auto r = make_shared<int>(42);
r = q;
shared_ptr
类通过析构函数 (destructor)自动销毁所指对象。每个类都有一个析构函数,用于控制对象销毁操作,一般用于释放对象所分配的资源。shared_ptr
的析构函数会递减引用计数,如果计数为 0,则会销毁对象,并释放内存。
cpp
shared_ptr<Foo> factory(T arg) {
return make_shared<Foo>(arg);
}
void use_factory(T arg) {
auto p = factory(arg);
}
shared_ptr
如果存放于一个容器中,后续不再需要全部元素,只需其中一部分时,要用erase
删除不再需要的那些元素。
使用动态内存出于以下三个原因之一:
- 不知道自己需要使用多少对象,比如容器。
- 不知道所需对象的精确类型。
- 需要在多个对象间共享数据。
使用动态内存的一个常见原因是允许多个对象共享相同状态。
某些类分配的资源具有与原对象相独立的生命周期。
直接管理内存
C++ 语言定义了两个运算符来分配和释放动态内存:
new
分配内存。delete
释放new
分配的内存。
自己管理内存的类与使用智能指针的类不同,它们不能依赖于类对象拷贝、赋值、销毁操作的任何默认定义。
自由空间分配的内存是匿名的,因此 new
表达式返回一个指向该对象的指针。
cpp
int *pi = new int;
string *ps = new string;
默认情况下,动态分配的对象默认初始化,这意味着内置类型或复合类型对象的值是未定义的,类类型对象使用默认构造函数初始化。动态分配对象可以直接初始化:
()
构造初始化,无参数表示值初始化{}
列表初始化
cpp
int *pi1 = new int(1024);
int *pi2 = new int();
string *ps1 = new string(10, '9');
string *ps2 = new string();
vector<int> *pv = new vector<int>{0, 1, 2, 3};
动态分配对象最好都进行初始化。
动态分配对象类型可以使用 auto
从单一初始化器中自动推断出来。
cpp
auto pia1 = new auto(42); // int *pia1
auto psa1 = new auto(string()); // string *psa1
auto pla1 = new auto{a, b, c}; // 错误
new
也可以分配 const
对象,动态分配 const
对象必须初始化。
cpp
auto pci1 = new const int(42); // const int *pci1
auto pcs1 = new const string; // const string *pcs1
一旦程序用光了所有可用的内存,new
表达式就会失败,默认情况下,将会抛出 bad_alloc
类型的异常。new
可以接受额外参数,这种形式称为定位 new
(placement new)。将标准库定义的名为 nothrow
的对象传递给 new
可以避免抛出异常,此时返回空指针来表示分配内存失败。
bad_alloc
和nothrow
都定义在头文件new
中。
cpp
int *p1 = new int;
int *p2 = new (nothrow) int;
动态内存通过 delete
表达式 (delete expression)来释放。delete
表达式接受一个指针,销毁所指对象并释放相应内存。
cpp
delete p;
delete
所接受的指针必须指向动态分配内存,或者是一个空指针。释放一个非 new
分配的内存,或者对同一指针重复释放多次,结果都是不确定的。delete
非指针对象,编译器将会报错;但由于编译器无法分辨指针所指对象是否为动态分配的,也不能分辨所指对象内存是否已被释放,因此这些操作可以通过编译,但结果不确定。
cpp
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // 报错
delete pi1; // 行为不确定
delete pd;
delete pd2; // 行为不确定
delete pi2;
auto pci = new const int(1024);
delete pci;
动态对象的生命周期直到被释放为止。返回指向动态内存的指针的函数需要调用者释放内存,这很容易出问题。由于内置类型对象被销毁时不会做额外操作,指针所指对象不会被自动释放,因此内置指针管理的动态内存在被显式释放前会一直存在。使用 new
和 delete
管理动态内存有三个常见问题,坚持只使用智能指针可以避免这些问题:
- 忘记
delete
,导致 "内存泄漏"。这类问题难以定位,需要程序运行很长时间耗尽内存后,才能检测出问题。 - 使用已释放的对象。释放内存后将指针置空,可以检测此类问题。
- 同一内存重复释放,导致自由空间被破坏。比如,两个指针指向同一动态分配对象,各自都被释放一次。
空悬指针(dangling pointer)是指向曾保存数据对象但现已无效的内存的指针,它和未初始化的指针有同样的问题。为了避免空悬指针,可以:
- 在指针即将离开作用域前释放相应内存。
- 在
delete
之后给指针赋值nullptr
;但是当多个指针指向同一内存时,这种方式也很难解决问题。
shared_ptr
和 new
结合使用
shared_ptr
有一个接受指针参数的 explicit
构造函数。可以使用 new
返回的指针来初始化智能指针。
cpp
shared_ptr<int> p1 = new int(1024); // 错误
shared_ptr<int> p2(new int(42));
shared_ptr<int> clone(int p) {
return new int(p); // 错误
}
默认情况下,初始化智能指针的普通指针必须指向动态内存,智能指针默认使用 delete
释放所关联的对象。如果要绑定到其它类型的资源的指针上,就必须要提供自己的操作代替 delete
。
shared_ptr
对对象析构的协调仅限于自身的拷贝,因此最好使用 make_shared
而不是 new
,这样可以在分配对象时将 shared_ptr
与之绑定,避免无意中将一块内存绑定到多个独立创建的 shared_ptr
。
cpp
void process(shared_ptr<int> ptr) {
// ...
}
int *x(new int(1024));
process(shared_ptr<int>(x)); // x 所指内存会被释放
int j = *x; // x 为空悬指针
当一个 shared_ptr
绑定到内置指针时,内存管理将交给 shared_ptr
,不应再使用内置指针来访问 shared_ptr
所指内存。
使用内置指针访问智能指针所指对象很危险,因为无法知道对象何时销毁。
智能指针的 get
成员返回一个指向所管理对象的内置指针。get
用于将指针的访问权限传递给代码,只有确定代码不会 delete
指针的情况下,才能使用 get
;而且永远不要用 get
初始化另一个智能指针。
cpp
shared_ptr<int> p(new int(42));
int *q = p.get();
{
shared_ptr<int>(q);
}
int foo = *p;
shared_ptr
的 reset
成员用于重置所指对象,并更新引用计数,必要时会释放所指的旧对象,常与 unique
成员一起使用。
cpp
if (!p.unique()) {
p.reset(new string(*p));
}
*p += newVal;
智能指针与异常
异常安全的程序需要在异常发生后正确释放资源。确保资源正确释放的一个简单方法是使用智能指针。而使用内置指针直接管理内存不会自动释放。
cpp
void f() {
shared_ptr<int> sp(new int(42));
// 此处抛出异常
} // shared_ptr sp 自动释放
void f1() {
int *ip = new int(42);
// 此处抛出异常,下面释放内存的代码将不会执行
delete ip;
}
不是所有的类都定义了析构函数,特别是那些为 C、C++ 两种语言设计的类,通常要求用户显式释放所使用的资源。使用 shared_ptr
可以避免资源泄漏。
默认情况下,shared_ptr
假定所指的都是动态内存,因此销毁时默认对所管理的指针执行 delete
;也可以在创建 shared_ptr
时传入一个自定义的删除器 (deleter)函数代替 delete
。
cpp
struct destination; // 正在连接的目标
struct connection; // 连接所需信息
connection connect(destination*); // 打开连接
void disconnect(connection); // 关闭连接
void end_connection(connection *p) {
disconnect(*p);
};
void f(destination &d) {
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
}
使用智能指针必须遵守一些基本规范:
- 同一内置指针不能初始化(或
reset
)多个智能指针。 - 不要对
get
返回的指针执行delete
。 - 不能
get
初始化或reset
另一个智能指针。 get
返回的指针在最后一个相应的智能指针销毁后就无效了。- 使用智能指针管理的资源如果不是
new
分配的内存,必须传一个删除器。
unique_ptr

unique_ptr
独占所指对象,任一时刻只能有一个 unique_ptr
指向给定对象。unique_ptr
可以默认构造,也可以接受内置指针 explicit
构造。
cpp
unique_ptr<double> p1;
unique_ptr<int> p2(new int(42));
unique_ptr
独占所指对象,因此不支持拷贝、赋值操作,必须通过 release
、reset
将非 const
的 unique_ptr
的指针所有权转移给另一个 unique_ptr
。
cpp
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1); // 错误
unique_ptr<string> p3;
p3 = p2; // 错误
unique_ptr<string> p2(p1.release());
p3.reset(p2.release());
release
会切断 unique_ptr
与它所管理对象间的联系。release
返回的指针通常被用来初始化或绑定到另一个智能指针。如果不用另一个智能指针保存 release
返回的指针,就必须自己负责释放资源。
cpp
p2.release(); // 错误,丢失了指针
auto p = p2.release(); // p 需要手动释放
禁止拷贝 unique_ptr
有一个例外,可以拷贝或赋值一个即将销毁的 unique_ptr
,比如从函数返回一个 unique_ptr
:
cpp
unique_ptr<int> clone(int p) {
return unique_ptr<int>(new int(p));
}
较早的标准库中有一个
auto_ptr
类,它具有unique_ptr
部分特性,而非全部。不能在容器中保存auto_ptr
,也不能从函数返回auto_ptr
。应尽可能使用unique_ptr
。
unique_ptr
默认使用 delete
释放所指对象。重载 unique_ptr
删除器会改变 unique_ptr
的类型,需要在生成 unique_ptr
类型时额外指定删除器类型;并在创建或 reset
一个 unique_ptr
对象时,提供一个指定类型的删除器。
cpp
unique_ptr<objT, delT> p(new objT, fcn);
weak_ptr

weak_ptr
指向 shared_ptr
所管理的对象,它不控制所指对象生命周期,不会改变所绑定的 shared_ptr
的引用计数。
cpp
auto sp = make_shared<int>(42);
weak_ptr<int> wp(sp);
由于对象可能不存在,不能使用 weak_ptr
直接访问对象,而必须通过 lock
返回的 shared_ptr
。
cpp
if (shared_ptr<int> np = wp.lock()) {
// ...
}
动态数组
C++ 及标准库提供了两种分配对象数组的方法:new T[]
表达式、allocator
类。
allocator
将分配与初始化分离,使用allocator
通常会有更好的性能和更灵活的内存管理能力。大多数应用都没有直接访问动态数组的需求,应该使用标准库容器而不是动态分配数组。使用容器可以使用默认的拷贝、赋值、析构,而分配动态数组的类必须自定义拷贝、赋值、析构。
new
和数组
new
分配对象数组时:
- 可以通过
[]
将一个整数(不必是常量)指定为数组长度。 - 也可以使用数组的类型别名分配数组,编译器仍会调用
new T[]
。
不管哪种方式,new T[]
得到的都是第一个元素的指针。由于得到的不是一个数组类型,因此不能对动态数组调用 begin
、end
,也不能使用范围 for
。
cpp
int *pia = new int[get_size()];
typedef int arrT[42];
int *p = new arrT;
默认情况下,new
分配的对象都是默认初始化。可以对数组中的元素执行值初始化、列表初始化:
cpp
int *pia = new int[10];
int *pia2 = new int[10]();
string *psa = new string[10];
string *psa2 = new string[10]();
int *pia3 = new int[3]{0, 1, 2};
string *psa3 = new string[3]{"hello"};
与内置数组一样,列表只初始化动态数组开始部分的元素,剩余元素执行值初始化,而且列表中元素数目不能大于数组长度,否则依具体情况,可能在编译时报错或在运行时抛出 bad_array_new_length
异常。
类型
bad_array_new_length
定义在头文件new
中。动态数组不能在
()
中使用初始化器,因此不能使用auto
分配数组。
动态分配空数组是合法的。此时,new
返回的是一个合法的非空指针,此指针保证与 new
返回的其它指针都不同。它和尾后指针一样,可以进行比较操作,可以为其加、减 0,可以减去自身,但不能解引用。
cpp
char *cp = new char[0];
动态数组使用 delete []
释放内存,数组中元素按逆序销毁。
cpp
delete [] pa; // pa 指向一个动态分配的数组或为空
释放数组时必须使用 delete []
,它告诉编译器该指针指向数组的首元素,如果使用 delete
,行为将不确定。
如果使用
delete
释放数组,或者使用delete []
释放非数组对象,编译器可能不会给出警告,但程序的运行时行为不确定。
为对象类型加上 []
,便可以使用 unique_ptr
管理 new
分配的动态数组,这种 unique_ptr
将使用 delete []
释放内存。
cpp
unique_ptr<int []> up(new int[10]);
up.reset();
指向数组的 unique_ptr
不能使用点和箭头成员运算符,但可以使用下标运算符。
cpp
for (size_t i = 0; i != 10; ++i) {
up[i] = i;
}

shared_ptr
不直接支持管理动态数组,若要使用 shared_ptr
管理动态数组,必须提供自定义删除器。管理动态数组的 shared_ptr
不支持下标运算和指针算术运算,需要通过 get
来获取数组元素。
cpp
shared_ptr<int> sp(new int[10], [](int *p) { delete [] p; });
sp.reset();
for (size_t i = 0; i != 10; ++i) {
*(sp.get() + i) = i;
}
allocator
类
new
将内存分配和对象构造合在一起,delete
将对象析构和内存释放合在一起,这丧失了一定的灵活性,而且对于按需构造的场景,付出了更多的初始构造开销。

标准库头文件 memory
中的 allocator
模板类将内存分配和对象构造分离开,它提供了一种类型感知的内存分配方法。allocator
分配内存时,会根据给定的对象类型来确定内存大小和对齐位置:
cpp
allocator<string> alloc;
auto const p = alloc.allocate(n);
allocator
分配的内存是原始的、未构造的。借助 construct
成员可以按需构造,它接受一个指针和一些额外参数,并使用给定参数在给定位置构造一个对象。
如果未构造对象就使用原始内存,程序的行为将不确定。
cpp
auto q = p;
alloc.construct(q++); // 默认构造
alloc.construct(q++, 10, 'c');
alloc.construct(q++, "hello");
销毁对象必须借助成员函数 destroy
按需销毁,它接受一个指针,对所指对象调用析构函数。
只能对真正构造过的元素进行
destroy
。
cpp
while (q != p)
alloc.destroy(--q);
deallocate
成员用于释放内存,它接受一个非空指针和一个大小参数,指针必须指向 allocate
分配的内存,大小参数必须和分配内存时提供的一样。
cpp
alloc.deallocate(p, n);

标准库在头文件 memory
中为 allocator
类定义了两个伴随算法,用于在未初始化内存中创建对象。
cpp
auto p = alloc.allocate(vi.size() * 2);
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
uninitialized_fill_n(q, vi.size(), 42);
uninitialized_copy
接受的第三个参数是目标位置迭代器,必须指向未构造的内存。