Learn C Programming

lesson ১ / ৯ · File

Module ১৩ · File

File: program শেষ হলেও data যেন থাকে

Freeপড়া

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

  • বলতে পারবে file কেন program শেষ হওয়ার পরেও থেকে যায়, fopen আর একটা mode দিয়ে file open করতে পারবে, আর fclose দিয়ে close করতে পারবে।
  • fopen কী return করল দেখে NULL ধরতে পারবে, আর perror দিয়ে কারণটা print করতে পারবে।
  • stdin, stdout আর stderr-কে আগে থেকেই open থাকা তিনটা file হিসেবে ব্যবহার করতে পারবে, আর বলতে পারবে Playground file নিয়ে কী কী করতে দেয়।

Maria-র দোকানের till program সারাদিনের বিক্রির হিসাব যোগ করে রাখে একটা variable-এ। দোকান বন্ধের সময় ও program-টা থামিয়ে দেয়। পরদিন সকালে total আবার 0 থেকে শুরু হয়, আগের দিনের বিক্রির কোনো চিহ্নই থাকে না।

কিছুই crash করেনি। Program-কে যা বলা হয়েছিল ও ঠিক তা-ই করেছে; মনে রাখতে তো কেউ বলেনি।

এই lesson Maria-র বিক্রির হিসাবকে এমন একটা জায়গা দেয়, যেটা program শেষ হলেও থেকে যায়: একটা file। সাথে এটাও দেখায় যে তোমার program-গুলো এতদিন ধরেই file ব্যবহার করে আসছে।

Variable বাঁচে শুধু program চলার সময়টুকু

Variable হলো memory-র একটা বাক্স (Module 2)। Program শুরু হলে memory পায়, আর শেষ হলে পুরোটাই ফেরত দিয়ে দেয়, সাথে সব বাক্সও।

File হলো নাম দেওয়া এক সারি byte, যেটা operating system একটা disk-এ রেখে দেয়। Operating system হলো সেই software, যেটা computer আর ওর disk চালায়, যেমন Linux, Windows বা macOS। যে program file-টা লিখেছে, সেটা শেষ হয়ে গেলেও file থেকে যায়।

দিনের বিক্রি কোথায় থাকেProgram শেষ হলেপরদিন সকালে
একটা variable-এ, int totalProgram-এর memory-র সাথেই হাওয়াআবার 0 থেকে শুরু
একটা file-এ, sales.txtDisk-এ থেকে যায়Program ওটা আবার read করে নেয়

তাই variable টেকে program-এর এক run পর্যন্ত, আর file টেকে যতক্ষণ না কেউ ওটা মুছে ফেলে।

File open করা: fopen, একটা mode আর একটা FILE *

C একটা file read আর write করে একটা stream-এর ভিতর দিয়ে: অক্ষরের একটা স্রোত, যেখান থেকে তুমি ক্রম ধরে নাও, বা যেখানে ক্রম ধরে যোগ করো। <stdio.h>-এর fopen একটা file-এর সাথে একটা stream জুড়ে দেয়, আর তোমার হাতে ওটার একটা handle তুলে দেয়।

Handle-টার type হলো FILE *। FILE হলো একটা open stream-এর জন্য library-র রাখা record: stream-টা কতদূর এগোল, আর এখনো হাতে কী ধরে আছে। fopen ওটা সাজিয়ে দেয়, আর তোমাকে দেয় ওর ঠিকানা, মানে একটা pointer (Module 11)।

File open করা

FILE *f = fopen("sales.txt", "w");
  • FILE *f হলো handle। এই file নিয়ে পরের প্রতিটা call f নেয়।
  • "sales.txt" হলো file-এর নাম। নামে কোনো folder না থাকলে file থাকে সেই folder-এ, যেখান থেকে program run হচ্ছে।
  • "w" হলো mode: file-টা নিয়ে তুমি কী করবে। এটাও একটা string, তাই double quote লাগে।
  • File open করা না গেলে fopen return করে NULL।

এই module-এর text file-গুলোর জন্য তিনটা mode-ই যথেষ্ট। Text file-এ থাকে লাইনে লাইনে সাজানো অক্ষর, যেকোনো text editor-এ যেটা open করা যায়।

