Learn C Programming

lesson ৬ / ৮ · Operator আর type কনভার্শন

Module ৪ · Operator আর type কনভার্শন

Type conversion: promotion, casting আর চুপচাপ হারিয়ে যাওয়া মান

Freeপড়া

এই lesson-এ যা শিখবে

  • একটা operator-র দুই পাশের type আলাদা হলে ও কোন type-এ কাজ করবে সেটা আগেই বলতে পারবে।
  • (type) দিয়ে cast লিখতে পারবে আর তার দুইটা সৎ ব্যবহারের প্রতিটা কীসের জন্য বলতে পারবে।
  • তিনটা নীরব ক্ষতির নাম বলতে পারবে: truncation, narrowing আর চিহ্ন হারানো।

Maria-র একটা খারাপ import-র পরে stock-র গোনা -1, আর তার একটা পরীক্ষা দেখে গোনাটা 1u সীমার নিচে কি না।

ও লেখে count < limit। উত্তর আসে মিথ্যা। Program বলছে, মাইনাস এক এক-র চেয়ে ছোট না।

কিছুই নষ্ট হয়নি। সীমার গায়ের ওই u-টা বদলে দিয়েছে তুলনাটা কোন type-এ হবে, আর ওই বদলে -1 টেকে না।

C কাজ করার আগে রূপান্তর করে, সব সময়

C-র প্রায় প্রতিটা operator-র দুই পাশে একই type লাগে। না মিললে C আগে একটাকে বদলে নেয়।

এটা তুমি না চাইতেই ঘটে আর কোনো বার্তা ছাড়াই ঘটে। এটার নাম implicit conversion, আর এই lesson-র বিষয় ওটাই।

দুই সেট নিয়ম কাজটা করে: integer promotion, যেটা ছোট type-গুলোর উপর ঘটে, আর usual arithmetic conversions, যেটা বাকিটা মেটায়।

Integer promotion: ছোট type-র উপরে আদৌ কাজ হয় না

একটা char বা short কখনোই সেই type না যেটায় arithmetic operator কাজ করে। দুইটাকেই আগে int-এ তোলা হয়।

#include <stdio.h>

int main(void)
{
    char small = 'A';
    short shorter = 300;

    printf("a char is       %zu byte\n", sizeof(small));
    printf("a short is      %zu bytes\n", sizeof(shorter));
    printf("char + char is  %zu bytes\n", sizeof(small + small));
    printf("short + short is %zu bytes\n", sizeof(shorter + shorter));
    return 0;
}
a char is       1 byte
a short is      2 bytes
char + char is  4 bytes
short + short is 4 bytes

এক byte-র দুইটা মান যোগ হলো আর উত্তরটা চার byte চওড়া। + যখন ওদের দেখল, তখন কোনোটাই আর char ছিল না।

এটা ভালো খবর। এই জন্যই 'A' + 1 উপচে পড়তে পারে না, আর এই জন্যই Module 2-র character নিয়ে অঙ্ক বুদ্ধিমানের মতো চলেছিল।

Usual arithmetic conversions, একটা মই হিসেবে

দুই পাশ অন্তত int হয়ে গেলে C এই মইয়ে দুইটার উঁচুটা বাছে আর অন্যটাকে তুলে ওখানে নিয়ে যায়।

রূপান্তরের মই: নিচের type-টাকে উপরেরটায় তোলা হয় নিচের মানটা তোলা হয়; উপরেরটা কখনো নড়ে না উপরে long double double float unsigned long long long long unsigned int int char আর short পৌঁছানোর আগেই int-এ তোলা হয় 1 + 2.5 হয় double-এ -1 < 1u হয় unsigned int-এ আর ওখানেই মাইনাস চিহ্নটা হারায়

প্রথম ধাপটা মেনে নেওয়া সহজ: একটা int-র সাথে একটা double মেশালে double পাও। Lesson 1-র 3.0 এই নিয়মটাই ছিল।

int আর unsigned int-র মাঝের ধাপটাই মানুষের টাকা খরচ করায়।

Signed-র সাথে unsigned-র দেখা, আর চিহ্নটা হারে

int আর unsigned int একই চওড়া। ওদের দেখা হলে C signed-টাকে unsigned-এ বদলে নেয়।

-1-কে unsigned int-এ বদলালে কোনো error বা শূন্য আসে না। আসে 4294967295, ওই type-র ধরতে পারা সবচেয়ে বড় মান।

