1 / 27

Introduction to C++

ENGR 445: Embedded System Design

Generated by Claude.Ai with helpful prompting from Dr. Forsyth

What is C++?

C++ is a powerful programming language widely used in embedded systems.

Why C++ for Embedded Systems?

Key Point: C++ gives you the power to control hardware directly while providing tools to organize your code effectively.

Data Types

Variables store data. Each variable has a type that determines what kind of data it can hold.

Type Description Example Values Typical Size
int Integer numbers -10, 0, 42 4 bytes
long Larger integers -1000000, 2147483647 4-8 bytes*
unsigned int Non-negative integers 0, 42, 65535 4 bytes
float Single precision 3.14, -0.5 4 bytes
double Double precision 3.14159265 8 bytes
char Single character 'A', 'x' 1 byte
bool True or false true, false 1 byte
Important: *Size can vary by system! Always check your platform's documentation for embedded systems.
🔨 Try It: Declare Different Data Types

Declare variables of different types and print their values:

Click "Run Code" to compile and execute...

Fixed-Width Integer Types (stdint.h)

For embedded systems, we need guaranteed sizes. Use #include <stdint.h>

Type Description Range Guaranteed Size
uint8_t Unsigned 8-bit integer 0 to 255 1 byte
int8_t Signed 8-bit integer -128 to 127 1 byte
uint16_t Unsigned 16-bit integer 0 to 65,535 2 bytes
int16_t Signed 16-bit integer -32,768 to 32,767 2 bytes
uint32_t Unsigned 32-bit integer 0 to 4,294,967,295 4 bytes
int32_t Signed 32-bit integer -2,147,483,648 to 2,147,483,647 4 bytes

Example:

#include <stdint.h>

uint8_t sensorReading = 250;    // Exactly 1 byte
int16_t temperature = -40;      // Exactly 2 bytes
uint32_t timestamp = 1000000;   // Exactly 4 bytes
Best Practice: Use these types in embedded systems for consistent behavior across platforms!
🔨 Try It: Fixed-Width Types

Experiment with guaranteed-size integer types:

Click "Run Code" to compile and execute...

Single vs Double Precision

Both float and double store decimal numbers, but with different levels of precision.

Key Differences:

Feature float (Single Precision) double (Double Precision)
Size 4 bytes (32 bits) 8 bytes (64 bits)
Precision ~6-7 decimal digits ~15-16 decimal digits
Range ±3.4 × 1038 ±1.7 × 10308
Best for Memory-constrained systems High-precision calculations

Example - Precision Difference:

float pi_f = 3.14159265358979;    // Stored as ~3.141593
double pi_d = 3.14159265358979;   // Stored as ~3.14159265358979

cout << pi_f << endl;  // 3.14159
cout << pi_d << endl;  // 3.14159265358979

Performance on ARM Cortex-M3:

Critical: The Cortex-M3 has NO hardware FPU (Floating-Point Unit)!

ARM Cortex-M3 Guidance: Avoid floating-point math when possible! Use integer arithmetic (scaled integers) for time-critical code. If you must use FP, prefer float over double - it's 3-5x faster and uses half the memory.
🔨 Try It: Float vs Double Precision

See the precision difference between float and double:

Click "Run Code" to compile and execute...

Anatomy of a C++ Program

Every C++ program has a basic structure. Let's break it down:

#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

Breaking it down:

Remember: Every statement in C++ ends with a semicolon ;
🔨 Try It: Your First C++ Program

Run this Hello World program, then modify it to print your name:

Click "Run Code" to compile and execute...

Variables and Declaration

Variables must be declared before use. Declaration specifies the type and name.

Syntax:

type variableName = value;

Examples:

int sensorValue = 0;
float temperature = 25.5;
char grade = 'A';
bool isActive = true;
🔨 Try It: Declare and Print a Variable

Complete the code to declare an integer variable 'count' set to 10, then print it:

Click "Run Code" to compile and execute...

Strings and Char Arrays

In C++, there are two ways to work with text: C-style char arrays and C++ strings

C-Style Char Arrays:

char message1[6] = "Hello";  // Must include space for '\0'
char message2[] = "World";    // Size determined automatically

// Access individual characters
char first = message1[0];     // 'H'
message1[0] = 'h';           // Change to "hello"