ModeFile open হয় যে কাজেFile থাকলেFile না থাকলে
"r"read করতেশুরু থেকে read করেfopen return করে NULL
"w"write করতেআগে খালি করে দেয়নতুন বানায়
"a"append করতে, মানে শেষে যোগ করতেযা আছে রেখে দেয়, লেখে তার পরেনতুন বানায়

Mode-এ একটা b জুড়ে দিলে, যেমন "rb" বা "wb", file open হয় binary file হিসেবে, byte-গুলো যেমন আছে ঠিক তেমন জমা থাকে। b-টা আসলে কাজে লাগে Windows-এ, কারণ সেখানে text mode প্রতিটা \n-কে দুইটা অক্ষর করে লেখে। Linux-এ, যেখানে Playground চলে, এতে কিছুই বদলায় না। Binary file লেখা হবে lesson 4-এ।

Maria sales.txt-এ দুইটা বিক্রি write করে, file close করে, আবার read করার জন্য open করে, আর ভিতরে যা আছে print করে। fprintf মানে সামনে একটা stream বসানো printf।

#include <stdio.h>

#define LINE_LEN 256

int main(void)
{
    FILE *f = fopen("sales.txt", "w");
    char line[LINE_LEN] = "";

    if (f == NULL) {
        perror("sales.txt");
        return 1;
    }
    fprintf(f, "rice 40\n");
    fprintf(f, "oil 12\n");
    fclose(f);

    f = fopen("sales.txt", "r");
    if (f == NULL) {
        perror("sales.txt");
        return 1;
    }
    while (fgets(line, sizeof line, f) != NULL) {
        printf("from the file: %s", line);
    }
    fclose(f);
    return 0;
}
from the file: rice 40
from the file: oil 12

Read করার loop-টা Module 10-এর: fgets একটা পুরো লাইন read করে, newline-সহ, আর লাইন ফুরিয়ে গেলে return করে NULL। এখানে ও keyboard থেকে না, f থেকে read করছে। Loop-টা খুলে খুলে দেখাবে lesson 2।

এই program Playground-এ যেমন আছে তেমনই run হয়। সেখানে একটা run file বানাতে, write করতে, close করতে আর আবার read করতে পারে; বাকি কথা আছে নিচের Playground অংশে।

তাই fopen একটা নাম আর একটা mode নিয়ে দেয় একটা FILE *, আর fprintf আর fgets ওই handle দিয়েই কাজ করে।

Mode w আগে file-টা খালি করে দেয়

পরদিন সকালে Maria-র program আবার "w" দিয়ে sales.txt open করে, আর দিনের প্রথম বিক্রিটা write করে। Maria ভাবে, file-এ এখন তিনটা লাইন থাকবে। এই program দুই দিনের কাজই এক run-এ করে দেখায়।

#include <stdio.h>

#define LINE_LEN 256

void print_lines(FILE *in);

int main(void)
{
    FILE *f = fopen("sales.txt", "w");

    if (f == NULL) {
        perror("sales.txt");
        return 1;
    }
    fprintf(f, "rice 40\n");
    fprintf(f, "oil 12\n");
    fclose(f);

    f = fopen("sales.txt", "w");
    if (f == NULL) {
        perror("sales.txt");
        return 1;
    }
    fprintf(f, "salt 30\n");
    fclose(f);

    f = fopen("sales.txt", "r");
    if (f == NULL) {
        perror("sales.txt");
        return 1;
    }
    printf("sales.txt now holds:\n");
    print_lines(f);
    fclose(f);
    return 0;
}

void print_lines(FILE *in)
{
    char line[LINE_LEN] = "";

    while (fgets(line, sizeof line, in) != NULL) {
        printf("  %s", line);
    }
}
sales.txt now holds:
  salt 30

একটাই লাইন। File আগে থেকে থাকলে "w" একটা byte লেখার আগেই ওটা খালি করে দেয়, তাই fopen return করার মুহূর্তেই আগের দিনের লাইনগুলো শেষ। কোনো command line-এই কোনো বার্তা নেই: Playground, -Wall আর -Wall -Wextra, সবাই চুপ।

