Category Progra 1

The SOLID principles

The SOLID principles are a set of five design principles in object-oriented programming that help developers write code that is easy to maintain and extend. They were introduced by Robert C. Martin, also known as Uncle Bob.

  • SRP – Single Responsibility Principle OCP
  • Open/Closed Principle LSP
  • Liskov’s Substitution Principle ISP
  • Interface Segregation Principle DIP
  • Dependency Inversion Principle

Here’s a brief overview of each principle with an example:

Single Responsibility Principle (SRP)

  • Definition: A class should have one, and only one, reason to change. This means that a class should only have one job or responsibility.
  • Example: Consider a Report class that has methods for data analysis and methods for printing the report. According to SRP, these responsibilities should be separated into two classes: one for data analysis (ReportAnalyzer) and another for report formatting and printing (ReportPrinter).
C++
// Example: Separate classes for handling user data and file operations.

class UserData {
public:
    UserData(std::string userName, int age) : userName(userName), age(age) {}
    // ... Other user-related functionalities ...

private:
    std::string userName;
    int age;
};

class FileHandler {
public:
    void saveUserData(const UserData& userData) {
        // Code to save user data to a file
    }
    // ... Other file handling functionalities ...
};

Intent: Each class should have one responsibility, one single purpose. This means that a class will do only one job, which leads us to conclude it should have only one reason to change.

SRP states that classes should be cohesive to the point that it has a single responsibility, where responsibility defines as “a reason for the change.”

SRP violated

  • As we start adding more domain objects like Movies, Games, etc. you have to implement save method for everyone separately which is not the actual problem.
  • The real problem arises when you have to change or maintain save functionality. For instance, some other day, you will no longer save data on files and adopted database. In this case, you have to go through every domain object implementation and need to change code all over, which is not good.
  • We have violated the Single Responsibility Principle by providing Book class reasons to change it:
    • Saving the Book
    • Code will also become repetitive, bloated, and hard to maintain.

Solution using SRP

  • Moving the persistence operation to another class will clearly separate the responsibilities and we will be free to exchange persistence methods without affecting our Book class.
  • Book should only take care of entries & things related to the book.
  • SaveFileManager all related saving code will be at one place. You can also templates it to accept more domain / class objects.

Benefits

  • This improves your development speed and makes your life as a software developer a lot easier.
  • Usually the requirements of a system change over time, and so does the design/architecture. The more responsibilities your class has, the more often you need to change it. If your class implements multiple responsibilities, they are no longer independent of each other.
  • Isolated changes reduce the breaking of other unrelated areas of the software.
  • As programming errors are inversely proportional to complexity, being easier to understand makes the code less prone to bugs and easier to maintain.
  • Having a single responsibility means the class should be reusable without or less modification.

Example C++

Open/Closed Principle (OCP)

  • Definition: Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means you should be able to add new functionality without changing existing code.
  • Example: If you have a PaymentProcessor class, and you want to add a new payment method, you shouldn’t have to change the PaymentProcessor class. Instead, you can create a new class that extends the PaymentProcessor class or implements a common interface, adhering to OCP.
C++
// Example: Separate classes for handling user data and file operations.

class UserData {
public:
    UserData(std::string userName, int age) : userName(userName), age(age) {}
    // ... Other user-related functionalities ...

private:
    std::string userName;
    int age;
};

class FileHandler {
public:
    void saveUserData(const UserData& userData) {
        // Code to save user data to a file
    }
    // ... Other file handling functionalities ...
};

Intent: classes should be open for extension, closed for modification.

  • You should be able to extend a classes behaviour, without modifying it.
  • A class should be open for extension but closed for modification. It means that whenever you need new functionality, it’s better to extend the base functionality instead of modifying it.

OCP violated

  • Each Product object has three properties: nameprice, and weight.
  • After designing the Product class and the whole e-commerce platform, a new requirement comes from the clients. They now want to buy digital products, such as e-books, movies, and audio recordings.
  • Probably you will need to modify the Product class, and that change may break other parts of code as well.
  • We can achieve that by redesigning the Product class and making it an abstract base class for all products.
  • The open-closed principle states that your system should be open to extension but should be closed for modification. Unfortunately what we are doing here is modifying the existing code which is a violation of OCP.

Solution using OCP

  • Created two more classes extending the Product base class. PhysicalProduct and DigitalProduct
  • Removed the weight property from the Product class, and PhysicalProduct has a weight property and DigitalProduct does not have one. Instead, it has a filePath property.

Benefits

  • Prevent unintentional behaviors of existing components, instead of modifying the current existing code, one should extend the existing component to ensure that the original behavior of the component is not affected.
  • Maintainability. This encourages loose coupling between components that are closed modifications and new functionality that is added.

Example in C++

Liskov Substitution Principle (LSP)

  • Definition: Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. Subclasses should extend their base classes without changing their behavior.
  • Example: A solution to this would be to have a more general class, say Animal, and then have separate subclasses for FlyingBird and NonFlyingBirdBird can then be an abstract class or interface with different implementations for fly() in FlyingBird (like Eagle) and NonFlyingBird (like Penguin), ensuring that substitutions of base class instances with subclass instances do not affect the program’s behavior.
C++
// Example: Birds

class Bird {
public:
    virtual void eat() = 0;
    virtual ~Bird() {}
};

class FlyingBird : public Bird {
public:
    virtual void fly() = 0; // Flying functionality
};

class Sparrow : public FlyingBird {
public:
    void fly() override {
        // Implementation for flying
    }

    void eat() override {
        // Implementation for eating
    }
};

class Ostrich : public Bird {
public:
    void eat() override {
        // Ostriches don't fly, so no fly method here
    }
};

Interface Segregation Principle (ISP)

  • Definition: No client should be forced to depend on methods it does not use. This principle suggests that it is better to have several specific interfaces rather than one general-purpose interface.
  • Example: If you have an interface IPrinter with methods for printing, scanning, and faxing, a printer class that only supports printing would still need to implement (or at least stub out) the scanning and faxing methods. According to ISP, it’s better to have separate interfaces like IPrintableIScannable, and IFaxable, and implement only those that are relevant.
C++
// Example: Multi-function machine

class IPrinter {
public:
    virtual void print() = 0;
};

class IScanner {
public:
    virtual void scan() = 0;
};

class Printer : public IPrinter {
public:
    void print() override {
        // Print functionality
    }
};

class Scanner : public IScanner {
public:
    void scan() override {
        // Scan functionality
    }
};

Dependency Inversion Principle (DIP)

  • Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend upon details. Details should depend upon abstractions. In simpler terms, depend on interfaces or abstract classes rather than concrete classes.
  • Example: If you have a high-level module OrderProcessor that directly creates an instance of a low-level module EmailNotifier to send notifications, it’s a violation of DIP. Instead, OrderProcessor should depend on an INotifier interface, and EmailNotifier should be an implementation of INotifier. This way, the dependency is on an abstraction, not a concretion, and it’s easy to switch to a different notifier in the future.
C++
// Example: Message sending

class IMessageSender {
public:
    virtual void sendMessage(const std::string& message) = 0;
    virtual ~IMessageSender() {}
};

class EmailSender : public IMessageSender {
public:
    void sendMessage(const std::string& message) override {
        // Send email
    }
};

class NotificationManager {
public:
    NotificationManager(IMessageSender* sender) : sender(sender) {}

    void notify(const std::string& message) {
        sender->sendMessage(message);
    }

private:
    IMessageSender* sender;
};

Law of Demeter

The Law of Demeter, often termed as the principle of “least knowledge”, is a design guideline for developing software, particularly with regard to object-oriented programming. The core idea of this law is to promote loose coupling between classes and objects. The basic principle can be summarized as follows:

  1. Each unit should have only limited knowledge about other units: Only units closely related to the current unit.
  2. Each unit should only talk to its friends; don’t talk to strangers: Only talk to immediate “friends”, which typically means methods of the same class or methods of closely related classes.
  3. Only talk to your immediate friends: A method of an object should invoke only the methods of:
    • The object itself
    • Objects passed in as parameters
    • Any object the method creates or instantiates
    • Any components of the object

In essence, the Law of Demeter suggests that a method M of an object O should only call the methods of the following kinds of objects:

  • O itself
  • M’s parameters
  • Any objects created/instantiated within M
  • O’s direct component objects

In a practical sense, this law aims to reduce dependencies between different parts of a code, making it more maintainable and less prone to bugs due to changes in unrelated system parts.

Examples in C++

Car context

Here’s a simple example to illustrate the Law of Demeter in C++:

Suppose we have a Car class, a Engine class, and a Piston class. According to the Law of Demeter, the Car class should not directly access the methods of the Piston class because it’s not a direct component of Car but rather a component of Engine, which in turn is a component of Car.

C++
class Piston {
public:
    void move() {
        // Piston movement logic
    }
};

class Engine {
private:
    Piston piston;
public:
    void start() {
        piston.move();  // This is okay, Engine knows about Piston
    }
};

class Car {
private:
    Engine engine;
public:
    void startEngine() {
        engine.start();  // This is okay, Car knows about Engine
    }
};

