跳转至

第十七章 指针的高级应用


动态存储分配

任何单纯的数据结构(各种内置类型,数组,结构体),其大小在程序开始时已经确定了,且不能改变。而一些数据结构可能需要动态的改变其数据长度,比如链表。这就要用到动态存储分配(dynamic storage allocation)。

使用动态存储分配的数据块存放在“堆”上,和其它存储区域不同的是,“堆”里的数据应该让程序员来控制释放(free)时机。

为了动态地分配存储空间,将需要调用3种内存分配函数中的一种,这些函数都是声明在stdlib.h中的:

  1. malloc,分配内存块,但是不初始化它

  2. calloc,分配内存块,并对其清零

  3. realloc,调整先前分配的内存块

由于malloc函数不需要对分配的内存块进行清除,所以它比calloc函数更高效。

空指针

当调用内存分配函数时,无法定位满足我们需要的足够大的内存块,这种问题始终可能出现。如果真的发生了这类问题,函数会返回空指针。

空指针(null pointer)是指一个区别于所有有效指针的特殊值。

Warning

程序员的责任是测试任意内存分配函数的返回值,并且在返回空指针时采取适当的操作。通过空指针试图访问内存的效果是未定义的,程序可能会崩溃或者出现不可预测的行为。

用名为NULL的宏来表示空指针,可用下列方式测试malloc函数的返回值:

p = malloc(10000);
if (p == NULL) {
    /* allocation failed; take appropriate action */
}

动态分配字符串

动态内存分配经常用于字符串操作。字符串始终存储在固定长度的数组中,而且可能很难预测这些数组需要的长度。通过动态地分配字符串,可以推迟到程序运行时才作决定。

使用malloc函数为字符串分配内存

函数原型:

void *malloc(size_t size);

size_t是无符号整型,malloc分配了一段size个字节的连续空间,并返回该空间首地址。如果分配失败就返回NULL。

因为C语言保证char型值确切需要一个字节的内存,为了给n个字符的字符串分配内存空间,可以写成:

p = malloc(n + 1);

通常情况下,可以把void*型值赋给任何指针类型的变量。然而,一些程序员喜欢强制转换malloc函数的返回值:

char *p = (char*)malloc(n + 1);

由于使用malloc函数分配内存不需要清除或者以任何方式初始化,所以p指向带有n+1个字符的未初始化的数组。

可以调用strcpy函数对上述数组进行初始化:

strcpy(p, "abc");

数组中前4个字符分别为a, b, c和空字符。

动态分配数组

编写程序时,常常为难数组估计合适的大小。较方便的做法是等到程序运行时再来确定数组的实际大小。

虽然malloc函数可以为数组分配内存空间,但calloc函数确实是最常用的一种选择。因为calloc函数对分配的内存进行初始化。realloc函数允许根据需要对数组进行“扩展”或“缩减”。

使用malloc函数为数组分配存储空间

当使用malloc函数为数组分配存储空间时,需要使用sizeof运算符来计算出每个元素所需要的空间数量。

使用sizeof计算是必须的,因为这样计算的结果在不同平台下都是正确的。

int *a = malloc(n * sizeof(int));

这里的n可以在程序执行期间计算出来。

一旦a指向了动态分配的内存块,就可以把它用作数组的名字。这都要感谢C语言中数组和指针的紧密关系。可以使用下列循环对此数组进行初始化:

for (i = 0; i < n; ++i)
    a[i] = 0;

calloc函数

函数原型:

void *calloc(size_t nmemb, size_t size);

nmemb是数据单元的个数, size是一个数据单元的大小。返回成功申请的数据块首地址,失败返回NULL。

calloc不仅会从“堆”申请存储区域,还会把这段区域清零。也因此其执行效率没有malloc高。

下列calloc函数的调用为n个整数的数组分配存储空间,并且保证全部初始为0:

a = calloc(n, sizeof(int));

