Lesson ১০পড়তে ৯ মিনিট লাগবে

ইনহেরিটেন্স এবং পলিমরফিজম (Inheritance & Polymorphism)

ইনহেরিটেন্স (Inheritance) হলো অনেকটা একটি বংশলতিকার (family tree) মতো — যেখানে সন্তানরা তাদের পিতা-মাতার (parents) কাছ থেকে বিভিন্ন বৈশিষ্ট্যগুলো (traits) উত্তরাধিকারসূত্রে পেয়ে থাকে, কিন্তু চাইলে তারা এর পাশাপাশি নিজস্ব কিছু অনন্য (unique) বৈশিষ্ট্যও তৈরি করে নিতে পারে

বিদ্যমান জিনিসগুলোর ওপর ভিত্তি করে তৈরি (Building on What Exists)

ধরুন, আপনি ড্রয়িং (drawing) করার একটি অ্যাপ তৈরি করছেন। এর মধ্যে বিভিন্ন শেপ বা আকৃতি (shapes) রয়েছে — যেমন বৃত্ত বা সার্কেল (circles), চতুর্ভুজ বা রেক্ট্যাঙ্গেল (rectangles), কিংবা ত্রিভুজ বা ট্রায়াঙ্গেল (triangles)। এসকল শেপেরই মূলত নিজস্ব কিছু সাধারণ বৈশিষ্ট্য (shared traits) রয়েছে: যেমন নির্দিষ্ট একটি রঙ (color), একটি অবস্থান (position), বা একটি area() মেথড (method)। কিন্তু এই প্রতিটি শেপের (shape) নিজস্ব এরিয়া বা ক্ষেত্রফল (area) বের করার নিয়ম আলাদা।

এক্ষেত্রে আপনি চাইলেই এই সমস্ত শেপ ক্লাসে (shape class) ওই শেয়ার করা বা সাধারণ কোডগুলোকে কপি-পেস্ট (copy-paste) করে নিতে পারেন। অথবা এর সবচেয়ে ভালো উপায় হলো, একটি বেস ক্লাস (base class) তৈরি করা, যার নাম দেওয়া যেতে পারে Shape এবং যেখানে কোডটিকে শুধু একবারই (once) লেখা হবে, এবং এরপর ওই অন্যান্য নির্দিষ্ট শেপগুলোকে (specific shape) এর থেকে ইনহেরিট (inherit) করার সুযোগ দেওয়া হবে, যাতে করে সেগুলোতে শুধুমাত্র সেগুলোর নিজস্ব অনন্য (unique) অংশগুলোই যুক্ত করা যায়।

আর এটিকেই মূলত ইনহেরিটেন্স (inheritance) বা "ইজ-এ" ("is-a") রিলেশনশিপ বা সম্পর্ক বলা হয়। যেমন একটি Circle হলো একটি Shape। আবার একটি Rectangle-ও হলো একটি Shape। এরা মূলত একটি সাধারণ Shape-এর ইন্টারফেসগুলোকে (interface) শেয়ার করে, কিন্তু এদের প্রত্যেকেরই নিজস্ব কিছু আলাদা বাস্তবায়ন বা ইমপ্লিমেন্টেশন (implementation) থাকে।

বেসিক ইনহেরিটেন্স (Basic Inheritance) — শেপ হায়ারার্কি (Shape Hierarchy)

