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

টাইপ কনভার্শন — C যখন নীরবে মানের টাইপ বদলে দেয়

Type Conversion in C — Promotion, Arithmetic Conversions, Casts & Floating-Point Traps

Taught in Bangla, this lesson explains how C silently converts types: integer promotion, the usual arithmetic conversions, signed and unsigned mix-ups with size_t, where a cast must go, overflow before widening, and floating-point comparison and money pitfalls.

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

  • কেন C-কে টাইপ বদলাতে হয়, আর কোন কোন জায়গায় এটি লুকিয়ে ঘটে
  • Integer promotion আর usual arithmetic conversions — compiler যে নিয়মে টাইপ মেলায়
  • Signed-unsigned মেশানো, size_t আর strlen()-এর পরিচিত ফাঁদ চেনা
  • Cast কখন সত্যিই সাহায্য করে, আর কখন বাগ লুকিয়ে ফেলে
  • গুণের আগেই overflow, দশমিকের তুলনা আর টাকা-পয়সার নিরাপদ হিসাব

১ · নীরব অনুবাদক — C কেন নিজে থেকে টাইপ বদলায়

নাদিয়া একটি কোচিং সেন্টারের জন্য ছোট একটি প্রোগ্রাম লিখছেন। কাজ সহজ — তিনটি পরীক্ষার নম্বরের গড়, আর কতজন পাস করল তার শতকরা হার। একজন ছাত্রের নম্বর ৭৮, ৮৫ আর ৯০. হাতে হিসাব করলে গড় ৮৪.৩৩. প্রোগ্রাম দেখাল 84.00. আর ৫০ জনের মধ্যে ৪৫ জন পাস করলেও পাসের হার এলো 0.00!

কোনো error নেই, কোনো crash নেই। Compiler একটি warning-ও দেয়নি। অথচ দুটি ফলই ভুল। এই পর্বের মূল কথা এখানেই: C-এর বেশিরভাগ “রহস্যময়” বাগের পেছনে থাকে একটি লুকানো টাইপ বদল। পর্ব শেষে আপনি নাদিয়ার দুটি বাগই এক নজরে ধরতে পারবেন।

কেন বদলাতেই হয়

পর্ব ০৫-এ দেখেছেন, মেমরিতে সবকিছুই বিট। পর্ব ০৬-এ দেখেছেন, টাইপ হলো সেই বিটগুলো পড়ার চুক্তি — কত বিটের বাক্স, আর বিটগুলোর অর্থ কী। একটি char সাধারণত ৮ বিটের বাক্স, int ৩২ বিটের, double ৬৪ বিটের, এবং double-এর বিটগুলো একেবারে অন্য নিয়মে সাজানো (IEEE 754).

এখন CPU-র দিকটা ভাবুন। CPU-তে “৩২ বিটের পূর্ণসংখ্যা যোগ” একটি নির্দেশ, “৬৪ বিটের দশমিক যোগ” আরেকটি নির্দেশ। কিন্তু “একটি পূর্ণসংখ্যার সাথে একটি দশমিক যোগ” — এমন কোনো নির্দেশ নেই। তাই 3 + 2.5 লিখলে compiler-কে আগে দুটি মানকে একই টাইপে আনতে হয়। তারপর একটি নির্দেশে যোগ।

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

কোথায় কোথায় লুকিয়ে ঘটে

পর্ব ০৬-এ 7 / 2 আর একটি signed-unsigned তুলনার উদাহরণ দেখেছিলেন। এবার পুরো ছবিটা দেখি। C-তে টাইপ বদল ঘটে অন্তত ছয়টি জায়গায়:

কোথায়উদাহরণকী ঘটে
হিসাবের ভেতরেc + 1, n * 2.5ছোট টাইপ int-এ ওঠে, তারপর দুই পাশ এক টাইপে আসে
তুলনায়len < strlen(s)হিসাবের মতোই টাইপ মেলানো — signed-unsigned ফাঁদ এখানেই
মান বসানোয়int x = 3.99;ডান পাশের মান বাঁ পাশের টাইপে বদলায় — কেটে যেতে পারে
ফাংশনে মান পাঠানোয়sqrt(16)16 ফাংশনের প্যারামিটারের টাইপ double-এ বদলায়
ফাংশন থেকে ফেরতেreturn 2.7; (ফাংশন int)ফেরত মান ফাংশনের টাইপে বদলায় — 2 হয়ে যায়
printf-এর মতো ফাংশনেprintf("%f", f)float নিজে থেকে double হয়, char হয় int

আপনি না বললেও compiler নিজে যে বদল করে, তাকে বলে implicit conversion. আপনি নিজে বন্ধনীতে টাইপ লিখে যা করান, সেটি cast. দুটিই একই যন্ত্র ব্যবহার করে — পার্থক্য শুধু কে সিদ্ধান্ত নিচ্ছে।

মূল ধারণা

রূপান্তর দুই রকম। Widening conversion — ছোট বাক্স থেকে বড় বাক্সে (char → int, int → long long). এখানে মান সাধারণত অক্ষত থাকে। Narrowing conversion — বড় থেকে ছোটতে (double → int, int → char). এখানে মান কেটে যেতে, মুড়ে যেতে, বা পুরো অর্থহীন হতে পারে। বাগ প্রায় সবসময় দ্বিতীয় দলে। তবে একটি ব্যতিক্রম আছে: signed থেকে unsigned-এ গেলে মাপ একই থাকলেও অর্থ বদলে যায় — অংশ ৪-এ দেখবেন।

২ · Integer promotion — ছোট বাক্স হিসাবে ঢোকার আগেই int

প্রথম নিয়মটি সবচেয়ে কম চোখে পড়ে, অথচ সবচেয়ে বেশি ঘটে। C কখনো char বা short-এর ওপর সরাসরি হিসাব করে না। হিসাবে ঢোকার আগেই এগুলো int হয়ে যায়। একে বলে integer promotion.

একটি ছোট পরীক্ষা: char a = 100, b = 100; তারপর int s = a + b; লিখলে s কত? char-এ ১২৭-এর বেশি ধরে না, তাই অনেকে ভাবেন ফল মুড়ে যাবে। কিন্তু ফল ঠিকঠাক 200. কারণ যোগের আগেই a আর b দুটিই int হয়েছে, আর int-এ 200 সহজেই আঁটে। এখানে promotion আপনাকে বাঁচাল।

আবার একই নিয়ম আপনাকে বিভ্রান্তও করতে পারে। sizeof(a + b) কত? 1 নয় — 4, কারণ a + b-এর টাইপ int.

প্রসারণের দুই পদ্ধতি — চিহ্ন টেনে আনা, নাকি শূন্য বসানো

৮ বিটকে ৩২ বিটে বাড়াতে হলে বাড়তি ২৪টি বিটে কিছু একটা বসাতে হয়। কী বসবে, তা নির্ভর করে মূল টাইপটি signed নাকি unsigned তার ওপর:

  • Sign extension — signed টাইপে বাঁয়ের সবচেয়ে উঁচু বিটটি (sign bit) বাড়তি সব ঘরে কপি হয়। তাই −56 বড় বাক্সেও −56 থাকে।
  • Zero extension — unsigned টাইপে বাড়তি সব ঘরে ০ বসে। তাই 200 বড় বাক্সেও 200 থাকে।

এবার একটি চতুর প্রশ্ন। C-এর মানদণ্ড বলেনি সাধারণ char signed হবে নাকি unsigned — এটি implementation-defined, অর্থাৎ প্রতিটি compiler ও প্ল্যাটফর্ম নিজে ঠিক করে এবং লিখে জানায়। Intel/AMD কম্পিউটারে (x86) GCC-তে char signed. কিন্তু ARM-ভিত্তিক Linux-এ (যেমন Raspberry Pi বা Android) সাধারণত unsigned. ফলে একই কোড দুই মেশিনে দুই উত্তর দিতে পারে। নিচের ডায়াগ্রামে দুটোই দেখুন:

1/* x86 + GCC: char is signed */2char c = 200;3int x = c + 1;4printf("%d\n", x);5/* ARM Linux: char is unsigned */
ভ্যারিয়েবলের মান (Watch)
c-এর ৮টি বিট
c-এর মান
int-এ তোলা c
x
  1. 200 কি আঁটবে? 200-এর বাইনারি রূপ 11001000 — ৮ বিটেই লেখা যায়। কিন্তু signed char-এর সীমা −১২৮ থেকে ১২৭. 200 সেই সীমার বাইরে। এটি একটি narrowing conversion: int 200 থেকে char-এ। (GCC এখানে একটি warning-ও দেয়।)
  2. বিট একই, অর্থ বদলাল। GCC তার নিয়মপুস্তিকায় লিখে রেখেছে: সীমার বাইরের মানকে signed টাইপে রাখলে ২৮ = ২৫৬ বিয়োগ করে মুড়িয়ে আনা হবে। 200 − 256 = −56. পর্ব ০৫-এর two's complement-এর চোখে 11001000 পড়লেও −56 আসে। মানদণ্ডের ভাষায় এটি implementation-defined — অন্য compiler চাইলে অন্য কিছু করতে পারত।
  3. Promotion: চিহ্ন টেনে আনা। c + 1 হিসাবের আগে c-কে int-এ তুলতে হবে। c signed, তাই sign extension: সবচেয়ে উঁচু বিটটি ১, তাই বাড়তি ২৪টি ঘরেই ১ বসল। Hex-এ 0xFFFFFFC8, মান এখনো −56.
  4. যোগ int-এ। −56 + 1 = −55. ফলের টাইপ int, আর x-ও int, তাই মান বসাতে আর কোনো বদল লাগল না। আপনি লিখেছিলেন 200, পেলেন −55!
  5. ছাপা হলো -55. Intel/AMD-এর সাধারণ Linux বা Windows কম্পিউটারে এই ফলই আসবে। যে কোডে char দিয়ে বাইট ধরে রাখা হয় — যেমন ফাইল বা নেটওয়ার্ক থেকে পড়া ডেটা — সেখানে এই বাগ খুব পরিচিত।
  6. অন্য মেশিন, অন্য উত্তর। ARM Linux-এ char unsigned, যার সীমা ০ থেকে ২৫৫. তাই 200 ঠিকঠাক আঁটে। Promotion-এ zero extension হয় — বাড়তি ঘরে ০, মান 200 থাকে। ফল 201. শিক্ষা: বাইট রাখতে চাইলে সাধারণ char নয়, স্পষ্ট করে unsigned char বা uint8_t লিখুন।

নিয়মটির পুরো রূপ: int-এর চেয়ে ছোট যেকোনো পূর্ণসংখ্যা টাইপ (_Bool, char, signed char, unsigned char, short, unsigned short) হিসাবে ঢুকলে — যদি তার সব মান int-এ আঁটে, তবে int হয়; না আঁটলে unsigned int. সাধারণ কম্পিউটারে int ৩২ বিট, তাই এদের সবাই int হয়।

৩ · Usual arithmetic conversions — rank-এর সিঁড়ি

Promotion শেষ হলেও কাজ শেষ নয়। দুই পাশে এখনো আলাদা টাইপ থাকতে পারে — যেমন int আর double, বা int আর unsigned int. তখন C যে নিয়মে একটি সাধারণ টাইপ বেছে নেয়, তার নাম usual arithmetic conversions. এর কেন্দ্রে আছে প্রতিটি পূর্ণসংখ্যা টাইপের একটি পদমর্যাদা, যাকে বলে conversion rank.

সিঁড়ির ধাপটাইপমন্তব্য
সবচেয়ে ওপরেlong double → double → floatদশমিক টাইপ সবসময় পূর্ণসংখ্যার ওপরে
পূর্ণসংখ্যা, উঁচুlong long / unsigned long longঅন্তত ৬৪ বিট
long / unsigned longLinux ৬৪-বিটে ৬৪ বিট, Windows-এ ৩২ বিট
int / unsigned intসাধারণত ৩২ বিট — হিসাবের “ন্যূনতম” টাইপ
পূর্ণসংখ্যা, নিচুshort, char, _Boolএরা হিসাবে ঢোকার আগেই promotion পায়

খেয়াল করুন, একই ধাপে signed আর unsigned রূপ দুটিই থাকে — int আর unsigned int-এর rank সমান। এই ছোট্ট তথ্যটিই পরের সব ঝামেলার মূল। নিয়মটি ধাপে ধাপে চলে, আর প্রথম যে শর্ত মেলে সেখানেই থামে:

  1. কোনো পাশে দশমিক টাইপ থাকলে, অন্য পাশও সেই দশমিক টাইপ হয় (সবচেয়ে উঁচুটি জেতে)। 3 + 2.5 → 3.0 + 2.5.
  2. নইলে দুই পাশে integer promotion. তারপর টাইপ এক হলে কাজ শেষ।
  3. দুটিই signed বা দুটিই unsigned হলে, নিচু rank-এরটি উঁচুতে ওঠে।
  4. unsigned পাশের rank সমান বা বেশি হলে — signed-টি unsigned হয়ে যায়।
  5. signed পাশের rank বেশি এবং সে unsigned-এর সব মান ধরতে পারলে — unsigned-টি signed হয়।
  6. নইলে দুটিই signed টাইপটির unsigned রূপে যায়।

লিখে পড়লে জটিল লাগে। একটি উদাহরণে চালিয়ে দেখলে সহজ হয়ে যায়। পর্ব ০৬-এ দেখেছেন, -1 > 0u সত্য হয়। কিন্তু ঠিক কোন নিয়মে? পর্ব ০৭-এর শেষে প্রশ্নটি উল্টো দিক থেকে রেখেছিলাম — -1 < 0u কেন মিথ্যা? এবার নিয়মের প্রতিটি ধাপ ধরে দেখুন:

  1. টাইপ চেনাint · unsignedদুই পাশ আলাদা
  2. দশমিক আছে?নাপূর্ণসংখ্যার পথ
  3. Rank তুলনাসমানunsigned জেতে
  4. -1 বদলাল4294967295বিট একই
  5. তুলনা0 (মিথ্যা)গণিত বলে সত্য
-1 < 0uদুই পাশের টাইপ
-1     → int (signed, ৩২ বিট)
0u     → unsigned int (u মানে unsigned)
ধাপ ১ ও ২দশমিক? promotion?
দশমিক টাইপ আছে?          না
promotion দরকার?          না — দুটিই অন্তত int
টাইপ এক?                  না → পরের ধাপে
ধাপ ৩ ও ৪চিহ্ন আর rank
চিহ্ন একই?                না (একটি signed, একটি unsigned)
unsigned-এর rank ≥ signed-এর? হ্যাঁ (দুটিই int-এর ধাপে)
সিদ্ধান্ত:                 signed → unsigned int
-1 → unsigned int২৩২ যোগ
বিট:  11111111 11111111 11111111 11111111
int হিসেবে পড়লে      = -1
unsigned হিসেবে পড়লে = 4294967295   (-1 + 4294967296)
চূড়ান্ত তুলনাCPU যা দেখে
4294967295 < 0    → 0   (মিথ্যা)
তাই  -1 < 0u       → 0
-1L < 0uএকই কোড, দুই প্ল্যাটফর্ম
Linux 64-bit:  long = 64 bit → unsigned int-এর সব মান ধরে
               0u → 0L,  -1L < 0L  → 1 (সত্য)
Windows 64-bit: long = 32 bit → সব মান ধরে না
               দুটিই unsigned long → 0 (মিথ্যা)
  1. টাইপ চেনা। -1 একটি সাধারণ int. 0u-এর শেষের u বলে দেয় এটি unsigned int. তুলনার দুই পাশে টাইপ আলাদা, তাই usual arithmetic conversions শুরু।
  2. দশমিক নেই, promotion লাগে না। কোনো পাশে double বা float নেই, তাই প্রথম শর্ত খাটে না। দুটিই আগে থেকেই int-এর ধাপে, তাই promotion-এ কিছু বদলায় না। কিন্তু টাইপ এখনো এক নয়।
  3. Rank সমান — unsigned জেতে। চিহ্ন আলাদা। unsigned int-এর rank int-এর সমান, তাই চতুর্থ শর্ত মেলে: signed পাশটিকে unsigned বানাও। লক্ষ করুন, নিয়মটি মানের দিকে তাকায় না — শুধু টাইপের দিকে।
  4. −1 হয়ে গেল 4294967295. Unsigned-এ রূপান্তরের নিয়ম গাণিতিক ও নির্দিষ্ট: মান সীমার মধ্যে না আসা পর্যন্ত ২৩২ যোগ করো। −1 + 4294967296 = 4294967295. Two's complement মেশিনে কোনো বিট বদলায় না — বদলায় শুধু পড়ার চুক্তি।
  5. তুলনা মিথ্যা। CPU এখন তুলনা করছে 4294967295 < 0 — অবশ্যই মিথ্যা, ফল 0. গণিতে যা নিশ্চিত সত্য, C-তে তা মিথ্যা — আর কোনো error নেই।
  6. একটি অক্ষর বদলালেই উল্টো ফল — তাও প্ল্যাটফর্মভেদে। -1L < 0u-এ বাঁ পাশ long. ৬৪-বিট Linux-এ long ৬৪ বিটের, যা unsigned int-এর সব মান ধরতে পারে, তাই পঞ্চম শর্ত: unsigned পাশটিই long হয়, ফল সত্য। ৬৪-বিট Windows-এ long মাত্র ৩২ বিট, তাই ষষ্ঠ শর্ত: দুটিই unsigned long, ফল মিথ্যা। একই কোড, দুই উত্তর!
নিয়ম টাইপ দেখে, মান দেখে না।
সমান rank-এ unsigned সবসময় জেতে।

৪ · Signed আর unsigned — size_t-এর ফাঁদ

“আমি তো unsigned লিখিই না, আমার কী ভয়?” — নতুনদের এই আত্মবিশ্বাস দ্রুত ভাঙে। কারণ unsigned টাইপ আপনার কোডে ঢোকে লাইব্রেরি থেকে, আপনার অজান্তে। সবচেয়ে বড় উৎস size_t: sizeof-এর ফল, strlen()-এর ফল, malloc()-এর আকার — সবই size_t, যা একটি unsigned টাইপ। ৬৪-বিট Linux-এ এটি ৬৪ বিটের unsigned long.

তিনটি পরিচিত ফাঁদ

১. দৈর্ঘ্যের বিয়োগ

if (strlen(a) - strlen(b) > 0) — দুটি দৈর্ঘ্য আলাদা হলেই সত্য, কারণ unsigned বিয়োগের ফল কখনো ঋণাত্মক হয় না। strlen("Dhaka") - 6 হয় 18446744073709551615. এখানে wraparound নিয়মমতোই ঘটে।

সমাধান: strlen(a) > strlen(b)

২. ঋণাত্মকের সাথে তুলনা

int pos = -1; তারপর if (pos < strlen(s)) — মিথ্যা! pos রূপান্তরিত হয়ে বিশাল একটি unsigned সংখ্যা হয়ে যায়। “খুঁজে পাইনি” বোঝাতে −1 ফেরত দেওয়ার কোডে এটি নিয়মিত ঘটে।

সমাধান: আগে pos >= 0 যাচাই

৩. পেছনে গোনা লুপ

for (size_t i = n - 1; i >= 0; i--) — কখনো থামে না। size_t কখনো ০-এর নিচে যায় না; ০ থেকে ১ কমালে সবচেয়ে বড় মানে চলে যায়। আর n শূন্য হলে n - 1 শুরু থেকেই বিশাল।

সমাধান: for (size_t i = n; i-- > 0; )

তৃতীয় সমাধানটি একটু অদ্ভুত দেখায়, তাই একটু খুলে বলি। i-- > 0 আগে পুরোনো মান দিয়ে তুলনা করে, তারপর কমায়। n = ৩ হলে লুপের ভেতরে i পায় ২, ১, ০ — তারপর তুলনা 0 > 0 মিথ্যা, লুপ শেষ। আর n = ০ হলে একবারও ঢোকে না। প্রথমবার দেখলে অচেনা লাগে, কিন্তু অভিজ্ঞ C প্রোগ্রামাররা এটি চেনেন।

Compiler আপনার পাহারাদার — যদি তাকে জাগিয়ে রাখেন

GCC-তে এই ফাঁদগুলোর জন্য আলাদা warning আছে। -Wsign-compare signed আর unsigned তুলনা ধরলে সতর্ক করে — C-তে এটি -Wextra দিলেই চালু হয়। আর -Wtype-limits (এটিও -Wextra-এর অংশ) ধরে “unsigned ≥ ০ সবসময় সত্য” ধরনের শর্ত:

warning: comparison of integer expressions of different signedness: 'int' and 'size_t'
warning: comparison of unsigned expression in '>= 0' is always true

এই warning দেখলে সবচেয়ে খারাপ “সমাধান” হলো চোখ বন্ধ করে cast বসিয়ে দেওয়া: if (pos < (int)strlen(s)). এটি এই ক্ষেত্রে কাজ করে, কিন্তু খুব বড় স্ট্রিংয়ে (int) নিজেই মান কেটে ফেলবে। সঠিক পথ হলো আগে ভাবা — এই দুটি মান কি আসলে একই জাতের? সূচক আর দৈর্ঘ্য হলে দুটোকেই size_t রাখুন; ঋণাত্মক হতে পারে এমন মান হলে আগে চিহ্ন যাচাই করুন, তারপর তুলনা।

৫ · গড় আর শতকরা — integer division-এর আসল ক্ষতি

পর্ব ০৬-এ দেখেছেন 7 / 2 হয় 3, আর cast কোথায় বসালে ফল বাঁচে। এবার নাদিয়ার আসল প্রোগ্রামে ফিরি। এখানে দুটি আলাদা বাগ আছে, আর দ্বিতীয়টি প্রথমটির চেয়ে অনেক বিপজ্জনক — কারণ সেখানে ফল একটু ভুল নয়, পুরোপুরি শূন্য। ধাপে ধাপে দেখুন:

1int m1 = 78, m2 = 85, m3 = 90;2int sum = m1 + m2 + m3;3double avg = sum / 3;4double ok  = (double)sum / 3;5int passed = 45, total = 50;6double rate = passed / total * 100;7double fix  = 100.0 * passed / total;
ভ্যারিয়েবলের মান (Watch)
sum
avg
ok
rate
fix
  1. তিনটি নম্বর। ৭৮, ৮৫ আর ৯০ — তিনটিই int. নম্বর পূর্ণসংখ্যা, তাই টাইপ বাছাই এখানে ঠিকই আছে।
  2. যোগফল 253. int + int = int. কোনো বদল নেই, কোনো ভুল নেই।
  3. প্রথম বাগ: ভাগ হলো পূর্ণসংখ্যায়। sum / 3-এর দুই পাশই int, তাই usual arithmetic conversions-এর কিছু করার নেই — integer division, ফল 84. দশমিক অংশ ০.৩৩ ফেলে দেওয়া হলো। তারপর মান বসানোর সময় 84 থেকে double 84.0. বাঁ পাশের double দেরিতে এসেছে — ক্ষতি আগেই হয়ে গেছে।
  4. সমাধান: ভাগের আগে একটি পাশ দশমিক। (double)sum আগে 253.0 হলো। এবার নিয়মের প্রথম শর্ত খাটল: অন্য পাশের 3-ও 3.0 হলো। দশমিক ভাগ, ফল 84.333333. শুধু একটি পাশে cast-ই যথেষ্ট — বাকি কাজ compiler নিজেই করে।
  5. পাসের হিসাব। ৫০ জনে ৪৫ জন পাস — আসল হার ৯০%. দুটিই int, এখানেও ঠিক আছে।
  6. দ্বিতীয় বাগ: ফল শূন্য! পর্ব ০৭ মনে করুন: / আর *-এর অগ্রাধিকার সমান, বাঁ থেকে ডানে চলে। তাই আগে 45 / 50 — integer division, ফল 0. তারপর 0 × 100 = 0. ১-এর চেয়ে ছোট যেকোনো অনুপাত integer division-এ সোজা শূন্য হয়। এ কারণেই এই বাগ এত বিপজ্জনক — ফলাফল একটু ভুল নয়, পুরোপুরি অর্থহীন।
  7. সমাধান: দশমিকটি সবার আগে। 100.0 * passed আগে হিসাব হয় — একটি পাশ double, তাই passed 45.0 হলো, ফল 4500.0. তারপর 4500.0 / 50 — আবারও দশমিক ভাগ — 90.0. খেয়াল করুন, passed / total * 100.0 লিখলে কিন্তু কাজ হতো না: বাঁ থেকে ডানে আগে 45 / 50 = 0 হয়ে যেত। দশমিক টাইপটিকে ভাগের আগে আনতে হবে।
মূল ধারণা

C প্রতিটি অপারেটরের টাইপ ঠিক করে শুধু সেই অপারেটরের দুই পাশ দেখে, পুরো এক্সপ্রেশন বা ফল কোথায় যাবে তা দেখে না। তাই প্রশ্নটি সবসময় এই: “ভাগের ঠিক মুহূর্তে দুই পাশের টাইপ কী?” এই একটি প্রশ্নই গড়, শতকরা, রান রেট, CGPA — সব হিসাবের বাগ ধরিয়ে দেয়।

