به نام خدا

سلام خدمت دوستان و هموطنان گرامی!

در این آموزش مبحث جالب کار با فایل را یاد میگیریم و انواع فایل ها از نظر متنی و باینری و تفاوت این دو را در این جلسه مورد بحث قرار میدهیم.

خوب بدون هیج مقدمه شروع میکنم!

ببینید زبان C قابلیت کار با فایل ها رو داره! ساده ترین کار اینه که ما بیایم و با زبان C یه فایل متنی بسازیم و متنی که از کاربر دریافت میکنیم و توی اون بزاریم!

خوب در این آموزش قصد داریم کد برنامه بالا را بنویسیم!

برای باز کردن فایل ها به موارد زیر نیاز داریم:

  1. نام فایل
  2. نوع فایل از نظر ذخیره اصلاعات متنی یا باینری
  3. نوع فایل از نظر خواندنی یا نوشتنی

خوب مورد سه رو بیشتر توضیح میدهم. اگر فایلی خواندنی باشد فقط میتوان از آن فایل خواند و نمیتوان در آن نوشت. و اگر فایلی نوشتنی باشد فقط میتوان در آن فایل نوشت و نمیتوان از آن خواند. در زبان C نوعی دیگر به نام خواندنی/نوشتنی داریم که روی آن فایل میتوان نوشت و هم میتوان از آن خواند.

بریم سراغ برنامه نویسی :

برای باز کردن یک فایل از تابع زیر، که در کتابخانه stdio.h هست استفاده میشود:

FILE *fopen (char *filename , *mode)

در این تایع filename آدرس فایل ما هست. بهتر است که در مورد آدرس دهی نسبی و مطلق یک توضیح کوتاهی بدهم.

آدرس دهی نسبی یعنی مکان یک فایل نسبت به مکان فعلی و آدرس دهی مطلق یعنی مکان دقیق فایل! فرض کنید از شما میپرسم که امتحان چه ساعتی شروع میشود شما در پاسخ میگویید 2 ساعت دیگر که این پاسخ، زمان نسبی است که نسبت به زمان فعلی 2 ساعت باقی مانده در حالی که من هیچ اطلاعی از زمان فعلی ندارم ولی میدانم امتحان 2 ساعت دیگر شروع میشود! یکبار دیگر ممکن است در پاسخ من بگویید ساعت 4 که این یک زمان مطلق است (البته دقیقتر آن است که بگویید ساعت 4 در تاریخ فلان که کاملا مطلق باشد) اینگونه مثال ها را میتوان خیلی در روزمره پیدا کرد که ما از نسبتی بودن یک موضوع استفاده میکنیم.

آدرس دهی نسبی در ویندوز به صورت C:\Ali\file.txt و در لینوکس به صورت /home/ali/Desktop میباشد! (به \ در ویندوز و / در لینوکس توجه کنید). استفاده از آدرس دهی نسبی برنامه را قابل حمل تر میکند و نیازی به تغییر برنامه برای کامپایل در سیستم عامل دیگر نیست!

اگه شما در حال برنامه نویسی در فولدر C:\Ali باشید و برنامه شما main.c باشد اگر با استفاده از آدرسی دهی نسبی فایلی ایجاد کنید (مثلا file.txt) فایل شما در C:\Ali\file.txt ذخیره خواهد شد.

در تابع fopen آرگومان mode مشخص میکند که فایل باید چگونه باز شود (ورودی ، خروجی ، ورودی - خروجی) مقادیری که میتوانند در تابع بالا به جای mode نوشته شوند در جدول زیر آمده است!

