目录

第12章 动态内存

静态内存用来存储静态局部对象,类静态成员和全局对象。栈内存用于定义在函数内的非静态对象。静态内存和栈内存分配的对象由编译器自动创建和销毁。栈对象只当其定义的块在执行时存在。静态对象在被使用之前已经创建,当程序结束时销毁。

除了静态内存和栈内存,每一个程序还有一个自由存储区或叫堆。程序使用堆来动态创建对象。由程序控制动态对象的生命周期。必须显式销毁不再需要的动态对象。

12.1 动态内存和智能指针

C++使用newdelete来管理动态内存。new在堆上分配并初始化一个对象,返回指向该对象的指针。delete使用指向一个动态对象的指针,销毁该对象,释放相关的内存。

新标准定义了2种智能指针。shared_ptr允许多个指针指向同一个对象,unique_ptr独占其指向的对象。新标准还定义了一个weak_ptr,它是shared_ptr管理对象的一个弱引用。这3个都定义在memory头文件中。

12.1.1 shared_ptr类

vector一样,智能指针是模板。因此当创建智能指针时,需要提供指向的类型信息:

1
2
shared_ptr<string> p1;    // shared_ptr that can point at a string
shared_ptr<list<int>> p2; // shared_ptr that can point at a list of ints

默认初始化的智能指针保存一个空指针。智能指针的用法和普通指针类似。解引用智能指针返回所指向的对象,当在条件中使用智能指针时,其效果是测试指针是否为空:

1
2
3
// if p1 is not null, check whether it's the empty string
if (p1 && p1->empty())
    *p1 = "hi";  // if so, dereference p1 to assign a new value to that string

下面是shared_ptrunique_ptr共有的操作:

操作 说明
shared_ptr<T> sp
unique_ptr<T> up
指向T的空指针
p 使用p作为条件,如果p指向对象则为true
*p 解引用p获得p指向的对象
p->mem 等同于(*p).mem
p.get() 返回p维护的底层指针。小心使用,返回指针指向的对象可能被智能指针删除
swap(p, q) 交换p和q的底层指针
p.swap(q) 同上

特定于shared_ptr的操作:

操作 说明
make_shared<T>(args) 返回指向动态创建的T类型对象的shared_ptr,使用args初始化该对象
shared_ptr<T> p(q) p是q的一个拷贝,增加q的引用计数。q包含的指针必须能转换成T*
p = q p和q包含的指针可以转换为另一个。减少p的引用计数,增加q的引用计数。如果p的引用计数为0,删除p指向的对象
p.unique() 如果p.use_count()为1,返回true,否则返回false
p.use_count() 返回引用计数的个数。可能是一个慢操作,主要用于调试

make_shared函数

分配和使用动态内存最安全的方式是调用make_shared库函数。它定义在memory头文件中,是一个函数模板。这个函数在堆上分配和初始化一个对象并返回指向该对象的shared_ptr

1
2
3
4
5
6
// shared_ptr that points to an int with value 42
shared_ptr<int> p3 = make_shared<int>(42);
// p4 points to a string with value 9999999999
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5 points to an int that is value initialized (§ 3.3.1 (p. 98)) to 0
shared_ptr<int> p5 = make_shared<int>();

和顺序容器的emplace成员函数一样,make_shared使用其参数构造一个指定类型的对象。如果不传任何参数,则对象被值初始化。当然通常使用auto可以简化make_shared返回指针的定义:

1
2
// p6 points to a dynamically allocated, empty vector<string>
auto p6 = make_shared<vector<string>>();

复制和赋值shared_ptr

当复制或赋值一个shared_ptr时,每一个shared_ptr跟踪指向同一个对象的shared_ptr的个数:

1
2
3
auto p = make_shared<int>(42); // object to which p points has one user
auto q(p); // p and q point to the same object
           // object to which p and q point has two users

引用计数增加的场景:

  • 复制一个shared_ptr
  • 赋值操作符右边的操作数
  • 传值方式传递给函数参数
  • 从函数通过值返回

