Module ৩ · কনস্ট্যান্ট, qualifier, ইনপুট আর আউটপুট
Const আর volatile: compiler-কে দেওয়া প্রতিশ্রুতি
এই lesson-এ যা শিখবে
- একটা মানে
constবসাতে পারবে, আর compiler তারপর ঠিক কী করতে মানা করে সেটা বলতে পারবে। - এক বাক্যে
volatileবোঝাতে পারবে, আর কোন ধরনের program-র ওটা লাগে সেটা চিনতে পারবে। static,externআরregister-কে storage class হিসেবে চিনবে, যেটা আলাদা একটা ধারণা।
Kenji এমন একটা মেশিনের software লেখে, যেটা button চাপলে থেমে যায়। ওর program একটা flag পড়ে অপেক্ষা করে, যেটা button-র circuit memory-তে লিখে দেয়। এক বছর ঠিকঠাক চলল, তারপর ও optimiser চালু করল, আর মেশিন থামা বন্ধ করে দিল।
Flag-র কিছুই বদলায়নি। যেটা বদলেছে সেটা হলো, compiler দেখল program-র কেউ কখনো ওতে লেখে না, তাই ও একবার পড়ে সেই উত্তরটাই চিরকাল ব্যবহার করল। এই lesson সেই দুইটা শব্দ নিয়ে, যেগুলো দিয়ে তুমি compiler-কে এমন কথা বলো যা ও তোমার code পড়ে বের করতে পারে না।
Const একটা প্রতিশ্রুতি, আর compiler সেটা ধরে রাখে
Qualifier হলো type-র পাশে বসা একটা বাড়তি শব্দ। এটা variable-র মাপ বা তার ধরতে পারা মান বদলায় না। এটা বদলায় তুমি ওটা দিয়ে কী করতে পারবে, আর compiler কী ধরে নিতে পারবে।
const একটা কথাই বলে: এই program-র কিছুই জন্মের পরে এই variable-এ assign করে না। প্রতিশ্রুতি ভাঙলে build থেমে যায়।
Qualifier কোথায় বসে
const int limit = 60; চেনা ক্রম
int const limit = 60; একই জিনিস, বৈধ, বাস্তবে কম দেখা যায়
const double PI = 3.14159; যেকোনো type-ই এটা নেয়
volatile int sensor; এই lesson-র অন্য qualifier
const volatile int clock; দুইটাই একসাথে, আর এটা সত্যিই ঘটে
- Qualifier আর type-র ক্রম মুক্ত। সবাই qualifier আগে লেখে, তাই তুমিও আগে লেখো।
const-কে declare করার জায়গাতেই মান দিতে হয়, কারণ পরে আর সুযোগ নেই।restrictতৃতীয় qualifier। ওটা pointer নিয়ে একটা প্রতিশ্রুতি, আর Module 11-এ ওটা সৎভাবে বোঝানো যায়।
#include <stdio.h>
int main(void)
{
const int SEATS_PER_ROW = 8;
const int ROWS = 12;
int booked = 71;
printf("capacity : %d\n", SEATS_PER_ROW * ROWS);
printf("booked : %d\n", booked);
printf("free : %d\n", SEATS_PER_ROW * ROWS - booked);
return 0;
}
capacity : 96
booked : 71
free : 25
Declaration-র নিচে যেকোনো জায়গায় একটা লাইন বসাও, SEATS_PER_ROW = 10;। Playground-র GCC 12 উত্তর দেয় error: assignment of read-only variable 'SEATS_PER_ROW', আর কোনো program তৈরি হয় না।
পুরো ব্যবস্থাটা এটুকুই। চলার সময় কোনো পরীক্ষা নেই আর কোনো খরচও নেই: মানা করাটা হয় তুমি টাইপ করতে করতেই, আর তৈরি program-এ বাড়তি কিছু থাকে না।
Const যা করে না
তিনটা জিনিস মানুষ const-র কাছে আশা করে আর পায় না, আর এগুলো আগে জানলে প্রতিটার জন্য একটা করে বিকেল বাঁচে।
এটা compile-র সময়ের constant বানায় না। একটা const int হলো এমন variable যেটা তুমি বদলাবে না বলে কথা দিয়েছ। ওটা constant expression না, তাই ওটা দিয়ে নির্দিষ্ট মাপের array বানানো যায় না, আর সেভাবে লিখলে একটা variable length array হয়ে যায়। ওটার মানে কী, Module 9 বলে।
এটা মানটাকে সব কিছু থেকে বাঁচায় না। const সরিয়ে দিয়ে একটা pointer-কে একটা const variable-র দিকে তাক করানো যায়, আর সেই pointer দিয়ে লিখলে সেটা build হয়। তারপর কী হয় সেটা undefined behaviour, আর এই lesson-র শেষের মাথার খোরাক সেটা ঘটতে দেখায়।
এটা thread-র মাঝে কিছুকে নিরাপদ করে না। যে মানে কেউ কখনো লেখে না, সেটা যেকোনো জায়গা থেকে পড়া নিরাপদ, তবে সেটা না-লেখার গুণ, const শব্দটার গুণ না।
Const আর pointer: দুই জায়গা, দুই মানে
Module 11 pointer ঠিকমতো শেখায়। একটা জিনিস এখানেই বলা দরকার। এটা C-র সবচেয়ে বেশি ভুল পড়া লাইন, আর Module 11-র অনেক আগেই তুমি এটা কোনো library header-এ দেখবে।
একটা pointer-র declaration-এ দুইটা জিনিস constant হতে পারে: যেটার দিকে তাক করা, আর pointer নিজে। শব্দটা কোথায় বসালে তা-ই ঠিক করে কোনটা।
| Declaration | যেটার দিকে তাক, তার মান বদলানো যায় কি | অন্য জায়গায় তাক করানো যায় কি |
|---|---|---|
int *p; | যায় | যায় |
const int *p; | যায় না | যায় |
int const *p; | যায় না, উপরের সারির সমান | যায় |
int *const p; | যায় | যায় না |
const int *const p; | যায় না | যায় না |
পড়ার কায়দাটা হলো নাম থেকে শুরু করে উল্টো দিকে পড়া। int *const p পড়তে হয়: p হলো int-র দিকে তাক করা একটা const pointer।
দুই ভুলেরই নিজের কথা আছে। প্রথম ধরনটার ভিতরে লিখলে আসে error: assignment of read-only location '*p', আর দ্বিতীয়টাকে অন্য দিকে তাক করালে আসে error: assignment of read-only variable 'q'। Module 11-র lesson 5 memory এঁকে এখানে ফিরে আসে।
Volatile: যে variable তোমার পিছনে বদলে যায়
Compiler-র প্রতিটা optimisation একটাই ধারণার উপর দাঁড়ানো: তোমার variable-এ যা যা হাত দেয়, সবই ও দেখতে পায়। বেশির ভাগ program-র জন্য কথাটা সত্যি।
তিন জায়গায় কথাটা মিথ্যা, আর তিনটাই বাস্তব। Hardware-র সাথে জোড়া একটা memory address। Signal handler যে variable-এ লেখে। অন্য একটা thread যে variable-এ লেখে।
volatile বলে, তোমার কোনো code না বদলালেও এই variable বদলাতে পারে। তাই প্রতিবার memory থেকেই পড়ো, আর কোনো পড়া জমিয়ে রেখো না বা মুছে দিয়ো না।
Program-র ভিতর থেকে এটা দেখা যায় না, কারণ মানটা দুইভাবেই এক। Compiler যে machine code বানায়, তাতে এটা দেখা যায়।
সাধারণ variable compiler যত কমবার খুশি পড়ে
দুইটা function, একটা শব্দ ছাড়া হুবহু এক। প্রতিটা নিজের variable দুইবার পড়ে আর দুই পড়া যোগ করে।
#include <stdio.h>
volatile int sensor = 21;
int counter = 21;
int twice_volatile(void)
{
int first = sensor;
int second = sensor;
return first + second;
}
int twice_plain(void)
{
int first = counter;
int second = counter;
return first + second;
}
int main(void)
{
printf("twice_volatile : %d\n", twice_volatile());
printf("twice_plain : %d\n", twice_plain());
return 0;
}
twice_volatile : 42
twice_plain : 42
দুইটা উত্তরই 42, আর চিরকাল তা-ই থাকবে। Program-র ভিতর থেকে তফাতটা দেখা যায় না, তাই বরং machine code-টা দেখো।
Playground-র optimisation স্তরে, gcc -O2 -S দিয়ে, function দুইটা এভাবে বেরোয়। যে লাইনগুলো গুরুত্বপূর্ণ, সেগুলোই memory পড়ছে।
twice_volatile:
movl sensor(%rip), %eax
movl sensor(%rip), %edx
addl %edx, %eax
ret
twice_plain:
movl counter(%rip), %eax
addl %eax, %eax
ret
দুইটা অংশ পাশাপাশি পড়ো। Volatile-টা memory থেকে দুইবার পড়ে, ঠিক যেমন source চেয়েছিল। সাধারণটা একবার পড়ে আর যা পেল তাকেই দ্বিগুণ করে।
সাধারণ variable-র জন্য দ্বিতীয়টাই ঠিক, আর ওটাই Kenji-র bug। দুইটা পড়াই হওয়া দরকার ছিল, আর compiler-র সেটা জানার কোনো উপায় ছিল না।
একই file -O0-তে compile করো, দুইটা function-ই দুইবার পড়ে। এই কারণেই Kenji-র program ততদিন চলেছিল, যতদিন ও optimiser চালু করেনি।
তাই volatile মান, type বা মাপ কিছুই বদলায় না। ও compiler-র হাত থেকে একটা অনুমতি কেড়ে নেয়।
Storage class পুরো আলাদা একটা শব্দ
Type-র পাশে static, extern, auto বা register-ও দেখতে পারো। ওগুলো storage class, qualifier না, আর ওরা আলাদা একটা প্রশ্নের উত্তর দেয়।
Qualifier উত্তর দেয় "এই মান নিয়ে কী করা যাবে"। Storage class উত্তর দেয় "এই variable কোথায় থাকে, আর কতক্ষণ"।
| শব্দ | ধরন | যা বলে | যেখানে শেখানো হয় |
|---|---|---|---|
const | qualifier | এখানে কেউ এতে assign করে না | এই lesson |
volatile | qualifier | বাইরের কিছু এটা বদলাতে পারে | এই lesson |
restrict | qualifier | অন্য কোনো pointer এই memory-তে পৌঁছায় না | Module 11 |
static | storage class | call-র মাঝে মান ধরে রাখে | Module 7 |
extern | storage class | অন্য file-এ define করা | Module 7 |
register | storage class | একটা ইঙ্গিত, যেটা আজকের compiler গ্রাহ্য করে না | Module 7 |
ওরা একসাথেও আসতে পারে, আর static const int limit = 60; লেখা খুবই স্বাভাবিক। Module 7 ডান দিকের কলামটাকে নিজের একটা lesson দেয়।
দুইটা নাম দেওয়া মান, দুইটাই কাজে লাগে, কোনোটাই বদলায় না। দশবারের নয়বারই তুমি এটাই লিখবে।
#include <stdio.h>
int main(void)
{
const int MINUTES_PER_HOUR = 60;
int hours = 7;
printf("%d hours is %d minutes\n", hours, hours * MINUTES_PER_HOUR);
return 0;
}
7 hours is 420 minutes
hours যত খুশি বদলাও। MINUTES_PER_HOUR বদলাতে গেলে compiler নাম ধরে তোমাকে আটকায়।
Program যেসব মান দিয়ে সাজানো, সব এক জায়গায়, উপরে, type আর নাম সহ।
#include <stdio.h>
const int MAX_UPLOAD_MB = 25;
const int SESSION_MINUTES = 15;
const int RETRY_LIMIT = 3;
const double VAT_RATE = 0.15;
int main(void)
{
printf("upload limit : %d MB\n", MAX_UPLOAD_MB);
printf("session length : %d minutes\n", SESSION_MINUTES);
printf("retries allowed : %d\n", RETRY_LIMIT);
printf("VAT rate : %.2f\n", VAT_RATE);
printf("session seconds : %d\n", SESSION_MINUTES * 60);
return 0;
}
upload limit : 25 MB
session length : 15 minutes
retries allowed : 3
VAT rate : 0.15
session seconds : 900
শেষ লাইনে একটা magic number আছে। ষাট হলো এক minute-এ কত second, যেটা বদলাবে না, আর ওটাকে নাম দিলে কেবল ভিড় বাড়ত। এটা বিচারের কাজ, নিয়মের না।
Run in Compilerএটা Kenji-র program-র গড়ন, অপেক্ষাটা বাদ দিয়ে, যাতে এটা শেষ হয়। Flag-টা volatile, তাই দুইটা পড়াই ঘটে।
#include <stdio.h>
/* Something outside this program can write to stop_requested: a button,
a signal handler, another thread. Nothing in this file writes to it. */
volatile int stop_requested = 0;
int main(void)
{
int first_look = stop_requested;
int second_look = stop_requested;
printf("first look : %d\n", first_look);
printf("second look : %d\n", second_look);
printf("both agree : %d\n", first_look == second_look);
return 0;
}
first look : 0
second look : 0
both agree : 1
এখানে কেউ button চাপছে না, তাই দুইটা দেখাই মেলে। Kenji-র মেশিনে মিলত না। শব্দটার পুরো কথাই হলো, compiler এই দুই অবস্থার তফাত বুঝতে পারে না।
Run in Compilerএটা কোথায় কাজে লাগে
- Linux kernel-র device driver। Memory-তে map করা register
volatileহিসেবে declare করা হয়, কারণ একই address দুইবার পড়লে সত্যিই দুই রকম উত্তর আসে। শব্দটার জন্মের আসল কারণ এটাই। - যেকোনো C program-র signal handler। Standard বলে, handler নিরাপদে ছুঁতে পারে
volatile sig_atomic_ttype-র variable, আর প্রায় আর কিছুই না। Terminal-এ Ctrl আর C চাপো, আর program যে টের পায় সেটা এই ব্যবস্থার জন্যই। - তুমি যত standard library header include করো।
<string.h>বলেstrlenনেয় একটাconst char *। ওইconstতোমার কাছে লিখিত একটা প্রতিশ্রুতি, যে function তোমার লেখাটা বদলাবে না। - SQLite-র configuration table। SQLite compile-র সময়ের setting রাখে একটা
conststructure-এ, যেটা গোটা library পড়ে আর কেউ লেখে না। তাই ওটা program-র কেবল-পড়ার অংশে থাকতে পারে।
যে ভুলগুলো সবাই করে
১. const-এ assign করা।
const double VAT_RATE = 0.15;
VAT_RATE = 0.20;
GCC 12 বলে error: assignment of read-only variable 'VAT_RATE' আর থেমে যায়। এটাই ভালো ঘটনা: ভুলটা সবচেয়ে আগের মুহূর্তে ধরা পড়ে আর নাম ধরে বলা হয়। Program চলতে চলতে হারটা যদি সত্যিই বদলায়, তাহলে ওটা কখনোই constant ছিল না আর ওর সাধারণ variable দরকার।
২. const declare করে পরে ভরতে চাওয়া।
const int limit;
limit = 60;
GCC 12 বলে error: assignment of read-only variable 'limit', আর প্রথম লাইনের বদলে দ্বিতীয় লাইনটা দেখায়। const জন্মেই মান পায়, নয়তো কখনোই পায় না, কারণ যে একটামাত্র লাইন ওটাকে মান দিতে পারত compiler সেটাই মানা করে।
৩. Pointer-র ভুল পাশে const বসানো।
int a = 1;
int b = 2;
int *const q = &a;
q = &b;
GCC 12 বলে error: assignment of read-only variable 'q'। Pointer-টা জমে গেছে, ও যেটার দিকে তাক করা সেটা না, আর লেখক প্রায় সবসময় অন্যটাই বোঝাতে চেয়েছিল। নাম থেকে উল্টো দিকে পড়ো, আর উপরের table-র সাথে মিলিয়ে নাও।
৪. volatile thread-র জন্য নিরাপদ করবে ভাবা।
volatile int counter = 0;
counter = counter + 1;
দুই command line-র কোনোটাতেই কিছু বলে না, আর program build হয়। ওই লাইনটা একটা পড়া, একটা যোগ আর একটা লেখা, আর যেকোনো দুইটার মাঝখানে অন্য thread ঢুকে পড়তে পারে। volatile compiler-কে মান জমিয়ে রাখা থেকে আটকায়; একসাথে দুইটা ঘটনা নিয়ে ও কিছুই কথা দেয় না। ওটার জন্য জিনিস আছে <stdatomic.h>-তে আর সেগুলো এই track-র বাইরে।
Kenji-র exam hall-এ প্রতি block-এ 12টা সারি আর প্রতি সারিতে 8টা আসন। Block-র সংখ্যা আর booked আসনের সংখ্যা পড়ো, তারপর তিনটা মোট ছাপো।
Input. এক লাইনে দুইটা পূর্ণসংখ্যা: block-র সংখ্যা, তারপর আগে থেকে booked আসন।
Output. তিন লাইন: rows, capacity আর free, প্রতিটার পরে একটা ফাঁকা জায়গা আর সংখ্যাটা।
Constraints. 0 <= block <= 10000, 0 <= booked <= capacity। 12 আর 8 একবার করে থাকবে, নাম দেওয়া constant হিসেবে।
Sample. Input 3 71 দিলে rows 36, capacity 288, free 217।
#include <stdio.h>
int main(void)
{
const int SEATS_PER_ROW = 8;
const int ROWS_PER_BLOCK = 12;
int blocks = 0;
int booked = 0;
scanf("%d %d", &blocks, &booked);
/* Three printf calls. Every number in them comes from a name. */
return 0;
}
seat-plan নামে graded। Judge তোমার output দেখে, constant দেখে না, তাই শেষ নিয়মটা তোমার নিজের হাতে। একবার উল্টোভাবে লিখে গুনে দেখো, hall নতুন করে বানালে কয়টা edit লাগত।
নিচে একটা চলতি program, তাতে ছয়টা খালি সংখ্যা। এমনভাবে আবার লেখো যাতে যে মান কখনো বদলায় না তার একটা নাম থাকে, আর output এখন যা আছে byte ধরে ধরে তা-ই থাকে।
#include <stdio.h>
int main(void)
{
int students = 45;
printf("exam minutes : %d\n", 90);
printf("pass mark : %d\n", 40 * 60 / 100);
printf("papers : %d\n", students * 3);
printf("bundles : %d\n", students * 3 / 25);
return 0;
}
exam minutes : 90
pass mark : 24
papers : 135
bundles : 5
এই module-এ graded না, কারণ judge output পড়ে আর const দেখতে পায় না। তবু করো, তারপর pass-র ভগ্নাংশ 40 থেকে 50 করো আর গুনে দেখো দুই রূপে কয়টা লাইনে হাত দিতে হলো।
যে প্রশ্নগুলো সবার মনে আসে
যা বদলায় না সবকিছুতেই কি
constদেব?মানের ক্ষেত্রে হ্যাঁ, আর তাতে কোনো খরচ নেই। অভ্যাসটা দুইবার লাভ দেয়: compiler ভুল করে করা assign ধরে ফেলে, আর পাঠক বুঝে যায় কোনগুলো setting আর কোনগুলো কাজের সংখ্যা।
আমি কি কখনো
volatileলিখব?কেবল যদি firmware, signal handler বা operating system লেখো। এটা এখানে আছে কারণ অন্যের code-এ তুমি এটা পড়বে, আর কারণ এটার ভুল ব্যবহার খুব সাধারণ।
const intআরint constকি আলাদা?না, দুইটা একই declaration। দ্বিতীয় রূপটা pointer-র পাশে পড়তে ভালো লাগে, আর কেউ ওটা লিখলে সেটাই একমাত্র কারণ।
volatileকি আমার program ধীর করে দেয়?একটু, আর কেবল যেখানে ওটা ব্যবহার হয়েছে সেখানে। প্রতিটা পড়া সত্যিই ঘটে, তাই compiler মানটা register-এ রেখে দিতে পারে না, আর ওটাই তো পুরো কথা।
Compiler আমাকে
constcast করে সরাতে দেয় কেন?কারণ C তোমাকে বিশ্বাস করে, আর কারণ পুরনো library-তে এমন function আছে যারা সাধারণ pointer নেয় আর কিছু বদলায় না। Cast হলো পালানোর দরজা, আর মাথার খোরাক দেখায় তার দাম কী।
মূল কথা
- Qualifier বদলায় মান নিয়ে কী করা যাবে, তার মাপ বা সীমা কখনোই না।
constcompile-র সময়েই assign করা মানা করে, নাম ধরে, আর চলার সময় কোনো খরচ নেই।- C-তে
const intconstant expression না, তাই ওটা নির্দিষ্ট মাপের array-র মাপ দেয় না। - Pointer-এ
constকোন পাশে বসেছে, তা-ই ঠিক করে লক্ষ্যটা জমল না pointer-টা জমল। volatilecompiler-কে বলে প্রতিবার memory পড়তে, কারণ program-র বাইরের কিছু ওতে লিখতে পারে।static,externআরregisterহলো storage class, আলাদা একটা প্রশ্ন, শেখানো হয় Module 7-এ।
এরপর এই module-র বাকি অর্ধেক। শুরু হয় সেই একটা function দিয়ে, যেটা তুমি এতদিন প্রতিটা program-এ ব্যবহার করেছ আর কখনো পড়োনি।
lesson ২ শেষ
শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।
পরেরটা: Printf: format specifier, width আর precision