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

আধুনিক C — C99 থেকে C23, কোন সংস্করণে কী এলো

Modern C — From K&R to C23: what each standard fixed

Taught in Bangla, this lesson traces C from K&R to C23 and what each standard fixed: C99 features like stdint.h and designated initializers, C11 static assertions, _Generic and threads, the C17 and C23 updates, compile-time checks, and portable modern style.

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

  • K&R, C89, C99, C11, C17, C23 — কোনটি কবে, কোন সমস্যার সমাধানে এসেছে
  • -std= flag আর __STDC_VERSION__ দিয়ে নিজের compiler-এর স্ট্যান্ডার্ড চেনা
  • Flexible array member, compound literal, _Generic, _Static_assert — ভেতরে কী ঘটে
  • C23-এর নতুন জিনিসগুলো, আর কোনটি কোথায় সাবধানে ব্যবহার করবেন
  • “আধুনিক কিন্তু portable” C লেখার অভ্যাস, আর AI-এর মেশানো কোড ধরার চেকলিস্ট

১ · C কি ১৯৭৮ সালেই থেমে আছে?

অনেকের মনে একটি ধারণা আছে — C হলো “পুরোনো ভাষা”, Dennis Ritchie যেমন লিখে গেছেন, তেমনই রয়ে গেছে। ইন্টারনেটে খুঁজলে আজও এমন টিউটোরিয়াল পাবেন, যেখানে void main(), conio.h আর gets() দিয়ে C শেখানো হয়। কিন্তু সত্যিটা হলো, C একটি জীবন্ত ভাষা। প্রায় প্রতি দশকে এর নিয়ম হালনাগাদ হয়েছে, আর প্রতিবারই কোনো একটি বাস্তব কষ্ট দূর করা হয়েছে।

এই কোর্সে এতক্ষণ আপনি যে C লিখেছেন, সেটি আসলে বেশ আধুনিক C. for (int i = 0; …) লেখা, // দিয়ে মন্তব্য, int32_t, bool — এগুলোর কোনোটিই ১৯৭৮ সালের C-তে ছিল না। এই পর্বে আমরা পেছনে ফিরে দেখব কোন জিনিস কবে, কেন এলো। তাতে দুটি বড় লাভ হবে। এক, পুরোনো বা অন্যের লেখা কোড পড়লে বুঝবেন সেটি কোন যুগের। দুই, নিজের প্রজেক্টে সচেতনভাবে একটি স্ট্যান্ডার্ড বেছে নিতে পারবেন।

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

মূল ধারণা

C-এর কোনো “মালিক কোম্পানি” নেই। ভাষার আনুষ্ঠানিক নিয়মের বইটির নাম C standard, আর এটি লেখে ISO-র একটি কমিটি — WG14. সেখানে GCC, Clang, Microsoft-এর মতো compiler নির্মাতা আর নানা কোম্পানির প্রতিনিধিরা প্রস্তাব আনেন, তর্ক করেন, ভোট দেন। Compiler-গুলো তারপর সেই নিয়ম মেনে চলার চেষ্টা করে। তাই “C-এ এটা করা যায় কি না” প্রশ্নের দুটি অংশ আছে: স্ট্যান্ডার্ড কী বলে, আর আপনার compiler কতটুকু মানে।

পর্ব ০১-এ আপনি C-এর জন্মকথা দেখেছেন — ১৯৭২ সালে Bell Labs, ১৯৭৮ সালে Kernighan ও Ritchie-র বই। সেই বইয়ের C-কে আজ বলা হয় K&R C. ১৯৮০-এর দশকে নানা কোম্পানি নিজের মতো করে C compiler বানাতে থাকে, আর এক compiler-এর কোড আরেকটিতে চলত না। সেই বিশৃঙ্খলা থামাতেই ১৯৮৯ সালে আসে প্রথম আনুষ্ঠানিক স্ট্যান্ডার্ড — ANSI C. সেখান থেকে শুরু হয় স্ট্যান্ডার্ডের যাত্রা, যা আজও চলছে।

২ · সময়রেখা — K&R থেকে C23

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

সংস্করণবছরপ্রধান সংযোজন__STDC_VERSION__
K&R C১৯৭৮বইয়ের বর্ণনা, আনুষ্ঠানিক নিয়ম নেই; টাইপ ছাড়া প্যারামিটারনেই
C89 / C90১৯৮৯ / ১৯৯০Function prototype, void, const, স্ট্যান্ডার্ড libraryনেই (শুধু __STDC__)
C95১৯৯৫ছোট সংশোধনী: wide character (wchar.h)199409L
C99১৯৯৯//, long long, stdint.h, stdbool.h, VLA, inline199901L
C11২০১১_Static_assert, _Generic, thread, atomic; gets বাদ201112L
C17২০১৮শুধু ভুল সংশোধন — নতুন বৈশিষ্ট্য প্রায় নেই201710L
C23২০২৪bool, nullptr, constexpr, typeof, [[attributes]], 0b202311L

একটি বিষয় খেয়াল করুন। নামের সংখ্যা আর প্রকাশের বছর সবসময় মেলে না। C17 প্রকাশ পায় ২০১৮ সালে, আর C23-এর আনুষ্ঠানিক নাম ISO/IEC 9899:2024 — প্রকাশ ২০২৪ সালে। নামটি এসেছে কাজ শেষ হওয়ার আনুমানিক বছর থেকে। এজন্য C17-কে কেউ কেউ C18-ও বলেন।

আপনার compiler কোন স্ট্যান্ডার্ড মানছে?

GCC ও Clang-এ -std= flag দিয়ে স্ট্যান্ডার্ড বেছে নেওয়া যায়। দুই রকম নাম আছে: c11, c17, c23 মানে “শুধু স্ট্যান্ডার্ড মেনে চলো”; আর gnu11, gnu17, gnu23 মানে “স্ট্যান্ডার্ড, তার সাথে GCC-র নিজস্ব বাড়তি সুবিধা” — যাকে বলে GNU extension.

gcc -std=c11 -Wall -Wextra -pedantic main.c -o main   # কঠোরভাবে C11
gcc -std=c23 -Wall -Wextra main.c -o main             # C23 মেনে
gcc -dM -E -x c /dev/null | grep STDC_VERSION         # ডিফল্ট কোনটি দেখুন

-pedantic যোগ করলে স্ট্যান্ডার্ডের বাইরের কিছু লিখলেই compiler warning দেয়। কোড অন্য compiler-এ নেওয়ার পরিকল্পনা থাকলে এটি খুব কাজের। আর কোডের ভেতর থেকে স্ট্যান্ডার্ড জানতে আছে __STDC_VERSION__ — একটি predefined macro, যা পর্ব ২৭-এ এক ঝলক দেখেছেন। এর মান সংখ্যা, তাই #if __STDC_VERSION__ >= 201112L লিখে “C11 বা নতুন হলে” শর্ত দেওয়া যায়।

এই পাতার “Run” বোতাম যে compiler-এ কোড পাঠায়, সেটি খুব নতুন একটি GCC. GCC 15 থেকে ডিফল্ট স্ট্যান্ডার্ড gnu23, তাই নিচের প্রথম প্রোগ্রামটি চালালে 202311 দেখবেন। আপনার কম্পিউটারের GCC পুরোনো হলে সম্ভবত 201710 (gnu17) দেখাবে। একই কোড, ভিন্ন compiler — ভিন্ন ডিফল্ট। এজন্যই পেশাদার প্রজেক্টে -std= সবসময় স্পষ্ট লিখে দেওয়া হয়।

একই প্রোগ্রাম, চার যুগে

কথার চেয়ে চোখে দেখা ভালো। নিচে একটি ছোট প্রোগ্রাম — ১ থেকে ৩ পর্যন্ত যোগ — চারটি যুগের লেখারীতিতে দেখানো হলো। “পরে” চাপুন এবং প্রতি ধাপে কী বদলাল, তা খেয়াল করুন।

  1. K&R১৯৭৮টাইপ ছাড়া প্যারামিটার
  2. C89১৯৮৯prototype এলো
  3. C99১৯৯৯আধুনিক লেখারীতি
  4. C23২০২৪keyword ও attribute
K&R C১৯৭৮ সালের বইয়ের ধাঁচ
add(a, b)          /* ফেরত টাইপ নেই = int ধরে নেওয়া */
int a, b;          /* প্যারামিটারের টাইপ আলাদা লাইনে */
{
    return a + b;
}

main()
{
    int i, total;
    total = 0;
    for (i = 1; i <= 3; i++)
        total = add(total, i);
    printf("%d\n", total);   /* header ছাড়াই */
}
C89 / ANSI Cprototype, void, return 0
#include <stdio.h>

int add(int a, int b)   /* টাইপসহ প্যারামিটার */
{
    return a + b;
}