引用计数减少的场景:

  • 赋值操作符左边的操作数
  • shared_ptr本身被销毁

当引用计数为0时,shared_ptr自动销毁它管理的对象:

1
2
3
4
5
auto r = make_shared<int>(42); // int to which r points has one user
r = q;  // assign to r, making it point to a different address
        // increase the use count for the object to which q points
      // reduce the use count of the object to which r had pointed
    // the object r had pointed to has no users; that object is automatically freed

注解 是否使用计数器或其他数据结构来跟踪有多少个指针共享状态取决于实现。关键点是shared_ptr类跟踪有多少个shared_ptr指针指向相同对象并在合适的时候自动释放该对象。

shared_ptr的析构函数减少指向对象的引用计数。当引用计数为0时,shared_ptr析构函数销毁指向的对象并释放内存。

注解 如果将shared_ptr放进容器,记得删除不需要的shared_ptr元素以释放内存。

拥有动态生命周期资源的类

程序倾向于使用动态内存的3个目的:

  1. 不知道需要多少个对象
  2. 不知道所需对象的准确类型
  3. 想要在多个对象之间共享数据

定义StrBlob类

实现一个新的集合类型最简单的方法是使用一个标准库容器管理元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class StrBlob {
public:
    typedef std::vector<std::string>::size_type size_type;
    StrBlob();
    StrBlob(std::initializer_list<std::string> il);
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    // add and remove elements
    void push_back(const std::string&t) {data->push_back(t);}
    void pop_back();
    // element access
    std::string& front();
    std::string& back();
private:
    std::shared_ptr<std::vector<std::string>> data;
    // throws msg if data[i] isn't valid
    void check(size_type i, const std::string &msg) const;
};

12.1.2 直接管理内存

C++定义了2个操作符管理内存。new分配内存,delete释放由new分配的内存。使用newdelete直接管理内存容易出错,而且直接管理内存的类不能依赖默认定义的复制,赋值和析构成员函数。

使用new动态分配和初始化对象

动态分配的对象默认初始化,这意味着内置类型或组合类型的值未定义,类对象的值由默认构造函数初始化。

1
2
string *ps = new string;  // initialized to empty string
int *pi = new int;        // pi points to an uninitialized int

可以直接初始化动态分配的对象,可以使用传统的(),也可以使用新标准的列表初始化{}

1
2
3
4
int *pi = new int(1024); // object to which pi points has value 1024
string *ps = new string(10, '9');   // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

也可以值初始化动态对象,通过类型名加一对空括号:

1
2
3
4
string *ps1 = new string;  // default initialized to the empty string
string *ps = new string(); // value initialized to the empty string
int *pi1 = new int;        // default initialized; *pi1 is undefined
int *pi2 = new int();      // value initialized to 0; *pi2 is 0

最佳实践 和初始化变量的原因一样,最好初始化动态分配的对象

C++11:当在括号里面提供初始值时,可以使用auto推导出我们要分配的对象。由于编译器使用初始值的类型推导要动态分配的对象类型,因此auto只能使用括号初始化,新分配的对象用括号里面的值初始化:

1
2
3
auto p1 = new auto(obj);   // p points to an object of the type of obj
                           // that object is initialized from obj
auto p2 = new auto{a,b,c}; // error: must use parentheses for the initializer

动态分配的const对象

使用new动态分配const对象合法:

1
2
3
4
// allocate and initialize a const int
const int *pci = new const int(1024);
// allocate a default-initialized const empty string
const string *pcs = new const string;

内存耗尽

如果new不能分配内存,默认抛出bad_alloc异常。可以使用另一种形式的new阻止抛出异常:

1
2
3
// if allocation fails, new returns a null pointer
int *p1 = new int; // if allocation fails, new throws std::bad_alloc
int *p2 = new (nothrow) int; // if allocation fails, new returns a null pointer

这种形式的new称为placement newplacement new表达式允许传递额外的参数给new。我们传递标准库定义的nothrow对象给new,告诉new不能抛出异常。如果new不能分配内存,则返回空指针。bad_allocnothrow都定义在头文件new中。

