首页 > C语言入门 > 指针 阅读:1,189

动态内存分配,C语言动态内存分配详解

< 上一页指针和数组的关系 多级指针下一页 >

pk10开户推出辅导班啦,包括「C语言辅导班、C++辅导班、算法/数据结构辅导班」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践 + 永久学习。QQ在线,随时响应!

动态内存是相对静态内存而言的。所谓动态和静态就是指内存的分配方式。动态内存是指在堆上分配的内存,而静态内存是指在栈上分配的内存。

前面所写的程序大多数都是在栈上分配的,比如局部变量、形参、函数调用等。栈上分配的内存是由系统分配和释放的,空间有限,在复合语句或函数运行结束后就会被系统自动释放。而堆上分配的内存是由程序员通过编程自己手动分配和释放的,空间很大,存储自由。堆和栈后面还会专门讲,这里先了解一下。

传统数组的缺点

“传统数组”就是前面所使用的数组,与动态内存分配相比,传统数组主要有以下几个缺点:

1) 数组的长度必须事先指定,而且只能是常量,不能是变量。比如像下面这么写就是对的:
int a[5];
而像下面这么写就是错的:
int length = 5;
int a[length];  //错误
2) 因为数组长度只能是常量,所以它的长度不能在函数运行的过程当中动态地扩充和缩小。

3) 对于数组所占内存空间程序员无法手动编程释放,只能在函数运行结束后由系统自动释放,所以在一个函数中定义的数组只能在该函数运行期间被其他函数使用。

而动态内存就不存在这个问题,因为动态内存是由程序员手动编程释的,所以想什么时候释放就什么时候释放。只要程序员不手动编程释放,就算函数运行结束,动态分配的内存空间也不会被释放,其他函数仍可继续使用它。除非是整个程序运行结束,这时系统为该程序分配的所有内存空间都会被释放。

所谓“传统数组”的问题,实际上就是静态内存的问题。我们讲传统数组的缺陷实际上就是以传统数组为例讲静态内存的缺陷。本质上讲的是以前所有的内存分配的缺陷。正因为它有这么多缺陷,所以动态内存就变得很重要。动态数组能很好地解决传统数组的这几个缺陷。

malloc函数的使用

那么动态内存是怎么造出来的?在讲如何动态地把一个数组造出来之前,我们必须要先介绍 malloc 函数的使用。

malloc 是一个系统函数,它是 memory allocate 的缩写。其中memory是“内存”的意思,allocate是“分配”的意思。顾名思义 malloc 函数的功能就是“分配内存”。要调用它必须要包含头文件<stdlib.h>。它的原型为:

# include <stdlib.h>
void *malloc(unsigned long size);

malloc 函数只有一个形参,并且是整型。该函数的功能是在内存的动态存储空间即堆中分配一个长度为size的连续空间。函数的返回值是一个指向所分配内存空间起始地址的指针,类型为 void*型。

简单的理解,malloc 函数的返回值是一个地址,这个地址就是动态分配的内存空间的起始地址。如果此函数未能成功地执行,如内存空间不足,则返回空指针 NULL。

“int i=5;”表示分配了 4 字节的“静态内存”。这里需要强调的是:“静态内存”和“静态变量”虽然都有“静态”两个字,但是它们没有任何关系。不要以为“静态”变量的内存就是“静态内存”。静态变量的关键字是 static,它与全局变量一样,都是在“静态存储区”中分配的。这块内存在程序编译的时候就已经分配好了,而且在程序的整个运行期间都存在;而静态内存是在栈中分配的,比如局部变量。

那么,如何判断一个内存是静态内存还是动态内存呢?凡是动态分配的内存都有一个标志:都是用一个系统的动态分配函数来实现的,如 malloc 或 calloc。

calloc 和 malloc 的功能很相似,我们一般都用 malloc。calloc 用得很少,这里不做讲解,有兴趣的话可自行查阅。

如何用 malloc 动态分配内存呢?比如:
int *p = (int *)malloc(4);
它的意思是:请求系统分配 4 字节的内存空间,并返回第一字节的地址,然后赋给指针变量 p。当用 malloc 分配动态内存之后,上面这个指针变量 p 就被初始化了。

需要注意的是,函数 malloc 的返回值类型为 void* 型,而指针变量 p 的类型是 int* 型,即两个类型不一样,那么可以相互赋值吗?

上面语句是将 void* 型“强制类型转换”成 int*型,但事实上可以不用转换。C 语言中,void* 型可以不经转换(系统自动转换)地直接赋给任何类型的指针变量(函数指针变量除外)。

