সরাসরি পর্বে যান
পর্ব ২২ · ৫৬-এর মধ্যে · মডিউল ৩

struct, union আর enum — বাস্তব জিনিসকে ডেটায় সাজানো

Structs, Unions & Enums — Modelling Real Things as Data

Taught in Bangla, this lesson shows how to model real things as data in C: struct and typedef, the dot and arrow operators, arrays and nesting, passing structs to functions, alignment and padding, enums, unions and bit fields.

এই পর্বে যা শিখবেন

  • বাস্তবের একটি জিনিস দেখে তার data model বানানো — কোন তথ্য রাখবেন, কী টাইপে
  • struct, typedef, ডট ও তীর চিহ্ন, struct-এর array আর struct-এর ভেতরে struct
  • মেমরিতে struct কীভাবে বসে — alignment, padding আর কেন member-এর ক্রম গুরুত্বপূর্ণ
  • enum, union, tagged union আর bit field — কোনটি কখন, কোনটির কী বিপদ
  • AI-এর লেখা struct কোডের সাধারণ ভুল ধরার একটি চেকলিস্ট

১ · একটি জিনিস, অনেকগুলো তথ্য — ডেটা মডেলিং

আপনার কলেজের একজন ছাত্রের কথা ভাবুন। তার একটি নাম আছে, একটি রোল নম্বর আছে, একটি GPA আছে। এই তিনটি তথ্য আলাদা আলাদা টাইপের — নাম হলো লেখা (char-এর array), রোল পূর্ণসংখ্যা (int), আর GPA দশমিক সংখ্যা (double). কিন্তু তিনটি মিলে একজন মানুষ।

এ পর্যন্ত আমাদের হাতে ছিল শুধু আলাদা আলাদা ভ্যারিয়েবল আর একই টাইপের array. তাই ৬০ জন ছাত্রের তথ্য রাখতে গেলে বানাতে হতো তিনটি আলাদা array:

char   names[60][20];
int    rolls[60];
double gpas[60];      /* rolls[7] আর gpas[7] একই ছাত্রের — শুধু আমরা জানি */

এটা কাজ করে, কিন্তু খুব ভঙ্গুর। কেউ যদি শুধু rolls sort করে আর gpas ভুলে যায়, তাহলে রাফির GPA চলে যাবে নুসরাতের নামে। কোনো error আসবে না — শুধু ফলাফল ভুল হবে। সমস্যার মূল হলো, “এই তিনটি তথ্য একসাথে একজন ছাত্র” — এই সম্পর্কটি কোডের কোথাও লেখা নেই। সম্পর্কটি আছে শুধু প্রোগ্রামারের মাথায়।

বোর্ড পরীক্ষার প্রবেশপত্রের কথা ভাবুন। একটি কার্ডেই নাম, রোল, রেজিস্ট্রেশন নম্বর, কেন্দ্রের নাম আর ছবি — সব একসাথে। কেউ যদি নামগুলো একটি খাতায়, রোলগুলো আরেকটি খাতায় আর ছবিগুলো তৃতীয় একটি বাক্সে রাখত, তাহলে একটি পাতা ছিঁড়ে গেলেই সব গুলিয়ে যেত। struct হলো সেই প্রবেশপত্র — একজনের সব তথ্য এক কার্ডে।

বাস্তবের কোনো জিনিসকে প্রোগ্রামে আনার আগে নিজেকে তিনটি প্রশ্ন করুন: এর কোন কোন তথ্য আমার কাজে লাগবে? প্রতিটি তথ্য কোন টাইপে রাখব? কোন তথ্যগুলো একসাথে বদলায় বা একসাথে চলাফেরা করে? এই চিন্তাকেই বলে data modelling. কয়েকটি উদাহরণ দেখুন:

🎓 ছাত্র

নাম (char[20]), রোল (int), GPA (double), ঠিকানা (আরেকটি struct).

ক্লাসের তালিকা, রেজাল্ট শিট

💸 মোবাইল ব্যাংকিং লেনদেন

TrxID (char[11]), টাকার পরিমাণ পয়সায় (long long), সময় (time_t), ধরন — Send Money, Cash Out না Recharge.

bKash, Nagad-এর হিসাব

🚌 বাসের টিকিট

কোথা থেকে, কোথায়, তারিখ, সিট নম্বর ("B3"), ভাড়া, যাত্রীর মোবাইল নম্বর।

অনলাইন টিকিট বুকিং

লেনদেনের কার্ডে টাকার পরিমাণ রাখা হয়েছে পয়সায়, পূর্ণসংখ্যা হিসেবে — double নয়। পর্ব ০৫-এ দেখেছেন, double অনেক দশমিক মান হুবহু রাখতে পারে না (0.1 + 0.2 ঠিক 0.3 হয় না)। টাকার হিসাবে এক পয়সার গরমিলও গ্রহণযোগ্য নয়। এই সিদ্ধান্তটিও data modelling-এর অংশ — কোন তথ্য কোন টাইপে, সেটা ভেবে বাছাই করা।

২ · struct, typedef আর নাম ধরে শুরুর মান

struct দিয়ে আপনি C-কে বলেন: “এই কয়েকটি তথ্য মিলে একটি নতুন টাইপ হলো।” ভেতরের প্রতিটি তথ্যকে বলে member (বা field). প্রথমে লেখার সবচেয়ে সরল রূপ:

struct Student {
    char   name[20];
    int    roll;
    double gpa;
};                       /* শেষের সেমিকোলন ভুলবেন না */

struct Student a;        /* টাইপের নাম দুই শব্দের: struct Student */

খেয়াল করুন, এই অংশটি শুধু একটি নকশা। এটি লেখার সময় কোনো মেমরি নেওয়া হয় না — যেমন প্রবেশপত্রের ফাঁকা ফর্ম ছাপালে কোনো ছাত্রের তথ্য তৈরি হয় না। মেমরি নেওয়া হয় তখনই, যখন struct Student a; লিখে একটি আসল ভ্যারিয়েবল বানান।

typedef — টাইপকে ছোট একটি নাম দেওয়া

প্রতিবার struct Student লেখা একটু ঝামেলার। typedef একটি পুরোনো টাইপকে নতুন নাম দেয়। সাধারণত struct-এর সাথে একসাথেই লেখা হয়:

typedef struct {
    char   name[20];
    int    roll;
    double gpa;
} Student;               /* এখন থেকে শুধু Student লিখলেই চলবে */

Student a;

typedef নতুন কোনো টাইপ তৈরি করে না, শুধু একটি ডাকনাম দেয়। size_t, uint32_t — এগুলোও আসলে standard library-র typedef. একটি নিয়ম মনে রাখুন: নাম যেন কাজ বোঝায়। Student, Transaction, Ticket ভালো নাম; S1, MyType খারাপ।

Designated initializer — নাম ধরে মান বসানো

struct ভ্যারিয়েবল বানানোর সাথেই মান দিতে পারেন। পুরোনো পদ্ধতিতে মানগুলো ক্রম অনুযায়ী লিখতে হয়: {"Rafi", 17, 3.40}. কিন্তু ক্রম ভুল হলে? কেউ যদি পরে struct-এ নতুন একটি member মাঝখানে ঢোকায়? C99 থেকে এর একটি ভালো সমাধান আছে — designated initializer, অর্থাৎ member-এর নাম ধরে মান বসানো:

Student a = { .name = "Rafi", .roll = 17, .gpa = 3.40 };
Student b = { .roll = 18 };    /* name হবে "" আর gpa হবে 0.0 */
মূল ধারণা

Designated initializer-এর দুটি সুবিধা। এক, পড়লেই বোঝা যায় কোন মান কোন member-এর — 17 রোল না বয়স, সন্দেহ থাকে না। দুই, যে member-এর নাম লেখেননি, সেটি নিশ্চিতভাবে শূন্য হয়ে যায় — কোনো garbage value থাকে না। তবে Student c; লিখে কিছু না দিলে (ফাংশনের ভেতরে) সব member-এই garbage থাকে, ঠিক সাধারণ ভ্যারিয়েবলের মতো।

একটি struct আরেকটিতে সরাসরি কপি করা যায়: b = a; লিখলে a-এর সব বাইট b-তে চলে যায়। কিন্তু a == b দিয়ে দুটি struct তুলনা করা যায় না — compiler error দেবে। তুলনা করতে হয় member ধরে ধরে। কেন বাইট ধরে তুলনা করাও বিপজ্জনক, সেটা অংশ ৫-এ padding দেখার পর পরিষ্কার হবে।

৩ · ডট আর তীর — member পড়া, struct-এর array, struct-এর ভেতরে struct

struct-এর ভেতরের একটি member পড়তে বা বদলাতে লাগে dot operator: a.roll = 20; বা printf("%s", a.name);. বাক্যটি এভাবে পড়ুন — “a-এর roll”.

