C++资源管理:堆、栈、RAII

基础概念

C++中的内存管理概要说明

  • 栈:由编译器管理分配和回收,存放局部变量和函数参数。

  • 堆:由程序员管理,需要⼿动 new malloc delete free 进⾏分配和回收,空间较⼤,但可能会出现内存泄漏和空闲 碎⽚的情况。

  • 全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。

  • 常量存储区:存储常量,⼀般不允许修改。

  • 代码区:存放程序的⼆进制代码。

  • 说明

    C++ 标准里一个相关概念是自由存储区,英文是 free store,特指使用 new 和 delete 来分配和释放内存的区域。一般而言,这是堆的一个子集:

    new 和 delete 操作的区域是 free store

    malloc 和 free 操作的区域是 heap

    但 new 和 delete 通常底层使用 malloc 和 free 来实现,所以 free store 也是 heap。 但是对其区分的实际意义并不大。

  • 简单使用

    致在堆上分配内存(并构造对象)。

    auto ptr = new std::vector();
  • 内存泄露

    • 错误写法(针对CPP)

      void foo() {
      
      bar* ptr = new bar();
      
      …
      
      delete ptr; 
      }
      1. 中间省略的代码部分也许会抛出异常,导致最后的 delete ptr 得不到执行。
      2. 这个代码不符合 C++ 的惯用法。在 C++ 里,这种情况下有 99% 的可能性不应该使用堆内存分配,而应使用栈内存分配。
    • 正确写法:分配和释放不在一个函数里

    bar *make_bar(…)
    {
        …
        try
        {
            bar *ptr = new bar();
            …
        }
        catch (...)
        {
            delete ptr;
            throw;
        }
        return ptr;
    }
    
    void foo()
    {
        …
        bar *ptr = make_bar(…) 
        …
        delete ptr;
    }  

  • 说明
    英文是 stack,在内存管理的语境下,指的是函数调用过程中产生的本地变量调用数据的区域。这个栈和数据结构里的栈高度相似,都满足“后进先出”(last-in-first-out 或 LIFO)。
  • 程序栈演示

    void foo(int n) { 
    …
    }
    
    void bar(int n) {
    
    int a = n + 1;
    
    foo(a); 
    }
    
    int main() {
    
    …
    
    bar(42);
    
    … 
    }

    解释

    • 栈是向上增长的,栈的增长方向是低地址,因而上方意味着低地址。
    • 任何一个函数,根据架构的约定,只能使用进入函数时栈指针向上部分的栈空间。
    • 当函数调用另外一个函数时,会把参数也压入栈里(此处忽略使用寄存器传递参数的情况),然后把下一行汇编指令的地址压入栈,并跳转到新的函数。
    • 新的函数进入后,首先做一些必须的保存工作,然后会调整栈指针,分配出本地 量所需的空间,随后执行函数中的代码,并在执行完毕之后,根据调用者压入栈的地址,返 回到调用者未执行的代码中继续执行。
    • 本地变量所需的内存就在栈上,跟函数执行所需的其他数据在一起。
    • 栈上的分配极为简单,移动一下栈指针而已。
    • 栈上的释放也极为简单,函数执行结束时移动一下栈指针即可。
    • 由于后进先出的执行过程,不可能出现内存碎片。【堆会】

    上述都是本地变量都是简单类型POD 类型(Plain Old Data)。对于有构造和析构函数的非 POD 类型,栈上的内存分配也同样有效,只不过 C++ 编译器会在生成代码的合适位置,插入对构造和析构函数的调用。

    这里尤其重要的是:编译器会自动调用析构函数,包括在函数执行发生异常的情况。在发生异常时对析构函数的调用,还有一个专门的术语,叫栈展开(stack unwinding)。

