01 | C++ 资源管理:堆、栈、RAII
作者:互联网
一、基本概念
堆(heap),在内存管理的语境下,指的是动态分配内存的区域,与数据结构里的堆不是一个概念。这里的内存,被分配之后需要手工释放,否则,就会造成内存泄漏。
C++ 标准里使用 new 和 delete 分配和释放内存的区域叫自由存储区(free store),这是堆的一个子集:
- new 和 delete 操作的区域是 free store
- malloc 和 free 操作的区域是 heap
- new 和 delete 底层通常使用 malloc 和 free 实现,所以 free store 也是 heap
栈(stack),在内存管理的语境下,指的是函数调用过程中产生的本地变量和调用数据的区域。这个栈和数据结构的栈高度相似,满足后进先出。
RAII (Resource Acquisition Is Initialization, 资源获取即初始化),C++ 特有的资源管理方式,依托栈和析构函数对所有资源(包括堆)进行管理。
- RAII要求:资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题。
- RAII 机制的使用,使得 C++ 不需要类似 Java 那样的垃圾收集方法,也能有效地对内存进行管理。
二、堆
在堆上分配内存并构造对象
std::vector<int> nums; // 栈
auto ptr = new std::vector<int>(); // 堆
在堆上分配内存,通常涉及三个可能的内存管理操作:
- 让内存管理器分配一个某个大小的内存块
- 让内存管理器释放一个之前分配的内存块
- 让内存管理器进行垃圾收集操作,寻找不再使用的内存块并予以释放
C++ 通常做 1 和 2,Java 通常做 1 和 3,Python 通常做 1、2、3。
- 分配内存要考虑当前已经有多少未分配的内存,绝大多数情况下,可用内存会比要求分配的内存大;如果内存管理器支持垃圾收集的话,分配内存的操作还可能触发垃圾收集。
- 释放内存不只是简单地把内存标记为未使用,还需要将连续未使用的内存块整合;
- 内存配合和释放的管理,是内存管理器的任务,只需要正确地使用 new 和 delete,对每个 new 出来的对象都用 delete 来释放。
漏掉 delete 是常见情况,这叫“内存泄漏”。看一个例子:
void foo(){
Bar *ptr = new Bar();
// ...
delete ptr;
}
- 如果中间省略的代码抛出异常,会导致会面的 delete 得不到执行;
- 不符合 C++ 惯用法,这种情况 C++ 中应该使用栈内存分配,即
Bar ptr;
更常见、更合理情况,是分配和释放不在一个函数里:
bar* make_bar(…)
{
bar* ptr = nullptr;
try {
ptr = new bar();
…
}
catch (...) {
delete ptr;
throw;
}
return ptr;
}
void foo()
{
…
bar* ptr = make_bar(…)
…
delete ptr;
}
但同样存在漏 delete 的可能性。
三、栈
本地变量所需的内存会在栈上,跟函数执行所需的其他数据在一起。当函数执行完后,这部分内存会释放掉。
- 栈上的分配极为简单,移动一下栈指针;
- 栈上的释放也简单,函数执行结束时移动一下栈指针;
- 由于后进先出的执行过程,不可能出现内存碎片。
POD类型是C++中常见的概念,用来说明类/结构体的属性,是指没有使用面向对象的思想来设计的类/结构体,其目的是为了解决C++与C之间数据类型的兼容性问题,具体可看这篇文章 C++之POD数据类型。
对于有构造和析构函数的非 POD 类型,栈上的内存分配同样有效,编译器会在生成代码的合适位置,插入对构造函数和析构函数的调用。
编译器会自动调用析构函数,包括函数执行发生异常的情况。发生异常时对析构函数的调用叫栈展开。下面演示栈展开:
class Obj {
public:
Obj() { puts("Obj()"); }
~Obj() { puts("~Obj()"); }
};
void foo(int n)
{
Obj obj;
if (n == 42)
throw "life, the universe and everything";
}
int main()
{
try {
foo(41);
foo(42);
}
catch (const char* s) {
puts(s);
}
}
执行结果:
Obj()
~Obj()
Obj()
~Obj()
life, the universe and everything
不管是否发生异常,析构函数都会先得到执行。
在 C++ 里,所有变量的缺省都是值语义,如果不使用 * 和 & 的话,变量不会像 java/python 一样引用一个堆上的对象。
四、RAII
RAII(Resource Acquisition Is Initialization, 资源获取即初始化),这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
也就是说,RAII 机制会对资源申请、释放的操作成对封装,利用的是栈内存自动销毁对象的特性。
C++不应该存储在栈上的情形:
- 对象很大
- 对象的大小在编译时不能确定
- 对象是函数的返回值,但由于特殊原因,不应使用对象的值返回。
先看一个工厂模式(返回值类型是基类的指针或引用)下内存泄露的例子:
enum class shape_type {
circle,
triangle,
rectangle,
// …
};
class shape { … };
class circle : public shape { … };
class triangle : public shape { … };
class rectangle : public shape { … };
shape* create_shape(shape_type type)
{
// …
switch (type) {
case shape_type::circle:
return new circle(…);
case shape_type::triangle:
return new triangle(…);
case shape_type::rectangle:
return new rectangle(…);
// …
}
}
这种情况函数的返回值只能是指针或引用,如果返回值(shape),实际返回 circle,不会报错,但会出现对象切片,指的是将派生类对象给基类时,丢失了一部分信息。
通过析构函数和它的栈展开,可以确保在使用create_shape
的返回值时不会发生内存泄漏。
class shape_wrapper {
public:
// 声明为 explicit 的构造函数不能在隐式转换中使用
explicit shape_wrapper(shape* ptr = nullptr): ptr_(ptr){}
~shape_wrapper()
{
delete ptr_;
}
shape* get() const { return ptr_; }
private:
shape* ptr_;
};
// 调用 foo 函数时会新建一个 shape_wrapper,函数结束时编译器会自动调用析构函数释放内存
void foo()
{
// …
shape_wrapper ptr_wrapper(create_shape(…));
// …
}
注意:delete 空指针是一个合法操作。
当 new 一个对象和 delete 一个指针时,编译器大致会做如下操作:
// new circle(…)
{
void* temp = operator new(sizeof(circle)); // 先分配内存
try {
circle* ptr = static_cast<circle*>(temp);
ptr->circle(…); // 不是合法的 C++ 代码
return ptr;
}
catch (...) {
operator delete(ptr);
throw;
}
}
# delete
if (ptr != nullptr) {
ptr->~shape(); // 不是合法的 C++ 代码
operator delete(ptr);
}
new 时先分配内存,然后在这个结果指针上构造对象;构造成功则 new 操作整体完成,否则释放刚分配的内存并继续向外抛构造函数产生的异常。
析构函数里做必要的清理工作,这就是 RAII 的基本用法。这种清理并不限于释放内存,也可以是:
- 关闭文件(fstream 的析构就会这么做)
- 释放同步锁
- 释放其他重要的系统资源
例如,我们应该使用:
std::mutex mtx;
void some_func()
{
std::lock_guard<std::mutex> guard(mtx);
// 做需要同步的工作
}
而不是:
std::mutex mtx;
void some_func()
{
mtx.lock();
// 做需要同步的工作……
// 如果发生异常或提前返回,
// 下面这句不会自动执行。
mtx.unlock();
}
另一个例子:
class Test
{
public:
Test(int i)
{
this->i = i;
cout << "constructor~" << i << endl;
}
~Test()
{
cout << "deconstructor~" << i << endl;
}
private:
int i;
};
int main()
{
Test *test = new Test(1);
Test test2(2);
}
constructor~1
constructor~2
deconstructor~2
可以看到,对于堆上分配内存新建的对象 test,没有手动 delete 的情况下不会调用其析构函数。
class Test
{
public:
Test(int i)
{
this->i = i;
cout << "constructor~" << i << endl;
}
~Test()
{
cout << "deconstructor~" << i << endl;
}
private:
int i;
};
class wrap
{
public:
wrap(Test *test) : test(test) {}
~wrap()
{
delete test;
}
private:
Test *test;
};
int main()
{
Test *test = new Test(1);
Test test2(2);
wrap test3(test);
}
constructor~1
constructor~2
deconstructor~1
deconstructor~2
参考资料
标签:01,RAII,C++,shape,内存,new,ptr,delete 来源: https://www.cnblogs.com/cscshi/p/15476647.html