C and C++ Essentials

Types and sizes, pointers and arrays, structures, the preprocessor, bit operations, undefined behavior and safe habits.

A small C program

C is a small language close to the hardware: you declare types, manage memory yourself, and the compiler turns your code into machine instructions with little in between. That makes it fast, predictable and available on every microcontroller, and it also means that mistakes (an array index too large, a pointer to nothing) compile happily and fail later. Learn it deliberately.

#include <stdio.h>
#include <stdint.h>

/* average of n samples; returns 0 for n == 0 */
static double mean(const double *x, size_t n) {
    if (n == 0) return 0.0;
    double sum = 0.0;                       /* double: avoids losing digits in the sum */
    for (size_t i = 0; i < n; i++) sum += x[i];
    return sum / (double)n;                 /* cast so that the division is not integer division */
}

int main(void) {
    double v[] = { 20.1, 20.3, 19.9, 20.2 };
    printf("mean = %.2f\n", mean(v, sizeof v / sizeof v[0]));   /* sizeof gives the element count */
    return 0;
}
  • A program is functions; main is where it starts and returns 0 on success. Declare a function before you use it (a prototype in a header).
  • Everything has a type; the compiler checks them, but converts between them silently (see the traps below).
  • Statements end in a semicolon, blocks use braces, and indentation is only for people.
  • Arrays carry no length: pass the size along with the pointer.

Types, sizes and ranges

The size of int and long varies with the platform (16 bits for int on an 8-bit AVR, 32 on most ARM chips and PCs), so for anything portable or tied to hardware use the fixed-width types from <stdint.h>:

WidthUnsigned (uint8_t…)Signed (int8_t…)
8 bits0 to 255−128 to 127
16 bits0 to 65 535−32 768 to 32 767
32 bits0 to 4 294 967 295−2 147 483 648 to 2 147 483 647
64 bits0 to 18 446 744 073 709 551 615−9 223 372 036 854 775 808 to 9 223 372 036 854 775 807
TypeMeaningNotes
charA character or a small integerSigned or unsigned depending on the platform: use uint8_t for bytes
int, unsignedThe “natural” integerAt least 16 bits; avoid when the size matters
size_tUnsigned type for sizes and array indexesReturned by sizeof; use it in loops over arrays
float, double32-bit and 64-bit floating pointOn many small chips double is only 32 bits; see Numbers and Floating Point
bool (<stdbool.h>)true or falseUse instead of 0 and 1 for flags
enumNamed integer constantsStates and modes: enum State { IDLE, RUN, FAULT };

Worked example: promotion and overflow

  • On a 16-bit int platform, uint16_t a = 500, b = 500; then a * b is 250 000, which does not fit in 16 bits: the result is 250 000 mod 65 536 = 53 392 as unsigned, or -12 144 if it lands in a signed int16_t (and signed overflow is undefined behavior). The fix is to widen one operand first: (uint32_t)a * b = 250 000.
  • Integer division truncates: 7 / 2 is 3 and -7 / 2 is −3; 7 / 2.0 is 3.5. Convert before you divide, not after.
  • Unsigned arithmetic wraps predictably ((uint8_t)(200 + 100) is 44); signed overflow does not (see Undefined behavior). for (size_t i = n - 1; i >= 0; i--) never ends because an unsigned index is always ≥ 0.
  • Mixed signed and unsigned comparisons surprise: -1 < 1u is false, because −1 converts to a huge unsigned number.

Pointers and arrays

A pointer holds the address of a variable. Declare one with *, take an address with &, and follow it with *. Pointers are how C passes arrays and lets a function change its caller’s variables.

int  n = 5;
int *p = &n;            /* p holds the address of n */
*p = 7;                 /* n is now 7 */

int a[4] = { 10, 20, 30, 40 };
int *q = a;             /* an array name decays to a pointer to its first element */
q[3];  *(q + 3);        /* the same element: 40 */

void scale(double *x, size_t n, double k) {      /* the function changes the caller's array */
    for (size_t i = 0; i < n; i++) x[i] *= k;
}

Worked example: pointer arithmetic

An array of 32-bit integers starts at address 0x2000. Element 3 lives at 0x2000 + 3 × 4 = 0x200C. Adding 1 to a pointer moves it by the size of what it points to (4 bytes here), not by 1 byte, which is why q + 3 and &a[3] are the same address.

  • No bounds checking: a[8] on an array declared a[8] reads or writes the memory just beyond it, which may be another variable or the stack. Check the index before you use it.
  • Null pointers: set pointers to NULL when they point to nothing, check before dereferencing, and never use a pointer after the thing it points to is gone (a local variable, freed memory): a dangling pointer.
  • Pointers to const (const double *x) promise not to change the data: use them for inputs.
  • Function pointers hold the address of a function: callbacks, tables of commands and interrupt handlers.
  • Dynamic memory (malloc, free) gives memory of a size you choose at run time; check for failure, free it once, and on microcontrollers avoid it (see Embedded Systems: Memory).