#include <iostream>
#include <string>
using namespace std;
class Shape {
protected:
string color;
public:
Shape(const string& c) : color(c) {}
void printColor() const {
cout << "Color: " << color << endl;
}
double area() const {
return 0.0; // ডিফল্ট (Default) — খুব একটা কাজের নয় (not very useful)
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(const string& c, double r) : Shape(c), radius(r) {}
double area() const {
return 3.14159 * radius * radius;
}
};
int main() {
Circle c("red", 5.0);
c.printColor(); // এটি Shape থেকে ইনহেরিট (Inherited) করা হয়েছে
cout << "Area: " << c.area() << endl; // এটি সার্কেলের (Circle's) নিজস্ব (own) মেথড
return 0;
}
Output
Color: red
Area: 78.5397

আসল সমস্যাটি হলো: একটি বেস পয়েন্টারের (Base Pointer) মাধ্যমে আসলে কী ঘটে?

আর এটিই হলো এখানকার সবচেয়ে ইন্টারেস্টিং (interesting) — এবং ট্রিকি (tricky) অংশ। যদি আপনি একটি Shape* পয়েন্টারের (pointer) মাধ্যমে কোনো একটি Circle-কে স্টোর বা সংরক্ষণ করেন এবং এর ওই area() মেথডটিকে কল করেন, তবে মূলত এর কোন ভার্সনটি রান (runs) করবে? এক্ষেত্রে কোনো virtual কিওয়ার্ড (keyword) ছাড়া এটি মূলত Shape-এর ভার্সনটিকেই (version) কল করবে — যদিও এখানকার আসল অবজেক্টটি (actual object) হলো একটি সার্কেল (Circle)। একে মূলত স্ট্যাটিক বাইন্ডিং (static binding) বলা হয়, এবং এটি কখনোই আপনার আশানুরূপ বা কাঙ্ক্ষিত ফল দেবে না।

তাহলে এর সমাধানটি কী (fix)? এখানকার এই বেস ক্লাসের (base class) মেথডটিকে মূলত virtual হিসেবে তৈরি করা। যা মূলত তার কম্পাইলারকে (compiler) এই নির্দেশটি দেয় যে: "এটিকে কম্পাইল টাইমের (compile time) সময় নির্ধারণ করো না — বরং এর রানটাইমের (runtime) সময় সরাসরি এর এই বর্তমান অবজেক্টের টাইপটিকে (actual object type) চেক (check) করো।" আর এটিকেই পলিমরফিজম (polymorphism) বলা হয়।

virtual ফাংশনগুলো — অ্যাকশন বা বা বাস্তবে (Action) পলিমরফিজম (Polymorphism)

#include <iostream>
#include <cmath>
#include <vector>
#include <memory>
using namespace std;
class Shape {
protected:
string name;
public:
Shape(const string& n) : name(n) {}
// virtual = "এটির আসল অবজেক্টের টাইপটিকে (actual object type) এটি ঠিক করতে (decide) দাও"
virtual double area() const {
return 0.0;
}
// একটি ভার্চুয়াল ডেস্ট্রাক্টর (Virtual destructor) — এটি থাকা অত্যন্ত জরুরি! (নিচে বা below এটি ব্যাখ্যা করা হয়েছে)
virtual ~Shape() = default;
string getName() const { return name; }
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : Shape("Circle"), radius(r) {}
double area() const override { // অভাররাইড (override) = সুরক্ষার চেক (safety check)
return M_PI * radius * radius;
}
};
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h)
: Shape("Rectangle"), width(w), height(h) {}
double area() const override {
return width * height;
}
};
class Triangle : public Shape {
double base, height;
public:
Triangle(double b, double h)
: Shape("Triangle"), base(b), height(h) {}
double area() const override {
return 0.5 * base * height;
}
};
int main() {
// এর এই বেস ক্লাসের পয়েন্টারের (base class pointers) মাধ্যমে এখন বিভিন্ন শেপ বা আকৃতিগুলোকে স্টোর বা Store করুন
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(5.0));
shapes.push_back(make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(make_unique<Triangle>(3.0, 8.0));
// পলিমরফিজম (Polymorphism): এখন এর প্রতিটি আকৃতিই (shape) নিজস্ব ক্ষেত্রফল বা area গণনা (calculates) করতে পারবে
for (const auto& s : shapes) {
cout << s->getName() << " area: " << s->area() << endl;
}
return 0;
}
Output
Circle area: 78.5398
Rectangle area: 24
Triangle area: 12

এই override কিওয়ার্ডটি (Keyword) — আপনার নিজস্ব সুরক্ষা জালের (Safety Net) মতো

