跳转至

C++内存管理

内存管理是C++程序员在对于程序设计等方面,可以由程序员所控制资源分配的优秀机制,其他一些语言像Javapython等,有自己的内存(垃圾)回收机制,不需要程序员自己操心。但是内存管理机制意味着需要对内存有足够的管理把握,否则会造成资源浪费;严重的还会带来严重的危害

在C语言中,内存一般分为四个区域

内存四区

一般情况下,每个程序的内存都是独立的,互不干扰

  • 堆的内存分配情况:低地址往高地址分配
  • 连续情况:不连续;在系统中是以链表的形式出现
  • 程序分配内存时,会根据所需的大小在系统中找到第一块比所需内存大的内存块,在链表中删除这块内存分配给程序使用
  • 用于动态内存的分配
  • 有程序员手动开辟手动释放;程序员在在程序中不对该内存进行释放则会在程序结束时自动回收

数据区

  • 存放全局变量、静态变量和常量
  • 数据区又分为:全局区和静态区,分别存放全局变量和静态变量

代码区

  • 存放可执行代码
  • 属性:只读

栈区

  • 存放临时变量,包括局部变量,函数参数,返回值,函数地址等等
  • 是系统底层所支持的,不需要程序员手动管理,效率高
  • 对于32bit系统来说,对于栈的默认分配一般是1M,可以手动设置分配大小
  • 连续情况:是连续的
  • 栈的内存分配情况:高地址往低地址分配,即栈底高地址,栈顶低地址

在C++中略微有点不同:

同上

补充:由newdelete分配和释放的内存

自由存储区

malloc/free分配的内存,可以说是堆的一个子集

全局/静态存储区

C语言分为初始化和未初始化;C++中没有这种区分

常量存储区

存放常量


可能在划分的术语上有些不同,但是我们看得出实际上是差不多的

malloc和free

  • malloc和free是C语言的库函数;
    malloc是在自由存储区分配内存,free是释放内存

  • malloc的语法:

int*p=(int*)malloc(sizeof(int)*m);

上面的代码表示: 利用malloc分配了m个 int类型的内存空间,用int*的指针去维护
函数原型为:

void* malloc(size_t,size)

malloc的返回值是void*万能指针,可以接收任意数据类型地址,所以在上述写malloc 语法的例子时,在malloc前面加上(int*)进行类型转换

  • free的语法:
free(p);

函数原型为:

void free(*ptr)

函数传入的参数是一个维护一块手动开辟的内存地址

new和delete

既然有了malloc和free函数,为什么C++还需要new和delete来管理内存呢?其实new/delete与malloc/free是有区别的,是C++必不可少的。

  • new和delete不是库函数,而是关键字;
    底层封装的函数其实是malloc和free,但是new和delete比它们多实现一些东西。

  • new关键字:

  • 语法:

    Complex *p=new Complex(1,2);
    
  • 编译器转为:

    Complex *pc;
    try{
      // operator new from new_op.cc
      void* mem=operator new(sizeof(Complex));// allocate
      pc=static_cast<Complex*>(mem);// static cast
      pc->Complex::Complex(1,2);// constructor
      // note: 只有编译器才可以实现上面的直接呼叫ctor
    }
    catch(std::bad_alloc){
      // if allocate failed,no ctor
    }
    
    • 先调用operator new()函数分配内存
    • 再进行static_cast<T>类型转换
    • 再调用该类的构造函数(编译器呼叫)
    • 内部:是调用malloc()函数
  • 几种new:

    • plain new:普通的new
    void* operator new(std::size_t) throw(std::bad_alloc);
    // delete
    void operator delete(void*)throw();
    

    在空间分配失败时,抛出异常std::bad_alloc而不是NULL,通过判断返回值是否为NULL没有用

    • nothrow new:不抛出异常的new
    void*operator new(std::size_t,const std::nothrow_t&) throw();
    // delete
    void operator delete(void*) throw();
    

    在分配空间失败时返回NULL,不抛出异常

    • placement new:不担心内存分配失败,因为它根本不分配内存,唯一一件事需要它做就是调用对象的构造函数
    void* operator new(size_t,void*);
    // delete
    void operator delete(void*,void*);
    