#include <stdio.h>

int main(void)
{
    int count = -1;

    printf("as unsigned  %u\n", (unsigned int)count);
    printf("count < 1u   %d\n", count < 1u);
    printf("count < 1    %d\n", count < 1);
    return 0;
}
as unsigned  4294967295
count < 1u   0
count < 1    1

মাঝের লাইনটাই Maria-র bug। তুলনাটা ঠিক অঙ্কই করেছে, কিন্তু ভুল জোড়া মানের উপর।

Playground কিছুই বলে না। স্থানীয় gcc -Wall বলে warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int'।

তাই সমাধান হলো দুই পাশই signed রাখা, নয়তো unsigned পাশটাকে ইচ্ছে করে নামিয়ে আনা, যখন তুমি জানো ওটা যথেষ্ট ছোট।

Cast, আর তার দুইটা সৎ ব্যবহার

Cast মানে রূপান্তরটা তুমি নিজে, লিখে করছ। ওটা lesson 5-র table-র 2 নম্বর সারি, তাই ওটা যেকোনো অঙ্কের চেয়ে শক্ত করে ধরে।

Cast operator

(type)value

(double)total / count      একটা পাশকে double করো, তারপর ভাগ করো
(int)price                 ইচ্ছে করে double-র পূর্ণ অংশটা রাখো
(unsigned int)n            signed একটা মানকে unsigned হিসেবে পড়ো
  • ওটা ডান পাশের একটামাত্র মানের উপর খাটে, পুরো expression-র উপর না। (double)total / count শুধু total-কেই বদলায়।
  • ওটুকুই যথেষ্ট: এক পাশ double হয়ে গেলে মই অন্য পাশটাকেও তুলে আনে।
  • (double)(total / count) আলাদা জিনিস, আর ওটাই bug, সমাধান না।

দুইটা ব্যবহার সৎ। প্রথমটা হলো আসল ভাগে বাধ্য করা, যেটা lesson 1 একটা 3.0 দিয়ে করেছিল আর ভাজক variable হলে করতে পারত না।

দ্বিতীয়টা এমন একটা narrowing যেটা তুমি চাও আর লিখে রাখতে রাজি। (int)price বলে, "আমি জানি এতে ভগ্নাংশ হারাবে, আর আমি সেটাই চাই"।

#include <stdio.h>

int main(void)
{
    int total = 239;
    int count = 3;
    double price = 99.87;

    printf("no cast      %d\n", total / count);
    printf("cast left    %.2f\n", (double)total / count);
    printf("cast outside %.2f\n", (double)(total / count));
    printf("to int       %d\n", (int)price);
    return 0;
}
no cast      79
cast left    79.67
cast outside 79.00
to int       99

তৃতীয় লাইনটাই সেই ভুল যেটা সমাধানের মতো দেখায়। Cast-র কাছে পৌঁছানোর আগেই ভাগটা integer division হিসেবে শেষ হয়ে গেছে।

তৃতীয় আরেকটা ব্যবহার, না বুঝে compiler-র warning চুপ করানো, সৎ না, আর এই track ওটা করে না।

তিনটা নীরব ক্ষতি, আর প্রতিটা কোথায় ঘটে

নিচের তিনটার একটাও Playground-র command line-এ কোনো বার্তা দেয় না। প্রতিটাই type-র ফাঁক দিয়ে একটা মান বেরিয়ে যাওয়া।

  • Truncation. একটা double int-এ রাখলে ভগ্নাংশ হারায়, আর ওটা round না করে শূন্যের দিকে কাটে। 99.87 হয়ে যায় 99।
  • Narrowing. চওড়া একটা পূর্ণসংখ্যা সরু একটায় রাখলে শুধু যতটুকু আঁটে ততটুকু bit থাকে। Signed type-এ মানটা না আঁটলে ফলটা implementation defined, আর GCC মুড়িয়ে দেয়: একটা char-এ 300 হয়ে যায় 44।
  • চিহ্ন হারানো. ঋণাত্মক একটা মান unsigned-এ বদলালে বড় একটা ধনাত্মক মান হয়ে যায়। এটা standard-এ পুরোপুরি নির্ধারিত, আর তাতে ব্যাপারটা আরও খারাপ: কিছুই ভুল না, তাই কেউ নালিশও করতে পারে না।

