Post #1.1: OOP Fundamentals
Welcome to Post #1.1 of the Mastering Object-Oriented Programming (OOP), C++, and Embedded Systems Series!
This series will take you through the journey of mastering OOP and C++ concepts and applying them in embedded systems development. Each topic is split into two posts: the first part focuses on core C++ concepts, while the second part shows how to apply those concepts to embedded systems using the ESP32-S3 microcontroller.
Note: This series is based on my personal learning path. If you’re completely new to OOP, you may find some parts challenging, but if you have a basic understanding of OOP, you’ll find useful applications of these principles in embedded systems.
Check out the full series here.
In this post, we start with setting up your development environment and exploring the fundamentals of OOP and C++.
Setting Up Your Environment for C++ Development in VS Code
To get started with C++ development in Visual Studio Code (VS Code), follow these steps:
- Install VS Code:
- Download and install Visual Studio Code.
- Install the C++ Extension:
- Open VS Code, navigate to the Extensions view, and install the C/C++ extension by Microsoft.
- Install a C++ Compiler and Configure VS Code:
- Follow the guide on Setting Up C++ in VS Code for details on installing a C++ compiler and configuring your environment.
- Install PlatformIO:
- PlatformIO is an open-source ecosystem for embedded systems and IoT development. To install it:
- Open VS Code.
- Go to the Extensions view (
Ctrl+Shift+X
). - Search for “PlatformIO” and install the PlatformIO IDE extension.
- Once installed, you’ll find a new PlatformIO icon on the Activity Bar. Click it to open the PlatformIO home screen and follow the prompts to complete the setup.
- PlatformIO is an open-source ecosystem for embedded systems and IoT development. To install it:
With these tools in place, you’re ready to dive into both OOP and embedded systems projects.
Understanding Classes and Objects
Object-oriented programming is an approach to programming where you first create objects to represent real-world things, and then use those objects to model and solve your problem.
In C++, a class is a blueprint for creating objects. It encapsulates data and functions that operate on the data. An object is an instance of a class.
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
class Car {
public:
// Data members
string brand;
int year;
// Member function
void displayInfo() {
cout << "Brand: " << brand << ", Year: " << year << endl;
}
};
int main() {
// Create an object of Car
Car myCar;
myCar.brand = "Toyota";
myCar.year = 2020;
myCar.displayInfo();
return 0;
}
The Single Responsibility Principle (SRP) is one of the SOLID principles of object-oriented design and programming. It states:
A class should have only one reason to change.
In other words, a class should have only one job or responsibility. If a class has more than one responsibility, it becomes more complex and harder to maintain. Changes in one responsibility may affect others.
Example of SRP Violation:
Consider a class that manages both car data and car-related notifications:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Car {
public:
// Data members
string brand;
int year;
// Member functions
void displayInfo() {
cout << "Brand: " << brand << ", Year: " << year << endl;
}
void sendNotification() {
// Code to send a notification (e.g., email, alert)
}
};
In this example, the Car
class has two responsibilities: managing car data and sending notifications, violating SRP.
Adhering to SRP:
To follow SRP, separate these responsibilities into different classes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Car {
public:
// Data members
string brand;
int year;
// Member function
void displayInfo() {
cout << "Brand: " << brand << ", Year: " << year << endl;
}
};
class NotificationSender {
public:
void sendNotification() {
// Code to send a notification (e.g., email, alert)
}
};
Here, the Car
class focuses solely on managing car data, while the NotificationSender
class handles notifications. This separation adheres to SRP, making each class simpler and more maintainable.
Constructors and Destructors
Constructors and destructors are special member functions in C++ that play a crucial role in managing the lifecycle of objects and resources:
Constructor: A constructor is a special member function that is automatically called when an object is created. Its primary purpose is to initialize the object. You can define custom constructors to initialize object attributes or allocate resources.
Destructor: A destructor is a special member function that is automatically called when an object is destroyed. It is used to release resources that were allocated during the object’s lifetime. Even if you do not explicitly define a destructor, C++ provides a default version that will be automatically invoked.
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
class Resource {
public:
// Constructor
Resource() {
cout << "Resource acquired." << endl;
}
// Destructor
~Resource() {
cout << "Resource released." << endl;
}
};
int main() {
Resource res; // Constructor is called here
// Some operations with 'res'
return 0; // Destructor is called here
}
In this example, the Resource
class has a constructor that prints a message when an object is created and a destructor that prints a message when the object is destroyed. These messages help demonstrate when the constructor and destructor are invoked.
As a developer, you are responsible for cleaning up after yourself.
This quote emphasizes the importance of properly managing resources and ensuring that objects are cleaned up correctly to avoid memory leaks and other resource management issues.
Access Specifiers and Friend Functions
In object-oriented programming, access specifiers define how the members of a class can be accessed. They help to enforce encapsulation and protect the integrity of data.
Access specifiers control the visibility of class members. There are three main access specifiers in C++:
public
: Members are accessible from outside the class.private
: Members are accessible only within the class.protected
: Members are accessible within the class and its derived classes.
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
class Employee {
private:
string name;
double salary;
public:
// Constructor
Employee(string n, double s) : name(n), salary(s) {}
// Public method to display information
void displayInfo() {
cout << "Name: " << name << ", Salary: $" << salary << endl;
}
// Friend function declaration
friend void increaseSalary(Employee& emp, double amount);
};
// Friend function definition
void increaseSalary(Employee& emp, double amount) {
emp.salary += amount;
}
int main() {
Employee emp("John Doe", 50000);
emp.displayInfo();
// Increase salary using friend function
increaseSalary(emp, 5000);
emp.displayInfo();
return 0;
}
In this example:
name
andsalary
areprivate
members of theEmployee
class.displayInfo
is apublic
method that allows access to private members from within the class.increaseSalary
is afriend
function that can access private members ofEmployee
directly.
Friend Functions
A friend
function is a special function that is not a member of the class but has access to its private and protected members. While friend functions can be useful for certain operations, their use is sometimes criticized because they break the encapsulation principle by granting external functions access to a class’s internals. This can potentially lead to tightly coupled code and reduced flexibility.
Protected Members
The protected
access specifier allows access to members within the class itself and its derived classes. While it can be useful for extending functionality in derived classes, it also exposes members to subclasses that might not need direct access, which can sometimes lead to unintended dependencies and maintenance challenges.
Pillars of OOP
1. Encapsulation:
Encapsulation involves bundling the data (attributes) and methods (functions) that operate on the data into a single unit called a class. It also involves restricting direct access to some of an object’s components, which helps to prevent accidental interference and misuse.
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;
class BankAccount {
private:
double balance;
public:
BankAccount(double initialBalance) : balance(initialBalance) {}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}
double getBalance() const {
return balance;
}
};
int main() {
BankAccount account(1000.0);
account.deposit(500.0);
account.withdraw(200.0);
cout << "Balance: " << account.getBalance() << endl;
return 0;
}
Explanation of Encapsulation:
- Private Data Members:
- The
balance
variable is declared asprivate
. This means it cannot be accessed directly from outside theBankAccount
class. This restriction helps prevent unauthorized or unintended changes to the balance, ensuring that the internal state is protected from external interference.
- The
- Public Methods:
- The methods
deposit
,withdraw
, andgetBalance
are declared aspublic
. These methods provide controlled access to the privatebalance
data. By using these methods, external code can interact with theBankAccount
object without directly manipulating its internal state. - The
deposit
andwithdraw
methods include validation logic to ensure that operations are performed safely. For example,withdraw
checks that the amount is valid and does not exceed the available balance, protecting thebalance
from being set to an invalid state.
- The methods
- Constructor:
- The
BankAccount
constructor initializes thebalance
when a newBankAccount
object is created. The use of a constructor ensures that thebalance
is set to a valid initial value and encapsulates the process of setting up the object.
- The
- Accessing Data:
- The
getBalance
method allows external code to retrieve the current balance without directly accessing thebalance
variable. This method is marked asconst
, indicating it does not modify the object’s state, further emphasizing encapsulation and const-correctness.
- The
By encapsulating the balance
data and controlling how it is accessed and modified, the BankAccount
class ensures that the internal state is protected and maintains the integrity of the object. This approach reduces the risk of errors and enhances the maintainability of the code.
2. Abstraction:
Abstraction is a key principle of Object-Oriented Programming (OOP) that focuses on simplifying complex systems by exposing only the essential features of an object while hiding the underlying implementation details. This allows programmers to work at a higher level of abstraction, making it easier to manage and interact with complex systems.
How Abstraction Works:
- Hiding Complexity:
- Abstraction hides the complex details of an object’s implementation and only exposes the necessary interfaces to the user. This helps to reduce complexity and allows the user to interact with the object using simple and understandable methods.
- Abstract Classes:
- In C++, an abstract class is a class that contains at least one pure virtual function. A pure virtual function is a method that is declared but not defined in the abstract class. It is intended to be overridden by derived classes.
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using namespace std;
// Abstract base class
class Shape {
public:
// Pure virtual function
virtual void draw() const = 0; // This makes Shape an abstract class
};
// Derived class representing a Circle
class Circle : public Shape {
public:
// Overriding the pure virtual function
void draw() const override {
cout << "Drawing a circle." << endl;
}
};
// Derived class representing a Square
class Square : public Shape {
public:
// Overriding the pure virtual function
void draw() const override {
cout << "Drawing a square." << endl;
}
};
int main() {
Circle circle; // Create a Circle object
Square square; // Create a Square object
// Use polymorphism to call the draw method
circle.draw(); // Outputs: Drawing a circle.
square.draw(); // Outputs: Drawing a square.
return 0;
}
Explanation of the Example Code:
- Abstract Base Class
Shape
:Shape
is an abstract class because it contains a pure virtual functiondraw()
. This means thatShape
cannot be instantiated directly, and any derived class must provide an implementation for thedraw()
function.
- Derived Classes
Circle
andSquare
:- Both
Circle
andSquare
inherit fromShape
and provide their own implementations of thedraw()
function. This is an example of how abstraction allows different derived classes to provide specific behavior while adhering to a common interface defined by the base class.
- Both
- Using Abstraction:
- In the
main()
function, objects ofCircle
andSquare
are created. Although the exact implementation ofdraw()
is different for each class, the code interacts with these objects using the abstractShape
interface. - This allows for flexibility and scalability, as new shapes can be added by simply extending
Shape
and implementing thedraw()
function, without modifying the existing code that usesShape
.
- In the
3. Inheritance:
Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows a class to inherit attributes and methods from another class. This mechanism promotes code reuse, simplifies maintenance, and establishes a natural hierarchy between classes.
How Inheritance Works:
- Base Class:
- The base class (or parent class) is the class that provides common attributes and methods. It serves as the foundation for other classes.
- Derived Class:
- The derived class (or child class) inherits from the base class and can use its attributes and methods. It can also add new attributes and methods or override existing ones.
- Access Specifiers:
- In C++, the access specifiers (
public
,protected
,private
) control how the members of the base class are inherited by the derived class.
- In C++, the access specifiers (
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;
// Base class
class Animal {
public:
// Method in the base class
void eat() {
cout << "This animal is eating." << endl;
}
};
// Derived class
class Dog : public Animal {
public:
// Method in the derived class
void bark() {
cout << "Woof! Woof!" << endl;
}
};
int main() {
Dog myDog; // Create an object of the derived class Dog
// Access inherited method
myDog.eat(); // Outputs: This animal is eating.
// Access method of the derived class
myDog.bark(); // Outputs: Woof! Woof!
return 0;
}
Explanation of the Example Code:
- Base Class
Animal
:- The
Animal
class has a public methodeat()
that prints a message indicating that the animal is eating. This method is common to all animals and will be inherited by any class derived fromAnimal
.
- The
- Derived Class
Dog
:- The
Dog
class inherits fromAnimal
usingpublic
inheritance. This means that all public and protected members ofAnimal
are accessible inDog
in the same access level. - The
Dog
class adds its own methodbark()
that prints a message specific to dogs.
- The
- Using Inheritance in
main()
:- An object of the
Dog
class,myDog
, is created. myDog
can call both the inheritedeat()
method fromAnimal
and thebark()
method defined inDog
.- This demonstrates how the
Dog
class reuses the functionality provided by theAnimal
class while adding its own specific functionality.
- An object of the
Benefits of Inheritance:
- Code Reuse: Inheritance allows you to reuse code from existing classes, which reduces redundancy and improves maintainability. For example,
Dog
inheritseat()
fromAnimal
without having to redefine it. - Hierarchy: Inheritance helps establish a natural hierarchy among classes, making it easier to model real-world relationships. For instance,
Dog
is a specialized form ofAnimal
. - Extensibility: You can extend base classes by adding new functionality in derived classes, allowing for the flexible evolution of your software. New classes can be created by extending existing ones, leveraging the existing functionality and adding new features as needed.
4. Polymorphism:
Polymorphism is a core concept in Object-Oriented Programming (OOP) that allows objects to be treated as instances of their base class rather than their actual derived class. It enables a single interface to be used for different underlying data types, allowing for flexible and reusable code.
How Polymorphism Works:
- Base Class Pointer or Reference:
- You use a base class pointer or reference to refer to objects of derived classes. This allows the program to use a single interface to handle different types of objects.
- Virtual Functions:
- To enable polymorphism, you need to declare the base class method as
virtual
. This tells the compiler to use the derived class’s implementation of the method when it is called through a base class pointer or reference.
- To enable polymorphism, you need to declare the base class method as
- Method Overriding:
- Derived classes override the base class’s virtual method to provide specific implementations. The
override
keyword in C++11 and later is used to indicate that a method is intended to override a virtual method in the base class.
- Derived classes override the base class’s virtual method to provide specific implementations. The
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;
// Base class
class Base {
public:
// Virtual function
virtual void show() {
cout << "Base class" << endl;
}
};
// Derived class
class Derived : public Base {
public:
// Override virtual function
void show() override {
cout << "Derived class" << endl;
}
};
int main() {
Base* bptr; // Base class pointer
Derived derived; // Derived class object
bptr = &derived; // Base class pointer points to Derived class object
bptr->show(); // Calls Derived's show() method
return 0;
}
Explanation of the Example Code:
- Base Class
Base
:- The
Base
class has a virtual methodshow()
. Thevirtual
keyword indicates that this method can be overridden in derived classes.
- The
- Derived Class
Derived
:- The
Derived
class inherits fromBase
and overrides theshow()
method. Theoverride
keyword is used to specify that this method is intended to override theBase
class’s virtual method.
- The
- Polymorphism in
main()
:- A pointer to the
Base
class,bptr
, is created. - An object of the
Derived
class,derived
, is instantiated. - The
bptr
pointer is assigned the address of thederived
object. This meansbptr
now points to aDerived
object but is treated as aBase
pointer. - When
bptr->show()
is called, theshow()
method of theDerived
class is invoked, not the one in theBase
class. This is because theshow()
method is virtual, allowing the program to call the most derived version of the method.
- A pointer to the
Benefits of Polymorphism:
Flexibility: Polymorphism provides flexibility by allowing the same function call to behave differently based on the object type. This is useful in scenarios where you need to work with different derived classes using a common interface.
Code Reusability: You can write more generic and reusable code. For example, you can use a base class pointer to handle different types of derived class objects in a uniform way.
Maintainability: Polymorphism allows for easier maintenance and extensibility of code. If new derived classes are added, they can be integrated into existing code without changing the base class or the code that uses base class pointers or references.
Binding Techniques: Early Binding vs. Late Binding
Early Binding (Static Binding)
Definition:
- Early binding, also known as static binding, occurs when the method or function call is resolved at compile time. The compiler determines the exact method to be invoked based on the type of the reference or pointer used to make the call.
Characteristics:
- Compile-Time Resolution: The method or function to be called is determined during compilation.
- Efficiency: Early binding is generally more efficient because it involves direct method calls and does not require runtime checks.
- Function Overloading and Operator Overloading: Both function overloading (same function name but different parameters) and operator overloading (customizing operator behavior) are examples of early binding.
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
class Example {
public:
void display(int i) { // Function overload
cout << "Integer: " << i << endl;
}
void display(double d) { // Function overload
cout << "Double: " << d << endl;
}
};
int main() {
Example ex;
ex.display(10); // Calls display(int)
ex.display(10.5); // Calls display(double)
return 0;
}
In this example, which display()
method is called is determined at compile time based on the type of argument passed.
Late Binding (Dynamic Binding)
Definition:
- Late binding, also known as dynamic binding, occurs when the method or function call is resolved at runtime. The exact method to be invoked is determined based on the actual object type pointed to or referenced, not the type of the reference or pointer.
Characteristics:
- Run-Time Resolution: The method or function to be called is determined during runtime.
- Flexibility: Late binding allows for more flexible and dynamic behavior, such as calling overridden methods in derived classes.
- Virtual Functions: To achieve late binding, methods must be declared as
virtual
in the base class.
Example Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { // Virtual function
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void show() override { // Override virtual function
cout << "Derived class" << endl;
}
};
int main() {
Base* bptr;
Derived derived;
bptr = &derived; // Base class pointer to Derived class object
bptr->show(); // Calls Derived's show() method
return 0;
}
In this example, the method show()
is called on a Base
class pointer (bptr
) that actually points to a Derived
class object. The Derived
class’s show()
method is called, even though the pointer is of type Base*
. This decision is made at runtime, showcasing late binding.