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

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

תרגול מס' 2 מצביעים הקצאת זיכרון דינאמית מבנים - Structures

מצגות קשורות


מצגת בנושא: "תרגול מס' 2 מצביעים הקצאת זיכרון דינאמית מבנים - Structures"— תמליל מצגת:

1 תרגול מס' 2 מצביעים הקצאת זיכרון דינאמית מבנים - Structures
טיפוסי נתונים - Data types העברת פרמטרים ל-main וידוא הנחות בזמן ריצה

2 שימוש בסיסי אריתמטיקת מצביעים void* מצביע למצביע
מבוא לתכנות מערכות

3 מחרוזת “hello” המורכבת ממספר תווים
מבנה הזיכרון - תזכורת 0x0200 0x0201 0x0202 0x0203 0x0204 0x0205 0x0206 0x0207 0x0208 0x0209 0x020A 0x020B 0x020C 0x020D 7 104 101 108 111 int התופס 4 בתים מחרוזת “hello” המורכבת ממספר תווים ערך הבית כתובת הזיכרון מורכב מתאים הקרויים בתים (bytes) כל בית מכיל ערך מספרי פירוש הערכים כערך לא מספרי הוא ע"י התכנית לכל בית יש שם – כתובת הכתובת היא מיקומו בזיכרון בד"כ כתובות בזיכרון נרשמות בבסיס הקסדצימלי (16) משתנים מאוחסנים בבתים: טיפוסים שונים דורשים מספר שונה של בתים, למשל: int צורך 4 או 8 בתים char צורך בית יחיד. מספרים הקסדצימיליים נהוג לרשום עם “0x” מוביל וזאת כדי להבדיל בין המספר 10 ל-0x10 (שהוא כאמור 16). בד"כ כאשר מדפיסים כתובות זיכרון לצרכי debugging נהוג לכן להדפיסן כמספרים הקסדצימליים. הדבר אפשרי ע"י שימוש ב-%x ב-printf בשפת C. הגודל של char מובטח להיות "בית" 1 בשפת C/C++. הגודל של int הוא 4 או 8 בתים. אין צורך לדעת את גדלים אלו או להגדיר אותם כאשר יש בהם צורך, ניתן להשתמש באופרטור sizeof אשר מחזיר את גודלם, למשל sizeof(int) יהיה 4 או 8 בהתאם למכונה בה הקוד מקומפל. מבוא לתכנות מערכות

4 מצביעים - תזכורת עבור טיפוס T נקרא ל-T* מצביע ל-T
למשל int* הוא מצביע ל-int מצביע מסוג T* הוא משתנה אשר שומר כתובת של משתנה מטיפוס T. ניתן לקבל את כתובתו של משתנה ע"י שימוש באופרטור & לא ניתן להשתמש ב-& על ביטויים או קבועים (מדוע?) ניתן לקרוא את ערכו של המצביע ע"י אופרטור * פעולה זו קרויה dereferencing int n = 8; int* ptr = &n; // ptr now points to n printf("%d",*ptr); // dereferencing ptr 0x208 34 37 8 20 11 9 0x0204 0x0205 0x0206 0x0207 0x0208 0x020A 0x020B 0x020C בהכרזה על משתנה חדש ה-* נצמדת למה שמימינה, לכן למשל השורה הבאה: int *ptr, n; מכריזה על ptr מטיפוס int* ועל n מטיפוס int. אם ברצוננו להכריז על שני מצביעים באותה שורה יש לרשום: int *ptr1, * ptr2; בכל מקרה כמו שיוזכר בהמשך, הכרזה על מצביע ללא אתחולו היא מסוכנת מאוד ולכן שורה שבה הכרזת כמה מצביעים צריכה להיראות כך (תמיד): int *ptr1 = NULL, *ptr2 = NULL; בנוסף, המנעו מהכרזה על מספר משתנים בשורה אחת מהסיבות הבאות: על משתנים יש להכריז כמה שיותר קרוב לשימוש הראשון בהם (ב-C99 ניתן להכריז על משתנים בכל שורה בפונקציה ולא רק בתחילתה) שורות קצרות וקריאות יותר הכרזה על קבוצה של מספר משתנים בבת אחת בד"כ מעידה על חלוקה רעה לפונקציות לסיכום: ניתן בהכרזה לרשום את ה-* כצמודה לשם המשתנה או כצמודה לשם הטיפוס כאשר מדובר בהכרזה של משתמש יחיד. ההעדפה כאן היא שאלה של סגנון. (אם כי הצמדת ה-* לשם הטיפוס טבעית יותר במיוחד בהמשך הקורס במעבר ל-C++) אופרטור הכתובת & אינו יכול לקבל ביטויים או קבועים מאחר ואלו ערכים זמניים וכתובתם בזיכרון (אם בכלל קיימת כזו) אינה מסמלת כלום ולא מובטח עליה כלום. לכן ביטויים מהסוג הבא אינם מתקמפלים: int* ptr = &5; // 5 is a constant, you cannot get its address to change “5” int a = 2, b = 3; int* ptr2 = &(a+b); // a+b is a value, it does not have a defined address in memory and such an address cannot be kept. המונח lvalue משמש במדעי המחשב כדי לציין שערך מסוים יכול להיות בצידו השמאלי של אופרטור ההשמה =. כלומר lvalue הוא ערך שיש לו כתובת מוגדרת בזיכרון וניתן לעדכנו. לכן ניתן להגדיר עכשיו בצורה נוחה יותר את השימוש ב-&: ניתן להפעיל את אופרטור הכתובת & רק על lvalues. מבוא לתכנות מערכות

