};
int create_vector(struct vector *vc, size_t num) {
if (vc == NULL) {
return VECTOR_NULL_ERROR;
}
vc->data = 0;
vc->size = 0;
/* check for integer and SIZE_MAX overflow */
if (num == 0 || SIZE_MAX / num < sizeof(int)) {
errno = ENOMEM;
return VECTOR_SIZE_ERROR;
}
vc->data = 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;
}
2.1 避免致命错误
一般避免动态内存分配问题的方法无非是尽可能把代码写得谨慎、有防御性。本文列举了一些常见问题和少量避免这些问题的方法。
1) 重复释放内存
调用free 可能导致此问题,此时入参指针可能为NULL(依照《C++ Primer Plus》,free(0)不会出现问题。译者注)、未使用malloc 类函数分配的指针,或已经调用过free /realloc(realloc参数中大小填0,可释放内存。译者注)的指针。考虑下列几点可让代码更健壮:
指针初始化为NULL,以防不能立即传给它有效值的情况
GCC和Clang都有-Wuninitialized参数来警告未初始化的变量
静态和动态分配的内存不要用同一个变量
调用free 后要把指针设回为NULL,这样一来即便无意中重复释放也不会导致错误
测试或调试时使用assert之类的断言(如C11中静态断言,译者注)
char *ptr = NULL;
/* ... */
void nullfree(void **pptr) {
void *ptr = *pptr;
assert(ptr != NULL)
free(ptr);
*pptr = NULL;
}
2) 访问未初始化的内存或空指针
代码中的检查规则应只用于NULL或有效的指针。对于去除指针和分配的动态内存间联系的函数或代码块,可在开头检查空指针。
3) 越界访问内存
(孔乙己式译者注:你能说出strcpy/strncpy/strlcpy的区别么,能的话这节就不必看)
访问内存对象边界之外的地方并不一定导致程序崩溃。程序可能使用损坏了的数据继续运行,其行为可能很危险,也可能是故意而为之,利用此越界操作来改变程序的行为,以此获取其他受限的数据,甚至注入可执行代码。 老套地人工检查数组和动态分配内存的边界是避免此类问题的主要方法。内存对象边界的相关信息必须人工跟踪。数组的大小可由sizeof操作符指出,但数组被转换为指针后,函数调用sizeof仅返回指针大小(视机器位数而定,译者注),而非原来的数组大小。
C11标准中边界检查接口Annex K定义了一些新的库函数集合,这些函数可用于替换标准库(如字符串和I/O操作)常见部分,它们更安全、更易于使用。例如[the slibc library][slibc]都是上述函数的开源实现,但接口不被广泛采用。基于BSD(或基于Mac OS X)的系统提供了strlcpy、strlcat 函数来完成更好的字符串操作。其他系统可通过libbsd库调用它们。
许多操作系统提供了通过内存区域间接控制受保护内存的接口,以防止意外读/写操作,入Posxi mprotect。类似的间接访问的保护机制常用于所有的内存页。
2.2 避免内存泄露
内存泄露,常由于程序中未释放不再使用的动态分配的内存导致。因此,真正理解所需要的分配的内存对象的范围大小是很有必要的。更重要的是,要明白何时调用free。但当程序复杂度增加时,要确定free 的调用时机将变得更加困难。早期设计决策时,规划内存很重要。
以下是处理内存泄露的技能表:
1) 启动时分配
想让内存管理保持简单,一个方法是在启动时在堆中分配所有所需的内存。程序结束时,释放内存的重任就交给了操作系统。这种方法在许多场景中的效果令人满意,特别是当程序在一个批量操作中完成对输入的处理的情况。
2) 变长数组
如果你需要有着变长大小的临时存储,并且其生命周期在变量内部时,可考虑VLA(Variable Length Array,变长数组)。但这有个限制:每个函数的空间不能超过数百字节。因为C99指出边长数组能自动存储,它们像其他自动变量一样受限于同一作用域。即便标准未明确规定,VLA的实现都是把内存数据放到栈中。VLA的最大长度为SIZE_MAX字节。考虑到目标平台的栈大小,我们必须更加谨慎小心,以保证程序不会面临栈溢出、下个内存段的数据损坏的尴尬局面。
3) 自己编写引用计数
这个技术的想法是对某个内存对象的每次引用、去引用计数。赋值时,计数器会增加;去引用时,计数器减少。当引用计数变为0时,这意味着此内存对象不再被使用,可以释放。因为C不提供自动析构(事实上,GCC和Clang都支持cleanup语言扩展), 也不是重写赋值运算符,引用计数由调用retain/release的函数手动完成。更好的方式,是把它作为程序的可变部分,能通过这部分获取和释放一个内存对象的拥有权。但是,使用这种方法需要很多(编程)规范来防止忘记调用release(停止内存泄露)或不必要地调用释放函数 |