释放动态内存

delete销毁指针指向的对象,并释放相应的内存。传给delete的指针必须是指向动态分配的内存的指针或者是空指针。删除非new返回的指针或删除同一个指针多次是未定义的:

1
2
3
4
5
6
7
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i;   // error: i is not a pointer
delete pi1; // undefined: pi1 refers to a local
delete pd;  // ok
delete pd2; // undefined: the memory pointed to by pd2 was already freed
delete pi2; // ok: it is always ok to delete a null pointer

警告:动态内存管理是容易出错的

  1. 忘记释放内存。
  2. 使用一个已被释放的对象。
  3. 同一片内存释放2次。

删除时重置指针值

当删除一个指针时,指针就无效了。尽管指针无效,在多数机器上指针还是保存了被释放内存的地址,即指针成为所谓的悬垂指针。悬垂指针拥有未初始化指针的所有问题。可以将nullptr赋值给被删除的指针,清晰地指明指针不指向任何对象。

删除时重置指针值只提供有限的保护。如果多个指针指向同一个对象,重置被删除的那个指针值并不影响其他指针。

12.1.3 用new使用shared_ptr

可以使用new返回的指针初始化智能指针:

1
2
shared_ptr<double> p1; // shared_ptr that can point at a double
shared_ptr<int> p2(new int(42)); // p2 points to an int with value 42

其他定义和改变shared_ptr的方式:

操作 说明
shared_ptr<T> p(q) p管理内置指针q指向的对象,q必须是new返回的指针且能转换为T*
shared_ptr<T> p(u) p从unique_ptr指针u接管所有权,使u变成nullptr
shared_ptr<T> p(q, d) p接管内置指针q所指向的对象,q必须能转化为T*,p使用可调用对象d替代delete释放内存
shared_ptr<T> p(p2, d) p是shared_ptr p2的拷贝,使用可调用对象d释放内存
p.reset()
p.reset(q)
p.reset(q, d)
如果p是指向对象的唯一shared_ptr,reset释放指向的对象。如果传递了可选的内置指针q,则使p指向q,否则使p为空指针。如果提供了可调用对象d,则用d释放内存

智能指针带指针参数的构造函数是explicit的,因此不能隐式将内置指针转换为智能指针,必须使用直接初始化形式初始化智能指针:

1
2
shared_ptr<int> p1 = new int(1024);  // error: must use direct initialization
shared_ptr<int> p2(new int(1024));   // ok: uses direct initialization

不要混合使用普通指针和智能指针

一个shared_ptr只能和其他是自己拷贝的shared_ptr协调销毁操作。这是我们推荐使用make_shared的一个原因。它在对象创建的同时绑定到shared_ptr,这样就没有办法将一个地址绑定到多个独立创建的shared_ptr。

考虑下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ptr is created and initialized when process is called
void process(shared_ptr<int> ptr)
{
    // use ptr
} // ptr goes out of scope and is destroyed

int *x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer
process(x);  // error: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x;  // undefined: x is a dangling pointer!

警告 使用内置指针访问由智能指针管理的对象是非常危险的,因为我们不知道这个对象什么时候被释放了。

不要使用get初始化或赋值另一个智能指针

智能指针类型定义了一个get函数返回其管理对象的内置指针。使用get返回指针的代码不能删除该指针。将get返回的指针绑定到另外一个智能指针是错的。