int main(void)
{
    int i, total;          /* সব ঘোষণা ব্লকের শুরুতে */
    total = 0;
    for (i = 1; i <= 3; i++)
        total = add(total, i);
    printf("%d\n", total);
    return 0;
}
C99যেখানে দরকার সেখানেই ঘোষণা
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

static inline int32_t add(int32_t a, int32_t b) { return a + b; }

int main(void) {
    int32_t total = 0;                // নতুন ধাঁচের মন্তব্য
    for (int i = 1; i <= 3; i++)      // i শুধু লুপের ভেতরে
        total = add(total, i);
    bool ok = (total == 6);
    printf("%d %d\n", (int)total, ok);
    return 0;
}
C23keyword, constexpr, attribute
#include <stdio.h>
#include <stdint.h>
                                      // stdbool.h আর লাগে না
[[nodiscard]] static inline int32_t add(int32_t a, int32_t b) { return a + b; }

int main(void) {
    constexpr int N = 3;
    int32_t total = 0;
    for (int i = 1; i <= N; i++)
        total = add(total, i);
    bool ok = (total == 6);           // bool এখন সরাসরি keyword
    static_assert(sizeof(int32_t) == 4);
    printf("%d %d\n", (int)total, ok);
    return 0;
}
  1. K&R — বিশ্বাসের যুগ। ফাংশনের ফেরত টাইপ না লিখলে compiler ধরে নিত int. প্যারামিটারের টাইপ লেখা হতো আলাদা লাইনে, আর ফাংশন ডাকার সময় compiler যাচাই করত না আপনি ঠিক সংখ্যক ও ঠিক টাইপের আর্গুমেন্ট দিচ্ছেন কি না। add(1.5) লিখলেও কোনো error আসত না — শুধু ভুল ফল।
  2. C89 — prototype-এর যুগ। সবচেয়ে বড় পরিবর্তন function prototype: int add(int a, int b). এখন compiler প্রতিটি ডাক যাচাই করতে পারে। void এলো (main(void) মানে “কোনো প্যারামিটার নেই”)। তবে সব ঘোষণা ব্লকের একদম শুরুতে করতে হতো, আর মন্তব্য শুধু /* */.
  3. C99 — আধুনিক লেখারীতি। ভ্যারিয়েবল যেখানে দরকার সেখানেই ঘোষণা করা যায়; for (int i…)-এর i লুপের বাইরে দেখা যায় না, ফলে ভুল কমে। // মন্তব্য, নির্দিষ্ট মাপের int32_t, আর stdbool.h-এর bool এলো। ছোট ফাংশনের জন্য inline ইঙ্গিত দেওয়া গেল।
  4. C23 — পরিষ্কার-পরিচ্ছন্নতা। bool আর header লাগে না, এটি সরাসরি keyword. constexpr দিয়ে টাইপসহ ধ্রুবক, [[nodiscard]] দিয়ে “ফেরত মান ফেলে দিলে সতর্ক করো”, আর বার্তা ছাড়াই static_assert. চারটি সংস্করণের ফল একই — ৬। বদলেছে শুধু নিরাপত্তা আর পড়ার সহজতা।

৩ · C99 — আধুনিক C-এর আসল শুরু