In this example:

  • Car can call methods on Engine because Engine is a direct component of Car.
  • Car does not call methods directly on Piston, adhering to the Law of Demeter, as Piston is not a direct component of Car.
  • Engine handles the interaction with Piston, maintaining encapsulation and reducing the coupling between Car and Piston.

This design helps in maintaining a modular structure where changes in the Piston class won’t affect the Car class directly, thus making the system more maintainable and scalable.

Hospital Context

Violation of the Law of Demeter

In this example, we’ll create a situation where a Hospital class directly accesses and manipulates the Patient objects’ data, violating the Law of Demeter.

C++
#include <iostream>
#include <string>
#include <vector>

class Patient {
public:
    Patient(const std::string& name) : name(name) {}

    void diagnose() {
        std::cout << name << " has been diagnosed." << std::endl;
    }

    const std::string& getName() const {
        return name;
    }

private:
    std::string name;
};

class Hospital {
public:
    void admitPatient(Patient& patient) {
        patients.push_back(patient);
        std::cout << "Admitted patient: " << patient.getName() << std::endl;
    }

    void diagnosePatients() {
        for (Patient& patient : patients) {
            // Violation of the Law of Demeter
            patient.diagnose();
        }
    }

private:
    std::vector<Patient> patients;
};

int main() {
    Hospital hospital;
    Patient patient1("Alice");
    Patient patient2("Bob");

    hospital.admitPatient(patient1);
    hospital.admitPatient(patient2);

    hospital.diagnosePatients();

    return 0;
}

In this code, the Hospital class directly calls the diagnose method of each Patient object in the diagnosePatients function. This violates the Law of Demeter because the Hospital class should not have to know the internal details of the Patient class.

Adherence to the Law of Demeter

In this improved version, we will ensure that the Hospital class does not directly manipulate the Patient objects’ data and instead interacts with them through a more abstract interface.

C++
#include <iostream>
#include <string>
#include <vector>

class Patient {
public:
    Patient(const std::string& name) : name(name) {}

    void diagnose() {
        std::cout << name << " has been diagnosed." << std::endl;
    }

    const std::string& getName() const {
        return name;
    }

private:
    std::string name;
};

class Hospital {
public:
    void admitPatient(Patient& patient) {
        patients.push_back(patient);
        std::cout << "Admitted patient: " << patient.getName() << std::endl;
    }

    void diagnosePatients() {
        for (Patient& patient : patients) {
            // Adhering to the Law of Demeter by not directly calling diagnose on patients
            performDiagnosis(patient);
        }
    }

private:
    std::vector<Patient> patients;

    void performDiagnosis(Patient& patient) {
        patient.diagnose();
    }
};

int main() {
    Hospital hospital;
    Patient patient1("Alice");
    Patient patient2("Bob");

    hospital.admitPatient(patient1);
    hospital.admitPatient(patient2);

    hospital.diagnosePatients();

    return 0;
}

In this improved version, the Hospital class adheres to the Law of Demeter by not directly calling the diagnose method on patients. Instead, it introduces a private performDiagnosis method that encapsulates the diagnosis behavior, ensuring that the Hospital class interacts with patients through a more abstract interface and doesn’t reach deep into the internal details of the Patient class.

High Cohesion / Low Coupling

High Cohesion

Definition: High cohesion in software development refers to the degree to which the elements inside a module, class, or component are closely related and focused on performing a single task or a closely related set of tasks. High cohesion is often seen as a desirable attribute, as it makes modules more understandable, easier to maintain, and less prone to errors.

Characteristics of High Cohesion:

  1. (SOLID Principle) Single Responsibility: Each module or class has a clear, well-defined purpose.
  2. Ease of Maintenance: Changes in one part of the system have minimal impact on other parts.
  3. Reusability: Highly cohesive modules can be easily reused in different parts of the system.
  4. Improved Readability and Understanding: Each module or class is clear and straightforward to understand.

Example: Consider a class in a music player application named AudioPlayer. If this class is highly cohesive, it would only contain methods and properties directly related to playing audio, such as play(), pause(), stop(), and volumeControl(). It wouldn’t have unrelated functionalities like managing user playlists or network handling.

Here, we’ll design a simple AudioPlayer class that exemplifies high cohesion by focusing solely on audio playback functionalities.

C++
class AudioPlayer {
public:
    void play() {
        // Code to start playback
    }

    void pause() {
        // Code to pause playback
    }

    void stop() {
        // Code to stop playback
    }

    void setVolume(int level) {
        // Code to adjust volume
    }

    // Other methods directly related to audio playing
};

In this AudioPlayer class, all methods are tightly related to the single responsibility of playing audio. This class does not include unrelated functionalities like managing playlists or network operations, thus maintaining high cohesion.

Low Coupling

Definition: Low coupling refers to the degree of independence between modules, classes, or components in a system. In a low-coupled system, changes in one module have minimal effect on other modules. This principle reduces the interdependencies between components, making the system easier to modify, extend, and maintain.

Characteristics of Low Coupling:

  1. Independence: Modules can function and be modified independently.
  2. Change Resilience: Changes in one module require fewer changes in others.
  3. Ease of Testing: Independent modules are easier to test in isolation.
  4. Flexibility and Scalability: The system can be easily expanded with minimal impact on existing components.

Example: In the same music player application, suppose there is a PlaylistManager class responsible for managing user playlists. In a low-coupled design, PlaylistManager would interact with other components like AudioPlayer through well-defined interfaces or APIs, rather than directly manipulating their internal states. This way, changes in the AudioPlayer class (like modifying its internal data structures) won’t require changes in PlaylistManager.

For low coupling, let’s have two classes, PlaylistManager and AudioPlayer. These classes interact with each other, but they are designed to minimize dependencies.

C++
class AudioPlayer {
public:
    void playSong(const std::string& song) {
        // Code to play a specific song
    }
};

class PlaylistManager {
private:
    AudioPlayer& player;
    std::vector<std::string> playlist;

public:
    PlaylistManager(AudioPlayer& player) : player(player) {}

    void addSongToPlaylist(const std::string& song) {
        playlist.push_back(song);
    }

    void playPlaylist() {
        for (const auto& song : playlist) {
            player.playSong(song);
            // Additional code to handle playlist playback
        }
    }
};

Combining High Cohesion and Low Coupling

In software design, the combination of high cohesion within modules and low coupling between modules is ideal. It allows each part of the system to be developed, maintained, and understood more easily while providing flexibility to the overall system architecture. This combination leads to a more robust, maintainable, and scalable system.

Examples

Violation the rule

These principles can lead to code that is easier to understand and maintain. Here’s an example in C++ where high cohesion and low coupling are violated in the context of a hospital, doctors, and patients:

C++
#include <iostream>
#include <string>
#include <vector>

class Patient {
public:
    Patient(const std::string& name) : name(name) {}

    void diagnose() {
        std::cout << name << " has been diagnosed." << std::endl;
    }

    const std::string& getName() const {
        return name;
    }

private:
    std::string name;
};

class Doctor {
public:
    Doctor(const std::string& name) : name(name) {}

    void treatPatient(Patient& patient) {
        std::cout << name << " is treating patient: " << patient.getName() << std::endl;
        patient.diagnose();
        std::cout << "Prescribing medication to " << patient.getName() << std::endl;
    }

private:
    std::string name;
};

class Hospital {
public:
    void admitPatient(Patient& patient) {
        patients.push_back(patient);
        std::cout << "Admitted patient: " << patient.getName() << std::endl;
    }

    void assignDoctor(Doctor& doctor, Patient& patient) {
        std::cout << "Assigning " << doctor.getName() << " to treat " << patient.getName() << std::endl;
        doctor.treatPatient(patient);
    }

private:
    std::vector<Patient> patients;
};

int main() {
    Hospital hospital;
    Patient patient1("Alice");
    Patient patient2("Bob");
    Doctor doctor1("Dr. Smith");
    Doctor doctor2("Dr. Johnson");

    hospital.admitPatient(patient1);
    hospital.admitPatient(patient2);

    hospital.assignDoctor(doctor1, patient1);
    hospital.assignDoctor(doctor2, patient2);

    return 0;
}

In this code, high cohesion and low coupling are violated in the following ways:

  1. Violation of Low Coupling:
    • The Doctor class is tightly coupled with the Patient class because it directly treats patients by calling their diagnose method.
    • The Hospital class is also tightly coupled with both Doctor and Patient classes as it assigns doctors to patients and calls methods on both.
  2. Violation of High Cohesion:
    • The Doctor class has responsibilities that are not directly related to being a doctor, such as prescribing medication. This leads to low cohesion within the class.

To improve the design and adhere to high cohesion and low coupling principles, you could refactor the code to have separate classes or interfaces for tasks like diagnosis and medication prescription, ensuring each class has a clear and single responsibility and reducing the dependencies between classes.

Refactoring by adherence to the rule

To improve the design and adhere to high cohesion and low coupling principles in the context of a hospital, doctors, and patients, we can refactor the code by introducing additional classes that better separate responsibilities. Here’s an improved example:

C++
#include <iostream>
#include <string>
#include <vector>

