As someone who has worked in the embedded systems industry for over twenty years, I have witnessed tremendous advancements in technology—from 8-bit microcontrollers to today’s complex multi-core systems.
However, one thing has remained constant: pointers in C and C++. They are a double-edged sword, providing incredible flexibility in memory management, but mismanagement can lead to serious damage.
Recently, an incident where a NULL pointer caused a system crash clearly reminded us how important it is to safely use pointers in our code.
In this article, we will explore best practices for safely using pointers in C and C++, ensuring your embedded systems run smoothly without unexpected crashes.
Understanding Pointers
Pointers are essentially variables that store the memory addresses of other variables. Pointers can enable efficient memory operations and dynamic memory allocation, but they also come with risks, the most obvious being that dereferencing a NULL or uninitialized pointer can lead to catastrophic failures. They can also lead to security vulnerabilities, overwrite unintended locations, and other issues, so understanding how pointers work is the first step to safely using them.
Declaring Pointers
To declare a pointer, you can use the * operator:
int *ptr; // A pointer to an integer
This declaration does not allocate memory for an integer; it merely creates a pointer that can point to an integer memory location. It is crucial to initialize the pointer before using it, as using an uninitialized pointer may lead to undefined behavior. (Your compiler might initialize it to 0 or NULL, or it might just hold the value of the memory before allocation).
Initializing Pointers
You can initialize pointers in several ways:
1. Assigning the address of a variable:
int var = 42;
int *ptr = &var; // ptr now points to var
int *ptr = (int *)malloc(sizeof(int)); // Allocating memory for one integer
if (ptr == NULL) {
// Handle memory allocation failure
}
*ptr = 42; // Assign a value to the allocated memory
Best Practices for Safe Pointer Usage
Best Practice 1—Always Initialize Pointers
int *ptr = NULL; // Initialize to NULL
A pointer pointing to NULL is better than one pointing to a random location.
Best Practice #2—Check for NULL Before Dereferencing
The advantage of initializing a pointer to NULL is that we can check it before dereferencing to ensure it has been initialized. If the value is 0x08FF001234, I might assume this pointer has been initialized to the correct location. (Assumptions are bad! We might use MPU and other linker tricks to verify it points to the correct area).
Before dereferencing a pointer, ensure it is not NULL. This simple check can prevent crashes:
if (ptr != NULL) {
// Dereference and do useful work!
} else {
// Pointer is NULL, cannot dereference!
// Exception handling!
}
When I have an array of function pointers, I often use this trick. I might have a table like this:
typedef void (*LedCommand_t)(void);
LedCommand_t LedCommands[] = {
turnLedOn,
turnLedOff,
NULL
}
The last item in the table is NULL, making it easy to check. I can loop through the table and generate a line of code to call that function (as long as it is not NULL)!
Best Practice 3—Use Smart Pointers in C++
If you are using C++, consider using smart pointers like std::unique_ptr and std::shared_ptr, which provide automatic memory management:
#include
std::unique_ptr ptr(new int(42)); // Automatically deallocates memory
Smart pointers help manage memory automatically, reducing the chances of memory leaks and dangling pointers.
Best Practice #4—Use Pointer Arithmetic Cautiously
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // Pointer to the first element
for (int i = 0; i < 5; i++) {
printf(“%d\n”, *(ptr + i)); // Accessing elements using pointer arithmetic
}
Best Practice #5—Use Toolchain Files for Memory Management
set(MEMORY_START 0x20000000)
set(MEMORY_END 0x2001FFFF)
By defining memory regions, you can manage pointers more effectively, ensuring they point to valid memory addresses. You can also use these memory regions to check the integrity of pointer values! Just assigning a value to a pointer does not mean the pointer value is correct.
Safely Handling Dynamic Memory
Strategy 1–Always Free Allocated Memory
int *ptr = (int *)malloc(sizeof(int));
if (ptr != NULL) {
*ptr = 42;
free(ptr); // Free the allocated memory
}
Failing to free allocated memory can lead to memory exhaustion, especially in long-running embedded systems.
Strategy #2—Set Pointer to NULL After Freeing
free(ptr);
ptr = NULL; // Prevents accidental dereference
This simple step can prevent potential crashes caused by dereferencing freed memory.
Strategy 3—Use RAII
class Resource {
public:
Resource() {
data = new int[10]; // Allocate memory
}
~Resource() {
delete[] data; // Deallocate memory
}
private:
int* data;
};
Avoid Common Pointer Traps
Trap #1—Pointer Casting
int *ptr = (int *)malloc(sizeof(int)); // Avoid casting; it’s unnecessary in C
Letting the compiler do its job can help uncover potential issues during the compilation process.
Trap #2—Multiple Pointers Pointing to the Same Memory
int *ptr1 = (int *)malloc(sizeof(int));
int *ptr2 = ptr1;
*ptr1 = 10; // Both ptr1 and ptr2 point to the same memory
printf(“Value through ptr2: %d\n”, *ptr2); // Outputs 10
If you are not careful about who owns the memory, this situation can lead to unexpected behavior.
Conclusion

END