Core C/C++ Concepts and Algorithms for Embedded Systems Engineering

Preprocessor Stringification and Concatenation

The # dircetive transforms macro arguments into string literals during compilation. It must precede a parameter name within a parameterized macro definition.

#define DEBUG_IDENTIFIER(var) std::printf("[Check] %s evaluated\n", #var)

DEBUG_IDENTIFIER(sensor_temp);

The ## directive merges two preprocessing tokens into a single identifier. Whitespace surrrounding the operator is ignored.

#define CONFIG_REGISTER(channel) reg_ctrl_##channel

CONFIG_REGISTER(3) = 0x0F; // Expands to: reg_ctrl_3 = 0x0F;

The volatile Storage Qualifier

Applying volatile instructs the compiler to bypass optimizations that assume a variable's value remains stable between accesses. The memory location is read or written directly on every reference.

Thread Synchronization Flags Shared flags modified by concurrent execution contexts require volatile to prevent cached register reads.

class TaskScheduler {
private:
    volatile bool termination_signal = false;
public:
    void requestStop() { termination_signal = true; }
    bool isRunning() const { return !termination_signal; }
};

Hardware Memory Mapping Peripheral registers mapped into the address space must be marked volatile, as external hardware can alter their contents asynchronously.

volatile uint32_t* const uart_status_reg = reinterpret_cast<volatile uint32_t*>(0x40002004);

Asynchronous Signal Handling Global variables updated inside signal handlers require volatile to guarantee visibility to the main execution flow.

#include <csignal>
#include <iostream>

volatile sig_atomic_t capture_event = 0;

void handle_interrupt(int sig) { capture_event = 1; }

int main() {
    std::signal(SIGINT, handle_interrupt);
    while (capture_event == 0) {
        // Polling loop
    }
    std::cout << "Interrupt captured successfully\n";
    return 0;
}

Storage Class static

Function Scope Persistence A static local variable is allocated in the data segment rather than the stack. It is initialized once and retains its value across subsequent invocations.

void track_invocations() {
    static uint32_t call_counter = 0;
    ++call_counter;
}

Class-Level Shared Data Static class members belong to the type definition itself, not individual instances. All objects reference a single memory location.

class SensorNode {
public:
    static inline int active_instances = 0;
    void activate() { ++active_instances; }
};

Encapsulation via File Scope Placing static before global variables or functions restricts their linkage to internal translation unit scope, preventing symbol conflicts during linking.

Initialization Behavior of Static Storage

Static variables reside in the .data or .bss memory segments. Because these segments are allocated for the entire runtime duration, initialization occurs exactly once during program startup or the first function call. Subsequent executions skip the initialization phase, preserving the previous state.

C and C++ Linkage Compatibility: extern "C"

C++ compilers apply name mangling to support function overloading, while C compilers use exact symbol names. Wrapping code in extern "C" disables mangling, ensuring cross-language symbol resolution during the linking phase.

Immutability with const

The const qualifier enforces read-only semantics.

Constant Variables: Must be initialized at declaration and cannot be reassigned. Function Parameters: Prevents accidental modification of arguments within the function body. Return Values: Returning const pointers restricts callers from modifying the pointed-to data. Applying const to primitive return types is redundant since rvalues are temporary copies.

const char* fetch_buffer();
const char* data = fetch_buffer(); // Caller cannot alter buffer content

Compile-Time Macros vs. Compiler Constants

Macros are processed by the preprocessor via text substitution, offering no type safety or debug visibility. const variables are evaluated by the compiler, enforce strict typing, consume memory (unless optimized), and respect standard scoping rules.

Dynamic Memory Allocation Comparison

Feature new / delete malloc / free
Type C++ operators C standard library functions
Type Safety Returns exact pointer type Returns void*
Initialization Invokes constructors Raw memory allocation only
Cleanup Invokes destructors Raw deallocation only
Failure Handling Throws std::bad_alloc Returns nullptr
Array Support new[] / delete[] handles sizing Manual byte calculation required

String Measurement vs. Object Footprint

const char* sequence = "\0";
std::cout << strlen(sequence) << "\n"; // Output: 0
std::cout << sizeof(sequence) << "\n"; // Output: 2

strlen traverses memory until encountering the null terminator, returning the count of preceding characters. sizeof computes the compile-time byte footprint of the operand, including the terminating null character for string literals. sizeof is an operator yielding size_t, while strlen is a runtime function. When arrays decay to function parameters, they pass as pointers, losing dimension information.

Calculating Type Size Without sizeof

Pointer arithmetic can determine memory footprint by comparing adjacent addresses cast to byte pointers.

#define BYTE_FOOTPRINT(entity) \
    ((char*)(&(entity) + 1) - (char*)&(entity))