RAII机制

  • 说明

    完整的英文是 Resource Acquisition Is Initialization,是 C++ 所特有的资源管理方式。在主流的编程语言中, C++ 是唯一一个依赖 RAII 来做资源管理的。

    RAII 依托栈和析构函数,来对所有的资源:包括堆内存在内——进行管理。对 RAII 的 使用,使得 C++ 不需要类似于 Java 那样的垃圾收集方法,也能有效地对内存进行管理。 RAII 的存在,也是垃圾收集虽然理论上可以在 C++ 使用,但从来没有真正流行过的主要原因。

    类似智能指针。

    RAII机制保证了异常安全,并且也为程序员在编写动态分配内存的程序时提供了安全保证。

    缺点是有些操作可能会抛出异常,如果放在析构函数中进行则不能将错误传递出去,那么此时析构函数就必须自己处理异常。这在某些时候是很繁琐的。

C++ 支持将对象存储在栈上面。但是,在很多情况下,对象不能,或不应该,存储在栈 上。比如:

  • 对象很大;
  • 对象的大小在编译时不能确定;
  • 对象是函数的返回值,但由于特殊的原因,不应使用对象的值返回。

在工厂方法或其他面向对象编程的情况下,返回值类型是基类:

#include <iostream>
#include <mutex>
#include <fstream>
using namespace std;

enum class shape_type {
    circle,
    triangle,
    rectangle,
};

class shape {
public:
    shape() { cout << "shape" << endl; }

    virtual void print() {
        cout << "I am shape" << endl;
    }

    virtual ~shape() {}
};

class circle : public shape {
public:
    circle() { cout << "circle" << endl; }

    void print() {
        cout << "I am circle" << endl;
    }
};

class triangle : public shape {
public:
    triangle() { cout << "triangle" << endl; }

    void print() {
        cout << "I am triangle" << endl;
    }
};

class rectangle : public shape {
public:
    rectangle() { cout << "rectangle" << endl; }

    void print() {
        cout << "I am rectangle" << endl;
    }
};

/*
说明点1:利用多态 上转 如果返回值为shape,会存在对象切片问题【派生类中独有的成员变量和方法都被slice掉了,值剩下和基类相同的成员变量和属性。这个派生类对象被切成了一个基类对象】。

这个 create_shape 方法会返回一个 shape 对象,对象的实际类型是某个 shape 的子类,圆啊,三角形啊,矩形啊,等等。这种情况下,函数的返回值只能是指针或其变体形式。如果返回类型是 shape,实际却返回一个circle,编译器不会报错,但结果多半是错的。这种现象叫对象切片(object slicing),是 C++ 特有的一种编码错误。这种错误不是语法错误,而是一个对象复制相关的语义错误,也算是 C++ 的一个陷阱了。

基类如果没有虚函数,就会使用shape::print()。加了虚函数,就会使用circle::print()。
*/
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();
    }
}

/*
说明点2:在使用 create_shape 的返回值时不会发生内存泄漏的方法:
在析构函数和它的栈展开行为上:只需要把这个返回值放到一个本地变量里,并确保其析构函数会删除该对象即可。

在析构函数里做必要的清理工作,这就是RAII的基本用法。这种清理并不限于释放内存,也可以是:

1. 关闭文件(fstream 的析构就会这么做)
2. 释放同步锁
3. 释放其他重要的系统资源

*/

class shape_wrapper {
public:
    explicit shape_wrapper(shape *ptr = nullptr) : ptr_(ptr) {}

    ~shape_wrapper() {
        delete ptr_;
    }

    shape *get() const {
        return ptr_;
    }

private:
    shape *ptr_;
};

void foo() {
    shape_wrapper ptr(create_shape(shape_type::circle));
    ptr.get()->print();
}

int main() {

    // 第一种方式
    shape *sp = create_shape(shape_type::circle);
    sp->print();
    delete sp;

    // 第二种方式
    foo();

    return 0;
}

评论

  1. admin
    博主
    5月前
    2022-3-13 22:00:48

    对RAII还是有点云里雾里

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