Pointers and why we use them

 In Programming

Today, I want to talk to you about a construct that’s been around a L-O-N-G time that many love and just as many hate but, is one of the most powerful and versatile tools in the C programming language: pointers.

I’ve had the pleasure of working with a lot of interns over the past 4 or 5 years and I’m surprised at how many CS and EE students aren’t exposed to this concept and low level programming in general. I get that we’re designing systems at a much higher level of abstraction in most corporate enterprise environments these days but it’s an interesting thing to observe that most (or so it seems) software engineers that are being churned out have ONLY been exposed to interpreted high level languages.

Anyway, pointers are just plain cool so let’s define what a pointer is. A pointer is simply a variable that stores the memory address of another variable. This means that when you’re working with pointers, you’re not working with the actual data itself, but rather with a reference to where that data is stored in memory. Some of the best use cases for using pointers include:

  1. Dynamic memory allocation: Pointers can be used to dynamically allocate memory at runtime, which allows for more flexibility in terms of the amount of memory used by a program.
  2. Array manipulation: Pointers can be used to manipulate arrays, allowing for more efficient and powerful array processing.
  3. Function parameters: Pointers can be used to pass large data structures or arrays to a function, avoiding the overhead of copying the data.
  4. Linked data structures: Pointers can be used to implement linked data structures such as linked lists and trees.
  5. Pointers to functions: Pointers to functions can be used to create callback functions, which can be passed as arguments to other functions.
  6. Pointers to structure: Pointers can be used to access and manipulate the members of a structure, rather than copying the entire structure.

Here are two examples of common pointer usage.

Pointers allow you to pass large amounts of data to a function more efficiently. When you pass a variable to a function, the entire contents of that variable are copied into the function’s memory. This can be slow and costly when working with large amounts of data. However, if you pass a pointer to that data instead, only the memory address is copied, which is much faster and more efficient.

Another use case for pointers is in dynamic memory allocation. In C, you can use the malloc() function to allocate memory dynamically at runtime. This allows you to create and manipulate data structures, like arrays or linked lists, without needing to know the size of that data beforehand. The malloc() function returns a pointer to the newly allocated memory, which you can then use to access and manipulate the data.

Alright, so now that you know what pointers are and why you might want to use them, let’s take a look at some examples.

First, let’s say you have a variable called “x” that you want to pass to a function. Here’s what that might look like without using a pointer:

#include <stdio.h>
#include <stdlib.h>

void myFunction(int x) {
    x = x + 1;
}

int main() {
    int x = 5;
    myFunction(x);
    printf("%d\n", x);
}

Here, in the main function, we are initializing the integer variable x with the value 5. Then, we are passing the value of x to the function myFunction which increments the value of x by 1. But, since we passed the value of x to myFunction, the increment only applies to the copy of the value in the function’s memory and not the original value of x. Therefore, after the function call, the value of x in the main function remains unchanged and so the print statement outputs “5”.

5

Now, let’s see what happens when we pass a pointer to “x” instead:

#include <stdio.h>
#include <stdlib.h>

void myFunction(int *x) {
    *x = *x + 1;
}

int main() {
    int x = 5;
    myFunction(&x);
    printf("%d\n", x);
}

Here, in the main function, we are initializing the integer variable x with the value 5. Then, we are passing the address of x, using the ‘&’ operator, to the function myFunction which increments the value of the variable stored in that address by 1. The function then dereferences the pointer to access the value stored at that memory address, increments it by 1, and then assigns the new value back to that address. Therefore, after the function call, the value of x in the main function is 6 and so the print statement outputs “6”.

6

As you can see, using pointers allows us to directly manipulate the value of “x” in the main function, rather than just working with a copy of that value.

Now, let’s take a look at an example of dynamic memory allocation. Here’s how you might create an array of integers with malloc():

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *myArray;
    myArray = (int *)malloc(5 * sizeof(int));
    myArray[0] = 1;
    myArray[1] = 2;
    myArray[2] = 3;
    myArray[3] = 4;
    myArray[4] = 5;

    for (int i = 0; i < 5; i++) {
    printf("%d ", myArray[i]);
   }
}

Here, in the main function, we are using the malloc function to dynamically allocate memory space for an array of 5 integers. The malloc function returns a pointer to the newly allocated memory, which we are storing in the pointer variable myArray. Then we are assigning values to each element of the array using the array notation and then using a for loop to iterate over the array and print the values of each element. As a result, the program will print the values of each element of the array one after another separated by a space.

1 2 3 4 5

When you dynamically allocate memory using the malloc() function or a similar function, it’s important to properly deallocate that memory when you’re done with it to avoid memory leaks. A memory leak occurs when dynamically allocated memory is no longer needed by the program, but the program does not deallocate it and it remains allocated indefinitely. This can lead to decreased performance, stability issues, and even crashes.