mode توضیح
r یا rt  فایل موجود از نوع text را به عنوان خواندنی باز میکند
w یا wt  فایلی از نوع text را به عنوان نوشتنی باز میکند
a یا at  فایلی از نوع text را طوری باز میکند که بتون اطلاعاتی را به انتهای آن اضافه نمود (نوشتنی)
rb  فایل موجود از نوع باینری را به عنوان خواندنی باز میکند
wb  فایلی از نوع باینری را به عنوان نوشتنی باز میکند
ab  فایلی از نوع باینری را طوری باز میکند که بتوان اطلاعاتی را به انتهای آن اضافه نمود (نوشتنی)
r+ یا r+t  فایل موجود از نوع text را به عنوان خواندنی و نوشتنی باز میکند
w+ یا w+t  فایلی از نوع text را به عنوان خواندنی و نوشتنی باز میکند 
a+ یا a+t   فایلی از نوع text را طوری باز میکند که بتوان اطلاعاتی را به انتهای آن اضافه نمود یا از آن خواند
r+b  فایل موجود از نوع باینری را به عنوان خواندنی و نوشتنی باز میکند
w+b  فایلی از نوع باینری را به عنوان خواندنی و نوشتنی باز میکند
a+b  فایل باینری را طوری باز میکند که بتوان اطلاعاتی را به انتهای آن اضافه نمود یا از آن خواند


توجه به نکات زیر ضروری است:

  • اگر فایلی را با حالتی باز کنید که در آن حالت، از w استفاده شده باشد (مثل w , wb , w+ و..) آن فایل اگر وجود نداشته باشد ایجاد میشود و اگر وجود داشته باشد بازنویسی میشود! (یعنی اطلاعات داخل آن از بین میرود)
  • در حالت هایی که از r و مشتقات آن استفاده میشود. فایل باید حتما از قبل موجود باشد.
  • در حالت a اگر فایل وجود داشته باشد که انتهای فایل را باز میکند و در غیر اینصورت آنرا ایجاد میکند!
  • حالت هایی که + دارد هم خواندنی و هم نوشتنی هستند!
  •  استفاده از r و w (و r+ و w+) در زبان سی به طور پیشفرض فایل متنی را باز میکند (یعنی استفاده از این حالت ها به منزله استفاده از wt و rt و w+t و r+t میباشد)
  •  برای آنکه فایل را بتوانید به صورت باینری باز کنید باید از حرف b در آن حالت استفاده کنید!
  • حالت a (و همه مشتقات آن) فایل رو طوری باز میکند که بتوان به انتهای آن افزود! توجه کنید که هنگامی که از این حالت استفاده میکنیدنمیتوانید در فایل (با استفاده از fseek) جابه جا شوید! برای درک بهتر فرض کنید که وقتی فایل را در این حالت باز میکنید انتهای آن باز میشود و درواقع اصلا قبل از آن وجود ندارد!
  • اگر از حالت w استفاده کنید که فایل قبلی کلا از بین میرود! اگر از r استفاده کنید فقط میتوانید بخوانید و اگر از a استفاده کنید میتوانید به انتهای آن اضافه کنید! (بدون اینکه بتوانید جابه جا شوید). اگر از +r استفاده کنید فایل به صورت خواندنی و نوشتنی باز میشود! اگر (بدون استفاده از fseek) در آن بنویسید! روی متن قبلی نوشته میشود یعنی اگر عبارت "سلام دنیا" از قبل در فایل وجود داشته باشد و شما در آن "علی" بنویسید فایل به صورت "علیم دنیا" در می آید! پس استفاده از این حالت به دقت کافی نیاز دارد!
  • اگر از +w استفاده کنید! اگر فایلی قبلا وجود داشته باشد از بین میرود. تفاوتی که این حالت با w دارد اینست که میتوانید از توابع مربوط به خواندن (مثل fscanf) استفاده کنید! ممکن است به نظر برسد که این حالت مفید نیست

شاید بپرسید که +r و +w هر دو یک تعریف دارند و هر دو فایلی از نوع خواندنی و نوشتنی باز میکنند پس تفاوت آنها در چیست!؟ همانطور که در بالا چند بار اشاره کردیم استفاده از +w باعث میشود فایل قبلی از بین برود و فایلی جدید ایجاد شود در حالی که استفاده از +r اینکار را نمیکند همچنین برای استفاده از +r فایل حتما باید وجود داشته باشد وگرنه فایل باز نخواهد شد!

سوالی که پیش میاید این است که تفاوت حالت باینری و حالت متنی چیست؟

حالت متنی که همان حالت خودمان است! وقتی شما یک notepad باز میکنید و در آن مینویسید از حالت متنی استفاده میکنید! شما میتوانید از فایل متنی در زبان سی برای ایجاد فایل به نحوی که کاربر بتواند آن را بخواند و بفهمد استفاده کنید! (مثلا ارور های برنامه را در فایلی به اسم log.txt ذخیره کنید و....) 