关于new的分配不是此文重点,就不总结那么多了

  • delete关键字:

  • 语法:

    delete pc; // array: delete[] p;
    
  • 编译器转为:

    pc->~Complex();
    operator delete(pc);
    
    • 先调用该类的析构函数析构对象
    • 在释放分配的内存
  • array new/delete

  • 对于内置数据类型来说,分配内存数组之后使用delete p/delete[] p是没有区别的,因为内置数据类型没有构造函数和析构函数。直接使用delete p会直接释放掉所分配的内存,不会造成内存泄漏

  • 但是对于自定义数据类型来说,我们在实际应用中是要自己定义构造函数和析构函数的,而对于在自定义类中有指针类型成员对象并需要构造该对象时,这个指针类型的类成员有时需要自己在构造时分配内存等操作,则如果此时只是使用delete p来达到析构对象并释放内存的目的,其只会析构释放该类对象,并不会去析构释放指针对象分配的内存,造成此种程度上的内存泄漏。
  • 按照上面所说的情况,使用delete[] p,编译器会知道释放的内存是一个数组,则会根据分配内存的情况去释放内存,该内存对象中的指针指向的内存也会一一执行相应的析构函数并释放内存。

总结完堆的一些操作之后,我们再来总结一下栈的操作。
栈是一种先进后出的数据结构,在程序执行时的工作为:

#include<iostream>
using namespace std;

int Func(int a,int b){
  return a+b;
}

int main(){
  int a=10,b=20;
  int c=Func(a,b);
  return 0;
}
  • 栈是从高地址往低地址分配内存,则栈底为高地址,栈顶为低地址。从栈底开始,建立一个为main的栈帧,分别把变量a,b压入栈中;然后检测到函数Func(),为Func建立一个栈帧,将函数的返回地址和参数a、b压入栈;跳转到Func函数,执行a+b的操作并将结果压入栈中;Func函数执行完成之后,将结果作为返回值弹出栈,再弹出a、b变量和Func函数的返回地址,找到Func所在位置,返回值给变量c接收,再把变量c压入栈中;检测到return 0main函数结束,将栈中的数据按照压栈顺序的逆序一一弹出。
  • 在压栈的过程中,内存地址根据变量或栈帧等字节大小递减,弹出时地址按照压入的变量等递增
  • 上述的本地变量是内置的数据类型,其实本地变量不仅可以是内置数据类型,也可以是类类型等。如果函数调用结束后或者发生异常时,编译器会自动调用析构函数,这个过程就做栈展开

RAII

RAII是一种资源管理规则:在分配资源的同时构造对象,在释放资源的同时析构对象。
这里举一个例子:

class B{
  B(){cout<<This is Bs constructor!\n}
  ~B(){cout<<This is Bs deconstructor\n}
};

void test(){
  B*b=new B;
  // ...
  delete b;
}

中间// ...的一些操作可能会抛出异常,导致后面的析构函数用不上,造成内存泄漏 解决方法是可以在实例化对象时抛出异常并且接收异常,但这个就不是RAII机制了,而是异常机制,我们可以在中间操作// ...中用到实例化对象时直接将实例化的过程放入。
举个例子:

// something operator like this API
Func(new B);

假设这个函数的传入参数是一个B类对象或者其子类对象,可以使用这种方式。这种方式就可以说是RAII。

boost::scope_exit

boost库中的scope_exit是对于RAII很好的使用,可以在内部定义释放资源的函数

以下是对boost::scope_exit的简单实现:

test_boostScopeExit
#include <iostream>
#include <boost/scope_exit.hpp>
#include <fstream>
#include <functional>

class ScopeExit {
public:
    ScopeExit() = default;
    ScopeExit(const ScopeExit&) = delete; //non_copy
    void operator=(const ScopeExit&) = delete; //non_assignment

    ScopeExit(ScopeExit&&) = default;
    ScopeExit& operator=(ScopeExit&&) = default;

    // 递归执行传入的函数
    template<typename F, typename... Args>
    ScopeExit(F&& f, Args&&... args) {
        func_ = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
    }

    ~ScopeExit() {
        if (func_) {
            func_();
        }
    }

private:
    std::function<void()> func_;
};

#define _CONCAT(a, b) a##b //a连接b
#define _MAKE_SCOPE_(line) ScopeExit _CONCAT(defer, line)=[&]()

#undef SCOPE_GUARD
#define SCOPE_GUARD _MAKE_SCOPE_(__LINE__)

void test() {
    char* test = new char[100];
    std::ofstream ofs("test.txt");
    SCOPE_GUARD{
        delete[] test;
        ofs.close();
    };
}

int main() {
    test();
    return 0;
}





  有关于内存管理这次就到这,有错误的地方欢迎到加我微信向我说明:ZJPeng6485,毕竟我也是在学习中。共勉!