看完这篇你还能不懂C语言/C++内存管理?

C 语言内存管理指对系统内存的分配、建立、使用这一系列操做。在内存管理中,因为是操做系统内存,使用不当会形成毕竟麻烦的结果。本文将从系统内存的分配、建立出发,而且使用例子来举例说明内存管理不当会出现的状况及解决办法。ios

1、内存

在计算机中,每一个应用程序之间的内存是相互独立的,一般状况下应用程序 A 并不能访问应用程序 B,固然一些特殊技巧能够访问,但此文并不详细进行说明。例如在计算机中,一个视频播放程序与一个浏览器程序,它们的内存并不能访问,每一个程序所拥有的内存是分区进行管理的。c++

在计算机系统中,运行程序 A 将会在内存中开辟程序 A 的内存区域 1,运行程序 B 将会在内存中开辟程序 B 的内存区域 2,内存区域 1 与内存区域 2 之间逻辑分隔。程序员

1.1 内存四区

在程序 A 开辟的内存区域 1 会被分为几个区域,这就是内存四区,内存四区分为栈区、堆区、数据区与代码区。web

栈区指的是存储一些临时变量的区域,临时变量包括了局部变量、返回值、参数、返回地址等,当这些变量超出了当前做用域时将会自动弹出。该栈的最大存储是有大小的,该值固定,超过该大小将会形成栈溢出。编程

堆区指的是一个比较大的内存空间,主要用于对动态内存的分配;在程序开发中通常是开发人员进行分配与释放,若在程序结束时都未释放,系统将会自动进行回收。数组

数据区指的是主要存放全局变量、常量和静态变量的区域,数据区又能够进行划分,分为全局区与静态区。全局变量与静态变量将会存放至该区域。浏览器

代码区就比较好理解了,主要是存储可执行代码,该区域的属性是只读的。安全

1.2 使用代码证明内存四区的底层结构

因为栈区与堆区的底层结构比较直观的表现,在此使用代码只演示这两个概念。首先查看代码观察栈区的内存地址分配状况:微信

#include<stdio.h>
int main()
{
 int a = 0;
 int b = 0;
 char c='0';
 printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);

}

运行结果为:编辑器

咱们能够观察到变量 a 的地址是 2293324 变量 b 的地址是 2293320,因为 int 的数据大小为 4 因此二者之间间隔为 4;再查看变量 c,咱们发现变量 c 的地址为 2293319,与变量 b 的地址 2293324 间隔 1,由于 c 的数据类型为 char,类型大小为 1。在此咱们观察发现,明明我建立变量的时候顺序是 a 到 b 再到 c,为何它们之间的地址不是增长而是减小呢?那是由于栈区的一种数据存储结构为先进后出,如图:

首先栈的顶部为地址的“最小”索引,随后往下依次增大,可是因为堆栈的特殊存储结构,咱们将变量 a 先进行存储,那么它的一个索引地址将会是最大的,随后依次减小;第二次存储的值是 b,该值的地址索引比 a 小,因为 int 的数据大小为 4,因此在 a 地址为 2293324 的基础上往上减小 4 为 2293320,在存储 c 的时候为 char,大小为 1,则地址为 2293319。因为 a、b、c 三个变量同属于一个栈内,因此它们地址的索引是连续性的,那若是我建立一个静态变量将会如何?在以上内容中说明了静态变量存储在静态区内,咱们如今就来证明一下:

#include<stdio.h>
int main()
{
 
 int a = 0;
 int b = 0;
 char c='0';
 static int d = 0;
 
 printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);
 
 printf("静态变量d的地址是:%d\n", &d);

}

运行结果以下:

以上代码中建立了一个变量 d,变量 d 为静态变量,运行代码后从结果上得知,静态变量 d 的地址与通常变量 a、b、c 的地址并不存在连续,他们两个的内存地址是分开的。那接下来在此建一个全局变量,经过上述内容得知,全局变量与静态变量都应该存储在静态区,代码以下:

#include<stdio.h>
int e = 0;
int main()
{
 
 int a = 0;
 int b = 0;
 char c='0';
 static int d = 0;
 
 printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);
 
 printf("静态变量d的地址是:%d\n", &d);
 printf("全局变量e的地址是:%d\n", &e);

}

运行结果以下:

从以上运行结果中证明了上述内容的真实性,而且也获得了一个知识点,栈区、数据区都是使用栈结构对数据进行存储。

在以上内容中还说明了一点栈的特性,就是容量具备固定大小,超过最大容量将会形成栈溢出。查看以下代码:

