Lesson পড়তে ৮ মিনিট লাগবে

সি++ (C++) অ্যারে এবং ভেক্টর (Arrays & Vectors)

এখানকার র অ্যারেগুলো (Raw arrays) হলো মূলত মোটরসাইকেলের (motorcycle) মতো — যেগুলো বেশ দ্রুত বা ফাস্ট হলেও (fast), একইভাবে অনেক বিপজ্জনকও (dangerous)। আর ভেক্টরগুলো (Vectors) হলো আধুনিক গাড়ির (car) মতো — যেগুলো একইসাথে দ্রুত এবং নিরাপদ (safe), এবং এগুলো মূলত আপনার যেকোনো ভারী কাজকেও (heavy lifting) অনেক সহজে হ্যান্ডেল (handle) করে নিতে পারে

যেকোনো লিস্ট (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)

#include <iostream>
using namespace std;
int main() {
int scores[5] = {90, 85, 78, 92, 88};
// ইনডেক্স (index) অনুসারে অ্যাক্সেস বা Access করা
cout << "First: " << scores[0] << endl;
cout << "Last: " << scores[4] << endl;
// বিপদ বা Danger! কোনো বাউন্ড চেকিং (bounds checking) করা হচ্ছে না
// scores[10] = 42; // এর কম্পাইল ঠিকভাবেই হয় (Compiles fine), তবে এটি মেমোরিকে করাপ্ট বা নষ্ট (corrupts memory) করে দেয়!
// সাইজ ট্রিক বা Size trick (যদি এটিকে কোনো ফাংশনে বা function-এ পাস (passed) করা হয়, তবে এটি আর কাজ করবে না)
int len = sizeof(scores) / sizeof(scores[0]);
cout << "Length: " << len << endl;
return 0;
}
Output
First: 90
Last:  88
Length: 5

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)

#include <iostream>
#include <array>
using namespace std;
int main() {
array<int, 4> temps = {72, 68, 75, 80};
cout << "Size: " << temps.size() << endl;
cout << "First: " << temps.front() << endl;
cout << "Last: " << temps.back() << endl;
// বাউন্ডস চেকিং বা bounds checking-এর সাথে নিরাপদ অ্যাক্সেস (Safe access)
try {
cout << temps.at(10) << endl; // ছুঁড়ে দেয় বা থ্রো (Throws) করে!
} catch (const out_of_range& e) {
cout << "Caught: " << e.what() << endl;
}
return 0;
}
Output
Size: 4
First: 72
Last:  80
Caught: array::at

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)

#include <iostream>
#include <vector>
using namespace std;
int main() {
// ইনিশিয়ালাইজার লিস্ট (initializer list) দিয়ে তৈরি করা (Create)
vector<int> nums = {10, 20, 30};
// এলিমেন্ট বা উপাদান (elements) যোগ (Add) করা
nums.push_back(40);
nums.push_back(50);
cout << "Size: " << nums.size() << endl;
cout << "Capacity: " << nums.capacity() << endl;
// লাস্ট বা সবশেষ উপাদানটিকে (last element) রিমুভ (Remove) করা
nums.pop_back();
cout << "After pop_back, size: " << nums.size() << endl;
// পজিশন ১ (position 1)-এ প্রবেশ (Insert) করানো
nums.insert(nums.begin() + 1, 15);
// সমস্ত উপাদান বা elements-গুলোকে প্রিন্ট (Print) করা
for (int n : nums) {
cout << n << " ";
}
cout << endl;
return 0;
}
Output
Size: 5
Capacity: 6
After pop_back, size: 4
10 15 20 30 40

at() বনাম [] — সাইজ বা সেফটি (Safety) বনাম স্পিড (Speed)

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<string> names = {"Alice", "Bob", "Charlie"};
// [] সুপার ফাস্ট (fast), কিন্তু এর কোনো বাউন্ড চেকিং বা bounds checking-এর ক্ষমতা নেই
cout << names[0] << endl; // "Alice"
// names[99] = "Oops"; // এটি একটি আনডিফাইন্ড বিহেভিয়ার বা অনির্ধারিত আচরণ (Undefined behavior)!
// at() মূলত বাউন্ডগুলোকে বা bounds চেক (checks) করে এবং এটি রেঞ্জের (range) বাইরে হলে থ্রো (throws) বা ছুড়ে দেয়
try {
cout << names.at(1) << endl; // "Bob"
cout << names.at(99) << endl; // থ্রো (Throws) বা ছুড়ে দেয়!
} catch (const out_of_range& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
Output
Alice
Bob
Error: vector::_M_range_check: __n (which is 99) >= this->size() (which is 3)
Note: ডেভেলপমেন্টের (development) সময় vec.at(i) ব্যবহার করুন — এটি সীমার বাইরে (out-of-bounds) অ্যাক্সেসের (access) ক্ষেত্রে ব্যতিক্রম (exception) ছুঁড়ে দেয়। vec[i] দ্রুততর কিন্তু সীমার বাইরে গেলে চুপচাপ মেমোরি ধ্বংস (silently corrupts memory) করে। শুধুমাত্র পারফরম্যান্স-ক্রিটিকাল (performance-critical) পাথে []-এ যান যখন আপনি সঠিকতা (correctness) যাচাই বা ভেরিফাই (verified) করেছেন।

reserve() — অপ্রয়োজনীয় বা আননেসেসরি রিঅ্যালরকেশনগুলো (Unnecessary Reallocations) এড়ানো (Avoiding)

#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
v.reserve(1000); // আগে থেকেই ১০০০ (1000) উপাদানের (elements) জন্য স্পেস বা জায়গা প্রি-অ্যালোকেট (Pre-allocate) করে রাখা
cout << "Size: " << v.size() << endl; // ০ (0) — এখনো কোনো উপাদান (elements) আসেনি
cout << "Capacity: " << v.capacity() << endl; // ১০০০ (1000) — এর রুম (room) বা জায়গা প্রস্তুত বা ready আছে
for (int i = 0; i < 1000; i++) {
v.push_back(i); // এখানে কোনো প্রকার পুনঃঅ্যালোকেশন বা reallocations হবে না!
}
cout << "Size after filling: " << v.size() << endl;
return 0;
}
Output
Size: 0
Capacity: 1000
Size after filling: 1000

2D বা দ্বিমাত্রিক ভেক্টর (2D Vectors) — ভেক্টরের ভেক্টর (Vector of Vectors)

#include <iostream>
#include <vector>
using namespace std;
int main() {
// ০ (0) দিয়ে ইনিশিয়ালাইজ (initialized) করা একটি 3x4 গ্রিড (grid) তৈরি করা
vector<vector<int>> grid(3, vector<int>(4, 0));
grid[0][0] = 1;
grid[1][2] = 5;
grid[2][3] = 9;
// গ্রিডটিকে (grid) প্রিন্ট (Print) করা
for (const auto& row : grid) {
for (int val : row) {
cout << val << " ";
}
cout << endl;
}
return 0;
}
Output
1 0 0 0
0 0 5 0
0 0 0 9

কখন কোনটি ব্যবহার করা উচিত? (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) দূরে সরিয়ে রাখতে পারে।

চ্যালেঞ্জ

ছোট কুইজ

মাত্র ৩টি এলিমেন্ট (3 elements) থাকা কোনো ভেক্টরে (vector) vec[100]-কে অ্যাক্সেস (access) করলে আসলে কী ঘটতে পারে?
LoopsStrings