3.1 C++ Introduction and Types
The Evolution and Core Principles of C++
Section titled “The Evolution and Core Principles of C++”
Initially known as “C with Classes,” C++ extended the C language by introducing object-oriented programming (OOP) features. These include the ability for engineers to define classes and create objects from these classes, facilitating a more organised and modular approach to software design. Beyond OOP, C++ also enhanced C with features like improved type checking.
C++ rapidly gained widespread adoption, largely due to its strong resemblance to C’s syntax. This familiarity allowed developers to integrate existing C code seamlessly, making it a natural progression for many projects. While C++ embraces an object-oriented organisational structure, it’s not a purely object-oriented language; it’s a hybrid language. This means it retains the efficiencies of C, such as direct access to types and pointers, while also offering the benefits of OOP.
Standardisation and Challenges
Section titled “Standardisation and Challenges”To address inconsistencies between various C++ compilers, the ANSI/ISO (American National Standards Institute/International Organisation for Standardisation) committee adopted a worldwide uniform language specification in 1998 (ISO/IEC 14882). Unfortunately, even today, not all compilers fully support this standard, leading to potential compatibility issues where code developed with one compiler on a particular operating system might not compile on another.
However, the standardisation efforts continue, with significant updates like C++11 (approved in 2011) and C++14 (approved in 2014). The most recent version, C++23, was approved in February 2023, and discussions for C++26 are already underway, demonstrating the ongoing evolution of the language.
C++ in Embedded Systems: Bridging Low-Level Control and High-Level Abstraction
Section titled “C++ in Embedded Systems: Bridging Low-Level Control and High-Level Abstraction”C++ significantly extends the capabilities of C by integrating OOP features and modern memory management mechanisms like smart pointers. This unique blend makes it a popular and powerful choice for contemporary embedded systems. Its core strength lies in its ability to bridge the gap between low-level hardware control, inherited from C, and the high-level abstractions provided by OOP and templates. This dual capacity allows developers to write highly efficient machine code that optimises system resources while maintaining a clean, modular design.
C++ is particularly effective for complex embedded applications, facilitating the development of scalable and maintainable code without compromising performance. Its strong compatibility with legacy C libraries further enhances its utility, allowing for the reuse of existing codebases within modern embedded ecosystems.
Practical Applications of C++ OOP in Smart Edge Environments
Section titled “Practical Applications of C++ OOP in Smart Edge Environments”The object-oriented capabilities of C++ find numerous practical applications in smart edge environments:
- Device Drivers: C++ is extensively used for writing device drivers, providing both the necessary low-level hardware control and the benefits of code abstraction through OOP. This combination results in efficient and flexible drivers for modern microcontrollers and peripherals. We discuss bit manipulation operations in this chapter.
- RTOS Integration: Many Real-Time Operating Systems (RTOS) integrate effectively with C++, enabling embedded developers to manage concurrency, multithreading, and task scheduling efficiently. Features like RAII (Resource Acquisition Is Initialisation) and smart pointers are particularly valuable for robust resource management in RTOS-based projects, ensuring resources are properly acquired and released. We discuss this in Chapter 5.
- Communication Protocols: C++ excels in implementing custom communication protocols and leveraging existing networking libraries. Its performance capabilities enable optimised message handling and error detection algorithms, which are crucial for efficient and reliable data transfer in connected edge systems.
- Graphical User Interfaces (GUIs): C++ and its object-oriented approach significantly simplifies the implementation of graphical user interfaces for embedded systems. Frameworks like Qt are prime examples of C++‘s strength in developing rich GUIs for embedded devices. Interesting, complex real-time 3D displays are becoming commonplace in edge applications such as modern vehicle navigation and parking support displays.
- Industrial Automation & IoT: C++ plays a crucial role in large-scale IoT and industrial automation projects, where its OOP features enable the creation of highly modular and maintainable applications. Notably, C++ accounts for a large fraction of automotive embedded software, underscoring its importance in industries demanding precision and reliability.
Considerations for Embedded Environments
Section titled “Considerations for Embedded Environments”While C++ offers substantial benefits, certain OOP features, if not managed carefully, can introduce performance or memory overheads in resource-constrained embedded environments. These considerations are crucial for optimising embedded system performance and are often discussed in detail in specialised literature.
This chapter focuses on providing examples of programming in the C of C++. It covers the fundamentals of the language for functional programming and prepares the reader for the next chapter in which classes are introduced.
The ‘C’ of C++ by Example
Section titled “The ‘C’ of C++ by Example”A First Application
Section titled “A First Application”The first program in a new language should always be “Hello world!”. There is a surprising amount of knowledge required to understand this, the simplest of programs:
// Hello World Application#include <iostream> // 1using namespace std; // 2
int main() { // 3 cout << "Hello world!\n"; // 4 return 0; // 5}- The
<iostream>header file is required for thecoutcall. The#includedirective is a C++ pre-processor1 instruction that causes the compiler, at compile time, to insert the contents of the specified file into your program at that location. This allows you to import libraries of code into your application as needed. The overall size of your application will increase by the size of each included header file, but the inclusion of<iostream>is necessary to perform any input/output operations, such as printing to the screen in C++ (this is discussed further later). - The
using namespacestatement is a feature of C++ that imports the header file into the appropriate namespace (we will explain this properly later). While convenient for small programs, using the entirestdnamespace in larger projects is often discouraged to avoid name collisions. - The
main()function is the entry point for all command-line C and C++ applications. In standard C++, a return type must always be specified;main()returns anintvalue. - The
coutcall sends the string"Hello World!"to the standard output stream, typically the screen. The output stream<<operator evaluates the parameter that follows it and places it onto the output stream. To end a line of output, you can use either<< "\n"or<< endl. - The
return 0statement tells the function to return the integer value0to the calling process. This statement is not strictly necessary inmain(), as it will return0by default if no return value is specified.
The source code for this example is contained in a file called HelloWorld.cpp.
You might assume that this is the shortest possible C++ program. It is not. The shortest valid C++ program is :
int main(){}In this case, no libraries are needed because there is no input or output. While historical versions of C allowed omitting the return type, modern C++ requires main to return int. This demonstrates C++’s flexibility in assuming default states and values — although this flexibility is also one of its greatest weaknesses.
In the upcoming and most modern agreed version of C++ (C++23), the Hello World application will be adapted slightly as follows:
#include <print>
int main() { std::println("Hello World!");}Few compilers currently support C++23 and g++ only supports the standard with the -std=c++2b compiler setting in early releases. The previous code will still work fine.
Knowledge Check
Section titled “Knowledge Check”Complete the Simple Hello World Program
Setting up a compiler
Section titled “Setting up a compiler”While closely related in the software development process, a C/C++ compiler and an Integrated Development Environment (IDE) serve distinct purposes. A compiler is a specialised program that translates human-readable source code (written in C or C++) into machine code or other low-level code that a computer’s processor can directly understand and execute. An IDE, on the other hand, is a comprehensive software application that bundles various development tools into a single, cohesive environment. This typically includes a source-code editor, build automation tools (which utilise compilers), and a debugger, among other features like syntax highlighting and code completion, all designed to streamline the entire software development workflow and enhance developer productivity. In essence, the compiler is the engine that converts your code, while the IDE is the complete workshop that provides all the tools you need to build and refine your software.

Figure 1. A terrifying AI representation of Octo Dog!
There are several C++ compilers and Integrated Development Environments that you can use for this module. Once the compiler is ANSI compliant there should be no issue in using it with this module as we just require a standard non-windowing compiler. A Unix/Linux compiler will also work. The current recommended compiler is GCC (GNU Compiler Collection) and the recommended IDE is VS Code. Other compilers such as Clang/LLVM (default compiler on MacOS with XCode) and Microsoft Visual C++ (MSVC) are also fine but not all will be available during the examination.
Integrated Development Environments (IDEs)
Section titled “Integrated Development Environments (IDEs)”Visual Studio Code (VS Code) is a lightweight, cross-platform source code editor widely used in modern C and C++ development. It offers powerful features such as intelligent code completion (IntelliSense), debugging, integrated terminal, and support for version control systems like Git. With extensions like the C/C++ extension from Microsoft, VS Code provides syntax highlighting, code navigation, refactoring tools, and seamless integration with build systems such as CMake and Make. Its flexibility, customisability, and active extension ecosystem make it a popular choice for both professional developers and students working on C/C++ projects across various platforms. See Figure 1.

Eclipse CDT (C/C++ Development Tooling) is a powerful, open-source IDE widely used for C and C++ development, particularly in embedded and enterprise environments. Built on the Eclipse platform, it offers advanced features such as code completion, static code analysis, refactoring tools, and a fully integrated debugger. Eclipse CDT supports a range of build systems, including Make, CMake, and managed builds, and integrates well with version control systems like Git through additional plugins. Its extensive plugin ecosystem and strong support for cross-compilation toolchains make it especially suitable for complex, large-scale, and embedded C/C++ projects.

Once you have your IDE in place you can use this simple example to test that it is working correctly:
// Hello World Application#include<iostream>using namespace std;
int main() { cout << "Hello EEN1097!\n"; return 0;}C++ Data Types and Functions
Section titled “C++ Data Types and Functions”Variables
Section titled “Variables”A variable is a data item stored in a block of memory that has been reserved for it. The variable type defines the amount of memory reserved. This is illustrated in Figure 4.

C++ supports many variable types, such as:
intintegers ( -5, -1, 0, 1, 3 etc.)charcharacters ( ‘a’, ‘b’, ‘c’, etc.) (typically 8-bits in size; the range0-255applies to anunsigned char)floatfloating point numbers (4.5552324, 1.001, -4.5553 etc.)doublelarger more accurate floating point numbers (i.e. to more decimal places or with a larger magnitude)long(long int) larger integer value range than int if using 16-bitintvalues.boolcontains the true or false value (i.e. a 1 or 0 respectively).short(short int) smaller integer value range thanintunsigned int0,1,2,3, etc.unsigned long0,1,2,3,4, etc.auto(C++11) tells the compiler to automatically deduce the type of a variable from its initialiser. (We will use this later)registerwas used in older C/C++ to suggest storing a variable in a CPU register for speed. It is deprecated since C++11 and was effectively removed in C++17 (though the keyword remains reserved).
Variables may be defined using these types, as illustrated in Figure 3.
int main() { float a = 25.0; int b = 545; double c = 123.0; char d = 'A'; // ASCII value of 'A' is 65. bool e = true;}The source for this test program is given in SizeofVariables.cpp. Using these variables we can assign values to them, modify them and print them to the output if required:
// Variables Application #include<iostream> using namespace std;
int main() { int x = 7, y = 10; //1 x=2; // assign variable x a value of 2 x++; // increment x by 1 x+=2; // increment x by 2 cout << "x equals " << x << "\n"; //2 }The source for this is in Variables1.cpp
- You can define multiple variables on one line.
- At this point, the program will result in an output of
x equals 5
Notes about variables:
- Variables can be introduced as required!
cinallows values to be read in.- C++ will usually complain if you assign a value of one type to a variable of another type of lower resolution.
- Variables can be initialised as they are defined.
- We can use the
constkeyword to protect the value of a variable from change. In embedded applications,constvariables may be stored in ROM (rather than RAM) and will only be placed there once, no matter how many times they are used. As such, useconstrather than#definewhenever possible. In modern C++,constexpris often preferred for constants that can be evaluated at compile time. - A
volatilevariable is one that can change outside of the control of the compiler, such as value changed by hardware, threading or interrupts. We use thevolatilekeyword to tell the compiler not to perform any form of optimisation on this data. We can also set this value asconst volatileto prevent the programmer from changing this value — It can still change, but outside the control of the programmer
Virtual Lab: Basic Memory
Section titled “Virtual Lab: Basic Memory”The lab below is pre-loaded with the textbook example: float a = 25.0;,
int b = 545;, and double c = 123.0;. Drag any type from the palette onto
the memory strip to allocate it. Click a placed variable to rename it or
change its value.
C++ Memory Lab
Drag a type onto memory and watch the variable claim the bytes its size demands.
There are certain conversion rules for basic types:
// Using variables with automatic conversion#include<iostream>
int main() { int x,y; //(see 1)
x = 6.73; // x becomes 6 cout << "x = " << x << "\n";
char c = 'w'; // (see 2) cout << "c = " << c << "\n";
x = c; // x becomes the integer // equivalent of 'w' which is 119 cout << "x = " << x << "\n";
y = 2.110; // y becomes 2 double d; // (see 3) d = y; // d becomes 2.0 cout << "d = " << d << "\n";
const float pi = 3.14159; // (see 4) //pi = 223.34; // would be an error if included return 0;}The source for this is in Variables2.cpp
xandyare being “declared” as variables. Variables in C++ are not automatically initialised to zero, so it would be better practice to use the statementint x=0, y=0;- The variable
cis initialised as it is defined. - The variable
dis introduced as required. - The
pivariable is defined as constant so that it cannot be modified without causing a compile-time error.
This program will output:
x = 6 c = w x = 119 d = 2The byte size of your variables matters and can have a significant impact on your code. For embedded applications floating point operations are very expensive, especially if you do not have a hardware floating point unit (what was once called a maths coprocessor). On the other hand, if you choose incorrect precision then strange things can happen. Take this very short segment of code:
#include <print>int main(){ float balance = 200000000.0f; balance = balance + 1.0f; std::println("My balance is now €{:12.2f}", balance);}What will this output be? Well you might expect that the balance of my bank account would now be €200,000,001, but when compiled using a 32-bit compiler it is actually:
My balance is now €200000000.00And I have lost €1. I can live with that, but if it was a ‘for loop’ with 100,000,000 x €1 lodgements, I would lose them too! Big numbers tend to consume small numbers on embedded devices.
Why does this happen? Well the precision of a 32-bit float is about 7 decimal places and since this number will actually be stored as 2.0e+8, it cannot represent 2.00000001e+8. We could easily solve this by using 64-bit floating point numbers in this case, but please remember that the same problem will recur, just with bigger numbers.
Knowledge Check
Section titled “Knowledge Check”Integer Division vs. Floating Point
Scope of Variables
Section titled “Scope of Variables”The scope of a variable is the area in a program where the variable is visible and valid. If we examine the code segment:
void someFunction() { int y = 5; x++; // invalid - x is not defined in someFunction() y++; // valid - y now equals 6 }
int main() { int x = 1; x++; // valid - x now equals 2 y++; // invalid - y is not defined in main() }The variable y is defined in someFunction() and so is only valid in that function. x is defined in the main() function and so is only valid in that method.
A more complex case can be seen below:
main() { int x = 7; cout << "x = " << x << "\n"; { cout << "x = " << x << "\n"; int x = 2; cout << "x = " << x << "\n"; } cout << "x = " << x << "\n"; }This code segment will result in the output:
x = 7 x = 7 x = 2 x = 7The first definition of x is initialised with the value 7. This first x is displayed first and next within the {}. As a new x is then defined within the {} it now has scope and is displayed on the next line with a value of 2. Once we go outside the {} then that x variable is destroyed and scope once again returns to the original x variable, resulting in an output of 7. Although the use of {} to create an inner level of scope might seem unusual it is only the general case of for(){}, if(){}, while(){} etc…
typedef
Section titled “typedef”When working with embedded devices it can be important to define exactly what we mean when working with types that are machine dependent. For example int might be a 32-bit number on one platform and a 16-bit number on a different platform. Clearly, this could cause errors if you were trying to port a code library between two such devices. To assist with this there are standard types available in the <cstdint> (or <stdint.h>) header file, and are included by default on recent C++ compilers. These include:
| Size | Signed | Unsigned |
|---|---|---|
| 8-bit | int8_t (signed char) | uint8_t (unsigned char) |
| 16-bit | int16_t | uint16_t |
| 32-bit | int32_t | uint32_t |
| 64-bit | int64_t | uint64_t |
Use these types whenever you require portability and readability.
We also sometimes need to give a variable type another name. We can use typedef to reduce the apparent complexity of the code, for example:
typedef unsigned char uchar; typedef unsigned int cardinal; typedef int integer; //etc..We can then just use this defined type as normal:
integer x;You should use this carefully and only where the definition of a necessary data type is required. If you type define int as elephant, it may make your code more interesting, but it will make it difficult for another programmer to comprehend.
One side effect in C++ is that if you are defining:
int* a,b;it does not create two pointers, rather one int pointer a and one int variable b as the * binds to the right. If you were to use a typedef for this then we would not have the same problem. For example,
typedef int* intPointer;intPointer a,b;declares two pointers a and b, both of dereference type int.
Modern Best Practice: Type Aliasing with using
Section titled “Modern Best Practice: Type Aliasing with using”While typedef is still common, modern C++ (C++11 and later) prefers the using keyword for type aliasing. It is often considered more readable because it follows the Name = Value assignment pattern:
using uchar = unsigned char;using cardinal = unsigned int;using intPointer = int*;The using keyword is also more flexible, as it can be used with templates (template aliases), which typedef cannot.
Volatile Variables
Section titled “Volatile Variables”Compilers are designed to optimise your code and generate efficient program code, however this sometimes leads to unintended consequences. For example, take the following segment of code:
int a, b; //using global variables for emphasis
void function() { a = 10; b = a * 7; if (a == 10) { cout << "a has the value 10" << "\n"; } else { cout << "a does not have the value of 10" << "\n"; }}Since the value of the variable a does not change between when it is assigned and when it is compared, an optimising compiler might reduce this code to something like:
int a, b; //using global variables for emphasis
void function(){ a = 10; b = a * 7; // The compiler 'knows' a is 10, so it removes the if check cout << "a has the value 10" << "\n";}Depending on your program and intention, this might not be correct or safe. For example, if you are working on a multithreaded embedded device, or a device where the variable a is actually mapped to an input/output hardware then it could indeed be possible that the variable a is modified external to the function between when the variable is created and the subsequent comparison. Therefore it might not be correct for you to allow the compiler to optimise this code.
The keyword volatile allows us to inform the compiler that a variable can be modified outside of the program it is compiling, and that it should not make assumptions about a variable that lead to incorrect optimisation. To fix this in the function above you can simply state:
volatile int a;int b;
void function() { …}Now, any code that uses the variable a will not optimise the code related to that variable. In embedded applications, this can also be useful for setting up delays that might be needed for serial communication. For example,
void delay() { volatile int a = 0; while (a++<100000) {}}This loop would likely be removed by an optimising compiler, but might be needed for an embedded application. Making the variable a (in this case) volatile prevents this removal.
The level of compiler optimisation is set at compile time. For example, with a simple C++ program:
molloyd@desktop:~/een1079$ g++ test.cpp -o test -O3Where O3 sets the most aggressive optimisation level. The default, -O0 is the least aggressive option, which is suitable for code development and debugging and then O1 to O3 increase the level of optimisation by performing additional tests on the code, which increases compilation time. See the note above.
Knowledge Check
Section titled “Knowledge Check”Find the line of code that causes a problem in this example, assuming that an optimising compiler is used to build the executable code. Click on the line of code that is causing the problem.
Spot the Hardware Polling Bug
Compound Types
Section titled “Compound Types”Structs in C/C++: Grouping Related Data
Section titled “Structs in C/C++: Grouping Related Data”A struct (short for structure) is a user-defined data type that groups variables of different data types under a single name. Think of it as a container that allows you to create a logical bundle of related data. Unlike classes in C++, a basic struct in C is a simple data aggregate without member functions (methods) or access specifiers (like public or private). This makes them a fundamental feature in C and a lightweight alternative to classes in C++ when you only need to store data. We discuss classes in the next chapter.
Example: A 2D Point.
A common use case for a struct is to represent a simple object that has multiple properties. For example, a point in a 2D coordinate system has an x-coordinate and a y-coordinate. Instead of using two separate variables, x and y, you can bundle them into a single struct for clarity and convenience.
Here’s how you can define and use a struct in C/C++:
#include <iostream>
// Define the Point struct with x and y 'grouped'struct Point { int x; int y;};
int main() { // Declare a variable of type Point Point myPoint;
// Access and assign values to its members myPoint.x = 10; myPoint.y = 20;
// Print the values std::cout << "The x-coordinate is: " << myPoint.x << "\n"; std::cout << "The y-coordinate is: " << myPoint.y << "\n"; return 0;}In this example, the Point struct is a blueprint for creating variables that each have an x and a y integer. We declare myPoint as a Point variable (note that the struct keyword is optional in C++) and then access its individual members using the dot operator (.). This makes the code more organised and readable, especially when dealing with complex data structures.
Enums in C/C++: Creating a Set of Named Constants
Section titled “Enums in C/C++: Creating a Set of Named Constants”An enumeration or enum in C/C++ is a user-defined data type that consists of a set of named integer constants. It provides a way to assign names to integral values, making code more readable and self-documenting. Instead of using “magic numbers” like 0, 1, 2, and 3, you can use meaningful names such as FORWARD, BACKWARD, LEFT, and RIGHT. This is especially useful in robotics and embedded systems where you need to define a limited set of states, commands, or modes.
Example: Robot Movement Commands.
Let’s imagine we are programming a simple robot. The robot can only perform a specific set of movements. We can use an enum to define these valid commands.
Here’s how you can declare an enum:
enum RobotMovement { FORWARD, BACKWARD, LEFT, RIGHT, STOP};Note: Modern C++ often uses enum class (scoped enums), which provides even better type safety and prevents name collisions by requiring the use of the RobotMovement:: prefix (e.g., RobotMovement::FORWARD).
By default, the compiler assigns integer values starting from 0. So, FORWARD is 0, BACKWARD is 1, LEFT is 2, RIGHT is 3, and STOP is 4. You can also manually assign specific values if needed, like so: enum Command { START = 1, PAUSE = 2, END = 4 };
One of the primary advantages of using an enum is type safety. It makes your code more robust by preventing you from passing invalid, out-of-range values to a function. A function that expects a RobotMovement enum can’t be accidentally called with an arbitrary integer that doesn’t correspond to a valid movement command.
Consider a function that controls the robot’s motors. Without enums, you might write a function that takes an int and then use a series of if-else or switch-case statements to handle the different values.
// This function is less safe and more error-pronevoid moveRobot(int command) { switch(command) { case 0: // FORWARD // ... code to move forward break; case 1: // BACKWARD // ... code to move backward break; // ... and so on default: // Handle invalid command break; }}The problem here is that you can call moveRobot(99), and the function will just go to the default case, which might not be what you want.
With the enum, the compiler helps you by ensuring only valid RobotMovement values are used.
#include <iostream>
enum RobotMovement { FORWARD, BACKWARD, LEFT, RIGHT, STOP};
void moveRobot(RobotMovement command) { switch(command) { case FORWARD: std::cout << "Moving robot forward." << "\n"; break; case BACKWARD: std::cout << "Moving robot backward." << "\n"; break; case LEFT: std::cout << "Turning robot left." << "\n"; break; case RIGHT: std::cout << "Turning robot right." << "\n"; break; case STOP: std::cout << "Stopping robot." << "\n"; break; }}
int main() { moveRobot(FORWARD); // This is a valid call. moveRobot(STOP); // This is a valid call. // moveRobot(99); // This will cause a compile-time error! return 0;}In this improved version, if a programmer tries to call moveRobot(99), the compiler will flag it as an error because 99 is not of the RobotMovement type. This prevents a whole class of bugs and makes the code more robust and maintainable. It clearly communicates the function’s expectations, making it easier for others to use and understand your code.
Modern Best Practice: enum class
Section titled “Modern Best Practice: enum class”The modern way to write the above code is using an enum class. This forces you to use the scope operator (e.g., RobotMovement::FORWARD), which prevents the names from leaking into the surrounding code and potentially causing conflicts.
enum class RobotMovement { FORWARD, BACKWARD, LEFT, RIGHT, STOP};
void moveRobot(RobotMovement command) { switch(command) { case RobotMovement::FORWARD: std::cout << "Moving forward." << "\n"; break; // ... and so on }}
int main() { moveRobot(RobotMovement::FORWARD); // Must use RobotMovement::}Knowledge Check
Section titled “Knowledge Check”Which of the following statements about the evolution of C++ are true?
Modern C++23 Printing
In a standard C++ 'Hello World' program using iostream, which statements are correct?
Preventing Compiler Optimisation
What are the roles of the 'const' and 'volatile' keywords in embedded systems?
Portable Fixed-Width Integers
Why might adding 1.0 to a float variable with a value of 200,000,000 result in no change to the value?
Enumerated State Management
Which of the following are benefits of using 'structs' and 'enums'?
Footnotes
Section titled “Footnotes”-
The C++ preprocessor runs before compilation and processes directives such as
#include,#define, and conditional compilation statements. It inserts code from header files, performs macro substitution, and controls compilation flow based on conditions. This is detailed in the next chapter. ↩
© 2026 Derek Molloy, Dublin City University. All rights reserved.