|
||||||||||||||||||||||||||||||||||||||||||
קורס:"שפת C"
מתוך הספר: C - מדריך מקצועי
סוגי קבציםכדי לשמור מידע בין ריצות עוקבות של התוכנה מבצעים שמירת נתונים לקבצים בדיסק. ניתן לשמור נתונים בשני סוגי פורמט:
קיימים 3 שלבים בטיפול בקבצים:
בפתיחת קובץ מקבלים ממערכת ההפעלה מצביע מטיפוס FILE. טיפוס זה הוא רשומה ששדותיה מתארים תכונות שונות של הקובץ. פונקציות הקלט/פלט לקבצים מקבלות כולן מצביע ל- FILE כפרמטר. הטיפוס FILE ופונקציות הקלט/פלט לקבצים מוגדרים בספרייה stdio.h. קיימים 3 קבצי קלט/פלט תקניים הנפתחים ע"י מערכת ההפעלה בתחילת התכנית ומוגדרים להם 3 מצביעים בהתאם:
פתיחת קובץראשית מגדירים מצביע לקובץ: 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 הוא קוד ASCII של האות 'F', 49 הוא קוד ASCII של הספרה '1', 50 הוא קוד ה- ASCII של הספרה '2' וכן הלאה. לעומת זאת, בפורמט בינרי המספר יישמר במערכת שבה שלם תופס 4 בתים כך:
מסקנה: פורמט בינרי הוא חסכוני יותר ואילו פורמט טקסט הוא נוח יותר מבחינת היכולת לקרוא ולהבין את הקובץ.
קריאת וכתיבת טקסט לקובץהקריאה והכתיבה לקובץ טקסט דומה לפעולות המקבילות בקלט/פלט התקני: לפונקציות הקלט/פלט התקני מתאימות פונקציות קלט/פלט לקובץ עם האות 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 );
שתי הפונקציות מקבלות את הפרמטרים:
הפונקציה fwrite מעתיקה מהמקום בזיכרון ש- buffer מצביע עליו size*count בתים לקובץ. בדומה, fread מבצעת את הקריאה. הערך המוחזר - מטיפוס size_t - משתי הפונקציות הוא מספר הבתים שנקראו / נכתבו. size_t הוא מטיפוס (typedef) שלם לא מסומן המציין גודל של נתון בזיכרון. תכנית הדוגמא שבעמ' 336 מדגימה כתיבה וקריאה של סוגי נתונים שונים לקובץ בפורמט בינרי. חיפוש בקבצים בינרייםאחת הפעולות השכיחות בטיפול בקבצים בינריים היא חיפוש של רשומה כלשהי. שימוש נפוץ לחיפוש מעין זה קיים בתוכנות בסיסי נתונים, בביצוע שאילתות או בהפקת דוח"ות. בסעיף זה נראה כיצד לחפש רשומות בקובץ בינרי. הפונקציה fseek מאפשרת להזיז את סמן הקריאה/כתיבה של הקובץ ממקומו הנוכחי למיקום מסויים בקובץ: int fseek( FILE *fp, long offset, int origin ); הפונקציה מקבלת 3 פרמטרים:
לדוגמא, נתון קובץ בינרי של רשומות מטיפוס 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;
fseek(file, 0, SEEK_END); file_size = ftell(file);
min=0; max=file_size/sizeof(Student) - 1; printf("\nSearching for record %d...reading records ", ID);
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); }
התכנית המשתמשתהתכנית המשתמשת מובאת בעמ' 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. סיכום
בפורמט בינרי הנתונים נשמרים לקובץ בדיוק כפי שהם מאוחסנים בזיכרון. בפורמט טקסט הנתונים מפורקים לתווים והם נשמרים בייצוג התווי שלהם.
תרגילי סיכוםבצע/י את תרגילי הסיכום שבסוף פרק זה.
|