Skip to content
This repository has been archived by the owner on May 6, 2021. It is now read-only.

Commit

Permalink
ch6.
Browse files Browse the repository at this point in the history
  • Loading branch information
wizardforcel committed Jul 7, 2016
1 parent cac37c3 commit b27ef0b
Showing 1 changed file with 78 additions and 1 deletion.
79 changes: 78 additions & 1 deletion ch6.md
Expand Up @@ -13,4 +13,81 @@ C提供了4中用于动态内存分配的函数:
+ `malloc`,它接受表示字节单位的大小的整数,返回指向新分配的、(至少)为指定大小的内存块的指针。如果不能满足要求,它会返回特殊的值为`NULL`的指针。
+ `calloc`,它和`malloc`一样,除了它会清空新分配的空间。也就是说,它会设置块中所有字节为0。
+ `free`,它接受指向之前分配的内存块的指针,并会释放它。也就是说,使这块空间可用于未来的分配。
+ `realloc`,它接受指向之前分配的内存块的指针,和一个新的大小。它使用新的大小
+ `realloc`,它接受指向之前分配的内存块的指针,和一个新的大小。它使用新的大小来分配内存块,将旧的块中的数据复制到新的块中,释放旧的块,并返回指向新的块的指针。

这套API是出了名的易错和小气。内存管理是设计大型系统中,最具有挑战性的一部分,它正是许多现代语言提供高阶内存管理特性,例如垃圾回收的原因。

## 6.1 内存错误

C的内存管理API有点像Jasper Beardly,动画片《辛普森一家》中的一个配角,它是一个严厉的代课老师,喜欢体罚别人,并使用戒尺惩罚任何违规行为。

下面是一些应受到惩罚的程序行为:

+ 如果你访问任何没有分配的内存块,就应受到惩罚。
+ 如果你释放了某个内存块之后再访问它,就应受到惩罚。
+ 如果你尝试释放一个没有分配的内存块,就应受到惩罚。
+ 如果你释放多次相同的内存块,就应受到惩罚。
+ 如果你使用没有分配或者已经释放的内存块调用`realloc`,就应受到惩罚。

这些规则听起来好像不难遵循,但是在一个大型程序中,一块内存可能由程序一部分分配,在其他部分中使用,之后在另一个部分中释放。所以一部分中的变量也需要其它部分跟着变化。

同时,同一个内存块在程序的不同部分中,也可能有许多别名或者引用。这些内存块在所有引用不再使用时,才应该被释放。正确处理这件事情通常需要细心的分析程序的所有部分,这非常困难,并且与良好的软件工程的基本原则相违背。

理论上,每个分配内存的函数都应包含内存如何释放的信息,作为接口文档的一部分。成熟的库通常做得很好,但是实际上,软件工程的实践通常不是这样理想化的。

内存错误非常难以发现,因为这些症状是不可预测的,这使得事情更加糟糕,例如:

+ 如果从未分配的块中读取值,系统可能会检测到错误,触发叫做“段错误”的运行时错误,并且中止程序。这个结果非常合理,因为它表示程序所读取的位置会导致错误。但是,遗憾的是,这种结果非常少见。更通常的是,程序读取了未分配的块,而没有检测到错误,程序所读取的未分配内存正好储存在一块特定区域中。如果这个值没有解释为正确的类型,结果可能会难以解释。例如,如果你读取字符串中的字节,将它们解释为浮点数,你可能会得到一个无效的数值,非常大或非常小的数值。如果你向函数传递它无法处理的值,结果会非常怪异。
+ 如果你向未分配的块中写入值,会更加糟糕。因为在值被写入之后,需要很长时间值才能被读取并且发生错误。此时寻找问题来源就会非常困难。事情还可能更加糟糕!C风格内存管理的一个最普遍的问题是,用于实现`malloc``free`的数据结构(我们将会看到)通常和分配的内存块储存在一起。所以如果你无意中越过动态分配块的末尾写入值,你就可能破坏了这些数据结构。系统通常直到最后才会检测到这种问题,当你调用`malloc``free`时,这些函数会由于一些谜之原因调用失败。

你应该从中总结出一条规律,就是安全的内存管理需要设计和规范。如果你编写了一个分配内存的库或模块,你应该同时提供释放它的结构,并且内存管理从开始就应该作为API设计的一部分。

如果你使用了分配内存的库,你应该按照规范使用API。列入,如果库提供了分配和释放储存空间的函数,你应该一起使用或都不使用它们。例如,不要在不是`malloc`分配的块上调用`free`。你应该避免在程序的不同部分中持有相同的块的多次引用。

通常在安全的内存管理和性能之间有个权衡。例如,内存错误的的最普遍来源是数组的越界写入。这一问题的最显然的解决方法就是边界检查。也就是说,每次对数组的访问都应该检查下标是否越界。提供数组结构的高阶库通常会进行边界检查。但是C风格数据和大多数底层库不会这样做。

## 6.2 内存泄漏

有一种可能会也可能不会受到惩罚的内存错误。如果你分配了一块内存,并且没有释放它,就会产生“内存泄漏”。

对于一些程序,内存泄露是OK的。如果你的程序分配内存,对其执行计算,之后推出,这可能就不需要释放内存。当程序退出时,所有分配的内存都会由操作系统释放。在退出前立即释放内存似乎很负责任,但是及通常很浪费时间。

但是如果你个程序运行了很长时间,并且泄露内存的话,它的内存总量会无限增长。此时会发生一些事情:

+ 某个时候,系统会耗完所有物理内存。在没有虚拟内存的系统上,下一次的`malloc`调用会失败,返回`NULL`
+ 在带有虚拟内存的系统上,操作系统可以将其它进程的页面从内存移动到磁盘上,之后分配更多空间给泄露的进程。我会在7.8节解释这一机制。
+ 单个进程可能有内存总量的限制,超过它的话,`malloc`会返回`NULL`
+ 最后,进程可能会用完它的虚拟地址空间(或者可用的部分)。之后,没有更多的地址可分配,`malloc`会返回`NULL`

如果`malloc`返回了`NULL`,但是你仍旧把它当成分配的内存块进行访问,你会得到段错误。因此,在使用之前检查`malloc`的结果是个很好的习惯。一种选择是在每个`malloc`调用之后添加一个条件判断,就像这样:

```c
void *p = malloc(size);
if (p == NULL) {
perror("malloc failed");
exit(-1);
}
```

`perror``stdio.h`中声明,它会打印出关于最后发生的错误的错误信息和额外的信息。

`exit``stdlib.h`中声明,会使进程终止。它的参数是一个表示进程如何终止的状态码。按照惯例,状态码0表示通常终止,-1表示错误情况。有时其它状态码用于表示不同的错误情况。

错误检查的代码十分讨厌,并且使程序难以阅读。但是你可以通过将库函数的调用和错误检查包装在你自己的函数中,来解决这个问题。例如,下面是检查返回值的`malloc`包装:

```c
void *check_malloc(int size)
{
void *p = malloc (size);
if (p == NULL) {
perror("malloc failed");
exit(-1);
}
return p;
}
```
由于内存管理非常困难,多数大型程序,例如Web浏览器都会泄露内存。你可以使用Unix的`ps`和`top`工具来查看系统上的哪个程序占用了最多的内存。
## 6.3 实现

0 comments on commit b27ef0b

Please sign in to comment.