যখনই আপনি কোনো একটি ভার্চুয়াল ফাংশনকে (virtual function) ওভাররাইড (override) করতে চাইবেন, তখন সবসময়ই override কিওয়ার্ডটি ব্যবহার করুন। এই কিওয়ার্ডটি ছাড়া এর ফাংশনের সিগনেচারে (function signature) কোনো সাধারণ একটি ভুল বা টাইপোও (typo) আপনাকে নীরবে ওই বেসটিকে (base) ওভাররাইড করার বদলে একটি সম্পূর্ণ নতুন (new) ফাংশন তৈরি করে দেবে। কিন্তু এই override-এর সাহায্যে এখানকার কম্পাইলারটি (compiler) মূলত আপনার যেকোনো ধরনের ভুলকে ধরে (catches) ফেলতে পারবে।

// override ছাড়া — এর সিগনেচারটি (signature) না মিললে এটি মূলত সাইলেন্ট বাগ (silent bug) তৈরি করতে পারে
double area() const { ... }  // এটি হয়তো আসলটিকে ঠিকমতো ওভাররাইড (override) নাও করতে পারে!

// override-এর সাহায্যে — এটি কোনো বেস ভার্চুয়ালের (base virtual) সাথে না মিললে তৎক্ষণাৎ কম্পাইলার এরর (compiler error) দেবে
double area() const override { ... }  // এটি সম্পূর্ণ নিরাপদ বা Safe!

পিওর ভার্চুয়াল ফাংশনগুলো (Pure Virtual Functions) ও অ্যাবস্ট্রাক্ট ক্লাসগুলো (Abstract Classes)

কখনো কখনো কোনো একটি বেস ক্লাসের মেথডে (base class method) কোনো যৌক্তিক ডিফল্ট ইমপ্লিমেন্টেশন বা বাস্তবায়ন (reasonable default implementation) থাকে না। যেমন এক্ষেত্রে প্রতিটি শেপেরই (shape) একটি ক্ষেত্রফল (area) থাকা দরকার, কিন্তু শুধু একটি "Shape" নিজে থেকে কখনোই জানতে পারে না যে কীভাবে সেটি গণনা বা ক্যালকুলেট (calculate) করতে হয়। আর ঠিক এখানেই পিওর ভার্চুয়াল ফাংশনগুলোর (pure virtual functions) প্রয়োজনীয়তাগুলো চলে আসে।

এক্ষেত্রে মূলত = 0 লেখার মাধ্যমে আপনি এটি বলে দিচ্ছেন যে: "আমি এটিকে নিজে থেকে ইমপ্লিমেন্ট বা প্রয়োগ করছি না — বরং এর প্রতিটি ডেরাইভড ক্লাসকেই (derived class) এটি অবশ্যই করতে হবে (MUST)।" অর্থাৎ এমন যেকোনো ক্লাস (class) যার অন্তত একটি পিওর ভার্চুয়াল ফাংশন (pure virtual function) রয়েছে তাকে মূলত অ্যাবস্ট্রাক্ট (abstract) বলা হয় — যেখানে আপনি এগুলোকে সরাসরি এর কোনো ইনস্ট্যান্স (instances) তৈরি করার জন্য ব্যবহার করতে পারবেন না।

অ্যাবস্ট্রাক্ট ক্লাসগুলো (Abstract Classes) — বিভিন্ন বাধ্যবাধকতা (Contract) তৈরি করা

#include <iostream>
#include <memory>
using namespace std;
class Animal {
public:
// পিওর ভার্চুয়াল (Pure virtual) — এর ভেতরের প্রত্যেকটি প্রাণীকেই (animal) অবশ্যই (MUST) এদেরকে প্রয়োগ বা implement করতে হবে
virtual void speak() const = 0;
virtual string type() const = 0;
// একটি সাধারণ মেথড বা Regular method — যা এখানকার সব প্রাণীর (animals) মধ্যেই শেয়ার (shared) করা হয়েছে
void describe() const {
cout << "I'm a " << type() << " and I say: ";
speak();
}
virtual ~Animal() = default;
};
// Animal a; // এরর বা ERROR! এই অ্যাবস্ট্রাক্ট ক্লাসটিকে (abstract class) সরাসরি কোনো ইনস্ট্যান্টিয়েট (instantiate) করা যাবে না
class Dog : public Animal {
public:
void speak() const override { cout << "Woof!" << endl; }
string type() const override { return "Dog"; }
};
class Cat : public Animal {
public:
void speak() const override { cout << "Meow!" << endl; }
string type() const override { return "Cat"; }
};
int main() {
unique_ptr<Animal> pets[] = {
make_unique<Dog>(),
make_unique<Cat>()
};
for (const auto& pet : pets) {
pet->describe();
}
return 0;
}
Output
I'm a Dog and I say: Woof!
I'm a Cat and I say: Meow!

