C++:动态内存

前言

为了尽快能够开始Canvas的开发,先提前看一下比较重要的部分


动态内存

到目前为止,大多数程序中所使用的对象都有着严格定义的生存期。全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。

除了自动化和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。

动态对象的正确释放被证明是编程中极其容易出错的地方。为了更安全得使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

我们的程序到目前为止只使用过静态内存或栈内存。静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的全局变量。栈内存用来保存定义在函数那的自动变量。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称为自由空间堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象——即那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式的销毁它们。

动态内存与智能指针

在C++中,动态内存的管理是通过一对运算符来完成的:

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针
  • delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存

动态内存的使用很容易出问题,因为确保在正确的时间释放内存极其困难:

  • 忘记释放内存会导致产生内存泄漏
  • 在尚有指针引用内存的情况下释放内存,会产生引用非法内存的指针

为了更容易同时更安全地使用动态内存,新的标准库提供了两种智能指针(smart point)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种指针的区别在于管理底层指针的方式:

  • shared_ptr:允许多个指针指向同一个对象
  • unique_ptr:“独占”所指向的对象
  • weak_ptr:伴随类,是一个指向shared_ptr所管理对象的弱引用

程序使用动态内存出于以下三种原因之一:

  • 程序不知道自己需要使用多少对象
  • 程序不知道所需对象的准确类型
  • 程序需要再多个对象间共享数据

shard_ptr类

智能指针也是模版,因此,创建智能指针时,必须提供指针可以指向的类型

1
2
shared_ptr<string> p1;
shared_ptr<list<int>> p2;

默认初始化的智能指针中保存着一个空指针,因此如果在一个条件判断中使用智能指针,效果就是检测它是否为空

1
2
3
if (p1 & p1->empty()) {
*p1 = "hi";
}

智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。

下表是shared_ptrunique_ptr都支持的操作

操作说明
shared_ptr<T> sp空指针,可以指向类型为T的对象
unique_ptr<T> up空指针,可以指向类型为T的对象
p将p作为一个条件判断,若p指向一个对象,则为true
*p解引用p,获得它指向的对象
p->value等价于(*p).value
p.get()返回p中保存的指针
swap(p, q) p.swap(q)交换p和q中的指针

shared_ptr独有的操作

操作说明
make_shared<T>(args)返回一个shared_ptr指针,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptr<T>p (q)p是shared_ptr q的拷贝;此操作会增加q中的计数器。q中的指针必须能转换为T*
p = qp和q都是shared_ptr,所保存的指针必须能相互转换。此操作会减少p内执政的引用计数,增加q内指针的引用计数,如果p内指针的引用计数为0,则其管理的原内存将被释放
p.use_cout()返回与p共享对象的智能指针数量
p.unique()若p.use_count()为1返回true,否则返回false

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中。

1
shared_ptr<int> p = make_shared<int>(45);

shared_ptr的拷贝和赋值

每个shared_ptr都有一个关联的计数器。

以下的操作都会导致 shared_ptr的计数器增加:

  • shared_ptr被拷贝
  • 被用于初始化另一个shared_ptr
  • 作为参数传递给函数
  • 作为函数返回值

而当我们给shared_ptr赋新值或者shared_ptr已经被销毁,计数器就会减少。一旦一个shared_ptr计数器为0,则它会自动释放自己管理的对象。

shared_pt自动销毁与释放对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会执行自己的析构函数,并在自己的析构函数中调用对象的析构函数,销毁对象,并释放对象所占用的内存空间。例如,我们有一个函数,它返回一个shared_ptr,指向一个Foo类型的动态分配的对象,对象是通过一个类型为T的参数进行初始化的:

1
2
3
4
shared_ptr<Foo> factory(T arg) {
// shared_ptr负责释放内存
return make_shared<Foo>(arg);
}

假设接下来我们调用函数,创建对象,并分配内存

1
2
3
4
5
void use_factory(T arg) {
// 创建并引用shared_ptr,引用计数加1
shared_ptr<Foo> p = factory(arg);
// p离开了作用域,引用计数减1
}