আরেকটি বিকল্প আছে: পুরোটাই পূর্ণসংখ্যায় রেখে ক্রম বদলানো — passed * 100 / total দেয় 90. এটি তখনই ঠিক, যখন আপনি ইচ্ছা করেই দশমিক অংশ বাদ দিতে চান (যেমন ৮৯.৯%-কে ৮৯% দেখানো), আর গুণফল int-এর সীমা পার হয় না। দ্বিতীয় শর্তটি কতটা গুরুত্বপূর্ণ, তা দেখবেন অংশ ৭-এ।

৬ · মান বসানো আর cast — কাটা, মোড়ানো আর লুকানো বাগ

এখন পর্যন্ত দেখলাম হিসাবের ভেতরের রূপান্তর। এবার মান বসানোর সময়ের রূপান্তর — যেখানে ডান পাশের মানকে জোর করে বাঁ পাশের টাইপে ঢুকতে হয়। এখানে কোনো “সিঁড়ি” নেই; বাঁ পাশের টাইপই চূড়ান্ত। তাই এখানেই সবচেয়ে বেশি narrowing ঘটে।

কী থেকে কীউদাহরণফলনিয়মের মর্যাদা
দশমিক → পূর্ণসংখ্যাint t = -3.99;-3 — শূন্যের দিকে কাটেনির্দিষ্ট (truncation)
বিশাল দশমিক → পূর্ণসংখ্যাint t = 1e20;যা খুশিUndefined behavior
পূর্ণসংখ্যা → unsigned ছোট টাইপunsigned char u = 300;44 (300 − 256)নির্দিষ্ট — মডুলো ২N
পূর্ণসংখ্যা → signed ছোট টাইপsigned char s = 200;GCC-তে -56Implementation-defined
বড় পূর্ণসংখ্যা → floatfloat f = 16777217;16777216Implementation-defined — পাশের দুটি মানের একটি (সাধারণত নিকটতমটি)
পূর্ণসংখ্যা → _Bool_Bool b = 256;1 (শূন্য নয় মানেই 1)নির্দিষ্ট

পঞ্চম সারিটি অনেককে অবাক করে। float-এর ৩২ বিটের মধ্যে আসল অঙ্ক রাখার জায়গা মাত্র ২৪ বিটের সমান (precision). তাই ২২৪ = ১,৬৭,৭৭,২১৬-এর পর থেকে প্রতিটি পূর্ণসংখ্যা আর হুবহু রাখা যায় না। ১ কোটি ৬৭ লাখের বেশি গ্রাহকের আইডি float-এ রাখলে দুজন আলাদা গ্রাহক একই আইডি পেয়ে যেতে পারেন। অর্থাৎ “বড় টাইপে যাচ্ছি” ভাবলেও int → float আসলে নিরাপদ নয়।

Cast — কখন বন্ধু, কখন ছদ্মবেশী শত্রু

Cast হলো compiler-কে আপনার স্পষ্ট নির্দেশ: “এই মানটিকে এই টাইপে বদলাও, আমি জানি কী করছি।” এটি তিনটি জায়গায় সত্যিই কাজের:

  • হিসাবের টাইপ বদলাতে: (double)sum / 3 — ভাগের আগেই দশমিকে নেওয়া।
  • গুণের আগে বড় বাক্সে নিতে: (long long)a * b — অংশ ৭-এ দেখবেন।
  • উদ্দেশ্য স্পষ্ট করতে: int whole = (int)price; — “হ্যাঁ, দশমিক বাদ দেওয়াটাই চাই” — পাঠক আর compiler দুজনেই বোঝে।

কিন্তু cast কোনো জাদুর কাঠি নয়। এটি মান ঠিক করে না, শুধু বদলায়। দুটি সাধারণ ভুল ধারণা:

  • “Cast গোল করে।” না। (int)2.99 হয় 2, (int)-2.99 হয় −2. গোল করতে চাইলে <math.h>-এর round() বা lround() আছে; সেগুলো পর্ব ২৬-এ স্ট্যান্ডার্ড লাইব্রেরির সাথে দেখবেন।
  • “Cast দিলে warning চলে যায়, তাই সমস্যাও চলে যায়।” না। Warning চলে যায়, সমস্যা থেকে যায় — শুধু এখন আর কেউ দেখতে পায় না। (int)strlen(s) বা (unsigned)x দেখলে নিজেকে জিজ্ঞেস করুন: এখানে মান হারানো কি সম্ভব? সম্ভব হলে আগে পরিসীমা যাচাই করা হয়েছে কি?

printf-এর টাইপ — যেখানে কোনো রূপান্তরই হয় না

printf একটি variadic function — যত খুশি আর্গুমেন্ট নেয়, কিন্তু সেগুলোর টাইপ আগে থেকে জানে না। তাই এখানে C শুধু একটি সরল নিয়ম মানে, যার নাম default argument promotion: char আর short হয় int, float হয় double. ব্যস, আর কিছু না। এজন্যই float ছাপতেও %f চলে।

কিন্তু printf("%d", 3.5) লিখলে 3.5 মোটেই int হয় না। printf একটি int খুঁজবে, অথচ পাঠানো হয়েছে double. এটি undefined behavior. ৬৪-বিট Linux-এ double আর int আলাদা রেজিস্টারে পাঠানো হয়, তাই printf এমন একটি রেজিস্টার পড়ে যেখানে আপনার মান নেই — ছাপা হয় অর্থহীন কোনো সংখ্যা। সুখবর হলো, -Wall এটি ধরে (-Wformat): format '%d' expects argument of type 'int', but argument 2 has type 'double'.

এবার এতক্ষণের সব নিয়ম একসাথে চালিয়ে দেখুন। প্রতিটি লাইনের বাঁ পাশে লেবেল, ডান পাশে C-এর উত্তর:

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

int main(void) {
    int a = 7, b = 2, m = -1;
    unsigned int u = 0;
    double d = -3.99;
    char c = 'A';
    printf("7 / 2              = %d\n", a / b);
    printf("(double)7 / 2      = %.2f\n", (double)a / b);
    printf("(int)-3.99         = %d\n", (int)d);
    printf("sizeof(c + 1)      = %zu\n", sizeof(c + 1));
    printf("(unsigned)-1       = %u\n", (unsigned)m);
    printf("-1 < 0u ?          = %d\n", (unsigned)m < u);
    printf("strlen(\"Dhaka\") - 6 = %zu\n", strlen("Dhaka") - 6);
    printf("16777217 as float  = %.1f\n", (float)16777217);
    return 0;
}

ফলগুলো ক্রমানুসারে: 3 (integer division), 3.50 (একটি পাশ দশমিক), -3 (শূন্যের দিকে কাটা, গোল নয়), 4 (char + int-এর টাইপ int), 4294967295 (−1-এর unsigned রূপ), 0 (তুলনা মিথ্যা), 18446744073709551615 (৬৪-বিট size_t-এ ০ থেকে ১ কম), আর 16777216.0 (float এক ঘর হারাল)। ষষ্ঠ লাইনে (unsigned)m আমরা ইচ্ছা করেই লিখেছি — শুধু m < u লিখলে compiler ঠিক এই রূপান্তরটিই নীরবে করত, আর -Wextra একটি signedness warning দিত। এখানে আমরা সেই লুকানো কাজটিই চোখের সামনে এনেছি।

৭ · গুণের আগেই overflow — বড় বাক্সে রাখলেও বাঁচা যায় না

অংশ ৫-এর মূল ধারণাটি এবার আরেকটি রূপে ফিরে আসছে, এবং এটি বাস্তবের সবচেয়ে ব্যয়বহুল বাগগুলোর একটি। ধরুন একটি মোবাইল ওয়ালেট কোম্পানির রিপোর্ট: ৫০,০০০ জন গ্রাহক, প্রত্যেকে গড়ে ৫০,০০০ পয়সা (৫০০ টাকা) পাঠিয়েছেন। মোট কত পয়সা? উত্তর ২৫০ কোটি — যা ৩২-বিট int-এর সর্বোচ্চ মান (প্রায় ২১৪ কোটি) ছাড়িয়ে যায়। প্রোগ্রামার সতর্ক, তাই ফল রাখলেন long long-এ। তবু ফল ভুল এলো। কেন?

কোডদেখতে নিরাপদ
int a = 50000, b = 50000;
long long x = a * b;      /* x তো ৬৪ বিট, তাহলে সমস্যা কী? */
a * bশুধু ডান পাশ, নিজের টাইপে
a       *  b
int        int     → দুটিই int, গুণও int-এ (৩২ বিট)
আসল ফল    = 2500000000
int-এর সীমা = 2147483647   → সীমা পার!
Signed overflowনিয়মের বাইরে
মানদণ্ড বলে:  undefined behavior
অনেক মেশিনে বাস্তবে দেখা যায়:
2500000000 - 4294967296 = -1794967296
(optimizer চাইলে অন্য কিছুও করতে পারে)
long long x = …রূপান্তর এখন — অনেক দেরিতে
x = (long long)(-1794967296)
x = -1794967296      → ৬৪ বিটে ভুল মানটিই নিখুঁতভাবে রাখা হলো
সমাধানগুণের আগে বড় বাক্স
long long x = (long long)a * b;
a → long long;  b → long long (usual arithmetic conversions)
৬৪-বিট গুণ = 2500000000   ✔