স্থানীয় gcc -Wconversion প্রথম দুইটা ধরে: warning: conversion from 'double' to 'int' may change value। ওই flag -Wall-এ নেই, আর বেশির ভাগ project ওটা কখনো চালু করে না।

তাই রক্ষাটা কোনো compiler flag না। রক্ষাটা হলো মানটা যেখানে জন্মায় সেখানেই তার type বেছে নেওয়া, আর Module 2-র পুরো যুক্তিটাই ছিল সেটা।

Example 1: সবচেয়ে ছোট cast যেটা গুরুত্বপূর্ণ

দুইটা পূর্ণসংখ্যা ঢোকে, একটা আসল গড় বেরোয়।

#include <stdio.h>

int main(void)
{
    int total = 0;
    int count = 0;
    scanf("%d %d", &total, &count);

    printf("%.2f\n", (double)total / count);
    return 0;
}
79.67

ওই output-টা 239 3 input-র জন্য। বাঁ পাশের মানে একটা cast, আর বাকিটা মই করে দেয়।

Run in Compiler
Example 2: যে তুলনাটা সত্য ছিল আর তারপর ছিল না

একই মানকে একবার signed সীমার সাথে, একবার unsigned সীমার সাথে মেলানো।

#include <stdio.h>

int main(void)
{
    int count = 0;
    scanf("%d", &count);

    printf("against 1   %d\n", count < 1);
    printf("against 1u  %d\n", count < 1u);
    printf("cast fixes  %d\n", count < (int)1u);
    return 0;
}
against 1   1
against 1u  0
cast fixes  1

ওই output-টা -1 input-র জন্য। 0 দিলে তিন লাইনই মিলে যায়, আর এই জন্যই অসতর্ক একটা পরীক্ষায় এটা ধরা পড়ে না।

Run in Compiler
Example 3: ইচ্ছে করে করা একটা narrowing, আর সেটা লিখে রাখা

টাকা-পয়সার একটা দাম, একটা রসিদের দরকারি দুই পূর্ণসংখ্যায় ভাঙা।

#include <stdio.h>

int main(void)
{
    double price = 0.0;
    scanf("%lf", &price);

    int taka = (int)price;
    int paisa = (int)(price * 100) - taka * 100;

    printf("%d taka %d paisa\n", taka, paisa);
    return 0;
}
99 taka 87 paisa

ওই output-টা 99.87 input-র জন্য। দুইটা cast-ই ইচ্ছাকৃত আর দুইটাই লেখা আছে।

এটা ভঙ্গুরও, আর Module 2-র lesson 3 কারণটা বলেছে: কিছু দাম গুণ করলে তুমি যে পূর্ণ সংখ্যাটা আশা করেছিলে তার সামান্য নিচে নামে। আসল একটা দোকানের খাতা শুরু থেকেই পয়সায় গোনে আর দশমিক দাম কখনো রাখেই না।

Run in Compiler

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

  • প্রতিটা report-র প্রতিটা গড়। যে dashboard 79.67-র জায়গায় 79 দেখায়, তার একটা cast বাদ পড়েছে, আর নতুন code-এ এটাই সবচেয়ে চেনা সংখ্যার bug।
  • size_t দিয়ে index করা। sizeof আর C library-র প্রতিটা দৈর্ঘ্য unsigned। Signed একটা index নিয়ে শূন্যের নিচে নামা আর ওগুলোর একটার সাথে তুলনা করা loop-টাই Maria-র bug, Module 9-এ।
  • Byte স্তরের code। একটা network বা file parser unsigned char পড়ে আর int-এ কাজ করে, integer promotion-র কারণে। ওই জোড়াটা ভুল হলেই parser ঋণাত্মক দৈর্ঘ্য পড়তে শুরু করে।
  • Ariane 5, 1996। Module 2-র lesson 2 এই গল্পটা বলেছিল: 64 bit-র একটা দশমিক মান 16 bit-র signed একটা পূর্ণসংখ্যায় ঢোকানো হয়েছিল, যেটা ওটা ধরতে পারত না। উৎক্ষেপণের 37 সেকেন্ড পরে রকেটটা ধ্বংস করা হয়।

যে ভুলগুলো সবাই করে

১. Signed একটা মানকে unsigned একটার সাথে মেলানো।

int count = -1;
printf("%d\n", count < 1u);

Playground-এ নীরব; স্থানীয় gcc -Wall বলে warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int'। এটা 0 ছাপে। দুই পাশই signed রাখো।

