דוא"ל:
תפריט משתמש




שתף |

קורס:"שפת C"

שיעור 13: קלט / פלט עם קבצים

[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]

מתוך הספר: C - מדריך מקצועי       C - מדריך מקצועי
תוכן עניינים



סוגי קבצים

כדי לשמור מידע בין ריצות עוקבות של התוכנה מבצעים שמירת נתונים לקבצים בדיסק. ניתן לשמור נתונים בשני סוגי פורמט:

    פורמט טקסט (ascii) - הנתונים מאוחסנים בקובץ כתווים.
    פורמט בינרי - הנתונים מאוחסנים בקובץ כפי שהם בזיכרון.

קיימים 3 שלבים בטיפול בקבצים:

    1. פתיחת קובץ
    2. קריאה/כתיבה לקובץ
    3. סגירת הקובץ

בפתיחת קובץ מקבלים ממערכת ההפעלה מצביע מטיפוס FILE. טיפוס זה הוא רשומה ששדותיה מתארים תכונות שונות של הקובץ.

פונקציות הקלט/פלט לקבצים מקבלות כולן מצביע ל- FILE כפרמטר. הטיפוס FILE ופונקציות הקלט/פלט לקבצים מוגדרים בספרייה stdio.h.

קיימים 3 קבצי קלט/פלט תקניים הנפתחים ע"י מערכת ההפעלה בתחילת התכנית ומוגדרים להם 3 מצביעים בהתאם:

    stdin - מצביע לקובץ הקלט התקני
    stdout - מצביע לקובץ הפלט התקני
    stderr - מצביע לקובץ השגיאה התקני

פתיחת קובץ

ראשית מגדירים מצביע לקובץ:

 
FILE  *in_fp;      /* input file pointer */
FILE  *out_fp;   /* output file pointer */
 

קוראים לפונקציה fopen() ומעבירים לה פרמטרים: שם קובץ לפתיחה ומוד הפתיחה

 
in_fp = fopen("file.dat", "rt"); 
out_fp = fopen("file.dat", "wt");
 

המחרוזת "rt" מציינת פתיחה במוד קריאה ובפורמט טקסט, המחרוזת "wt" מציינת פתיחה במוד כתיבה ובפורמט טקסט.

במידה ופעולת הפתיחה/סגירה נכשלת מוחזר NULL ע"י הפונקציה fopen().

דוגמא לפתיחת קבצים במוד בינרי:

 
in_fp = fopen("file.dat", "rb"); 
out_fp = fopen("file.dat", "wb");
 

האות b מציינת פורמט בינרי. קיימים מודי פתיחה נוספים - פתיחה לקריאה וכתיבה("rw") , פתיחה להוספה ("a" - append) כאשר ברירת המחדל - אם לא צויין אחרת - היא פורמט קובץ טקסט.

קריאה / כתיבה לקובץ

קיימים 2 פורמטים לאחסון נתונים בקובץ: פורמט טקסט ופורמט בינרי.

בפורמט טקסט הנתונים נשמרים עפ"י הייצוג המחרוזתי שלהם, כלומר בקוד ה- ASCII של התווים.

בפורמט בינרי לעומת זאת נשמר הנתון בדיוק כפי שהוא מיוצג בזיכרון.

לדוגמא, המספר ההקסה-דצימלי F123456 יאוחסן בקובץ בפורמט טקסט ע"י 7 תווי ה- ASCII המרכיבים אותו:

70

49

50

51

52

53

54

(כל משבצת מתארת בית בודד בקובץ). 70 הוא קוד ASCII  של האות 'F', 49 הוא קוד ASCII של הספרה '1',  50 הוא קוד ה- ASCII של הספרה '2' וכן הלאה.

לעומת זאת, בפורמט בינרי המספר יישמר במערכת שבה שלם תופס 4 בתים כך:

0F

12

34

56

מסקנה: פורמט בינרי הוא חסכוני יותר ואילו פורמט טקסט הוא נוח יותר מבחינת היכולת לקרוא ולהבין את הקובץ.

קריאת וכתיבת טקסט  לקובץ

הקריאה והכתיבה לקובץ טקסט דומה לפעולות המקבילות בקלט/פלט התקני: לפונקציות הקלט/פלט התקני מתאימות פונקציות קלט/פלט לקובץ עם האות f בתחילתן, כאשר פרמטר נוסף המועבר הוא המצביע לקובץ.

לדוגמא, המקבילות לפונקציות printf ו- scanf הן fprintf ו- fscanf:

 
fscanf(in_fp, "%d %s", &num, str);
fprintf(out_fp, "%d %s", num, str);
 

הפונקציות fputc ו-fgetc כותבות וקוראות תו בודד אל/מקובץ. ממשק הפונקציות:

 
int fputc( int c, FILE *fp);
int fgetc( FILE *fp);
 

