分享

动态内存分配

 @IT小小鸟@ 2012-02-20


7.4  内存的使用

指针是一个非常灵活且强大的编程工具,有非常广泛的应用。大多数C程序都在某种程度上使用了指针。C语言还进一步增强了指针的功能,为在代码中使用指针提供了很强的激励机制,它允许在执行程序时动态分配内存。只有使用指针,才能动态分配内存。

第5章的一个程序计算一组学生的平均分,当时它只处理10个学生。假设要编写一个程序,但事先不知道要处理多少个学生,若使用动态内存分配(dynamic memory allocation),所使用的内存就不会比指定的学生分数所需的内存多。可以在执行时创建足以容纳所需数据量的数组。

在程序的执行期间分配内存时,内存区域中的这个空间称为堆(heap)。还有另一个内存区域,称为堆栈(stack),其中的空间分配给函数的参数和本地变量。在执行完该函数后,存储参数和本地变量的内存空间就会释放。堆中的内存是由程序员控制的。如本章后面所述,在分配堆上的内存时,由程序员跟踪所分配的内存何时不再需要,并释放这些空间,以便于以后重用它们。

7.4.1  动态内存分配:malloc()函数

在运行时分配内存的最简单的标准库函数是malloc()。使用这个函数时,需要在程序中包含头文件。使用malloc()函数需指定要分配的内存字节数作为参数。这个函数返回所分配内存的第一个字节的地址。因为返回的是一个地址,所以这里可以使用指针。

动态内存分配的一个例子如下:

int *pNumber = (int *)malloc(100); 

这条语句请求100个字节的内存,并把这个内存块的地址赋予pNumber。只要不修改它,任何时间使用这个变量pNumber,它都会指向所分配的100个字节的第一个int的位置。这个内存块能保存25个int值,每个int占4个字节。

注意,类型转换(int*)将函数返回的地址转换成int类型的指针。这么做是因为malloc()是一般用途的函数,可为任何类型的数据分配内存。这个函数不知道要这个内存作什么用,所以它返回的是一个void类型的指针,写做void*。类型void*的指针可以指向任意类型的数据,然而不能取消对void指针的引用,因为它指向未具体说明的对象。许多编译器会把malloc()返回的地址自动转换成适当的类型,且不会伤害具体指定的对象。

可以请求任意数量的字节,字节数仅受制于计算机中未用的内存以及malloc()的运用场合。如果因某种原因而不能分配请求的内存,malloc()会返回一个NULL指针。这个指针等于0。最好先用if语句检查请求动态分配的内存是否已分配,再使用它。就如同金钱,没钱又想花费,会带来灾难性的后果。因此,应编写如下语句:

if(pNumber == NULL)
{
/*Code  to  deal  with  no  memory  allocated  */
}

如果指针是NULL,最好执行适当的操作。例如,至少可以显示一条信息"内存不足",然后中止程序。这比允许程序继续执行,使之使用NULL地址存储数据导致崩溃要好得多。然而,在某些情况下,可以释放在别的地方使用的内存,以便程序有足够的内存继续执行下去。

7.4.2  分配内存时使用sizeof运算符

前一个例子很不错,但我们不常处理字节,而常常处理int、double等数据类型。例如给75个int类型的数据项分配内存,可以使用以下的语句:

pNumber = (int *) malloc(75*sizeof(int)); 

如前所述,sizeof是一个运算符,它返回一个size_t类型的无符号整数,该整数是存储它的参数需要的字节数。它把关键字如int或float等作为参数,返回存储该类型的数据项所需的字节数。它的参数也可以是变量或数组名。把数组名作为参数时,sizeof返回存储整个数组所需的字节数。前一个例子请求分配足以存储75个int数据项的内存。以这种方式使用sizeof,可以根据不同的C编译器为int类型的值自动调整所需的内存空间。

试试看:动态内存分配

下面使用指针来计算质数,将动态内存分配的概念应用于实践。质数是只能被1和这个数本身整除的整数。

查找质数的过程非常简单。首先,由观察得知,2、3和5是前三个质数,因为它们不能被除了1以外更小的数整除。其他质数必定都是奇数(否则它们可以被2整除),所以要找出下一个质数,可以从最后一个质数开始,给它加2。检查完这个数后,再给它加2,继续检查。

检查一个数是否为质数,而不只是奇数,可以用这个数除以比它小的所有奇数。其实不需要这么麻烦。如果一个数不是质数,它必定能被比它小的质数整除。我们要按顺序查找质数,所以可以把已经找到的质数作为除数,确定所检查的数是否为质数。