long long x = (long long)(a * b);   /* ভুল! গুণ আগেই int-এ হয়ে গেছে */
  1. দেখতে নিরাপদ। ফল রাখার বাক্স long long, যাতে প্রায় 9.2 × 1018 পর্যন্ত আঁটে। প্রোগ্রামার ভাবলেন, বাক্স বড় হলেই হিসাব নিরাপদ।
  2. ডান পাশ নিজের টাইপে হিসাব হয়। অংশ ৫-এর সেই মূল কথা: C অপারেটরের দুই পাশ দেখে টাইপ ঠিক করে। a আর b দুটিই int, তাই গুণও int-এ — বাঁ পাশে কী আছে, তা গুণের সময় কেউ দেখে না। আসল ফল ২৫০ কোটি, কিন্তু int-এর সীমা ২১৪ কোটির কিছু বেশি।
  3. Signed overflow — undefined behavior. Signed পূর্ণসংখ্যার overflow নিয়মে নির্দিষ্ট নয়। বাস্তবে অনেক মেশিনে বিটগুলো মুড়ে গিয়ে −1794967296 আসে। কিন্তু optimizer ধরে নিতে পারে overflow কখনো হয় না, এবং সেই অনুমানে কোড সাজাতে পারে — তাই কোনো ফলের ওপরই ভরসা করা যায় না।
  4. রূপান্তর এখন — খুব দেরিতে। ভুল মানটি এবার long long-এ বদলায়। ৬৪ বিটের বাক্স ভুল মানটিকেই নিখুঁতভাবে ধরে রাখল। মোট লেনদেন দেখাল ঋণাত্মক ১৭৯ কোটি পয়সা!
  5. সমাধান: গুণের আগেই বড় বাক্স। (long long)a লিখলে a আগে ৬৪ বিটে ওঠে। এবার গুণের দুই পাশ আলাদা — long long আর int — তাই usual arithmetic conversions b-কেও long long বানায়। ৬৪-বিট গুণে ফল ঠিক 2500000000. আর (long long)(a * b) লিখলে বন্ধনীর ভেতরের গুণ আগেই int-এ হয়ে যায় — সেই একই বাগ, শুধু দেখতে বেশি সতর্ক।

এই ধরনের বাগ শুধু পাঠ্যবইয়ের উদাহরণ নয়। ২০০৬ সালে Google-এর প্রকৌশলী Joshua Bloch একটি ব্লগ লেখায় দেখান, binary search-এর প্রচলিত কোডে মাঝের সূচক mid = (low + high) / 2 হিসাবে low + high যোগফলটিই overflow করতে পারে — যখন অ্যারেতে শত কোটির কাছাকাছি উপাদান থাকে। বহু বছর ধরে এই কোড পাঠ্যবইয়ে আর Java-র লাইব্রেরিতেও ছিল। প্রচলিত সমাধান: mid = low + (high - low) / 2. পর্ব ৪১-এ binary search নিয়ে কাজ করার সময় এটি মনে রাখবেন।

৮ · দশমিকের রূপান্তর — সহনশীলতা আর টাকা-পয়সা

পর্ব ০৫-এ দেখেছেন কেন 0.1 + 0.2 হুবহু 0.3 হয় না: ১/১০ বাইনারিতে শেষ হয় না, যেমন ১/৩ দশমিকে শেষ হয় না (০.৩৩৩…)। ফলে 0.1 লিখলে মেমরিতে থাকে তার সবচেয়ে কাছের বাইনারি মান — 0.1000000000000000055…. এই পর্বের প্রশ্ন একটু ভিন্ন: এই ছোট্ট গরমিল টাইপ বদলের সময় কীভাবে বড় ভুলে পরিণত হয়?

প্রথম নিয়ম: দশমিকে == নয়

দুটি দশমিক সংখ্যা “সমান” কি না জানতে চাইলে জিজ্ঞেস করুন — “এরা কি যথেষ্ট কাছাকাছি?” এর জন্য একটি ছোট সহনশীলতা (tolerance) বেছে নিন:

#include <math.h>   /* fabs() = absolute value of a double */

if (fabs((0.1 + 0.2) - 0.3) < 1e-9) {
    /* close enough: treat as equal */
}

সহনশীলতা কত হবে, তা নির্ভর করে কাজের ওপর — মিটার মাপলে ১ মাইক্রোমিটার যথেষ্ট, জ্যোতির্বিদ্যায় হাজার কিলোমিটারও হতে পারে। খুব বড় বা খুব ছোট সংখ্যায় স্থির সহনশীলতার বদলে আপেক্ষিক সহনশীলতা লাগে; সেই গভীর আলোচনা ABCL TECH-এর Numerical Methods কোর্সে আছে।

দ্বিতীয় নিয়ম: টাকা গুনুন পয়সায়

এবার সবচেয়ে বাস্তব ক্ষেত্র — টাকা। একটি ছোট দোকানে দিনে ১,০০০টি ১০ পয়সার লেনদেন হলো। মোট হওয়ার কথা ঠিক ১০০ টাকা। তিনভাবে যোগ করে দেখি — float, double আর পয়সায় পূর্ণসংখ্যা। শেষ দুই লাইনে একটি টাইপ বদলের ফাঁদও আছে: ২৯ পয়সাকে টাকা থেকে পয়সায় নেওয়া।

#include <stdio.h>

int main(void) {
    float f = 0.0f;
    double d = 0.0;
    long long paisa = 0;
    for (int i = 0; i < 1000; i++) {   /* 1000 payments of Tk 0.10 */
        f += 0.10f;
        d += 0.10;
        paisa += 10;
    }
    printf("float  total: %.6f\n", f);
    printf("double total: %.15f\n", d);
    printf("paisa  total: %lld.%02lld\n", paisa / 100, paisa % 100);

    double price = 0.29;                 /* Tk 0.29 */
    printf("(int)(0.29 * 100)       = %d\n", (int)(price * 100));
    printf("(int)(0.29 * 100 + 0.5) = %d\n", (int)(price * 100 + 0.5));
    return 0;
}

float-এ মোট আসে 99.999046 — প্রায় এক পয়সা গরমিল, মাত্র এক হাজার লেনদেনেই। double-এ 99.999999999998593 — অনেক কাছে, কিন্তু তবু ১০০ নয়; লাখ লাখ লেনদেনে এই ভুলও জমে। আর পয়সার হিসাবে হুবহু 100.00, কারণ পূর্ণসংখ্যার যোগে কোনো গোল করার ভুল নেই। সবচেয়ে চমকপ্রদ শেষ দুই লাইন: 0.29 * 100 মেমরিতে আসলে 28.999999999999996, তাই (int) কেটে দেয় 28 — এক পয়সা হারিয়ে গেল! ০.৫ যোগ করে কাটলে (ধনাত্মক মানে) কাছের পূর্ণসংখ্যা পাওয়া যায়: 29. ভালো উপায় হলো round(), তবে সবচেয়ে ভালো উপায় হলো শুরু থেকেই দশমিক টাকা না রাখা।

মূল ধারণা

আর্থিক হিসাবে টাকাকে সবচেয়ে ছোট এককে (পয়সা) পূর্ণসংখ্যা হিসেবে রাখুন — long long বা int64_t. শুধু দেখানোর মুহূর্তে / 100 আর % 100 দিয়ে “টাকা.পয়সা” রূপে ছাপুন। ব্যবহারকারীর লেখা “১২.৫০” পড়তে হলেও টাকা আর পয়সার অংশ আলাদা করে পূর্ণসংখ্যায় পড়ুন। একে বলে fixed-point পদ্ধতি — দশমিক বিন্দুর অবস্থান আপনি নিজেই ঠিক করে রেখেছেন।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি (যেমন BCS, ব্যাংকের IT বা সরকারি প্রোগ্রামার পদ) আর কোডিং ইন্টারভিউর পরীক্ষায় এই বিষয়ে যে ধরনের প্রশ্ন আসতে পারে:

  • float f = 5 / 2; হলে f কত? — 2.0; ভাগ আগে int-এ হয়, পরে বসানোর সময় float.
  • Implicit আর explicit conversion-এর পার্থক্য কী? — প্রথমটি compiler নিজে করে, দ্বিতীয়টি প্রোগ্রামার cast লিখে করান।
  • unsigned int x = -1; ছাপলে (%u) কী আসে? — ৩২-বিট int-এ 4294967295, কারণ −1-এর সাথে ২৩২ যোগ হয়।
  • if (0.1 + 0.2 == 0.3) কেন মিথ্যা? — ০.১ ও ০.২ বাইনারিতে হুবহু রাখা যায় না; সহনশীলতা দিয়ে তুলনা করতে হয়।
  • char আর short হিসাবে কোন টাইপে যায়? — integer promotion-এ int-এ.