class Patient {
public:
    Patient(const std::string& name) : name(name) {}

    const std::string& getName() const {
        return name;
    }

private:
    std::string name;
};

class DiagnosisService {
public:
    void diagnosePatient(const Patient& patient) {
        std::cout << "Diagnosing patient: " << patient.getName() << std::endl;
        // Perform diagnosis logic here
    }
};

class MedicationService {
public:
    void prescribeMedication(const Patient& patient) {
        std::cout << "Prescribing medication to patient: " << patient.getName() << std::endl;
        // Perform medication prescription logic here
    }
};

class Doctor {
public:
    Doctor(const std::string& name) : name(name) {}

    void treatPatient(const Patient& patient, DiagnosisService& diagnosisService, MedicationService& medicationService) {
        std::cout << name << " is treating patient: " << patient.getName() << std::endl;
        diagnosisService.diagnosePatient(patient);
        medicationService.prescribeMedication(patient);
    }

    const std::string& getName() const {
        return name;
    }

private:
    std::string name;
};

class Hospital {
public:
    void admitPatient(const Patient& patient) {
        patients.push_back(patient);
        std::cout << "Admitted patient: " << patient.getName() << std::endl;
    }

    void assignDoctor(Doctor& doctor, const Patient& patient) {
        std::cout << "Assigning " << doctor.getName() << " to treat " << patient.getName() << std::endl;
        doctor.treatPatient(patient, diagnosisService, medicationService);
    }

private:
    std::vector<Patient> patients;
    DiagnosisService diagnosisService;
    MedicationService medicationService;
};

int main() {
    Hospital hospital;
    Patient patient1("Alice");
    Patient patient2("Bob");
    Doctor doctor1("Dr. Smith");
    Doctor doctor2("Dr. Johnson");

    hospital.admitPatient(patient1);
    hospital.admitPatient(patient2);

    hospital.assignDoctor(doctor1, patient1);
    hospital.assignDoctor(doctor2, patient2);

    return 0;
}

In this improved example:

  1. Low Coupling:
    • The Doctor class is no longer directly coupled with the Patient class. Instead, it relies on DiagnosisService and MedicationService to perform diagnosis and prescription, respectively.
    • The Hospital class also reduces its direct coupling with Doctor, Patient, and the associated services.
  2. High Cohesion:
    • Responsibilities are separated into distinct classes: Patient for patient-related information, DiagnosisService for diagnosis, MedicationService for medication prescription, Doctor for doctor-related information and treatment, and Hospital for patient management.
    • Each class now has a clearer and more focused responsibility.

This refactoring improves code maintainability and makes extending and modifying the system easier while adhering to the principles of high cohesion and low coupling.

General concepts

Conditionals

Conditionals in C++ are used to perform different actions based on different conditions. The if, else if, and else statements are commonly used.

C++
class ConditionalExample {
public:
    void checkNumber(int number) {
        if (number > 0) {
            std::cout << "Number is positive\n";
        } else if (number < 0) {
            std::cout << "Number is negative\n";
        } else {
            std::cout << "Number is zero\n";
        }
    }
};

Switch

The switch statement in C++ allows a variable to be tested for equality against a list of values.

C++
class SwitchExample {
public:
    void dayOfWeek(int day) {
        switch (day) {
            case 1: std::cout << "Monday\n"; break;
            case 2: std::cout << "Tuesday\n"; break;
            //... other cases
            default: std::cout << "Invalid day\n";
        }
    }
};

Bucle (Loops)

Loops in C++ are used to repeatedly execute a block of code. Here’s an example using a for loop.

C++
class LoopExample {
public:
    void printNumbers(int n) {
        for (int i = 1; i <= n; i++) {
            std::cout << i << " ";
        }
        std::cout << "\n";
    }
};

Vectors

Vectors in C++ are sequence containers representing arrays that can change in size.

C++
#include <vector>

class VectorExample {
public:
    std::vector<int> createVector(int size) {
        std::vector<int> vec(size, 0); // Vector of given size with all values initialized to 0
        return vec;
    }
};

Matrix

Matrix implementation can be done using a vector of vectors.

C++
#include <vector>

class MatrixExample {
public:
    std::vector<std::vector<int>> createMatrix(int rows, int cols) {
        std::vector<std::vector<int>> mat(rows, std::vector<int>(cols, 0));
        return mat;
    }
};

Class and Objects

Class

A class in programming is a blueprint or a template for creating objects. It defines the characteristics and behaviors that objects of that class will have. A class encapsulates data (attributes) and functions (methods) that operate on that data within a single unit.

Attributes represent the state of an object and can be of various data types, such as integers, strings, or custom-defined types. Methods define the actions or operations that can be performed on the object.

A class can have visibility specifiers, such as public, private, and protected, which control the accessibility of its members. public members are accessible from outside the class, private members are only accessible within the class itself, and protected members are accessible within the class and its derived classes.

Classes are the fundamental building blocks of object-oriented programming (OOP). They promote code reusability, maintainability, and help organize code into logical units.

Elements

  • Attributes: These are data members that hold the state of the object. They can be of any data type.
  • Methods: Functions defined inside a class that operate on or with the object’s attributes.
  • Visibility: The access specifiers (private, public, and protected) control the visibility of the members of a class. private members are accessible only within the class, public members are accessible from outside the class, and protected members are accessible within the class and its derived classes.
  • Direct and Indirect Access: Direct access refers to accessing members of a class directly using the dot operator. Indirect access involves using methods to manipulate or access the members, typically used for private members.

Object

An object is an instance of a class. When a class is defined, it acts as a blueprint, specifying the attributes and behaviors that objects of that class should possess. When an object is created, memory is allocated to store its attributes and methods.

An object represents a unique entity with its own state and behavior. The state of an object is determined by its attribute values, while the behavior is defined by the methods associated with the class.

Objects can interact with each other by invoking methods or accessing attributes of other objects. They can also be used to represent real-world entities, such as a person, car, or bank account, as well as abstract concepts or data structures.

Object-oriented programming allows for the creation of multiple objects from a single class, each with its own identity, state, and behavior. This concept of object instantiation enables modular and reusable code, as objects can be created, manipulated, and controlled independently.

Elements

  • State: Refers to the current values of an object’s attributes.
  • Identity: While the state of two objects can be identical, their identities are not. Each object occupies a unique memory location.
  • Abstraction and Encapsulation: Abstraction means exposing only the necessary details to the user, hiding the internal implementation. Encapsulation involves bundling the data (attributes) and the methods that operate on the data into a single unit (class), and keeping the details hidden.

Examples in C++

Full example with class and objects

A full combination of all the examples with classes and objects.

C++
class Car {
private: // Encapsulation
    std::string color; // Attribute
    int speed; // Attribute

public:
    Car(std::string c, int s) : color(c), speed(s) {} // Constructor

    void accelerate() { // Method
        speed += 10;
    }

    void display() const { // Method showing state
        std::cout << "Car color: " << color << ", Speed: " << speed << " km/h\n";
    }

    // Getter and Setter for encapsulation and direct/indirect access
    void setColor(std::string c) {
        color = c;
    }

    std::string getColor() const {
        return color;
    }
};

Usage Example

C++
int main() {
    ConditionalExample ce;
    ce.checkNumber(5);

    SwitchExample se;
    se.dayOfWeek(3);

    LoopExample le;
    le.printNumbers(5);

    VectorExample ve;
    auto vec = ve.createVector(10);

    MatrixExample me;
    auto mat = me.createMatrix(3, 3);

    Car car("Red", 50);
    car.accelerate();
    car.display();

    return 0;
}

This code provides a basic structure for each concept. Each class is designed to demonstrate a specific concept and should be extended or modified as needed to fit more complex scenarios or requirements.

Student class example

C++
#include <iostream>
#include <string>

class Student {
private:
    std::string name; // Attribute (Encapsulation)
    int age; // Attribute (Encapsulation)

public:
    // Constructor (Method for initialization)
    Student(std::string n, int a) : name(n), age(a) {}

    // Method to display student info (Abstraction)
    void display() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }

    // Getter (Indirect access to private attribute)
    int getAge() const {
        return age;
    }

    // Setter (Indirect access to private attribute)
    void setAge(int a) {
        if (a > 0) {
            age = a;
        }
    }

    // Additional methods can be added here...
};

int main() {
    Student student("Alice", 20); // Creating an object

    student.display(); // Direct access to public method

    std::cout << "Student's initial age: " << student.getAge() << std::endl; // Indirect access to private attribute

    student.setAge(21); // Indirect access to modify private attribute
    student.display(); // State of the object is now changed

    return 0;
}

In this example:

  • Student Class: Defines the blueprint for student objects with attributes (name, age) and methods (display, getAge, setAge).
  • Visibility: The name and age attributes are private, meaning they cannot be accessed directly outside the class.
  • Object: student is an instance of the Student class.
  • State: Defined by the values of name and age. For instance, the state changes when age is updated.
  • Identity: Each object of Student (e.g., student) has a unique memory location.
  • Abstraction: The display method provides a simple interface for showing student details, hiding the implementation.
  • Encapsulation: name and age are encapsulated within the Student class, and their access is controlled through public methods (getAge and setAge).