这个程序将使用指针和动态内存分配:

/* Program 7.11 A dynamic prime example */
#include
#include
#include
int main(void)
{
unsigned long *primes = NULL; /* Pointer to primes storage area */
unsigned long trial = 0;       /* Integer to be tested */
bool found = false;            /* Indicates when we find a prime */
size_t total = 0;              /* Number of primes required */
size_t count = 0;              /* Number of primes found */
printf("How many primes would you like - you'll get at least 4? ");
scanf("%u", &total); /* Total is how many we need to find */
total = total<4U ? 4U:total; /* Make sure it is at least 4 */
/* Allocate sufficient memory to store the number of primes required */
primes = (unsigned long *)malloc(total*sizeof(unsigned long));
if(primes == NULL)
{
printf(" Not enough memory. Hasta la Vista, baby. ");
return 1;
}
/* We know the first three primes */
/* so let's give the program a start. */
*primes = 2UL;      /* First prime */
*(primes+1) = 3UL;  /* Second prime */
*(primes+2) = 5UL;  /* Third prime */
count = 3U;         /* Number of primes stored */
trial = 5U;         /* Set to the last prime we have */
/* Find all the primes required */
while(count
{
trial += 2UL;                 /* Next value for checking */
/* Try dividing by each of the primes we have */
/* If any divide exactly - the number is not prime */
for(size_t i = 0 ; i < count ; i++)
if(!(found = (trial % *(primes+i))))
break; /* Exit if no remainder */
if(found) /* we got one - if found is true */
*(primes+count++) = trial; /* Store it and increment count */
}
/* Display primes 5-up */
for(size_t i = 0 ; i < total ; i ++)
{
if(!(i%5U))
printf(" ");                /* Newline after every 5 */
printf ("%12lu", *(primes+i));
}
printf(" ");                     /* Newline for any stragglers */
return 0;
}
程序的输出如下:
How many primes would you like - you'll get at least 4? 25
2       3      5       7     11
13     17     19     23     29
31     37     41     43     47
53     59     61     67     71
73     79     83     89     97

代码的说明

在这个例子中,可以输入要程序产生的质数个数。指针变量primes引用一块用于存储所计算的质数的内存区。然而,在程序中没有一开始就定义内存。这块空间是在输入质数个数后分配的:

printf("How many primes would you like - you'll get at least 4? ");
scanf("%u", &total); /* Total is how many we need to find */
total = total<4U ? 4U:total; /* Make sure it is at least 4 */

在提示后,输入的值存储在total中。下一行语句确保total至少是4。这是因为程序将定义并存储已知的前三个质数。

然后,使用total的值分配适当数量的内存来存储质数:

primes = (unsigned long *)malloc(total*sizeof(unsigned long));
if(primes == NULL)
{
printf(" Not enough memory. Hasta la Vista, baby. ");
return 1;
}

质数的大小增长得比其数量快,所以把它们存储在unsigned long类型中。但如果要指定可以处理的最大质数,可以使用unsigned long long类型。程序把每个质数存储为类型long,所以需要的字节数是total*sizeof(unsigned long)。如果malloc()函数返回NULL,就不分配内存,而是显示一条信息,并结束程序。

可以指定最大的质数个数取决于计算机的可用内存和编译器使用malloc()一次能分配的内存量,前者是主要的限制。malloc()函数的参数是size_t类型,所以size_t对应的整数类型限制了可以指定的字节数。如果size_t对应4字节的无符号整数,则一次至多可以分配4 294 967 295个字节。

一旦有了分配给质数的内存,就定义前三个质数,将它们存储到primes指针指向的内存区的前三个位置:

*primes = 2UL;      /* First prime */
*(primes+1) = 3UL;  /* Second prime */
*(primes+2) = 5UL;  /* Third prime */

可以看到,引用连续的内存位置是很简单的。primes是unsigned long类型的指针,所以primes+1引用第二个位置的地址-- 这个地址是primes加上存储一个unsigned long类型数据项所需的字节数。使用间接运算符存储每个值;否则就要修改这个地址本身。

有了三个质数,就把count变量设定为3,用最后一个质数5初始化变量trial:

count = 3U; /* Number of primes stored */
trial = 5U; /* Set to the last prime we have */

开始查找下一个质数时,给trial中的值加2,得到下一个要测试的数。所有的质数都在while循环内查找:
while(count
{
...
}

在循环内每找到一个质数,就递增count变量,当它到达total值时,循环就结束。

在while循环内,首先将trial的值加2UL,然后测试它是否是质数:

trial += 2UL; /* Next value for checking */
/* Try dividing by each of the primes we have */
/* If any divide exactly - the number is not prime */
for(size_t i = 0 ; i < count ; i++)
if(!(found = (trial % *(primes+i))))
break; /* Exit if no remainder */

for循环用于测试。在这个循环内,把trial除以每个质数的余数存放到found中。如果除尽,余数就是0,因此found设置为false。如果余数是0,就表示trial中的值不是质数,可以继续测试下一个数。

赋值表达式的值存储到赋值运算符左边的变量中。因此,表达式(found= (trial %*(primes+i))) 的结果存储到found中。如果除尽,found就是false,表达式!(found=(trial%*(primes+i)))将是true,执行break语句。因此,如果trial能整除任一个先前存储的质数,for循环就会结束。

如果没有一个质数除trial是整除,当所有的质数都试过后,就结束for循环,found的结果是把最后一个余数(它是某个正整数)转换为bool类型的值。如果trial能被某个质数整除,循环会通过break语句结束,found会含有false。因此,可以在完成for循环时,使用存储在found中的值确定是否找到一个新的质数:

if(found)                        /* we got one - if found is true */
*(primes+count++) = trial;  /* Store it and increment count */

如果found是true,就将trial的值存储到内存区的下一个位置上。下一个位置的地址是primes+count。第一个位置是primes,所以当有count个质数时,最后一个质数所占的位置是primes+count-1。这个语句存储了新的质数后,递增count的值。

while循环重复这个过程,直到找出所有的质数为止。然后,以5个一行输出质数:

for(size_t i = 0 ; i < total ; i ++)
{
if(!(i%5U))
printf(" "); /* Newline after every 5 */
printf ("%12lu", *(primes+i));
}
printf(" ");     /* Newline for any stragglers */
for循环会输出total个质数。printf()函数在当前行上显示每个质数,但if语句在5次迭代后输出一个换行符,所以每行显示5个质数。因为质数的个数不会刚好是5的倍数,所以在结束循环后,输出一个换行符,以确保在输出的最后至少有一个换行符。

7.4.3  用calloc()函数分配内存

头文件中声明的calloc()函数与malloc()函数相比有两个优点。第一,它把内存分配为给定大小的数组,第二,它初始化了所分配的内存,所有的位都是0。calloc()函数需要两个参数:数组的元素个数和数组元素占用的字节数,这两个参数的类型都是size_t。该函数也不知道数组元素的类型,所以所分配区域的地址返回为void *类型。

下面的语句使用calloc()为包含75个int元素的数组分配内存:

int *pNumber = (int *) calloc(75, sizeof(int));

如果不能分配所请求的内存,返回值就是NULL,也可以检查分配内存的结果,这非常类似于malloc(),但calloc()分配的内存区域都会初始化为0。

将程序7.11改为使用calloc()代替malloc()来分配需要的内存,只需修改一条语句,如下面的粗体显示,其他代码不变:

/* Allocate sufficient memory to store the number of primes required */

primes = (unsigned long *)calloc(total, sizeof(unsigned long));

if (primes == NULL)

{

printf(" Not enough memory. Hasta la Vista, baby. ");

return 1;

}

7.4.4  释放动态分配的内存

在动态分配内存时,应总是在不需要该内存时释放它们。堆上分配的内存会在程序结束时自动释放,但最好在使用完这些内存后立即释放,甚至是在退出程序之前,也应立即释放。在比较复杂的情况下,很容易出现内存泄漏。当动态分配了一些内存时,没有保留对它们的引用,就会出现内存泄漏,此时无法释放内存。这常常发生在循环内部,由于没有释放不再需要的内存,程序会使用越来越多的内存,最终占用所有内存。

当然,要释放用malloc()或calloc()分配的内存,必须使用函数返回的引用内存块的地址。要释放动态分配的内存,而该内存的地址存储在pNumber指针中,可以使用下面的语句:

free(pNumber);

free()函数的形参是void *类型,所有指针类型都可以自动转换为这个类型,所以可以把任意类型的指针作为参数传送给这个函数。只要pNumber包含分配内存时malloc()或calloc()返回的地址,就会释放所分配的整个内存块,以备以后使用。

如果给free()函数传送一个空指针,该函数就什么也不做。应避免两次释放相同的内存区域,因为在这种情况下,free()函数的操作是不确定的,因此也就无法预料。如果多个指针变量引用已分配的内存,就有可能两次释放相同的内存,所以要特别小心。

下面修改前面的例子,使用calloc(),并在程序的最后释放内存。

试试看:释放动态分配的内存

这个程序使用指针和动态分配的内存:

/* Program 7.11A Allocating and freeing memory */
#include
#include
#include

int main(void)
{
unsigned long *primes = NULL;  /* Pointer to primes storage area */
unsigned long trial = 0;       /* Integer to be tested */

bool found = false;            /* Indicates when we find a prime */
size_t total = 0;              /* Number of primes required */
size_t count = 0;              /* Number of primes found */

printf("How many primes would you like - you'll get at least 4? ");
scanf("%u", &total); /* Total is how many we need to find */
total = total<4U ? 4U:total; /* Make sure it is at least 4 */

/* Allocate sufficient memory to store the number of primes required */
primes = (unsigned long *)calloc(total, sizeof(unsigned long));
if (primes == NULL)
{
printf(" Not enough memory. Hasta la Vista, baby. ");
return 1;
}

/* Code to determine the primes as before...*/
/* Display primes 5-up */
for(int i = 0 ; i < total ; i ++)
{
if(!(i%5U))
printf(" "); /* Newline after every 5 */
printf ("%12lu", *(primes+i));
}
printf(" "); /* Newline for any stragglers */

free(primes); /* Release the memory */
return 0;
}

如果输入相同,这个程序的输出与上一个版本相同。只要两行粗体显示的代码与上一个版本不同。程序现在使用calloc()来分配内存,该函数的第一个参数是long类型的字节数,第二个参数是total,即需要的质数个数。在结束程序的return语句之前,用primes作为参数调用free()函数,释放了分配的内存。

7.4.5  重新分配内存

realloc()函数可以重用前面通过malloc()或calloc() (或realloc())分配的内存。函数需要两个参数:一个是指针,它包含前面调用malloc()、calloc()或realloc()返回的地址,另一个是要分配的新内存的字节数。

realloc()函数释放第一个指针参数引用的之前分配的内存,然后重新分配该内存区域,以满足第二个参数指定的新请求。显然,第二个参数的值不应超过以前分配的字节数。否则,新分配的内存将与以前分配的内存区域大小相同。

下面的代码演示了如何使用realloc()函数:

long *pData = NULL;     /* Stores the data */
size_t count = 0;       /* Number of data items */
size_t oldCount = 0;    /* previous count value */
while(true)
{
oldCount = count;     /* Save previous count value */
printf("How many values would you like? ");
scanf("%u", &count);  /* Total is how many we need to find */
if(count == 0)        /* If none required, we are done */
{
if(!pData)          /* If memory is allocated */
free(pData);       /* release it */
break;              /* Exit the loop */
}
/* Allocate sufficient memory to store count values */
if((pData && (count <= oldCount) /* If there's big enough old memory... */
pData = (long *)realloc(pData, sizeof(long)*count); /* reallocate it. */
else
{                    /* There wasn't enough old memory */
if(pData)           /* If there's old memory... */
free(pData);       /* release it. */
/* Allocate a new block of memory */
pData = (long *)calloc(count, sizeof(long));
}
if (pData == NULL)   /* If no memory was allocated... */
{
printf(" Not enough memory. ");
return 1;          /* abandon ship! */
}
/* Read and process the data and output the result... */
}

很容易通过注释理解这段代码。循环读取任意个由用户提供的数据项,如果以前分配过内存空间,且该空间足以满足新请求,就再次使用该空间。如果以前没有分配过内存空间,或空间不够大,代码就使用calloc()分配一块新内存。

从这段代码中可以看出,重新分配内存需要做许多工作,因为一般需要确保已有的内存块足以满足新请求。在大多数情况下,最好明确释放旧内存块,再分配一块全新的内存。

下面是使用动态分配的内存的基本规则:

●避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大。

●仅在需要时分配内存。只要使用完堆上的内存块,就释放它。

●总是确保释放已分配的内存。在编写分配内存的代码时,就要确定在代码的什么地方释放内存。

●在释放内存之前,确保不会无意中覆盖堆上分配的内存的地址,否则程序就会出现内存泄漏。在循环中分配内存时,要特别小心。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多