所以“int*p=(int*)malloc(4);”就可以写成“int*p=malloc(4);”。此句执行完之后指针变量 p 就指向动态分配内存的首地址了。

void和void*

可能有人会问,void 不是不会有返回值吗?为什么 malloc 还会有返回值?需要注意的是,malloc 函数的返回值类型是 void*,而不是 void。void*和void是有区别的。

void* 是定义一个无类型的指针变量,它可以指向任何类型的数据。任何类型的指针变量都可以直接赋给 void* 型的指针变量,无需进行强制类型转换。本教程后面很多函数的参数都是 void* 型的,表示它们可以接收任何类型的数据。

同样,根据我们上面所讲的,void* 型也可以直接赋给任何类型的指针变量,而无需进行强制类型转换,但前提是必须在C语言中。

注意,不能对 void* 型的指针变量进行运算操作,如指针的运算、指针的移动等。原因很简单,前面讲int*型的指针变量加 1 就是移动 4 个单元,因为 int* 型的指针变量指向的是 int 型数据;但是 void* 型可以指向任何类型的数据,所以无法知道“1”所表示的是几个内存单元。

另外,在“int*p=malloc(4);”中,指针变量 p 是静态分配的。前面介绍过,动态分配的内存空间都有一个标志,即都是用一个系统的动态分配函数实现的。而指针变量 p 是用传统的方式定义的,所以是静态分配的内存空间。而 p 所指向的内存是动态分配的。

那么,动态分配和静态分配到底有什么区别呢?稍后你就明白了。

我们在前面讲过,编程的时间长了就会发现编程中百分之八九十的问题都属于内存的问题,如内存什么时候分配、什么时候释放、由谁分配、由谁释放、怎么分配、怎么释放、哪块内存可以用、哪块内存不能用、哪块内存可以读、哪块内存可读可写、哪块内存不能读也不能写。这些问题形成了计算机语言的语法规则,如 C 语言语法、C++ 语法、Java 语法,它们本质上都是内存的问题。包括局部变量、静态变量等都一样。所以内存是很关键的问题。

下面利用“int*p=malloc(4);”语句给大家写一个很有意思的程序:
# include <stdio.h>
# include <stdlib.h>  //malloc()的头文件
int main(void)
{
    while (1)
    {
        int *p = malloc(1000);   
    }
    return 0;
}
这个程序是非常简单的一个木马病毒。只要运行一会儿,你的计算机就死机了。死机速度的快慢取决于 malloc 后面括号中数字的大小。数字越大,“死”得越快。我们可以试验一下:按“Ctrl+Alt+Delete”键打开 Windows 任务管理器,然后选择“性能”,如图 1 所示。


图 1
 
图 1 中 CPU 使用的情况是 1%,内存的使用情况是使用了 1.64GB。现在我们运行上面那个程序看看有什么变化。单击“编译”→“链接”→“执行”,再看看 Windows 任务管理器有什么变化,如图 2 所示。


图 2
 
CPU 瞬间就使用了 27%,内存瞬间使用了 3.43GB,而且这个数值还在往上涨!当内存全部使用完后会启动虚拟内存,就是把硬盘里的一块区域当成内存来使用。如果虚拟内存也用完了,整个系统就死机了。我们只要把 VC++6.0 中执行时弹出的黑色窗口关掉,CPU 和内存的使用就会恢复正常了。因为把黑窗口关闭后程序就结束了,程序运行结束后系统为该程序分配的内存空间就都会被释放。

这是最简单的一个木马程序,这个程序有一个专业的名称,叫“内存泄漏”。什么是“内存泄漏”呢?每台电脑都有内存,所有的程序都是先存放到内存里面才能运行。但是上面这个程序将内存空间都占满了,那么其他程序就没有地方存放了,所以内存就好像泄漏了一样。

下面使用 malloc 函数写一个程序,程序的功能是:调用被调函数,将主调函数中动态分配的内存中的数据放大 10 倍。
# include <stdio.h>
# include <stdlib.h>
void Decuple(int *i);  //函数声明, decuple是10倍的意思
int main(void)
{
    int *p = malloc(4);
    *p = 10;
    Decuple(p);
    printf("*p = %dn", *p);
    return 0;
}
void Decuple(int *i)
{
    *i = (*i) * 10;
    return;
}
输出结果是:
*p = 100