This example illustrates the fundamental concepts of classes and objects in C++, demonstrating how they can be used to create structured and maintainable code.

Classes and their objects

Instance members

Instance members in a C++ class are elements (variables or functions) that belong to instances of the class, rather than to the class itself. Each object (instance) of the class has its own separate copy of these members. There are two main types of instance members in a C++ class:

Instance Variables (Non-static Data Members): These are the variables defined in a class that are not declared as static. Each object of the class will have its own copy of these variables. They represent the state of an object.

Instance Methods (Non-static Member Functions): These are the functions defined in a class that are not declared as static. These methods operate on the instance variables of the object that invokes them.

Here’s an example to illustrate instance members in a C++ class:

C++
class Car {
    private:
        // Instance variable
        int speed;

    public:
        // Constructor
        Car() : speed(0) {}

        // Instance method
        void accelerate(int increment) {
            speed += increment;
        }

        // Another instance method
        int getSpeed() const {
            return speed;
        }
};

int main() {
    Car myCar; // Creating an object of Car
    myCar.accelerate(10); // Calling an instance method
    int currentSpeed = myCar.getSpeed(); // Using another instance method
    return 0;
}

In this example, speed is an instance variable, and accelerate and getSpeed are instance methods of the Car class. Each Car object will have its own speed, and the methods accelerate and getSpeed will operate on the speed of the Car object that calls them.

Class Members (Static)

Class members in C++ that are declared as static belong to the class itself, rather than to any particular instance of the class. This means that they are shared among all instances of the class. Static members can include both variables and functions.

Here are the key points about static members in C++:

Static Variables (Class Variables): These are variables declared with the static keyword within a class. They are shared by all objects of the class. All instances of the class access the same static variable, and any changes made to the variable are reflected across all instances.

Static Methods (Class Methods): These are functions declared as static within a class. They can be called without creating an instance of the class. Static methods can only access static variables or other static methods; they cannot access non-static members of the class.

Here’s an example to illustrate static members in a C++ class:

C++
#include <iostream>

class Car {
    private:
        static int totalCars; // Static variable

    public:
        // Constructor
        Car() {
            totalCars++; // Increment total cars
        }

        // Static method
        static int getTotalCars() {
            return totalCars;
        }
};

// Initialize the static member
int Car::totalCars = 0;

int main() {
    Car car1; // First instance of Car
    Car car2; // Second instance of Car

    // Access static method
    std::cout << "Total cars: " << Car::getTotalCars() << std::endl;

    return 0;
}

Virtual Functions

Virtual functions in C++ are a fundamental concept for achieving runtime polymorphism. They allow a function in a base class to be overridden in a derived class, enabling dynamic method binding.

Definition: A virtual function is a function in a base class that is declared using the keyword virtual. When a function is marked as virtual, it can be overridden in any derived class. The version of the function that gets executed is determined at runtime based on the type of the object that invokes the function.

Runtime Polymorphism

  • How It Works: When you have a pointer or a reference to a base class object, it can point to objects of the derived class as well. Calling a virtual function through this pointer or reference will execute the function defined in the derived class, if it is overridden there.

Example:

Consider an example with a base class Animal and derived classes Dog and Cat:

C++
#include <iostream>
using namespace std;

class Animal {
public:
    // Virtual function
    virtual void speak() {
        cout << "Some animal sound" << endl;
    }
};

class Dog : public Animal {
public:
    // Overriding the virtual function
    void speak() override {
        cout << "Woof Woof" << endl;
    }
};

class Cat : public Animal {
public:
    // Overriding the virtual function
    void speak() override {
        cout << "Meow Meow" << endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->speak();  // Outputs: Woof Woof
    animal2->speak();  // Outputs: Meow Meow

    delete animal1;    // Clean up
    delete animal2;    // Clean up
    return 0;
}

In this example:

  • Animal has a virtual function speak().
  • Dog and Cat override this function.
  • When speak() is called on an Animal pointer that points to a Dog or Cat object, the corresponding overridden function in the Dog or Cat class is executed.
  • This demonstrates runtime polymorphism: the decision about which function to execute is made at runtime based on the actual type of the object that the Animal pointer is referring to.

Key Points

  • Virtual functions enable runtime polymorphism, allowing different behaviors for objects of derived classes when accessed through a base class reference or pointer.
  • The virtual function mechanism is central to achieving dynamic method binding in C++, which is the ability to decide which function to invoke at runtime rather than at compile time.

Pure Virtual Functions

Definition: A pure virtual function is a function that is declared in a base class but must be implemented in a derived class. It is declared by assigning 0 to the function declaration in the base class. Pure virtual functions are used to create abstract classes in C++, which are classes that cannot be instantiated directly and are meant to be inherited.

Purpose

  • Creating Abstract Base Classes: Pure virtual functions are used to define an interface in the base class. Any class that inherits from this base class must provide an implementation for these functions, ensuring a consistent interface across derived classes.
  • Ensuring Implementation in Derived Classes: They are essential for scenarios where the base class cannot provide a meaningful implementation for a function, and the implementation must be provided by the derived classes.

Example:

C++
#include <iostream>
using namespace std;

class Shape {
public:
    // Pure virtual function
    virtual void draw() = 0;
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing Circle" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "Drawing Rectangle" << endl;
    }
};

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();

    shape1->draw();  // Outputs: Drawing Circle
    shape2->draw();  // Outputs: Drawing Rectangle

    delete shape1;   // Clean up
    delete shape2;   // Clean up
    return 0;
}

In this example:

  • Shape is an abstract base class with a pure virtual function draw().
  • Circle and Rectangle are derived classes that provide specific implementations of the draw() function.

Differences between Virtual Functions and Pure Virtual Functions

  • Default Implementation:
    • Virtual Function: Can have a default implementation in the base class. It’s optional for the derived class to override it.
    • Pure Virtual Function: Does not have a default implementation in the base class. It must be overridden by the derived class.
  • Abstract Class Creation:
    • Virtual Function: The presence of a virtual function does not make a class abstract. The class can still be instantiated.
    • Pure Virtual Function: If a class has at least one pure virtual function, it becomes an abstract class and cannot be instantiated.
  • Use Case:
    • Virtual Function: Used when the base class has a default behavior that can be shared or overridden by derived classes.
    • Pure Virtual Function: Used when the base class defines only an interface, and the actual implementation is mandatory in each derived class.

In summary, while virtual functions allow derived classes to override a method, pure virtual functions require it, making them a key tool in defining abstract interfaces in C++.

Abstract Classes

Definition

An abstract class in C++ is a class that is designed to be specifically used as a base class. An abstract class contains at least one pure virtual function. It represents a high-level concept that cannot be instantiated on its own.

Characteristics

  • Cannot Be Instantiated: You cannot create objects of an abstract class.
  • Intended for Inheritance: Abstract classes are intended to be inherited by concrete classes that implement the pure virtual functions.
  • Defines Interface: They are often used to define an interface for other classes to follow.

Purpose

  • Enforcing a Contract for Derived Classes: By declaring at least one pure virtual function, an abstract class ensures that any derived class must implement these functions, providing a consistent interface.
  • Code Reusability: Abstract classes can also contain normal (non-pure) virtual functions with implementation, promoting code reuse.

Example:

Scenario:

We want to model a system where different types of vehicles can be represented. All vehicles should have a method to start, but the specific way they start depends on the type of vehicle.

C++
#include <iostream>
using namespace std;

// Abstract class
class Vehicle {
public:
    // Pure virtual function
    virtual void startEngine() = 0;

    // A normal member function with implementation
    void honk() {
        cout << "Honk! Honk!" << endl;
    }
};

class Car : public Vehicle {
public:
    void startEngine() override {
        cout << "Car engine started" << endl;
    }
};

class Boat : public Vehicle {
public:
    void startEngine() override {
        cout << "Boat engine started" << endl;
    }
};

class Bicycle : public Vehicle {
public:
    void startEngine() override {
        cout << "Bicycle has no engine!" << endl;
    }
};

int main() {
    // Vehicle v; // Error: Cannot instantiate an abstract class
    Vehicle* car = new Car();
    Vehicle* boat = new Boat();
    Vehicle* bicycle = new Bicycle();

    car->startEngine();     // Outputs: Car engine started
    boat->startEngine();    // Outputs: Boat engine started
    bicycle->startEngine(); // Outputs: Bicycle has no engine!
    
    car->honk();            // Outputs: Honk! Honk!
    boat->honk();           // Outputs: Honk! Honk!
    bicycle->honk();        // Outputs: Honk! Honk!

    delete car;             // Clean up
    delete boat;            // Clean up
    delete bicycle;         // Clean up
    return 0;
}

In this example:

  • Vehicle is an abstract class with a pure virtual function startEngine().
  • CarBoat, and Bicycle are derived classes that provide specific implementations of startEngine().
  • The honk() method is a regular member function of Vehicle, which can be used by all derived classes.
  • We cannot instantiate Vehicle directly, as demonstrated by the commented line, but we can instantiate CarBoat, and Bicycle and call both startEngine() and honk() on them.