কিন্তু পর্ব ১৭ আর ১৮-তে দেখেছেন, বড় জিনিস নিয়ে কাজ করতে আমরা প্রায়ই তার ঠিকানা (pointer) হাতে রাখি। ধরুন Student *p = &a;. এখন p দিয়ে roll পড়তে চাইলে আগে ঠিকানা থেকে struct-টি বের করতে হবে, তারপর তার roll: (*p).roll. বন্ধনী বাদ দিলে ভুল হয়, কারণ .-এর precedence *-এর চেয়ে বেশি। এত বন্ধনী ঝামেলার, তাই C একটি ছোট চিহ্ন দিয়েছে — arrow operator:

p->roll  =  (*p).roll
ভ্যারিয়েবল হাতে থাকলে ডট, ঠিকানা হাতে থাকলে তীর।

struct-এর array — পুরো ক্লাসের তালিকা

এখন সেই ৬০ জনের তালিকা আর তিনটি আলাদা array-তে নয়, একটিতেই: Student cls[60];. প্রতিটি ঘরে একজন পূর্ণ ছাত্র। cls[7].gpa মানে “তালিকার ৭ নম্বর ঘরের ছাত্রের GPA”. এখন sort করলে পুরো struct একসাথে সরে — নাম আর GPA আর কখনো আলাদা হয় না।

নিচের debugger সিমুলেটরে তিনজন ছাত্রের তালিকা থেকে সর্বোচ্চ GPA খোঁজা হচ্ছে। লক্ষ করুন, আমরা পুরো struct কপি করে রাখছি না — শুধু সেরা ছাত্রের ঘর নম্বর (best) মনে রাখছি। “পরে” চাপুন:

1Student cls[3] = {2  {"Rafi",   1, 3.40},3  {"Nusrat", 2, 3.85},4  {"Tanvir", 3, 3.60} };5int best = 0;6for (int i = 1; i < 3; i++) {7    if (cls[i].gpa > cls[best].gpa)8        best = i;9}10printf("Top: %s (%.2f)\n",11       cls[best].name, cls[best].gpa);
ভ্যারিয়েবলের মান (Watch)
i
best
cls[i].gpa
cls[best].name
cls[best].gpa
  1. শুরুতে প্রথমজনকেই সেরা ধরা। best = 0 — অর্থাৎ আপাতত রাফি (৩.৪০) সেরা। শূন্য ছাত্রের তালিকা হলে এই কৌশল চলে না; বাস্তব কোডে আগে তালিকা খালি কি না দেখতে হয়।
  2. লুপ শুরু, i = 1. ১ < ৩ সত্য, তাই ভেতরে ঢুকছে। ০ নম্বর ঘর তো আগেই দেখা, তাই ১ থেকে শুরু।
  3. তুলনা: cls[1].gpa > cls[0].gpa? ৩.৮৫ > ৩.৪০ — সত্য। খেয়াল করুন, cls[i] একটি পুরো struct, আর .gpa তার ভেতর থেকে শুধু GPA বের করে আনছে।
  4. best = 1. এখন সেরা নুসরাত। শুধু একটি int বদলাল — নাম, রোল, GPA কিছুই কপি করতে হয়নি। ডানে cls[best].name নিজে থেকেই বদলে গেছে।
  5. i = 2. ২ < ৩ সত্য — আবার ভেতরে।
  6. তুলনা: ৩.৬০ > ৩.৮৫? মিথ্যা। তাই best যেমন ছিল তেমনই থাকল — লাইন ৮ এবার চলবে না।
  7. i = 3. ৩ < ৩ মিথ্যা, লুপ শেষ। i শুধু লুপের ভেতরেই বাঁচে, তাই এখন আর নেই।
  8. ফল ছাপা। cls[best] মানে cls[1] — আউটপুট Top: Nusrat (3.85). একটি array, একটি index আর ডট চিহ্ন — পুরো রেকর্ড আমাদের হাতে।

struct-এর ভেতরে struct

ছাত্রের ঠিকানাও তো কয়েকটি অংশে ভাগ করা — এলাকা, জেলা, পোস্ট কোড। ঠিকানার জন্য আলাদা একটি struct বানিয়ে সেটি ছাত্রের ভেতরে রাখা যায়। একে বলে nested struct:

typedef struct { char area[24]; char district[16]; int post_code; } Address;
typedef struct { char name[20]; int roll; double gpa; Address home; } Student;

Student s = { .name = "Rafi", .roll = 17, .home = { .district = "Rajshahi", .post_code = 6000 } };
printf("%s\n", s.home.district);      /* ডট দুবার: s-এর home-এর district */

এতে দুটি লাভ। এক, ঠিকানার নকশা একবার লিখে ছাত্র, শিক্ষক, দোকান — সবার ভেতরে ব্যবহার করা যায়। দুই, কোড পড়তে বাস্তবের মতো লাগে। ছোট ছোট অর্থপূর্ণ টুকরো জুড়ে বড় জিনিস বানানো — এটাই abstraction-এর প্রথম ধাপ।

এবার সব একসাথে চালিয়ে দেখুন — typedef, designated initializer, struct-এর array আর const ঠিকানা দিয়ে ফাংশনে পাঠানো:

#include <stdio.h>

typedef struct { char name[20]; int roll; double gpa; } Student;

void print_student(const Student *s) {
    printf("%-8s roll %2d  GPA %.2f\n", s->name, s->roll, s->gpa);
}

int main(void) {
    Student cls[] = {
        { .name = "Rafi",   .roll = 1, .gpa = 3.40 },
        { .name = "Nusrat", .roll = 2, .gpa = 3.85 },
        { .name = "Tanvir", .roll = 3, .gpa = 3.60 },
    };
    int n = sizeof cls / sizeof cls[0];
    for (int i = 0; i < n; i++) print_student(&cls[i]);
    printf("sizeof(Student) = %zu bytes\n", sizeof(Student));
    return 0;
}

print_student পুরো struct নয়, তার ঠিকানা নেয় — তাই ভেতরে s->name, তীর চিহ্ন। const লেখা মানে ফাংশনটি শুধু পড়বে, কিছু বদলাতে চাইলে compiler আটকে দেবে। sizeof cls / sizeof cls[0] পুরো array-র বাইটকে একজনের বাইট দিয়ে ভাগ করে ছাত্রের সংখ্যা বের করে (পর্ব ১৬-এর কৌশল), তাই তালিকায় নতুন নাম যোগ করলে লুপ নিজে থেকেই ঠিক থাকে। শেষ লাইনে সাধারণত ৬৪-বিট Linux-এ 32 দেখাবে: name ২০ বাইট, roll ৪ বাইট (মোট ২৪), তারপর gpa ৮ বাইট — ২৪ সংখ্যাটি ৮-এর গুণিতক বলে মাঝে কোনো ফাঁক লাগেনি। সব struct এত ভাগ্যবান নয় — অংশ ৫-এ দেখবেন।

৪ · ফাংশনে struct পাঠানো — পুরো কপি, নাকি শুধু ঠিকানা

পর্ব ১৩-এ দেখেছেন, C-তে ফাংশনে মান পাঠানো হয় pass by value পদ্ধতিতে — ফাংশন পায় একটি কপি। struct-এর বেলাতেও তাই। void show(Student s) ডাকলে পুরো struct-এর সব বাইট নতুন করে কপি হয়ে ফাংশনের stack frame-এ বসে।

📄 কপি পাঠানো (by value)

void show(Student s) — ফাংশন নিজের একটি ফটোকপি পায়। ভেতরে বদলালে আসলটি বদলায় না। ছোট struct-এ (যেমন দুটি int-এর একটি বিন্দু) এটি সহজ ও নিরাপদ।

খরচ: sizeof(struct) বাইট কপি

📍 ঠিকানা পাঠানো (by pointer)

void show(const Student *s) — কপি হয় শুধু ঠিকানাটি, সাধারণত ৮ বাইট। struct যত বড়ই হোক, খরচ একই। const থাকায় ফাংশন আসলটি বদলাতে পারবে না।

বদলাতে চাইলে const বাদ দিন

ধরুন প্রিন্সিপাল স্যার একজন ছাত্রের পুরো ফাইল দেখতে চান। একটি উপায় হলো ফাইলের প্রতিটি পাতা ফটোকপি করে পাঠানো। আরেকটি উপায় হলো শুধু একটি চিরকুট পাঠানো: “ফাইলটি আলমারির ৩ নম্বর তাকে আছে, দেখে নিন — কিন্তু কিছু লিখবেন না।” ফাইল ২০০ পাতার হলে দ্বিতীয় উপায়টি অনেক সস্তা। const Student * হলো সেই চিরকুট।

কপির খরচ ছোট struct-এ চোখে পড়ে না। কিন্তু ধরুন একটি struct-এর ভেতরে ৪০৯৬ বাইটের একটি buffer আছে, আর একটি লুপ ফাংশনটিকে দশ লাখবার ডাকছে — প্রতিবার ৪ KB কপি। তার ওপর, stack-এর জায়গা সীমিত; খুব বড় struct by value পাঠালে বা recursion-এর ভেতরে পাঠালে stack overflow-এর ঝুঁকিও বাড়ে।

পরিস্থিতিযেভাবে লিখবেনকারণ
ছোট struct (১৬ বাইটের মতো), শুধু পড়বেনPoint pকপি সস্তা, কোড সহজ
বড় struct, শুধু পড়বেনconst Student *sকপি নেই, বদলানোর ভয়ও নেই
ফাংশন struct বদলাবেStudent *sঠিকানা দিয়ে আসলটিতেই হাত দেওয়া
ফাংশন নতুন একটি ছোট struct বানিয়ে ফেরত দেবেPoint make_point(int x, int y)struct ফেরত দেওয়া বৈধ ও পরিষ্কার

ঠিকানা দিয়ে পাঠানোর একটি নতুন দায়িত্ব আছে: ঠিকানাটি NULL হতে পারে, বা এমন কোনো struct-এর হতে পারে যার মেমরি আর নেই (পর্ব ২০-এর dangling pointer). ফাংশনের ভিতরে s->roll লেখার আগে ভাবুন — এই ঠিকানা কি নিশ্চিতভাবে বৈধ? by value-তে এই ঝুঁকি নেই, কারণ কপিটি ফাংশনের নিজের।

৫ · মেমরিতে struct — alignment আর padding

এবার একটি ধাঁধা। নিচের struct-এ আছে একটি char (১ বাইট), একটি double (৮ বাইট) আর একটি int (৪ বাইট)। যোগ করলে ১৩ বাইট। কিন্তু সাধারণত ৬৪-বিট Linux-এ sizeof বলবে ২৪! বাকি ১১ বাইট কোথায় গেল?

struct Bad { char c; double d; int i; };   /* member-এর যোগফল 13, sizeof সাধারণত 24 */

কেন alignment লাগে

CPU মেমরি থেকে এক বাইট এক বাইট করে পড়ে না। সে পড়ে বড় বড় টুকরোয় — একসাথে ৪ বা ৮ বাইট, আর cache-এ আনে আরও বড় একটি টুকরো (cache line, সাধারণত ৬৪ বাইট)। এই টুকরোগুলো শুরু হয় নির্দিষ্ট সীমানা থেকে — ০, ৮, ১৬, ২৪… ঠিকানায়।

একটি ৮ বাইটের double যদি ৮-এর গুণিতক ঠিকানায় শুরু হয়, তাহলে পুরোটা একটি টুকরোর ভেতরে পড়ে — CPU এক চেষ্টায় পড়ে ফেলে। কিন্তু যদি ঠিকানা ১ থেকে শুরু হয়, তাহলে মানটি দুটি টুকরোয় ভাগ হয়ে যায় — CPU-কে দুবার পড়ে জোড়া লাগাতে হয়। আধুনিক Intel/AMD CPU এটা পারে, শুধু কখনো কখনো ধীর হয়। কিন্তু কিছু CPU (যেমন SPARC, আর অনেক ছোট microcontroller) এমন বেঠিক ঠিকানা থেকে পড়তে গেলে সরাসরি error দেয় — প্রোগ্রাম crash করে।

ঢাকার মেট্রো রেলের কথা ভাবুন। প্ল্যাটফর্মে দাগ টানা থাকে — দরজা ঠিক কোথায় খুলবে। যাত্রীরা দাগ মেনে দাঁড়ালে এক ধাক্কায় সবাই উঠে যায়। কেউ দুই দরজার মাঝখানে দাঁড়ালে তাকে হেঁটে সরে আসতে হয় — সময় নষ্ট। Alignment হলো সেই দাগ: প্রতিটি টাইপ তার মাপের গুণিতক ঠিকানায় “দাঁড়ায়”। আর দাগ মানতে গিয়ে মাঝে যে খালি জায়গা থাকে, সেটাই padding.

নিয়মটি সাধারণত এরকম: char যেকোনো ঠিকানায় বসতে পারে, int বসে ৪-এর গুণিতকে, double বসে ৮-এর গুণিতকে। Compiler struct-এর member-দের যে ক্রমে লিখেছেন সেই ক্রমেই বসায় (C-র নিয়ম, ক্রম বদলানোর অনুমতি নেই), আর দরকার হলে মাঝে ফাঁকা বাইট ঢোকায়। নিচে ধাপে ধাপে দেখুন — প্রতিটি সারি ৮ বাইট, · মানে padding:

হিসাব (সাধারণত ৬৪-বিট Linux)
এখন পর্যন্ত বসানো member
আসল তথ্যের বাইট
padding বাইট
sizeof
struct Badতিনটি member, তিন মাপের
struct Bad {
    char   c;   /* 1 বাইট, যেকোনো ঠিকানায় */
    double d;   /* 8 বাইট, 8-এর গুণিতকে */
    int    i;   /* 4 বাইট, 4-এর গুণিতকে */
};
offset 0c বসল প্রথম ঘরে
offset   0  1  2  3  4  5  6  7
 0-7   [c][ ][ ][ ][ ][ ][ ][ ]
offset 8d-এর জন্য 7 বাইট ফাঁক
offset   0  1  2  3  4  5  6  7
 0-7   [c][·][·][·][·][·][·][·]   ← padding
 8-15  [d][d][d][d][d][d][d][d]
offset 16i বসল, 16 তো 4-এর গুণিতক
offset   0  1  2  3  4  5  6  7
 0-7   [c][·][·][·][·][·][·][·]
 8-15  [d][d][d][d][d][d][d][d]
16-23  [i][i][i][i][ ][ ][ ][ ]
sizeof = 24শেষেও 4 বাইট padding
offset   0  1  2  3  4  5  6  7
 0-7   [c][·][·][·][·][·][·][·]
 8-15  [d][d][d][d][d][d][d][d]
16-23  [i][i][i][i][·][·][·][·]   ← শেষের padding

array-র পরের struct শুরু হবে 24-এ — 8-এর গুণিতক
struct Good { double d; int i; char c; }বড় আগে, ছোট পরে
offset   0  1  2  3  4  5  6  7
 0-7   [d][d][d][d][d][d][d][d]
 8-15  [i][i][i][i][c][·][·][·]

sizeof = 16   একই তথ্য, 8 বাইট কম
  1. তিনটি member. মোট আসল তথ্য ১ + ৮ + ৪ = ১৩ বাইট। Compiler এগুলো লেখার ক্রমেই বসাবে — c, তারপর d, তারপর i. দেখা যাক প্রতিটি কোথায় জায়গা পায়।
  2. c বসল offset ০-তে। char-এর কোনো দাগ মানার দরকার নেই, তাই প্রথম ঘরেই বসে গেল। পরের খালি ঘর এখন ১ নম্বর।
  3. d-এর জন্য ৭ বাইট ফাঁক। double চায় ৮-এর গুণিতক ঠিকানা। ১ নম্বর ঘর তো তা নয়, পরের গুণিতক ৮. তাই compiler ১ থেকে ৭ পর্যন্ত সাতটি ঘর ফাঁকা রেখে d বসাল ৮-এ। এই সাতটি ঘরই padding — এগুলোতে কোনো তথ্য নেই, ভেতরে যা আছে তা আবর্জনাও হতে পারে।
  4. i বসল offset ১৬-তে। int চায় ৪-এর গুণিতক। ১৬ নিজেই ৪-এর গুণিতক, তাই এখানে কোনো ফাঁক লাগল না। এখন পর্যন্ত ব্যবহার হলো ২০ বাইট।
  5. শেষেও padding, sizeof = ২৪. কেন ২০ নয়? কারণ struct Bad arr[2]; লিখলে দ্বিতীয় struct ঠিক প্রথমটির পরে শুরু হবে। ২০ থেকে শুরু হলে তার d বসত ২৮-এ — যা ৮-এর গুণিতক নয়! তাই compiler শেষে ৪ বাইট যোগ করে মাপটিকে ৮-এর গুণিতক (২৪) বানায়। নিয়ম: struct-এর মাপ তার সবচেয়ে কড়া member-এর alignment-এর গুণিতক হয়।
  6. ক্রম বদলালেই ১৬. একই তিনটি member, শুধু বড় থেকে ছোট ক্রমে: d (০–৭), i (৮–১১), c (১২), তারপর শেষে ৩ বাইট padding. মোট ১৬ বাইট — আগের চেয়ে এক-তৃতীয়াংশ কম। দশ লাখ রেকর্ডের একটি array-তে পার্থক্য প্রায় ৮ MB. কোনো লাইন কোড বদলাতে হয়নি, শুধু member-এর ক্রম।

নিজে মেপে দেখুন। stddef.h-এর offsetof বলে দেয় কোনো member struct-এর শুরু থেকে কত বাইট দূরে বসেছে:

#include <stdio.h>
#include <stddef.h>

struct Bad  { char c; double d; int i; };
struct Good { double d; int i; char c; };

int main(void) {
    printf("Bad : size %zu | c@%zu d@%zu i@%zu\n", sizeof(struct Bad),
           offsetof(struct Bad, c), offsetof(struct Bad, d),
           offsetof(struct Bad, i));
    printf("Good: size %zu | d@%zu i@%zu c@%zu\n", sizeof(struct Good),
           offsetof(struct Good, d), offsetof(struct Good, i),
           offsetof(struct Good, c));
    printf("sum of members = %zu\n",
           sizeof(char) + sizeof(double) + sizeof(int));
    return 0;
}