পুরনো লাইন রেখে তার পরে যোগ করতে চাইলে open করো "a" দিয়ে; Maria-র রোজকার log-এর জন্য lesson 2 ঠিক এটাই করবে। Print করার কাজটা আছে print_lines-এ, যেটা যেকোনো open stream-কে FILE * parameter হিসেবে নেয়।

তাই "w" প্রতিটা file শুরু করে একদম খালি থেকে, আর "a" যা আছে তার সাথে যোগ করে।

fopen fail করলে: NULL আর perror

fopen fail করে যখন file-টা নেই, বা তুমি যেখানে লিখতে বলেছ সেখানে program-এর লেখার অনুমতি নেই। তখন ও return করে NULL, সেই pointer যেটা কোথাও দেখায় না (Module 11)। NULL দিয়ে read বা write করা undefined behaviour, তাই এই track প্রতিটা fopen-এর পরপরই check করে।

Zara সবার আগে না-থাকা file দিয়েই test করে। ওর program শুধু read করার জন্য data.txt open করে, অথচ কোনো run ওটা লেখেনি।

#include <stdio.h>

int main(void)
{
    FILE *in = fopen("data.txt", "r");

    if (in == NULL) {
        perror("data.txt");
        return 1;
    }
    printf("data.txt is still here\n");
    fclose(in);
    return 0;
}

perror print করে তোমার দেওয়া string, একটা colon, একটা space, আর library-র শেষ call-টা কেন fail করল সেই কারণ। Library কারণটা একটা সংখ্যা হিসেবে রাখে errno-তে, যেটা আসে <errno.h> থেকে। File-এর নামটাই পাঠাও, তাহলে বার্তা পড়েই বোঝা যায় কোন file।

Live Playground-এ মেপে দেখা গেছে: এই run দেখায় badge Runtime error (বাংলা page-এ badge-টা বাংলায় লেখা থাকে), লাইন data.txt: No such file or directory, আর Exit: 1। কিছুই crash করেনি: return 1; ইচ্ছা করেই জানাচ্ছে যে কাজটা হয়নি।

main যে সংখ্যাটা return করে, সেটাই program-এর exit status: 0 মানে কাজ হয়েছে, অন্য যেকোনো সংখ্যা মানে হয়নি। Playground 0 ছাড়া যেকোনো exit status-কে দেখায় Runtime error হিসেবে, আর সংখ্যাটা বসে Exit:-এর পরে।

তাই fopen fail করলে return করে NULL, আর perror বলে দেয় কেন। ও লেখে দ্বিতীয় একটা output stream-এ, stderr-এ, যেটার কথা আছে এই lesson-এর শেষ অংশে।

fclose file-টা ফেরত দেয়

fclose(f) stream-টা close করে। আগে library file-টার জন্য যা কিছু এখনো হাতে ধরে রেখেছে, সেটা লিখে দেয়। তারপর file-টা operating system-কে ফেরত দেয়।

fprintf প্রতিটা লাইনের জন্য disk-এ ছোটে না। ও অক্ষরগুলো জমায় একটা buffer-এ, মানে memory-র একটা টুকরোয়, তারপর পুরো টুকরোটা একবারে লেখে। অনেকগুলো ছোট লেখার চেয়ে একটা বড় লেখায় খরচ অনেক কম। Buffer-এ জমানো লেখা file-এ ঢেলে দেওয়াকে বলে flush করা, আর fclose flush করে।

Operating system একটা program-কে একসাথে গোনা কয়েকটার বেশি file open রাখতে দেয় না, আর fclose ছাড়া প্রতিটা fopen তার একটা আটকে রাখে। কখনো flush না হওয়া buffer-এর কী দশা হয় crash হলে, সেটা মেপে দেখাবে lesson 5 আর 6।

তাই কাজ শেষ হলেই প্রতিটা file close করো, আর যা লিখেছ সেটা আবার read করার আগে তো অবশ্যই।

Playground-এ file: প্রতিটা run শুরু হয় খালি folder-এ