#include<stdio.h>

int main()
{
 char arr_char[1024*1000000];
    arr_char[0] = '0';
}

以上代码定义了一个字符数组 arr_char,而且设置了大小为 1024*1000000,设置该数据是方便查看大小;随后在数组头部进行赋值。运行结果以下:

这是程序运行出错,缘由是形成了栈的溢出。在日常开发中若须要大容量的内存,须要使用堆。

堆并无栈同样的结构,也没有栈同样的先进后出。须要人为的对内存进行分配使用。代码以下:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int main()
{
 char *p1 = (char *)malloc(1024*1000000);
 strcpy(p1, "这里是堆区");
 printf("%s\n", p1);
}

以上代码中使用了strcpy 往手动开辟的内存空间 p1 中传数据“这里是堆区”,手动开辟空间使用 malloc,传入申请开辟的空间大小 1024*1000000,在栈中那么大的空间一定会形成栈溢出,而堆自己就是大容量,则不会出现该状况。随后输出开辟的内存中内容,运行结果以下:

在此要注意p1是表示开辟的内存空间地址。

2、malloc 和 free

在 C 语言(不是 C++)中,malloc 和 free 是系统提供的函数,成对使用,用于从堆中分配和释放内存。malloc 的全称是 memory allocation 译为“动态内存分配”。

2.1 malloc 和 free 的使用

在开辟堆空间时咱们使用的函数为 malloc,malloc 在 C 语言中是用于申请内存空间,malloc 函数的原型以下:

void *malloc(size_t size);

在 malloc 函数中,size 是表示须要申请的内存空间大小,申请成功将会返回该内存空间的地址;申请失败则会返回 NULL,而且申请成功也不会自动进行初始化。

细心的同窗可能会发现,该函数的返回值说明为 void *,在这里 void * 并不指代某一种特定的类型,而是说明该类型不肯定,经过接收的指针变量从而进行类型的转换。在分配内存时须要注意,即时在程序关闭时系统会自动回收该手动申请的内存 ,但也要进行手动的释放,保证内存可以在不须要时返回至堆空间,使内存可以合理的分配使用。

释放空间使用 free 函数,函数原型以下:

void free(void *ptr);

free 函数的返回值为 void,没有返回值,接收的参数为使用 malloc 分配的内存空间指针。一个完整的堆内存申请与释放的例子以下:

#include<stdio.h>
#include<string.h>
#include <malloc.h>

int main() {
    int n, *p, i;
    printf("请输入一个任意长度的数字来分配空间:");
    scanf("%d", &n);
    
    p = (int *)malloc(n * sizeof(int));
 if(p==NULL){
  printf("申请失败\n");
  return 0;
 }else{
  printf("申请成功\n");
 } 
 
 memset(p, 0, n * sizeof(int));//填充0 
 
 //查看 
    for (i = 0; i < n; i++)
        printf("%d ", p[i]);
    printf("\n");

    free(p);
    p = NULL;
    return 0;
}

以上代码中使用了 malloc 建立了一个由用户输入建立指定大小的内存,判断了内存地址是否建立成功,且使用了 memset 函数对该内存空间进行了填充值,随后使用 for 循环进行了查看。最后使用了 free 释放了内存,而且将 p 赋值 NULL,这点须要主要,不能使指针指向未知的地址,要置于 NULL;不然在以后的开发者会误觉得是个正常的指针,就有可能再经过指针去访问一些操做,可是在这时该指针已经无用,指向的内存也不知此时被如何使用,这时若出现意外将会形成没法预估的后果,甚至致使系统崩溃,在 malloc 的使用中更须要须要。

2.2 内存泄漏与安全使用实例与讲解

内存泄漏是指在动态分配的内存中,并无释放内存或者一些缘由形成了内存没法释放,轻度则形成系统的内存资源浪费,严重的致使整个系统崩溃等状况的发生。

内存泄漏一般比较隐蔽,且少许的内存泄漏发生不必定会发生没法承受的后果,但因为该错误的积累将会形成总体系统的性能降低或系统崩溃。特别是在较为大型的系统中,如何有效的防止内存泄漏等问题的出现变得尤其重要。例如一些长时间的程序,若在运行之初有少许的内存泄漏的问题产生可能并未呈现,但随着运行时间的增加、系统业务处理的增长将会累积出现内存泄漏这种状况;这时极大的会形成不可预知的后果,如整个系统的崩溃,形成的损失将会难以承受。由此防止内存泄漏对于底层开发人员来讲尤其重要。

