0
点赞
收藏
分享

微信扫一扫

《深入理解C指针》——C的动态内存管理

魔都魅影梅杜萨 2022-05-02 阅读 51

动态内存分配

在C中动态分配内存的基本步骤有:

  1. 用malloc类的函数分配内存;
  2. 用这些内存支持应用程序;
  3. 用free函数释放内存。
int *pi = (int*) malloc(sizeof(int));
*pi = 5;
printf("pi:%d\n",*pi);
free(pi);

考虑如下代码段,我们为字符串分配内存,让它可以存放最多5个字符外加结尾的NUL字符。for循环在每个位置写入0,但是没有在写人6字节后停止。for语句的结束条件是写入8字节。写入的0是二进制О而不是ASCII字符0的值。

char *pc = (char*) malloc(6);
for(int i = 0; i < 8; i++){
	*pc[i] = 0;
}

在本例中,6字节的字符串后面还分配了额外的内存,这是堆管理器用来记录内存分配的。如果我们越过字符串的结尾边界写入,额外的内存中的数据会损坏。

内存泄漏

导致内存泄漏的情况可能如下:

  • 丢失内存地址;
  • 应该调用free函数却没有调用(有时候也称为隐式泄漏)。

丢失地址

当pi被赋值为一个新地址时丢失内存地址的例子:

int *pi = (int*) malloc(sizeof(int));
*pi = 5;
...
pi = (int*) malloc(sizeof(int));

为字符串分配内存,将其初始化,并逐个字符打印字符串分配内存的起始地址丢失了:

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

隐式内存泄漏

如果程序应该释放内存而实际却没有释放,也会发生内存泄漏。如果我们不再需要某个对象但它仍然保存在堆上,就会发生隐式内存泄漏,一般这是程序员忽视所致。
在释放用struct关键字创建的结构体时也可能发生内存泄漏。如果结构体包含指向动态内存的指针,那么可能需要在释放结构体之前先释放这些指针。

动态内存分配函数

动态内存分配函数:

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

使用malloc函数

传递参数给这个函数时要小心,因为如果参数是负数就会引发问题:

void* malloc(size_t);

malloc函数经典用法:

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

执行malloc函数时会进行以下操作:

  1. 从堆上分配内存;
  2. 内存不会被修改或是清空;
  3. 返回首字节的地址。

因为当malloc无法分配内存时会返回NULL,在使用它返回的指针之前先检查NULL是不错的做法,如下所示:

int *pi = (int*) malloc(sizeof(int));
if(pi != NULL){
	// 指针没有问题
}else{
	// 无效的指针
}

要不要强制类型转换:C引入void指针后,可将void指针赋值给其他任何指针类型,所以就不再需要显式类型转换了。但有些开发者认为显式类型转换是不错的做法。

初始化静态或全局变量时不能调用malloc函数。

static int *pi = malloc(sizeof(int));	//出错,全局变量也一样

对于静态变量,可以通过在后面用一个单独的语句给变量分配内存来避免这个问题。但是全局变量不能用单独的赋值语句,因为全局变量是在函数和可执行代码外部声明的,赋值语句这类代码必须出现在函数中:

sttaic int *pi;
pi = malloc(sizeof(int));

使用calloc函数

void *calloc(size_t numElements, size_t elementSize);

calloc函数会根据numElements和elementSize两个参数的乘积来分配内存,并返回一个指向内存的第一个字节的指针。如果不能分配内存,则会返回 NULL。此函数最初用来辅助分配数组内存。

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

等价于:

int *pi = malloc(5 * sizeof(int));
memset(pi, 0, 5 * sizeof(int));

如果内存需要清零可以使用calloc,不过执行calloc可能比执行malloc慢。

使用realloc函数

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

realloc函数的行为

第一个参数第二个参数行为
同malloc
非空0原内存块被释放
非空比原内存块小利用当前的块分配更小的块
非空比原内存块大要么在当前位置要么在其他位置分配更大的块

alloca函数和变长数组

alloca函数(微软为malloca)在函数的栈帧上分配内存。函数返回后会自动释放内存。若底层的运行时系统不基于栈,alloca函数会很难实现,所以这个函数是不标准的,如果应用程序需要可移植就尽量避免使用它。

C99引入了变长数组(VLA),允许函数内部声明和创建其长度由变量决定的数组。在下例中,我们分配了一个在函数内使用的char数组:

void compute(int size){
	char* buffer[size];
	...
}

这意味着内存分配在运行时完成,且将内存作为栈帧的一部分来分配。另外,如果数组用到sizeof操作符,也是在运行时而不是编译时执行。

用free函数释放内存

void free(void *ptr);

指针参数应该指向由malloc类函数分配的内存的地址,这块内存会被返还给堆。尽管指针仍然指向这块区域,但是我们应该将它看成指向垃圾数据。稍后可能重新分配这块区域,并将其装进不同的数据。

将已释放的指针赋值为NULL

已释放的指针仍然可能造成问题。如果我们试图解引一个已释放的指针,其行为将是未定义的。所以有些程序员会显式地给指针赋NULL来表示该指针无效,后续再使用这种指针会造成运行时异常。

