640 likes | 869 Views
תרגול 4. פיתוח מודולרי ב- C makefile ADT-Abstract Data Type דוגמא פשוטה ל ADT- (תאריך (. לי-טל משיח litalma@cs.technion.ac.il נערך והורחב ע''י ודים אייזנברג. הבעיה: קשה לפתח ולתחזק תוכנה גדולה. תוכנה גדולה יכולה להגיע למיליוני שורות קוד למוח האנושי קשה לתפוס כמות כזאת של מידע
E N D
תרגול 4 פיתוח מודולרי ב-C makefile ADT-Abstract Data Type דוגמא פשוטה לADT- (תאריך( לי-טל משיח litalma@cs.technion.ac.il נערך והורחב ע''י ודים אייזנברג
הבעיה: קשה לפתח ולתחזק תוכנה גדולה • תוכנה גדולה יכולה להגיע למיליוני שורות קוד • למוח האנושי קשה לתפוס כמותכזאת של מידע • יש צורך להבין, לדבג ולשנות את התוכנה לכל אורך החיים שלה
שלבים בחיים של תוכנה • פיתוח • תחזוקה • תוכנה יכולה "לחיות" הרבה שנים אחרי שהגרסה הראשונה פותחה • הוצאותעל תחזוקהמהוות עד 80% מכל ההוצאות על התוכנה • התוכנה משתנהכל הזמן • תיקוני באגים • הוספת אפשרויות/סביבות עבודה נוספות • שינוייםבחוקים/כללים עסקיים/נהלים שמצריכים שינויים בתוכנה
תחזוקה של התוכנה • התוכנה נקראת יותר פעמים מאשר נכתבת • לכן הקריאותכל כך חשובה • התוכנה צריכה להיכתבבראש ובראשונה בשביל האנשים שיתחזקו אותה • כמובן היא גם צריכה להתקמפל ולבצע את מה שמצופה ממנה • התוכנה צריכה להיכתב כך שיהיה • קל להביןאותה • קל לדבג אותה • קל לשנות אותה • קשה להכניס באגים
הפיתרון:תכנות מודולרי • חלוקת התוכנה למודוליםבצורה לוגית • בנוסף לחלוקה לפונקציות • הסתרת מידע לא נחוץ להבנה של המודלים • מקלה על התמודדותעם כמויות מידע גדולות • מאפשרת שינוי מימוש של המודולים בלי להשפיע על שאר התוכנה • שימוש חוזר במודולים (code reuse) • אין צורך לכתוב אותו קוד כמה פעמים • מקטין את התוכנה • פחות קוד להבין, לתחזק ולדבג
הפרדה לממשק ומימוש • כל מודול מורכב משני חלקים: • ממשק(interface) • מימוש(implementation) • ממשק מהווה מעין "חוברת הוראות שימוש" או "לוח כניסות" של המודול • מסתירכל פרטי המימוש של המודול הלא נחוצים לעבודה עם המודול
חלוקה למודולים והסתרת המימוש • הסתרת פרטים שלא נחוצים לעבודה עם המודול ועלולים לבלבל את המשתמשים
חלוקה למודולים והסתרת המימוש • כשרוצים להבין את התמונה הגדולה – מתעלמים מהמימוש של המודולים • כשרוצים להבין מימוש של מודול כלשהו – נכנסים פנימה ומבינים איך המודול מומש
שינוים במימוש של מודול כלשהו • שינויים במודול כלשהו לא משפיעים על שאר המודולים
שימוש חוזר במודולים בתוכנה אחרת תוכנה ב תוכנה א
יתרונות נוספים של תכנות מודולארי • כל מודול יכול להיכתבולהיבדקבנפרד • על ידי אנשיםאו צוותים שונים • בזמנים שונים • במקומות שונים
תכנות מודולארי ב-C : הממשק • הממשק נכתב בקובץ header(.h) • הקובץ מכיל • ההצהרותעל הפונקציותאותן מממש המודול • הגדרותשל קבועים והגדרות של טיפוסים אשר המשתמש במודול זה צריך להכיר • למשל ערכי שגיאה אפשריים של הפונקציות • הפונקציות המוצהרות ב header הינן השירותים אותם מספק המודול לשאר התוכנה
תכנות מודולארי ב-C : המימוש • המימושנכתב בקובץ .c • הקובץ מכיל • מימוש הפונקציות אשר הוצהרו ב-header • פונקציות פנימיות למודול אשר לא נועדו לשימוש מחוץ למודול • משתנים סטטיים של הקובץ
דוגמא: מודול תאריך • בתוכנית שנכתוב מתבצעות פעולותהקשורות לתאריכים • נממש את הפעולות במודול נפרד בשם date
קובץ ה-date.h :header #include <stdio.h> /* date module This module defines a date type and implements some date manipulation functions. */ typedefstructDate_t { int day; char month[4]; int year; } Date; typedefenum{DateSuccess,DateFail,DateFailRead, DateBadArgument} DateResult;
המשך קובץ ה-date.h :header /* reads a date from an open file – ‘inputFile’ expects the ‘date’ to point to an allocated Date */ DateResultdateRead (FILE* inputFile, Date* date); /* writes the date to the output file ‘outputFile’ */ DateResultdateWrite (FILE* outputFile, Date* date); /* returns the number of days between dates */ DateResultdateDifference(Date* date1, Date* date2, int* difference);
קובץ המימוש : date.c #include <string.h> #include <assert.h> #include <stdbool.h> #include "date.h" #define MIN_DAY 1 #define MAX_DAY 31 #define MIN_MONTH 1 #define MAX_MONTH 12 #define DAYS_IN_YEAR 365 static char* months[]= {"JAN", "FEB", "MAR", " APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; קבועים שמוגדרים בקובץ .c– מוסתרים משאר התוכנה משתנה סטאטי של קובץ – מוסתר משאר התוכנה
קובץ המימוש : date.c /* returns the date’s month number (1-12) */ staticDateResultdateToMonth(Date* date, int* month); /* returns the number of days passed from 01/01/00 */ staticDateResultdateToDays(Date* date, int* day); פונקציות פנימיות של המודול – מוסתרות משאר התוכנה
קובץ המימוש : date.c static boolisDayValid(int day){ return ((day >= MIN_DAY) && (day <= MAX_DAY)); } static boolisMonthNumberValid(intmonth){ return ((month >= MIN_MONTH) && (month <= MAX_MONTH)); { static boolisMonthStringValid(char*month){ for (inti = MIN_MONTH; i <= MAX_MONTH; i++){ if (strcmp(month, months[i-1]) == 0){ return true; } } return false; } פונקציות פנימיות של המודול – מוסתרות משאר התוכנה
קובץ המימוש : date.c DateResultdateRead(FILE* inputFile, Date* date) { if (inputFile == NULL || date == NULL) returnDateBadArgument; if (fscanf (inputFile, "%d %s %d", &(date->day), date->month, &(date->year)) != 3) returnDateFailRead; if (!isDayValid(date->day) || !isMonthStringValid(date->month)) returnDateFail; returnDateSuccess; }
קובץ המימוש : date.c DateResultdateWrite(FILE* outputFile, Date* date){ if(outputFile == NULL || date == NULL) returnDateBadArgument; if (fprintf (outputFile, "%d %s %d ", date->day, date->month, date->year) < 0) returnDateFail; returnDateSuccess; }
קובץ המימוש : date.c staticDateResultdateToMonth(Date* date, int* month) { int i = 0; if(date== NULL || month == NULL) returnDateBadArgument; for (i = MIN_MONTH; i <= MAX_MONTH; i++){ if (strcmp(date->month, months[i-1]) == 0){ *month = i; returnDateSuccess; } } returnDateFail; }
קובץ המימוש : date.c staticDateResultdateToDays(Date* date, int* days) { int month; if(date == NULL || days == NULL) returnDateBadArgument; if (dateToMonth(date,&month) != DateSuccess) returnDateFail; assert(isMonthNumberValid(month)); *days = date->day + month*(MAX_DAY – MIN_DAY + 1) + DAYS_IN_YEAR *date->year; returnDateSuccess; } אם ה-assert נכשל - יש באג ב-dateToMonth
קובץ המימוש : date.c DateResultdateDifference(Date* date1, Date* date2, int* difference) { int days1, days2; if(date1 == NULL || date2 == NULL || difference == NULL) returnDateBadArgument; if((dateToDays(date1,&days1) != DateSuccess) || (dateToDays(date2,&days2) != DateSuccess)) returnDateFail; *difference = (days1 > days2) ? (days1 – days2) : (days2 - days1); assert(*difference >= 0); returnDateSuccess; }
מודול שמשתמש במודול תאריך: proc.c #include<stdio.h> #include "date.h" intmain() { Date date1,date2; int difference; if (dateRead(stdin,&date1) == DateFail || dateRead(stdin,&date2) == DateFail) return 1; printf ("The difference between "); dateWrite(stdout,&date1); printf(" and "); dateWrite(stdout,&date2); dateDifference(&date1, &date2,&difference); printf("is %d days.\n", difference); return 0; }
תמיכת שפת C בכתיבת מודולים • כתיבת מודולים הוטמעה בשפת C בכך שבניית התוכנה נעשית בשני שלבים: • הידור(compilation) של כל קובץ בנפרד • קישור(linking) של כל הקבצים ביחד
הידור ב-C • כל קובץ מקומפל בנפרד • הבעיה: איך אפשר לקמפלקובץ כלשהו בנפרדאם הוא משתמש בפונקציות חיצוניות שנמצאות בקבצים אחרים ? • הפתרון: הקומפיילר אינו צריך לראות את המימוש של הפונקציות החיצוניות, אלא רק את הצהרותיהן • יש לעשות includeשל קבצי header שמכילים הצהרותשל פונקציותשהמודול משתמשבהן
שימוש בקבציheader • כל קובץ שצריך להשתמשבפריטים כלשהם של מודול מסוים, יבצע includeלקובץ הheader- שלו : #include “date.h” #include <stdio.h> • include של header של התוכנה • ניתן לציין path מלא של הקובץ • include של header סטנדרטי (של מערכת ההפעלה) • כל ה-headers הסטנדרטיים נמצאים במקום מוגדר מראש ע''י מערכת ההפעלה
הוראת include • חייבים לעשות includeרקלדברים שצריכיםאותם במקום שבו עושים include • עושים include ב-headerרק אם יש צורך בהגדרות של הקובץ הנכלל ב-header עצמו • בהצהרה של פונקציות של הheader- • המטרה - לצמצם תלויות • הוראות includeללא צורך או במקום הלא נחוץ - סגנון תכנות רע
מגנון #ifndef בקבצי header • מכיוון שקובץ ה-header עשוי להיכלל במספר קבצים, יש להשתמש במנגנון ifndef#, אשר בודק האם קבוע מסוים הוגדר. • רק בפעם הראשונה בה יתבצע,#include "date.h" הקובץ date.h ייכלל בפועל • חייב להופיע בכל קובץ header #ifndefDATE_H #define DATE_H /* file date.h – the header of module date */ ... #endif
הידור ב-C • תוצאת ההידור היא קובץ object • מכיל קוד מכונה שמתאים לקוד מקור של המודול • במקום קריאות לפונקציות חיצוניות יש "חורים" • ה"חורים" ימולאו בשלב הבא –שלב הקישור 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 14: e8 00 00 00 00callq 19 <foo+0x19> 19: c7 45 fc 00 00 00 00 movl $0x0,0xfffffffffffffffc(%rbp) 36: eb e8 jmp 20 <foo+0x20> 38: b8 00 00 00 00 mov $0x0,%eax 3d: e8 00 00 00 00callq 42 <foo+0x42> 42: c9 leaveq 43: c3 retq קוד מכונה ניתן פה להמחשה בלבד – אין צורך להבין אותו
קישור (linking) • ה-linkerמחבראת כל קבצי ה-object לתוכנית אחת • ממלאאת כל ה"חורים" בקבצי object במידע אמיתי
הידור וקישור על ידי gcc • כאשר מבצעים • ניתן להפריד את השלבים: • gccprog.cdate.c -o prog • מבוצעים שני השלבים ביחד – נוצר קובץ הרצה prog • gcc -c prog.cdate.c • יוצר את קבצי ה- object • gccprog.odate.o-o prog • מקשר את קבצי ה- objectויוצר קובץ הרצה prog
שינוי במודולים של תוכנה • אם קובץמסוים משתנה– אילו קבצים צריך לקמפל/לקשר מחדש? • אם השינוי נעשה בקובץ .cניתן לקמפל רק אותו • אם השינוי נעשה בקובץ header, חייבים לקמפל את כל הקבצים שתלויים בקובץ ששונה • ואחרי שקימפלנו קובץ כלשהוא חייבים לבצע קישור מחדש של כל קבצי ה-object
בנייה יעילה של תוכנה ממספר קבצים • דוגמא: תוכנית המורכבת מהקבצים הבאים: • calc.c • control.c • main_prog.c • בכל פעם שמשנים קובץ .c מסוים, מספיק להדר שוב רק אותו ולבצע קישור מחדש. • לדוגמא, אם שונה control.c, מספיק לבצע: gcc -c calc.c gcc -c control.c gcc -c main_prog.c gcccalc.ocontrol.omain_prog.o -o prog • gcc -c control.c • gcccalc.ocontrol.omain_prog.o -o prog
אוטומציה של בניית התוכנה • תהליך בניית תוכנה גדולה יכול להיות מורכב • אלפי קבצים (מודולים) • עלול לקחת כמה שעות • כל קובץ יכול לדרוש הידור בצורה שונה • דגלי קומפילציה שונים • שפות תכנות שונות • קומפיילרים שונים • תלויות מורכבות בין הקבצים • כשקובץ מסוים משתנה – אילו קבצים צריך לקמפל מחדש? • קיימת סכנה בפספוס חלק מהתלויות ובניית תוכנה פגומה כתוצאה מכך • לא יעיל לקמפל את כל הקבצים בשביל כל שינוי
אוטומציה של בניית התוכנה - make • ניתן להפוך את תהליךבניית התוכנה לאוטומטי ע"י שימוש בתוכנה make • מקבלת כקלט קובץ Makefileשבו מקודדים חוקי בניית התוכנה • אילו קבצים יש לקמפל ולקשר, מתי ובאילו תנאים • איךלקמפל/לקשר את הקבצים • תלויותבין הקבצים • כאשר משניםקובץ כלשהו ומריצים ,makeהפקודה מחשבת אילו צעדים בבניית התוכנה חייבים לבצע ומבצעת רק אותם • חוסכת קימפול מחדש של כל הקבצים
קבצי Makefile • קובץ ה-Makefileהמתאים לדוגמא הנ"ל הוא: • prog:main_prog.ocalc.ocontrol.o • gcccalc.ocontrol.omain_prog.o -o prog • calc.o:calc.c • gcc -c calc.c • control.o:control.c • gcc -c control.c • main_prog.o:main_prog.c • gcc -c main_prog.c
Makefile – דוגמא נוספת • תוכנה שמורכבת מהקבצים הבאים :
make • קובץ ה-Makefileהמתאים לתוכנה הזאת: • הפעלת makeעל התוכנה prog:a.ob.oc.o gcca.ob.oc.o-o prog a.o:a.ca.hb.h gcc -c a.c b.o:b.cb.h gcc -c b.c c.o:c.cc.hb.h gcc -c c.c > make > make prog
מבנה קובץ Makefile • קובץ Makefile מורכב ממספר כניסות • מבנה הכניסות : <target>: <other targets or files, the target depends on> <TAB> <build command for the target> • כל targetבדרך כלל מציין קובץ (קובץ מקור, header, object או הרצה) • #מסמן הערה עד סוף השורה • נקודות נוספות: • בלי TABבתחילת שורת הפקודה, הפקודה לא תתבצע. • כדי לשבור שורה ארוכה לכמה שורות יש להוסיף \ בסוף כל שורה פרט לאחרונה (ועדיין יש לשים TAB בתחילת כל שורה) • אין לשים רווחים לא בתחילת שורה ולא בסופה • סיום שורות ב-Enter
שימוש ב-make make [ -f filename ] [ target ] • makeמחפשאת קובץ ה-Makefile באופן הבא: • אם לא משתמשים באופציה -f הוא מחפש קובץ בשם makefile או Makefile בתיקיה הנוכחית. • אם משתמשים באופציה -f הוא מחפש את הקובץ filename • אםtargetמופיעבהפעלת make,הפקודה מבצעת את רשימת הפקודות המופיעות ב-target ב-Makefile • אם targetלא מופיע בהפעלת make הפקודה מבצעת את ה-target הראשון ב-Makefile
אופן פעולת make • בודקת את התלויות ב-target אותו הוא הולך לבצע • אם יש קובץ ברשימת התלויות שהוא חדש יותר מקובץ ה- target יש לבצע את הפקודה של ה-target על מנת לעדכנו • בדיקת התלויות נעשית באופן רקורסיבי, כך שיבוצעו כל הפקודות של הקבצים שבהם ה-target תלוי, בסדר הנכון
הגדרת מאקרו ב-Makefile • ב-Makefile כדאי להגדיר כמאקרו מחרוזת • שחוזרת כמה פעמים • שעשויה להשתנות בעתיד • ניתן להגדיר מאקרו בצורה הבאה: EXEC = prog • אחרי שמאקרו כלשהו מוגדר, ניתן להתיחס אליו על ידי $ושם המאקרו בסוגריים: ($(EXEC • קיימים מאקרו מוגדרים מראש: • $@ הוא ה-target הנוכחי • $* הוא ה-target הנוכחי ללא סיומת
דוגמא לשימוש במאקרו ב-Makefile CC = gcc OBJS = a.ob.oc.o EXEC = prog DEBUG_FLAG = # now empty, assign -g for debug COMP_FLAG = -c -Wall $(EXEC) : $(OBJS) $(CC) $(DEBUG_FLAG) $(OBJS) -o $@ a.o : a.ca.hb.h $(CC) $(DEBUG_FLAG) $(COMP_FLAG) $*.c b.o : b.cb.h $(CC) $(DEBUG_FLAG) $(COMP_FLAG) $*.c c.o : c.cc.hb.h $(CC) $(DEBUG_FLAG) $(COMP_FLAG) $*.c clean: rm -f $(OBJS) $(EXEC) > make clean rm -f a.o b.o c.o prog > make gcc -c -Wall a.c gcc -c -Wall b.c gcc -c -Wall c.c gcc a.o b.o c.o -o prog
יצירת תלויות ל-Makefile באופן אוטומטי • אפשר למצוא תלויות בין קבצים ע"י שימוש בפקודה הבאה: gcc -MM <c files> • למשל: • ניתן לשמורפלט זה בקובץ ולהשתמש בו כשלד ל- Makefile > gcc -MM *.c a.o : a.c a.h b.h b.o : b.c b.h c.o : c.c c.h b.h
ADT Abstract Data Type