সাধারণত ৬৪-বিট Linux-এ আউটপুট হবে: Bad : size 24 | c@0 d@8 i@16, Good: size 16 | d@0 i@8 c@12 আর sum of members = 13 — ঠিক উপরের ডায়াগ্রামের মতো। %zu হলো size_t ছাপার সঠিক format. মনে রাখুন, এই সংখ্যাগুলো C standard-এ লেখা নেই; এগুলো নির্ভর করে CPU আর compiler-এর নিয়মের (ABI) ওপর। ৩২-বিট কিছু সিস্টেমে double-এর alignment ৪ হতে পারে, তখন সংখ্যা বদলে যাবে। তাই struct-এর মাপ কখনো হাতে হিসাব করে কোডে বসাবেন না — সবসময় sizeof ব্যবহার করুন।

মূল ধারণা

Padding কোনো বাগ নয় — এটি গতির বিনিময়ে জায়গা। তিনটি অভ্যাস রাখুন: (১) অনেক রেকর্ড রাখার struct-এ member সাজান বড় থেকে ছোট — double/pointer, তারপর int, তারপর short, শেষে char; (২) মাপ জানতে সবসময় sizeof; (৩) padding-এর বাইটে garbage থাকতে পারে, তাই দুটি struct memcmp দিয়ে বাইট ধরে তুলনা করবেন না — একই মানের দুটি struct-ও “আলাদা” দেখাতে পারে।

GCC আর Clang-এ __attribute__((packed)) লিখে padding পুরোপুরি বন্ধ করা যায় — একে বলে packed struct. তখন struct Bad-এর মাপ হবে ১৩. কিন্তু তখন d আর i বেঠিক ঠিকানায় বসবে — পড়া ধীর হতে পারে, কিছু CPU-তে crash-ও করতে পারে। এটি C standard-এর অংশ নয়; কখন ব্যবহার করা ঠিক, তা ভাবনার প্রশ্ন ১-এ আলোচনা করা হয়েছে।

৬ · enum — জাদুর সংখ্যার বদলে নাম

লেনদেনের struct-এ একটি তথ্য বাকি — ধরন: Send Money, Cash Out, না Mobile Recharge. অনেকে (এবং অনেক AI) এটা লেখেন এভাবে:

if (tx.type == 2) { ... }     /* 2 মানে কী? Cash Out? Recharge? */

এই 2-কে বলে magic number — কোডের মাঝে বসানো একটি সংখ্যা, যার অর্থ শুধু লেখক জানেন (এবং ছয় মাস পরে তিনিও ভুলে যান)। কেউ ভুল করে tx.type = 7 লিখলেও কেউ আটকাবে না। সমাধান হলো enum — সম্ভাব্য পছন্দগুলোকে নাম দিয়ে একটি টাইপ বানানো:

typedef enum { TX_SEND, TX_CASHOUT, TX_RECHARGE } TxType;   /* 0, 1, 2 */

typedef struct {
    char      id[11];          /* TrxID */
    long long amount_poisha;   /* 500 taka = 50000 */
    time_t    when;
    TxType    type;
} Transaction;

if (tx.type == TX_CASHOUT) { ... }   /* পড়লেই বোঝা যায় */

ভেতরে enum-এর প্রতিটি নাম আসলে একটি int ধ্রুবক — প্রথমটি ০, তারপর ১, ২… চাইলে নিজে মান দিতে পারেন: enum { LOW = 1, MID = 5, HIGH = 10 };. তাহলে লাভ কী? তিনটি:

  • পড়া সহজ: TX_CASHOUT নিজেই নিজের অর্থ বলে।
  • এক জায়গায় তালিকা: নতুন ধরন (যেমন TX_PAYBILL) যোগ করতে একটি লাইন বদলালেই হয়।
  • Compiler সাহায্য করে: enum-এর ওপর switch লিখে কোনো একটি নাম বাদ দিলে, আর default না রাখলে, -Wall চালু থাকলে GCC সতর্ক করে: enumeration value 'TX_RECHARGE' not handled in switch. নতুন ধরন যোগ করার পর কোথায় কোথায় কোড বদলাতে হবে, compiler নিজেই দেখিয়ে দেয়।

enum C-তে খুব কড়া পাহারাদার নয়। TxType t = 7; লিখলেও সাধারণত compiler আটকায় না, কারণ ভেতরে তো এটি একটি পূর্ণসংখ্যাই। বাইরের উৎস (ফাইল, নেটওয়ার্ক, ব্যবহারকারীর ইনপুট) থেকে আসা সংখ্যাকে enum-এ বসানোর আগে নিজে যাচাই করুন যে মানটি বৈধ তালিকার মধ্যে আছে।

৭ · union — এক জায়গা, অনেক রূপ

struct-এ প্রতিটি member-এর নিজের জায়গা থাকে, পাশাপাশি। union দেখতে প্রায় একই রকম, কিন্তু একটি বড় পার্থক্য: সব member একই ঠিকানা থেকে শুরু হয়, একই বাইট ভাগ করে নেয়। তাই union-এর মাপ হয় তার সবচেয়ে বড় member-এর মাপ (alignment-এর জন্য দরকার হলে একটু বেশি)। একটিতে লিখলে বাকিগুলোর মান মুছে যায় বা বদলে যায়।

একটি ক্লাসরুমের হোয়াইটবোর্ড ভাবুন। সকালে গণিতের স্যার সূত্র লিখলেন, দুপুরে বাংলার ম্যাডাম মুছে কবিতা লিখলেন। বোর্ড একটাই, জায়গাও একই — শুধু একসময় একটি বিষয়ই থাকে। দুপুরে কেউ যদি বোর্ডে তাকিয়ে গণিতের সূত্র খোঁজে, সে কবিতার অক্ষরগুলোকেই সূত্র ভেবে ভুল পড়বে। union ঠিক এই বোর্ড।

নিচের ডায়াগ্রামে একটি ৪ বাইটের union-এ প্রথমে একটি পূর্ণসংখ্যা, পরে একটি float লেখা হচ্ছে, আর একই বাইটগুলো ভিন্ন ভিন্ন member দিয়ে পড়া হচ্ছে। পর্ব ০৫-এর endianness মনে রাখুন — x86 আর বেশিরভাগ ARM সিস্টেম little-endian, অর্থাৎ সংখ্যার ছোট দিকের বাইট আগে বসে।

1union Num {2    unsigned int  i;3    float         f;4    unsigned char b[4];5};6union Num u;7u.i = 65;8printf("%c\n", u.b[0]);9u.f = 1.0f;10printf("%u\n", u.i);
একই ৪টি বাইট (little-endian)
b[0] b[1] b[2] b[3]
u.i হিসেবে পড়লে
u.f হিসেবে পড়লে
u.b[0] হিসেবে পড়লে
  1. ৪ বাইটের একটি union. i (৪ বাইট), f (৪ বাইট), b (৪টি char) — তিনজনই একই ৪টি বাইটের ওপর বসে আছে। তাই sizeof(union Num) = ৪, ১২ নয়। এখনো কিছু লেখা হয়নি, তাই বাইটগুলো অজানা।
  2. u.i = 65. ৬৫ হেক্সে 0x00000041. little-endian মেশিনে ছোট দিকের বাইট 41 বসে সবার আগে, b[0]-এ। একই বাইটগুলোকে float হিসেবে পড়লে পাওয়া যায় প্রায় শূন্যের কাছাকাছি একটি অর্থহীন সংখ্যা — কারণ float-এর বিটের নিয়ম সম্পূর্ণ আলাদা।
  3. u.b[0] পড়া হলো। প্রথম বাইট 0x41 = ৬৫, আর ASCII-তে ৬৫ হলো 'A'. তাই স্ক্রিনে এলো A. একটি পূর্ণসংখ্যাকে বাইট হিসেবে খুলে দেখা গেল — endianness পরীক্ষার পুরোনো একটি কৌশল এটাই। big-endian মেশিনে b[0] হতো 00.
  4. u.f = 1.0f. একই ৪টি বাইটে এবার লেখা হলো একটি float. IEEE-754 নিয়মে ১.০-এর বিটগুলো হলো 0x3F800000, little-endian-এ বাইট 00 00 80 3F. আগের ৬৫ পুরোপুরি মুছে গেছে — বোর্ডে নতুন বিষয়।
  5. u.i পড়লে ১ নয়, 1065353216! কেউ যদি ভাবে “শেষবার ১ লিখেছিলাম, তাই i-ও ১” — ভুল। একই বিটগুলো পূর্ণসংখ্যা হিসেবে পড়লে আসে এই বিশাল সংখ্যা। একে বলে type punning. C এটি অনুমতি দেয় (C++ দেয় না), কিন্তু ফল নির্ভর করে মেশিনের ওপর। ইচ্ছে করে করলে এটি একটি কৌশল; ভুলে করলে এটি একটি বাগ।

