Learn C Programming

lesson ৫ / ৭ · Variable আর data type

Module ২ · Variable আর data type

Sizeof আর limits.h: আন্দাজ না করে মেশিনকে জিজ্ঞেস করা

Freeপড়া

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

  • Table মুখস্থ না করে sizeof দিয়ে মেশিনকে type-র মাপ জিজ্ঞেস করতে পারবে।
  • <limits.h>-র প্রতিটা integer সীমা আর <float.h>-র প্রতিটা দশমিক সীমা ছাপতে পারবে।
  • মাপটা প্রতিটা মেশিনে এক হতে হলে কোন fixed-width type নিতে হবে বলতে পারবে।

Kenji-র program এক বছর ধরে ওর laptop-এ চলে। তারপর সেটা একটা card reader-র ছোট chip-এ তোলা হয়, আর 32,767-র উপরের প্রতিটা সংখ্যা ভুল বেরোয়।

ও bug লেখেনি। ও একটা মাপ মাথায় লিখে রেখেছিল, আর সেটা নিয়ে এমন একটা মেশিনে গিয়েছিল যে একমত না। এই পুরো lesson একটাই অভ্যাস: জিজ্ঞেস করো, আন্দাজ করো না।

sizeof: যে operator byte-এ উত্তর দেয়

sizeof দেখতে function-র মতো, আর ওটা function না। ওটা ভাষার ভেতরে বসানো একটা operator, আর program চালু হওয়ার আগেই compiler তার উত্তর বের করে ফেলে।

sizeof-র দুই রূপ

sizeof(int)         একটা TYPE-র জন্য bracket লাগবেই
sizeof marks        একটা EXPRESSION-র জন্য লাগে না
sizeof(marks)       এটাও ঠিক, আর বেশির ভাগ মানুষ এটাই লেখে

sizeof marks + 1    মানে (sizeof marks) + 1, যেটা 5
sizeof (marks + 1)  মানে expression-টার মাপ, যেটা 4
  • উত্তরটা byte-র গোনা, আর একটা byte হলো CHAR_BIT bit, যেটা তুমি যেখানেই যাও 8।
  • যত C implementation কোনোদিন ছিল, সবগুলোতেই sizeof(char) সংজ্ঞা অনুযায়ী 1।
  • ভেতরের expression-টা কখনো চলে না। sizeof(x / 0) একটা মাপ, crash না।
  • এর type হলো size_t, আর তার specifier %zu।

তাই type-র জন্য bracket বাধ্যতামূলক আর variable-র জন্য ঐচ্ছিক, আর ওই card-র শেষ দুই লাইন এমন একটা ফাঁদ যেটা দুইবার পড়া দরকার।

size_t, আর তার সঙ্গের %zu

sizeof একটা int ফেরত দেয় না। ও ফেরত দেয় একটা size_t, একটা unsigned type, যেটা মেশিনের রাখতে পারা সবচেয়ে বড় জিনিসের মাপ ধরার মতো চওড়া।

Playground-এ আর যেকোনো 64 bit মেশিনে ওটা 8 byte, আর ছোট একটা chip-এ 2 byte। এটাই ওর মানে: ওকে মেশিন ঠিক করে, কারো বেছে নেওয়া কোনো সংখ্যা না।

Unsigned বলেই size_t কখনো ঋণাত্মক হতে পারে না, আর শূন্য পেরিয়ে বিয়োগ করলে lesson 2-র বর্ণনামতোই ঘুরে যায়।

তাই ওটা %zu দিয়ে ছাপো, আর অঙ্কের জন্য সাধারণ int চাইলে cast-টা নিজে লেখো আর বুঝে লেখো।

limits.h আর float.h: ছাপা, টুকে রাখা না

দুইটা header-এ প্রতিটা সীমার মান থাকে, তুমি যে মেশিনে আছ তার জন্য আগে থেকেই ঠিক করা।

#include <stdio.h>
#include <limits.h>
#include <float.h>

int main(void)
{
    printf("bits in a byte : %d\n", CHAR_BIT);
    printf("int range      : %d to %d\n", INT_MIN, INT_MAX);
    printf("long long top  : %lld\n", LLONG_MAX);
    printf("float digits   : %d\n", FLT_DIG);
    printf("double digits  : %d\n", DBL_DIG);
    printf("double largest : %e\n", DBL_MAX);
    return 0;
}
bits in a byte : 8
int range      : -2147483648 to 2147483647
long long top  : 9223372036854775807
float digits   : 6
double digits  : 15
double largest : 1.797693e+308