Key Points

  • This example demonstrates how an abstract class can be used to define a common interface (startEngine()) for various derived classes.
  • It also shows how abstract classes can contain non-pure virtual functions (honk()) that provide default behavior.
  • The abstract class Vehicle serves as a blueprint for creating specific types of vehicles with their unique engine start behaviors, while also sharing common functionality like honk().

Core Principles

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of “objects”, which can contain data, in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods). A key feature of OOP is the concept of class and instance. Here’s a breakdown of its core principles:

Classes and Objects

  • Class: A blueprint for creating objects (a particular data structure), providing initial values for state (member variables or attributes), and implementations of behavior (member functions or methods).
  • Object: An instance of a class. Each object can have unique attributes and behaviors as defined by its class.

Encapsulation

POO – Encapsulamiento
  • This principle is about bundling the data (attributes) and the methods that operate on the data into a single unit or class. It also involves restricting direct access to some of an object’s components, which is a means of preventing accidental interference and misuse of the methods and data.
  • In C++, encapsulation helps us keep related data and functions together, which makes our code cleaner and easy to read.
  • Data hiding is a way of restricting the access of our data members by hiding the implementation details.

Access Modifiers

  • public members (including base classes) can be accessed by
    • The current class
    • Derived classes
    • Free functions
    • Unrelated classes
  • protected members (including base classes) can be accessed by
    • The current class
    • Derived classes
  • private members (including base classes)
    • The current class

Specifiers apply to all following members

C++
class MyClass {
    public:
        MyClass() = default;
        int number = 2;
    private:
        double doubleNumber = 3.2;
};

Friend

  • Functions, classes, and templates can be declared as friends
  • friends can access anything of the related class
  • Friendship is the closest relationship a class can have and bleaks all encapsulations
  • Friendship is probably best avoided because of this.

Run in Repl.it https://repl.it/join/zovqdpto-maikolguzman

C++
#include <iostream>
using namespace std;

class Distance {
    private:
        int meter;
        
        // friend function
        friend int addFive(Distance distance) {
          //accessing private members from the friend function
          distance.meter += 5;
          return distance.meter;
        }

    public:
        Distance() : meter(0) {}
        
};

int main() {
    cout << "Welcome to the UNA!" << std::endl;  // display message
    Distance distance;
    cout << "Distance: " << addFive(distance) << std::endl;
    return 0;
}

Example

Inheritance

POO – Herencia
  • Inheritance allows a new class to adopt the properties and methods of an existing class. The new class (derived or child class) inherits attributes and behaviors from the old class (base or parent class) and can introduce its own.
  • The derived class inherits the properties and characteristics from the base class and can have additional features of its own
  • Inheritance is one of the most important feature of Object Oriented Programming.
C++
class Animal {
    // eat() function
    // sleep() function
};

class Dog : public Animal {
    // bark() function
};

Multiple Inheritance

  • Be aware of ambiguous access
  • Use explicit namespaces to resolve ambiguities
  • You cannot inherit from the same class twice
  • You can override the same virtual member from multiple base classes.

Run in Repl.it: https://repl.it/@MaikolGuzman/multi-inheritance-01-basic

C++
#include <iostream>  // allows program to output data to the screen
struct Base1 {
  void doSomething(int num) {
    std::cout << "INT - Base 1 ::doSomething(" << num << ")\n" << std::endl;
  }
};
struct Base2 {
  void doSomething(int num) {
    std::cout << "INT - Base 2 ::doSomething(" << num << ")\n" << std::endl;
  }
};
struct Derived : Base1, Base2 {
};
// function main begins program execution
int main(int argc, const char *argv[]) {
  std::cout << "Welcome to the UNA!" << std::endl;
  Derived derived;
  derived.Base1::doSomething(1);
  derived.Base2::doSomething(2);
  
}  // end function main

Polymorphism

POO – Polimorfismo
  • Polymorphism permits objects to be treated as instances of their parent class rather than their actual class. This allows for flexibility – methods or functions can be written that don’t need to know the exact type of their arguments, making it possible to use objects of different classes interchangeably as long as they follow a certain interface.
  • Polymorphism in C++ means the same entity (function or object) behaves differently in different scenarios.
  • Polymorphism means having many forms. A person can have different characteristics. As a Mom, at the same time am a mother, wife, and employee.
  • Types:
    • Compile Time
    • Runtime

Compile-time

  • In compile-time polymorphism, a function is called at the time of program compilation. We call this type of polymorphism as early binding or Static binding.
  • Function overloading and operator overloading is the type of Compile time polymorphism.
  • An example in C++ this can be represented by templates
  • Templates allow you to specify at compile time what types you want to work on
  • The standard library is full of example:
    • std::vector<T>;
    • std::list<T>;
    • std::map<T, U>;

Polymorphism is a core concept in object-oriented programming that allows objects of different classes to be treated as objects of a common superclass. It’s typically divided into two types in C++: overloading and overriding.

Overloading (Compile-time Polymorphism)

  • Definition: Overloading occurs when two or more functions in the same scope have the same name but different parameters. This can apply to both function overloading and operator overloading.
  • Characteristics: It’s resolved at compile time and it’s a way of implementing polymorphism that gives multiple meanings or functions to a single function or an operator.
  • Function overloading means one function can perform many tasks.
  • In C++, a single function is used to perform many tasks with the same name and different types of arguments.
  • In the function overloading function will call at the time of program compilation. It is an example of compile-time polymorphism.

Example of Overloading:

C++
class Print {
public:
    void show(int i) {
        cout << "Integer: " << i << endl;
    }
    void show(string s) {
        cout << "String: " << s << endl;
    }
};

int main() {
    Print p;
    p.show(5);         // Calls show(int)
    p.show("Hello");   // Calls show(string)
    return 0;
}

Operator Overloading

  • Operator overloading means defining additional tasks to operators without changing its actual meaning. We do this by using operator function.
  • The purpose of operator overloading is to provide a special meaning to the user-defined data types.
  • The advantage of Operators overloading is to perform different operations on the same operand.

Runtime

  • In a Runtime polymorphism, functions are called at the time the program execution. Hence, it is known as late binding or dynamic binding
  • Function overriding is a part of runtime polymorphism. In function overriding, more than one method has the same name with different types of the parameter list.
  • The standard library uses this in the exception library

Overriding (Run-time Polymorphism)

  • Definition: Overriding involves redefining a method of a base class in a derived class. This is used in situations where a subclass needs to modify or extend the behavior of that method.
  • Characteristics: It’s a feature that allows a subclass or child class to provide a specific implementation of a method that is already provided by one of its super-classes or parent classes.
  • New definition to base class function in the derived class.
  • It can be only possible in the derived class. In function overriding, we have two definitions of the same function, one in the superclass and one in the derived class.
  • Generally, this means virtual function and inheritance in C++
  • The decision about which function definition requires calling happens at runtime.

Example of Overriding:

C++
class Animal {
public:
    virtual void speak() {
        cout << "Some sound" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "Bark" << endl;
    }
};

int main() {
    Animal* animal = new Dog();
    animal->speak();  // Calls Dog's speak method
    delete animal;
    return 0;
}

In this example, the speak method in the Dog class overrides the speak method in the Animal class. The virtual keyword in the base class (Animal) and override keyword in the derived class (Dog) are used to make the overriding explicit and more readable.

Abstraction

  • Abstraction involves hiding the complex reality while exposing only the necessary parts. It is a way of creating a simple model of a more complex entity, by only showing its essential features and hiding the details.

Object-oriented programming aims to implement real-world entities like inheritance, hiding, polymorphism, etc. in programming. It is a paradigm widely used for structuring complex software programs into manageable and reusable code.

Memory Management

Definition: Memory management in programming, specifically in C++, involves the allocation, management, and release of memory in the computer’s RAM to optimize the efficiency and speed of applications.

Importance: Discuss why memory management is crucial in C++ for resource management, preventing memory leaks, and ensuring efficient use of memory.

Types of Memory

Stack Memory

Stack memory is a region of memory where local variables are stored. It works on the principle of “last in, first out” (LIFO). This means the last variable pushed onto the stack will be the first one to be popped off when it’s no longer needed.

Characteristics of Stack Memory:

  • Automatic Memory Management: Memory allocation and deallocation for variables on the stack are handled automatically. When a function is called, its local variables are pushed onto the stack, and when the function returns, these variables are popped off the stack.
  • Limited Size: The stack has a limited size, which means it’s not suitable for large data structures or for allocating memory that needs to exist beyond the lifetime of a function call.
  • Fast Access: Accessing stack memory is generally faster than heap memory because of its contiguous nature and the simplicity of the allocation and deallocation mechanism.
  • Scope and Lifetime: Variables on the stack exist within the scope of the block in which they are defined. Once the block (like a function) is exited, these variables are automatically destroyed.

Example:

C++
#include <iostream>

void displayNumber() {
    int num = 50;  // Local variable 'num' is allocated on the stack.
    std::cout << "Number: " << num << std::endl;
    // 'num' is automatically deallocated when this function ends.
}