Tagged union — “কয়েক রকমের মধ্যে একটি”

union-এর আসল কাজ হলো এমন ডেটা রাখা, যা একসময় কয়েক রকমের যেকোনো একটি। একটি চ্যাট অ্যাপের মেসেজ ভাবুন: সেটি হয় লেখা, নয় ছবি, নয় টাকা পাঠানো। তিনটির জন্য তিনটি আলাদা জায়গা রাখা অপচয়, কারণ একটি মেসেজ একসাথে তিনটি হয় না। কিন্তু শুধু union রাখলে প্রশ্ন ওঠে — এখন বোর্ডে কোন বিষয় লেখা আছে? union নিজে তা মনে রাখে না।

সমাধান: একটি enum ট্যাগ আর union একসাথে একটি struct-এ রাখা। ট্যাগ বলে দেয় কোন member এখন বৈধ। একে বলে tagged union:

typedef enum { MSG_TEXT, MSG_IMAGE, MSG_PAYMENT } MsgKind;

typedef struct {
    MsgKind kind;                                   /* ট্যাগ: এখন কোনটি বৈধ */
    union {
        char text[64];                              /* kind == MSG_TEXT    */
        struct { int width, height; } image;        /* kind == MSG_IMAGE   */
        struct { long long poisha; char to[12]; } pay;   /* MSG_PAYMENT    */
    } as;
} Message;

এখন যেকোনো মেসেজ হাতে পেলে আগে ট্যাগ দেখুন, তারপর শুধু সেই member পড়ুন। নিচের ডায়াগ্রামে তিনটি মেসেজ একটি switch-এর ভেতর দিয়ে যাচ্ছে:

মেসেজ ১kind = MSG_TEXT
মেসেজ ২kind = MSG_PAYMENT
মেসেজ ৩kind = MSG_IMAGE
switch (m.kind)আগে ট্যাগ দেখো
case MSG_TEXTm.as.text পড়ো
case MSG_IMAGEm.as.image পড়ো
case MSG_PAYMENTm.as.pay পড়ো
Message-এর ভেতরেট্যাগ + সবচেয়ে বড় member-এর সমান জায়গা
[ kind ][ ----------- as (64 বাইট) ----------- ]
           text  : 64 বাইটের লেখা
           image : width, height  (একই জায়গার শুরুতে)
           pay   : poisha, to     (একই জায়গার শুরুতে)
মেসেজ ১ট্যাগ মিলল, সঠিক member পড়া হলো
case MSG_TEXT:
    printf("Text: %s\n", m.as.text);
    break;
Text: Kal class 9tay
মেসেজ ২একই struct, অন্য member
case MSG_PAYMENT:
    printf("Sent %lld.%02lld Tk to %s\n",
           m.as.pay.poisha / 100, m.as.pay.poisha % 100, m.as.pay.to);
    break;
Sent 500.00 Tk to 017XXXXXXXX
মেসেজ ৩ — বাগওয়ালা কোডট্যাগ না দেখে পড়লে
/* কেউ ট্যাগ না দেখেই লিখল: */
printf("%s\n", m.as.text);
����@  ← width/height-এর বাইটকে লেখা ভেবে পড়া

compiler কিছুই বলে না — টাইপ তো ঠিকই আছে
  1. একটি Message-এর গঠন। সামনে ট্যাগ kind, তারপর union as. union-এর মাপ তার সবচেয়ে বড় member (৬৪ বাইটের লেখা)-এর সমান। ছবি বা পেমেন্টের তথ্য সেই একই জায়গার শুরুতে বসে। তিনটি আলাদা জায়গা রাখলে প্রতিটি মেসেজে অনেক বাইট নষ্ট হতো।
  2. মেসেজ ১: ট্যাগ বলছে TEXT. switch (m.kind) ট্যাগ দেখে case MSG_TEXT-এ গেল, আর শুধু সেখানেই m.as.text পড়া হলো। ট্যাগ আর পড়া member মিলে গেছে — নিরাপদ।
  3. মেসেজ ২: ট্যাগ বলছে PAYMENT. একই Message টাইপ, কিন্তু এবার m.as.pay পড়া হলো। টাকা পয়সায় রাখা আছে, তাই ছাপার সময় ১০০ দিয়ে ভাগ করে টাকা আর ভাগশেষ দিয়ে পয়সা।
  4. মেসেজ ৩: ট্যাগ না দেখলে কী হয়। মেসেজটি আসলে ছবি, কিন্তু কেউ ট্যাগ না দেখেই m.as.text পড়ল। width আর height-এর বাইটগুলো অক্ষর হিসেবে ছাপা হলো — অর্থহীন চিহ্ন, আর লেখার শেষে '\0' না থাকলে পরের মেমরিও পড়তে থাকবে। compiler এটি ধরতে পারে না। নিয়ম একটাই: union-এ সবসময় আগে ট্যাগ, তারপর member.

৮ · Bit field — বিট গুনে জায়গা দেওয়া

পর্ব ০৯-এ দেখেছেন, bit flag দিয়ে একটি সংখ্যার ভেতরে অনেকগুলো হ্যাঁ/না রাখা যায়, আর bit mask দিয়ে নির্দিষ্ট বিট চালু, বন্ধ বা পরীক্ষা করা যায়। struct-এর ভেতরে একই কাজ আরেকভাবে করা যায় — member-এর মাপ বিট গুনে বলে দিয়ে:

struct AccountFlags {
    unsigned int verified : 1;   /* 0 বা 1 */
    unsigned int blocked  : 1;   /* 0 বা 1 */
    unsigned int level    : 3;   /* 0 থেকে 7 */
};

struct AccountFlags f = { .verified = 1, .level = 5 };
if (f.verified && !f.blocked) { ... }    /* মাস্ক লিখতে হলো না */

এগুলোকে বলে bit field. পাঁচটি বিটের তথ্য, অথচ কোড পড়তে সাধারণ member-এর মতো — f.blocked = 1. compiler নিজেই পেছনে &, |, শিফট লিখে দেয়। level-এ ৩ বিট মানে সর্বোচ্চ ৭; ৯ বসাতে চাইলে মানটি কেটে যায় (ওপরের বিট হারায়), আর GCC সাধারণত একটি সতর্কবার্তা দেয়।

কোথায় কাজে লাগে

  • হার্ডওয়্যার register: একটি microcontroller-এর ৩২ বিটের control register-এ হয়তো ৩ বিট গতি, ১ বিট চালু/বন্ধ, ৪ বিট মোড। bit field দিয়ে লিখলে কোড ডেটাশিটের ছকের মতো দেখায়।
  • নেটওয়ার্ক প্রোটোকলের header: IPv4 header-এর প্রথম বাইটে ৪ বিট version আর ৪ বিট header-এর দৈর্ঘ্য। Linux kernel-এর struct iphdr এগুলো bit field দিয়েই লেখে।
  • লক্ষ লক্ষ ছোট রেকর্ড: প্রতিটিতে কয়েকটি হ্যাঁ/না থাকলে, আলাদা int-এর বদলে কয়েকটি বিট — মোট মেমরি অনেক কমে।

Bit field-এর portability সমস্যা গুরুতর। একটি বাইটের ভেতরে বিটগুলো বাঁ থেকে বসবে না ডান থেকে, একটি বিট ফিল্ড দুটি একক জুড়ে বসতে পারবে কি না, পুরো struct-এর মাপ কত — এসবের অনেকটাই compiler ঠিক করে, C standard নয়। সেজন্যই Linux-এর struct iphdr-এ little-endian আর big-endian মেশিনের জন্য দুই রকম ক্রম আলাদা করে লেখা আছে। আরও দুটি সীমা: bit field-এর ঠিকানা নেওয়া যায় না (&f.level error), আর টাইপ হিসেবে unsigned int, signed int বা _Bool ব্যবহার করাই নিরাপদ। মেমরির ভেতরে কাজের জন্য bit field সুবিধাজনক; কিন্তু ফাইল বা নেটওয়ার্কের ফরম্যাট হুবহু মেলাতে অনেক অভিজ্ঞ প্রোগ্রামার পর্ব ০৯-এর মাস্ক আর শিফটই পছন্দ করেন, কারণ তার ফল সব মেশিনে এক।

সব টুকরো একসাথে

টুলকী প্রকাশ করেমেমরিপ্রধান বিপদ
struct“এটা এবং ওটা এবং সেটা”সব member পাশাপাশি + paddingবড় struct by value পাঠানো; padding ভুলে যাওয়া
union“এটা অথবা ওটা”সবচেয়ে বড় member-এর মাপভুল member পড়া
enum“এই কয়েকটি নামের একটি”সাধারণত একটি intবাইরে থেকে আসা অবৈধ সংখ্যা
tagged union“কয়েক রকমের একটি, আর কোনটি তা জানি”ট্যাগ + unionট্যাগ না দেখে পড়া; ট্যাগ বদলাতে ভুলে যাওয়া
bit field“এত বিটের ছোট তথ্য”কয়েকটি বিটবিটের ক্রম compiler-নির্ভর