Strings

A C “string” is an array of characters ending with a zero byte ('\0'). There is no string type, so you manage the space yourself: char name[8] = "motor"; uses 6 bytes (five letters and the terminator) of the 8.

  • Copying: strcpy and strcat do not check the size: use snprintf(buf, sizeof buf, "T=%.1f", t) or strncpy with care, and always leave room for the terminator.
  • Comparing: strcmp(a, b) == 0, never a == b (that compares addresses).
  • Parsing numbers: strtol and strtod report errors; atoi and atof do not.
  • Formatting: printf("%d %u %ld %f %s %x", …) must match the argument types; %d with a uint32_t needs PRIu32 or a cast. On small chips printf with %f pulls in kilobytes of code (see Embedded Systems).
  • A buffer overflow from an unchecked string copy is the classic security hole.

Structures, unions and layout

typedef struct {
    float   position_mm;
    float   speed_mm_s;
    uint8_t axis_id;
    bool    enabled;
} Axis;

Axis x = { .position_mm = 0.0f, .speed_mm_s = 50.0f, .axis_id = 1, .enabled = true };
void move(Axis *a, float target_mm);       /* pass by pointer: no copy of the whole struct */

Worked example: padding

The compiler aligns each member to its size, inserting padding. struct { uint8_t a; uint32_t b; uint8_t c; } takes 1 + 3 (padding) + 4 + 1 + 3 (end padding) = 12 bytes on a 32-bit ARM. Reordering the members to uint32_t b; uint8_t a; uint8_t c; needs 4 + 1 + 1 + 2 (end padding) = 8 bytes; “packing” would give 6 but makes unaligned access slow or illegal on some chips. Never send a struct over a wire or save it to a file as raw bytes if the other end may use another compiler or byte order: write each field (see Bits, Bytes and Data Formats).

  • Unions store different types in the same memory; use them to look at the same bytes two ways, remembering byte order and the strict-aliasing rule (copy with memcpy to be safe).
  • Bit fields (unsigned ready : 1;) look convenient for registers, but their layout is compiler-dependent: use masks and shifts for portable code (see Bits, Bytes and Data Formats).
  • Initialize every field with designated initializers, as above, so that nothing is left undefined.

The preprocessor and headers

  • #include pastes a header file in. A header holds declarations (prototypes, types, constants); the code lives in a .c file. Guard every header against double inclusion: #ifndef FILTER_H … #define FILTER_H … #endif (or #pragma once).
  • #define replaces text before compiling. For constants prefer const or enum (they have types and scope); use macros for bit masks and conditional builds.
  • Macros with arguments are text substitution, which causes classic traps:
#define SQUARE(x)  x * x            /* wrong */
#define SQUARE2(x) ((x) * (x))      /* right: parenthesise every use of the argument and the whole */

With x = 3, SQUARE(x + 1) expands to x + 1 * x + 1 = 7 instead of 16, and SQUARE2(i++) increments i twice. An inline function has none of these problems.

  • Conditional compilation: #if DEBUG … #endif and #ifdef ARDUINO select code per build or platform; keep the number of variants small.
  • Use the right include paths and keep headers small: a header that includes everything slows builds and creates loops.

static, const and volatile

KeywordMeaningUse
static (on a function or global)Private to this fileHide helpers and module state; avoids name clashes
static (on a local variable)Keeps its value between callsA counter or a filter’s memory inside a function
constMust not be changedInputs, tables (placed in flash on a microcontroller), constants
volatileMay change outside the program’s flow: read it from memory every timeHardware registers; variables shared with an interrupt (see Embedded Systems: GPIO and Interrupts)
inline, restrictHints for the optimizerSmall functions; pointers that do not overlap

Undefined behavior

The C standard leaves many things undefined: the compiler may assume they never happen, and if they do, anything can follow (a crash, a wrong answer, or code that works until you change the optimization level). The usual suspects:

  • Out-of-bounds array access and use of a pointer after free or beyond the object.
  • Signed integer overflow (INT_MAX + 1); unsigned overflow is fine.
  • Reading an uninitialized variable.
  • Shifting by a negative amount or by the width of the type or more (1 << 32 on a 32-bit int); left-shifting a signed value into the sign bit.
  • Dereferencing a null pointer, or accessing data through a pointer of an incompatible type (strict aliasing).
  • Modifying a variable twice between sequence points (i = i++ + 1) and reaching the end of a non-void function without returning.
  • A data race between an interrupt (or thread) and the main code on a non-atomic variable.

