Memory Management in C: Tips & Best Practices

A guide to reliable and memory efficient C code

Ashish Abraham
Level Up Coding

--

Photo by Tim Gouw on Unsplash

Memory management is a critical aspect of programming, often overlooked by beginners but deeply appreciated by seasoned developers. It’s the heart of any software system and plays a pivotal role in determining the efficiency and effectiveness of software. Mastering it is essential to enhance the performance of your applications and prevent memory issues.

Each programming language has its unique approach to memory management, ranging from the automated garbage collection seen in Java and Python to the manual control required in languages Rust. Unlike other languages, Rust keeps things pretty strict when it comes to memory.

In this blog, we will comprehensively cover how C manages memory, common mistakes committed by programmers, and best practices to avoid them. Whether you are a novice or a veteran in this field, this is for you.

Memory Model in C

C program allocates memory primarily in 3 ways.

  1. Static Memory Allocation: All variables declared as static or global are allocated a fixed block of memory statically. These locations are fixed and are not deallocated during the execution of the program.
  2. Dynamic Memory Allocation: System memory is managed at runtime. Variables are allocated and deallocated as the program executes. It is achieved using functions like malloc and calloc in C.
  3. Automatic Memory Allocation: Memory is managed automatically by C. Normal variable declarations we do in C are by default automatic. The memory space for an automatic variable is reserved upon entering the block of code where it’s declared. This space is then released back to the system once the execution exits this block of code.

Functions for Memory Management

C offers various functions for memory management, especially for dynamic allocation and deallocation. Both <stlib.h> and <string.h> libraries offer various functions.

malloc

void *malloc(size_t size);

It is used to allocate a specific amount of memory during the execution of a program. The malloc function returns a pointer to the allocated memory, or NULL if the request fails. It’s important to remember to free any memory allocated with malloc when it’s no longer needed, to prevent memory leaks which we will explore next.

calloc

void *calloc(int num, int size);

It is slightly different from malloc in the sense that it allocates num blocks with num*size bytes of memory. Each byte is initialized to 0. This function is particularly useful when you’re working with arrays or other data structures and want to ensure they’re initially empty or zeroed out.

realloc

void *realloc(void *ptr, size_t newsize);

The realloc function in C is a dynamic memory management function that adjusts the size of previously allocated memory. It is used when you need to change the size of a block of memory that was allocated earlier using malloc or calloc. The realloc function takes two arguments: a pointer to the memory block and the new size for that memory. If the function is successful, it returns a pointer to the newly resized block of memory, which may be in a different location than the original block. It’s important to note that if the new size is larger, the additional memory is not initialized and if the new size is smaller, it truncates the existing memory contents to its length.

free

void free(void *ptr);

This function frees or deallocates the memory allocated to the pointer using malloc or calloc .

memcpy

void *memcpy(void *to, const void *from, size_t numBytes);

The memcpy function in <string.h> is a function that copies a block of memory from one location to another. It takes three arguments: a destination pointer, a source pointer, and the number of bytes to copy. The function is very efficient and is often used for moving larger chunks of data, such as when duplicating arrays. However, it doesn’t check for overlap between the source and destination, so care must be taken to avoid undefined behavior.

memove

void *memove(void *to, const void *from, size_t numBytes);

The memmove function in C is a standard library function that copies a block of memory from one location to another, similar to memcpy. However, memmove is safe to use when the source and destination memory blocks overlap, as it ensures that the copy is performed correctly in such cases. It takes three arguments: a destination pointer, a source pointer, and the number of bytes to copy.

memset

void* memset( void* ptr, int ch, size_t n);

The memset function is used to set a block of memory to a specific value. It takes three arguments: a pointer to the block of memory, the value to be set, and the number of bytes to set. This function is particularly useful when you need to initialize an array or a structure to a certain value, often zero. It’s important to note that memset works with bytes, so it’s not suitable for setting memory with a value that’s larger than a byte. It is also defined in the <string.h> header file.

Usual Missteps

Dangling Pointers