২. মানের বদলে উত্তরটাকে cast করা।

int total = 239;
printf("%.2f\n", (double)(total / 3));

কোনো command line-এই বার্তা নেই, আর ছাপে 79.00। ভাগটা শেষ হওয়ার পরে cast-টা এসেছে। (double)total / 3 লেখো।

৩. একটা double-কে int-এ রেখে round আশা করা।

double price = 99.87;
int whole = price;
printf("%d\n", whole);

দুইটা command line-এই নীরব; স্থানীয় gcc -Wconversion বলে warning: conversion from 'double' to 'int' may change value। এটা 99 ছাপে, 100 না। Round করতে <math.h>-র round নাও।

৪. যে মান আঁটে না সেটা সরু একটা type-এ রাখা।

int big = 300;
char narrow = (char)big;
printf("%d\n", narrow);

কোনো command line-এই বার্তা নেই, কারণ cast-টা বলছে তুমি ইচ্ছে করেই করেছ। Playground-এ এটা 44 ছাপে, আর ওই ফলটা নিয়ম না, implementation defined। সরু করার আগে সীমাটা মিলিয়ে নাও।

মাথা খাটাও

Zara একটা sensor থেকে পাওয়া byte-র মান জমা রাখে আর ওটা সংখ্যা হিসেবে ছাপে।

#include <stdio.h>

int main(void)
{
    char c = 200;

    printf("%d\n", c);
    return 0;
}

Playground-এ এটা ছাপে -56। ওই সংখ্যাটা কোন দুইটা আলাদা ধাপে তৈরি হলো বোঝাও, আর প্রতিটা ধাপ কোন type-এ হলো বলো। তারপর কঠিন অংশটা: এই ফলের সৎ নাম "implementation defined", "undefined" না। তফাতটা কী বলো, আর program-র কোন এক লাইন বদলালে প্রতিটা মেশিনে মানটা 200 হবে সেটা বলো।

Module 2-র lesson 4 বলেছিল সাধারণ char signed-ও হতে পারে unsigned-ও। 200 বিয়োগ 256 বের করো, তারপর জিজ্ঞেস করো ওই বিয়োগটাই কেন ঘটল।

অনুশীলন ১সহজ

David পাঁচটা পূর্ণ মার্কের গড় চায়, বাস্তব সংখ্যা হিসেবে ছাপা।

Input. এক লাইনে পাঁচটা পূর্ণসংখ্যা।

Output. এক লাইনে তাদের গড়, ঠিক দুই ঘর দশমিক পর্যন্ত।

Constraints. প্রতিটা মার্ক 0 আর 100-র মধ্যে।

Sample. Input 75 82 82 90 61 দিলে 78.00।

#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 0;
    int c = 0;
    int d = 0;
    int e = 0;
    scanf("%d %d %d %d %d", &a, &b, &c, &d, &e);

    /* Add as integers. Then one cast, on one operand of the division. */

    return 0;
}

এই module-এ গ্রেড হয় না। একবার ইচ্ছে করে cast-টা ভাগের বাইরে বসাও, ভুল উত্তরটা দেখার জন্য।

Run in Compiler
অনুশীলন ২মাঝারি

Amara মিলিয়ে দেখছে ছাপার আগেই ও একটা expression-র type বলে দিতে পারে কি না।

Input. এক লাইনে দুইটা পূর্ণসংখ্যা n আর m।

Output. তিন লাইন: n / m %d দিয়ে, তারপর n / (double)m %.2f দিয়ে, তারপর n * 1.0 / m %.2f দিয়ে।

Constraints. -1000 <= n <= 1000, আর 1 <= m <= 1000।

Sample. Input 239 3 দিলে 79, তারপর 79.67, তারপর 79.67।

#include <stdio.h>

int main(void)
{
    int n = 0;
    int m = 0;
    scanf("%d %d", &n, &m);

    /* Three lines. The last two reach the same answer by different routes. */

    return 0;
}

এই module-এ গ্রেড হয় না। চালানোর আগে মুখে বলো তিনটা ভাগের প্রতিটা কোন type-এ হলো।

Run in Compiler
অনুশীলন ৩মাঝারি

Kenji একটা রসিদের লাইন লিখছে আর দশমিক একটা দাম দুই পূর্ণসংখ্যা হিসেবে চায়।

Input. এক লাইনে একটা দশমিক মান, দশমিকের পরে বড়জোর দুই অঙ্ক।