C89 থেকে C99 পর্যন্ত দশ বছরে প্রোগ্রামিং জগৎ অনেক বদলে যায়। ৬৪-বিট কম্পিউটার আসছে, বৈজ্ঞানিক গণনায় C-কে Fortran-এর সাথে পাল্লা দিতে হচ্ছে, আর C++ থেকে কিছু ভালো ধারণা (যেমন // মন্তব্য, inline) C-তে আনার দাবি উঠছে। C99 ছিল সেই চাপের জবাব। এই কোর্সে আপনি অনেকগুলো আগেই ব্যবহার করেছেন:

📏 নির্দিষ্ট মাপের টাইপ

stdint.h-এর int32_t, uint8_t — যেকোনো যন্ত্রে মাপ একই। সাথে long long (অন্তত ৬৪ বিট)।

পর্ব ০৬, ২৬

🏷️ Designated initializer

নাম ধরে মান বসানো: { .roll = 17, .gpa = 3.8 }. ক্রম ভুলের ভয় নেই, বাকি member শূন্য।

পর্ব ২২

📐 VLA

চলার সময় মাপ ঠিক হওয়া অ্যারে: int grid[rows][cols];. C11-এ এটি ঐচ্ছিক করে দেওয়া হয়।

পর্ব ২১

⚡ inline ও restrict

inline ছোট ফাংশনের ইঙ্গিত; restrict বলে “এই pointer-এর মেমরি আর কেউ ছোঁবে না”, তাই optimizer সাহসী হতে পারে।

পর্ব ২৭, ৪৫

তালিকায় আরও আছে: snprintf (বাফারের মাপ জেনে লেখে — পর্ব ১৯), stdbool.h, __func__ (বর্তমান ফাংশনের নাম), variadic macro আর complex সংখ্যা। কিন্তু এর মধ্যে দুটি জিনিস একটু আলাদা মনোযোগ দাবি করে, কারণ এগুলো নতুনরা প্রায়ই চেনেন না: compound literal আর flexible array member.

Compound literal — নাম ছাড়া একটি struct

ধরুন একটি ফাংশন struct point নেয়। C89-এ আগে একটি ভ্যারিয়েবল বানাতে হতো, মান বসাতে হতো, তারপর পাঠাতে হতো। C99-এ সরাসরি লেখা যায় show((struct point){ .x = 3, .y = 7 }); — বন্ধনীর ভেতরে টাইপ, তারপর শুরুর মান। এটি একটি আসল object তৈরি করে, যার নাম নেই। ফাংশনের ভেতরে লিখলে এটি সেই ব্লকের শেষ পর্যন্ত বাঁচে, ঠিক সাধারণ local ভ্যারিয়েবলের মতো।

Flexible array member — header আর ডেটা এক টুকরোয়

নেটওয়ার্কের একটি মেসেজের কথা ভাবুন: শুরুতে দৈর্ঘ্য, তারপর যত খুশি byte. পুরোনো উপায়ে struct-এ একটি pointer রাখা হতো, আর ডেটার জন্য আলাদা malloc করতে হতো — অর্থাৎ দুটি বরাদ্দ, দুটি free, আর একটি ভুলে গেলেই memory leak. C99 একটি সুন্দর সমাধান দিল: struct-এর শেষ member হিসেবে মাপ ছাড়া অ্যারে।

struct msg {
    size_t len;     // কত byte ডেটা
    char data[];    // flexible array member — মাপ নেই, শেষে থাকতেই হবে
};

struct msg *m = malloc(sizeof *m + n + 1);   // header + n byte + '\0'

এখানে sizeof *m শুধু header-এর মাপ দেয় — data এই হিসাবে কোনো জায়গা নেয় না। তার সাথে যত byte যোগ করবেন, data ততটা লম্বা হবে। নিচের ডায়াগ্রামে মেমরিতে কী ঘটে, ধাপে ধাপে দেখুন।

  1. নকশাstruct msgsizeof = 8
  2. বরাদ্দmalloc(8+n+1)এক টুকরো
  3. ভরাটlen, dataপাশাপাশি
  4. মুক্তিfree(m)একবারেই শেষ
struct msg৬৪-বিট Linux-এ
sizeof(struct msg) == 8

┌──────────────┬
│ len (8 byte) │ data[] → এখনো ০ byte
└──────────────┴
data শুধু একটি “চিহ্ন”: header যেখানে শেষ, ডেটা সেখান থেকে শুরু
malloc(sizeof *m + n + 1)n = 22
m ──►┌──────────────┬────────────────────────────┐
     │ len  (8)     │ data (22 + 1 = 23 byte)    │
     └──────────────┴────────────────────────────┘
     একটিই ধারাবাহিক মেমরি ব্লক — মোট 31 byte
m->len = n; memcpy(m->data, text, n + 1);ভরাট
m ──►┌──────────────┬────────────────────────────┐
     │ len = 22     │ b K a s h :   5 0 0 ... \0 │
     └──────────────┴────────────────────────────┘
     header-এর ঠিক পরেই ডেটা — cache-এর জন্যও ভালো
free(m)পুরোনো উপায়ের সাথে তুলনা
Flexible array:  malloc ×1   free ×1
Pointer member:   malloc ×2   free ×2   (একটি ভুললেই leak)

ভুল: struct msg x;  // stack-এ বানালে data-র জন্য জায়গাই নেই
  1. নকশা। struct msg-এ শুধু len-এর জায়গা আছে — সাধারণ ৬৪-বিট Linux-এ size_t ৮ byte. data[] কোনো জায়গা নেয় না; এটি শুধু বলে “header যেখানে শেষ, অ্যারে সেখান থেকে শুরু”. এমন member থাকতে পারে শুধু struct-এর একদম শেষে, আর তার আগে অন্তত একটি সাধারণ member লাগে।
  2. এক টুকরো মেমরি। sizeof *m (৮) + লেখার দৈর্ঘ্য (২২) + শেষের '\0'-এর জন্য ১ = ৩১ byte, একটিমাত্র malloc-এ। গার্মেন্টসের একটি কার্টনের কথা ভাবুন — ওপরে লেবেল (কত পিস), নিচে মাল। লেবেল আর মাল আলাদা বাক্সে রাখলে হারানোর ভয় থাকত।
  3. ভরাট। m->len-এ দৈর্ঘ্য, m->data-তে লেখা কপি। ডেটা header-এর ঠিক পরের ঠিকানায় থাকে, তাই একবার মেমরি থেকে আনলেই দুটোই CPU cache-এ চলে আসে। নেটওয়ার্ক প্যাকেট, ফাইলের রেকর্ড, Linux kernel-এর বহু struct ঠিক এই কৌশল ব্যবহার করে।
  4. একবারেই মুক্তি। একটি free(m)-এই সব শেষ। Pointer member হলে দুটি malloc, দুটি free লাগত, আর ক্রম ভুল হলে use-after-free. সতর্কতা: এমন struct stack-এ সাধারণভাবে বানালে data-র জন্য কোনো জায়গা থাকে না, আর এমন struct কপি করলে (*a = *b) শুধু header কপি হয়।

এবার তিনটি C99 জিনিস একসাথে চালিয়ে দেখুন — designated initializer, compound literal আর flexible array member. প্রথম লাইনে আপনার compiler কোন স্ট্যান্ডার্ড ব্যবহার করছে, সেটিও ছাপা হবে।

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct point { int x, y; };
struct msg { size_t len; char data[]; };   // flexible array member

static void show(struct point p) { printf("point: x=%d y=%d\n", p.x, p.y); }

int main(void) {
    printf("__STDC_VERSION__ = %ld\n", (long)__STDC_VERSION__);
    show((struct point){ .y = 7, .x = 3 });   // compound literal
    const char *text = "bKash: 500 Tk received";
    size_t n = strlen(text);
    struct msg *m = malloc(sizeof *m + n + 1);
    if (m == NULL) return 1;
    m->len = n;
    memcpy(m->data, text, n + 1);
    printf("len=%zu data=%s\n", m->len, m->data);
    free(m);
    return 0;
}

প্রথম লাইনে দেখবেন 202311 — অর্থাৎ এই পাতার GCC ডিফল্টভাবে C23 (gnu23) মেনে চলছে। show(...)-এ নাম ছাড়া একটি struct point তৈরি হলো; .y আগে লিখলেও .x ঠিক জায়গায় বসেছে — নাম ধরে মান বসানোর সুবিধা। শেষে একটিমাত্র malloc-এ header ও ২৩ byte ডেটা নিয়ে মেসেজ তৈরি, ছাপা, আর একটি free. %zu হলো size_t ছাপার সঠিক format — এটিও C99-এর দান।

৪ · C11 — টাইপ-বুদ্ধি, নিরাপত্তা আর একাধিক core

২০০০-এর দশকে কম্পিউটারের গতি বাড়ানোর পথ বদলে যায়। এক core-কে আরও দ্রুত না করে কোম্পানিগুলো একটি চিপে একাধিক core বসাতে শুরু করে। কিন্তু C স্ট্যান্ডার্ডে তখনো “thread” বলে কিছু ছিল না — সবাই নিজের OS-এর library (যেমন POSIX-এর pthread) ব্যবহার করত, আর স্ট্যান্ডার্ড মেমরি মডেল না থাকায় একসাথে চলা কোডের আচরণ ঠিকমতো বলা যেত না। একই সময়ে buffer overflow-জনিত নিরাপত্তা-ত্রুটি বড় মাথাব্যথা হয়ে দাঁড়ায়। C11 এই দুই দিকেই কাজ করেছে।

সংযোজনকী করেছোট উদাহরণ
_Static_assertcompile-এর সময় শর্ত যাচাই_Static_assert(sizeof(int) == 4, "need 32-bit int");
_Genericটাইপ দেখে বিকল্প বেছে নেওয়া_Generic(x, int: "int", double: "double")
Anonymous struct/unionভেতরের member সরাসরি ব্যবহারstruct v { union { float xy[2]; struct { float x, y; }; }; };
_Alignas / _Alignofalignment নিয়ন্ত্রণ_Alignas(64) char buf[64];
threads.h, stdatomic.hস্ট্যান্ডার্ড thread, atomic ও মেমরি মডেলatomic_int hits; (পর্ব ৪৯)
_Noreturn“এই ফাংশন ফেরে না”_Noreturn void die(const char *msg);
gets() বাদসীমা না মানা ফাংশন স্ট্যান্ডার্ড থেকে মুছে ফেলাবদলে fgets (পর্ব ২৬)
Annex Kঐচ্ছিক “নিরাপদ” ফাংশনstrcpy_s, gets_s

লক্ষ করুন, নতুন keyword-গুলো দেখতে অদ্ভুত — underscore আর বড় হাতের অক্ষর দিয়ে শুরু (_Generic, _Noreturn). কারণটি backward compatibility. লক্ষ লক্ষ পুরোনো প্রোগ্রামে কেউ হয়তো noreturn বা alignas নামে ভ্যারিয়েবল রেখেছেন। নতুন keyword সেই নাম কেড়ে নিলে সেসব কোড ভেঙে যেত। আর _ + বড় হাতের অক্ষর দিয়ে শুরু নাম C-তে আগে থেকেই স্ট্যান্ডার্ডের জন্য সংরক্ষিত, সাধারণ প্রোগ্রামারের ব্যবহারের অনুমতি ছিল না। সুন্দর নাম চাইলে header ছিল — stdalign.h দিলে alignas, stdnoreturn.h দিলে noreturn.

gets() কেন স্ট্যান্ডার্ড থেকেই মুছে ফেলা হলো

gets(buf) জানেই না buf কত বড়। ব্যবহারকারী যত লম্বা লাইন দেবেন, ততটাই লিখে যাবে — বাফারের বাইরেও। ১৯৮৮ সালের বিখ্যাত Morris worm যেসব দুর্বলতা দিয়ে ছড়িয়েছিল, তার একটি ছিল fingerd প্রোগ্রামে gets-এর ব্যবহার। C99-এর পরের সংশোধনীতে একে deprecated ঘোষণা করা হয়, আর C11 একে পুরোপুরি বাদ দেয়। স্ট্যান্ডার্ড থেকে কোনো ফাংশন মুছে ফেলা প্রায় অভূতপূর্ব ঘটনা — এতেই বোঝা যায় বিপদ কত বড় ছিল।

_Generic — C-তে টাইপ অনুযায়ী আচরণ

C-তে একই নামে দুটি ফাংশন রাখা যায় না (C++-এর মতো overloading নেই)। তাই গণিতের library-তে sqrt, sqrtf, sqrtl — তিনটি আলাদা নাম। C99 tgmath.h-এ “type-generic” sqrt দিয়েছিল, কিন্তু সাধারণ প্রোগ্রামার নিজে এমন জিনিস বানাতে পারতেন না। C11-এর _Generic সেই ক্ষমতা সবার হাতে দিল। এটি compile করার সময় একটি expression-এর টাইপ দেখে তালিকা থেকে একটি বিকল্প বেছে নেয় — চলার সময় কোনো খরচ নেই। নিচের ডায়াগ্রামে দেখুন কোন আর্গুমেন্টে কোন শাখা বাছাই হয়।

1#define print_any(x) _Generic((x), \2    int:    print_int,    \3    double: print_double, \4    char *: print_str)(x)56char city[] = "Dhaka";7print_any(42);8print_any(3.14159);9print_any(city);10print_any('A');11print_any(2.5f);
Compiler যা দেখছে
(x)-এর টাইপ
বাছাই
  1. 42 — সোজা মিল। পূর্ণসংখ্যার লিটারালের টাইপ int. Compiler তালিকায় int: পেল, তাই পুরো _Generic(...) অংশটি হয়ে গেল শুধু print_int. শেষের (x) মিলে দাঁড়াল print_int(42). এই বাছাই হয় compile-এর সময়, চলার সময় কোনো if নেই।
  2. 3.14159 — double. দশমিক লিটারাল পেছনে কিছু না থাকলে double. তাই print_double বাছাই হলো, আর %.2f দিয়ে ছাপা হলো 3.14.
  3. city — অ্যারে থেকে pointer. city-র আসল টাইপ char[6], তালিকায় যা নেই। কিন্তু _Generic টাইপ দেখার আগে অ্যারেকে pointer-এ রূপান্তর করে (পর্ব ১৮-এর array decay), ফলে টাইপ হয় char * — বাছাই print_str.
  4. 'A' — চমক! অনেকে ভাবেন 'A'-র টাইপ char. কিন্তু C-তে অক্ষর লিটারালের টাইপ int (C++-এ char) — এটি C আর C++-এর একটি পরিচিত পার্থক্য। তাই print_int চলল, ছাপা হলো 65 (A-এর ASCII মান)।
  5. 2.5f — কোনো মিল নেই। f থাকায় টাইপ float; তালিকায় float: নেই, default:-ও নেই। _Generic স্বয়ংক্রিয়ভাবে float-কে double বানায় না — তাই compile error. ভুলটা চলার সময় নয়, প্রোগ্রাম তৈরির আগেই ধরা পড়ল। চাইলে default: print_other বা float: print_double যোগ করে সমাধান করা যায়।

এবার আসল কোডটি চালিয়ে দেখুন। সাথে আছে একটি _Static_assert — এটি নিশ্চিত করে যে int32_t সত্যিই ৪ byte; না হলে প্রোগ্রাম তৈরিই হবে না।

#include <stdio.h>
#include <stdint.h>

static void print_int(int v)       { printf("int    : %d\n", v); }
static void print_double(double v) { printf("double : %.2f\n", v); }
static void print_str(char *s)     { printf("string : %s\n", s); }

#define print_any(x) _Generic((x), \
    int:    print_int,             \
    double: print_double,          \
    char *: print_str)(x)

