动态内存管理

发布于 2020-10-14 18:56:50   阅读量 23  点赞 0  

 一般在程序中用到的内存分为三种:静态内存、栈内存与堆内存。

 分配在静态内存或栈内存中的对象由编译器自动创建与销毁。其中,静态内存用来保存:

  1. 全局变量

  2. static对象(全局与局部)

  3. static数据成员

 栈对象又称自动变量,其仅在定义的程序块运行时存在,在控制流进入变量作用域时系统自动为其分配存储空间,并在离开作用域时自动释放空间。

 除了静态内存与栈内存,每个程序还拥有一个内存池,这部分内存称为 自由空间,程序用堆来存储动态分配的对象。动态对象的生命周期由程序来控制,即当动态对象不再使用时,我们的代码必须能够销毁它。


一、直接管理动态内存

 C/C++ 定义了两个运算符来分配与释放内存:

  • new:在动态内存中为对象分配空间,并调用对象的构造函数,返回一个指向该对象的指针;

  • delete:接收一个动态对象的指针,调用该对象的析构函数,并释放与之关联的动态内存。

 使用动态内存时,需确保在正确的时间释放内存。若忘记释放内存,则会造成内存泄漏;若在尚有指针引用时释放内存,则会产生引用非法内存的空悬指针。


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

① 默认初始化

 直接new某个类型的对象是默认初始化的,这意味着内置类型的对象的值是未定义的,而类类型对象将用默认构造函数进行初始化:

int *pi = new int;      // 未初始化值的整数,具体值不确定
string *ps = new string;        // 初始化为空的 string


② 值初始化

 当在new表达式指定的类型后面加上空括号的时候

string *ps = new string();      // 指向对象值初始化为空 string
int *pi = new int();        // 指向对象值初始化为 0

/* 需区分值初始化与默认初始化,对于类类型而言,值初始化与默认初始化都为调用默认构造函数,结果相同;对于内置类型而言,值初始的结果为零值 */
string *ps2 = new string;   // 默认初始化为空值
int *pi2 = new int;     // 初始化为未定义结果


③ 直接初始化

 在new的类型后面加上括号即可进行直接初始化我们可以使用传统的构造方式(使用圆括号),也可以使用列表初始化(使用花括号):

int *pi = new int(1024);        // pi 指向值 1024 的整型数
string *ps = new string(10,'9');        // ps 指向内容 "9999999999" 的 string

vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