C++ Strings (using string class):

#include <string>
using namespace std;

string greeting = "Hello";
greeting = greeting + " World";  // Concatenation is easy!
int length = greeting.length();   // 11

Key Differences:

Feature Char Array String Class
Size Fixed, must specify Dynamic, grows as needed
Memory Less overhead More overhead
Null terminator Manual ('\0') Automatic
Best for Embedded systems, fixed data Complex text manipulation
For Embedded Systems: Char arrays are often preferred due to lower memory usage and predictable behavior.
🔨 Try It: C-Style Char Array

Working with C-style char arrays:

Click "Run Code" to compile and execute...
🔨 Try It: C++ String Class

Working with C++ string class:

Click "Run Code" to compile and execute...

Basic Operators

Arithmetic Operators (use int for whole numbers):

int a = 10, b = 3;
int sum = a + b;        // 13 (integer result)
int diff = a - b;       // 7 (integer result)
int product = a * b;    // 30 (integer result)
int quotient = a / b;   // 3 (integer division truncates!)
int remainder = a % b;  // 1 (modulo only works with int)

With floating-point (use float/double for decimals):

float x = 10.0, y = 3.0;
float result = x / y;   // 3.333333 (float preserves decimals)

double pi = 3.14159;
double area = pi * 5.0 * 5.0;  // 78.53975 (double for precision)

Choosing the right type:

Warning: Mixing int and float can cause unexpected results!
int a = 10; float b = 3.0; int c = a / b; will give c = 3 (truncated to int)
🔨 Try It: Arithmetic Operations (int)

Calculate the sum of 15 and 27 using int type, store in 'result', and print it:

Click "Run Code" to compile and execute...

Comparison Operators

int a = 10, b = 3;

bool result1 = (a == b);   // Equal: false
bool result2 = (a != b);   // Not equal: true
bool result3 = (a > b);    // Greater than: true
bool result4 = (a < b);    // Less than: false
bool result5 = (a >= b);   // Greater or equal: true
bool result6 = (a <= b);   // Less or equal: false

Assignment Operators:

int x = 5;
x += 3;    // x = x + 3  (x becomes 8)
x -= 2;    // x = x - 2  (x becomes 6)
x *= 4;    // x = x * 4  (x becomes 24)
x /= 3;    // x = x / 3  (x becomes 8)

x++;       // Increment by 1
x--;       // Decrement by 1
🔨 Try It: Comparison and Assignment Operators

Experiment with comparison and assignment operators:

Click "Run Code" to compile and execute...

Conditional Statements

Use comparison operators to make decisions in your code with if, if-else, and if-else if-else statements.

Simple if Statement:

int temperature = 75;

if (temperature > 70) {           // Check if temp is greater than 70
    cout << "It's warm!" << endl; // Only runs if condition is true
}                                  // If false, skip this block

if-else Statement:

int voltage = 3;

if (voltage >= 5) {                 // Check if voltage is 5 or higher
    cout << "System ON" << endl;    // Runs if condition is true
} else {                            // Otherwise (voltage < 5)
    cout << "System OFF" << endl;   // Runs if condition is false
}

if-else if-else Statement:

int sensorValue = 250;

if (sensorValue < 100) {            // First check: is it less than 100?
    cout << "Low" << endl;          // Runs if < 100
} else if (sensorValue < 200) {     // Second check: is it less than 200?
    cout << "Medium" << endl;       // Runs if 100-199
} else if (sensorValue < 300) {     // Third check: is it less than 300?
    cout << "High" << endl;         // Runs if 200-299
} else {                            // All conditions failed
    cout << "Critical" << endl;     // Runs if >= 300
}

Multiple Conditions:

int temp = 72;
int humidity = 45;

// Check multiple conditions at once (ALL must be true)
if (temp >= 68 && temp <= 75 && humidity < 60) {
    cout << "Comfortable conditions" << endl;  // Runs if all 3 are true
} else {                                       // If ANY condition is false
    cout << "Adjust environment" << endl;     // Take corrective action
}

// && means AND (all must be true), || means OR (any can be true), ! means NOT
Tip: Always use curly braces { } even for single statements to avoid bugs!
🔨 Try It: Temperature Check