עיין/י בתכנית הדוגמא המובאת בעמ' 334 המעתיקה את הקובץ autoexec.bat לקובץ גיבוי autoexec.bak.



קריאה וכתיבה בינריים לקובץ

כתיבה בינרית לקובץ מבצעת העתקה של תמונת הזיכרון של הנתונים בזיכרון לקובץ. קריאה בינרית מבצעת את הכיוון ההפוך. כתיבה וקריאה בינריים מבוצעות ע"י הפונקציות fread() ו- fwrite() :

 
size_t   fread( void *buffer,              size_t size, size_t count, FILE *fp );
size_t   fwrite( const void *buffer,   size_t size, size_t count, FILE *fp );
 

שתי הפונקציות מקבלות את הפרמטרים:

    buffer - מצביע לחוצץ הנתונים המיועדים לכתיבה / לקריאה
    size - גודלו של איבר בודד במערך בבתים
    count  - מספר האיברים : 1 לנתון בודד, למערך - מספר האיברים הנכתבים
    fp - מצביע לקובץ

הפונקציה fwrite מעתיקה מהמקום בזיכרון ש- buffer מצביע עליו size*count בתים לקובץ. בדומה, fread מבצעת את הקריאה.

הערך המוחזר - מטיפוס  size_t - משתי הפונקציות הוא מספר הבתים שנקראו / נכתבו. size_t הוא מטיפוס (typedef) שלם לא מסומן המציין גודל של נתון בזיכרון.

תכנית הדוגמא שבעמ' 336 מדגימה כתיבה וקריאה של סוגי נתונים שונים לקובץ בפורמט בינרי.

חיפוש בקבצים בינריים

אחת הפעולות השכיחות בטיפול בקבצים בינריים היא חיפוש של רשומה כלשהי.

שימוש נפוץ לחיפוש מעין זה קיים בתוכנות בסיסי נתונים, בביצוע שאילתות או בהפקת דוח"ות. בסעיף זה נראה כיצד לחפש רשומות בקובץ בינרי.

הפונקציה fseek מאפשרת להזיז את סמן הקריאה/כתיבה של הקובץ ממקומו הנוכחי למיקום מסויים בקובץ:

int fseek( FILE *fp, long offset, int origin );

הפונקציה מקבלת 3 פרמטרים:

  • fp - מצביע לקובץ
  • offset - מספר הבתים להזזה ביחס ל- origin
  • origin - מיקום התחלתי. ערך זה יכול להיות אחד מהשלושה:
    SEEK_SET - התחלת הקובץ
    SEEK_CUR - המיקום הנוכחי של סמן הקריאה/כתיבה של הקובץ
    SEEK_END - סוף הקובץ


לדוגמא, נתון קובץ בינרי של רשומות מטיפוס Student הכולל מספר מזהה, שם וממוצע ציונים:

 
typedef struct 
{
          int              ID;
          String       name;
          double       average;
} Student;
 

בקובץ מערך רשומות סטודנטים ממויינות עפ"י מספר הזהות:

הקובץ כולל 10 רשומות. סמן הקריאה/כתיבה של הקובץ נמצא לאחר הרשומה החמישית ולפני הרשומה השישית. למשל, ניתן להזיז את הסמן לרשומה השלישית ע"י

 
          fseek(file, 2*sizeof(Student), SEEK_SET);         
 

הסבר: הפונקציה fseek מזיזה את סמן הקריאה/כתיבה ביחס לראשית הקובץ(SEEK_SET) למיקום 2*sizeof(Student), שהוא מקום התחלת הרשומה השלישית.

כמו כן, ניתן לקרוא את הרשומה מהקובץ ע"י fread ולהדפיס את ערכיה.

לדוגמא, הפונקציה הבאה מזיזה את הסמן לרשומה התשיעית (אינדקס 8), קוראת ומדפיסה את ערכי הרשומה:

 
void f(FILE *file)
{
          Student s;
          fseek(file, 8*sizeof(Student), SEEK_SET);          
          fread(&s, sizeof(Student), 1, file); /* read record */
          printf("\nRecord %d name is %s, avg=%f",s.ID, s.name, s.average);
}
 

מודפס:

 
Record 38 name is Yfat, avg=8.500000
 

פונקציה לחיפוש בינרי בקובץ

