Panel For Example Panel For Example Panel For Example

C++ Programming Tips for Embedded Systems

Author : Adrian December 31, 2025

Writing C++ classes the right way

Using a complex number class as an example, the following items illustrate good C++ programming practices for embedded development.

 

1. Header guards

Prevent a header file from being included multiple times:

#ifndef __COMPLEX__ #define __COMPLEX__ class complex {}; #endif

 

2. Keep data private and provide accessors

Put data members under private and expose access through an interface:

#ifndef __COMPLEX__ #define __COMPLEX__ class complex { public: double real() const { return re; } double imag() const { return im; } private: double re, im; }; #endif

 

3. Mark member functions that do not modify object state as const

For example:

double real() const { return re; } double imag() const { return im; }

If a function does not modify the object, declare it const. The compiler will enforce the constness and readers of the code will understand the intent. Only const objects can call const member functions; const objects cannot call non-const member functions.

 

4. Use constructor initializer lists

class complex { public: complex(double r = 0, double i = 0) : re(r), im(i) { } private: double re, im; };

Initialization should be done in the initializer list. Assignments inside the constructor body are assignments, not initializations.

 

5. Prefer passing parameters as const reference when appropriate

Add an operator+= for the complex class:

class complex { public: complex& operator+=(const complex&); };

Passing by reference avoids object construction/destruction overhead. Use const to ensure the parameter is not modified. For built-in types the cost differences are small and sometimes passing by value is more efficient, but using references consistently is often reasonable.

 

6. Return references when appropriate

Returning a reference to a local variable causes undefined behavior because the local is destroyed when the function returns. A function may return a reference only when the referenced object has storage that outlives the function call. For example, operator+= returns a reference because the left-hand operand already exists in memory. Operator+ cannot return a reference because the result is a newly created temporary.

inline complex& complex::operator+=(const complex& r) { this->re += r.re; this->im += r.im; return *this; } inline complex operator+(const complex& x, const complex& y) { return complex(real(x) + real(y), imag(x) + imag(y)); // newly created object, cannot return reference }

Returning a reference from operator+= enables chained operations:

c3 += c2 += c1;

 

7. When overloading operators, consider providing multiple overloads

For the complex class, addition can appear in various forms:

complex c1(2,1); complex c2; c2 = c1 + c2; c2 = c1 + 5; c2 = 7 + c1;

Provide overloads to handle these cases:

inline complex operator+(const complex& x, const complex& y) { return complex(real(x) + real(y), imag(x) + imag(y)); } inline complex operator+(const complex& x, double y) { return complex(real(x) + y, imag(x)); } inline complex operator+(double x, const complex& y) { return complex(x + real(y), imag(y)); }

 

8. Place public interface at the top of the class declaration

Put the interface intended for external use near the beginning of the class declaration so users can quickly see how to use the class.

 

Classes with pointer members: implement the Big Three

Classes that have pointer data members generally need custom implementations of the copy constructor, copy assignment operator, and destructor.

class String { public: String(const char* cstr = 0); String(const String& str); String& operator=(const String& str); ~String(); char* get_c_str() const { return m_data; } private: char* m_data; };

If you do not implement these, the compiler will generate defaults that perform shallow copy, which is usually incorrect for classes that manage dynamic memory. The following sections illustrate why and how to implement them.

Destructor: release dynamically allocated memory

If a class allocates memory dynamically, the destructor must free it to avoid memory leaks. For example, this constructor allocates memory for m_data:

/* String constructor */ inline String::String(const char* cstr = 0) { if (cstr) { m_data = new char[strlen(cstr) + 1]; // Here, m_data allocates memory strcpy(m_data, cstr); } else { m_data = new char[1]; *m_data = '?'; } }

Corresponding destructor:

inline String::~String() { delete[] m_data; } string-memory-allocation-hello

Copy assignment and copy constructor must perform deep copy

The compiler-generated copy operations perform a bitwise copy (shallow copy), which copies pointers but not the data they point to. Given two String objects:

String a("Hello"); String b("World"); strings-in-memory

If you do a shallow copy with b = a, both objects will point to the same memory. The original memory for "World" becomes leaked, and when one object is destroyed the other is left with a dangling pointer. To avoid this, implement deep copy:

/* Copy assignment operator */ inline String& String::operator=(const String& str) { if (this == &str) // handle self-assignment return *this; delete[] m_data; // free current memory m_data = new char[strlen(str.m_data) + 1]; // allocate new memory strcpy(m_data, str.m_data); // copy the characters return *this; } assignment-step-2 assignment-step-3 assignment-step-4

The copy constructor should also perform a deep copy:

inline String::String(const String& str) { m_data = new char[strlen(str.m_data) + 1]; strcpy(m_data, str.m_data); }

Always check for self-assignment in operator=. If self-assignment is not handled, deleting the object's own memory before copying will lead to undefined behavior.

self-assignment-dangling-pointer

 

static members and classes

1. Use static for data not tied to an individual object

For example, interest rate in an account class should be shared by all accounts and declared static so only one copy exists program-wide.

2. static member functions have no this pointer

Static member functions behave like regular functions in that they have a single copy in the code segment. They do not have a this pointer and cannot access non-static members. They can be called either through an object or through the class name.

3. Define static data members outside the class

class A { private: static int a; // declaration }; int A::a = 10; // definition and initialization

4. Small applications of static in patterns

Static members are commonly used in singleton implementations. A simple eager singleton:

class A { public: static A& getInstance(); void setup() { ... } private: A(); A(const A& rhs); static A a; };

In this eager approach, a is created whether or not it is used. A lazily initialized singleton creates the instance only when needed:

class A { public: static A& getInstance(); void setup() { ... } private: A(); A(const A& rhs); ... }; A& A::getInstance() { static A a; return a; }

The lazy approach delays creation until getInstance is called, which is a common pattern in designs that postpone resource allocation until necessary.