ولی حالت باینری به این معنی است که اطلاعات به صورت عددی در فایل نوشته میشوند و برای انسان قابل فهم نیستند! فرض کنید میخواهید دمای هوا را در فایلی ذخیره کنید! برای سادگی فرض کنید دما 65 درجه باشد (فرض محال که محال نیست s04) اگر شما از حالت متنی استفاده کنید برای ذخیره 65 از دو کاراکتر '6' و '5' استفاده میشود! در حالی که اگر از حالت باینری استفاده کنید خود عدد 65 در فایل ذخیره میشود و اگر شما فایل را با notepad باز کنید (از آنجا که notrpad فایل را به صورت متنی باز میکنید) در آن کاراکتر A را خواهید دید (زیرا کد اسکی 65 معادل حرف A است! برای آنکه مطمئن شوید، در ویندوز کلید Alt را نگه دارید و با استفاده از NumPad عدد 65 را وارد کنید خواهید دید که حرف A را به شما نمایش میدهد) 

نکته: گفتیم که notepad فایل را به صورت متنی باز میکند! خوب اگر بخواهیم فایلی را به صورت باینری باز کنیم از چه برنامه ای استفاده کنیم؟ برنامه HxD در ویندوز گزینه مناسبی است! (میتوانید از اینجا دانلود کنید) همچنین در لینوکس میتوانید از hexdump استفاده کنید!

امیدوارم تفاوت حالت متنی و باینری را متوجه شده باشید!

استفاده از حالت باینری برای مواردی که فایل را فقط خود برنامه استفاده میکند (نه انسان) گزینه مناسبی است! زیرا سرعت بالاتری دارد! معمولا حجم کمتری اشغال میکند و در برنامه نویسی استفاده از آن راحت تر است (برای خواندن فایل متنی عدد 65 را که متشکل از دو کاراکتر '6' و '5' است باید به عدد تبدیل کنید که این خود دردسر برنامه نویسی را بیشتر و سرعت اجرای برنامه را کاهش میدهد!)

بریم سراغ استفاده از فایل و توابع مربوطه : 

برای باز کردن فایل باید یک اشاره گر از نوع فایل تعریف بشه تا به فایلی که با تابع fopen باز میشه اشاره کند! اگر فایل به هر دلیلی باز نشه این تابع NULL (با حروف بزرگ) رو بر میگردونه! (که NULL همون صفر هست که در یکی از کتابخانه ها define شده) بنابراین برای اینکه مطمئن بشیم فایل باز شده میتونیم از این ویژگی استفاده کنیم!

دو خط کد زیر یک اشاره گر از نوع فایل رو بوجود میاره بعدش اون اشاره گر رو مساوی فایل ما قرار میده. یعنی هرجا میخواستیم به فایل دسترسی پیدا کنیم از اشاره گرش استفاده میکنیم!

FILE *fo;
fo = fopen ("alefbair.txt","w");

خوب اولین کدی که مینویسیم این هست که یک فایل را باز میکنیم! اگر باز شد پیام "فایل با موفقیت باز شد" و اگر باز کردن فایل با مشکل مواجه شد پیام "باز کردن فایل با شکست مواجه شد" را نشان خواهیم داد!

#include 
#include 
int main()
{
    FILE *fp;
    fp = fopen("file.txt","w");
    if (fp != NULL) /* if(fp) */
        printf("file.txt Successfully opened!\n");
    else
        printf("Opening file.txt failed :(\n");
    return 0;
}

خوب در حالت w کمتر پیش میاید که در باز کردن فایل به مشکل بربخورید زیرا اگر فایل موجود نباشد آن را میسازد! پس بهتر است به جای w از r استفاده کنیم! در اینصورت خواهیم داشت:

#include 
#include 
int main()
{
    FILE *fp;
    fp = fopen("file1.txt","r");
    if (fp)
        printf("File Successfully opened!\n");
    else
        printf("Opening File failed :(\n");
    return 0;
}

