সি++ (C++) অ্যারে এবং ভেক্টর (Arrays & Vectors)
যেকোনো লিস্ট (List) স্টোর করার দুটি উপায় (Two Ways to Store a List)
ধরুন আপনি নতুন একটি অ্যাপার্টমেন্টে (new apartment) শিফট (moving) করছেন। এখন আপনি চাইলে আপনার বইগুলোকে (books) মেঝের ওপর খুব সুন্দরভাবে একটি সারিতে (neat row) সাজিয়ে রাখতে পারেন — যাকে মূলত একটি সি-স্টাইল অ্যারে (C-style array) বলা হয়ে থাকে। এক্ষেত্রে আপনার কাছে ঠিক মোট কতগুলো (how many) বই আছে তা আপনি নিজেও খুব ভালোভাবে জানেন, এদের এই সারিটি (row) কখনোই আর বড় হবে না (never grows), এবং ঘটনাক্রমে (accidentally) আপনি যদি কখনো এর ওপর পা (step) দিয়ে দেন তবে হয়তো আপনি আপনার কোনো দরকারি জিনিসকেই ভেঙে বা পিষে (crush) দিতে পরেন।
অথবা এর বদলে আপনি বর্ধনযোগ্য তাকগুলোসহ (expandable sections) একটি বুকশেলফ (bookshelf) কিনে নিতে পারেন — যা মূলত এখানকার std::vector-এর মতোই কাজ করে। এটি মূলত সবার প্রথমে আপনার নির্দিষ্ট করা কিছু জায়গা (some room) নিয়ে এর কাজ শুরু করে, আর যখন এর সমস্ত জায়গা শেষ (run out) হয়ে যায়, তখন এটি খুব নীরবেই (quietly) তার থেকে আরও অনেক বড় (bigger shelf) একটি বুকশেলফ নিয়ে আসে এবং তার ভেতরের সমস্ত জিনিসগুলোকে ওই নতুন সেলফটির ওপর সরিয়ে (moves everything over) রাখে। এতে আপনি কিছু টেরও (barely notice) পাবেন না।
সি-স্টাইল অ্যারে (C-Style Arrays) — পুরনো প্রহরীরা (The Old Guard)
সি++ (C++) মূলত এর C ভাষা (C) থেকেই এই সমস্ত র অ্যারেগুলোকে (raw arrays) উত্তরাধিকারসূত্রে (inherited) পেয়ে থাকে। এগুলো মূলত অনেক বেশি ফাস্ট বা দ্রুত (fast) হয়ে থাকে যা সরাসরি স্ট্যাকের (stack) ওপরেই বসে পড়ে, তবে এর বেশ কিছু গুরুতর ট্রেড-অফ বা ক্ষতিকর দিকও (serious trade-offs) রয়েছে:
- কম্পাইলের সময়ই (compile time) এদের আকার (Size) সম্পূর্ণ ফিক্সড বা নির্দিষ্ট (fixed) করে দেওয়া হয়
- এর মধ্যে কোনো বাউন্ড চেকিং বা সীমা যাচাই (bounds checking) করার সুযোগ নেই — এর সীমানার বাইরে চলে গেলেই এটি নীরবে (silently) মেমোরিটিকে পুরোপুরি করাপ্ট বা ধ্বংস (corrupt memory) করে দেবে
- কোনো ফাংশনে (functions) পাস (passed) করার সময় এগুলো সরাসরি পয়েন্টারে বা pointers-এ ক্ষয় (decay) হয়ে যায় এবং তাদের আকার বা সাইজটিকে (size) পুরোপুরি হারিয়ে (losing) ফেলে
তাই নতুন যেকোনো কোডের (new code) জন্য আপনার প্রায় কখনই (almost never) এই র অ্যারেগুলোর (raw arrays) প্রয়োজন পড়বে না। কিন্তু যেহেতু বিভিন্ন লেগাসি কোডবেস (legacy codebases) বা যেকোনো সাধারণ ইন্টারভিউগুলোর (interview questions) সময় আপনার এসবের দরকার হতে পারে, তাই চলুন সেগুলো সম্পর্কে সংক্ষেপে (briefly) কিছুটা জেনে নিই।
সাধারণ বা বেসিক সি-স্টাইল অ্যারে (C-Style Array Basics)
std::array — আধুনিক বা মডার্ন ফিক্সড-সাইজ অ্যারে (The Modern Fixed-Size Array)
সি++১১ (C++11) ভার্সনটিতে মূলত সি-স্টাইল অ্যারেগুলোর (C-style arrays) আরও একটি নিরাপদ বা সেফ র্যাপার (safe wrapper) হিসেবে std::array-টিকে ইনট্রোডিউস বা চালু করা হয়েছিল (introduced)। এটি মূলত তার নিজের আকারটিকে (size) আগে থেকেই জানে বা চেনে, নিজে থেকেই বিভিন্ন ইটারেটরগুলোকে (iterators) সাপোর্ট বা সমর্থন (supports) করে, এবং যেকোনো এসটিএল অ্যালগরিদমগুলোর (STL algorithms) সাথে সাধারণ র অ্যারের (raw array) মতোই কাজ করে — আর এর সবচেয়ে ভালো দিকটি হলো এটি জিরো ওভারহেডে (zero overhead) সমস্ত কাজ করে থাকে।
তাই যখন আপনি কম্পাইলের সময় (compile time) এর সাইজটিকে (size) আগে থেকেই জানবেন এবং এটি যে আর পরিবর্তন (change) হবে না তা সম্পর্কেও নিশ্চিন্ত থাকবেন, ঠিক তখনই এই std::array-টি ব্যবহার করুন।
std::array — নির্দিষ্ট সাইজ বা ফিক্সড সাইজ (Fixed Size), সম্পূর্ণ নিরাপত্তা (Full Safety)
std::vector — শো-এর প্রধান তারকা (The Star of the Show)
এটি মূলত এমন একটি কনটেইনার (container) যা আপনি আপনার সি++ (C++) প্রোগ্রামিং জীবনের ৯০% (90%) সময়েই ব্যবহার করবেন। যেকোনো ভেক্টর (vector) হলো মূলত একটি ডায়নামিক অ্যারে (dynamic array) — অর্থাৎ যা মূলত আপনার প্রয়োজন অনুসারে নিজেই নিজের সাইজটিকে বড় (grows) বা ছোট (shrinks) করে নিতে পারে। এটি মূলত ব্যাকগ্রাউন্ডে (Under the hood) হিপ মেমোরির (heap memory) কিছু অংশ নিয়ে কাজ (manages) করে এবং যখন এর জায়গার অভাব (runs out of room) দেখা দেয়, তখন এটি নিজেই নিজেকে পুনরায় অ্যালরকেট (reallocates) করে নেয়।
প্রধান বা মূল ধারণা (Key Concepts): সাইজ (Size) বনাম ক্যাপাসিটি (Capacity)
যেকোনো ভেক্টরে মূলত দুধরনের ভ্যালুর (numbers) দরকার পড়ে:
- সাইজ (Size) — এতে বর্তমানে ঠিক কতগুলো উপাদান (elements) রাখা বা স্টোর (stored) করা আছে
- ক্যাপাসিটি (Capacity) — এতে মোট কতটা মেমোরি বরাদ্দ (allocated) বা অ্যালরকেট করা হয়েছে (সর্বদা ≥ সাইজ বা size)
যখন আপনি এটিকে push_back করেন এবং এর সাইজটি (size) ক্যাপাসিটিকে (capacity) ছাড়িয়ে (exceed) যায়, তখন এই ভেক্টরটি (vector) মূলত একটি নতুন এবং অনেক বড় ব্লককে (new, larger block) (সাধারণত এর পুরনো বা old ক্যাপাসিটিটির ঠিক দ্বিগুণ বা 2x) অ্যালরকেট (allocates) করে নেয় এবং এর আগের ব্লকের (old block) সমস্ত কিছুকেই এই নতুন ব্লকে কপি (copies) করে নেয়, এবং শেষে ওই পুরোনো ব্লকটিকে (old block) রিলিজ বা ফ্রি (frees) করে দেয়। আর ঠিক এ কারণেই এই push_back-টিকে অ্যামর্টাইজড ও(১) (amortized O(1)) বলা হয়ে থাকে — এখানকার বেশিরভাগ কলই (calls) তাৎক্ষণিক বা ইনস্ট্যান্ট (instant) হয়ে থাকে, তবে কিছু কিছু সময় এটি পুনরায় অ্যালরকেশনের (reallocation) জন্যও কল করে থাকে।
ভেক্টর অপারেশনস (Vector Operations) — প্রয়োজনীয় জিনিসগুলো (The Essentials)
at() বনাম [] — সাইজ বা সেফটি (Safety) বনাম স্পিড (Speed)
vec.at(i) ব্যবহার করুন — এটি সীমার বাইরে (out-of-bounds) অ্যাক্সেসের (access) ক্ষেত্রে ব্যতিক্রম (exception) ছুঁড়ে দেয়। vec[i] দ্রুততর কিন্তু সীমার বাইরে গেলে চুপচাপ মেমোরি ধ্বংস (silently corrupts memory) করে। শুধুমাত্র পারফরম্যান্স-ক্রিটিকাল (performance-critical) পাথে []-এ যান যখন আপনি সঠিকতা (correctness) যাচাই বা ভেরিফাই (verified) করেছেন।reserve() — অপ্রয়োজনীয় বা আননেসেসরি রিঅ্যালরকেশনগুলো (Unnecessary Reallocations) এড়ানো (Avoiding)
2D বা দ্বিমাত্রিক ভেক্টর (2D Vectors) — ভেক্টরের ভেক্টর (Vector of Vectors)
কখন কোনটি ব্যবহার করা উচিত? (When to Use What?)
- std::vector — এর ডিফল্ট চয়েস (default choice)। এটিকে ব্যবহার না করার জন্য কোনো নির্দিষ্ট কারণ বা স্পেসিফিক রিজন (specific reason) না থাকলে, বেশিরভাগ ক্ষেত্রেই এটিকে ব্যবহার করুন।
- std::array — যখন এর আকার বা সাইজটি (size) আগে থেকেই কম্পাইল টাইমেই (compile time) জানা থাকে এবং ফিক্সড (fixed) করা থাকে (যেমন, RGB রঙ বা color = 3 মান বা values)।
- সি-স্টাইল অ্যারে (C-style arrays) — আপনার নতুন তৈরি কোনো কোডে (new code) এটিকে প্রায় কখনই (almost never) প্রয়োগ করবেন না। শুধুমাত্র সি-এর (C) কোনো লাইব্রেরির (libraries) সাথে কাজ করার ক্ষেত্রেই এগুলোকে ব্যবহার করা হয়।
আপনি যদি আগে থেকেই এর এই reserve()-টিকে ঠিকভাবে কল (reserve() ahead) করে নেন, তবে আপনার যেকোনো সিকোয়েন্সিয়াল অ্যাক্সেসের (sequential access) ক্ষেত্রে ভেক্টরগুলো (vectors) পারফরম্যান্সের (performance) দিক দিয়ে অনায়াসেই এর র অ্যারেগুলোর (raw arrays) সমকক্ষ (match) হয়ে যাবে। এখানকার কম্পাইলারটি (compiler) মূলত নিজেই এতটাই স্মার্ট বা চতুর (smart enough) যে, এটি নিজে থেকেই সমস্ত ওভারহেডগুলোকে (overhead) দূরে সরিয়ে রাখতে পারে।