动态内存分配是 C 编程中一个非常重要的话题。但是,我一直无法很好地解释这使我们能够做什么,或者为什么需要它。
难道我们不能只声明变量和结构而不必使用 malloc() 吗?
作为旁注,有什么区别:
ptr_one = (int *)malloc(sizeof(int));
和
int *ptr_one = malloc(sizeof(int));
动态内存分配是 C 编程中一个非常重要的话题。但是,我一直无法很好地解释这使我们能够做什么,或者为什么需要它。
难道我们不能只声明变量和结构而不必使用 malloc() 吗?
作为旁注,有什么区别:
ptr_one = (int *)malloc(sizeof(int));
和
int *ptr_one = malloc(sizeof(int));
您需要在以下情况下使用动态内存:
您并不总是知道在编译时需要留出多少内存。想象一下处理一个数据文件(比如温度的时间序列),其中文件中的记录数不固定。您可以拥有少至 10 条记录或多至 100000 条记录。如果要将所有数据读入内存以进行处理,则在读取文件之前您不会知道要分配多少内存。如果文件的结构使得第一个值是记录数,您可以执行以下操作:
size_t recs = 0;
double *temps = NULL;
FILE *fp = fopen ( filename, "r" );
if ( fp )
{
if ( fscanf( fp, "%zu", &recs ) == 1 )
{
temps = malloc( sizeof *temps * recs );
if ( temps )
{
// read contents of file into temps
}
}
}
有时您需要分配一个非常大的对象,例如
int ginormous[1000][1000][1000];
假设一个 4 字节整数,这个数组将需要 4GB。不幸的是,堆栈帧(在大多数体系结构中保留局部变量)往往比这小得多,因此尝试分配这么多内存可能会导致运行时错误(并且通常会)。动态内存池(又名堆)通常比堆栈大得多,更不用说任何一个堆栈帧。所以对于令人讨厌的东西,你需要写一些类似的东西
int (*ginormous)[1000][1000] = malloc( sizeof *ginormous * 1000 );
这样的请求仍然有可能失败;如果你的堆足够碎片化,你可能没有一个足够大的连续块来处理请求。如有必要,您可以进行零碎分配;行在内存中不一定是相邻的,但更有可能您能够获取所需的所有内存:
int ***ginormous = malloc( sizeof *ginormous * 1000 );
if ( ginormous )
{
for ( size_t i = 0; i < 1000; i++ )
{
ginormous[i] = malloc( sizeof *ginormous[i] * 1000 );
if ( ginormous[i] )
{
ginormous[i][j] = malloc ( sizeof *ginormous[i][j] * 1000 );
if ( ginormous[i][j] )
{
// initialize ginormous[i][j][k]
}
}
}
}
最后,动态内存允许您构建可以随着您添加或删除数据而增长和缩小的容器,例如列表、树、队列等。您甚至可以构建您自己的真实“字符串”数据类型,它可以随着您的追加而增长字符(类似于string
C++ 中的类型)。
当您不知道内存的最坏情况要求时,需要动态分配。然后,不可能静态分配必要的内存,因为您不知道需要多少。
即使您知道最坏情况的要求,仍可能需要使用动态内存分配。它允许多个进程更有效地使用系统内存。所有进程都可以静态提交其最坏情况下的内存需求,但这限制了系统上可以存在多少正在运行的进程。如果从来没有所有进程同时使用最坏情况的情况,那么系统内存不断地在未充分利用的情况下运行,这是对资源的浪费。
至于您的附带问题,您不应该在 C 中转换调用的结果malloc()
。它可以隐藏缺少声明的错误(在 C.99 之前允许隐式声明),并导致未定义的行为。总是更喜欢在malloc()
没有演员表的情况下获得结果。malloc()
被声明为 return void *
,并且在 C 中,void *
总是允许在和另一个指针类型之间进行转换(模类型限定符,如const
)。
作为旁注,有什么区别:
ptr_one = (int *)malloc(sizeof(int))
和int *ptr_one = malloc(sizeof(int))
看到这个。
首先,我知道这可能是一个荒谬的问题,因为动态内存分配是 C 编程中一个非常重要的话题。但是,我一直无法很好地解释这使我们能够做什么,或者为什么需要它。
与堆栈相比,内存池(或更常见的堆)非常大。考虑以下两个示例,了解为什么在堆栈上使用内存池很有用:
1.如果您定义了一个数组并希望它在多个堆栈帧中持续存在怎么办?当然,您可以将其声明为全局变量,并将其存储在内存的全局数据部分中,但是随着程序变得越来越大,这将变得混乱。或者,您可以将其存储在内存池中。
int *func( int k ) {
assert( k >= 1 );
int *ptr_block = malloc( sizeof( int ) * k );
if ( ptr_block == NULL ) exit( EXIT_FAILURE );
for ( int i = 0; i < k; i++ ) {
ptr_block[ i ] = i + 1;
}
return ptr_block; // Valid.
}
...但是,如果您在堆栈上定义了数组,这将不起作用。原因是,一旦弹出堆栈帧,所有内存地址都可以被另一个堆栈帧使用(并因此被覆盖),而使用内存池中的内存将持续到free
用户(您或客户端)d。
2.如果你想实现一个动态数组来处理读取任意大的数字序列怎么办?您将无法在堆栈上定义数组,您需要使用内存池。回想一下,将指针传递给结构,而不是结构本身(因为它们可能相当大),这是非常常见的(强烈推荐,除非您明确需要复制结构)。考虑这个动态数组的小实现:
struct dyn_array {
int *arr;
int len;
int cap;
};
typedef struct dyn_array *DynArray;
void insert_item( int const item, DynArray dyn_arr ) {
// Checks pre conditions.
assert( dyn_arr != NULL );
// Checks if the capacity is equal to the length. If so, double.
if ( dyn_arr->cap == dyn_arr->len ) {
dyn_arr->cap *= 2;
DynArray new_dyn_arr = malloc( sizeof( int ) * dyn_arr->cap ); // [oo]
// ... copy, switch pointers and free...
}
// ... insert, increase length, etc.
}
...在线[oo]
注意,如果这是在堆栈上定义的,那么一旦弹出此堆栈帧,将不再分配数组的所有内存地址。意思是,另一个堆栈帧(可能是下一个)将使用这些内存地址(或它的某个子集)。
备注:从我的代码片段中,ptr_block
存储在堆栈中:因此&ptr_block
是堆栈地址,但是 的值ptr_block
来自内存池的某个位置。