Skip to main content

Object oriented programming

[C++] OOPs - Constructor and Destructor, Friend and Inline Functions

Hey! So, have you ever wondered how we prepare something the moment we create it?

Let’s say we’re ordering coffee from an app. The moment we hit the order button, it immediately sets the coffee size, milk type, sugar count—everything based on our preferences.

Now, what if I told you that C++ allows us to do something similar when we create objects? But how?

Huh, like making sure everything's ready when the object is made? Is that even possible?
Yes! That’s where our first concept steps in—though we won’t name it right away. Let’s say we are designing a class called Coffee.

We want the coffee to always start with a default size and sugar level when it’s made. That’s the basic idea. Let’s build a basic intuition.


Object Setup Right After Creation (Constructor)

Real-life analogy:

When we enter a hotel room, the AC is already turned on, the bed is made, and the lights are dimmed. We don’t do it manually—it just happens. That’s the magic of a constructor in C++.
#include <iostream>
using namespace std;
 
class Coffee {
public:
    // Constructor to initialize default values
    Coffee() {
        cout << "Coffee is being brewed with default size Medium and 1 sugar cube.\n";
        size = "Medium";
        sugar = 1;
    }
 
    // Display coffee details
    void showCoffee() {
        cout << "Size: " << size << ", Sugar: " << sugar << " cube(s)\n";
    }
 
private:
    string size;
    int sugar;
};
 
int main() {
    // Creating a coffee object automatically calls the constructor
    Coffee myCoffee;
    myCoffee.showCoffee();
    return 0;
}
We define a constructor that sets default values right when we create a Coffee object. As soon as we say Coffee myCoffee;, the constructor runs, initializing size and sugar. We don’t call it manually—it’s automatic!

So this is the intuition behind constructors—they help us initialize objects with sensible defaults the moment they’re created.


What Happens When the Object Dies? (Destructor)

Let’s Imagine:

Once we finish our coffee, the cup needs to be thrown away, right? Otherwise, things will get messy. So, who’s in charge of cleanup?

That’s where destructors come in. When our object finishes its purpose, destructors handle the cleanup.

#include <iostream>
using namespace std;
 
class Coffee {
public:
    Coffee() {
        cout << "Coffee is being brewed.\n";
    }
 
    ~Coffee() {
        cout << "Cup is disposed.\n";
    }
};
 
int main() {
    Coffee myCoffee; // Constructor runs here
    // Destructor runs automatically at the end of main
    return 0;
}
We create a Coffee object, and when it goes out of scope (end of main()), the destructor automatically runs and handles cleanup. We don’t need to call it manually.

So we don’t manually clean up—C++ destructors make sure that cleanup happens automatically when the object goes out of scope.


Copying a Coffee? (Copy Constructor)

Here’s a funny one:

We make a perfect cup of coffee and suddenly someone says, "I want exactly what you're having." How would we give them a clone?

In C++, we need something called a copy constructor for that!

Let’s explore:

#include <iostream>
using namespace std;
 
class Coffee {
public:
    Coffee(string s, int su) {
        size = s;
        sugar = su;
        cout << "Coffee brewed: " << size << " with " << sugar << " sugar cube(s).\n";
    }
 
    // Copy constructor
    Coffee(const Coffee &c) {
        size = c.size;
        sugar = c.sugar;
        cout << "Copying coffee order: " << size << " with " << sugar << " sugar cube(s).\n";
    }
 
    void showCoffee() {
        cout << "Size: " << size << ", Sugar: " << sugar << " cube(s)\n";
    }
 
private:
    string size;
    int sugar;
};
 
int main() {
    Coffee original("Large", 2);
    Coffee copy = original; // Copy constructor is called here
    copy.showCoffee();
    return 0;
}
We first create original with specific values, and then use Coffee copy = original; to create a duplicate. This triggers the copy constructor, which clones the size and sugar values into the new object.

So, when we create a new object from an existing one, C++ uses the copy constructor to clone it.


Let Outsiders Access Internals? (Friend Functions)

Let’s imagine

A barista friend wants to check your coffee's ingredients, but the recipe is private. We don’t want to give them full control, but just allow them to peek.

That’s what friend functions are!

#include <iostream>
using namespace std;
 
class Coffee {
    string size;
    int sugar;
 