Playground তোমার program run করে Linux-এ, ওর নিজের একটা folder-এ। Live Playground-এ, sign in না করে, মেপে দেখা গেছে:

  • একটা run file বানাতে, write করতে, close করতে, আবার open করতে আর read করতে পারে। যে program data.txt-এ rice 40 লিখে আবার read করে, সেটা দেখায় Success (বাংলা page-এ বাংলায়) আর read back: rice 40।
  • File থাকে run শেষ হওয়া পর্যন্ত, আর প্রতিটা run শুরু হয় খালি folder-এ। Zara-র program ঠিক ওই run-এর পরেই চলেছিল, তবুও পেয়েছে No such file or directory।
  • যে folder-এ program-এর লেখার অনুমতি নেই, সেখানে fopen("/probe.txt", "w") দেয় /probe.txt: Permission denied।
  • একটা run একটা file-এ সবচেয়ে বেশি 1,048,576 byte লিখতে পারে, মানে 1 MiB। আর এক byte বেশি হলেই run থেমে যায় badge Output limit পার হয়েছে (1 MB) নিয়ে।

তাই এই module-এর যে program কোনো file read করে, সেটা আগে ওই একই run-এ file-টা লিখে নেয়।

তোমার নিজের মেশিনে file থেকে যায়। একটা program sales.c নামে save করো, তারপর terminal-এ compile আর run করো:

gcc -std=c17 -Wall -Wextra sales.c -o sales
./sales

দ্বিতীয়বার run করলে প্রথম run-এর রেখে যাওয়া file-টা পেয়ে যাবে। নিচের Example 2 ঠিক এই কারণেই দ্বিতীয় run-এ কম print করে।

তাই Playground file ব্যবহার করতে দেয়, কিন্তু প্রতিটা run শুরু হয় একটাও file ছাড়া: read করার আগে একই run-এ write করো, আর 1 MiB-এর মধ্যে থাকো।

stdin, stdout আর stderr: আগে থেকেই open থাকা file

প্রতিটা C program শুরু হয় তিনটা stream আগে থেকেই open অবস্থায়, main চলার আগেই। এদের বলে standard stream, আর <stdio.h> এদের নাম দিয়েছে তিনটা FILE * মান হিসেবে।

  • stdin, standard input: keyboard, বা judge-এর test input।
  • stdout, standard output: program-এর ফলাফল। printf এখানেই লেখে।
  • stderr, standard error: warning আর error-এর জন্য দ্বিতীয় একটা output। perror এখানেই লেখে।

তাই printf আসলে ছোট নামের fprintf(stdout, ...), আর scanf read করে stdin থেকে। fgets(line, sizeof line, stdin) হলো input-এর একটা লাইন read করার Module 10-এর উপায়: সেই একই fgets, শুধু হাতে অন্য একটা stream।

#include <stdio.h>

#define LINE_LEN 256

int main(void)
{
    char line[LINE_LEN] = "";

    if (fgets(line, sizeof line, stdin) == NULL) {
        fprintf(stderr, "warning: no input\n");
        return 1;
    }
    printf("printf: %s", line);
    fprintf(stderr, "fprintf to stderr: %s", line);
    fprintf(stdout, "fprintf to stdout: %s", line);
    return 0;
}
printf: the shop opens at 9
fprintf to stdout: the shop opens at 9

ওই output-টা the shop opens at 9 input-এর জন্য। মাঝের লাইনটা গেছে stderr-এ, তাই উপরের block-এ ওটা নেই। Playground ওটা দেখায় stdout-এর দুই লাইনের নিচে: page আগে stdout-এর সবকিছু দেখায়, তারপর stderr-এর সবকিছু।

Judge শুধু stdout read করে, stderr-এর দিকে ফিরেও তাকায় না। তাই stderr-এর কোনো warning-এ test fail হয় না, কিন্তু stdout-এ একটা বাড়তি লাইনেই হয়। এজন্যই এই module-এর graded problem-গুলো stdin থেকে read করে আর stdout-এ write করে, সেই function-গুলো দিয়েই, যেগুলো file-এও চলে।

একটা program, তিনটা standard stream, আর নিজে open করা একটা file main-এর আগেই open তিনটা stream, আর program-এর নিজের একটা file keyboard, অথবা judge-এর input stdin তোমার program লেখে printf, fprintf দিয়ে পড়ে scanf, fgets দিয়ে stdout screen, অথবা judge যা read করে stderr এটাও screen-এ; judge ফিরেও তাকায় না FILE *f, fopen থেকে fprintf লেখে, fgets পড়ে sales.txt disk-এ রাখা একটা file stdin, stdout আর stderr main শুরুর আগেই open: এগুলো তোমাকে open করতে হয় না। sales.txt তোমার নিজের: fopen ওটা open করে, আর fclose close করে।