FLT_DIG হলো 6 আর DBL_DIG হলো 15। Lesson 3 বলেছিল "প্রায় 7" আর "প্রায় 15 বা 16"; এগুলোই সেই সংখ্যা, যেগুলোর পিছনে মেশিন সত্যিই দাঁড়াবে।

তাই তুমি যে সীমা ছেপেছ সেটা আজকের মেশিন নিয়ে একটা সত্য। মুখস্থ থেকে লেখা সীমা একটা আশা।

long-কেই সন্দেহ করতে হবে

এখানে এমন একটা তথ্য, যেটা তুমি নিজে যাচাই করতে পারো, আর এই পুরো lesson থাকার কারণও এটাই।

Playground-এ, যেটা 64 bit Linux চালায়, sizeof(long) হলো 8। একই processor আর একই compiler-র একটা Windows মেশিনে ওটা 4।

একই code, একই chip, একই GCC। দুইটা উত্তর। কোনোটাতেই কিছু ভুল নেই: standard কেবল কথা দেয় long প্রায় দুইশো কোটির এদিক-ওদিক পর্যন্ত পৌঁছাবে, আর দুইটা মেশিনই সেই কথা রাখে।

এই module-র বাকি প্রতিটা মাপ দুই জায়গাতেই মেলে: char 1, short 2, int 4, long long 8, float 4, double 8।

তাই এই track long long লেখে আর কখনো long না, আর নিচের অনুশীলনটা তোমাকে এমন একটা মাপের তালিকা ছাপতে বলে যেখানে long ইচ্ছে করেই নেই।

Byte-এ type-র মাপ, আর যেটা বদলায় Playground-এ sizeof যা উত্তর দেয়, byte-এ char 1 short 2 int 4 float 4 long long 8 double 8 size_t 8 long Windows-এ 4, Linux-এ 8 সাতটা মাপে ভরসা করা যায়, একটায় যায় না। ধরে নেওয়ার আগে মেশিনকে জিজ্ঞেস করো।
ছবি 1। এখানকার প্রতিটা দণ্ড সত্যিকারের একটা sizeof থেকে এসেছে। শেষ দণ্ডের ফুটকি দেওয়া অর্ধেকটাই এই track-র long long লেখার কারণ।

stdint.h: মাপটা সব জায়গায় এক হতে হলে

মাঝে মাঝে "অন্তত 4 byte" যথেষ্ট না। একটা network packet, একটা file format আর একটা hash-র এমন একটা মান লাগে যেটা প্রতিটা মেশিনে, চিরকাল, ঠিক 32 bit চওড়া।

<stdint.h> তোমাকে সেই type-গুলো দেয়, আর তাদের নামেই চওড়াটা লেখা থাকে।

Typeচওড়াসীমাকীসের জন্য
int8_t8 bit-128 থেকে 127ঠাসা structure-এ ছোট একটা signed মান
uint8_t8 bit0 থেকে 255কাঁচা একটা byte, রঙের একটা ভাগ
int16_t16 bit-32,768 থেকে 32,767একটা audio sample
uint16_t16 bit0 থেকে 65,535একটা port number
int32_t32 bitপ্রায় দুইশো কোটির এদিক-ওদিকসাধারণ গোনার ঘর, যার মাপ বদলানো চলবে না
uint32_t32 bit0 থেকে 4,294,967,295একটা IPv4 address, একটা checksum
int64_t64 bitপ্রায় 9.2 কুইন্টিলিয়নের এদিক-ওদিকএকটা timestamp, একটা file-র মাপ

এগুলো নতুন type না। এগুলো সাধারণ type-গুলোরই অন্য নাম, প্রতিটা মেশিনে এমনভাবে বাছা যাতে চওড়াটা ঠিক থাকে। Playground-এ int32_t হলো int।

এগুলো ছাপতে একটু সাবধানতা লাগে, কারণ নিচের আসল type মেশিনভেদে আলাদা। এই module-র ভেতরে একটা fixed-width মান long long আর %lld দিয়ে পড়ো আর ছাপো, যেটা সবগুলোর জন্যই যথেষ্ট চওড়া।

তাই সাধারণ গোনার জন্য সাধারণ int নাও, আর bit-র সংখ্যাটা যখন নকশারই অংশ তখন একটা fixed-width type নাও।

Example 1: সবচেয়ে ছোট যে প্রশ্নটা করা যায়