৯ · AI-এর যুগে — struct কোডে AI যেসব ভুল করে

AI-কে “একটি ছাত্র ব্যবস্থাপনা প্রোগ্রাম লেখো” বললে সেকেন্ডেই struct-ভরা কোড পাবেন। দেখতে পরিষ্কার, চলেও। কিন্তু ভুলগুলো থাকে নকশায় — যেগুলো compiler ধরে না, শুধু বোঝেন এমন একজন মানুষ ধরেন। পরের পর্বের প্রজেক্টে আপনি নিজেই struct দিয়ে একটি টুল বানাবেন, তাই এই তালিকা এখনই কাজে লাগবে।

AI-এর ভুলকেন সমস্যাযা হওয়া উচিত
if (t.type == 2) — magic numberঅর্থ বোঝা যায় না, নতুন ধরন যোগ করলে কোথায় বদলাতে হবে খুঁজে পাওয়া যায় নাenum আর switch, -Wall চালু
void print(Student s) — বড় struct by valueপ্রতিবার পুরো struct কপি; ফাংশন ভেতরে বদলালে আসলটি বদলায় না — অনেকে অবাক হনconst Student *s
ট্যাগ না দেখে union-এর member পড়াgarbage পড়া, কখনো মেমরির বাইরে চলে যাওয়াপ্রতিবার switch (m.kind)
fwrite(&s, sizeof s, 1, f) দিয়ে struct সরাসরি ফাইলে বা নেটওয়ার্কেpadding, endianness আর টাইপের মাপ মেশিনভেদে আলাদা — অন্য মেশিনে ভুল পড়া হয়; padding-এর garbage বাইটও চলে যায়member ধরে ধরে নির্দিষ্ট ফরম্যাটে লেখা (serialization)
টাকার পরিমাণ double-এদশমিকের ছোট ছোট ভুল জমে হিসাব মেলে নাপয়সায় long long
struct-এর মাপ হাতে লেখা (malloc(13))padding ভুলে গেলে কম মেমরি — buffer overflowসবসময় sizeof(Type)

AI-কে struct-এর কোড লিখতে বললে নকশার সিদ্ধান্তগুলো আগেই বলে দিন, আর তাকে ব্যাখ্যা করতে বাধ্য করুন। এতে আপনি শুধু কোড পাবেন না, যাচাই করার মতো যুক্তিও পাবেন:

আমি C-তে একটি মোবাইল ব্যাংকিং লেনদেনের রেকর্ড বানাচ্ছি (TrxID, টাকা, সময়, ধরন: Send Money / Cash Out / Recharge). নিচের নিয়ম মেনে struct আর একটি ছাপার ফাংশন লেখো: ১. ধরনের জন্য enum ব্যবহার করো, কোনো magic number নয়। enum-এর ওপর switch-এ default রেখো না, যাতে -Wall নতুন ধরন বাদ পড়লে সতর্ক করে। ২. টাকা double-এ নয়, পয়সায় long long-এ রাখো। ৩. ছাপার ফাংশন struct-কে const pointer হিসেবে নেবে। ৪. member-গুলো এমন ক্রমে সাজাও যাতে padding কম হয়, আর প্রতিটি member-এর offset ও মোট sizeof (৬৪-বিট Linux ধরে) একটি মন্তব্যে লেখো। ৫. struct-টি সরাসরি fwrite দিয়ে ফাইলে লেখো না; কেন লিখলে না, দুই লাইনে বাংলায় বোঝাও।

AI-এর struct কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Padding যদি জায়গা নষ্ট করে, তাহলে compiler সবসময় struct “packed” করে না কেন? কখন packed করা ঠিক?

কারণ জায়গা আর গতি — দুটো একসাথে সবচেয়ে ভালো পাওয়া যায় না, আর C-র ডিফল্ট পছন্দ হলো গতি ও নিরাপত্তা।

  • হার্ডওয়্যারের বাস্তবতা: CPU আর মেমরির মাঝে ডেটা চলে নির্দিষ্ট মাপের টুকরোয়। একটি double যদি দুটি টুকরোর সীমানা পেরিয়ে বসে, CPU-কে দুবার পড়তে হয়। cache line-এর সীমানা পেরোলে দুটি cache line লাগে — আরও খরচ। কিছু CPU (SPARC, অনেক ছোট microcontroller) এমন পড়া একেবারেই করতে পারে না, সরাসরি bus error দেয়।
  • Atomic কাজের সমস্যা: একাধিক thread একই ভ্যারিয়েবল ব্যবহার করলে (পর্ব ৪৯), একটি মান “একবারে পুরোটা” লেখা-পড়া হওয়া জরুরি। বেঠিক ঠিকানায় বসা মানের ক্ষেত্রে অনেক CPU এই নিশ্চয়তা দেয় না।
  • packed-এর লুকানো ফাঁদ: packed struct-এর একটি member-এর ঠিকানা নিয়ে int *-এ রাখলে, পরে সেই pointer দিয়ে পড়ার সময় compiler জানে না যে ঠিকানাটি বেঠিক। GCC এজন্য -Waddress-of-packed-member নামে একটি সতর্কবার্তা যোগ করেছে।

তাহলে packed কখন? যখন বাইরের কোনো ফরম্যাট বাইট ধরে ধরে নির্দিষ্ট — একটি নেটওয়ার্ক প্যাকেটের header, BMP বা ZIP ফাইলের header, অথবা কোনো হার্ডওয়্যারের register-এর ছক। সেখানে লক্ষ্য হলো বাইরের নকশা হুবহু মেলানো। তবুও অনেকে packed struct-এর বদলে বাইটের array থেকে member ধরে ধরে পড়াকেই বেশি নিরাপদ মনে করেন।

আর শুধু মেমরি বাঁচাতে চাইলে প্রথম কাজ packed নয় — member-এর ক্রম বদলানো। বড় থেকে ছোট সাজালে বেশিরভাগ padding এমনিতেই চলে যায়, কোনো ঝুঁকি ছাড়া।

মূল উপলব্ধি: Padding হলো compiler-এর নেওয়া একটি প্রকৌশল সিদ্ধান্ত — কয়েকটি বাইটের বিনিময়ে গতি আর নিরাপত্তা। নিয়ম ভাঙা যায়, কিন্তু তখনই ভাঙুন যখন জানেন ঠিক কী দাম দিচ্ছেন।

প্র ০২fwrite(&s, sizeof s, 1, f) — এক লাইনে পুরো struct ফাইলে লেখা কত সহজ! তাহলে অভিজ্ঞরা একে বিপজ্জনক বলেন কেন?

কারণ এভাবে আপনি ফাইলে লিখছেন আপনার মেশিনের মেমরির হুবহু ছবি — আর সেই ছবি অন্য মেশিনে একই অর্থ বহন করে না। অন্তত চারটি কারণে:

  • Padding: কোন member-এর পরে কত বাইট ফাঁক, তা compiler আর CPU-র নিয়মে ঠিক হয়। Windows-এর MSVC আর Linux-এর GCC একই struct-এ কিছু ক্ষেত্রে আলাদা layout বানাতে পারে। ৩২-বিট আর ৬৪-বিটে তো প্রায়ই আলাদা।
  • Endianness (পর্ব ০৫): little-endian মেশিনে লেখা int একটি big-endian মেশিনে পড়লে বাইটগুলো উল্টো ক্রমে পড়া হবে — ৬৫ হয়ে যাবে এক বিশাল সংখ্যা। নেটওয়ার্কের নিয়মে সংখ্যা পাঠানো হয় big-endian-এ (“network byte order”), যেখানে আপনার ল্যাপটপ সম্ভবত little-endian.
  • টাইপের মাপ: long Linux-এর ৬৪-বিটে ৮ বাইট, কিন্তু Windows-এ ৪ বাইট। একই struct, দুই মাপ।
  • Pointer: struct-এর ভেতরে pointer থাকলে ফাইলে যায় শুধু একটি ঠিকানা — যা প্রোগ্রাম বন্ধ হওয়ার সাথে সাথেই অর্থহীন। আর padding-এর বাইটে থাকা পুরোনো মেমরির আবর্জনাও ফাইলে চলে যায় — কখনো সেটা গোপন তথ্যও হতে পারে।

পেশাদার সমাধান হলো serialization: একটি নির্দিষ্ট ফরম্যাট ঠিক করা (যেমন “প্রথম ৪ বাইট রোল, big-endian; তারপর ২০ বাইট নাম”), আর প্রতিটি member আলাদা করে সেই নিয়মে লেখা ও পড়া। সহজ ক্ষেত্রে CSV বা JSON-এর মতো টেক্সট ফরম্যাট, বড় সিস্টেমে Protocol Buffers-এর মতো টুল — যেগুলো ঠিক এই সমস্যার জন্যই তৈরি।

এক মেশিনে, একই প্রোগ্রামের একই build অস্থায়ী cache হিসেবে raw struct লিখলে কাজ চলে যায়। সমস্যা শুরু হয় যখন ফাইলটি অন্য মেশিনে যায়, বা প্রোগ্রামের নতুন version পুরোনো ফাইল পড়ে।