通过调用以1作为第一个实际参数的calloc函数,可以为任何类型的数据项分配空间:

struct point { int x, y; } *p;
p = calloc(1, sizeof(struct point));

此语句执行后,p指向结构,且此结构的成员x和y都会被设置为0。

realloc函数

一旦为数组分配完内存,稍后可能会发现数组过大或过小。realloc函数可以调整数组的大小使它更适合需要。

函数原型:

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

ptr必须指向内存块,且此内存块一定是先通过malloc函数、calloc函数或realloc函数的调用获得的。size表示内存块的新尺寸,新尺寸可能会大于或小于原有尺寸。

C标准列出几条关于realloc函数的规则:

  • 当扩展内存块时,realloc函数不会对添加进内存块的字节进行初始化。

  • 如果realloc函数不能按要求扩大内存块,那么它会返回空指针,并且在原有内存块中的数据不会发生改变。

  • 如果realloc函数调用时以空指针作为第一个实际参数,那么它的行为就像malloc函数一样。

  • 如果realloc函数调用时以0作为第二个实际参数,那么它会释放掉内存块。

Warning

一旦realloc函数返回,请一定要对指向内存块的所有指针进行更新,因为可能realloc函数移动了其他地方的内存块。

实际使用时,realloc应该始终对ptr指向的存储区域进行扩展。

realloc不是一个好用的函数,要很小心才行。这是因为原来的存储区域会被释放掉(虽然新的存储区域会可能和原来的重叠),其指针很可能都变的无效。

释放存储

malloc函数和其他内存分配函数所获得的内存块都来自一个称为(heap)的存储池。调用这些函数经常会耗尽堆,或者要求大的内存块也可能耗尽堆,这会导致函数返回空指针。

更糟的是,程序可能分配了内存块,然后又丢失了这些块的追踪路径,因而浪费了空间。如下例子:

p = malloc(...);
q = malloc(...);
p = q;

由于没有指针指向第一个内存块,所以再也不能使用此内存块了。

对于程序而言,不再访问到的内存块被称为是垃圾(garbage)。在后边留有垃圾的程序有内存泄漏(memory leak)。一些语言提供了垃圾收集器(garbage collector),但C语言不提供。每个C程序负责回收各自的垃圾,方法是调用free函数来释放不需要的内存。

如上例子,就是一个内存泄漏。第一块内存再也访问不到了,这应该就是上文所说的留有垃圾。

free

只有一个方法释放由动态存储分配函数分配的内存空间。就是使用free函数,如果不释放,那么这块资源就一直放在“堆”里,直到程序退出。

函数原型:

void free(void *ptr);

使用free函数很容易,只是简单地把指向不再需要的内存块的指针传递给free函数就可以了:

p = malloc(...);
free(p);

调用free函数来释放p所指向的内存块。然后会把这个释放的内存返回给堆,使此内存块可以被复用。

“悬空指针”问题

free操作会生成悬空指针(dangling pointer)。即调用free(p)函数会释放p指向的内存块,但是不会改变p本身。如果忘记了p不再指向有效内存块(而使用它),后果很严重。

悬空指针是很难发现的,因为几个指针可能指向相同的内存块。在释放内存块时,全部的指针都会留有悬空。

指向函数的指针

函数也有地址,所以就可以有指针指向。一些功能强大的函数(像模板一样)都是通过函数指针和void*实现的。

函数指针

函数指针主要被存放在:

  • 数组里,方便日后调用
  • 形参,成为模板函数的实现,比如qsort

定义一个函数指针类型的例子:

typedef void (*Func)();

函数入口地址

函数名就是函数地址,但通常会对函数名做&运算,其实得到的结果是一样的。同样对函数指针做*运算(解引用)和直接拿函数指针用也是一样的,都是代表了函数的入口地址。

一般会对函数名做&操作,对函数指针做*操作,让它们看上去比较像指针的使用。