Terminal-এ shell (যে program তোমার command পড়ে) একটা stream-কে file-এ redirect করতে পারে। ./sales < in.txt দিলে in.txt ঢোকে stdin হয়ে, আর ./sales > out.txt দিলে stdout চলে যায় out.txt-এ। Playground-এর runner-ও তা-ই করে: stdout আর stderr পাঠিয়ে দেয় দুইটা file-এ।

তাই standard stream-গুলো আর দশটা stream-এর মতোই। শুধু ওরা open হয় main-এর আগে, আর program শেষ হলে close হয়ে যায়।

Example 1: Alice-এর একটা function, যেকোনো stream

দোকান কখন খোলা থাকে, Alice সেটা print করে একটা function দিয়ে, যেটা stream-কে parameter হিসেবে নেয়। stdout আর stderr দুটোই FILE * মান, তাই যেকোনোটা পাঠানো যায়।

#include <stdio.h>

void print_hours(FILE *out);

int main(void)
{
    print_hours(stdout);
    print_hours(stderr);
    return 0;
}

void print_hours(FILE *out)
{
    fprintf(out, "Maria's shop: open from 9 to 21\n");
}
Maria's shop: open from 9 to 21

একই লাইন stderr-এও গেছে, তাই Playground ওটা দুইবার দেখায়। print_hours-এর হাতে fopen-এর দেওয়া একটা stream দাও, তখন ও file-এ লিখবে, পার্থক্যটা টেরও পাবে না।

Run in Compiler
Example 2: David-এর settings, প্রথম run-এ নেই

David-এর program ওর settings read করে config.txt থেকে। প্রথম run-এ এমন কোনো file নেই, আর তাতে program থেমে যাওয়া উচিত না। তাই ও কারণটা বলে, default settings লিখে ফেলে, তারপর সেটাই open করে।

#include <stdio.h>

#define LINE_LEN 256

int write_defaults(void);
void print_settings(FILE *in);

int main(void)
{
    FILE *f = fopen("config.txt", "r");

    if (f == NULL) {
        perror("config.txt");
        printf("no config.txt yet: writing the defaults\n");
        if (!write_defaults()) {
            return 1;
        }
        f = fopen("config.txt", "r");
        if (f == NULL) {
            perror("config.txt");
            return 1;
        }
    }
    print_settings(f);
    fclose(f);
    return 0;
}

int write_defaults(void)
{
    FILE *out = fopen("config.txt", "w");

    if (out == NULL) {
        perror("config.txt");
        return 0;
    }
    fprintf(out, "page_size 10\n");
    fprintf(out, "currency taka\n");
    fclose(out);
    return 1;
}

void print_settings(FILE *in)
{
    char line[LINE_LEN] = "";

    while (fgets(line, sizeof line, in) != NULL) {
        printf("setting: %s", line);
    }
}
no config.txt yet: writing the defaults
setting: page_size 10
setting: currency taka

stderr পেয়েছে config.txt: No such file or directory, তবুও program return করে 0: এখানে file না থাকা মানে প্রথম দিন, কোনো ব্যর্থতা না। Playground-এ প্রতিটা run তিনটা লাইনই print করে। তোমার নিজের মেশিনে দ্বিতীয় run config.txt পেয়ে যায়, আর print করে শুধু settings দুইটা।

Run in Compiler
Example 3: Amara শুধু আসল নম্বর রাখে

Amara প্রতিটা ছাত্রের নাম আর নম্বর type করে। 0 থেকে 100-এর বাইরের নম্বর মানে type করতে গিয়ে হাত ফসকেছে। ঠিক নম্বরগুলো যায় marks.txt-এ, প্রতিটা ভুলের জন্য stderr-এ একটা warning যায়, আর শেষে file-টা আবার print হয়।

#include <stdio.h>

#define NAME_LEN 20
#define LINE_LEN 256

void print_lines(FILE *in);

