C语言内存管理讲解

谨记

人生有两条路,一天需要用心走,叫做梦想;一条需要用脚走,叫做现实。心走的太快,会迷路的;脚走的太快,会摔倒的;心走的太慢,现实会苍白;脚走的太慢,梦不会高飞。人生的精彩,是心走得好,脚步刚好能跟上。掌控好你的心,让它走正;加快你的步伐,让所有梦想生出美丽的翅膀。

前言

今天为大家带来的是C语言里面的内存管理的知识点,这篇文章过后,我们C语言的大体基本知识就已经介绍完了,那么下一篇文章开始,我讲讲解OC语法,也就是苹果公司推出的Objective-C语言,这是苹果应用开发的语言,也欢迎大家阅读,本篇文章是对C语言内存管理的一个讲解,内存的使用是程序设计中需要考虑的重要因素之一,这不仅由于系统内存是有限的(尤其在嵌入式系统中),而且内存分配也会直接影响到程序的效率。因此,读者要对C语言中的内存管理,有个系统的了解。

内存管理

在C语言中,定义了4个内存区间:代码区;全局变量与静态变量区;局部变量区即栈区;动态存储区,即堆区。下面分别对这4个区进行介绍。
① 代码区。代码区中主要存放程序中的代码,属性是只读的。
② 全局变量与静态变量区。也称为静态存储区域。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如:全局变量、静态变量和字符串常量。分配在这个区域中的变量,当程序结束时,才释放内存。因此,经常利用这样的变量,在函数间传递信息。
③ 栈区。在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创
建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。在linux系统中,通过命令“ulimit –s”,可以看到,栈的容量为8192kbytes,即8M。
这种内存方式,变量内存的分配和释放都自动进行,程序员不需要考虑内存管理的问题,很方便使用。但缺点是,栈的容量有限制,且当相应的范围结束时,局部变量就不能在使用。
④ 堆区。有些操作对象只有在程序运行时才能确定,这样编译器在编译时就无法为他们预先分配空间,只能在程序运行时分配,所以称为动态分配。
比如:下面的结构体定义:

struct employee
{
char name[8];
int age;
char gender;
float salary;
};

在该结构体定义中,员工的姓名是用字符数组来存储。若员工的姓名由用户输入,则只有在用户输入结束后,才能精确的知道,需要多少内存,在这种情况下,使用动态内存分配更合乎逻辑,应该把结构体的定义改成下面的形式:

struct employee
{
char *name;
int age;
char gender;
float salary;
};

