Generated by Claude.Ai with helpful prompting from Dr. Forsyth
C++ is a powerful programming language widely used in embedded systems.
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 |
Declare variables of different types and print their values:
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 |
#include <stdint.h>
uint8_t sensorReading = 250; // Exactly 1 byte
int16_t temperature = -40; // Exactly 2 bytes
uint32_t timestamp = 1000000; // Exactly 4 bytes
Experiment with guaranteed-size integer types:
Both float and double store decimal numbers, but with different levels of precision.
| 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 |
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
Critical: The Cortex-M3 has NO hardware FPU (Floating-Point Unit)!
float operations: ~20-40 cycles per operation (addition/multiplication)double operations: ~100-200+ cycles per operation (much slower!)float over double - it's 3-5x faster and uses half the memory.
See the precision difference between float and double:
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;
}
#include <iostream> - Includes library for input/output operationsusing namespace std; - Allows us to use standard library functions without prefixint main() - The main function where program execution begins{ } - Curly braces define the beginning and end of a code blockcout - Outputs text to the screenendl - Ends the line (newline character)return 0; - Indicates successful program completion;
Run this Hello World program, then modify it to print your name:
Variables must be declared before use. Declaration specifies the type and name.
type variableName = value;
int sensorValue = 0;
float temperature = 25.5;
char grade = 'A';
bool isActive = true;
Complete the code to declare an integer variable 'count' set to 10, then print it:
In C++, there are two ways to work with text: C-style char arrays and C++ strings
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"
#include <string>
using namespace std;
string greeting = "Hello";
greeting = greeting + " World"; // Concatenation is easy!
int length = greeting.length(); // 11
| 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 |
Working with C-style char arrays:
Working with C++ string class:
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)
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)
int a = 10; float b = 3.0; int c = a / b; will give c = 3 (truncated to int)
Calculate the sum of 15 and 27 using int type, store in 'result', and print it:
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
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
Experiment with comparison and assignment operators:
Use comparison operators to make decisions in your code with if, if-else, and if-else if-else statements.
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
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
}
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
}
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
{ } even for single statements to avoid bugs!
Write an if-else statement to check if temperature is within safe range (20-30):
Arrays store multiple values of the same type in consecutive memory locations.
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)
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!
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!
int myArray[3] = {10, 20, 30};
myArray[5] = 99; // OUT OF BOUNDS! Writing to unknown memory!
What can happen:
int data[10];
int arraySize = sizeof(data) / sizeof(data[0]); // 10
Create an array of 5 sensor readings and print the third element (index 2):
For loops repeat a block of code a specific number of times.
for (initialization; condition; increment) {
// Code to repeat
}
for (int i = 0; i < 5; i++) {
// Runs 5 times: i = 0, 1, 2, 3, 4
}
Write a for loop that prints numbers from 1 to 5:
For loops are perfect for iterating through arrays to process each element.
int data[5] = {10, 20, 30, 40, 50};
// Print all elements
for (int i = 0; i < 5; i++) {
cout << "Element " << i << ": " << data[i] << endl;
}
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
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
Complete the code to sum all elements in the array:
While loops repeat as long as a condition is true.
while (condition) {
// Code to repeat
}
int count = 0;
while (count < 5) {
count++;
}
Write a while loop that prints numbers from 1 to 5:
Functions are reusable blocks of code that perform a specific task.
returnType functionName(parameters) {
// Function body
return value;
}
void printMessage() {
cout << "Hello!" << endl;
}
int add(int a, int b) {
return a + b;
}
int result = add(5, 3); // result = 8
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;
}
Complete the function to greet a user, then call it from main:
Understanding how data is passed to functions is critical for embedded systems.
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;
}
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;
}
// 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
}
See the difference between pass by value and pass by reference:
Functions must be declared before they can be used. Two approaches:
// 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;
}
// 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;
}
#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
}
Practice using forward declarations to organize your code:
float celsiusToFahrenheit(float celsius) {
return (celsius * 9.0 / 5.0) + 32.0;
}
float temp = celsiusToFahrenheit(25.0); // 77.0
Create a function called 'multiply' that takes two ints and returns their product. Then call it:
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;
}
Modify the complete example: add a function to find the maximum sensor reading:
Put all your skills together!
Create a function that:
A pointer is a variable that stores the memory address of another variable.
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
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)
Create a pointer and see how the & and * operators work:
Understanding pointer values, memory locations, and dereferencing.
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
Memory: Address: Variable: Value:
0x...8ac num 100
0x...8b0 ptr 0x...8ac (points to num!)
num stores the value 100ptr stores the address of num (where num lives in memory)*ptr accesses the value that ptr points to (100)num and ptr have their own addresses in memory!See all the different addresses and values:
Pointers can point to any data type. The type matters for correct memory access!
// 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...
int x = 100;
float* wrongPtr = (float*)&x; // Type mismatch! Dangerous!
cout << *wrongPtr; // Garbage - interprets int bytes as float!
Create variables and pointers, then print their values and addresses:
In C++, arrays and pointers are closely related. An array name is actually a pointer to its first element!
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)
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
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]
Experiment with array-pointer relationships and pointer arithmetic:
There are two ways to iterate through arrays: traditional indexing and pointer arithmetic.
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
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
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
| 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 |
Using traditional array indexing to iterate:
Using pointers to iterate through the array:
A struct (structure) groups related variables of different types into a single unit.
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
};
// 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!
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
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;
Create a struct and explore how data is organized in memory: