系统C/C++内存管理之内存分配
Posted 黑黑白白君
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了系统C/C++内存管理之内存分配相关的知识,希望对你有一定的参考价值。
- 相关预备知识:关于C/C++的内存模型可参考《【系统】C/C++内存管理之内存模型》。
0)内存分配方式
内存分配方式有三种:
-
从静态存储区域分配:
内存在程序编译的时候就已经分配好了,这块内存在程序的整个运行期间都存在。- 例如全局变量,static静态成员变量。
-
在栈上创建:
执行函数时,函数内部变量的存储单位可以在栈上创建。- 函数执行结束时,这些存储单元自动释放。
- 栈内存分配运算置于处理器的指令集中,效率很高,但是分配的内存容量有限。
-
在堆上分配:
也称为动态内存分配。- 程序在运行的时候用malloc或new申请任意多少内存,程序员自己负责在何时用free或delete来释放这块内存。
- 动态内存的生命周期由程序员决定,使用非常灵活。
- 但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。也就是我们常说的内存碎片。
*程序内存空间
一个程序将操作系统分配给其运行的内存分为5个区域:
- 栈区(stack area):由编译器自动分配释放,存放为函数运行的局部变量,函数参数,返回数据,返回地址等。操作方式与数据结构中的类似。
- 堆区(heap area):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
- 全局数据区(data area):也叫做静态区,存放全局变量,静态数据。程序结束后由系统释放。
- 程序代码区(code area):存放函数体的二进制代码。但是代码段中也分为代码段和数据段。
- 文字常量区:可以理解为常量区,常量字符串存放这里。程序结束后由系统释放。字常量是不可寻址的。
1)C语言内存分配方式
在C语言中,对象可以使用静态或动态的方式分配内存空间。
- 静态分配:编译器在处理程序源代码时分配。
- 静态内存分配是在程序执行之前进行的因而效率比较高。
- 动态分配:程序在执行时调用malloc库函数申请分配。
- 动态内存分配则可以灵活地处理未知数。
*静态与动态内存分配区别
- 静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的一段地址,需要通过指针间接地对它进行操作。
- 静态对象的分配与释放由编译器自动处理;动态对象的分配与释放必须由程序员显式地管理,它通过malloc()和free两个函数来完成。
1.1 静态分配方式
int a = 100;
- 此行代码指示编译器分配足够的存储区以存放一个整型值,该存储区与名字a相关联,并用数值100初始化该存储区。
1.2 动态分配方式
动态分配内存的定义是这样的,指在程序运行过程中,要申请内存,系统会根据程序的实际情况来分配,分配空间的大小是由程序的需求来决定的。
p1 = (char *)malloc(10*sizeof(int));
-
此行代码分配了10个int类型的对象,然后返回对象在内存中的地址,接着这个地址被用来初始化指针对象p1。
-
对于动态分配的内存唯一的访问方式是通过指针间接地访问,其释放方法为:
free(p1);
在C语言下面,举个例子,定义一个指针,
int *p;
- 此时指针 i 是一个野指针,是一个指向不确定位置的指针,对它进行操作是很危险的,此时我们需要动态分配内存空间,让 i 指向它。
- 而有一种形式是这样的,
int *p=&b
,这并非是一种动态内存分配方式,而是一种指针的初始化,把变量b的首地址给了指针p。
C语言下供了几个函数来实现动态内存分配,分别是malloc()、calloc()、realloc(),而释放内存的函数为free()。
1、malloc函数
函数原型为void *malloc(unsigned int size);
- 在内存的动态存储区中分配一块长度为"size" 字节的连续区域。
- 函数的返回值为该区域的首地址。
- “类型说明符”表示把该区域用于何种数据类型。
- (类型说明符*)表示把返回值强制转换为该类型指针。
- “size”是一个无符号数。
例如:
pc=(char *) malloc (100);
表示:
- 分配100个字节的内存空间
- 并强制转换为字符数组类型
- 函数的返回值为指向该字符数组的指针
- 把该指针赋予指针变量pc
- 若size超出可用空间,则返回空指针值NULL。
2、calloc 函数
函数原型为void *calloc(unsigned int num, unsigned int size)
- 按所给数据个数和每个数据所占字节数开辟存储空间。
- 其中num为数据个数,size为每个数据所占字节数,故开辟的总字节数为num*size。
- 函数返回该存储区的起始地址。
- calloc函数与malloc 函数的区别仅在于一次可以分配n块区域。
例如:
ps=(struct stu*) calloc(2,sizeof (struct stu));
- 其中的sizeof(struct stu)是求stu的结构长度。
- 因此该语句的意思是:按stu的长度分配2块连续区域,强制转换为stu类型,并把其首地址赋予指针变量ps。
3、realloc函数
函数原型为void *realloc(void *ptr, unsigned int size)
- 重新定义所开辟内存空间的大小。
- 其中ptr所指的内存空间是用前述函数已开辟的,size为新的空间大小,其值可比原来大或小。
- 函数返回新存储区的起始地址(该地址可能与以前的地址不同)。
例如
p1=(float *)realloc(p1,16);
将原先开辟的8个字节调整为16个字节。
4、free函数
函数原型为void free(void *ptr)
- 将以前开辟的某内存空间释放。
- 其中ptr为存放待释放空间起始地址的指针变量,函数无返回值。
- 应注意:ptr所指向的空间必须是前述函数所开辟的。
- 注意:动态申请的内存空间要进行手动用free()函数释放。
- malloc、calloc、realloc都是在堆上分配的,堆上分配的空间必须由用户自己来管理。
例如
free((void *)p1);
将上例开辟的16个字节释放。
- 可简写为
free(p1);
,由系统自动进行类型转换。
2)C++语言动态内存分配
C++语言中用new和delete来动态申请和释放内存。
2.1 申请
new 是个操作符。
运算符new使用起来要比函数malloc简单得多。
- 这是因为new内置了sizeof、类型转换和类型安全检查功能。
- 对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。
-
申请单个对象:
int *p; p=new int; //或者 p=new int(value);
new内部的调用顺序:(初始化一个对象时):
new-->operator new-->malloc-->构造函数
-
动态申请数组:
int *p; p=new int [100]; //这样可以申请长度为100的数组,但是不能进行初始化。
new内部的调用顺序:(初始化若干个对象时):
new-->operator new[]-->operator new-->malloc-->构造函数
2.2 释放
int *p, *q;
p=new int;
q=new int[10];
delete p;
delete []q;
- delete单个对象时,调用顺序为:
delete-->析构函数-->operator delete-->free
- delete多个对象时,调用顺序为:
delete-->析构函数-->operator delete[]-->operator delete-->free
3)new/delete与malloc/free
联系:
- 它们都是动态管理内存的入口。
- new/delete的底层调用了malloc/free。
区别:
- malloc/free是C/C++标准库的函数,new/delete是C++操作符。
- new 建立的是一个对象,会调用构造函数,malloc分配的是一块内存。
- malloc/free只是动态分配内存空间/释放空间。
- 而new/delete除了分配空间,还会调用构造/析构函数进行初始化与清理(清理成员)。
- malloc/free需要手动计算类型大小且返回值为void*,new/delete可自己计算类型的大小对应类型的指针。
- malloc/free申请空间后得判空,new/delete则不需要。
- new直接跟类型,malloc跟字节数个数。
*有了malloc/free为什么还要new/delete?
malloc与free是C /C语言的标准库函数,new/delete是C 的运算符。它们都可用于申请动态内存和释放内存。
- 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。
- 对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
- 由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
4)常见的内存错误及其对策
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。
-
内存分配未成功,却使用了它:
常用解决办法:在使用内存之前检查指针是否为NULL。
- 如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。
- 如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
-
内存分配虽然成功,但是尚未初始化就引用它:
- 犯这种错误主要有两个起因:
- 一是没有初始化的观念;
- 二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
- 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。
- 所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
- 犯这种错误主要有两个起因:
-
内存分配成功并且已经初始化,但操作越过了内存的边界:
- 例如在使用数组时经常发生下标“多1”或者“少1”的操作。
- 特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
-
忘记了释放内存,造成内存泄露:
- 含有这种错误的函数每被调用一次就丢失一块内存。
- 刚开始时系统的内存充足,你看不到错误。
- 终有一次程序突然死掉,系统出现提示:内存耗尽。
- 解决方法:动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
- 含有这种错误的函数每被调用一次就丢失一块内存。
-
释放了内存却继续使用它:
-
有三种情况:
- 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存。
- 此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
- 此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
- 函数的return语句写错了。
- 注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
- 注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
- 使用free或delete释放了内存后,没有将指针设置为NULL,导致产生“野指针”。
- 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存。
-
解决方法:
- 【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
- 【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
- 【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
- 【规则4】动态内存的申请与释放必须配对,防止内存泄漏。
- 【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
-
5)指针与数组的对比
- 数组:数组是用于储存多个相同类型数据的集合。
- 指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。
1、赋值
- 同类型指针变量可以相互赋值。
- 数组不行,只能一个一个元素的赋值或拷贝
2、存储方式
-
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。
- 数组是根据数组的下标进行访问的,多维数组在内存中是按照一维数组存储的,只是在逻辑上是多维的。
- 数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。
- 数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
-
指针:指针很灵活,它可以指向任意类型的内存块。
- 指针的类型说明了它指向地址空间的内存。
- 由于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。
3、求sizeof
数组:
- 数组所占存储空间的内存:sizeof(数组名)
- 数组的大小:sizeof(数组名)/sizeof(数据类型)
指针:
- 在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4。
- 在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。
4、初始化
数组:
(1)char a[]={"Hello"};//按字符串初始化,大小为6
(2)char b[]={'H','e','l','l'};//按字符初始化(错误,输出时将会乱码,没有结束符)
(3)char c[]={'H','e','l','l','o','\\0'};//按字符初始化
指针:
//(1)指向对象的指针:(()里面的值是初始化值)
int *p=new int(0) ; delete p;
//(2)指向数组的指针:(n表示数组的大小,值不必再编译时确定,可以在运行时确定)
int *p=new int[n]; delete[] p;
//(3)指向类的指针:(若构造函数有参数,则new Class后面有参数,否则调用默认构造函数,delete调用析构函数)
Class *p=new Class; delete p;
//(4)指针的指针:(二级指针)
int **pp=new (int*)[1];
pp[0]=new int[6];
delete[] pp[0];
【部分内容参考自】
- 深入理解C语言内存管理:https://www.cnblogs.com/jack-hzm/p/11545026.html
- C+±内存管理:https://blog.csdn.net/skrskr66/article/details/92769994
- c++详解【new和delete】:https://blog.csdn.net/xxpresent/article/details/53024555
- C和C++ 语言动态内存分配:https://www.cnblogs.com/zhj202190/archive/2011/05/11/2043620.html
- 数组和指针的区别与联系(详细):https://blog.csdn.net/cherrydreamsover/article/details/81741459
- C 内存管理详解:https://blog.csdn.net/ysdaniel/article/details/6643689
以上是关于系统C/C++内存管理之内存分配的主要内容,如果未能解决你的问题,请参考以下文章