C内存管理

  学过C/C++的都知道,内存管理是个令人头疼的问题,除了一些高手能够运用的很好之外,还有一些菜鸟,天天在为检查代码,找错误,但是这也是一个必不可少的步骤,在C语言无处不在,所以有一种语言Java诞生,它不用人管理内存,但是它没有C/C++运行速度快。今天就来看一看让我们C/C++程序猿恐惧的内存管理。

内存分配方式

1.简介

在C++里,有五个区,分别为堆、栈、自由存储区、全局/静态存储区和常量存储区。

  栈:在函数执行的时候,函数内部的变量的存储单元都会在栈上创建,在函数返回时会自动释放。栈内存分配运算内置于处理器的指令集中,效率高,但是内存容量有限。

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

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

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

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

2.堆与栈区分

很多人容易搞不清楚堆与栈的区别,这里举一个例子:

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

  这一个例子就包含了堆和栈,new很熟悉就是在堆上申请一块内存,指针p?就对应在栈里,其实就是在栈里有一个指针p,它指向了堆内的相应空间,在程序会先确定在堆内申请空间的大小,然后用operator new分配内存,然后返回其首地址存入栈中,其汇编代码(VS2017)如下:

0025ECFE  push        14h  
0025ED00  call        operator new[] (025B61Bh)  
0025ED05  add         esp,4  
0025ED08  mov         dword ptr [ebp-0D4h],eax  
0025ED0E  mov         eax,dword ptr [ebp-0D4h]  
0025ED14  mov         dword ptr [p],eax

  这里没有释放内存。那么怎么释放呢?delete?不,应该用delete []p,这是为了告诉编译器我们删除的是一个数组,编译器会根据相应的Cookie去释放内存。

3.堆与栈的具体区别

主要的区别由以下几点:

  (1). 管理方式不同

  (2). 空间大小不同

  (3). 能否产生碎片不同

  (4). 生长方向不同

  (5). 分配方式不同

  (6). 分配效率不同

  管理方式:栈是由编译器自动管理,堆是由程序员控制,容易产生memory leak。

  空间大小:一般在32位系统下堆内存可以达到4G,但是对于栈来讲仅仅只有几兆,具体在Linux下可通过命令 ulimit -s查看

  碎片问题:堆来讲,new/delete会让内存成一段一段的,空间不连续,效率降低,而栈的机制先进后出,在弹出之前,它前面的已经被弹出,决定了它不会有碎片。

  生长方向:堆是自底向上,向着内存增加的方向增长,栈是自顶向下,向着内存减小的地方增长。

  分配方式:堆只有动态分配,栈有静态分配和动态分配。

  分配效率:栈是由系统提供的数据结构,地址都储存在寄存器中,而堆是由C函数库提供支持的,实现很复杂,在堆申请空间时会经过函数库算法,寻找有没有足够大的空间来申请,如果没有,则会通过系统函数,重新向操作系统申请足够大的空间,显然栈的效率要比堆高得多。

这里可以看出,用new/delete会造成大量内存碎片,效率会变得很低,所以栈的应用很广泛,在函数调用的时候,函数的返回地址,ebp,参数等信息都是存储在栈里的,所以尽量多用栈,但是,堆有它的好处,相对灵活,对于分配大的空间还是堆好。两者都要防止越界现象的产生。

常见的内存错误及其对策

  关于指针的运用我相信很对人都会很谨慎,一个不小心就会导致出现错误,而且这种错误不易被发现,编译器也不会有提示,所以总结了一下几个方法对策:

  1.内存分配没有判断是否申请成功,没有检查是否为NULL,如果指针是函数参数则用assert(p!=NULL)进行检查,如果用new或malloc申请,用if(p!=NULL)来进行判断。

  2.申请成功,但未初始化。用了一个没有初始化的内存就会报错,这是一种陋习,时时刻刻记得初始化。

  3.初始化但越界了。访问了一个越界的不存在的内存会报错。

  4.忘释放内存,导致内存泄漏。比如,new/malloc和delete/free不匹配,导致每运行一次,丢失一块内存,总有一天会出错,提示内存耗尽。

  5.使用了已经释放过的内存。

  对策

  1.程序中的对象调用关系过于复杂,搞不清楚哪一个已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

  2.函数return错误,不能指向栈内存的指针或引用,因为在函数返回的时候栈内存会自动销毁。

  3.使用了free/delete后没有将其设置为NULL,产生了所谓的“野指针”。

下面给出几条避免“野指针”的规则:

  1、用new/malloc后,应该立即判断指针是否为空。

int* p = malloc(size of (int))
    if(p == NULL)
    {
      printf("malloc error!\n");
      exit(1);//跳出整个程序
    }

  2、防止数组或指针下标越界。

  3、将分配的内存空间初始化。因为如果不进行初始化可能会有之前的垃圾数据残留,影响程序。可以用memset(p,0,sizeof(int));memset会将p指向的前sizeof(int)空间都置为0。

  4、delete/free与new/malloc配对,防止内存泄漏。

  5、释放完后再将指针指向NULL,否则p会再次成为野指针。

#define NULL (void *)0   //NULL指向零地址,不允许对0地址对应的空间做操作。
malloc()函数,形参为要分配的字节大小,返回为这段空间的首地址。
eg : malloc(4)和malloc(sizeof(int))  后者提高了移植性。
memset(p,0,sizeof(int))  把p指向的空间全部初始化为0
free(p)释放。

指针和数组的对比

  指针和数组会让人产生错觉,数组名为地址,指针也是内存地址,有什么区别?
数组可以在静态区被创建,也可以在栈上被创建,地址只是代表数组一块内存的首地址,而指针是可以指向任何一块内存,运用比较灵活。

1.内容复制与比较

例子:

