Search code examples
cmemorymallocdynamic-memory-allocation

When should one use dynamic memory allocation function versus direct variable declaration?


Below is an example of direct variable declaration.

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

Below is an example of dynamic memory allocation.

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

If I had a choice, I will use direct variable declaration all the time because the code looks more readable. When are circumstances when dynamic memory allocation is more suitable?


Solution

  • "If I had a choice, I will use direct variable declaration all the time"

    As well you should. You don't use heap memory unless you need to. Which obviously begs the question: When do I need dynamic memory?

    • The stack space is limited, if you need more space, you'll have to allocate it yourself (think big arrays, like struct huge_struct array[10000]). To get an idea of how big the stack is see this page. Note that the actual stack size may differ.
    • C passes arguments, and returns values by value. If you want to return an array, which decays into a pointer, you'll end up returning a pointer to an array that is out of scope (invalid), resulting in UB. Functions like these should allocate memory and return a pointer to it.
    • When you need to change the size of something (realloc), or you don't know how much memory you'll need to store something. An array that you've declared on the stack is fixed in size, a pointer to a block of memory can be re-allocated (malloc new block >= current block size + memcpy + free original pointer is basically what realloc does)
    • When a certain piece of memory needs to remain valid over various function calls. In certain cases globals won't do (think threading). Besides: globals are in almost all cases regarded as bad practice.
    • Shared libs generally use heap memory. This is because their authors can't assume that their code will have tons of stack space readily available. If you want to write a shared library, you'll probably find yourself writing a lot of memory management code

    So, some examples to clarify:

    //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);
    

    Returning "arrays"

    //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;
    }
    

    Having to resize the object:

    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;
    

    Variables that need to stay in scope for longer:

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

    In this scenario, our code is calling some library function that processes something asynchronously. We can pass a callback function that handles the results of the library-stuff. The lib also provides us with a means of passing some data to that callback through a void *opaque.

    call_some_lib_func will have a signature along the lines of:

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

    Or in a more readable format:

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

    So it's a function, called call_some_lib_func, that takes 2 arguments: a void * called opaque, and a function pointer to a function that returns void, and takes an int and a void * as arguments.

    All we need to do is cast the void * to the correct type, and we can manipulate it. Also note that the some_func returns a pointer to the opaque pointer, so we can use it wherever we need to:

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