در برنامه بالا اگر فایل file1.txt وجود نداشته باشد 100 درصد به پیام "Opening File failed :(" برمیخورید زیرا هنگام باز کردن فایل در حالت r حتما فایل باید از قبل موجود باشد!

خوب بهتر است یک تابع برای بررسی وجود یا عدم وجود فایل بنویسیم زیرا کاربردی و آموزنده است!

#include 
#include 
int file_isexist (char* path){
    FILE *fp;
    fp = fopen(path,"r");
    if (!fp)
        return 0;
    fclose(fp);
    return 1;
}
int main()
{
    char path[50];
    printf("Enter File Name : ");
    scanf("%s",path);
    if (file_isexist(path))
        printf("File Exist!\n");
    else
        printf("File not Exist :(\n");
    return 0;
}

برنامه بالا نام فایل را میگیرد و میگوید که آیا فایل وجود دارد یا خیر. درتابع file_isexist سعی در باز کردن فایل میکنیم! اگر فایل باز شد یعنی موجود است و اگر باز نشد یعنی موجود نیست (البته باز نشدن یک فایل فقط به دلیل عدم وجود فایل نیست بلکه ممکن است دلایل دیگر داشته باشد، مثلا دسترسی غیر مجاز و...) اما ما برای سادگی کار فرض میکنیم که اگر باز کردن فایل با مشکل مواجه شده یعنی فایل موجود نبوده ( برای بررسی دقیقتر اینکه دقیقا چرا فایل باز نشده باید از errno استفاده کرد). اگر فایل باز شد، فایل را میبندیم و عدد 1 به معنی موجود بودن فایل را برمیگردانیم.

برای بستن فایل هم از دو تابع زیر استفاده میشود

int fclose (FILE *fo);
fcloseall();

تابع اول فایل مورد نظر رو میبندد و تابع دوم هر فایلی که باز باشد را میبندد!

 

برای نوشتن کاراکتر در فایل متنی میشود از تابع زیر استفاده کرد:

int putc(int ch, FILE *fo)

و برای خواندن یک کاراکتر از یک فایل متنی میشود از تابع زیر استفاده کرد:

int getc (FILE  *fo)

همچنین تابع زیر تشخیص دهنده اینست که فایل به انتها رسیده یا خیر و همچنین خودتان میتوانید با همین تابع getc انتهای فایل رو تشخص بدهید. برای اینکار باید این نکته رو بدانید که وقتی ما به انتهای فایل رسیدیم تابع getc مقدار EOF رو بر میگرداند EOF با حروف بزرگ یعنی End Of File که نشان دهنده پایان فایل هست! و تابع زیر هم همین کار رو میکند!

int feof (FILE *fo)

اگه واقعا به انتهای فایل رسیده باشیم تابع بالا ارزش درستی (1) رو بر میگرداند و اگه به انتهای فایل نرسیده باشیم ارزش نادرستی (0) رو بر میگرداند.

برنامه زیر متنی را که از کاربر دریافت میکند را در فایلی به نام alefbair.txt در مکان اجرای برنامه ذخیره میکند!

#include 
#include 

int main()
{
    char str [50];
    FILE *fp;   
    fp = fopen("alefbair.txt","w");

    printf("Enter a String to Save in alefbair.txt\n");
    scanf("%[^\n]s",str);

    fprintf(fp,"%s",str);
    fclose(fp);

    return 0;
}

 برنامه زیر هم با استفاده از  تابع  putc اینکار را انجام میدهد!

قبل از آن، تابع fflush رو توضیح بدم: هنگامی که یک کاراکتر یا رشته ای را در فایل ذخیره میکنید سیستم عامل آنرا در جایی نگه میدارد و هنگامی که تعداد کاراکتر ها برای ذخیره روی فایل به حد نصاب رسید روی فایل ذخیره میکند! برای اینکه به سیستم عامل بگوییم که به طور اجبار اطلاعات رو همین الان روی فایل ذخیره کن از تابع fflush استفاده میکنیم!