1
2
3
4
5
6
7
shared_ptr<int> p(new int(42)); // reference count is 1
int *q = p.get();  // ok: but don't use q in any way that might delete its pointer
{ // new block
    // undefined: two independent shared_ptrs point to the same memory
    shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed
int foo = *p; // undefined; the memory to which p points was freed

12.1.4 智能指针和异常

使用异常处理的程序需要确保当异常发生时资源被正确释放,一个简单的方法是使用智能指针。

智能指针和哑巴类

我们通常可以使用管理动态内存一样的技巧管理没有良好析构函数的类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct destination;  // represents what we are connecting to
struct connection;   // information needed to use the connection
connection connect(destination*);  // open the connection
void disconnect(connection);       // close the given connection
void f(destination &d /* other parameters */)
{
    // get a connection; must remember to close it when done
    connection c = connect(&d);
    // use the connection
    // if we forget to call disconnect before exiting f, there will be no way to close c
}

void end_connection(connection *p) { disconnect(*p); }

void f(destination &d /* other parameters */)
{
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // use the connection
    // when f exits, even if by an exception, the connection will be properly closed
}

警告:智能指针陷阱

  • 不要使用相同内置指针初始化(或重置)多个智能指针
  • 不要delete从get()返回的指针
  • 不要使用get()初始化或重置另一个智能指针
  • 如果使用get()返回的指针,记住当最后一个智能指针消失后,这个指针变无效
  • 如果使用智能指针管理不是new分配的资源,记得传递一个deleter

12.1.5 unique_ptr

一个unique_ptr拥有其指向的对象。不像shared_ptr,一次只有一个unique_ptr指向指定的对象。当unique_ptr销毁时,其指向的对象也被销毁。

unique_ptr操作:

操作 说明
unique_ptr<T> u1
unique_ptr<T, D> u2
指向T的空指针。u1使用delete释放指针,u2使用可调用对象D释放指针
unique_ptr<T, D> u(d) 指向T的空指针,使用d替代delete,d必须是D类型的对象
u=nullptr 删除u指向的对象,使u为空指针
u.release() 释放指针u的控制,返回u保存的指针,使u为空指针
u.reset()
u.reset(q)
u.reset(nullptr)
删除u指向的对象。如果提供内置指针q,则是u指向那个对象,否则使u为空指针

因为unique_ptr拥有其指向的对象,所以它不支持复制或赋值:

1
2
3
4
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);  // error: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2;                    // error: no assign for unique_ptr

尽管不支持复制和赋值操作,可以通过调用release或reset转移所有权:

1
2
3
4
5
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed

如果不使用另一个智能指针保存从release返回的指针,则记住要释放资源放。

返回unique_ptr

不能复制unique_ptr有一个例外,可以复制或赋值一个即将销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:

1
2
3
4
5
6
7
8
9
unique_ptr<int> clone(int p) {
    // ok: explicitly create a unique_ptr<int> from int*
    return unique_ptr<int>(new int(p));
}
unique_ptr<int> clone(int p) {
    unique_ptr<int> ret(new int (p));
    // . . .
    return ret;
}

向后兼容 早期版本的库包含一个auto_ptr类,它拥有一些但不是全部unique_ptr的特性。特别是auto_ptr不能存进容器,也不能从函数返回。尽管auto_ptr仍然是标准库的一部分,程序应该使用unique_ptr

传递deleter给unique_ptr

和shared_ptr一样,unique_ptr默认使用delete释放其指向的对象。同样,可以覆盖unique_ptr的默认deleter。必须在尖括号里面提供deleter类型

1
2
3
4
// p points to an object of type objT and uses an object of type delT to free that
object
// it will call an object named fcn of type delT
unique_ptr<objT, delT> p (new objT, fcn);

12.1.6 weak_ptr

weak_ptr是一个智能指针,它不控制其指向对象的生命周期。相反,weak_ptr指向一个由shared_ptr管理的对象。绑定一个weak_ptr到shared_ptr上不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr销毁后,对象本身也被销毁,即使有weak_ptr指向它。

weak_ptr的操作:

操作 说明
weak_ptr<T> w 可以指向T的空weak_ptr
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() 返回true如果w.use_count()为0,否则返回false
w.lock() 如果expired为true,返回一个空的shared_ptr,否则返回一个指向w所指对象的shared_ptr

当创建一个weak_ptr时,用shared_ptr初始化它:

1
2
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);  // wp weakly shares with p; use count in p is unchanged

因为对象可能不再存在,不能使用weak_ptr直接访问对象。为了访问对象,必须调用lock。

1
2
3
if (shared_ptr<int> np = wp.lock()) { // true if np is not null
    // inside the if, np shares its object with p
}