এই পর্বে যা শিখবেন
- class আর object-এর পার্থক্য, আর একটি class কেন “ডেটা + নিয়ম + অনুমোদিত কাজ”-এর প্যাকেট
public/privateদিয়ে encapsulation, আর যাচাই করা ফাংশন কীভাবে invariant রক্ষা করেthispointer, const member function আর const-correctness- class-এর বাইরে
ClassName::funcলেখা, header/source ভাগ, static member আর default member initializer - object মেমরিতে কত বড়, ভালো class নকশার চেকলিস্ট, আর AI-এর লেখা class যাচাইয়ের উপায়
১ · class কী — একটি ছাঁচ, অনেক জিনিস, আর একটি নিয়ম
পর্ব ২-এ (বস্তু দিয়ে ভাবা) আপনি কাগজে-কলমে একটি মোবাইল ওয়ালেটের নকশা করেছিলেন — কোন object কী জানে, কী করে, আর কোন নিয়ম কখনো ভাঙবে না। পর্ব ১২-এ (enum class, struct আর structured bindings) struct দিয়ে কয়েকটি তথ্যকে এক বাক্সে বেঁধেছিলেন। আজ এই দুটি ধারা এক জায়গায় মিলবে। মডিউল ৪ শুরু হলো, আর এর প্রথম প্রশ্ন: নিজের একটি টাইপ বানানো মানে আসলে কী?
পৌষ মাসে গ্রামের বাড়িতে পিঠা বানানোর কথা ভাবুন। কাঠের একটি নকশা-কাটা ছাঁচ আছে। ছাঁচ নিজে খাওয়া যায় না — সেটি শুধু বলে দেয় পিঠার আকার কেমন হবে, কোথায় কোন নকশা থাকবে। সেই একই ছাঁচ দিয়ে আপনি পঞ্চাশটি পিঠা বানাতে পারেন। প্রতিটি পিঠা আলাদা: কোনোটায় নারকেল, কোনোটায় গুড়, একটি ভাঙলে বাকিগুলো ভাঙে না। কিন্তু সবার আকার, সবার নকশা এক — কারণ সবাই একই ছাঁচের। class হলো ছাঁচ, object হলো পিঠা।
C++-এর ভাষায়, একটি class একটি নতুন টাইপের সংজ্ঞা — ঠিক যেমন int বা std::string একটি টাইপ। class ঠিক করে দেয় দুটি জিনিস: এই টাইপের প্রতিটি জিনিস কোন ডেটা ধরে রাখবে, আর সেই ডেটার ওপর কোন কোন কাজ করা যাবে। আর সেই টাইপের একটি আসল, মেমরিতে জায়গা নেওয়া জিনিসকে বলে object, বা ওই class-এর একটি instance. int x; লিখলে x একটি int-এর object; BankAccount rahim; লিখলে rahim একটি BankAccount-এর object.
কিন্তু “কিছু ডেটা আর কিছু ফাংশন একসাথে” — এটুকু বললে class-এর আসল কারণটা বাদ পড়ে যায়। সেই কারণ বুঝতে একটি ব্যাংক হিসাব ভাবুন। এর ডেটা সরল: মালিকের নাম আর ব্যালান্স। কিন্তু এর সাথে একটি নিয়ম জড়িয়ে আছে, যা ব্যাংকের কাছে নামের চেয়েও জরুরি:
এমন নিয়ম, যা object-এর পুরো জীবনে প্রতিটি মুহূর্তে সত্য থাকতে হবে, তাকে বলে invariant. পর্ব ২-এ এই শব্দটি কাগজে দেখেছিলেন; এখন দেখবেন C++ কীভাবে এটিকে কোডে পাহারা দেয়। একটি class-এর কাজ তিনটি, আর তৃতীয়টিই সবচেয়ে গুরুত্বপূর্ণ:
ডেটা বাঁধে
যে তথ্যগুলো একসাথে একটি জিনিসকে বোঝায় — নাম, ব্যালান্স — সেগুলো এক টাইপে থাকে। একে বলে object-এর অবস্থা (state).
data membersঅনুমোদিত কাজ দেয়
টাকা জমা, টাকা তোলা, ব্যালান্স দেখা — এই ডেটার ওপর কোন কাজ চলবে, তা class নিজেই ঠিক করে দেয়।
member functionsনিয়ম পাহারা দেয়
কোনো কাজই নিয়ম ভাঙতে পারবে না, আর বাইরের কেউ পাশ কাটিয়ে সরাসরি ডেটায় হাত দিতে পারবে না।
invariantclass মানে শুধু “ডেটা আর ফাংশনের থলি” নয়। class হলো একটি প্রতিশ্রুতি: “এই টাইপের যেকোনো object যেকোনো মুহূর্তে বৈধ অবস্থায় থাকবে।” ডেটা, ফাংশন আর private — সবই এই প্রতিশ্রুতি রাখার যন্ত্র।
২ · public আর private — দরজা খোলা রাখবেন, না পাহারাদার বসাবেন?
প্রথমে সবচেয়ে সরল পথে লিখে দেখা যাক — পর্ব ১২-এর মতো একটি struct, যার সব data member বাইরে থেকে খোলা:
struct Account {
std::string owner;
long long balance = 0; // নিয়ম: কখনো ঋণাত্মক নয় — কিন্তু কে মানাবে?
};
Account acc{"Rahim", 5000};
acc.balance -= 9000; // compile হয়। ব্যালান্স এখন -4000
acc.balance = 999999999; // এটাও compile হয়। টাকা আকাশ থেকে এলএখানে নিয়মটি শুধু একটি মন্তব্যে লেখা। প্রোগ্রামের যেকোনো জায়গা থেকে যে কেউ balance বদলাতে পারে, আর compiler কিছুই বলবে না। দশ হাজার লাইনের প্রোগ্রামে ব্যালান্স ঋণাত্মক হয়ে গেলে কে করল, খুঁজতে হবে প্রতিটি লাইনে — কারণ প্রতিটি লাইনই সন্দেহভাজন।
এটা এমন একটি ব্যাংক, যার ভল্টের দরজা খোলা, আর দেয়ালে একটি কাগজ লাগানো: “দয়া করে নিজের টাকার বেশি নেবেন না।” ভালো মানুষ মানবেন, তাড়াহুড়োয় কেউ ভুল করবেন, আর কেউ ইচ্ছা করে ভাঙবেন। আসল ব্যাংকে ভল্ট বন্ধ থাকে, আর কাউন্টারে একজন ক্যাশিয়ার বসেন। টাকা তুলতে হলে আপনাকে ক্যাশিয়ারের কাছে যেতেই হবে — আর তিনি আগে ব্যালান্স দেখে নেবেন।
C++-এ সেই বন্ধ ভল্ট হলো private, আর ক্যাশিয়ার হলো member function. এই দুটি শব্দ — public আর private — কে কোন অংশ দেখতে পাবে তা ঠিক করে, তাই এদের বলে access specifier. private অংশ শুধু class-এর নিজের member function ব্যবহার করতে পারে; public অংশ সবার জন্য খোলা। ডেটা লুকিয়ে শুধু নিয়ম-মানা কাজের দরজা খোলা রাখার এই কৌশলের নাম encapsulation.
#include <print>
class BankAccount {
long long balance_ = 0; // private: invariant balance_ >= 0
public:
bool deposit(long long taka) {
if (taka <= 0) return false;
balance_ += taka; return true;
}
bool withdraw(long long taka) {
if (taka <= 0 || taka > balance_) return false; // niyom bhangar age thamo
balance_ -= taka; return true;
}
long long balance() const { return balance_; }
};
int main() {
BankAccount acc;
acc.deposit(5000);
std::println("withdraw 2000: {}", acc.withdraw(2000));
std::println("withdraw 9000: {}", acc.withdraw(9000));
std::println("balance = {}", acc.balance());
}আউটপুট: withdraw 2000: true, withdraw 9000: false, balance = 3000. প্রথম তোলা বৈধ, তাই ব্যালান্স 5000 থেকে 3000 হলো। দ্বিতীয়টি নিয়ম ভাঙত, তাই withdraw কিছু না বদলে false ফেরত দিল। খেয়াল করুন, class-এ প্রথম public:-এর আগের সবকিছু ডিফল্টভাবে private — তাই balance_ লুকানো। এখন main-এ acc.balance_ = -1; লিখলে compile error: 'long long int BankAccount::balance_' is private within this context. নিয়ম ভাঙার একটিই পথ ছিল — সেটি compiler বন্ধ করে দিয়েছে। নামের শেষে _ একটি প্রচলিত রীতি, যাতে private ডেটা আর ফাংশনের নাম (balance()) গুলিয়ে না যায়।
withdraw-এর ভেতরে ঠিক কী ঘটে, দুটি ডাক ধাপে ধাপে দেখুন — একটি অনুমোদিত, একটি প্রত্যাখ্যাত:
1acc.withdraw(2000); // ব্যালান্স 50002bool withdraw(long long taka) {3 if (taka <= 0 || taka > balance_) return false;4 balance_ -= taka;5 return true;6acc.withdraw(9000); // ব্যালান্স 30007acc.balance_ -= 9000; // বাইরে থেকে সরাসরি?
| taka | |
| balance_ | |
| যাচাই | |
| invariant |
- বাইরে থেকে অনুরোধ।
mainসরাসরি ব্যালান্সে হাত দেয় না; সেacc-কে বলে “2000 টাকা তোলো”। কাজটা কীভাবে হবে, সেই সিদ্ধান্ত এখন object-এর নিজের। - ফাংশনের ভেতরে ঢোকা।
taka= 2000, আর ফাংশনacc-এরbalance_সরাসরি দেখতে পায় — কারণ এটি নিজের class-এর member function, private তার জন্য বন্ধ নয়। - পাহারাদারের প্রশ্ন। টাকা কি শূন্য বা ঋণাত্মক? ব্যালান্সের চেয়ে বেশি কি? দুটি প্রশ্নেরই উত্তর “না”, তাই কাজ চলতে পারে। পুরো class-এ নিয়মটি লেখা আছে ঠিক এই এক লাইনে।
- বৈধ বদল। ব্যালান্স 3000 হলো, ফাংশন
trueফেরত দিল। পরিবর্তনের পরেও invariant সত্য — এটাই প্রতিটি member function-এর দায়িত্ব: শুরুতে নিয়ম সত্য থাকলে শেষেও সত্য রাখা। - প্রত্যাখ্যান। এবার 9000. যাচাইয়ে ধরা পড়ল 9000 > 3000, তাই ফাংশন কিছু না বদলেই
falseফেরত দিল। ব্যালান্স একটুও নড়েনি। ভুল অনুরোধ এসেছে, কিন্তু ভুল অবস্থা তৈরি হয়নি। - পাশ কাটানোর চেষ্টা। কেউ যদি ক্যাশিয়ারকে এড়িয়ে সরাসরি ভল্টে হাত দিতে চায়?
balance_private, তাই compile-ই হবে না। নিয়মটি এখন মন্তব্যের অনুরোধ নয়, compiler-এর আইন। ব্যালান্স ভুল হলে খুঁজতে হবে শুধু class-এর ভেতরের তিনটি ফাংশনে — পুরো প্রোগ্রামে নয়।
struct আর class — পার্থক্য একটি শব্দের
পর্ব ১২-এ দেখেছেন, C++-এ struct আর class প্রায় হুবহু এক যন্ত্র। দুটিতেই ডেটা, member function, public/private, constructor — সব চলে। ভাষার নিয়মে পার্থক্য শুধু ডিফল্ট access-এ: struct-এ শুরু থেকে সব public, class-এ শুরু থেকে সব private (inheritance-এর ডিফল্টেও একই পার্থক্য, পর্ব ২৪-এ)। struct S { private: int x; }; আর class C { int x; }; একই জিনিস।
তাহলে কোনটি কখন? এখানে রীতিই আসল নিয়ম, আর C++ Core Guidelines সেটি স্পষ্ট বলে দেয়: যদি ডেটাগুলো স্বাধীনভাবে যেকোনো মান নিতে পারে — যেমন একটি বিন্দুর x আর y — তাহলে struct; যদি কোনো নিয়ম রক্ষা করতে হয়, তাহলে class, আর ডেটা private. পাঠক class শব্দটি দেখেই বোঝেন: “এখানে কোনো নিয়ম আছে, সাবধানে।”
C-র struct-এ শুধু ডেটা থাকে, আর সব সদস্য সবসময় খোলা; নিয়ম রক্ষা করতে C প্রোগ্রামাররা ফাংশনের নামে রীতি মানেন (account_withdraw(&acc, 500)) আর আশা করেন কেউ সরাসরি ফিল্ড ছোঁবে না। C++ সেই রীতিকেই ভাষার অংশ বানিয়েছে: ফাংশন struct-এর ভেতরে চলে এসেছে, প্রথম প্যারামিটার &acc হয়ে গেছে লুকানো this, আর “ছোঁবেন না” অনুরোধ হয়েছে private. C-র struct বিস্তারিত দেখতে C কোর্সের struct ও union পর্ব দেখুন।
৩ · এক নকশা, অনেক object — কার ডেটা কোথায়
এবার একই class থেকে কয়েকটি object বানালে মেমরিতে কী হয়? সবচেয়ে বড় ভুল ধারণাটি এখানেই ভাঙা দরকার: অনেকে ভাবেন প্রতিটি object-এর ভেতরে তার ফাংশনগুলোর একটি করে কপি থাকে। থাকে না। ধাপে ধাপে দেখুন:
stack (ডেটা) code (প্রোগ্রামের নির্দেশ) (খালি — এখনো কোনো object নেই) BankAccount::deposit BankAccount::withdraw BankAccount::balance
stack (ডেটা) code
rahim [ balance_ = 0 ] BankAccount::deposit
BankAccount::withdraw
BankAccount::balance
stack (ডেটা) code rahim [ balance_ = 0 ] BankAccount::deposit karim [ balance_ = 0 ] BankAccount::withdraw mitu [ balance_ = 0 ] BankAccount::balance (এখনো এই তিনটিই — কপি হয়নি)
stack (ডেটা) code rahim [ balance_ = 5000 ] ◄──┐ BankAccount::deposit ─┐ karim [ balance_ = 1200 ] ◄──┼────────────────────────┘ mitu [ balance_ = 0 ] (ছোঁয়া হয়নি) একটিই deposit, দুবার দুই object-এর জন্য চলল
object-এর ভেতরে যা থাকে object-এর ভেতরে যা থাকে না
✔ non-static data member ✘ member function (code-এ একটিই কপি)
✔ দরকার হলে padding ✘ static member (সবার জন্য একটি)
✘ public/private তথ্য (শুধু compile-এর সময়)
sizeof(BankAccount) = 8 ← একটি long long-এর সমান
- নকশা মেমরিতে জায়গা নেয় না। class লেখা মানে compiler-কে একটি নতুন টাইপ চেনানো। এখনো কোনো ব্যালান্স কোথাও রাখা হয়নি। কিন্তু member function-গুলোর নির্দেশ (machine code) প্রোগ্রামের code অংশে একবার তৈরি হয়ে থাকে।
- প্রথম object.
BankAccount rahim;লিখতেই stack-এ ৮ বাইটের একটি বাক্স তৈরি হলো, আর তার ভেতরেbalance_.= 0লেখা ছিল বলে শুরুর মান ঠিক 0 — কোনো আবর্জনা মান নেই। - আরও object, আরও বাক্স। karim আর mitu-র নিজের নিজের
balance_. তিনটি বাক্স সম্পূর্ণ আলাদা — একটি বদলালে অন্যগুলোর কিছু হয় না, ঠিক আলাদা পিঠার মতো। কিন্তু ফাংশন এখনো তিনটিই, ন-টি নয়। - একই ফাংশন, আলাদা ডেটা।
rahim.depositআরkarim.depositদুবারই code অংশের একইdepositচালায়। তাহলে ফাংশন জানে কীভাবে কার ব্যালান্স বাড়াতে হবে? উত্তর লুকিয়ে আছে একটি অদৃশ্য প্যারামিটারে —this, পরের অংশে। - আকারের হিসাব। একটি object-এর আকার মোটামুটি তার data member-দের আকারের যোগফল, সাথে প্রয়োজনে padding (পর্ব ১২)। ফাংশন যতই যোগ করুন, sizeof বাড়ে না।
public/private-এরও কোনো দাম নেই — সেটি শুধু compiler-এর পাহারা, চলার সময় কিছুই করে না। (ব্যতিক্রম পর্ব ২৫-এর virtual ফাংশন, যা প্রতিটি object-এ একটি লুকানো pointer যোগ করে।)
এই শেষ ধাপটি C++-এর একটি মূল দর্শনের ছোট উদাহরণ — zero-overhead নীতি। BankAccount আর একটি খোলা long long মেমরিতে হুবহু এক জিনিস, আর deposit-এর মতো ছোট ফাংশন সাধারণত compiler সরাসরি ডাকার জায়গায় বসিয়ে দেয়। অর্থাৎ নিরাপত্তা পাচ্ছেন, কিন্তু গতি বা মেমরিতে কোনো দাম দিচ্ছেন না। ভাবনার প্রশ্ন ০২-এ এ নিয়ে আরও আছে।
একটি মজার তথ্য: একেবারে খালি class (class Empty {};)-এর sizeof শূন্য নয়, অন্তত ১. কারণ C++-এ আলাদা দুটি object-এর ঠিকানা আলাদা হতেই হবে — শূন্য বাইটের দুটি object একই ঠিকানায় বসে যেত।
৪ · this — ফাংশন জানে কোন object তাকে ডেকেছে
আগের ধাপের প্রশ্ন: code অংশে deposit মাত্র একটি। rahim.deposit(500) ডাকলে সে rahim-এর ব্যালান্স বাড়ায়, karim.deposit(200) ডাকলে karim-এর। ফাংশনটি কীভাবে জানে?
উত্তর: compiler প্রতিটি non-static member function-কে একটি লুকানো প্যারামিটার দেয় — যে object-এর ওপর ডাকা হলো, তার ঠিকানা। ফাংশনের ভেতরে এই ঠিকানার নাম this. ফাংশনের ভেতরে আপনি যখন শুধু balance_ লেখেন, compiler সেটিকে পড়ে this->balance_ হিসেবে। মানে, rahim.deposit(500) আসলে প্রায় এমন কিছু: “deposit চালাও, আর this হিসেবে দাও &rahim”. C-তে আপনি নিজে হাতে যা পাঠাতেন (account_deposit(&rahim, 500)), C++ সেটাই চুপচাপ করে দেয়।
1rahim.deposit(500);2bool deposit(long long taka) {3 balance_ += taka; // আসলে: this->balance_ += taka;4karim.deposit(200);5 balance_ += taka; // একই লাইন, অন্য this
| rahim @1000 | |
| karim @2000 | |
| this | |
| taka |
- ডাক।
rahim.deposit(500). ডটের বাঁ দিকে যে object, সেটিই এই ডাকের “নিজ” object. compiler মনে মনে এটিকে একটি সাধারণ ফাংশন-ডাকে বদলায়, যার প্রথম (লুকানো) আর্গুমেন্ট rahim-এর ঠিকানা। - this পেল ঠিকানা। ফাংশনের ভেতরে
this-এর মান 1000 — rahim-এর ঠিকানা। এর টাইপBankAccount*, পর্ব ১৫-এর সাধারণ pointer-এর মতোই। - নাম খোঁজা।
balance_নামে কোনো local ভ্যারিয়েবল নেই, তাই compiler এটিকে member ধরে নেয়:this->balance_, মানে rahim-এর ব্যালান্স। 5000 থেকে 5500 হলো। karim-এর ডেটা ছোঁয়াই হয়নি। - অন্য object, একই ফাংশন।
karim.deposit(200). code অংশের সেই একইdeposit, কিন্তু এবারthis= 2000 — karim-এর ঠিকানা। - একই লাইন, অন্য ফল। হুবহু একই নির্দেশ এবার karim-এর ব্যালান্স 1200 থেকে 1400 করল। ফাংশন একটি, object অনেক —
this-ই দুটিকে জোড়া দেয়। এজন্যই ফাংশনের কপি প্রতিটি object-এ রাখার দরকার পড়ে না।
this কখন নিজে লিখবেন
বেশিরভাগ সময় this লিখতে হয় না — compiler নিজেই বসিয়ে দেয়। তিনটি জায়গায় এটি কাজে আসে:
- নামের সংঘর্ষ। প্যারামিটার আর member-এর নাম একই হলে (
void setLimit(long long limit) { this->limit = limit; }) প্যারামিটার member-কে ঢেকে দেয়।this->limitলিখে member বোঝাতে হয়। তবে ভালো সমাধান হলো নামেই পার্থক্য রাখা (limit_)। - নিজেকে ফেরত দেওয়া। ফাংশন
return *this;করলে ডাকার জায়গায় একই object ফিরে আসে, তাই একটির পর একটি ডাক জোড়া দেওয়া যায়:wallet.add(500).add(300);. একে বলে method chaining.std::cout << a << bঠিক এভাবেই কাজ করে। - নিজেকে অন্যের কাছে পাঠানো। যেমন একটি object নিজেকে কোনো তালিকায় নিবন্ধন করছে:
registry.add(this);.
this একটি সাধারণ, মালিকানাহীন pointer — পর্ব ১৭-এর ভাষায়, raw pointer মানে “আমি মালিক নই”। তাই delete this; বা this-কে কোনো smart pointer-এর হাতে দেওয়ার মতো কাজ প্রায় সবসময় বিপদ ডেকে আনে। আর this-কে কোথাও জমা রাখলে মনে রাখুন — object মরে গেলে সেই ঠিকানা dangling (পর্ব ১৮)।
৫ · const member function — “আমি শুধু দেখব, বদলাব না”
BankAccount-এর balance() ফাংশনের নামের পরে একটি ছোট শব্দ ছিল: long long balance() const. এই const একটি প্রতিশ্রুতি — “এই ফাংশন object-এর কোনো ডেটা বদলাবে না।” এমন ফাংশনকে বলে const member function. পর্ব ১২-এ passed() const-এ এটি এক ঝলক দেখেছিলেন; এবার দেখুন এটি কেন অপরিহার্য।
পর্ব ১০ থেকে আপনি জানেন, বড় object ফাংশনে পাঠানোর সেরা উপায় const T& — কপি নেই, আর ফাংশন বদলাতে পারবে না। ধরুন একটি রিপোর্ট ফাংশন:
void printStatement(const BankAccount& acc) {
std::println("Balance: {}", acc.balance()); // const ফাংশন — চলবে
acc.deposit(100); // const নয় — compile error
}এখানে acc একটি const object. compiler কীভাবে জানবে কোন ফাংশন ডাকা নিরাপদ? সে ফাংশনের ভেতরে ঢুকে দেখে না — শুধু ঘোষণা দেখে। নামের পরে const থাকলে ডাকা যাবে, না থাকলে যাবে না। ধাপে ধাপে দেখুন:
acc ──> [ balance_ = 3000 ] const — এই পথ দিয়ে বদলানো নিষেধ ফাংশনের ভেতরে this-এর টাইপ: সাধারণ member function → BankAccount* const member function → const BankAccount*
ঘোষণায় const আছে → this হবে const BankAccount*
const object-এর ঠিকানা const pointer-এ → মিলে গেল ✔
আউটপুট: Balance: 3000
ঘোষণায় const নেই → this হবে BankAccount* (বদলাতে পারে)
const object-কে non-const pointer-এ? → মিলল না ✘
error: passing 'const BankAccount' as 'this' argument discards qualifiers
printStatement-এর ভেতরে acc.balance() → এটাও error ✘
ফাংশনটি কিছুই বদলায় না, কিন্তু compiler তা জানে না —
সে শুধু ঘোষণা দেখে। ঘোষণায় প্রতিশ্রুতি নেই, তাই অনুমতি নেই।
সমাধান: যে ফাংশন বদলায় না, তার নামের পরে const লিখুন।
- const object.
const BankAccount&দিয়ে আসাacc-কে এই ফাংশন বদলাতে পারবে না। আর মনে রাখুন, member function-এর ভেতরেthisএকটি pointer — const ফাংশনে সেটিconst BankAccount*, অর্থাৎ শুধু পড়া যায়। - const ফাংশন — চলবে।
balance()-এর ঘোষণায়constআছে, তাই এরthisএকটি const pointer. const object-এর ঠিকানা সেখানে নিরাপদে বসে। কোনো ঝুঁকি নেই, তাই compiler অনুমতি দেয়। - non-const ফাংশন — আটকাবে।
depositobject বদলাতে পারে, তাই তারthisসাধারণ pointer. const object-কে সেখানে দিলে const-এর প্রতিশ্রুতি ভাঙত। g++-এর বার্তাটি একটু খটমটে — “discards qualifiers” মানে “const বাদ দিতে হতো”। - const ভুলে যাওয়া। সবচেয়ে কষ্টের ভুল এটা:
balance()আসলে কিছুই বদলায় না, কিন্তু লেখকconstলিখতে ভুলে গেছেন। এখন const reference দিয়ে আসা কোনো object-এর ব্যালান্সই দেখা যাচ্ছে না! তাই নিয়ম: যে member function object বদলায় না, তার নামের পরে সবসময়const.
এই অভ্যাসের নাম const-correctness: প্রোগ্রামের প্রতিটি জায়গায় স্পষ্ট বলে দেওয়া কোনটি বদলাতে পারে, কোনটি পারে না — আর compiler-কে সেটি যাচাই করতে দেওয়া। এর লাভ তিনটি। পাঠক ঘোষণা দেখেই বোঝেন কোন ফাংশন নিরাপদ। ভুল করে বদলানোর চেষ্টা compile-এর সময়েই ধরা পড়ে। আর const ফাংশন const ও non-const — দুই ধরনের object-এই ডাকা যায়, তাই সেটি বেশি জায়গায় কাজে লাগে।
দুটি সূক্ষ্ম বিষয়। এক, শুধু const থাকা-না-থাকার পার্থক্যে একই নামের দুটি ফাংশন লেখা যায় (const overload) — std::vector-এর operator[] এভাবেই const vector-এ শুধু-পড়ার reference আর সাধারণ vector-এ বদলানোর reference দেয়। দুই, কখনো এমন ডেটা থাকে যা বাইরে থেকে দেখা অবস্থার অংশ নয় — যেমন একটি cache বা কতবার পড়া হলো তার গোনা। সেটিকে const ফাংশনের ভেতরেও বদলাতে দিতে চাইলে mutable লেখা হয়। কিন্তু এটি ব্যতিক্রম; mutable দেখলেই প্রশ্ন করুন, সত্যিই কি এটি দরকার?
৬ · class-এর বাইরে member লেখা — ClassName::func আর header/source ভাগ
এতক্ষণ সব ফাংশনের শরীর class-এর ভেতরেই লিখেছেন। ছোট ফাংশনের জন্য এটি ঠিক আছে, কিন্তু class বড় হলে ভেতরটা ভিড় হয়ে যায় — কোন কোন কাজ করা যায়, এক নজরে আর দেখা যায় না। তাই C++ ঘোষণা আর সংজ্ঞা আলাদা করতে দেয়: class-এর ভেতরে শুধু ফাংশনের মাথা (ঘোষণা), আর বাইরে শরীর। বাইরে লেখার সময় বলে দিতে হয় ফাংশনটি কোন class-এর — scope resolution operator :: দিয়ে:
// bank_account.h — বাকি প্রোগ্রাম এটুকুই দেখে
#pragma once
class BankAccount {
long long balance_ = 0;
public:
bool deposit(long long taka);
bool withdraw(long long taka);
long long balance() const { return balance_; } // ছোট — ভেতরেই থাক
};
// bank_account.cpp — কীভাবে করা হয়
#include "bank_account.h"
bool BankAccount::withdraw(long long taka) { // কোন class-এর withdraw?
if (taka <= 0 || taka > balance_) return false; // balance_ এখানেও সরাসরি
balance_ -= taka;
return true;
}BankAccount::withdraw পড়ুন “BankAccount-এর withdraw”. এই নাম লেখার পর ফাংশনের ভেতরটা class-এর ভেতরের মতোই — balance_ সরাসরি দেখা যায়, this আছে, private-এ অনুমতি আছে। :: না লিখে শুধু bool withdraw(...) লিখলে সেটি হয়ে যায় একটি আলাদা সাধারণ ফাংশন, আর তখন balance_ চিনবে না। const ফাংশন বাইরে লিখলে const দুই জায়গাতেই লাগে: long long BankAccount::balance() const { ... }.
পর্ব ১১-এ (namespace আর header) দেখেছিলেন বড় প্রোগ্রাম কীভাবে header file আর source file-এ ভাগ হয়। class-এর ক্ষেত্রে প্রচলিত ভাগটি এমন:
| ফাইল | যা থাকে | কেন |
|---|---|---|
bank_account.h | পুরো class সংজ্ঞা: data member, সব ফাংশনের ঘোষণা, খুব ছোট ফাংশনের শরীর | যে ফাইল BankAccount ব্যবহার করে, compiler-কে তার আকার আর ফাংশনগুলো জানতে হয় |
bank_account.cpp | বড় ফাংশনগুলোর শরীর, BankAccount:: দিয়ে | শরীর বদলালে শুধু এই ফাইল আবার compile হয়; ব্যবহারকারীদের নয় |
main.cpp | #include "bank_account.h" আর ব্যবহার | নকশা জানে, ভেতরের কাজ জানে না |
একটি প্রশ্ন স্বাভাবিক: private ডেটা তো header-এ সবাই দেখতে পাচ্ছে, তাহলে লুকানো হলো কোথায়? উত্তর — private মানে “দেখা যাবে না” নয়, “ব্যবহার করা যাবে না”। compiler-কে object-এর আকার জানতে হয় (stack-এ কত জায়গা দেবে), তাই ডেটার তালিকা header-এ থাকতেই হয়। encapsulation এখানে নিরাপত্তার তালা নয়, ভুল থেকে বাঁচার বেড়া।
class-এর ভেতরে শরীরসহ লেখা member function নিজে থেকেই inline ধরা হয়। তাই header-এ এমন ফাংশন রাখলে একাধিক .cpp include করলেও linker-এর “একই ফাংশন দুবার” error হয় না (পর্ব ৯-এর ODR). কিন্তু class-এর বাইরে header-এ শরীর লিখলে, নিজে inline না লিখলে সেই error আসবে।
৭ · static member আর শুরুর মান — সবার জন্য একটি, প্রত্যেকের জন্য একটি
কখনো এমন তথ্য দরকার, যা কোনো একটি object-এর নয়, পুরো class-এর — যেমন “এ পর্যন্ত মোট কয়টি ওয়ালেট খোলা হয়েছে”। প্রতিটি object-এ এটি রাখলে সংখ্যাটি একশো জায়গায় একশো রকম হয়ে যেত। এর জন্য আছে static member: class-এর ভেতরে static লিখে ঘোষণা করা ডেটা বা ফাংশন, যার পুরো প্রোগ্রামে ঠিক একটি কপি — কোনো object-এর ভেতরে নয়। একটি মেস বাড়ির কথা ভাবুন: প্রত্যেকের নিজের খাট আর আলমারি আছে (সাধারণ member), কিন্তু দেয়ালের নোটিশবোর্ড সবার জন্য একটি (static member).
আর আরেকটি জিনিস আপনি ইতিমধ্যে ব্যবহার করছেন: long long balance_ = 0; — data member-এর ঘোষণাতেই শুরুর মান। একে বলে default member initializer (C++11 থেকে)। প্রতিটি নতুন object এই মান নিয়ে জন্মায়। নিচের bKash-ধাঁচের ওয়ালেটে দুটি ধারণা একসাথে দেখুন:
#include <print>
class Wallet {
static inline int count_ = 0; // shob Wallet mile ekti
int id_ = ++count_; // proti object-er nijer, jonmer somoy
long long balance_ = 0;
public:
Wallet& add(long long taka) { if (taka > 0) balance_ += taka; return *this; }
long long balance() const { return balance_; }
int id() const { return id_; }
static int count() { return count_; } // kono object lage na
};
int main() {
Wallet a, b;
a.add(500).add(300); // *this ferot, tai chain
const Wallet& view = b; // shudhu const function chole
std::println("a#{} = {}, b#{} = {}", a.id(), a.balance(), view.id(), view.balance());
std::println("wallets = {}, sizeof = {}", Wallet::count(), sizeof(Wallet));
}আউটপুট: a#1 = 800, b#2 = 0 আর wallets = 2, sizeof = 16. প্রতিটি Wallet জন্মের সময় id_ = ++count_ চালায়: a পেল 1, b পেল 2, আর ভাগ করা count_ হলো 2. a.add(500).add(300) কাজ করেছে কারণ add *this ফেরত দেয় — দ্বিতীয় add একই a-তে চলে। view const reference, তাই তাতে শুধু id() আর balance() চলে; view.add(1) লিখলে compile error. Wallet::count() static ফাংশন — ডাকতে কোনো object লাগেনি, তাই এর ভেতরে this-ও নেই। আর sizeof 16: id_ (৪ বাইট) + padding (৪) + balance_ (৮)। count_ object-এর ভেতরে নেই বলে আকারে যোগ হয়নি। (16 সংখ্যাটি সাধারণ 64-bit সিস্টেমের; অন্য সিস্টেমে আলাদা হতে পারে।)
static inlineকেন? C++17-এর আগে static ডেটা class-এর ভেতরে শুধু ঘোষণা করা যেত; একটি.cppফাইলে আলাদা করে সংজ্ঞা দিতে হতো (int Wallet::count_ = 0;). C++17 থেকেinlineলিখে class-এর ভেতরেই শুরুর মান দেওয়া যায়। ধ্রুবক হলেstatic constexpr int maxPin = 4;লিখুন।- static ফাংশনে
thisনেই। তাই সে কোনো object-এর non-static ডেটা সরাসরি দেখতে পারে না — কার ডেটা দেখবে, জানেই না। - সাবধান: একাধিক thread একসাথে Wallet বানালে এই সাধারণ
intগোনা নিরাপদ নয় (পর্ব ৪৬-এরstd::atomicলাগবে)। আর static ডেটা আসলে একটি global ভ্যারিয়েবলের মতোই — বেশি ব্যবহার করলে পর্ব ২-এর global-এর সমস্যাগুলো ফিরে আসে।
পরের পর্বের এক ঝলক: default member initializer সব object-কে একই শুরুর মান দেয়। কিন্তু যদি প্রতিটি ওয়ালেট খোলার সময় মালিকের নাম আর নম্বর দিতেই হয়, আর ভুল নম্বর দিলে ওয়ালেট তৈরিই না হয়? তার জন্য লাগে constructor — object জন্মের মুহূর্তে চলা বিশেষ ফাংশন, যা জন্ম থেকেই invariant নিশ্চিত করে। পর্ব ২১-এর পুরো বিষয় এটিই।
৮ · ভালো class-এর নকশা — চারটি প্রশ্ন
class লেখার নিয়ম শেখা সহজ; ভালো class নকশা করা কঠিন। একটি Student class দিয়ে দেখুন। প্রথম খসড়া, যা অনেকে (আর অনেক AI) প্রথমেই লেখেন:
class Student { // খসড়া — দেখতে “OOP”, আসলে খোলা struct
std::string name;
std::vector<double> gradePoints;
double gpa; // জমা রাখা — gradePoints-এর সাথে মিলতেই হবে
public:
void setGradePoints(std::vector<double> g) { gradePoints = g; }
void setGpa(double g) { gpa = g; } // যে কেউ ভুল GPA বসাতে পারে!
double getGpa() { return gpa; } // const-ও নেই
};এখানে ডেটা private, ফাংশনও আছে — তবু encapsulation প্রায় শূন্য। setGpa(4.9) ডেকে যে কেউ এমন GPA বসাতে পারেন, যা গ্রেডের সাথে মেলে না, এমনকি ৪-এর বেশি। gpa আর gradePoints দুটি আলাদা জায়গায় একই সত্য রাখছে, আর দুটি মিলিয়ে রাখার দায়িত্ব পড়েছে বাইরের সবার ঘাড়ে। এবার চারটি প্রশ্ন দিয়ে নকশাটি ঠিক করুন:
১. নিয়মগুলো কী?
প্রথমে invariant লিখুন। Student-এর: প্রতিটি grade point ০ থেকে ৪-এর মধ্যে; GPA সবসময় grade point-গুলোর গড়। নিয়ম না থাকলে হয়তো struct-ই যথেষ্ট।
invariants first২. যা হিসাব করা যায়, তা জমা রাখবেন না
GPA জমা রাখলে সেটি একদিন grade-এর সাথে অমিল হবেই। বরং দরকারের সময় হিসাব করুন। এক জায়গায় সত্য — অমিলের সুযোগই নেই।
derive, don't store৩. public অংশ যত ছোট সম্ভব
প্রতিটি public ফাংশন একটি চিরকালের প্রতিশ্রুতি — পরে বদলালে ব্যবহারকারীদের কোড ভাঙে। যা বাইরের কারও দরকার নেই, তা private রাখুন।
minimal interface৪. জিজ্ঞেস নয়, বলে দিন
“ব্যালান্স দাও, আমি দেখে কমিয়ে দিচ্ছি” নয় — “500 টাকা তোলো”। সিদ্ধান্ত নেয় object নিজে, কারণ নিয়ম তারই জানা।
tell, don't askclass Student {
std::string name_;
std::vector<double> gradePoints_; // invariant: প্রতিটি 0.0–4.0
public:
bool addGrade(double gp) { // “বলে দিন” — যাচাই ভেতরে
if (gp < 0.0 || gp > 4.0) return false;
gradePoints_.push_back(gp);
return true;
}
double gpa() const { // জমা নয় — প্রতিবার হিসাব
if (gradePoints_.empty()) return 0.0;
double sum = 0;
for (double g : gradePoints_) sum += g;
return sum / gradePoints_.size();
}
};এখন ভুল GPA বসানোর কোনো পথই নেই — কারণ GPA বসানোর ফাংশনই নেই। setGradePoints চলে গিয়ে এসেছে addGrade, যা একটি একটি করে যাচাই করে। আর gpa() const, তাই const Student-এও চলে। একটি তুলনা মনে রাখুন: পরীক্ষার মার্কশিটে মোট নম্বর হাতে লিখে রাখলে যোগে ভুল হতে পারে; কিন্তু সফটওয়্যারে যদি মোট সবসময় নম্বরগুলো থেকে হিসাব হয়, ভুল যোগফল তৈরিই হতে পারে না।
getter আর setter-এর ফাঁদ
প্রতিটি private ফিল্ডের জন্য একটি getter আর setter লেখা — getBalance(), setBalance() — এটি Java-র পুরোনো একটি রীতি থেকে আসা অভ্যাস। কিন্তু ভাবুন: setBalance(long long b) থাকলে balance_ private করে লাভ কী হলো? যে কেউ এখনো যেকোনো মান বসাতে পারে, শুধু একটু বেশি টাইপ করে। একে বলা যায় “পোশাক পরা public ডেটা”। setter-এর বদলে এমন কাজের নাম দিন যা বাস্তবে ঘটে — deposit, withdraw, addGrade, changePin(old, new) — আর প্রতিটির ভেতরে নিয়ম যাচাই করুন। getter-ও শুধু তখনই দিন, যখন বাইরের কারও সেই তথ্য সত্যিই দরকার।
আর সব ফাংশনকে member বানাতেও হবে না। যে কাজ public ফাংশন দিয়েই করা যায় — যেমন দুটি ওয়ালেটের মোট টাকা — সেটি একটি free function হিসেবে লিখুন: long long total(const Wallet& a, const Wallet& b) { return a.balance() + b.balance(); }. এতে private ডেটা ছুঁতে পারে এমন ফাংশনের সংখ্যা কম থাকে, আর invariant ভাঙলে খোঁজার জায়গাও কম (ভাবনার প্রশ্ন ০৫)।
class নকশার চেকলিস্ট
class ধারণাটি C++-এর আবিষ্কার নয়। এটি এসেছে নরওয়ের Simula 67 ভাষা থেকে, যা Ole-Johan Dahl আর Kristen Nygaard তৈরি করেছিলেন সিমুলেশনের জন্য। ১৯৭০-এর দশকের শেষে কেমব্রিজে পিএইচডির কাজে Bjarne Stroustrup Simula ব্যবহার করেন, আর এর class-এর প্রেমে পড়েন — কিন্তু বড় প্রোগ্রামে এটি ছিল খুব ধীর। ১৯৭৯ সালে Bell Labs-এ যোগ দিয়ে তিনি C-র গতির সাথে Simula-র class জোড়া দেওয়ার কাজ শুরু করেন। প্রথম নাম ছিল “C with Classes”, আর তাতে প্রথম দিকেই ছিল class, public/private access আর constructor-destructor. ১৯৮৩ সালে ভাষাটির নাম হয় C++.
- C++-এ struct আর class-এর পার্থক্য কী? — শুধু ডিফল্ট access (আর ডিফল্ট inheritance): struct-এ public, class-এ private; বাকি সব এক।
- Encapsulation কী, আর কেন দরকার? — ডেটা private রেখে শুধু নিয়ম-মানা public ফাংশন দিয়ে বদলাতে দেওয়া, যাতে invariant কেউ ভাঙতে না পারে আর ভুল খুঁজতে হয় এক জায়গায়।
thispointer কী? — non-static member function-এর ভেতরে লুকানো pointer, যা ডাকা object-এর ঠিকানা ধরে; static ফাংশনেthisনেই।- const member function কেন? — এটি object বদলাবে না বলে প্রতিশ্রুতি দেয়; const object বা const reference-এ শুধু এমন ফাংশনই ডাকা যায়।
- static data member কোথায় থাকে, sizeof-এ গোনা হয়? — পুরো class-এর জন্য একটি কপি, কোনো object-এর ভেতরে নয়; তাই sizeof-এ যোগ হয় না।
৯ · AI-এর যুগে — AI-এর লেখা class যাচাই
“একটি BankAccount class লেখো” বললে AI টুল প্রায় সবসময় compile হয় এমন কোড দেয়। কিন্তু অনলাইনের লাখো উদাহরণ থেকে শেখা বলে সে প্রায়ই পুরোনো, যান্ত্রিক রীতি অনুসরণ করে — প্রতিটি ফিল্ডে getter-setter, যাচাইহীন ডেটা, আর const বাদ। এই কোড প্রথম দিনে চলে; সমস্যা আসে মাস তিনেক পরে, যখন কেউ setBalance(-500) ডেকে ফেলেন। যা প্রায়ই দেখবেন:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
প্রতিটি ফিল্ডের জন্য getX() আর setX() | private থেকেও আসলে খোলা ডেটা; invariant রক্ষা হয় না | বাস্তব কাজের ফাংশন (deposit, withdraw), ভেতরে যাচাই |
নিয়ম-জড়িত ডেটা public, বা struct-এ রেখে দেওয়া | যে কেউ যেকোনো জায়গা থেকে নিয়ম ভাঙতে পারে | class + private ডেটা |
getter-এ const নেই | const reference দিয়ে আসা object-এ ডাকা যায় না | long long balance() const |
হিসাব করা যায় এমন মান জমা রাখা (gpa, total) | দুই জায়গার সত্য একদিন অমিল হয় | দরকারের সময় হিসাব করা const ফাংশন |
ভুল ইনপুটে চুপচাপ কিছু না করা, বা cout-এ লিখে দেওয়া | ডাকার মানুষ জানতেই পারে না কাজ হয়নি | bool ফেরত (বা পর্ব ৩৭-এর exception/std::expected) |
টাকার জন্য double | দশমিকের গোলমাল (0.1 + 0.2) | পয়সায় হিসাব, পূর্ণসংখ্যা (long long) |
| সব কাজ member function — দুটি object-এর তুলনাও | private ছুঁতে পারে এমন কোড অকারণে বাড়ে | public ফাংশনেই চললে free function |
AI-কে কোড লেখানোর আগে নকশার নিয়ম আগেই বলে দিন, আর তাকে দিয়ে invariant লিখিয়ে নিন — তাহলে যাচাই করা সহজ হয়:
AI-এর লেখা class যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১class-এর ধারণা এল কোথা থেকে? Simula 67 থেকে “C with Classes” — Stroustrup ঠিক কী নিলেন, আর কী বাদ দিলেন?
১৯৬০-এর দশকে নরওয়ের Norwegian Computing Center-এ Ole-Johan Dahl আর Kristen Nygaard জাহাজ চলাচল, কারখানা বা ব্যাংকের লাইনের মতো বাস্তব ব্যবস্থার Simula নামে সিমুলেশন-ভাষা বানাচ্ছিলেন। সিমুলেশনে স্বাভাবিকভাবেই অনেক “জিনিস” থাকে — প্রতিটি জাহাজ, প্রতিটি গ্রাহক — যাদের নিজের অবস্থা আর আচরণ আছে, আর একই ধরনের জিনিস অনেকগুলো। এই চাহিদা থেকে Simula 67-এ জন্ম নেয় class, object, subclass (পরে যাকে inheritance বলা হলো) আর virtual procedure — আজকের object-oriented প্রোগ্রামিংয়ের প্রায় পুরো ভিত্তি। এই কাজের জন্য দুজন ২০০১ সালে Turing Award পান।
Bjarne Stroustrup কেমব্রিজে পিএইচডির সময় distributed system-এর একটি সিমুলেশন লিখেছিলেন Simula-য়। তিনি পরে লিখেছেন, Simula-র class তাকে প্রোগ্রামকে সমস্যার ভাষায় সাজাতে দারুণ সাহায্য করেছিল — কিন্তু প্রোগ্রাম এত ধীর চলছিল যে প্রকল্প প্রায় থেমে যায়। শেষে তিনি পুরো প্রোগ্রাম BCPL-এ আবার লেখেন, যা দ্রুত কিন্তু খুবই নিচু স্তরের, প্রায় কোনো সাংগঠনিক সাহায্য দেয় না। এই অভিজ্ঞতা থেকে তার লক্ষ্য তৈরি হয়: Simula-র সংগঠন আর C-র গতি, একই ভাষায়।
১৯৭৯ সালে Bell Labs-এ তিনি “C with Classes” নিয়ে কাজ শুরু করেন। এটি ছিল C-র ওপর একটি preprocessor, যা class-যুক্ত কোডকে সাধারণ C-তে অনুবাদ করত (পরে এর উত্তরসূরি compiler-এর নাম হয় Cfront). তিনি নিলেন class, access control (public/private), constructor-destructor আর derived class. কিন্তু Simula-র দুটি জিনিস ইচ্ছা করে বাদ দিলেন: সব object-এর heap-এ থাকা আর garbage collection. C with Classes-এ object stack-এ থাকতে পারত, ঠিক int-এর মতো — এটাই আজ পর্যন্ত C++-এর গতির মূল কারণ। virtual ফাংশন এসেছিল পরে, ১৯৮৩ সালে ভাষাটি C++ নাম পাওয়ার সময়ের দিকে।
এই পর্বের this-ও সেই অনুবাদের চিহ্ন বহন করে। Cfront member function-কে একটি সাধারণ C ফাংশনে বদলাত, যার প্রথম প্যারামিটার object-এর pointer. reference তখনো ভাষায় আসেনি, তাই this হলো pointer — reference নয়। Stroustrup নিজেও তার প্রশ্নোত্তর-পাতায় (FAQ) this pointer হওয়ার এই কারণটিই দিয়েছেন — reference যোগ হয়েছিল this-এর পরে।
মূল উপলব্ধি: C++-এর class হলো Simula-র ধারণা, C-র দামে। প্রতিটি নকশা-সিদ্ধান্তের পেছনে একটি প্রশ্ন ছিল — “এই সুবিধার জন্য গতি বা মেমরিতে কত দাম দিতে হবে?” — আর উত্তর যতটা সম্ভব “কিছুই না” রাখার চেষ্টা।
প্র ০২C++ কেন “zero-overhead abstraction” বেছে নিল? একটি class আর একটি খোলা struct চলার সময় আসলে কতটা আলাদা?
Stroustrup zero-overhead নীতিকে দুই বাক্যে বলেছেন: যা ব্যবহার করেন না, তার দাম দেবেন না; আর যা ব্যবহার করেন, তা হাতে লিখেও এর চেয়ে ভালো করতে পারতেন না। এর পেছনে একটি বাস্তব হিসাব ছিল। ১৯৮০-র দশকে যাঁরা সিস্টেম প্রোগ্রাম লিখতেন — টেলিফোন সুইচ, অপারেটিং সিস্টেম — তাঁরা C ছাড়তেন না, যদি নতুন ভাষা একটু ধীরও হতো। নতুন সুবিধা যদি গতির দাম চায়, তাহলে মানুষ সুবিধাটি বাদ দিয়ে আবার হাতে লিখবেন। তাই abstraction টিকিয়ে রাখার একমাত্র উপায় ছিল সেটিকে বিনামূল্যে দেওয়া।
এই পর্বের BankAccount-এ এর মানে কী? sizeof(BankAccount) একটি long long-এর সমান — private, public, member function, কোনোটিই মেমরি নেয় না। access control শুধু compile-এর সময়ের পরীক্ষা; চলার সময় এর কোনো অস্তিত্বই নেই। member function-কে compiler একটি সাধারণ ফাংশনে বদলায়, যার লুকানো প্রথম প্যারামিটার this — ঠিক C প্রোগ্রামার হাতে যা লিখতেন। আর balance()-এর মতো ছোট ফাংশন optimization চালু থাকলে প্রায় সবসময় ডাকার জায়গায় বসে যায় (inline), তাই acc.balance() আর সরাসরি মেমরি পড়ার machine code প্রায়ই হুবহু এক। Compiler Explorer-এ -O2 দিয়ে দুটি সংস্করণ লিখে দেখলে নিজেই মিলিয়ে নিতে পারবেন।
তবে “zero-overhead” মানে “zero-cost” নয়। কিছু সুবিধার দাম আছে, শুধু সেটি এড়ানোর উপায়ও আছে। পর্ব ২৫-এর virtual ফাংশন প্রতিটি object-এ একটি লুকানো pointer যোগ করে, আর ডাকার সময় একটি বাড়তি ধাপ — কিন্তু এর চেয়ে কম খরচে হাতে লেখা “ফাংশন-pointer-এর টেবিল”-ও সম্ভব নয়, আর virtual না লিখলে কোনো দামই নেই। exception আর RTTI-এর দাম নিয়ে বিতর্ক আছে, তাই কিছু প্রকল্প (যেমন অনেক গেম ইঞ্জিন বা embedded সিস্টেম) এগুলো বন্ধ রেখে compile করে। আরেকটি দাম আছে, যা চলার সময় নয়: compile-এর সময় বাড়ে, আর header-এ private ডেটা দেখাতে হয় বলে ভেতরের পরিবর্তনে ব্যবহারকারীদেরও আবার compile করতে হয়।
অন্য ভাষার সাথে তুলনা করলে পার্থক্যটা স্পষ্ট হয়। Java-তে প্রতিটি object heap-এ থাকে, সাথে একটি header (টাইপের তথ্য, lock-এর জায়গা) — দুটি সংখ্যার একটি ছোট object-ও অনেক বেশি জায়গা নেয়। Python-এ একটি int-ও একটি পূর্ণ object. এসব ভাষা অন্য সুবিধা দেয়, কিন্তু “class বানালে খরচ বাড়ে” — এই ভয় তাদের প্রোগ্রামারদের কখনো কখনো ছোট টাইপ বানানো থেকে বিরত রাখে।
মূল উপলব্ধি: C++-এ ছোট, নিরাপদ টাইপ বানাতে দ্বিধা করবেন না — একটি Taka বা StudentId class-এর দাম সাধারণত একটি long long-এর সমান। abstraction যখন বিনামূল্যে, তখন নিরাপত্তা বেছে না নেওয়ার কোনো অজুহাত থাকে না।
প্র ০৩“Anemic” class কী? ডেটা আর ফাংশন একসাথে রাখার পক্ষে-বিপক্ষে যুক্তিগুলো কী, আর কখন শুধু-ডেটার struct-ই ভালো নকশা?
২০০৩ সালে Martin Fowler একটি লেখায় anemic domain model নামটি জনপ্রিয় করেন। তিনি দেখছিলেন, অনেক বড় প্রকল্পে class-গুলো দেখতে object-oriented — private ফিল্ড, প্রতিটির getter-setter — কিন্তু ভেতরে কোনো আচরণ নেই। সব নিয়ম আর সিদ্ধান্ত থাকে আলাদা “service” class-এ, যারা object থেকে ডেটা টেনে নেয়, হিসাব করে, আবার setter দিয়ে বসিয়ে দেয়। Fowler-এর মতে এটি object-oriented নকশার মূল সুবিধা — ডেটা আর তার নিয়ম এক জায়গায় — হারিয়ে ফেলে, অথচ object-এর জটিলতা ঠিকই বহন করে। এই পর্বের Student-এর প্রথম খসড়া ঠিক এমন: private ডেটা, কিন্তু নিয়মটি (GPA = গড়) বাইরের সবার দায়িত্ব।
এর সাথে জড়িত একটি নকশা-পরামর্শ tell, don't ask, যা Andy Hunt আর Dave Thomas (The Pragmatic Programmer-এর লেখক) জনপ্রিয় করেছেন: object-কে তার অবস্থা জিজ্ঞেস করে বাইরে সিদ্ধান্ত নেবেন না; তাকে বলুন কী করতে হবে, সিদ্ধান্ত সে নিজে নিক। if (acc.balance() >= x) acc.setBalance(acc.balance() - x); — “জিজ্ঞেস” করা কোড, যেখানে নিয়মটি ডাকার জায়গায় ছড়িয়ে গেছে, আর প্রতিটি ডাকার জায়গায় আবার লিখতে হবে। acc.withdraw(x) — “বলে দেওয়া” কোড, নিয়ম এক জায়গায়।
কিন্তু উল্টো দিকের যুক্তিও শক্ত, আর C++ সম্প্রদায় সেটি গুরুত্ব দিয়ে নেয়। সব ডেটার নিয়ম থাকে না। একটি Point{x, y}, একটি কনফিগারেশনের মান, নেটওয়ার্কে পাঠানো একটি মেসেজ — এদের ফিল্ডগুলো স্বাধীন, যেকোনো মান বৈধ। এখানে getter-setter দিয়ে মুড়ে দেওয়া শুধু কোড লম্বা করে, কোনো সুরক্ষা দেয় না। Core Guidelines-এর C.131 সরাসরি বলে: তুচ্ছ getter আর setter এড়িয়ে চলুন। এমন ক্ষেত্রে খোলা struct সৎ নকশা — সবাই বোঝেন এখানে কোনো লুকানো নিয়ম নেই। পর্ব ২-এ উল্লেখ করা data-oriented design-ও এই দিকে টানে: গেম ইঞ্জিনের মতো ক্ষেত্রে লাখো object-এর ডেটা সাধারণ array-তে রেখে আলাদা ফাংশন দিয়ে একসাথে প্রক্রিয়া করা অনেক দ্রুত।
তাহলে সীমারেখা কোথায়? প্রশ্নটি “OOP ভালো, না খারাপ” নয়, বরং “এই ডেটার কি কোনো invariant আছে?”। থাকলে ডেটা আর নিয়ম একসাথে, ডেটা private, আর ফাংশনের নাম বাস্তবের কাজ। না থাকলে সরল struct, আর কাজগুলো free function. সবচেয়ে খারাপ অবস্থা মাঝামাঝিটি — নিয়ম আছে, কিন্তু setter দিয়ে খোলা — যা struct-এর সরলতাও দেয় না, class-এর সুরক্ষাও দেয় না।
মূল উপলব্ধি: encapsulation-এর উদ্দেশ্য লুকানো নয়, নিয়ম রক্ষা। যেখানে রক্ষা করার মতো নিয়ম আছে, সেখানে আচরণ ডেটার পাশে রাখুন; যেখানে নেই, সেখানে ডেটাকে ডেটাই থাকতে দিন।
প্র ০৪ভাষা যদি struct আর class-এ প্রায় কোনো পার্থক্যই না রাখে, তাহলে C++ Core Guidelines (C.2, C.8) কেন কোনটি কখন লিখতে হবে তা নিয়ে আলাদা নিয়ম দেয়?
ঐতিহাসিক কারণটা আগে। Stroustrup চেয়েছিলেন C-র প্রতিটি struct বৈধ C++ থাকুক, তাই struct-এর ডিফল্ট public রাখতে হলো। আবার নতুন class-এ তিনি চেয়েছিলেন নিরাপদ ডিফল্ট — যা আপনি খুলে না দেন, তা বন্ধ — তাই class-এর ডিফল্ট private. দুটি আলাদা ধারণা না বানিয়ে তিনি একটিই যন্ত্র রাখলেন, শুধু ডিফল্টে পার্থক্য। ফলে ভাষার নিয়মে struct দিয়ে class-এর সব কাজ করা যায়, উল্টোটাও।
তাহলে বেছে নেওয়ার দায়িত্ব পড়ল মানুষের ঘাড়ে, আর এখানেই C++ Core Guidelines-এর ভূমিকা। Stroustrup আর Herb Sutter-এর নেতৃত্বে ২০১৫ সাল থেকে লেখা এই নির্দেশিকা কয়েকটি নিয়মে বিষয়টি পরিষ্কার করে। C.2: class ব্যবহার করুন, যদি টাইপটির একটি invariant থাকে; struct ব্যবহার করুন, যদি data member-গুলো স্বাধীনভাবে বদলাতে পারে। C.8: কোনো member non-public হলে struct নয়, class লিখুন। আর এর সাথে C.4 বলে, একটি ফাংশনকে member বানান কেবল তখনই, যখন তার class-এর ভেতরের উপস্থাপনা (private ডেটা) সরাসরি ছোঁয়া দরকার।
এই রীতির মূল্য হলো পাঠকের কাছে তথ্য পৌঁছানো। কোড একবার লেখা হয়, কিন্তু পড়া হয় শতবার। struct Point দেখে পাঠক সঙ্গে সঙ্গে জানেন: সব ফিল্ড খোলা, যেকোনো মান বসানো নিরাপদ, aggregate initialization (Point{3, 4}) চলবে। class BankAccount দেখে জানেন: সাবধান, এখানে নিয়ম আছে, ফিল্ডে সরাসরি হাত দেওয়া যাবে না, constructor আর member function-এর মধ্য দিয়ে যেতে হবে। একটি শব্দ পুরো টাইপের চুক্তি ঘোষণা করে দিচ্ছে। clang-tidy-র মতো টুল এই নিয়মগুলোর কিছু অংশ যাচাইও করতে পারে।
রীতিটি কঠোর আইন নয়। standard library-তেই উদাহরণ আছে, যেখানে struct-এ ফাংশন আছে (যেমন std::pair-এর swap), আর অনেক প্রকল্পের নিজস্ব রীতি আছে — কেউ শুধু POD ডেটার জন্য struct রাখেন, কেউ ছোট ফাংশন-object-এর জন্যও struct লেখেন, কারণ তাতে public: লিখতে হয় না। গুরুত্বপূর্ণ হলো একটি প্রকল্পের ভেতরে একই নিয়ম মেনে চলা, যাতে শব্দটি সত্যিই কিছু বোঝায়।
মূল উপলব্ধি: যেখানে ভাষা দুটি পথ খোলা রাখে, সেখানে বেছে নেওয়াটি নিজেই একটি বার্তা। struct বা class লেখা মানে পাঠককে আগেভাগে বলে দেওয়া — “এই টাইপের কোনো নিয়ম আছে কি না।”
প্র ০৫Scott Meyers বলেছেন, অনেক ক্ষেত্রে member function-এর চেয়ে non-member ফাংশন encapsulation বাড়ায়। এই আপাত-উল্টো দাবির যুক্তি কী?
২০০০ সালে C/C++ Users Journal-এ Scott Meyers একটি প্রবন্ধ লেখেন — “How Non-Member Functions Improve Encapsulation”. পরে Effective C++-এর তৃতীয় সংস্করণে এটি একটি নিয়ম হয়: member function-এর চেয়ে non-member non-friend ফাংশন পছন্দ করুন। অনেকের কাছে এটি শুনতে অদ্ভুত লাগে — object-oriented মানে তো সব কাজ object-এর ভেতরে?
Meyers-এর যুক্তি শুরু হয় একটি মাপকাঠি দিয়ে। encapsulation কতটা, তা মাপার একটি উপায়: private ডেটা বদলালে কত কোড ভাঙতে পারে? উত্তর — যত ফাংশন private ডেটা ছুঁতে পারে, ঠিক ততটি। প্রতিটি member function (আর friend) private ডেটায় পৌঁছাতে পারে; একটি সাধারণ free function পারে না, সে শুধু public ফাংশন ডাকতে পারে। তাই যে কাজ public ফাংশন দিয়েই করা যায়, সেটি member বানালে private-এ পৌঁছানো ফাংশনের সংখ্যা অকারণে বাড়ে — encapsulation কমে। BankAccount-এর invariant ভাঙলে আপনাকে প্রতিটি member function পরীক্ষা করতে হবে; দুটি হিসাবের মোট বের করার total(a, b) যদি free function হয়, সে সন্দেহের তালিকায় আসেই না।
দ্বিতীয় লাভ নমনীয়তা। member function শুধু class-এর লেখক যোগ করতে পারেন; free function যে কেউ, যেকোনো header-এ লিখতে পারেন, class না ছুঁয়েই। standard library এই দর্শনে চলে: std::sort, std::find কোনো container-এর member নয়, তাই একবার লিখে সব container-এ চলে (পর্ব ৩৩)। উল্টো উদাহরণ হিসেবে প্রায়ই বলা হয় std::string-এর কথা — এর member function শতাধিক, যার অনেকগুলো free function হতে পারত। Herb Sutter তার GotW সিরিজে (“Monoliths Unstrung”) দেখিয়েছিলেন, এর বড় অংশ member না হয়েও লেখা যায়।
তাহলে কখন member? যখন ফাংশনটির সত্যিই private ডেটা লাগে (withdraw), যখন ভাষা বাধ্য করে (constructor, operator=, virtual ফাংশন), বা যখন কাজটি object-এর মূল পরিচয়ের অংশ। আর C++-এর ADL (পর্ব ১১) নিশ্চিত করে যে একই namespace-এ রাখা free function টাইপের সাথে স্বাভাবিকভাবে খুঁজে পাওয়া যায়, তাই “ইন্টারফেস” বলতে শুধু class-এর ভেতরের ফাংশন নয় — একই namespace-এর সংশ্লিষ্ট free function-ও তার অংশ।
মূল উপলব্ধি: encapsulation মানে যত বেশি ফাংশন class-এর ভেতরে ঢোকানো নয়, বরং private ডেটায় পৌঁছানোর দরজা যত কম রাখা। class-এর ভেতরে রাখুন শুধু সেই ছোট কেন্দ্র, যা নিয়ম রক্ষা করে; বাকি সব তার চারপাশে free function.
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
struct না class? কারণসহ বলুন — (ক) একটি মানচিত্রের বিন্দু: অক্ষাংশ আর দ্রাঘিমাংশ (খ) একটি ট্রেনের টিকিট, যেখানে আসন নম্বর ১ থেকে আসনসংখ্যার মধ্যে থাকতে হবে আর একই আসন দুবার বিক্রি হবে না (গ) একটি ফাংশনের ফেরত দেওয়া দুটি মান: সর্বনিম্ন আর সর্বোচ্চ নম্বর (ঘ) একটি ক্রিকেট ইনিংসের স্কোরবোর্ড, যেখানে উইকেট ১০-এর বেশি হতে পারে না।
(ক) struct — দুটি মান স্বাধীন, কোনো সম্পর্কের নিয়ম নেই (চাইলে সীমা যাচাই যোগ করা যায়, তবে মূলত ডেটার বাক্স)। (খ) class — আসনের সীমা আর “দুবার নয়”-এর নিয়ম আছে, তাই ডেটা private আর
book(seat)-এর মতো যাচাই করা ফাংশন। (গ) struct (বাstd::pair) — শুধু দুটি ফল একসাথে পাঠানো। (ঘ) class — উইকেট ০ থেকে ১০, রান কখনো কমে না;addRuns,addWicketভেতরে যাচাই করবে। -
আউটপুট অনুমান: উপরের
Walletclass (staticcount_,id_ = ++count_) দিয়েmain-এ লেখা:Wallet a; { Wallet b, c; } Wallet d; std::println("{} {}", d.id(), Wallet::count());— কী প্রিন্ট হবে?4 4. a পায় 1, b পায় 2, c পায় 3, d পায় 4. ভেতরের ব্লক শেষে b আর c ধ্বংস হলেওcount_কমে না, কারণ কেউ তাকে কমানোর কোড লেখেনি — এটি “মোট কয়টি তৈরি হয়েছে” গোনে, “এখন কয়টি বেঁচে আছে” নয়। বেঁচে থাকা গুনতে হলে destructor-এ--count_লাগবে (পর্ব ২১)। -
বাগ শিকার: একটি AI এই class দিয়েছে —
class Wallet { public: double balance; void setBalance(double b) { balance = b; } double getBalance() { return balance; } };আর এটি ব্যবহার হচ্ছেvoid show(const Wallet& w) { std::println("{}", w.getBalance()); }-এ। কয়টি সমস্যা খুঁজে পান?অন্তত চারটি। (১)
balancepublic — setter থাকুক বা না থাকুক, যে কেউ সরাসরিw.balance = -500লিখতে পারে। (২)setBalanceকোনো যাচাই করে না; এর বদলেcashIn/sendMoney-এর মতো যাচাই করা ফাংশন দরকার। (৩)getBalance()-এconstনেই, তাইshow-এরconst Wallet&-এ ডাকা compile error: “passing 'const Wallet' as 'this' argument discards qualifiers”. (৪) টাকার জন্যdouble— পয়সায়long longরাখা নিরাপদ। -
নিজে লিখুন (কোড): উপরের
bank_account.cpp-এ একটিtransferTo(BankAccount& other, long long taka)member function যোগ করুন, যা এই হিসাব থেকে টাকা কমিয়েother-এ যোগ করে, আর ব্যর্থ হলে কোনো হিসাবই বদলায় না। তারপর ভাবুন: এটি member হওয়া কি জরুরি, নাকি free function হতে পারত?যেমন:
bool transferTo(BankAccount& other, long long taka) { if (!withdraw(taka)) return false; other.deposit(taka); return true; }. আগেwithdraw— ব্যর্থ হলে সাথে সাথেfalse, কিছুই বদলায়নি। withdraw সফল হলে টাকা ধনাত্মক, তাইdepositব্যর্থ হবে না। খেয়াল করুন, এখানে শুধু publicwithdrawআরdepositব্যবহার হয়েছে, privatebalance_সরাসরি ছোঁয়া হয়নি — তাই Meyers-এর যুক্তিতে এটি একটি free function হতে পারত:bool transfer(BankAccount& from, BankAccount& to, long long taka). (একই হিসাবে নিজের কাছে transfer, মানেa.transferTo(a, x)-ও নিরাপদ: কমে, তারপর আবার যোগ হয়।)
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমC++-এ acc.withdraw(9000) ডাকা হলো, কিন্তু ব্যালান্স মাত্র 3000. ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- compiler
acc-এর ঠিকানা লুকানো প্যারামিটার হিসেবে পাঠায় - ফাংশনের ভেতরে
this=&acc balance_পড়া হয়this->balance_হিসেবে (3000)- যাচাই: 9000 > 3000 — নিয়ম ভাঙবে
- কিছু না বদলে
falseফেরত
C++-এ struct আর class-এর মধ্যে ভাষার নিয়মে মূল পার্থক্য কী?
- struct-এ member function রাখা যায় না
- শুধু ডিফল্ট access — struct-এ public, class-এ private
- struct-এর object stack-এ, class-এর object heap-এ থাকে
- class থেকে inheritance করা যায়, struct থেকে নয়
একটি C++ class-এ long long balance() const; আর void deposit(long long); আছে। const BankAccount& r = acc; লেখার পর কোনটি compile error দেয়?
- r.balance();
- acc.deposit(100);
- r.deposit(100);
- acc.balance();
C++-এ একটি non-static member function-এর ভেতরে this-এর মান কী?
- যে object-এর ওপর ফাংশনটি ডাকা হয়েছে, তার ঠিকানা
- ফাংশনটির নিজের ঠিকানা
- class-এর সংজ্ঞার ঠিকানা
- সবসময় nullptr, যদি না নিজে বসানো হয়
একটি C++ class-এ একটি int data member, তিনটি সাধারণ member function আর একটি static int member আছে। সাধারণ 64-bit সিস্টেমে এর sizeof কত হওয়ার কথা?
- ৪ + তিনটি ফাংশনের জন্য তিনটি pointer
- ৮ (int + static int)
- ০
- ৪
C++-এ class Account { long long balance_; };-এর একটি member function class-এর বাইরে সংজ্ঞায়িত করতে সঠিক রূপ কোনটি?
- bool withdraw(long long t) { ... }
- bool Account::withdraw(long long t) { ... }
- bool Account.withdraw(long long t) { ... }
- bool Account->withdraw(long long t) { ... }
একটি ব্যাংক হিসাবের C++ class-এ invariant রক্ষার সবচেয়ে ভালো নকশা কোনটি?
- balance public রাখা, আর মন্তব্যে নিয়ম লিখে দেওয়া
- balance private, সাথে যাচাইহীন getBalance আর setBalance
- balance private, আর যাচাই করা deposit/withdraw দিয়েই শুধু বদলানো
- balance static রাখা, যাতে সবাই একই মান দেখে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- class নকশা, object আসল জিনিস। প্রতিটি object-এর ডেটা নিজের; member function সবার জন্য একটি, আর object-এর আকারে যোগ হয় না।
- class-এর আসল কাজ invariant রক্ষা। নিয়ম-জড়িত ডেটা
private; বদলানোর পথ শুধু যাচাই করা public ফাংশন। struct আর class-এর পার্থক্য শুধু ডিফল্ট access. this= যে object ফাংশনটি ডেকেছে, তার ঠিকানা;return *this;দিয়ে chain করা যায়। static ফাংশনেthisনেই।- যে member function বদলায় না, তার নামের পরে
const. const object আর const reference-এ শুধু এমন ফাংশনই চলে। - অন্ধ getter-setter নয়; বাস্তব কাজের নাম, হিসাব করা যায় এমন মান জমা নয়, ছোট public অংশ, আর private না ছুঁলে free function.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২১ · constructor ও destructorপরবর্তী পর্বজন্ম থেকেই বৈধ object — initializer list, explicit আর ধ্বংসের ক্রম।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C প্রোগ্রামিং · struct ও unionগভীরে যানC-র struct কীভাবে মেমরিতে বসে — C++-এর class-এর পূর্বপুরুষ।