什么时候应该使用动态内存分配函数而不是直接变量声明?

Posted

技术标签:

【中文标题】什么时候应该使用动态内存分配函数而不是直接变量声明?【英文标题】:When should one use dynamic memory allocation function versus direct variable declaration? 【发布时间】:2015-09-27 14:47:42 【问题描述】:

下面是一个直接变量声明的例子。

double multiplyByTwo (double input) 
  double twice = input * 2.0;
  return twice;

以下是动态内存分配的示例。

double *multiplyByTwo (double *input) 
  double *twice = malloc(sizeof(double));
  *twice = *input * 2.0;
  return twice;

如果可以选择,我会一直使用直接变量声明,因为代码看起来更易读。什么情况下动态内存分配更合适?

【问题讨论】:

基于malloc()的出现,看起来更倾向于C @Jite 怎么需要动态内存分配? 【参考方案1】:

“如果可以选择,我会一直使用直接变量声明”

你也应该这样做。除非需要,否则不要使用堆内存。这显然引出了一个问题:我什么时候需要动态内存?

堆栈空间有限,如果您需要更多空间,则必须自己分配(想想大数组,如struct huge_struct array[10000])。了解堆栈有多大see this page。请注意,实际堆栈大小可能会有所不同。 C 传递参数,并按值返回值。如果要返回一个衰减为指针的数组,最终将返回一个指向超出范围(无效)的数组的指针,从而导致 UB。像这样的函数应该分配内存并返回一个指向它的指针。 当您需要更改某些内容的大小时 (realloc),或者您不知道需要多少内存来存储某些内容。您在堆栈上声明的数组大小固定,指向内存块的指针可以重新分配(malloc 新块 >= 当前块大小 + memcpy + free 原始指针基本上是realloc 做了什么) 当某块内存需要在各种函数调用中保持有效时。在某些情况下,全局变量不会这样做(想想线程)。此外:在几乎所有情况下,全局变量都被视为不好的做法。 共享库通常使用堆内存。这是因为他们的作者不能假设他们的代码将有大量可用的堆栈空间。如果你想写一个共享库,你可能会发现自己要写很多内存管理代码

所以,一些例子来澄清:

//perfectly fine
double sum(double a, double b)

    return a + b;

//call:
double result = sum(double_a, double_b);
//or to reassign:
double_a = (double_a, double_b);
//valid, but silly
double *sum_into(double *target, double b)

    if (target == NULL)
        target = calloc(1, sizeof *target);
    *target = b;
    return target;

//call
sum_into(&double_a, double_b);//pass pointer to stack var
//or allocate new pointer, set to value double_b
double *double_a = sum_into(NULL, double_b);
//or pass double pointer (heap)
sum_into(ptr_a, double_b);

返回“数组”

//Illegal
double[] get_double_values(double *vals, double factor, size_t count)

    double return_val[count];//VLA if C99
    for (int i=0;i<count;++i)
        return_val[i] = vals[i] * factor;
    return return_val;

//valid
double *get_double_values(const double *vals, double factor, size_t count)

    double *return_val = malloc(count * sizeof *return_val);
    if (return_val == NULL)
        exit( EXIT_FAILURE );
    for (int i=0;i<count;++i)
        return_val[i] = vals[i] * factor;
    return return_val;

必须调整对象的大小:

double * double_vals = get_double_values(
    my_array,
    2,
    sizeof my_array/ sizeof *my_array
);
//store the current size of double_vals here
size_t current_size = sizeof my_array/ sizeof *my_array;
//some code here
//then:
double_vals = realloc(
    double_vals,
    current_size + 1
);
if (double_vals == NULL)
    exit( EXIT_FAILURE );
double_vals[current_size] = 0.0;
++current_size;

需要在范围内停留更长时间的变量:

struct callback_params * some_func( void )

    struct callback_params *foo = malloc(sizeof *foo);//allocate memory
    foo->lib_sum = 0;
    call_some_lib_func(foo, callback_func);


void callback_func(int lib_param, void *opaque)

    struct callback_params * foo = (struct callback_params *) opaque;
    foo->lib_sum += lib_param;

