主页
文章
分类
系列
标签
简历
【C++ Primer(edition 5) 11】动态内存
发布于: 2021-10-7   更新于: 2021-10-7   收录于: Cpp
文章字数: 812   阅读时间: 4 分钟   阅读量:

动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

对象的生命周期:

  • 全局对象在程序启动时分配,结束时销毁。

  • 局部对象在进入程序块时创建,离开块时销毁。

  • 局部static对象在第一次使用前分配,在程序结束时销毁。

  • 动态分配对象:只能显式地被释放。

  • 对象的内存位置:

    • 静态内存用来保存局部static对象、类static对象、定义在任何函数之外的变量。
    • 栈内存用来保存定义在函数内的非static对象。
    • 堆内存,又称自由空间,用来存储动态分配的对象。

一、new和delete直接管理内存

1、new动态分配内存

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

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

1
int *pi = new int(123);

1.2 初始化new分配的对象的方法

  • 默认情况下,动态分配的对象是默认初始化的。这意味着内置类型和组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化
1
2
string *ps = new string; //初始化为空的string
int *pi = new int; //pi指向一个未初始化的int
  • 可以使用直接初始化的方式来初始化一个动态分配的对象。即传统的圆括号构造和新标准下的列表初始化
1
2
3
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};
  • 也可以对动态分配的对象进行值初始化。只需要在其后加一对括号即可
1
2
3
4
string *ps1 = new string;     //默认初始化为空string 
string *ps2 = new string();   //值初始化为空string 
int *pi1 = new int;           //默认初始化;*pil的值未定义 
int *pi2 = new int();         //值初始化为0; *pi2为 0

1.3 动态分配const对象

new返回的也是一个const指针 除非有默认构造函数的类,不然必须显示化初始化变量

1.4 内存耗尽

一旦内存耗尽,会抛出类型是bad_alloc的异常。

2、delete释放内存

2.1 释放动态内存

delete将动态内存归还给系统。 接受一个指针,这个指针必须指向动态分配的内存或者一个空指针。完成两个动作 :

  • 销毁给定的指针指向的对象
  • 释放对应的内存。

2.2 delete和指针

编译器不能分辨一个指针指向的是静态还是动态分配的对象,也不能判断是否指针已经被delete

3、直接管理动态内存存在的常见问题

1.忘记delete内存。 2.使用已经释放掉的对象。 3.同一块内存释放两次。 4.多个指针指向同一块动态内存的删除问题 5.空悬指针问题:delete后的指针称为[[对 delete -ptr 的理解(释放内存、空悬指针、重复释放).pdf | 空悬指针]](dangling pointer)

C/C++ 中为什么 delete 一块内存后,该内存不可复用

坚持只使用智能指针可以避免上述所有问题。

二、动态内存与智能指针

动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。 为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。

智能指针:

  • 管理动态对象。
  • 行为类似常规指针。
  • 负责自动释放所指向的对象。
  • 智能指针也是模板。

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

1、shared_ptr类

1.1 定义

1
2
shared_ptr<string> p1; //p1可以指向string
shared_ptr<list<int>> p2; //p2可以指向int的list

1.2 初始化

1.2.1 结合new初始化

用new返回的指针来初始化,但是必须直接初始化 ,因为智能指针的构造函数是explicit,所以不能将一个内置指针隐式转化为智能指针

1
2
3
shared_ptr<double> pl; //shared_ptr可以指向一个double 
shared_ptr<int> p2(new int(42)); //p2指向一个值为42的int
shared_ptr<int> p3 = new int(1024); //错误,只能直接初始化

1.2.2 make_shared函数

  • 为什么使用 :最安全的分配和使用动态内存的方法是调用一个名为make_shared 的标准函数
  • 作用 : 此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
  • 用法 : make_shared 用其参数来构造给定类型的对象当要用make_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同, 在函数名之后跟一个尖括号,在其中给出类型