关于不同初始化类型的详解,见[https://wjiaman.fun/blog/C53ZCqMHM9R4BLyhHtj985/]


内存耗尽

 当程序用光了其所有可用的内存,new表达式将会失败,抛出bad_alloc类型的异常。

 想要阻止异常的抛出,可以向new表达式传递一个由标准库定义的nothrow对象,此时若不能分配内存,将返回空指针。

int *p = new (nothrow) int;

bad_allocnothrow都定义在头文件new中。


delete 释放动态内存

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

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


delete 之后重置指针值

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

 推荐的方法是在即将离开指针的作用域时,释放掉所关联的动态内存,这样局部指针变量会被自动销毁,不会产生空悬指针的问题。此外,若仍需要保留指针,可以在delete之后给指针变量赋nullptr



二、智能指针

 为了更加方便地使用动态内存,标准库引入了两种 智能指针 类型来管理动态对象。指针指针封装了底层内置指针,行为与内置指针类似,重要的区别在于其负责自动释放所指向的对象。两种不同的智能指针区别在于它们管理底层指针的方方式:

  1. shared_ptr:允许多个指针指向同一个对象

  2. unique_ptr:独占所指向的对象

 智能指针的操作:

shared_ptr 和 unique_ptr 都支持的操作
shared_ptr<T> sp      空智能指针,可指向类型为 T 的对象
p                将 p 作为条件判断,若P指向一个对象,则为 true
*p               解引用 p,获得它指向的对象
p->mem             访问指针对象的成员
p.get()             返回 p 保存的内置指针,需小心使用
swap(p,q)            交换 p 和 q 中的指针
p.swap(q)            交换 p 和 q 中的指针
shared_ptr 独有的操作
make_shared<T>(args)    返回一个 shared_ptr,用动态分配类型 T 的对象并由 agrs 初始化
shared_ptr<T>p(q)     将 p 拷贝至 q,此操作会递增 q 中的计数器
p=q               此操作会递减 p 指向对象的引用计数,递增 q 指向对象的引用计数
p.unique()         若 p.use_count() 为 1,返回 true;否则返回 false
p.use_count()        返回与 p 共享对象的智能指针数量

 除了智能指针外,标准库还定义了一个weak_ptr伴随类,它是一种弱引用,指向shared_ptr管理的对象。

以上介绍的三种类都定义在memory头文件中。


1. shared_prt 类

 我们可以认为每个shared_ptr对象都有一个计数器,称为 引用计数,无论我们何时拷贝一个shared_ptr,计数器都会递增。

 即shared_ptr通过记录shared_ptr对象上的拷贝来记录指向同一个对象的引用数,也正是如此,shared_ptr无法得知有多少个内置指针指向了该对象。


① 使用

 智能指针是模板,因此当我们创建一个智能指针时,必须提供指针指向的类型:

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

 智能指针的使用方式与普通指针类似,解引用一个智能指针返回它指向的对象,在条件判断中使用智能指针的效果就是检测它是否为空。


② make_shared 创建智能指针

 该函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr

 使用make_shared时,需指定想要创建对象的类型:

shared_ptr<int> p = make_shared<int>(42);

 类似顺序容器的emplace成员,make_shared使用其参数来构造给定类型的对象。若不传递任何参数,对象就会进行 值初始化


③ shared_ptr 的拷贝和赋值

 当我们拷贝一个shared_ptr时,计数器递增;当我们给shared_ptr重新赋值或shared_ptr被销毁时,计数器就会递减。

 一旦一个shared_ptr的计数器变为 0 时,它的析构函数就会自动销毁自己管理的对象,并释放对象的内存。

记得清理无用的 shared_ptr:

 由于计数器为 0 时才会销毁shared_ptr所管理的对象,故需记得将无用的shared_ptr清理掉,以免影响对象的回收。如在一个shared_ptr容器中,对于不再需要的元素,因及时调用earse删除。


④ 智能指针与异常

 对于函数的退出,一般有两种可能:正常返回或发生了异常,无论哪种情况,局部对象都会被销毁。

 借助于此,使用智能指针管理的对象都能够得到正确的处理。与之相反的是,由于异常以后可能的delete操作无法被执行,故直接管理的内存可能不会被正确释放。


⑤ get 存在的风险
  1. 不要使用 get() 初始化或赋值智能指针
     智能指针类型定义了名为get的函数,返回指向智能指针所管理的对象的内置指针。

     而将另一个智能指针绑定到get返回的指针上是错误的,因为这样得到的两个智能指针是独立创建的,它们维护独立的引用计数,这将带来空悬指针与重复delete的问题。

  2. 不 delet get() 返回的指针


⑥ 智能指针管理哑类

 对于分配了资源,而又没有定义良好的析构函数来释放这些资源的哑类,需要显式释放资源以防止资源泄漏。

 我们可以在这样的类中使用智能指针管理资源以解决这样的问题。



2. shared_ptr 和 new 的结合使用

 除了使用make_shared直接分配动态内存并返回智能指针外,还可以用new返回的指针初始化智能指针,既使用智能指针管理new分配的动态内存。

且只能使用由new返回的内置指针初始化shared_ptr

 接受指针参数的智能指针是explicit的,因此不能将内置指针隐式转换为智能指针,必须通过直接初始化形式:

shared_ptr<int> p1 = new int(1024);     // 错误
shared_ptr<int> p2(new int(1024));      // 正确

 用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它管理的对象。

 我们可以将智能指针绑定到指向其他类型资源的指针上,但必须提供自己的操作来代替delete

定义与改变 shared_ptr 行为的其他方法
shared_ptr<T> p(q)   p 管理内置指针 q 指向的对象,q 必须能够转换为 T* 类型
shared_ptr<T> p(q,d)  p 管理内置指针 q 指向的对象,并将使用可调用对象 d 代替 delete
p.reset()        停止 p 对其对象的共享,且置空;将引用计数减一,若计数为 0,引发释放
p.reset(q)       同 reset() 调用,但不会将 p 置空,而是用它管理内置指针 q 指向的对象
p.reset(q,d)      同 reset(q),但引发释放操作时会调用 d 而非 delete


定义自己的释放操作

 自定义的删除器函数接受所管理对象类型的指针,进行相应的释放操作:

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

 当我们创建一个shared_ptr时,可以传递一个指向删除器函数的参数:

void f(destination &d){
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
}


不要混用普通指针与 shared_ptr

shared_ptr可以协调对象的析构,但仅限于shared_ptr之间的拷贝。不建议混用内置指针与shared_ptr,这样有可能带来内置指针意外成为空悬指针的风险。

因此,相比于new分配动态内存,并使用内置指针绑定到shared_ptr上,更推荐使用make_shared的原因。



3. unique_ptr

 一个unique_ptr拥有它指向的对象。

 某个时刻只能有一个unique_ptr指向给定对象,当unique_ptr被销毁时,它所指向的对象也被销毁,且默认使用delete释放对象。

unique_ptr 操作
u-nullpter     将 u 置为空,释放其指向的对象
u.release()     放弃 u 对对象的控制权,返回指向对象的内置指针,并将 u 置空
u.reset()      释放 u 指向的对象,并将 u 置空
u.reset(q)     释放 u 指向的对象,并令 u 指向内置指针 q 指向的对象

① 创建

 与shared_ptr不同,没有类似的make_shared的标准库函数返回unique_ptr;当定义一个unique_ptr时,只能使用内置指针直接初始化:

unique_ptr<int> p1(new int(42));


② 不支持普通的拷贝或赋值

 由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值:

unique_ptr<int> p1(new int(42));
unique_ptr<int> p2(p1);     // 错误,unique_ptr 不支持拷贝
unique_ptr<int> p3;
p3 = p2;        // 错误,unique_ptr 不支持赋值


③ 传递 unique_ptr 参数和返回 unique_ptr

 不能拷贝unique_ptr规则有个例外:我们可以拷贝或赋值一个即将被销毁的unique_ptr

 最常见的例子是可以将unique_ptr从函数返回。

unique_ptr<int> clone(int p){
    unique_ptr ret(new int(p));
    ...
    return ret;
}



4. weak_ptr

weak_ptr(弱引用)是不影响所指向对象生存周期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会影响shared_ptr的引用计数。

weak_ptr 的操作
weak_ptr<T> w    声明指向 T 类型的空 weak_ptr
weak_ptr<T> w(sp)  与 shared_ptr 指向相同对象的 weak_ptr,T 能够转换为 sp 指向的类型
w=p         p 可为 shared_ptr 或 weak_ptr,赋值后共享对象
w.reset()      将 w 置空
w.use_count()    与 w 共享对象的 shared_ptr 的数量
w.expired()     w.use_count() 为 0 时返回 true,否则返回 false
w.lock()       w.expired() 为 true 返回空 shared_ptr;否则返回指向对象的 shared_ptr


Last Modified : 2021-03-12 20:46:42