An advantage of C programming languages over other languages is that the user may optimize and be creative with memory management, whereas in Python or R, the memory management is done automatically. For high-performance computing users, it allows users to optimize their programs to the extreme and efficiently use their HPC resources. In this section, we will introduce the process of dynamic memory allocation.

In C, arguments to functions are passed by value. When we want to pass a large C structure into a function, that value is copied in the memory for the following function. If many functions take the same data as arguments, the same data will occupy several locations in memory, which is undesirable for performance and memory usage. To avoid this redundancy, we can use dynamic memory allocation. The idea is to allocate a chunk of data in memory of just the right size to store this data. Instead of passing the data to functions, we pass a pointer to the data. This reduces the overhead of copying large amounts of data and ensures only one copy exists in the memory. As a result, the data is now shared between all functions, and all changes are shared. Memory usage is more efficient because only one instance of the data exists. Dynamic memory allocation involves assigning memory as needed during program execution. In contrast, static memory allocation assigns memory at compile time, with the memory lasting until the program or scope ends. With the correct implementation of memory management, the C program may see significant performance improvement.

Here is how to allocate memory and work with pointers:

Suppose we want to allocate enough memory for an integer array of size 10. We use the function malloc to dynamically allocate memory. Although other memory management functions exist, malloc is the most common. The argument to malloc is the size of the memory to be allocated, which can be considered as the number of items times the size of each item. For convenience, the sizeof function can be used to calculate the size of types or structures. If malloc is successful, it will return a generic pointer void *. In this example, we want to cast it to int * , so we place (int *) in front to correctly cast the pointer to the right type.

We should always check malloc is successful, as there are several scenarios where allocation might fail. To confirm if arr has a pointer after malloc, check if the arr is NULL.

We can assign values to arr just like statically allocated arrays.

When arr is no longer needed, we should call free on arr to free the memory, so other data may be allocated in its place.

Although memory management in C is beneficial, there are some common pitfalls.

  1. Using malloc when it's not necessary: In the example above, we used a small array as a demonstration, and arr was not passed to any functions or used in any meaningful way. Memory allocation, in this case, would be impractical, as none of its advantages were utilized.
  2. We must check if memory allocation is successful: If there is not enough memory or if memory fragmentation (there are no contiguous blocks of memory that can fit your data) occurs, then memory allocation will fail, and the program will crash upon referencing a NULL pointer.
  3. Always call free on unused pointers, as too many unfreed memory allocations can cause a memory leak. In addition, referencing the pointer after freeing, freeing a pointer twice, and accessing memory beyond the bounds will result in errors and unexpected behaviors.
 
©  |   Cornell University    |   Center for Advanced Computing    |   Copyright Statement    |   Access Statement