动态内存管理
发布于 2020-10-14 18:56:50 阅读量 23 点赞 0
一般在程序中用到的内存分为三种:静态内存、栈内存与堆内存。
全局变量
static
对象(全局与局部)类
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_alloc
与nothrow
都定义在头文件new
中。
delete 释放动态内存
传递给delete
的指针必须指向动态分配的内存或是一个空指针。
new
分配的指针或将相同指针值释放多次,其行为是未定义的。
delete 之后重置指针值
在delete
之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存。
delete
之后给指针变量赋nullptr
。
二、智能指针
为了更加方便地使用动态内存,标准库引入了两种 智能指针 类型来管理动态对象。指针指针封装了底层内置指针,行为与内置指针类似,重要的区别在于其负责自动释放所指向的对象。两种不同的智能指针区别在于它们管理底层指针的方方式:
shared_ptr
:允许多个指针指向同一个对象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 存在的风险
- 不要使用 get() 初始化或赋值智能指针
智能指针类型定义了名为get
的函数,返回指向智能指针所管理的对象的内置指针。
而将另一个智能指针绑定到get
返回的指针上是错误的,因为这样得到的两个智能指针是独立创建的,它们维护独立的引用计数,这将带来空悬指针与重复delete
的问题。 - 不 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 |
-
0