_Static_assert(sizeof(int32_t) == 4, "int32_t must be 4 bytes");

int main(void) {
    char city[] = "Dhaka";
    print_any(42);
    print_any(3.14159);
    print_any(city);
    print_any('A');
    return 0;
}

চারটি ডাক, চারটি ভিন্ন ফাংশন — অথচ আপনি লিখেছেন একটিই নাম print_any. _Generic আসলে একটি macro-র ভেতরে বসে “compile-time switch” হিসেবে কাজ করে। শেষ লাইনে int : 65 দেখে অবাক হবেন না — C-তে 'A' একটি int. _Static_assert ফাইলের যেকোনো জায়গায় (ফাংশনের বাইরেও) বসানো যায়, আর এর জন্য প্রোগ্রাম চলার সময় একটি নির্দেশও খরচ হয় না। এই দুটিই C11-এর, তাই -std=c11 বা gnu17-তেও একইভাবে চলবে।

Annex K — ভালো উদ্দেশ্য, ব্যর্থ বাস্তবায়ন

C11 একটি ঐচ্ছিক অংশ যোগ করেছিল — Annex K, যেখানে strcpy_s, strcat_s, gets_s-এর মতো ফাংশন আছে, যারা গন্তব্যের মাপ নেয় এবং সীমা পার হলে একটি “constraint handler” ডাকে। ধারণাটি মূলত Microsoft-এর। কিন্তু সমস্যা হলো, Microsoft-এর নিজের বাস্তবায়ন স্ট্যান্ডার্ডের সাথে পুরোপুরি মেলে না, আর glibc বা musl-এর মতো বড় open-source library এটি বানায়নি। ফলে Annex K দিয়ে লেখা কোড প্রায় কোথাও portable নয়। ২০১৫ সালে কমিটিতে জমা দেওয়া একটি অভিজ্ঞতা-প্রতিবেদন এটিকে বাদ দেওয়ার প্রস্তাবও করেছিল। আজও এটি ঐচ্ছিক অংশ হিসেবে রয়ে গেছে, কিন্তু বাস্তবে খুব কম ব্যবহৃত। (ভাবনার প্রশ্ন ৩-এ বিস্তারিত।)

৫ · C17 আর C23 — বিরতি, তারপর বড় সংস্কার

C17 নিয়ে বলার কথা কম, আর সেটাই এর পরিচয়। এটি C11-এর ভেতরে পাওয়া অস্পষ্টতা ও ভুলগুলোর (defect report) সমাধান — নতুন কোনো বৈশিষ্ট্য নেই। বহু বছর GCC-র ডিফল্ট ছিল gnu17, তাই আজকের বেশিরভাগ চালু প্রজেক্ট কার্যত C17-এ লেখা।

C23 এর ঠিক উল্টো — অনেক বছরের মধ্যে সবচেয়ে বড় পরিবর্তন। এর একটি বড় অংশ হলো দীর্ঘদিন ধরে compiler-গুলোর extension হিসেবে থাকা জিনিসকে স্ট্যান্ডার্ডে তোলা, আর C++-এর সাথে কিছু লেখারীতি মিলিয়ে নেওয়া। প্রধান সংযোজনগুলো দেখুন:

বৈশিষ্ট্যআগে কী করতেনC23-এ
bool, true, false keyword#include <stdbool.h>সরাসরি লিখুন, header লাগে না
nullptrNULL (যা 0 বা (void*)0 — implementation-এর ওপর নির্ভর)নিজস্ব টাইপ nullptr_t, সবসময় pointer
constexpr (object)#define N 10 বা enumconstexpr int N = 10; — টাইপসহ, compile-time
typeof, typeof_unqualGCC-র __typeof__ extensionস্ট্যান্ডার্ড
auto দিয়ে type inferenceটাইপ লিখতেই হতোauto rate = 3.5; → double
[[attributes]]__attribute__((...)) (GCC)[[nodiscard]], [[maybe_unused]], [[deprecated]], [[fallthrough]], [[noreturn]]
Binary literal, digit separatorhex লিখে মাথায় হিসাব0b1010, 1'000'000
#embedবাইরের টুল দিয়ে ফাইলকে বিশাল অ্যারেতে রূপান্তরunsigned char logo[] = { #embed "logo.png" };
#elifdef, #elifndef, __has_include#elif defined(X)ছোট ও পরিষ্কার
static_assert বার্তা ছাড়া_Static_assert(cond, "msg")static_assert(cond);
খালি initializer {}= {0}struct cfg c = {}; — সব শূন্য
unreachable()__builtin_unreachable()stddef.h-এ স্ট্যান্ডার্ড macro
stdbit.hbuiltin বা নিজে লেখা লুপstdc_count_ones(x) (পর্ব ০৯-এর popcount)
memset_explicitmemset — optimizer বাদ দিতে পারতগোপন ডেটা মোছা নিশ্চিত
_BitInt(N)নেইনিজের পছন্দের বিট-সংখ্যার পূর্ণসংখ্যা

কয়েকটি জিনিস একসাথে দেখতে এমন দেখায় (এই অংশটি শুধু পড়ার জন্য; C23-সমর্থিত compiler-এ চলে):

#include <stdbit.h>

constexpr unsigned MASK = 0b1111'0000;         // binary + digit separator
[[nodiscard]] int save_file(const char *path); // ফল উপেক্ষা করলে warning

void demo(int *p) {
    if (p == nullptr) return;                  // nullptr, NULL নয়
    typeof(*p) copy = *p;                      // copy-র টাইপ int
    auto bits = stdc_count_ones(MASK);         // কয়টি 1-বিট? 4
    struct { int a, b; } pair = {};            // সব শূন্য
    (void)copy; (void)bits; (void)pair;
}

যা চলে গেছে বা বদলে গেছে

C23 শুধু যোগ করেনি, কিছু পুরোনো জিনিস সরিয়েও দিয়েছে। এগুলো জানা জরুরি, কারণ পুরোনো কোড বা AI-এর কোড এখানেই আটকাতে পারে:

  • K&R ধাঁচের ফাংশন সংজ্ঞা বাদ। int add(a, b) int a, b; {…} আর স্ট্যান্ডার্ড C নয়। তবে GCC এখনো gnu মোডে এটি মেনে নেয়, শুধু old-style function definition warning দেয় — বহু পুরোনো কোড যাতে হঠাৎ ভেঙে না যায়।
  • int f();-এর অর্থ বদলেছে। আগে খালি বন্ধনী মানে ছিল “প্যারামিটার সম্পর্কে কিছু বলছি না”, তাই f(1, 2, 3) ডাকলেও compiler চুপ থাকত। C23-এ () মানে (void) — কোনো প্যারামিটার নেই, ঠিক C++-এর মতো।
  • Two's complement বাধ্যতামূলক। আগে স্ট্যান্ডার্ড ঋণাত্মক সংখ্যার তিন রকম উপস্থাপন মেনে নিত (sign-magnitude, ones' complement, two's complement). বাস্তবে কয়েক দশক ধরে সব যন্ত্র two's complement ব্যবহার করে, তাই C23 এটিকেই একমাত্র নিয়ম করেছে (পর্ব ০৫)। খেয়াল করুন: signed overflow এখনো undefined behavior — পর্ব ৪৬-এর আলোচনা বদলায়নি।

নতুন সবকিছু ব্যবহারের আগে নিজের compiler যাচাই করুন। GCC ও Clang-এর সাম্প্রতিক সংস্করণে C23-এর বেশিরভাগই আছে, কিন্তু সব compiler সমান এগিয়ে নেই — বিশেষ করে embedded কাজের পুরোনো বা বিক্রেতার নিজস্ব compiler-এ। #embed-এর মতো কিছু জিনিস একদম সম্প্রতি এসেছে। আর unreachable() ভুল জায়গায় বসালে সেটি সরাসরি undefined behavior — compiler ধরে নেবে সেই পথ কখনো আসবে না, আর সেই অনুযায়ী কোড কেটে ফেলতে পারে।