1
2
3
4
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>>();

1.2.3 使用非动态内存初始化

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的 资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。

1
shared_ptr<string> p(&c , end_string) //其中end_string是自己定义的删除函数

1.3 定义和改变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是唯一指向其对象的shared_ptrreset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置空。若还传递了参数d,则会调用d而不是delete来释放q
p.reset(q) 同上
p.reset(q, d) 同上

1.4 shared_ptr类的操作

shared_ptr和unique_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中的指针

shared_ptr独有的操作

操作 解释
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共享对象的智能指针数量;可能很慢,主要用于调试。
使用动态内存的三种原因
  • 程序不知道自己需要使用多少对象(比如容器类)
  • 程序不知道所需要对象的准确类型。
  • 程序需要在多个对象间共享数据。

1.5 智能指针和异常

  • 如果使用智能指针,即使程序块由于异常过早结束,智能指针类也能确保在内存不需要的时候将其释放。
  • 智能指针陷阱
    • 不用相同的内置指针初始化(或reset)多个智能指针
    • delete get()返回的指针。
    • 如果你使用get()返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。
    • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

2、unique_ptr

  • 某一个时刻只能有一个unique_ptr指向一个给定的对象,并“拥有”该对象。
  • 当定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,必须采用直接初始化形式。
  • 不支持拷贝或者赋值操作。只能可以通过调用release或 reset将指针的所有权从一个(非const) unique_ptr转移给另一个 unique
1
2
3
4
5
6
7
// 将所有权从pl (指 向 string Stegosaurus )转移给p2 
unique_ptr<string> p2 (pl.release () ) ; 
// release 将 pl 置为空 
unique_ptr<string> p3(new string("Trex"));
// 将所有权从p3转移给p2
p2.reset (p3.release () ) ; 
// reset 释放了 p2 原来指向的内存
  • 向后兼容:auto_ptr:老版本,具有unique_ptr的部分特性。特别是,不能在容器中保存auto_ptr,也不能从函数返回auto_ptr
  • 不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr
1
2
3
4
5
//从函数返回unique_ptr
unique_ptr<int> clone (int p) {
//正确:从 int*创建一个unique_ptr<int> 
return unique_ptr<int>(new int(p));
}
1
2
3
4
5
6
//返回一个局部对象的拷贝
unique_ptr<int> clone(int p) {
	unique_ptr<int> ret (new int(p));
	//
	return ret;
}

重载删除器

1
unique_ptr<objT , delT> p (new objT , fcn);

unique_ptr操作:

操作 解释
unique_ptr<T> u1 unique_ptr,可以指向类型是T的对象。u1会使用delete来是释放它的指针。
unique_ptr<T, D> u2 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指向q指向的对象
u.reset(nullptr) u置空

3、weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针。指向一个由shared_ptr管理的对象,不改变shared_ptr的引用计数。 一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。

weak_ptr操作:

操作 解释
weak_ptr<T> w weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) shared_ptr指向相同对象的weak_ptrT必须能转换为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

创建一个weak_ptr

我们创建一个weak_ptr时,要用一个shared_ptr来初始化它

1
2
auto p = make_shared<int>(42); 
weak_ptr<int> wp (p) ; // wp弱共享p; p 的引用计数未改变

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,必须调用lock此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的 shared_ptr。

1
2
if(shared_ptr<int> np = wp.lock())
{}

三、动态数组

1、new和数组

1.1 new一个动态数组

  • 类型名之后加一对方括号,指明分配的对象数目(必须是整型,不必是常量)。
  • 返回指向第一个对象的指针
1
int *p = new int[size];
  • 方括号中的大小必须是整形,但不必是常量。也可以用一个表示数组类型的类型别名来分配一个数组这样new表达式中就不需要方括号了。
1
2
typedef int arrT [42] ; //arrT 表示 42 个 int 的数组类型 
int *p = new arrT; //分配一个42个 int的数组;p 指向第一个int
  • 动态分配一个空数组也是合法的,这样的指针就类似于尾后指针。