کد زیر کاراکتر به کاراکتر از کاربر رشته را دریافت میکند و آنرا در فایل به صورت آنی ذخیره میکند! برای آنکه ببینید fflush چه کاری انجام میدهد! برنامه زیر را اجرا کنید و چند حرف بنویسید و بدون اینکه برنامه را ببندید فایل را باز کنید، خواهید دید کاراکتر هایی که وارد کردید در فایل ذخیره شده! با زدن هر کاراکتر، آن کاراکتر در فایل ذخیره خواهد شد! حال تابع fflush را حذف کنید خواهید دید دیگر ذخیره در فایل به صورت آنی صورت نخواهد گرفت! 

#include 
#include 

int main()
{
    char ch;
    FILE *fp;
    fp=fopen("file.txt","w");
    
    if (!fp) { /* Check Open File */
        printf("Can't Open File :(\n");
        exit(0);
    }

    ch=getche();
    while (ch!='#'){
        putc(ch,fp);
        fflush(fp);
        ch=getche();
    }
    fclose(fp);
    return 0;
}

اینم از تابع fflush  و putc 

 

» توابع مربوط به باینری 

دو تابع خیلی مهم وجود دارد که میتوان با آن فایل باینری را خواند یا در آن نوشت.

شکل تابع fread برای خواندن فایل باینری به صورت زیر است:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

و شکل تابع fwrite برای نوشتن فایل باینری به شکل زیر است:

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

این دو تابع شباهت بسیاری در آرگومان ها به یکدیگر دارند.

حال برنامه قبل را برای حالت باینری مینویسیم!

#include 
#include 

int main()
{
    char str [50];
    FILE *fp;
    fp = fopen("alefbair.txt","wb");

    printf("Enter a String to Save in alefbair.txt\n");
    scanf("%[^\n]s",str);

    fwrite(str,sizeof(str),1,fp);
    fclose(fp);

    return 0;
}

شاید بپرسید که مگه شما نگفتید که فایل باینری برای انسان قابل خواندن نیست پس چطور این فایل باز میشود؟! بله همینطور است اما وقتی کاراکتری در فایل متنی ذخیره میشود تفاوتی با اینکه در فایل باینری ذخیره شود نمیکند! چون هر دو یک کدی را ذخیره میکنند!

برای اینکه بهتر درک کنید یک مثال میزنم! اگر تا حالا کنجکاوی کرده باشید و یک فایل غیر قابل باز شدن از نظر متنی (یعنی متن نباشد و نشود با notepad آن را باز کرد) را با notepad باز کرده باشید کاراکتر های عجیب و غریب میبینید اما در بین آنها متن هایی هم دیده میشود! شما این موضوع رو هم همونطوری فرض کنید!

وقتی بخواهید یک برنامه بنویسید که یک int را ذخیره کند به خوبی متوجه تفاوت این دو خواهید شد!

حال دو برنامه برای ذخیره عدد در دوحالت متنی و باینری مینویسیم :

برای حالت متنی:

#include 
#include 

int main()
{
    int a = 65;
    FILE *fp;
    fp = fopen("alefbair.txt","w");

    fprintf(fp,"%d",a);
    fclose(fp);

    return 0;
}

فایل alefbair.txt به صورت زیر میشود:

65

که اگه به مقدار فایل نگاه کنید اندازه اش 2 بایت هست!

و حالا برای حالت باینری مینویسیم:

#include 
#include 

int main()
{
    int a = 65;
    FILE *fp;
    fp = fopen("alefbair.txt","wb");
    fwrite(&a,sizeof(int),1,fp);
    fclose(fp);

    return 0;
}

اگر فایل ایجاد شده در بالا رو باز کنید به شکل زیر هست:

A

که اگه به اندازه فایل نگاه کنید میبینید نوشته 4 بایت چون هر int مقدار 4 بایت هست!

مزیت دیگه ی استفاده از روش باینری این هست که میتونید یک struct رو به طور کامل بنویسید و سپس بازیابی کنید! به شکل زیر:

#include 
#include 
#include 

struct student {
    char student_number [11];
    int age;
};

int main()
{
    struct student student;
    FILE *fp;
    fp = fopen("alefbair.bin","wb");
    strcpy(student.student_number,"1234567890");
    student.age = 19;
    fwrite(&student,sizeof(struct student),1,fp);
    fclose(fp);
    return 0;
}

و برای خواندن همان ساختار (باید فایل بالا ایجاد شده باشد):