C 程序员在开发过程当中,不可避免的面对内存操做的问题,特别是频繁的申请动态内存时会及其容易形成内存泄漏事故的发生。如申请了一块内存空间后,未初始化便读其中的内容、间接申请动态内存但并无进行释放、释放完一块动态申请的内存后继续引用该内存内容;如上所述这种问题都是出现内存泄漏的缘由,每每这些缘由因为过于隐蔽在测试时不必定会彻底清楚,将会致使在项目上线后的长时间运行下,致使灾难性的后果发生。

以下是一个在子函数中进行了内存空间的申请,可是并未对其进行释放:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
void m() 
 char *p1; 
 p1 = malloc(100); 
 printf("开始对内存进行泄漏...");
}
 
int main() {
    m();
    return 0;
}

如上代码中,使用 malloc 申请了 100 个单位的内存空间后,并无进行释放。假设该 m 函数在当前系统中调用频繁,那将会每次使用都将会形成 100 个单位的内存空间不会释放,长此以往就会形成严重的后果。理应在 p1 使用完毕后添加 free 进行释放:

free(p1);

如下示范一个读取文件时不规范的操做:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *filename) 
 FILE* f;
 int key; 
 f = fopen(filename, "r"); 
 fscanf(f, "%d", &key); 
 return key; 
}
 
int main() {
    m("number.txt");
    return 0;
}

以上文件在读取时并无进行 fclose,这时将会产生多余的内存,可能一次还好,屡次会增长成倍的内存,可使用循环进行调用,以后在任务管理器中可查看该程序运行时所占的内存大小,代码为:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *filename) 
 FILE* f;
 int key; 
 f = fopen(filename, "r"); 
 fscanf(f, "%d", &key); 
 return key; 
}
 
int main() {
 int i;
 for(i=0;i<500;i++) {
     m("number.txt");
 }
    return 0;
}

可查看添加循环后的程序与添加循环前的程序作内存占用的对比,就能够发现二者之间添加了循环的代码将会成本增长占用容量。

未被初始化的指针也会有可能形成内存泄漏的状况,由于指针未初始化所指向不可控,如:

int *p;
*p = val;

包括错误的释放内存空间:

pp=p;
free(p); 
free(pp);

释放后使用,产生悬空指针。在申请了动态内存后,使用指针指向了该内存,使用完毕后咱们经过 free 函数释放了申请的内存,该内存将会容许其它程序进行申请;可是咱们使用事后的动态内存指针依旧指向着该地址,假设其它程序下一秒申请了该区域内的内存地址,而且进行了操做。当我依旧使用已 free 释放后的指针进行下一步的操做时,或者所进行了一个计算,那么将会形成的结果天差地别,或者是其它灾难性后果。因此对于这些指针在生存期结束以后也要置为 null。查看一个示例,因为 free 释放后依旧使用该指针,形成的计算结果天差地别:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *freep) 
 int val=freep[0];
 printf("2*freep=:%d\n",val*2);
 free(freep);
 val=freep[0];
 printf("2*freep=:%d\n",val*2);
}
 
int main() {
 int *freep = (int *) malloc(sizeof (int));
 freep[0]=1;
 m(freep);
    return 0;
    
}

以上代码使用 malloc 申请了一个内存后,传值为 1;在函数中首先使用 val 值接收 freep 的值,将 val 乘 2,以后释放 free,从新赋值给 val,最后使用 val 再次乘 2,此时形成的结果出现了极大的改变,并且最恐怖的是该错误很难发现,隐蔽性很强,可是形成的后顾难以承受。运行结果以下:

3、 new 和 delete

C++ 中使用 new 和 delete 从堆中分配和释放内存,new 和 delete 是运算符,不是函数,二者成对使用(后面说明为何成对使用)。

new/delete 除了分配内存和释放内存(与 malloc/free),还作更多的事情,全部在 C++ 中再也不使用 malloc/free 而使用 new/delete。

3.1 new 和 delete 使用

new 通常使用格式以下:

  • 指针变量名 = new 类型标识符;
  • 指针变量名 = new 类型标识符(初始值);
  • 指针变量名 = new 类型标识符[内存单元个数];

在C++中new的三种用法包括:plain new, nothrow new 和 placement new。

plain new 就是咱们最常使用的 new 的方式,在 C++ 中的定义以下:

void* operator new(std::size_t) throw(std::bad_alloc);  
void operator delete( void *) throw();

plain new 在分配失败的状况下,抛出异常 std::bad_alloc 而不是返回 NULL,所以经过判断返回值是否为 NULL 是徒劳的。