কেন এই ভার্চুয়াল ডেস্ট্রাক্টরগুলো (Virtual Destructors) এত বেশি গুরুত্বপূর্ণ (Matter)

এটি মূলত সি++ (C++)-এর অন্যতম সবচেয়ে গুরুত্বপূর্ণ (most important) একটি নিয়ম, আর এটিকে উপেক্ষা (ignoring) করলে এটি সাইলেন্ট মেমরি লিকের (silent memory leaks) মতো অনেক বড় বড় সমস্যা তৈরি করতে পারে।

আপনি যখন কোনো একটি বেস ক্লাস পয়েন্টারের (base class pointer) (যেমন delete basePtr;) সাহায্যে কোনো একটি অবজেক্টকে (object) ডিলিট (delete) করেন, তখন কম্পাইলারটির (compiler) এটি জানার প্রয়োজন পড়ে যে এটি ঠিক কোন ডেস্ট্রাক্টরটিকে (destructor) কল (call) করবে। যদি এটি virtual না হয়, তবে এটি মূলত শুধুমাত্র ওই বেস ক্লাস ডেস্ট্রাক্টরটিকেই (base class destructor) কল করে, আর পুরো ডেরাইভড ক্লাসের (derived class) ক্লিনআপগুলোকে (cleanup) মূলত এড়িয়ে বা স্কিপ (skipping) করে চলে যায়।

ভার্চুয়াল ডেস্ট্রাক্টর (Virtual Destructor) — যেকোনো মেমরি লিককে (Memory Leaks) প্রতিরোধ করা বা Preventing

#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base constructed" << endl; }
virtual ~Base() { cout << "Base destroyed" << endl; }
// virtual ছাড়া: ~Base() { ... } — এটি খুবই বিপজ্জনক (DANGEROUS)!
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {
cout << "Derived constructed (allocated memory)" << endl;
}
~Derived() override {
delete[] data;
cout << "Derived destroyed (freed memory)" << endl;
}
};
int main() {
cout << "--- এর বেস পয়েন্টারের বা base pointer-এর মাধ্যমে তৈরি (Creating) করা হচ্ছে ---" << endl;
Base* ptr = new Derived();
cout << "--- এর বেস পয়েন্টারের বা base pointer-এর মাধ্যমে ডিলিট (Deleting) করা হচ্ছে ---" << endl;
delete ptr; // virtual এর সাথে: এটি মূলত প্রথমে Derived টি কল করে এবং এরপর Base ডেস্ট্রাক্টরটিকে
// virtual ছাড়া: শুধু Base কেই কল (calls) করে — যা মেমরি লিক (memory leak)
return 0;
}
Output
--- এর বেস পয়েন্টারের বা base pointer-এর মাধ্যমে তৈরি (Creating) করা হচ্ছে ---
Base constructed
Derived constructed (allocated memory)
--- এর বেস পয়েন্টারের বা base pointer-এর মাধ্যমে ডিলিট (Deleting) করা হচ্ছে ---
Derived destroyed (freed memory)
Base destroyed
Note: যদি কোনো একটি ক্লাসের (class) যেকোনো (ANY) ভার্চুয়াল ফাংশন (virtual function) থেকে থাকে, তবে তার ডেস্ট্রাক্টরটিকেও (destructor) অবশ্যই (MUST) ভার্চুয়াল (virtual) হতে হবে। অন্যথায়, বেস পয়েন্টারের (base pointer) মাধ্যমে ওই ডাটাগুলোকে ডিলিট (deleting) করা হলে তা কখনোই এর ওই ডেরাইভড ডেস্ট্রাক্টরটিকে কল (call) করবে না — যার ফলে বেশ বড় রকমের মেমরি লিক (memory leaks) দেখা দেবে। তাই এর সবচেয়ে সহজ সমাধানটি (simplest fix) হলো: virtual ~Base() = default; ব্যবহার করা।