Write an if-else statement to check if temperature is within safe range (20-30):

Click "Run Code" to compile and execute...

Arrays

Arrays store multiple values of the same type in consecutive memory locations.

Declaring Arrays:

int temperatures[7];              // Array of 7 integers (UNINITIALIZED - DANGEROUS!)
int values[5] = {10, 20, 30, 40, 50};  // Initialize with values (SAFE)
float voltages[] = {3.3, 5.0, 12.0};   // Size determined automatically (3)

⚠️ Why Uninitialized Arrays Are Dangerous:

int data[5];                // UNINITIALIZED - contains random garbage!
cout << data[0];            // Could print 0, 42847629, or anything!

int safe[5] = {0};          // GOOD: Initialize all to 0
int safe2[5] = {1,2,3,4,5}; // GOOD: Initialize all values

Problem: Uninitialized arrays contain whatever random data was in that memory location. This causes unpredictable behavior and hard-to-find bugs!

Accessing Array Elements:

int sensors[4] = {100, 200, 300, 400};

int first = sensors[0];     // 100 (indices start at 0!)
int second = sensors[1];    // 200
sensors[2] = 350;          // Change third element to 350

// sensors[4] would be OUT OF BOUNDS - undefined behavior!

⚠️ Out-of-Bounds Access - CRITICAL DANGER:

int myArray[3] = {10, 20, 30};
myArray[5] = 99;  // OUT OF BOUNDS! Writing to unknown memory!

What can happen:

Array Size:

int data[10];
int arraySize = sizeof(data) / sizeof(data[0]);  // 10
Critical Rules:
1. ALWAYS initialize arrays before use!
2. Array indices: 0 to N-1 (for array of size N)
3. Accessing out-of-bounds = undefined behavior = BAD!
4. In embedded systems, these bugs can be catastrophic!
🔨 Try It: Initialize and Access an Array

Create an array of 5 sensor readings and print the third element (index 2):

Click "Run Code" to compile and execute...

For Loops

For loops repeat a block of code a specific number of times.

Syntax:

for (initialization; condition; increment) {
    // Code to repeat
}

Example:

for (int i = 0; i < 5; i++) {
    // Runs 5 times: i = 0, 1, 2, 3, 4
}
🔨 Try It: Create a For Loop

Write a for loop that prints numbers from 1 to 5:

Click "Run Code" to compile and execute...

For Loops with Arrays

For loops are perfect for iterating through arrays to process each element.

Basic Array Iteration:

int data[5] = {10, 20, 30, 40, 50};

// Print all elements
for (int i = 0; i < 5; i++) {
    cout << "Element " << i << ": " << data[i] << endl;
}

Example 1: Sum All Elements

int numbers[6] = {5, 10, 15, 20, 25, 30};
int sum = 0;

for (int i = 0; i < 6; i++) {    // Loop through each element
    sum += numbers[i];            // Add each element to sum
}

cout << "Sum: " << sum << endl;  // Output: Sum: 105

Example 2: Find Minimum Value

int values[5] = {42, 17, 88, 9, 55};
int minValue = values[0];         // Start with first element

for (int i = 1; i < 5; i++) {    // Start from index 1
    if (values[i] < minValue) {   // If current is smaller
        minValue = values[i];     // Update minimum
    }
}

cout << "Min: " << minValue << endl;  // Output: Min: 9
🔨 Try It: Sum Array Elements

Complete the code to sum all elements in the array:

Click "Run Code" to compile and execute...

While Loops

While loops repeat as long as a condition is true.

Syntax:

while (condition) {
    // Code to repeat
}

Example:

int count = 0;
while (count < 5) {
    count++;
}
🔨 Try It: Create a While Loop

Write a while loop that prints numbers from 1 to 5:

Click "Run Code" to compile and execute...

Functions

Functions are reusable blocks of code that perform a specific task.

Syntax:

returnType functionName(parameters) {
    // Function body
    return value;
}

Example without return:

void printMessage() {
    cout << "Hello!" << endl;
}

Example with parameters and return:

int add(int a, int b) {
    return a + b;
}

int result = add(5, 3);  // result = 8

Variable Scope:

int globalVar = 100;        // Visible everywhere

void myFunction() {
    int localVar = 50;      // Only visible inside myFunction
    cout << globalVar;      // OK: can access global
    cout << localVar;       // OK: can access local
}