הפונקציה הבאה מבצעת חיפוש בינרי של רשומה בקובץ. הפונקציה מקבלת כפרמטרים מצביע לקובץ ומספר מזהה לחיפוש:

 
void bin_search(FILE *file, int ID)
{
          int file_size;
          int min, max, avg;
          Student s;
          Boolean found;
 
  - ראשית מחשבים את גודל הקובץ בבתים ע"י הזזת הסמן לסוף הקובץ, וקריאה לפונקציה ftell, המחזירה את מיקום הסמן ביחס לתחילת הקובץ בבתים:
 
          fseek(file, 0, SEEK_END);  
          file_size = ftell(file);
 
  - כעת מתחיל תהליך החיפוש: מאתחלים את שני המשתנים, min ו- max:
 
          min=0; 
          max=file_size/sizeof(Student) - 1;
          printf("\nSearching for record %d...reading records ", ID);
 
  - בלולאה הבאה מבצעים את החיפוש הבינרי: בכל חזרה, מחשבים את החציון, avg, ומשווים את ערך המספר המזהה שמחפשים עם זה של הרשומה שבחציון. בהתאם לכך ממשיכים לחפש בחציון העליון, או התחתון:
 
          for(found=FALSE; min <= max; )
          {
                   avg = (max + min)/2;
                   fseek(file, avg*sizeof(Student), SEEK_SET); /* move to the record avg */
                   fread(&s, sizeof(Student), 1, file); /* read record */
                   printf("%d ", s.ID);
                   if(s.ID==ID)
                   {
                            printf("\nRecord %d found: name is %s",s.ID, s.name);
                            found = TRUE;
                            break;
                   }
                   else
                            if(ID < s.ID)
                                      max = avg - 1;
                            else
                                      min = avg + 1;
          }
          if(!found)
                   printf("\nRecord %d not found!", ID);
}
 
    בכל חזרה מזיזים את min או max לכיוון החציון (עפ"י תוצאת ההשוואה) עד למציאת הרשומה, או עד ש- min ו- max בעלי ערך זהה, מה שמציין שהרשומה שמחפשים לא קיימת בקובץ.

התכנית המשתמשת

התכנית המשתמשת מובאת בעמ' 340.

הוספת שמירה לקובץ לתוכנית הספרייה

בעיה: הנתונים שהמשתמש מקליד בכל הרצה של תכנית הספרייה שכתבנו הולכים לאיבוד בסופה. בכל הרצה של התכנית מתחילים מאפס.

כיצד נשמור את הנתונים בין ריצות עוקבות של התכנית?

פתרון: נשמור את הנתונים לקובץ, ובהרצת התכנית נקרא אותם מתוכו.

הוספת פונקציות שמירה (Save)

נוסיף למודול book פונקציה לשמירת רשומת ספר בודד לקובץ בפורמט טקסט:

 
void book_save_to_file(FILE *fp, BookPtr pbook)
{
          fprintf(fp, "\n%s\n%s\n%s\n%d\n%f\n%d",
                   pbook->name, 
                   pbook->writer, 
                   pbook->publisher, 
                   pbook->year, 
                   pbook->cost, 
                   pbook->ID);
}
 

נתוני הספר נכתבים לקובץ ע"י fprintf נתון בשורה. למודול booklist נוסיף פונקציה לשמירת כלל הרשימה לקובץ:

 
void  list_save_to_file(List *plist, char *file_name);
 

קוד הפונקציה והסבר מובאים בעמ' 342.

פונקציות טעינה מהקובץ

מאחר שנתוני התכנית נשמרים לקובץ, יש להגדיר פונקציה שקוראת אותם מהקובץ למשתנים בזיכרון.

לפני כן, נגדיר פונקצית עזר לקריאת שורה מקובץ:

 
char *getline(FILE *fp)
{
          char temp[80];
 
          fgets(temp, 80, fp);
          temp[strlen(temp)-1] = '\0'; /* instead of the '\n' inserted by fgets */
          return strdup(temp);
}
 

הסבר: הפונקציה קוראת שורת טקסט מהקלט ע"י fgets ומחזירה שיכפול שלה - ע"י strdup - לפונקציה הקוראת.

לפני ההחזרה, הפונקציה מתקנת בעיה שנגרמת ע"י הפונקציה fgets(): השורה שנקראה מהקלט מכילה גם את תו השורה החדשה '\n' כתו אחרון במחרוזת (לפני תו סיום המחרוזת '\0').

כדי למחוק את התו האחרון '\n' מציבים את תו סיום המחרוזת במקומו:

 
temp[strlen(temp)-1] = '\0';
 

הפונקציה הקוראת רשומת ספר מהקובץ מוגדרת כך במודול book :

 
void book_read_from_file(FILE *fp, BookPtr pbook)
{
          String temp;
          pbook->name = getline(fp);
          pbook->writer = getline(fp);
          pbook->publisher  = getline(fp);
          fscanf(fp, "%d", &pbook->year);
          fscanf(fp, "%f", &pbook->cost);
          fscanf(fp, "%d", &pbook->ID);
          fgets(temp, MAX_STR_LEN, fp); /* to read up to the end of line */
}
 

במודול booklist מוגדרת הפונקציה הטוענת את כל רשומות הספרים מהקובץ לרשימה המקושרת :

 
void  list_load_from_file(List *plist, char *file_name);
 

קוד הפונקציה והסבר מובאים בעמ' 343.

הפונקציה הראשית main()

הקריאה לפונקצית השמירה לקובץ מבוצעת בתכנית הראשית כתגובה לבחירת המשתמש באופציה s - שמירה לקובץ - שהוספה לתפריט:

 
void menu()
{
          printf("\n\n\n\n\n");
          puts("Book program main menu");
          .
          .
        puts("s. Save books records to file");
          puts("x. Exit");
          puts("\n\n\n\n\n\n\n\n");
}
 

הקריאה לפונקצית הטעינה לעומת זאת, נקראת תמיד בתחילת התכנית בכדי לטעון את הנתונים הקיימים:

 
int main()
{
        char  *DATA_FILE_NAME = "book.out";
          ...
          list_init(&list);
 
        list_load_from_file(&list, DATA_FILE_NAME);
          for(;;)  /* forever */
          {
                   ...
                   switch(c)
                   {
                            .
                            .
                   case 's': /* save to file */
                       list_save_to_file(&list,DATA_FILE_NAME);
                            pause();
                            break;
                   default:
                            break;
                   }
          }
          return 0;
}
 

מובן שלצורך כך יש להכריז על 2 הפונקציות הנ"ל בקובץ הממשק booklist.h.

שמירה ביציאה מהתכנית

המשתמש יכול כעת לבצע שמירה יזומה לתכנית, בכל רגע שירצה. אולם מה אם הוא ישכח ויבקש לצאת מהתכנית לפני ששמר את השינויים האחרונים שביצע?

כאשר המשתמש מבקש לסיים את התכנית, נהוג לבדוק אם הוא ביצע שינויים במבני הנתונים מאז השמירה האחרונה ואם כן, לשאול אותו האם ברצונו לבצע שמירה. עפ"י החלטתו מבוצעת השמירה.

שאלה: כיצד נדע בבקשת היציאה אם בוצע שינוי בתכנית מאז השמירה האחרונה?

תשובה: נחזיק משתנה בוליאני - בעל ערך "אמת" או "שקר" - שיציין בכל רגע נתון האם נתוני התכנית שונו מאז השמירה האחרונה או לא.

ערכו של הנתון יהיה בתחילת התכנית "שקר", ולאחר כל ביצוע שינוי - קרי הוספת ספר או הסרת ספר - הוא ישונה ל- "אמת".

לאחר שמירת הנתונים לקובץ שוב יוצב למשתנה ערך "שקר".

נגדיר משתנה בוליאני בטיפוס הרשימה שיציין האם נתוני הרשימה שונו מאז השמירה האחרונה לקובץ:

 
typedef struct 
{
          ListNode  *head;        /* head of the book list */
          ListNode  first;           /* first dummy element */
        Boolean  changed_fl; /* list change flag */
          int              count;
} List;
 

ערכו של המשתנה מאותחל ל- FALSE ומעודכן בכל שינוי במבנה הנתונים.

עיין/י בקוד הפונקציות בהן הוא מעודכן בעמ' 345-347.

סיכום

  • כדי לשמור את הנתונים שהמשתמש עורך בתכנית בין ריצות עוקבות שלה, יש לשמור אותם לקובץ.
  • קיימים 2 פורמטים לכתיבה/קריאה ל/מקובץ:
  - פורמט בינרי
  - פורמט טקסט

בפורמט בינרי הנתונים נשמרים לקובץ בדיוק כפי שהם מאוחסנים בזיכרון. בפורמט טקסט הנתונים מפורקים לתווים והם נשמרים בייצוג התווי שלהם.

  • קיימים מספר שלבים בשמירת / שחזור נתונים אל/מקבצים:
  - פתיחת הקובץ במוד המתאים (קריאה/כתיבה, טקסט/בינרי)
  - ביצוע פעולות הקריאה/כתיבה
  - סגירת הקובץ
  • הפונקציות לפתיחת וסגירת קובץ הן fopen ו- fclose בהתאמה.
  • הפונקציות לקריאה וכתיבה בינריים הן fread() ו- fwrite(). הפונקציות לכתיבה וקריאה במוד טקסט הן פונקציות מקבילות לפונקציות הקלט/פלט התקניות בתוספת האות f בתחילתן: fscanf(), fprintf(), fgetchar() וכו'.
  • הפונקציה fseek משמשת להזזת סמן הקריאה/כתיבה של הקובץ למיקום מסויים. הפונקציה ftell מחזירה את מרחק הסמן מתחילת הקובץ בבתים.

תרגילי סיכום

בצע/י את תרגילי הסיכום שבסוף פרק זה.



[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]