int main(void)
{
    FILE *f = fopen("marks.txt", "w");
    char name[NAME_LEN] = "";
    int mark = 0;
    int saved = 0;

    if (f == NULL) {
        perror("marks.txt");
        return 1;
    }
    while (scanf("%19s %d", name, &mark) == 2) {
        if (mark < 0 || mark > 100) {
            fprintf(stderr, "warning: %s %d is not a mark, skipped\n", name, mark);
        } else {
            fprintf(f, "%s %d\n", name, mark);
            saved = saved + 1;
        }
    }
    fclose(f);

    f = fopen("marks.txt", "r");
    if (f == NULL) {
        perror("marks.txt");
        return 1;
    }
    printf("marks.txt holds %d marks:\n", saved);
    print_lines(f);
    fclose(f);
    return 0;
}

void print_lines(FILE *in)
{
    char line[LINE_LEN] = "";

    while (fgets(line, sizeof line, in) != NULL) {
        printf("%s", line);
    }
}
marks.txt holds 3 marks:
Amara 91
Zara 78
Kenji 85

ওই output-টা input Amara 91, Bob 140, Zara 78 আর Kenji 85-এর জন্য, প্রতি লাইনে একটা। stderr বলে warning: Bob 140 is not a mark, skipped, তাই marks.txt-এ থাকে শুধু সেই নম্বরগুলো, যেগুলোর উপর Amara ভরসা করতে পারে।

Run in Compiler

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

  • Log। nginx web server প্রতিটা request-এর উত্তর দেওয়ার পর ওর access log-এ একটা করে লাইন যোগ করে। Log-টা একটা text file, তাই server restart হলেও টিকে থাকে।
  • Settings। git একটা repository-র settings রাখে .git/config-এ, সাধারণ একটা text file-এ। git config user.name Maria সেখানে একটা লাইন লিখে দেয়, যাতে পরের git command-গুলো ওটা read করতে পারে।
  • পুরো database। SQLite একটা গোটা database, ওর প্রতিটা table-সহ, রাখে একটাই সাধারণ file-এ। Android আর iOS দুটোই ওদের app-গুলোর data-র জন্য SQLite রাখে।
  • Command-line tool। grep যে লাইনগুলো খুঁজে পায় সেগুলো print করে stdout-এ, আর grep: missing.txt: No such file or directory-এর মতো অভিযোগ পাঠায় stderr-এ।

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

১. FILE *-এর star বাদ পড়ে গেছে।

FILE f = fopen("sales.txt", "w");

fprintf(f, "rice 40\n");
fclose(f);

প্রতিটা command line-এ error, Playground-সহ: error: invalid initializer। এরপর f যতবার ব্যবহার হয়, ততবার আরেকটা করে error আসে, যেমন error: incompatible type for argument 1 of 'fprintf', সাথে note: expected 'FILE * restrict' but argument is of type 'FILE'। fopen return করে একটা pointer, আর pointer দিয়ে একটা গোটা FILE শুরু করা যায় না। লেখো FILE *f। Star-টা বাদ পড়বে, কারণ int-এর বেলায় তো কখনো লাগেনি।

২. NULL check নেই।

FILE *f = fopen("data.txt", "r");
char line[LINE_LEN] = "";

while (fgets(line, sizeof line, f) != NULL) {
    printf("%s", line);
}

কোনো command line-এই বার্তা নেই: data.txt run-এর সময় থাকবে কি না, GCC জানতেই পারে না। নতুন run-এ থাকে না, তাই f হয় NULL, আর NULL-এর উপর fgets undefined behaviour। Compiler Explorer-এর GCC 12-এ একবার run করতেই program মারা গেছে Program terminated with signal SIGSEGV (11) দেখিয়ে। NULL check করো, আর perror call করো। এটা বাদ দেবে, কারণ তুমি যখন test করেছিলে তখন file-টা ছিল।

৩. Bob file close করার আগেই আবার read করে।

fprintf(out, "rice 40\n");

in = fopen("sales.txt", "r");
if (in == NULL) {
    perror("sales.txt");
    return 1;
}
if (fgets(line, sizeof line, in) == NULL) {
    printf("sales.txt looks empty\n");
}

