C++内存管理

本片文章参考了网上的资源,如有侵权,请与本人联系。 参考网站:C/C++内存管理

内存分配方式


   简介

        在C++中,内存分为5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

        :在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

        :就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统就会自动回收。

        自由存储区:由malloc分配的内存块,它和堆十分相似,不过是用free来释放。

        全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区别,他们共同占据同一块内存区域。

        常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不予许修改。

 

明确区分堆与栈

         堆与栈的区分问题,初学者往往分不清。下面举一个例子:         

void f(){ int* p = new int[5];}

这条短短的语句就包含了堆与栈,看到new,我们首先想到分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是说:在栈内存中存放了一个指向一块堆内存的指针p。在程序中会先确定在堆中分配内存大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中。

这里,我们为了简单并没有释放内存,那么该如何去释放呢?是delete p么?哦,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,编译器就会根据相应的cookie信息去进行内存释放的工作。

堆和栈究竟有什么区别?

           好了,回到我们的主题:堆和栈究竟有什么区别?

          主要区别有以下几点:

            (1) 管理方式不同:对于栈来讲,是由编译器自动管理,无需手动控制,对于堆来讲,释放工作由程序员控制,容易产生memory leak。

         (2)空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度看堆内存几乎是无限的,但是对于栈来讲,一般都是有一定的空间大小,例如,在VC6下面,默认的栈空间大小是1M。

        (3)能否产生碎片不同:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如何的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在某一内存块从栈中弹出时,位于它上面的肯定已经弹出了,因此不会出现内存碎片。

        (4)生长方向不同:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

           (5) 分配方式不同:堆都是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手动实现。

           (6)分配效率不同:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间,就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率要比栈低的多。

      

          到这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量使用栈,少使用堆。

          虽然栈的好处这么多,但是栈的内存比较小,因此在申请大量的内存空间的时候,还是要用堆好一些。

          无论是用堆还是栈,都要防止越界现象的发生,因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生难以想象的后果,就算在程序运行过程中没有崩掉,但还是要小心,因为不知什么时候程序就可能崩掉,那时候debug是相当困难的。

 

控制C++的内存分配


       在嵌入式系统中使用C++的一个常见错误是内存分配,即对new和delete操作符的失控。

       具有讽刺意味的是,问题的根源却是C++对内存的管理非常的容易和安全。具体的说,当一个对象被消除时,它的析构函数能够安全的释放所分配的内存。

      这当然是个好事情,但是这种使用的简单性质使得程序员们过度使用new和delete,而不注意嵌入式C++环境中的因果关系。并且,在嵌入式系统中,由于内存的限制,频繁的使用内存分配不定大小的内存会引起很大的问题以及堆破碎的风险。

      作为忠告,保守的使用内存分配是嵌入式环境中的第一原则。

      但当你必须要使用new和delete的时候,你不得不控制C++中的内存分配。你需要一个全局的new和delete来代替系统的内存分配符,并且一个类一个类的重载new和delete。一个防止堆破碎的通用办法是从不同大小的内存中分配不同类型的对象。对每个类重载new和delete就提供了这样的控制。

重载全局的delete和new操作符

void* operator new(size_t size){
     void *p = malloc(size);
     return p;
}

void operator delete(void* p){
     free(p);
}

这段代码可以代替默认的操作符来满足内存分配的请求。出于解释C++的目的,我们也可以直接调用malloc()和free().

也可以对单个类的new和delete操作符重载。这是你能灵活的控制对象的内存分配。

class TestClass{
      public:
       void* operator new(size_t size);
       void operator delete(void* p);
      //...other members here...
};

void *TestClass::operator new(size_t size){
      void* p = malloc(size);
      return (p);
}

void TestClass::operator delete(void *p){
      free(p);
}

所有TestClass对象的内存分配都采用这段代码。更进一步,任何从TestClass继承的类也都采用这一方式,除非它自己也重载了new和delete操作符。通过重载new和delete操作符的方法,你可以自由地采用不同的分配策略,从不同的内存池中分配不同的内存对象。

 

为单个类重载 new[]和delete[]

必须小心对象数组的分配。你可能希望调用被你重载的new和delete操作符,但并不如此,内存的请求会被定向全局的new []和delete[]操作符,而这些内存来自于系统堆。

C++将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。为了改变这中方式,你需要重载new []和delete []操作符。