int main() {
    cout << globalVar;      // OK: can access global
    cout << localVar;       // ERROR: localVar not visible here!
    return 0;
}
Key Point: Variables declared inside a function are local - they only exist within that function. They're created when the function is called and destroyed when it returns.
🔨 Try It: Create a Simple Function

Complete the function to greet a user, then call it from main:

Click "Run Code" to compile and execute...

Function Parameters: Pass by Value

Understanding how data is passed to functions is critical for embedded systems.

Pass by Value (Default):

void modifyValue(int x) {
    x = 100;                   // Changes local copy only
}

int main() {
    int num = 5;
    modifyValue(num);          // Pass copy of num
    cout << num;               // Still 5! Original unchanged
    return 0;
}

Pass by Reference (with &):

void modifyValue(int& x) {     // Note the &
    x = 100;                   // Changes original!
}

int main() {
    int num = 5;
    modifyValue(num);          // Pass reference to num
    cout << num;               // Now 100! Original changed
    return 0;
}

⚠️ Why Not Pass Large Objects by Value?

// BAD for embedded systems - copies entire array!
void processArray(int data[1000]) {  // Actually passes pointer, but...
    // ...
}

// BETTER - pass by reference (pointer)
void processArray(int* data, int size) {
    // No copying, just pass memory address
}
Embedded Systems Rule: Passing by value creates a COPY - wastes memory and time! For arrays, structs, or large data:
• Use pointers or references
• Saves RAM (critical on microcontrollers)
• Saves CPU cycles (no copying)
• But be careful - you can now modify the original!
🔨 Try It: Pass by Value vs Reference

See the difference between pass by value and pass by reference:

Click "Run Code" to compile and execute...

Function Declarations and Visibility

Functions must be declared before they can be used. Two approaches:

Approach 1: Define Before Use

// Define function first
int multiply(int a, int b) {
    return a * b;
}

int main() {
    int result = multiply(5, 3);  // OK: multiply is already defined
    return 0;
}

Approach 2: Forward Declaration (Prototype)

// Declare function signature at top (prototype)
int multiply(int a, int b);

int main() {
    int result = multiply(5, 3);  // OK: compiler knows multiply exists
    return 0;
}

// Define function later
int multiply(int a, int b) {
    return a * b;
}

Best Practice for Embedded Systems:

#include 
using namespace std;

// Forward declarations at top
void initSensors();
int readTemperature();
void processData(int* data, int size);

int main() {
    initSensors();
    int temp = readTemperature();
    // ...
    return 0;
}

// Function definitions below
void initSensors() {
    // Initialization code
}

int readTemperature() {
    return 25;  // Simplified
}
Why Forward Declarations?
• Keep main() at top for readability
• Functions can call each other
• Organize code better (common in embedded projects)
• Required when functions call each other recursively
🔨 Try It: Forward Declarations

Practice using forward declarations to organize your code:

Click "Run Code" to compile and execute...

More Function Examples

Example: Convert Celsius to Fahrenheit

float celsiusToFahrenheit(float celsius) {
    return (celsius * 9.0 / 5.0) + 32.0;
}

float temp = celsiusToFahrenheit(25.0);  // 77.0
🔨 Try It: Create a Function

Create a function called 'multiply' that takes two ints and returns their product. Then call it:

Click "Run Code" to compile and execute...

Putting It All Together

A complete example combining everything we've learned:

#include <iostream>
using namespace std;

// Function to calculate average
float calculateAverage(int values[], int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += values[i];
    }
    return (float)sum / size;
}

int main() {
    int sensorData[5] = {20, 25, 22, 28, 23};
    
    // Calculate average
    float avg = calculateAverage(sensorData, 5);
    cout << "Average: " << avg << endl;
    
    // Check if in acceptable range
    if (avg >= 20.0 && avg <= 30.0) {
        cout << "System OK" << endl;
    }
    
    return 0;
}
🔨 Try It: Complete Sensor System

Modify the complete example: add a function to find the maximum sensor reading:

Click "Run Code" to compile and execute...

Final Challenge

Put all your skills together!

🎯 Challenge: Temperature Monitor

