קריאה וכתיבה מקבצים בחלונות
מטרת תרגיל זה לתרגל כתיבת והרצת תוכנית פשוטה בחלונות ולתרגל שימוש
בקריאות מערכת בסיסיות לגישה לקבצים.
מבנה תכנית c רגילה בחלונות
(שלוש נקודות מציינות קטעים חסרים):
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <tchar.h>
...
int _tmain(int argc, LPTSTR argv[])
{
if (argc < 2) { /* assuming the program needs 1 argument */
_tprintf( _T("usage: %s <arg>\n"),
argv[0]);
exit(1); /* stop the program; report error */ }
...
return 0; /* successful return */
}
אפילו תוכנית פשוטה כל כך מפגינה שני צדדים מורכבים מעט של פיתוח בסביבת
חלונות. המורכבות הראשונה נובעת מכך שבחלונות ניתן ליצור תוכניות שבהן
תוים יכולים להיות בני 8 או 16 סיביות.
כאשר תוים מיוצגים רק ב--8 סיביות, המשמעות של תו תלויה
בקידוד של הקובץ: למשל, בקובץ טקסט עברי תו מספר 224
מייצג את האות אלף, אבל בקובץ טקסט צרפתי אותו תו מייצג אות לטינית.
כאשר תוים מיוצגים ב--16 סיביות, מערכת ההפעלה משתמשת
תמיד בקידוד בסטנדט בשם יוניקוד (Unicode) שבו יש יצוג נפרד לכל
אות בכל שפה שהיא. בקידוד יוניקוד מחרוזת אחת יכולה להכיל טקסט בעברית,
צרפתית, יפנית וערבית, למשל. על מנת שניתן יהיה לייצר מאותו קובץ מקור
גם תוכניות שבהן תוים מיוצגים ב--8 סיביות וגם תוכניות
שבהן תוים מיוצגים ב--16 סיביות, מייקרוסופט הגדירה טיפוס
נתונים של תוים מוכללים, TCHAR. כאשר בונים את התוכנית
ושני משתני ה-preprocessor שמוגדרים כאן בשורות הראשונות, UNICODE
ו--_UNICODE, מוגדרים, משתנים מטיפוס TCHAR
יתפסו שני בתים (16 סיביות), אבל כאשר המשתנים הללו לא
מוגדרים, משתנים מטיפוס TCHAR יתפסו רק בית אחד. גם המשמעות
של המקרו _T ושל פונקציות כגון _tprintf
תשתנה בהתאם. כאן אנו מעוניינים בקידוד יוניקוד, אבל אם נסיר מהתוכנית
את שתי השורות הראשונות נקבל תוכנית תקינה שבה תוים תופסים בית אחד בלבד.
על מנת לכתוב תוכניות תומכות יוניקוד (וכן תומכות בתוים בני בית אחד),
יש להקפיד על הנקודות הבאות:
-
הפונקציה הראשית בתוכנית היא _tmain ולא main.
- מחרוזות מפורשות יש לעטוף במקרו _T.
- תוים ומערכים של תוים יש להגדיר בעזרת הטיפוס TCHAR במקום
char.
- יש לקרוא לפונקציות ספריה שמטפלות במחרוזות מתוך הספריה של מייקרוסופט
ולא מתוך הספריה הסטנדרטית של שפת C.
על מנת להשלים את הדיון, כדאי לציין שגם רוב מערכות יוניקס ולינוקס תומכות
ביוניקוד, אבל בגישה שונה. הגישה ביוניקס ולינוקס מבוססת בדרך כלל על
קידוד של תווי יוניקוד במספר משתנה של בתים, בפורמט הנקרא UTF-8.
היתרון בגישה זו הוא שמחרוזות שמועברות אל ומאת מערכת ההפעלה ממשיכות
להיות מחרוזות של בתים בודדים (מערכים של char), וכן שהקידוד
של מחרוזות ASCII אינו משתנה (כלומר של מחרוזות של ספרות, סימני
פיסוק, ותוים לטיניים ללא סימני עזר). החסרון העיקרי של גישה זו לעומת
הגישה בחלונות הוא שמספר הבתים במחרוזת אינו מלמד מה מספר האותיות במחרוזת
ולהיפך.
התוכנית הפשוטה הזו מראה על עוד צד מורכב בתכנות בחלונות: טיפוסי משתנים
רבים שהוגדרו מראש, ויש צורך להבין את משמעותם. הטיפוסים החשובים ביותר
הם:
-
משתנים בולאניים מטיפוס BOOL והקבועים המתאימים TRUE
ו--FALSE.
- משתנים שלמים אי-שליליים בני 32 סיביות מטיפוס DWORD.
- מצביעים, שתבנית שמם היא LP*, כגון מצביע חסר טיפוס LPVOID,
מצביע ל--TCHAR ששמו משום מה הוא LPTSTR,
וכדומה.
- משתנים שמייצגים משאב כלשהו שמערכת ההפעלה העניקה גישה אליו, מסוג HANDLE.
משתנים כאלה מייצגים קובץ פתוח, למשל.
כמעט כל תוכנית שדורשת גישה לקריאות המערכת של Win32 צריכה לכלול
את הקבצים windows.h ו--tchar.h. מעתה ואילך
לא נזכיר אותם, אבל הם תמיד דרושים.
חלק גדול מקריאות המערכת בחלונות מחזיר ערך בוליאני. הערך TRUE
מציין הצלחה והערך FALSE מציין כישלון. ניתן לברר את סיבת
הכישלון בעזרת קריאת המערכת GetLastError(), שמחזירה קוד
שגיאה מטיפוס DWORD. שגרת הספריה FormatMessage
מחזירה מחרוזת שמתארת את משמעות קוד השגיאה.
פתיחת קובץ:
קריאת המערכת CreateFile פותחת קובץ קיים ו/או יוצרת קובץ
חדש. היא מחזירה מזהה שבעזרתו ניתן לקרוא ולכתוב מהקובץ (ולבצע מספר
פעולות נוספות), או את הערך INVALID_HANDLE_VALUE במקרה
של כשלון.
HANDLE CreateFile(
LPCTSTR filename,
DWORD access_flags,
DWORD share_mode_flags,
LPSECURITY_ATTRIBUTES sa,
DWORD create_flags,
DWORD attributes_and_flags,
HANDLE template_file);
הארגומנט הראשון הוא שם הקובץ שרוצים לפתוח, למשל ..\data
או C:\Temp\xyz.dat.
הארגומנט השני מתאר האם רוצים לפתוח את הקובץ לקריאה, כתיבה, או שתיהן.
כל אחת מהפעולות מצויינת על ידי קבוע סימבולי,GENERIC_READ
ו--GENERIC_WRITE. ניתן לבקש לבצע את שתיהן על ידי פעולת
or של שני הקבועים.
הארגומנט השלישי מתיר או אוסר על פתיחת הקובץ לקריאה או כתיבה בזמן שהקובץ
פתוח. הערך 0 אוסר על מערכת ההפעלה לפתוח את הקובץ (מתכניות
אחרות או אפילו פעם נוספת מהתוכנית הזו) עד שאנו נסגור אותו, והערכים
FILE_SHARE_READ ו--FILE_SHARE_WRITE
מתירים לתוכניות אחרות לפתוח אותו לקריאה ו/או כתיבה בזמן שהוא פתוח
על ידינו.
הארגומנט הרביעי קשור בהרשאות ובינתיים נשתמש בערך NULL,
שמציין שיש להשתמש בברירות המחדל.
הארגומנט החמישי מתאר מה לעשות אם הקובץ קיים, או אם הוא איננו:
-
הערך CREATE_NEW גורם ליצירת קובץ חדש אם אין קובץ בשם
זה, ולכשלון אם הקובץ קיים.
- הערך CREATE_ALWAYS גורם ליצירת קובץ חדש, בין אם היה
קובץ בשם זה או לא.
- הערך OPEN_EXISTING גורם לכשלון אם הקובץ לא קיים.
- הערך OPEN_ALWAYS גורם לפתיחת קובץ קיים או ליצירת קובץ
חדש.
- הערך TRUNCATE_EXISTING גורם לקיצוץ קובץ קיים לגודל אפס,
ולכשלון אם אין כזה קובץ.
הארגומנט השישי מציין תכונות של קובץ חדש (במקרה שאכן נוצר קובץ חדש)
ומאפשר לבקש או להמליץ על מנגנוני גישה מיוחדים. בינתיים נשתמש בערך
ברירת המחדל שהוא FILE_ATTRIBUTE_NORMAL. בין היתר, ניתן
לבקש בעזרת ארגומנט זה שהקובץ החדש ימחק כאשר נסגור אותו, שקריאות מערכת
שכותבות לקובץ לא יחזרו עד שהמידע לא יכתב פיזית לדיסק, ולהכריז שגישות
לקובץ הן סדרתיות בעיקר או שאינן סדרתיות.
הארגונמט השביעי מאפשר ליצור קובץ חדש עם מאפיינים של קובץ קיים פתוח.
אנו לא נשמש בינתיים ביכולת זו ונעביר את הערך NULL.
סגירת קובץ או החזרת משאב אחר המיוצג על ידי handle:
BOOL CloseHandle(HANDLE h);
הפרמטר הוא המזהה שהוחזר על ידי CreateFile, או על ידי
פונקציה אחרת שהחזירה מזהה מטיפוס זה. הפונקציה מחזירה קוד שגיאה. בדרך
כלל סיבת הכישלון האפשרית היחידה היא שהועבר מזהה לא חוקי, כלומר מזהה
שלא מייצג קובץ פתוח או משאב אחר שהוקצה על ידי מערכת ההפעלה.
קריאה וכתיבה:
BOOL ReadFile (HANDLE h,
LPVOID buffer,
DWORD number_of_bytes_to_read,
LPDWORD number_of_bytes_actually_read,
LPOVERLAPPED overlapped);
BOOL WriteFile(HANDLE h,
LPVOID buffer,
DWORD number_of_bytes_to_read,
LPDWORD number_of_bytes_actually_read,
LPOVERLAPPED overlapped);
האגומנט הראשון הוא מזהה של קובץ פתוח והמזהה השני הוא מצביע למערך שאותו
יש להעביר אל או מתוך הקובץ. הארגומנט השלישי הוא מספר הבתים שהתוכנית
מבקשת להעביר, והרביעי הוא מצביע למשתנה שיכיל, לאחר חזרת קריאת המערכת,
את מספר הבתים שהועברו בפועל. המספר הזה עשוי להיות קטן יותר מהמספר
שביקשנו להעביר אם הגענו לסוף הקובץ בבקשת קריאה, או שאין מקום בדיסק
בבקשת כתיבה. הארגומנט האחרון משמש לצורת גישה לקבצים שלא נדון בה כעת,
ונעביר בינתיים את הערך NULL.
לכל קובץ פתוח יש מצביע שמגדיר את המקום בקובץ (בבתים) שבו תתבצע הקריאה
או הכתיבה הבאה. כל פעולת קריאה וכתיבה מקדמת את המצביע במספר הבתים
שהועברו. כאשר הקובץ נפתח המצביע מצביע לתחילת הקובץ.
הערות לגבי fopen, fread, וכדומה.
הפונקציות fopen, fread,
fwrite, fseek
מספקות שירותים דומים לקריאות המערכת שמתוארות כאן אך הן חלק מהספריה
הסטנדרטית של c ולא קריאות ישירות למערכת ההפעלה. שימוש
בהן הופך תוכנית ליותר פורטבילית אך לא ניתן לבצע דרכן כל פעולה שניתן
לבצע דרך הממשק הישיר למערכת ההפעלה (בגלל שוני בין מערכות הפעלה). בספר
זה נשתמש בממשק הישיר למערכת ההפעלה ולא בספריות עזר.
התרגיל.
כתוב/כיתבי תכנית שמעתיקה קובץ. התכנית צריכה לצאת ולהדפיס הודעת שגיאה
אם אין מספיק פרמטרים לתכנית, אם קובץ הקלט אינו קיים, ואם קובץ הפלט
קיים. התכנית צריכה להתשמש בקריאות המערכת המתוארות בתרגיל. ההעתקה צריכה
להתבצע תוך שימוש בחוצץ שגודלו n בתים, כלומר התכנית קוראת n
בתים בקריאה אחת, כותבת אותם וחוזר חלילה. לתכנית שלושה פרמטרים, שם
קובץ הקלט, הפלט, ו--n. על מנת להפוך את המחרוזת המכילה את הייצוג
של n לשלם אפשר להשתמש ב-_stscanf(argv[3],_T("%d"),&n).
יש להשתמש ב--malloc על מנת להקצות את הזיכרון לחוצץ:
#include <stdlib.h>
...
char* buffer;
...
buffer = (char*) malloc(n);
מידדו את זמן הריצה של התוכנית בעזרת הפקודה processtimes
(באתר) כאשר אתם משתמשים בתוכנית להעתיק קובץ בגודל מליון בתים או יותר.
מידדו את זמני הריצה תוך שימוש בחוצץ בגדלים הבאים (בבתים): 1,
64, 512, 1024, 8192,
ו--63556. אם יש שוני ניכר בין זמני הריצה עם חוצץ בגדלים שונים, יש
להסביר את הסיבות לשוני.
Copyright Sivan Toledo 2004