৬ · Compile-এর সময় যাচাই বনাম চলার সময় যাচাই

আধুনিক C-এর একটি বড় শিক্ষা হলো: যত আগে ভুল ধরা পড়ে, তত কম খরচ। static_assert আর assert (পর্ব ৩০) দেখতে প্রায় এক, কিন্তু কাজ করে দুই ভিন্ন সময়ে। একটি ধরে প্রোগ্রাম তৈরির আগে, অন্যটি প্রোগ্রাম চলার সময় — তাও যদি সেই লাইনে পৌঁছানো হয়। নিচে একটি ধারণা দুই ভাবে যাচাই করা দেখুন: “আমাদের ফাইল-ফরম্যাটের header ঠিক ১৬ byte হতে হবে।”

  1. কোড লেখাheader.cদুই রকম যাচাই
  2. Compilestatic_assertএখানেই থামে
  3. চলার সময়assertলাইনে পৌঁছালে
  4. Release-DNDEBUGassert উধাও
header.cএকই শর্ত, দুই জায়গায়
#include <assert.h>
#include <stdint.h>

struct file_header { uint32_t magic; uint32_t ver; uint64_t size; };

static_assert(sizeof(struct file_header) == 16, "header must be 16 bytes");

void read_header(const struct file_header *h) {
    assert(h != NULL);      // চলার সময় যাচাই
    ...
}
Compilerকেউ struct-এ একটি field যোগ করলেন
$ gcc -c header.c
header.c:6:1: error: static assertion failed: "header must be 16 bytes"