Create a function that:

  • Takes a temperature value (int)
  • Returns true if temp is between 20 and 30 (inclusive)
  • Name it: isTemperatureNormal
  • Test it with values 25, 15, and 32
Click "Run Code" to compile and execute...

Introduction to Pointers

A pointer is a variable that stores the memory address of another variable.

Why Pointers Matter in Embedded Systems:

Pointer Syntax:

int x = 42;          // Regular integer variable
int* ptr = &x;       // Pointer to an integer

// Read this as: "ptr is a pointer to an int"
// The & operator gets the address of x

Two Key Operators:

int value = 10;
int* ptr = &value;   // & = "address-of" operator

cout << ptr;         // Prints the memory address (e.g., 0x7fff5fbff8ac)
cout << *ptr;        // * = "dereference" - gets value at address (10)
Key Concept: A pointer is itself an object (takes memory), but it points to another object. Think of it like a street address that tells you where a house is located.
🔨 Try It: Your First Pointer

Create a pointer and see how the & and * operators work:

Click "Run Code" to compile and execute...

Pointers and Memory Addresses

Understanding pointer values, memory locations, and dereferencing.

Complete Pointer Anatomy:

int num = 100;
int* ptr = #

// Print everything about the pointer
cout << "Value of num: " << num << endl;           // 100
cout << "Address of num: " << &num << endl;        // 0x7fff5fbff8ac (example)
cout << "Value of ptr: " << ptr << endl;           // 0x7fff5fbff8ac (same!)
cout << "Address of ptr: " << &ptr << endl;        // 0x7fff5fbff8b0 (different!)
cout << "Value at ptr: " << *ptr << endl;          // 100

Visual Representation:

Memory:     Address:        Variable:    Value:
            0x...8ac        num          100
            0x...8b0        ptr          0x...8ac (points to num!)

Key Points:

Important: The pointer itself occupies memory (typically 4 or 8 bytes on most systems). It's a variable that happens to store an address as its value.
🔨 Try It: Explore Memory Addresses

See all the different addresses and values:

Click "Run Code" to compile and execute...

Pointers with Different Data Types

Pointers can point to any data type. The type matters for correct memory access!

Examples with Various Types:

// Integer pointer
int num = 42;
int* pNum = #
cout << "int value: " << *pNum << endl;        // 42
cout << "int address: " << pNum << endl;       // 0x7fff...

// Float pointer
float temp = 98.6;
float* pTemp = &temp;
cout << "float value: " << *pTemp << endl;     // 98.6
cout << "float address: " << pTemp << endl;    // 0x7fff...

// Character pointer
char grade = 'A';
char* pGrade = &grade;
cout << "char value: " << *pGrade << endl;     // A
cout << "char address: " << (void*)pGrade << endl; // Need (void*) to print address

// Bool pointer
bool flag = true;
bool* pFlag = &flag;
cout << "bool value: " << *pFlag << endl;      // 1 (true)
cout << "bool address: " << pFlag << endl;     // 0x7fff...

⚠️ Type Safety Matters:

int x = 100;
float* wrongPtr = (float*)&x;  // Type mismatch! Dangerous!
cout << *wrongPtr;              // Garbage - interprets int bytes as float!
🔨 Try It: Explore Pointers

Create variables and pointers, then print their values and addresses:

Click "Run Code" to compile and execute...

Pointers and Arrays

In C++, arrays and pointers are closely related. An array name is actually a pointer to its first element!

Array Name as Pointer:

int arr[5] = {10, 20, 30, 40, 50};

// These are equivalent!
cout << arr;        // Address of first element: 0x7fff...
cout << &arr[0];    // Same address!

// Array name is a pointer to first element
int* ptr = arr;     // No & needed! arr already is an address
cout << *ptr;       // 10 (value at first element)

Pointer Arithmetic:

int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;

cout << *ptr;       // 10 (arr[0])
cout << *(ptr+1);   // 20 (arr[1])
cout << *(ptr+2);   // 30 (arr[2])

// ptr+1 doesn't just add 1 byte!
// It adds sizeof(int) bytes (usually 4)
// So ptr+1 points to the next int in the array

Relationship Diagram:

arr[0]  arr[1]  arr[2]  arr[3]  arr[4]
  10      20      30      40      50
  ↑
  ptr (points here)
  
