C++ Memory Management — Embedded Automotive
Expert practices for safe, deterministic, and efficient memory management in C++14–C++20 embedded automotive code running on Linux, QNX, and Android NDK.
When to Use This Skill
- Designing ownership model for a new class or subsystem.
- Reviewing code for raw pointer misuse, leaks, or dangling references.
- Replacing manual
new/deletewith RAII idioms. - Designing a component that must avoid heap allocation in real-time paths.
- Auditing code for MISRA C++ violations related to memory.
- Investigating a crash suspected to be a use-after-free or heap corruption.
Ownership Vocabulary
| Concept | Meaning |
|---|---|
| Owner | Entity responsible for deleting the resource |
| Non-owning pointer/reference | Observes but never deletes |
| Unique ownership | Exactly one owner (std::unique_ptr) |
| Shared ownership | Multiple owners via reference counting (std::shared_ptr) |
| Weak reference | Observes shared-owned resource without extending lifetime (std::weak_ptr) |
Rule of thumb: 99% of C++ code should have zero explicit delete calls. Use smart pointers.
The Rule of Zero / Three / Five
// ── Rule of Zero (preferred) ─────────────────────────────────────
// If your class uses ONLY smart pointers / standard containers,
// let the compiler generate everything:
class AudioTrack {
std::string name_;
std::vector<int16_t> samples_;
std::unique_ptr<Codec> codec_;
// No destructor, copy/move constructors, or operators needed.
};
// ── Rule of Five (resource-owning classes) ───────────────────────
class SharedMemBuffer {
public:
explicit SharedMemBuffer(std::size_t size);
~SharedMemBuffer(); // 1. destructor
SharedMemBuffer(const SharedMemBuffer&) = delete; // 2. copy ctor
SharedMemBuffer& operator=(const SharedMemBuffer&) = delete; // 3. copy assign
SharedMemBuffer(SharedMemBuffer&& other) noexcept; // 4. move ctor
SharedMemBuffer& operator=(SharedMemBuffer&& other) noexcept; // 5. move assign
private:
void* ptr_ = nullptr;
std::size_t size_ = 0;
};
Never define only some of these — define all or none (use = default / = delete).
Smart Pointers
std::unique_ptr — Default Choice
// Creation
auto codec = std::make_unique<AacCodec>(AacCodec::Profile::kLC);
auto buffers = std::make_unique<int16_t[]>(1024); // array form
// Transfer ownership
void processTrack(std::unique_ptr<AudioTrack> track); // takes ownership
// Factory function (returns by unique_ptr)
std::unique_ptr<IDecoder> createDecoder(Codec codec) {
switch (codec) {
case Codec::kAac: return std::make_unique<AacDecoder>();
case Codec::kMp3: return std::make_unique<Mp3Decoder>();
default: return nullptr;
}
}
std::shared_ptr — Shared Ownership
// Use ONLY when multiple owners truly need the same lifetime
auto config = std::make_shared<AudioConfig>(sampleRate, channels);
// Inject into multiple consumers
AudioPlayer player(config);
AudioRecorder recorder(config); // both keep config alive
// Custom deleter (e.g. for C API handles)
auto dbus_conn = std::shared_ptr<DBusConnection>(
dbus_bus_get(DBUS_BUS_SESSION, nullptr),
[](DBusConnection* c) { dbus_connection_unref(c); }
);
std::weak_ptr — Breaking Cycles
class Widget {
std::shared_ptr<Widget> parent_; // WRONG — creates cycle
std::weak_ptr<Widget> parent_; // CORRECT — observes without owning
public:
void doSomethingWithParent() {
if (auto p = parent_.lock()) { // only valid if still alive
p->notify();
}
}
};
Raw Pointers — When Allowed
// Non-owning observers only:
void adjustVolume(AudioManager* mgr, int delta); // mgr is not owned here
void processBuffer(const int16_t* data, std::size_t len); // read-only view
RAII — Resource Acquisition Is Initialization
// Every resource must be managed by an object whose destructor releases it.
// File handle
class FileHandle {
public:
explicit FileHandle(const std::string& path)
: fd_(::open(path.c_str(), O_RDONLY))
{
if (fd_ < 0) throw std::system_error(errno, std::system_category());
}
~FileHandle() { if (fd_ >= 0) ::close(fd_); }
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&&) = default;
int get() const noexcept { return fd_; }
private:
int fd_;
};
// Lock guard (already in std::)
{
std::lock_guard<std::mutex> lock(mutex_);
// critical section
} // lock released here, regardless of exceptions
// Generic scope guard
template<typename F>
struct ScopeGuard {
F fn;
explicit ScopeGuard(F f) : fn(std::move(f)) {}
~ScopeGuard() { fn(); }
ScopeGuard(const ScopeGuard&) = delete;
};
template<typename F>
auto makeScopeGuard(F f) { return ScopeGuard<F>{std::move(f)}; }
// Usage
auto guard = makeScopeGuard([&] { cleanup(); });
Avoiding Heap Allocation in Real-Time Paths
For QNX hard real-time and RTOS-like components on Linux:
// 1. Pre-allocate everything at startup
class RealTimeAudioProcessor {
public:
explicit RealTimeAudioProcessor(std::size_t maxFrames)
: buffer_(maxFrames) // vector allocated ONCE in constructor
, scratchpad_(maxFrames)
{}
// process() is called in real-time context — ZERO allocations inside
void process(const int16_t* in, int16_t* out, std::size_t frames) noexcept {
// Use pre-allocated buffer_/scratchpad_
}
private:
std::vector<int16_t> buffer_;
std::vector<int16_t> scratchpad_;
};
// 2. Fixed-size ring buffer (no heap)
template<typename T, std::size_t N>
class RingBuffer {
std::array<T, N> buf_{};
std::size_t head_ = 0, tail_ = 0, count_ = 0;
public:
bool push(T val) noexcept {
if (count_ == N) return false;
buf_[tail_] = std::move(val);
tail_ = (tail_ + 1) % N;
++count_;
return true;
}
std::optional<T> pop() noexcept {
if (count_ == 0) return std::nullopt;
T val = std::move(buf_[head_]);
head_ = (head_ + 1) % N;
--count_;
return val;
}
};
AddressSanitizer (ASan) — Detecting Memory Bugs
# CMake: enable ASan for debug/test builds
if(ENABLE_ASAN)
target_compile_options(my_target PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g)
target_link_options(my_target PRIVATE -fsanitize=address,undefined)
endif()
# Run test with ASan
ASAN_OPTIONS=detect_leaks=1:check_initialization_order=1 ./build/my_test
ASan detects: heap/stack/global buffer overflow, use-after-free, use-after-return, double-free, memory leaks.
Common Memory Bugs and Fixes
// ── 1. Use-After-Free ────────────────────────────────────────────
{
auto obj = std::make_unique<Foo>();
Foo* raw = obj.get();
obj.reset(); // object deleted
raw->doStuff(); // UB: use-after-free — WRONG
}
// ── 2. Dangling reference ────────────────────────────────────────
std::string& getRef() {
std::string local = "hello";
return local; // WRONG: returns reference to local
}
// ── 3. Double delete ─────────────────────────────────────────────
Foo* p = new Foo();
delete p;
delete p; // UB: double delete — WRONG (use unique_ptr instead)
// ── 4. Iterator invalidation ─────────────────────────────────────
std::vector<int> v = {1, 2, 3};
for (auto it = v.begin(); it != v.end(); ++it) {
v.push_back(*it); // WRONG: push_back may reallocate, invalidating it
}
// ── 5. Shared ownership cycle ────────────────────────────────────
struct Node {
std::shared_ptr<Node> next; // WRONG for doubly-linked: creates cycle
std::weak_ptr<Node> prev; // CORRECT: weak for back-links
};
MISRA C++ Memory Rules Summary
| Rule | Requirement |
|---|---|
| M18-0-4 | Do not use dynamic heap memory after initialization |
| M5-3-3 | Expressions with side effects not used as delete operand |
| A18-5-2 | operator new / delete not used directly; use smart pointers |
| A18-5-8 | Objects not allocated on the heap when not necessary |
Prerequisites
- GCC 9+ or Clang 12+ toolchain installed.
- CMake 3.16+ on PATH.
- For cross-compilation: ARM/AArch64 sysroot or Android NDK r25+.
ninjabuild tool recommended for faster builds.
Step-by-Step Workflows
Step 1: Audit ownership
Identify each resource's lifetime; apply the Rule of Zero/Five to every class.
Step 2: Replace raw pointers with smart pointers
Use unique_ptr for exclusive ownership; shared_ptr only when shared ownership is genuinely required.
Step 3: Apply RAII
Wrap every resource acquisition in a constructor; release in the destructor.
Step 4: Run AddressSanitizer
Build with -fsanitize=address,undefined; run all tests; fix every reported issue.
Step 5: Review with clang-tidy
Enable cppcoreguidelines-* and modernize-* checks; fix owning_memory and avoid-c-arrays warnings.
Troubleshooting
- AddressSanitizer reports
heap-use-after-free— a raw pointer is used after its owningunique_ptrwas destroyed; refactor to useshared_ptr/weak_ptr. shared_ptrreference cycle causes undetected leak — useweak_ptrfor back-references; verify with AddressSanitizer--detect_leaks=1.std::bad_allocin production — preallocate memory pools for real-time paths; avoid dynamic allocation in interrupt handlers.- RAII destructor throwing exception — destructors must never throw; catch exceptions internally and log them.
Pre-Commit Checklist
- No raw
new/delete— usestd::make_uniqueorstd::make_shared. - No raw owning pointers in function signatures — use smart pointer parameters.
- Every class with a user-defined destructor follows the Rule of Five (or uses
= deletefor copy). - No
std::shared_ptrcycles — back-pointers usestd::weak_ptr. - RAII used for all resources: file descriptors, locks, memory-mapped regions, D-Bus connections.
- Real-time path verified to contain zero heap allocations.
- ASan run on unit tests (no leaks reported at exit).
- No
reinterpret_castfor pointer arithmetic — usestd::byte*orchar*.