【深入C指针】第二章:动态内存分配

深入浅出讲解c语言动态内存分配,内存管理!


第二章:C的动态内存管理

2.0 导言

所谓动态内存管理,就是在程序运行时调用malloc()free()函数(当然还有其他函数,都会讲),即分配释放

这一章我们就是讲如何分配释放

2.1 动态分配步骤

C语言动态分配内存步骤有三步:

  1. 用malloc类函数分配内存
  2. 使用
  3. 用free类函数释放内存

例子中,用malloc()函数为整数分配内存,指向的值赋值为5.

int* num = (int*)malloc(sizeof(int));
*num = 5;
printf("*p = %d\n",*num);
free(num);

输出:

5

malloc函数参数指定要分配的字节数。成功返回指针,失败返回空指针。

常见的错误代码:

int *p;
*p = (int*)malloc(sizeof(int));

问题出在*p是解引指针,而malloc返回的是指针。

重点:每次调用malloc(或类似函数),程序结束时必须有对应的free函数调用,以防止内存泄漏。

2.2 内存泄漏

内存泄漏分为两种:

  1. 丢失地址
  2. 隐式内存泄漏

2.2.1 丢失地址内存泄漏

下面代码展示了当指针被赋予一个新的地址时丢失时,旧的内存丢失的情况,例子:

int* p = (int*)malloc(sizeof(int));
*p = 22;
...
p = (int*)malloc(sizeof(int));

第一次malloc的地址依旧存储在计算机中,但是我们永远的找不到他了。且计算机系统不会自动清理,所以造成内存泄漏。

再来一个常见的丢失内存地址造成内存泄漏的例子,为字符串分配内存,初始化,并逐一打印:

char* name = (char*)malloc(strlen("remoo")+1);
strcpy(name,"remoo");
while(*name != 0){
    printf("%c",*name);
    name++;
}

问题出在,每次while循环name++,最后name会指向字符串结尾NULL("\0"),从而导致起始内存地址丢失,造成内存泄漏。

2.2.2 隐式内存泄漏

当我们不需要某个对象,但是这个对象仍然保存在堆上,这就会造成隐式内存泄漏

2.3 动态分配函数

大部分系统的stdlib.h头文件都有以下函数:

  • malloc
  • realloc
  • calloc
  • free
函数 描述
malloc 从堆上分配内存
realloc 在之前内存块的基础上,重新分配更大或更小的部分
calloc 从堆上分配内存并清零
free 将内存快返回堆中

2.3.1 malloc()函数

函数原型:

void* malloc(size_t);​

malloc函数从堆上分配一块内存,返回值是void*,如果计算机内存不足则返回NULL。

malloc函数不会清空所分配的内存空间,所以我们认为其分配的内存包含垃圾数据。

malloc的参数不能是负数。

下面是malloc典型用法:

int* p = (int*)malloc(sizeof(int));

我们使用(int*)对void*类型的指针强制转换。

使用之前最好检查一下p是否成功分配了内存,这是一个好习惯,例子:

int* p = (int*)malloc(sizeof(int));
if(p == NULL)
    //内存申请失败!

初始化全局变量或静态变量指针时不能调用函数,例子:

static int* p = (int*)malloc(sizeof(int));

这样会得到一个错误:

Initializer element is not a compile-time constant

全局变量也一样。对于静态变量,可以在语句后面单独赋值,但对于全局变量来说则不行。正确例子:

static int* p;
p = (int*)malloc(sizeof(int));

2.3.2 calloc()函数

函数原型:

void *calloc(size_t numElements, size_t elementSize);​

calloc函数分配内存大小 = numElements * elementSize 。分配失败返回NULL。

calloc函数会在分配的同时清空内存(赋二进制0),使用例子:

int* p = (int*)calloc(5,sizeof(int));

上下例子等价:

int* p = (int*)malloc(5 * sizeof(int));
memset(p,0,5*sizeof(int));
memset函数会用某个值填充内存块。第一个参数是指向要填充的缓冲区的指针,第二个参数是填充缓冲区的值,最后一个参数是要填充的字节数。

另外值得一提的是,以前使用cfree释放calloc分配的内存,现在已经没用了。

2.3.3 realloc()函数

函数原型:

void *realloc(void *ptr, size_t size);​​

realloc函数用来重新分配不同大小的内存块,原本的内容还会保留。

如果新分配的内存块比原来小,则多余的部分会返回给堆。

有趣的例子:

char* str1;
char* str2;
str1 = (char*)malloc(16);
strcpy(str1,"0123456789AB");
str2 = (char*)realloc(str1,8);
str2[7]='\0';
printf("%p - %s\n",str1,str1);
printf("%p - %s",str2,str2);

结果:

0x101007c00 - 0123456

0x101007c00 - 0123456

我们发现修改str2[7]的值,str1竟然也跟着变了。

这是因为,当realloc的参数若小于原来的大小,则会在原来的内存地址上分配。当大于原来分配的内存,则会重新分配,例子:

char* str1;
char* str2;
str1 = (char*)malloc(16);
strcpy(str1,"0123456789AB");
str2 = (char*)realloc(str1,80);//此处改为80
str2[7]='\0';
printf("%p - %s\n",str1,str1);
printf("%p - %s",str2,str2);

结果:

0x1011ac240 - 0123456789AB

0x1011c5ac0 - 0123456

2.3.4 alloca()函数

这个函数比较先进,在上分配内存。

以前的一些系统可能就不支持。

C99引入了变长数组(VLA),允许函数内部声明和创建其长度由变量决定的数组。

变长数组这个名字具有误导性,大家要好好理解。更好的翻译应该是:由 变量的长度 决定数组长度的数组。
例子:

void func(int size){
    char* buffer[size];//这个就是变长数组,根据size变量而决定buffer数组的长度的数组
}

包括我们之前的sizeof()函数,也是在运行的时候执行的,所以都叫做变长数组。

由此可见,这样的内存在运行结束后就会自动释放,不需要手动释放。

2.4 free函数释放内存

函数原型:

void free(void *p);

free函数释放内存。

例子:

int* p = (int*)malloc(sizeof(int));
...
free(p);

上面的例子,当p被free后,指针变量p仍然会保存地址。这种情况叫做迷途指针

为了避免迷途指针,我们在free后给p赋值NULL。
例子(续上):

p = NULL;

2.4.1 重复释放

重复释放指的是:两次释放同一块内存。例子:

int* p = (int*)malloc(sizeof(int));
*p = 6;
free(p);
//...
free(p);

此时编译器会报错:

signal SIGABRT

意思是,经过free后的p,属于“非释放类型”。不能再次free。

不幸的是,堆管理器很难判断一个块是否已经被释放,因此它们不会试图去检测是否两次释放了同一块内存。

所以说这样的机制(signal SIGABRT)很有必要,当你free后,系统有可能向这一块内存写入其他数据。如果你再次free,则会导致其他数据的丢失!

有人建议free函数应该在返回时将NULL或其他某个特殊值赋给自身的参数。但指针是传值的,因此free函数无法显式地给它赋值NULL,我们在下一章详细讨论。

2.4.2 堆与系统内存

堆一般利用操作系统的功能来管理内存。堆的大小可能在程序创建后就固定不变了,也可能可以增长。不过堆管理器不一定会在调用free函数时将内存返还给操作系统。释放的内存只是可供应用程序后续使用。所以,如果程序先分配内存然后释放,从操作系统的角度看,释放的内存通常不会反映在应用程序的内存使用上。

2.5 迷途指针

上面我们说了,如果内存已经释放,而指针还在引用原始内存,这样的指针就称为迷途指针。

除了上面那种情况,下面这种情况也会导致迷途指针,例子:

int* p;
{
    int temp = 5;
    p = &temp;
}
//这里temp不再有效
//所以p在此处是迷途指针

2.6 调试器检测内存泄漏

微软是通过使用一种特殊的数据结构管理内存分配来做到这一点的。这种结构维护调试信息,比如malloc调用点的文件名和行号,还会在实际的内存分配之前和之后分配缓冲区来检测对实际内存的覆写。关于这种技术的更多信息可以参考Microsoft Developer Network(http://msdn.microsoft.com/en-us/library/x98tx3cf.aspx) 。

Mudflap库(http://gcc.fyxm.net/summit/2003/mudflap.pdf) 为GCC编译器提供了类似的功能,它的运行时库支持对内存泄漏的检测和其他功能,这种检测是通过监控指针解引操作来实现的。

2.7 一些特别的技术

2.7.1 堆管理器

堆管理器需要处理很多问题,比如堆是否基于进程和(或)线程分配,如何保护堆不受安全攻击。

堆管理器有不少,包括OpenBSD的malloc、Hoard的malloc和Google开发的TCMalloc。
GNU C库的分配器基于通用分配器dlmalloc(http://dmalloc.com),它提供调试机制,能追踪内存泄漏。
dlmalloc的日志特性可以追踪内存的使用和内存事务,还有一些其他功能。

2.7.2 垃圾回收

Boehm-Weiser Collector(http://www.hpl.hp.com/personal/HansBoehm/gc/) 可以作为手动内存管理的替换方法,不过已经不属于c语言的范畴了。

2.7.3 资源获取即初始化

资源获取即初始化(Resource Acquisition Is Initialization, RAII),是Bjarne Stroustrup发明的技术,可以用来解决C++中资源的分配和释放。

即使程序异常,资源也能回收。

2.7.4 异常处理

Microsoft Visual Studio专属:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void exceptionExample() {
    int* pi = NULL;
    __try {
        pi = (int*)malloc(sizeof(int));
        *pi = 5;
        printf("%d\n", *pi);
    }
    __finally {
        free(pi);
    }
}

int main() {
    exceptionExample();
    return(0);
}

2.8 小结

使用malloc、free、realloc、calloc、alloca、以及一些特殊方法对c语言动态内存管理。

评论区 0