// 数组…
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); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)

  数组不能直接用数组名复制和比较而应该用strcpy()和strcmp()进行内容的比较,对于指针就要先用malloc()申请一个比其长度大1的空间,然后用strcpy和strcmp进行复制和比较。

2.计算内存容量

例子1:

char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12字节
cout<< sizeof(p) << endl; // 4字节

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

例子2:

void Func(char a[100]){
    cout<< sizeof(a) << endl; // 4字节而不是100字节
}

当数组作为参数时,sizeof(a)不是数组长度,而是数组其指针大小长度,相当于sizeof(a)=sizeof(char*)//4字节。

3.指针参数的传递

如果函数的参数是一个指针,不能用该指针去申请动态内存,输出仍然为NULL。

void GetMemory(char *p, int num){
    p = (char *)malloc(sizeof(char) * num);
}
void Test(void){
    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 GetMemory2(char **p, int num){
    *p = (char *)malloc(sizeof(char) * num);
}
void Test2(void){
    char *str = NULL;
    GetMemory2(&str, 100); // 注意参数是 &str,而不是str
    strcpy(str, "hello");
    cout<< str << endl;
    free(str);
}

由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,如下:

char *GetMemory3(int num){
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void){
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}

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

char *GetString(void){
    char p[] = "hello world";
    return p; // 编译器将提出警告
}
void Test4(void){
    char *str = NULL;
    str = GetString(); // str 的内容是垃圾
    cout<< str << endl;
}

  用调试器逐步跟踪Test4,发现执行str = GetString语句后str不再是NULL指针,但是str的内容不是“hello world”而是垃圾。

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

char *GetString2(void){
    char *p = "hello world";
    return p;
}
void Test5(void){
    char *str = NULL;
    str = GetString2();
    cout<< str << endl;
}

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

杜绝“野指针”

   野指针不是指向NULL,是指向垃圾内存的指针,野指针的成因:

  1.指针变量没有初始化。创建时不会自动指向NULL,若没有初始化他会乱指一气,合法指向:

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

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

  3.指针操作超越了变量的作用域范围。指针指向了一个已经消失了的对象,该指针就变成了野指针。

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

  malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  我们都知道对象在创建的同时要自动执行构造函数,对象在销毁之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看malloc/free和new/delete如何实现对象的动态内存管理,例子如下:

class Obj{
public :
  Obj(void){ cout << “Initialization” << endl; }//构造函数
  ~Obj(void){ cout << “Destroy” << endl; }//析构函数
  void Initialize(void){ cout << “Initialization” << endl; }//初始化
  void Destroy(void){ cout << “Destroy” << endl; }//销毁
};
void UseMallocFree(void){
    Obj *a = (obj *)malloc(sizeof(obj)); // malloc申请动态内存
    a->Initialize(); // 初始化
    //…
    a->Destroy(); // 清除工作
    free(a); // 释放内存
}
void UseNewDelete(void){
    Obj *a = new Obj; // new申请动态内存并且初始化
    //…
    delete a; // 清除并且释放内存
}

  上述有Initialize模拟构造函数、Destroy模拟析构函数,在malloc时不会执行构造和析构函数,必须调用成员函数Initialize、Destroy来实现。new则简单多了,那么为什么还要用malloc/free呢?因为C++程序经常调用C函数,而C程序必须要用malloc/free来动态管理内存。new/delete、malloc/free要搭配用,混用会导致程序出错。

内存耗尽的办法

  如果内存耗尽,申请时找不到足够大的内存块,则返回NULL指针,宣告内存申请失败,处理方法:

1.判断指针是否为空,为空则return终止本函数。例:

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

2.判断指针是否为空,为空则exit(1)终止整个程序。例:

void Func(void){
    A *a = new A;
    if(a == NULL){
        cout << “Memory Exhausted” << endl;
        exit(1);
    }
}

  3.为new和malloc设置异常处理函数。可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。

  1、2方法很普遍,但是在比较复杂的程序中,第一种方法就力不从心了,因该用第二种方法,return退出本函数,exit()退出整个程序,现在32位以上的系统几乎不可能导致“内存耗尽”,似乎不需要错误处理,这是不对的,不加错误处理的程序代码质量很差,很可能会因小失大。

malloc/free的使用要点

函数malloc的原型如下:

void * malloc(size_t size);

用malloc申请一块长度为length的整数类型的内存,程序如下:

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

我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。

  * malloc返回值的类型是void*,所以在调用malloc时要显式地进行类型转换,将void *转换成所需要的指针类型。

  * malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int, float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。最好用以下程序作一次测试:

cout << sizeof(char) << endl;//1
cout << sizeof(int) << endl;//4
cout << sizeof(unsigned int) << endl;/4
cout << sizeof(long) << endl;//4
cout << sizeof(unsigned long) << endl;/4
cout << sizeof(float) << endl;//4
cout << sizeof(double) << endl;//8
cout << sizeof(void *) << endl;//4

函数free的原型如下:

void free( void * memblock );

  为什么free函数不像malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。

new/delete的使用要点

  运算符new使用起来要比函数malloc简单得多,例如:

int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];

  这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如:

class Obj{
    public :
    Obj(void); // 无参数的构造函数
    Obj(int x); // 带一个参数的构造函数
    …
}
void Test(void){
    Obj *a = new Obj;
    Obj *b = new Obj(1); // 初值为1
    …
    delete a;
    delete b;
}

如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如:

Obj *objects = new Obj[100]; // 创建100个动态对象

不能写成:

Obj *objects = new Obj[100](1);// 创建100个动态对象的同时赋初值1

在用delete释放对象数组时,留意不要丢了符号‘[]’。例如:

delete []objects; // 正确的用法
delete objects; // 错误的用法

后一个很可能引起程序崩溃和内存泄漏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值