To properly deallocate memory, you can use the free() function. The free() function takes a pointer to the memory that you want to deallocate as its argument. Here’s an example of how you would use free() to deallocate the memory that we allocated in the previous example:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *myArray;
    myArray = (int *)malloc(5 * sizeof(int));
    myArray[0] = 1;
    myArray[1] = 2;
    myArray[2] = 3;
    myArray[3] = 4;
    myArray[4] = 5;

    for (int i = 0; i < 5; i++) {
    printf("%d ", myArray[i]);
    }
    free(myArray);
}

It’s important to note that once you’ve called free() on a pointer, you should not use that pointer again. Doing so can lead to undefined behavior, such as accessing freed memory or crashing the program. On a side note a really good article on the fun you can have with dynamically allocated memory is here.

Another important thing to keep in mind is to deallocate memory in a LIFO (Last In First Out) order, meaning the last allocated memory should be deallocated first. This is known as the “stack” concept. This is because if you deallocate memory in a different order, you might end up having a pointer to a memory area that has already been deallocated.

A stack is a data structure that follows the Last In First Out (LIFO) principle. It is a container of elements that are inserted and removed according to the last-in first-out (LIFO) principle.

A stack can be visualized as a vertical container with two main operations: “push” and “pop”. “Push” operation is used to insert an element into the stack and “pop” operation is used to remove the last element that was inserted into the stack. The element that is removed is the last element that was inserted, also known as the top element of the stack.

A stack can also be used to implement function calls in programming languages. When a function is called, the current state of the program is pushed onto the stack, including the values of the variables and the memory address of the next instruction to be executed. When the function returns, its state is popped off the stack and the program resumes execution at the instruction stored in the memory address.

Stacks are also used in the implementation of many algorithms, such as depth-first search, and the evaluation of expressions in reverse Polish notation.

It’s worth noting that stack memory is a limited resource and it’s possible to cause a stack overflow, which occurs when the stack pointer exceeds the stack limit. This can happen, for example, when a function recurs too many times or when an infinite loop occurs.

It’s also good practice to initialize pointers that will be used to store dynamically allocated memory to NULL or 0 after they are freed, in order to help catch any attempts to use the pointer after it has been freed.

In addition, you can use memory leak detection tools and techniques to find and fix memory leaks in your code. These tools can help you identify the location of leaked memory and the cause of the leak, making it easier to fix the problem.

There are several memory leak detection tools and techniques that you can use to find and fix memory leaks in your code. Some of the most popular and widely used ones include:

  1. Valgrind: Valgrind is a powerful tool that can detect a wide variety of memory errors, including memory leaks. It works by instrumenting the binary executable of your program and running it under a memory-checking tool called Memcheck. This allows it to detect and report any memory leaks, as well as other memory-related issues such as buffer overflows and accesses to uninitialized memory.
  2. Address Sanitizer (ASan): Address Sanitizer is a fast and efficient memory error detector that is built into the Clang and GCC compilers. It can detect memory leaks, buffer overflows, and use-after-free errors, among others. ASan works by instrumenting the binary executable at runtime and detecting errors as they occur.
  3. LeakSanitizer (LSan): LeakSanitizer is a memory leak detector that is built into the Clang and GCC compilers. It works by instrumenting the binary executable at runtime and detecting memory leaks as they occur. LSan is efficient and can find leaks that other leak detectors might miss.
  4. Electric Fence: Electric Fence is a tool that can be used to detect buffer overflows and memory leaks by redirecting memory allocations to a special memory area that is protected by the operating system. This allows it to detect any attempts to write beyond the bounds of an allocated block of memory.
  5. Dr. Memory: Dr. Memory is a memory debugging tool that can detect memory leaks, buffer overflows, and use-after-free errors. It works by instrumenting the binary executable of your program and running it under a memory-checking tool called Dr. Memory.
  6. Memory Management API’s: Most of the modern operating systems provide memory management API’s that can help to identify memory leaks, for example Windows provides the CRT Debug Heap API, Visual Leak Detector, and Application Verifier which can be used to detect memory leaks in Windows applications.
  7. Memory Profilers: Memory profilers are tools that can be used to track the memory usage of a program over time. These tools can help you identify memory leaks by showing you how the memory usage of your program changes as it runs. Some popular memory profilers include valgrind massif, Xcode instruments, and Visual Studio memory profiler.

Proper memory management is an important aspect of writing efficient, stable, and robust C code. By using the malloc() and free() functions correctly, and by keeping in mind the best practices mentioned above, you can avoid memory leaks and keep your program running smoothly.

The official documentation for pointers in the C programming language can be found in the C standard library, specifically in the <stdio.h> and <stdlib.h> headers. These headers contain the function and macro declarations for the standard input/output and memory management functions, respectively, including malloc() and free().

Some good books on the subject:

  • “The C Programming Language” by Brian W. Kernighan and Dennis M. Ritchie
  • “C Programming Absolute Beginner’s Guide (3rd Edition)” by Greg Perry and Dean Miller
  • “Effective C: An Introduction to Professional C Programming” by Bill H. Sanders

Thanks for reading!

Recent Posts

Start typing and press Enter to search