כל מה שתוכנית מחשבת נעלם כשהיא יוצאת, אלא אם היא כותבת אותו. ממשק הקבצים של C נמצא ב-<stdio.h>, אותו קובץ כותרת של printf, והוא דומה בכוונה: קובץ הוא זרם (stream) של בתים, ולפונקציות שעובדות על המסך יש תאומות שמקבלות ארגומנט של זרם.
טיפוס הידית הוא FILE *. אף פעם לא מסתכלים לתוך FILE; זה טיפוס אטום (ראו typedef) ומחזיקים רק מצביע אליו.
שלושת השלבים
כל פעולה על קובץ הולכת לפי אותה צורה: פותחים, משתמשים, סוגרים.
בדיקת ה-NULL אינה פרנויה אופציונלית. fopen נכשלת בכל פעם שהקובץ לא קיים (במצב קריאה), שאין הרשאת כתיבה לתיקייה, שהנתיב שגוי או שלתהליך נגמרו ידיות הקבצים, וכל אחד מהמקרים האלה מחזיר NULL. שימוש בידית NULL הוא segmentation fault.
שליחת השגיאה ל-stderr במקום ל-stdout היא המוסכמה: הוא לא עובר דרך חוצץ, ואפשר להפנות אותו בנפרד מהפלט האמיתי.
המצבים של fopen
הארגומנט השני הוא מחרוזת מצב קצרה. טעות בה היא הטעות ההרסנית ביותר בכל התחום הזה, כי "w" מרוקן בשקט קובץ קיים.
| מצב | קריאה | כתיבה | אם הקובץ קיים | אם הוא לא קיים |
|---|---|---|---|---|
"r" | כן | לא | נפתח בהתחלה | נכשל, מחזיר NULL |
"w" | לא | כן | מרוקן לגמרי | נוצר |
"a" | לא | כן | כותב רק בסוף | נוצר |
"r+" | כן | כן | נפתח בהתחלה | נכשל, מחזיר NULL |
"w+" | כן | כן | מרוקן לגמרי | נוצר |
"a+" | כן | כן | קורא מכל מקום, כותב בסוף | נוצר |
הוסיפו b לכל אחד מאלה ("rb", "wb", "ab+") למצב בינארי; עוד על זה בסוף.
שני כללים שמונעים אובדן נתונים אמיתי:
- השתמשו ב-
"r"כשאתם מתכוונים לקרוא."r+"על שם קובץ עם שגיאת הקלדה נכשל בבטחה;"w+"יוצר קובץ ריק ולא איבדתם כלום חוץ מזמן."w"על שם הקובץ הנכון כשהתכוונתם ל-"r"הורס את הנתונים. - השתמשו ב-
"a"ליומנים. כלfprintfנוחת בסוף בלי קשר למיקום של הזרם, וזה בדיוק מה שיומן רוצה.
כתיבה וקריאה בתוכנית אחת
העורך שלמטה מריץ סבב מלא: הוא יוצר קובץ, כותב לתוכו רשומות, סוגר אותו, פותח אותו מחדש לקריאה ומדפיס את מה שהוא מוצא.
fprintf ו-fscanf הן printf ו-scanf עם זרם כארגומנט הראשון; כל מה שנוגע למצייני הפורמט שלהן זהה, כולל הרוחב %31s ששומר על name מגלישה.
תנאי הלולאה הוא == 2, מספר הפריטים שהפורמט מבקש. בדיקה מול EOF במקום זה היא באג קלאסי: שורה פגומה גורמת ל-fscanf להחזיר 0, לא EOF, והלולאה מסתובבת לנצח על קלט שהיא לא יכולה לצרוך.
קריאה שורה אחר שורה עם fgets
fscanf נוחה לנתונים בפורמט נוקשה. עבור קובצי טקסט, קובצי הגדרות, יומנים, CSV, כל דבר שאדם כתב, קראו שורות שלמות. זו הלולאה הקלאסית:
מה שהופך את fgets לברירת המחדל הנכונה:
- היא מקבלת את גודל החוצץ, ולכן לא יכולה לגלוש. העבירו
sizeof lineוהקריאה נשארת נכונה גם אם תשנו את גודל המערך. - היא מחזירה
NULLבסוף הקובץ או בשגיאה, וזה תנאי לולאה נקי. - היא שומרת את תו השורה החדשה כשהשורה נכנסה בחוצץ. זה שימושי: אם אין
'\n'במה שקיבלתם, השורה הייתה ארוכה מהחוצץ ושאר השורה עדיין מחכה.strcspn(line, "\n")מוצאת את האינדקס של תו השורה החדשה (או את אורך המחרוזת אם אין כזה), כך שהשמת'\0'שם חותכת אותו בכל מקרה.
כדי להבחין בין סוף קובץ אמיתי לבין שגיאה, שאלו אחרי הלולאה:
if (ferror(in)) {
fprintf(stderr, "read error\n");
} else if (feof(in)) {
/* normal end */
}
אל תכתבו while (!feof(fp)) כתנאי לולאה. feof הופכת לאמת רק אחרי שקריאה כבר נכשלה, כך שהלולאה הזו מעבדת את התוכן האחרון של החוצץ פעם נוספת. בדקו במקום זה את ערך ההחזרה של פונקציית הקריאה, כמו ששתי הלולאות למעלה עושות.
תו אחר תו: fgetc ו-fputc
לעבודה ברמת הבית, ספירת תווים, שינוי של קובץ, העתקה, fgetc ו-fputc מטפלות בתו אחד בכל קריאה.
פרט אחד שחשוב: c מוצהר כ-int, לא כ-char. fgetc מחזירה int כדי שתוכל להחזיר כל ערך אפשרי של בית וגם את הזקיף הנפרד EOF (שהוא -1). שמירתו ב-char הופכת את ההשוואה מול EOF לשקר תמיד, או לאמת בטעות עבור הבית 0xFF, תלוי אם char הוא עם סימן בפלטפורמה שלכם. זו אחת המלכודות הוותיקות ביותר של C.
בדיקת שגיאות כמו שצריך
קריאה בקוד ייצור נראית כך:
errno מחזיק קוד שמתאר את הכישלון האחרון ו-strerror הופכת אותו למשפט; perror מדפיסה את ההודעה שלכם ועוד את המשפט הזה בקריאה אחת. הן צריכות את <errno.h> ואת <string.h> בהתאמה.
גם fclose יכולה להיכשל: היא מרוקנת נתונים מהחוצץ, והכתיבה עלולה לא להיכנס בדיסק, אז בכל דבר חשוב, בדקו אותה:
if (fclose(fp) != 0) {
fprintf(stderr, "failed to flush and close\n");
}
מצב בינארי בפסקה אחת
מצב טקסט עשוי לתרגם סיומות שורה (ב-Windows, \n הופך ל-\r\n בכתיבה וחוזר בקריאה) ועשוי להתייחס לבתים מסוימים באופן מיוחד. לנתונים שאינם טקסט, תמונה, struct שנשפך כמו שהוא, גוש דחוס, פתחו עם b והשתמשו ב-fread/fwrite, שמעבירות בתים גולמיים:
fwrite(ptr, size, count, fp) כותבת count פריטים של size בתים ומחזירה כמה מהם היא הצליחה לכתוב; fread היא תמונת המראה שלה. שימו לב שקובץ שנכתב כך קשור למכונה שכתבה אותו: ריפוד של structs, גודל של מספרים שלמים וסדר בתים כולם דולפים לתוך הבתים, כך שזה בסדר לקובץ מטמון או לקובץ זמני ושגוי לפורמט שתוכניות אחרות צריכות לקרוא.
טעויות נפוצות
- לא לבדוק את
fopenמולNULL. הקריסה שבאה אחר כך מואשמת בקריאה, לא בפתיחה. - פתיחה עם
"w"כשהתכוונתם ל-"r". הקובץ מרוקן לפני שאתם שמים לב. - לשכוח
fclose. פלט בחוצץ הולך לאיבוד, וכל קובץ שלא נסגר מדליף ידית. while (!feof(fp)). מעבד את השורה האחרונה פעמיים. בדקו במקום זה את ערך ההחזרה של פונקציית הקריאה.char c = fgetc(fp). שובר את ההשוואה מולEOF. השתמשו ב-int.fscanf("%s", buf)בלי רוחב. אותה גלישת חוצץ כמו scanf מהמקלדת.- נתיבים יחסיים.
fopen("data.txt", "r")מחפשת בתיקיית העבודה, לא ליד קובץ ההרצה. אם קובץ "נעלם", זו בדרך כלל הסיבה.
שאלות נפוצות
איך פותחים קובץ ב-C?
FILE *fp = fopen("data.txt", "r"); פותח קובץ לקריאה ומחזיר ידית מטיפוס FILE *, או NULL אם זה נכשל. תמיד בדקו מול NULL לפני שמשתמשים בידית, וקראו ל-fclose(fp) כשמסיימים.
מה הם המצבים של fopen ב-C?
"r" קריאה (הקובץ חייב להתקיים), "w" כתיבה (יוצר, או מרוקן קובץ קיים לגמרי), "a" הוספה (יוצר, כותב בסוף). הוספת + הופכת כל אחד מהם לקריאה וכתיבה: "r+", "w+", "a+". הוספת b ("rb", "wb") פותחת במצב בינארי.
איך קוראים קובץ שורה אחר שורה ב-C?
השתמשו ב-fgets בתוך לולאת while: while (fgets(line, sizeof line, fp) != NULL) { ... }. היא עוצרת בכל תו שורה חדשה או כשהחוצץ מלא, מחזירה NULL בסוף הקובץ, ולא יכולה לגלוש כי מעבירים לה את גודל החוצץ.
למה הקובץ שלי ריק אחרי שכתבתי אליו ב-C?
לרוב שכחתם fclose. הפלט עובר דרך חוצץ, כך שהנתונים עשויים עדיין לשבת בזיכרון כשהתוכנית מסתיימת באופן לא תקין. fclose מרוקנת את החוצץ וסוגרת; fflush(fp) מרוקנת בלי לסגור. הסיבה האחרת היא פתיחה עם "w" בפעם השנייה, שמרוקנת את הקובץ שזה עתה כתבתם.