动态内存(《C++ Primer》)

  • 静态内存用来保存局部 static 对象、类 static 数据成员以及定义在任何函数之外的变量
  • 栈内存用来保存定义在函数内的非 static 对象
  • 分配在静态或栈内存中的对象由编译器自动创建和销毁
  • 对于栈对象,仅在其定义的程序块运行时才存在;static 对象在使用之前分配,在程序结束时销毁
  • 除了静态内存和栈内存,每个程序还拥有一个内存池,这部分内存被称作自由空间(free store)或堆(heap)
  • 程序用堆来存储动态分配(dynamically allocate)的对象,即那些在程序运行时分配的对象
  • 动态对象的生存期由程序来控制,即当动态对象不再使用时,代码必须显式地销毁它们

动态内存与智能指针

容易出错的两种运算符

  • new
    • 在动态内存中为对象分配空间并返回一个指向该对象的指针
    • 可以选择对对象进行初始化
  • delete
    • 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存

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

  • 忘记释放内存 - 产生内存泄漏
  • 在尚有指针引用内存的情况下就释放了它 - 产生引用非法内存的指针

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

  • shared_ptr 允许多个指针指向同一个对象
  • unique_ptr 则“独占”所指向的对象
  • 标准库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象
  • 这三种类型都定义在 memory 头文件中

shared_ptr 类

(前6个为 shared_ptrunique_ptr 都支持的操作)

  • shared_ptr<T> sp / unique_ptr<T> up
    • 空智能指针,可以指向类型为 T 的对象
  • p
    • p 用作一个条件判断,若 p 指向一个对象,则为 true
  • *p
    • 解引用 p,获得它指向的对象
  • p→mem
    • 等价于(*p).mem
  • p.get()
    • 返回 p 中保存的指针
    • 要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
  • swap(p, q) / p.swap(q)
    • 交换 pq 中的指针
  • make_shared<T> (args)
    • 返回一个 shared_ptr,指向一个动态分配的类型为 T 的对象
    • 使用 args 初始化此对象
  • shared_ptr<T>p (q)
    • pshared_ptr q 的拷贝
    • 此操作会递增 q 中的计数器
    • q 中的指针必须能转换为 T*
  • p = q
    • pq 都是 shared_ptr,所保存的指针必须能相互转换
    • 此操作会递减 p 的引用计数,递增 q 的引用计数;若 p 的引用计数变为0,则将其管理的原内存释放
  • p.unique()
    • p.use_count()为1,返回 true;否则返回 false
  • p.use_count()
    • 返回与 p 共享对象的智能指针数量
    • 可能很慢,主要用于调试

make_shared 函数

最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。

// 指向一个值为42的 int 的 shared_ptr
shared_ptr<int> p3 = make_shared<int> (42);
// p4 指向一个值为“9999999999”的 string
// 传递的参数需与 string 的某个构造函数相匹配
shared_ptr<string> p4 = make_shared<string> (10, '9');
// p5 指向一个值初始化的 int,即值为0
shared_ptr<int> p5 = make_shared<int> ();
// p6 指向一个动态分配的空 vector<string>
auto p6 = make_shared<vector<string>>();

shared_ptr 的拷贝和赋值

  • 每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数(reference count)

  • 无论何时拷贝一个 shared_ptr,计数器都会递增

    • 当用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增
    • 当给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(例如一个局部的 shared_ptr 离开其作用域)时,计数器就会递减
    • 一旦一个 shared_ptr 的计数器变为0,它就会自动释放自己所管理的对象
    auto r = make_shared<int> (42); // r 指向的 int 只有一个引用者
    r = q; // 给 r 赋值,令它指向另一个地址
    			 // 递增 q 指向的对象的引用计数
    			 // 递减 r 原来指向的对象的引用计数
    			 // r 原来指向的对象已没有引用者,会自动释放
    

shared_ptr 自动销毁所管理的对象

  • shared_ptr 的析构函数会递减它所指向的对象的引用计数
  • 如果引用计数变为0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存

shared_ptr 自动释放相关联的内存

// factory 返回一个 shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg)
{
	// 恰当地处理 arg
	// shared_ptr 负责释放内存
	return make_shared<Foo> (arg);
}

void use_factory(T arg)
{
	shared_ptr<Foo> p = factory(arg);
	// 使用 p
} // p 离开了作用域,它指向的内存会被自动释放掉