int main() {
    displayNumber();  // Calls 'displayNumber', pushing 'num' onto the stack.
    // 'num' is popped off the stack and destroyed here as 'displayNumber' returns.
    return 0;
}

In this example, when displayNumber() is called from main(), the local variable num is pushed onto the stack. As soon as displayNumber() finishes execution, num is popped off the stack, and its memory is automatically reclaimed.

Heap Memory

Heap memory, also known as dynamic memory, is a region of memory used for dynamic memory allocation. Unlike stack memory, which is automatically managed and limited in size, heap memory provides greater flexibility but requires explicit management by the programmer.

Characteristics of Heap Memory:

  • Manual Memory Management: Programmers are responsible for allocating and freeing memory in the heap. This is done using new and delete (or new[] and delete[] for arrays) in C++.
  • Larger Size: The heap is typically much larger than the stack, making it suitable for large data structures or for data that must persist beyond the scope of a function call.
  • Slower Access: Accessing heap memory can be slower compared to stack memory due to the complexities of memory allocation and deallocation mechanisms.
  • Lifetime Independent of Scope: Memory on the heap is not bound to the scope in which it is allocated. It remains allocated until it is explicitly freed, even after the function in which it was allocated returns.

Example:

C++
#include <iostream>

int* allocateArray(int size) {
    int* array = new int[size];  // Dynamically allocate an array on the heap.
    for (int i = 0; i < size; ++i) {
        array[i] = i * i;  // Initialize array elements.
    }
    return array;  // Return the pointer to the heap-allocated array.
}

int main() {
    int size = 10;
    int* myArray = allocateArray(size);  // Allocate array on the heap.

    for (int i = 0; i < size; ++i) {
        std::cout << myArray[i] << " ";
    }
    std::cout << std::endl;

    delete[] myArray;  // It's crucial to free the heap memory.
    return 0;
}

In this example, allocateArray function dynamically allocates an array of integers on the heap and returns a pointer to it. The main function then prints the array’s contents. Finally, delete[] is used to free the allocated memory, preventing a memory leak.

Memory Allocation

  • new Operator:
    • Syntax: Type* variable = new Type;
    • Use: Dynamically allocates memory for a single object of Type.
  • new[] Operator:
    • Syntax: Type* array = new Type[n];
    • Use: Allocates memory for an array of n objects of Type.

Example:

C++
int* ptr = new int;      // Allocating memory for an integer.
int* arr = new int[10];  // Allocating memory for an array of 10 integers.

Freeing Memory

  • delete Operator:
    • Syntax: delete pointer;
    • Use: Deallocates memory allocated for a single object.
  • delete[] Operator:
    • Syntax: delete[] array;
    • Use: Deallocates memory allocated for an array of objects.

Example:

C++
delete ptr;       // Freeing memory allocated for a single integer.
delete[] arr;     // Freeing memory allocated for an array of integers.

Best Practices in Memory Management

  • Avoid memory leaks by ensuring every new is matched with a delete.
  • Handle exceptions to prevent memory leaks.
  • Prefer smart pointers (e.g., std::unique_ptrstd::shared_ptr) for automated memory management.

Pointers

The basic concept of pointers in C++ involves delving into their fundamental characteristics and uses.

Basic Concept of Pointers

What are Pointers?

  • Definition: Pointers are variables that store the memory addresses of other variables. Unlike normal variables that hold a specific value (like an integer or a character), pointers hold the address where a value is stored in memory.
  • Why Pointers? Pointers provide a way to access and manipulate data in memory. They are powerful tools in C++ because they allow for the manipulation of memory and the creation of complex data structures like linked lists, trees, and graphs.

Anatomy of a Pointer

  • Type: A pointer has a type, which indicates the type of data it points to. For example, int* is a pointer to an integer, char* is a pointer to a character.
  • Address Operator (&): The address operator & is used to find the address of a variable. For example, &x gives the memory address of the variable x.
  • Dereference Operator (*): The dereference operator * is used to access the value at the address the pointer is pointing to. If ptr is a pointer, *ptr gives the value stored in the memory location pointed to by ptr.

Pointer Initialization

  • Declaration: A pointer is declared by specifying the type it points to followed by an asterisk. For example, int *ptr; declares a pointer to an integer.
  • Initialization: It’s good practice to initialize pointers to nullptr (a null pointer introduced in C++11) when they are declared. This prevents them from pointing to a random memory location, which can cause undefined behavior.
C++
  int *ptr = nullptr;

Example: Using a Pointer

C++
int main() {
    int var = 5;      // Declare an integer variable
    int *ptr;         // Declare a pointer to an integer
    ptr = &var;       // Assign the address of var to ptr

    cout << "Value of var: " << var << endl;             // Output the value of var
    cout << "Address of var: " << &var << endl;          // Output the address of var
    cout << "Value of ptr (address): " << ptr << endl;   // Output the value of ptr (which is the address of var)
    cout << "Value at ptr: " << *ptr << endl;            // Output the value at the address stored in ptr (dereferencing)

    return 0;
}

In this example, ptr is a pointer that stores the address of the integer variable var. The address of var is obtained using &var, and the value of var is accessed using *ptr.

Key Points to Emphasize

  • Pointers are a core feature of C++ that provide a powerful but complex capability.
  • They are essential for dynamic memory management, creating complex data structures, and improving performance for certain operations.
  • Understanding and using pointers correctly is crucial for writing efficient and robust C++ programs.

Expanding on the concept of pointer arithmetic involves understanding how pointers can be manipulated using arithmetic operations. This is a unique aspect of pointers, different from normal variable arithmetic, due to the way memory is organized and accessed.

Pointer Arithmetic

Basic Operations

  1. Increment (++): When you increment a pointer, it advances to point to the next memory location of the type it points to. For instance, if ptr is an int* (pointer to an int), incrementing ptr (ptr++) will advance it to the next integer in memory, typically 4 bytes ahead on most systems.
  2. Decrement (--): Similarly, decrementing a pointer moves it back to the previous memory location of its type.
  3. Addition/Subtraction with an Integer: You can add or subtract an integer value to/from a pointer. If ptr is an int* and ptr + 5 is computed, the pointer moves ahead by 5 integer memory locations.
  4. Subtracting Two Pointers: Subtracting one pointer from another gives the number of elements between them, assuming they point to elements of the same array.

Important Considerations

  • Type-Specific Scaling: Pointer arithmetic is scaled by the size of the type it points to. For example, if ptr is a char* (each char is typically 1 byte), ptr++ moves the pointer by 1 byte. If ptr is an int* (each int is typically 4 bytes), ptr++ moves it by 4 bytes.
  • Bounds and Validity: Pointer arithmetic should stay within the bounds of the array or memory block it points to. Going beyond can lead to undefined behavior.
  • Dereferencing Arithmetic Results: The result of pointer arithmetic can be dereferenced to access or modify the value at the new memory location.

Example: Pointer Arithmetic in Action

C++
int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr;    // Pointer to the first element of arr

    cout << *ptr << endl;      // Output: 10
    ptr++;                      // Point to the next integer
    cout << *ptr << endl;      // Output: 20

    ptr += 3;                   // Move the pointer 3 integers ahead
    cout << *ptr << endl;      // Output: 50

    ptr -= 2;                   // Move back 2 integers
    cout << *ptr << endl;      // Output: 30

    int distance = ptr - arr;   // Calculate the distance from the beginning of the array
    cout << "Distance from start: " << distance << endl;  // Output: 2

    return 0;
}

In this example, the pointer ptr is manipulated to point to different elements in the array arr. We use increment, addition, and subtraction to navigate through the array. Notice how the arithmetic operations take into account the size of the data type (int in this case).

Key Points to Emphasize

  • Use diagrams to illustrate how pointers move through memory.
  • Reinforce that pointer arithmetic depends on the data type of the pointer.
  • Provide exercises where students have to navigate arrays or structures using pointer arithmetic.
  • Discuss common errors, such as going past the end of an array or incorrectly calculating the distance between pointers.

Pointers and Arrays

Array Name as a Pointer

  • Array as Pointer: In C++, the name of an array acts like a pointer to the first element of the array. For example, if you have an array int arr[5], arr can be used as a pointer to the first element arr[0].

Accessing Elements

  • Using Indexing: You can access elements of an array using the subscript operator [], just like with a normal array. If ptr is a pointer to an array, ptr[2] accesses the third element of the array.
  • Using Pointer Arithmetic: Alternatively, you can use pointer arithmetic to navigate through the array. If ptr points to the first element of an array, *(ptr + 2) accesses the third element.

Differences Between Pointers and Arrays

  • Memory Allocation: Arrays are fixed in size, and the memory is allocated at compile-time (static memory allocation). Pointers can be used for dynamic memory allocation (using new and delete), allowing for arrays whose size is determined at runtime.
  • Assignment: An array name cannot be reassigned to point to a different memory location, while a pointer can be reassigned.
  • Size Information: The size of an array can be determined using sizeof(arr), which is not possible with a pointer. For pointers, the size of the memory block they point to is not inherently known.

Example: Pointers with Arrays

