Article

Pointers and memory leaks in C

Avoiding the pitfalls

By

Manish Virmani

Introduction

Ask anybody working with C what bothers them the most about C, and many of them will probably answer Pointers and memory leaks. These are truly the items that consume most of the debugging time for developers. Pointers and memory leaks might seem to be deterrents to some programmers but, once you understand the fundamentals of pointers and associated memory operations, they will be the most powerful tool you posses in C.

This article shares the secrets that developers should know before they start programming with pointers. The article covers:

  • What kinds of pointer operations cause memory corruption
  • Checkpoints that must be considered while working with dynamic memory allocation
  • Scenarios that result in memory leaks

If you are aware of what can go wrong beforehand, then you can take care to avoid the pitfalls and get rid of most of the pointers and memory-related problems.

What can go wrong?

Several problematic scenarios can occur that might cause trouble after your build is done. While working with pointers, you can use the information in this article to avoid many of the problems.

Uninitialized memory

In this example, p has been allocated 10 bytes. The 10 bytes might contain garbage data, as shown in Figure 1.

char ∗p = malloc ( 10 );

Figure 1. Garbage data
Garbage data

If a code segment tries to access this p before a value has been assigned to it, it might get that garbage value and your program could behave mysteriously. p might have a value that your program never expected.

A good practice is to always use memset along with malloc, or always use calloc.

char ∗p = malloc (10);
memset(p,’\0’,10);

Now, even if the same code segment tries to access p before a value has been assigned to it and it has proper handling of the Null value (which ideally it should be), then it will behave properly.

Memory overwrite

Since p has been allocated to 10 bytes, if some code snippet tries to write a value to p that is 11 bytes, then this operation will silently, without telling you, eat up one byte from some other location. Let's assume pointer q represents this memory.

Figure 2. Original contents of q
Original contents of q
Figure 3. Overwritten contents of q
Overwritten contents of q

As a result, the pointer q will have contents that were never expected. Even if your module is coded well, it might behave incorrectly due to a coexisting module doing some memory overwriting. The example code snippet below can also explain this scenario.

char ∗name = (char ∗) malloc(11); 
// Assign some value to name
memcpy ( p,name,11); // Problem begins here

In this example, the memcpy operation is trying to write 11 bytes to p, whereas it has been allocated only 10 bytes.

As a good practice, whenever writing values to pointers make sure to cross check the number of bytes available and number of bytes being written. Generally, the memcpy function will be a checkpoint for this.

Memory overread

A memory overread is when the number of bytes being read are more that what they are supposed to be. This is not too serious, so I won't dwell on it. The following code gives an example.

char ∗ptr = (char ∗)malloc(10);
char name[20] ;
memcpy ( name,ptr,20); // Problem begins here

In this example, the memcpy operation is trying to read 20 bytes from ptr, but it has been allocated only 10 bytes. This will also result in undesired output.

Memory leak

Memory leaks can be really annoying. The following list describes some scenarios that result in memory leaks.

  • Reassignment I'll use an example to explain reassignment.
    char ∗memoryArea = malloc(10);
    char ∗newArea = malloc(10);
    This assigns values to the memory locations shown in Figure 4 below.
    Figure 4. Memory locations
    Memory locations memoryArea and newArea have been allocated 10 bytes each and their respective contents are shown in Figure 4. If somebody executes the statement shown below (pointer reassignment )…
    memoryArea = newArea;
    then it will surely take you into tough times in the later stages of this module development. In the code statement above, the developer has assigned the memoryArea pointer to the newArea pointer. As a result, the memory location to which memoryArea was pointing to earlier becomes an orphan, as shown in Figure 5 below. It cannot be freed, as there is no reference to this location. This will result in a memory leak of 10 bytes.
    Figure 5. Memory leak
    Memory leak Before assigning the pointers, make sure memory locations are not becoming orphaned.
  • Freeing the parent block first Suppose there is a pointer memoryArea pointing to a memory location of 10 bytes. The third byte of this memory location further points to some other dynamically allocated memory location of 10 bytes, as shown in Figure 6.
    Figure 6. Dynamically allocated memory
    Dynamically allocated memory
    free(memoryArea)
    If memoryArea is freed by making a call to free, then as a result the newArea pointer also will become invalid. The memory location to which newArea was pointing cannot be freed, as there is no pointer left pointing to that location. In other words, the memory location pointed by newArea becomes an orphan and results in memory leak. Whenever freeing the structured element, which in turn contains the pointer to dynamically allocated memory location, first traverse to the child memory location (newArea in the example) and start freeing from there, traversing back to the parent node. The correct implementation here will be:
    free( memoryArea‑>newArea);
    free(memoryArea);
  • Improper handling of return values At time, some functions return the reference to dynamically allocated memory. It becomes the responsibility of the calling function to keep track of this memory location and handle it properly.
    char ∗func ( )
    {
            return malloc(20); // make sure to memset this location to ‘\0’…
    }
    
    void callingFunc ( )
    {
            func ( ); // Problem lies here
    }
    In the example above, the call to the func() function inside the callingFunc() function is not handling the return address of the memory location. As a result, the 20 byte block allocated by the func() function is lost and results in a memory leak.

Give back what you acquire

When components are developed, there can be a lot of dynamic memory allocations. You might forget to keep track of all the pointers (pointing to these memory locations), and some of the memory segments are not freed and stay allocated to the program.

Always keep track of all the memory allocations, and do free them whenever appropriate. In fact, a mechanism can be developed to keep track of these allocations, like keeping a counter in the link list node itself (but you'd have to consider the additional overhead of this mechanism as well!).

Accessing null pointer

Accessing the null pointer is very dangerous, as it might crash your program. Always make sure that you are not accessing null pointers.

Summary

This article discussed several pitfalls you can avoid when working with dynamic memory allocations. To avoid memory-related problems, it is good practice to:

  • Always use memset along with malloc, or always use calloc.
  • Whenever writing values to pointers, make sure you cross check the number of bytes available and number of bytes being written.
  • Before assigning the pointers, make sure no memory locations will become orphaned.
  • Whenever freeing the structured element (which in turn contains the pointer to dynamically allocated memory location), first traverse to the child memory location and start freeing from there, traversing back to the parent node.
  • Always properly handle return values of functions returning references of dynamically allocated memory.
  • Have a corresponding free to every malloc.
  • Make sure you are not accessing null pointer.