C++的内存管理

290 阅读10分钟

内存管理

我们常常编写内存管理程序,但是经常会出现错误,唯一的办法是提前知晓内存管理的性质。本文就是说一说内存管理的各个性质

cpp内存管理的详解

在CPP中,内存分为五个区,栈区,堆区,常量区,全局/静态存储区,自由存储区 栈:在函数内局部变量的存储单元都可以在栈上进行创建,在函数结束时进行释放存储单元,栈内存分配运算内置于处理器的指令集内,效率很高,但是栈区比较小,分配的内存有限。 堆:就是那些由new分配的内存块,堆区的内存是由程序来自主释放的,一个new对应一个delete,如果程序员没人释放,在程序结束后,操作系统会自动进行回收。 自由存储区:就是malloc等函数分配的内存块,和堆有些相似,但是它是由free来结束的。 全局/静态存储区:全局变量和静态变量分配到一块内存里 常量存储区:这里边存储的是不允许修改的常量

区分堆和栈

void f() { int* p=new int[5]; } 这是一个简短的例子,包含了堆和栈,看到new,也就想到了堆内存,但是指针p是一块栈内存:这个例子的意思是,在栈内存中存放了一块堆内存的指针p,程序会先确定堆中的分配内存大小,再调用operator new来分配内存,返回这块内存的首地址。释放时用delete []p,目的是声明我删除的是一个数组。

区别

1.管理方式不一样:对于栈来说,编译器自动管理,对于堆来说,释放工作由程序员控制,容易产生memory leak

2.空间大小:32位系统,堆可以接近4G的空间,但是栈一般都是有空间限制的,例如VC下,默认的栈空间是1MB

3.能否产生碎片,栈里面先进后出,地址从大到小,一一对应,永远都不可能有一个内存块从栈空间弹出,对于堆,频繁的使用new/delete会造成空间上的不连续,从而造成大量碎片,十程序效率降低。

4.生长方向:对于堆来说,生长方向是向上的,就是向着内存地址增加的方向,对于栈来说,生长方向向下,向内存地址减小的方向增长

5.分配方式:堆是动态分配,栈的动态分配是编译器进行释放的,无需手工实现,静态分配是alloca函数进行分配

6.分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器,效率很高。但是堆的函数是函数库提供的,机制十分复杂,例如分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间,就有可能调用系统功能来增加数据段的内存空间,先让你堆的效率比栈要低得多。

我们更加推荐使用栈,但是它并没有堆这么灵活。

控制c++的内存分配

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

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

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

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

但当你必须要使用new 和delete时,你不得不控制C++中的内存分配。你需要用一个全局的new 和delete来代替系统的内存分配符,并且一个类一个类的重载new 和delete。

一个防止堆破碎的通用方法是从不同固定大小的内存持中分配不同类型的对象。对每个类重载new 和delete就提供了这样的控制。

常见的内存错误以及对策

1.内存分配未成功,却使用了它。 解决办法是使用内存之前检查指针是否为NULL,如果指针p是函数的参数,那么在函数的入口用assert(p!=NULL)检查,如果用malloc和new来申请内存应该用if(p==NULL)进行防错处理。

2.内存分配成功而且初始化,但是操作越过了内存的边界。

比如循环里面多一的操作中,导致数组操作越界

3.内存分配成功,但是还没有初始化就引用它

这种错误主要有两个起因:第一是没有初始化的概念;第二是误以为内存的缺省初值权威0,导致引用初值错误。

4.忘记释放内存导致内存泄漏

动态内存的申请和释放必须配对,malloc和free次数要一样,new/delete一样 5.释放了内存但是继续使用它 有三种情况:

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

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

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

指针和数组的对比

C++/C程序中,指针和数组不少地方可以互相替换着用,但是两者并不等价 数组要么在静态存储区内被创建,要么在栈上被创建,而数组名对应的是一块内存,地址和容量在生命期内是不变的,只有数组的内容可以改变。

指针可以随时指向不同数据类型的内存块,我们常常用指针来操作动态内存,指针比数组灵活也更危险。

计算内存容量

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

char a[] = "hello world";

char *p = a;

cout<< sizeof(a) << endl; // 12字节

cout<< sizeof(p) << endl; // 4字节

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

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

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

如果函数的参数是一个指针,那么不可以用该指针来申请内胎内存,下面的示例中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是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"); // 运行错误}

\

这里的函数Get Memory中,编译器要为这个函数指针制作一个副本,指针参数的副本是_p,编译器让_p=p,,如果函数体的程序修改了_p的内容,那么就导致参数p的内容作相应的修改,这就是指针可以用作输出参数的原因,但是在本例中_p申请了新的内存,只是把_p指向的内存地址进行改变了,但是p丝毫没变,所以函数不能输出任何东西,事实上,每执行一次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);

returnp;

}

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";

returnp; // 编译器将提出警告

}

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"; returnp;}void Test5(void){ char *str = NULL; str = GetString2(); cout<< str << endl;}

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

malloc/calloc/realloc的区别?

malloc是在堆区开辟一段某个数据类型的内存,realloc是在一段已经知道的内存区间里面再添上一段内存,也就是增长一块空间,calloc就是再malloc的基础上把每一段数据空间初始化为0

operator new与operator delete函数

operator new 其实也是通过malloc来申请空间,如果申请成功就直接返回,如果不成功就抛异常 operator delete 函数其实是通过free来释放空间的

new和delete的实现原理

内置类型

如果申请的是内置类型的空间,new和malloc类似,delete和free类似,但是delete和delete【】一个是释放单个元素的空间,一个是释放连续一段空间,new和new【】类似

-原理:new:先调用operator new来申请空间,在申请空间上执行构造函数,完成对象的构造。 delete:先调用析构函数,完成对象中的资源清理工作,再调用operator delete 函数释放对象的空间

new T【n】原理 调用operator new【】来再调用operator new完成n个对象空间的申请,在申请空间上执行n次构造函数 delete T【】和上面的相反,不再赘述

malloc 和new的区别

  • malloc是函数,new是操作符
  • malloc单纯开辟空间,new还要调用构造函数
  • malloc不可以初始化,new可以初始化
  • malloc调用失败返回NULL,因此使用是必须判空,new不需要,但是new要捕获异常
  • malloc要计算空间大小,还要强制转换类型,new后面直接跟上类型,直接写上多少个空间

写的累死我了,还不点个赞