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;
mainis 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>:
| Width | Unsigned (uint8_t…) | Signed (int8_t…) |
|---|---|---|
| 8 bits | 0 to 255 | −128 to 127 |
| 16 bits | 0 to 65 535 | −32 768 to 32 767 |
| 32 bits | 0 to 4 294 967 295 | −2 147 483 648 to 2 147 483 647 |
| 64 bits | 0 to 18 446 744 073 709 551 615 | −9 223 372 036 854 775 808 to 9 223 372 036 854 775 807 |
| Type | Meaning | Notes |
|---|---|---|
char | A character or a small integer | Signed or unsigned depending on the platform: use uint8_t for bytes |
int, unsigned | The “natural” integer | At least 16 bits; avoid when the size matters |
size_t | Unsigned type for sizes and array indexes | Returned by sizeof; use it in loops over arrays |
float, double | 32-bit and 64-bit floating point | On many small chips double is only 32 bits; see Numbers and Floating Point |
bool (<stdbool.h>) | true or false | Use instead of 0 and 1 for flags |
enum | Named integer constants | States and modes: enum State { IDLE, RUN, FAULT }; |
Worked example: promotion and overflow
- On a 16-bit
intplatform,uint16_t a = 500, b = 500;thena * bis 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 signedint16_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 / 2is 3 and-7 / 2is −3;7 / 2.0is 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 < 1uis 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 declareda[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
NULLwhen 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:
strcpyandstrcatdo not check the size: usesnprintf(buf, sizeof buf, "T=%.1f", t)orstrncpywith care, and always leave room for the terminator. - Comparing:
strcmp(a, b) == 0, nevera == b(that compares addresses). - Parsing numbers:
strtolandstrtodreport errors;atoiandatofdo not. - Formatting:
printf("%d %u %ld %f %s %x", …)must match the argument types;%dwith auint32_tneedsPRIu32or a cast. On small chipsprintfwith%fpulls 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
memcpyto 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
.cfile. Guard every header against double inclusion:#ifndef FILTER_H … #define FILTER_H … #endif(or#pragma once). - #define replaces text before compiling. For constants prefer
constorenum(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 … #endifand#ifdef ARDUINOselect 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
| Keyword | Meaning | Use |
|---|---|---|
static (on a function or global) | Private to this file | Hide helpers and module state; avoids name clashes |
static (on a local variable) | Keeps its value between calls | A counter or a filter’s memory inside a function |
const | Must not be changed | Inputs, tables (placed in flash on a microcontroller), constants |
volatile | May change outside the program’s flow: read it from memory every time | Hardware registers; variables shared with an interrupt (see Embedded Systems: GPIO and Interrupts) |
inline, restrict | Hints for the optimizer | Small 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
freeor 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 << 32on a 32-bitint); 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
| Stage | What happens | Typical error |
|---|---|---|
| Preprocess | Includes and macros expanded | “No such file”: a missing include path |
| Compile | Each .c file becomes an object file | Syntax and type errors; warnings you should read |
| Link | Objects and libraries joined into the program | “Undefined reference”: a function declared but not defined, or a missing library (-lm) |
| Run | The program starts at main | Segmentation 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
gdbon 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:
| Feature | What it gives you | Care needed |
|---|---|---|
| Classes and RAII | Data and functions together; a constructor acquires a resource and the destructor releases it, even on an early return | The best reason to use C++: no forgotten free or unlock |
References and const correctness | void f(const Matrix &m) passes without copying and without allowing changes | |
std::vector, std::array, std::string | Arrays that know their length and manage their memory | On a small MCU, avoid heap-allocating containers or use std::array |
| Templates | Generic code (a filter for float or fixed-point types) with no run-time cost | Error messages are long; compile times grow |
constexpr | Compute tables and constants at compile time | Good for lookup tables in flash |
| Exceptions, RTTI, iostreams | Convenient on a PC | Often disabled or avoided in embedded builds because of code size and timing |
- Embedded subset: many projects use C++ for classes, templates and
constexprbut 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,staticand 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
- Programming Languages: the overview of this guide.
- Bits, Bytes and Data Formats and Numbers and Floating Point: masks, byte order, integer and floating-point behavior.
- Embedded Systems: C on a microcontroller: memory, interrupts, timers.
- Bitwise and Register Field Calculator: masks and shifts.
- Software Practice, Testing and Debugging: tests, version control and debugging.
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.