当调用 Decuple 函数时,是把指针变量 p 中的地址传递给指针变量 i,此时 i 和 p 指向的是同一个内存单元。所以操作 i 所指向的内存空间就是操作 p 所指向的内存空间。

程序中,int*p=malloc(4); 中的 4 表示分配了 4 字节的动态内存。因为在前面用 sizeof 试过本人的计算机,其给 int 型变量分配的是 4 字节,所以这么写没有问题。但如果把这个程序移植到其他计算机中呢?系统给 int 型变量分配的就不一定是 4 字节(有可能是 8 字节),因此直接写“4”的可移植性很差。最好的方式为 sizeof(int),即:
int *p = malloc(sizeof(int));
sizeof(int)的值是 int 型变量所占的字节数,这样就能很好地表示当前计算机中 int 型变量占几字节。这样写程序的可移植性就增强了。所以动态内存最好是需要多少就构建多少。多了虽然不会有问题,但是会浪费内存,而少了就可能出就可能出问题。

如果还想简单一点的话,也可以像下面这样写:
int *p = malloc(sizeof*p);
前面讲过,sizeof 的后面可以紧跟类型,也可以直接跟变量名。如果是变量名,那么就表示该变量在内存中所占的字节数。所以 *p 是 int 型的,那么 sizeof*p 就表示 int 型变量在内存中所占的字节数。

free函数的使用

前面定义了一个指向动态内存的指针变量 p:
int *p = malloc(sizeof*p);
前面讲过,动态分配的内存空间是由程序员手动编程释放的。那么怎么释放呢?用 free 函数。

free 函数的原型是:

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

free 函数无返回值,它的功能是释放指针变量 p 所指向的内存单元。此时 p 所指向的那块内存单元将会被释放并还给操作系统,不再归它使用。操作系统可以重新将它分配给其他变量使用。

需要注意的是,释放并不是指清空内存空间,而是指将该内存空间标记为“可用”状态,使操作系统在分配内存时可以将它重新分配给其他变量使用。

指针变量 p 被释放之后,它仍然是指向那块内存空间的,只是那块内存空间已经不再属于它了而已。下面写一个程序看一下:
# include <stdio.h>
# include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof*p);
    *p = 10;
    printf("p = %pn", p);
    free(p);
    printf("p = %pn", p);
    return 0;
}
输出结果是:
p = 002C2ED0
p = 002C2ED0

由该程序可见,释放前后,p 所指向的内存空间是一样的。所以释放后 p 所指向的仍然是那块内存空间。既然指向的仍然是那块内存空间,那么就仍然可以往里面写数据。可是释放后该内存空间已经不属于它了,该内存空间可能会被分配给其他变量使用。如果其他变量在里面存放了值,而你现在用 p 往里面写入数据就会把那个值给覆盖,这样就会造成其他程序错误。所以当指针变量被释放后,要立刻把它的指向改为 NULL。

那么,当指针变量被释放后,它所指向的内存空间中的数据会怎样呢?free 的标准行为只是表示这块内存可以被再分配,至于它里面的数据是否被清空并没有强制要求。不同的编译器处理的方式可能不一样。我们就看一下 VC++6.0 这个编译器是怎么处理的:
# include <stdio.h>
# include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof*p);
    *p = 10;
    printf("p = %pn", p);
    printf("*p = %dn", *p);
    free(p);
    printf("p = %pn", p);
    printf("*p = %dn", *p);
    return 0;
}
输出结果是:
p = 00842ED0
*p = 10
p = 00842ED0
*p = -572662307

可见在 VC++6.0 中,当指针变量被释放后,虽然它仍然是指向那个内存空间的,但那个内存空间中的值将会被重新置一个非常小的负数。

动态创建的内存如果不用了必须要释放。注意,一个动态内存只能释放一次。如果释放多次程序就会崩溃,因为已经释放了,不能再释放第二次。

综上所述,malloc 和 free 一定要成对存在,一一对应。有 malloc 就一定要有 free,有几个 malloc 就要有几个 free,与此同时,每释放一个指向动态内存的指针变量后要立刻把它指向 NULL。

最后需要强调的是,只有动态创建的内存才能用 free 把它释放掉,静态内存是不能用free释放的。静态内存只能由系统释放。比如:
int a = 10;
int *p = &a;
如果试图用 free(p) 把指针变量 p 所指向的内存空间释放掉,那么编译的时候不会出错,但程序执行的时候立刻就出错。
< 上一页指针和数组的关系 多级指针下一页 >