在这种情况下,我们的代码正在调用一些异步处理某些内容的库函数。我们可以传递一个回调函数来处理库内容的结果。该库还为我们提供了一种通过void *opaque 将一些数据传递给该回调的方法。

call_some_lib_func 将有如下签名:

void call_some_lib_func(void *, void (*)(int, void *))

或者以更易读的格式:

void call_some_lib_func(void *opaque, void (*callback)(int, void *))

所以它是一个名为 call_some_lib_func 的函数,它接受 2 个参数:一个名为 opaquevoid * 和一个指向返回 void 的函数的函数指针,并接受一个 int 和一个 void * 作为参数。

我们需要做的就是将void * 转换为正确的类型,然后我们就可以对其进行操作了。另请注意,some_func 返回一个指向不透明指针的指针,因此我们可以在需要的任何地方使用它:

int main ( void )

    struct callback_params *params = some_func();
    while (params->lib_sum < 100)
        printf("Waiting for something: %d%%\r", params->lib_sum);
    puts("Done!");
    free(params);//free the memory, we're done with it
    //do other stuff
    return 0;

【讨论】:

【参考方案2】:

使用 malloc 进行动态内存分配将内存放在堆上,因此在离开函数时它不会被破坏。

稍后您需要手动释放内存。

直接声明落在堆栈上,并在离开函数时被删除。 return 语句发生的情况是在变量被销毁之前创建了一个副本。

考虑这个例子:

堆上

void createPeople():
    struct person *p = makePerson();
    addToOffice(p);
    addToFamily(p);

对比。在堆栈上

void createPeople():
    struct person p = makePerson();
    addToOffice(p);
    addToFamily(p);

在第一种情况下,只创建一个人并将其添加到办公室和家庭中。现在如果这个人被删除,在办公室和家庭中都将失效,而且,如果他的数据发生了变化,那么两者都会发生变化。

在第二种情况下,会为办公室和家庭创建此人的副本。现在可能发生您更改办公室副本的数据而家庭副本保持不变的情况。

所以基本上如果你想让多方访问同一个对象,它应该在堆栈上。

【讨论】:

您的结构示例不是堆分配的可靠示例:struct person p; make_person(&amp;p); 和后来的add_to_family(&amp;p); 不会有问题,您可以将指针传递给堆栈变量 感谢您的意见。我更改了代码,使其位于函数中。现在我们真的需要一个堆分配,否则离开createPeople时它会超出范围@【参考方案3】:

什么情况下动态内存分配更合适?

当编译时不知道分配大小时,我们需要使用动态内存分配。

除了上面的情况,还有一些其他的场景,比如

    如果我们想要一个在运行时re-sizeable的数据结构,我们需要进行动态内存分配。

    动态分配内存的生命周期保持有效,除非它是free()d。有时,在从函数调用返回变量的某个地址时,它会派上用场,否则,使用 auto 变量会超出范围。

    通常堆栈大小会受到适度限制。如果要创建和使用一个 huge 数组,最好使用动态内存分配。这将从堆中分配内存。

【讨论】:

先生,我有一个疑问。我在编写代码时总是使用这种技术。请参阅此ideone.com/LhRL4o。如果我用 malloc 代替会更好吗? @ARBY int arr[n]; 称为 VLA,可能并非所有编译器都支持。只要您有一个带有 VLA 的编译器,并且不需要在其定义的范围之外使用 VLA 的数组,就可以了。【参考方案4】:

当您打算将数据传输到本地范围(例如函数)之外时,需要动态内存分配。 此外,当您无法提前知道需要多少内存时(例如用户输入)。 最后,当您确实知道所需的内存量但它会溢出堆栈时。 否则,出于可读性、运行时开销和安全性的考虑,您不应使用动态内存分配。

【讨论】:

以上是关于什么时候应该使用动态内存分配函数而不是直接变量声明?的主要内容,如果未能解决你的问题,请参考以下文章

结构体变量 和 结构体指针

类和动态内存分配

c语言中啥是动态分配内存?

第十二章 类和动态内存分配

C++动态内存

C - 声明变量并调用 malloc