sizeof-র দুই রূপ, একটা type-র উপর আর একটা variable-র উপর, আর শেষ লাইনে অগ্রাধিকারের ফাঁদ।

#include <stdio.h>

int main(void)
{
    int marks = 90;

    printf("sizeof(int)        : %zu\n", sizeof(int));
    printf("sizeof marks       : %zu\n", sizeof marks);
    printf("sizeof marks + 1   : %zu\n", sizeof marks + 1);
    printf("sizeof (marks + 1) : %zu\n", sizeof (marks + 1));
    return 0;
}
sizeof(int)        : 4
sizeof marks       : 4
sizeof marks + 1   : 5
sizeof (marks + 1) : 4

তৃতীয় লাইনটা 5, কারণ sizeof +-র চেয়ে শক্ত করে ধরে। ও মাপটা চেয়েছে, 4 পেয়েছে, তারপর তাতে 1 যোগ করেছে। Bracket দিলে প্রশ্নটাই থাকে না।

Run in Compiler
Example 2: মাপগুলো, মুখস্থ না করে জিজ্ঞেস করে

এই module-র প্রতিটা মাপ, মেশিনের ছাপা। long ইচ্ছে করেই বাদ, আর শেষ লাইনটা কারণটা বলে।

#include <stdio.h>

int main(void)
{
    printf("char      : %zu\n", sizeof(char));
    printf("short     : %zu\n", sizeof(short));
    printf("int       : %zu\n", sizeof(int));
    printf("long long : %zu\n", sizeof(long long));
    printf("float     : %zu\n", sizeof(float));
    printf("double    : %zu\n", sizeof(double));
    printf("size_t    : %zu\n", sizeof(size_t));
    printf("long      : ask your own machine\n");
    return 0;
}
char      : 1
short     : 2
int       : 4
long long : 8
float     : 4
double    : 8
size_t    : 8
long      : ask your own machine

ওই সাতটা সংখ্যা Playground-এ আর একটা Windows laptop-এ এক। long-র জন্য একটা লাইন যোগ করো, আর দুইটা মেশিন আর একমত থাকে না।

Run in Compiler
Example 3: Fixed-width type, beginner আসলে যেটা লেখে

ছোট একটা packet-র header: একটা version byte, একটা port আর একটা timestamp। প্রতিটা ঘরের চওড়া নকশারই অংশ।

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

int main(void)
{
    uint8_t  version = 4;
    uint16_t port = 443;
    int64_t  timestamp = 1758240000;

    printf("version   : %lld in %zu byte\n", (long long)version, sizeof version);
    printf("port      : %lld in %zu bytes\n", (long long)port, sizeof port);
    printf("timestamp : %lld in %zu bytes\n", (long long)timestamp, sizeof timestamp);
    return 0;
}
version   : 4 in 1 byte
port      : 443 in 2 bytes
timestamp : 1758240000 in 8 bytes

ছাপার জন্য প্রতিটা মান long long-এ চওড়া করা হয়েছে, যেটা সবসময় কাজ করে আর কখনো চমকে দেয় না। <inttypes.h>-র সঠিক specifier-গুলো Module 15-র বিষয়।

Run in Compiler

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

  • TCP-র header। উৎস আর গন্তব্যের port হলো uint16_t, sequence number হলো uint32_t। ওই চওড়াগুলো গোটা internet যে standard মানে তাতেই লেখা, তাই "আরও চওড়া হতেও পারে" এমন একটা C type সেখানে অকেজো।
  • PNG file format। প্রতিটা chunk শুরু হয় 32 bit big-endian একটা দৈর্ঘ্য দিয়ে। প্রতিটা ভাষার প্রতিটা PNG reader ওই ঘরটাকে ঠিক 32 bit হিসেবেই declare করে, আর libpng সেটা করে <stdint.h>-র নাম দিয়ে।
  • SHA-256। পুরো hash-টা সংজ্ঞায়িত 32 bit unsigned গণিতে, ঘুরে যাওয়া সহ। সাধারণ int দিয়ে লিখলে একটা 16 bit chip-এ অন্য উত্তর আসত, তাই প্রতিটা implementation uint32_t ব্যবহার করে।
  • Embedded firmware। একটা 8 bit microcontroller-এ int 2 byte, তাই laptop-এ চলা একই source সেখানে 32,767-এ overflow করে। Firmware-র code int16_t আর int32_t ব্যবহার করে ঠিক এটাকে চমক হতে না দেওয়ার জন্য।

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

১. sizeof-র ফল %d দিয়ে ছাপা।