→ কোনো .o বা executable তৈরি হয়নি। ভুল ব্যবহারকারীর কাছে পৌঁছানোর প্রশ্নই নেই।
TerminalNULL দিয়ে ডাকা হলো
$ ./reader
reader: header.c:9: read_header: Assertion `h != NULL' failed.
Aborted (core dumped)

→ ধরা পড়ল, কিন্তু শুধু তখনই যখন এই পথটি সত্যিই চলল।
gcc -O2 -DNDEBUGপ্রকাশের build
static_assert  → এখনো যাচাই হয় (compile-এর সময়)
assert(...)    → পুরোপুরি মুছে যায় — কোনো যাচাই নেই

তাই assert-এর ভেতরে কখনো দরকারি কাজ (যেমন i++) রাখবেন না।
  1. দুই রকম অনুমান। কিছু অনুমান কোডের গঠন নিয়ে — struct-এর মাপ, int কত বিট, একটি enum-এর সংখ্যা। এগুলো compile-এর সময়েই জানা, তাই static_assert. আর কিছু অনুমান চলার সময়ের ডেটা নিয়ে — pointer NULL কি না, index সীমার ভেতরে কি না। সেগুলোর জন্য assert.
  2. Compile-এই থামল। ছয় মাস পরে একজন সহকর্মী struct-এ একটি field যোগ করলেন। ফাইলে লেখা পুরোনো ডেটার সাথে নতুন মাপ মিলবে না — কিন্তু static_assert সঙ্গে সঙ্গে build ভেঙে দিল, স্পষ্ট বার্তাসহ। কোনো test লাগল না, কোনো ব্যবহারকারী ক্ষতিগ্রস্ত হলেন না।
  3. চলার সময় ধরা। assert তখনই কাজ করে, যখন প্রোগ্রাম সেই লাইনে পৌঁছায়। যে পথ test-এ কখনো চলেনি, সেখানকার ভুল ধরা পড়বে না। তবু debug-এর সময় এটি অমূল্য — ভুল কোথায়, ফাইল ও লাইন নম্বরসহ জানিয়ে দেয়।
  4. প্রকাশের build. -DNDEBUG দিলে NDEBUG সংজ্ঞায়িত হয়, আর সব assert মুছে যায় — গতি বাড়ে, কিন্তু সুরক্ষাও যায়। static_assert-এর কোনো খরচ কখনোই ছিল না, তাই সে থেকেই যায়। নিয়ম: যা compile-এর সময় যাচাই করা যায়, তা সেখানেই করুন।

৭ · “আধুনিক C” স্টাইল — কিন্তু portable

নতুন বৈশিষ্ট্য জানা এক কথা, সেগুলো দিয়ে ভালো কোড লেখা আরেক কথা। আজকের পেশাদার C প্রজেক্টগুলো — Linux kernel থেকে SQLite, curl, Redis — নিজেদের মতো নিয়ম মেনে চলে, তবে কিছু অভ্যাস প্রায় সবখানেই দেখা যায়:

📏 মাপ স্পষ্ট রাখুন

ফাইল, নেটওয়ার্ক বা হার্ডওয়্যারের ডেটায় uint32_t-এর মতো নির্দিষ্ট মাপের টাইপ; গোনা ও সূচকে size_t.

🏷️ নাম ধরে শুরুর মান

Designated initializer দিয়ে config ও টেবিল — struct-এ field যোগ হলেও পুরোনো কোড ভাঙে না।

🔒 const যেখানে সম্ভব

Const-correctness (পর্ব ৪৫): যে pointer দিয়ে শুধু পড়বেন, সেটি const.

🧩 ছোট static ফাংশন

ফাইলের ভেতরের সাহায্যকারী ফাংশন static — বাইরে দেখা যায় না, compiler সহজে inline করে।

সাথে আরও দুটি অভ্যাস: অনুমানগুলো static_assert দিয়ে লিখে রাখা, আর build-এ কড়া warning চালু রাখা — -std=c17 -Wall -Wextra -pedantic, debug build-এ sanitizer (পর্ব ৩১)। মনে রাখবেন, “আধুনিক” মানে নতুনতম সব বৈশিষ্ট্য গুঁজে দেওয়া নয়। মানে হলো এমন কোড, যা পড়তে সহজ, ভুল করা কঠিন, আর compiler যেটা যাচাই করতে পারে।

Portability — সবাই একই গতিতে এগোয় না

আপনার কোড কোথায় compile হবে, তার ওপর নির্ভর করে কোন স্ট্যান্ডার্ড বাছবেন। GCC ও Clang সাধারণত নতুন স্ট্যান্ডার্ড দ্রুত গ্রহণ করে। কিন্তু Microsoft-এর MSVC বহু বছর ধরে মূলত C++-এর দিকে মনোযোগ দিয়েছিল; C99-এর অনেক অংশও সেখানে দেরিতে এসেছে। ২০২০ সালের দিকে Visual Studio 2019-এর একটি আপডেটে /std:c11 ও /std:c17 যোগ হয়, তবে VLA-র মতো ঐচ্ছিক অংশ বাদ রেখে। Embedded জগতের অনেক বিক্রেতার compiler আজও C99 বা তারও পুরোনো স্ট্যান্ডার্ডে আটকে থাকতে পারে (পর্ব ৫১)।

তাই বাস্তব কৌশল হলো: একটি ন্যূনতম স্ট্যান্ডার্ড ঠিক করুন (অনেক library-র জন্য আজ সেটি C11 বা C17), আর নতুন কিছু ব্যবহার করতে চাইলে শর্তসাপেক্ষে করুন:

#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
    #define NODISCARD [[nodiscard]]
#elif defined(__GNUC__)
    #define NODISCARD __attribute__((warn_unused_result))
#else
    #define NODISCARD
#endif

NODISCARD int save_file(const char *path);

এভাবে C23-সমর্থিত compiler-এ নতুন attribute, পুরোনো GCC/Clang-এ তাদের নিজস্ব রূপ, আর অন্য সবখানে কিছুই না — কোড সবখানে compile হয়। বড় open-source library-গুলো ঠিক এই কৌশলে চলে।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই বিষয়ে যেসব প্রশ্ন আসতে পারে:

  • C99-এ আসা তিনটি বৈশিষ্ট্যের নাম বলুন। — // মন্তব্য, stdint.h/long long, designated initializer (এছাড়া VLA, inline, restrict).
  • gets() কেন ব্যবহার করা উচিত নয়? — বাফারের মাপ জানে না, তাই buffer overflow ঘটায়; C11 এটি স্ট্যান্ডার্ড থেকে বাদ দিয়েছে, বদলে fgets.
  • sizeof('A') C-তে কত? — sizeof(int), সাধারণত 4; কারণ C-তে অক্ষর লিটারালের টাইপ int (C++-এ 1).
  • Flexible array member কী, আর কেন? — struct-এর শেষে মাপ ছাড়া অ্যারে; header ও ডেটা একটি malloc-এ রাখা যায়, এক free-এ মুক্তি।
  • static_assert আর assert-এর পার্থক্য? — প্রথমটি compile-এর সময়, খরচ শূন্য; দ্বিতীয়টি চলার সময়, NDEBUG দিলে মুছে যায়।

৮ · AI-এর যুগে — স্ট্যান্ডার্ড গুলিয়ে ফেলা কোড

AI মডেল শিখেছে ইন্টারনেটের কোটি কোটি লাইন কোড থেকে — যেখানে ১৯৯০-এর টিউটোরিয়াল, আধুনিক GitHub প্রজেক্ট আর C++ কোড সব মিলেমিশে আছে। ফলে AI-এর লেখা একটি C ফাইলে একসাথে তিন যুগের জিনিস দেখা খুব সাধারণ ঘটনা। সমস্যা হলো, এমন কোড এক compiler-এ চলে, আরেকটিতে চলে না — আর আপনি বুঝতেই পারবেন না কেন।

AI যা লেখেসমস্যাঠিক উপায়
nullptr, constexpr — কিন্তু আপনার প্রজেক্ট C17পুরোনো compiler-এ 'nullptr' undeclaredC17-এ NULL, ধ্রুবকে enum বা #define
bool ব্যবহার, stdbool.h নেইC23-এর আগে compile error#include <stdbool.h> রাখুন — C23-এও ক্ষতি নেই
int *p = new int[10];, std::, classএগুলো C++, C নয়malloc আর free
int *a = (int*)malloc(...)C-তে cast অপ্রয়োজনীয় (C++-এর অভ্যাস); ভুল লুকাতে পারেint *a = malloc(n * sizeof *a);
gets(name);, void main(), conio.hবাদ পড়া বা অ-স্ট্যান্ডার্ড পুরোনো রীতিfgets, int main(void)
strcpy_s “নিরাপদ” বলেLinux-এর glibc-তে নেই (Annex K)snprintf বা মাপ যাচাই করে memcpy
K&R ধাঁচের সংজ্ঞা বা int f()C23-এ বাদ বা অর্থ বদলসবসময় পূর্ণ prototype: int f(void)

সমাধানের প্রথম ধাপ খুব সহজ: AI-কে স্ট্যান্ডার্ড, compiler আর flag বলে দিন। তাহলে সে নিজেই অনেক ভুল এড়িয়ে যাবে, আর আপনি যাচাইয়ের একটি মাপকাঠি পাবেন।

আমি C17 স্ট্যান্ডার্ডে কাজ করছি। Compiler: GCC 12, Linux. Build কমান্ড: gcc -std=c17 -Wall -Wextra -pedantic. নিচের কাজটির জন্য C কোড লেখো: [এখানে কাজের বর্ণনা] নিয়ম: ১. শুধু C17-এ বৈধ জিনিস ব্যবহার করবে — কোনো C++ (new, class, std::, reference) নয়, C23-এর nullptr/constexpr/[[attribute]]-ও নয়। ২. Annex K-এর _s ফাংশন (strcpy_s ইত্যাদি) এবং gets ব্যবহার করবে না। ৩. bool লাগলে stdbool.h include করবে; নির্দিষ্ট মাপের পূর্ণসংখ্যায় stdint.h. ৪. কোডের শেষে আলাদা করে লিখবে — কোন কোন লাইন C99 বা C11-এর বৈশিষ্ট্য ব্যবহার করছে।

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

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

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

প্র ০১Python বা JavaScript প্রতি বছর নতুন সংস্করণ পায়। C কেন এত ধীরে বদলায় — প্রায় দশ বছরে একবার?

ধীরগতিটা দুর্বলতা নয়, এটি জেনেবুঝে নেওয়া একটি নকশা-সিদ্ধান্ত। কয়েকটি কারণ একসাথে কাজ করে:

  • অকল্পনীয় পরিমাণ পুরোনো কোড: অপারেটিং সিস্টেম, ডেটাবেস, ব্যাংকের লেনদেন-ব্যবস্থা, বিমান ও চিকিৎসা-যন্ত্রের firmware — কয়েক দশকের কোটি কোটি লাইন C. একটি নতুন keyword যদি কারও ভ্যারিয়েবলের নাম কেড়ে নেয়, বা কোনো পুরোনো আচরণ বদলায়, তার মূল্য দিতে হয় সারা দুনিয়াকে। তাই কমিটির অলিখিত নীতি: “বিদ্যমান কোড ভাঙবে না” — যতক্ষণ না ভাঙার যুক্তি খুব জোরালো (যেমন gets).
  • কমিটির কাজের ধরন: WG14-এ প্রতিটি প্রস্তাব লিখিত আকারে আসে, একাধিক সভায় আলোচনা হয়, ভোট হয়। কমিটি সাধারণত চায় একটি বৈশিষ্ট্য অন্তত একটি বাস্তব compiler-এ আগে পরীক্ষিত হোক — “existing practice”. typeof GCC-তে কয়েক দশক extension হিসেবে ছিল, তারপরেই C23-এ স্ট্যান্ডার্ড হলো।
  • অনেক রকম compiler: C শুধু GCC আর Clang নয়। ছোট microcontroller-এর জন্য বহু বিক্রেতার নিজস্ব compiler আছে, যাদের দল ছোট। নতুন বৈশিষ্ট্য মানে তাদের সবার জন্য কাজ। জটিল বৈশিষ্ট্য চাপিয়ে দিলে অনেকে স্ট্যান্ডার্ডই মানবে না — যেমনটা C99-এর পর কিছু ক্ষেত্রে হয়েছিল, আর তাই C11-এ VLA ঐচ্ছিক করা হয়।
  • ভাষার পরিচয়: C-এর শক্তি তার সরলতা ও পূর্বানুমেয়তা — আপনি কোড দেখে মোটামুটি বলতে পারেন যন্ত্রে কী ঘটবে। কমিটি সচেতনভাবে বড় বৈশিষ্ট্য (class, exception, garbage collection) এড়িয়ে চলে, যাতে ভাষাটি “ছোট” থাকে।

উল্টো দিকে খরচও আছে। দীর্ঘ বিরতির কারণে C কিছু ক্ষেত্রে পিছিয়ে পড়েছে — নিরাপদ string, bounds-checking বা module-এর মতো জিনিস এখনো নেই। অনেক কাজ তাই compiler extension বা বাইরের টুলের ওপর নির্ভর করে।

মূল উপলব্ধি: যে ভাষার ওপর দুনিয়ার অবকাঠামো দাঁড়িয়ে, তার কাছে “স্থিরতা” নিজেই একটি বৈশিষ্ট্য। দ্রুত বদল আর নির্ভরযোগ্যতার মধ্যে C বরাবর নির্ভরযোগ্যতাকে বেছে নিয়েছে — এটি একটি trade-off, ভুল নয়।

প্র ০২অনেকে বলেন “C++ হলো C-এর superset — সব C কোড C++-এ চলে।” কথাটি কি ঠিক?

না। ১৯৮০-এর দশকে Bjarne Stroustrup C++ শুরু করেছিলেন “C with Classes” নামে, C-কে ভিত্তি করে। কিন্তু তারপর দুটি ভাষা আলাদা কমিটির হাতে আলাদা পথে এগিয়েছে, আর আজ বহু বৈধ C প্রোগ্রাম C++-এ compile হয় না, কিছু আবার compile হলেও ভিন্ন আচরণ করে। কয়েকটি উদাহরণ:

  • void * রূপান্তর: C-তে int *p = malloc(40); সম্পূর্ণ বৈধ। C++-এ void * থেকে অন্য pointer-এ স্বয়ংক্রিয় রূপান্তর নেই, তাই cast লাগে। AI যে (int*)malloc লেখে, সেটি আসলে C++-এর অভ্যাস।
  • অক্ষর লিটারাল: C-তে sizeof('A') == sizeof(int), C++-এ 1. একই কোড, ভিন্ন ফল।
  • নাম: int new = 5; বা int class; C-তে বৈধ, C++-এ এগুলো keyword.
  • C-তে আছে, C++-এ নেই: VLA, restrict, flexible array member, _Generic. Designated initializer C++20-এ এসেছে, কিন্তু সেখানে member-এর ক্রম মেনে লিখতে হয়; C-তে যেকোনো ক্রমে।
  • খালি বন্ধনী: C23-এর আগে int f(); মানে C-তে “প্যারামিটার অজানা”, C++-এ “কোনো প্যারামিটার নেই” — C23 এই পার্থক্য মিটিয়েছে।

মজার ব্যাপার হলো, দুই ভাষা একে অপরের কাছ থেকে ধার করে। // মন্তব্য, inline, bool, nullptr, constexpr, [[attributes]] — সব C++ থেকে C-তে এসেছে; আর designated initializer C থেকে C++-এ গেছে। দুই কমিটির মধ্যে যোগাযোগ আছে, যাতে অপ্রয়োজনীয় পার্থক্য না বাড়ে। তবু C-এর constexpr C++-এর তুলনায় অনেক সীমিত — শুধু object, ফাংশন নয়।

মূল উপলব্ধি: C আর C++ ঘনিষ্ঠ আত্মীয়, কিন্তু যমজ নয়। কোনো header দুই ভাষাতেই ব্যবহার করতে চাইলে দুটির সাধারণ অংশেই থাকুন, আর দুই compiler-এই যাচাই করুন।

প্র ০৩Annex K-এর “নিরাপদ” ফাংশন (strcpy_s ইত্যাদি) কেন জনপ্রিয় হলো না, যদিও buffer overflow C-এর সবচেয়ে বড় সমস্যাগুলোর একটি?

Annex K একটি ভালো উদ্দেশ্যের গল্প, যা বাস্তবের সাথে মেলেনি। এর মূল প্রস্তাব এসেছিল Microsoft থেকে; তাদের compiler আগে থেকেই strcpy ব্যবহার করলে “unsafe, use strcpy_s” ধাঁচের সতর্কবার্তা দিত। C11 এটিকে ঐচ্ছিক অংশ হিসেবে গ্রহণ করে। কিন্তু কয়েকটি সমস্যা দেখা দেয়:

  • কেউ পুরোপুরি বানায়নি: glibc, musl-এর মতো প্রধান open-source library এটি বানায়নি। আর Microsoft-এর নিজের সংস্করণ স্ট্যান্ডার্ড হওয়ার আগের নকশা অনুসরণ করে, ফলে কিছু ফাংশনের আচরণ স্ট্যান্ডার্ডের সাথে মেলে না। ফলাফল: একই কোড Windows-এ এক রকম, অন্যত্র compile-ই হয় না।
  • নকশার সমস্যা: সীমা ভাঙলে একটি “constraint handler” ডাকা হয়, যা পুরো প্রোগ্রামের জন্য একটিই (global). Multithreaded প্রোগ্রাম বা library-তে এটি সামলানো কঠিন — এক library handler বদলালে আরেকটির আচরণ বদলে যায়।
  • আসল সমস্যা মেটে না: আপনাকে এখনো ঠিক মাপটি দিতে হবে। ভুল মাপ দিলে strcpy_s-ও আপনাকে বাঁচাবে না। অর্থাৎ ভুলের ধরন একই থাকে, শুধু রূপ বদলায়।

২০১৫ সালে কমিটিতে জমা পড়া একটি প্রতিবেদন (“Field Experience With Annex K”) বাস্তব অভিজ্ঞতা বিশ্লেষণ করে বলে যে এটি প্রত্যাশিত ফল দেয়নি, এবং বাদ দেওয়ার বা বদলানোর প্রস্তাব করে। শেষ পর্যন্ত এটি ঐচ্ছিক হিসেবেই রয়ে গেছে।

তাহলে বাস্তবে নিরাপত্তা আসে কোথা থেকে? মাপ জানা ফাংশন (snprintf, fgets), compiler-এর সুরক্ষা (-D_FORTIFY_SOURCE, stack canary), sanitizer আর fuzzing, কোড রিভিউ — আর ক্রমশ বেশি করে, নতুন অংশ Rust-এর মতো memory-safe ভাষায় লেখা।

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

প্র ০৪Rust, Zig, Carbon — অনেক ভাষাকে “C-এর উত্তরসূরি” বলা হয়। তাহলে C টিকে আছে কেন, আর ভবিষ্যতে কী হতে পারে?

প্রথমে নামগুলোর পার্থক্য পরিষ্কার করি। Rust (Mozilla-য় শুরু, ২০১৫ সালে ১.০) compile-এর সময় ownership যাচাই করে memory-safety দেয়। Zig C-এর মতো সরল ও নিম্নস্তরের থাকতে চায়, তবে আরও নিরাপদ ডিফল্ট আর ভালো build টুল নিয়ে; এটি C কোড সরাসরি compile ও ব্যবহার করতে পারে। আর Google-এর Carbon আসলে C++-এর সম্ভাব্য উত্তরসূরি হিসেবে ঘোষিত একটি পরীক্ষামূলক প্রজেক্ট, C-এর নয়।

এদের প্রভাব বাস্তব। ২০২২ সালে Linux kernel-এ (সংস্করণ 6.1) Rust দিয়ে কোড লেখার ব্যবস্থা যোগ হয়। Android-এর নতুন অংশে Rust বাড়ছে। আমেরিকার সরকারি সংস্থাগুলোও memory-safe ভাষার দিকে যাওয়ার পরামর্শ দিয়েছে।

তবু C সহজে যাচ্ছে না, কারণ:

  • বিদ্যমান কোড: কয়েক দশকের পরীক্ষিত কোড নতুন করে লেখা ব্যয়বহুল ও ঝুঁকিপূর্ণ। নতুন কোড নতুন ভাষায়, পুরোনো কোড C-তে — এই মিশ্র অবস্থাই সম্ভবত বহু বছর চলবে।
  • সর্বজনীন ABI: প্রায় সব ভাষা অন্য ভাষার সাথে কথা বলে C-এর ফাংশন-ডাকার নিয়ম (ABI) দিয়ে। Python, Rust, Go — সবাই C library ডাকতে পারে। C হলো প্রোগ্রামিং জগতের “সাধারণ ভাষা”।
  • সবখানে compiler: প্রায় যেকোনো নতুন chip-এর জন্য প্রথম যে compiler আসে, সেটি C compiler.
  • সরলতা: C পুরোটা একজন মানুষ মাথায় রাখতে পারেন; ছোট দলের embedded কাজে এটি বড় সুবিধা।

আর C নিজেও থেমে নেই। C23-এর পরের স্ট্যান্ডার্ড — অস্থায়ী নাম C2y — নিয়ে কাজ চলছে। আলোচনার বড় অংশ নিরাপত্তা ঘিরে: undefined behavior-এর তালিকা ছোট করা, রিসোর্স পরিষ্কারের জন্য defer-জাতীয় ব্যবস্থা নিয়ে আলাদা একটি technical specification, আর ছোট ছোট সুবিধা। কোনটি শেষ পর্যন্ত স্ট্যান্ডার্ডে ঢুকবে, তা এখনো চূড়ান্ত নয়।

মূল উপলব্ধি: প্রশ্নটি “C নাকি Rust” নয়, বরং “কোন অংশে কোনটি”. যে প্রকৌশলী C-এর মেমরি-মডেল গভীরভাবে বোঝেন, তিনি Rust কেন এমন নিয়ম চাপায় সেটাও সবচেয়ে ভালো বোঝেন।

প্র ০৫একটি নতুন open-source C library শুরু করছেন। কোন স্ট্যান্ডার্ড বেছে নেবেন — C99, C11, C17, নাকি C23?

এটি একটি ক্লাসিক trade-off: নতুন বৈশিষ্ট্যের সুবিধা বনাম কতজন ব্যবহার করতে পারবেন। উত্তর নির্ভর করে আপনার ব্যবহারকারী কারা, তার ওপর।

  • C99 — সবচেয়ে বিস্তৃত নাগাল: প্রায় সব compiler, অনেক পুরোনো embedded toolchain-সহ, এটি মানে। SQLite-এর মতো কিছু প্রজেক্ট আরও পুরোনো C-এর সাথেও মিল রাখে, যাতে যেকোনো জায়গায় চলে। খরচ: _Static_assert, _Generic, atomic নেই।
  • C11/C17 — আজকের নিরাপদ মাঝামাঝি: GCC, Clang আর এখন MSVC — তিন প্রধান compiler-ই মানে। Static assert, alignment, atomic পাবেন। নতুন বেশিরভাগ library-র জন্য এটিই যুক্তিসংগত ডিফল্ট।
  • C23 — সবচেয়ে আধুনিক, নাগাল কম: nullptr, constexpr, attribute, stdbit.h — পড়তে সুন্দর, ভুল কম। কিন্তু পুরোনো Linux distribution-এর compiler, অনেক embedded compiler আর অনেক ব্যবহারকারীর সিস্টেম এখনো পুরোপুরি প্রস্তুত নয়। নিজের অ্যাপ বা দলের ভেতরের টুল হলে, যেখানে compiler আপনিই ঠিক করেন, এটি ভালো পছন্দ হতে পারে।

একটি বাস্তবসম্মত কৌশল: public header (যা ব্যবহারকারীর কোডে include হবে) রাখুন রক্ষণশীল — C99/C11-এর সাধারণ অংশে, যাতে C++ থেকেও ব্যবহার করা যায়। ভেতরের .c ফাইলে C11/C17. আর C23-এর সুবিধা নিন শর্তসাপেক্ষ macro দিয়ে, যেমন অংশ ৭-এর NODISCARD. CI-তে কয়েকটি compiler ও কয়েকটি -std= দিয়ে build চালান, যাতে কেউ ভুল করে নতুন কিছু ঢুকিয়ে দিলে সঙ্গে সঙ্গে ধরা পড়ে।

আর যে সিদ্ধান্তই নিন, README-তে স্পষ্ট লিখুন: “Requires C11; tested with GCC ≥ X, Clang ≥ Y, MSVC 2022.” ব্যবহারকারী আর AI — দুজনেরই কাজে লাগবে।

মূল উপলব্ধি: স্ট্যান্ডার্ড বাছাই একটি প্রকৌশল-সিদ্ধান্ত, রুচির প্রশ্ন নয়। “সবচেয়ে নতুন” নয়, “আমার ব্যবহারকারীদের জন্য সবচেয়ে উপযুক্ত” — এভাবে ভাবুন, আর সিদ্ধান্তটি লিখে রাখুন।

অনুশীলন

বেশিরভাগ কাজ পর্যবেক্ষণ আর ভাবনার — কোড চোখে পড়ে যুগ চেনাই আসল দক্ষতা।

  1. যুগ চিনুন: নিচের প্রতিটি লাইন অন্তত কোন স্ট্যান্ডার্ড থেকে বৈধ? (ক) for (int i = 0; i < n; i++) (খ) _Static_assert(sizeof(long) == 8, "x"); (গ) int *p = nullptr; (ঘ) uint8_t flags = 0; (ঙ) int f(a) int a; { return a; }

    (ক) C99 — for-এর ভেতরে ঘোষণা। (খ) C11 — _Static_assert. (গ) C23 — nullptr; আগের স্ট্যান্ডার্ডে NULL. (ঘ) C99 — stdint.h include করতে হবে। (ঙ) K&R — C89 থেকে C17 পর্যন্ত বৈধ কিন্তু “obsolescent” (পুরোনো, এড়িয়ে চলার মতো), আর C23-এ স্ট্যান্ডার্ড থেকে বাদ। GCC তবু warning দিয়ে মেনে নেয়।

  2. Compiler-এর ডিফল্ট খুঁজুন: godbolt.org-এ printf("%ld\n", (long)__STDC_VERSION__); দিয়ে একটি ছোট প্রোগ্রাম লিখুন। Compiler তালিকা থেকে একটি পুরোনো GCC (যেমন 9 বা 11) আর একটি নতুন GCC বেছে Execute চালান। তারপর option-এ -std=c99 দিয়ে আবার চালান। কী দেখলেন?

    পুরোনো GCC-তে সাধারণত 201710 (ডিফল্ট gnu17), খুব নতুন GCC-তে 202311 (gnu23) আসবে। -std=c99 দিলে দুটিতেই 199901. শিক্ষা: একই কোড compiler-এর সংস্করণভেদে ভিন্ন স্ট্যান্ডার্ডে compile হয় — তাই build কমান্ডে -std= স্পষ্ট লিখে দেওয়াই নিরাপদ। (Clang ও MSVC-তেও চেষ্টা করে দেখতে পারেন; MSVC-তে /std:c11 না দিলে __STDC_VERSION__ সংজ্ঞায়িত নাও থাকতে পারে।)

  3. _Generic বাড়ান: উপরের generic.c-তে print_any(2.5f) যোগ করলে compile error হয়। দুইভাবে ঠিক করুন — (ক) float-এর জন্য আলাদা শাখা, (খ) একটি default শাখা। কোনটি কখন ভালো?

    (ক) তালিকায় যোগ করুন float: print_double, — float মানটি double প্যারামিটারে গিয়ে স্বয়ংক্রিয়ভাবে বড় হবে, ছাপা হবে double : 2.50. (খ) default: print_double দিলে তালিকার বাইরের সব টাইপ এই শাখায় যাবে — কিন্তু সাবধান, তখন long বা একটি pointer-ও সেখানে যাবে, যা হয়তো ভুল ফল বা নতুন error দেবে। নিয়ম: যে টাইপগুলো আশা করেন সেগুলো স্পষ্ট লিখুন; default শুধু তখনই, যখন সত্যিই একটি নিরাপদ সাধারণ পথ আছে। Compile error অনেক সময় একটি উপহার — অপ্রত্যাশিত টাইপ চুপচাপ ভুল পথে যাওয়ার চেয়ে ভালো।

  4. AI অডিট: যেকোনো AI-কে বলুন “C-তে একটি linked list বানাও” — কোনো স্ট্যান্ডার্ড না বলে। তারপর উত্তরটি gcc -std=c99 -pedantic -Wall দিয়ে compile করুন এবং উপরের চেকলিস্ট মিলিয়ে দেখুন। এরপর অংশ ৮-এর prompt দিয়ে আবার চেষ্টা করুন। পার্থক্য কী?

    প্রথম উত্তরে প্রায়ই দেখা যায়: (struct node*)malloc(...) cast, কখনো bool কিন্তু stdbool.h নেই, কখনো nullptr বা C++-এর new, আর malloc-এর ফেরত মান যাচাই নেই। স্ট্যান্ডার্ড ও flag বলে দেওয়ার পর কোড সাধারণত পরিষ্কার হয় — NULL, সঠিক header, cast ছাড়া malloc. শিক্ষা: AI-কে প্রেক্ষাপট দিলে সে প্রেক্ষাপট মেনে চলে; না দিলে ইন্টারনেটের “গড়” কোড দেয়, যা কয়েক যুগের মিশ্রণ। তবু শেষ কথা compiler-এর — prompt যত ভালোই হোক, -pedantic দিয়ে নিজে যাচাই করুন।

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

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

ক্রমC-এর সংস্করণগুলো পুরোনো থেকে নতুন ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📘 K&R C — বইয়ের বর্ণনা, টাইপ ছাড়া প্যারামিটার
  2. 📜 C89 — function prototype ও void
  3. 🚀 C99 — //, stdint.h, designated initializer
  4. 🧵 C11 — _Generic, thread, gets বাদ
  5. ✨ C23 — nullptr, constexpr, bool keyword

একটি প্রোগ্রাম __STDC_VERSION__ ছাপলো 201112. Compiler কোন স্ট্যান্ডার্ডে চলছে?

  • C99
  • C11
  • C17
  • C23

struct msg { size_t len; char data[]; }; — malloc-এ সঠিক মাপ কোনটি, যদি n byte-এর একটি string ('\0' সহ নয়) রাখতে চান?

  • sizeof(struct msg)
  • n + 1
  • sizeof *m + n + 1
  • sizeof m + n

C-তে print_any('A') — যেখানে print_any একটি _Generic macro, যার int ও char দুটি শাখাই আছে। কোন শাখা বাছাই হবে?

  • char শাখা
  • দুটিই চলবে
  • Compile error
  • int শাখা

C23-এ নিচের কোনটি স্ট্যান্ডার্ড থেকে বাদ পড়েছে?

  • K&R ধাঁচের ফাংশন সংজ্ঞা
  • Designated initializer
  • stdbool.h header
  • NULL macro

আপনার প্রজেক্ট -std=c17 দিয়ে build হয়। AI-এর কোডে আছে int *p = nullptr;. কী করবেন?

  • যেহেতু নতুন GCC-তে চলে, রেখে দেবেন
  • NULL দিয়ে বদলে দেবেন, আর AI-কে লক্ষ্য-স্ট্যান্ডার্ড জানাবেন
  • পুরো প্রজেক্ট সঙ্গে সঙ্গে C23-এ নিয়ে যাবেন
  • সব জায়গায় 0 লিখবেন

static_assert সম্পর্কে কোনটি সত্য?

  • -DNDEBUG দিলে এটি মুছে যায়
  • ব্যবহারকারীর ইনপুট যাচাই করতে কাজে লাগে
  • শর্ত মিথ্যা হলে প্রোগ্রাম তৈরিই হয় না, চলার সময় খরচ শূন্য
  • শুধু C23-এ পাওয়া যায়

শব্দকোষ

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

মনে রাখুন

  1. C একটি জীবন্ত ভাষা: K&R → C89 → C99 → C11 → C17 → C23; প্রতিটি কোনো বাস্তব সমস্যা মিটিয়েছে।
  2. স্ট্যান্ডার্ড বাছুন -std= দিয়ে, যাচাই করুন __STDC_VERSION__ দিয়ে — compiler-এর ডিফল্টের ওপর ভরসা করবেন না।
  3. C99: flexible array member-এ header ও ডেটা এক malloc-এ; compound literal-এ নাম ছাড়া object.
  4. C11: _Generic compile-এর সময় টাইপ দেখে বাছাই করে; _Static_assert অনুমান যাচাই করে বিনা খরচে; gets বাদ।
  5. C23 আধুনিক ও পরিচ্ছন্ন, কিন্তু সবখানে নেই — লক্ষ্য-compiler জানুন, আর AI-কে স্ট্যান্ডার্ড বলে দিন।

পরের পর্বে: ভাষার নিয়ম থেকে এবার অপারেটিং সিস্টেমের দিকে। একটি প্রোগ্রাম কীভাবে নিজের একটি কপি বানায় (fork), কীভাবে অন্য প্রোগ্রামে রূপ নেয় (exec), আর দুটি প্রোগ্রাম pipe দিয়ে কীভাবে কথা বলে।

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

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