shared_ptr<Foo> use_factory(T arg)
{
	shared_ptr<Foo> p = factory(arg);
	// 使用 p
		return p; // 当我们返回 p 时,引用计数进行了递增操作
} // p 离开了作用域,但它指向的内存不会被释放掉
	// 在此版本中,use_factory 中的 return 语句向此函数的调用者返回了一个 p 的拷贝

由于在最后一个 shared_ptr 销毁前内存都不会释放,保证 shared_ptr 在无用之后不再保留就非常重要了。如果忘记了销毁程序不再需要的 shared_ptr,程序仍会正确执行,但会浪费内存。

shared_ptr 在无用之后仍然保留的一种可能情况是,将 shared_ptr 存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,应该确保用 erase 删除那些不再需要的 shared_ptr 元素。


使用了动态生存期的资源的类

程序使用动态内存出于以下三种原因之一:
1. 程序不知道自己需要使用多少对象(比如:容器)
2. 程序不知道所需对象的准确类型
3. 程序需要在多个对象间共享数据


直接管理内存

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

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

对象初始化

  • 默认初始化

    • 内置类型或组合类型的对象的值将是未定义的
    • 类类型对象将用默认构造函数进行初始化
    string *ps = new string; // 初始化为空string
    int *pi = new int; // pi 指向一个未初始化的 int
    
  • 直接初始化

    • 传统的构造方式(使用圆括号)
    • 在新标准下,可以使用列表初始化(使用花括号)
    int *pi = new int(1024);
    string *ps = new string(10, '9');
    
    vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    
  • 值初始化

    • 在类型名之后跟一对空括号
    string *ps1 = new string; // 默认初始化为空 string
    string *ps = new string(); // 值初始化为空 string
    
    int *pi1 = new int; // 默认初始化;*pi1 的值未定义
    int *pi2 = new int(); // 值初始化为0;*pi2 为0
    
  • auto:推断想要分配的对象的类型

    auto p1 = new auto(obj); // p 指向一个与 obj 类型相同的对象,该对象用 obj 进行初始化
    auto p2 = new auto{a, b, c}; // 错误:括号中只能有单个初始化器
    

动态分配的 const 对象

一个动态分配的 const 对象必须进行初始化。

对于一个定义了默认构造函数的类类型,其 const 动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。

由于分配的对象是 const 的,new 返回的指针是一个指向 const 的指针。


内存耗尽

一旦一个程序用光了它所有可用的内存,new 表达式就会失败。

int *p1 = new int; // 如果分配失败,new 抛出 std::bad_alloc
int *p2 = new (nothrow) int; // 如果分配失败,new 返回一个空指针

定位 new(placement new)

  • 允许向 new 传递额外的参数
    • 比如 nothrow:不能抛出异常,这种形式的 new 不能分配所需内存,会返回一个空指针
    • bad_allocnothrow 都定义在头文件 new 中

释放动态内存

delete

  1. 销毁给定的指针指向的对象
  2. 释放对应的内存

指针指和 delete

  • 传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针

  • 释放一块并非 new 分配的内存,或者将相同的指针值释放多次,其行为是未定义的

  • 虽然一个 const 对象的值不能被改变,但它本身是可以被销毁的

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

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

shared_ptr 管理的内存在最后一个 shared_ptr 销毁时会被自动释放。但对于一个由内置指针管理的动态对象,直到被显式释放之前他它都是存在的。

void use_factory(T arg)
{
	Foo *p = factory(arg); // p 是一个内置指针,而不是一个智能指针
	// ...
	delete p;
}

Foo* use_factory(T arg)
{
	Foo *p = factory(arg);
	// ...
	return p; // 调用者必须释放内存
}

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

  1. 忘记 delete 内存
  2. 使用已经释放掉的对象
  3. 同一块内存释放两次

delete 之后重置指针值

当我们 delete 一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。

delete 之后,指针就变成了人们所说的空悬指针(dangling pointer),即,指向一块曾经保存数据对象但现在已经无效的内存的指针。

未初始化指针的所有缺点空悬指针也都有。

可以在指针即将要离开其作用域之前释放掉它所关联的内存,这样就没有机会继续使用指针了。

如果需要保留指针,可以在 delete 之后将 nullptr 赋予指针,这样就清楚地指出指针不指向任何对象。但这个方法也只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。


shared_ptr 和 new 结合使用

不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针。

shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式

shared_ptr<int> clone(int p) {
	return new int(p); // 错误:隐式转换为 shared_ptr<int>
}

shared_ptr<int> clone(int p) {
	// 正确:显式地用 int* 创建 shared_ptr<int>
	return shared_ptr<int>(new int(p));
}