char *getMemory(unsigned long size)   
{    
    char * p = new char[size];   
    return p; 
}   
void main(void)   
{
    try{   
        char * p = getMemory(1000000);    // 可能发生异常
        // ...   
        delete [] p;   
    }   
    catch(const std::bad_alloc &amp; ex)   
    {
        cout &lt;&lt; ex.what();
    }   
}

nothrow new 是不抛出异常的运算符new的形式。nothrow new在失败时,返回NULL。定义以下:

void * operator new(std::size_t, const std::nothrow_t&) throw();
void operator delete(void*) throw();
void func(unsinged long length)   
{
    unsinged char * p = new(nothrow) unsinged char[length];   
    // 在使用这种new时要加(nothrow) ,表示不使用异常处理 。
 
    if (p == NULL)  // 不抛异常,必定要检查
        cout << "allocte failed !";   
        // ...   
    delete [] p;
}

placement new 意即“放置”,这种new容许在一块已经分配成功的内存上从新构造对象或对象数组。placement new不用担忧内存分配失败,由于它根本不分配内存,它作的惟一一件事情就是调用对象的构造函数。定义以下:

void* operator new(size_t, void*);
void operator delete(void*, void*);

palcement new 的主要用途就是反复使用一块较大的动态分配的内存来构造不一样类型的对象或者他们的数组。placement new构造起来的对象或其数组,要显示的调用他们的析构函数来销毁,千万不要使用delete。

void main()   
{  
    using namespace std;   
    char * p = new(nothrow) char [4];   
    if (p == NULL)   
    {
        cout << "allocte failed" << endl;  
        exit( -1 );
    }   
    // ...   
    long * q = new (p) long(1000);   
    delete []p;    // 只释放 p,不要用q释放。
}

p 和 q 仅仅是首址相同,所构建的对象能够类型不一样。所“放置”的空间应小于原空间,以防不测。当”放置new”超过了申请的范围,Debug 版下会崩溃,但 Release 能运行而不会出现崩溃!

该运算符的做用是:只要第一次分配成功,再也不担忧分配失败。

void main()   
{
    using namespace std;   
    char * p = new(nothrow) char [100];   
    if (p == NULL)   
    {  
        cout << "allocte failed" << endl;
        exit(-1);
    }   
    long * q1 = new (p) long(100);   
    // 使用q1  ...   
    int * q2 = new (p) int[100/sizeof(int)];   
    // 使用q2 ...   
    ADT * q3 = new (p) ADT[100/sizeof(ADT)];   
    // 使用q3  而后释放对象 ...   
    delete [] p;    // 只释放空间,再也不析构对象。
}

注意:使用该运算符构造的对象或数组,必定要显式调用析构函数,不可用 delete 代替析构,由于 placement new 的对象的大小再也不与原空间相同。

void main()   
{  
    using namespace std;   
    char * p = new(nothrow) char [sizeof(ADT)+2];   
    if (p == NULL)   
    {  
        cout << "allocte failed" &lt;&lt; endl;
        exit(-1); 
    } 
    // ... 
    ADT * q = new (p) ADT; 
    // ... 
    // delete q; // 错误
    q->ADT::~ADT();  // 显式调用析构函数,仅释放对象
    delete [] p;     // 最后,再用原指针来释放内存
}

placement new 的主要用途就是能够反复使用一块已申请成功的内存空间。这样能够避免申请失败的徒劳,又能够避免使用后的释放。

特别要注意的是对于 placement new 毫不能够调用的 delete, 由于该 new 只是使用别人替它申请的地方。释放内存是 nothrow new 的事,即要使用原来的指针释放内存。free/delete 不要重复调用,被系统当即回收后再利用,再一次 free/delete 极可能把不是本身的内存释放掉,致使异常甚至崩溃。

上面提到 new/delete 比 malloc/free 多作了一些事情,new 相对于 malloc 会额外的作一些初始化工做,delete 相对于 free 多作一些清理工做。

class A
{
 public:
     A()
     {
        cont<<"A()构造函数被调用"<<endl;
     }
     ~A()
     {
        cont<<"~A()构造函数被调用"<<endl;
     }
}

在 main 主函数中,加入以下代码:

A* pa = new A();  //类 A 的构造函数被调用
delete pa;        //类 A 的析构函数被调用

能够看出:使用 new 生成一个类对象时系统会调用该类的构造函数,使用 delete 删除一个类对象时,系统会调用该类的析构函数。能够调用构造函数/析构函数就意味着 new 和 delete 具有针对堆所分配的内存进行初始化和释放的能力,而 malloc 和 free 不具有。