মূল উপলব্ধি: মেমরির layout হলো একটি মেশিনের নিজস্ব ব্যাপার; ফাইল আর নেটওয়ার্কের ফরম্যাট হলো দুই পক্ষের চুক্তি। চুক্তি লিখে রাখুন — মেমরির ছবির ওপর ভরসা করবেন না।

প্র ০৩union তো ঝুঁকিপূর্ণ — বাস্তবের বড় সিস্টেমে কি এটা আসলেই ব্যবহার হয়? কোথায়?

হয়, এবং বেশ গুরুত্বপূর্ণ জায়গায়। যেখানেই ডেটা “কয়েক রকমের একটি”, সেখানেই tagged union-এর চিন্তা।

  • প্রোগ্রামিং ভাষার interpreter: Lua ভাষার ভেতরে (যা C-তে লেখা) প্রতিটি মান — সংখ্যা, boolean, টেবিল বা ফাংশন — একটি ট্যাগ আর একটি union দিয়ে রাখা হয়। ভ্যারিয়েবলে ৫ রাখুন বা একটি টেবিল, একই মাপের একটি ঘরেই আঁটে। অনেক script ভাষার “dynamic typing”-এর নিচে আসলে এই কৌশলই কাজ করে।
  • নেটওয়ার্ক ও প্রোটোকল: একটি প্যাকেট আসার পর তার header-এর একটি ঘর বলে দেয় পরের অংশটি কী — TCP না UDP, কোন ধরনের বার্তা। প্রোগ্রাম সেই ঘর (ট্যাগ) দেখে ঠিক করে বাকি বাইটগুলো কোন struct হিসেবে পড়বে।
  • GUI আর গেম: X11-এর মতো পুরোনো C লাইব্রেরিতে একটি event হতে পারে মাউস ক্লিক, কি-বোর্ড চাপ বা জানালার মাপ বদল — একটি ট্যাগ আর একটি union দিয়ে রাখা।
  • Compiler নিজে: syntax tree-র একটি node হতে পারে সংখ্যা, যোগ, ফাংশন কল — আবার সেই একই কৌশল।

ঝুঁকি কমানোর উপায়: union কখনো খালি হাতে ব্যবহার করবেন না — সবসময় ট্যাগের সাথে। ট্যাগ আর member যেন একসাথে বদলায়, সেজন্য ছোট ছোট “constructor” ফাংশন লিখুন (make_text_msg(), make_payment_msg()), যাতে কেউ ট্যাগ এক রকম আর ডেটা আরেক রকম বসাতে না পারে। পড়ার সময় সবসময় switch, আর enum-এর ওপর default ছাড়া — যাতে নতুন ধরন যোগ করলে compiler সতর্ক করে।

আধুনিক ভাষাগুলো ঠিক এই শৃঙ্খলাকে ভাষার নিয়ম বানিয়ে দিয়েছে। Rust-এর enum আর match আসলে compiler-পাহারায় থাকা tagged union.

মূল উপলব্ধি: union-এর ঝুঁকি union-এ নয়, ট্যাগ ছাড়া ব্যবহারে। ট্যাগ + union + switch — এই তিনটি একসাথে একটি শক্তিশালী নকশা, যা interpreter থেকে নেটওয়ার্ক সবখানে চলে।

প্র ০৪বলা হয় struct হলো “class”-এর পূর্বপুরুষ। C-তে class নেই — তাহলে C প্রোগ্রামাররা object-এর মতো কাজ কীভাবে করেন?

Object-oriented ভাষার একটি class-এ থাকে দুটি জিনিস: ডেটা (field) আর সেই ডেটার ওপর কাজ করা ফাংশন (method). struct দেয় প্রথমটি। দ্বিতীয়টি C প্রোগ্রামাররা নিয়ম মেনে বানান।

  • ইতিহাস: ১৯৭৯ সালে Bell Labs-এ Bjarne Stroustrup “C with Classes” নামে কাজ শুরু করেন, যা পরে C++ নামে পরিচিত হয়। C++-এ আজও struct আর class-এর পার্থক্য প্রায় শুধু একটি — member-গুলো ডিফল্টভাবে সবার জন্য খোলা না গোপন।
  • C-তে method-এর মতো ফাংশন: নিয়ম করে প্রতিটি ফাংশনের প্রথম parameter হয় struct-এর ঠিকানা — account_deposit(Account *a, long long poisha). C++-এর a.deposit(500) ভেতরে প্রায় এটাই করে; সেখানে সেই লুকানো ঠিকানার নাম this.
  • ভেতরের তথ্য লুকানো (encapsulation): header ফাইলে শুধু typedef struct Account Account; লিখে ভেতরের member না দেখালে বাইরের কোড member সরাসরি ছুঁতে পারে না, শুধু ফাংশন ডাকতে পারে। standard library-র FILE এভাবেই কাজ করে — আপনি কখনো FILE-এর ভেতরে হাত দেন না।
  • Polymorphism: struct-এর ভেতরে ফাংশনের ঠিকানা (function pointer, পর্ব ২৮) রাখলে একই ডাকে ভিন্ন ভিন্ন কাজ হয়। Linux kernel-এ struct file_operations ঠিক এটাই — প্রতিটি ড্রাইভার নিজের read/write ফাংশন বসায়, আর kernel না জেনেই সঠিকটি ডাকে।

অর্থাৎ object-oriented চিন্তা কোনো ভাষার জাদু নয়, একটি নকশার ধরন। C-তে সেটা হাতে করতে হয়, C++/Java-তে ভাষা নিজে করে দেয়।

মূল উপলব্ধি: class = struct + তার ওপর কাজ করা ফাংশন + কিছু নিয়ম। struct ভালো বুঝলে যেকোনো object-oriented ভাষা শিখতে গিয়ে দেখবেন — নিচে আসলে সেই পরিচিত মেমরির ব্লকই আছে।

প্র ০৫১০ লাখ ছাত্রের GPA-র গড় বের করতে হবে। struct-এর array, নাকি প্রতিটি তথ্যের আলাদা array — কোনটি দ্রুত, আর কেন?

মজার ব্যাপার — এই পর্বের শুরুতে যে “তিনটি আলাদা array”-কে ভঙ্গুর বলেছিলাম, কিছু ক্ষেত্রে সেটাই দ্রুততর! দুটি নকশার নাম আছে: Array of Structs (AoS) — Student cls[N], আর Struct of Arrays (SoA) — একটি struct-এর ভেতরে names[N], rolls[N], gpas[N].

  • Cache-এর হিসাব: CPU মেমরি থেকে একবারে একটি cache line (সাধারণত ৬৪ বাইট) আনে। AoS-এ প্রতিটি Student ৩২ বাইটের, তার মধ্যে GPA মাত্র ৮ বাইট। তাই একটি cache line-এ আসে ২ জনের GPA, সাথে আসে অপ্রয়োজনীয় নাম আর রোল। SoA-তে gpas array-তে GPA-গুলো পাশাপাশি, একটি cache line-এ আসে ৮টি GPA — প্রায় ৪ গুণ কম মেমরি আনতে হয়।
  • SIMD: আধুনিক CPU এক নির্দেশে একসাথে কয়েকটি সংখ্যা যোগ করতে পারে, যদি সংখ্যাগুলো পাশাপাশি থাকে। SoA-তে compiler এই সুবিধা সহজে নিতে পারে।
  • কিন্তু AoS-এর সুবিধাও আছে: যদি কাজটি হয় “একজন ছাত্রের সব তথ্য দেখাও” (রেজাল্ট কার্ড), তাহলে AoS-এ সব তথ্য এক জায়গায়, এক-দুটি cache line-এই পাওয়া যায়। কোড পড়াও সহজ, ভুলের সুযোগও কম।

এই চিন্তার নাম data-oriented design — “ডেটা কীভাবে ব্যবহার হবে, সেই অনুযায়ী মেমরিতে সাজাও।” গেম ইঞ্জিনে হাজার হাজার বস্তুর অবস্থান প্রতি ফ্রেমে আপডেট করতে হয়, তাই সেখানে এর ব্যবহার ব্যাপক। ডেটাবেসেও একই প্রশ্ন: সারি ধরে রাখা (row store, AoS-এর মতো) লেনদেনের জন্য ভালো, আর কলাম ধরে রাখা (column store, SoA-এর মতো) বিশ্লেষণের জন্য — যেমন “সব লেনদেনের মোট টাকা”।