定义和改变 shared_ptr 的其他方法

  • shared_ptr<T> p(q)
    • p 管理内置指针 q 所指向的对象
    • q 必须指向 new 分配的内存,且能够转换为 T* 类型
  • shared_ptr<T> p(u)
    • punique_ptr u 那里接管了对象的所有权
    • u 置为空
  • shared_ptr<T> p(q, d)
    • p 接管了内置指针 q 所指向的对象的所有权
    • q 必须能转换为 T* 类型
    • p 将使用可调用对象 d 来代替 delete
  • shared_ptr<T> p(p2, d)
    • pshared_ptr p2 的拷贝,唯一的区别是 p 将用可调用对象 d 来代替 delete
  • p.reset() / p.reset(q) / p.reset(q, d)
    • p 是唯一指向其对象的 shared_ptrreset 会释放此对象
    • 若传递了可选的参数内置指针 q,会令 p 指向 q,否则会将 p 置为空
    • 若还传递了参数 d,将会调用 d 而不是 delete 来释放 q

不要混合使用普通指针和智能指针,也不要用 get 初始化另一个智能指针或为智能指针赋值。

// 在函数被调用时 ptr 被创建并初始化
// process 的参数是传值方式传递的,因此实参会被拷贝到 ptr 中
void process(shared_ptr<int> ptr)
{
	// ...
} // ptr 离开作用域,被销毁

shared_ptr<int> p(new int(42)); // 引用计数为1
process(p); // 拷贝 p 会递增他的引用计数;在 process 中引用计数值为2
int i = *p; // 正确:引用计数值为1

int *x(new int(1024)); // 危险:x 是一个普通指针,不是一个智能指针
process(x); // 错误:不能将 int* 转换为一个 shared_ptr<int>
process(shared_ptr<int> (x)); // 合法的,但内存会被释放!
int j = *x; // 未定义的:x 是一个空悬指针

get 返回一个内置指针,指向智能指针管理的对象。

当将一个 shared_ptr 绑定到一个普通指针时,我们就将内存的管理责任交给了这个 shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问 shared_ptr 所指向的内存了。

shared_ptr<int> p(new int(42))// 引用计数为1
int *q = p.get(); // 正确:但使用 q 时要注意,不要让它管理的指针被释放
{ // 新程序块
	// 未定义:两个独立的 shared_ptr 指向相同的内存,各自的引用计数都是1
	shared_ptr<int> (q);
} // 程序块结束,q 被销毁,它指向的内存被释放
int foo = *p; // 未定义:p 指向的内存已经被释放了,p 变成一个空悬指针

get 用来将指针的访问权限传递给代码,只有在确定代码不会 delete 指针的情况下,才能使用 get。特别是,永远不要用 get 初始化另一个智能指针或者为另一个智能指针赋值。

善用 shared_ptr,远离内存泄漏


其他 shared_ptr 操作

与赋值类似,reset 会更新引用计数,如果需要的话,会释放 p 指向的对象。

p = new int(1024); // 错误:不能将一个指针赋予 shared_ptr
p.reset(new int(1024)); // 正确:p 指向一个新对象

if (!p.unique())
	p.reset(new string(*p)); // 我们不是唯一用户;分配新的拷贝
*p += newVal; // 现在我们知道自己是唯一的用户,可以改变对象的值

智能指针和异常

函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。使用 shared_ptr 会自动释放内存:

void f()
{
	shared_ptr<int> sp(new int(42));
	// 这段代码抛出一个异常,且在 f 中未被捕获
} // 在函数结束时 shared_ptr 自动释放内存

与之相对的,当发生异常时,直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在 new 之后在对应的 delete 之前发生了异常,则内存不会被释放:

void f()
{
	int *ip = new int(42);
	// 这段代码抛出一个异常,且在 f 中未被捕获
	delete ip; // 在退出之前释放内存
}

在资源分配和释放之间发生了异常,程序也会发生资源泄漏

分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误:

struct destination; // 表示我们正在连接什么
struct connection; // 使用连接所需的信息
connection connect(destination*); // 打开连接
void disconnect(connection); // 关闭给定的连接
void f(destination &d /* 其他参数 */)
{
	// 获得一个连接;记住使用完后要关闭它
	connection c = connect(&d);
	// 使用连接
	// 如果在 f 退出前忘记调用 disconnect,就无法关闭 c 了
}

定义一个删除器deleter)来代替 delete 操作:

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

void f(destination &d /* 其他参数 */)
{
	connection c = connect(&d);
	shared_ptr<connection> p(&c, end_connection);
	// 使用连接
	// 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
}

