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

অপারেটর — C যেভাবে হিসাব করে, তুলনা করে আর যুক্তি সাজায়

Operators — Arithmetic, Comparison, Logic, Bits & Precedence

Taught in Bangla, this lesson covers C operators: arithmetic and the modulus operator, assignment and increment, comparison and short-circuit logic, bitwise and shift operators, precedence and associativity, and classic operator mistakes.

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

  • C-র অপারেটরগুলোর পাঁচটি পরিবার — কোনটি কী কাজে লাগে
  • ভাগশেষ, পূর্ণসংখ্যার ভাগ আর ++/---এর সূক্ষ্ম নিয়ম
  • Short-circuit দিয়ে কীভাবে crash ঠেকানো যায়
  • বিট মাস্ক দিয়ে অনুমতি চালু, বন্ধ ও পরীক্ষা করা — বাস্তব উদাহরণে
  • Precedence-এর ক্লাসিক ফাঁদ চেনা, আর AI-এর লেখা এক্সপ্রেশন যাচাই করা

১ · অপারেটর কী — এক্সপ্রেশনের ব্যাকরণ

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

একটি ছোট লাইন দেখুন: total = price * qty + 20. এখানে *, + আর = হলো অপারেটর। যাদের ওপর এরা কাজ করছে — price, qty, 20 — তারা operand. আর অপারেটর ও operand মিলে যে পুরো অংশটি হিসাব শেষে একটি মান দেয়, তার নাম expression.

বাংলা বাক্যের কথা ভাবুন। “রহিম চা খায়” — এখানে রহিম আর চা হলো বিশেষ্য, “খায়” ক্রিয়া। C-তে ভ্যারিয়েবল আর সংখ্যাগুলো বিশেষ্যের মতো, অপারেটর ক্রিয়ার মতো। ক্রিয়া ছাড়া যেমন বাক্য হয় না, অপারেটর ছাড়াও প্রোগ্রাম কিছুই করে না।

কিছু অপারেটর একটিমাত্র operand নেয় — যেমন -x বা !ok. এদের বলে unary. বেশিরভাগ নেয় দুটি — যেমন a + b; এরা binary. পুরো C ভাষায় অপারেটর আছে চল্লিশের কাছাকাছি, কিন্তু এগুলোকে পাঁচটি পরিবারে ভাগ করলে মনে রাখা সহজ:

হিসাব (Arithmetic)

যোগ, বিয়োগ, গুণ, ভাগ আর ভাগশেষ। দোকানের ক্যালকুলেটরের কাজ।

+ - * / %

মান বসানো (Assignment)

হিসাবের ফল বাক্সে রাখা, আর ১ করে বাড়ানো-কমানো।

= += -= *= ++ --

তুলনা ও যুক্তি

বড়-ছোট-সমান যাচাই, আর “এবং / অথবা / না” দিয়ে শর্ত জোড়া।

== != < > && || !

বিটের অপারেটর

সংখ্যার ভেতরের প্রতিটি 0/1 ধরে ধরে কাজ — হার্ডওয়্যারের সবচেয়ে কাছের ভাষা।

& | ^ ~ << >>
মূল ধারণা

C-তে প্রতিটি expression-এর একটি মান থাকে — এমনকি তুলনা আর মান বসানোরও। 5 > 3-এর মান ১, 5 < 3-এর মান ০, আর x = 7-এর মান ৭। এই একটি নিয়ম বুঝলে এই পর্বের প্রায় সব “অদ্ভুত” আচরণ পরিষ্কার হয়ে যাবে।

২ · হিসাবের অপারেটর — আর ভাগশেষের জাদু

যোগ (+), বিয়োগ (-) আর গুণ (*) স্কুলের অঙ্কের মতোই চলে। নতুনত্ব দুটি জায়গায় — ভাগ আর ভাগশেষ।

পূর্ণসংখ্যার ভাগ: দশমিক হারিয়ে যায়

পর্ব ০৬-এ দেখেছেন, দুটি int ভাগ করলে ফলও int হয়। একে বলে integer division. 7 / 2-এর ফল ৩, সাড়ে তিন নয় — দশমিক অংশটি স্রেফ কেটে ফেলা হয়, গোল করা হয় না। তাই 9 / 10-এর ফল ০। দশমিকসহ উত্তর চাইলে অন্তত একটি operand দশমিক সংখ্যা হতে হবে, যেমন 7.0 / 2, যার ফল ৩.৫।

শূন্য দিয়ে ভাগ C-তে নিষিদ্ধ। দুটি পূর্ণসংখ্যার ভাগে ভাজক ০ হলে আচরণ undefined behavior — বেশিরভাগ কম্পিউটারে প্রোগ্রামটি সঙ্গে সঙ্গে crash করে। C নিজে থেকে কোনো সুন্দর error দেখায় না; পাহারা বসানোর দায়িত্ব আপনার। চতুর্থ অংশে দেখবেন কীভাবে এক লাইনে সেই পাহারা বসানো যায়।

ভাগশেষ %: ছোট চিহ্ন, বড় কাজ

%-কে বলে modulus অপারেটর — এটি ভাগের পর যা বাকি থাকে তা দেয়। ধরুন ১৭টি বিস্কুট ৫ জন শিশুর মধ্যে সমান ভাগ করবেন। প্রত্যেকে পাবে 17 / 5 = ৩টি, আর বাকি থাকবে 17 % 5 = ২টি। শুধু এটুকু দিয়েই অনেক বাস্তব সমস্যার সমাধান হয়:

প্রশ্নএক্সপ্রেশনকেন কাজ করে
সংখ্যাটি জোড় না বিজোড়?n % 2 == 0জোড় সংখ্যাকে ২ দিয়ে ভাগ করলে কিছুই বাকি থাকে না
শেষ অঙ্কটি কত?2024 % 10 → 4১০ দিয়ে ভাগের ভাগশেষই একক ঘরের অঙ্ক
এখন ১০টা, ৫ ঘণ্টা পর কয়টা?(10 + 5) % 12 → 3১২-তে পৌঁছালেই ঘড়ি আবার শূন্য থেকে ঘোরে
৩ জন ক্যাশিয়ারে পালা করে খদ্দের পাঠানোcustomer % 3০, ১, ২, ০, ১, ২ … ঘুরে ঘুরে আসে (round-robin)
১৩৫ মিনিট = কত ঘণ্টা কত মিনিট?135 / 60, 135 % 60ভাগফল ঘণ্টা (২), ভাগশেষ মিনিট (১৫)

দেয়ালঘড়ির কাঁটার কথা ভাবুন। ১১টার পর ১২টা, তারপর আবার ১টা — সংখ্যা কখনো ১৩-তে যায় না, বৃত্তে ঘুরে আসে। % ঠিক এই “ঘুরে আসা” গণিত। ক্রিকেটে ওভার শেষে বোলার বদলানো, রোস্টারে পালা করে নাইট ডিউটি, সপ্তাহের সাত দিন — যেখানেই চক্র, সেখানেই %.

একটি সূক্ষ্ম বিষয়: ঋণাত্মক সংখ্যায় C99 থেকে নিয়ম হলো — ভাগফল সবসময় শূন্যের দিকে কাটা হয়। তাই -7 / 2 = -3, আর ভাগশেষের চিহ্ন হয় ভাজ্যের (বাঁ পাশের সংখ্যার) মতো: -7 % 2 = -1. অর্থাৎ n % 2 == 1 দিয়ে বিজোড় যাচাই করলে ঋণাত্মক বিজোড় সংখ্যায় ভুল উত্তর আসবে! নিরাপদ লেখা হলো n % 2 != 0. আর একটি কথা — % শুধু পূর্ণসংখ্যায় চলে; দশমিক সংখ্যায় ভাগশেষ লাগলে fmod() ফাংশন আছে।

৩ · মান বসানো, বাড়ানো, কমানো — =, +=, ++

গণিতে x = x + 1 লিখলে শিক্ষক লাল কালি দেবেন — কোনো সংখ্যা নিজের চেয়ে ১ বেশি হতে পারে না। কিন্তু C-তে = মানে “সমান” নয়। এর মানে “ডান পাশ হিসাব করো, তারপর ফলটি বাঁ পাশের বাক্সে রাখো।” একে বলে assignment. তাই x = x + 1 মানে — x-এর পুরোনো মানে ১ যোগ করে নতুন মানটি আবার x-এ রাখো।

