C语言内存管理讲解

96
长风留言
2017.11.22 10:31 字数 3627

谨记

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

前言

今天为大家带来的是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语言中的内存管理,希望读者掌握。

结尾

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

C语言
Web note ad 1