动态分配内存就是在堆区上分配。程序在运行的时候用malloc申请任意多少的内存,程序员自己负责在何时用free释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
下面的这段程序说明了不同类型的内存分配。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*C语言中数据的内存分配*/
int a = 0;
char *p1;
int main()
{
    int b;                  /* b在栈 */
    char s[] = "abc";           /* s在栈, "abc"在常量区 */
    char *p2;                   /* p2在栈 */
    char *p3 = "123456";        /*"123456"在常量区,p3在栈*/
    static int c =0;            /*可读可写数据段*/
    p1 = (char *)malloc(10);    /*分配得来的10个字节的区域在堆区*/
    p2 = (char *)malloc(20);    /*分配得来的20个字节的区域在堆区*/
    /* 从常量区的“Hello World”字符串复制到刚分配到的堆区 */
    strcpy(p1, “Hello World");
    return 0;
}

动态内存的申请和分配

当程序运行到需要一个动态分配的变量时,必须向系统申请取得堆中的一块所需大小的存储空间,用于存储该变量。当不再使用该变量时,也就是它的生命结束时,要显式释放它所占用的存储空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。下面将介绍动态内存申请和释放的函数。

malloc函数
在C语言中,使用malloc函数来申请内存。函数原型如下:

#include <stdlib.h>

void *malloc(size_t size);

其中,参数size代表需要动态申请的内存的字节数。若内存申请成功,函数返回申请到的内存的起始地址,若申请失败,返回NULL。使用该函数时,有下面几点要注意:
(1)只关心申请内存的大小。该函数的参数,很简单,只有申请内存的大小,单位是字节。
(2)申请的是一块连续的内存。该函数一定是申请一块连续的区间,可能申请到的内存比实际申请的大。也可能申请不到,若申请失败,返回NULL。读者,一定记得写出错判断。
(3)返回值类型是void *。函数的返回值是void *,不是某种具体类型的指针。读者可以理解成,该函数只是申请内存,对在内存中存储什么类型的数据,没有要求。因此,返回值是void *。在实际编程中,根据实际情况,将void * 转换成所需要的指针类型。
(4)显示初始化。注意,堆区是不会自动在分配时做初始化的(包括清零),所以程序中需要显式的初始化。

free函数
在堆区上分配的内存,需要用free函数显示释放。函数原型如下:

#include <stdlib.h>
void free(void *ptr);

函数的参数ptr,指的是需要释放的内存的起始地址。该函数没有返回值。使用该函数,也有下面几点需要注意:
(1)必须提供内存的起始地址。调用该函数时,必须提供内存的起始地址,不能提供部分地址,释放内存中的一部分是不允许的。因此,必须保存好malloc返回的指针值,若丢失,则所分配的堆空间无法回收,称内存泄漏。
(2)malloc和free配对使用。编译器不负责动态内存的释放,需要程序员显示释放。因此,malloc与free是配对使用的,避免内存泄漏。
示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int *get_memory(int n){
    int *p, i;
    if ((p = (int *)malloc(n * sizeof(int))) == NULL){
        printf("malloc error\n");
        return p;
    }
    memset(p, 0, n * sizeof(int));
    for (i = 0; i < n; i++)
        p[i] = i+1;
    return p;
}
int main(int argc, const char * argv[]) {
    int n, *p, i;
    printf("input n:");
    scanf("%d", &n);
    if ((p = get_memory(n)) == NULL)
        return 0;
    for (i = 0; i < n; i++)
        printf("%d ", p[i]);
    printf("\n");
    free(p);
    p = NULL;
    return 0;
}
输出结果:
input n:10
1 2 3 4 5 6 7 8 9 10
Program ended with exit code: 0

说明:该程序演示了动态内存的标准用法。动态内存的申请,通过一个指针函数来完成。内存申请时,判断是否申请成功,成功后,对内存初始化。在主调函数中,动态内存依然可以访问,不再访问内存时,用free函数释放。
3)不允许重复释放。同一空间的重复释放也是危险的,因为该空间可能已另分配。在上面程序中,如果释放堆空间两次(连续调用两次free(p)),会出现崩溃,控制台打印很多内存指令。
(4)free只能释放堆空间。像代码区、全局变量与静态变量区、栈区上的变量,都不需要程序员显示释放,这些区域上的空间,不能通过free函数来释放,否则执行时,会出错。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, const char * argv[]) {
    int a[10] = {0};
    free(a);
    return 0;
}
输出结果:
内存管理(1624,0x10007f000) malloc: *** error for object 0x7fff5fbff820: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
这里程序运行后会报错,直接会崩溃,这里读者自己去尝试一下。

野指针
提到野指针,前面我们说指针的时候,都已经提过了,这里就简单的在提一下。
野指针指的是指向“垃圾”内存的指针,不是NULL指针。出现“野指针”主要有以下原因:
(1)指针变量没有被初始化。指针变量和其它的变量一样,若没有初始化,值是不确定的。也就是说,没有初始化的指针,指向的是垃圾内存,非常危险。

#include <stdio.h>
int main(int argc, const char * argv[]) {
    int *p;
    printf("%d\n", *p);
    *p = 10;
    printf("%d\n", *p);
    return 0;
}