Output. এক লাইনে পূর্ণ অংশ, একটা space, তারপর দুই অঙ্কের ভগ্নাংশটা পূর্ণসংখ্যা হিসেবে।

Constraints. 0 <= দাম <= 100000, দশমিকের পরে বড়জোর দুই ঘর।

Sample. Input 99.87 দিলে 99 87।

#include <stdio.h>

int main(void)
{
    double price = 0.0;
    scanf("%lf", &price);

    /* Two deliberate casts, and one subtraction between them. */

    return 0;
}

এই module-এ গ্রেড হয় না। দ্বিতীয় cast-র আগে 0.5 যোগ করো আর বলো তাতে কোন input-গুলোর উত্তর বদলায়।

Run in Compiler
অনুশীলন ৪কঠিন

Maria-র import-র পরে কিছু গোনা ঋণাত্মক হয়ে গেছে, আর সীমার পরীক্ষাটাকে সেটা সামলাতে হবে।

Input. এক লাইনে দুইটা পূর্ণসংখ্যা: signed একটা count আর unsigned হিসেবে দেওয়া একটা সীমা।

Output. দুই লাইন: সীমাটা unsigned রেখে count < limit তুলনার মান, তারপর signed অঙ্কে ঠিকভাবে করা তুলনার মান।

Constraints. -1000000 <= count <= 1000000, আর 0 <= limit <= 1000000।

Sample. Input -1 1 দিলে 0, তারপর 1।

#include <stdio.h>

int main(void)
{
    int count = 0;
    unsigned int limit = 0;
    scanf("%d %u", &count, &limit);

    /* First line: no cast. Second line: one cast on the limit. */

    return 0;
}

unsigned-fix নামে গ্রেড হয়। ঋণাত্মক নয় এমন প্রতিটা count-এ দুই লাইনই মেলে, তাই লুকানো test-গুলো ওখানেই যেখানে চিহ্নটা বদলায়।

Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • Cast কি variable-টাকে বদলে দেয়?

    না। ওটা শুধু ওই একটা expression-র জন্য বদলানো একটা মান বানায়। Variable-এ যা ছিল তাই থাকে।

  • (double)total / count-এ একটাই cast লাগে কেন?

    কারণ বাকিটা মই করে দেয়। এক পাশ double হয়ে গেলে C ভাগ করার আগে অন্য পাশটাকেও তুলে মিলিয়ে নেয়।

  • তাহলে কি unsigned ব্যবহার করাই খারাপ?

    না, bit-র জন্য আর ঋণাত্মক হতে পারে না এমন গোনার জন্য ওটাই ঠিক type। বিপদটা কেবল ওই সীমানায়, যেখানে ওর সাথে signed একটা মানের দেখা হয়।

  • কাটার বদলে round করব কীভাবে?

    <math.h>-র round নাও, তারপর cast করো। Cast-র আগে 0.5 যোগ করা কেবল ঋণাত্মক নয় এমন মানে চলে, আর ওই আধা-উত্তরটা চেনা একটা bug।

  • -Wconversion চালু করা উচিত?

    নতুন একটা project-এ চেষ্টা করে দেখো। ওটা এত লাইনের নালিশ করে যেগুলো আসলে ঠিক, যে কারণে ওটা -Wall-এ নেই আর বেশির ভাগ দল ওটা বন্ধ রাখে।

মূল কথা

  • প্রায় প্রতিটা operator-র আগে C দুই পাশকে একটা সাধারণ type-এ বদলে নেয়।
  • char আর short-কে int-এ তোলা হয়, তাই ওদের ভিতরে কখনো অঙ্ক হয় না।
  • মই নিচের মানটাকে তোলে; int-র পাশে unsigned int থাকলে ফল unsigned।
  • ঋণাত্মক মান unsigned-এ বদলে খুব বড় ধনাত্মক হয়ে যায়, নীরবে আর বৈধভাবে।
  • Cast ডান পাশের একটামাত্র মানে খাটে, তাই ওটা বসে মানের গায়ে, উত্তরের গায়ে না।
  • Truncation, narrowing আর চিহ্ন হারানো, -Wconversion ছাড়া তিনটাই নীরব।

এরপর আসছে module-র গ্রেড হওয়া সেট, যেখানে এই ছয়টা lesson দশটা problem আর একটা judge-র মুখোমুখি হয়।

lesson ৬ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: Problem: যে expression কামড় দেয়