যেহেতু assignment-ও একটি expression, তার নিজেরও মান আছে — যে মানটি বসানো হলো সেটাই। এ কারণে a = b = 0; লেখা যায়: আগে b = 0 হয়, তার মান ০, সেটি যায় a-তে। আবার এ কারণেই পর্ব ০৩-এর সেই বাগটি ঘটে — if (marks = 100) তুলনা করে না, 100 বসিয়ে দেয়, আর 100 শূন্য নয় বলে শর্ত সবসময় সত্য।

সংক্ষিপ্ত রূপ: +=, -=, *= …

“নিজের সাথে কিছু করে আবার নিজেতেই রাখা” এত বেশি লাগে যে C-তে এর জন্য আলাদা লেখা আছে — compound assignment. balance -= 500 মানে balance = balance - 500, যেমন bKash থেকে ৫০০ টাকা ক্যাশ-আউট। একইভাবে +=, *=, /=, %=, এমনকি বিটের জন্য |=, &=.

একটি ফাঁদ: x *= 2 + 3 মানে x = x * (2 + 3), অর্থাৎ ৫ দিয়ে গুণ — x * 2 + 3 নয়। কারণ *=-এর ডান পাশটি পুরোটা আগে হিসাব হয়।

++ আর --: আগে না পরে?

১ বাড়ানো (++) আর ১ কমানো (--) এত সাধারণ কাজ যে এদের জন্য আলাদা অপারেটর আছে — increment ও decrement. গোলমাল একটাই: চিহ্নটি ভ্যারিয়েবলের আগে বসবে, না পরে?

Postfix: a++

“আগে পুরোনো মানটি দাও, তারপর বাড়াও।” a = 5; b = a++; → b পায় ৫, a হয় ৬।

ক্লাসের রোল কলে আগে নাম ডাকা, পরে খাতায় টিক

Prefix: ++a

“আগে বাড়াও, তারপর নতুন মানটি দাও।” a = 5; b = ++a; → a হয় ৬, b-ও পায় ৬।

আগে টিক, পরে নাম ডাকা

একা একটি লাইনে i++; আর ++i; — দুটোর ফল একদম এক। পার্থক্য শুধু তখনই, যখন এরা একটি বড় expression-এর ভেতরে বসে। এখানেই লুকিয়ে আছে C-র সবচেয়ে কুখ্যাত ফাঁদগুলোর একটি।

i = i++ কেন undefined

i++ দুটি কাজ করে — একটি মান দেয়, আবার i-কেও বদলায়। এই দ্বিতীয় কাজটিকে বলে side effect. এখন i = i++; লাইনে একই i-কে দুজন বদলাতে চাইছে: ++ বলছে “i বাড়াও”, = বলছে “i-তে পুরোনো মানটি রাখো।” কে আগে, কে পরে?

C-র উত্তর হলো — কোনো নিয়ম নেই। C কোডে কিছু নির্দিষ্ট বিন্দু ঠিক করে দেয়, যেখানে পৌঁছানোর আগে আগের সব পরিবর্তন শেষ হয়ে যাওয়ার নিশ্চয়তা থাকে — যেমন প্রতিটি পূর্ণ statement-এর শেষের ;, &&, || আর কমা অপারেটর। এদের বলে sequence point (C11 থেকে একই ধারণাকে “sequenced before” নামে লেখা হয়)। দুটি sequence point-এর মাঝে একই ভ্যারিয়েবল দুবার বদলালে, বা একবার বদলে অন্য জায়গায় আলাদাভাবে পড়লে, ফল undefined behavior. i = i++, i++ + i++, a[i] = i++ — সব এই দলে।

Undefined মানে “কোনো একটা সংখ্যা আসবে” নয়। মানে হলো, C-র নিয়ম এখানে compiler-কে কিছুই বাধ্য করে না — এক compiler ১০ দেবে, আরেকটি ১১, optimization চালু করলে হয়তো অন্য কিছু। পরীক্ষায় “i++ + ++i-এর মান কত?” ধরনের প্রশ্ন দেখলে জানবেন, সঠিক উত্তর হলো “নির্ধারিত নয়।” gcc -Wall সাধারণত এখানে সতর্ক করে: operation on 'i' may be undefined.

নিচের কোডটি চালিয়ে দেখুন — ভাগ, ভাগশেষ, ঘড়ির হিসাব আর ++-এর আগে-পরে, সব এক জায়গায়:

#include <stdio.h>

int main(void) {
    int biscuits = 17, kids = 5;
    printf("each gets %d, left over %d\n", biscuits / kids, biscuits % kids);
    printf("-7 / 2 = %d, -7 %% 2 = %d\n", -7 / 2, -7 % 2);
    printf("10 o'clock + 5 hours = %d o'clock\n", (10 + 5) % 12);
    printf("last digit of 2024 = %d\n", 2024 % 10);

    int a = 5;
    int b = a++;   /* b gets the OLD value, then a becomes 6 */
    int c = ++a;   /* a becomes 7 first, then c gets 7       */
    printf("a = %d, b = %d, c = %d\n", a, b, c);
    return 0;
}

প্রথম লাইনে আসবে each gets 3, left over 2. দ্বিতীয় লাইনে -3 আর -1 — ভাগফল শূন্যের দিকে কাটা হয়েছে, ভাগশেষ পেয়েছে ভাজ্যের চিহ্ন। printf-এর ভেতরে %% লিখতে হয়েছে, কারণ একা % সেখানে format-এর শুরু বোঝায়। শেষ লাইনে a = 7, b = 5, c = 7 — b পুরোনো ৫ পেয়েছে, c পেয়েছে বাড়ানোর পরের ৭। লক্ষ করুন, প্রতিটি ++ আলাদা statement-এ আছে, তাই কোনো undefined behavior নেই।

৪ · তুলনা আর যুক্তি — ==, &&, ||, !

প্রোগ্রাম সিদ্ধান্ত নেয় প্রশ্ন করে: নম্বর ৮০-র বেশি? ব্যালান্স যথেষ্ট? পাসওয়ার্ড মিলেছে? এই প্রশ্নগুলো করে relational operator:

অপারেটরঅর্থউদাহরণমান
==সমান কি না5 == 5১
!=অসমান কি না5 != 5০
< >ছোট / বড়3 < 8১
<= >=ছোট বা সমান / বড় বা সমান80 >= 80১

C-তে আলাদা কোনো “সত্য” বা “মিথ্যা” জিনিস নেই (পরে bool এসেছে, কিন্তু ভেতরে সেটিও সংখ্যা)। তুলনার ফল একটি int — সত্য হলে ১, মিথ্যা হলে ০। আর শর্ত পরীক্ষার সময় নিয়ম আরও উদার: ০ মানে মিথ্যা, শূন্য ছাড়া যেকোনো মান সত্য।

গণিতের মতো শিকল চলে না। if (0 < marks < 100) দেখতে ঠিক, কিন্তু C এটিকে পড়ে (0 < marks) < 100 হিসেবে। প্রথম অংশের মান ০ বা ১ — দুটোই ১০০-র চেয়ে ছোট, তাই marks ৫০০ হলেও শর্ত সত্য! সঠিক লেখা: 0 < marks && marks < 100.

শর্ত জোড়া: এবং, অথবা, না

একাধিক প্রশ্ন জুড়তে লাগে logical operator. মেট্রো রেলের গেট খোলে যখন কার্ড বৈধ এবং ব্যালান্স যথেষ্ট — এটি &&. স্কলারশিপ পাওয়া যায় GPA ৫ অথবা বোর্ডে মেধাতালিকায় থাকলে — এটি ||. আর ! উল্টে দেয়: !0 = ১, !7 = ০। নিচের truth table সব সম্ভাবনা দেখায়:

ABA && BA || B!A
০০০০১
০১০১১
১০০১০
১১১১০

Short-circuit: অলসতাই নিরাপত্তা

টেবিলটি খেয়াল করুন। &&-এ A যদি ০ হয়, B যা-ই হোক ফল ০। আর ||-এ A যদি ১ হয়, ফল ১। C এই সুযোগটি নেয়: বাঁ পাশ দেখেই উত্তর নিশ্চিত হলে ডান পাশ হিসাবই করে না। একে বলে short-circuit evaluation. এটি শুধু সময় বাঁচানোর কৌশল নয় — C-র নিয়মে এটি নিশ্চিত, তাই এর ওপর ভরসা করে পাহারা বসানো যায়।