(2)指针p被free之后,没有置为NULL。free函数是把指针所指向的内存释放掉,使内存成为了自由内存。但是,该函数并没有把指针本身的内容清楚。指针仍指向已经释放的动态内存,这是很危险。
程序员稍有疏忽,会误以为是个合法的指针。就有可能再通过指针去访问动态内存。实际上,这时的内存已经是垃圾内存了。
关于野指针会造成什么样的后果,这是很难估计的。若内存仍然是空闲的,可能程序暂时正常运行;若内存被再次分配,又通过野指针对内存进行了写操作,则原有的合法数据,会被覆盖,这时,野指针造成的影响将是无法估计的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char * argv[]) {
    int n = 5, *p, i;
    if ((p = (int *)malloc(n * sizeof(int))) == NULL){
        printf("malloc error\n");
        return 0;
    }
    memset(p, 0, n * sizeof(int));
    for (i = 0; i < n; i++){
        p[i] = i+1;
        printf("%d ", p[i]);
    }
    printf("\n");
    printf("p=%p *p=%d\n", p, *p);
    free(p);
    printf("after free:p=%p *p=%d\n", p, *p);
    *p = 100;
    printf("p=%p *p=%d\n", p, *p);
    return 0;
}
说明:该程序中,故意在执行了“free(p)”之后,通过野指针p对动态内存进行了读写,程序正常执行,也在预料之中。前面已经分析过,内存释放后,若继续访问甚至修改,后果是不可预料的。

(3)指针操作超越了变量的作用范围。指针操作时,由于逻辑上的错误,导致指针访问了非法内存,这种情况让人防不胜防,只能依靠程序员好的编码风格,已及扎实的基本功。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char * argv[]) {
    int a[5] = {1, 9, 6, 2, 10}, *p, i, n;
    n = sizeof(a) / sizeof(n);
    p = a;
    for (i = 0; i <= n; i++){
        printf("%d ", *p);
        p++;
    }
    printf("\n");
    *p = 100;
    printf("*p=%d\n", *p);
    return 0;
}
说明:该程序故意出了两个错误,一是for循环的条件“i <= n”,p指针指向了数组以外的空间。二是“*p = 100”,对非法内存进行了写操作。

(4)不要返回指向栈内存的指针。在函数中,详细介绍了指针函数,指针函数会返回一个指针。在主调函数中,往往会通过返回的指针,继续访问指向的内存。因此,指针函数不能返回栈内存的起始地址,因为栈内存在函数结束时会被释放。

堆和栈的区别

1.申请方式
栈(stack)是由系统自动分配的。例如,声明函数中一个局部变量“int b;”,那么系统自动在栈中为b开辟空间。堆(heap)需要程序员自己申请,并在申请时指定大小。使用C语言中的malloc函数的例子如下所示。
p1 = (char *)malloc(10);
2.申请后系统的响应
堆在操作系统中有一个记录空闲内存地址的链表。当系统收到程序的申请时,系统就会开始遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小。这样,代码中的删除语句才能正确地释放本内存空间。如果找到的堆节点的大小与申请的大小不相同,系统会自动地将多余的那部分重新放入空闲链表中。
只有栈的剩余空间大于所申请空间,系统才为程序提供内存,否则将报异常,提示栈溢出。
3.申请大小的限制
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统用链表来存储的空闲内存地址,地址是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存,因此堆获得的空间比较灵活,也比较大。
栈是向低地址扩展的数据结构,是一块连续的内存区域。因此,栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将提示栈溢出,因此,能从栈获得的空间较小。
4.申请速度的限制
堆是由malloc等语句分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来很方便。栈由系统自动分配,速度较快,但程序员一般无法控制。
5.堆和栈中的存储内容
堆一般在堆的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排。
在调用函数时,第一个进栈的是函数调用语句的下一条可执行语句的地址,然后是函数的各个参数,在大多数的C语言编译器中,参数是由右往左入栈的,然后是函数中的局部变量。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始的存储地址,也就是调用该函数处的下一条指令,程序由该点继续运行。

C语言关键字

C语言关键字volatile
C语言关键字volatile(注意它是用来修饰变量而不是上面介绍的volatile)表明某个变量的值可能随时被外部改变(例如,外设端口寄存器值),因此对这些变量的存取不能缓存到寄存器,每次使用时需要重新读取。
该关键字在多线程环境下经常使用,因为在编写多线程的程序时,同一个变量可能被多个线程修改,而程序通过该变量同步各个线程。对于C语言编译器来说,它并不知道这个值会被其他线程修改,自然就把它缓存到寄存器里面。volatile的本意是指这个值可能会在当前线程外部被改变,此时编译器知道该变量的值会在外部改变,因此每次访问该变量时会重新读取。这个关键字在外设接口编程中经常被使用。

