系统C/C++内存管理之内存分配

Posted 黑黑白白君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了系统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、初始化

数组:

1char a[]={"Hello"};//按字符串初始化,大小为62char b[]={'H','e','l','l'};//按字符初始化(错误,输出时将会乱码,没有结束符)3char 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++内存管理之内存分配的主要内容,如果未能解决你的问题,请参考以下文章

MySQL系列:innodb源代码分析之内存管理

好未来源码分析:Golang内存分配

Java内存回收机制

在 C/C++ 中实现实时最佳拟合内存分配算法

高性能go服务之高效内存分配

Android 性能优化之内存泄漏检测以及内存优化(上)