class TestClass{
     public:
      void* operator new[] (size_t size);
      void* operator delete[](void* p);
      // ...other members here...
};

void *TestClass::operator new[](size_t size){
      void *p = malloc(size);
      return p;
}

void TestClass::operator delete[](void* p){
      free(p);
}

int main(){
     TestClass *p = new TestClass[10];
     //...etc...
     delete[] p;
}

但是注意:对于多数C++的实现,new []操作符中的个数是数组的大小加上额外的存储对象数目的一些字节。在你的内存机制中要重要考虑这一点。你应该尽量避免分配对象数组,从而使你的内存分配策略简单。

常见的内存错误及对策


发生内存错误是件麻烦的事,编译器不能自动发现这些错误,通常是程序运行时才能动态捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲的找你来,程序却没有任何问题,你一走,错误又发生了。常见的内存错误及对策如下:

  • 内存分配未成功,却使用了它。编程新手常犯这种错误,因为他们没有意识到内存分配不会成功。常用解决办法是,在使用内存前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果用malloc或new进行内存分配,应该用if(p==NULL)或if(p!=NULL)进行防错处理。
  • 内存分配虽然成功,但是尚未初始化就引用它。犯这个错误的原因主要有两个:一、没有初始化的概念;二、误以为内存的初始值全为0,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么并没有统一的标准,尽管有时候为0值,我们宁可信其有不可信其无。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋0值也不可忽略,不要嫌麻烦。
  • 内存分配成功并且已经初始化,但操作越过了内存的边界。例如在使用数组时经常发生下标多1或少1的操作。特别是在for循环中,循环次数很容易搞错,导致数组操作越界。
  • 忘记了释放内存,造成内存泄露。含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误,终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中malloc和free的使用次数一定要相同,否则肯定有错误。(new/delete同理)
  • 释放了内存却继续使用它。有三种情况:

       (1).程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱。

      (2). 函数的return语句写错了,注意不要返回指向“栈内存”的对象或“引用",因为该内存在函数体结束时就会自动销毁。

      (3).使用free或delete释放了内存后,没有将指针设置为NULL,导致产生”野指针“。

     那么如何避免”野指针“呢?这里列出了5条规则,平常写程序时多注意一下,养成良好的习惯。

    规则一、用malloc或new申请内存后,应该立即检查是否为NULL,防止使用指针值为NULL的内存。

   规则二、不要忘记为数组和动态内存赋予初值,防止将未被初始化的内存作为右值使用。

   规则三、 避免数组指针的下标越界,特别小心发生多1或少1的情况。

   规则四、动态内存的申请与释放必须配对,防止内存泄露。

   规则五、 用free或delete释放了内存后,立即将指针设置为NULL,防止产生”野指针“。

 

指针与数组的对比


C/C++程序中,指针和数组在不少地方都可以相互交替使用,让人产生一种错觉,以为两者是等价的。

数组要么在静态区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。

指针可以随时指向任意类型的内存块,它的特征是”可变“,所以我们可以常用指针来操作动态内存。指针远比数组灵活,但是也更加危险。

以下为字符串为例比较指针与数组的特性。

修改内容

                       下面示例中,字符数组a的容量是6个字符,其内容为”hello“。a的内容可以改变,如a[0]='x'。指针p指向常量字符串”world"(位于静态存储区,内容为world),常量字符串的内容是不可以被修改。从语法上讲,编译器并不觉得语句p[0]='x'有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。

char a[]="hello";
a[0] = 'x';
cout<

        内容复制与比较

            不能对数组名进行直接复制与比较。若想吧数组a的内容复制给数组b,不能用语句b=a,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a)来判断,应该用标准库函数strcmp进行比较。

           语句p=a并不能把a的内容复制给指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a)比较的不是内容而是地址,应该用库函数strcmp来比较。

//数组。。。
char a[] = "hello";
char b[10];
strcpy(b,a);  //不能用b=a
if(strcmp(b,a)==0) //不能用if(b==a)
...

//指针
int len = strlen(a);
char *p = (char*) malloc(sizeof(char)*(len+1));
strcpy(p,a);
if(strcmp(p,a)==0) //不能用if(p==a)
...

计算内存容量

           用运算符sizeof可以计算出数组的容量(字节数).如下示例中,sizeof(a)的值是12.指针p指向a,但是sizeof(p)的值却是4.这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char *),而不是p所指向的内存内容。C/C++语言没法知道指针所指向的内存容量,除非在申请内存时记住它。