কোনো command line-এই বার্তা নেই। Bob-এর পুরো program দুইটা fopen-ই check করে, আর Compiler Explorer-এর GCC 12-এ একবার run করলে print হয় sales.txt looks empty। লাইনটা তখনো out-এর buffer-এ বসে ছিল। Playground-ও একই কথা বলে: একই file-এ দ্বিতীয় একটা stream file-টাকে খালি দেখেছে, যতক্ষণ না প্রথমটা flush হয়েছে। আবার open করার আগে fclose(out); call করো। এটা ভুলবে, কারণ fprintf কোনো আপত্তি ছাড়াই return করেছে।

৪. Single quote-এর ভিতরে mode।

FILE *f = fopen("sales.txt", 'w');

প্রতিটা command line-এ warning, Playground-সহ, কারণ এটা default-এই চালু: warning: passing argument 2 of 'fopen' makes pointer from integer without a cast [-Wint-conversion], সাথে note: expected 'const char * restrict' but argument is of type 'int'। GCC 14-এ এটা সোজা error। 'w' একটা অক্ষর, মানে ছোট একটা integer, অথচ mode হতে হবে string। Compiler Explorer-এর GCC 12-এ একবার run করতেই program SIGSEGV-তে মারা গেছে। লেখো "w"। ভুলটা হবে, কারণ এক অক্ষরের mode দেখতে একটা char-এর মতোই লাগে।

মাথা খাটাও

Kenji-র backup program আগে একটা warning লেখে, তারপর ওর report।

#include <stdio.h>

int main(void)
{
    fprintf(stderr, "warning: 1 file skipped\n");
    printf("backup: 12 files copied\n");
    return 0;
}

নিজের মেশিনে ও program-টা run করে ./backup > out.txt দিয়ে, তবুও screen-এ একটা লাইন আসে। কোন লাইন, আর out.txt-এ কী আছে? তারপর কঠিন অংশ: Playground-এ দুই লাইনের কোনটা আগে আসে?

Program কয়টা stream-এ লিখছে, গুনে দেখো। তারপর ভাবো, > কোনটাকে redirect করে।

অনুশীলন ১সহজ

Maria-র till প্রতিটা বিক্রি print করে খদ্দেরের receipt-এর জন্য, আর একটা summary ওর নিজের log-এর জন্য। void print_sale(FILE *out, const char *item, int price) লেখো, যেটা হাতে পাওয়া stream-এ একটা বিক্রি print করে; main ওকে দেয় stdout। Judge শুধু stdout দেখে।

Input. এক লাইনে n, তারপর n লাইনে item price।

Output. stdout-এ প্রতিটা বিক্রির জন্য এক লাইন, <item> <price>, যে ক্রমে read করা হয়েছে সেই ক্রমে। stderr-এ, যেটা judge দেখে না: sales: <n>, total: <sum>।

Constraints. 0 <= n <= 1000; item হলো 1 থেকে 19টা ছোট হাতের অক্ষর; 1 <= price <= 100000।

Sample. Input 3, rice 65, oil 180, salt 40, প্রতি লাইনে একটা, দিলে stdout-এর তিন লাইনে আসে rice 65, oil 180 আর salt 40। stderr দেখায় sales: 3, total: 285।

#include <stdio.h>

#define NAME_LEN 20

void print_sale(FILE *out, const char *item, int price);

int main(void)
{
    int n = 0;
    char item[NAME_LEN] = "";
    int price = 0;

    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%19s %d", item, &price);
        print_sale(stdout, item, price);
    }
    /* The report goes to stderr, where the judge never looks:
       sales: the count, total: the sum. Keep a running total in the loop. */
    return 0;
}

void print_sale(FILE *out, const char *item, int price)
{
    /* One line on the stream it is given: the item, a space, the price. */
}

till-streams নামে গ্রেড হয়। Hidden test-এ আছে n = 0, যেখানে stdout খালি থাকে; আর summary stdout-এ print করলে প্রতিটা test-ই fail করে।

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

Zara সবার আগে না-থাকা file দিয়েই test করে। Program আগে notes.txt লেখে, তারপর একটা file-এর নাম read করে। int can_open(const char *name) লেখো। File-টা read করার জন্য open হলে ওটা আবার close করে 1 return করবে। নইলে perror stderr-এ কারণটা print করবে, আর function return করবে 0।

