Project 8.07 Section 8 ⚡ Embedded Relevance: High Array of Pointers Fixed-Block Allocator Intrusive List Bitmask Pool Memory Management

8.07 Managing Fixed Arrays of Heap Objects vs Intrusive Lists & Bitmask Memory Pools

Executive Summary: Tracking museum exhibits via a fixed array of heap pointers (Exhibit* exhibitPtrs[COUNT]). We explore the cleanup lifecycle of pointer arrays, analyze the fragmentation hazards of out-of-order deallocations in long-running firmware, and build a deterministic bitmask fixed-block memory pool.

💻 1. Annotated Source Code

#include <iostream>
#include "Exhibit.h"
using namespace std;


int main() {
	const int EXHIBIT_COUNT = 3;
	Exhibit* exhibitPtrs[EXHIBIT_COUNT];

	exhibitPtrs[0] = new Exhibit("T-Rex Skeleton", 101, 350.5);
	exhibitPtrs[1] = new Exhibit("Ancient Egypt", 204, 480.25);
	exhibitPtrs[2] = new Exhibit("Space Exploration", 309, 290.75);

	for (int i = 0; i < EXHIBIT_COUNT; i++) {
		cout << "Exhibit: " << exhibitPtrs[i]->getName() << endl;
		cout << "\tRoom: " << exhibitPtrs[i]->getRoomNumber() << endl;
		cout << "\tDisplay Size (sq ft): " << exhibitPtrs[i]->getDisplaySize() << endl;
		cout << endl;
	}//end for

	for (int i = 0; i < EXHIBIT_COUNT; i++) {
		delete exhibitPtrs[i];
		exhibitPtrs[i] = nullptr;
	}

	return 0;
}
#ifndef EXHIBIT_H
#define EXHIBIT_H

#include <string>
using namespace std;

class Exhibit {
	public:
		Exhibit(string name, int roomNumber, double displaySize);
		string getName() const;
		int getRoomNumber() const;
		double getDisplaySize() const;

	private:
		string name;
		int roomNumber;
		double displaySize;
};

#endif 
#include "Exhibit.h"

Exhibit::Exhibit(string name, int roomNumber, double displaySize) {
	this->name = name;
	this->roomNumber = roomNumber;
	this->displaySize = displaySize;
}

string Exhibit::getName() const {
	return name;
}

int Exhibit::getRoomNumber() const {
	return roomNumber;
}

double Exhibit::getDisplaySize() const {
	return displaySize;
}

📐 2. Architecture & UML Class Model

📐 Museum Exhibit Revenue & Attendance Tracker Model
+ Public - Private # Protected
<<class>> Exhibit Exhibit Model
-name : std::string
-visitorsCount : int32_t = 0
-ticketPriceCents : int32_t
+Exhibit(name: string, price: int)
+recordVisitor() : void
+getTotalRevenueCents() : int64_t const
+getName() : std::string const

📚 3. Core C++ Concepts Deep-Dive

1. Fixed Arrays of Pointers

A static array of pointers (Exhibit* exhibits[3]) holds pointers to individually allocated heap objects. While the array container is statically sized on the stack or in BSS, the individual objects reside on the dynamic heap.

2. Multi-Object Deallocation Lifecycle

To prevent memory leaks, every element in the pointer array must be explicitly deleted and reset to nullptr when its lifecycle ends.

⚡ 4. Embedded Systems & Hardware Reality

1. Out-of-Order Deallocation Fragmentation

If exhibits (or network packets, sensor readings) are allocated and freed in random order, general-purpose heap allocators create fragmented memory holes that cannot be coalesced, eventually causing allocation failure.

2. Fixed-Block Allocator with Bitmask Free-List

A Fixed-Block Allocator pre-allocates $N$ memory blocks of identical size in static SRAM. A single integer bitmask tracks which blocks are free. Allocation and deallocation are $O(1)$ constant time operations with zero fragmentation.

💡 5. Production-Ready Embedded Refactoring

Here is a deterministic, zero-fragmentation Fixed-Block Pool Allocator using a bitmask:

💡 Production-Ready Refactor
#include <cstdint>
#include <cstddef>
#include <new>

template <typename T, size_t BlockCount = 32>
class FixedBlockPool {
    static_assert(BlockCount <= 32, "Bitmask supports up to 32 blocks");

    alignas(alignof(T)) std::byte memory_[BlockCount][sizeof(T)];
    uint32_t allocation_mask_{0}; // Bit = 1 (Used), Bit = 0 (Free)

public:
    template <typename... Args>
    T* allocate(Args&&... args) noexcept {
        for (size_t i = 0; i < BlockCount; ++i) {
            if (!(allocation_mask_ & (1UL << i))) {
                allocation_mask_ |= (1UL << i); // Mark block as used
                return new (memory_[i]) T(std::forward<Args>(args)...);
            }
        }
        return nullptr; // Out of blocks
    }

    void free(T* ptr) noexcept {
        if (!ptr) return;
        for (size_t i = 0; i < BlockCount; ++i) {
            if (reinterpret_cast<T*>(memory_[i]) == ptr) {
                ptr->~T();
                allocation_mask_ &= ~(1UL << i); // Clear bitmask
                return;
            }
        }
    }
};

📝 Knowledge Verification Quiz

Test your understanding of the C++ concepts and embedded microcontroller trade-offs covered in this guide. Click any option for instant feedback.

Q1. What is the primary advantage of a Fixed-Block Memory Pool over general-purpose malloc() in embedded firmware?
A It guarantees zero memory fragmentation and strictly deterministic O(1) allocation/deallocation time
B It dynamically increases the SRAM capacity of the microcontroller
C It automatically runs garbage collection during interrupt routines
D It encrypts the heap memory
Detailed Explanation: Because all memory blocks are uniform in size, any freed block can satisfy any future allocation request, completely eliminating heap fragmentation and providing guaranteed $O(1)$ latency.
Q2. In a 32-block Fixed-Block Allocator, how much memory is required to track the free/used state of all 32 blocks using a bitmask?
A Exactly 4 bytes (a single 32-bit integer)
B 128 bytes
C 32 kilobytes
D 1 byte per object pointer
Detailed Explanation: A single 32-bit unsigned integer (uint32_t, 4 bytes) has 32 individual bits, where each bit represents the used/free state of one block.
Q3. What happens if a loop deletes exhibitPtrs[i] but does not set exhibitPtrs[i] = nullptr, and a subsequent function checks 'if (exhibitPtrs[i] != nullptr)'?
A The condition evaluates to true (because the pointer still holds the old address), leading to a hazardous use-after-free crash
B The compiler converts the pointer to nullptr automatically
C The condition evaluates to false safely
D The microcontroller reboots into DFU bootloader mode
Detailed Explanation: Calling delete frees the target memory but does not modify the pointer variable itself. Failing to set nullptr causes null checks to pass incorrectly, resulting in use-after-free bugs.
Q4. What is an 'intrusive data structure' in low-level systems programming?
A A data structure where linkage pointers (next/prev) are embedded directly inside the payload object itself, requiring zero auxiliary node memory allocation
B A virus that infects microcontroller firmware
C A data structure that only operates inside hardware registers
D A structure that allocates memory on external SPI Flash
Detailed Explanation: Intrusive containers store node pointers directly inside the managed objects, allowing objects to be linked into queues/lists without requiring separate node memory allocations.