অ্যারে, রেকর্ড ও পয়েন্টার
এই পাঠে যা শিখবেন
- Array-এর ঠিকানা গণনার সূত্র এবং কেন এটি O(1)
- Record/struct-এর ফিল্ড অফসেট কীভাবে হিসাব হয়
- Pointer/reference কীভাবে ইনডাইরেক্ট অ্যাক্সেস দেয়
- একটি সিমুলেটেড মেমরিতে বাস্তব
Array,Record,Pointerক্লাস, হাতে-হিসাবের সাথে মিলিয়ে যাচাই
১ · Array — সমসত্ব কালেকশন ও অ্যাড্রেস ক্যালকুলেশন
ArrayArrayএকই টাইপের এলিমেন্টের একটি ফিক্সড-সাইজ কালেকশন, একটি সংখ্যাসূচক ইনডেক্স দিয়ে অ্যাক্সেস করা হয়।
হলো একই টাইপের এলিমেন্টের একটি (অনেক ক্লাসিক ভাষায় ফিক্সড-সাইজ) কালেকশন, যা একটি সংখ্যাসূচক ইনডেক্স দিয়ে
অ্যাক্সেস করা হয়। একটি array-এর base ঠিকানা থেকে শুরু করে, প্রতিটি এলিমেন্ট
elem_size পরিমাণ জায়গা নেয় বলে, ইনডেক্স i-এর এলিমেন্টের ঠিকানা সরাসরি সূত্রে বের
করা যায়:
$$\text{address}(i) = base + i \times elem\_size$$
এটি একটি সরাসরি, constant-time (O(1)) হিসাব — কোনো লুপ বা ট্র্যাভার্সাল লাগে না। এজন্যই array ইনডেক্সিং এত দ্রুত, লিংকড স্ট্রাকচারের (যেখানে i-তম নোড খুঁজতে i ধাপ হাঁটতে হয়) তুলনায়।
২ · Record/Struct — heterogeneous ফিল্ড ও অফসেট ক্যালকুলেশন
Record/StructRecord / Structনামযুক্ত ফিল্ডের একটি ফিক্সড কালেকশন, প্রতিটি ফিল্ড ভিন্ন টাইপের হতে পারে।
হলো নামযুক্ত ফিল্ডের একটি ফিক্সড কালেকশন, যেখানে প্রতিটি ফিল্ড ভিন্ন টাইপের হতে পারে (array-এর বিপরীতে, যা
সমসত্ব)। প্রতিটি ফিল্ডের মেমরি অফসেট হলো তার আগে ডিক্লেয়ার করা সব ফিল্ডের সাইজের যোগফল —
record.field3 অ্যাক্সেস আসলে কম্পাইল হয়ে যায় base_address + offset_of_field3-এ,
array-এর সূত্রের মতোই একটি সরাসরি, constant-time হিসাব।
base + offset সূত্রেই বের হয়।৩ · Pointer/Reference — ইনডাইরেক্ট অ্যাক্সেস
PointerPointer / Referenceএকটি মেমরি অ্যাড্রেস ধরে রাখা একটি ভ্যালু — অন্য কোথাও রাখা একটি ভ্যালুতে ইনডাইরেক্ট অ্যাক্সেস দেয়।
হলো এমন একটি ভ্যালু যা নিজে একটি মেমরি অ্যাড্রেস ধরে রাখে — অন্য কোথাও রাখা একটি ভ্যালুতে
ইনডাইরেক্ট অ্যাক্সেস দেয়। dereference() করা মানে সেই ধরে-রাখা অ্যাড্রেসে গিয়ে
আসল মানটি পড়ে আনা। পয়েন্টার বাস্তব-জগতের একটি সাধারণ বাগ-উৎসও বটে — একটি dangling pointer
(এমন অ্যাড্রেস যেখানে আর বৈধ ডেটা নেই) dereference করলে অনির্দিষ্ট আচরণ হতে পারে। কিছু ভাষা এই পুরো বাগ-ক্যাটাগরি
অনেকটাই দূর করতে গার্বেজ কালেকশন ব্যবহার করে (M12/L54-এ বিস্তারিত)।
এই base + i * elem_size ও base + offset সূত্র দুটো আসলে
Computer Architecture কোর্সের অ্যাড্রেসিং মোড
পাঠ-এই বাস্তবায়িত হার্ডওয়্যার অ্যাড্রেসিং মোডের সরাসরি সফটওয়্যার-সাইড প্রতিফলন — একটি কম্পাইলার
array[i] বা record.field-কে ঠিক এই ধরনের base+offset addressing mode
ইনস্ট্রাকশনে রূপান্তর করে।
৪ · সিমুলেটেড মেমরিতে বাস্তবায়ন ও যাচাই
নিচের কোড সেলে একটি সরল Memory ক্লাস (একটি ফ্ল্যাট Python লিস্টকে "র মেমরি" হিসেবে ব্যবহার করে)
দিয়ে Array, Record, ও Pointer — তিনটেই বাস্তবায়ন করা হয়েছে। প্রতিটি
ক্ষেত্রেই কোডের হিসাব করা ঠিকানা হাতে-হিসাব করা প্রত্যাশিত ঠিকানার সাথে মিলিয়ে assert দিয়ে
যাচাই করা হয়েছে।
class Memory:
"""একটি ফ্ল্যাট 'র মেমরি' সিমুলেশন -- প্রতিটি ইনডেক্স একটি মেমরি অ্যাড্রেস।"""
def __init__(self, size=64):
self.cells = [None] * size
def write(self, address, value):
self.cells[address] = value
def read(self, address):
return self.cells[address]
class Array:
def __init__(self, memory, base_address, elem_size, length):
self.memory = memory
self.base_address = base_address
self.elem_size = elem_size
self.length = length
def address_of(self, i):
if not (0 <= i < self.length):
raise IndexError(f"ইনডেক্স {i} সীমার বাইরে")
return self.base_address + i * self.elem_size
def __setitem__(self, i, value):
self.memory.write(self.address_of(i), value)
def __getitem__(self, i):
return self.memory.read(self.address_of(i))
class Record:
def __init__(self, memory, base_address, fields):
# fields: [(name, size_in_units), ...] -- ডিক্লেয়ার করা ক্রমেই
self.memory = memory
self.base_address = base_address
self.offsets = {}
running_offset = 0
for name, size in fields:
self.offsets[name] = running_offset
running_offset += size
def address_of(self, field_name):
return self.base_address + self.offsets[field_name]
def set_field(self, field_name, value):
self.memory.write(self.address_of(field_name), value)
def get_field(self, field_name):
return self.memory.read(self.address_of(field_name))
class Pointer:
def __init__(self, memory, address):
self.memory = memory
self.address = address
def dereference(self):
return self.memory.read(self.address)
memory = Memory(size=64)
# --- Array: base=10, elem_size=4, length=5 ---
arr = Array(memory, base_address=10, elem_size=4, length=5)
values = [11, 22, 33, 44, 55]
for i, v in enumerate(values):
arr[i] = v
expected_addr_3 = 10 + 3 * 4 # হাতে-হিসাব: base + i*elem_size
print(f"হাতে-হিসাব করা addr(3) = 10 + 3*4 = {expected_addr_3}")
print(f"কোডের হিসাব করা arr.address_of(3) = {arr.address_of(3)}")
assert arr.address_of(3) == expected_addr_3
assert arr[3] == 44
print(f"arr[3] = {arr[3]} (প্রত্যাশিত 44) ঠিকানা মিলেছে: {arr.address_of(3) == expected_addr_3}")
# --- Record: base=40, fields id(1), age(1), salary_cents(4) ---
rec = Record(memory, base_address=40, fields=[("id", 1), ("age", 1), ("salary_cents", 4)])
rec.set_field("id", 7)
rec.set_field("age", 30)
rec.set_field("salary_cents", 500000)
expected_addr_salary = 40 + (1 + 1) # হাতে-হিসাব: base + sum(আগের ফিল্ডগুলোর সাইজ)
print(f"\nহাতে-হিসাব করা addr(salary_cents) = 40 + (1+1) = {expected_addr_salary}")
print(f"কোডের হিসাব করা rec.address_of('salary_cents') = {rec.address_of('salary_cents')}")
assert rec.address_of("salary_cents") == expected_addr_salary
assert rec.get_field("salary_cents") == 500000
print(f"rec.get_field('salary_cents') = {rec.get_field('salary_cents')} ঠিকানা মিলেছে: {rec.address_of('salary_cents') == expected_addr_salary}")
# --- Pointer: array[3]-এর ঠিকানার দিকে নির্দেশ করা ---
ptr = Pointer(memory, address=arr.address_of(3))
print(f"\nptr.dereference() = {ptr.dereference()} (arr[3] = {arr[3]}-এর সমান হওয়া উচিত)")
assert ptr.dereference() == arr[3]
Array.address_of ও Record.address_of দুটোই গঠনগতভাবে একই রকম:
একটি base-এর সাথে একটি হিসাব-করা অফসেট যোগ করা। পার্থক্য শুধু অফসেটের ধরনে — array-এ
i * elem_size (গুণ, কারণ সব এলিমেন্ট সমান সাইজ), record-এ আগের ফিল্ডগুলোর সাইজের যোগফল (কারণ
ফিল্ডগুলো ভিন্ন সাইজের হতে পারে)।
Array ও Record — দুটোই মূলত একই নীতির উপর দাঁড়িয়ে: একটি base ঠিকানা থেকে একটি হিসাব-করা
অফসেট যোগ করে সরাসরি, constant-time-এ যেকোনো এলিমেন্ট/ফিল্ডের ঠিকানা বের করা। Pointer এই একই ঠিকানা-ভিত্তিক
মডেলকে আরেক ধাপ এগিয়ে নেয় — নিজেই একটি ঠিকানা ধরে রেখে ইনডাইরেক্ট অ্যাক্সেস দেয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ array-এর যেকোনো ইনডেক্সে অ্যাক্সেস O(1) হয়, কিন্তু একটি লিংকড লিস্টে i-তম নোডে পৌঁছাতে O(n) সময় লাগে কেন?
Array-এর সব এলিমেন্ট মেমরিতে পরপর, সমান সাইজে সাজানো থাকে বলে base + i *
elem_size সূত্রে সরাসরি ঠিকানা হিসাব করা যায় — কোনো এলিমেন্ট "খুঁজতে" হয় না। একটি লিংকড লিস্টের
নোডগুলো মেমরিতে ছড়িয়ে-ছিটিয়ে থাকে, প্রতিটি নোড শুধু পরের নোডের ঠিকানা জানে — তাই i-তম নোডে পৌঁছাতে
শুরু থেকে i বার "পরের নোড"-এ হাঁটতে হয়, প্রতিটি ধাপে একটি pointer dereference করে।
প্র ০২
উপরের কোডে যদি Record-এর ফিল্ড অর্ডার পাল্টে [("salary_cents", 4), ("id", 1), ("age", 1)] করা হতো, তাহলে id-এর ঠিকানা কী হতো?
অফসেট সবসময় "আগের ফিল্ডগুলোর সাইজের যোগফল" — তাই এখন salary_cents প্রথম হওয়ায় অফসেট 0,
তারপর id-এর অফসেট হবে 4 (salary_cents-এর সাইজ), অর্থাৎ ঠিকানা
base + 4 = 44। এটাই দেখায়, ফিল্ড ডিক্লেয়ারেশনের ক্রম সরাসরি মেমরি লে-আউট
নির্ধারণ করে — record-এর ফিল্ড রিঅর্ডার করা মানে ভিন্ন ঠিকানা বিন্যাস।
প্র ০৩
ptr.dereference() ঠিক কী কাজ করছে, এবং এটি arr[3] সরাসরি পড়ার থেকে কীভাবে আলাদা?
ptr নিজে শুধু একটি সংখ্যা (একটি অ্যাড্রেস, এক্ষেত্রে arr.address_of(3)-এর মান)
ধরে রাখে — dereference() সেই ধরে-রাখা অ্যাড্রেসে গিয়ে memory.read() কল করে
আসল মান বের করে আনে। arr[3]-ও ভেতরে ভেতরে একই কাজ করে (নিজে ঠিকানা হিসাব করে তারপর read
করে), কিন্তু ptr-এর ক্ষেত্রে ঠিকানাটি ইতিমধ্যে হিসাব করে ধরে রাখা হয়েছে — এটাই
পয়েন্টারের ইনডাইরেকশনের মূল ধারণা: একটি অ্যাড্রেসকে নিজে একটি ভ্যালু হিসেবে সংরক্ষণ ও পাস করা যায়।
অনুশীলন
-
হাতে হিসাব করুন: উপরের কোডে
arr-এর জন্যbase_address=100,elem_size=8হলে ইনডেক্স5-এর ঠিকানা কত হবে? কোড পাল্টে যাচাই করুন।সূত্র অনুযায়ী:
address(5) = 100 + 5*8 = 140। কোডেArray(memory, base_address=100, elem_size=8, length=6)বানিয়েarr.address_of(5)প্রিন্ট করলে ঠিক140পাওয়া যাবে — array-এর সাইজ (length) অ্যাড্রেস ফর্মুলাকে প্রভাবিত করে না, শুধু বৈধ ইনডেক্স রেঞ্জ নির্ধারণ করে। -
চিন্তা করুন: উপরের কোডে যদি
ptr = Pointer(memory, address=999)(memory-এর সাইজ 64-এর বাইরে) বানিয়েdereference()কল করা হতো, কী হতো — এবং বাস্তব সিস্টেমে এর সমতুল্য বাগটির নাম কী?Python-এ এটি একটি
IndexErrorছুড়বে, কারণself.cellsলিস্টের সাইজ মাত্র 64। বাস্তব সিস্টেমে (যেখানে মেমরি এত কড়াভাবে সীমাবদ্ধ প্রতিটি অ্যাক্সেসে চেক করা হয় না) এই একই ধরনের ভুল — একটি অবৈধ/অ্যাক্সেস-করা-যায়-না এমন ঠিকানা dereference করা — সাধারণত dangling pointer বা segmentation fault নামে পরিচিত, প্রোগ্রামিংয়ের একটি ক্লাসিক, গুরুতর বাগ ক্যাটাগরি।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ এরপর M11-এ যাচ্ছি — সাবপ্রোগ্রাম, অ্যাক্টিভেশন রেকর্ড ও কল স্ট্যাক।
- Computer Architecture: অ্যাড্রেসিং মোড সহোদর কোর্স এই পাঠের base+offset সূত্র আসলে হার্ডওয়্যার-লেভেলে এই অ্যাড্রেসিং মোডগুলোরই সরাসরি প্রতিফলন।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স Pointer/reference-ভিত্তিক লিংকড স্ট্রাকচার (linked list, tree) এই কোর্সে বিস্তারিত কভার করা হয়েছে।