#include 
#include 
#include 

struct student {
    char student_number [11];
    int age;
};

int main()
{
    struct student student;
    FILE *fp;
    fp = fopen("alefbair.bin","rb");
    fread(&student,sizeof(struct student),1,fp);

    printf("Student Number = %s\n",student.student_number);
    printf("Age = %d\n",student.age);

    fclose(fp);
    return 0;
}

اگر برنامه اولی رو اجرا کنید یک فایل خروجی میدهد که در برنامه دوم آنفایل خوانده میشود و اطلاعات آن روی صفحه نشان داده میشود!

که به صورت زیر خواهد بود:

Student Number = 1234567890
Age = 19

دیدید که خیلی راحت میشه اطلاعات رو بازیابی کرد. برای اینکه کاملا متوجه بشوید که در حالت متنی برای انجام اینکار به چه چیزی نیاز داشتید و کار شما چه قدر دشوار تر میشد میتوانید کد بالا را برای فایل متنی بنویسید!

 

» توابع مشترک باینری و متنی: 

حالا شاید بخواهید در زبان C یک فایل را حذف کنید. برای اینکار از تابع زیر استفاده کنید!

int remove (char  *filename)

توجه داشته باشید که در تابع فوق filename آدرس فایل هست نه اشاره گر!

برای فهم بیشتر مثال زیر را آوردم که از کاربر یک آدرس دریافت میکند و فایل مربوطه را حذف میکند.

#include 
#include 

int main()
{
   char str[20];
   printf("Please Enter Addres file for remove: \n");
   scanf("%s",str);
   if ((remove (str)) != 0){
    printf("the file can't remove \n");
   }
   else  printf("file deleted! \n");
    return 0;
}

 

برای جابه جا شدن در طول فایل مثلا برگشت به ابتدا یا رفتن به انتها یا تغییر مکان نسبت موقعیت فعلی! (مثلا دو کاراکتر جلوتر برو) میتوانید از تابع fseek استفاده کنید!

int fseek(FILE *stream, long int offset, int whence)

stream : اشاره گر فایل است

offset : میزان جابه جایی نسبت به whence (پارامتر سوم)

whence : مکان جدید! میتواند فقط مقادیر 

  1. SEEK_SET : ابتدای فایل   
  2. SEEK_CUR : مکان فعلی   
  3. SEEK_END : انتهای فایل

را داشته باشند.

ممکن است کمی گنگ به نظر برسد برای همین یک مثال میزنم! فرض کنید:

میخواهید به اول فایل بروید باید whence  را SEEK_SET و آفست (offset) را 0 انتخاب کنید!

میخواهید به آخر فایل بروید باید whence را SEEK_END و آفست (offset) را 0 انتخاب کنید!

میخواهید همینجا که هستید بمانید باید whence را SEEK_CUR و آفست (offset) را 0 انتخاب کنید!

میخواهید دو بایت (کاراکتر) جلوتر بروید باید  whence را SEEK_CUR و آفست (offset) را 2 انتخاب کنید!

میخواهید دو بایت (کاراکتر) عقبتر بروید باید  whence را SEEK_CUR و آفست (offset) را 2- انتخاب کنید!

میخواهید به بایت دهم فایل بروید باید whence  را SEEK_SET و آفست (offset) را 10 انتخاب کنید!

امیدوارم مثال ها رو متوجه شده باشید! 

 

تابع ftell به ما میگوید که ما در حال خواندن بایت چندم از فایل هستیم:

long ftell(FILE *pointer)

به عنوان مثال میخواهیم اندازه یک فایل را به کمک این تابع بدست آوریم:

#include 
#include 

int main()
{
    int size;
    char ch;
    FILE *fp;
    fp=fopen("file.txt","r");

    if (!fp) { /* Check Open File */
        printf("Can't Open File :(\n");
        exit(0);
    }

    fseek(fp,0,SEEK_END);
    size = ftell(fp);
    printf("Size of File = %d byte",size);
    fclose(fp);
}
  

تابع rewind ما را به ابتدای فایل میبرد!

void rewind(FILE *stream)

امیدوارم این مطلب به دردتون خورده باشه...!

مخلص همتونم!

فعلا یا علی مدد...!