মূল উপলব্ধি: ডেটার “সঠিক” আকার বলে কিছু নেই — আছে কাজ অনুযায়ী সঠিক আকার। আগে পরিষ্কার নকশা (AoS) দিয়ে শুরু করুন; মেপে দেখে যদি গতিই সমস্যা হয়, তখন ডেটার আকার বদলানোর কথা ভাবুন।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — খাতা-কলমে নকশা আর হিসাবই আসল কাজ।

  1. Padding হাতে হিসাব: সাধারণত ৬৪-বিট Linux ধরে struct A { char a; int b; char c; }; আর struct B { int b; char a; char c; }; — দুটির sizeof কত? প্রতিটি member-এর offset লিখুন, তারপর উপরের padding.c-তে বসিয়ে মিলিয়ে দেখুন।

    struct A: a বসে ০-তে। b-র দরকার ৪-এর গুণিতক, তাই ১–৩ padding, b বসে ৪–৭-এ। c বসে ৮-এ। এখন ৯ বাইট, কিন্তু মাপ হতে হবে ৪-এর গুণিতক (সবচেয়ে কড়া member int), তাই শেষে ৩ বাইট padding — sizeof = ১২.

    struct B: b ০–৩, a ৪, c ৫, শেষে ২ বাইট padding — sizeof = ৮. একই তথ্য, ক্রম বদলেই এক-তৃতীয়াংশ জায়গা কম। ৬ বাইট তথ্যের জন্য ১২ বাইট খরচ — মোট জায়গার অর্ধেকই ফাঁকা ছিল!

  2. বাসের টিকিটের data model: ঢাকা থেকে রাজশাহীর একটি অনলাইন বাস টিকিটের জন্য একটি struct নকশা করুন। কোন কোন member, কী টাইপে, আর কোথায় enum লাগবে? (কোড না লিখে শুধু তালিকা বানালেও চলবে।)

    একটি ভালো নকশা হতে পারে: char ticket_id[16]; যাত্রার শহর দুটি — from, to (শহরের তালিকা ছোট ও নির্দিষ্ট হলে enum, নইলে char[20]); তারিখ ও সময় (time_t বা আলাদা একটি Date struct — nested); সিট নম্বর char seat[4] (যেমন "B3", কারণ এতে অক্ষর আছে); ভাড়া পয়সায় long long; যাত্রীর মোবাইল char phone[12] — সংখ্যা নয়, কারণ শুরুর ০ হারিয়ে যাবে আর এর ওপর কেউ যোগ-বিয়োগ করে না; বাসের ধরন enum { AC, NON_AC, SLEEPER }; অবস্থা enum { BOOKED, PAID, CANCELLED }.

    খেয়াল করুন সিদ্ধান্তগুলো: মোবাইল নম্বর char-এর array, টাকা পূর্ণসংখ্যায়, সীমিত পছন্দগুলো enum-এ। এই ভাবনাগুলোই data modelling.

  3. বাগ শিকার: নিচের কোডে দুটি ভুল আছে। খুঁজে বের করুন।
    void add_bonus(Student s) { s.gpa += 0.25; } — আর ডাকা হয়েছে add_bonus(cls[2]);. পাশাপাশি একটি Message m-এ m.kind = MSG_IMAGE; লেখার পর strcpy(m.as.text, "hi");.

    ভুল ১: add_bonus struct-টি by value পায় — একটি কপি। কপির GPA বাড়ে, আর ফাংশন শেষ হলেই কপি মুছে যায়। cls[2]-এর GPA একটুও বদলায় না। ঠিক রূপ: void add_bonus(Student *s) { s->gpa += 0.25; } আর ডাকতে হবে add_bonus(&cls[2]);.

    ভুল ২: ট্যাগ বলছে ছবি, কিন্তু union-এ লেখা হয়েছে টেক্সট। যে কোড পরে ট্যাগ দেখে m.as.image.width পড়বে, সে "hi"-এর বাইটগুলোকে প্রস্থ ভেবে একটি অর্থহীন সংখ্যা পাবে। ট্যাগ আর member সবসময় একসাথে বদলান — সবচেয়ে ভালো হয় একটি ফাংশনের ভেতরে, যেমন make_text_msg("hi").

  4. AI অডিট: যেকোনো AI-কে বলুন: “C-তে একটি লাইব্রেরির বই ব্যবস্থাপনা প্রোগ্রাম লেখো, যেখানে বইয়ের অবস্থা থাকবে — available, borrowed, lost.” পাওয়া কোডটি উপরের চেকলিস্ট ধরে যাচাই করুন। কয়টি ঘরে টিক দিতে পারলেন?

    সাধারণত যা চোখে পড়ে:

    • বইয়ের অবস্থা প্রায়ই int status; /* 0 = available, 1 = borrowed */ — মন্তব্যে অর্থ লেখা, কিন্তু enum নেই। status = 5 লিখলেও কেউ আটকাবে না।
    • void print_book(struct Book b) — পুরো struct by value, যেখানে ভেতরে হয়তো ১০০ বাইটের শিরোনাম আছে।
    • সংরক্ষণের জন্য fwrite(books, sizeof(struct Book), n, fp) — অন্য মেশিনে বা struct বদলানোর পর পুরোনো ফাইল ভুল পড়া হবে।

    এরপর AI-কে নির্দিষ্ট করে বলুন কোনগুলো ঠিক করতে হবে, আর কেন। কোড লেখা নয়, নকশার এই ভুলগুলো ধরাই আপনার আসল দক্ষতা।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমpadding যত কম সম্ভব রাখতে struct-এর member-গুলো কোন ক্রমে সাজাবেন? (▲▼ চেপে সরান)

  1. double total; — ৮ বাইট
  2. int count; — ৪ বাইট
  3. short year; — ২ বাইট
  4. char grade; — ১ বাইট

Student *p = &a; — p দিয়ে roll পড়ার সঠিক উপায় কোনটি?

  • p.roll
  • p->roll
  • *p.roll
  • &p->roll দিয়ে মান পাওয়া

struct X { char c; int i; }; — সাধারণত ৬৪-বিট Linux-এ sizeof(struct X) কত?

  • ৫
  • ৬
  • ৮
  • ১৬

union { int i; double d; char s[20]; } u; — এর মাপ সম্পর্কে কোনটি সত্য?

  • অন্তত ২০ বাইট — সবচেয়ে বড় member-এর মাপ, alignment-এর জন্য সাধারণত ২৪
  • ৪ + ৮ + ২০ = ৩২ বাইট
  • ৪ বাইট — সবচেয়ে ছোট member
  • শেষবার যে member-এ লেখা হয়েছে, তার মাপ

একটি tagged union-এ ট্যাগের কাজ কী?

  • union-এর জায়গা কমানো
  • compiler-কে ভুল member পড়া আটকাতে বাধ্য করা
  • প্রতিটি member-কে আলাদা মেমরি দেওয়া
  • জানানো যে union-এর কোন member এখন বৈধ

১০০০ বাইটের একটি struct একটি ফাংশন শুধু পড়বে, বদলাবে না। parameter কেমন হওয়া উচিত?

  • BigData d
  • const BigData *d
  • BigData *d
  • struct-টিকে global বানিয়ে ফেলা

AI লেনদেনের ধরন রেখেছে int type;, আর কোডে আছে if (type == 2). সবচেয়ে ভালো সংশোধন কোনটি?

  • পাশে একটি মন্তব্য লেখা: /* 2 = recharge */
  • ২-এর বদলে আরও বড় একটি সংখ্যা ব্যবহার করা
  • enum বানিয়ে if (type == TX_RECHARGE)
  • ধরনটি char type[20]-এ লেখা হিসেবে রাখা

একটি struct সরাসরি fwrite দিয়ে ফাইলে লিখে অন্য একটি মেশিনে পড়লে ভুল আসতে পারে। নিচের কোনটি এর কারণ নয়?

  • member-গুলোর নাম ফাইলে লেখা হয় না
  • দুই মেশিনে padding আলাদা হতে পারে
  • দুই মেশিনের endianness আলাদা হতে পারে
  • একই টাইপের মাপ দুই মেশিনে আলাদা হতে পারে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. বাস্তবের একটি জিনিসের সব তথ্য একসাথে রাখুন struct-এ; নাম দিন typedef দিয়ে, মান দিন নাম ধরে (.roll = 17).
  2. ভ্যারিয়েবল হাতে থাকলে ডট, ঠিকানা হাতে থাকলে তীর; বড় struct ফাংশনে যায় const ঠিকানা দিয়ে।
  3. sizeof ≥ member-এর যোগফল — alignment-এর জন্য padding বসে; বড় member আগে রাখলে জায়গা বাঁচে, মাপ জানতে সবসময় sizeof.
  4. পছন্দের তালিকা মানেই enum, magic number নয়; “কয়েক রকমের একটি” মানেই ট্যাগসহ union, আর পড়ার আগে সবসময় ট্যাগ দেখা।
  5. bit field মেমরির ভেতরে সুবিধাজনক, কিন্তু বিটের ক্রম compiler-নির্ভর; ফাইল বা নেটওয়ার্কে struct-এর মেমরির ছবি নয়, নির্দিষ্ট ফরম্যাট পাঠান।

পরের পর্বে: এ পর্যন্ত শেখা সবকিছু — ফাংশন, array, pointer, স্ট্রিং, ডায়নামিক মেমরি আর আজকের struct — একসাথে জুড়ে আপনি বানাবেন একটি আসল command-line টুল। শুরু হচ্ছে কোর্সের মাঝপথের প্রজেক্ট।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান