המצגת נטענת. אנא המתן

המצגת נטענת. אנא המתן

רשימות מקושרות עבודה עם קבצים דוגמה

מצגות קשורות


מצגת בנושא: "רשימות מקושרות עבודה עם קבצים דוגמה"— תמליל מצגת:

1 רשימות מקושרות עבודה עם קבצים דוגמה
תרגול מס' 3 רשימות מקושרות עבודה עם קבצים דוגמה

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

3 מבנים המצביעים לעצמם נסתכל על המבנה הבא:
typedef struct node { int data; struct node* next; } *Node; איך נראים המבנים בזיכרון לאחר ביצוע הקוד הבא: Node node1 = malloc(sizeof(*node1)); Node node2 = malloc(sizeof(*node2)); node1->data = 1; node1->next = node2; node2->data = 2; node2->next = NULL; data=1 next=0xdff268 node1 data=2 next=0x0 node2 מבוא לתכנות מערכות

4 רשימה מקושרת data=17 next=0xdff268 רשימה מקושרת הינה שרשרת של משתנים בזיכרון כאשר כל משתנה מצביע למשתנה הבא בשרשרת סוף הרשימה מיוצג ע"י מצביע ל-NULL רשימה מקושרת הינה מבנה נתונים המאפשר שיטה מסוימת לשמירת ערכים בזיכרון מערך הוא דוגמה נוספת למבנה נתונים רשימה מקושרת מאפשרת: שמירה של מספר ערכים שאינו חסום על ידי קבוע הכנסה והוצאה של משתנים מאמצע הרשימה בקלות חיבור וחלוקת רשימות נוחה data=200 next=0xdef4c4 data=-15 next=0x43ba12 data=8 next=0x0 מבוא לתכנות מערכות

5 דוגמה - הדפסה בסדר הפוך כתבו תכנית המקבלת רשימת תאריכים מהמשתמש ומדפיסה אותם בסדר הפוך פתרון: ניצור רשימה מקושרת של תאריכים לא ניתקל בבעיות בגלל חוסר ההגבלה על גודל הקלט בכל פעם שנקלוט תאריך חדש נוכל להוסיפו בקלות לתחילת הרשימה next=0x0fffe0 data day=9 month="JUN" year=1962 newNode 0x0fffe0 list next=0x0ffe00 data day=9 month="JUN" year=1962 next=0x0ffef6 data day=9 month="JUN" year=1962 מבוא לתכנות מערכות

6 פתרון למה לא ניתן להשתמש ב-Node?
typedef struct node { Date data; struct node* next; } *Node; Node createNode(Date d) { Node ptr = malloc(sizeof(*ptr)); if(!ptr) { return NULL; } ptr->data = d; ptr->next = NULL; return ptr; void destroyList(Node ptr) { while(ptr) { Node toDelete = ptr; ptr = ptr->next; free(toDelete); למה לא ניתן להשתמש ב-Node? למה חייבים להקצות דינאמית את כל העצמים ברשימה? הגדרת ה-typedef בתוקף רק אחרי סופה. לכן השם Node עבור struct node* אינו מוגדר עדיין באמצע הגדרת המבנה. חייבים להקצות דינאמית את כל הרשימה מאחר ומספר הצמתים אינו ידוע, לא ניתן להקצות על המחסנית כמות לא חסומה של משתנים. (אמנם ניתן להיכנס לפונקציה ושוב ושוב כדי לקבל עוד עותקים של משתנה מקומי, אך לא נוכל לצאת מהן. אכן, סטודנטים עם כשרון התחכמות ישימו לב שניתן לפתור את התרגיל הנוכחי בלי הקצאות דינאמיות כלל. אך הפתרון יתאים רק למקרה הפשוט של התרגיל הזה ולא למקרה הכללי. אם לא נשתמש במשתנה toDelete נאלץ לשחרר את ptr לפני שנקרא ממנו את המצביע next וכזכור אסור לגשת למשתנה אחרי ששוחרר כי ההתנהגות לא מוגדרת. ניתן לכתוב בקלות את הקוד בעזרת רקורסיה בקלות, זאת מאחר והניתן לשנות את הסדר ברקורסיה בקלות: void destroyList(Node ptr) { if(!ptr) { return; } destroyList(ptr->next); free(ptr); למה צריך את toDelete? כיצד ניתן לכתוב קוד זה עם רקורסיה? מבוא לתכנות מערכות

7 פתרון int main() { Node head = NULL; Date input; while (dateRead(&input)) { Node newNode = createNode(input); newNode->next = head; head = newNode; } for(Node ptr = head ; ptr != NULL ; ptr=ptr->next) { datePrint(ptr->data); destroyList(head); return 0; כאשר יש קריאה לפונקציה שגורמת ליצירת עצמים חדשים כגון createNode תמיד כדאי לשים לב להשפעות שלה: חייב להיות מקום שבו המשאבים משוחררים. עשו לעצמכם הרגל כשאתם כותבים שורה שמקצה משאבים לחשוב איפה השחרור המתאים יהיה. יש להיזהר במיוחד בזמן טיפול בשגיאות, יציאה מהפונקציה באמצע עלולה להשאיר דליפות זיכרון. מבוא לתכנות מערכות

8 הערות נוספות רשימה מקושרת יכולה להיות גם דו-כיוונית. במקרה כזה לכל צומת שני מצביעים: next ו-previous יתרונה של רשימה מקושרת דו-כיוונית הוא בסיבוכיות בלבד - לכן עדיף להתחיל בכתיבת רשימה חד-כיוונית מאחר והיא פשוטה יותר נוח להוסיף איבר דמה לתחילת הרשימה כיד לצמצם את מקרי הקצה שצריכים לטפל בהם בקוד 1 2 3 4 5 רשימה דו כיוונית מאפשרת מספר פעולות בסיבוכיות טובה יותר, אך קשה יותר לכתוב אותה. סיבוכיות אינה שיקול בקורס. עדיף לכתוב קוד פשוט ואח"כ להחליף אותו בקוד יעיל יותר במקומות שבהם אכן חשוב שיפור איבר דמה מאפשר לצמצם את המקרי הקצה של טיפול ברשימה ריקה כמו הוספת איבר (ברשימה ריקה צריך לעדכן את הראש ולהתייחס למקרים שאין אחד) או הסרה של איבר (שאז תמיד יבדק המקרה המיוחד שבוזה האיבר האחרון, לאיפוס המצביע) שימו לב שאין בעיה של "מחיר" איבר הדמה. אנחנו לא מתעסקים בשיקולי יעילות כאמור וגם אם היינו מתעסקים לא ברור כלל אם איבר כזה מאט או מזרז את התכנית. איבר הדמה הינה איבר חסר תוכן, לכן אין משמעות למידע שהוא מכיל. למרות זאת, יש צורך לאתחל את איבר הדמה מאחר שזה ימנע שגיאות בגישה לרשימה (אם היא עדיין ריקה). ? 2 3 4 5 מבוא לתכנות מערכות

9 רשימות מקושרות - סיכום ניתן ליצור רשימות מקושרות ע"י הוספת מצביע למבנה a בתוך המבנה a רשימות מקושרות אינן מוגבלות בגודלן ומאפשרות הכנסה נוחה של איברים באמצע הרשימה את הצמתים ברשימה יש להקצות דינאמית ולזכור לשחררם כאשר מממשים רשימה מקושרת מומלץ להוסיף איבר דמה בתחילתה מבוא לתכנות מערכות

10 FILE* פונקציות שימושיות דוגמה
עבודה עם קבצים FILE* פונקציות שימושיות דוגמה מבוא לתכנות מערכות

11 קבצים קבצים הם משאב מערכת בדומה לזיכרון:
יש לפתוח קובץ לפני השימוש בו יש לסגור קובץ בסוף השימוש בו אותן בעיות אשר צצות מניהול זיכרון לא נכון עלולות לצוץ מניהול קבצים לא נכון בכדי להשתמש בקבצים מתוך התכנית שלנו נשתמש בטיפוס הנתונים FILE המוגדר ב-stdio.h כמו כל טיפוס מורכב השימוש בו נעשה בעזרת מצביעים, כלומר נשתמש בטיפוס FILE* מבוא לתכנות מערכות

12 פתיחת קובץ באמצעות fopen
FILE* fopen (const char* filename, const char* mode); filename היא מחרוזת המכילה את שם הקובץ mode היא מחרוזת המתארת את מצב הפתיחה של הקובץ "r" עבור פתיחת הקובץ לקריאה "w" עבור פתיחת הקובץ לכתיבה "a" עבור פתיחת הקובץ לשרשור (כתיבה בהמשכו) במקרה של כשלון מוחזר NULL המילה const מיצצגת בשפה משתנה שאסור לשנות את ערכו. ניתן להשתמש ב-const כדי לציין שהפונקציה fopen למשל אינה משנה את ערכי התווים במחרוזות הנשלחות לה (כמו שאכן היינו מצפים ממנה) Filename היא מחרוזת המתארת שם קובץ בהתאם למערכת ההפעלה. במקרה של UNIX מדובר בכל מחרוזת לתיאור שם קובץ במערכת הקבצים כמו שנלמד בתרגול 1. שימו לב שהתוצאה כאן היא קוד שיכול לעבוד שונה במערכות הפעלה שונות (למשל ב-Windows שמות קבצים אינם case-sensitive וב-UNIX הם כן). אם הקובץ לא קיים או המחרוזת בכלל אינה מתארת שם קובץ חוקי הפונקציה פשוט תיכשל ותחזיר NULL. להלן רשימת תנאים להצלחת הפתיחה של קובץ. הרשימה אינה מלאה וזאת מאחר ויש קיימים הרבה שיקולים המשתנים בין מערכות הפעלה ומיקומי הקבצים בנושא זה: פתיחה למצב קריאה מצליחה אם: הקובץ קיים ויש הרשאות קריאה עבורו פתיחה למצב כתיבה מצליחה אם: הקובץ לא קיים אבל ניתן ליצור אותו (לא ניתן ליצור קובץ אם שמו לא חוקי, לא קיימת תיקיה מתאימה בה יש ליצור אותו, לא ניתן ליצור קובץ בתיקיה בגלל הגבלת הרשאות וכדומה) הקובץ קיים וניתן למחוק ולהחליפו (לשם כך הקובץ אינו יכול להיות בשימוש כרגע וצריכות להתקיים ההרשאות המתאימות) קיימים מצבים נוספים מלבד “r” ו-“w”. למשל “a” עבור שרשור (append) ומצבים נוספים עבור קריאה וכתיבה יחדיו ועוד. מצב שרשור יוצר קובץ חדש לכתיבה אם הוא לא קיים עדיין ואם הוא קיים פותח אותו לכתיבה כך שהכתיבה תתחיל מסוף הקובץ הנוכחי. מצב שרשור מתאים למשל לניהול יומן (log) של התכנית שימו לב לדמיון בין פונקציות היצירה השונות. בין malloc לבין fopen דמיון רב למרות שהן עושות דברים שונים. מבוא לתכנות מערכות

13 סגירת קובץ באמצעות fclose
int fclose (FILE* stream); בניגוד ל-free שליחת מצביע שהינו NULL תגרום להתרסקות התכנית ככלל, לא ניתן לשלוח NULL לפונקציות קלט/פלט המקבלת FILE* גם ניסיון לסגור קובץ פעמיים הוא שגיאה, ועשוי לגרום להתרסקות התכנית ערך ההחזרה הוא 0 בהצלחה ו-EOF במקרה של כשלון EOF: קבוע שלילי המשמש לציון שגיאות בפונקציות קלט פלט, בדרך כלל כאלו שנובעות מהגעה לסוף הקובץ (End of file) גם במקרה של כשלון לא ניתן יותר להשתמש בקובץ שנשלח EOF מוחזר ע"י פונקציות קלט כמו fgetc, fgets ועוד. מטרתו לציין את ההגעה לסוף הקובץ וזאת מאחר ולא קיים תו ascii מיוחד לציון סוף קובץ כמו במחרוזות. איך בכלל יכולה להיכשל סגירת קובץ? הדבר תלוי במערכת ההפעלה שוב אבל דוגמה פשוטה היא בגלל שנגמר המקום בדיסק הקשיח עליו נשמר הקובץ. בכל מקרה הטיפול בשגיאות כאלו מסובך ותלוי במקרה הספציפי. דוגמה נוספת מתקדמת: נסיון לכתיבת קובץ במחשב מרוחק. בחלק ממערכות הקבצים התומכות ברשת יתכן ששגיאות ידווחו רק כאשר מנסים לסגור את הקובץ. אין צורך לבדוק הצלחת fclose() בקורס שלנו מבוא לתכנות מערכות

14 פונקציות קלט/פלט עבור קבצים
ניתן לכתוב ולקרוא מקובץ בעזרת הפונקציות fprintf ו-fscanf: int fprintf (FILE* stream, const char* format, ...); int fscanf (FILE* stream, const char* format, ...); התנהגותן זהה לזו של printf ו-scanf הרגילות מלבד הוספת הפרמטר stream שהוא הקובץ אליו יש לכתוב או ממנו יש לקרוא stream חייב להיות פתוח במצב המתאים כדי שהפעולה תצליח מצב המאפשר כתיבה עבור fprintf מצב המאפשר קריאה עבור fscanf דוגמה - כתיבת המחרוזת Hello world לקובץ בשם hello.txt: FILE* stream = fopen("hello.txt", "w"); fprintf(stream, "Hello world!\n"); fclose(stream); חסרה בדיקה להצלחה בפתיחת הקובץ. במקרה של כשלון מוחזר NULL והתכנית תתרסק. שימו לב שיש מגוון רחב מאוד של שגיאות בפתיחת קובץ והן נפוצות מאוד (למשל משתמש שמכניס קלט לא חוקי) מה חסר? מבוא לתכנות מערכות

15 פונקציות קלט/פלט עבור קבצים
fgets מקבלת חוצץ ואת גודלו, קוראת שורה מ-stream וכותבת אותה אל החוצץ char* fgets (char* buffer, int size, FILE* stream); אם יש יותר מ-size-1 תווים בשורה היא תחתך הפונקציה מחזירה את buffer אם הקריאה הצליחה, או NULL במקרה של שגיאת קלט. במקרה של הצלחה, הפונקציה מוסיפה תו ‘\0’ בסוף המחרוזת שנקראה. fputs כותבת את המחרוזת str לתוך stream: int fputs (const char* str, FILE* stream); דוגמה - קריאת שורה מקובץ: char buffer[BUFFER_SIZE] = ""; fgets(buffer, BUFFER_SIZE, stream); printf("Line is: %s",buffer); מה ההבדל בין str לבין buffer? כשאנחנו מתכוונים למחרוזת ב-C אנחנו מדברים על מערך של char אשר מובטח שיש בו תו ‘\0’ כדי לזהות את סוף המחרוזת. ואילו כשאר אנחנו מדברים על חוצצים מובטח לנו מערך של char שיש בו מקום בגודל כלשהו כך שנוכל לרשום לתוכו מחרוזות. כאשר נתונה לנו מחרוזת ללא ידיעה של גודל החוץ בו היא נמצאת מסוכן לבצע עליה פעולות של עיבוד המחרוזת (ללא העתקתה לחוצץ שגודלו ידוע מראש) מה ההבדל בין fputs לfprintf? פונקצית הprintf נועדה להדפיס לפי פורמט מסוים, הפורמט ניתן כקלט לפונקציה ועל פיו הפלט מעוצב. פונקצית puts רק מדפיסה את המחרוזת שקיבלה כפי שקיבלה אותה. מבוא לתכנות מערכות

16 תרגיל copy כתבו תכנית המקבלת כפרמטרים שני שמות קבצים <file1> ו-<file2> ומעתיקה את תוכנו של <file1> ל-<file2> דוגמה: > cat file1.txt This is text in a file > ./copy file1.txt file2.txt > cat file2.txt מבוא לתכנות מערכות

17 copy - פתרון #include <stdio.h> #include <stdlib.h> #define CHUNK_SIZE 256 void copy(FILE* input, FILE* output) { char buffer[CHUNK_SIZE]; while (fgets(buffer, CHUNK_SIZE, input) != NULL) { fputs(buffer, output); } void error(char* message, char* filename) { printf("%s %s\n", message, filename ? filename : ""); exit(1); מבוא לתכנות מערכות

18 copy - פתרון int main(int argc, char** argv) { if (argc != 3) { error("Usage: copy <file1> <file2>", NULL); } FILE* input = fopen(argv[1], "r"); if (!input) { error("Error: cannot open", argv[1]); FILE* output = fopen(argv[2], "w"); if (!output) { fclose(input); error("Error: cannot open", argv[2]); copy(input, output); fclose(output); return 0; מבוא לתכנות מערכות

19 ערוצים סטנדרטיים הזכרנו בתרגול מספר 1 את שלושת ערוצי הקלט/פלט הסטנדרטיים: הקלט הסטנדרטי, הפלט הסטנדרטי וערוץ השגיאות הסטנדרטי. ערוצים אלו מיוצגים ב-C כמשתנים גלובליים מטיפוס FILE*: stdout – ערוץ הפלט הסטנדרטי stdin – ערוץ הקלט הסטנדרטי stderr – ערוץ השגיאות הסטנדרטי הטיפוס FILE* משמש כהפשטה (abstraction) של ערוצי קלט/פלט מאפשר עבודה אחידה על דברים שונים ומקל על המשתמש בו מבוא לתכנות מערכות

20 פונקציות קלט/פלט לרוב הפונקציות עבור קלט ופלט קיימות גרסאות נפרדות עבור שימוש בערוצים הסטנדרטיים: למשל printf עבור fprintf הפונקציה puts כותבת מחרוזת ל-stdout ומוסיפה ירידת שורה בסופה int puts (const char* str); הפונקציהgets קוראת שורה מהקלט הסטנדרטי. המחרוזת אינה מכילה את ירידת השורה בתוכה מה חסר כאן? מה הופך את הפונקציה הזו למסוכנת? char* gets (char* buffer); אסור להשתמש ב-gets ופונקציות דומות המאפשרות חריגה מהזיכרון לקלט זהו מקור לשגיאות זיכרון ובעיות אבטחה מבוא לתכנות מערכות

21 copy המשופרת נשפר את התכנית copy:
אם התכנית מקבלת שני שמות קבצים היא פועלת כמקודם אם התכנית מקבלת שם קובץ יחיד היא מדפיסה את תוכנו לפלט הסטנדרטי אם התכנית אינה מקבלת פרמטרים היא מדפיסה מהקלט הסטנדרטי אל הפלט הסטנדרטי כמו כן, הודעות השגיאה של התכנית יודפסו אל ערוץ השגיאות הסטנדרטי > ./copy file1.txt This is text in a file > ./copy Hello! Hello! מבוא לתכנות מערכות

22 copy המשופרת - פתרון #include <stdio.h> #include <stdlib.h> #define CHUNK_SIZE 256 void copy(FILE* input, FILE* output) { char buffer[CHUNK_SIZE]; while (fgets(buffer, CHUNK_SIZE, input) != NULL) { fputs(buffer, output); } void error(char* message, char* filename) { fprintf(stderr,"%s %s\n", message, filename ? filename : ""); exit(1); הפונקציה copy אינה משתנה מאחר והיא מתאימה גם לערוצים הסטנדרטיים השימוש ב-exit מקשה על תחזוקת קוד בהמשך. הוא נעשה כאן רק על מנת לשמור על הקוד קצר. הודעות שגיאה מומלץ להדפיס לערוץ השגיאות השימוש ב-exit הוא בדרך כלל תכנות רע, המנעו משימוש בה מבוא לתכנות מערכות

23 copy המשופרת - פתרון FILE* initInputFile(int argc, char** argv) { if (argc < 2) { return stdin; } return fopen(argv[1], "r"); FILE* initOutputFile(int argc, char** argv) { if (argc < 3) { return stdout; return fopen(argv[2], "w"); מבוא לתכנות מערכות

24 איזו בעיה עלולה להיווצר כאן?
copy המשופרת - פתרון int main(int argc, char** argv) { if (argc > 3) { error("Usage: copy <file1> <file2>", NULL); } FILE* input = initInputFile(argc, argv); if (!input) { error("Error: cannot open ", argv[1]); FILE* output = initOutputFile(argc, argv); if (!output) { fclose(input); error("Error: cannot open ", argv[2]); copy(input, output); fclose(output); return 0; השימוש ב-fclose ללא בדיקה עלול לסגור את stdout ו-stdin. אמנם כאן התכנית נגמרת ואין עם זה בעיה במקרה שלנו, אך קוד נוטה לעבור שינויים וקוד שנמצא ב-main היום יכול להפוך לפונקציה נפרדת מחר. לכן לא מומלץ להסתמך על היציאה מ-main כתירוץ להתעצל ויש לנקות בצורה מסודרת אחרי הקוד גם אם הוא נמצא ב-main. הפתרון הוא בדיקה פשוטה ש-input!=stdin שימו לב שמומלץ להשתמש במשתנה אחר ולא לשחק ב-stdout ולשנות את ערכו. פתרון אשר משנה את ערך המשתנים הגלובליים ישפיע על קוד אחר בתוכנה גדולה אשר עוברת שינויים כל הזמן. איזו בעיה עלולה להיווצר כאן? מבוא לתכנות מערכות

25 הכוונות קלט/פלט מה ההבדל בין שתי הפקודות הבאות? כיצד הוא מתבטא בתכנית?
> ./copy data1.txt data2.txt > ./copy < data1.txt > data2.txt הכוונת קלט פלט יכולה להתבצע הן בקוד התכנית והן ע"י מערכת ההפעלה בקריאה לתכנית הכוונת קלט ופלט מתוך הקוד מתבצעת ע"י שינוי ערכם של המשתנים stdout ו-stdin הכוונת קלט ופלט ממערכת ההפעלה מתבצעת ע"י חיבור הערוץ הסטנדרטי מחוץ לתהליך, כך ש-stdout ו-stdin לא הוחלפו, אבל הפועל הם מתייחסים לקובץ ולא לפלט הסטנדרטי הרגיל (למשל) מבוא לתכנות מערכות

26 קלט פלט עבור טיפוסי נתונים
כעת נוכל לשפר את טיפוס הנתונים Date כך שיתמוך גם בקלט ופלט לקבצים: void datePrint(Date date, FILE* outputFile) { fprintf(outputFile, "%d %s %d\n", date.day, date.month, date.year); } bool dateRead(Date* date, FILE* inputFile) { if (date == NULL) { return false; if (fscanf(inputFile, "%d %3s %d", &(date->day), date->month, &(date->year)) != 3) { return dateIsValid(*date); הפונקציות החדשות מקבלות כפמרטר נוסף את הערוץ אליו יש לכתוב או לקרוא. ניתן להשתמש בהן כמקודם ע"י שימוש במשתנים stdout ו-stdin. מומלץ ליצור פונקציות כאלו לכל טיפוס נתונים ולו רק כדי לאפשר הצגה נוחה של עצמים למסך המקלה משמעותית על דיבוג הקוד. מבוא לתכנות מערכות

27 עבודה עם קבצים - סיכום קבצים הם משאב מערכת – יש להקפיד על ניהול נכון שלהם קבצים והערוצים הסטנדרטיים מיוצגים ע"י הטיפוס FILE* ניתן לכתוב ולקרוא מקבצים בדומה לביצוע פעולות קלט ופלט רגילות הערוצים הסטנדרטיים מיוצגים ב-C כמשתנים הגלובליים stdin, stdout ו-stderr הכוונות קלט ופלט ניתנות לביצוע בתוך הקוד ומחוצה לו ע"י שימוש ב-FILE* ניתן ליצור פונקציות קלט/פלט לטיפוסי נתונים כך שיתאימו גם לכתיבה לערוצים סטנדרטיים וגם לקבצים מבוא לתכנות מערכות

28 דוגמה - תאריכים היסטוריים
תכן בסיסי פתרון מבוא לתכנות מערכות

29 דוגמה - תאריכים היסטוריים
1 JAN 404 Last gladiator competition 6 MAY 1889 Eiffel tower completed 21 NOV 1794 Honolulu harbor discovered 1 JAN 1852 First public bath in NY 21 NOV 1971 First takeoff of the Concorde 6 MAY 1915 Orson Welles born 6 MAY 1626 Manhattan purchased for 24$ 21 NOV 1971 First landing of the Concorde > important_dates events Enter a date: 21 NOV 1971 First takeoff of the concorde First landing of the concorde Enter a date: 2 MAY 1971 Nothing special נתון קובץ המכיל תאריכים ומאורעות אשר התרחשו בהם בפורמט הבא: כתבו תכנית אשר קוראת קובץ המכיל תאריכים היסטוריים כמתואר ומאפשרת למשתמש לחפש את רשימת המאורעות שקרו בתאריך מסוים מבוא לתכנות מערכות

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

31 פתרון אפשרי ניצור רשימה של תאריכים היסטוריים:
Historical Dates List Description Description ניצור רשימה של תאריכים היסטוריים: כל תאריך היסטורי יכיל את רשימת המאורעות שקרו בו כדי להדפיס את רשימת המאורעות של תאריך מסוים נחפש אותו ברשימה ונדפיס את המאורעות שלו אם מצאנו תאריך כזה Event Date Events list Next Description Next Description Next Description Date Events list Next Description Next Description Date Events list Next Description Next מבוא לתכנות מערכות

32 פתרון אפשרי - מימוש #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #define BUFF_SIZE 256 char* stringDuplicate(const char* str) { char* copy = malloc(strlen(str) + 1); return copy ? strcpy(copy, str) : NULL; } stringDuplicate מונעת את שכפול הקוד שנוצר משכפול מחרוזות. במקרה זה יש בקוד רק מקום אחד שצריך להתחשב בתו ה-‘\0’ והסיכוי לשכוח זאת ולגרום לבאג קטן. מבוא לתכנות מערכות

33 Event typedef struct event_t { char* description; struct event_t* next; } *Event; Event eventCreate(const char* description) { Event event = malloc(sizeof(*event)); if (!event) { return NULL; } event->description = stringDuplicate(description); if (!event->description) { free(event); // A better idea? event->next = NULL; return event; ניתן ליצור רשימה מקושרת ישר בתוך הטיפוס וללא טיפוס נוסף בשם Node. לשיטה זו חסרון חשוב ביכולת לשימוש חוזר בטיפוס - אולי בכלל לא נרצה לשמור אותו ברשימה דווקא? ואם קיים טיפוס קיים מתאים לבעיה אולי הוא לא תומך ברשימות מקושורת לשיטה זו יתרון במקרה שלנו - היא חוסכת לנו כתיבת הרבה קוד. בתרגול 5 נראה כיצד ניתן לשמור על יתרון השימוש החוזר בטיפוסים בלי להצטרך לכתוב הרבה קוד עבור ניהול הרשימה מחדש בכל פעם - ע"י כתיבת מבני נתונים גנריים שימו לב לשחרור במקרה של שגיאה - רעיון טוב יותר הוא לקרוא ישירות ל-eventDestroy. כך במקרה שנוספים עוד שדות עדיין הפונקציה תנקה אחרי עצמה כהלכה. מבוא לתכנות מערכות

34 Event Event eventAddNext(Event head, Event newEvent) { if (!head) { return newEvent; } Event ptr = head; while (ptr->next) { ptr = ptr->next; ptr->next = newEvent; return head; ההוספה כאן מתבצעת בסוף הרשימה למרות שאין לכך חשיבות, וזאת על מנת להדגים כיצד מוסיפים איבר בסוף הרשימה מבוא לתכנות מערכות

35 Event void eventDestroy(Event event) { while (event) { Event toDelete = event; event = event->next; free(toDelete->description); free(toDelete); } ההוספה כאן מתבצעת בסוף הרשימה למרות שאין לכך חשיבות, וזאת על מנת להדגים כיצד מוסיפים איבר בסוף הרשימה מבוא לתכנות מערכות

36 Historical Date typedef struct historical_date_t { Date date; Event events; struct historical_date_t* next; } *HistoricalDate; HistoricalDate historicalDateCreate(Date date, Event event) { HistoricalDate historicalDate = malloc(sizeof(*historicalDate)); if (!historicalDate) { return NULL; } historicalDate->date = date; historicalDate->events = event; historicalDate->next = NULL; return historicalDate; מבוא לתכנות מערכות

37 Historical Date מימוש רקורסיבי!
void historicalDateDestroy(HistoricalDate historicalDate) { if (!historicalDate) { return; } historicalDateDestroy(historicalDate->next); eventDestroy(historicalDate->events); free(historicalDate); HistoricalDate historicalDateFind(HistoricalDate historicalDate, Date date) { for (HistoricalDate ptr = historicalDate; ptr != NULL; ptr = ptr->next) { if (dateEquals(ptr->date, date)) { return ptr; return NULL; מימוש רקורסיבי! שחרור הרשימה הפעם מתבצע ברקורסיה מבוא לתכנות מערכות

38 Historical Date HistoricalDate historicalDateAddEvent(HistoricalDate historicalDate, Date date, Event event) { HistoricalDate target = historicalDateFind(historicalDate, date); if (target) { target->events = eventAddNext(target->events, event); return historicalDate; } HistoricalDate newDate = historicalDateCreate(date, event); newDate->next = historicalDate; return newDate; void historicalDatePrintEvents(HistoricalDate historicalDate) { for (Event ptr = historicalDate->events; ptr != NULL; ptr = ptr->next) { printf("%s", ptr->description); במקרה זה מוסף האיבר בתחילת הרשימה. שוב, אין חשיבות לסדר במקרה שלנו באמת ולכן סתם הדגמנו את שני המקרים. מבוא לתכנות מערכות

39 קריאת קובץ הקלט HistoricalDate readEvents(char* filename) { FILE* fd = fopen(filename, "r"); if (!fd) { return NULL; } HistoricalDate history = NULL; char buffer[BUFF_SIZE] = ""; Date inputDate; while (dateRead(&inputDate,fd) && fgets(buffer,BUFF_SIZE,fd) != NULL) { Event newEvent = eventCreate(buffer); history = historicalDateAddEvent(history, inputDate, newEvent); fclose(fd); return history; מבוא לתכנות מערכות

40 main int main(int argc, char** argv) { if (argc != 2) { printf("Usage: %s <events file>\n", argv[0]); return 0; } HistoricalDate history = readEvents(argv[1]); if (!history) { printf("Error loading events from %s\n",argv[1]); printf("Enter a date: "); Date inputDate; while (dateRead(&inputDate, stdin)) { HistoricalDate h = historicalDateFind(history, inputDate); if (h) { historicalDatePrintEvents(h); } else { printf("Nothing special\n"); historicalDateDestroy(history); שימו לב ששחרור כל מבנה הנתונים המורכב מתבצע בקלות בגלל שהוא נבנה בשלבים ולכל טיפוס בו יש פונקציות שחרור משלו. כל שנותר לנו לעשות הוא לקרוא לפונקצית השחרור הראשית וכל המבנה ישוחרר בצורה מסודרת ללא בעיות נשים לב, אנו קוראים את שם קובץ ההרצה ע"י argv[0] כאשר מדפיסים את שורת ה- "usage". מבוא לתכנות מערכות

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


הורד את "ppt "רשימות מקושרות עבודה עם קבצים דוגמה

מצגות קשורות


מודעות Google