01 | 堆、栈、RAII:C++里该如何管理资源?
编辑基本概念
堆
堆,英文是(heap),在内存管理的语境下,指的是动态分配内存的区域。
这个堆和数据结构的堆不是一回事。
这里的内存,被分配之后,需要手动释放,否则就会造成内存泄漏。
在C++中,标准里面有一个概念叫自由存储区,英文是 free store,特制使用 new 和 delete 关键字来分配和释放内存的区域。一般而言,这是堆的子集。
- new 和 delete 操作的区域是 free store
- malloc 和 free 操作的区域是 heap
但是 new 和 delete 通常底层使用 malloc 和 free 来实现。因此 free store 也是 heap。
auto ptr = new std::vector<int>();
从历史的角度,动态内存分配出现较晚,因为会带来不确定性(内存分配需要多久?是否分配成功?等),至今有很多场合会禁止使用动态内存,尤其是在实时性要求特别高的场合,例如飞行控制器和电信设备。
在堆上分配内存,程序通常有如下操作:
- 让内存管理器分配一个某个大小的内存块
- 让内存管理器释放一个之前分配的内存块
- 让内存管理器进行垃圾收集操作,寻找不再使用的内存块并予以释放
C++ 通常做操作1、2,Java是1和3,python则是1、2、3均进行。
上述3个操作都不简单,而且彼此相关。
分配内存要考虑当前有多少未分配的内存,内存不足的时候要从操作系统申请新的内存,充足时要从可用的内存中取出一块大小合适的内存,将其标记为已用,返回给要求内存的代码。
绝大部分时候可用内存都会比要求分配的内存大,所以代码只被允许使用其被分配的内存区域,而剩余的内存属于未被分配的状态,可以在后面的分配中使用。如果内存管理器支持垃圾收集,则分配内存的操作可能触发垃圾收集。
释放内存并非简单将内存标记为使用,对于连续未使用的内存块,通常内存管理器要将其合并成一块,以便满足后续较大内存的分配需求。
内存分配和释放的管理,是内存管理器的任务,一般不需要我们介入。我们只需要使用好 new 和 delete 就行。
但事实证明,漏掉 delete 非常常见。这会导致“内存泄漏”。
void foo() {
bar* ptr = new bar();
/* 业务逻辑的代码,此处省略 */
delete ptr;
}
上述的代码看着没什么问题,但是有隐藏的坑:
- 中间的业务逻辑代码可能抛出异常,导致delete未被执行。
- **这个代码不符合C++的惯用法。**在c++中,这种情况有很大可能性不需要使用堆内存分配,而应该使用栈内存分配。
更常见的情况如下:
bar* make_bar(...)
{
try {
bar* ptr = new bar();
...
}
catch (...) {
delete ptr;
throw;
}
return ptr;
}
void foo()
{
bar * ptr = make_bar(...);
...
return ptr;
}
栈
在大多数计算机体系结构中,栈的增长方向是低地址。
当函数调用另一个函数时,会把参数压入栈中(忽略寄存器传参),然后把下一行汇编指令的地址压入栈(方便函数返回),并跳转新的函数。新的函数进入后,首先做一些必须的准备工作,然后会调整栈指针,分配出本地变量所需的空间,随后执行函数中的代码,并在执行完毕之后,根据调用者压入栈的地址,返回调用者未执行的代码中继续执行。
也就是说,本地变量存储在栈上,和函数执行所需的其他数据一起。当函数执行完成后,这些内存也会随之释放。
即:
- 栈上的内存分配极为简单,仅仅移动一下指针而已。
- 释放也极为简单,函数执行结束后移动栈指针即可。
- 由于后进先出的执行过程,不可能出现内存碎片。
在栈中分配简单类型(int、char、double……)很常见,C++称为 POD 类型(Plain Old Data)。对于有构造函数和析构函数的非POD类型,栈上的内存分配也同样有效,只不过编译器会在合适位置,插入构造函数和析构函数的调用。
#include <iostream>
class Obj {
public:
Obj() { std::cout << "Obj constructor" << std::endl; }
~Obj() { std::cout << "Obj destructor" << std::endl; }
};
void func(int val) {
Obj obj;
if (val == 42) throw "life, the universe and everything";
}
int main()
{
try {
func(41);
func(42);
} catch (const char* msg) {
std::cout << msg << std::endl;
}
}
代码中,不管 func
函数是否发生了异常,obj 的析构函数都会执行。
在C++中,所有的变量缺省都是值语义。如果不使用 *
或 &
的话,变量不会向 Java 或 Python 一样引用一个堆上的对象。
RAII
很多时候,变量不能,或者不应该,存储在栈上。例如:
- 对象很大
- 对象的大小在编译时不能确定
- 对象是函数的返回值,但由于特殊的原因,不应使用对象的值返回
常见的情况之一是,在工厂方法或其他面向对象编程的情况下,返回值类型是基类。
// 简单的工厂方法
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();
}
}
如何确保在使用 create_shape
函数的返回值时不会发生内存泄漏?
只需要把这个返回值放到一个本地变量里,并确保析构函数会删除该对象即可。
enum class shape_type {
circle,
triangle,
rectangle,
};
class shape {};
class circle : public shape {};
class triangle : public shape {};
class rectangle : public shape {};
class shape_wrapper {
public:
explicit shape_wrapper(
shape* ptr = nullptr)
: ptr_(ptr) {}
~shape_wrapper() {
delete ptr_;
}
shape* ptr() const { return ptr_; }
private:
shape* ptr_;
};
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();
}
}
void func() {
...
shape_wrapper wrapper(create_shape(shape_type::circle));
...
}
说明一点:delete一个空指针是合法操作。
- 0
- 0
-
分享