৯ · AI-এর যুগে — টাইপ বদলের বাগ যা “চলে” কিন্তু ভুল

টাইপ কনভার্শনের বাগের একটি বৈশিষ্ট্য AI-এর লেখা কোডে এদের বিশেষ বিপজ্জনক করে তোলে: কোড compile হয়, চলে, এবং প্রায় সঠিক উত্তর দেয়। AI সাধারণত প্রচুর পুরোনো কোড দেখে শিখেছে, আর সেই কোডে এই ভুলগুলো অসংখ্যবার আছে। তার ওপর AI আপনার প্ল্যাটফর্মে char signed নাকি unsigned, long কত বিটের — তা জানে না।

AI সাধারণত যেসব টাইপ-ভুল করে

AI-এর ভুললক্ষণযা চাইবেন
গড় বা শতকরা int / int-এগড়ের দশমিক নেই, শতকরা হার 0ভাগের আগে একটি পাশ double
int i দিয়ে strlen() বা sizeof-এর সাথে তুলনাsignedness warningসূচক size_t, বা আগে চিহ্ন যাচাই
টাকা float/double-এযোগফলে পয়সার গরমিলপয়সায় long long
Warning চাপা দিতে cast(int), (unsigned) ছড়ানোপ্রতিটি cast-এর কারণ ও পরিসীমা যাচাই
বড় গুণ int-এ, ফল long long-এবড় ইনপুটে ঋণাত্মক ফলগুণের আগে একটি পাশ cast
বাইটের জন্য সাধারণ charএক মেশিনে ঠিক, আরেকটিতে ভুলunsigned char বা uint8_t

AI-এর লেখা যেকোনো হিসাবের কোড হাতে পেলে নিচের প্রম্পটটি দিন। এখানে AI-কে কোড নতুন করে লিখতে বলা হচ্ছে না — বলা হচ্ছে প্রতিটি লুকানো রূপান্তর চিহ্নিত করতে। এতে আপনি নিজে শেখেন, আর AI-এর ভুলও ধরা পড়ে:

নিচের C কোডটি আমি gcc -Wall -Wextra -Wconversion দিয়ে compile করব, প্ল্যাটফর্ম ৬৪-বিট Linux. [এখানে কোড পেস্ট করুন] ১. কোডের প্রতিটি implicit type conversion-এর তালিকা দাও — লাইন নম্বর, কোন টাইপ থেকে কোন টাইপে, আর এতে মান হারানো সম্ভব কি না। ২. কোথাও signed আর unsigned মিশেছে কি না, integer division-এ দশমিক হারাচ্ছে কি না, আর বড় গুণ int-এ overflow করতে পারে কি না, দেখাও। ৩. প্রতিটি cast কেন আছে বলো; কোনো cast যদি শুধু warning চাপা দেয়, সেটি আলাদা করে চিহ্নিত করো। ৪. পুরো কোড আবার দিও না — শুধু যে লাইনগুলো বদলাতে হবে সেগুলো দাও, আর প্রতিটির জন্য একটি পরীক্ষার ইনপুট দাও যা আগের কোডে ভুল ফল দিত।

যাচাইয়ের চেকলিস্ট

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

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

প্র ০১১৯৯৬ সালে একটি মাত্র টাইপ রূপান্তর কীভাবে একটি রকেট ধ্বংস করল? এখান থেকে সফটওয়্যার প্রকৌশলের কী শিক্ষা?

১৯৯৬ সালের ৪ জুন ইউরোপীয় মহাকাশ সংস্থার নতুন রকেট Ariane 5-এর প্রথম উৎক্ষেপণ (Flight 501) হয়। উৎক্ষেপণের প্রায় ৩৭ সেকেন্ড পর রকেটটি পথ থেকে সরে যায়, ভেঙে পড়তে শুরু করে এবং আত্মবিনাশ ব্যবস্থায় ধ্বংস হয়। সাথে হারিয়ে যায় চারটি বৈজ্ঞানিক উপগ্রহের Cluster মিশন।

তদন্ত কমিটি (প্রধান ছিলেন ফরাসি গণিতবিদ Jacques-Louis Lions) কারণটি খুঁজে পায় দিক-নির্ণয় যন্ত্রের (inertial reference system) সফটওয়্যারে। সেখানে একটি ৬৪-বিট দশমিক মানকে (অনুভূমিক বেগ-সংক্রান্ত একটি মান) ১৬-বিট signed পূর্ণসংখ্যায় রূপান্তর করা হচ্ছিল। ১৬ বিটে সর্বোচ্চ ৩২,৭৬৭ পর্যন্ত আঁটে। Ariane 5 আগের Ariane 4-এর চেয়ে অনেক দ্রুত গতি নেয়, তাই মানটি সীমা ছাড়িয়ে গেল। সফটওয়্যারটি Ada ভাষায় লেখা, যেখানে এমন overflow একটি exception তোলে — কিন্তু এই রূপান্তরটির জন্য কোনো সুরক্ষা রাখা হয়নি। যন্ত্রটি বন্ধ হয়ে গেল।

আরও শিক্ষণীয় বিষয়গুলো:

  • পুনর্ব্যবহারের ফাঁদ: কোডটি Ariane 4 থেকে হুবহু নেওয়া হয়েছিল, যেখানে বছরের পর বছর নিখুঁত চলেছে। Ariane 4-এর গতিপথে মানটি কখনো সীমা ছাড়াত না। “আগে কাজ করেছে” মানে “নতুন পরিবেশে কাজ করবে” নয়।
  • অদরকারি কোড: ফাংশনটির কাজ ছিল মূলত উৎক্ষেপণের আগে যন্ত্র সাজানো। উৎক্ষেপণের পর এর কোনো প্রয়োজনই ছিল না, তবু Ariane 4-এর একটি পুরোনো চাহিদার কারণে কিছুক্ষণ চলত।
  • ব্যাকআপও একই রকম: দুটি যন্ত্র ছিল — কিন্তু দুটিতে একই সফটওয়্যার, তাই দুটিই একই মুহূর্তে একই কারণে বন্ধ হলো। একই নকশার কপি কোনো স্বাধীন সুরক্ষা নয়।

মূল উপলব্ধি: প্রতিটি narrowing conversion একটি অনুমান — “এই মান কখনো এত বড় হবে না।” সেই অনুমান কোথাও লিখে রাখা, পরীক্ষা করা এবং পরিবেশ বদলালে আবার যাচাই করা প্রকৌশলীর দায়িত্ব। AI-এর দেওয়া পুরোনো কোড নিজের প্রজেক্টে বসানোর সময়ও ঠিক এই প্রশ্নটি করুন।

প্র ০২JavaScript-এ সংখ্যার মাত্র একটি টাইপ — double. এতে কী সুবিধা, আর কী দাম দিতে হয়?

JavaScript-এর মূল Number টাইপ আসলে IEEE 754-এর ৬৪-বিট double. সেখানে int, long, unsigned — কিছুই আলাদা নেই। ফলে C-এর এই পর্বের অনেক ঝামেলা সেখানে নেই: 7 / 2 সোজা 3.5, signed-unsigned তুলনার ফাঁদ নেই, promotion-এর নিয়ম মুখস্থ করার দরকার নেই। নতুনদের জন্য এটি অনেক সহজ।