ধরুন একটি কোচিং সেন্টারের সফটওয়্যার জানতে চায় — গড় নম্বর ৫০-এর বেশি কি না। গড় মানে মোট নম্বর ভাগ ছাত্রসংখ্যা। কিন্তু নতুন ব্যাচে ছাত্রসংখ্যা ০ হলে? নিচে দুটি ক্ষেত্র ধাপে ধাপে দেখুন:

1/* case A: n = 0, total = 0   */2/* case B: n = 4, total = 240 */3if (n != 0 && total / n > 50)4    printf("high average\n");5else6    printf("no data or low\n");
ভ্যারিয়েবল ও হিসাব
n
total
বাঁ পাশ: n != 0
ডান পাশ: total / n > 50
পুরো শর্ত
  1. ক্ষেত্র A — নতুন ব্যাচ। এখনো কোনো ছাত্র ভর্তি হয়নি: n = ০, total = ০. বোকার মতো সরাসরি total / n করলে শূন্য দিয়ে ভাগ — প্রোগ্রাম crash.
  2. বাঁ পাশ আগে। && সবসময় বাঁ থেকে শুরু করে। n != 0 মানে “০ কি ০-র অসমান?” — না। ফল ০, অর্থাৎ মিথ্যা।
  3. ডান পাশ বাদ! “এবং”-এর একটি অংশ মিথ্যা হলেই পুরোটা মিথ্যা — ডান পাশ দেখার দরকারই নেই। তাই total / n একবারও হিসাব হলো না। শূন্য দিয়ে ভাগের বিপদ এভাবেই এড়ানো গেল। বাঁ পাশটি এখানে দারোয়ানের কাজ করছে।
  4. else অংশ চলল। পুরো শর্ত ০, তাই স্ক্রিনে এলো no data or low. প্রোগ্রাম নিরাপদে এগিয়ে গেল।
  5. ক্ষেত্র B — চালু ব্যাচ। এবার ৪ জন ছাত্র, মোট নম্বর ২৪০।
  6. বাঁ পাশ: 4 != 0 — সত্য, মান ১। কিন্তু “এবং”-এ একটি সত্য যথেষ্ট নয়, তাই এবার ডান পাশেও যেতে হবে।
  7. ডান পাশ: এখন ভাগ নিরাপদ, কারণ বাঁ পাশ নিশ্চিত করেছে n শূন্য নয়। 240 / 4 = ৬০, আর ৬০ > ৫০ — সত্য।
  8. দুই পাশই সত্য। পুরো শর্ত ১, ছাপা হলো high average. শিক্ষা: যে শর্ত বিপদ ঠেকায়, সেটি সবসময় &&-এর বাঁ পাশে লিখুন। উল্টো করে total / n > 50 && n != 0 লিখলে ভাগটি আগে হতো — আর ক্ষেত্র A-তে crash.

এই কৌশল সব জায়গায় চলে। ভবিষ্যতে pointer শিখলে দেখবেন, p != NULL && p->age > 18 — আগে নিশ্চিত হওয়া যে ঠিকানাটি বৈধ, তারপর সেখানে হাত দেওয়া। ||-এর ক্ষেত্রেও একই চিন্তা উল্টোভাবে: is_admin || has_permission(file) — অ্যাডমিন হলে দ্বিতীয় যাচাইয়ের দরকারই পড়ে না।

একটি কাজের সূত্র (De Morgan-এর নিয়ম): !(a && b) আর !a || !b একই কথা; !(a || b) আর !a && !b একই কথা। “পাস করেনি” মানে “থিওরিতে পাস এবং প্র্যাকটিক্যালে পাস — এমন নয়”, অর্থাৎ “থিওরিতে ফেল অথবা প্র্যাকটিক্যালে ফেল।”

৫ · বিটের ভাষায় কথা — & | ^ ~

পর্ব ০৫-এ দেখেছেন, কম্পিউটারে প্রতিটি সংখ্যা আসলে কয়েকটি bit-এর সারি। এতক্ষণের অপারেটরগুলো পুরো সংখ্যাকে একটি মান হিসেবে দেখে। Bitwise operator দেখে ভেতরের প্রতিটি বিটকে আলাদা করে — একই ঘরের বিট জোড়ায় জোড়ায় মিলিয়ে।

অপারেটরপ্রতিটি বিটে নিয়মউদাহরণ (৪ বিট)
& (AND)দুটোই 1 হলে 11100 & 1010 = 1000
| (OR)যেকোনো একটি 1 হলে 11100 | 1010 = 1110
^ (XOR)দুটো আলাদা হলে 11100 ^ 1010 = 0110
~ (NOT)প্রতিটি বিট উল্টে দাও~1100 = 0011

একটি বাড়ির সুইচবোর্ড ভাবুন — পাশাপাশি আটটি সুইচ, প্রতিটি একটি করে বাতি বা ফ্যানের। পুরো বোর্ডের অবস্থা একটি ৮ বিটের সংখ্যা দিয়ে লেখা যায়। | দিয়ে নির্দিষ্ট সুইচ অন করা যায়, & দিয়ে দেখা যায় কোনটি অন, ~ আর & মিলিয়ে নির্দিষ্ট সুইচ অফ, আর ^ দিয়ে উল্টে দেওয়া — অন থাকলে অফ, অফ থাকলে অন।

সবচেয়ে ছোট উদাহরণ: বিজোড় কি না, এক বিটে