C++
int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // ptr points to the first element of arr

    cout << "First element: " << *ptr << endl;  // Output: 10
    cout << "Second element using array indexing: " << arr[1] << endl;  // Output: 20
    cout << "Third element using pointer: " << *(ptr + 2) << endl;  // Output: 30

    // Iterating over the array using a pointer
    for (int i = 0; i < 5; ++i) {
        cout << "Element " << i + 1 << ": " << *(ptr + i) << endl;
    }

    return 0;
}

In this example, ptr is a pointer to the first element of the array arr. The elements of the array are accessed using both array indexing and pointer arithmetic.

Pointers to Pointers

Basic Concept

  • Definition: A pointer to a pointer is a type of pointer that holds the address of another pointer. In other words, it’s a double pointer.
  • Declaration: It’s declared using two asterisks (**). For example, int **ptr declares a pointer to a pointer to an integer.

Why Use Pointers to Pointers?

  • Dynamic Arrays: They are often used to create dynamic multidimensional arrays.
  • Modifying Pointers in Functions: They allow you to modify a pointer passed to a function. Without them, changes to a pointer in a function would only affect the local copy.
  • Complex Data Structures: Useful in advanced data structures like a graph represented using an adjacency list, where each node may point to a list of other nodes.

Memory Layout

  • The first pointer points to a second pointer, which in turn points to the actual data.
  • This indirection means accessing the data requires two dereference operations.

Example: Using Pointers to Pointers

C++
int main() {
    int var = 10;
    int *ptr = &var;
    int **ptrToPtr = &ptr;

    cout << "Value of var: " << var << endl;
    cout << "Value accessed through ptr: " << *ptr << endl;
    cout << "Value accessed through ptrToPtr: " << **ptrToPtr << endl;

    return 0;
}

In this example, ptrToPtr is a pointer to the pointer ptr, which in turn points to the integer variable var. The value of var is accessed using **ptrToPtr.

Dynamic Memory Allocation

Basic Concepts

  • Heap Memory: Dynamic memory allocation involves allocating memory at runtime from the heap, a large pool of memory used for dynamic allocation.
  • new and delete Operators: C++ uses new to allocate memory and delete to free it. These operators not only manage memory but also call constructors and destructors for objects.

Using new and delete

  • Single Variable:
  • Allocation: int *ptr = new int;
  • Deallocation: delete ptr;
  • Arrays:
  • Allocation: int *array = new int[10]; (allocates memory for an array of 10 ints)
  • Deallocation: delete[] array; (the brackets [] indicate it’s an array)

Why Dynamic Memory?

  • Flexibility: Allows for the creation of variables and arrays whose size is determined at runtime.
  • Lifetime Control: Memory allocated dynamically remains allocated until explicitly freed, giving control over the lifetime of the memory.

Memory Management Best Practices

  • Avoid Memory Leaks: Always delete what you new. Failure to release memory results in memory leaks.
  • Handle Allocation Errors: Ensure your program gracefully handles situations where new cannot allocate memory (usually by throwing a std::bad_alloc exception).
  • Initialize Allocated Memory: Uninitialized memory might contain garbage values.
  • Avoid Dangling Pointers: Set pointers to nullptr after deleting to avoid dangling pointers.

Example: Dynamic Memory in Action

C++
int main() {
    int *ptr = new int(5);   // Dynamically allocate an integer and initialize it to 5
    int *array = new int[10]; // Allocate an array for 10 integers

    // Use the allocated memory
    *ptr = 10;
    for (int i = 0; i < 10; ++i) {
        array[i] = i * i;
    }

    // Clean up
    delete ptr;
    delete[] array;

    return 0;
}

In this example, dynamic memory is allocated for a single integer and an array of integers. The memory is then initialized, used, and finally released using delete and delete[].

Pointers and Functions

Passing Pointers to Functions

  • Modifying Arguments: Passing pointers to functions allows the function to modify the original arguments, not just a copy. This is particularly useful for modifying large structures or arrays without the overhead of copying.
  • Syntax: To pass a pointer to a function, you declare the function parameters as pointer types. For example, void func(int *ptr) means func takes an int pointer as its parameter.

Returning Pointers from Functions

  • Use Cases: Functions can return pointers to provide access to dynamically allocated memory, to return arrays from functions, or to give access to some internal static variable.
  • Caution: Be cautious when returning pointers from functions, especially with pointers to local variables (which can lead to undefined behavior) or dynamically allocated memory (which can lead to memory leaks if not handled correctly).

Pointers to Arrays and Strings

  • Array Parameters: When passing an array to a function, you are essentially passing a pointer to the first element of the array.
  • String Handling: Strings (C-style strings) are arrays of char, so functions dealing with strings often use char* to manipulate them.

Example: Using Pointers in Functions

C++
void modifyValue(int *p) {
    *p = 10;  // Modifies the value pointed to by p
}

void processArray(int *arr, int size) {
    for (int i = 0; i < size; ++i) {
        // Process each element
        arr[i] *= 2;
    }
}

int main() {
    int x = 5;
    modifyValue(&x);
    cout << "x after modification: " << x << endl; // Output: x after modification: 10

    int myArray[5] = {1, 2, 3, 4, 5};
    processArray(myArray, 5);
    // Print modified array
    for (int i: myArray) {
        cout << i << " "; // Output: 2 4 6 8 10
    }

    return 0;
}

Common Pitfalls in Pointer Usage

Uninitialized Pointers

  • Problem: Using a pointer that hasn’t been initialized. An uninitialized pointer points to some arbitrary memory location, leading to unpredictable behavior.
  • Solution: Always initialize pointers, preferably to nullptr if they’re not immediately assigned a valid address.

Dangling Pointers

  • Problem: A dangling pointer occurs when a pointer still references a memory location that has been freed or gone out of scope. Accessing such a pointer can lead to undefined behavior.
  • Solution: Set pointers to nullptr after freeing the memory or when the memory they point to is no longer valid.

Memory Leaks

  • Problem: Forgetting to free dynamically allocated memory with delete or delete[], leading to memory leaks.
  • Solution: Ensure every new is paired with a delete. Consider using smart pointers (like std::unique_ptr or std::shared_ptr) which automatically manage memory.

Invalid Memory Access

  • Problem: Accessing memory outside the bounds of an allocated region, such as reading past the end of an array.
  • Solution: Be diligent with array bounds and pointer arithmetic. Always ensure that pointers stay within valid memory limits.

Double Free or Multiple Deletes

  • Problem: Attempting to free the same memory region more than once can cause program crashes or other erratic behavior.
  • Solution: After freeing memory, set the pointer to nullptr to prevent accidental double frees.

Mismatched new[] and delete

  • Problem: Using delete instead of delete[] for memory allocated with new[] (or vice versa).
  • Solution: Match new with delete and new[] with delete[].

Incorrect Use of & and * Operators

  • Problem: Misusing the address-of (&) and dereference (*) operators, leading to incorrect addresses being accessed or modified.
  • Solution: Carefully distinguish between when you need the value pointed to by a pointer (*ptr) and when you need the address of a variable (&var).

Over reliance on Raw Pointers

  • Problem: Overusing raw pointers, especially in modern C++ where smart pointers and standard library containers are often more appropriate.
  • Solution: Leverage modern C++ features like smart pointers (std::unique_ptr, std::shared_ptr) and containers (std::vector, std::array) which manage memory automatically and are safer to use.

Full C++ Program Example

C++
#include <iostream>
using namespace std;

// Function to modify the value of an integer using a pointer
void modifyValue(int *p) {
    *p = 10; // Modifies the value pointed to by p
}

// Function to double the elements of an array using pointer arithmetic
void doubleArray(int *arr, int size) {
    for (int i = 0; i < size; ++i) {
        *(arr + i) *= 2; // Pointer arithmetic to access array elements
    }
}

// Function that returns a dynamically allocated array
int* createDynamicArray(int size) {
    int *dynamicArray = new int[size]; // Dynamic memory allocation
    for (int i = 0; i < size; ++i) {
        dynamicArray[i] = i * i; // Initialize array elements
    }
    return dynamicArray; // Return the pointer to the dynamically allocated array
}

int main() {
    // Demonstrate modifying a value via pointer
    int x = 5;
    modifyValue(&x);
    cout << "x after modification: " << x << endl;

    // Working with arrays and pointer arithmetic
    int myArray[5] = {1, 2, 3, 4, 5};
    doubleArray(myArray, 5);
    cout << "Array after doubling: ";
    for (int i: myArray) {
        cout << i << " ";
    }
    cout << endl;

    // Demonstrate dynamic memory allocation
    int arraySize = 5;
    int *dynamicArray = createDynamicArray(arraySize);
    cout << "Dynamically allocated array: ";
    for (int i = 0; i < arraySize; ++i) {
        cout << dynamicArray[i] << " ";
    }
    cout << endl;

    // Clean up dynamic memory
    delete[] dynamicArray;

    return 0;
}