কিন্তু দাম আছে:

  • বড় পূর্ণসংখ্যা হারায়: double-এ হুবহু পূর্ণসংখ্যা রাখা যায় কেবল ২৫৩ − ১ (৯০০৭১৯৯২৫৪৭৪০৯৯১) পর্যন্ত — JavaScript-এ এর নাম Number.MAX_SAFE_INTEGER. এর ওপরে দুটি আলাদা সংখ্যা একই হয়ে যেতে পারে। এজন্যই Twitter (এখন X)-এর মতো সেবা তাদের API-তে ৬৪-বিট আইডি সংখ্যার পাশাপাশি স্ট্রিং হিসেবেও পাঠাত — JavaScript-এ সংখ্যা হিসেবে পড়লে শেষের অঙ্কগুলো বদলে যেত।
  • দশমিকের গরমিল সবখানে: 0.1 + 0.2 সেখানেও 0.30000000000000004. টাকার হিসাবে একই সতর্কতা লাগে।
  • লুকানো রূপান্তর তবুও আছে: বিটওয়াইজ অপারেটর (|, &, <<) চালানোর আগে JavaScript মানটিকে ৩২-বিট signed পূর্ণসংখ্যায় বদলায়। তাই x | 0 একটি পরিচিত কৌশল — দশমিক কেটে ৩২-বিট পূর্ণসংখ্যা বানানো।

শেষ পর্যন্ত ভাষাটিকে নতুন টাইপ যোগ করতে হয়েছে: ২০২০ সালের মানদণ্ডে (ES2020) এসেছে BigInt, যা যত বড় খুশি পূর্ণসংখ্যা হুবহু রাখে। মজার ব্যাপার, BigInt আর Number সরাসরি মেশানো যায় না — 1n + 1 লিখলে error দেয়। ভাষার নকশাকারীরা ইচ্ছা করেই এখানে নীরব রূপান্তর রাখেননি, কারণ তাঁরা C-এর মতো ভাষার অভিজ্ঞতা থেকে শিখেছেন।

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

প্র ০৩C কেন সব ছোট পূর্ণসংখ্যাকে হিসাবের আগে int-এ তোলে? সরাসরি char-এ হিসাব করলে কী ক্ষতি ছিল?

উত্তরটি লুকিয়ে আছে C-এর জন্মের যন্ত্রে। ১৯৭০-এর দশকের শুরুতে Bell Labs-এ Dennis Ritchie C তৈরি করেন মূলত DEC-এর PDP-11 কম্পিউটারে Unix লেখার জন্য। C-এর আগের ভাষা B-তে টাইপই ছিল না — সবকিছু ছিল একটি মেশিন-“word”. PDP-11 বাইট ধরে মেমরি পড়তে পারত, তাই C-তে char এলো; কিন্তু CPU-র রেজিস্টার ছিল ১৬ বিটের, আর হিসাব স্বাভাবিকভাবে সেই মাপেই চলত।

এখান থেকেই নীতিটি: int হলো মেশিনের “স্বাভাবিক” মাপ। C-এর মানদণ্ডেও প্রায় এই ভাষাতেই লেখা আছে — সাধারণ int-এর মাপ হবে যন্ত্রের স্থাপত্য যা স্বাভাবিক বলে। রেজিস্টারে ঢুকলে একটি বাইটও পুরো রেজিস্টার জুড়ে থাকে, তাই “পুরো রেজিস্টারে হিসাব, শেষে দরকার হলে কেটে নেওয়া” যন্ত্রের জন্য সহজ ও দ্রুত। আজকের অনেক CPU-তেও (যেমন ARM) ৮-বিটের আলাদা যোগের নির্দেশ নেই; রেজিস্টার ৩২ বা ৬৪ বিটের।

প্রথম দিকের C আরও এক ধাপ এগিয়ে ছিল: ১৯৭৮ সালের K&R বইয়ের C-তে দশমিকের সব হিসাবও double-এ হতো, float শুধু জায়গা বাঁচানোর জন্য। ১৯৮৯ সালের ANSI মানদণ্ড এটি শিথিল করে।

আরেকটি কম-জানা ইতিহাস: মানদণ্ড তৈরির সময় একটি বিতর্ক ছিল — unsigned char-কে promotion-এ unsigned int বানানো হবে (তখনকার কিছু compiler যা করত), নাকি int? কমিটি বেছে নেয় “মান রক্ষা”-র নিয়ম: যদি int-এ সব মান আঁটে, তবে int. যুক্তি ছিল, এতে বেশিরভাগ হিসাব গণিতের মতো আচরণ করে। দাম হলো, ছোট unsigned টাইপও হিসাবে signed হয়ে যায় — আর uint16_t দুটির গুণফল int-এর সীমা পার হলে signed overflow-এর মতো চমক তৈরি হতে পারে।

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

প্র ০৪ব্যাংক আর আর্থিক সফটওয়্যার কেন double এড়িয়ে পূর্ণসংখ্যা বা “decimal” টাইপ ব্যবহার করে?

কারণ টাকার হিসাবে “প্রায় ঠিক” বলে কিছু নেই। একটি ব্যাংকের হিসাব শেষে প্রতিটি পয়সা মিলতে হয় — নিরীক্ষক, নিয়ন্ত্রক সংস্থা আর গ্রাহক সবাই সেটাই আশা করেন। অথচ double বাইনারি ভগ্নাংশে কাজ করে, আর ০.১০ টাকার মতো সাধারণ মানও সেখানে হুবহু রাখা যায় না। এই পর্বের কোডেই দেখলেন, মাত্র এক হাজার যোগে float প্রায় এক পয়সা হারায়।

শিল্পে মূলত তিনটি পথ দেখা যায়:

  • ছোট এককে পূর্ণসংখ্যা: পয়সা বা সেন্টে long long. সহজ, দ্রুত, এবং যোগ-বিয়োগে কোনো গরমিল নেই। অনেক পেমেন্ট ব্যবস্থার API-ও এভাবেই সবচেয়ে ছোট এককে টাকার পরিমাণ পাঠায়।
  • দশমিক টাইপ: সংখ্যাকে দশ-ভিত্তিক অঙ্কে রাখা। ডেটাবেসে DECIMAL(p, s) বা NUMERIC, Java-তে BigDecimal, Python-এ decimal মডিউল। ১৯৫৯ সালে তৈরি COBOL ভাষা শুরু থেকেই দশমিক অঙ্কে হিসাব করত — এবং বহু ব্যাংকের পুরোনো মূল ব্যবস্থা আজও COBOL-এ চলে। ২০০৮ সালে IEEE 754 মানদণ্ডে দশমিক ভিত্তির floating point-ও যুক্ত হয়, আর IBM-এর কিছু প্রসেসরে এর জন্য হার্ডওয়্যারও আছে।
  • লিখিত গোল করার নিয়ম: সুদ, ভ্যাট বা কিস্তির হিসাবে ভগ্নাংশ আসবেই। তখন কোন দিকে গোল হবে, তা আগে থেকে নীতিতে লেখা থাকে। একটি পরিচিত পদ্ধতি “banker’s rounding” — ঠিক মাঝখানের মান (যেমন ২.৫) জোড় সংখ্যার দিকে গোল করা, যাতে লাখ লাখ লেনদেনে গোল করার ঝোঁক এক দিকে জমে না যায়।

খেয়াল করুন, এই পর্বের (int)(0.29 * 100) = 28 উদাহরণটি ঠিক সেই জায়গা দেখায়, যেখানে দুই জগৎ মেলে — দশমিক টাকা থেকে পূর্ণসংখ্যার পয়সায় রূপান্তর। একটি পেমেন্ট সিস্টেমে এমন রূপান্তর দিনে লাখবার ঘটলে প্রতিবার এক পয়সা হারানোও বড় অঙ্কে দাঁড়ায়।

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

প্র ০৫-Wconversion এত কাজের হলে -Wall-এর অংশ নয় কেন? এত “বিরক্তিকর” warning নিয়ে একটি দল কী করবে?

GCC আর Clang-এ -Wconversion এমন প্রতিটি implicit conversion-এ সতর্ক করে, যেখানে মান বদলে যেতে পারে — double → int, long → int, int → char, আর C-তে signed-unsigned বদলও (-Wsign-conversion এর সাথে চালু হয়)। এই পর্বের প্রায় প্রতিটি নীরব বাগ এটি ধরতে পারে। তাহলে ডিফল্টে চালু নয় কেন?

কারণ এটি সত্যিই “শব্দবহুল”। Integer promotion-এর কারণে char c = a + b; (যেখানে a, b দুটিই char) লিখলেও warning আসে — যোগফল int, আর int থেকে char-এ narrowing. পুরোনো একটি বড় প্রজেক্টে এটি চালু করলে কয়েক হাজার warning আসা অস্বাভাবিক নয়, যার বেশিরভাগ বাস্তবে নিরীহ। সেই ভিড়ে আসল বাগগুলো হারিয়ে যায়, আর দলটি warning দেখাই বন্ধ করে দেয় — যা সবচেয়ে খারাপ ফল।