printf("%d\n", sizeof(int));

Playground-এ নীরব, কারণ format-র পরীক্ষাটার জন্য -Wall লাগে। ও সাধারণত 4 ছাপে আর তাতেও ভুলই থাকে। নিজের মেশিনে gcc -Wall বলে warning: format '%d' expects argument of type 'int', but argument 2 has type ..., আর ওই message-র শেষটায় Linux আর Windows-এ আলাদা type-র নাম আসে, যেটা এক লাইনেই পুরো lesson-টা। লেখো %zu।

২. Header ছাড়াই একটা সীমার দিকে হাত বাড়ানো।

#include <stdio.h>

int main(void)
{
    printf("%d\n", INT_MAX);
    return 0;
}

GCC 12 বলে error: 'INT_MAX' undeclared (first use in this function) আর তারপর note: 'INT_MAX' is defined in header '<limits.h>'। Note-টা তোমার উপকারই করছে। প্রতিটা note পড়ো, কেবল তার উপরের error না।

৩. Bracket ছাড়া sizeof-এ যোগ করা।

int marks = 90;
printf("%zu\n", sizeof marks + 1);

কোনো message নেই, আর ছাপে 5। তুমি expression-টার মাপ চেয়েছিলে আর পেয়েছ variable-টার মাপ, আরও একটা বেশি। যেটা বোঝাতে চাও সেটা হলে লেখো sizeof(marks + 1)।

৪. মাপটা জিজ্ঞেস না করে লিখে রাখা।

long total_bytes = 8;      /* "a long is 8 bytes" */

কোনো message নেই, আর Playground-এ এটা ঠিক আর Windows-এ ভুল। Bug-টা comment-টাই: ও একটা আন্দাজকে তথ্য হিসেবে লিখে রাখছে। sizeof(long) লেখো, আর program-টার আর ওই comment-র দরকারই থাকে না।

মাথা খাটাও

Zara একটা int-এ কতগুলো bit আছে জানতে চায়, তাই এটা লেখে।

#include <stdio.h>
#include <limits.h>

int main(void)
{
    printf("bits: %zu\n", sizeof(int) * 8);
    printf("bits: %zu\n", sizeof(int) * CHAR_BIT);
    return 0;
}

Playground-এ দুইটা লাইনই 32 ছাপে। তবু দ্বিতীয় লাইনটা কেন ভালো, বলো, আর এমন একটা মেশিনের নাম বলো যেখানে দুইটা মিলত না। তারপর কঠিন অংশ: দ্বিতীয় লাইনটাও একটা int-র মান রাখার bit-র সংখ্যা না। ওই byte-গুলোর ভেতরে আর কী থাকতে পারে বলো।

একটা byte নিয়ে C standard আসলে কী কথা দেয় দেখে নাও। তারপর জিজ্ঞেস করো, একটা type-র প্রতিটা bit-কেই কি মানের একটা অংশ বইতে হবে।

অনুশীলন ১সহজ

এই module যে ছয়টা type ব্যবহার করেছে তাদের মাপ একটা নির্দিষ্ট চেহারায় ছাপো, মুখস্থ থেকে কিছু না লিখে।

Input. নেই।

Output. ছয় লাইন, এই ক্রমে: char, short, int, long long, float, double। প্রতিটা লাইনে type-র নাম, একটা কোলন, একটা space, আর byte-এ মাপ।

Constraints. প্রতিটা সংখ্যা sizeof থেকে আসবে। long লিখো না, আর কেন লিখলে না সেটা বলতে তৈরি থেকো।

Sample. প্রথম লাইনটা char: 1।

#include <stdio.h>

int main(void)
{
    printf("char: %zu\n", sizeof(char));

    /* Five more lines, same shape. */

    return 0;
}

এই module-এ judge করা হয় না। এর একটা judge করা রূপ ওই মুহূর্তেই ভেঙে পড়ত যখন কেউ এমন মেশিনে চালাত যেখানে long 4 byte, আর ওটাই type-টা বাদ থাকার কারণ।

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

Amara হিসাব করছে reading-র একটা table-র জন্য কত memory লাগবে, চাওয়ার আগেই।

Input. এক লাইনে একটা পূর্ণসংখ্যা n, reading-র সংখ্যা।

Output. এক লাইনে কত byte লাগবে, যদি double ধরনের n-টা মান রাখতে হয়।

Constraints. 0 <= n <= 1000000000। double-র মাপ sizeof থেকে নাও, নিজের লেখা কোনো সংখ্যা থেকে না।

