为什么或何时需要在C中动态分配内存



动态内存分配是C编程中一个非常重要的主题。然而,我一直无法找到一个很好的解释来解释这使我们能够做什么,或者为什么需要这样做。

难道我们不能只声明变量和结构,而不必使用malloc()吗?

附带说明一下,之间有什么区别

ptr_one = (int *)malloc(sizeof(int));

int *ptr_one = malloc(sizeof(int));

时需要使用动态内存

  • 您无法确定编译时要使用的最大内存量
  • 您想要分配一个非常大的对象
  • 您希望构建没有固定上限的数据结构(容器)

您并不总是知道在编译时需要留出多少内存。想象一下,处理一个数据文件(比如温度的时间序列),其中文件中的记录数量是不固定的。你可能只有10张唱片,也可能多达10万张。如果您想将所有数据读取到内存中进行处理,那么在读取文件之前,您将不知道要分配多少内存。如果文件的结构使得第一个值是记录的数量,那么可以这样做:

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]
      }
    }
  }
}

最后,动态内存允许您构建容器,这些容器可以在添加或删除数据时增长和收缩,如列表、树、队列等。您甚至可以构建自己的真实"字符串"数据类型,这些类型可以在向其添加字符时增长(类似于C++中的string类型)。

当您不知道内存的最坏情况要求时,需要动态分配。然后,不可能静态地分配必要的内存,因为您不知道需要多少内存。

即使您知道最坏情况下的需求,也可能仍然需要使用动态内存分配。它允许多个进程更有效地使用系统内存。所有进程都可以静态地提交其最坏情况下的内存需求,但这限制了系统上可以存在的运行进程的数量。如果所有进程都不同时使用最坏的情况,那么系统内存总是未充分利用,这是对资源的浪费。

至于你的附带问题,你不应该在C中强制转换对malloc()的调用的结果。它可以隐藏丢失声明的错误(在C.99之前允许隐式声明),并导致未定义的行为。总是喜欢在不带强制转换的情况下获取malloc()的结果。malloc()被声明为返回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.
}

在第CCD_ 11行注意到,如果这是在堆栈上定义的,那么一旦弹出该堆栈帧,就不再分配该阵列的所有存储器地址。也就是说,另一个堆栈帧(可能是下一个)将使用这些内存地址(或其某个子集)。

备注:根据我的代码片段,ptr_block存储在堆栈上:因此&ptr_block是堆栈地址,但ptr_block的值在内存池的某个位置。

相关内容

  • 没有找到相关文章

最新更新