How to Use Dynamic Memory Allocation and Pointers on Arduino
2026-01-14 | By Maker.io Staff

Pointers are a powerful tool in C and C++ that enable programs to work directly with memory, allowing for flexible data structures, efficient storage, and dynamic behavior. For makers, they open the door to creating more efficient Arduino projects. Read on to learn how pointers and dynamic memory allocation can improve code and unlock new possibilities.
What Are Pointers, and When Are They Used?
In C and C++, pointers are variables that store the address of a value they reference. This value can represent a single primitive entry, such as an integer, a complex structure, a block of data, or even a function stored in memory.
When referencing a consecutive block of memory, you can use square brackets to work with pointers as if they were arrays. Pointer arithmetic allows the addition or subtraction of an integer from the referenced address to move the pointer forward or backward. Similarly, you can subtract two pointers if they reference the same array. The resulting number represents the number of elements between the two pointers. Although technically possible, other operations are meaningless in C/C++.
The most common use case for function pointers is implementing dynamic behavior without repeated code segments. Programmers can adjust which function is called at a certain point in the code based on a condition. Function callbacks for various outcomes (for example, success, timeout, or failure) are a concrete example.
How to Create Pointers
Pointers are denoted by adding an asterisk either after the variable type or before the variable name:
int* numberPointer = nullptr; // or int *numberPointer = nullptr;
The nullptr keyword is an abbreviation for null-pointer, and it denotes that a pointer doesn’t reference a valid location in memory. Function pointers can be created by writing the function’s return type followed by the function pointer’s name, prefixed with an asterisk, in parentheses. The function’s parameter types have to be added after the pointer name:
// Example functions to reference
void hello(void) { /* … */ }
int add(int a, int b) { /* … */ }
// Pointers that reference the functions
void (*helloFunctionPointer)() = &hello;
int (*addFunctionPointer)(int, int) = add; // The &-operator is optional for functions
The Address-of and Dereference Operators
The most important operators for working with pointers are the address-of operator (denoted by an ampersand – &) and the dereference operator (asterisk – *). As the name suggests, you can obtain an address using the ampersand symbol, for example, to create a pointer that references an existing variable or function:
int repeatsLeft = 10; int* repeatsLeftPointer = &repeatsLeft;
Dereferencing a pointer returns the value stored at the target memory address or, in the case of function pointers, a new pointer to the same function. To dereference a pointer, you must prepend the pointer’s name with an asterisk:
int val = *repeatsLeftPointer; // Returns 10 int res = (*addFunctionPointer)(2, 5); // Returns 7
Note that dereferencing a function pointer is possible but optional. The function can also be called without dereferencing the function pointer:
int result = (*addFunctionPointer)(2, 5); // or int result = addFunctionPointer(2, 5);
Remember that computers know only binary digits, and it’s up to the programmer and compiler to ensure that the bits stored in a pointer’s target location get interpreted correctly when dereferencing a pointer. Otherwise, the data stored at that address cannot be restored accurately. Therefore, whenever possible, avoid using void-pointers that can represent an arbitrary value in memory. Doing so ensures that the compiler enforces type safety.
What Is Malloc in Arduino C++?
Pointers reference an address in memory, typically a position within an uninterrupted block of memory. However, the program must request a block of memory from the heap manager before it can use it. Each program’s memory is mostly divided into stack and heap, with other areas reserved for global data and code.
The stack memory keeps track of vital information, such as local variables, function calls, return addresses, and the program counter. This section automatically grows and shrinks as needed during runtime. In contrast, developers must manage the heap memory, which is used for storing dynamic data. A program must request space on the heap before using it and free up memory that’s no longer needed.
In C and C++, the malloc function is used to request a continuous segment of heap memory. It has a single parameter, denoting the number of required bytes, and it returns a void pointer that references the start of the allocated block:
// Request a block for storing 32 integers int* startAddress = (int*) malloc(32 * sizeof(int));
The function returns a null pointer if the allocation fails, for example, because the requested size exceeds the available space. Note that some platforms, like different Arduino boards, use a different number of bytes to represent the same data types in memory. Therefore, it’s important to use the sizeof function, paired with the target type, to ensure compatibility between CPU architectures.
Changing the Allocated Block Space and Freeing Memory
Once allocated, you can resize a block of memory by passing the pointer created by malloc and the new target size to the realloc function. Upon success, the realloc call returns a new pointer to the new continuous heap segment. Similar to malloc, the function returns a null pointer if the allocation fails:
// Initial allocation
int* buffer = (int*) malloc(32 * sizeof(int));
// Double the buffer size
buffer = (int*) realloc(buffer, 64 * sizeof(int));
if (buffer == NULL) {
/* Allocation failed */
}
Finally, programmers must ensure to free an allocated memory block once it’s no longer needed. Otherwise, the system might run out of memory, which can result in unexpected behavior and the program crashing:
int* buffer = (int*) malloc(32 * sizeof(int)); // Do something with the memory free(buffer); buffer = nullptr; // Optionally, mark the pointer as invalid
Common Pointer and Malloc Pitfalls
It’s easy to overuse pointers and dynamic memory allocation even when the program does not benefit from the added performance and flexibility. However, every programmer’s main goal should be to write code that’s easy to understand and maintain. Using pointers and dynamic memory allocation when it’s unnecessary makes programs harder to read and understand.
Void pointers can be practical because they allow referencing various data types and interpreting them dynamically. However, they also erase the compiler’s ability to ensure type safety, thus overcomplicating programs. Therefore, you should use concrete types whenever possible to clearly show intent.
Finally, since it’s up to the programmer to manage the program’s heap memory, not allocating enough, overallocating space, or failing to free memory that’s no longer used can cause reliability issues and program crashes. Therefore, heap memory must always be freed when it’s no longer needed to avoid memory leaks, and programmers must take care not to read any addresses outside of the allocated memory space.
Summary
This image summarizes the basic pointer workflow in C/C++.
Pointers are variables that store memory addresses, allowing programs to reference values, arrays, structures, or functions. They enable flexible memory access and dynamic behavior. Using pointer arithmetic lets programmers work with memory blocks as if they were arrays, and function pointers allow calling functions dynamically.
Pointers are created with the asterisk operator, and addresses are obtained with the ampersand. Using typed pointers ensures the compiler enforces type safety. Dereferencing a pointer retrieves the value stored at that memory location. Functions, referenced by pointers, can be called with or without dereferencing the pointer.
Dynamic memory resides on the heap, allocated with malloc and resized with realloc. The stack stores local variables, function calls, return addresses, and the program counter. Memory allocated with malloc must be released using free to avoid memory leaks.
Common mistakes when working with pointers include reading outside allocated memory, overusing pointers when unnecessary, and mismanaging heap memory. Keeping pointers typed and freeing dynamic memory when it’s done helps maintain reliable, readable code.