1.2 初始化动态数组

  1. 方法一:在数组大小之后跟一对空括号
1
2
3
4
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
  1. 方法二:提供一个元素初始化器的花括号列表
1
2
3
4
// 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')};

2、delete释放一个动态数组

为了释放动态数组,我们使用一种特殊形式的delete— 在指针前加上一个空方括号对

1
delete [] p; //数组中的元素按逆序销毁

方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。即使使用类型别名,也需要方括号对

3、智能指针unique_ptr和动态数组

3.1 定义与销毁

  • 在定义时,在对象类型后面跟一对空方括号。
1
2
3
//up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[10]); 
up.release(); // 自动用delete []销毁其指针

类型说明符中的方括号(<int[]>)指 出 up 指向一个int数组而不是一个int。由于 up指向一个数组,当 up销毁它管理的指针时,会自动使用delete[]

3.2 操作

指向数组的unique_ptr不支持成员访问运算符(点和箭头)。当一个unique_ptr指向一个数组时, 我们可以使用下标运算符来访问数组中的元素

操作 解释
unique_ptr<T[]> u u可以指向一个动态分配的数组,整数元素类型为T
unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
u[i] 返回u拥有的数组中位置i处的对象。u必须指向一个数组。
  • shared_ptr不直接支持管理动态数组,如果希望使用则需要定义自己的删除器。并且其未定义下标运算符,而且智能指针类型不支持指针算数术运算。为了访问元素,必须用get获取一个内置指针然后访问元素

4、allocator类

4.1 new、delete 管理动态数组之困

new、detele将内存分配和对象构造组合在了一 起 但是存在一种情况,希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作。 这暴露了这种管理动态数组的缺点:可能会造成不必要的浪费

1
2
3
4
5
6
7
string *const p = new string [n] ; //构造 n 个空 string 
string s; 
string *q = p; // q 指向第一个 string 
while cin>>s & & q ! = p + n  *q++ = s; // 赋予*q一个新值
const size_t size = q - p; //记住我们读取了多少个string
// 使用数组
delete [] p; // p 指向一个数组;记得用delete []来释放

问题1:在不知道有多少输入字符串时,就预先分配了n个元素的数组 问题2:赋值了两次,第一次默认初始化,随后又赋值 问题3:没有默认构造函数的类就不能动态分配数组

4.2 allocator类

标准库allocator类定义在头文件memory中,帮助我们将内存分配和对象构造分离开。

  • 分配的是原始的、未构造的内存。
  • allocator是一个模板。当一个 allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置

4.2.1 allocator分配未构造的内存

标准库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*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。
a.destroy(p) pT*类型的指针,此算法对p指向的对象执行析构函数。

allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对 象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
allocator<string> alloc; //可以分配string的allocator对现
auto const p = alloc.allocate(n); //分配n个未初始化的string
auto q = p; // q 指向最后构造的元素之后的位置
//为了使用allocate返回的内存,我们必须用construct构造对象
alloc.construct (q++) ; // *q 为空字符串 
alloc.construct(q++, 10, 'c' ); // *q 为 cccccccccc
alloc.construct(q++, "hi"); //*q为hi!

cout << *p << endl; // 正确:使用string的输出运算符 
cout << *q << endl; // 灾难:q 指向未构造的内存!
//当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy 接受一个指针,对指向的对象执行析构函数
while (q != p)
alloc.destroy (--q) ; // 释放我们真正构造的string
//元素销毁后,1.继续分配给其他元素 2.deallocate释放内存,还给系统
alloc.deallocate(p, n);

allocator算法

操作 解释
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必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
  • 定义在头文件memory中。
  • 在给定目的位置创建元素,而不是由系统分配内存给他们。动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象 分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接 受一个动态对象的指针,销毁该对象,并释放与之关联的内存。