Explanation of the Example

  1. Modifying Value via Pointer: The function modifyValue demonstrates how a pointer can be used to modify the value of a variable passed to it.
  2. Pointer Arithmetic with Arrays: doubleArray shows how to use pointer arithmetic to iterate through and modify the elements of an array.
  3. Dynamic Memory Allocation: createDynamicArray illustrates dynamic memory allocation by returning a pointer to a newly allocated array.
  4. Safety and Best Practices: The program includes examples of initializing pointers, careful use of dynamic memory (including correct use of delete[]), and using pointers for array manipulation.

This example serves as a practical demonstration of several key concepts related to pointers in C++, illustrating how they can be used effectively while also highlighting important best practices and common pitfalls.

Collections

C++ provides a variety of collections, commonly known as containers, each designed for specific purposes. Here’s a brief overview of some of the main types of collections in C++ along with examples:

Vector (std::vector)

  • A dynamic array that can grow and shrink in size.
  • Example: std::vector<int> numbers = {1, 2, 3, 4, 5};

List (std::list)

  • A doubly linked list that allows efficient insertion and deletion from both ends.
  • Example: std::list<int> numbers = {1, 2, 3, 4, 5};

Deque (std::deque)

  • A double-ended queue that supports fast insertion and deletion at both the beginning and the end.
  • Example: std::deque<int> numbers = {1, 2, 3, 4, 5};

Stack (std::stack)

  • Adapts other containers to provide LIFO (Last In, First Out) stack behavior.
  • Example: std::stack<int> numbers; numbers.push(1); numbers.push(2);

Queue (std::queue)

  • Adapts other containers to provide FIFO (First In, First Out) queue behavior.
  • Example: std::queue<int> numbers; numbers.push(1); numbers.push(2);

Priority Queue (std::priority_queue)

  • A max heap that allows access to the largest element, with the option to pop it off.
  • Example: std::priority_queue<int> numbers; numbers.push(1); numbers.push(2);

Set (std::set)

  • A collection of unique elements, sorted by keys.
  • Example: std::set<int> numbers = {1, 2, 3, 4, 5};

Map (std::map)

  • A sorted associative array that maps unique keys to values.
  • Example: std::map<int, std::string> keyValue = {{1, "one"}, {2, "two"}};

Unordered Set (std::unordered_set)

  • A hash set for storing unique elements in no particular order.
  • Example: std::unordered_set<int> numbers = {1, 2, 3, 4, 5};

Unordered Map (std::unordered_map)

  • A hash map that maps unique keys to values.
  • Example: std::unordered_map<int, std::string> keyValue = {{1, "one"}, {2, "two"}};

Each of these collections has its own set of characteristics and use-cases. The choice of which one to use depends on factors like the need for sorting, the frequency of insertions and deletions, the importance of random access, etc.

Dynamic Collections

Dynamic collections are containers that can dynamically resize themselves to accommodate new elements or to shrink when elements are removed. They allocate memory during runtime and can change their size accordingly.

Vector (std::vector):

  • Behaves like a dynamic array. It can grow and shrink in size as elements are added or removed.Example: Adding elements to a vector can cause it to resize automatically.Example:

C++
std::vector<int> numbers;
numbers.push_back(1); // The vector grows dynamically

List (std::list):

  • A doubly linked list that allows insertion and deletion of elements at any point efficiently.
  • It doesn’t need contiguous memory and can grow as needed.

Deque (std::deque):

  • Similar to a vector but optimized for fast insertions and deletions at both ends.
  • It can grow in both directions.

Unordered Set/Map (std::unordered_set, std::unordered_map):

  • These use hash tables to store elements. They can grow dynamically as elements are added.

Static Collections

Static collections, on the other hand, are those that have a fixed size that must be known at compile time. These are not as flexible as dynamic containers, as they cannot grow or shrink during runtime.

Array (std::array):

  • A wrapper around native C-style arrays. It requires the size to be known at compile time and cannot change its size once created.Example:

C++
std::array<int, 5> numbers = {1, 2, 3, 4, 5}; // Size is fixed to 5

Native C-style Arrays:

  • The traditional fixed-size array in C++. They are not considered safe and flexible compared to C++ container classes but are static in size.

C++
int numbers[5]; // Size is fixed to 5

Unidimensional Arrays

A unidimensional array is a linear sequence of elements. They are the simplest form of arrays in C++.

Declaration and Initialization:

Declaration:

  • Syntax: dataType arrayName[arraySize];
  • Example: int numbers[5]; // An array of 5 integers

Initialization:

  • At Declaration: int numbers[5] = {1, 2, 3, 4, 5};
  • After Declaration:
C++
int numbers[5];
for(int i = 0; i < 5; i++) {
    numbers[i] = i + 1;
}

Accessing Elements:

  • Elements are accessed using the index, starting from 0.
  • Example: int firstNumber = numbers[0]; // Accesses the first element

Bidimensional Arrays

Bidimensional arrays are arrays of arrays, often used to represent matrices, grids, or tables.

Declaration and Initialization:

Declaration:

  • Syntax: dataType arrayName[rows][columns];
  • Example: int matrix[3][3]; // A 3×3 matrix

Initialization:

  • At Declaration: int matrix[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
  • After Declaration:
C++
int matrix[3][3];
for(int i = 0; i < 3; i++) {
    for(int j = 0; j < 3; j++) {
        matrix[i][j] = i * 3 + j + 1;
    }
}

Accessing Elements:

  • Elements are accessed using two indices: one for the row and one for the column.
  • Example: int element = matrix[0][1]; // Accesses the element in the first row, second column

Notes on Usage

  • Memory Layout: In C++, arrays are stored in row-major order. This means in a bidimensional array, the elements of each row are stored in contiguous memory locations.
  • Static Size: The size of both unidimensional and bidimensional arrays must be known at compile time (except for Variable-Length Arrays in C99, not standard in C++).
  • Dynamic Arrays: For arrays where the size is determined at runtime, dynamic memory allocation (new and delete operators) or container classes like std::vector are used.
  • Zero-Based Indexing: Array indexing starts at 0, so the last index of an array with n elements is n-1.

Arrays, both unidimensional and bidimensional, are foundational in C++ programming, especially when handling data sets and matrices. They provide a straightforward way to store and manipulate a fixed-size collection of elements.

Key Differences between static vs dynamic

  • Memory Allocation: Dynamic collections allocate memory at runtime and can expand or contract as needed. Static collections have a fixed size determined at compile time.
  • Flexibility: Dynamic collections offer more flexibility in handling data as they can adjust their size based on the program’s requirements. Static collections are limited by their predetermined size.
  • Use Cases: Dynamic collections are ideal when the number of elements is not known in advance or can change. Static collections are suitable when the number of elements is fixed and known beforehand.

In summary, the choice between dynamic and static collections in C++ depends on the specific requirements of your application, particularly concerning the size of the collection and how it might change during the program’s execution.

Linked List

In C++, a simple linked list can be implemented using pointers. A linked list is a data structure consisting of nodes, where each node contains a data element and a pointer to the next node in the list. Here’s a basic implementation of a singly linked list with some fundamental operations like insertion, deletion, and traversal.

Basic Node Structure

First, define a structure for a node in the list:

C++
struct Node {
    int data;   // Data field
    Node* next; // Pointer to the next node

    // Constructor to initialize the node
    Node(int value) : data(value), next(nullptr) {}
};

Basic Operations

Inserting at the Head:

  • Adds a new node at the beginning of the list.
C++
void insertHead(Node*& head, int value) {
    Node* newNode = new Node(value); // Create a new node
    newNode->next = head;            // Point it to the old head
    head = newNode;                  // Update the head to the new node
}

Deleting from the Head:

  • Removes the node at the beginning of the list.
C++
void deleteHead(Node*& head) {
    if (head != nullptr) {
        Node* temp = head; // Temporary pointer to the head
        head = head->next; // Update head to the next node
        delete temp;       // Delete the old head
    }
}

Traversing the List:

  • Print out the elements in the list.
C++
void printList(Node* head) {
    Node* current = head;
    while (current != nullptr) {
        std::cout << current->data << " ";
        current = current->next;
    }
    std::cout << std::endl;
}

Example Usage

C++
void insertHead(Node*& head, int value) {
    Node* newNode = new Node(value); // 1. Create a new node
    newNode->next = head;            // 2. Set the new node's next pointer to the current head
    head = newNode;                  // 3. Update the head to point to the new node
}

int main() {
    Node* head = nullptr; // Start with an empty list

    // Insert some elements
    insertHead(head, 3);
    insertHead(head, 2);
    insertHead(head, 1);

    // Print the list
    std::cout << "The list: ";
    printList(head);

    // Delete the head
    deleteHead(head);

    // Print the list again
    std::cout << "After deleting the head: ";
    printList(head);

    // Remember to delete the remaining nodes to avoid memory leaks...
}

Example of dynamic menu using linked list

Notes

  • The head pointer keeps track of the first node in the list.
  • When inserting or deleting at the head, you need to update the head pointer.
  • Always remember to free the memory allocated for nodes to avoid memory leaks.
  • This is a basic implementation. In a more robust implementation, you might want to add more functionality, like inserting at the tail, searching for elements, etc.

This linked list implementation using pointers in C++ is a good starting point to understand how dynamic data structures are managed and manipulated at a low level.