2.2 delete 与 delete[] 的区别

c++ 中对 new 申请的内存的释放方式有 delete 和 delete[] 两种方式,到底这二者有什么区别呢?

咱们一般从教科书上看到这样的说明:

  • delete 释放 new 分配的单个对象指针指向的内存
  • delete[] 释放 new 分配的对象数组指针指向的内存 那么,按照教科书的理解,咱们看下下面的代码:
int *a = new int[10];
delete a;        //方式1
delete[] a;     //方式2
  1. 针对简单类型 使用 new 分配后的无论是数组仍是非数组形式内存空间用两种方式都可 如:
int *a = new int[10];
delete a;
delete[] a;

此种状况中的释放效果相同,缘由在于:分配简单类型内存时,内存大小已经肯定,系统能够记忆而且进行管理,在析构时,系统并不会调用析构函数。

它直接经过指针能够获取实际分配的内存空间,哪怕是一个数组内存空间(在分配过程当中 系统会记录分配内存的大小等信息,此信息保存在结构体 _CrtMemBlockHeader 中,具体状况可参看 VC 安装目录下 CRTSRCDBGDEL.cpp)。

  1. 针对类 Class,两种方式体现出具体差别

当你经过下列方式分配一个类对象数组:

class A
   {
    private:
      char *m_cBuffer;
      int m_nLen;

   `` public:
      A(){ m_cBuffer = new char[m_nLen]; }
      ~A() { delete [] m_cBuffer; }
   };

   A *a = new A[10];
   delete a;         //仅释放了a指针指向的所有内存空间 可是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而形成内存泄漏
   delete[] a;      //调用使用类对象的析构函数释放用户本身分配内存空间而且   释放了a指针指向的所有内存空间

因此总结下就是,若是 ptr 表明一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:

delete ptr  表明用来释放内存,且只用来释放 ptr 指向的内存。delete[] rg   用来释放rg指向的内存,!!还逐一调用数组中每一个对象的destructor!!

对于像 int/char/long/int*/struct 等等简单数据类型,因为对象没有 destructor ,因此用 delete 和 delete []是同样的!可是若是是 C++ 对象数组就不一样了!

关于 new[] 和 delete[],其中又分为两种状况:

  • (1) 为基本数据类型分配和回收空间;
  • (2) 为自定义类型分配和回收空间;

对于 (1),上面提供的程序已经证实了 delete[] 和 delete 是等同的。可是对于 (2),状况就发生了变化。

咱们来看下面的例子,经过例子的学习了解 C++ 中的 delete 和 delete[] 的使用方法

#include <iostream>
using namespace std;

class Babe
{
public:
    Babe()
    {
        cout << \"Create a Babe to talk with me\" << endl;
    }

    ~Babe()
    {
        cout << \"Babe don\'t Go away,listen to me\" << endl;
    }
};

int main()
{
    Babe* pbabe = new Babe[3];
    delete pbabe;
    pbabe = new Babe[3];
    delete[] pbabe;
    return 0;
}

结果是:

Create a babe to talk with me
Create a babe to talk with me
Create a babe to talk with me
Babe don\'t go away,listen to me
Create a babe to talk with me
Create a babe to talk with me
Create a babe to talk with me
Babe don\'
t go away,listen to me
Babe don\'t go away,listen to me
Babe don\'
t go away,listen to me

你们都看到了,只使用 delete 的时候只出现一个 Babe don’t go away,listen to me,而使用 delete[] 的时候出现 3 个 Babe don’t go away,listen to me。不过无论使用 delete 仍是 delete[] 那三个对象的在内存中都被删除,既存储位置都标记为可写,可是使用 delete 的时候只调用了 pbabe[0] 的析构函数,而使用了 delete[] 则调用了 3 个 Babe 对象的析构函数。

你必定会问,反正无论怎样都是把存储空间释放了,有什么区别。

答:关键在于调用析构函数上。此程序的类没有使用操做系统的系统资源(好比:Socket、File、Thread等),因此不会形成明显恶果。若是你的类使用了操做系统资源,单纯把类的对象从内存中删除是不稳当的,由于没有调用对象的析构函数会致使系统资源不被释放,这些资源的释放必须依靠这些类的析构函数。因此,在用这些类生成对象数组的时候,用 delete[] 来释放它们才是王道。而用 delete 来释放也许不会出问题,也许后果很严重,具体要看类的代码了。

最后祝各位保持良好的代码编写规范下降严重错误的产生。

本文分享自微信公众号 - C语言与CPP编程(cwdushu)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索