= calloc(num, sizeof(int)); /* calloc faild */ if (vc->data == NULL) { return VECTOR_ALLOC_ERROR; } vc->size = num * sizeof(int); return VECTOR_OK; } int grow_vector(struct vector *vc) { void *newptr = 0; size_t newsize; if (vc == NULL) { return VECTOR_NULL_ERROR; } /* check for integer and SIZE_MAX overflow */ if (vc->size == 0 || SIZE_MAX / 2 < vc->size) { errno = ENOMEM; return VECTOR_SIZE_ERROR; } newsize = vc->size * 2; newptr = realloc(vc->data, newsize); /* realloc faild; vector stays intact size was not changed */ if (newptr == NULL) { return VECTOR_ALLOC_ERROR; } /* upon success; update new address and size */ vc->data = newptr; vc->size = newsize; return VECTOR_OK; }
避免致命错误
在动态内存申请上,避免错误的通用方法,小心翼翼的写代码,尽可能的做好异常保护。但是有很多常见的问题上,有一些方法可以避免他们。
1 )重复调用free导致崩溃
这个问题由free函数的参数为下列情况引起:空指针,或者指针没有用malloc等函数分配的(野指针),或者已经被free/recalloc释放了(野指针)。为了避免这个问题,可以采取下列方法:
1 如果不能立即给指针赋有效的值,那么在声明的时候,初始化指针为NULL,
2 gcc 和clang 都会对未初始化的变量进行警告。
3 不要用同一个指针去指向静态内存和动态内存。
4 在使用free之后,将指针设置为NULL, 这样如果你不小心又调用free,也不会出错。
5 为了避免两次释放,在测试和调试的时候,使用assert 或许类似的函数。
char *ptr = NULL;
/* ... */
void nullfree(void **pptr) {
void *ptr = *pptr;
assert(ptr != NULL)
free(ptr);
*pptr = NULL;
}
2 )通过空指针或者未初始化的指针访问内存。
使用上述规则,你的代码只需要处理空指针或有效的指针。只需要在函数或者代码段开始的时候,检测动态内存的指针是否为空。
3 )访问越界的内存
访问越界的内存并不一定都会导致程序崩溃。程序可能继续操作使用错误的数据,产生危险的后果,或者程序可能利用这些操作,进入其他的分支,或者进入执行代码。逐步的人工检测数组边界和动态内存边界,是主要避免这些危险的主要方法。内存边界的信息可以人工跟踪。数组的大小可以用sizeof函数,但是有时候array也会被转换为指针,(例如在函数中,sizeof 会返回指针的大小,而不是数组。)c11标准中的接口Annex k 是边界检测的接口,定义了一系列新库函数,提供了一些简单安全的方法去代替标准库(例如string 和I/O操作) 还有一些开源的方法例如 slibc,但是他的接口没有广泛采用。基于BSD系统(包括Mac OS X)提供了strlcpy,strlcat函数,可以更好的进行字符串操作。对于其他系统可以使用libbsd libraray.很多操作系统提供了接口,控制获取内存区域,保护内存读写例如posix mporst,这些机制主要用于整个内存页的控制。
避免内存泄露
内存泄露是由于有些动态内存不在使用了,但是程序没有释放而导致的。所以真正理解分配的内存空间作用域,最重要的是 free函数什么时候调用。但是随着程序复杂性的增强,这个就会变得越来越困难,所以在开始的设计中需要加入内存管理的功能。下面是一些方法去解决这些问题:
1)启动的时候申请
将所有需要的堆内存分配防止程序启动的时候可以让内存管理变得简单。在程序结束的时候由操作系统释放(这里的意思是程序结束调用free么?还是程序关闭后系统自己free)。在很多情况下,这个方法是令人满意的,特别是程序批处理输入,然后完成。
2)可变长度的数组
如果你需要一个可变大小的临时存储空间,生命周期只在一个函数中,那么可以考虑使用VLA(可变长度数组)。但是使用它是受限制的,每个函数使用它的空间不能超过百个字节。因为可变长度数组在C99中定义的(C11优化)有自动存储区域,它和其他的自动变量一样有一定的范围。尽管标准没有明确指出,通常会将VLA放在栈空间中。 VLA的最大可以分配的内存空间大小为 SIZE_MAX字节。先要知道目标平台的栈空间大小,我们要谨慎使用,确保不出现栈溢出,或者读取内存段下面的错误数据。
3)人工引用计数
这个技术的背后思想是记录每次分配和失去引用的数目。在每次分配引用的时候计数增加,每次失去引用的时候分配减少。当引用的数目为0的时候,表示内存空间不再使用了,然后进行释放。但是C语言不支持自动析构(实际上,GCC和Clang都支持cleanup扩展)但并不意味着要重写分配操作符,通过人工的调用retain/release来完成计数。 函数。换一个思路,程序中有多个地方会占用或者解除和一块内存空间的关系。即便是使用这个方法,要遵守很多准则来确保不会忘记调用release(导致内存泄露)或过多的调用(提前释放)。但是如果一个内存空间的生命期,是由由外部事件确定,并且程序的结构决定,它会用各种方法来处理内存空间,那么使用这种麻烦的方法也是很值得的。下面代码块是一个简单的引用计数去进行内存管理。
#include
#include
#define MAX_REF_OBJ 100 #define RC_ERROR -1 struct mem_obj_t{ void *ptr; uint16_t count; }; static struct mem_obj_t references[MAX_REF_OBJ]; static uint16_t reference_count = 0; /* create memory object and return handle */ uint16_t create(size_t size){ if (reference_count >= MAX_REF_OBJ) return RC_ERROR; if (size){ void *ptr = calloc(1, size); if (ptr != NULL){ references[reference_count].ptr = ptr; references[reference_count].count = 0; return reference_count++; } } return RC_ERROR; } /* get memory object and increment reference counter */ void* retain(uint16_t handle){ if(handle < reference_count && handle >= 0){ references[handle].count++; return references[handle