技术交流QQ群:1027579432,欢迎你的加入!

1.Cpp中的内存分配

  • 了解动态内存在C++中是如何工作的是成为一名合格的C++程序员必不可少的。C++程序中的内存分为两个部分:
    • 栈:在函数内部声明的所有变量都将占用栈内存。
    • 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存
  • 很多时候,在无法提前预知需要多少内存来存储某个定义变量中的特定信息,所需内存的大小需要在运行时才能确定。在C++中,可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即new运算符。如果不再需要动态分配的内存空间,可以使用delete运算符,删除之前由new运算符分配的内存。

2.new和delete运算符

  • 下面是使用new运算符来为任意的数据类型动态分配内存的通用语法:
        new 数据类型;
    
  • 上面的数据类型可以是包括数组在内的任意内置的数据类型,也可以是包括类或结构在内的用户自定义的任何数据类型。先来看下内置的数据类型。例如,可以定义一个指向double类型的指针,然后请求内存,该内存在执行时被分配。可以按照下面的语句使用new运算符来完成这点:
        double *ptr = NULL;  // 初始化为null的指针
        ptr = new double;  // 为变量请求内存
    
  • 如果自由存储区已被用完,可能无法成功分配内存。所以建议检查new运算符是否返回NULL指针,并采取以下适当的操作:
        double *ptr = NULL;
        if(!(ptr == new double){
            cout << "Error: out of memory.\n";
            exit(1);
        }
    
  • malloc()函数在C语言中就出现了,在C++中仍然存在,但建议尽量不要使用malloc()函数。new与malloc()函数相比,主要的优点是:new不只是分配了内存,还创建了对象!在任何时候,当觉得某个已经动态分配内存的变量不再需要使用时,可以使用delete操作符释放它所占用的内存,如下所示:
        delete ptr;  // 释放ptr所指向的内存
    
  • 下面的实例使用了上面的概念,演示了如何使用new和delete的运算符
        #include "iostream"
    
        using namespace std;
    
    
    
        int main(){
            double *ptr = NULL;   // 初始化为null的指针
            ptr = new double;   // 为变量请求内存
    
            *ptr = 3.14;   // 在分配的地址存储值
            cout << "*ptr = " << *ptr << endl;
            delete ptr;   // 释放内存
            return 0;
        }
    

3.数组的动态内存分配

  • 假设要为一个字符数组(一个有20个字符的字符串)分配内存,可以使用上面实例中的语法来为数组动态地分配内存,如下所示:
        char *ptr = NULL;   // 初始化为 null 的指针
        ptr = new char[20];  // 为变量请求内存
    
  • 删除刚创建的数组,语句如下:
        delete [] ptr;     // 删除 pvalue 所指向的数组
    
  • 下面是new操作符的通用语法,可以为***数组分配内存,如下所示:
        // 一维数组如下
        int *array = new int [m];   // 动态分配,数组长度为 m
        delete [] array;  //释放内存
        -------------------------------------------------------------------------------
        // 二维数组如下
        int **array;
        // 假定数组第一维长度为m, 第二维长度为n
        // 动态分配空间
        array = new int *[m];
        for(int i = 0; i < m; i++)
            array[i] = new int [n];
        // 释放
        for(int i = 0; i < m; i++)
            delete [] array[i];
        delete [] array;
    
  • 二维数组实例测试
        // 二维数组
        int **p;
        int i, j;  // p[4][8]
        // 开始分配4行8列的二维数组数据
        p = new int *[4];
        for(i = 0; i < 4; i++)
            p[i] = new int [8];
        
        for(i = 0; i < 4; i++){
            for(j = 0; j < 8; j++)
                p[i][j] = j * i;
        }
        // 打印数据
        for(i = 0; i < 4; i++){
            for(j = 0; j < 8; j++){
                if(j == 0)
                    cout << endl;
                cout << p[i][j] << "\t";
            }
        }
        cout << endl;
        // 开始释放申请的堆
        for(i = 0; i < 4; i++)
            delete [] p[i];
        delete p;
        cout << "-------------------------------\n";
    
  • 三维数组测试实例
        // 三维数组
        int i1, j1, k1; //p[2][3][4]
        int ***pt;
        pt = new int **[2];
        for(i1 =0; i1 < 2; i1++){
            pt[i1] = new int *[3];
            for(j1 = 0;j1<3;j1++)
                pt[i1][j1] = new int[4];
        } 
        // 输出pt[i][j][k]三维数据
        for(i1 = 0;i1 <2;i1++){
            for(j1=0;j1 <3;j1++){
                for(k1=0; k1 < 4; k1++){
                    pt[i1][j1][k1] = i1 + j1 + k1;
                    cout << pt[i1][j1][k1] << " ";
                }
                cout << endl;
            }
            cout << endl;
        }
        // 释放内存
        for(i1 = 0; i1 < 2; i1++){
            for(j1 = 0; j1 < 3; j1++)
                delete [] pt[i1][j1];
        }
    
        for(i1 = 0; i1 < 2; i1++)
            delete [] pt[i1];
    
        delete [] pt;
        cout << "-------------------------------\n";
    

4.对象的动态内存分配

  • 对象与简单的数据类型没有什么不同,例如,请看下面的代码,将使用一个对象数组来理清这一概念,如果要为一个包含4个Box对象的数组分配内存,构造函数将被调用4次.同样地,当删除这些对象时,析构函数也将被调用相同的次数(4次)。
        // 对象的动态内存分配
        class Box{
            public:
                Box(){
                    cout << "调用构造函数\n";
                }
                ~Box(){
                    cout << "调用析构函数\n";
                }
        };
        Box *boxarray = new Box[4];
        delete [] boxarray;  // 删除数组
    

5.动态内存分配中的细节知识点

  • delete与delete []区别
    • 针对简单类型,使用new分配后的不管是数组还是非数组形式内存空间用两种方式均可,此种情况中的释放效果相同。原因在于:分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理。在析构时,系统并不会调用析构函数,它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间(在分配过程中 系统会记录分配内存的大小等信息,此信息保存在结构体_CrtMemBlockHeader中,具体情况可参看VC安装目录下CRT\SRC\DBGDEL.cpp)。如:
          int *a = new int a[10];
          delete a;
          delete [] a;
      
    • 针对类class,两种方式体现出具体差异
      • delete a: 仅释放了a指针指向的全部内存空间,但是只调用了a[0]对象的析构函数,剩下的从a[1]到a[9]这9个用户自行分配的m_buffer对应内存空间将不能释放,从而造成内存泄漏。
      • delete [] a: 调用使用类对象的析构函数释放用户自己分配内存空间并且释放了a指针指向的全部内存空间。
          class A{
              private:
                  char *m_buffer;
                  int m_len;
              public:
                  A(){
                      m_buffer = new char[m_len];
                  }      
                  ~A(){
                      delete [] m_buffer;
                  }
          };
          A *a = new A[10];
          delete a;
      
          delete [] a;
      
    • 总结:如果ptr代表一个用new申请的内存返回的内存空间地址,即所谓的指针,则:
      • delete ptr: 代表用来释放内存,且只用来释放ptr指向的内存
      • delete [] ptr: 用来释放ptr指向的内存,还逐一调用数组中每个对象的destructor!!
      • 对于像int/char/long/int*/struct等简单数据类型,由于对象没有destructor,所以用delete和delete []是一样的!但是如果是C++对象数组就不同了
  • new和malloc内部的实现方式上的区别
    • new的功能是在堆区新建一个对象,并返回该对象的指针。所谓的新建对象的意思就是,将调用该类的构造函数,因为如果不构造的话,就不能称之为一个对象;而malloc只是机械的分配一块内存,如果用mallco在堆区创建一个对象的话,是不会调用构造函数的。严格来说,用malloc不能算是新建了一个对象,只能说是分配了一块与该类对象匹配的内存而已,然后强行把它解释为这是一个对象,按这个逻辑来,也不存在构造函数什么事。
    • 用delete去释放一个堆区的对象,会调用该对象的析构函数;用free去释放一个堆区的对象,不会调用该对象的析构函数
          // new和malloc的区别
          class TEST{
              private:
                  int num1;
                  int num2;
              public:
                  TEST(){
                      num1 = 10;
                      num2 = 20;
                  }
                  void Print(){
                      cout << num1 << " " << num2 << endl;
                  }
          };
          
          /*
          用malloc()函数在堆区分配一块内存空间,然后对该块内存空间进行强制类型(TEST*)转换
          解释为是一个TEST类对象,这不会调用TEST的默认构造函数
          */
          TEST *t_obj1 = (TEST *)malloc(sizeof(TEST));
          t_obj1->Print();
          // 用new在堆区创建一个TEST类的对象,这会调用TEST类的默认构造函数
          TEST *t_obj2 = new TEST;
          t_obj2->Print();