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

Slides:



Advertisements
Similar presentations
C: Advanced Topics-II Winter 2013 COMP 2130 Intro Computer Systems Computing Science Thompson Rivers University.
Advertisements

I/O means Input and Output. One way: use standard input and standard output. To read in data, use scanf() (or a few other functions) To write out data,
Chapter 11: Data Files & File Processing In this chapter, you will learn about Files and streams Creating a sequential access file Reading data from a.
C Programming - Lecture 3 File handling in C - opening and closing. Reading from and writing to files. Special file streams stdin, stdout & stderr. How.
CSCI 171 Presentation 12 Files. Working with files File Streams – sequence of data that is connected with a specific file –Text Stream – Made up of lines.
1 CSE1301 Computer Programming: Lecture 21 File I/O.
ISP - 2 nd Recitation Functions Pointers Structs Files Code Examples Homework!
C Language Summary HTML version. I/O Data Types Expressions Functions Loops and Decisions Preprocessor Statements.
File I/O.
CSE1301 Computer Programming: Lecture 19 File I/O
An Introduction to C Programming (assuming that you already know Java; this is not an introduction to C++)
1 CSE1301 Computer Programming: Lecture 19 File I/O.
C Basic File Input/Output Manipulation C Programming – File Outline v File handling in C - opening and closing. v Reading from and writing to files.
Lone Leth Thomsen Input / Output and Files. April 2006Basis-C-8/LL2 sprintf() and sscanf() The functions sprintf() and sscanf() are string versions of.
File Handling Spring 2013Programming and Data Structure1.
22. FILE INPUT/OUTPUT. File Pointers and Streams Declarations of functions that perform file I/O appear in. Each function requires a file pointer as a.
File IO and command line input CSE 2451 Rong Shi.
ECE 103 Engineering Programming Chapter 44 File I/O Herbert G. Mayer, PSU CS Status 6/4/2014 Initial content copied verbatim from ECE 103 material developed.
1 File Handling. 2 Storage seen so far All variables stored in memory Problem: the contents of memory are wiped out when the computer is powered off Example:
1 Data Structures and Algorithms Programs. Different problems. Operations. Size of data. Resources available.
Kymberly Fergusson WELCOME CSE1303 Part A Data Structures and Algorithms Summer 2002.
Chapter 11: Data Files and File Processing Files and streams Creating a sequential access file Reading data from a sequential access file Using fgetc()
Chapter 7 : File Processing1 File-Oriented Input & Output CHAPTER 7.
1 CHAPTER6 CHAPTER 6. Objectives: You’ll learn about;  Introduction  Files and streams  Creating a sequential access file  Reading data from a sequential.
Using Files Declare a variable, called file pointer, of type FILE * Use function fopen to open a named file and associate this file with the file pointer.
CS 261 – Recitation 7 Spring 2015 Oregon State University School of Electrical Engineering and Computer Science.
C Programming Lecture 12 : File Processing
Gramming An Introduction to C Programming (assuming that you already know Java; this is not an introduction to C++)
Files A collection of related data treated as a unit. Two types Text
1 Computer Programming Lecture 15 Text File I/O Assist. Prof Dr. Nükhet ÖZBEK Ege University Department of Electrical&Electronics Engineering
Connecting to Files In order to read or write to a file, we need to make a connection to it. There are several functions for doing this. fopen() – makes.
6/9/2016Course material created by D. Woit 1 CPS 393 Introduction to Unix and C START OF WEEK 10 (C-4)
An Introduction to C Programming (assuming that you already know Java; this is not an introduction to C++)
 Dynamic Memory Allocation  Linked Lists  void main() {{ ◦ FILE *file; ◦ char file_name[] = “file.txt”; ◦ if ((file = fopen(file_name, "w")) ==
Chapter 4.
Chapter 22 – part a Stream refer to any source of input or any destination for output. Many small programs, obtain all their input from one stream usually.
Lecture 11 File input/output
An Introduction to C Programming
File I/O.
CS 261 – Recitation 7 Fall 2013 Oregon State University
CSC215 Lecture Input and Output.
Plan for the Day: I/O (beyond scanf and printf)
CS111 Computer Programming
CSE1320 Files in C Dr. Sajib Datta
CSE1320 Files in C Dr. Sajib Datta
Computer Programming Lecture 15 Text File I/O
Files I/O, Streams, I/O Redirection, Reading with fscanf
CSE1320 Files in C Dr. Sajib Datta
File I/O We are used to reading from and writing to the terminal:
CSC215 Lecture Input and Output.
CSE1320 Strings Dr. Sajib Datta
CSC215 Lecture Input and Output.
Input/Output and the Operating Systems
sscanf()- string scan function
Programming and Data Structure
C What you Know* Objective: To introduce some of the features of C. This assumes that you are familiar with C++ or java and concentrates on the features.
File Input and Output.
File Handling.
File I/O (1) Prof. Ikjun Yeom TA – Hoyoun
Line at a time I/O with fgets() and fputs()
Files.
C Preprocessing File I/O
Module 12 Input and Output
File I/O & UNIX System Interface
C Programming - Lecture 3
CSc 352 File I/O Saumya Debray Dept. of Computer Science
File I/O.
I/O CS580U - Fall 2018.
File I/O We are used to reading from and writing to the terminal:
Files Chapter 8.
Presentation transcript:

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

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

מבנים המצביעים לעצמם נסתכל על המבנה הבא: 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 מבוא לתכנות מערכות - 234122

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

דוגמה - הדפסה בסדר הפוך כתבו תכנית המקבלת רשימת תאריכים מהמשתמש ומדפיסה אותם בסדר הפוך פתרון: ניצור רשימה מקושרת של תאריכים לא ניתקל בבעיות בגלל חוסר ההגבלה על גודל הקלט בכל פעם שנקלוט תאריך חדש נוכל להוסיפו בקלות לתחילת הרשימה 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 מבוא לתכנות מערכות - 234122

פתרון למה לא ניתן להשתמש ב-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? כיצד ניתן לכתוב קוד זה עם רקורסיה? מבוא לתכנות מערכות - 234122

פתרון 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 תמיד כדאי לשים לב להשפעות שלה: חייב להיות מקום שבו המשאבים משוחררים. עשו לעצמכם הרגל כשאתם כותבים שורה שמקצה משאבים לחשוב איפה השחרור המתאים יהיה. יש להיזהר במיוחד בזמן טיפול בשגיאות, יציאה מהפונקציה באמצע עלולה להשאיר דליפות זיכרון. מבוא לתכנות מערכות - 234122

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

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

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

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

פתיחת קובץ באמצעות 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 דמיון רב למרות שהן עושות דברים שונים. מבוא לתכנות מערכות - 234122

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

פונקציות קלט/פלט עבור קבצים ניתן לכתוב ולקרוא מקובץ בעזרת הפונקציות 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 והתכנית תתרסק. שימו לב שיש מגוון רחב מאוד של שגיאות בפתיחת קובץ והן נפוצות מאוד (למשל משתמש שמכניס קלט לא חוקי) מה חסר? מבוא לתכנות מערכות - 234122

פונקציות קלט/פלט עבור קבצים 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 רק מדפיסה את המחרוזת שקיבלה כפי שקיבלה אותה. מבוא לתכנות מערכות - 234122

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

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); מבוא לתכנות מערכות - 234122

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; מבוא לתכנות מערכות - 234122

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

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

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

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 הוא בדרך כלל תכנות רע, המנעו משימוש בה מבוא לתכנות מערכות - 234122

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"); מבוא לתכנות מערכות - 234122

איזו בעיה עלולה להיווצר כאן? 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 ולשנות את ערכו. פתרון אשר משנה את ערך המשתנים הגלובליים ישפיע על קוד אחר בתוכנה גדולה אשר עוברת שינויים כל הזמן. איזו בעיה עלולה להיווצר כאן? מבוא לתכנות מערכות - 234122

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

קלט פלט עבור טיפוסי נתונים כעת נוכל לשפר את טיפוס הנתונים 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. מומלץ ליצור פונקציות כאלו לכל טיפוס נתונים ולו רק כדי לאפשר הצגה נוחה של עצמים למסך המקלה משמעותית על דיבוג הקוד. מבוא לתכנות מערכות - 234122

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

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

דוגמה - תאריכים היסטוריים 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 נתון קובץ המכיל תאריכים ומאורעות אשר התרחשו בהם בפורמט הבא: כתבו תכנית אשר קוראת קובץ המכיל תאריכים היסטוריים כמתואר ומאפשרת למשתמש לחפש את רשימת המאורעות שקרו בתאריך מסוים מבוא לתכנות מערכות - 234122

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

פתרון אפשרי ניצור רשימה של תאריכים היסטוריים: 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 מבוא לתכנות מערכות - 234122

פתרון אפשרי - מימוש #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’ והסיכוי לשכוח זאת ולגרום לבאג קטן. מבוא לתכנות מערכות - 234122

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. כך במקרה שנוספים עוד שדות עדיין הפונקציה תנקה אחרי עצמה כהלכה. מבוא לתכנות מערכות - 234122

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; ההוספה כאן מתבצעת בסוף הרשימה למרות שאין לכך חשיבות, וזאת על מנת להדגים כיצד מוסיפים איבר בסוף הרשימה מבוא לתכנות מערכות - 234122

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

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; מבוא לתכנות מערכות - 234122

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; מימוש רקורסיבי! שחרור הרשימה הפעם מתבצע ברקורסיה מבוא לתכנות מערכות - 234122

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); במקרה זה מוסף האיבר בתחילת הרשימה. שוב, אין חשיבות לסדר במקרה שלנו באמת ולכן סתם הדגמנו את שני המקרים. מבוא לתכנות מערכות - 234122

קריאת קובץ הקלט 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; מבוא לתכנות מערכות - 234122

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". מבוא לתכנות מערכות - 234122

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