5 הכתובת 0x0 - NULL הכתובת 0 הינה כתובת לא חוקית:
אף עצם אינו יכול להיות בעל כתובת זו ניתן לעשות שימוש בכתובת זו כדי לציין שמצביע מסוים אינו מצביע לאף עצם כרגע השתמשו ב-NULL (שהוא #define ל-0) ולא בקבוע 0 מפורשות כאשר אתם מתייחסים לכתובת זו שימוש בקבועים כאלו משפר את קריאות הקוד נסיון לקרוא מצביע המכיל את הכתובת NULL יגרום לקריסת התוכנה. ב-UNIX תתקבל ההודעה: “segmentation fault” גישה למצביע המכיל "זבל" תגרום לתכנית להתנהג בצורה לא צפויה אסור להשאיר מצביעים לא מאותחלים בקוד! ניתן להכריז על המשתנה מאוחר יותר (C99) במקרה ולא ניתן יש לאתחל אותו ל-NULL מבוא לתכנות מערכות

6 אריתמטיקת מצביעים ניתן בנוסף לבצע פעולות חשבוניות על מצביעים
חיבור עם מספר שלם: int n = ...; int* ptr2 = ptr + n; התוצאה היא כתובתו של המשתנה מהטיפוס המתאים n תאים קדימה/אחורה חיסור שני מצביעים: int diff = ptr2 - ptr; התוצאה היא מספר שלם (int) ניתן לחסר רק מצביעים מאותו טיפוס פעולות אלו מאפשרות להסתכל על המשתנה הבא/הקודם בזיכרון הכרחי לשימוש במערכים ומחרוזות מסוכן – טעויות חשבוניות עלולות לגרום לחריגה ולקריאת "זבל" מהזיכרון שאלה: מדוע לא ניתן לחבר שני מצביעים? שימו לב להבדלים בהגדרת הפעולות החשבוניות עבור מצביעים: חיבור מצביע ומספר שלם (int) מחזיר מצביע חדש לאותו טיפוס. הקידום הוא לפי גודל המשתנה המוצבע כך למשל מצביע char* המקודם ב-1 יגדיל את ערכו ב-1 ואילו מצביע מטיפוס int* המקודם ב-1 יגדיל את ערכו ב-4 או 8 (sizeof(int) ליתר דיוק) חיסור שני מצביעים נותן לנו הפרש, ההפרש הוא מספר שלם (כמה משתנים מהטיפוס הזה נכנסים בזיכרון בין הכתובת הראשונה לכתובת השניה. התשובה היא מטיפוס שונה מאשר הפרמטרים לחיסור בניגוד לפעולות חיסור על משתנים פשוטים אחרים. לא ניתן לחבר שני מצביעים כי הפעולה תהיה חסרת משמעות, בהינתן שני משתנים, אחד בכתובת a והשני בכתובת b אין משמעות לכתובת a+b ולכן עדיף שקוד שכזה לא יתקמפל מלכתחילה כי הוא מעיד על שגיאות של של המתכנת. מבוא לתכנות מערכות

7 מצביעים – גישה למערך *(ptr + n) ≡ ptr[n]
ניתן להשתמש במצביע כדי לגשת למשתנים הנמצאים בהמשך בזיכרון, למשל כך: int* ptr = ...; int n = *(ptr + 5); האופרטור [ ] משמש כקיצור לפעולה זו: int n = ptr[5]; כלומר הפעולות הבאות שקולות: *(ptr + n) ≡ ptr[n] שימו לב להבדל בטיפוס המוחזר מ-ptr+n לבין ptr[n]. בהינתן ש-ptr הוא מטיפוס int*: ptr+n מחזיר מצביע חדש מטיפוס int*. ptr[n] מחזיר מספר שלם, כלומר מטיפוס int. כמו בפעולת ה-*, האחריות על כך שבזיכרון הנקרא אכן קיים משתנה חוקי היא של המשתמש, במקרה והדבר אינו נכון ההתנהגות אינה מוגדרת. מבוא לתכנות מערכות

8 ב-C99 ניתן להכריז על משתנה בתוך לולאת for
מצביעים ומערכים מערכים ומצביעים מתנהגים בצורה דומה ניתן להשתמש בשם המערך כמצביע לאיבר הראשון בו כאשר שולחים מערך לפונקציה ניתן לשלוח אותו כמצביע: void sort(int* array, int size); מצביע יכול לשמש כאיטרטור עבור מערך int array[N]; //... for(int* ptr = array; ptr < array+N; ptr++) { printf("%d ",*ptr); } הבדלים: הכרזה על מערך מקצה זיכרון כגודל המערך, הכרזה על מצביע אינה מקצה זיכרון לאחסון המשתנים! ניתן לשנות את ערכו של מצביע, אך לא ניתן לשנות את "ערכו" של תחילת המערך איטרטור - עצם המשמש למעבר על האיברים באוסף כלשהו. בהמשך נראה איטרטורים נוספים. הערות חשובות בקשר לשקף: הכרזה על משתנה הלולאה בתוכה עדיפה על הכרזה מוקדמת. היא מונעת מצבים של שימוש בטעות במשתנה לאחר הלולאה או בין הכרזתו לאתחולו. מעבר על מערך בעזרת מצביע לכאורה מהיר יותר ממעבר בעזרת אינדקס. עם זאת, זוהי דוגמה לדגשים החשובים באמת בקוד: עדיף קוד פשוט וקריא מקוד מהיר! אם הקוד יותר ברור עם אינדקס כותבים עם אינדקס! שיפור המהירות שולי, לא קידום המספר הוא שיאט תוכנה מודרנית. אל תהיו מוטרדים ממהירות התוכנה ובטח שאל תהיו מוטרדים ממנה לפני שאכן בדקתם מספרית שקטע קוד מסוים אחראי להאטה שלה. (90% מהקוד רץ 10% מהזמן, ולכן שיפורו שולי למהירות ממילא) קומפיילר טוב ממילא ידע לבצע את האופטימיזציה הזו בעצמו בקוד האסמבלר - אל תנסו לעשות את העבודה של הקומפיילר במקומו! זה מוביל לקוד פחות קריא וברוב המקרים גם פחות יעיל (בני אדם טועים הרבה יותר מקומפיילרים) ב-C99 ניתן להכריז על משתנה בתוך לולאת for מבוא לתכנות מערכות

9 void* ניתן להגדיר מצביעים מטיפוס void*. מצביעים אלו יכולים לקבל את כתובתו של כל משתנה לא ניתן לקרוא מצביע מטיפוס void*, יש להמירו קודם לכן int n = 5; double d = 3.14; void* ptr = &n; ptr = &d; double d2 = *ptr; // Error: cannot dereference void* double d3 = *(double*)ptr; // O.K. – option 1 double* dptr = ptr; // Implicit cast from void* to double* double d4 = *dptr; // O.K. – option 2 כמו ברוב המקרים עם מצביעים, שימוש ב-void* פותח פתח לשגיאות זיכרון בעייתיות בזמן ריצה. המרות בין void* למצביע אחר יבוצעו בצורה אוטומטית ב-C. כך למשל ניתן להציב את &n לתוך ptr ללא רישום מפורש של ההמרה. הערה להמשכו הרחוק של הקורס: ב-C++ המרות אלו אינן מתבצעות בצורה לא מפורשת. מצביע מטיפוס void* אינו זוכר את הטיפוס האמיתי של הכתובת אליה הוא מצביע ולכן ניתן בטעות לבצע המרות שיגרמו לתוצאות לא מוגדרות: int n = 5; void* ptr = &n; double d = *(double*)ptr; // ptr points to an int! not a double! // A correct conversion from integer // to floating point will not be made printf("%lf",d) // Prints an undefined string, on windows it will print "0.0000" מבוא לתכנות מערכות

10 מצביע למצביע ניתן ליצור מצביע לכל טיפוס, בפרט עבור טיפוס T* ניתן ליצור מצביע מטיפוס T** מתקבל מצביע למצביע של T אפשר להמשיך לכל מספר של * דוגמאות: שליחת מערך של מצביעים לפונקציה: void sort_pointers(int** array, int size); כתיבת פונקצית swap עבור מחרוזות: void swap_strings(char** str1, char** str2) { char* temp = *str1; *str1 = *str2; *str2 = temp; } מדוע יש כאן צורך במצביע למצביע? - char* מסמל מחרוזת, ולמעשה אנו צריכים להחליף את התוכן המוצבע על ידי משתנה המחרוזת. לכן אנו צריכים את הכתובת של המחרוזת, כדי שנוכל לשנותה. כפי שפונקצית swap למשתני int תראה כך: void swap_ints(int* a, int* b); פונקצית swap למשתני מחרוזת (שסוגם הוא char*) תזדקק למצביע כפול (char**). מבוא לתכנות מערכות

11 מצביעים - סיכום מצביעים משמשים להתייחסות לתאי זיכרון
ניתן לקבל את כתובתו של משתנה ע"י אופרטור & ניתן לקרוא ממצביע ולקבל את הערך המוצבע ע"י * הערך NULL מציין שאין עצם מוצבע ואסור לקרוא אותו ניתן לבצע פעולות חשבוניות על מצביעים מאפשר התייחסות למצביעים בדומה למערכים חשוב לאתחל מצביעים הרצת קוד הניגש למצביעים המכילים ערך לא תקין תגרום להתנהגות לא מוגדרת הכרזה על מצביע אינה מאתחלת זיכרון עבור המשתנה המוצבע! מצביע מטיפוס void* יכול להצביע לעצם מכל סוג ומשמש לכתיבת קוד גנרי מבוא לתכנות מערכות

12 סוגי משתנים הקצאת זיכרון שחרור זיכרון נזילות זיכרון
הקצאת זיכרון דינאמית סוגי משתנים הקצאת זיכרון שחרור זיכרון נזילות זיכרון מבוא לתכנות מערכות

13 סוגי משתנים את המשתנים השונים בקוד ניתן לסווג לפי טווח ההכרה ואורך חייהם: משתנים מקומיים: משתנים פנימיים של פונקציות. נגישים רק בבלוק בו הם הוגדרו. משתנים אלו מוקצים בכל פעם שהבלוק מורץ ומשוחררים בסופו. משתנים גלובליים: משתנים אשר מוגדרים לכל אורך התכנית וניתן לגשת אליהם מכל מקום. המשתנים מוקצים כאשר התכנית מתחילה ונשמרים לכל אורך זמן הריצה משתנים סטטיים של פונקציה: משתנים פנימיים של פונקציה. משתנים אלו שומרים על ערכם בין הקריאות השונות לפונקציה. מאותחלים בריצה הראשונה של הפונקציה, משוחררים בסוף ריצת התכנית משתנים דינאמיים: מוקצים ומשוחררים באופן מפורש ע"י המתכנת באמצעות קריאה לפונקציה הערות: קיימים גם משתנים סטטיים של קובץ. משתנים אלו זהים למשתנים גלובליים מלבד העובדה שהם נגישים רק מקוד באותו קובץ שבו הוגדרו. ב-C99 ניתן להכריז על משתנה מקומי גם באמצע בלוק, המשתנה יאותחל החל מהשורה בה הוכרז וישוחרר בסוף הבלוק. מבוא לתכנות מערכות

14 הרוע של משתנים גלובליים
משתנים גלובליים, משתנים סטטיים של קובץ (נלמד בהמשך) ומשתנים סטטיים של פונקציה נחשבים לתכנות רע הסיבה העיקרית לכך - שימוש במשתנים אלו מקשה על הבנת ודיבוג הקוד: כדי להבין פונקציה המשתמשת במשתנה גלובלי יש להסתכל בקוד נוסף קשה לצפות את תוצאת הפונקציה כי היא אינה תלויה רק בפרמטרים שלה קשה לצפות השלכות של שינויים על ערך המשתנה בשימוש במשתנה סטטי של פונקציה - בשביל לצפות את תוצאת הפונקציה צריך לדעת מה קרה בהרצות קודמות אין להשתמש במשתנים גלובליים במת"מ בקורסים מתקדמים בהמשך התואר תראו מקרים בהם חובה או מומלץ להשתמש במשתנים כאלו סיבה לא נכונה המופיעה רבות ברשימת הנימוקים נגד משתנים גלובליים היא "בזבוז שמות". אמנם, קריאה למשתנה גלובלי בשם קצר כמו max אכן תגרום נזק רב, אבל זו בעיה שולית. גם פונקציות "תופסות" שמות ובמקרה זה אנו פשוט נאלצים לתת שמות ארוכים יותר ומפורשים לפונקציות. לעומת זאת משתנה גלובלי, גם אם שמו ארוך מאוד, גורם לאותו נזק בקריאות התוכנה. לכן גם משתנים סטטיים של פונקציה או קובץ הם רעים כמעט באותה מידה כמו משתנה גלובלי, מאחר והיתרון היחיד שלהם הוא בכך ששמם אינו מפריע לכל שאר התוכנה. מבוא לתכנות מערכות

15 משתנים דינאמיים משתנים דינאמיים הם משתנים שזמן החיים שלהם הוא בשליטת המתכנת קוד מפורש מקצה אותם וקוד מפורש דרוש לשחרורם המשתנים מוקצים באזור זיכרון שקרוי ה-heap בניגוד למשתנים מקומיים שמוקצים על מחסנית הקריאות (ה-stack) stack מול heap: זיכרון ה-heap גדול משמעותית ממחסנית הקריאות. מחסנית הקריאות איננה מתוכננת להכיל כמויות נתונים גדולות במקרה של חוסר זיכרון ב-heap, התוכנית מקבלת על כך התראה מסודרת. לא ניתן להתאושש מכישלון הקצאה על המחסנית! נשתמש במשתנים דינאמיים כאשר: צריך לאחסן כמות גדולה של נתונים, או להקצות זיכרון בגודל שאינו ידוע מראש יש צורך לשמור את הנתונים בזיכרון גם לאחר יציאה מהפונקציה הגישה למשתנים דינאמיים נעשית תמיד בעזרת מצביעים ב-C99 ניתן להכריז על מערך שגודלו הוא גודל של משתנה. אך פתרון זה עדיין אינו מאפשר את שמירת המערך לאחר יציאה מהפונקציה, או מצבים שבהם יש להכריז על הרבה מערכים כאלו לאורך ריצת התכנית כך שיתקיימו בו זמנית. מבוא לתכנות מערכות

16 הקצאת זיכרון באמצעות malloc
void* malloc(size_t bytes); malloc מקבלת גודל בבתים של זיכרון אותו עליה להקצות ערך החזרה מכיל מצביע לתחילת גוש הזיכרון שהוקצה התוצאה היא תמיד גוש זיכרון רציף במקרה של כשלון מוחזר NULL לאחר מכן ניתן להתייחס לשטח המוצבע כאל משתנה או מערך: int* my_array = malloc(sizeof(int) * n); for (int i=0; i<n; i++) { my_array[i] = i; } size_t הוא טיפוס ייעודי עבור גדלים של זיכרון. מדוע ערך ההחזרה הוא void*? מאחר ולא נקבע הטיפוס של גוש הזיכרון המוחזר ולכן נשתמש במצביע גנרי. הטיפוס size_t הוא הטיפוס המוגדר ב-C/C++ כדי לייצג גדלים של זיכרון. size_t מכיל ערכים שהם מספרים שלמים אי שליליים. למרות הדמיון ל-unsigned int ייתכן ואלו טיפוסים שונים (בעיקר במערכות הפעלה 64 ביט) לא חייבים להמיר את המצביע מפורשות כי כאמור המרה מ-void* מתבצעת אוטומטית ב-C. ניתן בכל מקרה לבחור בשתי הדרכים: int* ptr = (int*)malloc(sizeof(int)); int* ptr = malloc(sizeof(int)); מבוא לתכנות מערכות

17 קביעת הגודל אותו מקצים כיצר נדע כמה בתים עלינו להקצות עבור משתנה מסוג int? נסיון ראשון: int* ptr = malloc(4); 4 הוא מספר קסם - מספר לא ברור המופיע בקוד שאינו 0 או 1 מספרי קסם הם הרגל תכנותי רע: פוגעים בקריאות הקוד מקשים על שינויים עתידיים בקוד למשל מעבר לסביבה בה גודלו של int הוא 8 ככל שצריך יותר שינויים הסיכוי לפספס אחד מהם גדל יש להימנע ממספרי קסם: ע"י הגדרת קבועים בעזרת #define שיקלו על שינויים ועל קריאת הקוד ע"י שמירת ערכם במשתנה קבוע בעל שם ברור במקרים מסוימים 1- הוא גם מספר תקין. למשל עבור החזרת 1,0 או 1- מפונקצית השוואה בדומה ל-strcmp. המנעו גם משימוש ב-0 ו-1 עבור ערכים בוליאניים והשתמשו ב-bool של C99 המוגדר ב-stdbool.h. מבוא לתכנות מערכות

18 קביעת הגודל - נסיון שני נסיון שני - נגדיר את הגודל של int כקבוע:
#define SIZE_OF_INT 4 int* ptr = malloc(SIZE_OF_INT); עכשיו הקוד קריא וקל לשנות את הערך אבל אם נעביר את הקוד לסביבה אחרת עדיין נצטרך לעדכן את הערך קוד שדורש שינויים במעבר בין סביבות שונות נקרא non-portable תמיד כדאי לשמור על הקוד portable ככל שניתן, קוד טוב שורד עשרות שנים וניתן להתאמה למערכות הפעלה חדשות. קוד שנכתב היטב בשנות ה-80 יכול לעבור ברובו ללא שינויים כך שיעבוד על טלפונים סלולריים למשל. מבוא לתכנות מערכות

19 אפשר להוריד את sizeof(char), מובטח שהוא תמיד 1
int* ptr = malloc(sizeof(int)); ניתן להפעיל את sizeof על שם טיפוס, על משתנה, או על ביטוי עבור שם טיפוס יוחזר הגודל בבתים של הטיפוס: עבור הפעלה על משתנה או ביטוי יוחזר הגודל של המשתנה או של ערך הביטוי בבתים: int* ptr = malloc(sizeof(*ptr)); // = sizeof(int) שימו לב להבדל בין גודל של מצביע לגודל העצם המוצבע מה נעשה אם ברצוננו להקצות זיכרון לעותק של מחרוזת? char* str = "This is a string"; char* copy = malloc(sizeof(char)*(strlen(str)+1)); sizeof(char) הוא תמיד 1 לפי ההגדרה של הסטנדרט ב-C. עם זאת, עבור הקצאות של מערכים מטיפוסים אחרים נצטרך להשתמש ב-sizeof(type)*size. אם sizeof מופעל על מערך בעל גודל קבוע (כלומר לא אחד שהוקצה דינאמית ע"י malloc) אז sizeof יחזיר את גודלו של כל המערך, למשל: int array[N]; int size = sizeof(array)/sizeof(array[0]); // size will be initialized to N, the size of the array. שימוש במשתנה ולא בשם הטיפוס המפורש עדיף מאחר והוא חוסך שכפול קוד אם נשתמש בשם הטיפוס, כאשר נשנה את הטיפוס נצטרך לזכור לשנות בשני המקומות. בד"כ תראו דווקא את השימוש בשם המשתנה - פשוט כי שיטה זו הייתה קיימת לפני הקודמת והושרשה עמוק בסגנון התכנות של C. כאשר מקצים זיכרון למחרוזת צריך לזכור להקצות מקום לתו המסיים ‘\0’. הפונקציה strlen מחזירה את אורכה של המחרוזת ללא תו זה ולכן יש להוסיף בית 1 להקצאה מדוע strlen מעצבנת ולא סופרת את התו המסיים? מאחר ובכל שאר מקרי השימוש שאינם לצורך שכפול מחרוזות נתעניין במספר התווים האמיתיים במחרוזת בד"כ. מומלץ לשכפל את כל המחרוזות בקוד בעזרת פונקציה אחת, וכך להימנע משכפול קוד של ה-1+. למה השיטה הזו עדיפה? אפשר להוריד את sizeof(char), מובטח שהוא תמיד 1 למה צריך 1+? מבוא לתכנות מערכות

20 בדיקת ערכי חזרה malloc עלולה להיכשל בהקצאת הזיכרון - במקרה זה מוחזר NULL מה קורה במקרה זה אם malloc נכשלת? int* my_array = malloc (sizeof(int) * n); for (int i=0; i<n; i++) { my_array[i] = i; } הפתרון: בדיקת ערך ההחזרה של פונקציות העלולות להיכשל וטיפול בו הטיפול צריך להופיע מיד לאחר ההקצאה ולפני השימוש הראשון int* my_array = malloc(sizeof(int) * n); if (my_array == NULL) { // or !my_array handle_memory_error(); בהמשך נראה מקרים נוספים של שגיאות יותר שכיחות ופשוטות להתמודדות בהרבה מקרים בשקפים "נשכח" לבדוק ערכי חזרה מפאת חוסר מקום. בכל מקרה הקפידו על בדיקות אלו בש"ב בקורס ובכלל. מבוא לתכנות מערכות

21 שחרור זיכרון באמצעות free
הפונקציה free משמשת לשחרור גוש זיכרון שהוקצה ע"י malloc void free(void* ptr); המצביע שנשלח ל-free חייב להצביע לתחילת גוש הזיכרון (אותו ערך שהתקבל מ-malloc) לאחר שחרור הזיכרון אסור לגשת יותר לערכים בזיכרון ששוחרר אם שולחים NULL ל-free לא מתבצע כלום (זו אינה שגיאה) כלומר אין צורך לבדוק את הפרמטר הנשלח ולוודא שאינו NULL למה זה טוב? אסור לשחרר את אותו זיכרון פעמיים או לשלוח ל-free מצביע שאינו מצביע לתחילת גוש זיכרון שהוקצה דינאמית (או NULL) int* my_array = malloc(sizeof(int) * n); // ... using my_array ... free(my_array); איך free יודעת כמה זיכרון לשחרר? ניהול ה-heap מתבצע על ידה ועל malloc ביחד כך שקיימים מבני נתונים ותיאום למציאת זיכרון פנוי פתרון פשוט כדי לזכור את הגודל המוקצה: malloc מקצה 4 בתים יותר, ומשתמשת ב-4 הבתים הראשונים לרשום את גודל ההקצאה. המצביע המוחזר הוא לתחילת הבתים הנותרים. Free במקרה הזה צריכה פשוט להסתכל על 4 הבתים שלפני הפרמטר כדי לדעת את גודל גוש הזיכרון. מבוא לתכנות מערכות

22 מקרי קצה במקרה ונשלח NULL ל-free לא מתבצע כלום
if (ptr != NULL) { free(ptr); } ניתן להחליף את הקוד הקודם בזה: NULL הוא מקרה קצה עבור free מה היה קורה אם free לא היתה מתמודדת עם מקרה הקצה הזה? עדיף לטפל במקרי קצה בתוך הפונקציה מונע מהמשתמש בה ליצור באגים ושכפולי קוד לעיתים קרובות נרצה להקצות הרבה זכרון ברציפות, ובמידה ואחת ההקצאות נכשלה – לשחרר את כל הזכרון. במצב כזה, נאתחל את כל המשתנים ל-NULL, לפני שנקצה אותם, ואם אחת או יותר מההקצאות נכשלה, לא תהיה בעיה לבצע free על כולם, מכיוון ש-free אינה עושה כלום ל-NULL. מבוא לתכנות מערכות

23 גישה לזיכרון אחרי ששוחרר
גישה לכתובת זיכרון שאינה מוקצה (או הוקצתה ושוחררה) אינה מוגדרת שחרור כפול של כתובת זיכרון אינו מוגדר קוד שתוצאתו אינה מוגדרת הוא קוד שמתקמפל ורץ אך תוצאת ריצתו אינה מוגדרת התוצאה יכולה להשתנות בין מערכות הפעלה, בין קומפיילרים, אפילו בין ריצות התוכנית יכולה לקרוס (למה זה עדיף?), או להמשיך לרוץ ולתת תוצאה שגויה בחלק מהמקרים התוצאה אף עשויה להתאים לציפיות, ולכן קשה לאתר שגיאה זו קוד שתוצאתו אינה מוגדרת הוא באג קשה לטיפול קשה לצפות את התנהגותו והשלכותיו יכול להשפיע על משתנים באזור אחד לחלוטין בקוד חשוב להקפיד על שימוש נכון בשפה כדי להימנע ממקרים אלו ההתנהגות של קוד לא מוגדר כשמה כן היא, לא מוגדרת. הכוונה כאן היא שלכל מימוש של השפה C קיים סטנדרט המכתיב את התנהגות הקוד במקרים מסוימים. חשוב לנו להקפיד על שימוש רק במקרים אלו - כלומר על כתיבת קוד סטנדרטי. כאשר התהנהגות הקוד לא מוגדרת היא יכולה להשתנות ממחשב למחשב ומהרצה להרצה. בפרט ייתכן שלא תופיעה אף שגיאה או תסמין במחשב אחד ובמחשב אחר התכנית תתרסק. דיבאגרים יכולים לאתר בקלות הרבה פתחים להתנהגות לא מוגדרת. מבוא לתכנות מערכות

24 התנהגות לא מוגדרת - דוגמה
האם שתי התכניות הבאות מתנהגות בצורה זהה? #include <stdio.h> #define N 7 int main() { int a[N] = {0}; int i; for (i=0; i < N; i++) { printf("%d\n", i); a[N-1-(i+1)] = a[i]; } return 0; #include <stdio.h> #define N 7 int main() { int i; int a[N] = {0}; for (i=0; i < N; i++) { printf("%d\n", i); a[N-1-(i+1)] = a[i]; } return 0; התשובה היא כמובן לא. ההתנהגות של שתי התכניות אינה מוגדרת מאחר והתכנית ניגשת מחוץ לגבולות המערך a. כאשר i=N-1 התכנית ניגשת לתא ה-1-. בגלל שגישה מחוץ לגבולות המערך אינה מוגדרת לא ניתן לצפות את התנהגות התכנית והיא משתנה בין קומפיילרים ומערכות הפעלה שונות. למשל: התכנית השמאלית נכנסת ללולאה אינסופית לאחר הידור ב-gcc על ה-stud מאחר והחריגה מגבולות המערך גורמת להשמת הערך 0 לתוך i בטעות. התכנית הימנית רצה על ה-stud לאחר קמפול עם gcc ללא בעיות ומדפיסה את המספרים 1 עד 6. שימו לב שמרגע שקיימת התנהגות לא מוגדרת, היא משפיעה גם במקומות לא צפויים, למשל במקרה זה החריגה מגבולות המערך משפיעה על התנהגות התכנית כתוצאה מהכרזה של משתנים בסדר שונה. מבוא לתכנות מערכות

25 דליפות זיכרון דליפת זיכרון מתרחשת כאשר שוכחים לשחרר זיכרון שהוקצה:
void sort(int* array, int n) { int* copy = malloc(sizeof(int) * n); // ... some code without free(copy) return; } דליפת זיכרון אינה גורמת ישירות לשגיאות בהתנהגות התוכנה דליפת זיכרון יגרמו לצריכת זיכרון גדלה של התוכנה ככל שזמן ריצתה גדל ולהאטת התוכנה ומערכת ההפעלה כולה תחת UNIX ניתן להשתמש בכלי valgrind לאיתור דליפות זיכרון valgrind מריץ את התכנית שלכם ומחפש גושי זיכרון שהוקצו אך לא שוחררו ניתן למצוא מידע נוסף על השימוש ב-valgrind בתרגול עזר 3 חלק גדול מדליפות הזיכרון הוא תוצאה של טיפול לוקה בשגיאות - כאשר נתקלים בשגיאה ויוצאים מפונקציה יש להקפיד לנקות את כל המשאבים שהוקצו בפונקציה לפני החזרה. דוגמה ספציפית אפשרית היא כאשר קיימות שתי הקצאות בפונקציה והשניה מביניהן נכשלת: void copy_two_strings(char* str1, char* str2, char** out1, char** out2) { char* temp1 = malloc(strlen(str1)+1); if (!temp1) { return; // O.K. } char* temp2 = malloc(strlen(str2)+1); if (!temp2) { return; // Memory leak!! Should have freed temp1 before returning. strcpy(temp1,str1); strcpy(temp2,str2); *out1 = temp1; *out2 = temp2; מומלץ להקצות משאבים מאוחר ככל הניתן בפונקציה ולשחררם מוקדם ככל האפשר כדי לצמצם שגיאות אלו כאשר פונקציה מבצעת הקצאה כלשהי (ייתכן בעקיפין!) יש לוודא שכל מסלולי היציאה מהפונקציה אינם שוכחים לנקות אחריהם מבוא לתכנות מערכות

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

27 הקצאת זיכרון דינאמית - סיכום
מומלץ לא להשתמש במשתנים גלובליים וסטטיים משתנים דינמיים משמשים בכל מקרה שצריך כמות זיכרון שאינה קטנה מאוד ניתן להשתמש ב-malloc ו-free כדי להקצות ולשחרר זיכרון בצורה מפורשת עבור יצירת מערכים גדולים או בגודל לא ידוע עבור שמירת ערכים לאורך התכנית ניהול הזיכרון מתבצע ע"י מצביעים לתחילת גוש הזיכרון שהוקצה יש לבדוק הצלחת הקצאת זיכרון יש לזכור לשחרר את הזיכרון המוקצה כאשר אין בו צורך יותר ניתן להשתמש ב-valgrind כדי למצוא בקלות גישות לא מוגדרות לזיכרון מבוא לתכנות מערכות

28 הגדרת מבנה פעולות על מבנים typedef
מבוא לתכנות מערכות

29 הטיפוסים הקיימים אינם מספיקים
נניח שברצוננו לכתוב תוכנה לניהול אנשי קשר, לכל איש קשר נשמור: שם פרטי, שם משפחה, מספר טלפון, כתובת וכתובת מגורים. לשם כך נצטרך לשמור 5 מערכים שונים! כל פונקציה שתצטרך לקבל את פרטיו של איש קשר כלשהו תצטרך לקבל 5 פרמטרים שונים לפחות! void someFunction(char* firstname, char* lastname, char* address, char* , int number, ... more?); כדי להימנע מריבוי משתנים ניתן להגדיר טיפוסים חדשים המהווים הרכבה של מספר טיפוסים קיימים void someFunction(Contact contact, ...); מבוא לתכנות מערכות

30 מבנים - Structures ניתן להגדיר טיפוסים חדשים המהווים הרכבה של מספר טיפוסים קיימים בעזרת המילה השמורה struct: struct <name> { <typename 1> <field name 1>; <typename 2> <field name 2>; ... <typename n> <field name n>; } <declarations>; הטיפוס החדש מורכב משדות: לכל שדה יש שם טיפוס השדה נקבע לפי הגדרת המבנה השדות של מבנה נשמרים בזיכרון ברצף ניתן להשתמש במערכים בעלי גודל קבוע כשדות - כל המערך נשמר במבנה ניתן להשתמש במצביעים כשדות - במקרה זה הערך המוצבע אינו חלק מהמבנה מבנים שונים ממערכים: במערכים כל המשתנים מאותו טיפוס במערכים לטיפוס אין שם ניתן להעביר מערכים מגדלים שונים לפונקציה, הדבר לא אפשרי עם מבנים (לפחות בלי התחכמות) חלק ה-declarations מאפשר הכרזה על משתנים מטיפוס המבנה ישירות. לא נשתמש בזה ישירות אלא רק בהמשך בשילוב עם typedef. מבוא לתכנות מערכות

31 מבנים - דוגמאות point date כל המערך נשמר בתוך המבנה למה 4? person
struct point { double x; double y; }; struct date { int day; char month[4]; int year; struct person { char* name; struct date birth; x=3.0 y=2.5 point day=31 month="NOV" date year=1971 כל המערך נשמר בתוך המבנה חשוב להבדיל בין שמירת הנתונים במבנה או שימוש במצביע - במקרה של שימוש במצביע נצטרך להתאים את העתקת והקצאות המבנה בהתאם. month מורכב מ-4 תווים כדי לכלול גם את התו ‘\0’ המציין את סוף המחרוזת. בדרך כלל מימוש עם char month[4] למערך הוא רעיון לא נוח ועדיף להשתמש ב-enum. הדוגמא כאן מובאת עם מערך כדי להדגים תכונות של מבנים. למה 4? name=0x0ffef6 birth person "Ehud Banai" day=31 month="MAR" year=1953 המחרוזת נשמרת מחוץ למבנה מבוא לתכנות מערכות

32 שימוש במבנים הטיפוס החדש מוגדר בשם struct <name> כדי לגשת לשדות של משתנה מטיפוס המבנה נשתמש באופרטור . (נקודה) struct point p; p.x = 3.0; p.y = 2.5; double distance = sqrt(p.x * p.x + p.y * p.y); עבור מצביע למבנה ניתן להשתמש באופרטור החץ <- struct point* p = malloc(sizeof(*p)); (*p).x = 3.0; // Must use parentheses, annoying p->y = 2.5; // Same thing, only clearer double distance = sqrt(p->x * p->x + p->y * p->y); חסרה בדיקה להצלחת ההקצאה (גם צריך לזכור לשחרר את הזיכרון בהמשך) מאחר והמבנה נשמר בגוש זיכרון רציף ניתן להקצות גוש זיכרון כגודל המבנה ולהתשתמש בו מה חסר? מבוא לתכנות מערכות

33 פעולות על מבנים ניתן לאתחל מבנים בעזרת התחביר הבא:
struct date d = { 31, "NOV", 1970 }; ניתן לבצע השמה בין מבנים מאותו הטיפוס: struct date d1,d2; // ... d1 = d2; במקרה זה מתבצעת השמה בין כל שני שדות תואמים מבנים מועברים ומוחזרים מפונקציות by value – כלומר מועתקים גם במקרה זה מתבצעת ההעתקה שדה-שדה הפעולות האלו אינן מתאימות למבנים מסובכים יותר (בד"כ בגלל מצביעים) מבוא לתכנות מערכות

34 מבנים עם מצביעים מבנים המכילים מצביעים אינם מתאימים בדרך כלל לביצוע השמות והעתקות מה יקרה אם נבצע השמה בין שני המבנים בדוגמה זו? מסיבה זו וכדי למנוע העתקות כבדות ומיותרות של מבנים בדרך כלל נשתמש במבנים ע"י מצביעים נשלח לפונקציות (ונקבל כערכי חזרה) מצביעים למבנה יוצא הדופן הוא מבנים קטנים ופשוטים כגון point name=0x0ffef6 birth person1 "Ehud Banai" day=31 month="MAR" year=1953 name=0x0ffed0 birth person2 "Yuval Banai" day=9 month="JUN" year=1962 ביצוע השמה בין המבנים ייצור דליפת זיכרון מהזיכרון עבור המחרוזת של אחד המבנים ובנוסף שני המבנים יצביעו מעתה לאותה מחרוזת, מכאן הדרך ל-free כפול לאותה כתובת קצרה. אותן בעיות יקרו כתוצאה מהעתקות. מבוא לתכנות מערכות

35 הגדרת טיפוסים בעזרת typedef
typedef int length; פקודת typedef עובדת על שורת הכרזה של משתנה – אך מגדירה טיפוס חדש במקום משתנה. נשתמש בפקודת typedef כדי לתת שמות נוחים לטיפוסים: typedef struct point Point; במקרה זה נוכל להתייחס למבנה מעכשיו כ-Point (ללא המילה השמורה struct) נוח לתת שם גם לטיפוס המצביע למבנה: typedef struct date Date, *pDate; עבור מבנים מסובכים נשתמש תמיד במצביעים ולכן במקרים האלו נשמור את השם ה"נוח" לטיפוס המצביע: typedef struct person *Person; כדי להבין פקודת typedef מסובכת הסתכלו על השורה ללא המילה tyepdef. השורה ללא ה-typedef היא שורת הכרזה על משתנים. הוספת ה-typedef אומרת שבמקום כל הכרזה של משתנה מטיפוס מסוים מוכרז שם חדש לטיפוס הזה. מבוא לתכנות מערכות

36 הגדרת טיפוסים בעזרת typedef
typedef struct point { double x; double y; } Point; ניתן להשמיט את שם הטיפוס בהגדרה ולהשאיר רק את השם החדש: typedef enum { RED, GREEN, BLUE } Color; typedef struct { כזכור, הגדרת מבנה מאפשרת הכרזה על משתנים מטיפוס המבנה כבר בסופה (בעיקר כדי לאפשר מבנים אנונימיים, תכונה שאין לנו בה צורך בקורס). לכן ניתן להוסיף typedef בתחילת ההכרזה על המבנה ולהכריז על שמות הטיפוסים בבת אחת. שימו לב שתוקף הכרזת ה-typedef רק מאחריה ולכן שם המבנה עוד לא מוכר באמצע ההכרזה עליו (משפיע על הגדרת רשימות מקושרות שיילמדו בתרגול 3) מבוא לתכנות מערכות

37 מבנים - סיכום מבנים מאפשרים הרכבה של מספר טיפוסים קיימים כדי להקל על קריאות הקוד מבנה מורכב משדות בעלי שם ניתן לגשת לשדות ע"י האופרטורים . ו- ->. העתקה והשמה של מבנים בטוחה כל עוד אין בהם מצביעים מומלץ להשתמש ב-typedef כדי לתת שם נוח לטיפוס החדש מבוא לתכנות מערכות

38 טיפוסי נתונים מבוא לתכנות מערכות

39 טיפוסי נתונים – Data types
typedef struct date_t { int day; char month[4]; int year; } Date; int main() { Date d1 = {21, "NOV", 1970}; Date d2; scanf("%d %3s %d", &d2.day, d2.month, &d2.year); printf("%d %s %d\n", d1.day, d1.month, d1.year); printf("%d %s %d\n", d2.day, d2.month, d2.year); // deja-vu if (d1.day == d2.day && strcmp(d1.month,d2.month) == 0 && d1.year == d2.year) { printf("The dates are equal\n"); } return 0; אלו בעיות יש בקוד הזה? שכפול קוד בהדפסת התאריך - אם נרצה לשנות את שיטת ההדפסה של התאריך (שאכן משתנה בין מדינות) נצטרך לעדכן את הקוד במספר מקומות אין בדיקה של נכונות התאריך - המשתמש יכול להכניס ערכים לא נכונים לתאריך, כולל תאריכים לא נכונים כמו 31 בספטמבר הקוד לבדיקת שוויון התאריכים יוצר תנאי ארוך ולא נוח לקריאה, בנוסף, אם התוכנה תגדל יתבצעו השוואות נוספות וגם הן יגרמו לשכפול קוד מבוא לתכנות מערכות

40 טיפוסי נתונים - Data types
תאריך הוא יותר מהרכבה של שני מספרים שלמים וארבעה תווים לא כל צירוף של ערכים עבור המבנה Date הוא אכן תאריך חוקי 5 BLA אין חודש מתאים ל-“BLA” 31 SEP ב-ספטמבר יש רק 30 ימים 29 FEB בפברואר 2010 יש רק 28 ימים מי שמשתמש במבנה התאריך צפוי להשתמש בו בצורות מסוימות הדפסת תאריך מציאת התאריך המוקדם יותר מבין שני תאריכים מציאת מספר הימים בין שני תאריכים מבוא לתכנות מערכות

41 טיפוסי נתונים - Data types
כדי לוודא את נכונות השימוש בתאריכים ולמנוע את שכפולי הקוד בשימוש בתאריכים עלינו לכתוב פונקציות מתאימות לטיפול בתאריכים לצירוף של טיפוס והפעולות האפשריות עליו קוראים טיפוס נתונים - Data type טיפוסי הנתונים המובנים בשפה נקראים טיפוסי נתונים פרימטיביים למשל int, float ומצביעים (לכל אחד מהם פעולות שונות אפשריות) יצירת טיפוסי נתונים מהווה את הבסיס לכתיבת תוכנה גדולה בצורה מסודרת ופשוטה מבוא לתכנות מערכות

42 טיפוס נתונים לתאריך מבצעים include רק לקבצים שהכרחיים לקמפול הקוד:
#include <stdio.h> #include <string.h> #include <stdbool.h> #define MIN_DAY 1 #define MAX_DAY 31 #define INVALID_MONTH 0 #define MONTH_NUM 12 #define DAYS_IN_YEAR 365 #define MONTH_STR_LEN 4 const char* const months[] = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; typedef struct Date_t { int day; char month[MONTH_STR_LEN]; int year; } Date; מבצעים include רק לקבצים שהכרחיים לקמפול הקוד: stdio.h - עבור printf ו-scanf string.h - עבור strcmp stdbool.h - עבור הגדרת הטיפוס bool ב-C99 ניתן להגדיר משתנים כ-const ומומלץ לעשות זאת עבור קבועים כגון months. עם זאת שימוש ב-const יילמד לעומק רק בתרגולי ה-C++ ולכן נתעלם ממנו בינתיים. ההכרזה עם const עבור months צריכה להיות: const char* const months[] = { "JAN", "FEB", "MAR", " APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; הערך של MONTH_LEN מוכרח להיות קבוע (לכן הוא מוגדר כ- define ולא const), כיוון שב- c לא ניתן להגדיר מערך עם טיפוס שהוא משתנה. הגדרת קבועים מבוא לתכנות מערכות

43 טיפוס נתונים לתאריך /** writes the date to the standard output */ void datePrint(Date date); /** Reads a date from the standard input. * Returns true if valid, false otherwise */ bool dateRead(Date* date); /** Returns true if both dates are identical */ bool dateEquals(Date date1, Date date2); /** Returns the number of days between the dates */ int dateDifference(Date date1, Date date2); /** Translates a month string to an integer */ int monthToInt(char* month); /** Calculates the number of days since 01/01/0000 */ int dateToDays(Date date); /** Checks if the date has valid values */ bool dateIsValid(Date date); מומלץ לתעד לפחות בקצרה את משמעות הפונקציות מעל הכרזתן תיעוד צריך להופיע מעל הפונקציה ולא בתוכה הערות באמצע הקוד בד"כ מיותרות או מסבירות קוד שהיה צריך להיכתב ברור יותר מבוא לתכנות מערכות

44 טיפוס נתונים לתאריך מפושט משמעותית מפושט משמעותית
int monthToInt(char* month) { for (int i = 0; i < MONTH_NUM; i++) { if (strcmp(month, months[i]) == 0) { return i+1; } return INVALID_MONTH; int dateToDays(Date date) { int month = monthToInt(date.month); return date.day + month*(MAX_DAY - MIN_DAY + 1) + DAYS_IN_YEAR * date.year; bool dateIsValid(Date date) { if (date.month[MONTH_STR_LEN-1] != ‘\0’) return false; return date.day >= MIN_DAY && date.day <= MAX_DAY && monthToInt(date.month) != INVALID_MONTH; dateIsValid מפושטת משמעותית. כדי לבדוק חוקיות תאריך יש בדיקות נוספות. בדיקות אלו לא נכללו כאן כדי לשמור על קוהרנטיות הדוגמה אך שימו לב שיש לבדוק: מספר ימים לכל חודש קיום 29 בפברואר גם dateToDays מבצעת פישוט לא נכון של הבעיה, ממספר הימים משתנה בין חודשים. מפושט משמעותית מפושט משמעותית מבוא לתכנות מערכות

45 טיפוס נתונים לתאריך יש לבדוק את תקינות הקלט בכניסה לפונקציה
void datePrint(Date date) { printf("%d %s %d\n", date.day, date.month, date.year); } bool dateRead(Date* date) { if (date == NULL) { return false; if (scanf("%d %3s %d", &(date->day), date->month, &(date->year)) != 3) { return dateIsValid(*date); יש לבדוק את תקינות הקלט בכניסה לפונקציה במיוחד מצביעים! בהמשך נראה כיצד ניתן להכליל פונקציות מצורה זו כך שיעבדו עם מגוון ערוצים ולא רק הקלט והפלט הסטנדרטיים. בהמשך נשתמש בערכי שגיאה מפורטים יותר ולא סתם true/false. המנעו משכפול קוד, אם קוד כלשהו כבר נכתב הקפידו לקרוא לפונקציה המבצעת אותו ולא לכתוב אותו מחדש! אם אין פונקציה מתאימה וקוד חוזר על עצמו - יש לכתוב פונקצית עזר ולקרוא לה! מבוא לתכנות מערכות

46 טיפוס נתונים לתאריך bool dateEquals(Date date1, Date date2) { return date1.day == date2.day && strcmp(date1.month,date2.month) == 0 && date1.year == date2.year; } int dateDifference(Date date1, Date date2) { int days1 = dateToDays(date1); int days2 = dateToDays(date2); return days1 - days2; מבוא לתכנות מערכות

47 פונקצית ה-main המעודכנת
int main() { Date date1 = { 21, "NOV", 1970 }; Date date2; if(!dateRead(&date2)) { printf("Invalid date\n"); return 0; } datePrint(date1); datePrint(date2); if (dateEquals(date1,date2)) { printf("The dates are equal\n"); } else { int diff = dateDifference(date1,date2); printf("The dates are %d days apart\n", abs(diff)); מבוא לתכנות מערכות

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

49 הפרמטרים argc ו-argv תכנית לדוגמה
העברת פרמטרים ל-main הפרמטרים argc ו-argv תכנית לדוגמה מבוא לתכנות מערכות

50 העברת פרמטרים ל-main את הפונקציה main המתחילה את ריצת התכנית ניתן להגדיר גם כך: int main(int argc, char** argv) במקרה זה יילקחו הארגומנטים משורת ההרצה של התכנית ויושמו לתוך המשתנים argc ו-argv ע"י מערכת ההפעלה argc יאותחל למספר הארגומנטים בשורת הפקודה (כולל שם הפקודה) argv הוא מערך של מחרוזות כאשר התא ה-𝑛 בו יכיל את הארגומנט ה-𝑛 בשורת הפקודה בנוסף, קיים איבר אחרון נוסף במערך המאותחל ל-NULL מבוא לתכנות מערכות

51 כיצד ניתן לכתוב את הקוד הזה ללא שימוש במשתנה argc?
דוגמה - תכנית echo #include <stdio.h> int main(int argc, char** argv) { for(int i = 1; i < argc; i++) { printf("%s ", argv[i]); } return 0; > ./echo Hello world Hello world > ./echo Hello > world > cat world Hello כיצד ניתן לכתוב את הקוד הזה ללא שימוש במשתנה argc? שימו לב: argc תמיד גדול או שווה ל-1 argv[0] הוא שם פקודת ההרצה argv[1] הוא הארגומנט ה"אמיתי" הראשון argv[argc-1] הוא הארגומנט האחרון argv[argc] הוא תמיד NULL כל המחרוזות ב-argv הן כמובן מחרוזות חוקיות של C - כלומר מסתיימות בתו ‘\0’. כדי לכתוב את הקוד ללא שימוש ב-argc ניתן להסתמך על האיבר האחרון המאותחל ל-NULL ב-argv. הלולאה תיראה כך: for(char** ptr = argv; *ptr != NULL; ptr++) { printf("%s ", *ptr); } כאשר משתמשים בהכוונת קלט/פלט מערכת ההפעלה מטפלת בהכוונה לפני הקריאה לפקודה, לכן מהפקודה השניה מורד החלק “> world” ומפורש כהפניית הפלט לקובץ world. הפקודה שנשארת היא פקודת ההרצה אותה מנתחים לקביעת הערכים של argv ו-argc. argc הוא קיצור של argument counter argv הוא קיצור של argument vector argv[0] "./echo" "Hello" "world" argv[1] argv[2] argv[3] argv 3 argc לאן נעלמה המילה world? מבוא לתכנות מערכות

52 הערות בתוך הקוד שימוש במאקרו assert כיבוי המאקרו מתי משתמשים ב-assert
וידוא הנחות בזמן ריצה הערות בתוך הקוד שימוש במאקרו assert כיבוי המאקרו מתי משתמשים ב-assert מבוא לתכנות מערכות

53 הערות בתוך הקוד מה הבעיה בקוד הזה? int main(int argc, char** argv) {
if (argc > 3) { ... } else if ( argc < 2) { } else { // if we are here argc is 2 } הערות בתוך הקוד אינן מחייבות ואינן נאכפות. ההערה יכולה להכיל שקר - לא ניתן לסמוך על הערות. הקוד יכול להשתנות - מתכנת שישנה את התנאי הראשון ל- n>2 לאו דווקא ישים לב להשפעה בהמשך הקוד. הסיכוי לבעיה זו גדל כאשר ההנחות מסובכות יותר וחוצות פונקציות. מבוא לתכנות מערכות

54 // if we are here argc is 2 −−→ assert(argc == 2);
assert(<expression>); המאקרו מוגדר בקובץ הממשק assert.h ועל מנת להשתמש בו יש לעשות #include. בזמן ריצת הקוד הביטוי מוערך ונבדק אם הוא נכון - לא קורה כלום והקוד ממשיך אם הוא אינו נכון - התכנית נעצרת ומודפסת הודעה המפרטת את מיקום ההנחה שכשלה. נשתמש ב-assert כדי להגן על הקוד מפני הכנסת באגים. // if we are here argc is 2 −−→ assert(argc == 2); שינויים עתידיים המפרים הנחות קיימות יגרמו להתראות מוקדמות הנחות לא נכונות לגבי הקוד יימצאו כבר בפעם הראשונה שהן אינן מתקיימות > ./prog a > ./prog a b prog: main.c:12: main: Assertion `argc==2' failed. Abort יתרון החשוב משימוש במאקרו assert הוא הודעת השגיאה המפורטת. השוו את הודעה זו להודעה כמו “Segmentation fault” שממנה קשה להבין היכן התרחש הבאג. יתרון נוסף הוא בתפיסת השגיאות מוקדם יותר - לעתים שגיאה יכולה להתרחש מוקדם אך לגרום לבאג הראשון במקום אחר בקוד מאוחר יותר. שגיאות שהמרחק בין היווצרותן לגילויין מקשות על דיבוג. שימוש ב-assert עוזר למצוא שגיאות מוקדם יותר. כיצד המאקרו assert יודע איך להדפיס מידע כגון מספר השורה בקובץ שבה קרתה השגיאה? בשפת C מוגדרים מספר "קבועים" מיוחדים עבור ה-preprocessor כמו למשל __LINE__ קבועים אלה מוחלפים לפני ההידור על ידי העיבוד המקדים אך בניגוד לקבועים רגילים הם מוחלפים בערכים מיוחדים. כך למשל __LINE__ מוחלף במספר השורה הקובץ. __FILE__ מוחלף בשם הקובץ ועוד. שימו לב ש-assert חייב להיות מוגדר כמאקרו, זאת מאחר ופונקציה אינה יכולה לגשת למידע בצורות הדרושות עבור assert - למשל כתיבת הפרמטר המועבר אליה מחדש כמחרוזת. (בשביל לעשות זאת ניתן להוסיף # לפני שימוש בפרמטר בהגדרת מאקרו) למידע מדויק וברור יותר לגבי תכונות מתקדמות של ה-preprocessor ניתן לבדוק את הקישור הבא: הקוד הנוצר ע"י assert(x > y) נראה בערך כך: #ifndef NDEBUG if(!(x > y)) { fprintf(stderr, "%s:%d: %s: Assertion ‘x > y’ failed.", __FILE__,__LINE__,__func__); abort(); } #endif מבוא לתכנות מערכות

55 כיבוי המאקרו ניתן לכבות את המאקרו assert ע"י הגדרת הקבוע NDEBUG
#define NDEBUG אם NDEBUG מוגדר המאקרו יוחלף בקוד שאינו עושה כלום כך ניתן לשחרר גרסה סופית של הקוד שאינה מואטת ע"י הבדיקות ללא הסרתן ידנית ניתן להגדיר את NDEBUG ישירות משורת ההידור ע"י הוספת הדגל -DNDEBUG הדגל -D<string> מוסיף בתחילת כל קובץ הגדרה של המאקרו בשם <string> שימו לב: קוד שבתוך המאקרו לא יורץ כלל אם המאקרו כבוי אסור לשים חישוביים הכרחיים לקוד בתוך assert. מה הבעיה כאן? מה הפתרון? assert(doSomethingImportant() != FAILED); אם המאקרו כבוי השורה assert(doSomethingImportant() != FAILED); תוחלף בקוד ריק והקריאה ל-doSomethingImportant לא תתבצע. כדי לבצע את הבדיקה הזו השתמשו בקוד הבא: Result result = doSomethingImportant(); assert(result != FAILED); במקרה זה הקוד מתבצע בכל מקרה, ללא קשר למצבו של המאקרו assert. וכמובן שעם המאקרו דלוק אז הבדיקה מתבצעת כרגיל. נוצרת בעיה מסויימת בקוד הנ"ל: כאשר הקוד מתקמפל עם הדגל –DNDEBUG המשתנה result אינו בשימוש, והקומפיילר ייתן על כך אזהרה. מכיוון שבקורס נשתמש בדגל –Werror, האזהרה תהפוך לשגיאה והקוד לא יתקמפל. פתרון אפשרי הוא להגדיר את המשתנה רק עם הדגל NDEBUG כבוי. כלומר: #ifndef NDEBUG Result result = #endif doSomethingImportant(); מבוא לתכנות מערכות

56 מתי משתמשים ב-assert ב-assert משתמשים לוידוא נכונות של הנחות הנעשות בקוד אם ההנחות שגויות ייתכן וקיימים באגים נוח לבדוק עם assert את נכונות הארגומנטים, ערכי החזרה ואינווריאנטות של טיפוסי נתונים לא משתמשים ב-assert כדי לבדוק קלט מהמשתמש לא משתמשים ב-assert כאשר אסור לעצור את התכנית בגלל השגיאה int getInput() { int input; printf("Enter a positive number:"); scanf("%d",&input); assert(input > 0); return input; } ייתכן וההנחות הנבדקות ב-assert פשוט נכתבו לא נכון, במקרה זה תיקון ה"באג" הוא בעדכון ה-assert אך גם ממנו אנו למדים על הנחות שחשבנו שמתקיימות בקוד וטעינו. בדיקת ארגומנטים: בדיקות לוידוא הערכים הנכנסים לפונקציה, אין NULL, ערכים מספרים בתחום הנכון וכדומה. בדיקת ערכי חזרה: בדיקות פנימיות לפני ה-return. למשל וידוא שלא החזרנו גודל שלילי. ובדיקות חיצוניות, למשל וידוא הצלחה אחרי קריאה לפונקציה מאחר ואנחנו בטוחים ששלחנו פרמטרים תקינים לפונקציה. בדיקת אינווריאנטות: נוח להשתמש בבדיקה קבועה המוודאת שהנתונים בטיפוס נתונים כלשהו הגיוניים. למשל ניתן בכל אחת מהפונקציות של טיפוס הנתונים Date להוסיף את הבדיקה assert(dateIsValid(date)). כך ניתן לוודא שבשום שלב לא נוצר תאריך לא חוקי במערכת. השימוש ב-assert בדוגמה בשקף זה שגוי משום שאם המשתמש יכניס קלט לא חוקי התכנית תעצר עם הודעה שאינה מתאימה למשתמש. למשתמש לא אכפת מכך שבשורה x בקובץ a.c לא מתקיים input>0. למשתמש יש להחזיר שגיאות בשפה טבעית המובנות לו וכמעט תמיד אסור לעצור את התכנית לאחר הכנסת קלט לא נכון ע"י המשתמש. כמו כן, כאשר נקמפל את הגירסא הסופית עם הדגל –DNDEBUG, לא תוצג שגיאה כלל והתוכנית תמשיך לרוץ עם קלט שגוי. מה הבעיה ב-assert כאן? מה צריך לעשות במקום? מבוא לתכנות מערכות

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


הורד את "ppt "תרגול מס' 2 מצביעים הקצאת זיכרון דינאמית מבנים - Structures

מצגות קשורות


מודעות Google