总结

本篇文章简单的介绍C语言中的内存管理,希望读者掌握。

结尾

希望读者真诚的对待每一件事情,每天都能学到新的知识点,要记住,认识短暂,开心也是过一天,不开心也是一天,无所事事也是一天,小小收获也是一天,欢迎收藏和点赞、喜欢。最后送读者一句话:你的路你自己选择。

(0)

相关推荐

  • 【C语言核心基础】基本运算、变量、数组、指针、函数、结构体...

    C 语言基础 // 引入头文件.里面包含了重要的 printf. #include <stdio.h> // 入口函数. // 参数一指输入的参数个数,参数二保存了所有参数. // 返回值 ...

  • C/C 指针详解之提高篇

    目录 一. 堆空间与指针的相爱相杀 1.1 堆上一维空间 1.1.1 返回值返回(一级指针) 1.1.2 参数返回(二级指针) 1.2 堆上二维空间 1.2.1 指针作返值输出 1.2.2 空间申请与 ...

  • 详解C语言那些可怕的野指针

    一.什么是野指针? 指针是C语言的灵魂,同时也是很容易让人犯错的重难点,用错了指针将是一个灾难. 指针变量的本质是值,这个特殊的值是一个内存地址值,而合法的内存地址包括定义的变量的地址(栈).mall ...

  • 一份通俗易懂的C语言内存总结

    C语言程序需要载入内存才可以运行,其不同的数据保存在不同的区域.所使用的内存可以分成两类:一类是静态存储区,另一类是动态存储区.C语言程序的存储区如下图所示: 1静态存储区 静态存储区分为三类:只读数 ...

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

    C 语言内存管理指对系统内存的分配.创建.使用这一系列操作.在内存管理中,由于是操作系统内存,使用不当会造成毕竟麻烦的结果.本文将从系统内存的分配.创建出发,并且使用例子来举例说明内存管理不当会出现的 ...

  • 万字整理,肝翻Linux内存管理所有知识点

    Linux的内存管理可谓是学好Linux的必经之路,也是Linux的关键知识点,有人说打通了内存管理的知识,也就打通了Linux的任督二脉,这一点不夸张.有人问网上有很多Linux内存管理的内容,为什 ...

  • 图解 Go 内存管理分配

    GCTT:dust347 Go语言中文网 今天 Illustration created for "A Journey With Go", made from the origin ...

  • Linux 内存管理之vmalloc

    走进vmalloc 根据前面的系列文章,我们知道了buddy system是基于页框分配器,kmalloc是基于slab分配器,而且这些分配的地址都是物理内存连续的.但是随着碎片化的积累,连续物理内存 ...

  • Python一切皆是对象,但这和内存管理有什么关系?

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理. PS:如有需要Python学习资料的小伙伴可以点击下方链接自行获取 Python免费学习资料 ...

  • Linux 内存管理之CMA

    什么是CMA CMA是reserved的一块内存,用于分配连续的大块内存.当设备驱动不用时,内存管理系统将该区域用于分配和管理可移动类型页面:当设备驱动使用时,此时已经分配的页面需要进行迁移,又用于连 ...

  • 枣树全年管理讲解!

    一.萌芽期(4月):进行春季清园,于上旬末至中旬初,彻底清理田园及周围环境的残枝落叶,尤其对枣园相邻的沟.渠.路及路边的杂草进行彻底清除,消灭适宜害虫滋生的条件,对田园环境喷杀菌剂消灭盲蝽象.红蜘蛛等 ...

  • HBase原理|HBase内存管理之MemStore进化论

    Java工程中内存管理总是一个绕不过去的知识模块,无论HBase.Flink还是Spark等,如果使用的JVM堆比较大同时对读写延迟等性能有较高要求,一般都会选择自己管理内存,而且一般都会选择使用部分 ...

  • 操作系统的内存管理算法

    本文主要介绍内存的基本概念以及操作系统的内存管理算法. 嵌入式专栏 1 内存的基本概念 内存是计算机系统中除了处理器以外最重要的资源,用于存储当前正在执行的程序和数据.内存是相对于CPU来说的,CPU ...