int *pi = (int*) malloc(sizeof(int));
...
free(pi);
pi = NULL;

重复释放

p1 = (int*) malloc(sizeof(int));
int *p2 = p1;
free(p1);
...
free(p2);

堆和系统内存

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

程序结束前释放内存

确保程序终止前释放所有内存:

  • 可能得不偿失;
  • 可能很耗时,释放复杂结构也比较麻烦;
  • 可能增加应用程序大小;
  • 导致更长的运行时间;
  • 增加引人更多编程错误的概率。

是否要在程序终止前释放内存取决于具体的应用程序。

迷途指针

迷途指针:如果内存已经释放,而还在引用原始内存的指针。
是用哪个迷途指针会造成一系列问题,包括:

  • 如果访问内存,则行为不可预期;
  • 如果内存不可访问,则是段错误;
  • 潜在的安全隐患。

导致这几类问题的情况可能如下:

  • 访问已释放的内存;
  • 返回的指针指向的是上次函数调用中的自动变量。

迷途指针示例

简单的案例

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

在这里插入图片描述
执行free函数将释放地址500处的内存,此后就不应该再使用这块内存了。但大部分运行时系统不会阻止后续的访问或修改。我们还是可以向这个位置写入数据,如下所示。这么做的结果是不可预期的。

free(pi);
*pi = 10;

指针别名的案例

还有一种迷途指针的情况更难觉察:一个以上的指针引用同一内存区域而其中一个指针被释放。

int *p1 = (int*) malloc(sizeof(int));
*p1 = 5;
...
int *p2;
p2 = p1;
...
free(p1);
...
*p2 = 10;	// 迷途指针

块语句的问题

int *pi;
...
{
	int tmp = 5;
	pi = &tmp;
}
// 这里pi变成了迷途指针
foo();

在这里插入图片描述

处理迷途指针

对付迷途指针的方法:

  • 释放指针后置为NULL,后续使用这个指针会终止应用程序。不过,如果存在多个指针的话还是会有问题。因为赋值只会影响一个指针。
  • 写一个特殊的函数代替free函数。
  • 有些系统(运行时或调试系统)会在释放后覆写数据(比如OxDEADBEEF,取决于被释放的对象,Visual Studio 会用0xCC、0xCD或者0xDD)。在不抛出异常的情况下,如果程序员在预期之外的地方看到这些值,可以认为程序可能在访问已释放的内存。
  • 用第三方工具检测迷途指针和其他问题。

调试器对检测内存泄漏的支持

特殊的内存管理技术:

  • 检测堆的完整性;
  • 检测内存泄漏
  • 模拟对内存不够的情况。

动态内存分配技术

C的垃圾回收

malloc和free函数提供了手动分配和释放内存的方法。不过对于很多问题,需要考虑使用C的手动内存管理,比如性能、达到好的引用局部性、线程问题,以及优雅地清理内存。
有些非标准的技术可以用来解决部分问题,本节将探讨其中一部分技术。这些技术的关键特性在于自动释放内存。内存不再使用之后会被收集起来以备后续使用,释放的内存称为垃圾,因此,垃圾回收就是指这个过程。
鉴于以下原因,垃圾回收是有价值的:

  • 不需要程序员费尽心思决定何时释放内存;
  • 让程序员专注应用程序本身的问题。

Boehm-Weiser Collector (http://www.hp1.hp.com/personal/Hans_Boehm/gc/)可以作为手动内存管理的替换方法,不过它不属于语言的一部分。

资源获取即初始化

资源获取即初始化(Resource Acquisition Is Initialization,RAII)是Bjarne Stroustrup发明的技术,可以用来解决C++中资源的分配和释放。即使有异常发生,这种技术也能保证资源的初始化和后续的释放。分配的资源最终总是会得到释放。

GNU的扩展要用到RAII_VARIABLE宏,它声明一个变量,然后给变量关联如下属性:

  • 一个类型;
  • 创建变量时执行的函数;
  • 变量超过作用域时执行的函数。
#define RAII_VARIABLE(vartype,varname ,initval,dtor) \
	void _dtor_ ## varname (vartype * v) { dtor(*v); }   \
	vartype varname __attribute__((cleanup(_dtor_ ## varname))) = (initval)

在下例中,我们将name变量声明为字符指针。创建它时会执行malloc函数,为其分配32字节。当函数结束时,name超出作用域就会执行free函数:

void raiiExample() {
RAII_VARIABLE(char*, name, (char*)malloc(32), free);
strcpy(name , "RAII Example");
printf("%s\%n" ,name);

函数执行后会打印“RAII_Example"字符串。

使用异常处理函数

另一种处理内存释放的方法是利用异常处理(http://www.adomas.org/excc/)。尽管异常处理不属于标准C,但如果可以使用它且不考虑移植问题,它会很有用。下面说明利用Microsoft Visua Studio版的C语言的方法。
这里的try块包含任何可能在运行时抛出异常的语句。不管有没有异常抛出,都会执行finally块,因此也一定会执行free函数。

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

小结

动态内存分配技术,malloc和free函数实现手动分配内存。
以及RAII和异常处理技术。

举报

相关推荐

0 条评论