Sample. Input 10 দিলে 80।

#include <stdio.h>

int main(void)
{
    long long n = 0;
    scanf("%lld", &n);

    /* Multiply by sizeof(double), print with %lld. */

    return 0;
}

bytes-for-doubles নামে judge করা হয়। সবচেয়ে বড় n-র উত্তরটা দুইশো কোটি ছাড়িয়ে যায়, তাই গুণটা একটা চওড়া বাক্সে হতে হবে।

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

Zara একটা file format-র কলাম নকশা করছে আর তার দরকার সবচেয়ে সরু signed fixed-width type, যেটা কলামের প্রতিটা মান ধরে।

Input. এক লাইনে দুইটা পূর্ণসংখ্যা lo hi, কলামের সবচেয়ে ছোট আর সবচেয়ে বড় মান।

Output. এক লাইনে bit-এ প্রস্থ: 8, 16, 32 বা 64। এটা int8_t, int16_t, int32_t আর int64_t-র মধ্যে সবচেয়ে ছোটটাকে বোঝায়, যেটা পুরো সীমা ধরে।

Constraints. -9000000000000000000 <= lo <= hi <= 9000000000000000000।

Sample. Input 0 100 দিলে 8। Input -200 300 দিলে 16। Input 0 5000000000 দিলে 64।

#include <stdio.h>

int main(void)
{
    long long lo = 0;
    long long hi = 0;
    scanf("%lld %lld", &lo, &hi);

    /* Lesson 4: a comparison is a 1 or a 0, and two of them multiplied is "both". */

    return 0;
}

type-fits নামে judge করা হয়। কোনো শর্ত লাগবে না, loop-ও না। আগে বের করো কয়টা সরু type ধরে, তারপর ওই গোনাটাকে একটা প্রস্থে বদলাও।

Run in Compiler

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

  • sizeof-র কি চলার সময় কোনো খরচ আছে?

    নেই। সাধারণ একটা type-র জন্য compiler ওটাকে compile করার সময়ই একটা সংখ্যা দিয়ে বদলে দেয়, তাই চলন্ত program কাউকে কিছু জিজ্ঞেসই করে না।

  • %zu এত দৃষ্টিকটু কেন?

    z মানে "একটা size_t-র দৈর্ঘ্য" আর u মানে unsigned। এটা C99-তে যোগ হয়েছিল ঠিক এই কারণেই যে size_t-র নির্দিষ্ট চওড়া নেই, তাই আগের কোনো specifier সব জায়গায় ঠিক হতে পারত না।

  • সব জায়গায় int-র বদলে int32_t নেব?

    না। সাধারণ গোনার জন্য int নাও, আর চওড়াটা যখন একটা file format, protocol বা hardware register-র অংশ তখন fixed-width type নাও।

  • একটা নামে কাজের অক্ষর কয়টা, sizeof কি সেটাই?

    না, আর ওই গোলমালটা আসে Module 10-এ। Character-র একটা array-তে sizeof বাক্স গোনে; strlen শেষ চিহ্নের আগের অক্ষরগুলো গোনে।

  • একটা byte কি কখনো 8 bit না হতে পারে?

    পারে, কিছু digital signal processor-এ, যেখানে CHAR_BIT 16 বা 32। তুমি সম্ভবত কখনোই একটার দেখা পাবে না, আর 8-র বদলে CHAR_BIT লিখতে কোনো খরচ নেই।

মূল কথা

  • sizeof একটা operator, উত্তর দেয় compile-র সময়, byte-এ, আর ভেতরের expression কখনো চালায় না।
  • Type-র জন্য bracket লাগে, variable-র জন্য লাগে না, আর sizeof x + 1 দেখতে যা মনে হয় তা না।
  • এর type size_t, ছাপে %zu দিয়ে, unsigned আর মেশিনের মাপে।
  • <limits.h> আর <float.h>-এ প্রতিটা সীমার মান আছে, তোমার মেশিনের জন্য ঠিক করা।
  • long Playground-এ 8 byte আর Windows-এ 4; এই module-র বাকি প্রতিটা মাপ মেলে।
  • Protocol, file format আর hardware-র জন্য <stdint.h> নির্দিষ্ট চওড়া দেয়।

পরেরটা Problems lesson: এই module-র সবকিছুর উপরে দশটা judge করা সমস্যা, লুকানো test-র বিরুদ্ধে।

lesson ৫ শেষ

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

পরেরটা: Problem: variable আর type