Blog

  • Virtual Function

    Virtual function in C++

    virtual function (also referred to as virtual methods) is a member function declared in a base class that can be overridden in a derived class. When a derived class object is referred to using a pointer or reference of the base class, the virtual function allows the derived class’s implementation to be executed.

    Virtual functions ensure the correct function is called based on the object type, regardless of the reference (or pointer) type used. They play a key role in enabling runtime polymorphism. Functions are declared using the virtual keyword in the base class, and their call resolution is performed at runtime.

    Key Rules for Virtual Functions
    • Virtual functions cannot be static.
    • They can be friends of another class.
    • To achieve runtime polymorphism, virtual functions should be accessed using a pointer or reference to a base class.
    • The function prototypes must be the same in the base and derived classes.
    • They are always defined in the base class and overridden in the derived class. If the derived class does not override the virtual function, the base class version will be used.
    • A class can have a virtual destructor but not a virtual constructor.
    Early Binding vs. Late Binding

    Virtual functions demonstrate late binding or runtime polymorphism, where the function call is resolved during runtime. In contrast, non-virtual functions exhibit early binding or compile-time binding, where the function call is resolved during compilation.

    Example: Runtime Behavior of Virtual Functions

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        virtual void display() { cout << "Base class display\n"; }
        void show() { cout << "Base class show\n"; }
    };
    
    class Derived : public Base {
    public:
        void display() { cout << "Derived class display\n"; }
        void show() { cout << "Derived class show\n"; }
    };
    
    int main() {
        Base* basePtr;
        Derived derivedObj;
        basePtr = &derivedObj;
    
        // Virtual function, resolved at runtime
        basePtr->display();
    
        // Non-virtual function, resolved at compile time
        basePtr->show();
    
        return 0;
    }

    Output:

    Derived class display
    Base class show

    Explanation: In this example, basePtr is a pointer of type Base, but it points to an object of type Derived. The virtual function display() is resolved at runtime, so the Derived class version is called. On the other hand, the non-virtual function show() is resolved at compile-time, and the Base class version is executed.

    Working of Virtual Functions (VTABLE and VPTR)

    When a class contains virtual functions, the compiler takes the following actions:

    1. For every class that contains a virtual function, the compiler creates a vtable (virtual table), which is a static array of function pointers. Each cell of the vtable stores the address of a virtual function in that class.
    2. For every object of a class containing virtual functions, a vptr (virtual pointer) is added as a hidden data member. This pointer refers to the vtable of the class to which the object belongs.

    This mechanism allows the correct virtual function to be called at runtime based on the actual object type.

    Example: Working of Virtual Functions

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        void func1() { cout << "Base - func1\n"; }
        virtual void func2() { cout << "Base - func2\n"; }
        virtual void func3() { cout << "Base - func3\n"; }
        virtual void func4() { cout << "Base - func4\n"; }
    };
    
    class Derived : public Base {
    public:
        void func1() { cout << "Derived - func1\n"; }
        void func2() { cout << "Derived - func2\n"; }
        void func4(int x) { cout << "Derived - func4 with parameter\n"; }
    };
    
    int main() {
        Base* basePtr;
        Derived derivedObj;
        basePtr = &derivedObj;
    
        // Early binding (compile-time)
        basePtr->func1();
    
        // Late binding (runtime)
        basePtr->func2();
    
        // Late binding (runtime)
        basePtr->func3();
    
        // Late binding (runtime)
        basePtr->func4();
    
        // Illegal: Early binding but function does not exist in base class
        // basePtr->func4(5);
    
        return 0;
    }

    Output:

    Base - func1
    Derived - func2
    Base - func3
    Base - func4
    Explanation:
    • func1() is a non-virtual function, so it is resolved at compile-time, and the Base class version is called.
    • func2() is a virtual function, so the Derived class version is called at runtime.
    • func3() and func4() are virtual functions, and since func3() is not overridden in the derived class, the Base class version is called. Similarly, func4() is also resolved to the Base class version.
    a < b  : 0
    a > b  : 1
    a <= b: 0
    a >= b: 1
    a == b: 0
    a != b : 1

    virtual function in a base class can be overridden by a derived class. When a derived class object is referenced or pointed to by a base class reference or pointer, calling a virtual function will invoke the version defined in the derived class.

    In C++, once a function is marked as virtual in a base class, it remains virtual throughout all derived classes. Therefore, you don’t need to explicitly declare it as virtual again in derived classes that override the function.

    For instance, in the example below, even though the keyword virtual is only used in the base class A, the function fun() in B and C is virtual. This is demonstrated by the fact that the program prints “C::fun() called”, as B::fun() inherits the virtual property.

    Example

    #include <iostream>
    using namespace std;
    
    class Animal {
    public:
        virtual void sound() { cout << "\n Animal sound"; }
    };
    
    class Dog : public Animal {
    public:
        void sound() { cout << "\n Dog barks"; }
    };
    
    class Puppy : public Dog {
    public:
        void sound() { cout << "\n Puppy squeaks"; }
    };
    
    int main() {
        // Create an object of class Puppy
        Puppy p;
    
        // Pointer of class Dog pointing to object of class Puppy
        Dog* dogPtr = &p;
    
        // This line prints "Puppy squeaks" due to the virtual function mechanism
        dogPtr->sound();
    
        return 0;
    }

    Output:

    Puppy squeaks

    Virtual Functions in Derived Classes

    virtual function is a member function in a base class that can be overridden in a derived class. When a pointer or reference to the base class is used to refer to an object of the derived class, the derived class’s version of the virtual function is executed.

    Once a function is marked as virtual in a base class in C++, it remains virtual throughout all derived classes. You don’t need to explicitly declare it as virtual in the derived classes when overriding it.

    For instance, in the following program, even though the keyword virtual is used only in class A, the function fun() in B and C behaves as virtual. As a result, the program outputs “C::display() called”, since the virtual function mechanism ensures that B::fun() is automatically virtual.

    Example

    #include <iostream>
    using namespace std;
    
    class Shape {
    public:
        virtual void draw() { cout << "\n Drawing Shape"; }
    };
    
    class Circle : public Shape {
    public:
        void draw() { cout << "\n Drawing Circle"; }
    };
    
    class FilledCircle : public Circle {
    public:
        void draw() { cout << "\n Drawing Filled Circle"; }
    };
    
    int main() {
        // Object of class FilledCircle
        FilledCircle filledCircle;
    
        // Pointer of class Circle pointing to object of FilledCircle
        Circle* circlePtr = &filledCircle;
    
        // This line prints "Drawing Filled Circle"
        circlePtr->draw();
    
        return 0;
    }

    Output:

    Drawing Filled Circle

    Default Arguments and Virtual Function

    Default arguments allow you to provide initial values in function declarations that will be used if no arguments are passed during a function call. If arguments are provided, the default values are overridden. Virtual functions, on the other hand, enable runtime polymorphism by allowing a derived class to override a function in the base class.

    Combining default arguments with virtual functions can lead to interesting behavior. Let’s explore how they work together.

    Example:

    #include <iostream>
    using namespace std;
    
    // Base class declaration
    class Animal {
    public:
        // Virtual function with a default argument
        virtual void sound(int volume = 5) {
            cout << "Animal sound at volume: " << volume << endl;
        }
    };
    
    // Derived class overriding the virtual function
    class Dog : public Animal {
    public:
        // Virtual function without a default argument
        virtual void sound(int volume) {
            cout << "Dog barks at volume: " << volume << endl;
        }
    };
    
    // Main function
    int main() {
        Dog dog;
    
        // Pointer to base class points to derived class object
        Animal* animalPtr = &dog;
    
        // Calls derived class function, default argument from base class
        animalPtr->sound();
    
        return 0;
    }

    Output:

    Dog barks at volume: 5

    Virtual Destructor

    Deleting a derived class object using a pointer of base class type that does not have a virtual destructor leads to undefined behavior. This is because when deleting an object through a base class pointer, only the base class destructor will be invoked, leaving the derived class destructor uncalled. To resolve this issue, the base class should be defined with a virtual destructor.

    Consider the following example, where the lack of a virtual destructor in the base class causes undefined behavior:

    // Example with non-virtual destructor, causing undefined behavior
    #include <iostream>
    
    class Base {
      public:
        Base() {
            std::cout << "Base class constructor\n";
        }
        ~Base() {
            std::cout << "Base class destructor\n";
        }
    };
    
    class Derived : public Base {
      public:
        Derived() {
            std::cout << "Derived class constructor\n";
        }
        ~Derived() {
            std::cout << "Derived class destructor\n";
        }
    };
    
    int main() {
        Derived* d = new Derived();
        Base* b = d;
        delete b; // Undefined behavior: Derived class destructor not called
        return 0;
    }

    Output:

    Base class constructor
    Derived class constructor
    Base class destructor

    As we can see, only the base class destructor is called, and the derived class destructor is skipped, leading to potential memory leaks or other issues.

    To ensure both base and derived destructors are properly invoked, we must make the base class destructor virtual, as shown in the following example:

    // Example with virtual destructor
    #include <iostream>
    
    class Base {
      public:
        Base() {
            std::cout << "Base class constructor\n";
        }
        virtual ~Base() {
            std::cout << "Base class destructor\n";
        }
    };
    
    class Derived : public Base {
      public:
        Derived() {
            std::cout << "Derived class constructor\n";
        }
        ~Derived() {
            std::cout << "Derived class destructor\n";
        }
    };
    
    int main() {
        Derived* d = new Derived();
        Base* b = d;
        delete b; // Both base and derived destructors are called
        return 0;
    }

    Output:

    Base class constructor
    Derived class constructor
    Derived class destructor
    Base class destructor

    Virtual Constructor

    In C++, we cannot make a constructor virtual. The reason is that C++ is a statically typed language, and a virtual constructor contradicts this because the compiler needs to determine the exact type at compile time to allocate the right amount of memory and initialize the object properly.

    Attempting to declare a constructor as virtual will result in a compiler error. In fact, apart from the inline keyword, no other keyword is permitted in constructor declarations.

    Problem: Tightly Coupled Object Creation

    In real-world scenarios, you may want to instantiate objects from a class hierarchy based on runtime conditions or user input. However, object creation in C++ is tightly coupled to specific class types, meaning that whenever a new class is added to a hierarchy, code changes and recompilation are required.

    For instance, consider a situation where the User class always creates an object of type Derived1. If at some point you want to create an object of Derived2, you would need to modify the User class to instantiate Derived2 instead, which forces recompilation—a design flaw.

    Example of Tight Coupling

    Here’s an example of how object creation can lead to tight coupling:

    // C++ program showing tight coupling issue in object creation
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
        Base() {}
    
        virtual ~Base() {}
    
        virtual void DisplayAction() = 0;
    };
    
    class Derived1 : public Base {
      public:
        Derived1() {
            cout << "Derived1 created\n";
        }
        ~Derived1() {
            cout << "Derived1 destroyed\n";
        }
        void DisplayAction() {
            cout << "Action from Derived1\n";
        }
    };
    
    class Derived2 : public Base {
      public:
        Derived2() {
            cout << "Derived2 created\n";
        }
        ~Derived2() {
            cout << "Derived2 destroyed\n";
        }
        void DisplayAction() {
            cout << "Action from Derived2\n";
        }
    };
    
    class User {
      public:
        User() : pBase(nullptr) {
            pBase = new Derived1();  // Always creating Derived1
            // But what if Derived2 is needed?
        }
        ~User() {
            delete pBase;
        }
    
        void Action() {
            pBase->DisplayAction();
        }
    
      private:
        Base *pBase;
    };
    
    int main() {
        User *user = new User();
        user->Action();
        delete user;
    }

    Output:

    Derived1 created
    Action from Derived1
    Derived1 destroyed

    In this example, the User class is tightly coupled to Derived1. If a consumer requires the functionality of Derived2, the User class must be modified and recompiled to create Derived2. This is inflexible and leads to poor design.

    Methods for Decoupling

    Here are common methods to decouple tightly coupled classes:

    1. Modifying with an If-Else Ladder (Not Extensible) : One way to address this issue is by adding an if-else ladder to select the desired derived class based on input. However, this solution still suffers from the same problem.

    // C++ program showing how to use if-else for object creation
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
        Base() {}
        virtual ~Base() {}
        virtual void DisplayAction() = 0;
    };
    
    class Derived1 : public Base {
      public:
        Derived1() {
            cout << "Derived1 created\n";
        }
        ~Derived1() {
            cout << "Derived1 destroyed\n";
        }
        void DisplayAction() {
            cout << "Action from Derived1\n";
        }
    };
    
    class Derived2 : public Base {
      public:
        Derived2() {
            cout << "Derived2 created\n";
        }
        ~Derived2() {
            cout << "Derived2 destroyed\n";
        }
        void DisplayAction() {
            cout << "Action from Derived2\n";
        }
    };
    
    class User {
      public:
        User() : pBase(nullptr) {
            int input;
            cout << "Enter ID (1 or 2): ";
            cin >> input;
    
            if (input == 1)
                pBase = new Derived1();
            else
                pBase = new Derived2();
        }
    
        ~User() {
            delete pBase;
        }
    
        void Action() {
            pBase->DisplayAction();
        }
    
      private:
        Base *pBase;
    };
    
    int main() {
        User *user = new User();
        user->Action();
        delete user;
    }

    Output:

    Enter ID (1 or 2): 2
    Derived2 created
    Action from Derived2
    Derived2 destroyed

    This approach is still not extensible. If another derived class, Derived3, is added to the hierarchy, the User class must be updated and recompiled.

    2. The Factory Method (Best Solution) : A better solution is to delegate the responsibility of object creation to the class hierarchy itself or a static function, thereby decoupling the User class from the derived classes. This approach is known as the Factory Method.

    // C++ program using Factory Method for object creation
    #include <iostream>
    using namespace std;
    
    class Base {
      public:
        static Base* Create(int id);
    
        Base() {}
        virtual ~Base() {}
    
        virtual void DisplayAction() = 0;
    };
    
    class Derived1 : public Base {
      public:
        Derived1() {
            cout << "Derived1 created\n";
        }
        ~Derived1() {
            cout << "Derived1 destroyed\n";
        }
        void DisplayAction() {
            cout << "Action from Derived1\n";
        }
    };
    
    class Derived2 : public Base {
      public:
        Derived2() {
            cout << "Derived2 created\n";
        }
        ~Derived2() {
            cout << "Derived2 destroyed\n";
        }
        void DisplayAction() {
            cout << "Action from Derived2\n";
        }
    };
    
    Base* Base::Create(int id) {
        if (id == 1) return new Derived1();
        else return new Derived2();
    }
    
    class User {
      public:
        User() : pBase(nullptr) {
            int input;
            cout << "Enter ID (1 or 2): ";
            cin >> input;
            pBase = Base::Create(input);
        }
    
        ~User() {
            delete pBase;
        }
    
        void Action() {
            pBase->DisplayAction();
        }
    
      private:
        Base *pBase;
    };
    
    int main() {
        User *user = new User();
        user->Action();
        delete user;
    }

    Output:

    Enter ID (1 or 2): 1
    Derived1 created
    Action from Derived1
    Derived1 destroyed

    Virtual Copy Constructor

    In the virtual constructor idiom, we have seen how to construct an object whose type is determined at runtime. But is it possible to copy an object without knowing its exact type at compile time? The virtual copy constructor addresses this problem.

    Sometimes, we need to create a new object based on the state of an existing object. Typically, this is done using the copy constructor, which initializes the new object with the state of the existing one. The compiler invokes the copy constructor when an object is instantiated from another. However, the compiler must know the exact type of the object to invoke the appropriate copy constructor.

    Example Without Virtual Copy Constructor:

    #include <iostream>
    using namespace std;
    
    class Base {
        // Base class
    };
    
    class Derived : public Base {
    public:
        Derived() {
            cout << "Derived created" << endl;
        }
    
        // Copy constructor
        Derived(const Derived &rhs) {
            cout << "Derived created by deep copy" << endl;
        }
    
        ~Derived() {
            cout << "Derived destroyed" << endl;
        }
    };
    
    int main() {
        Derived d1;
        Derived d2 = d1;  // Compiler calls the copy constructor
    
        // How to copy a Derived object through a Base pointer?
        return 0;
    }
    Problem: Copying at Runtime Without Knowing Type

    The virtual constructor creates objects of a derived class based on runtime input. When we want to copy one of these objects, we can’t use the regular copy constructor because the exact type of the object isn’t known at compile time. Instead, we need a virtual function that can duplicate the object during runtime, based on its actual type.

    Consider a drawing application where users can select an object and duplicate it. The type of object (circle, square, etc.) isn’t known until runtime, so the virtual copy constructor (or a clone function) is needed to correctly copy and paste the object.

    Updated Example Using Virtual Copy Constructor:

    Prefix Increment: Increases the operand’s value before it is used in an expression.

    #include <iostream>
    using namespace std;
    
    // Base class
    class Base {
    public:
        Base() {}
    
        // Virtual destructor to ensure correct cleanup
        virtual ~Base() {}
    
        // Pure virtual function to change object attributes
        virtual void ModifyAttributes() = 0;
    
        // Virtual constructor
        static Base* Create(int id);
    
        // Virtual copy constructor
        virtual Base* Clone() = 0;
    };
    
    // Derived1 class
    class Derived1 : public Base {
    public:
        Derived1() {
            cout << "Derived1 created" << endl;
        }
    
        // Copy constructor
        Derived1(const Derived1& rhs) {
            cout << "Derived1 created by deep copy" << endl;
        }
    
        ~Derived1() {
            cout << "~Derived1 destroyed" << endl;
        }
    
        void ModifyAttributes() {
            cout << "Derived1 attributes modified" << endl;
        }
    
        // Clone method (virtual copy constructor)
        Base* Clone() {
            return new Derived1(*this);
        }
    };
    
    // Derived2 class
    class Derived2 : public Base {
    public:
        Derived2() {
            cout << "Derived2 created" << endl;
        }
    
        // Copy constructor
        Derived2(const Derived2& rhs) {
            cout << "Derived2 created by deep copy" << endl;
        }
    
        ~Derived2() {
            cout << "~Derived2 destroyed" << endl;
        }
    
        void ModifyAttributes() {
            cout << "Derived2 attributes modified" << endl;
        }
    
        // Clone method (virtual copy constructor)
        Base* Clone() {
            return new Derived2(*this);
        }
    };
    
    // Derived3 class
    class Derived3 : public Base {
    public:
        Derived3() {
            cout << "Derived3 created" << endl;
        }
    
        // Copy constructor
        Derived3(const Derived3& rhs) {
            cout << "Derived3 created by deep copy" << endl;
        }
    
        ~Derived3() {
            cout << "~Derived3 destroyed" << endl;
        }
    
        void ModifyAttributes() {
            cout << "Derived3 attributes modified" << endl;
        }
    
        // Clone method (virtual copy constructor)
        Base* Clone() {
            return new Derived3(*this);
        }
    };
    
    // Factory method to create objects at runtime
    Base* Base::Create(int id) {
        if (id == 1)
            return new Derived1;
        else if (id == 2)
            return new Derived2;
        else
            return new Derived3;
    }
    
    // Utility class to use Base objects
    class User {
    public:
        User() : pBase(nullptr) {
            int id;
            cout << "Enter ID (1, 2, or 3): ";
            cin >> id;
    
            // Create object at runtime using factory method
            pBase = Base::Create(id);
        }
    
        ~User() {
            if (pBase) {
                delete pBase;
                pBase = nullptr;
            }
        }
    
        void PerformAction() {
            // Clone the current object (runtime copy)
            Base* clonedObj = pBase->Clone();
    
            // Modify the cloned object's attributes
            clonedObj->ModifyAttributes();
    
            // Cleanup the cloned object
            delete clonedObj;
        }
    
    private:
        Base* pBase;
    };
    
    // Client code
    int main() {
        User* user = new User();
        user->PerformAction();
        delete user;
    
        return 0;
    }

    Pure Virtual Functions and Abstract Classes

    Sometimes, we may not provide the complete implementation of all functions in a base class because the specifics are not known. Such a class is called an abstract class. For example, consider a base class called Shape. We cannot define how draw() works in the base class, but every derived class (like CircleRectangle, etc.) must provide an implementation for draw(). Similarly, an Animal class might have a move() function that all animals need to implement, but the way they move may differ. We cannot create objects of abstract classes.

    pure virtual function (also known as an abstract function) in C++ is a virtual function that has no definition in the base class, and we force derived classes to provide an implementation. If a derived class does not override this function, the derived class will also become an abstract class. A pure virtual function is declared by assigning it a value of 0 in its declaration.

    Example of a Pure Virtual Function:

    // Abstract class
    class AbstractClass {
    public:
        // Pure Virtual Function
        virtual void display() = 0;
    };

    Complete Example:  A pure virtual function must be implemented by any class that is derived from an abstract class.

    #include <iostream>
    using namespace std;
    
    class Base {
        // Private data member
        int x;
    
    public:
        // Pure virtual function
        virtual void show() = 0;
    
        // Getter function to access x
        int getX() { return x; }
    };
    
    // Derived class implementing the pure virtual function
    class Derived : public Base {
        // Private data member
        int y;
    
    public:
        // Implementation of the pure virtual function
        void show() { cout << "show() function called" << endl; }
    };
    
    int main() {
        // Creating an object of the Derived class
        Derived d;
    
        // Calling the show() function
        d.show();
    
        return 0;
    }

    Output:

    show() function called
    Key Points

    1. Abstract Classes: A class becomes abstract if it contains at least one pure virtual function.

    Example: In the code below, the class Example is abstract because it has a pure virtual function display().

    #include <iostream>
    using namespace std;
    
    class Example {
        int x;
    
    public:
        // Pure virtual function
        virtual void display() = 0;
    
        // Getter function for x
        int getX() { return x; }
    };
    
    int main() {
        // Error: Cannot instantiate an abstract class
        Example ex;
    
        return 0;
    }

    Output:

    Compiler Error: Cannot instantiate an abstract class

    The compiler will throw an error because Example is an abstract class, and we cannot create an object of it.

    2. Pointers and References to Abstract Classes: Although you cannot instantiate an abstract class, you can create pointers and references to it. This allows polymorphism, where a pointer to an abstract class can point to any derived class object.

    Example:

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        // Pure virtual function
        virtual void show() = 0;
    };
    
    class Derived : public Base {
    public:
        // Implementation of the pure virtual function
        void show() { cout << "Derived class implementation" << endl; }
    };
    
    int main() {
        // Pointer to Base class pointing to Derived object
        Base* ptr = new Derived();
    
        // Calling the show() function via the pointer
        ptr->show();
    
        delete ptr;
        return 0;
    }

    Output:

    Derived class implementation

    3. Derived Classes Become Abstract: If a derived class does not override the pure virtual function, it will also be treated as an abstract class, meaning you cannot instantiate it.

    Example:

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        // Pure virtual function
        virtual void show() = 0;
    };
    
    class Derived : public Base {
        // No override for the pure virtual function
    };
    
    int main() {
        // Error: Cannot create an object of an abstract class
        Derived d;
    
        return 0;
    }

    Output:

    Compiler Error: Derived is an abstract class because 'show()' is not implemented

    Pure Virtual Destructor in C++

    Can a Destructor Be Pure Virtual in C++?

    Yes, in C++, a destructor can be declared as pure virtual. Pure virtual destructors are legal and serve an essential role, especially in abstract base classes. However, if a class contains a pure virtual destructor, you must provide a definition for the destructor. This requirement arises from the fact that destructors are not overridden like other functions but are always called in reverse order of class inheritance. Without a definition for the pure virtual destructor, there will be no valid function body to call during object destruction.

    Why Must a Pure Virtual Destructor Have a Body?

    The reason behind this requirement is that destructors are always invoked in reverse order during the destruction of objects. When a derived class object is deleted, its destructor is called first, followed by the base class destructor. Since the base class destructor must still execute, a function body is needed, even for pure virtual destructors. This ensures that resources allocated by the base class are cleaned up properly when an object is destroyed.

    Example of a Pure Virtual Destructor

    #include <iostream>
    using namespace std;
    
    // Base class with a pure virtual destructor
    class Base {
    public:
        virtual ~Base() = 0; // Pure virtual destructor
    };
    
    // Definition of the pure virtual destructor
    Base::~Base() {
        cout << "Base class destructor called\n";
    }
    
    // Derived class implementing its destructor
    class Derived : public Base {
    public:
        ~Derived() { cout << "Derived class destructor called\n"; }
    };
    
    // Driver code
    int main() {
        Base* ptr = new Derived();
        delete ptr;
        return 0;
    }

    Output:

    Derived class destructor called
    Base class destructor called

    In the example above, both the derived class and base class destructors are called in reverse order, ensuring proper cleanup.

    How It Works

    This works because destructors are always called in reverse order, starting from the most derived class back to the base class. When the delete operator is called on a pointer to a derived object, the derived class destructor is executed first, followed by the base class destructor. This is only possible because we provided a definition for the pure virtual destructor in the base class.

    Important Points

    Example: Demonstrating Abstract Class with Pure Virtual Destructor

    #include <iostream>
    using namespace std;
    
    class AbstractClass {
    public:
        // Pure virtual destructor
        virtual ~AbstractClass() = 0;
    };
    
    // Defining the pure virtual destructor
    AbstractClass::~AbstractClass() {
        cout << "AbstractClass destructor called\n";
    }
    
    // Attempting to create an object of AbstractClass will result in an error
    int main() {
        // AbstractClass obj;  // This will cause a compiler error
        return 0;
    }
    Error: Cannot instantiate abstract class ‘AbstractClass’ because its destructor is pure virtual.

    This error occurs because AbstractClass is abstract, meaning it contains a pure virtual function, making it impossible to instantiate. The error message confirms that the pure virtual destructor makes the class abstract.

    Can Static Functions Be Virtual in C++?

    In C++, static member functions of a class cannot be virtual. Virtual functions are invoked through a pointer or reference to an object of the class, allowing polymorphic behavior. However, static member functions are associated with the class itself, rather than any specific instance of the class. This distinction prevents them from being virtual, as there is no object to refer to for invoking them virtually.

    Example Demonstrating Static Functions Cannot Be Virtual

    Attempting to declare a static member function as virtual will result in a compilation error:

    // C++ program demonstrating that static member functions
    // cannot be virtual
    #include <iostream>
    
    class Example {
    public:
        virtual static void display() {}
    };
    
    int main() {
        Example e;
        return 0;
    }

    Output:

    error: member ‘display’ cannot be declared both virtual and static

    The error occurs because C++ does not allow the combination of virtual and static for member functions, as static functions are tied to the class, not to specific instances.

    Static Member Functions Cannot Be const or volatile

    Static member functions are also not allowed to be declared const or volatile. Since const and volatile qualifiers are applied to member functions to indicate whether they can modify the state of the object or respond to volatility, static functions—being associated with the class rather than instances—cannot access instance-specific data members, making these qualifiers meaningless.

    Example Demonstrating Static Member Functions Cannot Be const

    Attempting to declare a static member function as const will also fail at compile time:

    // C++ program demonstrating that static member functions
    // cannot be const
    #include <iostream>
    
    class Example {
    public:
        static void display() const {}
    };
    
    int main() {
        Example e;
        return 0;
    }

    Example:

    error: static member function ‘static void Example::display()’ cannot have cv-qualifier

    Run-time Type Information (RTTI) in C++

    In this example, we demonstrate how dynamic_cast fails when the base class does not contain a virtual function. Since RTTI (Run-Time Type Information) requires at least one virtual function in the base class for dynamic_cast to function, this example will lead to a runtime failure.

    #include <iostream>
    using namespace std;
    
    // Base class without a virtual function
    class Base {};
    
    // Derived class inheriting from Base
    class Derived : public Base {};
    
    int main() {
        Base* basePtr = new Derived; // Base class pointer pointing to a Derived object
        // Attempting to downcast using dynamic_cast
        Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    
        if (derivedPtr != nullptr)
            cout << "Cast successful" << endl;
        else
            cout << "Cannot cast Base* to Derived*" << endl;
    
        return 0;
    }

    Expected Output:

    Cannot cast Base* to Derived*

    Here, the dynamic_cast fails because there is no virtual function in the base class Base, which makes RTTI unavailable. As a result, the cast does not succeed.

    Adding a Virtual Function to Enable RTTI

    By adding a virtual function to the base class, RTTI is enabled, allowing dynamic_cast to perform the necessary checks at runtime and making the cast succeed.

    Example With a Virtual Function in the Base Class

    #include <iostream>
    using namespace std;
    
    // Base class with a virtual function
    class Base {
    public:
        virtual void exampleFunction() {} // Virtual function to enable RTTI
    };
    
    // Derived class inheriting from Base
    class Derived : public Base {};
    
    int main() {
        Base* basePtr = new Derived; // Base class pointer pointing to a Derived object
        // Attempting to downcast using dynamic_cast
        Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    
        if (derivedPtr != nullptr)
            cout << "Cast successful" << endl;
        else
            cout << "Cannot cast Base* to Derived*" << endl;
    
        return 0;
    }

    Expected Output:

    Cast successful
  • Polymorphism

    Function Overriding in C++

    Polymorphism, derived from the term meaning “many forms,” refers to the ability of an entity to take on different behaviors or forms depending on the context. In Object-Oriented Programming (OOP), polymorphism is a core feature that enables a single action to be performed in various ways, depending on the object that invokes it. A common real-world example is a person who assumes different roles simultaneously, such as being a parent, an employee, and a friend. Similarly, polymorphism allows objects of different classes to respond uniquely to the same function call, making it a fundamental concept in OOP.

    Types of Polymorphism:

    1. Compile-Time Polymorphism (also called early binding or static polymorphism)
    2. Run-Time Polymorphism (also known as late binding or dynamic polymorphism)

    1. Compile-Time Polymorphism:

    This type of polymorphism is achieved using function overloading or operator overloading.

    A. Function Overloading:

    In function overloading, multiple functions share the same name but differ in the number or type of parameters. Depending on the arguments passed, the appropriate function is selected at compile time.

    Example of Function Overloading in C++:

    #include <iostream>
    using namespace std;
    
    class Example {
    public:
        void display(int a) {
            cout << "Integer: " << a << endl;
        }
    
        void display(double b) {
            cout << "Double: " << b << endl;
        }
    
        void display(int a, int b) {
            cout << "Two Integers: " << a << " and " << b << endl;
        }
    };
    
    int main() {
        Example obj;
    
        obj.display(5);        // Calls the function with an integer argument
        obj.display(3.14);     // Calls the function with a double argument
        obj.display(7, 8);     // Calls the function with two integer arguments
    }

    Output:

    Integer: 5
    Double: 3.14
    Two Integers: 7 and 8

    In this example, the display function behaves differently based on the type and number of arguments passed to it, demonstrating compile-time polymorphism.

    B. Operator Overloading:

    Operator overloading allows you to redefine the meaning of operators for user-defined types. For instance, the + operator can be overloaded to work with objects like complex numbers.

    Example of Operator Overloading in C++:

    #include <iostream>
    using namespace std;
    
    class Complex {
    private:
        int real, imag;
    
    public:
        Complex(int r = 0, int i = 0) : real(r), imag(i) {}
    
        // Overloading the '+' operator for complex numbers
        Complex operator+(const Complex& obj) {
            Complex res;
            res.real = real + obj.real;
            res.imag = imag + obj.imag;
            return res;
        }
    
        void display() const {
            cout << real << " + i" << imag << endl;
        }
    };
    
    int main() {
        Complex c1(3, 4), c2(1, 2);
        Complex c3 = c1 + c2;  // Calls overloaded '+' operator
        c3.display();
    }

    Output:

    4 + i6

    Here, the + operator is overloaded to add complex numbers, showing compile-time polymorphism using operator overloading.

    2. Run-Time Polymorphism

    This type of polymorphism is accomplished using function overriding, where a derived class provides a specific implementation of a function that is already defined in its base class. This allows the correct function to be called based on the object type at runtime.

    A. Function Overriding:

    When a derived class defines a function that overrides a function in its base class, the base function is “overridden.” The decision to call the base or derived class function is made at runtime.

    Example of Function Overriding in C++:

    #include <iostream>
    using namespace std;
    
    class Animal {
    public:
        virtual void sound() {
            cout << "Animal makes a sound" << endl;
        }
    };
    
    class Dog : public Animal {
    public:
        void sound() override {
            cout << "Dog barks" << endl;
        }
    };
    
    int main() {
        Animal* animalPtr;
        Dog dog;
    
        animalPtr = &dog;
        animalPtr->sound();  // Calls the overridden method in the Dog class
    }

    Output:

    Dog barks

    In this example, the sound method is overridden in the Dog class, and the function call is resolved at runtime based on the actual object type.

    B. Virtual Function:

    virtual function in a base class allows derived classes to override it. When a base class pointer points to a derived class object, the overridden function is called, enabling run-time polymorphism.

    Example of Virtual Function in C++:

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        virtual void show() {
            cout << "Base class show function" << endl;
        }
    
        void display() {
            cout << "Base class display function" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        void show() override {
            cout << "Derived class show function" << endl;
        }
    
        void display() {
            cout << "Derived class display function" << endl;
        }
    };
    
    int main() {
        Base* basePtr;
        Derived derivedObj;
    
        basePtr = &derivedObj;
    
        basePtr->show();     // Calls the overridden show() in Derived class
        basePtr->display();  // Calls the display() in Base class
    }

    Output:

    Derived class show function
    Base class display function

    Virtual Functions and Runtime Polymorphism in C++

    A virtual function is a member function that is declared in a base class using the virtual keyword and redefined (overridden) in a derived class. It enables late binding, meaning that the compiler decides which function to call at runtime based on the object type, not the pointer type. This is a key feature of runtime polymorphism.

    The term “Polymorphism” refers to the ability to take many forms. When applied to object-oriented programming (OOP), polymorphism allows different classes related by inheritance to behave in various ways under a common interface. Breaking it down, Poly means “many” and morphism means “forms”, implying different behaviors in different contexts.

    Concept of Virtual Functions

    Virtual functions in C++ allow a base class pointer to invoke derived class functions, achieving runtime polymorphism. The base class’s function is overridden by the derived class function, but the call is resolved at runtime. A key point to remember is that virtual functions aren’t allowed in constructors since the object construction follows a “bottom-up” approach.

    Example without Virtual Functions

    Let’s look at a simple example where we calculate the area of different shapes without using virtual functions:

    #include <iostream>
    using namespace std;
    
    class Shape {
    public:
        Shape(int l, int w) : length(l), width(w) {}
        int getArea() {
            cout << "This is a call to the base class area\n";
            return 1;
        }
    
    protected:
        int length, width;
    };
    
    class Square : public Shape {
    public:
        Square(int l = 0, int w = 0) : Shape(l, w) {}
        int getArea() {
            cout << "Square area: " << length * width << endl;
            return length * width;
        }
    };
    
    class Rectangle : public Shape {
    public:
        Rectangle(int l = 0, int w = 0) : Shape(l, w) {}
        int getArea() {
            cout << "Rectangle area: " << length * width << endl;
            return length * width;
        }
    };
    
    int main() {
        Shape* s;
        Square sq(4, 4);
        Rectangle rec(4, 5);
    
        s = &sq;
        s->getArea();  // Calls base class method
        s = &rec;
        s->getArea();  // Calls base class method
    
        return 0;
    }

    Output:

    This is a call to the base class area
    This is a call to the base class area

    Here, despite pointing to derived class objects, the getArea() function of the base class is called due to static binding.

    Example with Virtual Functions

    Now, let’s modify the code to include virtual functions for achieving runtime polymorphism:

    #include <iostream>
    using namespace std;
    
    class Shape {
    public:
        virtual void calculateArea() {
            cout << "Calculating area in base class" << endl;
        }
        virtual ~Shape() {
            cout << "Shape destructor called" << endl;
        }
    };
    
    class Square : public Shape {
    public:
        int side;
        Square(int s) : side(s) {}
    
        void calculateArea() override {
            cout << "Square area: " << side * side << endl;
        }
    
        ~Square() {
            cout << "Square destructor called" << endl;
        }
    };
    
    class Rectangle : public Shape {
    public:
        int width, height;
        Rectangle(int w, int h) : width(w), height(h) {}
    
        void calculateArea() override {
            cout << "Rectangle area: " << width * height << endl;
        }
    
        ~Rectangle() {
            cout << "Rectangle destructor called" << endl;
        }
    };
    
    int main() {
        Shape* shape;
        Square sq(4);
        Rectangle rec(5, 6);
    
        shape = &sq;
        shape->calculateArea();  // Calls Square's method
    
        shape = &rec;
        shape->calculateArea();  // Calls Rectangle's method
    
        return 0;
    }

    Output:

    Square area: 16
    Rectangle area: 30

    In this example, the calculateArea() function calls are resolved at runtime, based on the actual object being pointed to. This demonstrates the power of virtual functions and runtime polymorphism.

    How Virtual Functions are Resolved at Runtime

    To resolve virtual functions at runtime, the compiler typically uses two mechanisms:

    • vtable: A table of function pointers maintained per class.
    • vptr: A pointer to the vtable, maintained per object instance.

    At runtime, the vptr helps the compiler determine which function (base or derived) to invoke by consulting the appropriate vtable.

    Practical Use of Virtual Functions: Employee Management System

    Consider a scenario of managing employees in an organization. Suppose you have a base class Employee and derived classes like Manager and Engineer. Each class may override virtual functions like raiseSalary() or promote(). The virtual mechanism allows these functions to be called based on the actual type of employee without worrying about their specific implementation.

    class Employee {
    public:
        virtual void raiseSalary() {
            // General salary raise logic
        }
        virtual void promote() {
            // General promotion logic
        }
        virtual ~Employee() {}
    };
    
    class Manager : public Employee {
    public:
        void raiseSalary() override {
            // Manager-specific raise logic
        }
        void promote() override {
            // Manager-specific promotion logic
        }
    };
    
    class Engineer : public Employee {
    public:
        void raiseSalary() override {
            // Engineer-specific raise logic
        }
        void promote() override {
            // Engineer-specific promotion logic
        }
    };
    
    void applyRaise(Employee* employees[], int size) {
        for (int i = 0; i < size; ++i) {
            employees[i]->raiseSalary();
        }
    }
  • Encapsulation and Abstraction​

    Encapsulation in C++

    Encapsulation in C++ refers to the bundling of data and functions that operate on that data into a single unit or class. In Object-Oriented Programming (OOP), encapsulation plays a significant role in ensuring data protection by restricting direct access to an object’s attributes.

    Real-Life Example of Encapsulation

    Consider a company that has several departments, such as accounting, finance, and sales. Each department manages its own specific data:

    • The finance department handles all financial transactions and manages its own financial records.
    • The sales department is responsible for sales-related activities and keeps sales records.

    If a finance department official needs to access sales data for a particular month, they cannot directly access it. Instead, they need to request the relevant data from someone in the sales department, maintaining controlled access to the information.

    This exemplifies encapsulation: the sales department’s data and the methods to manage it are wrapped together, providing controlled access.

    Key Properties of Encapsulation

    1. Data Protection: Encapsulation safeguards an object’s internal state by keeping its data members private. External code can only interact with the data through public methods, which ensures that the data is manipulated securely and in a controlled manner.

    2. Information Hiding: Encapsulation conceals the internal workings of a class, exposing only a public interface to the outside world. This abstraction simplifies the use of the class, as external code doesn’t need to understand the internal details, allowing those details to be changed without impacting other parts of the program.

    Example 1: Simple Encapsulation

    #include <iostream>
    using namespace std;
    
    class Number {
    private:
        int value;
    
    public:
        void setValue(int input) {
            value = input;
        }
    
        int getHalf() {
            return value / 2;
        }
    };
    
    int main() {
        Number num;
        int n;
        cin >> n;
        num.setValue(n);
        cout << "Half of " << n << " is: " << num.getHalf() << endl;
        return 0;
    }

    In this example, the class Number encapsulates the data (the integer value) and provides a method to manipulate that data (calculate half of the number). The value member is private and can only be accessed or modified through the class’s public methods.

    Features of Encapsulation
    • Functions inside the class must interact with the class’s member variables. This ensures that encapsulation is effective.
    • Encapsulation improves the readability, maintainability, and security of code by organizing related data and methods into a cohesive unit.
    • It also provides control over how data members are modified, promoting better data integrity.

    Example 2: Person Class

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Person {
    private:
        string name;
        int age;
    
    public:
        Person(string n, int a) : name(n), age(a) {}
    
        void setName(string n) {
            name = n;
        }
    
        string getName() {
            return name;
        }
    
        void setAge(int a) {
            age = a;
        }
    
        int getAge() {
            return age;
        }
    };
    
    int main() {
        Person person("Alice", 25);
    
        cout << "Name: " << person.getName() << endl;
        cout << "Age: " << person.getAge() << endl;
    
        person.setName("Bob");
        person.setAge(30);
    
        cout << "Name: " << person.getName() << endl;
        cout << "Age: " << person.getAge() << endl;
    
        return 0;
    }

    Abstraction in C++

    Data abstraction is one of the core principles of Object-Oriented Programming (OOP) in C++. It involves showcasing only the essential features and hiding unnecessary details. In simpler terms, abstraction means focusing on what an object does rather than how it performs that task.

    Real-Life Example of Abstraction

    Imagine a person driving a car. The driver knows that pressing the accelerator increases the speed, and pressing the brakes stops the car. However, the driver is unaware of the complex internal mechanisms that make this happen, such as how the engine works or how the brake system operates. This concept illustrates abstraction, where only necessary information is provided to the user, and the intricate details are concealed.

    Types of Abstraction

    1. Data Abstraction: This form of abstraction presents only the relevant information about data, leaving out unnecessary details.

    2. Control Abstraction: This type of abstraction focuses on hiding the details of the implementation, exposing only the essential behavior.

    Abstraction in C++

    Abstraction can be achieved in C++ through classes, access specifiers, and even header files. Let’s explore these mechanisms:

    Abstraction using Classes

    In C++, classes allow us to group data members and functions, and we can control which members are accessible to the outside world. This is done through access specifiers such as public and private. A class can decide which data members and methods should be exposed to the external world while keeping the internal details hidden.

    Abstraction through Header Files

    Header files also provide a form of abstraction. For example, when using the pow() function from the math.h library to calculate the power of a number, we do not need to know the underlying logic that the function uses. We simply call pow() with the necessary parameters, and the result is returned.

    Abstraction using Access Specifiers

    Access specifiers are crucial for implementing abstraction in C++. They control the visibility of class members:

    • Public members can be accessed from anywhere in the code.
    • Private members can only be accessed from within the class itself. They are hidden from the outside world and can only be manipulated by class methods.

    By using access specifiers, we can enforce data abstraction by hiding the internal details and exposing only the necessary parts of the class.

    Example of Abstraction

    #include <iostream>
    using namespace std;
    
    class AbstractExample {
    private:
        int length, width;
    
    public:
        // Method to set values for private members
        void setDimensions(int l, int w) {
            length = l;
            width = w;
        }
    
        // Method to calculate and display the area
        void displayArea() {
            cout << "Area of the rectangle: " << length * width << endl;
        }
    };
    
    int main() {
        AbstractExample rect;
        rect.setDimensions(5, 10);
        rect.displayArea();
        return 0;
    }

    Output:

    Area of the rectangle: 50

    Difference between Abstraction and Encapsulation in C++

    Abstraction: In Object-Oriented Programming (OOP), abstraction refers to the process of selectively exposing only the necessary details of an object while ignoring the complexities and unnecessary information. Abstraction simplifies the user’s interaction with an object by providing only what is essential and hiding the implementation complexities. This leads to cleaner, more efficient code by focusing on what an object does rather than how it does it.

    Example of Abstraction:

    #include <iostream>
    using namespace std;
    
    class Calculator {
    private:
        int num1, num2, result;
    
    public:
        void add(int x, int y) {
            num1 = x;
            num2 = y;
            result = num1 + num2;
            cout << "Sum is: " << result << endl;
        }
    };
    
    int main() {
        Calculator calc;
        calc.add(7, 3);
        return 0;
    }

    Output:

    Sum is: 10

    Encapsulation:

    Encapsulation refers to the process of bundling the data and methods that operate on the data within a single unit, typically a class. This approach protects the data from unauthorized access and modification by restricting access to the data members through access specifiers like privateprotected, and public. Encapsulation also naturally leads to data abstraction.

    Example of Encapsulation:

    #include <iostream>
    using namespace std;
    
    class BankAccount {
    private:
        double balance;
    
    public:
        void setBalance(double amount) {
            balance = amount;
        }
    
        double getBalance() {
            return balance;
        }
    };
    
    int main() {
        BankAccount account;
        account.setBalance(1000.50);
        cout << "Current balance: " << account.getBalance() << endl;
        return 0;
    }

    Output:

    Current balance: 1000.5
    Difference Between Abstraction and Encapsulation:
    S.No.AbstractionEncapsulation
    1.Abstraction focuses on exposing only the necessary information.Encapsulation focuses on grouping data and methods into a single unit.
    2.Abstraction addresses problems at the design or interface level.Encapsulation addresses problems at the implementation level.
    3.It hides unnecessary details to simplify the interface.It hides data to protect it from external access.
    4.Achieved using abstract classes and interfaces.Achieved through access specifiers like privateprotected, and public.
    5.Implementation details are hidden by using abstract classes or interfaces.Data is hidden by using getters and setters to access or modify the data.
    6.Objects performing abstraction may use encapsulation to hide complexities.Objects using encapsulation do not necessarily need to involve abstraction.
  • Dynamic Memory

    new and delete Operators in C++ For Dynamic Memory

    Dynamic memory allocation in C/C++ allows programmers to manually manage memory usage, specifically allocating memory on the heap. Unlike stack-allocated memory for local and static variables, dynamic allocation provides the flexibility to allocate and deallocate memory at runtime, which is especially useful for structures like linked lists and trees.

    Applications of Dynamic Memory Allocation

    One key application of dynamic memory allocation is the ability to handle variable-sized memory requirements, which isn’t feasible with static allocations (except for variable-length arrays). This flexibility is crucial in numerous scenarios, such as creating dynamic data structures.

    Differences from Static Memory Allocation

    For static variables, such as int a or char str[10], memory management is automatic—allocated and deallocated by the compiler. Conversely, for dynamically allocated memory (e.g., int *p = new int[10]), the programmer is responsible for deallocation. Failure to do so can lead to memory leaks, where the memory remains allocated until the program terminates.

    Memory Management in C++

    C uses functions like malloc() and calloc() for dynamic memory allocation, along with free() for deallocation. C++, however, offers a more streamlined approach using the new and delete operators.

    Using the new Operator

    The new operator requests memory allocation from the heap. If successful, it returns a pointer to the allocated memory.

    Syntax :

    pointer-variable = new data-type;

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        // Pointer to store the address of allocated memory
        int* ptr = new int; // Allocates memory for an integer
        *ptr = 10;          // Assigns a value to the allocated memory
    
        // Printing the address and value
        cout << "Address: " << ptr << endl;
        cout << "Value: " << *ptr << endl;
    
        delete ptr; // Deallocating the memory
        return 0;
    }
    Initializing Dynamically Allocated Memory

    You can also initialize memory directly upon allocation:

    Example:

    #include <iostream>
    using namespace std;
    
    struct CustomType {
        int value;
        CustomType(int v) : value(v) {}
    };
    
    int main() {
        int* p = new int(42); // Initializes an integer with 42
        CustomType* obj = new CustomType(100); // Initializes CustomType
    
        cout << *p << " " << obj->value << endl;
    
        delete p;     // Deallocate integer
        delete obj;   // Deallocate CustomType
        return 0;
    }

    Output:

    a + b = 30
    a - b = 20
    a * b = 125
    a / b = 5
    a % b = 0
    +a = 25
    -a = -25
    a++ = 25
    a-- = 26

    Allocating Arrays : You can allocate memory for an array using the new operator:

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        int n = 5;
        int* array = new int[n]; // Allocates memory for an array of 5 integers
    
        for (int i = 0; i < n; ++i) {
            array[i] = i * 2; // Initializing array elements
        }
    
        // Displaying the values
        for (int i = 0; i < n; ++i) {
            cout << array[i] << " "; // Outputs: 0 2 4 6 8
        }
    
        delete[] array; // Deallocate the array
        return 0;
    }

    Output:

    a < b  : 0
    a > b  : 1
    a <= b: 0
    a >= b: 1
    a == b: 0
    a != b : 1

    Handling Memory Allocation Failures : When memory allocation fails, the new operator throws a std::bad_alloc exception unless nothrow is used:

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        int* p = new(nothrow) int; // Attempt to allocate memory
        if (!p) {
            cout << "Memory allocation failed\n";
        } else {
            *p = 42;
            cout << "Value: " << *p << endl;
            delete p; // Deallocate memory
        }
        return 0;
    }

    Output:

    a && b : 1
    a || b : 1
    !a: 0

    The delete Operator: To free memory allocated with new, use the delete operator:

    Syntax:

    delete pointer-variable;        // For single objects
    delete[] pointer-variable;      // For arrays

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        int* singleInt = new int(5);
        float* floatValue = new float(10.5);
    
        cout << "Integer: " << *singleInt << endl;
        cout << "Float: " << *floatValue << endl;
    
        delete singleInt; // Freeing single integer memory
        delete floatValue; // Freeing float memory
    
        int n = 3;
        int* intArray = new int[n]; // Allocating an array
        for (int i = 0; i < n; ++i) {
            intArray[i] = i + 1;
        }
    
        cout << "Array values: ";
        for (int i = 0; i < n; ++i) {
            cout << intArray[i] << " "; // Outputs: 1 2 3
        }
    
        delete[] intArray; // Freeing the entire array
        return 0;
    }

    In C++, memory can be dynamically allocated using the new and delete operators, while C and C++ also provide the malloc() and free() functions for similar purposes. Though they seem to serve the same function, there are key differences between them.

    #include <iostream>
    using namespace std;
    
    // Class A
    class A {
        int a;
    public:
        int* ptr;
    
        // Constructor of class A
        A() {
            cout << "Constructor was Called!" << endl;
        }
    };
    
    // Driver Code
    int main() {
        // Creating an object of class A using new
        A* a = new A;
        cout << "Object of class A was created using new operator!" << endl;
    
        // Creating an object of class A using malloc
        A* b = (A*)malloc(sizeof(A));
        cout << "Object of class A was created using malloc()!" << endl;
    
        // Cleanup
        delete a;
        free(b); // Note: This does not call the destructor
        return 0;
    }

    Output:

    Constructor was Called!
    Object of class A was created using new operator!
    Object of class A was created using malloc()!

    new vs malloc() and free() vs delete in C++

    Differences between new/delete and malloc()/free() in C++

    In C++, new and delete operators are used for dynamic memory allocation and deallocation, similar to malloc() and free() in C. Although they perform similar tasks, there are key differences between them, particularly in how they handle constructors and destructors.

    Key Differences: Constructors and Destructors
    • malloc(): This is a C library function that can also be used in C++. However, it doesn’t invoke the constructor of a class when allocating memory for an object.
    • new: The new operator, which is exclusive to C++, allocates memory and also calls the constructor for object initialization.

    Below is an example illustrating the difference between new and malloc():

    // C++ program to demonstrate `new` vs `malloc()`
    #include <iostream>
    using namespace std;
    
    // Class Example
    class Example {
        int value;
    
    public:
        int* pointer;
    
        // Constructor
        Example() {
            cout << "Constructor called!" << endl;
        }
    };
    
    int main() {
        // Allocating memory with `new`
        Example* obj1 = new Example;
        cout << "Object created using `new`!" << endl;
    
        // Allocating memory with `malloc`
        Example* obj2 = (Example*)malloc(sizeof(Example));
        cout << "Object created using `malloc()`!" << endl;
    
        return 0;
    }

    Output:

    Constructor called!
    Object created using `new`!
    Object created using `malloc()`!

    In this example, when using new, the constructor is called, whereas with malloc, it is not.

    free() vs delete
    • free(): A C library function that deallocates memory but does not call the destructor of a class.
    • delete: A C++ operator that not only deallocates memory but also calls the destructor of the class to handle cleanup.

    Example:

    // C++ program to demonstrate `free()` vs `delete`
    #include <iostream>
    using namespace std;
    
    // Class Example
    class Example {
        int value;
    
    public:
        int* pointer;
    
        // Constructor
        Example() {
            cout << "Constructor called!" << endl;
        }
    
        // Destructor
        ~Example() {
            cout << "Destructor called!" << endl;
        }
    };
    
    int main() {
        // Using `new` and `delete`
        Example* obj1 = new Example;
        cout << "Object created using `new`!" << endl;
        delete obj1;
        cout << "Object deleted using `delete`!" << endl;
    
        // Using `malloc` and `free`
        Example* obj2 = (Example*)malloc(sizeof(Example));
        cout << "Object created using `malloc()`!" << endl;
        free(obj2);
        cout << "Object deleted using `free()`!" << endl;
    
        return 0;
    }

    Output:

    Constructor called!
    Object created using `new`!
    Destructor called!
    Object deleted using `delete`!
    
    Object created using `malloc()`!
    Object deleted using `free()`!

    In this output, when using delete, the destructor is called, but with free, it is not.

    More Examples for Understanding

    Example 1: Demonstrating Automatic Destructor Call

    In this example, we show that the destructor is called automatically when an object goes out of scope, even without explicitly using delete.

    // C++ program to demonstrate automatic destructor call
    #include <iostream>
    using namespace std;
    
    // Class Example
    class Example {
        int value;
    
    public:
        int* pointer;
    
        // Constructor
        Example() {
            cout << "Constructor called!" << endl;
        }
    
        // Destructor
        ~Example() {
            cout << "Destructor called!" << endl;
        }
    };
    
    int main() {
        Example obj;
        return 0;
    }

    Output:

    Constructor called!
    Destructor called!

    Here, the destructor is called automatically when return 0 is executed, signaling the end of the program.

    To prevent the automatic destructor call, you could replace return 0 with exit(0), which terminates the program immediately. Here’s an example of how that would work:

    Example 2: Preventing Automatic Destructor Call with exit(0)

    // C++ program to prevent automatic destructor call
    #include <iostream>
    using namespace std;
    
    // Class Example
    class Example {
        int value;
    
    public:
        int* pointer;
    
        // Constructor
        Example() {
            cout << "Constructor called!" << endl;
        }
    
        // Destructor
        ~Example() {
            cout << "Destructor called!" << endl;
        }
    };
    
    int main() {
        Example obj;
        exit(0);
    }

    Output:

    Constructor called!

    Here, the destructor is not called because exit(0) halts the program immediately.

    Example 3: Dynamically Allocating an Object with new

    // C++ program to demonstrate `new` operator
    #include <iostream>
    using namespace std;
    
    // Class Example
    class Example {
        int value;
    
    public:
        int* pointer;
    
        // Constructor
        Example() {
            cout << "Constructor called!" << endl;
        }
    
        // Destructor
        ~Example() {
            cout << "Destructor called!" << endl;
        }
    };
    
    int main() {
        // Dynamically creating an object
        Example* obj = new Example;
        return 0;
    }

    Output:

    Constructor called!
  • Strings

    C++ Strings Overview

    C++ offers two primary ways to handle strings: traditional C-style character arrays and the std::string class from the standard library, which provides a more flexible, dynamic approach.

    1. C-Style Strings: C-style strings are simple character arrays ending with a null character ('\0'), inherited from the C language. They are efficient but lack the advanced features of the std::string class.

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        char s[] = "Programming";
        cout << s << endl;
        return 0;
    }

    Output:

    Programming

    2. std::string Class : The std::string class, part of the <string> library, introduces numerous advantages over C-style strings, such as dynamic sizing and various member functions for easy manipulation.

    #include <iostream>
    using namespace std;
    
    int main() {
        string str("Hello, C++");
        cout << str;
        return 0;
    }

    Output:

    Hello, C++
    Defining Strings with Repeating Characters

    To define strings with repeated characters:

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        string str(4, 'A');
        cout << str;
        return 0;
    }

    Output:

    AAAA
    Methods for Taking String Input

    1. Using cin
    2. Using getline
    3. Using stringstream

    1. Using cinThe simplest method is to use cin with the extraction operator (>>), which reads input until a space is encountered.

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        string s;
        cout << "Enter a word: ";
        cin >> s;
        cout << "Entered word: " << s << endl;
        return 0;
    }

    Output:

    Enter a word: Hello
    Entered word: Hello

    2. Using getlineThe getline() function reads an entire line of input, including spaces.

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        string s;
        cout << "Enter a sentence: ";
        getline(cin, s);
        cout << "Entered sentence: " << s << endl;
        return 0;
    }

    Output:

    Enter a sentence: C++ is powerful!
    Entered sentence: C++ is powerful!

    String Functions in C++

    Common functions for manipulating std::string objects include:
    • length()
    • swap()
    • size()
    • resize()
    • find()
    • push_back()
    • pop_back()
    • clear()

    Example of Basic String Functions:

    Length: 9
    After push_back: Developers
    After pop_back: Developer
    After clear: String is empty

    Iterators for String Traversal

    C++ provides iterators for traversing strings, including forward and reverse iterators.

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        string s = "Coding";
    
        cout << "Forward: ";
        for (auto itr = s.begin(); itr != s.end(); ++itr) {
            cout << *itr;
        }
        cout << endl;
    
        cout << "Reverse: ";
        for (auto ritr = s.rbegin(); ritr != s.rend(); ++ritr) {
            cout << *ritr;
        }
        cout << endl;
    
        return 0;
    }

    Output:

    Forward: Coding
    Reverse: gnidoC

    String Capacity Functions

    Capacity functions control a string’s memory allocation.

    1. length(): Returns the string’s length.
    2. capacity(): 
    Returns the memory allocated.
    3. resize(): 
    Changes the string size.
    4. shrink_to_fit(): 
    Reduces allocated capacity to the minimum size.

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        string s = "Optimize";
    
        cout << "Initial length: " << s.length() << endl;
        cout << "Initial capacity: " << s.capacity() << endl;
    
        s.resize(5);
        cout << "After resize: " << s << endl;
    
        s.shrink_to_fit();
        cout << "Capacity after shrink_to_fit: " << s.capacity() << endl;
    
        return 0;
    }

    Output:

    Initial length: 8
    Initial capacity: 15
    After resize: Optim
    Capacity after shrink_to_fit: 5
  • Pointers and References

    Pointers and References in C++

    In C++, pointers and references are mechanisms to manage memory and access data using memory addresses. Both serve different purposes in dealing with memory, addressing, and data manipulation within a program.Pointers are symbolic representations of addresses. They allow a program to implement call-by-reference and handle dynamic data structures. A pointer stores the memory address of a variable.

    Syntax:

    datatype *ptr;

    For example:

    int *ptr; // ptr is a pointer to an integer variable

    Example of Pointers in C++

    #include <iostream>
    using namespace std;
    
    int main() {
        int x = 15;   // Declare an integer variable
        int* myptr;   // Declare a pointer variable
    
        // Store the address of x in the pointer
        myptr = &x;
    
        // Print the value of x
        cout << "Value of x: " << x << endl;
    
        // Print the address stored in the pointer
        cout << "Address stored in myptr: " << myptr << endl;
    
        // Print the value of x using dereferencing
        cout << "Value of x using *myptr: " << *myptr << endl;
    
        return 0;
    }

    Output:

    Value of x: 15
    Address stored in myptr: 0x7ffeefbff5cc
    Value of x using *myptr: 15

    Applications of Pointers in C++

    Pointers are useful in various ways, such as:

    ApplicationDescription
    Passing Arguments by ReferencePointers allow call-by-reference, enabling modifications to be made directly to passed variables.
    Accessing Array ElementsPointers are used internally by the compiler to manage array indexing.
    Returning Multiple ValuesPointers allow functions to return multiple results, like a value and its square.
    Dynamic Memory AllocationPointers can be used to allocate and manage memory at runtime, providing flexibility.
    Implementing Data StructuresPointers are essential in creating dynamic data structures like linked lists, trees, and graphs.
    System-Level ProgrammingThey are heavily used in system-level programming to manipulate memory addresses directly.
    Features of Pointers
    FeatureDescription
    Memory EfficiencyPointers reduce memory usage by directly accessing variables without duplicating them.
    Dynamic AllocationMemory can be dynamically allocated and managed via pointers.
    File HandlingPointers are used for efficient file handling and buffer management.
    ‘this’ Pointer in C++The this pointer is implicitly passed to non-static member functions to access the current object instance.
    References in C++

    References provide an alias for another variable. A reference allows a function to operate on the original variable rather than a copy.

    Syntax:

    datatype &ref_var = original_var;

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        int y = 25;
    
        // Create a reference to y
        int& myref = y;
    
        // Modify y
        y = 40;
    
        cout << "Value of y: " << y << endl;
        cout << "Value of myref after modifying y: " << myref << endl;
    
        return 0;
    }

    Output:

    Value of y: 40
    Value of myref after modifying y: 40
    Pointers vs. References

    Both pointers and references can modify variables in other functions and avoid unnecessary copying of large objects. However, they have key differences:

    FeaturePointersReferences
    Syntaxint *ptr;int& ref = var;
    NullabilityCan be set to nullptr.Must always refer to an existing object.
    ReassignmentCan be reassigned to point to another variable.Cannot be reassigned after initialization.
    Memory AddressCan store the address of a variable.Acts as an alias for a variable.
    DereferencingRequires explicit dereferencing (*ptr).Automatically dereferenced.

    C++ Pointers

    Pointers are variables that hold the address of another variable. They enable operations such as call-by-reference and are useful for managing dynamic memory and creating complex data structures.

    Syntax

    datatype *pointer_name;

    Example of Pointers in C++

    The following program demonstrates basic pointer usage:

    #include <iostream>
    using namespace std;
    
    int main() {
        int num = 42;  // Declare an integer variable
        int* ptr;      // Declare a pointer
    
        ptr = &num;    // Store the address of 'num' in 'ptr'
    
        // Output the value of 'num'
        cout << "Value of num: " << num << endl;
    
        // Output the memory address stored in 'ptr'
        cout << "Address stored in ptr: " << ptr << endl;
    
        // Output the value of 'num' by dereferencing 'ptr'
        cout << "Value of num using ptr: " << *ptr << endl;
    
        return 0;
    }

    Output:

    Value of num: 42
    Address stored in ptr: 0x7ffd3c942a24
    Value of num using ptr: 42

    Applications of Pointers in C++

    1. Passing arguments by reference: Enables modification of variables inside a function without copying data.
    2. Accessing array elements: Internally, arrays are accessed using pointers.
    Returning multiple values: Pointers can be used to return more than one value from a function.
    3. Dynamic memory allocation: Pointers allow for dynamic allocation of memory, which remains allocated until explicitly released.
    4. Implementing data structures: Pointers are used in data structures like linked lists, trees, and graphs.
    5. System-level programming: Pointers enable direct manipulation of memory addresses.

    Features and Uses of Pointers
    • Efficient memory usage and dynamic allocation.
    • Essential for file handling operations.
    • Enable direct access to memory locations, improving performance in certain cases.
    The ‘this’ Pointer in C++

    In C++, the this pointer is automatically passed to non-static member functions, allowing access to the calling object’s members. Static member functions, however, do not have access to this since they can be called without an object.

    Declaration:

    this->x = x;
    References in C++

    A reference is simply another name for an existing variable, created using the & operator in a declaration. It allows for more intuitive manipulation of variables without needing explicit pointers.

    There are three ways to pass arguments to functions in C++:

    1. Call-by-value
    2. Call-by-reference using pointers
    3. Call-by-reference using references

    Example of References in C++

    #include <iostream>
    using namespace std;
    
    int main() {
        int value = 50;  // Declare an integer
    
        // Create a reference to 'value'
        int& ref = value;
    
        // Modify the value using the reference
        ref = 100;
    
        // Output the value of 'value' and 'ref'
        cout << "Value of 'value' is: " << value << endl;
        cout << "Value of 'ref' is: " << ref << endl;
    
        return 0;
    }

    Output:

    Value of 'value' is: 100
    Value of 'ref' is: 100
    Pointers and Arrays

    Arrays and pointers are closely related in C++. An array name holds the address of its first element, making it similar to a constant pointer.

    Example of Using a Pointer with an Array:

    #include <iostream>
    using namespace std;
    
    void demonstrateArrayPointer() {
        int arr[3] = { 2, 4, 6 };
        int* ptr = arr;  // Points to the first element
    
        cout << "Array elements accessed through pointer: ";
        for (int i = 0; i < 3; i++) {
            cout << *(ptr + i) << " ";
        }
        cout << endl;
    }
    
    int main() {
        demonstrateArrayPointer();
        return 0;
    }

    Output:

    Array elements accessed through pointer: 2 4 6
    Pointer Arithmetic

    Pointer arithmetic allows us to navigate arrays by incrementing and decrementing pointers. However, this should be done cautiously and only within the bounds of an array.

    Example of Pointer Arithmetic:

    #include <iostream>
    using namespace std;
    
    void pointerArithmetic() {
        int values[3] = { 1, 5, 9 };
        int* ptr = values;
    
        for (int i = 0; i < 3; i++) {
            cout << "Pointer at: " << ptr << " Value: " << *ptr << endl;
            ptr++;  // Move to the next element
        }
    }
    
    int main() {
        pointerArithmetic();
        return 0;
    }

    Output:

    Pointer at: 0x7ffe08c583e0 Value: 1
    Pointer at: 0x7ffe08c583e4 Value: 5
    Pointer at: 0x7ffe08c583e8 Value: 9
    Void Pointers

    Void pointers (type void*) are generic pointers that can point to any data type, but they must be cast to the correct type before dereferencing.

    Example of Void Pointer:

    #include <iostream>
    using namespace std;
    
    void manipulate(void* ptr, int size) {
        if (size == sizeof(char)) {
            char* charPtr = (char*)ptr;
            *charPtr = *charPtr + 1;
        } else if (size == sizeof(int)) {
            int* intPtr = (int*)ptr;
            *intPtr = *intPtr + 1;
        }
    }
    
    int main() {
        char c = 'A';
        int num = 10;
    
        manipulate(&c, sizeof(c));
        manipulate(&num, sizeof(num));
    
        cout << "New char value: " << c << endl;
        cout << "New int value: " << num << endl;
    
        return 0;
    }

    Output:

    New char value: B
    New int value: 11

    Dangling, Void , Null and Wild Pointers in C

    A dangling pointer arises when a pointer continues to reference a memory location that has been freed or deallocated. Accessing such pointers can lead to unexpected behavior and bugs.

    Common scenarios leading to dangling pointers:

     1. Deallocation of Memory: When memory allocated using malloc or similar functions is freed, the pointer becomes a dangling pointer unless explicitly set to NULL.

    Example:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int* ptr = (int*)malloc(sizeof(int));
    
        // Memory is freed, ptr is now a dangling pointer
        free(ptr);
        printf("Memory has been freed\n");
    
        // Avoid dangling pointer by setting it to NULL
        ptr = NULL;
    
        return 0;
    }

    Output:

    Memory has been freed

    2. Returning Local Variables from a Function: A pointer to a local variable becomes a dangling pointer after the function call ends because local variables are destroyed when the function returns.

    Example:

    #include <stdio.h>
    
    int* getPointer() {
        int x = 10;  // Local variable
        return &x;   // Returning address of local variable (invalid)
    }
    
    int main() {
        int* p = getPointer();
        printf("%d", *p);  // Undefined behavior, p is a dangling pointer
        return 0;
    }

    Output:

    0 (or unpredictable value)

    Fix: Declare the local variable as static to extend its lifetime.

    #include <stdio.h>
    
    int* getPointer() {
        static int x = 10;  // Static variable has global lifetime
        return &x;
    }
    
    int main() {
        int* p = getPointer();
        printf("%d", *p);  // Correct behavior
        return 0;
    }

    3. Out-of-Scope Variables: A pointer to a variable declared in a block becomes a dangling pointer once the block ends.

    Example:

    #include <stdio.h>
    
    int main() {
        int* ptr;
        {
            int a = 50;
            ptr = &a;  // Pointer becomes dangling after block ends
        }
    
        // Accessing dangling pointer leads to undefined behavior
        printf("%d\n", *ptr);
    
        return 0;
    }

    Output:

    Some undefined value
    Void Pointer in C

    A void pointer (void *) is a type of pointer that points to a memory location without specifying the data type. Since it has no type, it can point to any data but cannot be directly dereferenced without casting.

    Syntax:

    void *ptr;

    Example:

    #include <stdio.h>
    
    int main() {
        int x = 5;
        float y = 3.14;
    
        void* ptr;
    
        ptr = &x;  // Void pointer points to an integer
        printf("Integer value: %d\n", *((int*)ptr));  // Typecasting to int*
    
        ptr = &y;  // Void pointer points to a float
        printf("Float value: %.2f\n", *((float*)ptr));  // Typecasting to float*
    
        return 0;
    }

    Output:

    Integer value: 5
    Float value: 3.14
    NULL Pointer in C

    A null pointer points to “nothing” and is used to indicate that a pointer has not been assigned any valid memory address.

    Syntax:

    int *ptr = NULL;

    Example:

    #include <stdio.h>
    
    int main() {
        int* ptr = NULL;  // Pointer initialized to NULL
    
        if (ptr == NULL) {
            printf("Pointer is NULL\n");
        }
    
        return 0;
    }

    Output:

    #include <stdio.h>
    
    int main() {
        int a = 9;
        int b = 9;
    
        printf("Using Prefix Decrement on a = %d\n", --a);
        printf("Using Postfix Decrement on b = %d", b--);
        return 0;
    }
    Wild Pointer in C

    A wild pointer is a pointer that has not been initialized to a specific memory address. Accessing memory through a wild pointer leads to undefined behavior.

    Example:

    #include <stdio.h>
    
    int main() {
        int* p;  // Wild pointer (not initialized)
    
        // Attempting to dereference wild pointer can lead to crash or error
        // printf("%d", *p);  // Uncommenting this would cause undefined behavior
    
        return 0;
    }

    Understanding nullptr in C++

    nullptr is a keyword introduced in C++11 to address the issues associated with the traditional NULL. The primary problem with NULL is its definition and its behavior when passed to overloaded functions, which can lead to ambiguity.

    Consider the following C++ program that demonstrates the issue with NULL and the need for nullptr:

    // C++ program demonstrating the problem with NULL
    #include <iostream>
    using namespace std;
    
    // Function that takes an integer argument
    void fun(int N) {
        cout << "fun(int)" << endl;
    }
    
    // Overloaded function that takes a char pointer argument
    void fun(char* s) {
        cout << "fun(char*)" << endl;
    }
    
    int main() {
        // This should ideally call fun(char*),
        // but it leads to an ambiguous compiler error.
        fun(NULL);
    }

    Output:

    error: call of overloaded 'fun(NULL)' is ambiguous
    Why is this a problem?

    NULL is often defined as (void*)0, and its conversion to integral types is allowed. Therefore, when the function call fun(NULL) is made, the compiler is unable to decide between fun(int) and fun(char*), resulting in ambiguity.

    Example of the problem with NULL:

    #include <iostream>
    using namespace std;
    
    int main() {
        int x = NULL;  // This compiles but may produce a warning
        cout << "x = " << x << endl;
        return 0;
    }
    How does nullptr solve the problem?

    By using nullptr in place of NULL, the ambiguity is resolved. nullptr is a keyword that can be used in all places where NULL was expected. It is implicitly convertible to any pointer type but is not implicitly convertible to integral types, which avoids the ambiguity issue.

     Example:  if we modify the previous program to use nullptr instead of NULL, it compiles and behaves as expected:

    // C++ program using nullptr
    #include <iostream>
    using namespace std;
    
    // Function that takes an integer argument
    void fun(int N) {
        cout << "fun(int)" << endl;
    }
    
    // Overloaded function that takes a char pointer argument
    void fun(char* s) {
        cout << "fun(char*)" << endl;
    }
    
    int main() {
        // Using nullptr resolves the ambiguity
        fun(nullptr);
        return 0;
    }

    Output:

    fun(char*)
    nullptr and Integral Types:

    Unlike NULLnullptr cannot be assigned to an integral type. Attempting to do so results in a compilation error:

    #include <iostream>
    using namespace std;
    
    int main() {
        int x = nullptr;  // This will produce a compiler error
        return 0;
    }

    Output:

    Compiler Error
    Side Note: nullptr is convertible to bool

    nullptr can be compared in boolean expressions. If a pointer is assigned nullptr, it behaves like a false value when evaluated in a condition.

    #include <iostream>
    using namespace std;
    
    int main() {
        int* ptr = nullptr;
    
        // This will compile and check if ptr is null
        if (ptr) {
            cout << "Pointer is not null" << endl;
        } else {
            cout << "Pointer is null" << endl;
        }
        return 0;
    }

    Output:

    Pointer is null
    Side Note: nullptr is convertible to bool

    nullptr can be compared in boolean expressions. If a pointer is assigned nullptr, it behaves like a false value when evaluated in a condition.

    // C++ program to demonstrate comparisons with nullptr
    #include <iostream>
    using namespace std;
    
    int main() {
        // Variables of nullptr_t type
        nullptr_t np1, np2;
    
        // Comparison using <= and >= returns true
        if (np1 >= np2) {
            cout << "Comparison is valid" << endl;
        } else {
            cout << "Comparison is not valid" << endl;
        }
    
        // Initialize a pointer with nullptr
        char* x = np1;  // Same as x = nullptr
        if (x == nullptr) {
            cout << "x is null" << endl;
        } else {
            cout << "x is not null" << endl;
        }
    
        return 0;
    }

    Output:

    Comparison is valid
    x is null

    References in C++

    A reference in C++ is essentially an alias for an existing variable. Once a reference is initialized to a variable, it becomes an alternative name for that variable. To declare a reference, the & symbol is used in the variable declaration.

    Syntax:

    data_type &reference_variable = original_variable;

    Example:

    // C++ program demonstrating references
    #include <iostream>
    using namespace std;
    
    int main() {
        int x = 10;
    
        // ref is a reference to x.
        int& ref = x;
    
        // Modifying x through the reference
        ref = 20;
        cout << "x = " << x << endl;
    
        // Modifying ref will modify x as well
        x = 30;
        cout << "ref = " << ref << endl;
    
        return 0;
    }

    Output:

    x = 20
    ref = 30
    Applications of References in C++

    There are several practical uses of references in C++, some of which are:

    1. Modifying Function Parameters: Passing arguments by reference allows the function to modify the actual arguments.
    2. Avoiding Copies of Large Objects: References allow passing large objects without copying them, saving time and memory.
    3. Modifying Elements in Range-Based Loops: References can be used in for loops to modify container elements directly.
    4. Avoiding Object Copies in Loops: Using references in loops avoids copying large objects, improving efficiency.

    Modifying Passed Parameters in a Function

    When a function receives a reference to a variable, it can modify the original variable’s value.

    Example:

    // C++ program showing reference as function parameters
    #include <iostream>
    using namespace std;
    
    void swap(int& first, int& second) {
        int temp = first;
        first = second;
        second = temp;
    }
    
    int main() {
        int a = 2, b = 3;
    
        // Swapping variables using references
        swap(a, b);
    
        // Values of a and b are swapped
        cout << a << " " << b << endl;
        return 0;
    }

    Output:

    3 2
    Avoiding Copies of Large Structures

    Passing large structures without references creates unnecessary copies. Using references avoids this.

    Example:

    struct Student {
        string name;
        string address;
        int rollNo;
    };
    
    // Passing large object by reference
    void print(const Student &s) {
        cout << s.name << " " << s.address << " " << s.rollNo << endl;
    }
    Modifying Elements in Range-Based Loops

    By using references in a loop, you can modify all elements in a collection.

    Example:

    // C++ program demonstrating modifying vector elements using references
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    int main() {
        vector<int> vect {10, 20, 30, 40};
    
        // Modifying vector elements by reference
        for (int& x : vect) {
            x += 5;
        }
    
        // Printing modified elements
        for (int x : vect) {
            cout << x << " ";
        }
        cout << endl;
    
        return 0;
    }

    Output:

    15 25 35 45
    Avoiding Object Copies in Loops

    In some cases, especially when objects are large, avoiding copies in loops enhances performance.

    Example:

    // C++ program avoiding copy of objects using references
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    int main() {
        vector<string> vect {"example 1", "example 2", "example 3"};
    
        // Using reference to avoid copying
        for (const auto& str : vect) {
            cout << str << endl;
        }
    
        return 0;
    }

    Output:

    example 1
    example 2
    example 3
    Differences Between References and Pointers

    While both references and pointers allow modifying variables in other functions and avoiding unnecessary copies, they have key differences:

    Void Pointers vs. Void References: Pointers can be declared as void, but references cannot.

    int a = 10;
    void* ptr = &a;  // Valid
    // void& ref = a; // Invalid

    Multiple Levels of Indirection: Pointers can have multiple levels of indirection (e.g., pointers to pointers), whereas references only have one level of indirection.

    Reassignment: Once assigned, a reference cannot be made to reference another object. Pointers, however, can be reassigned.

    Null Values: Pointers can be NULL, but references must always refer to a valid object.

    int num = 4;
    printf("%d\n", num << 1);  // Multiplies 4 by 2, result: 8
    printf("%d\n", num >> 1);  // Divides 4 by 2, result: 2
    Advantages of Using References
    • Safer:
      Since references must be initialized, they avoid issues like wild pointers.
    • Easier to Use:
      References don’t require dereferencing to access the value, making them more intuitive to use.
    • Efficiency:
      Copy constructors and operator overloads often require references to avoid unnecessary copying.

    Example:

    #include <iostream>
    using namespace std;
    
    int& fun() {
        static int x = 10;
        return x;
    }
    
    int main() {
        fun() = 30;
        cout << fun();
        return 0;
    }

    Output:

    30

    Pointers vs References in C++

    C++ is unique in that it supports both pointers and references, unlike many other popular programming languages such as Java, Python, Ruby, and PHP, which only use references. At first glance, pointers and references seem to perform similar functions—both provide access to other variables. However, there are key differences between these two mechanisms that often cause confusion. Let’s explore these differences in detail.

    Pointers:

    A pointer is a variable that holds the memory address of another variable. To access the memory location a pointer is referring to, the dereference operator * is used.

    References:

    A reference is essentially an alias for another variable. Just like pointers, references store the address of an object, but the compiler automatically dereferences them for you.

    For example:

    int i = 3;
    
    // Pointer to variable i (stores the address of i)
    int *ptr = &i;
    
    // Reference (alias) for variable i
    int &ref = i;
    Key Differences

    Initialization:

    • A pointer can be initialized in multiple steps:
    int a = 10;
    int *p = &a;  // or
    int *p;
    p = &a;
    • A reference must be initialized when declared:
    int a = 10;
    int &r = a;  // Correct
    
    // However, the following is incorrect:
    int &r;
    r = a;  // Error: references must be initialized during declaration

    Reassignment:

    • Pointers can be reassigned to different variables:
    int a = 5;
    int b = 6;
    int *p;
    p = &a;  // p points to a
    p = &b;  // Now p points to b
    Passing by Reference

    Passing by reference allows the function to modify the original variables without creating copies. It works by using reference variables, which act as aliases for the original variables. This method is referred to as “call by reference.”

    Example of passing by reference:

    // C++ program to swap two numbers using pass by reference
    #include <iostream>
    using namespace std;
    
    void swap(int &x, int &y) {
        int temp = x;
        x = y;
        y = temp;
    }
    
    int main() {
        int a = 10, b = 20;
        cout << "Before Swap: a = " << a << ", b = " << b << endl;
    
        swap(a, b);
    
        cout << "After Swap using pass by reference: a = " << a << ", b = " << b << endl;
        return 0;
    }
    Comparison: Pass by Pointer vs. Pass by Reference
    FeaturePass by PointerPass by Reference
    Passing ArgumentsThe memory address of the argument is passed to the function.The argument itself is passed, not its memory address.
    Accessing ValuesValues are accessed using the dereference operator *.Values can be accessed directly using the reference.
    ReassignmentThe pointer can be reassigned to point to different memory.References cannot be reassigned after initialization.
    NULL ValuesPointers can be NULL or hold invalid addresses.References cannot be NULL and must always refer to a valid object.
    UsageUseful when you need pointer arithmetic or NULL handling.Typically used when a function does not need to change what it refers to.
    Differences Between Reference Variables and Pointer Variables
    • A reference is essentially another name for an object, and must always refer to an actual object. References cannot be NULL, making them safer than pointers.
    • Pointers can be reassigned, while references must be initialized when declared and cannot be changed afterward.
    • A pointer holds the memory address of a variable, while a reference shares the memory address with the object it refers to.
    • Pointer arithmetic is possible (e.g., incrementing a pointer to traverse arrays), but references do not support this.
    • Pointers use the -> operator to access class members, whereas references use the . operator.
    • You need to explicitly dereference a pointer with the * operator to access its value, while references do not require dereferencing.

    Updated Example Demonstrating Differences

    // C++ program to demonstrate differences between pointers and references
    #include <iostream>
    using namespace std;
    
    struct Example {
        int value;
    };
    
    int main() {
        int x = 10;
        int y = 20;
        Example obj;
    
        // Using a pointer
        int* ptr = &x;
        ptr = &y; // Pointer reassignment is allowed
    
        // Using a reference
        int& ref = x;
        // ref = &y; // Compile Error: References can't be reassigned
    
        ref = y;  // Changes the value of 'x' to 20
    
        ptr = nullptr; // Pointer can be assigned to NULL
        // ref = nullptr; // Compile Error: References can't be NULL
    
        cout << "Pointer address: " << ptr << endl;  // Pointer points to memory address
        cout << "Reference address: " << &ref << endl;  // Reference shares the same address as 'x'
    
        Example* ptrObj = &obj;
        Example& refObj = obj;
    
        ptrObj->value = 42;  // Accessing member using pointer
        refObj.value = 42;   // Accessing member using reference
    
        return 0;
    }
    Which to Use: Pass by Pointer or Pass by Reference?
    • Use references when you don’t need to reseat the reference and want safer, cleaner code. For example, references are ideal for function parameters and return values when you know the object being passed will never be NULL.
    • Use pointers when you need to handle NULL values or perform pointer arithmetic. Pointers are essential for more complex data structures like linked lists and trees,
  • Arrays

    Arrays in C++

    In C++, an array is a collection of elements of the same data type, stored in contiguous memory locations. It allows you to store multiple values in a single variable and access them using an index. The size of the array must be specified at the time of declaration, and it cannot be changed later

    Declaring and Initializing Arrays

    Here’s how to declare and initialize arrays in C++:

    1. Single-dimensional Array: This is the simplest form of an array.

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        // Declare and initialize a single-dimensional array
        int numbers[5] = {1, 2, 3, 4, 5};
    
        // Access and print array elements
        for (int i = 0; i < 5; i++) {
            cout << "Element at index " << i << ": " << numbers[i] << endl;
        }
    
        return 0;
    }

    Output:

    Element at index 0: 1
    Element at index 1: 2
    Element at index 2: 3
    Element at index 3: 4
    Element at index 4: 5

    2. Multi-dimensional Array: Arrays can have more than one dimension. A two-dimensional array is commonly used to represent matrices.

    #include <iostream>
    using namespace std;
    
    int main() {
        // Declare and initialize a two-dimensional array
        int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
    
        // Access and print array elements
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 3; j++) {
                cout << "Element at [" << i << "][" << j << "]: " << matrix[i][j] << endl;
            }
        }
    
        return 0;
    }

    Output:

    Element at [0][0]: 1
    Element at [0][1]: 2
    Element at [0][2]: 3
    Element at [1][0]: 4
    Element at [1][1]: 5
    Element at [1][2]: 6
    Important Points about Arrays in C++:
    • Indexing: Array indexing starts from 0. The first element is accessed using index 0, the second with index 1, and so on.
    • Size Limitation: The size of the array must be a constant expression and cannot be modified after declaration.
    • Memory Management: The size of the array affects memory usage. For large data sets, dynamic arrays (using new keyword) or std::vector from the STL (Standard Template Library) can be used for flexibility.
    Dynamic Arrays

    If you need to create an array whose size can be determined at runtime, you can use dynamic memory allocation.

    Example:

    #include <iostream>
    using namespace std;
    
    int main() {
        int size;
        cout << "Enter the size of the array: ";
        cin >> size;
    
        // Dynamically allocate an array
        int* dynamicArray = new int[size];
    
        // Initialize array elements
        for (int i = 0; i < size; i++) {
            dynamicArray[i] = i * 2;  // Assigning values
        }
    
        // Print the array elements
        for (int i = 0; i < size; i++) {
            cout << "Element at index " << i << ": " << dynamicArray[i] << endl;
        }
    
        // Free the allocated memory
        delete[] dynamicArray;
    
        return 0;
    }

    Output:

    Enter the size of the array: 5
    Element at index 0: 0
    Element at index 1: 2
    Element at index 2: 4
    Element at index 3: 6
    Element at index 4: 8

    But we can have array of void pointers and function pointers. The below program works fine.

    int main()
    {
        void *arr[200];
    }

    How to print size of array parameter in C++?

    In C++, calculating the size of an array can be misleading when done within functions. Here’s an example that illustrates this issue.

    // A C++ program demonstrating the incorrect method
    // of computing the size of an array passed to a function
    #include <iostream>
    using namespace std;
    
    void checkSize(int arr[])
    {
        cout << sizeof(arr) << endl;
    }
    
    int main()
    {
        int a[10];
        cout << sizeof(a) << " ";
        checkSize(a);
        return 0;
    }

    Output:

    40 8

    Time Complexity: O(1)
    Auxiliary Space: O(n) (where n is the array size)

    In the output, the first cout statement in main() displays 40, while the second in checkSize() shows 8. The difference arises because arrays are passed as pointers to functions. Thus, both checkSize(int arr[]) and checkSize(int *arr) are equivalent. The output inside checkSize() reflects the size of a pointer, not the array.

    // A C++ program demonstrating how to use a reference
    // to find the size of an array
    #include <iostream>
    using namespace std;
    
    void checkSize(int (&arr)[10])
    {
        cout << sizeof(arr) << endl;
    }
    
    int main()
    {
        int a[10];
        cout << sizeof(a) << " ";
        checkSize(a);
        return 0;
    }

    Output:

    40 40

    Time Complexity: O(1)
    Space Complexity: O(n) (where n is the array size)

    Making a Generic Function

    Example:

    // A C++ program to demonstrate how to use templates
    // and references to find the size of an array of any type
    #include <iostream>
    using namespace std;
    
    template <typename T, size_t n>
    void checkSize(T (&arr)[n])
    {
        cout << sizeof(T) * n << endl;
    }
    
    int main()
    {
        int a[10];
        cout << sizeof(a) << " ";
        checkSize(a);
    
        float f[20];
        cout << sizeof(f) << " ";
        checkSize(f);
        return 0;
    }

    Output:

    40 40
    80 80

    Time Complexity: O(1)
    Space Complexity: O(n) (where n is the array size)

    Determining the Size of a Dynamically Allocated Array

    The next step involves printing the size of a dynamically allocated array. Here’s a hint to get you started:

    #include <iostream>
    #include <cstdlib>
    using namespace std;
    
    int main()
    {
        int *arr = (int*)malloc(sizeof(int) * 20);
        // Note: You need to keep track of the size since sizeof will not work here
        return 0;
    }

    In this case, since malloc returns a pointer, you cannot directly determine the size of the allocated memory using sizeof.

    What is Array Decay in C++?

    Array decay refers to the loss of both type and dimensions of an array, which typically occurs when an array is passed to a function by value or pointer. In this process, the first address of the array is sent, treating it as a pointer. As a result, the size reported is that of the pointer, not the actual size of the array in memory.

    // C++ code to demonstrate array decay
    #include <iostream>
    using namespace std;
    
    // Function demonstrating Array Decay
    // by passing array as a pointer
    void demonstrateArrayDecay(int* p)
    {
        // Displaying the size of the pointer
        cout << "Size of the array when passed by value: ";
        cout << sizeof(p) << endl;
    }
    
    int main()
    {
        int a[7] = {1, 2, 3, 4, 5, 6, 7};
    
        // Displaying the original size of the array
        cout << "Actual size of array is: ";
        cout << sizeof(a) << endl;
    
        // Passing the array to the function
        demonstrateArrayDecay(a);
    
        return 0;
    }

    Output:

    Actual size of array is: 28
    Size of the array when passed by value: 8
    How to Prevent Array Decay?

    One common approach to address array decay is to pass the size of the array as a separate parameter and to avoid using sizeof with array parameters. Another effective method is to pass the array by reference, which prevents the conversion of the array into a pointer and thus avoids decay.

    // C++ code to demonstrate prevention of array decay
    #include <iostream>
    using namespace std;
    
    // Function that prevents array decay
    // by passing the array by reference
    void preventArrayDecay(int (&p)[7])
    {
        // Displaying the size of the array
        cout << "Size of the array when passed by reference: ";
        cout << sizeof(p) << endl;
    }
    
    int main()
    {
        int a[7] = {1, 2, 3, 4, 5, 6, 7};
    
        // Displaying the original size of the array
        cout << "Actual size of array is: ";
        cout << sizeof(a) << endl;
    
        // Calling the function by reference
        preventArrayDecay(a);
    
        return 0;
    }

    Output:

    Actual size of array is: 28
    Size of the array when passed by reference: 28

    Preserving array size by passing a pointer to the array:

    Another way to maintain the size is by passing a pointer to the array, which includes an extra dimension compared to the original array.

    #include <iostream>
    using namespace std;
    
    // Function to demonstrate Array Decay with a pointer
    void pDecay(int (*p)[7]) {
        // Display the size of the pointer to the array
        cout << "Size of array when passing a pointer: ";
        cout << sizeof(p) << endl;
    }
    
    int main() {
        int a[7] = {1, 2, 3, 4, 5, 6, 7};
    
        // Display original size of the array
        cout << "Original size of array: ";
        cout << sizeof(a) << endl;
    
        // Passing a pointer to the array
        pDecay(&a);
    
        return 0;
    }

    Output:

    Original size of array: 28
    Size of array when passing a pointer: 8
  • Functions

    Functions in C++

    A function is a set of statements designed to perform a specific task and is executed only when called. Functions allow you to modularize code, reducing redundancy and improving clarity. By encapsulating common tasks within functions, we avoid repeating the same logic multiple times, making the code more manageable and easier to maintain.

    Syntax:

    void function(int size)
    {
        int array[size];
        // code to manipulate the array
    }

    Example:

    // C++ Program to demonstrate the working of a function
    #include <iostream>
    using namespace std;
    
    // Function that takes two integers as parameters
    // and returns the larger of the two
    int max(int x, int y) {
        return (x > y) ? x : y;
    }
    
    int main() {
        int a = 15, b = 25;
    
        // Calling the max function
        int result = max(a, b);
    
        cout << "The maximum value is " << result;
        return 0;
    }

    Output:

    The maximum value is 25
    Why Do We Need Functions?
    • Code Reusability: Avoid redundancy by reusing functions across the program.
    • Modularity: Code becomes more organized and easier to understand when divided into functions.
    • Abstraction: Using functions hides internal details, making complex tasks easier to use through simple function calls.
    Function Declaration

    A function declaration tells the compiler about the function’s name, parameters, and return type. Here is an example:

    // Function declarations
    int add(int, int);         // Function with two integer parameters
    float subtract(float, int); // Function with float and int parameters
    char* reverseString(char*); // Function that returns a char pointer
    Types of Functions

    1. User-defined Functions: Functions created by the user to perform specific tasks.
    2. Library Functions: Predefined functions in C++ that you can use directly, such as sqrt(), strlen(), etc

    Passing Parameters to Functions

    There are two common ways to pass parameters to functions:

    • Pass by Value: A copy of the variable is passed. Modifications inside the function do not affect the original variable.
    • Pass by Reference: The function receives the memory address of the variable, and changes made inside the function affect the original variable.
    Function Example: Pass by Value
    #include <iostream>
    using namespace std;
    
    void modify(int x) {
        x = 50;  // Changes won't affect the original variable
    }
    
    int main() {
        int num = 10;
        modify(num);
        cout << "Value of num: " << num;  // Outputs 10
        return 0;
    }

    Function Example: Pass by Reference

    #include <iostream>
    using namespace std;
    
    void modify(int &x) {
        x = 50;  // Changes will affect the original variable
    }
    
    int main() {
        int num = 10;
        modify(num);
        cout << "Value of num: " << num;  // Outputs 50
        return 0;
    }

    Function Returning a String example: 

    #include <iostream>
    #include <string>
    
    std::string greet() {
        return "Hello, C++!";
    }
    
    int main() {
        std::string message = greet();
        cout << message;
        return 0;
    }

    Output:

    Hello, C++!

    Function Returning a Pointer example:

    #include <iostream>
    using namespace std;
    
    int* createArray(int size) {
        int* arr = new int[size];  // Dynamically allocate memory
        for (int i = 0; i < size; ++i) {
            arr[i] = i * 2;
        }
        return arr;
    }
    
    int main() {
        int* arr = createArray(5);
        for (int i = 0; i < 5; ++i) {
            cout << arr[i] << " ";  // Outputs: 0 2 4 6 8
        }
        delete[] arr;  // Free allocated memory
        return 0;
    }

    Output:

    0 2 4 6 8

    Callback Function Example:

    #include <iostream>
    using namespace std;
    
    typedef void (*Callback)();  // Define a callback function type
    
    void action(Callback callback) {
        cout << "Performing action...\n";
        callback();  // Call the passed callback function
    }
    
    void myCallback() {
        cout << "Callback function executed!";
    }
    
    int main() {
        action(myCallback);
        return 0;
    }

    Output:

    Performing action...
    Callback function executed!
    Differences Between Call by Value and Call by Reference
    Call by ValueCall by Reference
    A copy of the value is passed.The reference (memory address) is passed.
    Changes do not affect the original.Changes affect the original.
    Actual and formal parameters are stored at different locations.Actual and formal parameters share the same location.

    return statement

    The return statement in C++ transfers control back to the function that invoked the current function. Once the return statement is executed, the function ends and any subsequent code in that function is ignored. For non-void functions, a return statement must return a value, while in void functions, the return statement can be omitted or used without returning any value.

    Syntax:

    return [expression];
    Functions Without a Return Value (void Functions)

    In C++, if a function is defined with a void return type, it does not return any value. The return statement is optional and can be used to exit the function early, but it should not return any value.

    Example:

    // C++ Program demonstrating a void function without a return statement
    #include <iostream>
    using namespace std;
    
    void displayMessage() {
        cout << "Hello, World!";
    }
    
    int main() {
        displayMessage();  // Function call
        return 0;
    }

    Output:

    Hello, World!
    Functions With return in a Void Function:

    In a void function, the return statement can be used simply to exit the function early.

    Example:

    // C++ Program using return in a void function
    #include <iostream>
    using namespace std;
    
    void checkValue(int x) {
        if (x < 0) {
            return;  // Exit the function early if x is negative
        }
        cout << "The value is non-negative: " << x << endl;
    }
    
    int main() {
        checkValue(-5);  // Function call
        checkValue(10);  // Function call
        return 0;
    }

    Output

    The value is non-negative: 10

    Default Arguments in C++

    In C++, a default argument is a value that is automatically provided by the compiler when a calling function does not specify a value for the argument. If a value is passed for the argument, it overrides the default value.

    Example : Default Arguments in a Function

    The following C++ example demonstrates the use of default arguments. We don’t need to write multiple functions for different numbers of arguments; one function can handle various cases by using default values for some parameters.

    // C++ Program to demonstrate Default Arguments
    #include <iostream>
    using namespace std;
    
    // Function with default arguments
    // It can be called with 2, 3, or 4 arguments.
    int sum(int x, int y, int z = 0, int w = 0) {
        return (x + y + z + w);
    }
    
    // Driver Code
    int main() {
        // Case 1: Passing two arguments
        cout << sum(10, 15) << endl;
    
        // Case 2: Passing three arguments
        cout << sum(10, 15, 25) << endl;
    
        // Case 3: Passing four arguments
        cout << sum(10, 15, 25, 30) << endl;
    
        return 0;
    }

    Output:

    25
    50
    80
    Key Points to Remember:

    1. Inlining is a suggestion, not a directive. The compiler may choose to ignore the inline request.

    2. The compiler may not inline the function if it contains:

    • Loops (for, while, or do-while)
    • Static variables
    • Recursive calls
    • A return type other than void and lacks a return statement
    • Complex constructs such as switch or goto
    Why Use Inline Functions?

    When a function is called, there’s an overhead for:

    1. Storing the return address
    2. Passing function arguments
    3. Transferring control to the function’s code
    4. Returning the result from the function

    For small and frequently called functions, this overhead might exceed the time it takes to execute the function itself. By inlining such functions, you avoid the cost of the function call and may improve performance. Inline functions are beneficial for small, simple functions where function call overhead might be significant relative to execution time.

    Advantages of Inline Functions:
    • Reduced Function Call Overhead: No need to jump to a new memory address for function execution.
    • No Stack Push/Pop Overhead: Parameters don’t need to be pushed or popped from the stack.
    • No Return Overhead: Control doesn’t need to be returned to the caller.
    • Potential for Context-Specific Optimization: The compiler can perform optimizations that wouldn’t be possible with regular function calls, as the function’s code is available at the point of use.
    • Smaller Code Size for Embedded Systems: Inlining small functions can sometimes produce more compact code in certain situations (e.g., embedded systems).

    Example:

    // Inline function to calculate the square of a number
    #include <iostream>
    using namespace std;
    
    inline int square(int x) {
        return x * x;
    }
    
    int main() {
        int num = 5;
    
        // Using the inline function
        cout << "Square of " << num << " is " << square(num) << endl;
    
        // Another usage of the inline function
        cout << "Square of 10 is " << square(10) << endl;
    
        return 0;
    }

    Output:

    Square of 5 is 25
    Square of 10 is 100

    Lambda Expressions

    C++11 introduced lambda expressions, which allow you to write inline functions for short snippets of code without giving them a name. These functions are useful for situations where you won’t reuse the code, and therefore don’t need to define a separate function.

    Syntax:

    // C++ program demonstrating lambda expressions
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <numeric> // for accumulate
    
    using namespace std;
    
    // Function to print vector
    void printVector(vector<int> v)
    {
        // Lambda expression to print vector elements
        for_each(v.begin(), v.end(), [](int i)
        {
            cout << i << " ";
        });
        cout << endl;
    }
    
    int main()
    {
        vector<int> v {4, 1, 3, 5, 2, 3, 1, 7};
    
        printVector(v);
    
        // Finding the first number greater than 4
        auto p = find_if(v.begin(), v.end(), [](int i)
        {
            return i > 4;
        });
        cout << "First number greater than 4 is : " << *p << endl;
    
        // Sorting the vector in descending order
        sort(v.begin(), v.end(), [](int a, int b) -> bool
        {
            return a > b;
        });
    
        printVector(v);
    
        // Counting numbers greater than or equal to 5
        int count_5 = count_if(v.begin(), v.end(), [](int a)
        {
            return a >= 5;
        });
        cout << "The number of elements greater than or equal to 5 is : "
             << count_5 << endl;
    
        // Removing duplicate elements
        auto unique_end = unique(v.begin(), v.end(), [](int a, int b)
        {
            return a == b;
        });
        v.resize(distance(v.begin(), unique_end));
        printVector(v);
    
        // Calculating factorial using accumulate
        int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int factorial = accumulate(arr, arr + 10, 1, [](int i, int j)
        {
            return i * j;
        });
        cout << "Factorial of 10 is : " << factorial << endl;
    
        // Storing a lambda in a variable to compute square
        auto square = [](int i)
        {
            return i * i;
        };
        cout << "Square of 5 is : " << square(5) << endl;
    
        return 0;
    }

    Output:

    4 1 3 5 2 3 1 7
    First number greater than 4 is : 5
    7 5 4 3 3 2 1 1
    The number of elements greater than or equal to 5 is : 2
    7 5 4 3 2 1
    Factorial of 10 is : 3628800
    Square of 5 is : 25
  • Control Structures in C

    Control structures (also called decision control statements) let a C program choose what to do next based on conditions, and repeat actions using loops. They are essential for building real programs that respond to user input, data, and changing situations.

    C control flow mainly includes:

    • Decision making: if, if-else, nested if, else-if ladder, switch, ternary ?:
    • Loops: for, while, do-while
    • Jump statements: break, continue, goto, return

    1) Why Do We Need Conditional Statements?

    In programming, you often need logic like:

    • If marks ≥ 40 → pass, else fail
    • If age ≥ 18 → eligible, else not
    • If choice is 1/2/3 → do different actions

    Conditional statements help you implement that decision-making cleanly.


    2) Decision Making in C

    2.1 if Statement

    Executes a block only if the condition is true.

    Syntax

    if (condition) {
        // executes if condition is true
    }
    

    Example

    #include <stdio.h>
    
    int main() {
        int x = 10;
    
        if (x > 5) {
            printf("x is greater than 5\n");
        }
    
        printf("Program continues...\n");
        return 0;
    }
    

    2.2 if-else Statement

    Provides two paths:

    • if condition is true → run if block
    • else → run else block

    Syntax

    if (condition) {
        // true block
    } else {
        // false block
    }
    

    Example (Even/Odd)

    #include <stdio.h>
    
    int main() {
        int num = 4956;
    
        if (num % 2 == 0) {
            printf("%d is Even\n", num);
        } else {
            printf("%d is Odd\n", num);
        }
    
        return 0;
    }
    

    2.3 Nested if / Nested if-else

    An if (or if-else) inside another if block for more complex decisions.

    Syntax

    if (condition1) {
        if (condition2) {
            // both true
        } else {
            // condition1 true, condition2 false
        }
    } else {
        // condition1 false
    }
    

    Example

    #include <stdio.h>
    
    int main() {
        int x = 15;
    
        if (x > 10) {
            if (x < 20) {
                printf("x is between 10 and 20\n");
            } else {
                printf("x is 20 or more\n");
            }
        } else {
            printf("x is 10 or less\n");
        }
    
        return 0;
    }
    

    2.4 if - else if - else Ladder

    Used when you have multiple conditions to test in order.
    The first true condition runs, and the rest are skipped.

    Syntax

    if (condition1) {
        // ...
    } else if (condition2) {
        // ...
    } else if (condition3) {
        // ...
    } else {
        // none matched
    }
    

    Example (Positive/Negative/Zero)

    #include <stdio.h>
    
    int main() {
        int number = 0;
    
        if (number > 0) {
            printf("The number is Positive\n");
        } else if (number < 0) {
            printf("The number is Negative\n");
        } else {
            printf("The number is Zero\n");
        }
    
        return 0;
    }
    

    Tip: Ladder is different from nested if because it checks conditions sequentially at the same level.


    2.5 switch Statement

    Best when you compare one expression against many constant values.

    Syntax

    switch (expression) {
        case value1:
            // ...
            break;
        case value2:
            // ...
            break;
        default:
            // ...
    }
    

    Example

    #include <stdio.h>
    
    int main() {
        int day = 3;
    
        switch (day) {
            case 1: printf("Sunday"); break;
            case 2: printf("Monday"); break;
            case 3: printf("Tuesday"); break;
            default: printf("Invalid day");
        }
    
        return 0;
    }
    

    Important: If you forget break, C will fall through into the next case.


    2.6 Ternary Operator ?:

    Short form of if-else when assigning/returning a value.

    Syntax

    condition ? expr_if_true : expr_if_false;
    

    Example

    #include <stdio.h>
    
    int main() {
        int x = 5;
        int result = (x > 0) ? 1 : 0;
        printf("Result: %d\n", result);
        return 0;
    }
    

    3) Looping Control Structures in C

    Loops help repeat tasks efficiently.

    3.1 for Loop

    Best when number of iterations is known.

    for (int i = 1; i <= 5; i++) {
        printf("HelloWorld\n");
    }
    

    3.2 while Loop

    Runs while condition remains true (entry-controlled loop).

    int i = 0;
    while (i < 5) {
        printf("Hello, World\n");
        i++;
    }
    

    3.3 do-while Loop

    Runs at least once because the condition is checked after the body.

    #include <stdbool.h>
    #include <stdio.h>
    
    int main() {
        bool condition = false;
    
        do {
            printf("This is the loop body.\n");
        } while (condition);
    
        return 0;
    }
    

    4) Jump Statements (Flow Control Helpers)

    • break → exits loop/switch immediately
    • continue → skips to next loop iteration
    • return → exits the function
    • goto → jumps to a labeled statement (rarely recommended)

    Example (break)

    for (int i = 1; i <= 10; i++) {
        if (i == 6) break;
        printf("%d ", i);
    }
    

    Output: 1 2 3 4 5


    Summary

    • Use if for simple decisions
    • Use if-else when there are two choices
    • Use nested if for multi-level checks
    • Use else-if ladder for multiple conditions in order
    • Use switch for clean multi-option branching
    • Use loops (for/while/do-while) for repetition
    • Use jump statements (break/continue/return) to control execution flow

  • Operators

    Operators in C++

    An operator is a symbol that acts on a value to perform specific mathematical or logical operations, forming the core of any programming language. In C++, a range of built-in operators is available to perform essential tasks and calculations.

    Example:

    c = a + b;

    In this example, the + symbol is the addition operator, while a and b are the operands. The addition operator tells the compiler to sum the values of a and b.

    Types of Operators in C

    The C language provides a wide array of operators, categorized into six types:

    1. Arithmetic Operators
    2. Relational Operators
    3. Logical Operators
    4. Bitwise Operators
    5. Assignment Operators
    6. Miscellaneous Operators

    1. Arithmetic Operators in C : Arithmetic operators allow us to perform standard mathematical operations. In C, there are 9 arithmetic operators:

    SymbolOperatorDescriptionSyntax
    +AdditionAdds two numeric values.a + b
    -SubtractionSubtracts the right operand from the left.a - b
    *MultiplicationMultiplies two numeric values.a * b
    /DivisionDivides two numeric values.a / b
    %ModulusGives the remainder of a division.a % b
    +Unary PlusSpecifies positive values.+a
    -Unary MinusReverses the sign of a value.-a
    ++IncrementIncreases the value of a variable by 1.a++
    --DecrementDecreases the value of a variable by 1.a--

    Example:

    #include <stdio.h>
    
    int main() {
        int a = 25, b = 5;
    
        printf("a + b = %d\n", a + b);
        printf("a - b = %d\n", a - b);
        printf("a * b = %d\n", a * b);
        printf("a / b = %d\n", a / b);
        printf("a %% b = %d\n", a % b);
        printf("+a = %d\n", +a);
        printf("-a = %d\n", -a);
        printf("a++ = %d\n", a++);
        printf("a-- = %d\n", a--);
    
        return 0;
    }

    Output:

    a + b = 30
    a - b = 20
    a * b = 125
    a / b = 5
    a % b = 0
    +a = 25
    -a = -25
    a++ = 25
    a-- = 26

    2. Relational Operators in C : Relational operators are used to compare two operands. These operators return a boolean value (true or false), depending on the comparison result.

    SymbolOperatorDescriptionSyntax
    <Less thanReturns true if the left operand is less than the right.a < b
    >Greater thanReturns true if the left operand is greater than the right.a > b
    <=Less than or equal toReturns true if the left operand is less than or equal to the right.a <= b
    >=Greater than or equal toReturns true if the left operand is greater than or equal to the right.a >= b
    ==Equal toReturns true if both operands are equal.a == b
    !=Not equal toReturns true if the operands are not equal.a != b

    Example:

    #include <stdio.h>
    
    int main() {
        int a = 25, b = 5;
    
        printf("a < b  : %d\n", a < b);
        printf("a > b  : %d\n", a > b);
        printf("a <= b: %d\n", a <= b);
        printf("a >= b: %d\n", a >= b);
        printf("a == b: %d\n", a == b);
        printf("a != b : %d\n", a != b);
    
        return 0;
    }

    Output:

    a < b  : 0
    a > b  : 1
    a <= b: 0
    a >= b: 1
    a == b: 0
    a != b : 1

    3. Logical Operators in C : Logical operators are used to combine multiple conditions. They evaluate to a boolean value (true or false).

    SymbolOperatorDescriptionSyntax
    &&Logical ANDReturns true if both operands are true.a && b
    ` `Logical OR
    !Logical NOTReturns true if the operand is false.!a

    Example:

    #include <stdio.h>
    
    int main() {
        int a = 25, b = 5;
    
        printf("a && b : %d\n", a && b);
        printf("a || b : %d\n", a || b);
        printf("!a: %d\n", !a);
    
        return 0;
    }

    Output:

    a && b : 1
    a || b : 1
    !a: 0

    C++ sizeof Operator

    The sizeof Operator in C++

    The sizeof operator is a unary operator used during compilation to determine the size of variables, data types, constants, classes, structures, and unions in bytes.

    Syntax:

    sizeof(data_type)

    Example 1:

    The following program shows how to use sizeof to find the size of various data types.

    #include <iostream>
    using namespace std;
    
    int main()
    {
        cout << "Bytes taken up by short is " << sizeof(short) << endl;
        cout << "Bytes taken up by long long is " << sizeof(long long) << endl;
        cout << "Bytes taken up by bool is " << sizeof(bool) << endl;
        cout << "Bytes taken up by wchar_t is " << sizeof(wchar_t) << endl;
        return 0;
    }

    Output:

    Bytes taken up by short is 2
    Bytes taken up by long long is 8
    Bytes taken up by bool is 1
    Bytes taken up by wchar_t is 4

    Example 2:

    This example demonstrates the size of variables of different data types using the sizeof operator.

    #include <iostream>
    using namespace std;
    
    int main()
    {
        double pi;
        bool flag;
        char grade;
    
        cout << "Bytes taken up by pi is " << sizeof(pi) << endl;
        cout << "Bytes taken up by flag is " << sizeof(flag) << endl;
        cout << "Bytes taken up by grade is " << sizeof(grade) << endl;
        return 0;
    }

    Output:

    Bytes taken up by pi is 8
    Bytes taken up by flag is 1
    Bytes taken up by grade is 1

    Example 3:

    This example demonstrates using sizeof to find the size of expressions.

    #include <stdio.h>
    
    int main() {
        int result;
    
        // Expression with multiple operators
        result = 10 * 5 + 8 / 2;
    
        printf("Result = %d\n", result);
    
        return 0;
    }

    Output:

    Bytes taken up by (m + n) is 4
    Bytes taken up by (m + x) is 8
    Bytes taken up by (n + x) is 8

    Output:

    Bytes taken up by (m + n) is 4
    Bytes taken up by (m + x) is 8
    Bytes taken up by (n + x) is 8

    Example 4:

    This example uses sizeof to calculate the size of an array.

    #include <iostream>
    using namespace std;
    
    int main()
    {
        double values[] = {2.3, 4.6, 5.1, 8.9, 10.0};
        int length = sizeof(values) / sizeof(values[0]);
        cout << "Length of the array is " << length << endl;
        return 0;
    }

    Output:

    Length of the array is 5

    Example 5: 

    This example shows how to use sizeof to find the size of a class.

    #include <iostream>
    using namespace std;
    
    class SampleClass
    {
        char id;
        int count;
    };
    
    int main()
    {
        SampleClass obj;
        cout << "Size of SampleClass object is: " << sizeof(obj) << " bytes" << endl;
        return 0;
    }

    Output:

    Size of SampleClass object is: 8 bytes

    Example 6: 

    This example demonstrates the size of different pointers using the sizeof operator.

    #include <iostream>
    using namespace std;
    
    int main()
    {
        int *ptrInt = new int(5);
        float *ptrFloat = new float(7.2);
        char *ptrChar = new char('A');
    
        cout << "Size of pointer ptrInt is " << sizeof(ptrInt) << endl;
        cout << "Size of *ptrInt is " << sizeof(*ptrInt) << endl;
        cout << "Size of pointer ptrFloat is " << sizeof(ptrFloat) << endl;
        cout << "Size of *ptrFloat is " << sizeof(*ptrFloat) << endl;
        cout << "Size of pointer ptrChar is " << sizeof(ptrChar) << endl;
        cout << "Size of *ptrChar is " << sizeof(*ptrChar) << endl;
    
        return 0;
    }

    Output:

    Size of pointer ptrInt is 8
    Size of *ptrInt is 4
    Size of pointer ptrFloat is 8
    Size of *ptrFloat is 4
    Size of pointer ptrChar is 8
    Size of *ptrChar is 1

    Example 7:

    This example demonstrates nesting of the sizeof operator.

    #include <iostream>
    using namespace std;
    
    int main()
    {
        int num = 3;
        double rate = 5.5;
    
        cout << "Nesting sizeof operator: sizeof(num * sizeof(rate)) is "
             << sizeof(num * sizeof(rate)) << endl;
        return 0;
    }

    Output:

    Nesting sizeof operator: sizeof(num * sizeof(rate)) is 8

    Example 8:

    This example shows how to determine the size of a structure using sizeof.

    #include <iostream>
    using namespace std;
    
    struct Record
    {
        int id;
        float score;
        char name[50];
    } record;
    
    int main()
    {
        cout << "Size of structure Record is " << sizeof(record) << endl;
        return 0;
    }

    Output:

    Size of structure Record is 56