অভিজ্ঞ দলগুলো সাধারণত এভাবে এগোয়:

  • নতুন কোডে কড়া, পুরোনোতে ধীরে: নতুন মডিউলে প্রথম দিন থেকেই চালু, পুরোনো কোড একটু একটু করে পরিষ্কার।
  • Cast মানে দলিল: warning চাপা দিতে cast বসানো নিষেধ, যদি না সাথে লেখা থাকে কেন মান হারাবে না (যেমন আগের লাইনে পরিসীমা যাচাই)।
  • টাইপ ঠিক করা, cast নয়: বেশিরভাগ warning-এর আসল সমাধান হলো ভ্যারিয়েবলের টাইপ ঠিক বাছাই — যেমন সূচকের জন্য size_t.
  • চলার সময়ের পাহারা: Compiler সব ধরতে পারে না। -fsanitize=undefined (UBSan) চলার সময় signed overflow ধরে; Clang-এ -fsanitize=implicit-conversion নীরব কাটা বা চিহ্ন বদলও ধরে। এগুলো নিয়ে পর্ব ৩১-এ বিস্তারিত।

মূল উপলব্ধি: একটি টুল শুধু কতটা ধরে তা দিয়ে নয়, কতটা ভুল সংকেত দেয় তা দিয়েও বিচার হয়। ভালো দল শব্দবহুল টুলকে ফেলে দেয় না — তাকে এমনভাবে বসায়, যাতে প্রতিটি সংকেত মনোযোগ পায়।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — নিয়ম ধরে আগে ভাবুন, তারপর উত্তর মেলান।

  1. আগে বলুন, পরে মেলান: ধরে নিন int ৩২ বিট আর char ৮ বিট (GCC, x86). প্রতিটির মান কত? (ক) 5 / 2 * 2.0 (খ) 5 / 2.0 * 2 (গ) (unsigned char)300 (ঘ) (unsigned char)-1 (ঙ) 3 / 4 * 100

    (ক) 4.0 — বাঁ থেকে ডানে: আগে 5 / 2 = 2 (দুটিই int), তারপর 2 × 2.0 = 4.0. (খ) 5.0 — আগে 5 / 2.0 = 2.5, তারপর × 2 = 5.0. শুধু একটি দশমিক বিন্দুর জায়গা বদলেই ফল আলাদা। (গ) 44 — unsigned-এ রূপান্তর মডুলো ২৫৬: 300 − 256. (ঘ) 255 — −1 + 256. (ঙ) 0 — 3 / 4 = 0, তারপর 0 × 100. নাদিয়ার দ্বিতীয় বাগটিই।

  2. লুপটি থামে না কেন? একটি অ্যারের উপাদান উল্টো ক্রমে ছাপার কোড: for (size_t i = n - 1; i >= 0; i--) printf("%d ", arr[i]); সমস্যাটি ব্যাখ্যা করুন, compiler কী warning দেবে বলুন, আর সঠিক রূপ লিখুন।

    size_t unsigned, তাই i >= 0 সবসময় সত্য। i ০ হওয়ার পর i-- তাকে সবচেয়ে বড় মানে নিয়ে যায়, আর arr[i] অ্যারের সীমার অনেক বাইরে পড়ে — সাধারণত crash. n শূন্য হলে শুরুতেই একই বিপদ। -Wextra দিলে GCC বলে comparison of unsigned expression in '>= 0' is always true. সঠিক রূপ: for (size_t i = n; i-- > 0; ) printf("%d ", arr[i]); — এটি n = ০ হলেও নিরাপদ।

  3. পয়সা হারানোর শিকার: দ্বিতীয় কোডে (paisa.c) 0.29-এর বদলে একে একে 0.57, 1.15, 4.35 আর 0.50 বসিয়ে চালান। কোনগুলোতে (int)(price * 100) ভুল পয়সা দেয়? কেন সবগুলোতে নয়?

    0.57 → 56, 1.15 → 114, 4.35 → 434 — তিনটিতেই এক পয়সা কম। কারণ এই মানগুলো বাইনারিতে হুবহু রাখা যায় না, এবং রাখা মানটি আসল মানের সামান্য নিচে; ১০০ দিয়ে গুণের পর ফল পূর্ণসংখ্যার ঠিক নিচে থাকে, আর (int) কেটে ফেলে। কিন্তু 0.50 = ১/২, যা বাইনারিতে হুবহু রাখা যায়, তাই 50 ঠিকই আসে। কিছু মান সামান্য ওপরে রাখা হয় — সেগুলোতে কাটলেও ঠিক উত্তর আসে, তাই বাগটি মাঝে মাঝে দেখা দেয়, যা একে আরও বিপজ্জনক করে।

  4. AI অডিট: যেকোনো AI-কে বলুন “একটি C প্রোগ্রাম লেখো যা ৫টি বিষয়ের ক্রেডিট আর গ্রেড পয়েন্ট নিয়ে CGPA বের করবে, আর মোট টিউশন ফি টাকায় দেখাবে।” তারপর এই পর্বের প্রম্পট আর চেকলিস্ট দিয়ে যাচাই করুন। কোন কোন রূপান্তর-সমস্যা পেলেন?

    সাধারণত যা দেখা যায়:

    • ক্রেডিট int, আর মোট ক্রেডিট দিয়ে ভাগ — কোথাও একটি পাশ দশমিক না হলে CGPA পূর্ণসংখ্যা হয়ে যায়।
    • CGPA float-এ রেখে if (cgpa == 3.75) ধরনের তুলনা — সমান হওয়ার কথা হলেও মিথ্যা আসতে পারে।
    • ফি float-এ, আর ছাপার সময় %.2f দিয়ে গরমিল ঢেকে রাখা।
    • -Wconversion দিলে double থেকে float-এ রূপান্তরের warning — AI কোনো কারণ ছাড়াই দুটি টাইপ মিশিয়েছে।

    প্রতিটি ধরার পর AI-কে কারণসহ জানান, এবং দেখুন সে ঠিক কোথায় cast বসায় — ভাগের আগে, নাকি পরে।

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

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

ক্রমchar c = 'A'; থাকলে c + 2.5 হিসাবের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📥 c-এর মান মেমরি থেকে পড়া (৬৫)
  2. ⬆️ Integer promotion — char থেকে int
  3. ⚖️ অন্য পাশে double দেখে সাধারণ টাইপ বাছাই
  4. 🔁 int ৬৫ থেকে double ৬৫.০
  5. ➕ দশমিক যোগ — ফল double ৬৭.৫

int sum = 253; double avg = sum / 3; — avg-এর মান কত?

  • 84.333333
  • 84.0
  • 85.0
  • Compile error

৩২-বিট int-এর একটি সাধারণ কম্পিউটারে -1 < 0u-এর মান কী, আর কেন?

  • 1, কারণ −1 শূন্যের চেয়ে ছোট
  • Compile error
  • 0, কারণ −1 রূপান্তরিত হয়ে 4294967295 হয়
  • Undefined behavior — যা খুশি

char a = 100, b = 100; int s = a + b; — s কত?

  • 200
  • -56
  • 127
  • Undefined behavior

int a = 50000, b = 50000; — গুণফল ঠিকভাবে পেতে কোনটি লিখবেন?

  • long long x = a * b;
  • long long x = (long long)(a * b);
  • unsigned int x = a * b;
  • long long x = (long long)a * b;

(int)-2.7-এর মান কত?

  • -3
  • -2
  • 2
  • -3, কারণ cast কাছের পূর্ণসংখ্যায় গোল করে

একটি মোবাইল ওয়ালেটের ব্যালান্স রাখতে সবচেয়ে নিরাপদ পছন্দ কোনটি?

  • টাকায় float
  • টাকায় double
  • পয়সায় long long
  • পয়সায় int

শব্দকোষ

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

মনে রাখুন

  1. হিসাবের আগে ছোট পূর্ণসংখ্যা int হয় (integer promotion); তারপর rank-এর সিঁড়ি দুই পাশের সাধারণ টাইপ ঠিক করে।
  2. সমান rank-এ unsigned জেতে — তাই -1 < 0u মিথ্যা; size_t আর strlen()-এর সাথে ঋণাত্মক মান মেশাবেন না।
  3. প্রতিটি অপারেটরের টাইপ ঠিক হয় শুধু তার দুই পাশ দেখে — তাই (double)sum / 3 আর (long long)a * b, cast অপারেশনের আগে।
  4. Cast গোল করে না, আর warning চাপা দিলে বাগ যায় না; printf-এ format আর টাইপ মিলতে হবে।
  5. দশমিকে == নয়, সহনশীলতা; টাকা পয়সায় পূর্ণসংখ্যা; আর হিসাবের কোডে -Wall -Wextra -Wconversion.

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

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

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