由于p是use_factory的局部变量,在use_factory结束时它将被销毁。当p被销毁时,将减少对应内存的引用计数。由于p是唯一引用factory返回的内存的对象,因此当p被销毁时,p指向的对象也会被销毁,所占用的内存会被释放。

unique_ptr类

unique_ptr实现了独享所有权的语义。一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权从源指针转移给目标指针。拷贝一个unique_ptr是不被允许的,因为如果你拷贝一个unique_ptr,那么拷贝结束后这两个unique_ptr都会指向相同的资源,它们都认为自己拥有这块资源,所以都会企图释放,这将导致冲突。因此unique_ptr是一个仅能移动的类型。当指针析构时,它所拥有的资源也将被销毁。默认情况下,资源的析构时伴随着调用unique_ptr内部的原始指针的delete的操作的。

以下是unique_ptr类独有的操作

操作说明
unique_ptr<T> ul空unique_ptr,可以指向类型为T的对象。ul会使用delete来释放它的指针
unique_ptr<T, D> u2u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T, D> u(d)空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr释放u指向的对象,将u置为空
u.release()u放弃对指针的控制权,返回指针,并将u置为空
u.reset(q)如果提供了内置指针q,令u指向这个对象;否则将u置为空

创建unique_ptr

在C++14之前,unique_ptr不像shared_ptr一样拥有标准库函数make_shared来创建一个实例,C++14之后,可以使用make_unique来创建unique_ptr

1
2
unique_ptr<int> pInt(new int(5));
unique_ptr<double> pDouble = make_unique<double>(4.2);

无法进行复制构造和赋值操作

unique_ptr没有copy构造函数,不支持普通的拷贝和赋值操作

1
2
3
unique_ptr<int> pInt(new int(5));
unique_ptr<int> pInt2(pInt); //报错
unique_ptr<int> pInt3 = pInt; //报错

可以进行移动构造和移动赋值操作

1
2
3
4
unique_ptr<int> pInt(new int(5));
unique_ptr<int> pInt2 = std::move(pInt); //转移所有权
// cout << *pInt <<endl; // 出去,pInt已经为空
unique_ptr<int> pInt3(std::move(pInt2));

unique_ptr虽然没有支持普通的拷贝和赋值操作,但却提供了一种移动机制来将指针的所有权从一个unqiue_ptr转移给另一个unique_ptr。如果需要转移所有权,可以使用std::move()函数。

可以返回unique_ptr

unique_ptr不支持拷贝操作,但却有一个例外:可以从函数中返回一个unique_ptr

1
2
3
4
unique_ptr<int> clone(int p) {
unique_ptr<int> pInt(new int(p));
return pInt;
}

直接管理内存

C++使用定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。自己直接管理内存的类型与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。

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

在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指定该对象的指针:

1
int *pi = new int;

动态分配的const对象

用new分配const对象是合法:

1
const int *pci = new const int(1024);

类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,可以隐式初始化,而其他类型的对象都必须显式初始化。

释放动态内存

为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象:delete p。与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。

指针值和delete

我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的:

1
2
3
4
5
6
7
int i, *pi = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // 错误: i不是一个指针
delete pi1; // 未定义: pi1指向一个局部变量
delete pd; // 正确
delete pd2; // 未定义:pd2指向的内存已经被释放了
delete pi2; // 正确:释放一个空指针总是没有错的

对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。执行delete pi1和pd2所产生的错误则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的。

虽然一个const对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个const动态对象,只要delete指向它的指针即可:

1
2
const int *pci = new const int(1024);
delete pci

动态对象的生存期直到被释放时为止

对于一个由内置指针管理的动态对象,直到被显示释放之前它都是存在的。

返回指向动态内存的指针的函数给其调用者增加了一个额外负担——调用者必须记得释放内存。与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。

使用new和delete管理动态内存存在三个常见问题:

  • 忘记delete内存。忘记释放动态内存会导致人们常说的”内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄漏错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测出这种错误。

  • 使用已经释放掉的对象。这可能会导致程序运行过程中出现crash


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!