Input. একটা file-এর নাম।

Output. File open হলে opened <name>, নইলে cannot open <name>। কারণটা যায় stderr-এ।

Constraints. নামটা 1 থেকে 29 অক্ষরের একটা শব্দ, কোনো space ছাড়া।

Sample. Input notes.txt দিলে opened notes.txt। Input sales.txt দিলে cannot open sales.txt, আর stderr দেখায় sales.txt: No such file or directory।

#include <stdio.h>

#define FILE_NAME_LEN 30

int can_open(const char *name);

int main(void)
{
    FILE *f = fopen("notes.txt", "w");
    char name[FILE_NAME_LEN] = "";

    if (f == NULL) {
        perror("notes.txt");
        return 1;
    }
    fprintf(f, "rice 40\n");
    fclose(f);

    scanf("%29s", name);
    if (can_open(name)) {
        printf("opened %s\n", name);
    } else {
        printf("cannot open %s\n", name);
    }
    return 0;
}

int can_open(const char *name)
{
    /* Try to open name for reading. When fopen gives NULL, who says why?
       Anything you opened, close before you return. */
    return 0;
}

আলাদা করে গ্রেড হয় না। Judge-এর প্রতিটা run শুরু হয় খালি folder-এ, তাই program নিজে যেটা লিখেছে সেটা ছাড়া বাকি সব নামে একই উত্তর আসে।

Run in Compiler

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

  • নিজের মেশিনে sales.txt কোথায় গিয়ে বসে?

    Program-এর working folder-এ, মানে যে folder থেকে তুমি ওটা run করেছ। Project folder থেকে ./sales run করো, file-টা তৈরি হবে sales.c-এর ঠিক পাশেই।

  • শুধু FILE না লিখে FILE * কেন?

    fopen library-র record-টা সাজিয়ে তোমাকে দেয় ওর ঠিকানা; record-টা নিজে থাকে library-র কাছেই। Star বাদ দিলে GCC কী বলে, সেটা আছে ভুল 1-এ।

  • perror কি নিজের বার্তা দিয়ে printf করার মতোই?

    না। perror লেখে stderr-এ, তোমার ফলাফল থেকে আলাদা জায়গায়, আর সাথে জুড়ে দেয় errno থেকে পাওয়া কারণ, যেটা printf জানতেই পারে না। <string.h>-এর strerror(errno) ওই একই কথাগুলো দেয় একটা string হিসেবে।

  • stdin, stdout আর stderr কি আমাকে close করতে হবে?

    না। Program স্বাভাবিকভাবে শেষ হলে প্রতিটা stream flush আর close হয়ে যায়, এই তিনটাসহ। তুমি নিজে যে file-গুলো open করেছ, কাজ শেষ হওয়া মাত্র সেগুলো close করো।

মূল কথা

  • Variable বাঁচে শুধু program চলার সময়টুকু; disk-এর file ওর পরেও থেকে যায়।
  • fopen(name, mode) return করে একটা FILE *: "r" এমন file read করে যেটা থাকতেই হবে, "w" file বানায় বা খালি করে, "a" শেষে যোগ করে।
  • প্রতিটা fopen-এর পরে NULL check করো। perror(name) stderr-এ কারণটা print করে, আর Playground-এ return 1; দেখায় Runtime error, সাথে Exit: 1।
  • fclose buffer flush করে file ফেরত দেয়: যা লিখেছ সেটা আবার read করার আগে file close করো।
  • stdin, stdout আর stderr আগে থেকেই open থাকা FILE * মান, আর judge read করে শুধু stdout।
  • Playground-এর প্রতিটা run শুরু হয় খালি folder-এ: read করার আগে file write করো, আর ওটা 1 MiB-এর মধ্যে রাখো।

এরপর Maria একটা রোজকার log রাখে, যেটা প্রতিটা run-এ এক লাইন করে বাড়ে। Lesson 2 "a" দিয়ে append করে, একটা file লাইন ধরে ধরে শেষ পর্যন্ত read করে, আর rewind দিয়ে ওটা দুইবার read করে।

lesson ১ শেষ

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

পরেরটা: Text file পড়া আর লেখা