দ্য স্লাইসিং প্রব্লেম বা সমস্যা (The Slicing Problem)

এটি হলো এখানকার একেবারে শেষ সমস্যা। আপনি যখন কোনো একটি ডেরাইভড অবজেক্টকে (derived object) বাই ভ্যালুর (by value) সাহায্যে কোনো একটি বেস অবজেক্টে (base object) অ্যাসাইন (assign) করেন, তখন এর ডেরাইভড অংশগুলো স্লাইস বা কাটা (sliced off) যায়। এক্ষেত্রে শুধু তার ওই বেস বা মূল অংশটুকুই (base portion) অবশিষ্ট (remains) থাকে। আর এটিকেই মূলত অবজেক্ট স্লাইসিং (object slicing) বলা হয়, এবং এটি অনেক বেশি সূক্ষ্ম একটি বাগ বা সমস্যা (sneaky bug)।

অবজেক্ট স্লাইসিং (Object Slicing) — এক ধরনের সূক্ষ্ম বাগ (Subtle Bug)

#include <iostream>
using namespace std;
class Base {
public:
virtual void whoAmI() const {
cout << "I am Base" << endl;
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void whoAmI() const override {
cout << "I am Derived" << endl;
}
};
void byValue(Base b) { // এটি কপি (COPIES) করে দেয় — এখানকার স্লাইসিংটি (slicing) মূলত ঠিক এখানেই বা happens!
b.whoAmI();
}
void byReference(const Base& b) { // এটি মূলত রেফারেন্স বা Reference — তাই কোনো স্লাইসিং বা no slicing হয় না
b.whoAmI();
}
int main() {
Derived d;
cout << "By value: ";
byValue(d); // স্লাইস বা Sliced হয়ে গেছে! এটি Base::whoAmI-কে কল (Calls) করে
cout << "By reference: ";
byReference(d); // এখানে কোনো স্লাইসিং হয়নি (No slicing)! এটি Derived::whoAmI-কে কল (Calls) করে
return 0;
}
Output
By value:     I am Base
By reference: I am Derived

মূল কথাগুলো (Key Takeaways)

  • যেকোনো বেস ক্লাস মেথডের (base class methods) ক্ষেত্রে virtual ব্যবহার করুন, যাতে করে এর ডেরাইভড ক্লাসগুলো (derived classes) তাকে ওভাররাইড (override) করতে সক্ষম হয়
  • কম্পাইল-টাইমের সুরক্ষার (compile-time safety) জন্য এর ওভাররাইডগুলোকে (overrides) সবসময় override কিওয়ার্ডের সাহায্যে মার্ক বা চিহ্নিত (mark) করে রাখুন
  • পিওর ভার্চুয়াল ফাংশনগুলো (pure virtual functions) যখন এদের জন্য কোনো সঠিক বা যৌক্তিক (sensible default) ডিফল্ট খুঁজে না পায়, শুধু তখনই = 0 ব্যবহার করুন
  • যদি আপনার কাছে ভার্চুয়াল ফাংশন (virtual functions) থেকে থাকে, তবে নিশ্চিত করুন যে এর ডেস্ট্রাক্টরটিও (destructor) ভার্চুয়াল (virtual) কি না
  • অবজেক্ট স্লাইসিং (object slicing) এড়াতে সবসময় পয়েন্টার (pointers) বা রেফারেন্সগুলো (references) (ভ্যালু টাইপ নয়) ব্যবহার করুন
  • পলিমরফিজম অবজেক্টগুলো (polymorphic objects) ওয়েনিং বা নিজে ধারণ (owning) করার জন্য সবসময় unique_ptr<Base>-টিকে অগ্রাধিকার বা Prefer দিন
চ্যালেঞ্জ

ছোট কুইজ

যখন আপনি একটি বেস ক্লাস পয়েন্টারের (base class pointer) সাহায্যে — যা মূলত কোনো একটি ডেরাইভড অবজেক্টকে (derived object) পয়েন্ট (pointing) করছে — কোনো একটি নন-ভার্চুয়াল মেথডকে (non-virtual method) কল (call) করেন, তখন মূলত কী ঘটে?
Classes & ObjectsPointers & References