    // Declare friend function
    friend void displayIngredients(Coffee);
 
public:
    Coffee(string s, int su) : size(s), sugar(su) {}
};
 
// Friend function can access private members
void displayIngredients(Coffee c) {
    cout << "[Friend] Size: " << c.size << ", Sugar: " << c.sugar << " cube(s)\n";
}
 
int main() {
    Coffee myCoffee("Small", 3);
    displayIngredients(myCoffee); // Friend function accessing private data
    return 0;
}
We declared displayIngredients as a friend of the class, so even though size and sugar are private, it’s allowed to access them. It’s like giving special backstage access!

So even though size and sugar are private, our friend function can access them because we gave it permission!


Tiny, Reusable Code Blocks? (Inline Functions)

Ever heard someone say: “Just paste it instead of calling it!”

Let’s say we always compare two sugar counts to get the sweeter coffee. We don’t want the overhead of calling a function every time. So, we inline it.

#include <iostream>
using namespace std;
 
// Inline function to return max sugar level
inline int MaxSugar(int a, int b) {
    return (a > b) ? a : b;
}
 
int main() {
    cout << "Sweeter coffee has: " << MaxSugar(2, 3) << " sugar cubes\n";
    return 0;
}
We define a small function MaxSugar as inline. This suggests to the compiler: instead of jumping to a function call, just replace it directly with its code for speed.

Inline means the compiler replaces the function call with actual code. It's fast but must be used wisely.


The Mystery of Virtual Constructors?

Wait, virtual what?

We know about virtual functions, but can constructors be virtual too? Let’s think about it.

We want to create a coffee of any type—Espresso, Cappuccino—but we don’t know which type at compile time. So, we need a factory-like setup.

Since constructors can't be virtual directly, we achieve this behaviour using a factory method.

#include <iostream>
using namespace std;
 
class Coffee {
public:
    virtual void prepare() = 0; // Pure virtual function
    virtual ~Coffee() {}
};
 
class Espresso : public Coffee {
public:
    void prepare() override {
        cout << "Espresso prepared!\n";
    }
};
 
class Cappuccino : public Coffee {
public:
    void prepare() override {
        cout << "Cappuccino prepared!\n";
    }
};
 
// Factory function acts like a virtual constructor
Coffee* makeCoffee(string type) {
    if (type == "espresso") return new Espresso();
    else return new Cappuccino();
}
 
int main() {
    Coffee* myCoffee = makeCoffee("espresso");
    myCoffee->prepare();
    delete myCoffee;
    return 0;
}
We use a factory function to decide which derived class to instantiate. This gives us the behaviour of virtual constructors without needing the language to support them directly.

So even though we can’t make constructors virtual, we use a smart trick with factory functions to achieve the same flexibility.


Constructor Calling Constructor? (Constructor Chaining)

Last concept of the dayyyy!

Let’s say our basic coffee constructor sets size. Another constructor wants to set size and sugar. Can’t we reuse the first one?

This is called constructor chaining.

#include <iostream>
using namespace std;
 
class Coffee {
public:
    // Base constructor
    Coffee(string s) {
        size = s;
        sugar = 0;
        cout << "[Chained] Size set to " << size << ".\n";
    }
 
    // Calls the other constructor
    Coffee(string s, int su) : Coffee(s) {
        sugar = su;
        cout << "[Chained] Sugar set to " << sugar << ".\n";
    }
 
    void show() {
        cout << "Final Coffee -> Size: " << size << ", Sugar: " << sugar << "\n";
    }
 
private:
    string size;
    int sugar;
};
 
int main() {
    Coffee c("Large", 2);
    c.show();
    return 0;
}
We define one constructor and let another call it using : Coffee(s). This avoids repetition and ensures all constructors work consistently.

One constructor can call another using an initializer list. Super handy for reducing duplicate code.


So what did we discover together?

  • Constructors help us prepare objects like coffee.
  • Destructors clean up after us.
  • Copy constructors clone existing objects.
  • Friend functions get special access to private stuff.
  • Inline functions are like quick paste-and-run snippets.
  • Virtual constructor behavior is achieved via factory functions.
  • Constructor chaining avoids repetition.

All these are powerful tools that make C++ more intuitive, flexible, and organized. By comparing them with real-life actions, we demystified them together.

Let’s keep learning this way—asking questions and exploring step-by-step!