পাঠ ৪৭ · ৫৮-এর মধ্যে · মডিউল ১০
Home / Courses / Concepts of Programming Languages & Compiler Design / অ্যারে ও রেকর্ড

অ্যারে, রেকর্ড ও পয়েন্টার

Arrays, records & pointers
৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • 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 হিসাব।

Array — base=10, elem_size=4 [0] addr=10 [1] addr=14 [2] addr=18 [3] addr=22 [4] addr=26 Record — base=40 (id:1B, age:1B, salary_cents:4B) id addr=40 age addr=41 salary_cents addr=42
Array-এর প্রতিটি বক্স সমান সাইজ (elem_size=4) নেয়; Record-এর বক্সগুলো তাদের নিজস্ব ফিল্ড-সাইজ অনুযায়ী চওড়া — কিন্তু দুটোরই ঠিকানা base + offset সূত্রেই বের হয়।

৩ · Pointer/Reference — ইনডাইরেক্ট অ্যাক্সেস

PointerPointer / Referenceএকটি মেমরি অ্যাড্রেস ধরে রাখা একটি ভ্যালু — অন্য কোথাও রাখা একটি ভ্যালুতে ইনডাইরেক্ট অ্যাক্সেস দেয়। হলো এমন একটি ভ্যালু যা নিজে একটি মেমরি অ্যাড্রেস ধরে রাখে — অন্য কোথাও রাখা একটি ভ্যালুতে ইনডাইরেক্ট অ্যাক্সেস দেয়। dereference() করা মানে সেই ধরে-রাখা অ্যাড্রেসে গিয়ে আসল মানটি পড়ে আনা। পয়েন্টার বাস্তব-জগতের একটি সাধারণ বাগ-উৎসও বটে — একটি dangling pointer (এমন অ্যাড্রেস যেখানে আর বৈধ ডেটা নেই) dereference করলে অনির্দিষ্ট আচরণ হতে পারে। কিছু ভাষা এই পুরো বাগ-ক্যাটাগরি অনেকটাই দূর করতে গার্বেজ কালেকশন ব্যবহার করে (M12/L54-এ বিস্তারিত)।

Computer Architecture কোর্সের সাথে সম্পর্ক

এই base + i * elem_size ও base + offset সূত্র দুটো আসলে Computer Architecture কোর্সের অ্যাড্রেসিং মোড পাঠ-এই বাস্তবায়িত হার্ডওয়্যার অ্যাড্রেসিং মোডের সরাসরি সফটওয়্যার-সাইড প্রতিফলন — একটি কম্পাইলার array[i] বা record.field-কে ঠিক এই ধরনের base+offset addressing mode ইনস্ট্রাকশনে রূপান্তর করে।

৪ · সিমুলেটেড মেমরিতে বাস্তবায়ন ও যাচাই

নিচের কোড সেলে একটি সরল Memory ক্লাস (একটি ফ্ল্যাট Python লিস্টকে "র মেমরি" হিসেবে ব্যবহার করে) দিয়ে Array, Record, ও Pointer — তিনটেই বাস্তবায়ন করা হয়েছে। প্রতিটি ক্ষেত্রেই কোডের হিসাব করা ঠিকানা হাতে-হিসাব করা প্রত্যাশিত ঠিকানার সাথে মিলিয়ে assert দিয়ে যাচাই করা হয়েছে।

Python
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-এ আগের ফিল্ডগুলোর সাইজের যোগফল (কারণ ফিল্ডগুলো ভিন্ন সাইজের হতে পারে)।
মূল কথা · Key takeaway

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-এর ক্ষেত্রে ঠিকানাটি ইতিমধ্যে হিসাব করে ধরে রাখা হয়েছে — এটাই পয়েন্টারের ইনডাইরেকশনের মূল ধারণা: একটি অ্যাড্রেসকে নিজে একটি ভ্যালু হিসেবে সংরক্ষণ ও পাস করা যায়।

অনুশীলন

  1. হাতে হিসাব করুন: উপরের কোডে 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) অ্যাড্রেস ফর্মুলাকে প্রভাবিত করে না, শুধু বৈধ ইনডেক্স রেঞ্জ নির্ধারণ করে।

  2. চিন্তা করুন: উপরের কোডে যদি ptr = Pointer(memory, address=999) (memory-এর সাইজ 64-এর বাইরে) বানিয়ে dereference() কল করা হতো, কী হতো — এবং বাস্তব সিস্টেমে এর সমতুল্য বাগটির নাম কী?

    Python-এ এটি একটি IndexError ছুড়বে, কারণ self.cells লিস্টের সাইজ মাত্র 64। বাস্তব সিস্টেমে (যেখানে মেমরি এত কড়াভাবে সীমাবদ্ধ প্রতিটি অ্যাক্সেসে চেক করা হয় না) এই একই ধরনের ভুল — একটি অবৈধ/অ্যাক্সেস-করা-যায়-না এমন ঠিকানা dereference করা — সাধারণত dangling pointer বা segmentation fault নামে পরিচিত, প্রোগ্রামিংয়ের একটি ক্লাসিক, গুরুতর বাগ ক্যাটাগরি।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

পূর্ববর্তী পাঠ
অ্যাবস্ট্রাক্ট ডেটা টাইপ ও এনক্যাপসুলেশন