Tools catch many of them: compile with -Wall -Wextra, run the portable code on a PC with -fsanitize=address,undefined, and use static analysis (cppcheck, clang-tidy). On a microcontroller, a memory-protection unit and a debugger’s fault handler show the rest (see Embedded Systems: Debugging).

Compiling, warnings and debugging

gcc -std=c11 -Wall -Wextra -Wconversion -O2 -g -o filter filter.c main.c -lm
#   -Wall -Wextra   turn on the common warnings (treat them as errors while developing: -Werror)
#   -Wconversion    warns about silent narrowing conversions
#   -O2             optimise; use -Og -g for debugging
#   -lm             link the maths library (sqrt, sin, ...)

gdb ./filter          # break main / run / next / print x / backtrace / watch total
StageWhat happensTypical error
PreprocessIncludes and macros expanded“No such file”: a missing include path
CompileEach .c file becomes an object fileSyntax and type errors; warnings you should read
LinkObjects and libraries joined into the program“Undefined reference”: a function declared but not defined, or a missing library (-lm)
RunThe program starts at mainSegmentation fault: a bad pointer or index; use the debugger
  • Read the first error first: later ones are often consequences.
  • Build with warnings on and fix them all. A clean build makes the new warning visible.
  • Use a Makefile or CMake so that a build is one command and only rebuilds what changed.
  • Print the unexpected value, or step in the debugger; on a microcontroller a SWD probe replaces gdb on the PC (see Embedded Systems: Platforms and Toolchains).

C++ in embedded and engineering work

C++ is C plus facilities for organizing larger programs. The parts engineers use most:

FeatureWhat it gives youCare needed
Classes and RAIIData and functions together; a constructor acquires a resource and the destructor releases it, even on an early returnThe best reason to use C++: no forgotten free or unlock
References and const correctnessvoid f(const Matrix &m) passes without copying and without allowing changes
std::vector, std::array, std::stringArrays that know their length and manage their memoryOn a small MCU, avoid heap-allocating containers or use std::array
TemplatesGeneric code (a filter for float or fixed-point types) with no run-time costError messages are long; compile times grow
constexprCompute tables and constants at compile timeGood for lookup tables in flash
Exceptions, RTTI, iostreamsConvenient on a PCOften disabled or avoided in embedded builds because of code size and timing
  • Embedded subset: many projects use C++ for classes, templates and constexpr but without exceptions, RTTI and dynamic allocation.
  • Robotics (ROS) and simulation use C++ for the speed-critical parts and Python for the rest (see Robotics Tools).
  • Use a standard (C++17 or later), a style guide (the C++ Core Guidelines) and a static analyzer.

Habits that keep C safe

  • Initialize everything and prefer const, static and small scopes.
  • Check every index, size and return value; assert the “impossible” in development builds.
  • Use fixed-width types and name the unit in the identifier (timeout_ms).
  • Keep functions short (a screen) with one job, and test them (see Software Practice, Testing and Debugging).
  • Avoid clever code: macros that hide control flow, pointer tricks and dense expressions cost more to read than they save to type.
  • Follow a coding standard for safety-related work (MISRA C, CERT C), and use the tools that enforce it.
  • Let the compiler help: warnings on, sanitizers on the PC, static analysis in the build.

Related

FAQ

Why does my program crash with a segmentation fault?

It accessed memory it does not own: a null or dangling pointer, an index beyond an array, or a stack overflow. Run it under a debugger or with AddressSanitizer to see the exact line.

What is the difference between a pointer and an array?

An array is a block of elements; a pointer is a variable holding an address. An array name converts to a pointer to its first element when passed to a function, which is why the length must be passed separately.

Should I learn C or C++?

Learn C first for microcontrollers and to understand the machine; add the C++ features you need later. Many embedded projects are written in a C-like subset of C++.

Why does my code work with -O0 and fail with -O2?

Usually undefined behavior or a missing volatile on a variable shared with an interrupt or hardware: the optimizer exposes what the slow build hid. Enable warnings and sanitizers, and check the shared variables.

References

  • Kernighan, B. W. and Ritchie, D. M., The C Programming Language, 2nd ed.; King, K. N., C Programming: A Modern Approach.
  • ISO/IEC 9899 (C11 and C17), annex J (undefined behavior); Seacord, R., Effective C; the SEI CERT C Coding Standard.
  • Stroustrup, B., A Tour of C++; Meyers, S., Effective Modern C++; the C++ Core Guidelines (isocpp.github.io).

Last reviewed: October 2026. Sizes depend on the compiler and the platform: check sizeof on yours.