Introduction
Dynamic memory allocation should be avoided as much as possible in embedded programming. However, when it is required, not all ways of allocate memory are optimal. The use of smart pointers is highly recommended in C++.
new vs malloc
malloc is a C function defined in <cstdlib>. The function allocates raw bytes from the heap and returns a pointer to void void*. new is a C++ operator that allocates memory and constructs the object in the allocated memory. Thus, the most fundamental difference between new and malloc in C++ is that malloc does NOT construct the allocated object, as demonstrated in the code snippet below. Other differences are that cast and memory size allocation are required with malloc.
class MyClass {
public:
// constructor
MyClass() {
_a = 2;
_p = (uint8_t*) malloc(sizeof(uint8_t) * SIZE);
}
~MyClass() {
if (_p != nullptr) {
free(_p);
}
}
uint8_t _a = 0;
uint8_t* _p = nullptr;
static constexpr uint8_t SIZE = 10;
};
void memory_allocation() {
// Object not constructed!, cast required, size to be specified
MyClass *pInstance1 = (MyClass*) malloc(sizeof(MyClass));
// Uncomment the next line (compiler warning because pInstance1 is not constructed)
// printk("pInstance1->_a: %d\n", pInstance1->_a);
// malloc returns nullptr when memory cannot be allocated
if (pInstance1 == nullptr) {
printk("Cannot allocate memory\n");
}
// destructor not called!
free(pInstance1);
pInstance1 = nullptr;
// Constructor is called, no cast required, no size specified
// When memory cannot be allocated, new generates a exception or
// returns nullptr, depending on implementation.
// On zephyr rtos (and embedded systems), exceptions are usually not used.
//try {
MyClass *pInstance2 = new MyClass();
printk("pInstance2->_a: %d\n", pInstance2->_a);
//}
//catch (const std::bad_alloc &e) {
//
//}
if (pInstance2 == nullptr) {
printk("Cannot allocate memory\n");
}
}
new and delete rather than malloc and free. This improves type safety and properly constructs and destructs objects. However, the most appropriate way to construct objects dynamically in C++ is through the use of smart pointers!
Using Smart Pointers for owning Pointers
Using smart pointers in C++ allows for automatic handling of allocation and deallocation. They also allow you to explicitly specify the ownership mechanism (single or shared).
// prefer this — automatic memory management, no leaks
auto p = std::make_unique<MyClass>(); // single owner
auto p = std::make_shared<MyClass>(); // shared ownership
// over this
MyClass *p = new MyClass(); // must remember to delete
delete p;
Overloading new and delete for a Class
In C++, it is possible to overload operators. This also applies to the new and delete operators. This mechanism enables the implementation of safer custom allocation in C++ classes. This is of particularly interesting for embedded programming, where dynamic allocation must be used carefully. The following class illustrates how dynamic behavior using new can be transformed into a safer mechanism by overloading the new and delete operators:
// Forward declare — slab defined after class
extern struct k_mem_slab MY_CLASS_SLAB;
class MyClassWithSlab {
public:
MyClassWithSlab() : _a(2) {}
~MyClassWithSlab() {}
void* operator new(size_t size) {
__ASSERT(size == sizeof(MyClassWithSlab), "Incorrect size %zu", size);
void *block_ptr = nullptr;
int ret = k_mem_slab_alloc(&MY_CLASS_SLAB, &block_ptr, K_MSEC(0));
__ASSERT(ret == 0, "MyClassWithPool slab exhausted");
LOG_DBG("Number of used slots: %d", k_mem_slab_num_used_get(&MY_CLASS_SLAB));
return block_ptr;
}
void operator delete(void *p) {
k_mem_slab_free(&MY_CLASS_SLAB, p);
LOG_DBG("Number of used slots: %d", k_mem_slab_num_used_get(&MY_CLASS_SLAB));
}
// Prevent accidental array allocation falling back to global heap
void* operator new[](size_t) = delete;
void operator delete[](void*) = delete;
uint32_t _a = 0;
};
// Defined after class — sizeof() and alignof() are now available
static constexpr uint8_t NBR_OF_BLOCKS = 10;
K_MEM_SLAB_DEFINE(MY_CLASS_SLAB,
sizeof(MyClassWithSlab),
NBR_OF_BLOCKS,
alignof(MyClassWithSlab));
This implementation uses the Zephyr RTOS fixed-size memory slab and it provides the following advantages:
- No heap fragmentation.
- Bounded memory usage with a fixed size at compilation time.
- Transparency for callers, with calls to
newjust working as expected.