ptr+0 = &arr[0] = address of arr[0]
ptr+1 = &arr[1] = address of arr[1]
ptr+2 = &arr[2] = address of arr[2]
Critical for Embedded Systems: Understanding the array-pointer relationship is essential for:
• Passing arrays to functions efficiently
• Working with hardware buffers
• Memory-mapped peripheral access
• DMA (Direct Memory Access) operations
🔨 Try It: Pointers and Arrays

Experiment with array-pointer relationships and pointer arithmetic:

Click "Run Code" to compile and execute...

Array Iteration: Indexing vs Pointers

There are two ways to iterate through arrays: traditional indexing and pointer arithmetic.

Method 1: Array Indexing (Traditional):

int data[5] = {10, 20, 30, 40, 50};

// Using index
for (int i = 0; i < 5; i++) {
    cout << data[i] << " ";
}
// Output: 10 20 30 40 50

Method 2: Pointer Arithmetic:

int data[5] = {10, 20, 30, 40, 50};
int* ptr = data;

// Using pointer
for (int i = 0; i < 5; i++) {
    cout << *(ptr + i) << " ";  // Or: cout << *ptr++;
}
// Output: 10 20 30 40 50

Method 3: Pointer Increment:

int data[5] = {10, 20, 30, 40, 50};
int* ptr = data;
int* end = data + 5;  // Pointer to one past the end

while (ptr < end) {
    cout << *ptr << " ";
    ptr++;  // Move to next element
}
// Output: 10 20 30 40 50

Which Method to Use?

Method Pros Cons Best For
Array Indexing Clear, readable, safe bounds Slightly slower (index calculation) General use, beginners
Pointer Arithmetic Faster, more flexible Easy to make mistakes, less readable Performance-critical code
Embedded Systems Note: Pointer arithmetic can be faster, but modern compilers often optimize array indexing to the same machine code. Use indexing for clarity unless profiling shows a bottleneck!
🔨 Try It: Array Indexing Method

Using traditional array indexing to iterate:

Click "Run Code" to compile and execute...
🔨 Try It: Pointer Arithmetic Methods

Using pointers to iterate through the array:

Click "Run Code" to compile and execute...

Structs and Memory Layout

A struct (structure) groups related variables of different types into a single unit.

Why Structs Matter in Embedded Systems:

Defining a Struct:

struct SensorData {
    uint8_t id;           // 1 byte - sensor identifier
    uint16_t reading;     // 2 bytes - sensor value
    float temperature;    // 4 bytes - temperature in Celsius
    bool isActive;        // 1 byte - sensor status
    uint32_t timestamp;   // 4 bytes - time of reading
};

Memory Layout and Alignment:

// What you might expect: 1+2+4+1+4 = 12 bytes
// What you actually get: More! (due to padding)

SensorData sensor;
cout << "Size of struct: " << sizeof(sensor) << " bytes" << endl;
// Typically 16 bytes, not 12!

Why Padding Exists:

CPUs access memory more efficiently when data is aligned to addresses that are multiples of their size:

Memory Layout (with padding shown as --):
Offset  Field          Size   Value (example)
0       id             1      [0x42]
1       --padding--    1      [--]
2       reading        2      [0x00FF]
4       temperature    4      [0x41C80000]
8       isActive       1      [0x01]
9       --padding--    3      [-- -- --]
12      timestamp      4      [0x00001234]
Total: 16 bytes
Key Insight: The compiler adds padding bytes to ensure each field starts at an address that's a multiple of its size. This makes memory access faster but wastes space!

Using Pointers to Explore Memory:

SensorData sensor = {1, 1024, 25.5, true, 1000000};
SensorData* ptr = &sensor;

// Access through pointer
ptr->id = 2;                    // Same as (*ptr).id = 2
cout << ptr->temperature;       // Access member through pointer

// Show memory addresses of each field
cout << "Struct address: " << ptr << endl;
cout << "id address: " << (void*)&(ptr->id) << endl;
cout << "reading address: " << (void*)&(ptr->reading) << endl;
cout << "temperature address: " << (void*)&(ptr->temperature) << endl;
🔨 Try It: Explore Struct Memory Layout

Create a struct and explore how data is organized in memory:

Click "Run Code" to compile and execute...