বাইনারিতে প্রতিটি বিজোড় সংখ্যার একেবারে ডানের বিটটি 1, জোড় সংখ্যার 0. তাই n & 1 শুধু শেষ বিটটি রেখে বাকি সব মুছে দেয় — ফল ১ হলে বিজোড়, ০ হলে জোড়। ১৩ = 1101, তাই 13 & 1 = ১। মজার ব্যাপার, ঋণাত্মক সংখ্যাতেও (two's complement-এ) এটি ঠিক কাজ করে, যেখানে n % 2 == 1 ভুল করে।

বিট ফ্ল্যাগ: একটি সংখ্যায় অনেকগুলো হ্যাঁ/না

বিটের সবচেয়ে বড় ব্যবহার bit flag. Linux-এ প্রতিটি ফাইলের তিন রকম অনুমতি থাকে — পড়া (read), লেখা (write), চালানো (execute). প্রতিটি একটি বিট: r = 100 = ৪, w = 010 = ২, x = 001 = ১। এই একেকটি বিট-বাছাই করা সংখ্যাকে বলে bit mask. তিনটি জুড়ে দিলেই একটি অঙ্ক — আর তিন দলের (মালিক, গ্রুপ, বাকি সবাই) জন্য তিনটি অঙ্ক। এটাই বিখ্যাত chmod 755-এর রহস্য। ধাপে ধাপে দেখুন:

দুটি দলের অনুমতি (শেষ ৩টি বিট)
owner (মালিক)
group (গ্রুপ)
group-এর ls -l চেহারা
তিনটি মাস্কপ্রতিটিতে ঠিক একটি বিট 1
           r w x
READ  = 4  1 0 0
WRITE = 2  0 1 0
EXEC  = 1  0 0 1
owner = READ | WRITE | EXEC| দিয়ে বিট জোড়া
  READ    1 0 0
| WRITE   0 1 0
| EXEC    0 0 1
  -------------
  owner   1 1 1   = 7 → rwx
group = READ | EXECলেখার অনুমতি নেই
  READ    1 0 0
| EXEC    0 0 1
  -------------
  group   1 0 1   = 5 → r-x

  chmod 755 = owner 7, group 5, others 5
group & WRITE& দিয়ে একটি বিট পরীক্ষা
  group   1 0 1
& WRITE   0 1 0
  -------------
          0 0 0   = 0 → লেখার অনুমতি নেই
group |= WRITEএকটি বিট চালু
  group   1 0 1
| WRITE   0 1 0
  -------------
  group   1 1 1   = 7 → rwx
group &= ~EXECএকটি বিট বন্ধ
  EXEC    0 0 1
  ~EXEC   1 1 0   ← সব বিট উল্টানো

  group   1 1 1
& ~EXEC   1 1 0
  -------------
  group   1 1 0   = 6 → rw-
  1. তিনটি মাস্ক। প্রতিটি অনুমতির জন্য একটি করে সংখ্যা, যার ঠিক একটি বিট 1 — READ = ৪, WRITE = ২, EXEC = ১। এগুলো ২-এর ঘাত বলেই একটির বিট আরেকটির সাথে কখনো মেশে না।
  2. | দিয়ে জোড়া। মালিককে তিনটি অনুমতিই দিতে READ | WRITE | EXEC. প্রতিটি ঘরে যেকোনো একটি 1 থাকলেই ফল 1, তাই পাই 111 = ৭। এখানে | আর + একই ফল দিচ্ছে, কিন্তু একই মাস্ক দুবার জুড়লে | নিরাপদ থাকে, + ভুল সংখ্যা বানায়।
  3. গ্রুপের জন্য পড়া আর চালানো। READ | EXEC = 101 = ৫। বাকি সবার জন্যও ৫ দিলে পুরো সেটিং হয় ৭-৫-৫ — অর্থাৎ chmod 755, আর ls -l-এ দেখায় rwxr-xr-x. ওয়েবসাইটের ফোল্ডারে cPanel-এ যে ৭৫৫ দেখেন, সেটি এই তিনটি সংখ্যা।
  4. & দিয়ে পরীক্ষা। গ্রুপ কি লিখতে পারে? group & WRITE — মাস্কের 1-ওয়ালা ঘরটি ছাড়া সব ঘর মুছে যায়। ফল 000 = ০, অর্থাৎ না। ফল শূন্য ছাড়া কিছু হলে উত্তর হতো হ্যাঁ।
  5. |= দিয়ে চালু। গ্রুপকে লেখার অনুমতি দিতে group |= WRITE. শুধু মাঝের বিটটি 1 হলো; বাকি দুটি বিট যেমন ছিল তেমনই রইল — এটাই মাস্কের সৌন্দর্য। বিটটি আগে থেকেই চালু থাকলেও কোনো ক্ষতি নেই।
  6. &= ~ দিয়ে বন্ধ। চালানোর অনুমতি সরাতে আগে ~EXEC — সব বিট উল্টে 110 (আসলে বাঁয়ের বাকি সব বিটও 1 হয়, যা এখানে কিছু বদলায় না)। তারপর & করলে শুধু শেষ বিটটি 0 হয়। ফল 110 = ৬, অর্থাৎ rw-. তিনটি কৌশল মনে রাখুন: পরীক্ষা &, চালু |, বন্ধ & ~.

একই কৌশল হার্ডওয়্যারে: LED জ্বালানো

একটি microcontroller-এর ভেতরে কিছু বিশেষ মেমরি-ঘর থাকে, যাদের বলে register. কিছু register-এর প্রতিটি বিট সরাসরি চিপের একটি পিনের সাথে যুক্ত — বিট 1 করলে পিনে বিদ্যুৎ যায়, 0 করলে বন্ধ। যেমন Arduino Uno বোর্ডের ছোট LED-টি PORTB নামের register-এর ৫ নম্বর বিটে যুক্ত। তাই:

কাজকোডবাকি ৭টি পিনের কী হলো
LED জ্বালাওPORTB |= (1 << 5);কিছুই বদলায়নি
LED নেভাওPORTB &= ~(1 << 5);কিছুই বদলায়নি
অবস্থা উল্টাও (blink)PORTB ^= (1 << 5);কিছুই বদলায়নি
বোতাম চাপা কি না দেখোif (PINB & (1 << 2))শুধু পড়া হলো

ঠিক এই কারণে embedded system-এর প্রোগ্রামাররা প্রতিদিন বিটওয়াইজ অপারেটর লেখেন। PORTB = 32 লিখলেও LED জ্বলত, কিন্তু একই সাথে বাকি সাতটি পিন বন্ধ হয়ে যেত — হয়তো পাশের মোটরটিও থেমে যেত! মাস্ক দিয়ে কাজ করলে শুধু নিজের বিটটিতে হাত পড়ে।

এবার নিজে চালিয়ে দেখুন — ফাইলের অনুমতির পুরো হিসাবটি কোডে:

#include <stdio.h>

#define READ  4   /* 100 */
#define WRITE 2   /* 010 */
#define EXEC  1   /* 001 */

int main(void) {
    int owner = READ | WRITE | EXEC;     /* 111 = 7 */
    int group = READ | EXEC;             /* 101 = 5 */
    printf("owner = %d, group = %d\n", owner, group);
    if (group & WRITE) printf("group can write\n");
    else               printf("group cannot write\n");
    group |= WRITE;                      /* turn w on  */
    group &= ~EXEC;                      /* turn x off */
    printf("group now = %d\n", group);   /* 110 = 6 */
    printf("13 & 1 = %d, 5 << 3 = %d, -7 >> 1 = %d\n", 13 & 1, 5 << 3, -7 >> 1);
    return 0;
}

#define দিয়ে প্রতিটি মাস্কের একটি নাম দেওয়া হয়েছে (preprocessor নামগুলোকে সংখ্যায় বদলে দেয়, পর্ব ০৩ মনে করুন)। আউটপুটে আসবে owner = 7, group = 5, তারপর group cannot write, তারপর group now = 6 — ঠিক উপরের ডায়াগ্রামের মতো। শেষ লাইনে 13 & 1 = ১ (বিজোড়), 5 << 3 = ৪০ (৫ × ৮), আর -7 >> 1 = -4 — -7 / 2-এর -3 নয়! কেন, তা পরের অংশে।

৬ · শিফট — ২ দিয়ে গুণ-ভাগের শর্টকাট, আর তার সীমা

Bit shift সব বিটকে একসাথে বাঁয়ে বা ডানে সরিয়ে দেয়। দশমিকে একটি সংখ্যার ডানে একটি শূন্য বসালে মান ১০ গুণ হয় (৫ → ৫০)। বাইনারিতে একই কাজ করলে মান ২ গুণ হয়:

  5        = 0000 0101
  5 << 1   = 0000 1010   = 10   (x 2)
  5 << 3   = 0010 1000   = 40   (x 8)
  40 >> 2  = 0000 1010   = 10   (/ 4)
  1 << 5   = 0010 0000   = 32   (only bit 5 is on)

অর্থাৎ x << k মানে x × ২k, আর ধনাত্মক x-এর জন্য x >> k মানে x ÷ ২k (দশমিক বাদ দিয়ে)। আর 1 << k হলো “শুধু k নম্বর বিট চালু” এমন মাস্ক বানানোর সবচেয়ে পরিষ্কার উপায় — LED-এর উদাহরণে যা দেখলেন।

যেখানে শর্টকাট ভেঙে পড়ে

পরিস্থিতিC-র নিয়মবাস্তবে যা হয়
ঋণাত্মক সংখ্যা ডানে সরানো (-7 >> 1)Implementation-defined — compiler ঠিক করেGCC/Clang চিহ্ন-বিট কপি করে, ফল -4 (নিচের দিকে গোল); অথচ -7 / 2 = -3
ঋণাত্মক সংখ্যা বাঁয়ে সরানো (-1 << 2)Undefined behaviorপ্রায়ই “ঠিক” দেখায়, কিন্তু নিশ্চয়তা নেই
বাঁয়ে সরিয়ে signed সংখ্যার সীমা পার (1 << 31, ৩২ বিটের int)Undefined behavior (overflow)সমাধান: 1u << 31, অর্থাৎ unsigned সংখ্যা
টাইপের বিট-সংখ্যার সমান বা বেশি সরানো (x << 32)Undefined behaviorx86-এ প্রায়ই কিছুই সরে না — বিস্ময়কর বাগ
ঋণাত্মক পরিমাণে সরানো (x << -1)Undefined behaviorউল্টো দিকে সরে না!
নিয়মটি মনে রাখুন

বিট নিয়ে কাজ করলে unsigned টাইপ ব্যবহার করুন। Unsigned সংখ্যায় চিহ্নের ঝামেলা নেই, সীমা পার হলে নিয়ম মেনে ঘুরে আসে, আর শিফটের আচরণ সব compiler-এ এক। আর গুণ-ভাগের জন্য শিফট লিখবেন না — x * 8 লিখুন। Compiler নিজেই সেটিকে দ্রুততম নির্দেশে বদলে দেয় (প্রশ্ন ০৩-এ বিস্তারিত)।

৭ · কে আগে — precedence ও associativity

স্কুলে শিখেছেন BODMAS — আগে বন্ধনী, তারপর ভাগ-গুণ, শেষে যোগ-বিয়োগ। C-রও এমন একটি নিয়ম আছে, তবে অনেক বড়, কারণ অপারেটর অনেক বেশি। কোন অপারেটর আগে কাজ করবে, তার নাম precedence. আর একই স্তরের দুটি অপারেটর পাশাপাশি থাকলে কোন দিক থেকে জোড়া বাঁধা হবে, তার নাম associativity.

Compiler আসলে expression-টিকে একটি গাছের মতো সাজায় — উঁচু অগ্রাধিকারের অপারেটর থাকে নিচের ডালে, তাই তারা আগে হিসাব হয়। 2 + 3 * 4 - 8 / 2 কীভাবে ১০ হয়, ধাপে ধাপে দেখুন:

এখন expression-টি দেখতে যেমন
বাকি হিসাব
শুরুকোন অপারেটর কোন স্তরের
  2  +  3  *  4  -  8  /  2

  *  /   উঁচু স্তর  → আগে জোড়া বাঁধে
  +  -   নিচু স্তর  → পরে, বাঁ থেকে ডানে
গাছ তৈরিনিচের ডাল আগে হিসাব হবে
            [-]
           /   \
        [+]     [/]
       /   \    /  \
      2   [*]  8    2
          /  \
         3    4
3 * 4সবচেয়ে নিচের ডাল
            [-]
           /   \
        [+]     [/]
       /   \    /  \
      2    12  8    2
8 / 2ডান ডাল
            [-]
           /   \
        [+]     4
       /   \
      2    12
2 + 12বাঁ ডাল
            [-]
           /   \
         14     4
14 - 4গাছের গোড়া
            10

  পুরো expression-এর মান = 10
  1. সারি দেখে স্তর চেনা। চারটি অপারেটর, দুটি স্তর। * আর / উঁচু স্তরে, + আর - নিচু স্তরে। উঁচু স্তরের অপারেটর তার দুই পাশের সংখ্যাকে আগে “আঁকড়ে ধরে।”
  2. গাছ তৈরি। 3 * 4 আর 8 / 2 আলাদা ডাল হলো। তারপর + আর - একই স্তরের, আর তাদের associativity বাঁ থেকে ডানে — তাই আগে 2 + (3*4), তারপর সেটি থেকে (8/2) বিয়োগ। গোড়ায় বসল -, কারণ সেটি সবার শেষে হবে।
  3. নিচের ডাল: 3 * 4 = ১২। একটি সংখ্যা দিয়ে পুরো ডালটি বদলে গেল। গাছ ছোট হলো।
  4. 8 / 2 = ৪। একটি জরুরি কথা: 3 * 4 আগে হবে না 8 / 2 — C তা নির্দিষ্ট করে না, compiler নিজের সুবিধামতো বেছে নেয়। Precedence শুধু বলে কে কার সাথে জোড়া বাঁধবে, সময়ের ক্রম নয়। এখানে ফল একই থাকে, কিন্তু দুই ডালে side effect থাকলে (যেমন দুটি i++) বিপদ।
  5. 2 + 12 = ১৪। এবার বাঁ ডাল তৈরি।
  6. 14 - 4 = ১০। গোড়ায় পৌঁছে পুরো expression-এর মান পাওয়া গেল। Compiler আসলে এমন একটি গাছই বানায় — একে বলে syntax tree. আর সংখ্যাগুলো কোডে সরাসরি লেখা থাকলে compiler এই পুরো হিসাব নিজেই সেরে ফেলে, প্রোগ্রামে শুধু ১০ বসিয়ে দেয়।

পুরো precedence তালিকা মুখস্থ করার দরকার নেই। নিচের সংক্ষিপ্ত তালিকাটি (উপর থেকে নিচে, উঁচু থেকে নিচু অগ্রাধিকার) চিনে রাখুন, বিশেষ করে লাল দাগের জায়গাগুলো:

স্তরঅপারেটরজোড়া বাঁধার দিক
১() [] a++ a--বাঁ → ডান
২++a --a ! ~ -a (type) sizeofডান → বাঁ
৩* / %বাঁ → ডান
৪+ -বাঁ → ডান
৫<< >>বাঁ → ডান
৬< <= > >=বাঁ → ডান
৭== !=বাঁ → ডান
৮–১০&, তারপর ^, তারপর | — তুলনার চেয়েও নিচে!বাঁ → ডান
১১–১২&&, তারপর ||বাঁ → ডান
১৩? :ডান → বাঁ
১৪= += -= &= …ডান → বাঁ
১৫, (কমা)বাঁ → ডান

Associativity-র দুটি উদাহরণ: 20 - 5 - 3 = (20 - 5) - 3 = ১২ (বাঁ থেকে ডানে; উল্টো করলে হতো ১৮)। আর a = b = 5 = a = (b = 5) (ডান থেকে বাঁয়ে) — তাই দুটিতেই ৫ বসে। আবার 2 * 3 % 4: * আর % একই স্তরের, বাঁ থেকে ডানে, তাই (2 * 3) % 4 = ২।

৮ · ক্লাসিক ফাঁদ — যেখানে অভিজ্ঞরাও ভুল করেন

উপরের তালিকায় একটি অদ্ভুত ব্যাপার চোখে পড়েছে? বিটের & আছে ==-এর নিচে. অর্থাৎ তুলনা আগে হয়, বিটের AND পরে। এর ফল কী হতে পারে, একটি বাস্তব বাগ দিয়ে দেখি। কেউ লিখতে চাইলেন — “x জোড় হলে কিছু করো”:

যা লেখা হলোx = 4 (জোড়)
int x = 4;
if (x & 1 == 0) {
    printf("even\n");
}
/* প্রোগ্রামারের মনে: (x & 1) == 0 */
Compiler যেভাবে পড়ে== উঁচু স্তরে, & নিচে
     x & (1 == 0)

  == আগে জোড়া বাঁধল 1 আর 0-এর সাথে
ভেতরের তুলনা1 কি 0-এর সমান?
     x & 0

  1 == 0 মিথ্যা → মান 0
শেষ হিসাবযেকোনো কিছু & 0 = 0
     0100   (x = 4)
   & 0000
   ------
     0000   → শর্ত মিথ্যা, "even" কখনো ছাপা হয় না

  সমাধান: if ((x & 1) == 0)
  1. উদ্দেশ্য সরল। শেষ বিট 0 হলে সংখ্যাটি জোড় — তাই লেখা হলো x & 1 == 0. x = ৪ জোড়, তাই “even” ছাপা হওয়ার কথা।
  2. Compiler-এর চোখে। ==-এর অগ্রাধিকার &-এর চেয়ে বেশি। তাই সে 1 == 0-কে আগে জোড়া বাঁধে — প্রোগ্রামার যা ভেবেছিলেন তার একদম উল্টো।
  3. তুলনার ফল ০। ১ আর ০ সমান নয়, তাই 1 == 0-এর মান ০। এখন বাকি থাকল x & 0.
  4. শর্ত চিরকাল মিথ্যা। যেকোনো সংখ্যার সাথে ০-এর AND সবসময় ০। x যা-ই হোক, “even” কোনোদিন ছাপা হবে না। কোড compile হয়, চলে, কোনো crash নেই — শুধু নীরবে ভুল। সৌভাগ্যক্রমে gcc -Wall এখানে বলে suggest parentheses around comparison in operand of '&'. সমাধান: (x & 1) == 0.

এমন আরও কয়েকটি ফাঁদ, যেগুলো বারবার দেখা যায় — নতুনদের কোডে, পুরোনো প্রজেক্টে, এবং AI-এর লেখা কোডেও:

যা লেখা হলোC যা বোঝেসঠিক রূপ
if (x = 0)x-এ ০ বসাও; শর্ত সবসময় মিথ্যাif (x == 0)
avg = a + b / 2a + (b / 2)(a + b) / 2
flags & MASK != 0flags & (MASK != 0)(flags & MASK) != 0
1 << n + 11 << (n + 1) — + আগেযা চান তা বন্ধনী দিয়ে লিখুন
if (age >= 18 & ok)বিটের AND, short-circuit নেইage >= 18 && ok
if (0 < m < 100)(0 < m) < 100 — সবসময় সত্য0 < m && m < 100
সন্দেহ হলে বন্ধনী দিন।
বন্ধনী কোডকে ধীর করে না — শুধু পাঠকের ভুল বোঝা কমায়।

পেশাদার কোডে নিয়মটি আরও কড়া: যেখানেই বিটের অপারেটর আর তুলনা একসাথে, বা && আর || একসাথে, সেখানে বন্ধনী দিন — নিজের জন্য যতটা না, ছয় মাস পরে কোডটি যিনি পড়বেন তাঁর জন্য।

৯ · AI-এর যুগে অপারেটর যাচাই

AI খুব সাবলীলভাবে expression লেখে — আর ঠিক সেজন্যই এর ভুলগুলো চোখে পড়ে না। লাইনটি দেখতে নিখুঁত, compile হয়, সাধারণ ইনপুটে ঠিক উত্তরও দেয়। সমস্যা ধরা পড়ে শূন্য, ঋণাত্মক বা বড় সংখ্যায়। অপারেটর নিয়ে AI-এর সাধারণ ভুলগুলো:

AI-এর ভুলউদাহরণকীভাবে ধরবেন
Precedence গুলিয়ে ফেলাif (status & READY == READY)-Wall-এর -Wparentheses warning; বিট আর তুলনা একসাথে দেখলেই বন্ধনী খুঁজুন
Undefined behavior-এর ওপর ভরসাx = i++ + i++;, a[i] = i++;এক statement-এ একই ভ্যারিয়েবলে দুবার ++ বা =
ঋণাত্মক বা বড় সংখ্যায় শিফট1 << 31, value >> 1 দিয়ে ঋণাত্মক সংখ্যা ভাগবিটের কাজে unsigned আছে কি না দেখুন
লজিক্যাল জায়গায় বিটের অপারেটরif (n != 0 & total / n > 50)একটি & মানে short-circuit নেই — ক্ষেত্র A-তে crash
ভাগশেষে ঋণাত্মকের কথা ভুলে যাওয়াn % 2 == 1 দিয়ে বিজোড়; (h - 3) % 12 দিয়ে ঘড়ি পেছানোঋণাত্মক ইনপুট দিয়ে চালিয়ে দেখুন
গড়ে integer divisionavg = sum / count; (দুটোই int)হাতে একটি গড় হিসাব করে মিলিয়ে দেখুন

AI-কে expression লিখতে বা যাচাই করতে বললে নিচের মতো নির্দিষ্ট করে বলুন — যাতে সে নিজেই precedence আর সীমার ক্ষেত্রগুলো দেখায়:

নিচের C expression-টি পরীক্ষা করো: [এখানে expression বা কোডের লাইন পেস্ট করুন] ১. প্রতিটি অপারেটরের precedence অনুযায়ী পুরো বন্ধনী বসিয়ে দেখাও, C আসলে এটিকে কীভাবে পড়ে। ২. এখানে কোনো undefined behavior আছে কি না বলো — এক statement-এ একই ভ্যারিয়েবল দুবার বদলানো, শূন্য দিয়ে ভাগ, signed সংখ্যার overflow, ঋণাত্মক বা বড় শিফট। ৩. ইনপুট 0, একটি ঋণাত্মক সংখ্যা আর INT_MAX দিলে কী হবে, হাতে হিসাব করে দেখাও। ৪. && আর & বা || আর | কোথাও গুলিয়েছে কি না দেখো। সহজ বাংলায় উত্তর দাও, আর শুধু যে অংশ বদলাতে হবে সেটি দেখাও।

AI-এর দেওয়া expression হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১C-তে & আর && দুটো আলাদা অপারেটর কেন? আর &-এর precedence ==-এর নিচে কেন — এটা কি ভুল নকশা?

দুটি অপারেটর দুটি ভিন্ন প্রশ্নের উত্তর দেয়। & জিজ্ঞেস করে “দুটি সংখ্যার কোন কোন বিট একসাথে 1?” — ফল একটি নতুন সংখ্যা। && জিজ্ঞেস করে “দুটি শর্তই কি সত্য?” — ফল শুধু ০ বা ১। পার্থক্যটি চোখে পড়ে 4 & 2 আর 4 && 2-এ: প্রথমটি ০ (100 আর 010-এ কোনো মিল নেই), দ্বিতীয়টি ১ (দুটোই শূন্য নয়, অর্থাৎ দুটোই সত্য)। এর ওপর && short-circuit করে আর মাঝে একটি sequence point রাখে; & দুই পাশই হিসাব করে, এবং কোনটি আগে, তারও নিশ্চয়তা নেই।

ইতিহাসটি মজার। C-র পূর্বসূরি ভাষা B-তে আলাদা && ছিল না — একই & প্রসঙ্গ দেখে কখনো বিটের AND, কখনো যুক্তির AND হিসেবে কাজ করত। Dennis Ritchie C তৈরির সময় দুটিকে আলাদা করেন, && আর || যোগ করেন। কিন্তু তখন অনেক পুরোনো কোড ছিল যেখানে if (a == b & c) জাতীয় লেখা যুক্তির অর্থে ব্যবহার হতো। সেই কোড যাতে ভেঙে না যায়, তাই & আর |-এর অগ্রাধিকার তুলনার নিচেই রেখে দেওয়া হয়। Ritchie পরে নিজেই লিখেছেন, পেছনে তাকালে মনে হয় তখনই precedence বদলে ফেলা উচিত ছিল।

কেন আর বদলানো গেল না? কারণ ততদিনে লাখো লাইনের কোড এই নিয়মের ওপর দাঁড়িয়ে। একটি ভাষার নিয়ম বদলালে পুরোনো সব প্রোগ্রাম নীরবে ভিন্ন অর্থ পেয়ে যেত — compile হতো, কিন্তু ভুল কাজ করত। C++, Java, C#, JavaScript-ও এই একই ক্রম উত্তরাধিকারসূত্রে নিয়েছে, যাতে C-জানা প্রোগ্রামারদের অসুবিধা না হয়। ফলে পঞ্চাশ বছর পরেও আমরা x & 1 == 0-এর ফাঁদে পড়ি, আর compiler warning দিয়ে সেই ঐতিহাসিক সিদ্ধান্তের ক্ষতি সামলায়।

মূল উপলব্ধি: জনপ্রিয় সিস্টেমে ছোট একটি নকশার সিদ্ধান্তও কয়েক দশক টিকে থাকে, কারণ পেছনের দিকে সামঞ্জস্য (backward compatibility) প্রায়ই নিখুঁততার চেয়ে মূল্যবান। আজ যে API বা ফাইল-ফরম্যাট আপনি বানাবেন, কাল সেটাই হয়তো কেউ আর বদলাতে পারবে না।

প্র ০২C-তে -7 % 3 = -1, অথচ Python-এ -7 % 3 = 2. কোনটি ঠিক? C এই নিয়ম কেন বেছে নিল?

দুটোই “ঠিক” — তারা দুটি ভিন্ন সংজ্ঞা মানে। যেকোনো ভাষায় একটি সম্পর্ক রক্ষা করতে হয়: (a / b) * b + a % b == a. অর্থাৎ ভাগফল কীভাবে গোল করা হবে, তা ঠিক করলেই ভাগশেষ ঠিক হয়ে যায়।

  • C (C99 থেকে): ভাগফল শূন্যের দিকে কাটা হয়। -7 / 3 = -2 (−২.৩৩ থেকে দশমিক বাদ), তাই ভাগশেষ = −৭ − (−২ × ৩) = -1. ভাগশেষের চিহ্ন হয় ভাজ্যের মতো।
  • Python: ভাগফল নিচের দিকে (floor) গোল হয়। -7 // 3 = -3, তাই ভাগশেষ = −৭ − (−৩ × ৩) = 2. ভাগশেষের চিহ্ন হয় ভাজকের মতো।

C কেন এটি বেছে নিল? পুরোনো C89 মানে নিয়মটি নির্দিষ্টই ছিল না — ঋণাত্মক সংখ্যায় compiler নিজের মতো করতে পারত। কারণ C চেয়েছিল প্রতিটি CPU-র নিজস্ব ভাগের নির্দেশ সরাসরি ব্যবহার করতে, বাড়তি কোড ছাড়া। পরে দেখা গেল প্রায় সব CPU-র ভাগের নির্দেশ শূন্যের দিকেই কাটে, আর Fortran-এর মতো বৈজ্ঞানিক ভাষাও তা-ই করে। তাই C99 সেই আচরণটিকেই বাধ্যতামূলক করে — বহনযোগ্যতা (portability) বাড়ল, গতিও কমল না।

বাস্তবে এর প্রভাব: ঘড়ির হিসাবে পেছনে যেতে (h - k) % 12 লিখলে C-তে ঋণাত্মক ফল আসতে পারে, যেমন ২টা থেকে ৫ ঘণ্টা পেছালে -3. নিরাপদ সূত্র: ((h - k) % 12 + 12) % 12. তেমনি array-র শেষ থেকে শুরুতে ঘুরে আসার হিসাবেও এই ভুলে index ঋণাত্মক হয়ে মেমরির বাইরে চলে যেতে পারে — গুরুতর বাগ।

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

প্র ০৩শিফট যদি গুণ-ভাগের চেয়ে দ্রুত হয়, তাহলে সবাই x * 8-এর বদলে x << 3 লেখে না কেন?

কারণ compiler আপনার চেয়ে ভালোভাবে এই কাজটি করে। আধুনিক GCC বা Clang-এ optimization চালু থাকলে x * 8 লিখলে compiler নিজেই শিফটের নির্দেশ বসায়। এমনকি x * 10-কেও সে (x << 3) + (x << 1) বা CPU-র কোনো বিশেষ ঠিকানা-হিসাবের নির্দেশে ভেঙে দিতে পারে — যেটি দ্রুত। এই কৌশলের নাম strength reduction: দামি কাজকে সস্তা কাজ দিয়ে বদলানো। godbolt.org-এ -O2 দিয়ে নিজেই দেখে নিতে পারেন।

ভাগের ক্ষেত্রে একটি সূক্ষ্মতা আছে। Signed সংখ্যায় x / 2 আর x >> 1 এক নয় — ঋণাত্মক সংখ্যায় প্রথমটি শূন্যের দিকে, দ্বিতীয়টি (সাধারণত) নিচের দিকে গোল করে, যেমন -7-এ -3 বনাম -4. Compiler এটি জানে, তাই signed ভাগকে শিফটে বদলানোর সময় ছোট একটি সংশোধনী নির্দেশ যোগ করে। আপনি হাতে >> লিখলে সেই সংশোধন হবে না — অর্থাৎ “দ্রুত” করতে গিয়ে ভুল উত্তর।

তাহলে শিফট কোথায় সত্যিই লেখা উচিত? যখন আপনার উদ্দেশ্যটাই বিট নিয়ে — মাস্ক বানানো (1u << 5), প্যাকেট থেকে নির্দিষ্ট বিট বের করা, রঙের RGB মান আলাদা করা ((color >> 16) & 0xFF) — তখন শিফটই সবচেয়ে পরিষ্কার ভাষা। অনেক ছোট microcontroller-এ ভাগের জন্য আলাদা হার্ডওয়্যার থাকে না, তাই সেখানে প্রোগ্রামাররা ২-এর ঘাতে হিসাব সাজানোর চেষ্টা করেন — তবে সেখানেও সাধারণত compiler-ই রূপান্তরটি করে।

পুরোনো দিনে, যখন compiler এত চালাক ছিল না, হাতে শিফট লেখা সত্যিই দরকারি ছিল। সেই অভ্যাস অনেক পুরোনো বই আর ফোরামে রয়ে গেছে — আর সেখান থেকে AI-এর প্রশিক্ষণেও।

মূল উপলব্ধি: কোড লিখুন মানুষের জন্য — উদ্দেশ্য যা, তা-ই লিখুন। গুণ চাইলে *, বিট চাইলে <<. ছোটখাটো গতির কৌশল compiler-কে ছেড়ে দিন, আর সত্যিকারের ধীর জায়গা খুঁজতে মাপুন (profiling), অনুমান করবেন না।

প্র ০৪i++ + i++-এর মান নির্দিষ্ট করে দিলেই তো ঝামেলা মিটে যেত। C কেন এটিকে undefined রেখে দিল?

প্রশ্নটি যৌক্তিক, এবং কিছু ভাষা ঠিক সেটাই করেছে — Java বলে দেয় expression সবসময় বাঁ থেকে ডানে হিসাব হবে। তাহলে C কেন করেনি? উত্তর লুকিয়ে আছে C-র মূল দর্শনে: “যা দরকার নেই, তার দাম দিতে হবে না।”

একটি expression-এর অংশগুলো কোন ক্রমে হিসাব হবে, সেটি খোলা রাখলে compiler নিজের সুবিধামতো সাজাতে পারে — কোন মান কোন register-এ আছে, কোন নির্দেশ CPU-তে একসাথে চলতে পারে, কোনটি আগে শুরু করলে অপেক্ষা কম। ১৯৭০-৮০-র দশকে নানা রকম CPU-র জন্য দক্ষ কোড বানাতে এই স্বাধীনতা খুব কাজে লাগত। নির্দিষ্ট ক্রম বাধ্যতামূলক করলে কিছু ক্ষেত্রে compiler-কে বাড়তি নির্দেশ লিখতে হতো, শুধু এমন কোডের জন্য যা ভালো প্রোগ্রামার এমনিতেই লেখেন না।

তাহলে “undefined” কেন, “unspecified” নয়? Unspecified মানে “কয়েকটি সম্ভাব্য ফলের একটি হবে।” কিন্তু একই ভ্যারিয়েবলে দুটি অসমাপ্ত লেখা মেমরিতে আংশিকভাবে মিশে যাওয়ার মতো হার্ডওয়্যার-স্তরের সমস্যাও তাত্ত্বিকভাবে সম্ভব ছিল। Undefined বলে দিলে compiler ধরে নিতে পারে “এমন কোড কখনো আসবে না”, আর সেই অনুমানের ওপর optimization চালাতে পারে। ঝুঁকিটি হলো — এমন কোড এলে সত্যিই যা খুশি হতে পারে, আর optimization লেভেল বদলালে ফলও বদলায়।

দাম কিন্তু আছে। Undefined behavior থেকে আসা বাগ বহু নিরাপত্তা-ত্রুটির জন্ম দিয়েছে। তাই আধুনিক ভাষাগুলো উল্টো পথ নিয়েছে, আর C++ ২০১৭ সালে কিছু ক্ষেত্রে ক্রম নির্দিষ্ট করে দিয়েছে। C-তে সমাধান হলো শৃঙ্খলা: এক statement-এ একটি ভ্যারিয়েবল একবারই বদলান, আর -Wall-এর -Wsequence-point warning কখনো উপেক্ষা করবেন না।

মূল উপলব্ধি: প্রতিটি ভাষার নকশা একটি চুক্তি। C বলে “আমি আপনাকে গতি আর নিয়ন্ত্রণ দেব, বদলে আপনি নিয়ম মানবেন।” নিয়ম ভাঙলে compiler আপনাকে রক্ষা করার দায় নেয় না — এটাই C শেখার সবচেয়ে গুরুত্বপূর্ণ মানসিকতা।

প্র ০৫একটি সংখ্যার বিটে বিটে হ্যাঁ/না রাখা — এই কৌশল বাস্তব সিস্টেমে কোথায় কোথায় চলে, আর এর দাম কী?

আপনি প্রতিদিন এর ওপর নির্ভর করেন, না জেনেই। কয়েকটি উদাহরণ:

  • ফাইলের অনুমতি: Linux-এ প্রতিটি ফাইলের মালিক, গ্রুপ আর বাকিদের জন্য ৯টি অনুমতির বিট, সাথে কয়েকটি বিশেষ বিট। chmod 755 লিখলে আপনি আসলে সরাসরি বিট-প্যাটার্ন লিখছেন। ওয়েব হোস্টিংয়ের cPanel-এ ফাইলের পাশে যে ৬৪৪ বা ৭৫৫ দেখেন, তা এখান থেকেই।
  • ফাইল খোলার নির্দেশ: Linux-এ ফাইল খুলতে লেখা হয় open(path, O_WRONLY | O_CREAT | O_TRUNC) — “শুধু লেখার জন্য, না থাকলে বানাও, থাকলে খালি করো।” তিনটি ইচ্ছা, একটি সংখ্যায়।
  • নেটওয়ার্ক: ইন্টারনেটের TCP প্যাকেটের হেডারে SYN, ACK, FIN, RST নামের কয়েকটি ফ্ল্যাগ-বিট থাকে। সংযোগ শুরু, নিশ্চিতকরণ, শেষ — সব এই বিটগুলোর মাধ্যমে বোঝানো হয়। Firewall প্রতি সেকেন্ডে লাখো প্যাকেটে & দিয়ে এগুলো পরীক্ষা করে।
  • হার্ডওয়্যার: microcontroller-এর register, গেমপ্যাডের কোন বোতাম চাপা, CPU-র ফলাফল-ফ্ল্যাগ (শূন্য হলো কি না, overflow হলো কি না) — সবই বিট।

কেন এত জনপ্রিয়? প্রথমত জায়গা: ৩২টি হ্যাঁ/না একটি int-এ, যেখানে আলাদা ভ্যারিয়েবলে লাগত অনেক গুণ বেশি। নেটওয়ার্ক প্যাকেট বা লাখো ফাইলের হিসাবে এই পার্থক্য বিশাল। দ্বিতীয়ত গতি: একটি & নির্দেশেই কয়েকটি শর্ত একসাথে পরীক্ষা হয় — যেমন (perm & (READ | WRITE)) == (READ | WRITE) মানে “পড়া আর লেখা দুটোই আছে কি?” তৃতীয়ত, একটি সংখ্যা হিসেবে পুরো অবস্থাটি এক জায়গা থেকে আরেক জায়গায় পাঠানো বা ফাইলে রাখা সহজ।

দাম কী? পড়তে কঠিন — flags & 0x40 দেখে কেউ বুঝবে না এর অর্থ কী, তাই সবসময় নামযুক্ত মাস্ক ব্যবহার করুন। Precedence-এর ফাঁদ তো আছেই। আর একাধিক thread একই ফ্ল্যাগ-সংখ্যা একসাথে বদলালে একজনের বদল আরেকজন মুছে দিতে পারে, যদি বিশেষ সতর্কতা না নেওয়া হয়।

মূল উপলব্ধি: বিট মাস্ক হলো কম্পিউটারের “কম জায়গায় বেশি তথ্য” কৌশলের সবচেয়ে সরল রূপ। যেখানে আকার আর গতি গুরুত্বপূর্ণ — অপারেটিং সিস্টেম, নেটওয়ার্ক, embedded — সেখানে এটি অপরিহার্য; আর সাধারণ অ্যাপে পড়ার সুবিধাই বেশি জরুরি। কোথায় কোনটি, সেটা বোঝাই প্রকৌশল।

অনুশীলন

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

  1. হাতে হিসাব: চালানোর আগে খাতায় মান বের করুন — (ক) 20 - 5 - 3 (খ) 2 * 3 % 4 (গ) 20 / 3 * 3 (ঘ) -17 % 5. তারপর চাইলে প্রথম কোড ব্লকে বসিয়ে মিলিয়ে নিন।

    (ক) ১২ — বিয়োগ বাঁ থেকে ডানে: (২০ − ৫) − ৩.

    (খ) ২ — * আর % একই স্তরের, বাঁ থেকে ডানে: ৬ % ৪ = ২।

    (গ) ১৮ — 20 / 3 = ৬ (দশমিক বাদ), তারপর ৬ × ৩ = ১৮. ২০ নয়! integer division-এ তথ্য হারিয়ে যায়।

    (ঘ) −২ — -17 / 5 = -3 (শূন্যের দিকে কাটা), আর −১৭ − (−৩ × ৫) = −২. ভাগশেষ পেল ভাজ্যের চিহ্ন।

  2. অনুমতির সংখ্যা পড়ুন: (ক) chmod 644 দিলে মালিক, গ্রুপ আর বাকিরা কী কী পারবে? (খ) chmod 700? (গ) মালিক সব পারবে, গ্রুপ শুধু পড়তে ও চালাতে পারবে, বাকিরা কিছুই না — সংখ্যাটি কত?

    (ক) ৬৪৪ = rw-r--r-- — মালিক পড়া ও লেখা (৪ + ২), বাকি সবাই শুধু পড়া (৪)। ওয়েবসাইটের সাধারণ HTML ফাইলে এটিই দেওয়া হয়।

    (খ) ৭০০ = rwx------ — শুধু মালিক সব পারবে, আর কেউ কিছু নয়। ব্যক্তিগত চাবি বা গোপন ফোল্ডারে ব্যবহার হয়।

    (গ) ৭৫০ — মালিক ৪ + ২ + ১ = ৭, গ্রুপ ৪ + ১ = ৫, বাকিরা ০।

  3. বাগ শিকারি: AI তিনটি লাইন দিয়েছে। প্রতিটিতে কী ভুল, আর ঠিক রূপ কী? (ক) if (flags & ADMIN == ADMIN) (খ) if (0 < marks < 100) (গ) if (count != 0 & sum / count > 40)

    (ক) == আগে হিসাব হয়: flags & (ADMIN == ADMIN) = flags & 1. অর্থাৎ ADMIN বিট নয়, শুধু শেষ বিটটি পরীক্ষা হচ্ছে। ঠিক রূপ: (flags & ADMIN) == ADMIN.

    (খ) (0 < marks) < 100 — প্রথম অংশ ০ বা ১, দুটোই ১০০-র কম, তাই সবসময় সত্য। ঠিক রূপ: 0 < marks && marks < 100.

    (গ) একটি & — বিটের AND, short-circuit নেই। count ০ হলেও sum / count হিসাব হবে, ফলে শূন্য দিয়ে ভাগ। ঠিক রূপ: count != 0 && sum / count > 40.

  4. বিটের ধাঁধা: (n & (n - 1)) == 0 শর্তটি n = ৮ আর n = ৬-এর জন্য বিট এঁকে হিসাব করুন। শর্তটি আসলে কী পরীক্ষা করে? আর বাইরের বন্ধনীটি সরিয়ে দিলে কী হবে?

    ৮ = 1000, ৭ = 0111 — AND করলে 0000, শর্ত সত্য। ৬ = 0110, ৫ = 0101 — AND করলে 0100, শর্ত মিথ্যা। কারণ n - 1 করলে সবচেয়ে ডানের 1-টি 0 হয় আর তার ডানের সব 0 হয় 1. সংখ্যাটিতে একটিমাত্র 1 থাকলে AND-এ কিছুই বাকি থাকে না। অর্থাৎ শর্তটি পরীক্ষা করে n কি ২-এর ঘাত (১, ২, ৪, ৮, ১৬…)। (n = ০-তেও শর্ত সত্য হয়, তাই বাস্তবে n != 0 && যোগ করা হয়।)

    বন্ধনী সরালে n & (n - 1) == 0 হয়ে যায় n & ((n - 1) == 0) — আবার সেই precedence-এর ফাঁদ! তখন n = ১ ছাড়া প্রায় সবসময় ফল ০।

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

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

ক্রমঅপারেটরগুলো অগ্রাধিকার অনুযায়ী সাজান — সবচেয়ে আগে যে কাজ করে সেটি উপরে (▲▼ চেপে সরান)

  1. * / % — গুণ, ভাগ, ভাগশেষ
  2. + - — যোগ, বিয়োগ
  3. == != — সমান / অসমান
  4. & — বিটের AND
  5. && — যুক্তির AND
  6. = += — মান বসানো

23 % 4-এর মান কত?

  • 5
  • 3
  • 5.75
  • 23 এর 4 শতাংশ

int a = 5; int b = a++; — এরপর a আর b কত?

  • a = 6, b = 5
  • a = 6, b = 6
  • a = 5, b = 5
  • a = 5, b = 6

n = 0 হলে if (n != 0 && total / n > 50) চালালে কী হবে?

  • শূন্য দিয়ে ভাগ হয়ে প্রোগ্রাম crash করবে
  • Compile error হবে
  • শর্ত মিথ্যা হবে, ডান পাশের ভাগটি চলবেই না
  • ডান পাশ আগে চলবে, তারপর বাঁ পাশ

C x & 1 == 0-কে কীভাবে পড়ে?

  • (x & 1) == 0
  • বাঁ থেকে ডানে যেভাবে লেখা
  • এটি compile error
  • x & (1 == 0), অর্থাৎ সবসময় ০

2 + 3 * 4 - 8 / 2-এর মান কত?

  • 6
  • 10
  • 6.5
  • 14

i = 3; i = i++ + i++; — i-এর মান কত হবে?

  • 7
  • 8
  • নির্ধারিত নয় — undefined behavior
  • Compile error

শব্দকোষ

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

মনে রাখুন

  1. প্রতিটি expression-এর একটি মান আছে — তুলনার মান ১ বা ০, assignment-এর মান বসানো সংখ্যাটি।
  2. int ভাগে দশমিক কাটা পড়ে; % দেয় ভাগশেষ — জোড়-বিজোড়, শেষ অঙ্ক, ঘড়ি, পালা।
  3. && আর || short-circuit করে — পাহারার শর্তটি বাঁ পাশে লিখুন।
  4. বিট মাস্কে পরীক্ষা &, চালু |, বন্ধ & ~, উল্টানো ^; বিটের কাজে unsigned.
  5. & | আছে ==-এর নিচে — সন্দেহ হলে বন্ধনী; আর এক statement-এ একটি ভ্যারিয়েবল একবারই বদলান।

পরের পর্বে: হিসাব করতে শিখলেন, কিন্তু একটি প্রশ্ন বাকি — int আর double একসাথে মিশলে কী হয়? কেন 5 / 2 হয় ২, আর -1 < 0u কেন মিথ্যা? C নীরবে যে রূপান্তরগুলো করে, পরের পর্বে সেগুলো চোখের সামনে আনব।

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

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