רשימות מקושרות עבודה קבצים דוגמה
מבנים המצביעים לעצמם רשימות מקושרות 2
נסתכל על המבנה הבא: 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->data = NULL; node1 data=1 next=0xdff268 data=2 node2 next=0x0 3
data=17 next=0xdff268 data=200 next=0xdef4c4 data=-15 next=0x43ba12 data=8 next=0x0 רשימה מקושרת הינה שרשרת של משתנים בזיכרון כאשר כל משתנה מצביע למשתנה הבא בשרשרת סוף הרשימה מיוצג ע"י מצביע ל- NULL רשימה מקושרת הינה מבנה נתונים המאפשר שיטה מסוימת לשמירת ערכים בזיכרון מערך הוא דוגמה נוספת למבנה נתונים רשימה מקושרת מאפשרת: שמירה של מספר ערכים שאינו חסום על ידי קבוע הכנסה והוצאה של משתנים מאמצע הרשימה חיבור וחלוקת רשימות נוחה בקלות 4
data data data כתבו תכנית המקבלת רשימת תאריכים מהמשתמש ומדפיסה אותם בסדר הפוך newnode list next=0x0ffef6 day=9 month="jun" year=1962 פתרון: ניצור רשימה מקושרת של תאריכים לא ניתקל בבעיות בגלל חוסר ההגבלה על גודל הקלט בכל פעם שנקלוט תאריך חדש נוכל להוסיפו בקלות לתחילת הרשימה 0x0ffef6 next=0x0ffef6 day=9 next=0x0ffef6 day=9 month="jun" year=1962 month="jun" year=1962 5
typedef struct node { Date data; struct node* next; *Node; למה לא ניתן להשתמש ב- 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 tmp = ptr; ptr = ptr->next; free(tmp);?tmp למה חייבים להקצות דינאמית את כל העצמים ברשימה? למה צריך את כיצד ניתן לכתוב קוד זה עם רקורסיה? 6
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; 7
1 2 3 4 5 רשימה מקושרת יכולה להיות גם דו-כיוונית. במקרה כזה לכל צומת שני מצביעים: next ו- previous יתרונה של רשימה מקושרת דו-כיוונית הוא בסיבוכיות בלבד - לכן עדיף להתחיל בכתיבת רשימה חד-כיוונית מאחר והיא פשוטה יותר? 2 3 4 5 נוח להוסיף איבר דמה לתחילת הרשימה כיד לצמצם את מקרי הקצה שצריכים לטפל בהם בקוד 8
ניתן ליצור רשימות מקושרות ע"י הוספת מצביע למבנה a בתוך המבנה a רשימות מקושרות אינן מוגבלות בגודלן ומאפשרות הכנסה נוחה של איברים באמצע הרשימה את הצמתים ברשימה יש להקצות דינאמית ולזכור לשחררם כאשר מממשים רשימה מקושרת מומלץ להוסיף איבר דמה בתחילתה 9
FILE* פונקציות שימושיות דוגמה 10
קבצים הם משאב מערכת בדומה לזיכרון: יש לפתוח קובץ לפני השימוש בו יש לסגור קובץ בסוף השימוש בו אותן בעיות אשר צצות מניהול זיכרון לא נכון עלולות לצוץ מניהול קבצים לא נכון בכדי להשתמש בקבצים מתוך התכנית שלנו נשתמש בטיפוס הנתונים FILE המוגדר ב- stdio.h כמו כל טיפוס מורכב השימוש בו נעשה בעזרת מצביעים, כלומר נשתמש בטיפוס FILE* 11
פתיחת קובץ נעשית ע"י הפונקציה :fopen FILE* fopen (const char* filename, const char* mode); filename היא מחרוזת המתארת את שם הקובץ mode היא מחרוזת המתארת את מצב הפתיחה של הקובץ "r" עבור פתיחת הקובץ לקריאה "w" עבור פתיחת הקובץ לכתיבה "a" עבור פתיחת הקובץ לשרשור )כתיבה בהמשכו( במקרה של כשלון מוחזר NULL 12
סגירת הקובץ מתבצעת ע"י קריאה לפונקציה :fclose int fclose (FILE* stream); בניגוד ל- free שליחת מצביע שהינו NULL תגרום להתרסקות התכנית ככלל, לא ניתן לשלוח NULL לפונקציות קלט/פלט המקבלת FILE* ערך ההחזרה הוא 0 בהצלחה ו- EOF במקרה של כשלון :EOF קבוע שלילי המשמש לציון שגיאות בפונקציות קלט פלט, בדרך כלל כאלו שנובעות מהגעה לסוף הקובץ file) (End of גם במקרה של כשלון לא ניתן יותר להשתמש בקובץ שנשלח 13
ניתן לכתוב ולקרוא מקובץ בעזרת הפונקציות fprintf ו- fscanf : int fprintf (FILE* stream, const char* format,...); int fscanf (FILE* stream, const char* format,...); התנהגותן זהה לזו של printf ו- scanf הרגילות מלבד הוספת הפרמטר stream שהוא הקובץ אליו יש לכתוב או ממנו יש לקרוא stream חייב להיות פתוח במצב המתאים כדי שהפעולה תצליח מצב המאפשר כתיבה עבור fprintf מצב המאפשר קריאה עבור fscanf :hello.txt לקובץ בשם Hello world FILE* stream = fopen("hello.txt", "w"); fprintf(stream, "Hello world!\n"); fclose(stream); דוגמה - כתיבת המחרוזת מה חסר? 14
ואת גודלו, קוראת שורה מ- stream ומעתיקה אותה אל החוצץ fgets מקבלת חוצץ char* fgets (char* buffer, int size, FILE* stream); אם אין מספיק מקום בחוצץ לשורה היא תיחתך מחזירה את buffer אם הקריאה הצליחה ו- NULL אחרת. כותבת את המחרוזת str לתוך :stream int fputs (const char* str, FILE* stream); fputs char buffer[buffer_size] = ""; fgets(buffer, BUFFER_SIZE, stream); printf("line is: %s",buffer); דוגמה - קריאת שורה מקובץ: 15
כתבו תכנית המקבלת כפרמטרים שני שמות קבצים <file1> ומעתיקה את תוכנו של <file1> ל-< file2 > ו-< file2 > > cat file1.txt This is text in a file >./copy file1.txt file2.txt > cat file2.txt This is text in a file דוגמה: 16
#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); 17
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(input); fclose(output); return 0; 18
הזכרנו בתרגול מספר 1 את שלושת ערוצי הקלט/פלט הסטנדרטיים: הקלט הסטנדרטי, הפלט הסטנדרטי וערוץ השגיאות הסטנדרטי. ערוצים אלו מיוצגים ב- C כמשתנים גלובליים מטיפוס :FILE* stdout ערוץ הקלט הסטנדרטי stdin ערוץ הקלט הסטנדרטי stderr ערוץ השגיאות הסטנדרטי הטיפוס FILE* משמש כהפשטה (abstraction) של ערוצי קלט/פלט מאפשר עבודה אחידה על דברים שונים ומקל על המשתמש בו 19
לרוב הפונקציות עבור קלט ופלט קיימות גרסאות נפרדות עבור שימוש בערוצים הסטנדרטיים: למשל עבור printf fprintf הפונקציה puts כותבת מחרוזת ל- stdout ומוסיפה ירידת שורה בסופה int puts (const char* str); הפונקציה gets קוראת שורה מקלט הסטנדרטי. המחרוזת אינה מכילה את ירידת השורה בתוכה מה חסר כאן? מה הופך את הפונקציה הזו למסוכנת? אסור להשתמש ב- gets ופונקציות דומות המאפשרות חריגה מהזיכרון לקלט זהו מקור לשגיאות זיכרון ובעיות אבטחה char* gets (char* buffer); 20
נשפר את התכנית :copy אם התכנית מקבלת שני שמות קבצים היא פועלת כמקודם אם התכנית מקבלת שם קובץ יחיד היא מדפיסה את תוכנו לפלט הסטנדרטי אם התכנית אינה מקבלת פרמטרים היא מדפיסה מהקלט הסטנדרטי אל הפלט הסטנדרטי כמו כן, הודעות השגיאה של התכנית יודפסו אל ערוץ השגיאות הסטנדרטי >./copy file1.txt This is text in a file >./copy Hello! Hello! 21
#include <stdio.h> #include <stdlib.h> #define CHUNK_SIZE 256 הפונקציה copy אינה משתנה מאחר והערוצים והיא מתאימה גם לערוצים הסטנדרטיים 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); 22
FILE* initinput(int argc, char** argv) { if (argc < 2) { return stdin; return fopen(argv[1], "r"); FILE* initoutput(int argc, char** argv) { if (argc < 3) { return stdout; return fopen(argv[2], "w"); 23
int main(int argc, char** argv) { if (argc > 3) { error("usage: copy <file1> <file2>", NULL); FILE* input = initinput(argc, argv); if (!input) { error("error: cannot open", argv[1]); FILE* output = initoutput(argc, argv); if (!output) { fclose(input); error("error: cannot open", argv[2]); copy(input, output); fclose(input); fclose(output); return 0; איזו בעיה עלולה להיווצר כאן? 24
מה ההבדל בין שתי הפקודות הבאות? כיצד הוא מתבטא בתכנית? >./copy data1.txt data2.txt >./copy < data1.txt > data2.txt 25
כעת נוכל לשפר את טיפוס הנתונים Date כך שיתמוך גם בקלט ופלט לקבצים: void dateprint(date date, FILE* stream) { fprintf(stream, "%d %s %d\n", date.day, date.month, date.year); bool dateread(date* date, FILE* stream) { if (date == NULL) { return false; if (fscanf(stream, "%d %s %d", &(date->day), date->month, &(date->year))!= 3) { return false; return dateisvalid(*date); 26
קבצים הם משאב מערכת יש להקפיד על ניהול נכון שלהם קבצים והערוצים הסטנדרטיים מיוצגים ע"י הטיפוס FILE* ניתן לכתוב ולקרוא מקבצים בדומה לביצוע פעולות קלט ופלט רגילות הערוצים הסטנדרטיים מיוצגים ב- C כמשתנים הגלובליים stdin, stdout stderr הכוונות קלט ופלט ניתנות לביצוע בתוך הקוד ומחוצה לו ע"י שימוש ב-* FILE ניתן ליצור פונקציות קלט/פלט לטיפוסי נתונים כך שיתאימו גם לכתיבה לערוצים סטנדרטיים וגם לקבצים ו- 27
28
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 כתבו תכנית אשר קוראת קובץ המכיל תאריכים היסטוריים כמתואר ומאפשרת למשתמש לחפש את רשימת המאורעות שקרו בתאריך מסוים 29
בשלב הראשוני לפתרון התרגיל עלינו להחליט מאילו טיפוסי נתונים נרכיב את הפתרון כלל אצבע לזיהוי הטיפוסים כלל אצבע לזיהוי פונקציות של הטיפוסים שמות עצם המופיעים בתיאור התכנית פעלים המופיעים בתיאור התכנית כתבו תכנית אשר קוראת קובץ המכיל תאריכים היסטוריים כמתואר ומאפשרת למשתמש לחפש את רשימת המאורעות שקרו בתאריך מסוים כאשר התכן הבסיסי הושלם ניתן לגשת לשלב הקידוד נוח להתחיל מטיפוסי הנתונים הבסיסיים ביותר 30
Historical Dates List Event Description Description Date Events list Next Description Next Description Next Description Date Events list Next Description Next ניצור רשימה של תאריכים היסטוריים: כל תאריך היסטורי יכיל את רשימת המאורעות שקרו בו Date Events list Next Description Description Next כדי להדפיס את רשימת המאורעות של תאריך מסוים נחפש אותו ברשימה ונדפיס את המאורעות שלו אם מצאנו תאריך כזה 31
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #define MAX 256 char* stringcopy(const char* str) { char* copy = malloc(strlen(str) + 1); return copy? strcpy(copy, str) : NULL; 32
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 = stringcopy(description); if (!event->description) { free(event); // A better idea? return NULL; event->next = NULL; return event; 33
Event eventaddnext(event head, Event newevent) { if (!head) { return newevent; Event ptr = head; while (ptr->next) { ptr = ptr->next; ptr->next = newevent; return head; 34
void eventdestroy(event event) { while (event) { Event temp = event; event = event->next; free(temp->description); free(temp); 35
typedef struct Historical_Date_t { Date date; Event eventlist; 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->eventlist = event; historicaldate->next = NULL; return historicaldate; 36
void historicaldatedestroy(historicaldate historicaldate) { if (!historicaldate) { return; historicaldatedestroy(historicaldate->next); eventdestroy(historicaldate->eventlist); free(historicaldate); HistoricalDate historicaldatefind(historicaldate historicaldate, Date date) { for (HistoricalDate ptr = historicaldate; ptr!= NULL; ptr = ptr->next) { if (dateequals(ptr->date, date)) { return NULL; return ptr; 37
HistoricalDate historicaldateadd(historicaldate historicaldate, Date date, Event event) { HistoricalDate target = historicaldatefind(historicaldate, date); if (target) { target->eventlist = eventaddnext(target->eventlist, event); return historicaldate; HistoricalDate newdate = historicaldatecreate(date, event); newdate->next = historicaldate; return newdate; void historicaldateprintevents(historicaldate historicaldate) { for (Event ptr = historicaldate->eventlist; ptr!= NULL; ptr = ptr->next) { printf("%s", ptr->description); 38
HistoricalDate readevents(char* filename) { FILE* fd = fopen(filename, "r"); if (!fd) { return NULL; 39 HistoricalDate history = NULL; char buffer[max] = ""; Date tempdate; while (dateread(&tempdate, fd) && fgets(buffer, MAX, fd)!= NULL) { Event newevent = eventcreate(buffer); history = historicaldateadd(history, tempdate, newevent); fclose(fd); return history;
int main(int argc, char** argv) { if (argc!= 2) { printf("usage: search_history <events file>\n"); return 0; HistoricalDate history = readevents(argv[1]); if (!history) { printf("error loading events from %s\n",argv[1]); return 0; printf("enter a date: "); Date tempdate; while (dateread(&tempdate, stdin)) { HistoricalDate h = historicaldatefind(history, tempdate); if (h) { historicaldateprintevents(h); else { printf("nothing special\n"); printf("enter a date: "); historicaldatedestroy(history); return 0; 40
ניתן להשתמש ברשימות מקושרות כדרך נוחה לשמירת נתונים כדי לפתור בעיה גדולה ניתן לחלק אותה לפי טיפוסי הנתונים המעורבים בפתרון את הקוד נוח לכתוב "מלמטה למעלה" 41