char a[] = "hello world";
char *p = a;
cout<

           注意当数组作为参数进行传递时,该数组自动退化为同类型的指针。如下实例中,无论a数组的容量是多少,sizeof(a)始终是4

指针参数是如何传递内存的

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如下示例中,Test函数中的语句GetMemory(str,200)并没有获得期望的内存,str依旧是NULL,为什么?

void GetMemory(char *p,int num)
{
       p = (char*)malloc(sizeof(char)*num);
}

void Test(){
    char* str=NULL;
    GetMemory(str,100);    //str依旧是NULL
    strcpy(str,"hello");
}

毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p=p.函数体内的程序改变了_p的内容,就导致参数p指向的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指向的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。

如果非要用指针参数去申请内存,那么应该改用"指向指针的指针“,见示例:

void GetMemory(char**p,int num)
{
     *p = (char*) malloc(sizeof(char)*num);
}

void Test2(){
     char* str = NULL;
     GetMemory(&str,num);   //注意参数是&str,不是str
     strcpy(str,"hello");
     cout<

           由于“指向指针的指针”这个概念很不好理解,我们可以用函数值来传递动态内存。这种方法更加简单,见示例:

char * GetMemory3(int num)
{
      char *p = (char*)malloc(sizeof(char)*num);
      return p;
}

void Test3(){
    char* str = NULL;
    str = GetMemory3(100);
    strcpy(str,"hello");
    cout<

用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回“栈指针”的指针,因为该内存在函数结束时自动销毁,见示例:

char * GetString(){
    char p[] = "hello,world";
    return p;   //编译器将给出警告
}

void Test4(){
   char* str = NULL;
   str = GetString(); str的内容是垃圾
   cout<

如果把上述示例改成下面示例,会怎样?

char *GetString2(){
     char*p = "hello,world";
     return p;
}

void Test5(){
     char *str = NULL;
     str = GetString2();
     cout<

函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的“hello world”是常量字符串,位于静态存储区,他在程序生命期间恒定不变。无论什么时候调用GetString2(),它返回的始终是同一个”只读“内存块。

 

杜绝野指针


”野指针“不是NULL指针,是指向”垃圾“内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是”野指针“是很危险的,if语句对他不起作用。”野指针“的成因主要有”三种“:

(1)指针变量没有被初始化。任何指针变量刚被创建时会不会自动成为NULL指针的,它的缺省是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么是NULL,要么让它指向合法的指针。例如:

char *p = NULL;
char* str = (char*)malloc(100);

(2)指针p被free或delete之后,没有设置为NULL,让人误以为p是个合法的指针。

(3)指针操作超越了变量的作用域范围,这种情况让人防不胜防,示例程序如下:

class A{
    public:
    void Func(void){cout<<"Func of class A"<Func();    //p是野指针
}

函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p成了“野指针".

 

有了malloc/free后为什么还要new/delete?


malloc与free是C/C++语言的标准库,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器的控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意; new/delete不是库函数。我们看一看malloc/free和new/delete如何实现对象的动态内存管理,见示例:

class Obj{
   public:
    Obj(void){cout<<"Initialization"<Initialize();  //初始化
    //...
    a->Destroy();  //清除工作
    free(a);
}

void UseNewDelete(){
    Obj* a = new Obj; //申请动态内存并初始化
    //...
    delete a;
}

            类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数和析构函数,必须调用Initialize和Destroy来完成初始化工作与清除工作。函数UseNewDelete则简单的多。

        所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的”对象"没有构造和析构过程,对他们而言malloc/free和new/delete是等价的。

         既然new/delete的功能覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++经常调用C函数,而C函数只能用malloc/free管理动态内存。

         如果用free释放"new创建的动态对象“,那么该对象因无法执行析构函数而可能导致程序出错。如果delete释放malloc申请的内存,结果不会导致程序出错,但是程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

内存耗尽怎么办?


如果申请内存时没有找到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理”内存耗尽“问题。

(1)判断指针是否是NULL,如果是马上用return语句终止本函数。例如:

void Func(){
      A *a = new A;
      if(a==NULL)
         return;
...
}

(2) 判断指针是否为NULL,如果是马上用exit(1)终止整个程序的运行。

(3)为new和malloc设置异常处理函数

你可能感兴趣的:(c++)