A dangling pointer is a pointer that doesn’t point to a valid memory location. It usually occurs when an object is deleted or deallocated, without modifying the value of the pointer, so it still points to the memory location of the deallocated memory. As the memory location is no longer valid, accessing or manipulating objects through dangling pointers can lead to unpredictable results and program crashes.

int *ptr = (int*)malloc(sizeof(int));
free(ptr);
int val = *ptr; // Undefined behavior!

In this code, ptr becomes a dangling pointer after we call free(ptr). The subsequent attempt to access the memory location that ptr points to (int val = *ptr;) results in undefined behavior because the memory has already been deallocated. This is a simple example of how a dangling pointer can be created and why it can be problematic.

Double Free

As the name suggests, double free refers to deallocating an allocated memory twice with the free statement.

char* ptr= malloc(3);
free(ptr);
free(ptr);

The second free usually results in a segmentation fault as it is an undefined behavior.

Memory Leak

A memory leak occurs when an allocated block cannot be recovered or freed.

char* str = malloc(10);
strcpy(str, "leak");
str = "Here is a memory leak!";

In this case, we have lost access to the string “leak” as we have lost access to its pointer. Freeing that memory block is not possible now, indicating a memory leak.

Golden Rules

Keep track of memory allocations 👌

Ensure that all data blocks that we allocate using malloc is freed when no longer used. For more complex data structures or when allocating memory in loops or functions, it’s recommended to keep track of all allocated memory in a centralized location. This can be done using data structures like linked lists or arrays of pointers. Or use flags to keep track of allocated memory.

Avoiding Dangling Pointers

Dangling pointers can be avoided by making it a practice to set freed pointers to null. In the discussed code snippet, adding one more line solves the problem.

char* ptr= malloc(3);
free(ptr);
ptr = NULL;

Use dynamic memory allocation only when necessary

As evident, dynamic allocation of memory requires a lot of overhead for maintaining reliability. In coding standards like MISRA, it is recommended to avoid dynamic allocation whenever possible.

Use dynamic memory allocation advisably in scenarios like,

  1. Variable-sized data structures: When you need to allocate memory for data structures with a variable size that is not known at compile-time, such as linked lists, trees, or dynamic arrays, dynamic memory allocation is necessary.
  2. Large data structures: If you need to allocate a large amount of memory that is not feasible to store on the stack (due to stack size limitations), dynamic memory allocation from the heap is the way to go.
  3. Long-lived data: If you have data that needs to persist for a long time or across multiple function calls, dynamically allocating memory on the heap can be more appropriate than using automatic variables on the stack.
  4. Returning data from functions: If you need to return a dynamically-sized data structure from a function, you’ll need to allocate memory dynamically and return a pointer to it.
  5. Performance optimization: In some cases, dynamically allocating memory can provide better performance than using statically allocated memory, especially when dealing with large data structures or when the memory requirements are not known at compile time.

Avoid using unsafe library functions

Although deprecated in the latest versions, it is worth mentioning unsafe library functions like gets(). Avoid using them as they have inherent security vulnerabilities like,

  1. Buffer Overflow: The gets() function does not perform any bounds checking on the input string. This means that if the user input is longer than the allocated buffer size, it will cause a buffer overflow. Buffer overflows can lead to data corruption, crashes, and even potential security vulnerabilities if the overwritten memory contains sensitive information or code.
  2. No Length Limitation: The gets() function reads input until it encounters a newline character (\n) or an end-of-file condition. This means that it can potentially read an arbitrarily large amount of data, leading to excessive memory consumption and potential vulnerabilities.

Use safer alternatives like std:getline() or fgets() .

Testing and Code Reviews

Implement comprehensive unit tests to verify the correctness of your memory management code. Perform code reviews to identify potential issues related to memory management, including double frees, memory leaks, and dangling pointers.
Static code analysis tools are software programs that analyze source code without actually executing it. They automatically detect potential memory management issues in your codebase. Cpplint and Cppcheck are two examples of such tools.

The guidelines presented in this article serve as a basic outline for crafting reliable and efficient C code. Hope you found the article useful. Feel free to comment and connect with me regarding any discrepancies or new approaches that you may find. 😊

References

--

--