4.3 קבצים הרצאה
בlibc יש לנו ממשק נוח לעבודה עם קבצים, כדי לגרום לתוכנה שלנו לעבוד עם קובץ מסוים במערכת ההפעלה- עלינו לעשות לו open. אחרי שנבצע open, נקבל "file pointer"- מספר מזהה של הקובץ, באמצעותו נוכל לבצע על הקובץ פעולות שונות.
לאחר שנסיים עם פעולתינו על הקובץ, נצטרך לסגור אותו (לעשות לו close)- כדי לסמן את הקובץ שאנחנו רוצים לסגור אנחנו נשתמש שוב בfile pointer.
הפונקציה fopen משמשת לפתיחת קובץ לקריאה, כתיבה או מצב אחר. היא מחזירה מצביע מסוג FILE*, שמייצג את הקובץ שנפתח. אם הפתיחה נכשלת (למשל כי הקובץ לא קיים), הפונקציה מחזירה NULL.
כדי לפתוח קובץ, נסמן שני דברים
1. שם הקובץ שאותו נרצה לפתוח
2. מצב הפתיחה שנרצה להפעיל על הקובץ
מצב הפתיחה בעצם אומר למערכת ההפעלה אילו פעולות נרצה לבצע על הקובץ.
מצבי פתיחה נפוצים:
-
"r"– קריאה (הקובץ חייב להתקיים) -
"w"– כתיבה (אם הקובץ קיים הוא יימחק, אחרת ייווצר) -
"a"– כתיבה לסוף קובץ -
"r+"– קריאה וכתיבה -
"w+"– כתיבה וקריאה (מוחק תוכן קיים כבר) -
"a+"– קריאה והוספה
חובה לבדוק שהמצביע שהוחזר אינו NULL לפני שימוש בקובץ. (כלומר, אינו 0.)
הפונקציה fclose סוגרת קובץ שנפתח באמצעות fopen. פעולה זו משחררת את המשאבים (את הfile pointer) ומבטיחה שכל פעולת כתיבה שביצענו על הקובץ תשמר (בהמשך אציג איך כותבים).
הפונקציה fread קוראת מידע מהקובץ לתוך כתובת בזכרון.
הפונקציה מקבלת את הכתובת שאלייה היא כותבת (הכתובת של הbuffer), הגודל של הtype של הbuffer, הגודל של הbuffer, והfile pointer
char buffer[100];
FILE *f = fopen("file.bin", "rb");
fread(buffer, sizeof(char), 100, f); // קריאה של עד 100 תווים
fclose(f);
כלומר, פה היא מקבלת את הכתובת של הarray.
את הגודל של הtype שבמקרה הזה זה char (אז הגודל הוא 1).
הכמות תאים של הarray (שבמקרה הזה זה 100), כלומר 100 char-ים.
והfile pointer שממנו צריך לקרוא.
כאשר נעשה open נוסיף "b" למצב הפתיחה כדי לוודא שהקובץ ידע לקרוא תווים לא ascii-ים (כמו שאנחנו מכירים מפייתון)
הפונקציה fwrite כותבת מידע לקובץ מהזכרון. כמו fread
char data[] = "hello";
FILE *f = fopen("out.bin", "wb");
fwrite(data, sizeof(char), strlen(data), f);
fclose(f);
הפונקציה fprintf כותבת לקובץ באמצעות פורמט – כמו printf, אך לקובץ במקום למסך. הפורמט זהה לגמרי.
הפונקציה fscanf קוראת נתונים מקובץ לפי פורמט – כמו scanf, אך מקובץ במקום מהמשתמש.
int age;
char name[100];
FILE *f = fopen("info.txt", "r");
fscanf(f, "%d %s", &age, name);
fclose(f);
שימושית כאשר הקובץ מסודר בפורמט טקסט קבוע. יש לבדוק שפורמט הקריאה מתאים בדיוק לפורמט של הקובץ – אחרת ייתכנו שגיאות או תוצאות שגויות.
הפונקציה fseek מאפשרת לקפוץ למיקום מסוים בתוך קובץ פתוח. זו דרך לשלוט במיקום הקריאה או הכתיבה.
למעשה, כאשר אנחנו משנים את הpointer של הקובץ, (כאשר אנחנו עושים seek)- כאשר נבצע פעולות write ו- read הם יתבצעו מהמיקום הנוכחי של הpointer.
שלושת הערכים האפשריים לפרמטר האחרון:
-
SEEK_SET– מתחילת הקובץ -
SEEK_CUR– מהמיקום הנוכחי -
SEEK_END– מסוף הקובץ
הפונקציה fseek לוקחת את הפרמטר השלישי, ומוסיפה לה offset שמוגדר בפרמטר השני.
כאשר seek_set דואג למקם אותנו בתחילת הקובץ + offset (בדוגמה זה 10), ו- seek_cur ממקם אותנו במקום שהפוינטר נמצא + offset, וseek_end מסוף הקובץ + offset.
הפונקציה ftell מחזירה את המיקום הנוכחי בקובץ (ביחידות בתים), כלומר באיזה תו הקובץ "נמצא" כרגע.
כך שftell אומר לנו איפה הפוינטר כרגע נמצא, ועל פיו נדע לעשות fseek אם רלוונטי.
שילוב של
fseek ו־ftell מאפשר שליטה מלאה על תנועות בתוך קובץ –
קובצי ברירת מחדל – stdin, stdout, stderr¶
כדי לעבוד עם קבצים בצורה כללית, אנחנו צריכים לבצע fopen, לקבל מזהה (FILE*) ולבצע עליו פעולות. אבל למעשה, ברגע שהתוכנית שלנו מתחילה לרוץ, כבר קיימים שלושה קבצים שפתוחים מראש:
- stdin – משמש לקריאה מהמשתמש (לרוב מהמקלדת)
- stdout – משמש להדפסת פלט רגיל (למסך)
- stderr – משמש להדפסת הודעות שגיאה
שלושת הקבצים האלה לא דורשים fopen. הם כבר מוכנים מראש, ואפשר להשתמש בהם מיידית עם fprintf, fscanf, fgets, fputs וכו'. לדוגמה:
למעשה, printf ו־scanf הן קיצור דרך ל־fprintf(stdout, ...) ול־fscanf(stdin, ...).
מה זה file descriptor?¶
מאחורי הקלעים, כל קובץ שאנחנו פותחים (כולל stdin, stdout, stderr) מזוהה על ידי מספר שלם – זהו file descriptor. (או בקיצור fd) גם כאשר אנחנו עובדים עם FILE* של fopen, הוא מבוסס על אותו מנגנון מתחת – מספר שמזהה את הקובץ.
לשלושת קובצי ברירת המחדל יש מספרים קבועים:
| שם | file descriptor |
|---|---|
| stdin | 0 |
| stdout | 1 |
| stderr | 2 |
הfile descriptor-ים משתמשים בפונקציות open, read, write, close.
הן הפונקציות המקבילות של fopen, fwrite, fread, fclose שלמדנו עם file pointer-ים.
עבודה ישירה עם file descriptors – open, read, write¶
כפי שראינו, fopen, fwrite, fread ו־fclose נותנות לנו ממשק נוח לעבודה עם קבצים – אך מאחורי הקלעים הן משתמשות בקריאות ברמה נמוכה יותר: open, read, write ו־close.
הפונקציות הללו לא עובדות עם FILE*, אלא עם file descriptor – מספר שלם (int) שמייצג את הקובץ.
דוגמה: פתיחת קובץ עם open¶
#include <fcntl.h>
#include <unistd.h>
int fd = open("data.txt", O_RDONLY);
if (fd < 0) {
// טיפול בשגיאה
}
- הדגל
"O_RDONLY"פירושו: פתיחה לקריאה בלבד - הפונקציה מחזירה מספר שלם (file descriptor), או
-1אם נכשל
דוגמה: קריאה מהקובץ עם read¶
-
הפונקציה
readמנסה לקרוא עד 100 בתים מהקובץ לתוך המערךbuffer -
מחזירה את מספר הבתים שנקראו בפועל, או
-1אם הייתה שגיאה
דוגמה: כתיבה לקובץ עם write¶
- כותבת את תוכן המחרוזת לקובץ
- מחזירה את מספר הבתים שנכתבו
דוגמה: סגירת הקובץ¶
דוגמה מלאה – קריאה מקובץ והדפסה למסך¶
למעשה, printf מאחורי הקלעים משתמש בקובץ stdout כדי להדפיס (fd=1)
אז נוכל לבצע write לfd=1 בעצמנו כדי לכתוב למסך.
#include <fcntl.h>
#include <unistd.h>
int main() {
char buffer[100];
int fd = open("data.txt", O_RDONLY);
if (fd < 0) return 1;
ssize_t n = read(fd, buffer, sizeof(buffer));
if (n > 0) {
write(1, buffer, n); // 1 = stdout
}
close(fd);
return 0;
}
בשלבים מתקדמים יותר נלמד מתי באמת נעדיף גישה אחת על פני השנייה, כולל דוגמאות ממערכות אמיתיות, קוד קרנל, ו־syscall-ים ישירים.
כעת, מספיק לדעת ששתי האפשרויות קיימות, ושכל fopen שאנחנו כותבים – מתחת לפני השטח משתמשת ב־open.
מה יש ב־FILE* (fopen וכו') שאין ב־file descriptor?¶
הנה כמה פיצ'רים חשובים שיש בפונקציות של FILE* (כלומר, הפונקציות שמתחילות ב־f: fopen, fread, fprintf וכו'):
1. תמיכה בBuffer פנימי (זיכרון ביניים)¶
-
ה
FILE*מבצעת buffering אוטומטי – כלומר היא שומרת נתונים בזיכרון לפני שהיא כותבת או קוראת מהקובץ בפועל. -
זה משפר ביצועים (פחות קריאות איטיות לדיסק).
-
לעומת זאת,
writeו־readפועלות מיידית – כל קריאה פונה ישירות לדיסק או למערכת ההפעלה.
2. תמיכה בפורמט (fprintf, fscanf)¶
-
FILE*תומכת בהדפסה וקריאה לפי פורמט – כמו%d,%s, בדיוק כמוprintf. -
אין שום תמיכה כזו ב־
write/read– אתה צריך בעצמך להמיר מחרוזות, מספרים וכו'.
3. גישה סימבולית לקובצי ברירת מחדל¶
-
יש משתנים גלובליים מוכנים:
stdin,stdout,stderr -
אפשר להשתמש בהם מיידית עם
fprintf,fscanfוכו' -
ב־fd רגיל אתה צריך לזכור מספרים כמו 0, 1, 2
4. קלות שימוש ונוחות¶
-
הפונקציות של
FILE*יותר קריאות, ברורות ומאובטחות (במיוחדfgets,fprintf,fscanf) -
פחות סיכוי לטעויות נמוכות־רמה כמו overflow בזיכרון
סיכום¶
| תכונה | FILE* (fopen) |
int (open) |
|---|---|---|
| Buffering | כן | לא |
| פורמט (printf/scanf style) | כן | לא |
| נוחות | גבוהה | בסיסית וישירה |
| שליטה נמוכה וזמן אמת | פחות | יותר |