Aggregate Type Architecture: struct vs union

Structure (struct) Members occupy distinct memory regions aligned for access efficiency. Total size equals the sum of member sizes plus padding. All members are accessible simultaneously. Used for composite data modeling.

Union (union) Members overlay the same memory block. Total size matches the largest member plus alignment padding. Only one member holds a valid value at any time. Used for memory-efficient type punning or variant storage.

Algorithm: Single Element Frequency (Modulo Arithmetic)

Given an array where every integer appears three times except for one unique value, the solution can be derived using bitwise summation. Summing the bits at each of the 32 positions across all numbers yields a total. Since triplicate numbers contribute multiples of three, taking the sum modulo three isolates the bit value of the unique element.

int isolate_unique(std::vector<int>& data) {
    int result = 0;
    for (int bit = 0; bit < 32; ++bit) {
        int sum_bits = 0;
        int mask = 1 << bit;
        for (int val : data) {
            if (val & mask) sum_bits++;
        }
        if (sum_bits % 3) {
            result |= mask;
        }
    }
    return result;
}

Algorithm: Exponentiation by Squaring

Recursive reduction halves the exponent at each step, achieving logarithmic time complexity. Negative exponents are handled by computing the reciprocal.

class PowerCalculator {
    double recursive_compute(double base, unsigned long exp) {
        if (exp == 0) return 1.0;
        double half = recursive_compute(base, exp / 2);
        return (exp % 2 == 0) ? half * half : half * half * base;
    }
public:
    double calculate(double base, int exp) {
        unsigned long abs_exp = (exp < 0) ? -static_cast<unsigned long>(exp) : exp;
        double res = recursive_compute(base, abs_exp);
        return (exp < 0) ? 1.0 / res : res;
    }
};

Sorting: In-Place Partitioning

QuickSort recursively partitions arrays around a pivot, placing smaller elements left and larger elements right. Correct boundary checks prevent out-of-range access.

int partition_array(std::vector<int>& arr, int start, int end) {
    int pivot = arr[end];
    int i = start - 1;
    for (int j = start; j < end; ++j) {
        if (arr[j] <= pivot) {
            ++i;
            std::swap(arr[i], arr[j]);
        }
    }
    std::swap(arr[i + 1], arr[end]);
    return i + 1;
}

void sort_recursive(std::vector<int>& arr, int low, int high) {
    if (low < high) {
        int pivot_idx = partition_array(arr, low, high);
        sort_recursive(arr, low, pivot_idx - 1);
        sort_recursive(arr, pivot_idx + 1, high);
    }
}

Selection: QuickSelect for Kth Element

Adapting the partition strategy avoids full sorting. The algorithm recurses only into the partition containing the target index, yielding average O(n) time.

class ElementSelector {
    int select_partition(std::vector<int>& data, int left, int right, int target_idx) {
        int pivot_val = data[right];
        int store_idx = left;
        for (int i = left; i < right; ++i) {
            if (data[i] < pivot_val) {
                std::swap(data[store_idx++], data[i]);
            }
        }
        std::swap(data[store_idx], data[right]);
        return store_idx;
    }
public:
    int findKthLargest(std::vector<int>& nums, int k) {
        int target = nums.size() - k;
        int left = 0, right = nums.size() - 1;
        while (left <= right) {
            int pos = select_partition(nums, left, right, target);
            if (pos == target) return nums[pos];
            else if (pos > target) right = pos - 1;
            else left = pos + 1;
        }
        return nums[left];
    }
};

Pattern Matching: KMP Optimization

The Knuth-Morris-Pratt algorithm uses a prefix function table to skip redundant comparisons during string search.

std::vector<int> compute_prefix(const std::string& pattern) {
    int m = pattern.length();
    std::vector<int> pi(m, 0);
    int k = 0;
    for (int q = 1; q < m; ++q) {
        while (k > 0 && pattern[k] != pattern[q]) k = pi[k - 1];
        if (pattern[k] == pattern[q]) k++;
        pi[q] = k;
    }
    return pi;
}

int match_first_occurrence(const std::string& text, const std::string& pattern) {
    if (pattern.empty()) return 0;
    std::vector<int> pi = compute_prefix(pattern);
    int q = 0; // matched characters
    for (int i = 0; i < text.length(); ++i) {
        while (q > 0 && pattern[q] != text[i]) q = pi[q - 1];
        if (pattern[q] == text[i]) q++;
        if (q == pattern.length()) return i - q + 1;
    }
    return -1;
}

Tags: C++ Embedded Systems Low-Level Programming Memory Management algorithms

Posted on Sun, 10 May 2026 21:30:23 +0000 by ciciep