unique_ptr

一个 unique_ptr “拥有”它所指向的对象。与 shared_ptr 不同,某个时刻只能有一个 unique_ptr 指向一个给定对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁。

unique_ptr 特有的操作

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

weak_ptr

  • weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象

  • 将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数

  • 一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放

  • 即使有 weak_ptr 指向对象,对象也还是会被释放

  • weak_ptr<T> w

    • weak_ptr 可以指向类型为 T 的对象
  • weak_ptr<T> w(sp)

    • shared_ptr sp 指向相同对象的 weak_ptr
    • T 必须能转换为 sp 指向的类型
  • w = p

    • p 可以是一个 shared_ptr 或一个 weak_ptr
    • 赋值后 wp 共享对象
  • w.reset()

    • w 置为空
  • w.use_count()

    • w 共享对象的 shared_ptr 的数量
  • w.expired()

    • w.use_count() 为0,返回 true,否则返回 false
  • w.lock()

    • 如果 expiredtrue,返回一个空 shared_ptr;否则返回一个指向 w 的对象的 shared_ptr

动态数组

new 和数组

int *pia = new int[get_size()]; // pia 指向第一个 int

当用 new 分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针


初始化动态分配对象的数组

int *pia = new int[10]; // 10个未初始化的 int
int *pia2 = new int[10](); // 10个值初始化为0的 int
string *psa = new string[10]; // 10个空 string
string *psa2 = new string[10](); // 10个空 string

// 10个 int 分别用列表中对应的初始化器初始化
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 10个 string,前4个用给定的初始化器初始化,剩余的进行值初始化
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};

动态分配一个空数组是合法的

char arr[0]; // 错误:不能定义长度为0的数组
char *cp = new char[0]; // 正确:但 cp 不能解引用

当用 new 分配一个大小为0的数组时,new 返回一个合法的非空指针。此指针保证与 new 返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,可以像使用尾后迭代器一样使用这个指针。但此指针不能解引用——毕竟它不指向任何元素。


释放动态数组

delete [] pa;

智能指针和动态数组

// up 指向一个包含10个未初始化 int 数组
unique_ptr<int[]> up(new int[10]);
up.release();

指向数组的 unique_ptr

  • 指向数组的 unique_ptr 不支持成员访问运算符(点和箭头运算符)
  • unique_ptr<T[]> u
    • u 可以指向一个动态分配的数组,数组元素类型为 T
  • unique_ptr<T[]> u(p)
    • u 指向内置指针 p 所指向的动态分配的数组
    • p 必须能转换为类型 T*
  • u[i]
    • 返回 u 拥有的数组中位置 i 处的对象
    • u 必须指向一个数组

allocator 类

  • allocator<T> a
    • 定义了一个名为 aallocator 对象,它可以为类型为 T 的对象分配内存
  • a.allocate(n)
    • 分配一段原始的、未构造的内存,保存 n 个类型为 T 的对象
  • a.deallocate(p, n)
    • 释放从 T* 指针 p 中地址开始的内存,这块内存保存了 n 个类型为 T 的对象
    • p 必须是一个先前由 allocate 返回的指针,且 n 必须是 p 创建时所要求的大小
    • 在调用 deallocate 之前,用户必须对每个在这块内存中创建的对象调用 destroy
  • a.construct(p, args)
    • p 必须是一个类型为 T* 的指针,指向一块原始内存
    • arg 被传递给类型为 T 的构造函数,用来在 p 指向的内存中构造一个对象
  • a.destroy(p)
    • pT* 类型的指针,此算法对 p 指向的对象执行析构函数

为了使用 allocate 返回的内存,我们必须用 construct 构造对象。使用未构造的内存,其行为是未定义的。

这些函数在给定目的位置创建元素,而不是由系统分配内存给它们:

  • uninitialized_copy(b, e, b2)
    • 从迭代器 be 置出的输入范围中拷贝元素到迭代器 b2 指定的未构造的原始内存中
    • b2 指向的内存必须足够大,能容纳输入序列中元素的拷贝
  • uninitialized_copy_n(b, n, b2)
    • 从迭代器 b 指向的元素开始,拷贝 n 个元素到 b2 开始的内存中
  • uninitialized_fill(b, e, t)
    • 在迭代器 be 指定的原始内存范围中创建对象,对象的值均为 t 的拷贝
  • uninitialized_fill_n(b, n, t)
    • 从迭代器 b 指向的内存地址开始创建 n 个对象
    • b 必须指向足够大的未构造的原始内存,能够容纳给定数量的对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值