שיא הסי-ים )(C/C++ )חלק ב'( מאת :יורם ביברמן © כל הזכויות שמורות למחבר. אין לעשות כל שימוש מסחרי בספר זה או בקטעים ממנו .ניתנת הרשות להשתמש בו לצורכי לימוד של המשתמש בלבד. 1 .11מצביעים ומערכים 7 ..................................................................................... עקרונות בסיסיים 7 .......................................................................... 11.1 הדמיון והשוני בין מצביע לבין מערך 9 ................................................ 11.2 נוטציה מערכית לעומת נוטציה מצביעית 11 ............................. 11.2.1 פונקציות המקבלות מצביעים או מערכים 15 ...................................... 11.3 מצביע כפרמטר הפניה 19 .................................................................. 11.4 פונקציה המחזירה מצביע 22 ............................................................. 11.5 שינוי גודלו של מערך 24 .................................................................... 11.6 מערך של מצביעים כפרמטר ערך 25 ................................................... 11.7 מערך של מצביעים כפרמטר הפניה 28 ................................................ 11.8 מצביעים במקום פרמטרי הפניה 31 ................................................... 11.9 11.10 מצביע מדרגה שנייה )** (intכפרמט קבוע )33 ......................... (const פרמטרים המועברים ל argc :main -ו33 ............................ argv - 11.11 תרגילים 35 .................................................................................. 11.12 תרגיל מספר אחד :איתור תת-מערך במערך35 ......................... 11.12.1 תרגיל מספר שתיים :מסד נתוני משפחות 36 ............................ 11.12.2 תרגיל מספר שלוש :טיפול בסיסי במערכים ומצביעים 36 .......... 11.12.3 38 .......................................................................................... struct 12 מוטיבציה 38 ................................................................................... 12.1 12.2 העברת structכפרמטר לפונקציה 39 .............................................. מערך של 41 ....................................................................... struct 12.3 מצביע ל struct -כפרמטר ערך 45 ................................................... 12.4 מצביע ל struct -כפרמטר הפניה 46 ................................................ 12.5 תרגילים 48 ..................................................................................... 12.6 תרגיל מספר אחד :פולינומים 48 ............................................. 12.6.1 תרגיל מספר שתיים :סידור כרטיסים בתבנית רצויה 49 ............ 12.6.2 תרגיל מספר שלוש :בעיית מסעי הפרש 49 ................................ 12.6.3 תרגיל מספר ארבע :נתוני משפחות50 ...................................... 12.6.4 תרגיל מספר חמש :סימולציה של רשימה משורשרת 52 ............. 12.6.5 13רשימות מקושרות )משורשרות( 54 ................................................................. 13.1בניית רשימה מקושרת 54 ........................................................................ הצגת הנתונים השמורים ברשימה מקושרת 56 .................................... 13.2 בניית רשימה מקושרת ממוינת 58 ..................................................... 13.3 מחיקת איבר מרשימה 67 ................................................................. 13.4 שחרור תאי רשימה 68 ...................................................................... 13.5 היפוך רשימה 69 .............................................................................. 13.6 בניית רשימה ממוינת יחידאית מרשימה לא ממוינת 74 ........................ 13.7 מיון מיזוג של רשימות משורשרות 77 ................................................. 13.8 13.8.1הצגת אלגוריתם המיון :מיון מיזוג 77 ................................................ 13.8.2מיון מיזוג של רשימות משורשרות 81 ................................................. 13.8.3זמן הריצה של מיון מיזוג 84 .............................................................. בניית רשימה מקושרת ממוינת בשיטת המצביע למצביע 86 ................. 13.9 13.10הערה בנוגע ל93 ..................................................................... const- תרגילים 95 .................................................................................. 13.11 תרגיל מספר אחד :רשימות משורשרות של משפחות95 ............. 13.11.1 תרגיל מספר שתיים :איתור פרמידה ברשימה 97 ...................... 13.11.2 תרגיל מספר שלוש :רשימה מקושרת דו-כיוונית 98 ................. 13.11.3 תרגיל מספר ארבע :רשימה מקושרת עם מיון כפול98 .............. 13.11.4 2 תרגיל מספר חמש :רשימה מקושרת עם מיון כפול 99 .............. 13.11.5 תרגיל מספר שש :מאגר ציוני תלמידים 100 ............................. 13.11.6 תרגיל מספר שבע :מיון הכנסה של רשימה מקושרת 101 .......... 13.11.7 תרגיל מספר שמונה :פולינומים102 ........................................ 13.11.8 תרגיל מספר תשע :הפרדת רשימות משורשרות שהתמזגו102 .... 13.11.9 תרגיל מספר עשר :בדיקת איזון סוגריים 103 ......................... 13.11.10 תרגיל מספר אחת-עשרה :המרת תא ברשימה ברשימה 104 ..... 13.11.11 13.11.12תרגיל מספר שתים-עשרה :מחיקת איבר מינימלי מסדרות המרכיבות רשימה מקושרת 104 ................................................................. 14עצים בינאריים 106 ...................................................................................... 14.1הגדרת עץ בינארי 106 ............................................................................. מימוש עץ בינארי בתכנית מחשב 108 ................................................. 14.2 בניית עץ חיפוש בינארי 109 ............................................................... 14.3 14.4ביקור בעץ חיפוש בינארי 122 ................................................................... 14.5חיפוש ערך מבוקש בעץ בינארי כללי ובעץ חיפוש בינארי 124 ....................... 14.6ספירת מספר הצמתים בעץ בינארי 126 ..................................................... 14.7מציאת עומקו של עץ בינארי 127 .............................................................. 14.8האם עץ בינארי מקיים שלכל צומת פנימי יש שני בנים 128 ......................... 14.9האם עץ בינארי מקיים שעבור כל צומת סכום הערכים בבת-העץ השמאלי קטן או שווה מסכום הערכים בתת-העץ הימני 129 ............................................ 14.11מחיקת צומת מעץ בינארי 134 ............................................................... 14.11.1תיאור האלגוריתם 134 ................................................................... 14.11.2המימוש 135 .................................................................................. 14.12כמה זה עולה? 143 ................................................................................ 14.12.1הוספת נתון 143 ............................................................................. 14.12.2מחיקת נתון 144 ............................................................................. 14.12.3חיפוש נתון 144 .............................................................................. 14.13האם עץ בינארי מלא 144 ....................................................................... 14.14האם בכל הצמתים בעץ השוכנים באותה רמה מצוי אותו ערך 150 ............. 14.14.1הפונקציה151 ...................................... equal_vals_at_level : 14.14.2הפונקציה151 ........................................ get_val_from_level : 14.14.3הפונקציה156 .......................................................check_level : 14.14.4זמן הריצה של 157 ................................. equal_vals_at_level 14.14.5פתרון חלופי לבעיה158 ................................................................... 14.15האם כל הערכים בעץ שונים זה מזה 158 ................................................. 14.16תרגילים 160 ........................................................................................ 13.14.1תרגיל ראשון :עץ 160 ............................................................. radix 13.14.2תרגיל שני :מציאת קוטר של עץ 161 ................................................. 13.14.3תרגיל שלישי :מה עושה התכנית? 162 .............................................. 13.14.4תרגיל רביעי :בדיקה האם עץ א' משוכן בעץ ב' 162 ............................ 13.14.5תרגיל חמישי :תת-עץ מרבי בו השורש גדול מצאצאיו 164 .................. 13.14.6תרגיל שישי :בדיקה האם תת-עץ שמאלי מחלק כל שורש ,וכל שורש מחלק תת-עץ ימני 165 ................................................................................ 13.14.7תרגיל שביעי :בדיקה האם עץ סימטרי 165 ....................................... 13.14.8תרגיל שמיני :איתור הערך המזערי בעץ 166 ...................................... 13.14.9תרגיל תשיעי :עץ ביטוי )עבור ביטוי אריתמטי( 166 ............................ 13.14.10תרגיל עשירי :חישוב מספר עצי החיפוש בני nצמתים 167 ................ 13.14.11תרגיל מספר אחד-עשר :איתור הצומת העמוק ביותר בעץ 167 .......... 13.14.12תרגיל מספר שניים-עשר :בדיקה האם עץ כמעט מלא168 ................. 3 13.14.13תרגיל מספר שלושה-העשר :האם כל תת-עץ מכיל מספר זוגי של צמתים 168 ............................................................................................... 13.14.14תרגיל מספר ארבעה-עשר :שמירת עומקם של עלי העץ במערך 168 .... 15מצביעים לפונקציות 170 ............................................................................... 15.1דוגמה פשוטה ראשונה למצביע לפונקציה 170 ........................................... 15.2דוגמה שניה למצביע לפונקציה 172 .......................................................... אפשרות א' לכתיבת פונקצית המיון :שימוש בדגל בלבד 172 ...... 15.2.1 אפשרות ב' לכתיבת פונקצית המיון :שימוש בפונקציות השוואה 15.2.2 174 ובדגל אפשרות ג' לכתיבת פונקצית המיון :שימוש במצביע לפונקציה 15.2.3 176 תרגילים 180 ................................................................................... 15.5 15.3.1תרגיל מספר אחד :האם פונקציה אחת גדולה יותר משניה בתחום כלשהו 180 ................................................................................................ 15.3.2תרגיל מספר שתיים :מציאת שורש של פונקציה 181 ............................ .16מצביעים גנריים )* 182 ........................................................................ (void 16.1דוגמה ראשונה :פונ' המשתמשת במצביע גנרי 182 ..................................... 16.2דוגמה שניה ,משופרת :פונ' המשתמשת במצביע גנרי183 ............................ 16.3דוגמה שלישית :פונ' מיון גנרית 186 ......................................................... 16.4דוגמה שלישית :בניית רשימה מקושרת ממוינת גנרית 189 ......................... .17חלוקת תכנית לקבצים 195 ........................................................................... 17.1הנחיית הקדם מהדר 195 ............................................................. #define 17.2הנחיית הקדם מהדר 198 ............................................................... #ifdef 17.3הנחיית הקדם מהדר 199 ............................................................ #include 17.4חלוקת תכנית לקבצים ,דוגמה א'202 ....................................................... 17.4.1הקבצים שנכללים בתכנית 202 .......................................................... 17.4.2הידור התכנית 205 ........................................................................... 17.4.3שימוש בקובצי כותר )208 ...................................................... (header 17.5חלוקת תכנית לקבצים ,דוגמה ב' :תרומתה של הנחיית המהדר 210 ... ifndef 17.5.1הקובץ 211 ............................................................................Point.h 17.5.2הקובץ 212 .......................................................................... Point.cc 17.5.3הקובץ 213 ..................................................................... Rectangle.h 17.5.4הקובץ 214 .................................................................... Rectangle.cc 17.5.5הקובץ 214 ..................................................................... my_prog.cc 17.5.6בעיות ההידור המתעוררות והתיקונים הדרושים בעזרת 215 ...... #ifndef 17.5.7קובץ ה makefile -לפרויקט 217 .......................................................... 17.6קבועים גלובליים 217 ............................................................................. 17.7מבוא לתכנות מונחה עצמים :עקרון הכימוס 219 ....................................... 17.8יצירת ספריה סטאטית 220 ..................................................................... 17.9יצירת ספריה דינאמית )משותפת( 221 ...................................................... 4 ליעננ שבכל אחד מאיתנו, לנ. של כולנו ולמאבק איתם ובהם ָב ֶע ֶרב ֲר ִתי אָמ ָרה ִלי ַנע ָ ְכ ֶש ְ ֵל ְך ָר ְד ִתי ָל ְרחוֹב ְל ִה ְת ַה ֵל ְך יַ וּמ ְס ַת ֵב ְך הוֹל ְך ִ יתי ֵ וְ ָהיִ ִ הוֹל ְך ִמ ְס ַת ֵב ְך וְ ֵ וּמ ְס ַת ֵב ְך הוֹל ְך ִ הוֹל ְך וְ ֵ וְ ֵ )נתן זך( 5 6 עודכן 1/2011 .11מצביעים ומערכים בקצרה ובפשטות ניתן לומר כי מצביע הוא משתנה המכיל כתובת של משתנה אחר, במילים אחרות מצביע הוא משתנה אשר ַמפנה אותנו למשתנה אחר. 11.1עקרונות בסיסיים נניח כי בתכנית כלשהי הגדרנו את המשתנים הבאים: ; int num1, num2, *intp1, *intp2 מה קיבלנו? קיבלנו ארבעה משתנים ,טיפוסם של שני הראשונים ביניהם מוכר לנו היטב מימים ימימה :אלה הם משתנים אשר מסוגלים לשמור מספר שלם; טיפוסם של השניים האחרונים ,אלה שלפני שמם כתבנו כוכבית )*( ,הוא * ,intומהותם חדשה לנו :משתנים אלה הם מצביעים לתאי זיכרון )כלומר למשתנים( המכילים מספרים שלמים .ראשית ,נצייר כיצד נראית המחסנית בעקבות הגדרת המשתנים הללו ,ואחר נמשיך בהסבר .שימו לב כי מצביעים אנו מציירים עם חץ שראשו הוא בצורת ,vוזאת בניגוד לפרמטרי הפניה אותם אנו מציירים עם חץ שראשו הוא משולש שחור: num1 num2 intp1 intp2 המשתנים num1, num2מסוגלים לשמור מספר שלם ,על-כן נוכל לכתוב: ; num1 = 17; num2 = 3879ובתאי הזיכרון המתאימים ישמרו הערכים הרצויים .מצב המחסנית אחרי ביצוע שתי השמות אלה יהיה: num1= 17 num2 = 3879 intp1 intp2 7 לעומת זאת intp1, intp2 ,הם מצביעים .עליהם אנו רשאים לבצע פעולה כגון: ; . intp1 = &num1נסביר את משמעות הפעולה :אופרטור ה & -מחזיר את כתובתו של המשתנה )של האופרנד( עליו הוא מופעל ,במקרה שלנו הוא מחזיר את כתובתו של ,num1כתובת זאת אנו משימים למשתנה ,intp1ועל כן עתה intp1 מצביע על ,num1מבחינה ציורית נציג זאת: num1= 17 num2 = 3879 intp1 intp2 עתה אנו יכולים לפנות לתא הזיכרון הקרוי num1גם באופן הבא . *intp1 :למשל אנו רשאים לכתוב: ; *intp1 = 0והדבר יהיה שקול לכך שנכתוב: ; . num1 = 0אנו גם רשאים לכתוב , (*intp1)++; :והדבר יהיה שקול לפקודה . num1++; :כלומר *intp1הוא עתה שם נרדף ל .num1 -נדגיש כי intp1הוא מצביע ל ,int -לעומת זאת *intp1 :הוא תא הזיכרון עליו intp1 מורה מצביע .הפעולה בה אנו פונים באמצעות מצביע לשטח הזיכרון עליו המצביע ֵ נקראת .dereferencing עוד נדגיש כי בשפת סי לכוכבית יש משמעות שונה לגמרי בהגדרת המשתנה )עת אנו כותבים ; ,(int *pלעומת ַבשימוש בו בהמשך :בעוד בהגדרת המשתנה הכוכבית מורה שמדובר במצביע ל) int -ולא ב ;(int -בעת השימוש ַבמשתנה ַבהמשך הכוכבית מורה שיש לפנות לתא הזיכרון עליו המצביע מורה .על כן ההשמה: ; p = 0מכניסה את הערך אפס למצביע )ובכך ,למעשה ,מסמנת שהמצביע אינו מורה לשום מקום( ,בניגוד לה ,ההשמה *p =0; :מכניסה את הערך אפס לתא הזיכרון עליו pמורה .היא חוקית רק אם קודם לכן שלחנו את pלהצביע על תא זיכרון כלשהו )למשל ,עת ביצענו.(p = &num1; : באופן דומה אנו רשאים לבצע גם את ההשמה intp2 = &num2; :והאפקט שלה יהיה דומה .עתה נוכל לבצע את ההשמה , *intp1 = *intp2; :מה עושה השמה זאת? היא מכניסה לתוך תא הזיכרון עליו מצביע ,intp1כלומר למשתנה num1את הערך המצוי בתא הזיכרון עליו מצביע ,intp2כלומר את הערך המצוי ב .num2 -במילים אחרות ההשמה *intp1 = *intp2; :שקולה להשמה: ; . num1=num2 לעומת ההשמה האחרונה שראינו ,ההשמה intp1 = intp2; :מכניסה למצביע intp1את הערך המצוי במצביע .intp2במילים אחרות ב intp1 -תהיה אותה כתובת המצויה ב ;intp2 -ובמילים פשוטות ,עתה שני המצביעים יצביעו על תא הזיכרון .num2מבחינה ציורית נציג זאת כך: num1= 17 num2 = 3879 intp1 intp2 8 במצב הנוכחי *intp1, *intp2, num2הם שלושה שמות שונים לאותו תא זיכרון .הדבר עלול לגרום לתוצאות לכאורה מפתיעות .לדוגמה נביט בקטע התכנית הבא )אשר מניח כי מצב המצביעים הוא כפי שמתואר בציור האחרון(: ; num2 = 3 ; *intp1 = 5 ; cout << num2 הפלט שיוצג יהיה חמש ,וזאת למרות שהפקודה האחרונה אשר הכניסה ערך למשתנה num2שמה בו את הערך שלוש .הסיבה לכך שהפלט יהיה חמש היא שגם הפקודה ; *intp1 = 5השימה ערך לאותו שטח זיכרון ,ועשתה זאת אחרי ההשמהַ . num2 = 3; :למצב בו לאותו שטח בזיכרון ניתן לפנות באמצעות מספר שמות אנו קוראים .aliasזהו מצב לא רצוי ,שכן הוא עלול לגרום לתופעות לכאורה משונות ,כפי שראינו .למרות שהוא מצב לא רצוי ,בהמשך נעשה בו שימוש רב. שימו לב כי הפקודה intp1 = 3; :היא פקודה שגויה ,שכן אנו מנסים להכניס למצביע )שטיפוסו (int * :ערך שלם )כלומר ערך מטיפוס .(intזה לא יהיה בלתי סביר לחשוב שכתובות במחשב מיוצגות באמצעות מספרים טבעיים ,ולמרות זאת השמה כגון הנ"ל היא שגויה. 11.2הדמיון והשוני בין מצביע לבין מערך כל השימוש שעשינו עד כאן במצביעים היה כדי להפנותם לתאי זיכרון קיימים .יבוא הקנטרן וישאל :אם זה כל השימוש שניתן לעשות במצביעים ,אז מה תועלתם? התשובה היא שבמצביעים ניתן לעשות גם שימושים אחרים .מצביע הוא למעשה פוטנציאל למערך .נסביר :נניח שהגדרנו . double a[5], *floatp; :נציג את המחסנית: =a 4 3 1 2 0 =floatp מה קיבלנו על-גבי המחסנית? קיבלנו מערך בשם aבן חמישה תאים ,ומצביע בשם ) .floatpבהמשך יובהר מדוע גם לצד aציירנו חץ( .עתה אנו רשאים לבצע את פקודהְ . floatp = new (std::nothrow) double[7]; :למה תגרום פקודה זאת? פקודה זאת מממשת את הפוטנציאל הגלום במצביע ,והופכת אותו למערך ככל מערך אחר .מייד ניתן הסבר מפורט; אך נקדים לתשובה רקע הכרחי: כל המשתנים שראינו עד כה הוקצו על-גבי המחסנית .המחסנית היא שטח בזיכרון הראשי אשר מערכת ההפעלה מקצה לתכניתכם .בשלב זה של חיינו אנו גם מבינים מדוע שטח הזיכרון הזה מכונה מחסנית :בשל שיטת ה last in first out -על-פיה הוא מתנהל ,ואשר קובעת באיזה אופן חלקים נוספים מזיכרון זה מוקצים לתכנית, ואחר משוחררים .פרט למחסנית ,מקצה מערכת ההפעלה לתכניתכם גם שטח נוסף בזיכרון הראשי .שטח זה נקרא ערמה ) .(heapהוא נקרא כך שכן הוא מנוהל בבלגן יחסי ,באופן דומה לדרך בה אנו זורקים דברים נוספים על ערמה ,או מושכים floatp = new דברים מערמה .עת תכנית מבצעת פקודה כדוגמת: ;] (std::nothrow) double[7קורה הדבר הבא :על-גבי הערמה מוקצה מערך בן 7תאים ממשיים ,והמצביע floatpעובר להצביע לתא מספר אפס במערך זה. 9 מבחינה ציורית נציג זאת כך: =a 6 5 4 3 2 1 4 0 3 2 1 0 =floatp המלבן השמאלי מייצג את המחסנית ,והימני את הערמה. המערך שהתקבל יקרא .floatpאל תאיו נפנה כפי שאנו פונים לכל מערך אחר. לדוגמה אנו רשאים לכתוב floatp[0]=17; :או .cin >> float[6]; :אנו רואים ,אם כן ,כי אחרי שהקצנו למצביע מערך )באמצעות הפקודהfloatp = : … ,(newכלומר אחרי שמימשנו את הפוטנציאל הגלום במצביע ,אנו מתייחסים ל- floatpכאל כל מערך אחר. אנו אומרים כי המערך floatpהוקצה דינאמית ,בעת ריצת התכנית ,וזאת בניגוד למערך aשהוקצה סטאטית ,וגודלו נקבע בעת שהמתכנת כתב את התכנית. מספר הערות על פקודת ה:new - א .כמובן ששם הטיפוס שיופיע אחרי המילה השמורה newיהיה תואם לטיפוס המצביע .כלומר לא יתכן שנגדיר מצביע , int *p; :ובהמשך נכתוב: ;].p = new (std::nothrow) float[7 ב .הקפידו שגודל המערך אותו ברצונכם להקצות יופיע בתוך סוגריים מרובעים, )ולא בתוך סוגריים מעוגלים!(. ג .פקודת ה new -עלולה להיכשל ,כלומר היא עלולה שלא להצליח להקצות מערך כנדרש .במקרה זה ערכו של המצביע יהיה הקבוע .NULLהערך NULLמורה שהמצביע אינו מצביע לשום כתובת שהיא .נדון במשמעותו ביתר הרחבה בהמשך .כלל הזהב למתכנת המתחיל הוא כי אחרי ביצוע newיש לבדוק האם ערכו של המצביע המתאים שונה מ .NULL -במידה וערכו של המצביע הוא ,NULLסביר שתרצו לעצור את ביצוע התכנית ,תוך שאתם משגרים הודעה מתאימה .נציג דוגמה: ; ]floatp = new (std::nothrow) double[7 { )if (floatp == NULL ; "cout << "Can not allocate memory\n ; )exit(EXIT_FAILURE } הסבר :אם בעקבות ביצוע פקודת ה new -ערכו של floatpהוא NULLאזי אנו משגרים הודעת שגיאה ,ועוצרים את ביצוע התכנית באמצעות הפקודה ) . exit(EXIT_FAILUREפקודה זאת תחזיר את הקוד המופיע בסוגריים למערכת ההפעלה .על-מנת שהקומפיילר יכיר את פקודת ה , exit -ואת הקבוע NULLיש לכלול בתכניתכם את ההוראה . #include <stdlib.h> :בדרך כלל נוהגים לכלול את זוג הפקודות המבוצעות במקרה וההקצאה נכשלה בפונקציה קטנה נפרדת שמקבלת מחרוזת ,וערך שלם; הפונקציה מציגה את המחרוזת ,וקוטעת את ביצוע התכנית תוך החזרת ערך השלם. ד .הפסוקית ) (std::nothrowהיא זו שדואגת שעת הקצאת הזיכרון נכשלת התכנית לא תעוף ,אלא למצביע יוכנס הערך .NULLלו לא היינו כותבים פסוקית זאת ,עת הקצאת הזיכרון נכשלת 'נזרקת חריגה' ,מונח שאינו מוכר לנו עדיין, אך שתוצאתו הבסיסית היא העפת התכנית .על מנת שהמהדר יכיר את הפסוקית הנ"ל יש לכלול בתכנית את הנחיית המהדר#include <new> : 10 האפשרות להקצות מערך דינמית גורמת לכך שעתה יש ביכולתנו לכתוב קטע קוד כדוגמת הבא )נניח את ההגדרות:(int num_of_stud, *bible; : ; cin >> num_of_stud ; ]bible = new (std::nothrow) int[num_of_stud )if (bible == NULL ; )terminate(“can not allocate memory\n”, 1 )for (int i = 0; i < num_of_stud; i++ ; ]cin >> bible[i אחר אנו הסבר :ראשית אנו קוראים מהמשתמש כמה תלמידים יש בכיתתוַ , מקצים מערך בגודל הדרוש ,ולבסוף )בהנחה שההקצאה הצליחה( ,אנו קוראים נתונים לתוך המערך .במידה וההקצאה נכשלה ,אנו מזמנים את הפונקציה terminateכפי שתואר בפסקה הקודמת. 11.2.1 נוטציה מערכית לעומת נוטציה מצביעית אמרנו כי מצביע הוא פוטנציאל למערך .הוספנו שאנו מממשים את הפוטנציאל באמצעות פקודת ה ,new -ואחרי שמימשנו את הפוטנציאל המצביע והמערך היינו הך הם )כמעט( .עוד אמרנו כי המצביע ,אחרי שהוקצה לו מערך מצביע למעשה על תא מספר אפס במערך .נעקוב אחרי קביעות אלה שוב :נניח כי הגדרנו: ; int a[5], *ip ואחר הקצנו ל ip -מערך: ;]ip = new (std::nothrow) int[5 )נניח כי ההקצאה הצליחה(. עתה יש לנו בתכנית שני מערכים בגודל של חמישה תאים .על כן אנו יכולים לכתוב קוד כדוגמת הבא: )for (int i= 0; i< 5; i++ ; a[i] = ip[i] = 0 האם אנו יכולים לכתוב גם ? *ip = 3879; :ואם כן מה משמעות ההשמה הנ"ל? התשובה היא שאנו יכולים לכתוב השמה כנ"ל ,וההשמה תכניס לתא מספר אפס במערך ipאת הערך ;3879שכן כפי שאמרנו ipמצביע על התא מספר אפס ַבמערך שהוקצה דינמית. ;*a = 3879 מה שאולי יפתיע אתכם יותר הוא שאנו רשאים לכתוב גם: ופקודה זאת תכניס את הערך 3879לתא מספר אפס במערך .aכלומר גם aהוא למעשה מצביע :מצביע אשר מורה על התא מספר אפס ַבמערך שהוקצה סטאטית על-גבי המחסנית .עתה אתם גם מבינים מדוע גם לצד aציירנו חץ שהצביע על התא מספר אפס במערך המתאים. מערכית' ,אנו רשאים את לולאת איפוס המערכים שכתבנו קודם בצורת כתיבה ' ַ לכתוב גם בצורת כתיבה 'מצביעית': )for (i=0; i<5; i++ ; *(a + i) = *(ip + i) = 0 נסביר :אנו מבצעים כאן אריתמטיקה של מצביעים .ערכו של הביטוי(a + i) : הוא המצביע אשר מורה על התא שבהסטה iמהתא עליו מורה .aלכן אם aמורה על התא מספר אפס במערך ,וערכו של iהוא שלוש ,אזי הביטוי ) (a + iהוא המצביע לתא מספר שלוש במערך .לכן אם אנו כותבים ,*(a + i) :כלומר אנו הולכים בעקבות המצביע ,אזי אנו פונים לתא מספר שלוש במערך ,ובלולאה שלנו 11 אנו מאפסים אותו .במילים פשוטות :הכתיבה *(a + i) :שקולה לכתיבה: ] .a[iאנו רואים אם כן שוב את הדמיון בין מערך לבין מצביע שהוקצה לו מערך. האם קיימת זהות מלאה בין מערך שהוקצה סטטית )על-גבי המחסנית( ,לבין כזה שהוקצה דינמית )על-גבי הערמה(? לא לגמרי .עתה נציין את ההבדלים .נבחן את קטע הקוד הבא ,תוך שאנו מניחים את ההגדרות: ; int a[3], *p1, *p2, *p3 ; ]p1 = new (std::nothrow) int[4 ; ]p2 = new (std::nothrow) int[2 ; p3 = p1 ; p1 = p2 ; a = p2 מקטע הקוד השמטנו את הבדיקה האם ההקצאה הדינמית נסביר :ראשית ֵ הצליחה .אנו עושים זאת לצורך הקיצור ,וההתמקדות ַבעיקר .בתכניות שלכם הקפידו תמיד לערוך את הבדיקה המתאימה .מעבר לכך :אנו מקצים ל p1 -ולp2 - שני מערכים .עתה אנו מפנים את p3להצביע על המערך ,p1ולכן אם נכתוב ]p3[0 או נכתוב ] p1[0נִ פנה לאותו שטח זיכרון .נתאר זאת על-גבי המחסנית: =a =p1 =p2 =p3 מפנה את p1להצביע על אותו מערך כמו ) p2ולכן עתה הפקודה הבאהְ p1 = p2; : רק p3מצביע על המערך בן ארבעת התאים שהוקצה דינמית( .הפקודה האחרונהa : = p2היא שגיאה שתמנע מהתכנית להתקמפל בהצלחה a .הוא מצביע אשר קשור לנצח נצחים ַלמערך שהוקצה לו על-גבי המחסנית; שלא כמו p1את aלא ניתן להפנות להצביע על מערך אחר .מבחינה פורמאלית aהוא מצביע קבוע ,כזה שלא ניתן לשנות את ערכו. מאותה סיבה הפעולה ; p1++אשר מקדמת את p1להצביע על התא הבא במערך, היא תקינה :את p1ניתן להזיז באופן חופשי .לעומתה הפקודה ; a++היא שגיאה. שימו לב כי הפקודה ; p1++שונה לחלוטין מהפקודה ; (*p1)++אשר שקולה לפקודה . p1[0]++; :שתי הפקודות האחרונות מגדילות באחד את ערכו של תא הזיכרון עליו מצביע ,p1במילים אחרות הן מגדילות באחד את הערך המצוי ַבתא מספר אפס במערך ,p1כלומר הן מבצעות אריתמטיקה במספרים שלמים .בעוד הפקודה p1++היא אריתמטיקה במצביעים :היא מסיטה את p1להצביע מקום אחד ימינה יותר במערך. נִ ראה שימוש לפקודת ההגדלה העצמית במצביעים .נניח כי הגדרנו,char *cp; : אחר הקצנו ל cp -מערך )באמצעות הפקודהcp = new (std::nothrow) : ;] .(char[3879עתה אנו יכולים לאפס את המערך גם באופן הבא: )for (int i=0; i<3879; i++ } ; { *cp = 0; cp++ הסבר :בכל סיבוב בלולאה אנו מאפסים את התא במערך עליו מצביע ) ,cpוכזכור בתחילת התהליך cpמצביע על התא מספר אפס במערך( ,ואחר מקדמים את cpכך שהוא יצביע על התא הבא במערך. 12 להיכן מצביע cpאחרי ביצוע הלולאה? אל מחוץ לגבולות המערך ,ליתר דיוק ַלתו הראשון שאחרי המערך .כדי להשיב את cpלהצביע על התא מספר אפס במערך אנו יכולים לבצע את הפקודה cp -= 3879; :פקודה זאת מחזירה את cpלהצביע 3879תאים אחורנית ,כלומר לראש המערך .כמובן שלוּ היינו כותבים במקום את הפקודה cp -= 3879; :את הפקודה cp -= 4000; :אזי היינו שולחים את cpלהצביע אי שם למקום לא ידוע ַבזיכרון ,וזאת פעולה מסוכנת ,שאת אחריתה מי יישור. הדוגמה האחרונה רמזה לנו על כמה מהסכנות הטמונות בשימוש לא די זהיר וקפדני במצביעים .נראה דוגמה נוספת :נניח שהגדרנו bool *bp; :ואחר ,בלא שהפננו את bpלהצביע על תא זיכרון מוגדר כלשהו ,אנו מבצעים . *bp = true; :מה אנו עושים בכך? ב ,bp -כמו בכל משתנה שלא נקבע לו עדיין ערך ,יש ערך 'זבל' מקרי כלשהו ,כלומר הוא מצביע לתא מקרי כלשהו בזיכרון .עתה אנו נגשים לאותו תא מקרי ומכניסים לתוכו את הערך .trueהתא המקרי שעליו 'דרכנו' עשוי להכיל משתנים אחרים של התכנית .התוצאה עלולה להיות שתכניתנו תתנהג בצורה ביזארית ,שלא לומר אכזרית .לצערי ,כבר ראיתי מתכנתים מתחילים שבאמצעות שימוש לא די זהיר במצביעים יצרו באגים שאת האפקט שלהם ניתן לתאר בלשון המעטה כ'-מפתיע ,יצירתי ,עולה על כל דמיון' .הצד המרגיע של הנושא הוא שכל התקלות שתחוללו באמצעות שימוש קלוקל במצביעים תוגבלנה לזיכרון הראשי, ולכן תעלמנה עת תכבו את המחשב; במילים אחרות ,לא תגרמו לנזק בלתי הפיך למחשב ,או לתוכנו של הדיסק. אחד ממנגנוני הבטיחות שעוזר לנו להישמר מפני תקלות כדוגמת זו שראינו הוא השימוש ב .NULL -כאשר ערכו של מצביע pהוא NULLמשמעות הדבר היא כי p אינו מצביע לשום תא בזיכרון ,ועל-כן לא ניתן לפנות באמצעותו ַלזיכרון ,כלומר לא ניתן לבצע . *p = …; :כיצד נוכל להיעזר ב NULL -בתכניות שאנו כותבים? א .בעת הגדרת המצביע נאתחל אותו לערך . char *bp = NULL; :NULL בַ .בהמשך אולי כן ואולי לא נפנה את bpלהצביע על תא כלשהו ַבזיכרון )למשל, באמצעות פקודה כגון … .(bp = new ג .לבסוף ,לפני שאנו פונים לתא הזיכרון עליו מצביע ) bpכלומר לפני שאנו נישאל האם ערכו של bpשונה מ .NULL -אם ערכו של bp מבצעיםְ (*bp = … : שונה מ NULL -אנו מובטחים כי bpהופנה לתא כלשהו ַבזיכרון ,ועל כן הפנִ יה … = *bpהיא בטוחה .אם ,לעומת זאת bp ,לא הופנה להצביע על תא כלשהו ַבזיכרון ,אזי ערכו של bpיישאר כפי שהוא היה בעקבות האיתחול ,כלומר ערכו יהיה ,NULLואז לא ננסה לפנות לתא הזיכרון עליו bpמצביע. נראה דוגמה: הגדרת המצביע. char *bp = NULL; : אחר -כך ,אי שם בהמשך התכנית נכתוב: { )…( if ; ]bp = new (std::nothrow) char[17 )if (bp == NULL ; )terminate("Can not allocate memory\n”, 1 } כלומר אם תנאי כלשהו )שלא פרטנו( מתקיים אנו מקצים מערך ,ומפנים את bp להצביע עליו. ולבסוף ,הלאה יותר בתכנית אנו כותבים: )if (bp != NULL ; bp[1] = bp[3] = 3879 13 בזכות האיתחול של bpל NULL -בעת הגדרתו ,מתקיים כי הבדיקה: ) if(bp != NULLמעידה האם ל bp -הוקצה מערך ,ועל כן אנו יכולים לפנות בבטחה לתאים מספר אחד ,ומספר שלוש במערך ,או שמא ל bp -לא הוקצה מערך, ועל-כן ערכו נותר כפי שהוא היה בעת הגדרתו ,כלומר .NULL כלל הזהב האחרון אותו עלינו ללמוד ַב ֵהקשר של מצביעים ומערכים הוא :כל מערך שהוקצה באמצעות פקודת ,newישוחרר בשלב זה או אחר) ,אולי רק לקראת סיום ריצת התכנית( ,באמצעות פקודת .deleteלדוגמה ,כדי לשחרר את המערך עליו מצביע bpנכתוב . delete [] bp; :משמעות הפקודה היא שאנו מורים למערכת ההפעלה כי איננו זקוקים יותר למערך ,ולכן ניתן למחזר את שטח הזיכרון שלו .פניה לזיכרון שמוחזר מהווה שגיאה; על כן היזהרו מקוד כדוגמת הבא: ; ]intp1 = new (std::nothrow) int[17 ; inp2 = intp1 … ; delete [] intp2 ; intp1[0] = 12 בקוד זה intp2מצביע לאותו שטח זיכרון כמו .intp1על-כן עת שחררנו את שטח הזיכרון ,באמצעות הפקודה' , delete [] intp2; :שמטנו את השטיח גם מתחת לרגלי ,'intp1כלומר שטח הזיכרון עליו מצביע )עדיין( intp1כבר אינו עומד לרשותנו ,וזו שגיאה לפנות אליו ) ַבפקודה.(intp1[0] = 12; : פקודת ה delete -אינה מכניסה למצביע ערך .NULLהיא מותירה במצביע ערך 'זבל'. טעות שכיחה אצל מתכנתים מתחילים היא לכתוב קטע קוד כדוגמת הבא: ; ] … [intp2 = new int … ; ] … [intp1 = new (std::nothrow) int ; intp1 = intp2 מקְצה מערך ל .intp2 -בהמשך נסביר :הפקודה הראשונה בין השלוש המתוארות ְ התכניתַ ) ,בקטע המתואר בשלוש הנקודות( ,אנו עושים שימושים שונים במערך. עתה אנו רוצים להפנות את intp1להצביע לאותו מערך כמו ,intp2וזה לגיטימי; אולם להקדים ַלפקודה intp1 = intp2; :את הפקודהintp1 = new … : ; ] … [ intזו שטות .נסביר מדוע :נתאר את מצב הזיכרון אחרי ביצוע שתי פקודות ההקצאה: intp1 intp2 עתה ,הפקודה האחרונה intp1 = intp2; :יוצרת את המצב הבא: intp1 intp2 14 התוצאה היא שהמערך עליו הצביע intp1עד עתה הפך להיות מין ,zombieשמחד גיסא מצוי בזיכרון ,ומאידך גיסא אין כל דרך לפנות אליו .לכן אם ברצונכם להפנות מצביע pלהצביע על מערך עליו מורה מצביע ,qלעולם אל תקדימו לפקודת ההשמה ; p = qפקודה. p = new … : עד כאן הצגנו את אופן השימוש במצביעים לשם יצירת מערכים .הדוגמות שבהמשך הפרק תסייענה לכם ,ראשית ,להטמיע את שהוסבר עד כה ,ושנית ללמוד כיצד לשלב את השימוש במצביעים ובפונקציות. 11.3פונקציות המקבלות מצביעים או מערכים בעבר הכרנו את הפונקציה .strlenלהזכירכם ,פונקציה זאת מקבלת מערך של תווים ,ומחזירה את אורכו של הסטרינג השמור במערך )לא כולל ה .(‘\0’ -אנו יכולים לכתוב את הפונקציה באופן הבא: { )][int strlen( const char s ; int i )for (i=0; s[i] != ‘\0’; i++ ; ; )return(i } הסבר :לולאת ה for -מתקדמת על המערך כל עוד התו המצוי בתא הנוכחי במערך אינו ’ .‘\0בגוף הלולאה מתבצעת הפקודה הריקה .אחרי הלולאה אנו מחזירים את ערכו של .i עתה ,שלמדנו את השקילות בין מערכים ומצביעים ,אנו יכולים לכתוב את הפונקציה בצורה דומה אך שונה: { )int strlen( const char *s ; int i )for (i=0; s[i] != ‘\0’; i++ ; ; )return(i } הסבר :את הפרמטר תיארנו הפעם בנוטציה מצביעית ,במקום בנוטציה מערכית. בשל השקילות בין מערך למצביע הדבר לגיטימי .את גוף הפונקציה לא שינינו .נדגיש כי נוסח זה ,כמו הנוסח שראינו לפניו ,וכמו אלה שנראה בהמשך ,אינו מחייב קריאה עם מערך שהוקצה דינמית או כזה שהוקצה סטטית; ניתן לקרוא לכל אחת מהפונקציות שאנו כותבים בסעיף זה הן עם מערך שהוקצה סטטית על המחסנית, והן עם כזה שהוקצה דינמית על הערמה. אנו יכולים לכתוב את אותה פונקציה גם בצורה שלישית: { )int strlen( const char *s ; int i )for (i=0; *(s +i) != ‘\0’; i++ ; 15 ; )return(i } הסבר :הפעם גם את תנאי הסיום בלולאה המרנו מנוטציה מערכית לנוטציה מצביעית .כפי שאנו יודעים הביטוי ) *(s+iפונה לתא מספר iבמערך עליו מצביע .sבמילים אחרות ) (s+iהוא מצביע לתא המצוי בהסטה iמהתא עליו מצביע ,s ולכן ) *(s+iפונה לתא המצוי בהסטה iמהתא עליו מצביע .s ולבסוף נציג את אותה פונקציה בצורת כתיבה רביעית: { )int strlen( const char *s ; int i )for (i=0; *s != ‘\0’; i++, s++ ; ; )return(i } הסבר :הפעם אנו משתמשים באריתמטיקה על מצביעים .אנו מקדמים את המצביע sכל עוד התא עליו הוא מצביע כולל ערך שונה מ .‘\0’ -במקביל אנו סופרים כמה פעמים קידמנו את ,sוזהו אורכו של הסטרינג. 16 הגרסה האחרונה שכתבנו מדגימה נקודה אותה חשוב להבין :המצביע המועבר לפונקציה מועבר כפרמטר ערך ,ועל כן שינויים שהפונקציה עושה לפרמטר שלה ,s לא ישפיעו ,אחרי תום ביצוע הפונקציה ,על הארגומנט עמו הפונקציה נקראה. ארגומנט זה ימשיך להצביע על התא הראשון במערך .עת מצביע מועבר כפרמטר ערך לפונקציה ,המחשב פועל באותו אופן בו הוא פועל עת משתנה פרימיטיבי כלשהו מועבר כפרמטר ערך :המחשב מעתיק את ערכו של הארגומנט המתאים על הפרמטר המתאים .במילים אחרות הכתובת המוחזקת בארגומנט מועתקת לפרמטר .מבחינה ציורית לפרמטר יוקצה חץ משלו ,חץ זה יאותחל להצביע על אותו מקום כמו הארגומנט .נדגים זאת בציור .נניח כי בתכנית הוגדר מערך ] ,name[4ועתה אנו קוראים לפונקציה ) strlenבכל אחד מארבעת הנוסחים שכתבנו( .נציג את מצב המחסנית טרם הקריאה: = name ’‘\0 c a b אנו זוכרים כי מערך )כדוגמת (nameהוא למעשה מצביע לתא מספר אפס במערך. עתה נציג כיצד תראה המחסנית אחרי בניית רשומת ההפעלה של ) strlenנשמיט מרשומת ההפעלה את כתובת החזרה ,ואת המשתנה הלוקלי ,iשאינם מעניינים אותנו עתה(. = name ’‘\0 c b a = s הסבר :גם sהוא מצביע )אפילו אם הוא מוגדר בכותרת הפונקציה כ,(char s[] - ולכן ציירנו אותו עם חץ בצורת ) vכפי שאנו מציירים מצביעים; בניגוד למשולש שחור המשמש לציור פרמטרי הפניה( .לאן מצביע ?sלאותו מקום כמו הארגומנט המתאים לו ,כלומר .nameבמילים אחרות ערכו של nameהועתק על .sאם עתה נסיט את sלמקום אחר איננו משנים בכך את המקום אליו מצביע ) nameכפי שמתאים שיקרה עם פרמטר ערך(. הסבר זה מסייע לנו להבין תופעה שמוכרת לנו כבר מזמן :עת מערך מועבר כפרמטר ְלפונקציה ,הפונקציה יכולה לשנות את תוכנם של תאי המערך .עתה נניח כי בפונקציה שלנו אנו מבצעים *s = 'x’; :או במילים אחרות s[0]=’x’; :מה יקרה? אנו פונים לתא מספר אפס במערך עליו מצביע sולשם מכניסים את הערך ’ .‘xערך זה יוותר כמובן במערך גם אחרי תום ביצוע הפונקציה. כדי לחדד את התופעה נציג את הפונקציה הבאה: { )void f( int *pp, int nn ;np = 3879; *pp = 17; pp = NULL } לפונקציה יש שני פרמטרי ערך :הראשון בהם הוא מצביע )במילים אחרות מערך(, והשני הוא מספר שלם. 17 נניח כי בתכנית הראשית הוגדרו, int n = 0, *p = new int[5]; : ולחמשת תאי המערך ,pהוכנס הערך חמש. הזיכרון הכולל את משתני התכנית הראשית נראה: n = 0 5 5 5 5 5 = p עתה אנו קוראים לפונקציהf(p, n); : בעקבות הקריאה לפונקציה נוספת על-גבי המחסנית רשומת ההפעלה של .fנציג את מצב הזיכרון: n = 0 5 5 5 5 5 = p np = 0 = pp נסביר :כפי של np -הועתק ערכו של ,nכך ל pp -הועתק ערכו של ,pולכן ppמצביע לאותו מערך כמן .p עתה הפונקציה fמתחילה להתבצע .ההשמה np = 3879; :מכניסה ערך ל,np - ואינה משנה את ערכו של .nההשמה) *pp = 17; :השקולה להשמה: ; (pp[0] = 17מכניסה את הערך 17לתא עליו מצביע .ppההשמה: ; pp = NULLמכניסה ל pp -את הערך ,NULLואינה משנה את ערכו של .pמצב הזיכרון אחרי ביצוע שלוש הפעולות הללו יהיה: n = 0 5 5 5 5 17 = p np = 3879 = pp סימון 'ההארקה' לצד ppהוא הדרך בה אנו מציירים מצביע שערכו הוא .NULL עתה הפונקציה fמסתיימת ,ורשומת ההפעלה שלה מוסרת מעל המחסנית .כפי שאנו יודעים השינויים שהפונקציה הכניסה לתוך npאינם נותרים ב .n -באופן דומה ,השינויים שהפונקציה הכניסה ל pp -אינם נותרים ב .p -ולבסוף ,בהתאמה למה שאנו כבר מכירים היטב ,שינויים שהפונקציה הכניסה לתוך המערך עליו הורה ppנותרים במערך גם אחרי תום ביצוע הפונקציה. 18 בעבר ראינו כי אם אנו רוצים למנוע מפונקציה המקבלת מערך לשנות את ערכם של תאי המערך אנו יכולים להגדיר את הפרמטר של הפונקציה באופן הבאconst : ][ .int aאם מתכנת כלשהו ינסה בגוף הפונקציה לכתוב פקודה כגוןa[0]=17; : אזי התכנית לא תעבור קומפילציה )ולכן העברת מערך כפרמטר שהינו קבוע אינה שקולה להעברת משתנה פרימיטיבי כפרמטר ערך( .עתה למדנו כי אם פונקציה מקבלת מערך כפרמטר אנו יכולים לתאר את הפרמטר של הפונקציה גם באופן הבא . int *a :גם במקרה זה כדי למנוע מהפונקציה לשנות את ערכם של תאי המערך אנו רשאים לכתוב .const int *a :אולם כתיבה זאת לא תמנע מהפונקציה לשנות את ערכו של המצביע .בגוף הפונקציה נוכל לכתוב פקודה כגון: ; a = NULLולא יהיה בכך כל פסול .מה שלא נוכל לכתוב הוא*a = 3879; : כלומר אין ביכולתנו לשנות את ערכם של תאי המערך עליו מצביע ,aאך יש ביכולתנו לשנות את ערכו של .aכדי למנוע מפונקציה לשנות את ערכו של המצביע המועבר לה נכתוב . int * const a :עתה פקודה כגון a= NULL; :תגרום לשגיאת קומפילציה .לעומתה פקודה כגון *a = 1; :היא עתה לגיטימית ,שכן הפעם לא אסרנו לשנות את ערכם של תאי המערך עליו מצביע ,aרק אסרנו לשנות את המקום עליו aמצביע .כמובן שניתן לשלב את שני האיסורים גם יחדconst : . int * const aלבסוף נזכור כי אם מצביע הועבר כפרמטר ערך אזי בדרך כלל לא כל-כך יפריע לנו שהפונקציה תוכל לשנות את ערכו של הפרמטר שלה .שהרי השינוי לא יוותר בארגומנט המתאים לאותו פרמטר. 11.4מצביע כפרמטר הפניה עתה נניח כי ברצוננו לכתוב תכנית אשר מגדירה מצביע );,(int *p = NULL מקצה את המערך קוראת מהמשתמש גודל מערך רצוי )לתוך המשתנה ְ ,(size )באמצעות פקודת ,(newולבסוף ,קוראת נתונים לתוך המערך .עוד נניח כי כדי לסמן את סוף המערך )שגודלו אינו ידוע לתכנית הראשית( ,מציבה הפונקציה בתא האחרון במערך את הערך הקבוע .THE_ENDנציג את קטע התכנית הבא ,אשר מבצע את המשימה באמצעות פונקציה? { )void read_data(int *arr ; int size ; cin >> size ; ]arr = new (std::nothrow) int[size … )if (arr == NULL )for (int i=0; i< size -1; i++ ; ]cin >> arr[i ; a[size –1] = THE_END } נניח כי אחרי שהגדרנו את , int *bible = NULL; :אנו מזמנים את הפונקציה . read_data(bible); :האם קטע תכנית זה עונה על צרכינו? התשובה היא :לא ולא .נסביר :הפונקציה read_dataמקבלת את המצביע כפרמטר ערך .על כן שינויים שהפונקציה מכניסה ל ,arr -אינם נותרים בתום ביצוע הפונקציה ב ,bible -וערכו נותר NULLכפי שהוא היה לפני הקריאה .נציג את התהליך בעזרת המחסנית :נתחיל בהצגת מצב המחסנית עליה מוקצה המשתנה bibleשל התכנית הראשית: = bible 19 ערכו של bibleאותחל בהגדרה להיות ,NULLועל כן ציירנו אותו כמתואר .עתה נערכת קריאה לפונקציה .מצב המחסנית בעקבות הקריאה יהיה הבא )מהציור השמטנו פרטים שאינם מהותיים לצורך דיוננו הנוכחי(: = bible = arr מציור המחסנית השמטנו את המשתנים הלוקליים של ,read_dataואת כתובת החזרה .ערכו של arrהוא NULLשכן הארגומנט המתאים ל arr -הוא ,bible ערכו של bibleהוא ,NULLוערך זה מועתק ל.arr - עתה הפונקציה מתחילה להתבצע .הפקודה arr = new …; :יוצרת את המצב הבא: = bible = arr אחר הפונקציה כלומר על המערך שהוקצה מצביע ) arrאך לא ַ .(bible read_dataקוראת נתונים לתוך המערך ,ועת היא מסתיימת ,ורשומת הפעלה שלה מוסרת מעל-גבי המחסנית ,מצב הזיכרון הינו: = bible כלומר המערך הפך ל zombie -שאין דרך לפנות אליו ,והמצביע bibleנותר בתומתו :משמע ערכו עדיין .NULL אנו רואים אם כן ש read_data -לא עשתה את המצופה .הסיבה היא שהיא קבלה את המערך כפרמטר ערך .כיצד נשנה את הפונקציה על-מנת שהיא כן תשיג את המבוקש? כלומר שבתום פעולתה המצביע bibleיורה על המערך שהפונקציה הקצתה? התשובה היא שעלינו להעביר את המערך כפרמטר הפניה; ואז שינויים 20 שהפונקציה תבצע על הפרמטר ,arrיוותרו בתום ביצוע הפונקציה בארגומנט המתאים .bible עתה ,אם כך ,נשאל כיצד מעבירים מצביע כפרמטר הפניה? התשובה היא שבהגדרת הפרמטר ,אחרי שם הטיפוס יש להוסיף את התו & .טיפוסו של מצביע ל int -הוא * ,intועל-כן כדי שהפרמטר מטיפוס * intיהיה פרמטר הפניה עלינו להגדירו כ.int *& - נראה עתה את הפונקציה read_dataבגרסתה המתוקנת: { )void read_data(int *&arr ; int size ; cin >> size ; ]arr = new (std::nothrow) int[size … )if (arr == NULL )for (int i=0; i< size -1; i++ ; ]cin >> arr[i ; arr[size –1] = THE_END } השינוי היחיד שהכנסנו הוא תוספת ה & -אחרי שם הפרמטר .נתאר את התנהלות העניינים מבחינת מצב הזיכרון :ראשית ,כמו קודם ,מוגדר המשתנה bibleשל התכנית הראשית .מצב הזיכרון הוא: = bible עתה נערכת קריאה לפונקציה .read_dataאולם בגרסתה הנוכחית ל- read_dataיש פרמטר הפניה .כדרכם של פרמטרי הפניה ,אנו שולחים חץ )עם ראש משולש שחור( ,מהפרמטר ַלארגומנט המתאים .נצייר את מצב המחסנית )תוך שאנו מתרכזים רק בפרמטר ,arrומשמיטים מהציור את יתר הפרטים(: = bible =arr אנו רואים כי החץ נשלח מ ,arr -אל ,bibleוכל שינוי שיערך ב ,arr -יתבצע למעשה על .bible עתה הפונקציה מתחילה לרוץ .פקודת הקצאת הזיכרון תיצור ,בגרסה הנוכחית של הפונקציה ,את המצב הבא: = bible =arr 21 כלומר המצביע שערכו השתנה הוא ,bibleשכן עת הפונקציה בצעה … , arr = newומכיוון ש arr -הוא פרמטר הפניהַ ,הלך המחשב בעקבות החץ )עם הראש השחור( ,וביצע את השינוי על הארגומנט המתאים. ַבהמשך קוראת הפונקציה נתונים לתוך המערך .עת הפונקציה מסיימת ,ורשומת ההפעלה שלה מוסרת מעל-גבי המחסנית ,המערך לא נותר ,zombieו bible -אינו :NULLכי אם bibleמצביע על המערך כמבוקש. 11.5פונקציה המחזירה מצביע הפונקציה read_dataשכתבנו שינתה את הפרמטר היחיד שלה .כפי שמיצינו בעבר ,פונקציה אשר מחזירה רק נתון יחיד ראוי לכתוב לא עם פרמטר הפניה ,אלא כפונקציה המחזירה את הנתון המתאים באמצעות פקודת .returnנראה עתה גרסה של read_dataהפועלת על-פי עקרון זה .נקרא לה .read_data2הקריאה לפונקציה מהתכנית הראשית תהיה: ; )(bible = read_data2 מהקריאה אנו למדים ,באופן לא מפתיע ,שהפונקציה מחזירה מצביע ל) int -שכן הערך המוחזר על-ידה מוכנס לתוך משתנה שזה טיפוסו( .כיצד מוגדרת הפונקציה: {)(int *read_data2 ; int *arr, size ; cin >> size ; ]arr = new (std::nothrow) int[size … )if (arr == NULL )for (int i=0; i< size; i++ ; ]cin >> arr[i ; arr[size –1] = THE_END ; ) return( arr } נסביר :הפונקציה אינה מקבלת כל פרמטרים .יש לה משתנה לוקלי מטיפוס * int ששמו הוא .arrהפונקציה מקצה מערך ,מפנה את המצביע להצביע על המערך, וקוראת נתונים לתוך המערך .בסוף פעולתה הפונקציה מחזירה את ערכו של המצביע )כלומר את הכתובת עליה המצביע מורה( .מכיוון שכתובת זאת מוכנסת למשתנה bibleשל התכנית הראשית ,אזי מעתה bibleיצביע על המערך שהוקצה. 22 כדרכנו נציג את התנהלות העניינים בזיכרון .נתחיל עם המחסנית עליה הוגדר המשתנה bibleשל התכנית הראשית: = bible עתה נערכת קריאה לפונקציה .read_data2נציג את מצב המחסנית )תוך התרכזות ַבעיקר בלבד(: = bible = arr הסברַ :בפונקציה מוגדר משתנה לוקלי בשם .arrהמשתנה אינו מאותחל בהגדרה, ועל כן הוא מצביע למקום מקרי כלשהו. עתה הפונקציה מתחילה לרוץ ,בפרט היא מקצה מערך ,ומפנה את arrלהצביע עליו .מצב הזיכרון הוא עתה: = bible = arr אַחר הפונקציה קוראת ערכים לתוך המערך ,ולבסוף היא מסיימת ,תוך שהיא מחזירה את .arrאנו זוכרים כי עת פונקציה מחזירה ערך ,הערך שהיא מחזירה 'מושאר בצד' )ואחר מוכנס ַלמשתנה המתאים(ַ .במקרה של מצביע הערך ש'-מושם בצד' היא הכתובת אליה המצביע מורה ,ומבחינה ציורית המקום עליו החץ מורה. נציג זאת כך: = bible 2 5 2 7 המלבן הימני התחתון מציג את הערך ש' -הושאר בצד' ,והחץ שבו מורה לאותו מקום עליו הורה .arr עתה ,מכיוון שהקריאה לפונקציה הייתה , bible = read_data2(); :אזי הערך ש'-הושאר בצד' מוכנס למשתנה ,bibleכלומר bibleעובר להצביע על המערך שהוקצה על-ידי הפונקציה .והציור המתאים: = bible 5 2 7 2 23 ושוב ,השגנו את האפקט הרצוי :בתום ביצוע הפונקציה מצביע bibleעל מערך שהוקצה על-ידי הפונקציה ,ומכיל נתונים כנדרש. 11.6שינוי גודלו של מערך עתה ברצוננו לכתוב פונקציה אשר תוכל לקרוא מחרוזת באורך לא ידוע .הפונקציה תקרא את תווי המחרוזת בזה אחר זה עד קליטת התו מעבר-שורה ,אשר יציין את סופה של המחרוזת .הקריאה לפונקציה תהיה , a_s = read_s(); :עבור משתנה ; . char *a_sכלומר הפונקציה תחזיר מצביע ַלסטרינג שהיא קראה לתוך מערך שהוקצה דינאמית על-גבי הערמה. הפונקציה שונה מזו שראינו בסעיף הקודם בכך שהיא מקצה מערך בגודל Nתאים, וקוראת לתוכו תווים .עת המערך מתמלא הפונקציה מגדילה אותו בשיעור N וממשיכה בתהליך הקריאה; וכך שוב ושוב כל עוד יש בכך צורך. נציג את הפונקציה ,ואחר נסבירה ביתר פירוט: // pointer to the array the func alloc ][// the size of the_s // how many chars were read, so far { )(char *read_s char *the_s, *temp, ; c int arr_size, ; s_size the_s = new (nothrow) char[N] ; // alloc initial array … )if (the_s == NULL ; arr_size = N ; )(c= cin.get )'while (c != '\n { '// read the string until u read '\n if (s_size == arr_size -1) { // if the_s[] is full ; ]temp = new (nothrow) char[arr_size + N // alloc bigger 1 if (temp == NULL) ... )for (int i = 0; i < s_size; i++ ; ]temp[i] = the_s[i // copy old onto new ; delete [] the_s // free old ; the_s = temp // the_s point to new ; arr_size += N // update size } ; the_s[ s_size++ ] = c ; )(c = cin.get } ; ’the_s[s_size -1] = ‘\0 ; return the_s } יתר כדי להקל על הבנתה( :אנו נסביר את הפונקציה )אף שתיעדנו אותה תיעוד ֵ מתחילים בכך שאנו מקצים למצביע the_sמערך )וכמובן בודקים שההקצאה 24 הצליחה!( .המשתנה arr_sizeשומר את גודלו של המערך שהוקצה .עתה אנו נכנסים ללולאת whileאשר קוראת תו ,תו עד קליטת ’ .‘\nבכל סיבוב בלולאה אנו בודקים האם כבר אין מקום במערך להוספת תו נוסף )כלומר האם s_size .(== arr_size-1אם אכן זה המצב אנו) :א( מקצים מערך חדש בגודל ומפנים את ,arr_size +Nכלומר מערך הגדול ב N -תאים מהמערך הנוכחיְ , המשתנה tempלהצביע על המערך החדש) .ב( אנו מעתיקים ַלמערך החדש את תוכנו של המערך הישן) .ג( אנו משחררים את המערך הישן )באמצעות פקודת ה- מפנים את המצביע ) the_sשכבר אינו מצביע על מערך כלשהו( ) .(deleteד( אנו ְ להצביע על המערך עליו מורה ) .tempה( אנו מעדכנים את arr_sizeעל-ידי הגדלתו בשיעור .Nאחרי שהגדלנו )או לא הגדלנו( את המערך ,אנו פונים לקריאת התו הבא מהקלט. 11.7מערך של מצביעים כפרמטר ערך בסעיפים הקודמים ראינו כי במקום להגדיר מערך סטטי char s[N]; :אנו יכולים להגדיר מצביע char *sp; :אשר בהמשך יהפוך למערך )שמוקצה דינאמית( .אנו גם זוכרים כי בשפת Cמערך דו-ממדי הוא למעשה סדרה של מערכים חד-ממדיים ,או במילים אחרות מערך שכל תא בו הוא מערך חד-ממדי .אם נצרף שבמקום להגדיר מערך דו-ממדי: ְ את שתי המסקנות הללו יחד נוכל להסיק ; ] ,char ss[ROWS][COLSאנו יכולים להגדיר מערך של מצביעיםchar : ;] . *ssp[ROWSכל תא במערך זה הוא מצביע )כלומר יֵצור מטיפוס * (char שיכול בהמשך להפוך למערך חד-ממדי ,אשר יוכל לשמור מחרוזת. עתה נניח כי הגדרנו בתכנית הראשית את המשתנהchar *strings[ROWS]; : )כלומר הגדרנו מערך של מצביעים ,שהינו מערך של פוטנציאלים למערכים חד- ממדיים ,כלומר פוטנציאל למערך דו-ממדי(; אחר אתחלנו את תאיו )שכל אחד מהם הוא מצביע( להכיל את הערך :NULL )for (int i=0; i < ROWS; i++ ; strings[i] = NULL עתה אנו קוראים לפונקציה.read_strings(strings); : הגדרת הפונקציה: { ) ]void read_strings( char *strings[N ; ]char temp[COLS ; int i = 0 { )while (i < ROWS ; cin >> setw(COLS) >> temp )if (strcmp(temp, ".”) == 0 ; break ; ] strings[i]= new (nothrow) char[ strlen(temp) +1 if (strings[i] == NULL) ... ; )strcpy(strings[i++], temp } } נסביר :הפונקציה מתנהלת כלולאה .בכל סיבוב בלולאה אנו קוראים מחרוזת לתוך משתנה העזר ) tempשהינו מערך סטאטי( .במידה והסטרינג הוא ” “.אנו עוצרים את תהליך הקריאה )ולכן גם את ביצוע הפונקציה באופן כללי( .במידה והסטרינג אינו ” “.אנו מקצים מערך ַבגודל הדרוש )כלומר כאורכו של הסטרינג השמור ב- מפנים את ] strings[iלהצביע על tempועוד תא אחד ,בו יאוחסן הְ ,(‘\0’ - אותו מערך ,ומעתיקים את הסטרינג המצוי ב temp -על ] .strings[iשימו לב 25 שהתוצאה המתקבלת היא לא בדיוק מערך דו-ממדי ,אלא מערך בו כל שורה היא באורך שונה. עתה ברצוננו לשאול שאלה כללית יותר :האם הפונקציה תבצע את משימתה כהלכה? האם טיפוס הפרמטר של הפונקציה הוא כנדרש? האם שינויים שהפונקציה תכניס לתוך הפרמטר stringsיישארו בארגומנט המתאים בתום ביצוע הפונקציה? התשובה היא :כן! וכן! נסביר מדוע :אנו יודעים היטב כי שינויים שפונקציה מכניסה לתוך תאי מערך שהינו פרמטר של הפונקציה נשארים בתום ביצוע הפונקציה בארגומנט המתאים; וְ כלל זה נכון גם עבור מערך שכל תא בו הוא מצביע )אשר הופנה ַבפונקציה להצביע על סטרינג שהוקצה דינמית על הערמה(. נתאר את השתלשלות העניינים בזיכרון. 26 ראשית ,בתכנית הראשית ,על-גבי המחסנית ,מוגדר מערך של מצביעים ,ותאי מאותחלים לערך .NULLנציג את המחסנית: =strings עתה מתבצעת קריאה ַלפונקציה .המערך מועבר כפרמטר ערך ,כלומר ערכו של הארגומנט ,stringsמועתק על הפרמטר .stringsמבחינה ציורית משמעות הדבר היא שהפרמטר יצביע לאותו מקום כמו הארגומנט .נציג את מצב המחסנית: =strings =strings עת הפונקציה מבצעת פקודה כגון strings[0] = new char[4]; :מתקבל המצב הבא בזיכרון: =strings =strings כלומר המצביע שבתא מספר אפס במערך המצביעים המקורי )והיחיד הקיים( הוא שעובר להצביע על הסטרינג המוקצה .אם אחרי קריאת הסטרינג השני יוקצה מערך בגודל שלושה תאים אזי מצב הזיכרון יהיה: =strings =strings 27 ַבפונקציה read_stringsשכתבנו הגדרנו את הפרמטר באופן הבא: ] . *strings[Nכפי שאנו יודעים במקום לתאר פרמטר כמערך )ואפילו הוא מערך של מצביעים( אנו יכולים לתארו כמצביע .לכן במקום להגדיר את הפרמטר כ: ] char *strings[Nאנו יכולים להגדירו כ ,char **strings :במילים: כמצביע למצביע לתו )מבחינה תחבירית אנו ממירים ] [Nב .(* -גוף הפונקציה לא יצטרך לעבור שינוי גם אם נבחר לתאר את הפרמטר באופן זה. char 11.8מערך של מצביעים כפרמטר הפניה מערך המצביעים ,char **stringsשראינו בסעיף שעבר ,הועבר כפרמטר ערך, ולכן ביכולתה של הפונקציה היה לשנות רק את ערכם של תאי המערך ,אך לא את גודלו של המערך .נניח כי ברצוננו לכתוב פונקציה אשר קוראת סדרת מחרוזות. מספר המחרוזות אינו ידוע למתכנת ,ולכן בתכנית הראשית לא ניתן להגדיר משתנה ,char *strings[ROWS] :שכן משתנה שכזה מגדיר את מספר המחרוזות להיות לכל היותר .ROWSמה יהיה הפתרון? אנלוגי לזה שראינו עם מערך חד-ממדי שגודלו היה לא ידוע למתכנת :במקום להגדיר מערך ],char s[N הגדרנו מצביע ) char *sכלומר פוטנציאל למערך .מבחינה תחבירית המרנו את ] [Nב .(* -את המצביע העברנו כפרמטר הפניה ְלפונקציה אשר יכלה לממש אותו לכדי מערך ַבגודל הרצוי לה )ואף לשנות את גודלו של המערך עת זה מה שנדרש( .גם במקום להגדיר מערך סטטי של עם מערך של מצביעים ננקוט באותו פתרוןְ : מצביעים ] char *strings[ROWSנגדיר מצביע למצביע .char **strings )שוב המרנו את ה [N] -ב .(* -את המצביע למצביע נעבר כפרמטר הפניה לפונקציה אשר תקצה מערך של מצביעים בגודל הרצוי )ואף תוכל לשנות את גודלו של המערך עת זה מה שיידרש( .כיצד מתארים פרמטר הפניה שהינו מצביע למצביע? כותבים אחרי שם הטיפוס ,לדוגמה אחרי ** ,charאת התו &. נציג עתה את הפונקציה alloc_arr_n_read_stringsאשר תקבל מצביע למצביע כפרמטר הפניה ,תקרא מהמשתמש כמה מחרוזות ברצונו להזין ,תקצה ואחר תקרא סדרת מחרוזות תוך שימוש ַ מערך של מצביעים בגודל הדרוש, בפונקציה read_stringsשכתבנו בסעיף שעבר ,ואשר מקבלת מערך מוכן של מצביעים .בפונקציה read_stringsעלינו לבצע שינוי קל :הפונקציה תקבל פרמטר ערך נוסף ,int arr_size ,אשר יורה לה מה גודלו של מערך המצביעים המועבר לה) .להזכירכם ,בגרסה שכתבנו הנחנו כי גודלו של המערך הוא הקבוע תקרא מהיותו גלובלי( .הפונקציה ְ read_strings ,ROWSאשר מוכר לפונקציה ֵ במקום לכל היותר ROWSמחרוזות ,כפי שהיא לכל יותר arr_sizeמחרוזות ) ְ עשתה במקור(. הפונקציה alloc_arr_n_read_stringsשנכתוב תחזיר באמצעות פרמטר הפניה את גודלו של מערך המצביעים שהוקצה. בתכנית הראשית נגדיר ,אם כן: ; char **arr_of_strings ; int arr_size ונזמן את הפונקציה באופן הבא: ; )alloc_arr_n_read_strings(arr_of_strings, arr_size 28 הגדרת הפונקציה: void alloc_arr_n_read_strings(char **&arr_of_strings, )int &arr_size { ; cin >> arr_size ; ]arr_of_strings = new (nothrow) char *[arr_size if (arr_of_strings == NULL) ... )for (int i = 0; i < arr_size; i++ ; arr_of_strings[i] = NULL ; ) read_strings( arr_of_strings, arr_size } הסבר :הפונקציה מקבלת את המצביע למצביע )כלומר את הפוטנציאל למערך של מצביעים( כפרמטר הפניה .לכן שינויים שהפונקציה תערוך ַלפרמטר שלה יוותרו בתום ביצוע הפונקציה בארגומנט המתאים .פקודת ה new -המבוצעת על-ידי ַ הפונקציה מקצה מערך של מצביעים .שימו לב כי עת אנו כותבים ]…[new char אנו מקצים מערך של תווים ,ועת אנו כותבים ]…[* new charאנו מקצים מערך של מצביעים לתווים .כמובן שאחרי ביצוע ההקצאה יש לבדוק שערכו של המצביע שונה מ.NULL - אחרי שהקצאנו מערך של מצביעים ,ואיתחלנו את תאיו לערך ,NULLאנו יכולים לזמן את הפונקציה read_stringsאשר מצפה לקבל מערך קיים של מצביעים. נציג את התנהלות התכנית מבחינת ציור הזיכרון .כמו תמיד נתרכז רק ַבהיבטים המשמעותיים לצורך דיוננוַ .בתחילה ,אנו מגדירים ַבתכנית הראשית מצביע למצביע ,arr_of_stringsומשתנה שלם .arr_sizeנציג את מצב המחסנית: = Arr_of_strings = arr_size מכיוון שהמצביע לא אותחל ציירנו אותו כמורה למקום מקרי. עתה אנו מזַמנים את הפונקציה ַ . alloc_arr_n_read_stringsלפונקציה יש שני פרמטרי הפניה ,ועל כן אנו שולחים חץ )עם ראש בצורת משולש שחור( מכל אחד משני הפרמטרים אל הארגומנט המתאים לאותו פרמטר .נציג את מצב המחסנית: = arr_of_strings = arr_size = arr_of_strings = arr_size 29 עתה הפונקציה מתחילה להתבצע .עת הפונקציה מבצעת את פקודה ה new -מוקצה על-גבי הערמה מערך של מצביעים .מי יצביע על מערך זה? הפרמטר arr_of_stringsשל הפונקציה הוא פרמטר הפניה )חץ עם ראש משולש שחור(, כדרכו של פרמטר הפניה אנו הולכים בעקבות החץ ומטפלים בארגומנט המתאים, כלומר במשתנה arr_of_stringsשל התכנית הראשית; ומצביע זה )שכדרכו של מצביע צויר עם חץ שראשו בצורת (vהוא שמצביע על המערך שהוקצה .נתאר זאת ציורית: = arr_of_strings = arr_size = arr_of_strings = arr_size שהנם ,כזכור לנו ,מצביעים לתווים( .ולכן אחר ,אנו מאתחלים את תאי המערך ) ִ ַ בשלב הבא אנו קוראים לפונקציה בציור תארנו כל תא כמורה על ְ .NULL read_stringsומעבירים לה את מערך המצביעים כפרמטר ערך ,כלומר הפרמטר של הפונקציה )שהנו חץ עם ראש בצורת (vיצביע גם הוא על מערך המצביעים .גם גודלו של המערך מועבר כפרמטר ערך ,ולכן הערך המתאים מועתק לפרמטר .arr_sizeנציג זאת בציור: = arr_of_strings arr_size = 4 = arr_of_strings = arr_size = strings arr_size = 4 עת הפונקציה read_stringsמתחילה להתבצע אנו חוזרים למצב שתיארנו מבצעת: הפונקציה עת לדוגמה, הקודם. בסעיף ;] strings[0] = new char[3מוקצה מערך ,ותא מספר אפס ַבמערך stringsעובר להצביע על מערך זה .הציור המתאים יהיה: = arr_of_strings arr_size = 4 = arr_of_strings = arr_size = strings arr_size = 4 30 11.9מצביעים במקום פרמטרי הפניה בעבר ,ראינו את הפונקציה swapאשר מחליפה בין ערכם של שני הפרמטרים המועברים לה .כתבנו אותה באופן הבא: { )void swap( int &var1, int &var2 ; int temp = var1 ; var1 = var2 ; var2 = temp } int כלומר לפונקציה swapיש שני פרמטרי הפניה .אם בתכנית מוגדרים: ; a, bולתוך זוג המשתנים הכנסנו ערכים ,אזי אנו יכולים לקרוא לפונקציה ;) swap(a, bוהפונקציה תחליף בין ערכיהם של aושל .b יש המסתייגים מפונקציה כדוגמת swapהנ"ל .המסתייגים טוענים כי מהקריאה לפונקציה ) ;) (swap(a, bלא ניתן לדעת שהפרמטרים של הפונקציה הינם לשנות את ערכם של הפרמטרים, פרמטרי הפניה ,ולכן) :א( ַלפונקציה יש את הכוח ַ וכן) :ב( לא ניתן לזמן את הפונקציה באופן הבא . swap(a, 17); :המסתייגים טוענים כי עת תכניות נעשות גדולות מאוד ,ונכתבות לכן על-ידי צוות של מתכנתים, יש חשיבות עליונה לכך שמאופן הקריאה ַלפונקציה נוכל לדעת את סוג הפרמטרים של הפונקציה .מצביעים עשויים לבוא לעזרנו גם בסוגיה זאת. התחלנו את דיוננו בנושא מצביעים בכך שראינו שניתן להפנות מצביע להצביע על משתנה קיים .אם הגדרנו בתכנית , int num1 =17, *ip ; :אזי הפקודה: ; ip = &num1תפנה את המצביע ipלהורות על המשתנה .num1מבחינה ציורית אנו מתארים זאת כך: num1 = 17 = ip ַבעקרון שהצגנו ִ בפסקה האחרונה נעשה שימוש כדי לכתוב את swapבצורה שונה, שתענה על דרישות המסתייגים .שוב נניח כי בתכנית הוגדרו המשתנים: ; int a, bולצורך דיוננו הנוכחי נניח כי הוגדרו גם . int *pa, *pb; :עוד נניח כי ל a -ול b -הוכנסו ערכים ,ועתה ברצוננו להחליף בין ערכי המשתנים הללו. ;pa = &a נוכל לבצע זאת בדרך הבאה :ראשית בתכנית הראשית נכתוב: ; . pb = &bועתה נזמן את הפונקציה . new_swap(pa, pb); :נציג את הגדרת הפונקציה :new_swap { )void new_swap( int *pvar1, int *pvar2 ; int temp = *pvar1 ; *pvar1 = *pvar2 ; *pvar2 = temp } 31 עתה נבחן מדוע ואיך הפונקציה שהצגנו עושה את מלאכת ההחלפה בין הערכים. נתחיל בהצגת המחסנית הכוללת את ארבעת משתני התכנית הראשית .ונניח כי למשתנים a, bהוכנסו ערכים כמתואר בציור: a = 17 b = 3879 = pa = pb הפקודותpb = &b; : ;ִ pa = &aתצורנה את המצב הבא: a = 17 b = 3879 = pa = pb כלומר כל מצביע מורה על המשתנה המתאים לו .עתה אנו מזמנים את הפונקציה ;) new_swap(pa, pbאשר מקבלת את שני המצביעים כפרמטרי ערך ,ולכן ערכם של הארגומנטים pa, pbמועתק על הפרמטרים pvar1, pvar2בהתאמה; מבחינה ציורית pvar1, pvar2 ,יצביעו גם הם על אותם מקומות כמו ,pa, pb כלומר על .a, bנציג את מצב המחסנית: a = 17 b = 3879 = pa = pb = pvar1 = pvar2 = temp עתה הפונקציה מתחילה להתבצע :הפקודה temp = *pvar1; :מכניסה לtemp - את הערך המצוי בתא עליו מצביע ,pvar1כלומר את הערך .17הפקודה: ; *pvar1 = *pvar2מכניסה לתא הזיכרון עליו מצביע pvar1את הערך המצוי בתא הזיכרון עליו מצביע ,pvar2לכן לתוך תא הזיכרון aמוכנס ערכו של התא הזיכרון ,bכלומר .3879לבסוף הפקודה *pvar2 = temp; :מכניסה לתוך תא הזיכרון עליו מצביע ,pvar2כלומר לתוך תא הזיכרון ,bאת הערך 17השמור ב- .tempבכך השלימה הפונקציה את פעולתה בהצלחה. עתה נשאל מדוע new_swapעדיפה על-פני ?swapהתשובה היא שעת אנו מעבירים מצביע ְלמשתנה הדבר ניכר לעין גם בקריאה ַלפונקציה ,ומרמז על כך שהפונקציה עתידה לשנות את ערכו של המשתנה ,שהרי אחרת היינו מעבירים לה את ערכו של המשתנה )כלומר היה לה פרמטר ערך( ,ולא מצביע ַלמשתנה .מטעמים דידקטיים אנו קראנו לפונקציה new_swapבשני שלבים :ראשית הפננו את המצביעיםpa, : pbלהצביע על , a, bושנית קראנו לפונקציה עם .pa, pbלמעשה ניתן לקרוא ;) . new_swap(&a, &bבצורת קריאה זאת אנו לפונקציה בצעד אחד: מוותרים על משתני העזר ,pa, pbהקריאה לפונקציהֵ ,ראשית מחשבת את כתובתם של aושל ,bושנית משימה בפרמטרים pvar1, pvar2את הכתובות הללו ,באופן שהפרמטרים מצביעים על הארגומנטים המתאימים כנדרש .איחוד שני 32 הצעדים לכדי צעד אחד מדגיש יותר את העובדה כי new_swapמקבלת מצביעים ַלמשתנים ) a,bולכן סביר להניח שהיא עתידה לשנות את ערכם(. 11.10 מצביע מדרגה שנייה )** (intכפרמט קבוע )(const פונ' שאמורה לקבל ** intלקריאה בלבד תקבלוconst int * const * : הסיבה :נניח תסריט: // assume this is legel // (actually, fortunately, it is not). // legal as both have the same type: * // const int // but now p points to one * // BAD, but legal, p's type is int 11.11 ; const int one = 1 ; int *p = NULL ; const int **p2p = &p *p2p = &one ; *p = 2 פרמטרים המועברים ל argc :main -וargv - בפרק תשע ראינו כי כדי לשמור מערך של Nסטרינגים כל אחד באורך של לכל היותר M-1תווים אנו מגדירים מערך דו-ממדי של תווים. char s[N][M]; : במקום להגדיר מערך סטטי כנ"ל אנו יכולים להגדיר מערך בפרק הנוכחי ראינו כי ְ של מצביעים . char *s[N]; :מערך זה יוכל לשמור לכל היותר Nסטרינגים ,כל אחר ראינו כי במקום להגדיר את אחד מהם באורך שייקבע במהלך ריצת התכניתַ . גודלו של המערך להיות בן Nמצביעים אנו יכולים להגדיר מצביע למצביעchar : ; . **sבאופן זה נוכל תוך כדי ריצת התכנית לקבוע )ולשנות( הן את גודלו של שישמר במערך. ֵ המערך ,והן את אורכו של כל סטרינג וסטרינג בעבר העלנו את השאלה האם גם ַלתכנית הראשית ,main ,ניתן להעביר ַ ארגומנטים? ענִ ינו אז על השאלה בחיוב ,אך לא יכולנו להסביר כיצד בדיוק הדבר מתבצע .עתה חכמנו מספיק כדי ללמוד כיצד הדבר מתבצע .נניח כי ברצוננו לכתוב תכנית אשר מקבלת מספר כלשהו של מספרים שלמים ,ומציגה את סכומם .נוכל לעשות זאת ַבאופן הבא: א .נכתוב תכנית ,אותה נציג מייד ,ונשמור את התכנית בקובץ בשם . sum.cc )הסיומת ccמעידה כי זהו קובץ המכיל תכנית מקור בשפת ,C++התחילית sum שנתנו ַלקובץ כדי להעיד על מהות התכנית המצויה בו .במערכת הפעלה היא שם ַ חלונות ,ייקרא הקובץ .(sum.cpp בשפת מכונה שיקרא ) .sum.בחלונות ונייצר קובץ ְ ֵ ב .נקמפל את הקובץ ,sum.cc ייקרא הקובץ ,sum.exeהסיומת exeמעידה כי קובץ זה מכיל תכנית ניתנת להרצה.(executable code , ג .אם עבדנו בסביבת עבודה אזי עתה נצא מסביבת העבודה ,ונעבור לעבוד אל מול מערכת ההפעלה באופנוּת הקרויה ,command lineכלומר לאופנות בה אנו מקלידים )בשורה ,אות אחר אות( את הפקודות שברצוננו לבצע .בסביבת חלונות ,למשל ,אנו עוברים לעבוד בצורת עבודה של .MS-DOS prompt ד .נתמקם במחיצה בה מצוי הקובץ . sum.exe 33 הבא: באופן או ה .נריץ את התכנית באופן הבאsum 3879 17 9 : , sum –6 4 1 1כלומר תוך שאנו כותבים את שם התכנית ) (sumואחר את סדרת תסכום .לשם הפשטות נניח כי התכנית המספרים השלמים שברצוננו שהתכנית ְ מורצת עם סדרת מספרים תקינה. עתה נציג את התכנית ,ואחר נסבירה: >#include <iostream.h >#include <stdlib.h )(// needed for atoi { )int main(int argc, char **argv ; int sum = 0 )for (int i = 1; i < argc; i++ ; )]sum += atoi(argv[i ; cout << sum << endl ; return EXIT_SUCCESS } אנו רואים כי לתכנית הראשית יש שני פרמטרים: א .הפרמטר ) argcקיצור של (argument counterמעיד כמה מחרוזות כוללת הפקודה באמצעותה הורצה התכנית .מספר המחרוזות יהיה לכל הפחות אחד: שם התכנית .במידה והמשתמש הקליד בנוסף לשם התכנית מחרוזות נוספות יהיה ערכו של argcכמספר המחרוזות שהוקלדו )כולל שם התכנית( .לדוגמה אם התכנית מורצת באופן sum 643 –49 –3 1 :אזי ערכו של argcהוא חמש, שכן הוזנו חמיש מחרוזות) :א( ) ,sumב( ) ,643ג( ) ,49-ד( ) ,3-ה( .1 ב .הפרמטר ) argvקיצור של (argument vectorהוא מערך של מחרוזות .התא מספר אפס ַבמערך כולל את שם התכנית )בדוגמה שלנו .(sumהתאים הבאים כוללים את המחרוזות הנוספות ,זו אחר זו. בתכנית שלנו אנו מגדירים משתנה בשם sumשיכיל את סכום הארגומנטים .אחר אנו מבצעים לולאה שעוברת על הארגומנטים ,החל בארגומנט מספר אחד )שכן ] argv[0כולל ,כזכור ,את שם התכנית( ,עבוּר כל אחד ואחד מארגומנטים הלולאה ממירה את המחרוזת המצויה בתא המתאים במערך argvלכדי ערך שלם )באמצעות הפונקציה ,atoiששמה הוא קיצור של ,(ascii to integer ומוסיפה את הערך השלם ַלמשתנה .sumלבסוף הפונקציה מציגה את ערכו של .sum נראה דוגמה נוספת :נניח כי ברצוננו לכתוב תכנית בשם progאשר מקבלת שתי ְ מחרוזות ומודיעה האם כל אחד ואחד מתווי המחרוזת הראשונה מצוי במחרוזת השנייה .לדוגמה עבור ההרצה prog abbca cba :תציג התכנית את הפלט 'כן', שכן כל אחד ואחד מתווי המחרוזת abbcaמצוי במחרוזת .cbaלעומת זאת עבור ההרצה prog xyz xy :יוצג הפלט 'לא' ,שכן התו ’ ‘zמופיע במחרוזת הראשונה אך לא בשנייה. 34 התכנית תיכתב באופן הבא: { )int main(int argc, char **argv { )if (argc != 3 ]cout << "Program should be run:" << arv[0 ; ”<< “ <string> <string> \n ;) return( EXIT_FAILURE } )for (int i1 = 0; i1 < strlen( argv[1] ); i1++ { )for (int i2 = 0; i2 < strlen( argv[2] ); i2++ )]if (arvg[2][i2] == argv[1][i1 ; break ) ) ]if (i2 == strlen( argv[2 { ; "cout << "no\n ; )return(EXIT_SUCCESS } } ; ”cout << "yes\n ; )return(EXIT_SUCCESS } נסביר :ראשית ,אנו יודעים כי בתכנית זאת ערכו של argcצריך להיות בדיוק שלוש )שם התכנית פלוס שתי המחרוזות המועברות כארגומנטים לתכנית( ,לכן אם ערכו של argcשונה משלוש סימן שהמשתמש לא הריץ את התכנית כהלכה; אנו משגרים לו הודעת שגיאה ,ועוצרים את ביצוע התכנית. ַבהמשך אנו נכנסים ללולאה כפולה .הלולאה החיצונית עוברת על כל תווי ] .argv[1עבור כל תו ותו הלולאה בודקת האם הוא מצוי ב .argv[2] -עבור כל ערך קבוע של ,i1כלומר עבור כל תו של ] argv[1הלולאה הפנימית בודקת האם תא זה מצוי ב .argv[2] -הלולאה הפנימית רצה על כל תאי ] argv[2ובמידה והיא מוצאת תא ] argv[2][i2שערכו שווה ל argv[1][i1] -היא מסיקה שהתו המתאים ב argv[1] -מצוי ב .argv[2] -במקרה שכזה אנו שוברים את ביצוע הלולאה הפנימית .אחרי הרצת הלולאה הפנימית ,אם ערכו של i2הוא כאורכה של המחרוזת ] argv[2סימן שהתו לא נמצא .במקרה שכזה אנו מציגים הודעה מתאימה למשתמש ,ועוצרים את ביצוע התכנית .מנֵגד ,אם מיצינו את הלולאה החיצונית ,בלי שהודענו כי תו כלשהו מ argv[1] -אינו מצוי ב,arv[2] - סימן שכל תווי ] argv[1מצויים ב .argv[2] -אנו מודיעים על כך ,ועוצרים. 11.12 תרגילים 11.12.1 תרגיל מספר אחד :איתור תת-מערך במערך ממשו את הפונקציה הבאה: int array[], int array_size, int lower_bound, int upper_bound, )*subarray_sizep 35 int *subarray_in_range(const const const const =int בהינתן המערך arrayשגודלו array_sizeעליכם למצוא את תת המערך הראשון )קרי הקרוב ביותר לתחילת המערך (arrayמגודל לא טריביאלי )עם יותר מאבר יחיד( שכל אבריו נמצאים בין lower_boundלבין ,upper_boundכולל שני הגבולות. אם מצאתם תת מערך כזה ערך החזרה של הפונקציה יהיה כתובתו של האבר הראשון בתת המערך ,ו *subarray_sizep -יעודכן לגודלו של תת מערך זה. אחרת ערך החזרה ו *subarray_sizep -יעודכנו שניהם לאפס. שימו לב :על הפונקציה שלכם להיכתב תוך שימוש במצביעים בלבד. אל תגדירו משתנים מכל סוג אחר ,ואל תשתמשו בsubarray_sizep - לאף מטרה למעט זו שלשמה נועד. 11.12.2 תרגיל מספר שתיים :מסד נתוני משפחות תרגיל 11.6.4מתרגל גם מערכים ומצביעים באופן אינטנסיבי. 11.12.3 תרגיל מספר שלוש :טיפול בסיסי במערכים ומצביעים בתכנית מוגדרים: { struct a_nums_list ; int *nums ; unsigned int list_len ; } { struct many_nums_lists ; a_nums_list *lists ; unsigend int num_of_lists ; } ; many_nums_lists arr המשתנה arrמסוגל )אם יעשה בו שימוש מתאים( להיהפך למערך שיכיל שורות של מספרים שלמים ,כל-אחת באורך שונה .לצד כל שורה נחזיק את אורכה ,וכן נחזיק את מספר השורות שהמשתנה arrמחזיק בכל נקודת זמן) .ראשית בררו לעצמכם מדוע וכיצד מתקיימים כל ההיגדים שנזכרו עד כה( .בתכנית שנכתוב נשאף שלא להחזיק שורות יותר מהמינימום ההכרחי ,ואורכה של כל שורה לא יהיה ארוך מהמינימום ההכרחי. כתבו את הפונקציה ַ .add_valלפונקציה ארבעה פרמטרים :מערך )בשם (arr מטיפוס ,many_nums_listsערך שלם ) (valאותו יש להכניס ַלמערך ,מספר שורה ) (lineלתוכה יש להכניס את הערך ,ומספר תא רצוי ) (cellבאותה שורה. הפונקציה תכניס את הערך valלמקום cellבשורה lineבמערך arrתוך שהיא דואגת להיבטים הבאים: .1אם ב arr -אין די שורות אזי הוא יוגדל כך שתהייה בו שורה מספר .line .2אם בשורה הרצויה אין די מקום אזי היא תוגדל כך שיהיה בה תא מספר ַ .placeבתאים הריקים בשורה יושם הערך הקבוע .EMPTY 36 37 תוקן 10/10 struct 12 מבנים ) (structuresהם כלי נוסף אשר יסייע לנו לשפר את סגנונן של התכניות שאנו כותבים. 12.1מוטיבציה נניח כי ברצוננו לכתוב תכנית אשר מחזיקה נתונים אודות המשתמש בה .בתכנית נרצה לשמור את שמו הפרטי ,מינו ,ומספר הנעליים של המשתמש .לשם כך נוכל להגדיר משתנים: ; ]char name[N ; bool gender ; unsigned int shoe_num כדי לקרוא את הנתונים הדרושים נכתוב את הפונקציה: void read_data(char name[], bool &gender, ;)unsigned int &shoe_num הפונקציה מקבלת את פרמטרי ההפניה הדרושים. צורת הכתיבה שהצגנו היא לגיטימית ,אך היא אינה די מדגישה כי שלושת המשתנים שהגדרנו מתארים שלושה היבטים של אותו אדם ,ועל כן הם אינם בלתי קשורים זה לזה כפי ששלושה משתנים כלשהם עשויים להיות. נרצה ,על-כן ,כלי באמצעותו נוכל 'לארוז' יחד לכדי 'חבילה אחת' מספר משתנים המתארים מספר היבטים שונים של אותו אובייקט .הכלי שמעמידה לרשותנו שפת Cלשם השגת המטרה הוא ה.struct - ַבתכנית בה אנו דנים ,נוכל לכתוב מתחת להגדרת הקבועים: { struct User ; ]char _name[N ; bool _gender ; unsigned int _shoe_num ; } שימו לב לכמה הערות קטנות: א .שם של טיפוס\סוג מבנה נהוג שיתחיל באות גדולה. ב .שם של חברים במבנה נהוג להתחיל עם קו תחתון. ג .אחרי הסוגר הימני מופיעה נקודה-פסיק )כמו ב.(enum - מה קבלנו? אמרנו בכך למחשב כי אנו מגדירים בזאת 'סוג חבילה' שנקרא User )המילה השמורה structהיא ֵשמורה שאנו מגדירים כאן 'סוג חבילה' ,והמילה שמופיעה אחריה מתארת את שם 'החבילה' ,במקרה שלנו .(User :בתוך הסוגריים המסולסלים אנו מתארים אילו מרכיבים יהיו בכל חבילה מסוג .User אם אנו מעוניינים בכך אנו יכולים להגדיר בתכנית גם סוגים נוספים של חבילות. לדוגמה: { struct Course ; ]char _name[N ; unsigned int _weight ; char _for_year 38 ;} 'חבילה' מסוג Courseמיועדת לתיאור נתונים אודות קורסים :שם הקורס ,מספר נקודות הזכות שהוא מקנה ,לאיזה שנה אקדמית הוא מיועד )….(a,b,c, נדגיש כי הגדרת 'חבילה' אינה מקצה זיכרון בו מאוחסן משתנה כלשהו; היא רק מתארת סוג 'מארז' המכיל היבטים שונים המתארים נושא כלשהו אותו ברצוננו לייצג בתכניתנו .כלומר ,לעת עתה לא ניתן לשמור בתכנית שלנו מידע אודות משתמשים )או אודות קורסים(. אחרי שהגדרנו -structים כפי צרכינו ) ַבמקום שבין הגדרת הקבועים להצהרה על הפרוטוטיפים( אנו יכולים בגוף התכנית להגדיר משתנים: ; struct User user1, user2 ; struct Course c1 בכך הגדרנו זוג משתנים user1, user2שהם חבילות ,כלומר כל-אחד מהם כולל user1._name, user1._gender, שלושה מרכיבים .המרכיבים הם . user1._shoe_numמשמע כדי לפנות למרכיב כלשהו עלינו לציין את שם המשתנה ,אחר-כך את התו נקודה ,ואחר-כך את שם המרכיב .בשפת Cאנו קוראים לכל מרכיב ב struct -בשם חבר ) .(memberבשפות אחרות המרכיבים נקראים שדות ) .(fieldsחבר ְבמבנה הוא משתנה ככל משתנה אחר מהטיפוס המתאים .לכן אנו רשאים לכתוב פקודות כגון הבאות: ; cin >> setw(N) >> user1._name ; )cout << strlen(user1._name ; cin >> user1._shoe_num )if (user1._shoe_num < 19 || user1._shoe_num > 55 ; ”cout << "Are u shure?\n ; cin >> i )if (i == 1 ; user1._gender = FEMALE אנו רואים ,אם כן ,כי struct Userמאפשר לנו לאגד את ההיבטים השונים של המשתמש בתכניתנו לכדי משתנה יחיד. אעיר כי בשפת סי חובה להגדיר את המשתנה באופןstruct User user1 ; : בעוד בשפת C++ניתן לכתוב גם ,User usr1 :כלומר להשמיט את המילה .structלטעמי ,גם בשפת C++לא כדאי להשמיטה וזאת כדי שלקורא יהיה ברור שהטיפוס Userהוא טיפוס של מבנים. 12.2העברת structכפרמטר לפונקציה נניח כי בתכנית כלשהי הגדרנו את , struct Userואת המשתנים .user2עתה נרצה לכתוב פונקציה read_dataאשר תקרא נתונים לתוך משתנה מטיפוס .struct userהפונקציה תיכתב באופן הבא: user1, { )void read_data(struct User &an_user ; int temp ; cin >> setw(N) >> an_user._name ; cin >> an_user._shoe_num ; cin >> temp ; an_user._gender = (temp == 1) ? FEMALE : MALE 39 } זימון הפונקציה יעשה באופן הבא . read_data(user1); :מכיוון שהפרמטר של הפונקציה הוא פרמטר הפניה אזי שינויים שהפונקציה תכניס לתוך הפרמטר an_userיישארו בתום ביצוע הפונקציה בארגומנט המתאים )בדוגמה שלנו: .(user1 נבחן עתה פונקציה מעט שונה שתדגים לנו מה קורה עת מבנה מועבר כפרמטר ערך. ראשית נציג את הפונקציה ,ואחר נדון בה: { )void f(struct User u ; )”strcpy(u._name, “dana } של- ַמנים את הפונקציה fבאופן הבא f(user1); :וזאת אחרי ֵ נניח כי אנו מז ְ user1הוזנו פרטי המשתמש יוסי .האם בתום ביצוע הפונקציה יכיל user1.name את הערך דנה? בעבר ראינו כי לפונקציה יש את הכוח לשנות את ערכם של תאי מערך המועבר לה כפרמטר .בפרק 10גם הבנו מדוע הדבר קורה :שכן ַלפונקציה מועבר מצביע לתא הראשון של המערך ,והשינויים שהפונקציה מבצעת קורים על המערך המקורי .עתה נלמד כי אם המערך הוא חלק ממבנה המועבר לפונקציה כפרמטר ערך אזי אין בכוחה של הפונקציה לשנות את ערכם של חברי המבנה בארגומנט ִעמו היא נקראה ,בפרט לא את ערכו של החבר ;nameועל-כן בתום ביצועה של הפונקציה fימשיך user1.nameלהכיל את הערך ” .“yosiהסיבה לכך היא שעת מבנה מועבר כפרמטר ערך יוצר המחשב עותק חדש של המבנה במסגרת המחסנית של הפונקציה )הנבנית על-גבי המחסנית( .השינויים שהפונקציה שבפרמטר )ולא על הארגומנט(. עורכת מתבצעים על העותק ַ נדגים זאת בעזרת ציור המחסנית :נתחיל מכך שבתכנית הראשית הוגדרו שני משתנים user1, user2המכילים ערכים כמתואר )בציור הצגנו כל מבנה כמלבן נפרד בתוך המלבן המייצג את המחסנית(: = user1 "_name = "yosi _shoe_num = 36 _gender = MALE = user2 "_name = "yafa _shoe_num = 46 _gender = FEMALE 40 stack frame of main עתה נניח כי אנו קוראים . f(user1); :נציג את מצב המחסנית: = user1 "name = "yosi shoe_num = 36 gender = MALE stack frame of main = user2 "name = "yafa shoe_num = 46 gender = FEMALE =u "name = "yosi shoe_num = 36 gender = MALE stack frame of f עת fמבצעת את הפעולה strcpy(u._name, “dana”); :מועתק הסטרינג ” “danaעל החבר _nameבפרמטר uשל .fערכו של user1._nameאינו משתנה. ראינו כי עת מבנה מועבר כפרמטר ערך יוצר המחשב עותק נוסף של המבנה .עבור מבנה גדול ,הכולל חברים רבים ,ביניהם מערכים ,תחייב עבודת ההעתקה השקעה של זמן ושל זיכרון .על-כן נהוג להיזהר מלהעביר מבנים גדולים כפרמטרי ערך. במקום זאת נוהגים להעבירם כפרמטרי הפניה ,ואז נשלח חץ )עם ראש משולש שחור( מהפרמטר לארגומנט המתאים ,ולא מתבצעת ההעתקה של המבנה. לחילופין ,ניתן להעביר מצביע למבנה ,ובכך שוב לחסוך את עבודת ההעתקה. אם ברצוננו למנוע מהפונ' לשנות את המבנה אזי נעבירו כפרמטר הפניה קבוע: . const User &uבאופן זה ,מצד אחד תודות לכך שזהו פרמטר הפניה לא מבוצעת עבודת העתקה )הדורשת זמן וזיכרון( ,ומצד שני ,תודות לכך שהפרמטר הוא קבוע ,לא ניתן לשנותו .שימו לב שפרמטר שהוגדר כך אינו זהה לפרמטר ערך, שכן פרמטר ערך פונ' רשאית לשנות )רק שהשינוי לא ייוותר בארגומנט( ,כאן הפונ' אינה רשאית לשנות כלל את הפרמטר. 12.3מערך של struct בסעיף הקודם ראינו כיצד מגדירים מבנה יחיד .לעיתים אנו זקוקים למערך של מבנים .לדומה :נניח כי ברצוננו לשמור נתונים אודות כל תלמיד ותלמיד הלומד את הקורס 'מבוא לאגיפטולוגיה' .עבור כל תלמיד נרצה לשמור את שמו הפרטי ,את מינו ,מספר הנעליים שלו ,ומערך שיכיל את ציוניו בתרגילים השונים שניתנו בקורס. נגדיר לכן: { struct Stud ; ]char _name[N ; bool _gender ; unsigned _int shoe_num ; ]int _grades[MAX_EX ; } 41 אחר ,בתכנית הראשית ,נגדיר . struct Stud egypt[MAX_STUD]; :מה קיבלנו? מערך שכל תא בו הוא מבנה מטיפוס .struct Stud עתה נוכל לקרוא לפונקציה . read_data(egypt); :אנו מעבירים לפונקציה את המערך ,על-מנת שהיא תקרא לתוכו נתונים .כיצד תראה הגדרת הפונקציה? בפרט: האם את הפרמטר שלה נצטרך להגדיר כפרמטר הפניה? לא ,ולא! אנו יודעים כי עת מערך מועבר כפרמטר לפונקציה בכוחה של הפונקציה לשנות את ערכם של תאי המערך ,שכן הפונקציה למעשה מקבלת מצביע לתא מספר אפס במערך ,וכל השינויים שהפונקציה עורכת מתבצעים על המערך המקורי .נציג את הפונקציה ,read_dataנסבירה ,ואחר גם נציג את מצב המחסנית בעקבות הקריאה: { ) ][void read_data( struct Stud egypt ; int temp { )for (int i= 0; i < MAX_STUD; i++ ; cin >> setw(N) >> egypt[i]._name ; cin >> egypt[i]._shoe_num )for (int ex = 0; ex < MAX_EX; ex++ ; ]cin >> egypt[i]._grades[ex ; cin >> temp ; egypt[i]._gender = (temp == 1) ? FEMALE : MALE } } הסבר הפונקציה :הפונקציה מקבלת מערך של -Studים ,ולכן טיפוס הפרמטר שלה הוא ][ .struct Stud egyptהפונקציה מתנהלת כלולאה אשר קוראת נתונים בפקודה: למשל נדון המערך. תאי לתוך ; . cin >> setw(N) >> egypt[i]._nameהפרמטר egyptהוא מערך, לכן כדי לפנות לתא רצוי בו עלינו לכתוב ביטוי עם סוגריים מרובעים ,כדוגמת ] . egypt[iהתא ] egypt[iהוא מבנה מטיפוס ;Studכדי לפנות לחבר רצוי במבנה אנו משתמשים בצורת הכתיבה :שם-המבנה.שם-החבר ,ובמקרה שלנו: . egypt[i]._nameזהו מערך של תווים ,ולכן אנו יכולים לקרוא לתוכו מחרוזת. עתה נבחן את לולאת קריאת הציונים .בלולאה זאת המשתנה exעובר על מספרי התרגילים השונים .עת ערכו הוא ,למשל ,שלוש אנו קוראים ערך לתוך ] egypt[i]._grades[3נסביר egypt :הוא מערך ,ולכן כדי לפנות ַלתא של התלמיד מספר iעלינו לכתוב . egypt[i] :התא ] egypt[iהוא תא ְבמערך של מבנים ,ולכן משתנה מטיפוס ;Studלכן כדי לפנות לחבר gradesבמבנה עלינו לכתוב . egypt[i]._grades :החבר egypt[i]._gradesהוא מערך ,ולכן כדי לפנות לתא מספר שלוש בו עלינו לציין אינדקס רצוי: ]. egypt[i]._grades[3 עתה נבחן את מצב המחסנית .נתחיל במערך שהוגדר בתכנית הראשית: = egypt =name =gender =shoe =grades =name =gender =shoe =grades 42 =name =gender =shoe =grades המערך egyptכולל שלושה תאים .כל תא הוא מטיפוס ,struct Studולכן מכיל ארבעה חברים :החבר _nameהוא למעשה מערך ,אולם מכיוון שאנו חושבים עליו כעל מחרוזת לא ציירנו אותו כמערך; החברים _shoe_numו _gender -הם חברים פרימיטיביים ,וציירנו אותם כפי שאנו מציירים כל משתנה פרימיטיבי; החבר _gradesהוא מערך סטטי בן ארבעה תאים ,ולכן ציירנו את תאיו ,וכן מצביע משם החבר לתא מספר אפס במערך .אנו זוכרים כי egyptהוא למעשה מצביע לתא מספר אפס במערך המבנים ,ולכן לצד שם המשתנה ציירנו מצביע לתא הראשון במערך. עת מתבצעת קריאה לפונקציה ,read_dataאשר מקבלת את המערך ,egypt כלומר מצביע לתא מספר אפס של המערך ,נראית המחסנית באופן הבא: = egypt =name =gender =shoe =grades =name =gender =shoe =grades =name =gender =shoe =grades = egypt = i = ex אנו רואים כי מהפרמטר egyptשל הפונקציה נשלח מצביע לתא מספר אפס במערך של התכנית הראשית) .בדיוק כפי שנשלח מצביע מהמשתנה egyptשל התכנית הראשית לאותו מערך( .לכן עת הפונקציה פונה ל egypt[1]._shoe_num -היא מעדכנת ישירות את החבר _shoe_numבתא מספר אחד במערך המקורי )והיחיד הקיים(. עתה נניח כי ברצוננו לכתוב את הפונקציה read_dataבצורה שונה: { ) ][void read_data( struct tud egypt )for (int i= 0; i < MAX_STUD; i++ ; ) ]read_1_stud( egypt[i } כלומר הפונקציה read_dataקוראת בלולאה לפונקציה נוספת .read_1_stud הפונקציה הנוספת תקרא נתונים של תלמיד יחיד ,ולכן הפונקציה read_data מעבירה לפונקציה read_1_studכארגומנט תא יחיד במערך ,egyptכלומר מבנה יחיד מטיפוס .studעתה נשאל כיצד יראה הפרוטוטיפ של הפונקציה ?read_1_studהאם הפרמטר המועבר לה הוא פרמטר ערך או פרמטר הפניה? התשובה היא שעל-מנת שלפונקציה read_1_studיהיה את הכוח לשנות את ערכו של הארגומנט )מטיפוס (studהמועבר לה עליה לקבלו כפרמטר הפניה .שהרי הפונקציה read_1_studמקבלת מבנה יחיד ,וכבר מיצינו כי על-מנת שלפונקציה 43 המקבלת מבנה יהיה את הכוח לשנות את הארגומנט המועבר לה ,היא חייבת לקבל את המבנה כפרמטר הפניה .נציג ראשית את הפונקציה: { )void read_1_stud(sruct Stud &a_stud ; int temp ; cin >> a__stud.name ; cin >> a_stud._shoe_num )for (int ex = 0; ex < MAX_EX; ex++ ; ]cin >> a_stud._grades[ex ; cin >> temp ; a_stud._gender = (temp == 1) ? FEMALE : MALE } בקוד של הפונקציה אין רבותא .אך נציג את מצב המחסנית עת אנו קוראים לפונקציה באופן הבאread_1_stud(egypt[2]); : = egypt =name =gender =shoe =grades =name =gender =shoe =grades =name =gender =shoe =grades = egypt = i = ex = a_stud = temp = ex אנו רואים כי מהפרמטר a_studשל הפונקציה נשלח חץ )עם ראש משולש שחור( לתא מספר שתיים במערך של התכנית הראשית .ועל כן כל שינוי שהפונקציה מכניסה לפרמטר שלה מתחולל למעשה על התא המתאים במערך המקורי. מה היה קורה לו הפונקציה read_1_studהייתה מוגדרת באופן הבא: ; )void read_1_stud(struct Stud a_stud כלומר לוּ הפרמטר של הפונקציה היה פרמטר ערך? אזי הקריאה )] read_1_stud(egypt[2הייתה מעתיקה את תוכנו של התא מספר שתיים במערך egyptעל מבנה בשם a_studשהיה מוקצה ברשומת ההפעלה של . read_1_studהפרמטר a_studכבר לא היה מורה על התא מספר שתיים במערך המקורי .כל שינויי ש read_1_stud -הייתה מכניסה לפרמטר שלה היה מתחולל על העותק השמור ברשומת ההפעלה שלה במחסנית .כמובן שבתום ביצוע הפונקציה ערכו של ] egypt[2היה נותר כפי שהוא היה טרם הקריאה. 44 12.4מצביע ל struct -כפרמטר ערך את הפונקציה read_1_studאנו יכולים לכתוב גם בדרך שונה .במקום שהפונקציה תקבל פרמטר הפניה מטיפוס ,studהיא תקבל מצביע למבנה מטיפוס .studהאפקט שיתקבל מבחינה תכנותית יהיה אותו אפקט .את הפונקציה נכתוב באופן הבא: { )void read_1_stud( stud *stud_p ; int temp ; cin >> setw(N) >> (*stud_p)._name ; cin >> (*stud_p)._shoe_num )for (int ex = 0; ex < MAX_EX; ex++ ; ]cin >> (*stud_p)._grades[ex ; cin >> temp ; (*stud_p)._gender = (temp == 1) ? FEMALE : MALE } נסביר :עתה הפרמטר של הפונקציה מוגדר להיות ,sruct Stud *stud_p כלומר מצביע למבנה מטיפוס .Studעל כן בגוף הפונקציה ,כדי לפנות למבנה עליו stud_pמצביע עלינו להשתמש בכתיבה' .*stud_p :היצור' *stud_p :הוא מבנה ,ועל כן בו עלינו לפנות לחבר המתאים ,למשל. (*stup_p).gender : הקריאה לפונקציה בגרסתה הנוכחית תהיהread_1_stud( &(egypt[i])); : ,כלומר אנו מעבירים לפונקציה מצביע לתא מספר iבמערך. ציור המחסנית יישאר כפי שהוא היה בגרסה הקודמת פרט לשינוי יחיד :החץ )עם הראש השחור( שנשלח בגרסה הקודמת מהפרמטר a_studלתא המתאים במערך, מומר עתה במצביע )עם ראש בצורת (vאשר נשלח מאותו מקום ולאותו מקום. הסיבה לכך היא שעתה הפונקציה מקבלת מצביע )ולא פרמטר הפניה( .ובקריאה לפונקציה מעבירים בצורה מפורשת מצביע לתא המתאים )באמצעות הכתיבה …&(, ולא את התא המתאים כפרמטר הפניה )כפי שהיה בגרסה הראשונית(. מכיוון שבשפת Cמקובל מאוד להשתמש במצביע למבנה ,מאפשרת לנו השפה לכתוב במקום ביטוי כגון (*stup_p)._gender :ביטוי כדוגמת: במקום ראשית לפנות למבנה עליו מצביע stud_p . stup_p -> _genderכלומר ְ ) ,באמצעות (*stud_pושנית ַלחבר המתאים במבנה זה )באמצעות ,( (*stud_p)._genderאנו כותבים ישירותְ :פנֵה לחבר ַבמבנה עליו מצביע .stud_pלכן את הפונקציה שכתבנו לאחרונה נוכל לכתוב גם בצורת הכתיבה הבאה ,תוך שימוש בנוטציה שהצגנו זה עתה: { )void read_1_stud( struct Stud *stud_p ; int temp ; cin >> setw(N) >> stud_p-> _name ; cin >> stud_p-> _shoe_num )for (int ex = 0; ex < MAX_EX; ex++ ; ]cin >> stud_p-> _grades[ex ; cin >> temp ; stud_p-> _gender = (temp == 1) ? FEMALE : MALE } 45 12.5מצביע ל struct -כפרמטר הפניה במקום להגדיר מערך סטאטי , int a[N]; :אנו יכולים להגדיר בעבר ראינו כי ְ פוטנציאל למערך בדמות מצביע ,ובהמשך לממש את הפוטנציאל על-פי צרכי התכנית בעת ריצתה. מצב דומה חל עם מבנים ,ואין כל הבדל עקרוני בין מבנים למשתנים פרימיטיביים. נציג את הנושא עבור מבנים ,ולו בשם החזרה. נניח כי בתכנית הראשית הגדרנו struct Stud *egypt_p; :ועתה ברצוננו לקרוא לפונקציה alloc_arr_n_read_dataאשר תקצה מערך בגודל המתאים, ותקרא לתוכו נתונים .ניתן לכתוב את הפונקציה בדרכים שונות )למשל הפונקציה עשויה לקבל פרמטר הפניה מטיפוס מצביע ל ,(stud -אנו נציג את הגרסה הבאה, )בה הפונקציה מחזירה באמצעות פקודת returnמצביע למערך שהוקצה על-ידה(: { )(sruct Stud *alloc_arr_n_read_data ; sruct Stud *arr_p ; int size ; cin >> size ; ]arr_p = new stud[size +1 )if (arr_p == NULL ; )terminate(“Memory allocation error\n“, 1 // end of data sign ; )”“ strcpy(arr_p[size]._name, ; )read_data(arr_p, size ) return( arr_p } הקריאה לפונקציה תהיה. egypt_p = alloc_arr_n_read_data(); : הסבר הפונקציה :לפונקציה יש משתנה לוקלי . stud *arr_p :הפונקציה קוראת מהמשתמש את גודלו של המערך הדרוש לו ואחר מקצה מערך הכולל תא אחד יותר מהדרוש) .במידה וההקצאה נכשלת אנו עוצרים את ביצוע התכנית (.לתוך המרכיב _nameבתא האחרון שבמערך משימה הפונקציה את הסטרינג הריק ,וכך תדענה פונקציות אחרות לזהות את סוף המערך .אחר הפונקציה קוראת לפונקציה read_dataאשר תקרא נתונים לתאי המערך .הפונקציה read_dataדומה לפונקציה שכתבנו בעבר ,פרט לכך שאנו מעבירים לה גם את מספר התאים לתוכם read_dataאמורה לקרוא נתונים ) read_dataכפי שנכתבה בעבר קראה ערכים לתוך MAX_STUDתאים במערך(. לרצות הדוגמה האחרונה תשרת אותנו כדי להכיר מצב נוסף בו אנו עשויים ְ מצביע: הוגדר לאחרונה שראינו בתכנית במבנה: להשתמש , struct Stud *egypt_pואחר למצביע הוקצה מערך .את התא האחרון במקום במערך סימנו באמצעות שדה nameשכלל את הסטרינג הריק )”“(ְ . להשתמש בשיטת הסטרינג הריק לסימון סוף המערך נוכל לחשוב על אפשרות אחרת ,נקייה יותר .ראשית ,בנוסף למבנה ,studנגדיר מבנה: { struct Stud_arr ; struct Stud *_studs ; int _size ; } 46 כפי שאנו רואים ,מבנה מסוג Stud_arrמכיל שני מרכיבים :המרכיב _studs הוא פוטנציאל למערך של נתוני סטודנטים ,המרכיב _sizeמציין את גודלו של המערך עליו מצביע ._studs עתה נגדיר בתכנית הראשית משתנה .stud_arr egypt; :שימו לב כי איננו מגדירים מצביע כי אם מבנה )הכולל בתוכו מצביע( .נתאר את מצב המחסנית: = egypt = _studs = _size עתה נקרא לפונקציה alloc_arr_n_read_data2(egypt); :הפונקציה מקבלת את הארגומנט egyptכפרמטר הפניה .נבחן את הגדרת הפונקציה: { )void alloc_arr_n_read_data2(struct Stud_arr &egypt ; cin >> egypt._size ; ] egypt._studs = new stud[ egypt._size )if (egypt._size == NULL ; )terminate(“Memory allocation error\n“, 1 ; ) read_data2( egypt } הסבר :הפונקציה ראשית קוראת מהמשתמש את גודלו של המערך הדרוש לו לתוך החבר _sizeבפרמטר המשתנה .egyptאחר הפונקציה מקצה מערך ל- egypt._studsשהוא מרכיב מטיפוס * ) struct Studכפי שאנו רואים בהגדרת הטיפוס ,Stud_arrשהוא טיפוסו של הפרמטר . (egyptלבסוף הפונקציה מזמנת את . read_data2( egypt ); :נבחן את מצב הזיכרון עם הכניסה לפונקציה ) alloc_arr_n_read_data2וטרם שהיא הקצתה את המערך(: = egypt = studs = size = egypt 47 הסבר :הפונקציה מקבלת פרמטר הפניה בשם .egyptמכיוון שזה פרמטר הפניה אנו שולחים חץ מהפרמטר לארגומנט המתאים .לכן עת הפונקציה פונה ל- egypt._sizeאנו הולכים בעקבות החץ ,ומעדכנים את המרכיב _size בארגומנט שהועבר לפונקציה בקריאה לה .באפן דומה גם הקצאת זיכרון ַתפנה את המצביע studsבארגומנט של הפונקציה להצביע על המערך שהוקצה .נציג זאת בציור: = egypt =name =genger =shoe =grades =name =genger =shoe =grades =name =genger =shoe =grades = _studs _size = 3 = egypt מטרתה של הדוגמה האחרונה הייתה להראות כי לעיתים מבנה עשוי להכיל) :א( מערך כלשהו ,או מבנה אחר ,אשר שומרים את הנתונים בהם מעוניין המשתמש) ,ב( נתונים אודות המערך )או המבנה( כגון גודלו ,מתי הוא עודכן לאחרונה ,לאילו מתאי המערך יש להתייחס כריקים וכו'. 12.6תרגילים 12.6.1 תרגיל מספר אחד :פולינומים נחזור לתרגיל הפולינומים שהוצג בפרק שש .הפעם נייצג מונום באופן הבא: { struct monom ; float coef, degree ; } כלומר עבור כל מונום נשמור את המקדם והחזקה. פולינום ייוצג באופן הבא: { polynom ; monom *the_poly ; unsiged int num_of_monoms ; } כלומר נחזיק מערך של מונומים ,ושדה המציין כמה מונומים מרכיבים את הפולינום. סדרה של פולינומים תיוצג ,לפיכך ,כמערך של מבנים מטיפוס .polynomאת המערך נוכל להגדיר סטטית ַבאופן: ; ]polynom polynoms[MAX_POLY או שנקצה את המערך דינמית ,על הערמה ,ואז נוכל גם לשנות את גודלו במידת הצורך; אם נבחר באפשרות זאת נגדיר את המצביע: ; polynom *polynoms עתה כתבו שנית את תכנית הפולינומים כפי שתוארה בפרק שש ,תוך שאתם משתמשים במבנה הנתונים שתיארנו בפרק זה. 48 12.6.2 תרגיל מספר שתיים :סידור כרטיסים בתבנית רצויה המשחק לסדר תשעה קלפים ֵ במשחק בו על ְ בפרק שמונה הצגנו תרגיל אשר דן בתבנית רצויה .ראשית חיזרו לתיאור המשחק כפי שמופיע שם .עתה נרצה לכתוב שוב את התכנית הדרושה תוך שאנו עושים שימוש במבני נתונים חכמים יותר ,אשר יגבירו את קריאותה של התכנית. מבנה הנתונים הראשון שנגדיר ישמש לתיאור כל אחד מארבעת צדדיו של כל כרטיס: ; } enum color { RED, YELLOW, GREEN, BLUE { stuct side ; bool head_tail ; color a_color ; } כרטיס שלם מורכב מארבעה צדדים: ; } enum urdl { UP=0, RIGHT=1, DOWN=2, LEFT=3 { struct card ; ]side sides[4 ; urdl direction ; } הטיפוס urdlישמש אותנו הן כאינדקס למערך sidesכדי להגביר את הקריאות )] sides[UPהוא ביטוי קריא יותר מ ,(sides[0] -והן כדי לציין האם הכרטיס מוצב כאשר הצד המתואר כ UP -אכן למעלה ,או שמא הוא מסובב ימינה ,הפוך )כשצדו התחתון הוא שפונה כלפי מעלה( ,או שמאלה. מאגר הכרטיסים אותם יש לסדר במהלך המשחק יתואר באופן הבא: ; ]card cards[9 ולתוכו יהיה על התכנית לקרוא את הנתונים מהמשתמש. לבסוף ,על התכנית לסדר את תשעת הכרטיסים בתבנית הרצויה )תוך שימוש באלגוריתם שתואר בפרק שמונה( ,ואת הסידור הנ"ל יתאר המערך: ; ]unsigned int arrange[3][3 כל תא במערך יכיל אינדקס של כרטיס במערך .cards 12.6.3 תרגיל מספר שלוש :בעיית מסעי הפרש נחזור לבעיית מסעי הפרש ,כפי שהוצגה בפרק שמונה )תרגיל .(8.7.10להזכירכם, המשימה אותה היה עלינו לממש היא מלוי לוח שחמט בגודל NxNבצעדי פרש ,תוך שהפרש מבקר פעם אחת בדיוק בכל משבצת בלוח. הפעם נרצה להשלים את המשימה תוך שימוש במבנים .נגדיר על-כן את המבנה: ונגדיר את המשתנה: { stuct square ; bool visited ; unsigned int next_x, next_y ; } ;] .square board[N][Nמשתנה זה ייצג את הלוח. התכנית הראשית תאתחל את המשתנה כך שערכי השדה visitedבכל תאי המערך יכילו את הערך .false 49 אחר ,התכנית הראשית תקרא מהמשתמש את מקומה של המשבצת ממנה על הפרש ַ להתחיל את מסעו. הפונקציה הרקורסיבית שתהווה את לב ליבה של התכנית תמלא את הלוח בצעדי הפרש תוך שהיא מסמנת בכל תא בו הפרש מבקר ,בשדה ,visitedכי הפרש ביקר בתא זה .השדות next_x, next_yיתארו את המשבצת אליה התקדם הפרש מהמשבצת הנוכחית. בתום תהליך החיפוש הציגו פלט שיכלול שני מרכיבים: א .רישום של סדרת המשבצות בהן ביקר הפרש ,בסדר בו בוקרו המשבצות השונות. ב .תיאור של הלוח )כטבלה דו-ממדית( ,כאשר עבור כל משבצת מופיע מספר הצעד בו בוקרה משבצת זאת. 12.6.4 תרגיל מספר ארבע :נתוני משפחות בתרגיל זה נחזיק מסד נתונים הכולל נתוני משפחות. כל משפחה בתרגיל תהיה משפחה גרעינית הכוללת לכל היותר אם ,אב ומספר כלשהו של ילדים .נתוני משפחה יחידה ישמרו במבנה מהטיפוס: struct family { ; char *family_name char *mother_name, *father_name ; // parents' names ; child *children_list // list of the children in this family ; unsigned child_list_size // size of child_list array ;} נתוני כל ילד ישמרו ב: struct child { ; char *child_name ; bool sex ;} מבנה הנתונים המרכזי של התכנית יהיהfamily *families : התכנית תאפשר למשתמש לבצע את הפעולות הבאות: .1הזנת נתוני משפחה נוספת .לשם כך יזין המשתמש את שם המשפחה )וזהו מרכיב חובה( ,ואת שמות בני המשפחה .במידה והמשפחה אינה כוללת אם יזין המשתמש במקום שם האם את התו מקף )) (-המרכיב mother_nameבמבנה המתאים יקבע אז ,כמובן ,להיות ,(NULLבאופן דומה יצוין שאין אב במשפחה. לפני הזנת נתוני הילדים יוזן מספר הילדים במשפחה .התכנית תקצה מערך הכולל שני תאים נוספים ,מעבר למספר הילדים הנוכחי במשפחה ,וזאת לשימוש עתידי .שמות הילדים בתאים הריקים ייקבעו להיות NULLוכך נסמן כי התאים ריקים .תיתכן משפחה ללא ילדים .לא תיתכן משפחה בה אין לא אם ולא אב )יש לתת הודעת שגיאה במידה וזה הקלט מוזן ,ולחזור על תהליך קריאת שמות ההורים( .אתם רשאים להגדיר משתנה סטטי יחיד מסוג מערך של תווים לתוכו תקראו כל סטרינג מהקלט .את רשימת המשפחות יש להחזיק ממוינת על-פי שם משפחה .את רשימת ילדי המשפחה החזיקו בסדר בה הילדים מוזנים .הניחו כי שם המשפחה הוא מפתח קבוצת המשפחות ,כלומר לא תתכנה שתי משפחות 50 להן אותו שם משפחה )משמע ניסיון להוסיף משפחה בשם כהן ,עת במאגר כבר קיימת משפחת כהן מהווה שגיאה( .אין צורך לבדוק כי לא מוזנים באותה משפחה שני ילדים בעלי אותו שם .במידה ומערך המשפחות התמלא יש להגדילו. דוגמה לאופן קריאת הנתונים: Enter family name: Cohen Enter father name: Yosi Enter mother name:Enter children's names: Dani Dana - .2 .3 .4 .5 .6 .7 הוספת ילד למשפחה .יוזן שם המשפחה ושמו של הילד .הילד יוסף בסוף רשימת הילדים .במידה ומשפחה בשם הנ"ל אינה קיימת יש להודיע על-כך למשתמש )ולחזור לתפריט הראשי( .במידה ומערך הילדים מלא יש להגדילו בשיעור שלושה תאים נוספים. פטירת בן משפחה .יש להזין) :א( את שם המשפחה) ,ב( האם מדובר באם )ולשם כך יוזן הסטרינג ,(motherבאב )יוזן ,(fatherאו באחד הילדים )יוזן ) (childג( במידה ומדובר בילד יוזן גם שם הילד שנפטר .במידה ואין במאגר משפחה כמצוין ,או שבמשפחה אין בן-משפחה כמתואר )למשל אין אב והתבקשתם לעדכן פטירת אב ,או אין ילד בשם שהוזן( יש לדווח על שגיאה )ולחזור לתפריט הראשי( .כמו כן לא ניתן לדווח על פטירת הורה במשפחה חד-הורית )ראשית יש להשיא את המשפחה למשפחה חד-הורית אחרת ,כפי שמתואר בהמשך ,ורק אז יוכל ההורה המתאים לעבור לעולם שכולו טוב(. נשואי שתי משפחות קיימות .יש לציין את שמות המשפחות הנישאות ,ואת שם המשפחה החדש שתישא המשפחה המאוחדת .פעולה זאת לגיטימית רק אם אחת משתי המשפחות אינה כוללת אם בעוד השניה אינה כוללת אב .בעקבות ביצוע הפעולה תאוחדנה רשימות הילדים והתא שהחזיק את נתוני אחת משתי המשפחות ייחשב כריק ,ולכן תוזזנה הרשומות בהמשך המערך ,כדי לצמצם הפער. הצגת נתוני המשפחות בסדר עולה של שמות משפחה) .זהו ,כזכור ,הסדר בו מוחזקת רשימת המשפחות( .עבור כל משפחה ומשפחה יש להציג את שם המשפחה ,ואת שמות בני המשפחה. הצגת נתוני משפחה בודדת רצויה .יש להזין את שם המשפחה ויוצגו פרטי בני המשפחה) .במידה והמאגר אינו כולל משפחה כמבוקש תוצג הודעת שגיאה(. הצגת נתוני המשפחות בסדר יורד של מספר בני המשפחה .יש להציג את שם המשפחה ואת מספר בני המשפחה .לשם סעיף זה החזיקו מערך נוסף .כל תא במערך יהיה מטיפוס: struct family_size { unsigned the_family ; // index of a family in the families array ; int size ; // number of children in this family ;} המצביע למערך זה יהיה .family_size *families_sizesהמערך ימוין בסדר יורד של המרכיב .sizeעל-ידי סריקה של מערך זה ופניה ממנו בכל פעם למשפחה המתאימה )תוך שימוש באינדקס (the_familyתוכלו להציג את המשפחות בסדר יורד של גודל המשפחות .שימו לב כי יש גם לתחזק רשימה זאת .בין כל המשפחות להן אותו גודל אין חשיבות לסדר ההצגה .8הצגת נתוני שכיחות שמות ילדים .יש להציג עבור כל שם ילד המופיע במאגר, כמה פעמים הוא מופיע במאגר .אין צורך להציג את הרשימה ממוינת. )רמז\הצעה :סרקו את מאגר המשפחות משפחה אחר משפחה ,עבור כל משפחה עיברו ילד אחר ילד ,במידה ושמו של הילד הנוכחי עדיין לא מופיע ברשימת 51 הילדים ששמם הודפס סיפרו כמה פעמים מופיע שם הילד במאגר הנתונים, ואחר הוסיפו את שמו של הילד לרשימת שמות הילדים שהוצגו(. .9סיום .בשלב זב עליכם לשחרר את כל הזיכרון שהוקצה על-ידכם דינמית. הערות כלליות: א .התכנית תנהל כלולאה בה בכל שלב המשתמש בוחר בפעולה הרצויה לו ,הפעולה מתבצעת )או שמוצגת הודעת שגיאה( ,והתכנית חוזרת לתפריט הראשי. ב .הקפידו שלא לשכפל קוד ,למשל כתבו פונקציה יחידה אשר מציגה נתוני משפחה ,והשתמשו בה במקומות השונים בהם הדבר נדרש .וכו'. Needless to sayשיש להקפיד על כללי התכנות המקובלים ,בפרט ובמיוחד מודולריות ,פרמטרים מסוגים מתאימים )פרמטרי ערך vsפרמטרי הפניה( ,ערכי החזרה של פונקציות ושאר ירקות .תכנית רצה אינה בהכרח גם תכנית טובה! )קל וחומר לגבי תכנית מקרטעת(… תרגיל מספר חמש :סימולציה של רשימה משורשרת 12.6.5 נתונות ההגדרות הבאות: ; … = const int N { struct node ; char ch ; unsigned int next ; } { struct linked_list ; ]node list[N ; unsigned int head ; } משתנה מטיפוס linked_listמכיל שני מרכיבים: הפניה לתא נוסף במערך .דוגמה אפשרית א .מערך listהמכיל בכל תא תו ,וכן ְ למרכיב listברשומה כלשהי: d i a n a o x y x s 7 #9 17 #8 6 #7 5 #6 1#5 0 #4 1 #3 4 #2 3 #1 8 #0 ב .מספר של תא כלשהו במערך ,השמור בשדה .head מספר מחרוזות ,את כתובת ההתחלה של נוכל להתייחס למרכיב המערך כמכיל ְ אחת מהן מחזיק המרכיב .headלדוגמה :אם ערכו של headהוא ,2אזי המחרוזת בה מתמקדים מורכבת מהתווים הבאים :התו בתא מספר שתיים במערך ) ,(yמתו זה אנו מתקדמים על פי השדה nextלתא מספר ארבע במערך ,בו שמור התו ,o משם לתא מספר אפס המכיל את ,sומשם לתא מספר שמונה המכיל את .iמכיוון שבתא מספר שמונה מצוי ערך שאינו בתחום 0..N-1אנו מסיקים כי בכך תם הסטרינג ,ולפיכך הסטרינג בו עסקינן הוא .yosiבאופן דומה בתא מספר תשע במערך מתחיל הסטרינג .dana לעומת זאת בתא מספר שלוש במערך אנו מוצאים ַה ְפניה לתא מספר חמש ,בתא מספר חמש קיימת הפניה לתא מספר שלוש ,וחוזר חלילה .אנו אומרים כי סטרינג זה אינו נגמר ,ולכן הוא בגדר שגיאה. 52 כתבו פונקציה המקבלת מבנה מסוג linked_listומציגה את הסטרינג שעל התו הראשון שלו מורה המרכיב headאלא אם הסטרינג הינו שגוי ,ואז תוצג הודעת שגיאה ,ולא יוצג כל תו מהסטרינג. 53 1/2011עודכן 13רשימות מקושרות )משורשרות( בפרקים הקודמים ראינו כיצד יש ביכולתנו לשמור סדרה של נתונים במערך שמוקצה באופן סטטי או בכזה שמוקצה באופן דינמי .בפרק זה ,ובבא אחריו ,נכיר דרך אחרת לשמירת סדרת נתונים. 13.1בניית רשימה מקושרת נניח כי בתכנית כשלהי עלינו לשמור כמות שאינה ידועה מראש של מספרים שלמים. כמו כן לעיתים נרצה להוסיף נתונים לרשימה ,ולעיתים נרצה להסיר מהרשימה נתונים שאין לנו בהם עוד צורך .נוכל נקוט לשם כך בפתרון הבא :נגדיר בתכנית את המבנה: { struct Node ; int _data ; struct Node *_next ; } מהביצה, ִ מה קיבלנו? כמו הברון מנכאוזן שמשך את עצמו בשערות ראשו וכך נחלץ כך אנו שקענו בביצה שעה שהגדרנו מבנה בשם Nodeאשר אחד ממרכיביו הוא מצביע ַלמבנה .Nodeמייד נראה לאן תובילנו הגדרה מפתיעה ,אך לגיטימית זאת. עתה נניח כי בתכנית הגדרנו , struct Node *head = NULL, *temp; :וכן משתנה שלם .numנציג את מצב המחסנית: = head = temp = num בגוף התכנית נכלול את הלולאה הבאה: ; cin >> num { )while (num != 0 ; temp = new (std::nothrow) struct Node )if (temp == NULL ; )terminate(“cannot alloc mem”,1 ; temp -> _data = num ; temp -> _next = head ; head = temp ; cin >> num } נניח כי המשתמש מזין את הקלט 17) 0 3 3879 17 :מוזן ראשון 0 ,מוזן אחרון(. נעקוב אחר התנהלות התכנית :אחרי קריאת הערך 17ל num -מוקצית על-גבי הערמה קופסה יחידה מסוג ,Nodeו temp -עובר להצביע עליה) .כאשר אנו כותבים מבין ,new (std::nothrow) Nodeואיננו מציינים כמה תאים יש להקצותִ , המחשב כי ברצוננו להקצות תא יחיד( .לתוך המרכיב _dataבקופסה עליה מצביע tempמוכנס הערך ,17ולתוך המרכיב ) _nextשהינו מצביע לקופסות מסוג (Node מוכנס הערך המצוי ב ,head -כלומר הערך . NULLנציג את מצב המחסנית אחרי 54 ביצוע הפעולות הללו )ולפני ביצוע ההשמה .(head = temp; :מעתה נניח כי כל מה שלא מופיע בתוך המסגרת המציינת את המחסנית ,מוקצה על הערמה. = head = temp = num =data 17 =next מה תעשה ,עתה ,ההשמה ? head = temp; :היא תשלח את headלהצביע על אותה קופסה כמו .tempנציג זאת על הזיכרון: = head = temp = num =data 17 =next בכך מסתיים הסיבוב הראשון של הלולאה .אני מסב את תשומת לבכם לנקודה חשובה :המצביע בקופסה של ,17קיבל את ערכו של headעת ערכו של האחרון היה ,NULLולכן ערכו הוא .NULLהלולאה ממשיכה להתנהל .נפנה עתה למעקב אחר ביצוע הסיבוב השני בה .בסיבוב זה numמקבל את הערך .3879הפקודה = temp ; new (std::nothrow) Nodeיוצרת קופסה חדשה על הערמה ,ומפנה את tempלהצביע על קופסה זאת ) ִב ְמקום על הקופסה עליה הוא הצביע בעבר ,ושעתה רק headמצביע עליה( .למרכיב ה _data -בקופסה עליה מצביע tempאנו ולמרכיב _nextאנו משימים את הערך השמור ב,head - משימים את הערך ַ ,3879 על מה מצביע ?headעל הקופסה של ,17לכן גם המצביע _ nextבקופסה עליה מצביע ,tempיצביע על הקופסה של .17נציג את מצב המחסנית: = head = temp = num =data 17 =next =data 3879 =next הפקודה האחרונה בסיבוב זה של הלולאה הינה head = temp; :והיא מפנה את headלהצביע על אותה קופסה כמו ,tempכלומר על הקופסה של .3879מצב המחסנית הינו: = head = temp = num =data 17 =next =data 3879 =next עתה אנו פונים לסיבוב השלישי בלולאה .בסיבוב זה אנו קוראים את הערך 3לתוך המשתנה .numשוב אנו מקצים קופסה חדשה ,ושולחים את tempלהצביע עליה. שוב אנו מכניסים ל ,temp-> _data -כלומר למרכיב ה data_ -בקופסה עליה מצביע ,tempאת הערך שב .num -ושוב אנו שולחים את ,temp-> _nextכלומר את המצביע )שטיפוסו * (struct Nodeבקופסה עליה מצביע tempלהצביע כמו ) headשגם טיפוסו הוא * ;(struct Nodeהפעם הדבר גורם לtemp-> - _nextלהצביע על הקופסה של .3879נציג זאת בציור: = head = temp = num =data 17 =next =data 3879 data= 3 =next =next 55 הפקודה האחרונה בגוף הלולאה היא head = temp; :והיא מפנה את head להצביע על אותה קופסה כמו ,tempכלומר על הקופסה של .3נציג זאת: = head = temp = num =data 17 =next =data 3879 data= 3 =next =next בסיבוב הבא בלולאה אנו קוראים את הערך אפס ,ובזאת מסיימים את ביצוע הלולאה. מה קיבלנו? המצביע headמצביע על הקופסה של הנתון שהוזן אחרון )שלוש(, המצביע בקופסה זאת מצביע על הקופסה הכוללת את הנתון שהוזן לפני אחרון ) ;(3879והמצביע בקופסה של 3879מורה על הקופסה של הנתון שהוזן ראשון ).(17 ולבסוף ,ערכו של המצביע בקופסה האחרונה הוא .NULL קטע הקוד שבנה את הרשימה המקושרת כולל קריאה לפונקציה .terminate פונקציה זו אינה חלק מהשפה ,אלא עלינו לממשה .הייתי ממליץ לכם לכלול פונקציה זאת בכל תכנית שמקצה זיכרון: { )void terminate(const char *msg, int err_code ; cout << msg << endl ; ) exit( err_code } הסבר הפונקציה :הפונקציה מקבלת סטרינג ,וערך שלם .היא מציגה את הסטרינג על המסך ,ואחר קוטעת את ביצוע התכנית ,תוך החזרת הערך השלם המועבר לה. אני מזכיר לכם כי אם ברצונכם לכלול בתכניתכם את הפקודה ,exitאזי עליכם לכלול בתכנית הוראת includeלקובץ . stdlib.h מתכנתים מתחילים מוטרדים לעיתים מכך שהנתונים מופיעים ברשימה בסדר הפוך לסדר הזנתם .מספר מענים יש לנו על-כך: א .בקטע תכנית זה אנו מניחים כי מבחינת המשתמש אין חשיבות לסדר בו מוחזקים הנתונים שבין כה וכה אינם ממוינים. ב .אני מזמין אתכם לבנות את הרשימה כך שהנתונים ישמרו בה בסדר בו הם הוזנו .כדי לעשות זאת החזיקו מצביע נוסף struct Node *last; :אשר נק ֵצה קופסה יצביע כל פעם לתא האחרון ברשימה .כל פעם שנקרא נתון נוסף ְ חדשה עליה יצביע בתחילה .tempאחר נַפנה את last->_nextלהצביע כמו .tempולבסוף נקדם את lastלהצביע על הקופסה שנוספה אך זה עתה לקצה הרשימה .כיצד יאותחל ?lastוכיצד יטופל ?headזאת אני משאיר לכם. ג .בהמשך נראה כיצד ניתן לבנות את הרשימה המקושרת כך שהנתונים יוחזקו בה ממוינים. 13.2הצגת הנתונים השמורים ברשימה מקושרת לשם הפשטות לא הצגנו את בניית הרשימה המקושרת בפונקציה נפרדת .אולם מעתה נבצע כל פעולה על הרשימה בפונקציה נפרדת )כפי שראוי היה לעשות גם עם תהליך בניית הרשימה( .עתה נכתוב פונקציה אשר מציגה את הנתונים השמורים 56 ברשימה .הפונקציה תזומן באופן הבא . display_list(head); :נציג את הפונקציה: { )void display_list(const struct Node *run { )while (run != NULL ; " " << cout << run-> _data ; run = run-> _next } } הסבר :הפונקציה מתנהלת כלולאה אשר מתבצעת כל עוד ) . (run != NULLאנו רשאים לנסח תנאי זה שכן עת בנינו את הרשימה דאגנו לכך שערכו של המצביע _nextבקופסה האחרונה יהיה ,NULLולכן הלולאה תעצור בדיוק במקום המתאים .בכל סיבוב בלולאה אנו מציגים את ערכו של המרכיב _dataבקופסה עליה מצביע ,runואחר מקדמים את runלאיבר הבא ברשימה באמצעות הפקודה: ; . run = run -> _nextנציג סימולציית הרצה שתסייע לנו להשתכנע בנכונות הפונקציה ,ולהטמיע את השימוש ברשימה מקושרת :מצב הזיכרון עם הקריאה לפונקציה הוא כדלהלן: = head = temp = num =data 17 =next =data 3879 data= 3 =next =next = run הסבר :הפרמטר runהוא פרמטר ערך ,על-כן ערכו של headמועתק על ,run ומבחינה ציורית המצביע runמצביע לאותו מקום כמו .headמכיוון ש run -הוא פרמטר ערך אזי שינויים שנערוך עליו במהלך ביצוע הפונקציה לא ישנו את ערכו של headשל התכנית הראשית; headימשיך להצביע לראש הרשימה שבנינו. עתה אנו מתחילים לבצע את הפונקציה .התנאי בלולאה מתקיים ) runמצביע על קופסה נחמדה ויפה ,וערכו שונה מ ,(NULL -ולכן אנו נכנסים לסיבוב ראשון בלולאה .אנו מציגים את ,run-> _dataכלומר את ערכו של שדה הdata_ - בקופסה עליה מצביע ,runמשמע את הערך .3אחר אנו מבצעים את הפקודה: . run = run-> _nextנסביר פקודה זאת :ראשית נבחן את אגף ימין של ההשמה run .הוא מצביע run-> ,היא הקופסה עליה runמצביע ,כלומר הקופסה של run-> _next ,3הוא השדה nextבקופסה עליה runמצביע ,כלומר המצביע אשר מורה על הקופסה של .3879את ערכו של מצביע זה אנו מכניסים ל ,run -ולכן עתה גם runמצביע לקופסה של .3879בזאת הסתיים הסיבוב הראשון בלולאה. נציג את מצב הזיכרון: = head = temp = num =data 17 =next =data 3879 data= 3 =next =next = run לפני כניסה לסיבוב השני בלולאה אנו בודקים את תנאי הלולאה .התנאי עדיין מתקיים run ,מצביע ,כאמור ,לקופסה של ,3879וערכו אינו NULLכלל וכלל .אנו 57 נכנסים לסיבוב שני בלולאה .בסיבוב זה אנו שוב מציגים את ,run-> _data כלומר את מרכיב ה _data -בקופסה עליה מצביע ,runמשמע את הערך .3879 אחר אנו מבצעים שוב את הפקודה . run = run-> _next; :נבחן את האפקט של פקודה זאת :כמו קודם נתחיל בבחינת אגף ימין של ההשמה run :הוא מצביע, > run-היא הקופסה עליה runמצביע run-> _next ,הוא המרכיב _next בקופסה עליה runמצביע .מרכיב זה הוא מצביע ,והוא מורה על הקופסה של .17 לכן אם אנו מפנים את runלהצביע כמו run-> _nextאנו שולחים גם את run להצביע על הקופסה של .17בזאת סיימנו את הסיבוב השני בלולאה .לקראת כניסה לסיבוב השלישי אנו שוב בודקים את התנאי שבראש הלולאה; והוא עדיין מתקיים. אנו נכנסים לסיבוב שלישי בלולאה .שוב אנו מציגים את ,run-> _dataכלומר את הערך ,17ושוב אנו מקדמים את runבאמצעות הפקודה: ; . run = run-> _nextנבחן פקודה זאת )בפעם האחרונה ,אני מבטיח(run : הוא מצביע run-> ,היא הקופסה עליה runמצביע ,כלומר הקופסה של .17 run-> _nextהוא מרכיב ה _next -בקופסה עליה runמצביע; וערכו של מרכיב זה הוא .NULLלכן עת אנו משימים ל run -את ערכו של run-> _nextאנו מכניסים ל run -את הערך .NULLלקראת סיבוב נוסף בלולאה אנו בודקים שוב את התנאי ) .(run != NULLהפעם תנאי זה לא מתקיים ,וטוב שכך .איננו נכנסים לסיבוב נוסף בלולאה ,ומכיוון שהפונקציה אינה כוללת דבר פרט ללולאה ,אנו מסיימים בזאת את ביצוע הפונקציה. 13.3בניית רשימה מקושרת ממוינת בעבר ראינו כיצד בונים רשימה מקושרת בה כל איבר חדש נוסף בראש הרשימה. עתה נרצה לבנות רשימה מקושרת ממוינת )מקטן לגדול( ,בה כל איבר חדש נוסף לרשימה ַבמקום המתאים ,כך שבעקבות כל הוספה הרשימה נשארת ממוינת. נניח כי בתכנית הוגדר המבנה ,struct Nodeובתכנית הראשית הוגדר: ;struct Node *head = NULL עתה ברצוננו לקרוא לפונקציה . build_list(head); :פונקציה זאת תבנה את הרשימה ה מקושרת הממוינת .הפונקציה תקבל כארגומנט את .head נתחיל בכך שנשאל את עצמנו האם הפונקציה צריכה לקבל את headכפרמטר ערך או כפרמטר הפניה? התשובה היא שעל הפונקציה לבנות רשימה מקושרת שעל ראשה יצביע הארגומנט המועבר לה ,ובמקרה הנוכחי המשתנה headשל .main ערכו של headהוא עתה ,NULLכלומר על הפונקציה לשנות את ערכו של ,head ולכן הפרמטר של פונקציה צריך להיות פרמטר הפניה. נציג את הגדרת הפונקציה: { )void build_list(struct Node *&head ; int num ; cin >> num { )while (num != 0 ; )insert(num, head ; cin >> num } } 58 הפונקציה שכתבנו מתנהלת כלולאה ,בה בכל פעם אנו קוראים נתון חדש )עד קליטת אפס( ,ואז קוראים לפונקציה insertאשר מכניסה את הנתון החדש לרשימה .נעיר כי בתכניות המטפלות במבני נתונים שונים )כדוגמת רשימות משורשרות כאלה או אחרות( מקובל לכתוב פונקציה ייעודית להוספת נתון נוסף )שהתקבל ממקור כזה או אחר( למבנה הנתונים. לפני שאנו פונים לפונ' insertנעיר שאת הפונ' build_lustאפשר ,וגם עדיף לכתוב כך שהיא תחזיר מצביע לראש הרשימה המקושרת הנבנית על-ידה )ולא תשתנה את ערכו של פרמטר ההפניה המועבר לה( .שהרי build_listמחזירה לנו ערך יחיד ,וכבר מיצינו שעת פונ' מחזירה ערך יחיד ,ראוי דהוא יוחזר באמצעות ערך החזרה .על כן ראוי לכתוב את הפונ' באופן הבא: { )(struct Node *build_list ; struct Node *head = NULL ; int num ; cin >> num { )while (num != 0 ; )insert(num, head ; cin >> num } ; return head } זימון הפונ' ,בגירסתה נוכחית ,מהתכנית הראשית יהיה: ; )(head = build_list 59 נפנה עתה לדיון בפונ' .insertלפונקציה insertשני פרמטרים :הראשון הוא הנתון שיש להוסיף לרשימה המקושרת הממוינת ,השני הוא מצביע לראש הרשימה. נציג את הגדרת הפונקציה ואחר נדון בה בהרחבה: { )void insert(int num, struct Node *&head ; Node *front, *rear, *temp {)if (head == NULL // insert first item ; head = new (std::nothrow) struct Node )if (head == NULL ;)terminate("can not alloc mem", 1 ; head -> _data = num ; head -> _next = NULL // end of list sign } { )else if (num < head -> _data // insert before temp = new (std::nothrow) Node ; // current first )if (head == NULL ;)terminate("can not alloc mem", 1 ; temp -> _data = num ; temp -> _next = head ; head = temp } { else ; rear = head ; front = rear -> _next { )while (front != NULL && front-> _data < num ; rear = front ; front = front -> _next } ; temp = new (std::nothrow) struct Node )if (temp == NULL ;)terminate("can not alloc mem", 1 ; temp -> _data = num ; temp -> _next = front ; rear -> _next = temp } } עת אנו כותבים פונקציה המטפלת ברשימה אנו מבחינים בין מספר מקרים שונים, במילים אחרות בין מספר מצבים שונים .חובתנו לדאוג לכך שהפונקציה שנכתוב תטפל בכל המצבים הללו: א .הרשימה המועברת ריקהַ .בפונקציה insertמשמעותו של מקרה זה היא כי יש להוסיף לרשימה איבר ראשון. ב .יש לשנות את ראש הרשימה .בפונקציה insertמצב זה חל עת יש להוסיף נתון בראש הרשימה. ג .יש לערוך שינוי אי שם באמצע רשימה קיימת .בפונקציה insertמצב זה חל עת יש להוסיף איבר נוסף באמצע הרשימה. ד .יש לערוך שינוי באיבר האחרון ברשימה .בפונקציה insertמקרה זה מכוסה על-ידי הטיפול המתבצע עבור המקרה מסעיף ג' .בפונקציות אחרות יש להכניס טיפול מיוחד גם למקרה שכזה. 60 כדי להשתכנע שהפונקציה שלנו פועלת כהלכה נעקוב אחר ריצת התכנית עבור הקלט 17) 0 3879 15 3 13 17 :מוזן ראשון 0מוזן אחרון( .נסמלץ את הריצה של build_listבגירסתה הראשונה ,הפחות מוצלחת ,זו בה היא מקבלת פרמטר הפניה. בתחילה ,מוגדר בתכנית הראשית המשתנה. struct Node *head = NULL; : נציג את מצב המחסנית: = head עתה נערכת קריאה לפונקציה ,build_listאשר מקבלת את headכפרמטר הפניה .מצב המחסנית הוא: = head = head = num אני מזכיר לכם כי פרמטר הפניה אנו מציירים באמצעות חץ עם ראש משולש שחור הנשלח לכיוון הארגומנט המתאים; בניגוד לכך מצביע מצוייר כחץ עם ראש בצורת ,vהנשלח לכיוון האובייקט עליו המצביע מורה )במילים אחרות ,האובייקט שאת כתובתו המצביע מכיל(. הפונקציה build_listמתחילה להתבצע .היא קוראת את הערך 17לתוך המשתנה הלוקלי שלה ,numומזמנת את . insertל insert -יש שני פרמטרים: פרמטר ערך שלם ,לתוכו מועתק ערכו של הארגומנט , numופרמטר הפניה שהינו מצביע ,ועל-כן ממנו נשלח חץ לארגומנט headשל ) build_listומשם למצביע headשל .(mainנציג את מצב המחסנית: = head = head num = 17 = head num = 17 = temp = rear = front insertמתחילה להתבצע .ראשית אנו בודקים . if (head == NULL) :בכך אנו מטפלים במקרה הראשון מבין ארבעת המקרים בהם ציינו כי עלינו לטפל: המקרה שבו הרשימה ריקה .במצב הנוכחי הרשימה אכן ריקה .על כן אנו מבצעים את הקוד הבא: ; head = new new (std::nothrow) struct Node ; head -> _data = num ; head -> _next = NULL אנו מקצים קופסה חדשה ,ושולחים את המצביע headשל התכנית הראשית להצביע על קופסה זאת) .אנו מגיעים למשתנה headשל התכנית הראשית תוך שימוש בחצים עם הראש השחור המובילים מהפרמטרים המשתנים headשל שתי 61 הפונקציות הנוספות אל המשתנה headשל התכנית הראשית( .למרכיב ה_data - בקופסה שנוצרה אנו מכניסים את הערך ,17ולמרכיב ה _next -את הערך .NULL מצב הזיכרון הוא עתה: = head = head num = 17 data=17 =next = head num = 17 = temp = rear = front עתה insertמסתיימת ,ורשומת ההפעלה שלה מוסרת מעל-גבי המחסנית .אנו שבים ל ,build_list -קוראים את הערך 13לתוך המשתנה ,numומזמנים שוב נראה בשרטוט האחרון פרט לכך את .insertמצב הזיכרון יראה כפי שהוא ֵ שבמשתנה numשל ,build_listובפרמטר numשל insertמצוי עתה הערך ,13 )במקום 17כפי שמופיע בציור האחרון(. insertמתחילה להתבצע .התנאי ) (head == NULLאינו מתקיים; לעומת זאת התנאי ) (num < head-> _dataכן מתקיים .שימו לב כי אנו רשאים לבדוק תנאי זה רק משום שאנו מובטחים שערכו של headשונה מ ,NULL -כלומר ש- headמצביע על קופסה כלשהי )הכוללת בין היתר את המרכיב _ .(dataאנו מובטחים שערכו של headשונה מ NULL -שכן לו ערכו היה NULLהתנאי הקודם היה מתקיים ,ולא היינו מגיעים עד הלום) .אני מדגיש נקודה זאת שכן היא עקרונית וחשובה :אם pהוא מצביע אזי מותר להתעניין בערכו של …> p-רק אחרי שהבטחנו ש p -מצביע על קופסה קיימת כלשהי (.מכיוון שהתנאי >(num < head- ) _dataמתקיים אנו מבצעים את הגוש: ; = new (std::nothrow) struct Node ; -> _data = num ; -> _next = head ; = temp temp temp temp head שתי הפקודות הראשונות מקצות קופסה חדשה ,ומכניסות למרכיב ה_data - בקופסה את הערך .13מצב הזיכרון הוא: = head = head num = 17 data=17 =next data=13 =next 62 = head num = 17 = temp = rear = front הפקודה השלישית מבין ארבע הפקודות שאנו מבצעים ְ מפנה את המצביע _next בקופסה עליה מצביע tempלהצביע לאותו מקום כמו ,headכלומר לקופסה של .17מצב הזיכרון הוא: = head = head num = 17 data=17 =next data=13 =next = head num = 17 = temp = rear = front הפקודה הרביעית מפנה את headלהצביע לאותו מקום כמו ,tempכלומר לקופסה של .13מצב הזיכרון הוא: = head = head num = 17 data=17 =next data=13 =next = head num = 17 = temp = rear = front בכך מסתיימת .insertמה היה לנו? headמצביע על הקופסה של ,13המצביע בקופסה זאת מצביע על הקופסה של .17וערכו של המצביע בקופסה של 17הוא .NULLהרשימה שלנו מוחזקת ממוינת כנדרש .שימו לב כי הפעם טיפלנו במקרה בו יש להוסיף איבר בראש הרשימה. עם סיומה של insertחוזר הביצוע ל .build_list -הערך שנקרא עתה הוא .3 לא נעקוב אחר תהליך הוספתו לרשימה שכן מקרה זה דומה לקודמו :גם בו אנו מוסיפים איבר בראש הרשימה .מצב הזיכרון בתום ריצתה של ,insertאחרי שהיא הוסיפה את הערך 3הוא: = head = head num = 3 data=17 =next data=13 =next data=3 =next 63 = head num = 3 = temp = rear = front מסגרת המחסנית של insertמוסרת מהמחסנית .אנו שבים לbuild_list - אשר קוראת את הערך ,15ומזמנת שוב את .insertעתה יהיה על insertלטפל במקרה השלישי מבין המקרים שמנינו :הוספת איבר באמצע רשימה .נראה כיצד הדבר קורה :התנאי (head ==NULL) :אינו מתקיים .כך גם לגבי התנאי: ) ,(num < head-> _dataולכן אנו מגיעים ַל else -הכולל את הגוש: ; rear = head ; front = rear -> next { )while (front != NULL && front-> _data < num ; rear = front ; front = front -> _next } ; temp = new (std::nothrow) struct Node ; temp -> _data = num ; temp -> _next = front ; rear -> _next = temp שתי הפקודות הראשונות מפנות את rearלהצביע כמו ) headכלומר על הקופסה של ,(3ואת frontלהצביע כמו ,head-> _nextכלומר כמו המצביע next בקופסה עליה מצביע ,headמשמע על הקופסה של .13שימו לב כי גם הפעם הפניה ל head-> _next -הינה בטוחה רק משום שאנו מובטחים שאם הגענו עד הלום אזי ערכו של headשונה מ.NULL- נציג את מצב הזיכרון: = head = head num = 15 data=17 =next data=13 =next data=3 =next = head num = 15 = temp = rear = front עתה אנו נכנסים לביצוע הלולאה: { )while (front != NULL && front-> _data < num ; rear = front ; front = front -> _next } שני התנאים שבכותרת לולאה מתקיימים ,ועל כן אנו נכנסים לגוף הלולאה. בלולאה אנו מקדמים את זוג המצביעים rear, frontתא אחד קדימה. שימו לב לדרך בה אנו מנסחים את התנאי שבכותרת הלולאה :המרכיב השמאלי בודק האם ערכו של frontשונה מ .NULL -במידה ותנאי זה אינו מתקיים כבר ברור שלא ניכנס לסיבוב )נוסף( בגוף הלולאה ,ועל כן המחשב אינו בודק את התנאי הימני ) (front-> _data < numוטוב שכך ,שכן לו הוא היה מנסה לבדוק תנאי זה התכנית הייתה עפה )שכן אנו מנסים לפנות לשדה ה _data -בקופסה עליה 64 מצביע frontשעה שערכו של frontהוא .(NULLמנגד ,אם התנאי השמאלי מתקיים ,אזי המחשב יכול לגשת בבטחה לבדיקת התנאי הימני :אם התנאי השמאלי מתקיים אזי frontמצביע על קופסה כשלהי ,ולכן ניתן לבדוק את ערכו של .front-> _dataכמובן שלו היינו כותבים את התנאים בסדר הפוך תכניתנו הייתה שגויה. נציג את מצב הזיכרון אחרי ביצוע הסיבוב הראשון בלולאה: = head = head num = 15 data=17 =next data=13 =next data=3 =next = head num = 15 = temp = rear = front לקראת כניסה לסיבוב השני בלולאה נבדק שוב התנאי ,אולם עתה הוא כבר אינו מתקיים )לא נכון ש .(front-> _data < num :שימו לב כי הלולאה הביאה אותנו למצב בו frontמצביע לאיבר הראשון ברשימה הכולל נתון שערכו גדול או שווה ל ,num -בעוד rearמצביע על האיבר האחרון שכולל ערך שעדיין קטן מ- .numעתה עלינו לצור קופסה חדשה ,ולשרשרה בין הקופסה עליה מצביע rear לקופסה עליה מצביע .frontקטע הקוד שאחראי על כך הוא: ; = new (std::nothrow) struct Node ; -> _data = num ; -> _next = front ; -> _next = temp temp temp temp rear שתי הפקודות הראשונות יוצרות קופסה חדשה ,ומפנות את tempלהצביע על אותה קופסה .מצב הזיכרון הוא: data=3 = head =next = head num = 15 data=17 =next data=13 =next data=3 =next 65 = head num = 15 = temp = rear = front עתה מתבצעת הפקודה . temp-> _next = front; :פקודה זאת מפנה את המצביע בקופסה עליה מצביע tempלהצביע כמו ,frontכלומר על הקופסה של .17ולבסוף מתבצעת הפקודה . rear-> _next = temp; :פקודה זאת מסיטה את המצביע בקופסה עליה מצביע ,rearכלומר המצביע בקופסה של ,13להצביע על אותה קופסה עליה מצביע ,tempכלומר על הקופסה של ) 15במקום להצביע על הקופסה של ,17עליה הוא הצביע עד עתה( .מצב הזיכרון הוא: data=3 = head =next = head num = 15 data=17 =next data=13 =next data=3 =next = head num = 15 = temp = rear = front כפי שנקל לראות מהציור ,הרשימה שלנו נותרה ממוינת .בכך מסתיימת ריצתה הנוכחית של ,insertואנו שבים ל build_list -אשר קוראת את ) 3879הבלתי נמנע( .הפעם יהיה על insertלהוסיף איבר אחרון לרשימה .אני מזמין אתכם לבצע את סימולציית ההרצה ,ולהשתכנע שתכניתנו מטפלת כהלכה גם במקרה )רביעי ואחרון מבין המקרים שמנינו שבהם יש לטפל( זה. 66 13.4מחיקת איבר מרשימה מחיקת איבר מרשימה )ממוינת או שאינה( מתבצעת באופן דומה ביותר להוספת איבר לרשימה ממוינת .אנו מציגים גם פעולה זאת שכן היא אחת הפעולות הסטנדרטיות על רשימה )כמו על כל מבנה נתונים אחר( .יחד עם זאת ,בשל הדמיון, לא נסביר את הפונקציה שנכתוב בהרחבה .כדי לשנות מעט מהפונקציה שהוסיפה איבר לרשימה ממוינת נקבע כי הפונקציה שנכתוב תחזיר ערך בולאני שיעיד האם הנתון הרצוי נמצא ברשימה ,והוסר ממנה ,או שמא מלכתחילה הוא לא היה מצוי ברשימה .במקרה בו הנתון נמצא ברשימה אנו מסירים מופע יחיד שלו מהרשימה. נציג את הפונקציה המתאימה: { )bool del(int num, struct Node *&head struct Node *front, *rear, ; *temp )if (head == NULL ; ) return( false { )if (head-> _data == num ; temp = head ; head = head -> _next ; delete temp ; ) retun( true } ; rear = head ; front = rear -> _next { )while (front != NULL && front -> _data < num ; rear = front ; front = front -> _next } )if (front == NULL || front -> _data != num ; ) return( false ; rear -> _next = front -> _next ; delete front ; ) return( true } הסבר :לפונקציה אותם פרמטרים כמו במקרה הקודם .כמו קודם ראשית אנו בודקים האם הרשימה ריקה .במידה וזה אכן המצב אין מה למחוק ואנו מחזירים את הערך .false אחר אנו בודקים האם יש למחוק את האיבר שבראש הרשימה .אם זה המצב אנו שולחים את tempלהצביע על האיבר שבראש הרשימה ,אחר מקדמים את head להצביע על האיבר הבא ברשימה ,ולבסוף מקפידים לשחרר את הזיכרון המכיל את הנתון שיש למחוק .שימו לב כי אנו משתמשים בפקודה , delete temp; :ואיננו כותבים ][ שכן במקרה זה איננו משחררים מערך ,אלא רק איבר יחיד. לבסוף ,אם הרשימה אינה ריקה ,והנתון המבוקש אינו בראשה אנו סורקים את הרשימה עם זוג מצביעים ) (rear, frontעד אשר אנו מגיעים) :א( לאיבר שמכיל ערך גדול מהערך המבוקש ,או) :ב( לסיומה של הרשימה )ואז אנו מסיקים שהנתון המבוקש אינו מצוי ברשימה( ,או) :ג( עד שאנו מגיעים ַלקופסה שמכילה את האיבר 67 הרצוי .אם קופסה כזאת נמצאת אזי frontמצביע עליה ,ו rear -מצביע על הקופסה שלפניה .עתה עלינו לשנות את השרשור כך שהקופסה עליה מצביע front תנותק מהשרשרת ,כלומר המצביע _nextבקופסה עליה מצביע rearיעבור להצביע על הקופסה שמייד אחרי הקופסה שעליה מצביע .frontאני מזמין אתכם לבדוק בדקדקנות שזה אכן מה שנעשה. נקודה נוספת עליה כדאי לתת את הדעת היא הביטוי הבולאני: ) . (front == NULL || front -> _data != numשימו לב כי זוג התנאים קשורים בניהם ב .or -לכן אם התנאי השמאלי מתקיים ברור שאין כל צורך לבדוק את התנאי הימני ,שכן התנאי בכללותו יתקיים .המחשב ימנע על-כן מלהעריך את הביטוי הבולאני הימני )עת השמאלי מסופק( ,וטוב שכך. הפרמטר headשל הפונקציה הוא פרמטר הפניה .שימו לב כי הצורך בכך נובע רק מהמקרה בו יש למחוק את האיבר שבראש הרשימה ,שכן רק אז ערכו של head משתנה ) headעובר אז להצביע על האיבר שהיה שני ברשימה ,או על NULLאם הרשימה כללה איבר יחיד( .בכל המקרים האחרים השינוי הוא אי-שם בהמשך הרשימה .מכך אתם יכולים להסיק מסקנה נוספת :בכוחה של פונקציה המקבלת מצביע לראש רשימה מקושרת כפרמטר ערך לשנות את מצב הרשימה ,עת היא משנה מצביעים שנמצאים בתאי הרשימה .הפונקציה לא תוכל לשנות את ערכו של .headאולם שינוי שהפונקציה תכניס למשל ל , head->next -יישאר בתום ביצוע הפונקציה .נחזור לנקודה זאת בהמשך ,ואז נסבירה ביתר פירוט ,וגם נלמד איך להתגונן מכך. שאלה :הפונקציה שכתבנו מותאמת למחיקת איבר מרשימה ממוינת .היכן באה לידי ביטוי בקוד שכתבנו ממוינותה של הרשימה? במילים אחרות :איזה שינוי )קל ביותר( יש להכניס בפונקציה שכתבנו על-מנת שהיא תטפל כהלכה גם ברשימה שאינה ממוינת? 13.5שחרור תאי רשימה תכנית אשר הקצתה רשימה מקושרת צריכה גם לשחרר את תאי הרשימה לפני סיומה )או עת היא אינה זקוקה להם יותר( .אם התכנית תבצע רק פקודהdelete : headאזי כל שהיא תשחרר יהיה את האיבר הראשון ברשימה .יתר תאי הרשימה יישארו zombiesשמחד גיסא לא שוחררו ,ומאידך גיסא גם אין דרך לפנות אליהם. על כן הדרך לשחרר רשימה היא בלולאה שתשחרר את התאים בזה אחר זה. נציג את קוד הפונקציה הדרושה: { )void free_list(struct Node *head ; struct Node *temp = head { )while (head != NULL ; temp = head ; head = head -> _next ;delete temp } } מפנים את tempלהצביע הפונקציה מתנהלת כלולאה .בכל סיבוב בלולאה אנו ְ לאיבר שעליו מצביע ,headמקדמים את ,headואז משחררים את האיבר עליו מצביע .tempשימו לב כי בתום התהליך הרשימה כולה מוחזרה ,וערכו של head 68 הוא .NULLכאן עלינו לתת את הדעת לנקודה מעט עדינה :הפרמטר של הפונקציה הוא פרמטר ערך ,ועל כן שינוים שהפונקציה הכניסה לפרמטר שלה לא יוותרו בתום ביצוע הפונקציה בארגומנט המתאים .בפרט ערכו של הארגומנט לא יהיה ,NULL אלא הוא ימשיך להצביע לאותו איבר עליו הוא הצביע לפני הקריאה לפונקציה; רק שעתה הזיכרון בו שכן האיבר אינו שייך עוד לתכניתנו ,שכן הוא מוחזר על-ידי הפונקציה .כמובן שניתן להגדיר את הפרמטר גם כפרמטר הפניה ,ואז בתום ביצוע הפונקציה יהיה ערכו של הארגומנט המתאים .NULL 13.6היפוך רשימה עתה נרצה לכתוב פונקציה המקבלת רשימה מקושרת ,והופכת את כיוון ההצבעה של המצביעים ברשימה .לדוגמה ,אם לפונקציה יועבר מצביע לראש הרשימה: data=3 =next data=13 =next data=3879 =next data=17 =next אזי הפונקציה תחזיר מצביע לראש הרשימה: data=17 =next data=3879 =next data=13 =next data=3 =next הקריאה לפונקציה תהיה. rev_head = reverse(head); : נרצה שהפונקציה שנכתוב לא תקצה תאים חדשים )על-גבי הערמה( ,אלא תשנה את כיוון הצבעתם של המצביעים הקיימים באופן שיושג הרצוי .לדוגמה :את המצביע בקופסה של 3היא תפנה להצביע על הקופסה של ) 13במקום שהמצביע יכיל את הערך ,NULLכפי שמצוי בו בעת הקריאה לפונקציה(. הרעיון הבסיסי הוא שנחזיק שלושה מצביעים front, mid, rearשיתקדמו על-פני הרשימה .בכל שלב midיצביע על איבר כלשהו front ,יצביע על האיבר שלפני האיבר עליו מצביע ,midו rear -יצביע על האיבר שמאחורי האיבר עליו מצביע .midבכל סיבוב בלולאה נפנה את mid-> _nextלהצביע על אותו איבר כמו ,rearואחר נסיט את שלושת המצביעים תא אחד ימינה יותר. 69 הגדרת הפונקציה: { )struct Node *reverse( struct Node *head ; struct Node *rear, *mid, *front )if (head == NULL || head-> _next == NULL ; ) return( head // nothing to do ; rear = head mid = head-> _next ; // safe assignment as head!=NULL. front = mid-> _next ; // safe, as head->next != NULL. { )while (front != NULL ; mid -> _next = rear ; rear = mid ; mid = front ; front = front-> _next } ; mid -> _next = rear ; head -> _next = NULL ; ) return( mid } נציג סימולציית הרצה אשר תסייע לנו להשתכנע בנכונות פעולה של הפונקציה. נתחיל בכך שהמשתנה headשל התכנית הראשית מצביע על הרשימה הבאה: =head data=3 =next data=13 =next data=3879 =next data=17 =next עתה נערכת קריאה לפונקציה ,ו head -מועבר כפרמטר ערך .מצב הזיכרון הינו: =head )(of main data=3 =next data=13 =next data=3879 =next data=17 =next =head =rear =mid =front התנאי: להתבצע: מתחילה הפונקציה עתה ) (head==NULL || head-> _next ==NULLאינו מתקיים) .אני מסב את תשומת לבכם לסדר הופעתם של שני המרכיבים בתנאי .יש חשיבות לסדר(. תנאי זה מתייחס למקרה בו הרשימה ריקה ,או כוללת איבר יחיד .בשני המקרים הללו אין צורך לעשות דבר :היפוך הרשימה שווה לרשימה הנתונה; ניתן לסיים מיידית ,ולהחזיר את .head 70 מצב.rear, mid, front שלוש הפקודות הבאות מאתחלות את ערכם של :הזיכרון בתום ביצוע שלוש ההשמות הוא head= (of main) data=17 next= data=3879 next= data=13 next= data=3 next= head= rear= mid= front= בסיבוב הראשון.(front !=NULL) עתה אנו נכנסים ללולאה המתבצעת כל עוד כלומר את המצביע בקופסה של,mid-> _next מפנים את, ראשית,בלולאה אנו מצב הזיכרון.17 כלומר על הקופסה של,rear להצביע על אותו איבר כמו,3879 :הוא head= (of main) data=17 next= data=3879 next= data=13 next= data=3 next= head= rear= mid= front= :עתה אנו מקדמים את שלושת המצביעים rear = mid ; mid = front ; front = front-> _next ; :מצב הזיכרון הוא head= (of main) data=17 next= data=3879 next= head= rear= mid= front= 71 data=13 next= data=3 next= בזאת הסתיים הסיבוב הראשון בלולאה .התנאי שבכותרת הלולאה עדיין מתקיים, ולכן אנו נכנסים לסיבוב נוסף .בפקודה הראשונה בגוף הלולאה אנו מפנים את ,mid->nextכלומר את המצביע בקופסה של ,13להצביע על הקופסה עליה מצביע ,rearכלומר על הקופסה של .3879מצב הזיכרון הוא: =head )(of main data=3 =next data=13 =next data=3879 =next data=17 =next =head =rear =mid =front שלוש הפקודות הבאות בגוף הלולאה מקדמות את שלושת המצביעים .מצב הזיכרון הוא: =head )(of main data=3 =next data=13 =next data=3879 =next data=17 =next =head =rear =mid =front שימו לב כי frontקיבל את ערכו של ,front-> _nextולכן את הערך .NULL 72 איננו נכנסים לסיבוב נוסף בלולאה )התנאי front!= NULLכבר אינו מתקיים(. ואנו פונים לביצוע שלוש הפקודות המסיימות את הפונקציה .השתיים הראשונות ביניהן הן: ; mid -> _next = rear ; head -> _next = NULL הראשונה שולחת את המצביע בקופסה עליה מצביע ,midכלומר את המצביע בקופסה של ,3להצביע על הקופסה עליה מצביע ,rearכלומר על הקופסה של .13 השניה מכניסה ל ,head->next -כלומר למצביע בקופסה של ,17את הערך .NULL מצב הזיכרון הוא: =head )(of main data=3 =next data=13 =next data=3879 =next data=17 =next =head =rear =mid =front אם תבחנו את הציור ,המעט מסורבל ,תגלו שההצבעה התהפכה כנדרש :המצביע בקופסה של 3מצביע על הקופסה של ,13המצביע בקופסה של 13מצביע על הקופסה של ,3879המצביע בקופסה של 3879מצביע על הקופסה של ,17והמצביע בקופסה של 17ערכו .NULLכמו כן midמצביע על הקופסה של 3שנמצאת בראש הרשימה המהופכת .על-כן עת אנו מחזירים את ערכו של ,midאנו מחזירים מצביע לראש הרשימה המהופכת. 73 13.7בניית רשימה ממוינת יחידאית מרשימה לא ממוינת נניח כי בתכניתנו בנינו רשימה מקושרת לא ממוינת ,כדוגמת הבאה: data=2 =next data=15 =next data=17 =next data=17 =next data=15 =next עתה ברצוננו לבנות על-פי הרשימה הנתונה רשימה מקושרת ממוינת בה כל נתון יופיע פעם יחידה ,ולצדו יופיע מונה אשר יציין כמה פעמים הנתון הופיע ברשימה המקורית )הלא ממוינת(. ראשית ,נגדיר את טיפוס המבנה הדרוש: { struct Data_counter ; int _data, _counter ; Data_counter * _next_dc ; } למען שוחרי איכות הסביבה נעיר כי ניתן להגדיר את המבנה בצורה נקיה יותר: תחילה נגדיר מבנה שיכיל את הנתונים הדרושים: { struct D_c ; int _data, _counter ; } ועתה ,נגדיר רשימה מקושרת שכל איבר בה מכיל מבנה כנ"ל: { struct List_of_data_counter ; struct _d_c _info ; struct List_of_data_counter * _next ; } הגדרה זאת נקיה יותר שכן היא מפרידה בין הנתונים אותם ברצוננו לשמור )ושמיוצגים באמצעות המבנה ,(D_cלבין הרשימה ה מקושרת שאנו יוצרים ואשר אין קשר הכרחי בינה לבין הנתונים המיוצגים; היא רק דרך לארגן את הנתונים במקום ברשימה מקושרת המיוצגים .לפחות לכאורה יכולנו לארגן את הנתונים ְ במערך . struct D _c arr[N]; :עד כאן הערה לחובבי ח"נ .אנו נסתפק בצורה הפחות נקיה אך היותר קלה לעיקול ,ונשמש במבנה .struct Data_counter ברצוננו לכתוב את הפונקציה: ). struct Data_counter *rem_dup_n_sort(struct Node *head הפונקציה תקבל מצביע לרשימה מקושרת לא ממוינת הכוללת חזרות ,כפי שתיארנו בתחילת הסעיף ,ותחזיר מצביע לרשימה מקושרת ממוינת ,שאינה כוללת חזרות ,אך שמכילה מונה כפי שתיארנו .הקריאה לפונקציה תהיה: ;)head_sort = rem_dup_n_sort(head עבור המשתנים: ; struct Node *head ; struct Data_counter *head_sort = NULL תחת ההנחה שיצרנו רשימה מקושרת לא ממוינת עליה מצביע .head הפונקציה שנכתוב תשתמש בשתי פונקציות עזר: 74 א .הפונקציה searchמקבלת שני פרמטרים :ערך שלם ,numומצביע המורה על ראשה של הרשימה המקושרת הממוינת ֵ sort_n_unique_p נטולת כפילויות שנבנתה עד כה .הפונקציה מחפשת את הערך numברשימה עליה מצביע .sort_n_unique_pהפונקציה מחזירה אחד מהערכים הבאים: .1במידה והרשימה ריקה יוחזר הערך .NULL .2במידה והנתון לא מצוי ברשימה ,ויש להוסיפו בראש הרשימה יוחזר הערך .NULL .3במידה והנתון מצוי ברשימה יוחזר מצביע לאיבר הכולל את הנתון. .4במידה והרשימה אינה ריקה ,הנתון אינו מצוי ברשימה ,ואין להוסיפו בראש הרשימה ,יוחזר מצביע לאיבר שאחריו יש להוסיף את הנתון החדש. ב .הפונקציה insertמקבלת שלושה פרמטרים :ערך שלם ,numמצביע sort_n_unique_pהמורה על ראשה של הרשימה ה מקושרת הממוינת נטולת כפילויות שנבנתה עד כה )או שערכו הוא ,(NULLמצביע whereהמורה היכן יש לשלב תא חדש שיכיל את numברשימה עליה מצביע .sort_n_unique_pהפונקציה פועלת באופן הבא: במידה וערכו של המצביע sort_n_unique_pהוא NULLהפונקציה 1 יוצרת איבר חדש .מפנה את המצביע להורות על התא ,ומשימה באיבר את ,numתוך ציון שזה מופעו הראשון ברשימה הממוינת שתבנה. במידה וערכו של whereהוא NULLהפונקציה מוסיפה את numבראש 2 הרשימה הממוינת. בכל מקרה אחר ,הפונקציה יוצרת איבר חדש ,ומשלבת אותו ברשימה 3 במקום שארי האיבר עליו מצביע .where הגדרת הפונקציה: { )struct Data_counter *rem_dup_n_sort(struct Node *head ; struct Data_counter *new_list = NULL, *where { )while (head != NULL ; )where = search(head-> _data, new_list )if (where != NULL && where-> _data == head-> _data ; (where -> counter)++ ; )else insert(head-> _data, new_list, where ; head = head-> _next } ; ) return( new_list } הסבר הפונקציה :הפונקציה כוללת לולאה המתנהלת כל עוד לא סיימנו לעבור על כל אברי הרשימה על-פיה יש לבנות את הרשיתה הממוינת נטולת הכפילויות .בכל סיבוב בלולאה אנו ראשית מחפשים את הערך המצוי בתא הנוכחי ברשימה הלא ממוינת בקרב אברי הרשימה הממוינת .אם הערך נמצא אזי אנו מגדילים את המונה באחד ,ואחרת אנו מוסיפים איבר חדש לרשימה הנבנית. לפני שנפנה להגדרת פונקציות העזר ברצוני להדגיש שלכאורה אם הנתון >head- _dataמצוי ברשימה עליה מורה ,new_listאזי searchכבר יכולה ,עת היא מאתרת את האיבר בו מצוי הנתון ,להגדיל את ערכו של המונה באחד .לכאורה searchאכן יכולה לעשות זאת ,למעשה מטעמי מבניות לא ראוי שהיא תעשה 75 זאת ,שכן searchכשמה כן היא צריכה לנהוג :לחפש בלבד )ולא לשנות( .לכן את מלאכת עדכון המונה הטלנו על הפונקציה הראשית. rem_dup_n_sort , מנגד ,שימו לב שכתבנו את searchבצורה יחסית חכמה :היא לא מחזירה רק איתות בולאני האם הנתון הרצוי נמצא או לא ברשימה שהועברה לה .היא גם מחזירה מצביע שיסייע לנו הן במקרה בו הנתון נמצא )לעדכן את המונה( ,והן במקרה בו הוא לא נמצא )ויש להוסיפו לרשימה( .באופן זה בין אם יש להוסיף את הנתון לרשימה הנבנית ,ובין אם יש רק לעדכן את המונה ,כבר אין צורך לסרוק את הרשימה פעם נוספת' .תבונתה' של searchאינה משנה את מורכבות זמן הריצה של הפונקציה ,rem_dup_n_sortאך היא הופכת אותה ליעילה יותר) .אגב ,מהי מורכבות זמן הריצה של הפונקציה?(. עתה נציג את הפונקציה :search struct Data_counter *search(int num, * struct Data_counter { )sort_n_unique_p ; ; struct Data_counter *rear, *front || if (sort_n_unique_p == NULL )num < sort_n_unique_p-> _data ; )return(NULL ; front = sort_n_unique_p->next_dc ; rear = sort_n_unique_p { )while (front != NULL && front-> _data < num ; rear = front ; front = front ->next_dc } || if (front == NULL // num shld be added as last front -> data > num) // should be added in middle ; )return(rear ; )return(front // num exists in list } הסבר הפונקציה :אם ערכו של המצביע לראש הרשימה הנבנית הוא ,NULLאו שהאיבר שיש להוסיף קטן מהערך המצוי בראש הרשימה אזי searchמחזירה את הערך .NULLאחרת אנו נכנסים ללולאה אשר מתקדמת עד אשר frontמצביע על איבר הכולל את הנתון הרצוי ,או עד ש front -מגיע לאיבר הכולל ערך גדול מהערך הרצוי )ואז ניתן להסיק שהערך הרצוי אינו ברשימה( ,או עד אשר ערכו של front הוא ) NULLואז ניתן להסיק כי יש להוסיף את האיבר בסוף הרשימה הממוינת(. אחרי היציאה מהלולאה אנו מחזירים את הערך המתאים ,כפי שמוסבר בתיעוד בגוף הקוד. עתה נותר לנו להציג את .insertשימו לב כי בזכות 'תבונתה' של ,search הפונקציה insertאינה כוללת כל פקודות לולאה. void insert(int num, struct Data_counter *&sort_n_unique_p, { )struct Data_counter *where ; struct Data_counter *temp // list is empty { )if (sort_n_unique_p == NULL 76 ; = new struct Data_counter ; -> data = num ; -> counter = 1 ; -> _next_dc = NULL sort_n_unique_p sort_n_unique_p sort_n_unique_p sort_n_unique_p } // insert as first in non empty list { )else if (where == NULL ; temp = new struct Data_counter ; temp -> data = num ; temp -> counter = 1 ; temp -> _next_dc = sort_n_unique_p ; sort_n_unique_p = temp } { else // insert in the middle, or as last ; temp = new struct Data_counter ; temp -> data = num ; temp -> counter = 1 ; temp -> _next_dc = where -> _next_dc ; where -> _next_dc = temp } } אני מזמין אתכם לערוך בעצמכם מעקב אחרי insertולבדוק את נכונותה. 13.8מיון מיזוג של רשימות משורשרות בעבר ראינו את אלגוריתם המיון )הלא יעיל( מיון בועות ,ואת אלגוריתם מיון היעיל מיון מהיר )אשר ,כזכור לנו)?( רץ בד"כ בזמן ) n*log(nעבור nשמציין את כמות הנתונים שיש למיין ,אך עלול להתדרדר לכדי זמן ריצה של n2במקרים קיצוניים(. עתה נרצה להכיר אלגוריתם מיון יעיל נוסף :מיון מיזוג. מיון מיזוג ניתן לבצע על מערך ,או על רשימה מקושרת .מיון מיזוג על מערך מחייב שימוש במערך עזר נוסף שגודלו כגודלו של המערך המקורי ,כלומר מיון מיזוג על מערכים מחייב הכפלת גודל הזיכרון הנצרך ע"י התכנית .זה הופך אותו לאלגוריתם לא מעשי עבור מערכים שאינם קטנים .בניגוד לכך ,מיון מיזוג של רשימה לא מחייב תוספת של זיכרון )ובמובן זה הוא דומה למיון מהיר שלא מחייב תוספת של זיכרון(, וזה כבר משמח .בנוסף לכך ,מיון מיזוג )הן של מערך ,והן של רשימה( רץ תמיד בזמן של ) ,n*log(nובמובן זה יש לו יתרון על-פני מיון מהיר )אשר ,כאמור ,במצבים קיצוניים ,עלול להתדרדר לכדי זמן ריצה ריבועי(. 13.8.1הצגת אלגוריתם המיון :מיון מיזוג מיזוג ) (mergingהוא תהליך בו יוצרים סדרה ממוינת אחת משתיים )או יותר( סדרות ממוינות .הסדרות עשויות להיות מאוחסנות במערך ,ברשימה מקושרת ,או אחר שמאפשר שמירה של סדרת נתונים .לדוגמה אם נתונים בכל מבנה נתונים ֵ המערכים הממוינים )מקטן לגדול(: 3879 17 5 13 77 7 5 5- 5 אזי תהליך מיזוג ייצר מהם מערך בן שמונה ערכים: 5 5 7 13 17 3879 5 5- תהליך המיזוג פועל האופן הבא: א .התחל באיבר הראשון בכל אחת משתי הסדרות שיש למזג .קבע אותו כאיבר הנוכחי. ב .כל עוד הסדרה הראשונה לא הסתיימה וגם הסדרה שניה לא הסתיימה בצע: .1אם האיבר הנוכחי בסדרה הראשונה קטן או שווה מהאיבר הנוכחי בסדרה השניה ,אזי) :א( העבר את האיבר הנוכחי בסדרה הראשונה לסדרה הנבנית, ו) -ב( התקדם על הסדרה הראשונה לאיבר הבא. .2אחרת )האיבר הנוכחי בסדרה השניה קטן מהאיבר הנוכחי בסדרה הראשונה( ,בצע) :א( העבר את האיבר הנוכחי בסדרה השניה לסדרה הנבנית ,ו) -ב( התקדם על הסדרה השניה לאיבר הבא. עם היציאה מהלולאה שתוארה בסעיף ב' אחת הסדרות הסתיימה והשניה עדיין לא, לכן יש להעתיק בזה אחר זה את הנתונים מהסדרה שלא הסתימה לסדרה הנבנית. לכן בצע: ג .כל עוד לא הסתימה הסדרה הראשונה ,העתק בזה אחר זה את אבריה על הסדרה הנבנית. ד .כל עוד לא הסתימה הסדרה השניה ,העתק בזה אחר זה את אבריה על הסדרה הנבנית. בפועל רק אחת משתי הלולאות תתבצע )בלולאה שלא תבוצע התנאי לא יתקיים מייד עם ההגעה ללולאה(. בדוגמה שלנו :נתחיל מהתא הראשון בשני המערכים. א .הערך -5במערך השני קטן מהערך 5במערך הראשון ,לכן נעתיק את –5למערך שאנו יוצרים ,ונתקדם לתא השני במערך השני. ב .עתה הערך 5במערך הראשון קטן או שווה מהערך 5במערך השני ,לכן נעתיק את הערך 5מהמערך הראשון למערך הנבנה ,ונתקדם לתא השני במערך הראשון. ג .הערך 5בתא השני של המערך השני קטן מהערך 13בתא השני של המערך הראשון ,לכן נעתיק את 5למערך שאנו יוצרים ,ונתקדם לתא השלישי במערך השני. ד .הערך 5בתא השלישי של המערך השני קטן מהערך 13בתא השני של המערך הראשון ,לכן נעתיק את 5למערך שאנו יוצרים ,ונתקדם לתא הרביעי במערך השני. ה .הערך 7בתא הרביעי של המערך השני קטן מהערך 13בתא השני של המערך הראשון ,לכן נעתיק את 7למערך שאנו יוצרים ,ונתקדם לתא החמישי במערך השני. בזאת גמרנו להעביר את נתוני המערך השני .לכן אנו יוצאים מהלולאה הראשונה )המתנהלת כל עוד שני המערכים גם יחד לא מוצו( .כל שנותר לנו עתה לעשות הוא להעתיק את הנתונים מהמערך הראשון שטרם הועברו למערך הנבנה. ו .עתה נעתיק בזה אחר זה את הערכים 3879 ,17 ,13מהמערך הראשון למערך הנבנה. 78 כפי שראינו ,תהליך המיזוג סורק כל אחד משני המערכים פעם יחידה .ועל כן כדי למזג שני מערכים ממוינים בגודל Nלכדי מערך ממוין בגודל 2*Nעלינו להשקיע עבודה בסדר גודל של Nפעולות. אלגוריתם המיון מיון מיזוג מקבל סדרה של נתונים שיש למיין ,ומחזיר את הסדרה ממוינת .האלגוריתם פועל באופן הבא: א .אם הסדרה שיש למיין ריקה ,או הסדרה שיש לציין כוללת נתון יחיד ,אזי החזר את הסדרה עצמה) .אין צורך למיינה(. ב .אחרת )הסדרה שיש למיין כוללת לפחות שני נתונים(: .1פצל את הסדרה לשתי סדרות בעלות אורך שווה ככל שניתן )אם אורכה של הסדרה שיש למיין זוגי אזי היא תפוצל לשתי סדרות בעלות אורך שווה, אחרת :אחת הסדרות תכיל איבר אחד יותר מהשניה(. .2מיין את תת-הסדרה הראשונה באמצעות מיון מיזוג. .3מיין את תת-הסדרה השניה באמצעות מיון מיזוג. .4מזג את שתי תת-הסדרות הממוינות לכדי סדרה אחת שלמה ממוינת. .5החזר את הסדרה שהתקבלה בסעיף הקודם. נדגים ריצה של האלגוריתם על סדרת הנתונים הבאה{2 ,5 ,17 ,17 ,3879 ,13 ,7} : א .יש למיין סדרה בת 6ערכים על-כן: .1נפצלה לשתי סדרות } {17 ,3879 ,13 ,7ו. {2 ,5 ,17} - .2נקרא רקורסיבית עבור הסדרה.{17 ,3879 ,13 ,7} : .3נקרא רקורסיבית עבור הסדרה. {2 ,5 ,17} : .4הקריאה הרקורסיבית מסעיף #2תחזיר את הסדרה.{3879 ,17 ,13 ,7} : הקריאה הרקורסיבית מסעיף #3תחזיר את הסדרה . {17 ,5 ,2} :נמזג את שתי הסדרות לכדי הסדרה ,{3879 ,17 ,17 ,13 ,7 ,5 ,2} :ואותה נחזיר. ב .נדון בקריאה הרקורסיבית מסעיף א .'2כתובת החזרה שלה היא סעיף א.'2 קריאה זאת מקבלת את הסדרה .{17 ,3879 ,13 ,7} :היא מבצעת ארבעה צעדים: .1פיצול הסדרה לשתי סדרת {13 ,7} :ו.{17 ,3879} - .2קריאה רקורסיבית עבור הסדרה.{13 ,7} : .3קריאה רקורסיבית עבור הסדרה.{17 ,3879} : .4מיזוג הסדרה } {13 ,7שחזרה מסעיף ,'2עם הסדרה } {3879 ,17שחזרה מסעיף ,'3לכדי הסדרה } .{3879 ,17 ,13 ,7והחזרת סדרה זאת. ג .נדון בקריאה הרקורסיבית מסעיף ב .'2כתובת החזרה שלה היא סעיף ב.'3 קריאה זאת מקבלת את הסדרה .{13 ,7} :היא מבצעת ארבעה צעדים: .1פיצול הסדרה לשתי סדרת {7} :ו.{13} - .2קריאה רקורסיבית עבור הסדרה.{7} : .3קריאה רקורסיבית עבור הסדרה.{13} : .4מיזוג הסדרה } {7שחזרה מסעיף ,'2עם הסדרה } {13שחזרה מסעיף ,'3לכדי הסדרה } .{13 ,7והחזרת סדרה זאת. ד .נדון בקריאה הרקורסיבית מסעיף ג .'2כתובת החזרה שלה היא סעיף ג.'3 קריאה זאת מקבלת את הסדרה .{7} :ועל כן היא חוזרת מיידית :הסדרה שהיא קבלה כבר ממוינת. 79 ה .חזרנו לסעיף ג .'3הוא מנפיק קריאה רקורסיבית עבור הסדרה .{13} :כתובת החזרה של קריאה זאת היא סעיף ג .'4הקריאה עבור הסדרה } {13חוזרת מיידית. ו .אנו חוזרים לסעיף ג .'4ממזגים את שתי הרשימות לכדי הרשימה,{13 ,7} : וחוזרים מסעיף ג' .כתובת היא סעיף ב.'3 ז .סעיף ב '3מנפיק קריאה רקורסיבית עם הסדרה .{17 ,3879} :כתובת החזרה של קריאה זאת היא סעיף ב .'4הקריאה מבצעת ארבעה שלבים: .1פיצול הסדרה לשתי סדרת {3879} :ו.{17} - .2קריאה רקורסיבית עבור הסדרה.{3879} : .3קריאה רקורסיבית עבור הסדרה.{17} : .4מיזוג הסדרה } {3879שחזרה מסעיף ,'2עם הסדרה } {17שחזרה מסעיף ,'3 לכדי הסדרה } .{3879 ,17והחזרת סדרה זאת. ח .נדלג על הקריאות הרקורסיביות שמנפיקים סעיפים ז '2ו -ז.'3 ט .סעיף ז' משלים את פעולת המיזוג ומחזיר את הסדרה .{3879 ,17} :כתובת החזרה של סעיף ז' היא סעיף ב .'4עתה סעיף ב '4יכול למזג את שתי הסדרות הממוינות שהחזירו סעיפים ג' ו-ז' לכדי הסדרה .{3879 ,17 ,13 ,7} :סדרה זאת תוחזר לסעיף א.'2 י .עתה סעיף א' יכול להתקדם לסעיף א ,'3ולזמן קריאה רקורסיבית עם הסדרה: } .{2 ,5 ,17כתובת החזרה של קריאה זאת היא סעיף א .'4הקריאה הרקורסיבית עם הסדרה {2 ,5 ,17} :מתפצלת לארבעה שלבים: .1פיצול הסדרה לשתי סדרת {5 ,17} :ו.{2} - .2קריאה רקורסיבית עבור הסדרה.{5 ,17} : .3קריאה רקורסיבית עבור הסדרה.{2} : .4מיזוג הסדרה } {17 ,5שחזרה מסעיף ,'2עם הסדרה } {2שחזרה מסעיף ,'3 לכדי הסדרה .{17 ,5 ,2} :והחזרת סדרה זאת. יא .נדלג על הקריאות הרקורסיביות שמנפיק סעיף י '2וסעיף י .'3סעיף י '4ימזג את הסדרות החוזרת לכדי הסדרה .{17 ,5 ,2} :בכך סעיף י' יסתיים ,תוך שהוא מחזיר סדרה ממוינת זאת. יב .כתובת החזרה של סעיף י' היא סעיף א .'4סעיף א '4יכול עתה למזג את שתי הסדרות הממוינות {3879 ,17 ,13 ,7} :ו {17 ,5 ,2} -לכדי הסדרה הממוינת,2} : ,{3879 ,17 ,17 ,13 ,7 ,5ואותה נחזיר. 80 13.8.2מיון מיזוג של רשימות משורשרות מיון מיזוג של רשימות משורשרות הוא מקרה פרטי של מיון מיזוג ,בו מבנה הנתונים באמצעותו מוחזקת סדרת הנתונים הוא רשימה מקושרת. נציג את הפונקציה: { )struct Node *merge_sort(struct Node *head struct Node *list1 = NULL, *list2= NULL, ; *list )if (head == NULL || head-> _next == NULL ; return head ; ) split( head, list1, list2 ; )list1 = merge_sort(list1 ; )list2 = merge_sort(list2 ; )list = merge(list1, list2 ; return list } הפונקציה merge_sortהיא מימוש ישיר של האלגוריתם :במידה והרשימה שיש למיין ריקה ,או כוללת נתון יחיד היא כבר ממוינת ,וניתן להחזיר את .headאחרת, אנו מפצלים את הרשימה עליה מצביע headלשתי רשימות ,עליהן מצביעים .list1, list2אחר אנו קוראים רקורסיבית עם list1ועם ,list2ואת הערך המוחזר אנו מכניסים חזרה ל .list1, list2 -לבסוף אנו ממזגים את שתי הרשימות הממוינות עליהם מצביעים list1, list2לכדי רשימה מקושרת ממוינת שלמה עליה מצביע ,listואותה אנו מחזירים. כדרכו של תכנות מעלה-מטה את עיקר העבודה 'דחפנו מתחת לשטיח' לפונקציות ,splitו .merge -על-כן עתה נפנה אליהן. גישה נאיבית לכתיבת הפונקציה splitתאמר שאת המחצית הראשונה של אברי הרשימה נַפנֵה לרשימה ,list1ואת המחצית השניה נפנה לרשימה .list2זוהי גישה נאיבית שכן היא מחייבת מעבר מקדים אשר יספור כמה איברים יש ברשימה. אנו נציג חלופה יעילה מעט יותר לכתיבת הפונקציה :את האיברים במקומות הפרדיים נפנה לרשימה עליה יצביע ,list1ואת אלה שבמקומות הזוגיים נפנה לרשימה עליה יצביע .list2באופן כזה פעולת הפיצול תתבצע על-ידי ביצוע מעבר יחיד על הרשימה. הפונקציה שנכתוב מקבלת את list1, list2כפרמטרי הפניה ,שכן עליה להפנותם להצביע על רשימות שהפונקציה מקצה להם .הפונקציה אינה מקצה תאים חדשים בזיכרון ,וזו תכונה יפה שלה ,שכן כך היא אינה מגדילה את צריכת הזיכרון של התכנית. 81 נציג את הפונקציה ,ואחר נסבירה ביתר פירוט: void split(struct Node *list, { )struct Node *&list1, struct Node *&list2 ; bool to_list1 = true ; struct Node *temp { )while (list != NULL ; temp = list ; list = list -> _next { )if (to_list1 ; temp-> _next = list1 ; list1 = temp } { else ; temp-> _next = list2 ; list2 = temp } ; to_list1 = !to_list1 } } המשתנה הבולאני to_list1מורה להיכן יש להפנות את האיבר הבא ברשימה אותה מפצלים :לרשימה עליה מורה list1או לזאת עליה מורה .list2לכן בכל סיבוב בלולאה אנו מנגדים את ערכו ) האופרטור ! מחזיר את הערך הבולאני ההפוך לערכו של האופרנד שלו ,לכן הביטוי !to_list1 :מחזיר את הערך ההפוך לערך המצוי כרגע במשתנה ,to_list1וערך זה אנו מכניסים ל.(to_list1 - בכל סיבוב בלולאה אנו שומרים במשתנה tempמצביע לאיבר עליו מורה עתה ,listאחר אנו מקדמים את listלאיבר הבא ברשימה אותה יש לפצל ,ואז את האיבר עליו מורה tempמעבירים לראש הרשימה הרצויה )זו שעליה מורה list1 או זו שעליה מצביע .(list2האפקט המתקבל הוא שאיברים שהיו מאוחרים יותר ברשימה listיהיו מוקדמים יותר ברשימות list1, list2אך לכך אין כל משמעות שכן כל תפקידה של splitהוא לפצל את הרשימה עליה מצביע ,list ולא משנה באיזה סדר יועברו האיברים המתפצלים לרשימות הנוצרות .בכל מקרה הרשימות הנוצרות תמוינה )על-ידי קריאה רקורסיבית ל.(merge_sort - נקודה מעט עדינה אליה ראוי לתת את הדעת היא שאנו מסתמכים על כך שערכם של list1, list2בעת הקריאה לפונקציה splitהוא .NULLההסתמכות היא שכן המצביע _nextבאיבר הראשון שנוסף לכל רשימה )ושיהיה לכן האיבר האחרון ברשימה הנבנית( מקבל את ערכו של ) list1או list2בהתאמה( .כדי שערכו של המצביע _nextבאיבר האחרון יהיה NULLחשוב שערכם של list1, list2בעת הקריאה יהיה .NULL 82 עתה נציג את הפונקציה .merge struct Node *merge(struct Node *list1, { )struct Node *list2 struct Node *list = NULL, // head of new list *last, // pointer to last item in list ; *temp // while both lists r not empty { )while (list1 != NULL && list2 != NULL { )if (list1-> _data <= list2-> _data ; temp = list1 // hold the item that moves list1 = list1 -> _next ; // advance in its list } { else ; temp = list2 ; list2 = list2 -> _next } temp -> _next = NULL ; // rem the itm from its list )if (list == NULL ; last = list = temp { else ; last -> _next = temp // append as last ; last = temp // and update pointer 2 last } } )if (list1 == NULL ; last-> _next = list2 else ; last-> _next = list1 ; )return(list } הסבר הפונקציה :הפונקציה מחזיקה שני מצביעים לרשימה הנבנית list :מצביע על ראש הרשימה last ,מצביע על האיבר האחרון ברשימה. הלולאה מתנהלת כל עוד שתי הרשימות שיש למזג אינן ריקות .בכל סיבוב בלולאה אנו ראשית בודקים איזה איבר יש להעביר לרשימה הנבנית )האם את האיבר המצוי בראשה של ,list1או את זה שבראשה של .(list2המצביע tempעובר להצביע על האיבר המתאים ,והמצביע last1או last2מקודם לאיבר הבא ברשימה המתאימה .הפקודה temp -> _next = NULL ; :מנתקת את האיבר שיש להעביר לרשימה הנבנית מהרשימה שלו .מכיוון שאיבר זה יתווסף בסופה של הרשימה הנבנית מתאים שהמצביע _nextבו יכיל את הערך .NULLבמקרה הקצה בו אנו מוסיפים את האיבר הראשון לרשימה הנבנית ) (list == NULLאנו מפנים הן את listוהן את lastלהצביע על האיבר המועבר .בכל מקרה אחר אנו מפנים את המצביע שבתא האחרון ברשימה הנבנית ) (last-> _nextלהצביע על האיבר המוסף )עליו מצביע ,(tempואחר מקדמים את lastלהצביע על האיבר החדש )שהינו עתה האיבר האחרון ברשימה הנבנית(. 83 נצא מהלולאה עת אחת מהרשימות אותן יש למזג תמוצה .אם הלולאה שמוצתה היא זו עליה הצביע lilst1אזי list2מצביע על זנב הרשימה השניה ,שאת אבריה יש לשרשר בהמשכה של הרשימה הנבנית .הפקודה: last-> _next = list2מבצעת את החיבור המתאים :של זנב הרשימה עליה מצביע list2להמשכה של הרשימה הנבנית .אם הלולאה שמוצתה היא זו עליה הצביע lilst2אזי זהו המקרה הסימטרי. אתם מוזמנים לבצע סימולציית הרצה דקדקנית לפונקציה כדי לבדוק את נכונותה… 13.8.3זמן הריצה של מיון מיזוג נפנה עתה לשאלה הבלתי נמנעת מהו זמן הריצה של התכנית? הניתוח יהיה דומה מאוד לזה שביצענו עבור מיון מהיר .כל ריצה של האלגוריתם מבצעת את הצעדים הבאים: א .פיצול הרשימה לשתי תת-רשימות ,תוך השקעת עבודה לינארית באורך הרשימה שיש לפצל. ב .קריאה רקורסיבית עבור מחצית הרשימה האחת. ג .קריאה רקורסיבית עבור מחצית הרשימה השניה. ד .מיזוג שתי הרשימות שהחזירו הקריאות הרקורסיביות ,תוך השקעת עבודה לינארית באורך הרשימה המתקבלת. נניח כי האלגוריתם התבקש למיין רשימה בת Nנתונים .נציג את עץ הקריאות הרקורסיביות המתקבל .כל צומת בעץ מכיל ארבעה מרכיבים המתאימים לארבעת הצעדים המבוצעים בקריאה הרקורסיבית המתאימה. הציור: split )(N הציור: (ms )N/2 הציור: (merge N/2, )N/2 מייצג פיצול של רשימה בארוך Nלכדי שתי רשימות. מייצג קריאה רקורסיבית עם רשימה באורך N/2 מייצג מיזוג של שתי רשימות באורך N/2לכדי רשימה באורך .N 84 עץ הקריאות יראה לפיכך באופן הבא: (merge N/2, )N/2 (merge N/4, )N/4 (ms )N/4 (ms )N/4 (merge N/8, )N/8 (merge N/8, )N/8 (ms )N/8 (ms )N/8 (ms )N/2 (ms )N/2 split )(N/2 (ms )N/8 split )(N/4 (merge N/4, )N/4 split )(N/4 (ms )N/8 (merge N/8, )N/8 split )(N (ms )N/4 (merge N/8, )N/8 (ms )N/8 (ms )N/8 (ms )N/4 (ms )N/8 split )(N/2 (ms )N/8 split )(N/4 עתה נשאל את עצמנו כמה רמות תהינה בעץ? ומה כמות העבודה שתתבצע בכל קטן בחצי בכל רמה .ברמה רמה? אנו רואים שגודלה של הרשימה אותה יש למיין ֵ התחתונה יש למיין רשימה בת איבר יחיד .על-כן מספר הרמות יהיה ) ,log2(Nשכן הפונקציה ) log2(xמתארת כמה פעמים ניתן לחצות את xעד שמגיעים לערך אחד. כמות העבודה הנעשית בכל רמה הינה בסדר גודל של :Nבשורש אנו מפצלים רשימה בגודל ) ,Nתוך השקעת עבודה בשיעור ,(Nואחר ממזגים שתי רשימות באורך ) N/2תוך השקעת עבודה בשיעור .(Nברמה שמתחת לשורש :פעמיים אנו מפצלים רשימה בגודל N/2לכדי שתי רשימות בגודל ) N/4ועל כל פיצול כזה אנו משלמים עבודה בשיעור ,(N/2ופעמיים אנו ממזגים שתי רשימות בגודל N/4לכדי רשימה בגודל ) N/2ועל כל מיזוג משלמים עבודה בשיעור .(N/2וכך הלאה. מכיוון שבעץ יש ) log2(Nרמות ,ובכל רמה מתבצעת עבודה בשיעור ,Nאזי סך כל העבודה הנעשית היא ) . N*log2(Nכלומר כמות עבודה לזאת הנעשית על-ידי מיון מהיר על קלט מיטבי )ולמעשה ,אך אנו לא הוכחנו זאת ,גם על קלט ממוצע( .שימו לב כי במיון מיזוג זאת תהא כמות העבודה שתתבצע עבור כל קלט שהוא )בעוד מיון מהיר ,על קלט גרוע ,יידרש לבצע עבודה בשיעור .(N2 85 split )(N/4 13.9בניית רשימה מקושרת ממוינת בשיטת המצביע למצביע בסעיף 12.3ראינו כיצד ניתן לבנות רשימה מקושרת ממוינת .לשם הוספת איבר ַלרשימה השתמשנו בשני מצביעי עזר rear, frontאשר סרקו את הרשימה הקיימת כדי לאתר את המקום בו יש לשלב את האיבר החדש .הלולאה שהרצנו התקדמה עם המצביעים עד אשר frontהגיע לאיבר שהכיל ערך גדול מהערך אותו יש להוסיף .המצביע rearהצביע אז לאיבר שאחריו יש לשלב את האיבר החדש .זו היא הדרך הארוכה אך הקצרה לבניית רשימה מקושרת ממוינת :היא ארוכה באשר היא לא הכי אלגנטית ,היא קצרה שכן היא יחסית קלה יותר להבנה .בסעיף זה נרצה לראות את הדרך הקצרה אך ארוכה לבניית רשימה מקושרת ממוינת: דרך זו תהיה אלגנטית יותר ,אך קשה יותר להבנה. הכלי הבסיסי בו נשתמש לבניית הרשימה הוא מצביע למצביע. נניח כי בתכנית הראשית הוגדר . struct Node *head = NULL; :אחר זומנה הפונקציה . build_list(head); :הפונקציה build_listדומה לזו שראינו בעבר .נציג אותה: { )void build_list(struct Node *&head ; int num ; cin >> num { )while (num != 0 ; )insert(num, &head ; cin >> num } } הנקודה היחידה אליה יש צורך לתת את הדעת היא זימונה של הפונקציה .insert שימו לב כי אנו קוראים ל insert -עם ,&headכלומר עם מצביע ַלמצביע .head הוא: insert הפונקציה של השני הפרמטר טיפוס משמע ) . struct Node **headמי שהקריאה הנ"ל מבלבלת אותו יכול לבצעה בשני ; struct Node **tempלהכניס את כתובתו של צעדים) :א( ַלמשתנה: headכלומר את ,&headוזאת באמצעות ההשמה) , temp = &head; :ב( לקרוא לפונקציה.( insert(num, temp); : 86 : נציגה ואחר נסבירה.insert נפנה עתה לפונקציה void insert(int num, struct Node **p_2_head) { struct Node *temp ; if (*p_2_head == NULL) { *p_2_head = new (std::nothrow) struct Node ; if (*p_2_head == NULL) terminate("cannot allocate memory", 1) ; (*p_2_head)-> _data = num ; (*p_2_head)-> _next = NULL ; return ; } while ( (*p_2_head) != NULL && (*p_2_head)-> _data < num ) p_2_head = & ( (*p_2_head)-> _next ) ; temp = new (std::nothrow) struct Node ; if (temp == NULL) terminate("cannot allocate memory", 1) ; temp -> _data = num ; temp -> _next = (*p_2_head); (*p_2_head) = temp ; } ומי שנתקף בפיק ברכיים עת הוא רואה,יש להודות שהפונקציה אינה קלה להבנה .(*) טרם שהוא פונה לראות כוכביות, כדאי שיחזק את ברכיו,( לנגד עיניו->) חץ 5) .0 ,17 ,3879 ,3 ,5 :כדי להבינה נבצע לה סימולציית הרצה מדוקדקת עבור הקלט .( מוזן אחרון0 ,מוזן ראשון -נתחיל בהצגת מצב המחסנית של התכנית הראשית טרם הקריאה ל :build_list head = מצב המחסנית. כפרמטר הפניהhead את head = head = num= 87 מקבלתbuild_list הפונקציה :כן-בעקבות הקריאה הוא על הפונקציה build_listמתחילה להתבצע .היא קוראת את הערך 5לתוך ,num וקוראת ל .insert -הפונקציה insertמקבלת מצביע למצביע; נסמן מצביע זה כפי שאנו מציירים מצביעים בדרך כלל )עם ראש בצורת ,(vונַפנה את המצביע להצביע על המצביע שעליו הוא מורה ) headשל התכנית הראשית( .מצב המחסנית יהיה: = head = head num= 5 = p_2_head num= 5 =temp עתה הפונקציה insertמתחילה להתבצע .נזכור כי *p_2_headהוא ה'-יצור' עליו ) p_2_headשל (insertמצביע ,וזהו המשתנה headשל התכנית הראשית. לכן עת הפונקציה שואלת האם ערכו של *p_2_headהוא NULLהיא שואלת האם headשל התכנית הראשית ערכו הוא .NULLנוכל לראות זאת גם מהציור: p_2_headהוא מצביע *p_2_head ,הוא היצור עליו המצביע מצביע ,ומהציור אנו רואים שהחץ היוצא מ p_2_head -מצביע על המשתנה headשל התכנית הראשית. אם כן ,התנאי ) (*p_2_head == NULLמתקיים .לכן מבוצע הגוש הכפוף לו: המצביע ,*p_2_headכלומר המצביע עליו p_2_headמורה )שהוא המצביע head של התכנית הראשית( עובר להצביע על קופסה חדשה )מטיפוס (struct Node שמוקצית על הערמה .הביטוי (*p_2_head)-> _data :מתייחס למרכיב ה- _dataבקופסה שהוקצתה .נסביר מדוע p_2_head :הוא המצביע למצביע )טיפוסו ,(struct Node ** :לכן *p_2_headהוא המצביע עליו p_2_head מצביע ,כלומר המצביע headשל התכנית הראשית )שטיפוסו הוא struct Node *( .הביטוי (*p_2_head)-> :מפנה אותנו לקופסה שהוקצתה אך זה עתה, מורה .ולבסוף: ֵ ושעליה ) ,(*p_2_headכלומר headשל התכנית הראשית, (*p_to_head)-> _dataמתייחס לשדה ה data_ -בקופסה שעליה המצביע מצביע .לשדה (*p_2_head)-> _dataאנו משימים את הערך .5באופן דומה לשדה המצביע בקופסה עליה מצביע headשל התכנית הראשית אנו מכניסים את ְ הערך .NULLבזאת insertמסתיימת. מצב הזיכרון לקראת סיום ריצתה הראשונה של insertהוא: =data 5 = head = head num= 5 = p_2_head num= 5 =temp 88 עם תום ריצתה של insertרשומת ההפעלה שלה מוסרת מעל-גבי המחסנית ,ואנו שבים ל build_list -אשר קוראת את הערך ,3ומזמנת שנית את .insertגם בקריאה הנוכחית מעבירה build_listל insert -מצביע ל .head -מצב הזיכרון דומה למתואר בציור האחרון ,פרט לכך שערכו של numבשתי רשומות ההפעלה הוא עתה ) 3במקום .(5 אחרי בניית רשומת ההפעלה מתחילה הפונקציה להתבצע .התנאי: ) (*p_2_head == NULLאינו מתקיים )ערכו של המצביע עליו מצביע ,p_2_headכלומר המצביע headשל התכנית הראשית ,אינו .(NULLלכן אנו פונים ללולאה אשר התנאי בכותרתה הוא: ) ( (*p_2_head) != NULL && (*p_2_head)-> _data < num נעקוב אחר הביטוי p_2_head :הוא מצביע *p_2_head .הוא האובייקט עליו p_2_headמצביע ,כלומר המצביע headשל התכנית הראשית(*p_2_head)-> . _dataהוא מרכיב הנתונים בקופסה עליה *p_2_headמצביע ,כלומר מרכיב הנתונים בקופסה עליה headמצביע ,כלומר הערך .5לכן התנאי: (*p_2_head)-> _data < numאינו מתקיים )ערכו של numהוא ,(3ולכן איננו נכנסים אף לא לסיבוב יחיד בלולאה .ערכו של *p_2_headהוא עדיין המצביע headשל התכנית הראשית )כפי שהיה עת הפונקציה נקראה( .ברמה העקרונית הסיבה לכך היא שאת הנתון הנוכחי עלינו להוסיף בראש הרשימה. 89 עתה אנו פונים לארבע הפקודות שאחרי לולאת קידומו של .p_2_headהראשונה מבין הארבע מקצה קופסה חדשה על הערמה ,ומפנה את המצביע )הרגיל ,שטיפוסו * temp (struct Nodeלהצביע על הקופסה המוקצית )מחמת מיאוס( .הפקודה השניה מבין הארבע שותלת בשדה ה _data -של הקופסה את הערך .3הפקודה השלישית מפנה את temp-> _nextלהצביע כמו .*p_2_headלאן מצביע *p_2_head ? *p_2_headהוא המצביע headשל התכנית הראשית ,אשר מצביע על הקופסה של .5לכן עתה גם temp-> _nextמצביע על הקופסה של .5מצב הזיכרון הוא: = head =data 5 = head num= 3 =data 3 = p_2_head num= 3 =temp הפקודה הרביעית ,והאחרונה ,מפנה את ,*p_2_headכלומר את המצביע של התכנית הראשית ,להצביע כמו ,tempכלומר על הקופסה של ) 3זו שנוצרה אך זה עתה( .מצב הזיכרון הוא: head = head =data 5 = head num= 3 =data 3 = p_2_head num= 3 =temp אנו רואים כי 3הוסף בראש הרשימה .כפי שראוי היה .בכך מסתיימת .insert אנו שבים ל build_list -אשר קוראת את הערך ,3879ומזמנת שוב את .insertציור הזיכרון הוא כמו בציור האחרון ,פרט לכך שערכו של numבשתי רשומות ההפעלה הוא .3879 90 הפונקציה insertמתחילה להתבצע .התנאי ) (*p_2_head == NULLאינו מתקיים ,שכן *p_2_headהוא המצביע headשל התכנית הראשית ,וערכו אינו .NULLאנו מתקדמים ללולאה שהתנאי בראשה הוא: ) ( (*p_2_head) != NULL && (*p_2_head)-> _data < num שני המרכיבים של התנאי מסופקים .עבור השמאלי הדבר ברור .עבור הימני: *p_2_headהוא המצביע ,headולכן (*p_2_head)-> _dataהוא הערך ,3 וערך זה קטן מערכו של ) numשהינו .(3879על-כן אנו נכנסים לגוף הלולאה. הפרמטר p_2_headעובר להצביע על מצביע חדש .על איזה מצביע? נבדוק: (*p_2_head)-> _nextהוא המצביע _ nextבקופסה עליה מצביע ,*p_2_headכלומר המצביע _ nextבקופסה עליה מצביע ,headכלומר המצביע שמצביע על הקופסה של .5המצביע למצביע p_2_headמקבל את כתובתו של מצביע זה )בשל ה & -שנכתב לפני שמו של המצביע .( (*p_2_head)->next מבחינה ציורית המצב הוא: = head =data 5 = head num=3879 =data 3 = p_2_head num= 3879 =temp כלומר p_2_headמצביע על :המצביע שמורה על הקופסה של .5לקראת כניסה לסיבוב נוסף בלולאה אנו שוב בודקים את התנאי ,ושוב הוא מסופק ,על כן p_2_headעובר להצביע על המצביע .(*p_2_head)->nextנבדוק מיהו מצביע זה *p_to_head :הוא המצביע המורה על הקופסה של .5על כן (*p_2_head)-> _nextהוא שדה המצביע בקופסה עליה מצביע ,*p_2_head כלומר המצביע בקופסה של ) 5שערכו הוא .(NULLמבחינה ציורית מצב הזיכרון הוא: = head =data 5 = head num=3879 =data 3 91 = p_2_head num= 3879 =temp לקראת כניסה לסיבוב נוסף בלולאה אנו שוב בודקים את התנאי: ) ( (*p_2_head) != NULL && (*p_2_head)-> _data < num אולם המרכיב השמאלי בו אינו מתקיים :ערכו של המצביע *p_2_headאינו שונה מ .NULL -על כן איננו נכנסים לסיבוב נוסף בלולאה .אנו פונים לארבע הפקודות שאחרי הלולאה .השתיים הראשונות בניהן מביאות אותנו למצב: = head =data 5 = head num=3879 =data 3 =data 3879 = p_2_head num= 3879 =temp הפקודה השלישית היא . temp->next = (*p_2_head); :ערכו של המצביע ) *p_2_headכלומר המצביע עליו מצביע ,(p_2_headהוא .NULLעל-כן גם ערכו של temp->nextיהיה .NULLהפקודה הרביעית היא(*p_2_head) = temp; : .המצביע )(*p_2_headהוא המצביע בקופסה של ,5והוא עובר להצביע לאותו מקום כמו ,tempכלומר לקופסה של .3879מצב הזיכרון הוא: = head =data 5 = head num=3879 =data 3 =data 3879 = p_2_head num= 3879 =temp ושוב הרשימה ממוינת כנדרש. את המעקב אחרי תהליך הוספתו של 17לרשימה אשאיר לכם כתרגיל. לסיכום ,אנו רואים שבאמצעות שימוש במצביע למצביע קיצרנו את הפונקציה ,insertאשר אינה משתמשת עוד בזוג מצביעים ,rear, frontואינה מחייבת התייחסות למקרה הקצה בו יש להוסיף איבר בראש הרשימה הממוינת .מנגד הפונקציה שכתבנו קשה יותר להבנה. 92 13.10הערה בנוגע לconst- עת למדנו להעביר מערך כפרמטר לפונקציה אמרנו שבכוחה של הפונקציה לשנות את ערכם של תאי המערך .לכן אם בתכנית הוגדר, int a[3]={17,17,17}; : והוגדרה: )][void f1(int arr { } ; arr[0] = 0; arr[1] = 1 אזי ערכו של ] a[0לפני הקריאה לפונקציה הוא ,17וערכו אחרי ביצוע הפונקציה, עבור הקריאה f1(a); :הוא אפס. בהמשך ,ראינו כי אם ברצוננו למנוע מפונקציה לשנות את ערכם של תאי מערך המועבר לה כפרמטר ,אזי אנו יכולים להוסיף לפני שם הפרמטר את מילת המפתח .constהוספת מילת המפתח תמנע מהפונקציה לשנות את ערכם של תאי המערך. אמרנו כי האפקט אינו זהה לזה של פרמטר ערך ,שכן את ערכו של פרמטר ערך הפונקציה רשאית לשנות; )אולם הערך החדש יוכנס ַלפרמטר בלבד ,ולא ַלארגומנט המתאים(. עוד ראינו כי עת מערך מועבר כפרמטר לפונקציה אנו רשאים לתאר את הפרמטר גם באופן הבא: } … { )void f2(int *ip עתה נשאל את עצמנו :נניח שברצוננו למנוע מ f2 -לשנות את ערכם של תאי המערך עליו מצביע )פרמטר הערך( ,ipכיצד נעשה זאת? התשובה היא :כמו קודם ,כלומר נוסיף את מילת המפתח constלפני שם הטיפוס: } … { )void f2(const int *ip אם עתה f2תכלול פקודה כגון ip[0]=0; :אזי התכנית לא תתקמפל בהצלחה. שאלה :האם בפונקציה f2אנו רשאים לשנות את ערכו של ,?ipכלומר ,האם פקודה כגון ip = NULL; :או ;]ִ ip = new int[5ת ְמנע מהפונקציה להתקמפל בהצלחה? תשובה :לא; מילת המפתח constהמופיעה לפני שם הטיפוס רק מונעת מהפונקציה לשנות את ערכם של תאי המערך עליו מצביע ,ipהיא אינה מונעת מאתנו לשנות את ערכו של .ipנשים לב כי בפונקציה f2הפרמטר ipהוא פרמטר ערך ,ועל-כן שינויים שהפונקציה תכניס לתוכו לא יוותרו בארגומנט המתאים ל ip -אחרי ש f2 -תסתיים. נדגים את מה שהסברנו באמצעות הפונקציה .strlenבעבר ראינו את הגרסה הבאה של הפונקציה: { )int strlen(char *s ; int i )for (i = 0; *s != '\0’; i++, s++ ; ; return i } הפונקציה מקבלת מצביע לתו .היא מגדירה משתנה עזר ,iורצה על הסטרינג המועבר לה כל עוד התו עליו מצביע sשונה מ .‘\0’ -בכל סיבוב בלולאה הפונקציה גם מגדילה את ערכו של ,iוכך היא מונה כמה פעמים הסתובבנו בלולאה ,במילים אחרות מה אורכו של הסטרינג .ערך זה הפונקציה מחזירה .שימו לב כי גוף הלולאה כולל את הפקודה הריקה ,וזאת משום שכל העבודה שיש לבצע נעשית בכותרת הלולאה ,אשר גם מקדמת את iואת .s 93 הפונקציה strlenאינה אמורה לשנות את תוכנו של הסטרינג המועבר לה .מאידך, בגרסה שאנו כתבנו הפונקציה משנה את ערכו של הפרמטר ,sשכן בכל סיבוב בלולאה היא מסיטה אותו לתא הבא במערך )באמצעות הפקודה s++שהינה אריתמטיקה של מצביעים ,המקדמת את המצביע sתא אחד הלאה במערך( .האם אנו מעונינים להוסיף את מילת המפתח constלפני הפרמטר sשל ?strlenהאם אנו רשאים להוסיף את מילת המפתח constלפני הפרמטר sשל ?strlen התשובה לשתי השאלות היא :כן .מילת המפתח constתמנע מהפונקציה לשנות את ערכם של תאי המערך ,היא לא תמנע ממנה לשנות את ערכו של המצביע. עתה ישאלו השואלים :ומה קורה אם ברצוננו למנוע מהפונקציה גם לשנות את ערכו של המצביע? התשובה היא שגם לכך יש פתרון .ראשית ,נזכיר כי שינוי ערכו של מצביע שהינו פרמטר ערך אינו משפיע על הארגומנט המתאים ,ועל-כן נזק נוראי הוא לא יגרום .שנית אם נתאר את הפרמטר באופן הבא char * const s :אזי בכך אנו מונעים מהפונקציה לשנות את ערכו של המצביע )אך לא את תוכנם של תאי הזיכרון עליהם המצביע מורה( .אנו רשאים גם לשלב את שתי ההגבלות: ,const char * const sובכך נמנע מהפונקציה לשנות הן את ערכו של המצביע ,והן את ערכם של תאי הזיכרון עליהם המצביע מורה. עתה נבדוק את התסריט הבא) :א( פרמטר כלשהו ,בפונקציה כלשהי ,הוגדר באופן הבא) ,const int *ip :שכן רצינו למנוע מהפונקציה לשנות את ערכו של המערך אחר הגדרנו משתנה לוקלי , int *temp = ip; :כלומר עליו מצביע ) ,(ipב( ַ הגדרנו מצביע tempוהפננו אותו להצביע על המערך עליו מורה ) ,ipג( לבסוף כתבנו . *temp = 0; :מה עשינו? 'עבדנו' על התכנית ,ושינינו את ערכו של התא מספר אפס במערך לא דרך הפרמטר ,ipאלא באמצעות המצביע .tempהאומנם? פריֵרית .אם tempהוגדר כ ,int *temp -אזי השפה תמנע לא ,ולא! שפת Cאינה ַ מכם לבצע את ההשמה , temp = ip; :וזאת בדיוק כדי למנוע מכם לבצע תעלולים )במילים אחרות שטויות( כפי שתיארנו קודם לכן .ומה קורה אם ברצונכם בכל אופן להכניס את ערכו של ipלמשתנה ?tempהגדירו את tempבאופן הבא: ; . const int *tempעתה תוכלו להשים את ערכו של ipלתוך ,tempאך בשל הדרך בה הוגדר tempכבר לא תוכלו לבצע השמה. *temp = 0; : העקרונות שחלים ביחס למצביעים ,מערכים ו ,const -תקפים גם לגבי רשימות משורשרות .נבחן לדוגמה את הפונקציה disply_listשכתבנו בעבר ,ואשר אמורה להציג את הערכים השמורים ברשימה מקושרת: { ) void display_list( struct Node *head { )while (head != NULL ; cout << head -> _data ; head = head -> _next } } ראשית נשים לב כי אם מתכנת לא אחראי יוסיף מייד בתחילת הפונקציה את הפקודה head = NULL; :אזי הפונקציה לא תפעל כהלכה ,אולם היא לא תפגע ברשימה ה מקושרת שנבנתה .שינוי שהוכנס ְלפרמטר הערך של הפונקציה לא ישפיע על הארגומנט המתאים ,והאחרון ימשיך להצביע על האיבר הראשון ברשימה שבפונקציה אנו מקדמים את הפרמטר headללא חשש שנבנתה; זו גם הסיבה ַ שנפגע בארגומנט ִעמו הפונקציה מזומנת. אולם ,אם המתכנת הלא אחראי יכתוב את הפקודהhead-> _next = NULL; : בעינה גם אחרי ביצוע הפונקציה; וזאת משום ש- אזי הפגיעה שהוא מבצע תישאר ֵ 94 head-> _nextהוא מצביע המצוי בתאי הרשימה )המוקצים על הערמה( .ותאים אלה אינם משוכפלים עם הקריאה לפונקציה .עם הקריאה לפונקציה משוכפל רק הפרמטר .headשינוי שמתבצע על head-> _nextחל על המצביע האחד והיחיד היוצא מהאיבר הראשון ברשימה ,ואין דרך לשחזר את ערכו של מצביע זה. האם יש ביכולתנו לגרום לכך שהוספתה של פקודה כגוןhead-> _next = : ;ַ NULLלפונקציה display_listתמנע מהתכנית להתקמפל בהצלחה ,ועל-ידי כך תשמור עלינו מפני מתכנתים לא אחראיים? התשובה היא :כן! את הפרמטר של הפונקציה נגדיר כ . const struct Node *head :בכך אנו מונעים הכנסת שינויים לתאי הזיכרון עליהם headמצביע ,אך איננו מונעים הכנסת שינויים לערכו headעצמו ,וטוב שכך ,שכן בפונקציה אנו משנים את ערכו של head של באמצעות הפקודה. head = head -> _next; : בחלק ניכר מהפונקציות שכתבנו ניתן היה להגדיר את הפרמטר כconst - . struct Node *headאתם מוזמנים לסקור את הפונקציות השונות שהוגדרו בפרק זה ,ולאתר את אותן בהן היה ראוי לקבוע את הפרמטר כמצביע לקבוע. 13.11תרגילים התרגילים המופיעים להלן נלקחו כולם מבחינות ומתרגילים שניתנו על-ידי מורים במוסדות להשכלה גבוהה שונים .על-כן התרגילים אינם קלים ,ומחייבים השקעה מרובה הן של מחשבה והן של עבודה. 13.11.1 תרגיל מספר אחד :רשימות משורשרות של משפחות בתרגיל זה נדון ברשימות משורשרות של משפחות משולשלות )במובן שושלות(. כל משפחה בתרגיל תהיה משפחה גרעינית הכוללת לכל היותר אם ,אב ומספר כלשהו של ילדים .נתוני משפחה יחידה ישמרו במבנה מהטיפוס: struct family { ; char *family_name char *mother_name, *father_name ; // parents' names ; child *children_list // list of the children in this family ; family *next_family // pointer to the next family in our database ;} נתוני כל ילד ישמרו ב: struct child { ; char *child_name ; child *next_child ;} מבנה הנתונים המרכזי של התכנית יהיהfamily *families : התכנית תאפשר למשתמש לבצע את הפעולות הבאות: .10הזנת נתוני משפחה נוספת .לשם כך יזין המשתמש את שם המשפחה )וזהו מרכיב חובה( ,ואת שמות בני המשפחה .במידה והמשפחה אינה כוללת אם יזין המשתמש במקום שם האם את התו מקף )) (-המרכיב mother_nameבמבנה המתאים יקבע אז ,כמובן ,להיות ,(NULLבאופן דומה יצוין שאין אב במשפחה, גם סיום רשימת שמות הילדים יצוין באופן זה )הרשימה עשויה ,כמובן ,להיות 95 ריקה( .לא תיתכן משפחה בה אין לא אם ולא אב )יש לתת הודעת שגיאה במידה וזה הקלט מוזן ,ולחזור על תהליך קריאת שמות ההורים( .אתם רשאים להגדיר משתנה סטטי יחיד מסוג מערך של תווים לתוכו תקראו כל סטרינג מהקלט .את רשימת המשפחות יש להחזיק ממוינת על-פי שם משפחה .את רשימת ילדי המשפחה החזיקו בסדר בה הילדים מוזנים .הניחו כי שם המשפחה הוא מפתח קבוצת המשפחות ,כלומר לא תתכנה שתי משפחות להן אותו שם משפחה )משמע ניסיון להוסיף משפחה בשם כהן ,עת במאגר כבר קיימת משפחת כהן מהווה שגיאה( .אין צורך לבדוק כי לא מוזנים באותה משפחה שני ילדים בעלי אותו שם. דוגמה לאופן קריאת הנתונים: Enter family name: Cohen Enter father name: Yosi Enter mother name:Enter children's names: Dani Dana - .11הוספת ילד למשפחה .יוזן שם המשפחה ושמו של הילד .הילד יוסף בסוף רשימת הילדים .במידה ומשפחה בשם הנ"ל אינה קיימת יש להודיע על-כך למשתמש )ולחזור לתפריט הראשי(. .12פטירת בן משפחה .יש להזין) :א( את שם המשפחה) ,ב( האם מדובר באם )ולשם כך יוזן הסטרינג ,(motherבאב )יוזן ,(fatherאו באחד הילדים )יוזן ) (childג( במידה ומדובר בילד יוזן גם שם הילד שנפטר .במידה ואין במאגר משפחה כמצוין ,או שבמשפחה אין בן-משפחה כמתואר )למשל אין אב והתבקשתם לעדכן פטירת אב ,או אין ילד בשם שהוזן( יש לדווח על שגיאה )ולחזור לתפריט הראשי( .כמו כן לא ניתן לדווח על פטירת הורה במשפחה חד-הורית )ראשית יש להשיא את המשפחה למשפחה חד-הורית אחרת ,כפי שמתואר בהמשך ,ורק אז יוכל ההורה המתאים לעבור לעולם שכולו טוב(. .13נשואי שתי משפחות קיימות .יש לציין את שמות המשפחות הנישאות ,ואת שם המשפחה החדש שתישא המשפחה המאוחדת .פעולה זאת לגיטימית רק אם אחת משתי המשפחות אינה כוללת אם בעוד השניה אינה כוללת אב .בעקבות ביצוע הפעולה תאוחדנה רשימות הילדים )ראוי לעשות זאת ע"י מניפולציה של מצביעים ,ללא הקצאת מבנים חדשים!( ,והמבנה שתיאר את אחת המשפחות ישוחרר. .14הצגת נתוני המשפחות בסדר עולה של שמות משפחה) .זהו ,כזכור ,הסדר בו מוחזקת רשימת המשפחות( .עבור כל משפחה ומשפחה יש להציג את שם המשפחה ,ואת שמות בני המשפחה. .15הצגת נתוני משפחה בודדת רצויה .יש להזין את שם המשפחה ויוצגו פרטי בני המשפחה) .במידה והמאגר אינו כולל משפחה כמבוקש תוצג הודעת שגיאה(. .16הצגת נתוני המשפחות בסדר יורד של מספר בני המשפחה .יש להציג את שם המשפחה ואת מספר בני המשפחה .לשם סעיף זה החזיקו רשימה מקושרת נוספת .כל איבר ברשימה יהיה מטיפוס: struct family_size { family *the_family ; // pointer to a family at the families linked list ; int size ; // number of children in this family family_size *next_families_sizes_item ; // pointer to next item in the current list } המצביע לראש הרשימה הזאת יהיה .family_size *families_sizesהרשימה תמוין בסדר יורד של המרכיב .sizeעל-ידי סריקה של רשימה זאת ופניה ממנה בכל פעם למשפחה המתאימה )תוך שימוש במצביע (the_familyתוכלו להציג את 96 המשפחות בסדר יורד של גודל המשפחות .שימו לב כי יש גם לתחזק רשימה זאת :עת חל שינוי במשפחה כלשהי ברשימת המשפחות ,ונניח כי המצביע p מצביע על אותה משפחה ,יש לסרוק את הרשימה על ראשה מצביע ,families_sizesעד שמאתרים מרכיב ברשימה זאת שהמצביע the_familyבו מצביע לאותו מקום כמו המצביע ,pזוהי המשפחה שגודלה שונה .עתה יש להזיז את המרכיב המתאים ברשימת גודלי המשפחות למקום המתאים )על-ידי הסרתו מהרשימה ,והוספתו במקום חדש( .בין כל המשפחות להן אותו גודל אין חשיבות לסדר ההצגה .תלמיד שלא יענה על סעיף זה ציונו המרבי בתרגיל יהיה ,90תלמיד שיענה על סעיף זה יקבל בונוס של עד 7נקודות ,ובלבד שציונו לא יעלה על מאה. .17הצגת נתוני שכיחות שמות ילדים .יש להציג עבור כל שם ילד המופיע במאגר, כמה פעמים הוא מופיע במאגר .אין צורך להציג את הרשימה ממוינת. )רמז\הצעה :סרקו את מאגר המשפחות משפחה אחר משפחה ,עבור כל משפחה עיברו ילד אחר ילד ,במידה ושמו של הילד הנוכחי עדיין לא מופיע ברשימת הילדים ששמם הודפס סיפרו כמה פעמים מופיע שם הילד במאגר הנתונים, ואחר הוסיפו את שמו של הילד לרשימת שמות הילדים שהוצגו(. .18סיום .בשלב זב עליכם לשחרר את כל הזיכרון שהוקצה על-ידכם דינמית) .עבור כל משפחה ומשפחה :יש לעבור על רשימת הילדים ולשחרר כל מרכיב ברשימה זאת ,לפני שמשחררים כל קודקוד ברשימת הילדים יש לשחרר את הזיכרון שמחזיק את שמו של הילד ,אחרי שמשחררים את רשימת הילדים יש לשחרר את הזיכרון שמחזיר את שמות ההורים ואת שם המשפחה(. הערות כלליות: ג .התכנית תנהל כלולאה בה בכל שלב המשתמש בוחר בפעולה הרצויה לו ,הפעולה מתבצעת )או שמוצגת הודעת שגיאה( ,והתכנית חוזרת לתפריט הראשי. ד .הקפידו שלא לשכפל קוד ,למשל כתבו פונקציה יחידה אשר מציגה נתוני משפחה ,והשתמשו בה במקומות השונים בהם הדבר נדרש .וכו'. Needless to sayשיש להקפיד על כללי התכנות המקובלים ,בפרט ובמיוחד מודולריות ,פרמטרים מסוגים מתאימים )פרמטרי ערך vsפרמטרים משתנים( ,ערכי החזרה של פונקציות ושאר ירקות .תכנית רצה אינה בהכרח גם תכנית טובה! )קל וחומר לגבי תכנית מקרטעת(… 13.11.2 תרגיל מספר שתיים :איתור פרמידה ברשימה בתכנית מוגדר: struct struct Node { ; int _data ; struct Node *next ; } ברשימה מקושרת נגדיר פרמידה באורך ,2n +1כסדרה של איברים רציפים ברשימה המסודרים באופן הבא n +1 :האיברים הראשונים בפרמידה מקיימים שהערך בכל איבר גדול מהערך באיבר שקדם לו ,ו n +1 -האיברים האחרונים ,n +1מקיימים שהערך בכל איבר קטן בפרמידה ,המתחילים בתא שמספרו מהערך באיבר שקדם לו. 97 א .כתבו שגרה המקבלת מצביע מטיפוס * struct Nodeלרשימה מקושרת ומחזירה מצביע לפרמידה הארוכה ביותר המוכלת ברשימה ,בפרמטר הפניה יוחזר אורך הפרמידה. לדוגמה ברשימה: 0 0 1 1 6 7 10 2 9 5 קיימות פרמידה באורך 5המורכבת מהאיברים ,0 1 7 6 2 :ושלוש פרמידות באורך 3המורכבות מהאיברים 0 1 0 ;1 7 6 ;2 10 9 במידה וקיימות כמה פרמידות מרביות באורכן ניתן להחזיר מצביע לאחת מהן. ב .מה זמן הריצה של השגרה שכתבתם? 13.11.3 תרגיל מספר שלוש :רשימה מקושרת דו-כיוונית נגדיר: struct struct Node { ; int _data ; struct Node *next, *prev_odd_even ;} בתכנית כלשהי ברצוננו לשמור רשימה מקושרת ממוינת אשר תתוחזק באופן הבא: המצביע _ nextבכל תא יצביע על האיבר הבא ברשימה. באיבר שהערך בו זוגי יצביע המצביע prev_odd_evenלאיבר הזוגי הקודם ברשימה; באיבר שהערך בו פרדי יצביע המצביע prev_odd_evenלאיבר הפרדי הקודם ברשימה. דוגמה לרשימה אפשרית )המצביע _ nextצויר בקו שלם prev_odd_even ,בקו מרוסק .מצביעים שערכם ] NULLכגון _ nextמהתא של ,8וכן prev_odd_even מהתאים של 2ושל [1הושמטו מהציור(: 8 4 7 2 1 כתבו פונקציה אשר מקבלת מצביע לרשימה מקושרת בעלת מבנה כמתואר, וערך שלם ,ומוסיפה את הערך לרשימה. הערה :חשיבות יתרה תינתן לשיקולי יעילות. 13.11.4 תרגיל מספר ארבע :רשימה מקושרת עם מיון כפול בתכנית מוגדר: struct info { 98 ; ]int _data[2 ; ]info *next[2 ;} בתכנית הוגדרו גם המצביעים ,info *head0, *head1ונבנתה רשימה מקושרת. הרשימה נבנתה כך שאם נפנה אליה דרך המצביע head0ואחר נתקדם לאורכה באמצעות המצביעים _ ] next[0בכל תא ,נסרוק את הרשימה כשהיא ממוינת על- פי השדה _] .data[0מנגד ,אם נפנה לרשימה דרך המצביע head1ואחר נתקדם לאורכה באמצעות המצביעים _ ] next[1בכל תא ,נסרוק את הרשימה כשהיא ממוינת על-פי השדה _].data[1 דוגמה לרשימה: data[0] =6 data[1] =17 = ]next[0 = ]next[1 data[0] =5 data[1] =1 = ]next[0 = ]next[1 data[0] =2 data[1] =19 = ]next[0 = ]next[1 head0 head1 מקושרת כנ"ל ,וכן כתבו פונקציה המקבלת זוג מצביעים לרשימה מערך ] .int new_data[2הפונקציה תוסיף לרשימה איבר חדש שיחזיק את ערכי ][ new_dataאם ורק אם לא קיים ברשימה איבר עם שדה _] data[0השווה בערכו ל new_data[0] -וכן לא קיים ברשימה איבר עם שדה ] data[1השווה בערכו ל- ]. new_data[1 13.11.5 תרגיל מספר חמש :רשימה מקושרת עם מיון כפול בתכנית מוגדר: struct info { ; int _data ; ]info *next[2 ;} בתכנית הוגדר גם המצביע ,info *headונבנתה רשימה מקושרת באופן הבא: א .המצביע ] next[0מורה על הנתון הבא ברשימה ,כלומר הנתון שהוכנס בעקבות הנתון הנוכחי )במילים אחרות אם נסרוק את הרשימה תוך התקדמות על המצביעים ] next[0בכל תא ,אזי נסרוק את הנתונים בסדר הכנסתם(. ב .המצביע ] next[1בכל תא מורה על התא הבא ברשימה בו קיים אותו ערך כמו בתא הנוכחי )כשאנו אומרים 'התא הבא' כוונתנו תא שנמצא בהמשך הרשימה מבחינת סדר המצביעים ] .(next[0במידה ולא קיים תא בהמשך בו מצוי אותו ערך ,יצביע המצביע בתא לאיבר הראשון ברשימה בו קיים הערך )כשאנו אומרים 'ראשון ברשימה' אנו מתכוונים :ראשון אם נסרוק את הרשימה באמצעות ] .(next[0בפרט ,אם ברשימה קיים איבר יחיד בו יש ערך xכלשהו, אזי ] next[1באותו תא יצביע על התא עצמו. 99 דוגמה לרשימה :סדר הכנסת הערכים) 2 :שבתא השמאלי() 2 ,17 ,הימני( data = 2 = ]next[0 = ]next[1 data = 17 = ]next[0 = ]next[1 data = 2 = ]next[0 = ]next[1 head כתבו פונקציה המקבלת מצביע לרשימה מקושרת כנ"ל ,וערך שלם ,val הפונקציה תוסיף לרשימה איבר חדש שיחזיק את הערך .val 13.11.6 תרגיל מספר שש :מאגר ציוני תלמידים כתבו תכנית המטפלת בציוני תלמידים .לכל תלמיד תחזיק התכנית את הנתונים הבאים) :א( שם משפחה) ,ב( שם פרטי) ,ג( רשימת הקורסים שהתלמיד לקח ,כאשר לכל קורס יוחזק (1) :שם הקורס (2) ,כמות השעות השבועיות המוקצות לקורס )(3 ציונו של התלמיד בקורס. התכנית תאפשר למשתמש לבצע את הפעולות הבאות: .1תוספת תלמיד חדש לרשימת התלמידים )במידה וברשימה כבר קיים תלמיד שזה שמו תתקבל הודעת שגיאה(. .2מחיקת תלמיד מרשימת התלמידים )במידה והתלמיד המבוקש לא קיים ברשימה תתקבל הודעת שגיאה(. .3הוספת קורס נוסף לתלמיד המצוי ברשימה )במידה והתלמיד כבר רשום לאותו קורס תינתן הודעת שגיאה(. .4ביטול קורס לו רשום תלמיד כלשהו )במידה והתלמיד אינו מופיע ברשימה ,או שהתלמיד אינו רשום לאותו קורס תינתן הודעת שגיאה(. .5עדכון ציון של תלמיד כלשהו בקורס כלשהו )הודעות שגיאה בהתאם(. .6הצגת רשימת הקורסים והציונים עבור תלמיד מבוקש כלשהו; הרשימה המוצגת תחולק לשניים ראשית יוצגו הקורסים אותם התלמיד עבר ,ואחר הקורסים בהם התלמיד נכשל .מעבר להדפסת הרשימה הנ"ל יודפס גם ממוצע ציוני התלמיד )ממוצע משוקלל שישקלל עבור כל קורס את משקלו == מספר השעות השבועיות המוקצות לקורס( ,ומספר הקורסים בהם התלמיד נכשל. מבנה הנתונים המרכזי בתכנית יוגדר באופן הבא: struct course // holds info about a single course { ; char *course_name ; int grade ;} struct stud // holds info about a single student { ; char *last, *first course *courses ; // array of courses stud took stud *next_stud ; // pointer to the next student in // the students’ list ; } 100 פי שם משפחה-התכנית תחזיק רשימה מקושרת ממוינת של התלמידים )ממוינת על רשימת הקורסים של כל תלמיד תוחזק כמערך ממוין )על פי שם.( שם פרטי+ קורס( שיוקצה באופן דינמי ויוגדל בכל פעם שיש להוסיף קורס נוסף ואין די מקום .במערך בגודלו הנוכחי :התכנית הראשית תראה לערך כך void main() { stud *class ; // pointer to the list of students ... prototypes should be declared here... init(class) ; do { int option = get_option() ; switch (option) { case ADD_STUD : add_stud(&class) ; break ; case REM_STUD : rem_stud(&class) ; break ; case ADD_COURSE : add_course(class) ; break ; case REM_COURSE : rem_course(class) ; break ; case UPDATE_GRADE : update_grade(class) ; break ; case DISPLAY_GRADES : display_grades(class) ; break ; case END : dispose(&class) ; break } } while (option != END) ; } : תראהadd_stud :השגרה void add_stud(stud **my_class) { void get_stud_name( char **last_name, char **first_name) ; void add_stud_to_list(stud **a_class, char *a_last, char *a_first) ; char *last_n, *first_n ; get_stud_name(&last_n, &first_n) ; // read last/first name from user add_stud_to_list(my_class, last_n, first_n) ; } ...ואידך זיל גמור מיון הכנסה של רשימה מקושרת:תרגיל מספר שבע 13.11.7 :בתכנית מוגדר struct info { int _data ; info *next ; 101 ;} א .כתבו פונקציה אשר מקבלת כפרמטר מצביע לראשה של רשימה מקושרת כנ"ל .על הפונקציה למיין את אברי הרשימה בסדר עולה .בתום פעולתה של הפונקציה יצביע הפרמטר לראשה של הרשימה הממוינת. מיון הרשימה יתבצע באופן הבא :במעבר מספר iעל הרשימה )…(i = 1, 2, 3, יאותר האיבר ה-i -י בקוטנו )לדוגמה :במעבר הראשון יאותר האיבר הכי קטן ,במעבר השני יאותר האיבר השני הכי קטן ,וכו'( ,ויועבר למקום ה-i -י ברשימה )לדוגמה :האיבר השני הכי קטן יועבר למקום השני ברשימה(. הערות) :א( הקפידו על יעילות הפונקציה שתכתבו) .ב( עדיף שהפונקציה לא תקצה תאים חדשים! )ג( אין להניח דבר על תכולתה של הרשימה המועברת לפונקציה) .ד( כרגיל ,תנו דעתכם למודולריות. ב .מה זמן הריצה של הפונקציה שכתבתם? הסבירו בקצרה. 13.11.8 תרגיל מספר שמונה :פולינומים בפרק שש הצגנו תכנית המטפלת בפולינומים .בפרק אחת-עשרה שינינו את מבנה הנתונים באמצעותו יישמרו הפולינומים לכדי מערך של מבנים מטיפוס .monom עתה ברצוננו להכניס שינוי נוסף במבנה הנתונים ,ולייצג כל פולינום כרשימה מקושרת של מונומים .נוכל לעשות זאת באופן הבא: { struct monom ; float coef, degree ; } כלומר מונום מיוצג כמו בפרק אחת-עשרה. ייצוגו של פולינום יהיה: { struct polynom ; monom a_monom ; polynom *next_monom ;} סדרת פולינומים תיוצג באופן הבא: { list_of_polynoms ; polynom *a_polynom ; list_of_polynoms next_poly ; } ובתכנית נחזיק משתנהlist_of_polynoms *database; : אתם מוזמנים ,ראשית ,להתעמק במבנה הנתונים ולהבינו ,ושנית לכתוב את תכנית הפולינומים תוך שימוש במבנה נתונים זה. 13.11.9 תרגיל מספר תשע :הפרדת רשימות משורשרות שהתמזגו בתכנית כלשהי מוגדר struct Nodeכפי שמוכר לנו היטב ,והוגדרו ;.Node head1, head2 102 struct בתכנית עשוי לקרות מצב בו שתי הרשימות המשורשרות עליהן מצביעים head1, head2התמזגו החל ממקום מסוים ,כלומר קיימים איברים השייכים הן לרשימה עליה מצביע head1והן לרשימה עליה מצביע .head2 לדוגמה: 3 2 5 2 1 1 עליכם לכתוב פונקציה המקבלת שני מצביעים לשתי רשימות ,ואשר מפרידה את הרשימות באופן הבא :כל התאים השייכים )גם( לרשימה עליה מצביע הפרמטר הראשון ישויכו ,בעקבות ביצוע הפונקציה ,אך ורק לרשימה זאת .כל האיברים השייכים רק לרשימה עליה מצביע הפרמטר השני ישויכו לרשימה עליה מורה פרמטר זה. בדוגמה שלנו ,תיראנה הרשימות בתום ביצוע הפונקציה באופן הבא: 3 2 5 2 1 1 13.11.10 תרגיל מספר עשר :בדיקת איזון סוגריים מחסנית היא מבנה נתונים )במילים אחרות משתנה( המתנהל על-פי העיקרון הבא: בקצה המחסנית )כלומר בקצה המשתנה ,ולא באמצעו( .כל כל איבר חדש מוסף ְ איבר שמוסר מהמחסנית מוסר מאותו קצה אליו מוּספים הנתונים החדשים )ועל כן המחסנית מתנהלת בשיטה 'אחרון שנוסף הוא הראשון שמוסר' ובלע"ז last in first outובקיצור .LIFOנקל לממש מחסנית באמצעות רשימה מקושרת :כל איבר חדש יתווסף בראש הרשימה; בעת שיש להסיר איבר שולפים את זה המצוי בראש הרשימה. כתבו תכנית אשר קוראת ביטוי חשבוני שעשוי להכיל סוגריים ממסר סוגים) :א( ) (, )ב( ] [) ,ג( } {) ,ד( > < .התכנית תבדוק האם הסוגריים בביטוי מאוזנים כהלכה. )התכנית לא תתעניין כלל ביתר מרכיבי הביטוי פרט לסוגריים. לדוגמה הביטוי (5+3)*< (2+2) / ([6 / 7] + 2) > -6 :הוא ביטוי תקין ,אך הביטויים הבאים אינם תקינים: )א( ) (5+3*< (2+2) / ([6 / 7] + 2) > -6בביטוי זה נשמט הסוגר הימני שהופיע בביטוי הראשון מייד אחרי ה.(3 - )ב( )ב( ) (5+3)*< (2+2) / ([6 / 7] + 2) ) –6בביטוי זה הסוגר הימני הזוויתי שהופיע בביטוי המקורי לפני 6- -הוחלף בסוגר מעוגל(. 103 )ג( )ג( ) (5+3)*< (2+2) / ([6 / 7) + 2] > -6בביטוי זה הסוגר המרובע שהופיע במקור אחרי ה ,7 -הומר בסוגר מעוגל ,והסוגר המעוגל שהופיע אחרי ה 2 -השלישי הומר בסוגר מרובע. )ד( (.)5+5 האלגוריתם לבדיקת ביטוי אותו עליכם לממש הוא כמתואר: קרא את הביטוי מרכיב אחר מרכיב )כאשר מארכיב עשוי להיות :מספר טבעי כלשהו ,סימן פעולה ,או סוגר(: א .במידה ונתקלת בסוגר פותח מסוג כלשהו דחף אותו על-גבי המחסנית. ב .במידה ונתקלת בסוגר סוגר מסוג כלשהו בדוק האם בראש המחסנית מצוי סוגר פותח מאותו סוג; אם אכן מצוי סוגר כנדרש אזי שלוף אותו מהמחסנית, אחרת :הודע על שגיאה. בתום קריאת הביטוי בדוק האם המחסנית ריקה ,אם לא הודע על שגיאה. הניחו כי כל ביטוי יוזן בשורה יחידה ,וכי לא יוזנו שני ביטויים שונים באותה שורה. 13.11.11 תרגיל מספר אחת-עשרה :המרת תא ברשימה ברשימה בתכנית הוגדר המבנה nodeכמקובל. כתבו את הפונקציה replaceאשר מקבלת: א .מצביע head_mainלראשה של רשימה מקושרת אחת. ב .מצביע head_secondaryלראשה של רשימה מקושרת שניה. ג .מספר שלם .wanted הפונקציה תחליף כל תא המכיל את הערך wantedברשימה עליה מצביע head_mainבאברי הרשימה .head_secondary לדוגמה :אם head_mainמצביע על הרשימה: 5 2 7 ו head_secondary -מצביע על הרשימה: 2 3 9 6 וערכו של wanedהוא שתיים ,אזי בתום פעולה של הפונקציה יצביע head_main על הרשימה: 5 9 6 7 9 6 3 13.11.12תרגיל מספר שתים-עשרה :מחיקת איבר מינימלי מסדרות המרכיבות רשימה מקושרת בתכנית הוגדר struct struct Nodeכמקובל ,ונבנתה רשימה מקושרת .אנו אומרים כי הרשימה ה מקושרת מורכבת ממספר סדרות של מספרים .כל סדרה מסתיימת בערך אפס )שאינו חלק מהסדרה( ,ובתא שאחר-כך מתחילה הסדרה הבאה. 104 לדוגמה :הרשימה ה מקושרת הבאה: 0 1 2 5 0 2 9 2 0 5 מורכבת מהסדרות) :א( ) , 5ב( ) ,2 ,9 ,2ג( .1 ,2 ,5 כתבו פונקציה המקבלת מצביע לרשימה מקושרת כנ"ל ,ומוחקת את האיבר המזערי בכל סדרה .במידה ובסדרה כלשהי מופיע האיבר המזערי מספר פעמים יש למחוק את המופע הראשון שלו .במידה ואחרי מחיקת האיבר המזערי הסדרה התרוקנה )שכן הוא היה האיבר היחיד בסדרה( ,יש למחוק גם את האיבר המכיל את הערך אפס ,והמציין את תום הסדרה. לדוגמה :אחרי ביצוע הפונקציה תראה הרשימה הנ"ל באופן הבא: 0 2 105 5 0 2 9 עודכן 1/2011 14עצים בינאריים עץ בינארי הוא אמצעי לאחסון נתונים .אנו אומרים כי עץ בינארי הוא מבנה נתונים מופשט ) ,(Abstract Data Type, ADTאותו נוכל לממש בתכנית מחשב בדרכים שונות. 14.1הגדרת עץ בינארי עת בינארי מוגדר באופן הבא: א .עץ בינארי עשוי להיות ריק. ב .עץ בינארי שאינו ריק מכיל: .1שורש )שהינו צומת בעץ(. .2ילד שמאלי שהינו עץ בינארי. .3ילד ימני שהינו עץ בינארי. ג .כל צומת ) (nodeבעץ בינארי הוא ילד של צומת אחד בדיוק ,פרט לצומת יחיד, הקרוי שורש העץ ,שאינו ילד של אף צומת אחר) .יש המשתמשים במילה 'קודקוד' במקום במילה 'צומת' .בעבר ,עת העולם היה פחות תקין פוליטית, נהגו להשתמש במונחים אב ובנים בקשר לעץ ,כיום הוחלפו הכינויים להורה וילדים(. אנו רואים כי הגדרת עץ בינארי היא רקורסיבית :עץ בינארי מוגדר תוך שימוש במונח עץ בינארי; אולם בשלב זה של חיינו הדבר אינו גורם לנו למורא )קל וחומר שלא למשוא פנים( ,שכן יש גם מקרה קצה :העץ עשוי להיות ריק. נביט בדוגמה של עץ בינארי בה כל צומת מכיל נתון מספרי: 17 77 13 83 נבדוק האם ומדוע זהו עץ בינארי? א .הוא אינו ריק ,לכן עליו להכיל שורש ,ילד שמאלי שהוא עץ בינארי ,וילד ימני שהינו עץ בינארי .הצומת המכיל את 17הוא שורש העץ ,ועתה כדי להשלים את תהליך ההצדקה יש להראות כי ילדו השמאלי של הצומת של ,17וילדו הימני של הצומת של 17הם עצים בינאריים .ילדו השמאלי של השורש הוא תת-העץ: 13 וילדו הימני של השורש הוא תת-העץ: 83 106 77 ב .עתה נראה כי האובייקט: 13 הוא עץ בינארי .הוא כולל שורש ,שהינו הצומת של ,13ושני הילדים שהינם ריקים וככאלה הם עצים בינאריים. ג .עתה נבחן את: 77 83 הוא אינו ריק .הוא כולל שורש שהינו הצומת של ,77ילד שמאלי שהינו ריק )ועל-כן הוא עץ בינארי( ,ילד ימני שכולל את :הצומת של ,83ושני ילדים ריקים. הילד הימני של 77הוא עץ בינארי מאותה סיבה שתת-העץ שכלל את 13הוא עץ בינארי .הילד השמאלי של 77הוא עץ בינארי שכן הוא ריק ,ולכן גם כל הציור שבסעיף ג' הוא עץ בינארי. מעבר למה שבדקנו נשים לב כי כל צומת בעץ הוא ילד של צומת אחד בדיוק ,פרט לצומת של 17שאינו ילד של אף צומת אחר בעץ. 107 נשים לב כי ארבעת הבאים אינם עצים בינאריים ,ואני מזמין אתכם לבחון מדוע כל אחד ואחד מהם אינו עונה על ההגדרה: 77 57 83 17 77 17 83 57 77 57 88 83 17 77 83 17 עצים בינאריים נקראים 'בינאריים' שכן לכל צומת בהם ייתכנו לכל היותר שני ילדים. עץ בינארי יקרא עץ חיפוש בינארי ) binary search treeאו בקיצור (BSTאם הוא מקיים את התנאי הבא :עבור כל צומת nבעץ מתקיים שכל הערכים המצויים בתת העץ השמאלי של nקטנים או שווים מהערך המצוי בצומת ,nוכל הערכים המצויים בתת-העץ הימני של nגדולים מהערך המצוי ב .n -העץ הראשון שהצגנו הוא עץ חיפוש בינארי :בתת-העץ השמאלי של הצומת של 17מצוי ערך קטן מ ,17 -ובתת העץ הימני ערכים גדולים מ .17 -גם עבור הצומת של 77בתת-העץ הימני מצוי ערך גדול מ .77 -לו היינו ממירים את הערך 83בערך 53אזי העץ כבר לא היה עץ חיפוש שכן בתת-העץ הימני של 77כבר לא היו רק ערכים גדולים מ) .77 -אעיר כי מסיבות שונות יש המאפשרים לערך השווה לערך המצוי בשורש של עץ חיפוש בינארי להימצא בתת העץ השמאלי או בתת העץ הימני; אצלנו ערך שווה יימצא בהכרח בתת העץ השמאלי( .תלימידים לעתים מכנים עץ חיפוש בינארי בשם עץ ממוין; זה אינו שם מופרך ,אך גם לא מקובל באופן 'רשמי'. 14.2מימוש עץ בינארי בתכנית מחשב עד כה הצגנו את העץ הבינארי בצורתו המופשטת ,עתה נפנה לשאלה כיצד נייצג את העץ בתכנית מחשב בשפת ?Cבשלב הנוכחי של חיינו המשותפים רבים מכם ודאי יציעו לעשות זאת באמצעות מצביעים .זו אכן תשובה מתאימה ,ומייד נפנה להציגה בצורה מלאה; אולם ראשית נִ ראה כי ניתן לייצג עצים בינאריים גם באמצעות 108 מערכים; כלומר את מבנה הנתונים המופשט 'עץ בינארי' ניתן לממש בתכנית מחשב במספר אופנים. כיצד ימומש עץ בינארי באמצעות מערך? .Iתא מספר אחד במערך יכיל את הנתונים שבשורש העץ) .בתא מספר אפס במערך לא נעשה כל שימוש( .IIאם הנתונים שבצומת nשל העץ נשמרו בתא מספר xבמערך ,אזי הנתונים שבילדו השמאלי של nיישמרו בתא מספר ,2*xוהנתונים שבילדו הימני של n יישמרו בתא מספר .2*x +1 לדוגמה ,העץ שראינו בתחילת הפרק יישמר במערך של מספרים שלמים באופן הבא: 83 77 13 17 #9 #8 #7 #6 #5 #4 #3 #2 #1 #0 לתאים בהם לא מצוין ערך יש להתייחס כאל תאים ריקים .אנו רואים כי שורש העץ ,הנתון ,17מצוי בתא ,#1שני ילדיו של השורש שמורים בתאים .#3 ,#2לצומת שמכיל את הערך ) 13ושמצוי בתא #2במערך( אין ילדים ,ועל כן התאים #5 ,#4 במערך נותרו ריקים .גם לצומת שמכיל את 77אין ילד שמאלי ,ועל כן התא #6נותר ריק .התא #7מכיל את ילדו הימני של הצומת שנתוניו מופיעים בתא .#3 כמובן שגודלו של העץ שניתן לייצג חסום על-ידי גודלו של המערך. עתה נפנה לייצוג עץ בינארי באמצעות מצביעים .לאורך הפרק נניח כי כל צומת בעץ מכיל נתון מספרי בודד .במקרה הכללי הצומת עשוי להכיל מספר נתונים ,שיקובצו, קרוב לודאי ,ל , struct -ואז במקום השדה int _dataיהיה לנו שדה struct . infoנציג עתה את המבנה הבסיסי שישמש אותנו לאורך הפרק: { struct Node ; int _data ; struct Node *_left, *_right ; } טיפוס המבנה שהגדרנו כולל חבר שמכיל את הנתון המספרי ,ושני מצביעים לשני ילדיו של הצומת. 14.3בניית עץ חיפוש בינארי נניח כי בתכניתנו הוגדר . struct Node *root; :המצביע rootיצביע לשורשו של העץ הבינארי שנבנה בתכנית .את הפונקציה ,build_bstשבונה את העץ ,אנו מזמנים באופן הבא . root = build_bst() ; :הפונקציה build_bstתבנה עץ חיפוש בינארי ,ותחזיר מצביע לשורש העץ שנבנה .מצביע זה יוכנס למשתנה הבא: באופן הפונקציה את לכתוב יכולנו לחילופין .root ;) . other_build_bst(rootכלומר יכולנו להעביר לפונקציה את המשתנה rootכארגומנט .הפונקציה other_build_bstהייתה מקבלת פרמטר הפניה: &* .struct Nodeכפי שציינו עת דנו בפונ' ,מבחינת סגנון תכנותי ,הגרסה בה מחזירים ערך ,עדיפה על-פני זו המקבלת את המצביע כפרמטר הפניה. נציג את הפונקציה :build_bst { )(struct Node *build_bst ; struct Node *root = NULL 109 ; int num cin >> num { )while (num != 0 ; )insert_into_bst(num, root ; cin >> num } ; return root } בפונקציה build_bstאין רבותא .הדבר היחיד עליו ראוי לתת את הדעת הוא שהיא מגדירה מצביע). struct Node *root; :כלומר משתנה המוקצה על גבי המחסנית( ,מצביע זה יורה על שורש העץ הבינארי שייבנה )ושצמתיו ישכנו ַבערמה(. עם סיום ריצתה מחזירה הפונקציה את המצביע לשורש העץ. 110 14.3.1הכנסה לעץ חיפוש באמצעות פונ' רקורסיבית הפונקציה שתעניין אותנו יותר היא .insert_into_bstנציג שלוש גרסות שונות שלה. נתחיל בראשונה: { )void insert_into_bst(int num, struct Node *&root { )if (root == NULL ; root = new (std::nothrow) struct Node )if (root == NULL ; )terminate(“Can not allocate memory\n", 1 ; root -> _data = num ; root->_left = root->_right = NULL } )else if (num <= root->_data ; )insert_into_bst(num, root->_left else ; )insert_into_bst(num, root->_right } כפי שנקל לראות ,גרסתה הראשונה של ,insert_into_bstכמו מרבית הפונקציות לטיפול בעצים ,היא פונקציה רקורסיבית .היא פועלת באופן הבא :אם הגענו ְלמצביע שמורה על )תת(-עץ ריק ,אזי יש להוסיף את הערך numכאן ועכשיו ְלצומת עליו המצביע יורה .אחרת :יש להוסיף את הערך החדש או לבנו השמאלי של השורש או לבנו הימני )בהתאם לערכו של המספר המוסף( ,ועל כן אנו קוראים רקורסיבית עם המצביע המתאים .כלומר insert_into_bstמוסיפה ערכים חדשים בתחתית העץ; במילים אחרות ערך שהוכנס מאוחר יותר לעולם לא יהיה הורה של ערך שהוכנס מוקדם יותר ,הערך שהוכנס מאוחר יותר יהיה צאצאו של הערך שהוכנס מוקדם יותר ,או שהוא יוסף על ענף אחר של העץִ .בדקו לעצמכם כי לא יתכן שלא יהיה מקום בעץ לערך חדש .כל ערך נוסף תמיד יוכל למצוא את המקום המתאים לו ַבעץ )גם אם ערך זהה לו כבר מצוי אי-שם בעץ(. מכיוון ש insert_into_bst -מקבלת את המצביע המועבר לה כפרמטר הפניה, אזי שינויים שהפונקציה תכניס למצביע יוותרו בתום פעולתה בארגומנט המתאים אשר יעבור להצביע על הצומת שהוסף לעץ. כדרכנו ,נבצע סימולציית הרצה לפונקציה .נניח שהקלט המוזן הוא,13 ,83 ,77 ,17 : 17) 0מוזן ראשון 0 ,מוזן אחרון( .בתחילה ,בתכנית הראשית מוגדר המשתנה root )אשר אינו מאותחל ,ועל-כן כולל ערך 'זבל'( .נציג את מצב המחסנית: =root עת התכנית הראשית מזמנת את build_bstמצב הזיכרון הוא: =root =root =num נזכור כי rootשל build_bstהוא משתנה לוקלי ,ועל-כן אין קשר בינו לבין rootשל .mainהפונקציה build_bstמתחילה להתבצע .היא קוראת את הערך 17ומזמנת את ) insert_into_bstאשר מקבלת את המצביע כפרמטר משתנה(, מצב הזיכרון הוא: 111 =root =root num=17 =root num=17 עת insert_into_bstשואלת ) if (root == NULLאנו הולכים בעקבות החץ )עם הראש המשולש השחור( ,ובודקים את ערכו של rootשל .build_bst ערכו הוא אכן NULLועל-כן מוקצית קופסה חדשה על הערמה ו root -של build_bstעובר להצביע על הקופסה .כמו כן השדות הרצויים מאותחלים כפי שמתואר בקוד. מצב הזיכרון בתום ריצתה של insert_into_bstהוא: 17 a =root b =root num=17 =root num=17 )האותיות a, bשהוספו לציור תסייענה לנו בהמשך לזהות את המצביעים המתאימים .הן אינן חלק ממצב הזיכרון ,אלא רק תוספות שהוספנו כדי להקל על ההסברים(. אנו שבים ל build_bst -אשר קוראת את הערך ,77ומזמנת שוב את .insert_into_bstגם עתה build_bstמעבירה ל insert -כארגומנט את ) rootשל .(build_bstמצב הזיכרון דומה לציור האחרון ,פרט לכך שערכו של numבשתי רשומות ההפעלה הוא ) 77ולא 17כפי שמופיע בציור האחרון(. 112 עת insert_into_bstמתחילה להתבצע ,ובודקת את ערכו של ,rootהיא מגלה כי ערכו שונה מ .NULL -גם התנאי ) (num <= root-> _dataאינו מתקיים ,ועל כן insert_into_bstקוראת רקורסיבית לעצמה ,ומעבירה לעותק הנקרא הרקורסיבית את המצביע ) root-> _rightכלומר את המצביע הימני היוצא מהקופסה של ,17זה שסומן באות .(aשימו לב כי מצביע זה מועבר ְלפונקציה המקבלת פרמטר הפניה .לכן אם וכאשר הפונקציה תשנה את ערכו של הפרמטר שלה )ותפנה אותו להצביע על קופסה כלשהי שתוקצה על-גבי הערמה( ,אזי השינויים שהפונקציה תבצע יוותרו במצביע ,המסומן באות ,aהמועבר כארגומנט )ושערכו עתה הוא .(NULLנציג את מצב הזיכרון בעקבות הקריאה הרקורסיבית המזומנת: 17 =root b a =root num=77 =root num=77 =root num=77 113 העותק השני של insert_into_bstמתחיל להתבצע .עת הוא שואל האם ערכו של הפרמטר rootשלו הוא ,NULLהמחשב הולך בעקבות החץ השחור ,מגיע למצביע ,aומגלה שערכו הוא אכן .NULLלכן עתה המחשב מקצה קופסה חדשה ומפנה את המצביע aלהצביע עליה .שימו לב שוב שהשינוי קורה על המצביע aשכן מצביע זה הועבר כארגומנט לפונקציה המקבלת פרמטר הפניה; ועל-כן שינויים שהפונקציה עורכת לפרמטר חלים למעשה על הארגומנט .בעקבות ההקצאה גם מוכנסים ערכים לקופסה שהוקצתה .מצב הזיכרון הוא: 17 =root b a =root num=77 77 c =root num=77 =root num=77 בכך העותק השני של insert_into_bstמסתיים .כתובת החזרה שלו היא סיומו של העותק הראשון של ,insert_into_bstולכן גם העותק הראשון מסתיים. אנו שבים ל build_bst -אשר קוראת את הערך ,83ומזמנת שוב את insert_into_bstתוך שהיא מעביר לה את rootשל .build_bst מצב הזיכרון הוא: 17 =root b a =root num=83 77 c =root num=83 הפונקציה insert_into_bstמתחילה להתבצע .ערכו של rootאינו .NULLגם num <= root-> _dataאינו מתקיים ולכן insert_into_bst התנאי: קוראת רקורסיבית לעצמה ,תוך שהיא מעבירה לקריאה הרקורסיבית את המצביע המסומן באות .aמצב הזיכרון בעקבות הקריאה הוא: 17 =root b a =root num=83 77 c =root num=83 =root num=83 114 העותק השני של insert_into_bstמתחיל להתבצע .גם בו ערכו של rootאינו ,NULLוגם בו לא נכון ש) num <= root->_data :שכן ערכו של numהוא ,83 וערכו של root->_dataהוא .(77ולכן העותק הנוכחי של insert_into_bst מזמן קריאה לעותק שלישי של .insert_into_bstהעותק השני מעביר לעותק השלישי את root->_ _rightשהינו המצביע המסומן באות cבציור שלנו .מצב הזיכרון: 17 =root b a =root num=83 77 c =root num=83 =root num=83 =root num=83 עת העותק השלישי של insert_into_bstמתחיל להתבצע ,ובוחן את ערכו של rootשלו ,הוא מגלה כי ערכו הוא .NULLעל כן העותק השלישי יוצר קופסה חדשה ומפנה את הארגומנט שהועבר לו ,כלומר את המצביע ,cלהורות על הקופסה החדשה .אחר העותק השלישי גם מכניס ערכים לקופסה כפי שמתואר בקוד .מצב הזיכרון הוא עתה: 17 =root b a =root num=83 77 c =root num=83 83 =root num=83 =root num=83 שלושת העותקים של insert_into_bstמסתיימים בזה אחר זה ,ואנו שבים ל- build_bstאשר קוראת את הערך ,13ומזמנת שוב את insertכדי להוסיף ערך זה לעץ .אני משאיר לכם כתרגיל לצייר את השתלשלות העניינים בזיכרון .אחרי ש- ) insert_into_bstבאמצעות קריאה רקורסיבית אחת נוספת( תוסיף את 13 לעץ נשוב ל .build_bst -עתה build_bstתקרא את הערך אפס ,ולכן תצא מלולאת קריאת הערכים build_bst .תתקדם לפקודת ה return -שלה בה היא מחזירה את ערכו של rootשלה .הערך המוחזר יושם למשתנה rootשל התכנית הראשית )להזכירכם build_bstזומנה מה main -באמצעות הפקודה: 115 ;)( .( root = build_bstלכן עתה rootשל התכנית הראשית יעבור להצביע על העץ ש build_bst -בנתה. 14.3.2הכנסה לעץ חיפוש באמצעות פונ' איטרטיבית המקבלת פרמטר הפניה הפונקציה insert_into_bstשכתבנו פעלה באופן רקורסיבי .הפונקציה התקדמה על הענף המתאים של העץ עד קצהו ,כלומר עד שהיא הגיע ְלמצביע שערכו הוא ,NULLבאותו מקום הפונקציה הוסיפה את הצומת החדש .עת פונקציה מתקדמת על ענף יחיד של העץ ,ואינה סורקת מספר ענפים ,ניתן לכתבה גם באופן איטרטיבי ,תוך שימוש בלולאות .לכן עתה נציג גרסה איטרטיבית של .insert_into_bstאקדים את המאוחר ואציין שהגרסה אומנם איטרטיבית ולא רקורסיבית ,וזו הקלה ,אך היא מסורבלת למדי ,וזו מגרעת ,ועל כן בשלב שלישי נציג גרסה איטרטיבית ולא מסורבלת ,אך ,למרבה הצער)?( כזו העושה שימוש במצביע למצביע .אני תקווה שהשוואת שתי הגרסות גם תאפשר לכם ללמוד על היתרונות הגלומים בשימוש במצביע למצביע: )void insert_into_bst(int num, struct Node *&root { struct Node *temp = new (std::nothrow) struct Node, ;*run = root )if (temp == NULL ; )retminate("cannot allocate memoty", 1 )//(4 ; temp -> _data = num ; temp -> _left = temp ->_right = NULL { )if (toor == NULL ; root = temp ; return } )// (1 )// (2 )// (3 { )while(true )if (num <= run -> _data )if (run -> _left != NULL ; run = run -> _left { else ; run -> _left = temp ; break } else )if (run -> _right != NULL ;run = run -> _ right { else ; run -> _right = temp ; break } } } נסביר את הפונ'. 116 נניח שהפונ' מזומנת כבר אחרי שנבנה העץ כמתואר בציור )מצביעים שערכם NULL הושמטו מהציור ,פרט למצביע המתויג באות : (a 17 =root num=83 12 ... 14 10 =root num=13 =temp =run a ראשית מוקצה איבר חדש )קופסה חדשה( עליו מצביע משתנה העזר .tempהערך 13מוכנס למרכיב הנתונים בקופסה ,ושני המצביעים מקבלים את הערך .NULL עתה מצביע העזר runמקבל את ערכו של ) rootשל הפונ'( שהינו פרמטר הפניה, ולכן runעובר להצביע גם הוא על הקופסה של ,17כלומר על שורש העץ(. מכאן פונים ללולאה ,שהתנאי שלה מתקיים תמיד )לכאורה זו לולאה אינסופית, ובפועל פקודות ה break -בגוף הלולאה הופכות אותה ללולאה סופית(. התנאי המסומן בהערת התיעוד ) (1מתקיים ,וכך גם זה המסומן בהערה ) (2ולכן , run = run -> _leftכלומר runעובר להצביע על הקופסה של ,12ובזאת תם הסיבוב הראשון בלולאה. בסיבוב השני בלולאה ,התנאי ) (1אינו מתקיים ,והתנאי ) (3כן מתקיים ולכן run עובר להצביע על הקופסה של .14 בסיבוב השלישי בלולאה התנאי ) (1מתקיים ,והתנאי ) (2אינו מתקיים ,ולכן , run-> _leftשהינו המצביע המסומן ב a -בציור ,עובר להצביע על הקופסה החדשה שהוקצתה בתחילת הפונ' ,כלומר 13משולב בעץ במקום המתאים; ובזאת ביצוע הפונ' מסתיים. שאלות למחשבה: א .האם ולמה יש צורך בהעברת rootכפרמטר הפניה )ולא כפרמטר ערך(. ב .האם הפונ' הייתה שגויה לו בלולאה היינו מרצים את ) rootולא את runכפי שאנו עושים(? ג .האם ולמה יש חשיבות בהשמה המסומנת ב (4) -בפונ'? 117 נסכם :ראינו גרסה רקורסיבית של פונ' ההוספה לעץ חיפוש בינארי ,וגירסה איטרטיבית )אך מעט מסורבלת( .שתי הגרסות קיבלו את המצביע לשורש העץ כפרמטר הפניה. 14.3.3הכנסה לעץ חיפוש באמצעות פונ' איטרטיבית המקבלת מצביע למצביע עתה נציג גרסה איטרטיבית לא מסורבלת אשר תקבל כפרמטר שלה מצביע ַלמצביע לשורש העץ )וכמבן ,את הערך שיש להוסיף( .הגרסה שנראה עתה תזומן על ידי build_bstבאופן הבא .insert_into_bst(num, &root); :מכיוון ש- rootהוא מצביע ,אזי &rootהוא מצביע למצביע .נציג את קוד הפונקציה: { )void insert_into_bst(int num, struct Node **p_2_root )while (*p_2_root != NULL ) if ( num <=(*p_2_root)-> _data ; ) p_2_root = &( (*p_2_root)-> _left ; ) else p_2_root = &( (*p_2_root)-> _right ; NULL ; *p_2_root = new (std::nothrow) struct Node if (*p2root == NULL) ... ; (*p_2_root)-> _data = num = (*p_2_root)-> _left = (*p_2_root)-> _right } בפונקציה אנו מקדמים בלולאה את המצביע למצביע על הענף הרצוי ,עד אשר המצביע עליו המצביע-למצביע מורה ערכו .NULLבשלב זה אנו מקצים קופסה ומפנים את המצביע עליו המצביע-למצביע מורה להצביע על הקופסה ְ חדשה, החדשה. נציג דוגמת הרצה של הפונקציה על העץ הבא .נניח כי עתה בנוסף לערכים ,77 ,17 83עלינו להוסיף את 70תוך שימוש גרסה השלישית של פונ' ההכנסה .כאמור, build_bstמזמנת את הפונקציה באופן הבא: ) .insert_into_bst (num, &rootמצב הזיכרון עם הקריאה לפונקציה הוא: 17 =root b a =root num=70 77 c =p_2_root num=70 83 אנו שמים לב כי p_2_rootהוא מצביע ,ועל-כן מצויר עם חץ בעל ראש בצורת .v מכיוון שהוא מצביע למצביע )ולא מצביע לקופסה( הוא מצויר בקו מקווקוו .בעת הקריאה p_2_rootמצביע על המצביע rootשל .build_bst עת הפונקציה מתחילה להתבצע היא פונה ללולאה: )while (*p_2_root != NULL 118 ) if ( num <=(*p_2_root)-> _data ; ) p_2_root = &( (*p_2_root)-> _left ; ) else p_2_root = &( (*p_2_root)-> _right נזכור כי *p_2_root :הוא המצביע עליו p_2_rootמורה ,ובתחילה זהו root של build_bstשערכו שונה מ .NULL -לכן אנו נכנסים לסיבוב ראשון בלולאה. התנאי ( num <=(*p_2_root)-> _data ) :אינו מתקיים ,שכן (*p_2_root)-> _dataהוא שדה ה data -בקופסה עליה מורה המצביע ) ,(*p_2_rootכלומר שדה ה _data -בקופסה עליה מורה המצביע עליו מצביע ,p_2_rootבמילים פשוטות זהו שדה ה _data -בקופסה עליה מורה rootשל ,build_bstכלומר זה הערך .17לכן אנו מבצעים את הפקודה הכפופה ל,else - ומשימים למצביע p_2_rootאת כתובתו של ,root-> _leftכלומר את כתובתו של המצביע ,aמבחינה ציורית המצב הוא: 17 =root b a =root num=70 77 d c =p_2_root num=70 83 לקראת כניסה לסיבוב שני בלולאה אנו בודקים את התנאי בכותרתה*p_2_root . הוא עתה המצביע ) aהמצביע עליו מורה ,(p_2_rrotולכן התנאי מתקיים .אנו נכנסים לגוף הלולאה ,ובו התנאי( num <=(*p_2_root)-> _data : )מתקיים ,שכן (*p_2_root)-> _dataהוא שדה ה _data -בקופסה עליה מצביע ,aכלומר זהו הערך .77לכן עתה p_2_rootמקבל את הכתובת של הבן השמאלי של התא עליו מצביע המצביע עליו ,p_2_rootבמילים פשוטות: p_2_rootמקבל את כתובתו של המצביע .dוהציור המתאים: 17 =root b a =root num=70 77 c d =p_2_root num=70 83 לקראת כניסה לסיבוב נוסף בלולאה אנו בוחנים שוב האם*p_2_root!=NULL : והפעם התשובה היא 'לא' ,ערכו של המצביע עליו מורה p_2_rootהוא .NULLאנו הפקודה: הלולאה. שאחרי לפקודות מתקדמים *p_2_root = new struct Nodeמקצה קופסה חדשה על הערמה ,ושולחת את המצביע עליו p_2_rootמורה להצביע על קופסה זאת .הציור המתאים: 119 17 =root b a =root num=70 77 d c =p_2_root num=70 83 שלוש הפקודות הבאות מכניסות ערכים לקופסה שהוקצתה .לדוגמה ,הפקודה: ; (*p_2_root)-> _data = numמכניסה את הערך 80לשדה ה_data - בקופסה עליה מורה ) , (*p_2_rootכלומר מכניסה את הערך 80לשדה ה_data- בקופסה עליה מורה המצביע ) dשעליו מצביע עתה .(p_2_root כדי לחדד את הבנתנו את הפונקציה שהצגנו ,נבחן מה היה קורה לו היינו כותבים אותה באופן הבא: { )void iterative_insert(int num, struct Node *&root )while (root != NULL ) if ( num <= root-> _data ; root = root-> _left ; ) else root = root-> _right ; root = new (std::nothrow) struct Node if (root == NULL) ... ; root-> _data = num ; root-> _left = root->_right = NULL } במקום מצביע למצביע ,פרמטר משתנה הגרסה הנוכחית של הפונקציה מקבלת ְ שהנו מצביע .שוב נניח כי יש להוסיף את ,70לעץ הכולל את .83 ,77 ,17הקריאה מ- (iterative_insert build_bstלפונקציה ,בגרסתה הנוכחית תהא: ;) num, rootמצב הזיכרון בעקבות הקריאה יהא: 17 =root b a =root num=70 77 c d =root num=70 83 עת הפונקציה מתחילה להתבצע ושואלת האם ,root!=NULLאנו הולכים בעקבות החץ )עם המשולש השחור( ,ובודקים את ערכו של rootשל ,build_bstשערכו אכן אינו .NULLלכן אנו נכנסים ללולאה .בגוף הלולאה אנו בודקים האםnum <= : ,root-> _dataוהתשובה לכך היא שלילית .לכן אנו מבצעים את ההשמה 120 הפטלית .root = root-> _right; :מדוע השמה זאת הינה פטלית? שכן אנו מפנים את המצביע rootשל ) build_bstכלומר את הארגומנט שהועבר ְ לפונקציה( להצביע כמו המצביע , root-> _rightכלומר כמו המצביע .aבזאת הפננו את rootשל build_bstלהצביע על הקופסה של ,77ואיבדנו לעולמים את הקופסה של !17 14.3.4זמן הריצה של בניית עץ חיפוש בינארי עתה נשאל את עצמנו כמה עולה בניית עץ חיפוש בינארי במקרה הגרוע ביותר? אנו זוכרים שכל נתון חדש נוסף כעלה ,אחרי שאנו יורדים לאורך המסלול המתאים בעץ .לכן זמן הבניה יהיה הגדול עת כל עלה חדש יתווסף מעבר לקודמו ,ויאריך את המסלול היחיד הקיים ,כלומר עת העץ 'יתנוון' לכדי רשימה מקושרת ,למשל מפני שהנתונים יוזנו ממוינים .17, 20, 23, 25, 30, ... :העץ יראה: 17 20 23 25 30 במצב זה ,הכנסת הנתון ה-n -י תעלה nצעדים ,ולכן עלות בניית העץ השלם תהיה: 1 + 2 + …+nכלומר סד"ג של n2צעדים. לעומת זאת ,אם העץ הינו מאוזן' ,יפה' ,אזי אורך המסלול הארוך ביותר בו מהצומת לעלה העמוק ביותר הוא לכל היותר lg2(n+1)-1צעדים ,או סד"ג של ).lg2(n לא נוכיח זאת ,אך נביט בכמה דוגמות שיעזרו לנו להשתכנע בכך: בעץ השמאלי יש צומת יחיד .יש לבצע אפס צעדים מהשורש לעלה העמוק ביותר, כלומר מספר הצעדים הוא lg2(1+1)-1כפי שמורה נוסחה שלנו .בעץ האמצעי יש שלושה צמתים .מספר הצעדים הדרוש כדי להגיע מהשורש לעלה העמוק ביותר הוא 121 אחד ,ולכן ,שוב בהתאמה לנוסחה .lg2(3+1)-1 :בעץ הימני lg2(7+1)-1 = 2 :ואכן יש להתקדם שני צעדים מהשורש לעלה העמוק ביותר. מכיוון שפעולת ההכנסה מחייבת להתקדם מהשורש לעלים ,אזי nפעולות הכנסה יחייבו עבודה בשיעור של לכל היותר ) n*lg2(nצעדים. 14.4ביקור בעץ חיפוש בינארי עתה ברצוננו לכתוב פונקציה אשר מציגה את כל הנתונים השמורים בעץ החיפוש שבנינו .נרצה להציג את הנתונים ממוינים .כמובן שכדי להציג את כל הנתונים עלינו לסרוק את כל צמתי העץ) ,ולא די בסריקת ענף יחיד( .מסיבה זאת הפונקציה שנכתוב תהא בהכרח רקורסיבית. עת בנינו את העץ הכנסנו נתון שערכו קטן או שווה מהנתון המצוי ַבשורש לתת-העץ השמאלי; נתון שערכו גדול מהשורש הוכנס לתת-העץ הימני .על-כן גם ַבביקור בעץ, ראשית נבקר בתת-העץ השמאלי ,המכיל נתונים קטנים או שווים מאלה שבשורש, אחר נבקר בשורש ,ולבסוף נבקר בתת העץ הימני ,המכיל נתונים גדולים מאלה המצויים ַבשורש .אם נחזיר על עקרון זה עבור כל תת-עץ שהוא אזי הנתונים יוצגו תוֹכי או באנגלית: ממוינים מקטן לגדול .לביקור שכזה בעץ אנו קוראים ביקור ִ in-orderיען כי אנו מבקרים בשורש בין ביקורנו בילד השמאלי ,לביקורנו בילד הימני .אל מול in-orderעומדים) :א( ביקור תחילי או :pre-orederבו אנו מבקרים ראשית בשורש ,אחר בילד השמאלי ,ולבסוף בילד הימני) ,ב( ביקור סופי או post- :orderבו אנו מבקרים ראשית בילד השמאלי ,שנית בימני ,ורק לבסוף בשורש .שימו לב כי בכל שלושת הביקורים אנו מבקרים בילד השמאלי לפני הביקור בילד הימני. ההבדל בין הביקורים השונים הוא בתשובה לשאלה :מתי מבקרים בשורש? לעת עתה נדון בביקור .in orderביקורים אחרים יעזרו לנו במשימות אחרות )למשל ,עת נשחרר את העץ נרצה ראשית לשחרר את הילדים ,ורק לבסוף את השורש ,ולכן ננקוט בביקור סופי(. פונקצית הביקור ,וההצגה הינה: { )void display(const struct Node * root { )if (root != NULL ; )display(root-> _left ; cout << root-> _data ; )display(root-> _right } } קוד הפונקציה פשוט למדי :אם )תת( העץ בו אנו מבקרים אינו ריק )(root!=NULL אזי אנו פונים ראשית לבקר בילדו השמאלי ,אחר אנו מציגים את הנתון שבשורש, ולבסוף אנו מבקרים בילד הימני. 122 נבצע סימולציית הרצה מעט מקוצרת על עץ החיפוש הבא ,עליו מצביע המשתנה rootשל התכנית הראשית: 17 =root ב א 17 35 ד ג ה ו 35 ז ח עם הקריאה לפונקציה מצב הזיכרון הוא שעל גבי המחסנית נוספת רשומת ההפעלה של הפונקציה: =root עתה הפונקציה מתחילה להתבצע .ערכו של הפרמטר ) rootשל הפונקציה( אינו NULLועל כן הפונקציה קוראת רקורסיבית לעותק שני של הפונקציה ,ומעבירה לו את root-> _leftכלומר עותק של המצביע ב' )המועבר כפרמטר ערך( .על גבי המחסנית נוספת רשומת ההפעלה: =root העותק השני של הפונקציה מתחיל להתבצע .גם בו ערכו של rootאינו ,NULLועל- כן העותק השני קורא רקורסיבית לעותק שלישי של הפונקציה ומעביר לעותק השלישי את בנו השמאלי ,כלומר את המצביע ו' .בעותק השלישי ערכו של root הוא ,NULLועל כן העותק השלישי אינו מבצע דבר ,ומסיים מיידית .אנו שבים לעותק השני אשר עתה מציג את ערכו של root-> _dataכלומר את הערך .17 אחר מזמן העותק השני קריאה רקורסיבית שנסמנה כקריאה הרביעית ,ומעביר לה עותק של המצביע ה' .בקריאה הרביעית ערכו של rootהוא ,NULLעל-כן היא מסיימת בלי לבצע דבר ,אנו שבים לעותק השני אשר מסתיים בזאת .את העותק השני זימן העותק הראשון )עת האחרון קרא רקורסיבית עם ילדו השמאלי( .עתה העותק הראשון יכול להציג את root-> _dataשלו ,כלומר את הערך .17לאחר מכן קורא עותק הראשון רקורסיבית עם ילדו הימני ,כלומר עם עותק של המצביע א' .נסמן את הקריאה שזומנה עם עותק של המצביע א' כקריאה החמישית .ערכו של rootבה שונה מ NULL -ועל כן היא קוראת רקורסיבית עם ילדה השמאלי, כלומר עם המצביע ד' ,אחר הקריאה החמישית תציג את הערך ,35ולבסוף הקריאה החמישית תקרא רקורסיבית עם המצביע ג' .הקריאה עם ג' תקרא רקורסיבית לילדה השמאלי )הריק( ,אחר תציג את הערך ,35ולבסוף תקרא רקורסיבית עם ילדה הימני )הריק( .בזאת תסתיים הקריאה עם ג' .היא תחזור לקריאה עם א' ,אשר תסתיים בזאת ,ותחזור לקריאה הרקורסיבית הראשונה .גם קריאה זאת תסתיים, וזאת אחרי שהנתונים בעץ הוצגו ממוינים מקטן לגדול כנדרש. הערת תזכורת :את הפרמטר של פונקציה הגדרנו באופן הבא: . const struct Node * rootהמילה constמונעת מהפונקציה לשנות את ערכו של האיבר עליו מצביע המצביע .root 123 14.4.1זמן הריצה של סריקת העץ זמן הריצה של סריקת העץ הוא לינארי במספר הצמתים בעץ ,שכן עבור כל צומת אנו קוראים לפונ' פעם יחידה ,ועושים אז עבודה בישעור קבוע )בכל ביקור בכל צומת( .מעבר לכך אנו קוראים לפונ' פעם יחידה גם עבור כל המצביעים שערכם הוא ) NULLשכן אם המציע לשורש תת-עץ כלשהו אינו NULLאזי אנו קוראים לפונ' עם שני תתי-העצים ,כלומר עם שני הילדים ,ואלה עשויים להיות .(NULLמשפט קטן, שלא נוכיח ,טוען שבעץ בינארי שאינו ריק ,מספר המצביעים שערכם הוא NULLהוא לכל היותר כמספר הצמתים ,ולכן זימון הפונ' גם עבור מצביעים אלה מכפיל את זמן הריצה ,אך לא הופך אותו להיות לא לינארי במספר הצמתים. 14.5חיפוש ערך מבוקש בעץ בינארי כללי ובעץ חיפוש בינארי 14.5.1חיפוש ערך בעץ שאינו עץ חיפוש עתה ברצוננו לכתוב פונקציה אשר מקבלת מצביע לשורשו של עץ בינארי )שאינו דווקא עץ חיפוש( ,וערך שלם .הפונקציה תחזיר את הערך trueאם ורק אם הערך מצוי בעץ .הקריאה לפונקציה תהיה למשל: ) )if (search(num, root ; ”cout << “It exists in data\n הגדרת הפונקציה: { )bool search(int num, const struct Node * root ; bool found )if (root == NULL ; ) return( false )if (root -> _data == num ;) return( true ; )found = search(root-> _left )if (found ; return true ; )found = search(root-> _right ; ) return( found } הסבר הפונקציה :במידה ולפונקציה מועבר מצביע ל)תת(-עץ ריק אזי ברור שהערך לא נמצא בעץ ,ויש להחזיר את הערך .falseמנגד ,אחרי שוידאנו שהעץ אינו ריק, אנו רשאים לבדוק את ערכו של ,root-> _dataואם ערכו שווה לערך המבוקש ניתן להחזיר מיידית את הערך ,trueואין צורך להמשיך ולחפש בילדיו של השורש הנוכחי .במידה ושני התנאים הראשונים לא התקיימו אנו קוראים רקורסיבית לפונקציה על-מנת לבדוק האם הערך המבוקש נמצא בילד השמאלי .את הערך המוחזר על-ידי הקריאה הרקורסיבית אנו מכניסים למשתנה .foundבמידה וערכו של המשתנה הוא ,trueניתן להחזיר מייד איתות על מציאה ,ואין צורך להמשיך ולבדוק את הילד הימני .לבסוף ,אם העץ אינו ריק ,הערך המבוקש לא נמצא לא בשורש ולא בילד השמאלי ,אזי נבדוק האם הוא נמצא בילד הימני ,ותוצאת הבדיקה היא שתוחזר על-ידי העותק הנוכחי של הפונקציה; שכן אם הערך נמצא בילד הימני ,אזי ערכו של foundיהיה ,trueוזה הערך שיש להחזיר .מנגד אם הערך לא נמצא גם בילד הימני ,אזי ערכו של foundיהיה ,falseוזה הערך שיש להחזיר. 124 פונקציה זאת מדגימה לנו מצב שכיח למדי בפונקציות שפועלות על עצים ומחזירות ערך :בפונקציה אנו בודקים האם השורש מקיים תכונה רצויה כלשהי )למשל מכיל ערך מבוקש( .במידה והשורש אכן מקיים את התכונה )או לעיתים במידה והשורש אינו מקיים את התכונה( ,אזי ניתן לעצור את תהליך הבדיקה ולהחזיר ערך מתאים. אחרת יש לקרוא רקורסיבית עם הילדים בזה אחר זה ,לקלוט את הערך המוחזר על-ידי כל-אחד מהם ,ולהחזיר לבסוף ערך המייצג את מה שהוחזר מהילדים. מתכנתים מתחילים שוכחים לעיתים להקפיד על מילוי ההיבטים הללו: .Iהחזרת ערך במידה והשורש מקיים )לא מקיים( התכונה הרצויה. .IIקליטת הערך המוחזר על-ידי הקריאה הרקורסיבית לכל אחד משני הילדים. .IIIהחזרת ערך 'המסכם' את מה שהוחזר מהילדים. את הפונקציה שהצגנו יכולנו לכתוב גם בצורה קומפקטית יותר: { )bool search(int num, const struct Node * root )if (root == NULL ; ) return( false )if (root -> _data == num ;) return( true ;))return(search(root-> _left) || search(root-> _right } בגרסה הנוכחית אנו מחזירים ישירות )בלי שימוש במשתנה עזר( את תוצאת הקריאה הרקורסיבית ַלילדים .האם הגרסה הנוכחית פחות יעילה מקודמתה? האם בגרסה הנוכחית נקרא רקורסיבית גם עם הילד הימני אפילו אם הערך נמצא כבר בילד השמאלי )ועל כן הקריאה הרקורסיבית עם הילד השמאלי החזירה את הערך ?(trueהתשובה היא :לא .מכיוון ששפת Cמבצעת הערכה מקוצרת של ביטויים בולאניים ,ומכיוון שאנו מחזירים את תוצאת ה or -בין שתי הקריאות הרקורסיביות ,אזי אם הקריאה עם הילד השמאלי תחזיר את הערך ,trueברור שערכו של הביטוי השלם יהיה trueואין צורך לבדוק את המרכיב הימני בביטוי הבולאני שבפקודת ה.return - 14.5.2חיפוש ערך בעץ חיפוש כיצד נחפש ערך מבוקש בעץ חיפוש בינארי? האם גם כאן אנו עלולים להידרש לסרוק את כל העץ )תוך שימוש בפונקציה רקורסיבית(? לא .בעץ חיפוש בינארי איננו חייבים לסרוק את כל העץ ,אנו יכולים להתקדם על ענף מתאים עד שנאתר את הערך ,או עד שנגיע לתת-עץ ריק .במקרה השני נוכל להסיק כי הערך אינו מצוי בעץ .נציג את הפונקציה ואחר נסבירה: { )bool search(int num, const struct Node * root )while (root != NULL )if (root -> _data == num ;) return( true )else if (num < root -> _data ; root = root-> _left else ; root = root-> _right ; ) return( false } 125 ַבלולאה אנו מתקדמים עם המצביע rootכל עוד ערכו אינו .NULLאם הגענו לצומת המקיים שערכו של root-> _dataשווה לערך המבוקש ניתן לקטוע את ביצוע הפונקציה ,ולהחזיר מייד את הערך .trueאחרת ,על-פי היחס בין הערך המבוקש לערך המצוי בצומת הנוכחי אנו מקדמים את rootשמאלה או ימינה .אם מיצינו את הלולאה בלא שהחזרנו את הערך trueבשלב כל שהוא ,אות הוא וסימן שהערך המבוקש לא מצוי בעץ ,ויש ,על-כן ,להחזיר את הערך .false שימו לב כי תודות לכך ש root -מועבר כפרמטר ערך איו בעיה בכך שבפונ' אנו משנים את ערכו .הדבר לא יגרום לאובדן העץ בתכנית הראשית .הצורך בconst - הוא על מנת למנוע מהפונ' לשנות את הערכים שבעץ. שאלה לא קשה :מהו זמן הריצה של חיפוש ערך בעץ בינארי כללי ,ובעץ חיפוש בינארי במקרה הגרוע ביותר? 14.6ספירת מספר הצמתים בעץ בינארי עתה נרצה לכתוב פונקציה אשר מקבלת מצביע לשורשו של עץ בינארי ,ומחזירה את מספר הצמתים בעץ .הפונקציה דומה לפונקציית הביקור שראינו בסעיף ,4גם בה עלינו לבקר בכל צמתי העץ ,אולם הפעם איננו מתעניינים בערך המצוי בכל צומת, אלא רק מוסיפים אחד למניה שאנו עורכים .קוד הפונקציה יהיה על-כן: { )unsigned int count_nodes(const struct Node * root ; unsigned int lcount, rcount )if (root == NULL ; ) return( 0 ; )lcount = count_nodes( root-> _left ; )rcount = count_nodes(root -> _right ; )return(lcount + rcount + 1 } הסבר :הפונקציה פשוטה למדי :בעץ ריק יש אפס צמתים .בעץ שאינו ריק נמנה את מספר הצמתים בילד השמאלי ,נמנה את מספר הצמתים בילד הימני ,ואז מספר הצמתים בעץ שהצומת בו אנו נמצאים הוא שורשו הוא סכום הצמתים בשני הילדים ועוד אחד )עבור הצומת המשמש כשורש )תת( העץ(. יכולנו לכתוב את הפונקציה באופן מעט יותר קצר ,ללא שימוש במשתני העזר .lcount, rcountאני מזמין אתכם לחשוב כיצד. שימו לב שכפי שהזכרנו קודם: .Iאנו מקפידים להחזיר ערך עבור מקרה הקצה. .IIאנו מקפידים לקלוט את הערך המוחזר על-ידי הקריאות הרקורסיביות שמזמנת הקריאה הנוכחית. .IIIאנו מקפידים להחזיר ערך שמסכם את מה שהוחזר מהקריאות הרקורסיביות ואת 'תוצאת הבדיקה' שנערכה עבור השורש. זמן הריצה הוא כמו בכל סריקה של העץ :לינארי במספר הצמתים בעץ. 126 14.7מציאת עומקו של עץ בינארי עומקו של עץ בינארי מוגדר באופן הבא: .Iעומקו של עץ ריק הוא .-1 .IIעומקו של עץ שאינו ריק הוא אחד ועוד המקסימום בין עומקם של שני בניו של השורש. במילים פשוטות עומקו של עץ בינארי הוא כמספר הצעדים שיש להתקדם מהשורש אל העלה העמוק ביותר) .עלה בעץ בינארי הוא צומת שאין לו בנים(. לדוגמה :העץ הבא הוא בעומק ) 3לא ציירנו את הערכים בכל צומת שכן אין להם כל משמעות עת דנים בעומק העץ .כמו כן מצביעים שערכם NULLהושמטו מהציור(: הפונקציה לחישוב עומקו של עץ בינארי תסתמך על פונקציה אשר מחזירה את הערך הגדול בין שני המספרים הטבעיים שהועברו לה: { )int max(int num1, int num2 ; ) return( (num1 > num2) ? num1 : num2 } הגדרת הפונקציה לחישוב עומק: { )int depth(const struct Node * root )if (root == NULL ; ) return( -1 ; )int ldepth = depth(root-> _left ; )int rdepth = depth(root-> _right ; ) return( max(ldepth, rdepth) + 1 } הסבר הפונקציה :הפונקציה היא מימוש ישיר של הגדרת עומק עץ .עומקו של עץ ריק הוא ,-1ועומקו של עץ שאינו ריק הוא אחד ועוד המקסימום בין עומקי הילדים. זמן הריצה של הפונ' הוא לינארי במספר הצמתים בעץ ,שכן כל צומת אנו פוקדים פעם יחידה; במילים אחרות ,הפונ' פוקדת כל צומת פעם יחידה ,ועושה עבודה בשיעור קבוע עבור הצומת. 127 14.8האם עץ בינארי מקיים שלכל צומת פנימי יש שני בנים עבור הפונקציה שנכתוב בסעיף זה נגדיר מספר מושגים הקשורים לעת בינארי: .Iעלה ) (leafבעץ בינארי הוא צומת שאין לו ילדים כלל. .IIצומת בעץ בינארי שאינו עלה נקרא צומת פנימי ).(inner node )אעיר כי עת העץ כולל רק שורש ,אזי השורש הוא גם עלה ,ולכן שורש ועלה אינם מושגים מנוגדים זה לזה .לעומת זאת עלה וצומת פנימי הם כן מושגים מנוגדים :כל צומת בעץ הוא צומת פנימי או עלה ,אך לא שני המושגים גם יחד(. עתה נציג פונקציה אשר בודקת האם לכל צומת פנימי בעץ בינארי יש שני ילדים .על הפונקציה להחזיר את הערך trueעבור שני העצים השמאליים בציור ,ואת הערך falseעל העץ הימני )שכן בעץ הימני ,הילד הימני של השורש הוא צומת פנימי עם ילד יחיד(. { )bool non_leaf_2_sons( const struct Node * root ; bool ok )if (root == NULL ; ) return( true || )if ( (root->_left == NULL && root-> _right != NULL ) )(root-> _right == NULL && root->_left != NULL ; ) return( false ; ) ok = non_leaf_2_sons( root->_left )if (!ok ; )return(false ; ) ok = non_leaf_2_sons( root->_left ; )return(ok } בפונקציה אין רבותא ,והיא דומה לפונקציות שכבר כתבנו בעבר :אם הגענו לתת-עץ ריק ,אזי הוא 'תקין' וניתן להחזיר את הערך .trueמנגד ,אם הגענו לצומת עם ילד יחיד )כלומר שילדו השמאלי הוא NULLוילדו הימני אינו ,NULLאו להפך( ,אזי יש להחזיר את הערך .falseבכל מקרה אחר )לצומת שני ילדים ריקים ,או שני ילדים שאינם ריקים( אנו קוראים רקורסיבית ראשית עבור הילד השמאלי .אם בילד זה התגלתה 'תקלה' יש להחזיר .falseלבסוף ,אם הילד השמאלי אינו בעייתי אזי יש להחזיר את תוצאת הבדיקה של הילד הימני. 128 זמן הריצה של הפונ' הינו לינארי במספר הצמתים בעץ ,שכן עבור כל צומת הפונ' נקראת פעם יחידה )וכן היא נקראת גם עבור הילדים הריקים של עלי העץ ,כלומר המצביעים שערכם NULLבעלים ,אולם כאלה יש לכל היותר כמספר צמתי העץ( 14.9האם עץ בינארי מקיים שעבור כל צומת סכום הערכים בבת-העץ השמאלי קטן או שווה מסכום הערכים בתת-העץ הימני עתה נרצה להשלים את המשימה הבאה :נתון עץ בינארי שאינו עץ חיפוש ,ואין זה מעניינו עתה כיצד העץ נבנה .הפונ' שנכתוב מקבלת מצביע לשורשו של עץ זה ,ועליה להחזיר האם שורש העץ השלם ,וכל צומת אחר בעץ מקיימים שסכום הערכים בתת העץ השמאלי של הצומת קטן או שווה מסכום הערכים בתת-העץ הימני. לדוגמה :עבור העץ הבא ,הפונ' אמורה להחזיר את הערך :true 1 21 7 3 0 4 2 4 נסביר :לדוגמה ,תת-העץ השמאלי של שלוש הוא ריק ,ולכן סכומו אפס ,וסכום זה קטן משתיים ,שהינו סכמו של תת-העץ הימני של שלוש .עבור הצמות של ,7סכומו של תת-העץ השמאלי הוא חמש ,בעוד סכומו של תת-העץ הימני הוא שמונה .וכך הלאה .לו ,למשל ה 4-השמאלי היה מוחלף ב ,5-אזי עבור הצומת של אפס סכום הערכים בתת-העץ השמאלי היה גדול מסכום הערכים בתת-העץ הימני ,ולכן על הפונ' היה להחזיר את הערך .false זימון הפונ' יהיה: ))if (sum_left_leq_sum_right(root ; "cout << "sum left <= sum right\n else ; "cout << "sum left > sum right for some subtree\n נפנה עתה להגדרת הפונ' ,ונציג שתי דרכים שונות להשלמת המשימה :הדרך הראשונה יותר פשוטה ופחות יעילה ,הדרך השניה פחות פשוטה ויותר יעילה .נפתח בדרך הראשונה. רעיון האלגוריתם ַבפתרון הראשון הוא לסרוק את העץ )בסריקה תחילית( .עבור כל צומת אליו מגיעים לסכום את תת-העץ השמאלי של הצומת ,לסכום את תת-העץ 129 הימני של הצומת )וזאת על-ידי זימון פונ' אחרת ,כזו היודעת לסכום עץ( .אם סכומו של תת-העץ השמאלי גדול מזה של תת-העץ הימני אזי גילינו הפרה של הדרישה, ועל כן נחזיר מייד את הערך .falseאחרת ,נמשיך בבדיקה עם תת העץ השמאלי )ע"י קריאה רקורסיבית לפונ' עם תת העץ השמאלי( ,ואם הוא יתגלה כתקין אזי נמשיך בבדיקה גם עם תת-העץ הימני .אם גם הוא יתגלה כתקין אזי נחזיר את הרך .true כמובן שאם אחד מתתי-העצים יתגלה כלא תקין אזי נחזיר את הערך .false כלומר עבור הפתרון אנו זקוקים לפונ' עזר אשר סוכמת עץ .זוהי פונ' פשוטה מאוד ונציג אותה עתה: )int sum_tree(const struct Node *root { )if (root == NULL ; return 0 return(sum_tree(root -> _left) + sum_tree(root -> _right) + ; )root -> _data } הפונ' דומה מאוד לפונ' שראינו בעבר ,ועל כן לא נסביר אותה .נשתמש בה לשם השלמת המשימה עימה אנו מתמודדים בסעיף זה .נציג עתה את הגרסה הראשונה של הפונ' ,sum_left_leq_sum_rightכלומר הפונ' המרכזית בסעיף זה. )bool sum_left_leq_sum_right(const struct Node *root { ; int suml, sumr ; bool ok )if (root == NULL ; return true )// (1 )// (2 ; )suml = sum_tree(root -> _left ; )sumr = sum_tree(root -> _right )if (suml > sumr ; return false ; )ok = sum_left_leq_sum_right(root -> _left )if (!ok )// (3 ; return false ; )ok = sum_left_leq_sum_right(root -> _right ; return ok } נדגים את התנהלות הפונ' על העץ שמופיע בדוגמה מעל :עת הפונ' נקראת לראשונה, עבור שורש העץ ,התנאי root==NULLאינו מתקיים ,ולכן לא ניתן להחזיר מיידית את הערך .trueעל כן אנו מתקדמים להשמה המסומנת ב (1) -וקוראים לפונ' אשר תסכום את תת-העץ השמאלי .בדוגמה שלנו יוחזר הערך ,20אשר יוכנס למשתנה .sumlבאופן דומה ,בהשמה ) (2אנו סוכמים את ת-העץ הימני של הצומת של ,1וסכומו כמובן .21על כן התנאי suml>sumrאינו מתקיים ,ואין להחזיר .falseבזאת גמרנו לבדוק את השורש )אשר נבדק תחילה ,ולכן אמרנו קודם שהפונ' סורקת את העץ בביקור תחילי( .עת הפונ' שלנו קוראת רקורסיבית עם ת- העץ השמאלי ,כלומר עם המצביע לצומת של to make a long story .7 shortקריאה זאת תייצר עוד קריאות רקורסיביות רבות )תריסר ,ליתר דיוק. 130 בדקו( ובסופו של גבר תחזיר את הערך trueאשר ייכנס למשתנה .okהתנאי )(3 לא יתקיים ,ועל כן לא יוחזר הערך .falseעתה תזמן הקריאה הרקורסיבית הראשונה קריאה רקורסיבית לפונ' sum_left_leq_sum_rightעם תת-העץ הימני ,כלומר עם המצביע לצומת של .21גם קריאה זאת תחזיר את הערך true )אחרי שהיא ,מצדה ,תזמן עוד שתי קריאות רקורסיביות( .ולכן הפונ' שלנו תחזיר את הערך ,trueכראוי. נשאל עתה את עצמנו מהו זמן הריצה של הפונ' שכתבנו? קל לראות שזמן הריצה של sum_treeהוא לינארי במספר הצמתים בעץ )בדומה ל.(non_leaf_2_sons - הפונ' שלנו ,עבור כל צומת קוראת ל sum_tree -עם שני ילדיו של הצומת .נניח לרגע שהעץ שלנו 'התנוון' לכדי רשימה מקושרת )בת nאיברים( ,ולכן הוא נראה באופן הבא: 1 2 ... n עבור שורש העץ נקרא ל sum_tree -עם הילד השמאלי ,נעשה עבודה בשיעור קבוע ,וכן נקרא ל sum_tree -עם הילד הימני ,ועל כך נשלם סד"ג של n-1 צעדים .נגלה שבינתיים הכל בסדר ,ולכן נקרא רקורסיבית לפונ' עם ילדו השמאלי, הריק ,של הצומת של ,1ואח"כ עם הצומת של .2כאן שוב נקרא פעמיים ל- ,sum_treeונשלם על כך n-2צעדים .שוב הכל יהיה בסדר ,ולכן נקרא לפונ' עם ילדו השמאלי של ,2ומה שחשוב יותר ,עם ילדו הימני של ,2וכך הלאה .כמות העבודה שאנו עושים ,בכל הקריאות ל sum_tree -גם יחיד היא לפיכך: (n-1)+(n-2)+(n-3)+…+1כלומר סד"ג של n2צעדים. כמות העבודה הרבה יחסית נגרמת מכך שעבור כל צומת של העץ אנו קוראים רקורסיבית לפונ' sum_treeעם שני תתי-העצים של הצומת .זאת נרצה לחסוך .על כן עתה נכתוב גרסה שניה ,יעילה יותר ,של הפונ' .רעיון הגרסה השניה הוא שכל צומת יחזיר לנו באמצעות ערך ההחזרה האם הוא תקין או לא )כמו בגרסה הראשונה( ,אולם מעבר לכך ,באמצעות פרמטר הפניה ,יחזיר לנו הצומת גם מה סכום הערכים בתת-העץ שלו .בערך זה נשתמש כדי לבדוק האם השורש בו אנו נמצאים בכל קריאה רקורסיבית הינו תקין או לא. הזימון לפונ' הפעם יהיה: ))if (sum_left_leq_sum_right(root, x ; "cout << "sum left <= sum right\n else ; "cout << "sum left > sum right for some subtree\n 131 כלומר ,הפונ' מקבלת גם משתנה )בשם (xלתוכו הפונ' תכניס את סכום הערכים בעץ השלם .בתכנית הראשית ,ערכו של xלא יעניין אותנו .נשתמש בפרמטר זה רק בקריאות הרקוסיביות. נציג עתה את הפונ' ,ואחר נדון בה: bool sum_left_leq_sum_right(const struct Node *root, )int &sum { ; int suml, sumr ; bool ok { )if (root == NULL ; sum = 0 ; return true } ; )ok = sum_left_leq_sum_right(root-> _left, suml )if (!ok ; return false ; )ok = sum_left_leq_sum_right(root-> _right, sumr )if (!ok ; return false ; sum = suml + sumr + root -> _data ; ) return( (suml <= sumr) ? true : false } נסביר :תנאי העצירה של הרקורסיה הוא עת אנו מגיעים לתת-עץ ריק .במצב זה אנו מחזירים שני דברים :באמצעות פרמטר ההפניה אנו מחזירים שסכום הערכים בתת-העץ 'שלנו' הוא אפס ,ובאמצעות ערך ההחזרה אנו מחזירים שתת-העץ 'שלנו' תקין. במקרה הכללי )עת לא הגענו לתת-עץ ריק( :אנו מזמנים את הפונ' עם תת-נעץ השמאלי ,ומעבירים לקריאה גם את המשתנה .sumlהקריאה עם תת-העץ השמאלי תחזיר לנו שני דברים :האחד האם תת-העץ השמאלי תקין ,והשני ,באמצעות ,sumlאת סכומו של תת-העץ השמאלי .כמובן שאם תת-העץ השמאלי אינו תקין, אין לנו מנוס אלא להחזיר .falseאחרת ,נקרא רקורסיבית באופן דומה ,עם תת- העץ הימני .בהנחה שגם קריאה זו החזירה לנו ,trueאנו פונים לטפל בערך שעלינו להחזיר למי שקרא לנו :ראשית ,לפרמטר ההפניה sumנכניס את סכום הערכים בתת-העץ שלנו ,שהינו ,suml + sumr + root -> _dataושנית נחזיר את הערך הבולאני על-פי היחס בין sumlל) sumr -כמובן שאם אנו מחזירים את הערך falseאזי למעשה אין חשיבות לסכום הערכים בתת-העץ שלנו(. ומהו זמן הריצה של הפונ' בגרסתה נוכחית? עתה אנו מבצעים סריקה סופית רגילה של העץ :עבור כל צומת ,מזמנים את הפונ' עם ילדו השמאלי ,מזמנים את הפונ' עבור ילדו הימני )כמובן לא שוכחים להתעניין בערך שמחזירות שתי הקירות הרקורסיביות( ,ולבסוף 'מטפלים' בשורש עצמו: מעדכנים את sumומחזירים את הערך הרצוי .לכן זמן הריצה של הפונ' הוא לינארי במספר הצמתים בעץ .זהו כמובן שיפור ניכר יחסית לגרסה שראינו קודם ,ואשר זמן ריצתה עלול היה להתדרדר לכדי .n2 132 השיפור הושג ע"י שימוש בפרמטר הפניה אשר מאפשר לנו להחזיר מכל קריאה רקורסיבית שני נתונים :גם האם תת העץ הנוכחי תקין ,וגם את סכומו. 14.10שחרור עץ בינארי עת איננו זקוקים יותר לעץ הבינארי עלינו לשחררו .תהליך השחרור מחייב סריקה של העץ .הפעם נשתמש בסריקה סופית :עבור כל תת-עץ שאינו ריק, ראשית נשחרר את הילד השמאלי ,שנית נשחרר את הילד הימני ,ובסוף נשחרר את השורש. הקוד: )void free_tree(struct Node *root { )if (root != NULL { ; ) free_tree(root -> _left ; ) free_tree(root -> _right ; delete root } } זימון הפונ' )מהתכנית הראשית(free_tree(root); : הסבר הפונ' :אם הגענו לתת עץ שאינו ריק אזי נשחחר את תת -העץ השמאלי שלו, וזאת ע"י קריאה רקורסיבית עם הילד השמאלי ,אחר נשחרר את תת-העץ הימני של השורש )ע"י קריאה רקוסיבית עם הילד הימני( ,ולבסוף נשחרר את השורש. שאלה :האם יש להעביר את הפרמטר לפונ' כפרמטר הפניה )כלור עם &(? תשובה :זה לא הכרחי .על מנת לשחרר את הזיכרון שהוקצה די שיש לנו מצביע לשטח זיכרון זה ,כפי שמעמיד לרשותו פרמטר ערך. נשים לב שאחרי הקריאה לפונ' ערכו של המשתנה rootשל התכנית הראשית נותר בעינו ,כלומר הוא מצביע לאותו מקום אליו הוא הצביע קודם לכן ,אולם שטח הזיכרון הזה כבר לא שייך לנו ,שכן שחררנו אותו ,ובזאת העמדנו אותו שוב לרשות מערכת ההפעלה ,כך שהיא תוכל להקצותו שוב בפעולות הקצאה עתידיות. 133 14.11מחיקת צומת מעץ בינארי בסעיף זה ברצוננו לכתוב פונקציה אשר מקבלת מצביע לשורשו של עץ חיפוש בינארי ,וערך שלם .הפונקציה מוחקת מהעץ את הצומת בו מצוי הערך .במידה והערך אינו מצוי בעץ כלל ,לא ַתעשה הפונקציה דבר; במידה והערך מצוי בעץ מספר תמחק הפונקציה מופע יחיד שלו. פעמים ְ 14.11.1תיאור האלגוריתם נביט בעץ החיפוש הבא: 17 ב' א' 13 27 ג' 43 ה' ד' 3 16 ו' 14 האותיות שלצד המצביעים שבציור תשמשנה אותנו כדי לסמן )לתייג( את המצביעים השונים בנוחות. נבחן מקרים שונים של מחיקה: .Iמחיקת עלה :נניח שיש למחוק את הצומת המכיל את הערך ,43או את זה המכיל את הערך .14זה מקרה פשוט ,שכן יש למחוק צומת שהינו עלה בעץ .כל שיש לעשות הוא (1) :לשמור באמצעות מצביע עזר )נניח (tempמצביע לצומת שאותו עלינו למחוק (2) .להכניס את הערך NULLלמצביע אשר מורה על הצומת )המצביע ג' במקרה של ,43או המצביע ו' במקרה של (3) .(14לשחרר את הצומת עליו מצביע .temp .IIמחיקת צומת בעל ילד יחיד :עתה נניח כי עלינו למחוק את הצומת המכיל את הערך ,27או את זה המכיל את .16גם זה מקרה פשוט למדי .הפעולות הנדרשות הפנה את מצביע העזר tempלהורות על הצומת שיש למחוק. במקרה זה הןְ (1) : ) (2הפנה את המצביע המורה על הצומת שיש למחוק להצביע על ילדו הלא ריק נפנֵה את המצביע א' של הצומת שיש למחוקַ ) .במקרה של הצומת של ְ 27 ]המצביע על הצומת של [27להצביע כמו ג' ]ילדו הלא ריק של הצומת של .[27 במקרה של ,16נפנה את המצביע ד' להצביע כמו המצביע ו' ,כלומר ד' יעבור להצביע על הצומת של (3) .(14שחרר את הצומת עליו מצביע .temp .IIIמחיקת צומת בעל שני ילדים :זהו המקרה המורכב .כדי לטפל בו עלינו להגדיר את המושג :העוקב ַבעץ של ערך .v1העוקב בעץ של ערך ,v1הוא הערך הקטן ביותר v2המקיים ש .v2 >= v1 :לדוגמה :בעץ שהצגנו ,העוקב של 13הוא ,14והעוקב של 17הוא .27שימו לב כי ְלערך המצוי ְבצומת עם בן ימני תמיד ימצא בתת-העץ עליו מצביע בנו הימני של הצומת יהיה עוקב בעץ ,והעוקב ֵ 134 המכיל את .v1נשים לב כי העוקב של ערך v1בעץ מתקבל על-ידי ירידה לבנו הימני של הצומת של ,v1ואחר כל-עוד ניתן :ירידה לבן השמאלי של הצומת הנוכחי .לדוגמה :לעוקב של 13נגיע על-ידי ירידה לבנו הימני של הצומת בו מצוי ) 13הצומת של ,(16ואחר כך ירידה שמאלה לבנו השמאלי של .16באופן דומה לעוקב של 17נגיע על-ידי שנרד לבנו הימני של הצומת של ) 17כלומר לצומת של ,(27ומשם לא ניתן לרדת שמאלה ,לכן ַבצומת של 27נעצור .שימו לב כי לוּ ל27 - היה בן שמאלי שהכיל את ,23ולאחרון היה בן שמאלי שהכיל את ,21אזי היינו יורדים שמאלה לשני בנים אלה ,וכך מאתרים את העוקב של 17בעץ .עתה נשים לב כי העוקב של ַ v1בעץ מצוי ְבצומת שאין לו שני בנים :העוקב של v1בעץ מצוי בעלה ,או בצומת שיש לו רק בן ימני )לוּ היה לצומת גם בן שמאלי היינו מתקדמים ממנו לבנו השמאלי(. עתה ,אחרי שהגדרנו את העוקב של ערך v1בעץ ,ואחרי שלמדנו כיצד מגיעים מהצומת בו מצוי v1לצומת בו מצוי העוקב שלו ,נוכל לתאר את תהליך המחיקה של ערך מצומת לו שני בנים: הפנה את .Iאתר את הצומת בו מצוי העוקב בעץ של הערך אותו יש למחוקְ . מצביע העזר tempלהצביע על צומת זה. העתק את הערך ַ temp-> _dataלצומת בו מצוי הערך שיש למחוק. .II .III מחק מהעץ את הצומת עליו מצביע .tempמכיוון שצומת זה הוא עלה ,או מכיל בן ימני בלבד ,אזי המחיקה תעשה על פי סעיף א' או סעיף ב' באלגוריתם המחיקה. לדוגמה :כדי למחוק את 17מהעץ המלווה אותנו) :א( נאתר את הצומת המכיל את העוקב של 17בעץ .כפי שראינו זהו הצומת של ) .27ב( נעתיק את הערך 27 לצומת של ,17כלומר לשורש העץ) .ג( נמחק את הצומת של ) 27כלומר הצומת עליו מורה המצביע א'( .מכיוון שמדובר בצומת עם בן יחיד נבצע את המחיקה על פי המתואר בסעיף ב' באלגוריתם .דוגמה שניה :כדי למחוק את 13מהעץ: )א( נאתר את הצומת המכיל את העוקב ל 13 -בעץ ,כלומר את הצומת של ) .14ב( נעתיק את הערך 14לצומת של ) .13ג( נמחק את הצומת של 14מהעץ .מכיוון שמדובר בעלה אזי המחיקה תתבצע על-פי סעיף א' באלגוריתם. עד כאן תיארנו את אלגוריתם המחיקה .עתה נציג את מימושו. 14.11.2המימוש כפי שנראה ,כדי שהפונקציה למחיקת ערך מעץ לא תהא מסורבלת ,יהיה עלינו להעביר לה מצביע ַלמצביע אשר מורה על שורש העץ .נבחן ראשית מדוע העברת המצביע לשורש העץ כפרמטר משתנה תחייב אותנו לכתוב פונקציה מסורבלת )ועל כן לא ננקוט בגישה זאת ,אלא נעביר מצביע למצביע לשורש העץ( .לכן עתה נניח כי אנו מעבירים לפונקציה את המצביע לשורש העץ כפרמטר משתנה ,ונראה כיצד עלינו לכתוב את פונקצית המחיקה .מחיקת צומת מעץ דומה ,במובנים רבים, למחיקת צומת מרשימה משורשרת ,ועל כן נרצה להתקדם עם שני מצביעים .עת המצביע הקדמי יצביע על הצומת שמכיל את הערך הרצוי ,נַפנה את המצביע לבנו המתאים של הצומת עליו מצביע המצביע האחורי ַל ֵבן המתאים של הצומת שיש למחוק .אם כל כך טוב ,אז מה כל-כך רע? שלכל צומת יתכנו שני בנים ,ולא רק אחד כמו ברשימה משורשרת .על כן יש לנו ארבע אפשרויות שונות של שינויים :אנו עשויים להידרש להפנות את בנו השמאלי או הימני של הצומת עליו מצביע המצביע האחורי ,ואת המצביע שיש להפנות אנו עשויים להידרש להפנות לבנו השמאלי או הימני של הצומת שיש למחוק. נדגים את הקוד: { )void delete_node(int val, struct Node * &root 135 struct Node *wanted_node = root, *father_of_wanted = NULL ; הוא: ברשימות משורשרותfront - הוא האנלוגי לwanted_node המצביע father_of_wanted המצביע.שיתקדם עד לצומת שמכיל את הערך שיש למחוק ברשימותrear -כן האנלוגי ל- ויהיה על,wanted_node יצביע על אביו של .משורשרות : ואביו,הלולאה הבאה תתקדם עד לאיתור הצומת הרצוי while (wanted_node != NULL) { // find the node and // its father if (wanted_node -> _data == val) break ; if (wanted_node -> _data > val) { father_of_wanted = wanted_node ; wanted_node = wanted_node -> _left ; } else { father_of_wanted = wanted_node ; wanted_node = wanted_node -> _right ; } } מצביע על הצומתwanted_node אזי, אם הערך המבוקש מצוי בעץ,בשלב זה אם הערך, מנגד. מצביע על אביו של הצומתfather_of_wanted ;שכולל אותו : לפיכך עתה נבדוק.NULL הואwanted_node אזי ערכו של,אינו מצוי בעץ if (wanted_node == NULL) return ; // val not found המקרה הפשוט ביותר הינו.עתה נתקדם לטיפול במצב בו הערך המבוקש מצוי בעץ מקרה זה עלינו לחלק למספר.זה שבו הצומת בו מצוי הערך המבוקש הינו עלה )ב( הצומת בו מצוי הערך, )א( הצומת בו מצוי הערך הוא שורש העץ:מצבים שונים נציג. )ג( הצומת בו מצוי הערך הוא בנו הימני של אביו,הוא בנו השמאלי של אביו :את הקוד if (wanted_node->_left == NULL && // del a leaf wanted_node-> _right == NULL ) { if (wanted_node == root) { root = NULL ; delete wanted_node ; } else { // the wanted val is not in root if (father_of_wanted -> _left == wanted_node) father_of_wanted - >_left = NULL ; else // father_of_wanted -> son == wanted_node father_of_wanted -> _right = NULL ; delete wanted_node ; } ַלצומת בו שוכן הערך המבוקש יש רק בן:נפנה עתה לטפל במקרה הפשוט השני : והוא עצמו בן ימני או שמאלי של אביו,(יחיד )ימני או שמאלי else // wanted val is not in leaf 136 if (wanted_node -> _left == NULL || // but has 1 son { )wanted_node -> _right == NULL if (wanted_node -> _left != NULL) // has a left son // and wanted_node is left son of its father )if (father_of_wanted -> _left == wanted_node = father_of_wanted -> _left ;wanted_node->_left else // wanted is a right son of its dad = father_of_wanted -> _right ;wanted_node->_left else // wanted has only right son // and wanted_node is a left son of its dad { )if (father_of_wanted -> _left == wanted_node = father_of_wanted -> _left ;wanted_node-> _right else // wanted is a right son of its dad = father_of_wanted -> _right ;wanted_node-> _right } else // wanted_node has two sons בשלב זה ,כשאנו כבר יגעים למדי ,הגענו למקרה המורכב .אשאיר לכם להשלימו כתרגיל. כפי שאמרנו בתחילת הסעיף ,הדרך האלגנטית יותר לכתוב את פונקצית המחיקה היא להעביר לה מצביע למצביע .מדוע גישה זאת מקצרת את הפונקציה? כפי שראינו עם רשימות משורשרות ,עת אנו מטפלים במצביע למצביע אין לנו צורך בזוג מצביעים ,די לנו באחד; קל וחומר שאיננו מסתבכים עם עיסוק בשאלה האם מדובר בבן שמאלי או בבן ימני. בגרסה הנוכחית של פונקצית המחיקה ,הפרמטר השני מוגדר כ: struct Node **root_ptrכלומר מצביע למצביע .הקריאה לפונקציה תהא לפיכך delete_node(num, &root); :עבור משתנה rootשל התכנית הראשית ,אשר הוגדר כ , struct Node *root; :ושעתה מצביע לשורש העץ. מסיבה זאת בגוף הפונקציה ,עת אנו רוצים להשתמש במצביע rootאנו צריכים לכתוב ,*root_ptrשכן root_ptrמצביע ל ,root -ועל-כן *root_ptrהוא האובייקט עליו root_ptrמצביע ,כלומר .rootכדי לפנות לשדה הdata - ַבקופסה עליה מצביע rootעלינו לכתוב ,(*root_ptr)-> _data :שכן כאמור *root_ptrמביא אותנו למצביע ,rootולכן (*root_ptr)-> _dataמביא אותנו למרכיב ה data -בקופסה עליה מצביע .rootבהמשך נראה זאת גם בציור. כיצד תיערך קריאה רקורסיבית לפונקציה )מתוך הפונקציה(? ַלקריאה הרקורסיבית נרצה להעביר מצביע למצביע לאחד מבניו של שורש תת-העץ הנוכחי .כאמור ,(*root_ptr)->_leftמביא אותנו לבנו השמאלי של הצומת עליו מצביע המצביע ,שעליו מצביע .root_ptrלכן הביטוי: ) &( (*root_ptr)-> _dataמחזיר לנו מצביע לבנו השמאלי של הצומת עליו מצביע המצביע ,שעליו מצביע .root_ptr נציג עתה את קוד הפונקציה בגרסתו הרצויה: 137 void delete_node(int num, struct Node **root_ptr) { if (*root_ptr == NULL) return ; // if root_ptr points to the pointer // that points to the wanted node if ((*root_ptr) -> data == num) { // if no _left then move // pointer so it points to // _right if ((*root_ptr) -> _left ==NULL) { struct Node *temp = *root_ptr ; (*root_ptr) = (*root_ptr) -> _right ; delete temp ; } else if ((*root_ptr) -> _right ==NULL) { struct Node *temp = *root_ptr ; (*root_ptr) = (*root_ptr) -> _left ; delete temp ; } else // wanted node has 2 sons { struct Node *temp ; struct Node **succ_ptr = find_succ(root_ptr); (*root_ptr)-> _data = (*succ_ptr)-> _data; temp = *succ_ptr ; (*succ_ptr) = temp -> _right ; delete temp; } } else if (num < (*root_ptr) -> data) delete_node(num, &((*root_ptr) -> _left)); else delete_node(num, &((*root_ptr) -> _right)); } //============================================ struct Node **find_succ( struct Node **root_ptr) { struct Node **temp = &( (*root_ptr) -> _right ) ; while ((*temp) -> _left != NULL) temp = & ((*temp) -> _left ) ; return temp ; 138 } נציג דוגמת הרצה של הפונקציה על העץ המלווה אותנו .נניח כי יש למחוק את הערך 13מהעץ .מצב הזיכרון טרם הקריאה לפונקציה הינו: 17 א' ב' 13 27 ג' 43 =root ה' ד' 3 16 ו' 14 עם הקריאה לפונקציה נוספת על המחסנית רשומת ההפעלה של הפונקציה .המצביע root_ptrמצביע על המצביע rootשל התכנית הראשית .נציג את רשומת ההפעלה: =root_ptr num=13 =temp return=main שימו לב כי בעוד root_ptrהוא מצביע למצביע ,כלומר יודע להצביע על מצביע, tempהוא מצביע 'פשוט' אשר יודע להצביע על קופסה )לעיתים נשתמש בביטוי 'מצביע מדרגה ראשונה' ,בניגוד למצביע למצביע שהינו מצביע מדרגה שניה(. 139 של הראשון העותק התנאי: להתבצע. מתחיל delete_node )(*root_ptr == NULLאינו מתקיים ,שכן root_ptrמצביע על מצביע שערכו אינו .NULLגם התנאי )((*root_ptr)-> _data == numאינו מתקיים ,שכן בשדה ה data -בקופסה עליה מצביע המצביע *root_ptrיש את הערך ) 17ולא את הערך .(13אנו מתקדמים ,לפיכך ,לתנאי (num < (*root_ptr)-> _data ) אשר מתקיים ,דבר שגורם לפונקציה לקרוא רקורסיבית עם המצביע ) . &((*root_ptr)->_leftנבדוק מיהו מצביע זה *root_ptr :הוא המצביע לשורש העץ ,על כן ) ((*root_ptr)->_leftהוא הבן השמאלי של שורש העץ, כלומר המצביע ב' .הביטוי &(…) :מחזיר לנו מצביע למצביע ב' ,ומצביע זה הוא שמועבר )כפרמטר ערך( לקריאה הרקורסיבית השניה .כתובת החזרה של הקריאה הרקורסיבית השניה היא סיומה של הקריאה הראשונה ל . delete_node -נציג את מצב הזיכרון: 17 א' ב' 13 27 ג' 43 =root ה' ד' =root_ptr num=13 =temp return=main 3 16 ו' 14 =root_ptr num=13 =temp return=delete העותק השני של delete_nodeמתחיל להתבצע .גם הפעם התנאי: >(*root_ptr)- ) (*root_ptr ==NULLאינו מתקיים .לעומתו התנאי: )_data == numמתקיים ,שכן *root_ptrהוא הפעם המצביע ב' ,ולכן (*root_ptr)-> _dataהוא הערך המצוי בקופסה עליה מורה המצביע ב', כלומר הערך .13לכן אנו נכנסים לגוש בו מתבצעת המחיקה .התנאי: (*root_ptr)->_left == NULLאינו מתקיים ,שכן ערכו של הבן השמאלי של הקופסה עליה מצביע המצביע ב' )שהינו ,כזכור (*root_ptr ,אינו .NULLבאופן דומה גם התנאי(*root_ptr)-> _right == NULL :אינו מתקיים ,ולכן אנו מגיעים ל else -האחרון ,המטפל במקרה בו לצומת שיש למחוק יש שני בנים .עתה ַמנים את הפונקציה find_succאשר תחזיר מצביע ,למצביע אשר מורה על אנו מז ְ הצומת שמכיל את העוקב של 13בעץ .אנו מעבירים לפונקציה find_succעותק של ) root_ptrהפונקציה מקבלת פרמטר ערך מאותו טיפוס כמו :root_ptr מצביע למצביע( .נציג את מצב הזיכרון בעקבות זימון .find_succ 140 17 א' =root ב' 13 27 ג' ד' 43 ה' =root_ptr num=13 =temp return=main 3 16 =root_ptr num=13 =temp return=delete ו' 14 =root_ptr =temp הפונקציה find_succמתחילה להתבצע .למשתנה ) tempשהינו מטיפוס מצביע למצביע( מוכנס הערך . &( (*root_ptr)-> _right ) :נבחן ביטוי זה: *root_ptrהוא המצביע ב'; לכן (*root_ptr)-> _rightהוא המצביע ד' לכן: )בנו הימני של הצומת עליו מצביע המצביע ,שעליו מצביע ; (root_ptr ) &( (*root_ptr)-> _rightהוא מצביע למצביע ד'; וזה ,כאמור ,הערך שמוכנס ל .temp -בכך פנינו לבנו הימני של הצומת שברצוננו למחוק .נציג את מצב רשומת ההפעלה של ) find_succהתעלמו מהמלבן המקווקו ומהמצביע שבו(: 17 א' ב' 13 27 ג' 43 =root ה' ד' =root_ptr num=13 =temp return=main 3 16 ו' 14 =root_ptr num=13 =temp return=delete =root_ptr =temp עתה אנו פונים לולאה .התנאי( (*temp)->_left != : ) NULLמתקיים .שכן *tempהוא המצביע ד' ,ולכן (*temp)->_leftהוא המצביע ו' ,שערכו אינו .NULLלכן אנו ומפנים את tempלהצביע על (*temp)- נכנסים לגוף הלולאהְ , ,>_leftכלומר על המצביע ו' .רשומת ההפעלה של find_succנראית באופן הבא ) ִב ְמקום כפי שהיא צוירה קודם: )לא ציירנו שוב את root_ptrשכן ערכו נותר כמו קודם(. 141 =root_ptr =temp לקראת סיבוב נוסף בלולאה שוב נבדק התנאי( (*temp)->_left != NULL) : אולם הפעם הוא כבר אינו מתקיים ,שכן בנו השמאלי של הצומת עליו מצביע ,*tempכלומר בנו השמאלי של הצומת המצביע ו' ,מכיל את הערך .NULL הפונקציה אינה נכנסת ללולאה ,והיא מחזירה את ערכו של ,tempכלומר מצביע למצביע ו'. הערך המוחזר על-ידי .delete_nodeמצב הזיכרון הוא איפה: find_succ למשתנה מוכנס succ_ptr של 17 א' =root ב' 13 27 ג' ד' 43 ה' =root_ptr num=13 =temp return=main 3 16 =root_ptr num=13 =temp =succ_ptr return=delete ו' 14 העותק השני של delete_nodeמוצא מקיפאונו ,וממשיך להתבצע .הפקודה: (*root_ptr)-> _data = (*succ_ptr)-> _dataגורמת לאפקט הבא: *root_ptrהוא המצביע ב' ,לכן (*root_ptr)-> _dataהוא שדה הdata - בקופסה עליה מורה המצביע ב'; לשדה זה אנו מכניסים את הערך של: ; (*succ_ptr)-> _dataמכיוון ש *succ_ptr -הוא המצביע ו' ,אזי (*succ_ptr)-> _dataהוא הערך .14לסיכומון :הערך 13בקופסה עליה מצביע ב' מוחלף בערך .14הפקודה אחר-כך מפנה את המצביע ) ,tempשיודע להצביע על קופסות ,ולא על מצביעים( ,להצביע כמו ,*succ_ptrכלומר כמו המצביע ו' .מצב הזיכרון הוא עתה: 17 א' ב' 14 27 ג' 43 =root ה' ד' =root_ptr num=13 =temp return=main 3 16 ו' 14 =root_ptr num=13 =temp =succ_ptr return=delete >temp- הפקודה אחר-כך מפנה את המצביע ) (*succ_ptrלהצביע כמו ._rightערכו של temp-> _rightהוא ,NULLועל-כן גם המצביע ,*succ_ptr כלומר המצביע ו' ,מקבל ערך זה .הפקודה האחרונה בגוש משחררת את הזיכרון 142 עליו מצביע ,tempכלומר את הקופסה בה שכן ) 14לפני העתקתו גם לצומת של .(13 בכך מסתיים העותק השני של .delete_nodeעותק זה חוזר לנקודת הסיום של העותק הראשון ,ולכן בכך הסתיים תהליך המחיקה. 14.12כמה זה עולה? עץ חיפוש בינארי הינו מבנה נתונים חלופי למערך ממוין ,ועל כן מתאים להשוות את עלות הפעולות השונות על שני ה'-מתחרים' הללו .הפעולות בהן נתעניין הן הוספת נתון ,מחיקת נתון ,וחיפוש נתון .נשווה את עלותן היחסית של הפעולות השונות הן במקרה הממוצע והן במקרה הגרוע )כלומר במקרה בו הנתונים מסודרים כך שעלינו לעשות הכי הרבה עבודה כדי להשלים את הפעולה הרצויה(. 14.12.1הוספת נתון נניח שברצוננו להוסיף נתון שערכו vלמערך ממוין הכולל nנתונים .לשם כך, ראשית ,יהיה עלינו לאתר את המקום בו יש להוסיף את הנתון .vאיתור המקום המתאים יתבצע על-ידי הרצת חיפוש בינארי של ַ vבמערך .החיפוש הבינארי יעלה ) log2(nפעולות הן במקרה הממוצע )וזאת לא הוכחנו( ,והן במקרה הגרוע )וזאת הראנו( .במידה והחיפוש יסתיים בהצלחה ,כלומר הערך vכבר מצוי במערך ,נוסיף את הערך החדש לצד הערך הקיים .במידה והחיפוש יסתיים בכישלון ,כלומר הערך vלא מצוי במערך ,אזי המקום בו החיפוש הסתיים הוא המקום בו על הערך ִלשכון, לשם נכניס את הערך .בשני המצבים) ,הן אם הערך כבר מצוי במערך ,והן אם ולכן ַ במקרה הממוצע יהיה עלינו להזיז מחצית מאברי המערך תא אחד ימינה כדי לא(ִ : ַ לפנות מקום לנתון החדש ,ובמקרה הגרוע יהיה עלינו להזיז nנתונים תא אחד ימינה( .הן במקרה הממוצע ,והן במקרה הגרוע ,פעולת ההזזה תחייב עבודה בשיעור של nצעדים .כלומר הוספת איבר למערך ממוין תחייב עבודה בשיעור nצעדים ,הן במקרה הממוצע )בו הנתון מוכנס באמצע המערך( ,והן במקרה הגרוע )בו הנתון מוסף בתחילת המערך() .מעלוּת החיפוש התעלמנו שכן היא זניחה יחסית ל.(n - עתה נניח כי ברצוננו להוסיף נתון לעץ חיפוש הכולל nנתונים .במקרה הממוצע יהיה עומקו של העץ בסדר גודל של )) lg2(nוזאת לא הוכחנו( .כדי להוסיף את הנתון נצטרך לרדת על הענף המתאים ,עד למקום בו יש להוסיף את הנתון .ההתקדמות על הענף הרצוי תדרוש ) lg2(nצעדים .במקרה הגרוע העץ 'יתנוון' לכדי רשימה מקושרת. מציאת המקום בו יש להוסיף את הנתון ברשימה מקושרת מחייבת אותנו לסרוק בממוצע מחצית הרשימה ,ובמקרה הגרוע את כל הרשימה ,כלומר לבצע עבודה בשיעור של nפעולות .על-כן הוספת נתון לעץ חיפוש עלולה לחייב במקרה הגרוע ביצוע של nצעדים. לסיכום :הוספת איבר לעץ חיפוש יעילה יותר מאשר הוספתו למערך ממוין במקרה הממוצע ,אך לא במקרה הגרוע. כדי שלא להקלע בעצי חיפוש בינאריים למקרה הגרוע ,בו העץ התנוון לכדי רשימה, ישנם עצי חיפוש אשר יודעים 'לאזן את עצמם' ,כלומר להישאר בעומק של )lg2(n תמיד .דוגמות לעצים כאלה הם עצים אדומים שחורים ,או עצי שתיים שלוש. בקורס שלנו לא נכיר עצים אלה .רק כדי לסבר את העין ,בעץ כזה אם הקלט כולל את הנתונים ,1אח"כ ,2 :אח"כ ,3 :אזי העץ ידע 'לשנות את עצמו' כך שבשורש העץ יוצב הערך ,2בנו השמאלי יהיה ,1ובנו הימני ,3והוא יוותר מאוזן .כל זאת ,כמובן, בלי להשקיע בכך 'יותר מדי עבודה'. 143 14.12.2מחיקת נתון עלות מחיקת נתון דומה לעלות הוספת נתון .במערך ממוין נאלץ ,ראשית ,לאתר את הנתון ,תוך שימוש בחיפוש בינארי .אחר יהיה עלינו 'לצופף' את הנתונים כדי 'לסגור' את התא שנותר ריק .עלות החיפוש היא ) log2(nפעולות הן במקרה הממוצע והן במקרה הגרוע ,ועלות ההזזה תא אחד ימינה היא nפעולות הן במקרה הממוצע והן במקרה הגרוע. כדי למחוק איבר מעץ חיפוש יהיה עלינו ראשית לאתרו .במקרה הממוצע הדבר ידרוש ) lg2(nצעדים ,ובמקרה הגרוע nצעדים .מחיקת הנתון תחייב הן במקרה הממוצע והן במקרה הגרוע איתור של העוקב של הערך) .שימו לב כי אנו טוענים כאן ,בלי להוכיח בצורה מסודרת ,כי גם ַבמקרה הממוצע נאלץ לאתר את העוקב; שבעץ מלא ,שהינו הקרוּב ְלעץ ממוצע ,מחצית האינטואיציה שמאחורי הטענה היא ְ מהערכים מצויים בצמתים פנימיים ,יש להם שני בנים ,ועל-כן מחיקתם מחייבת איתור של העוקב להם( .איתור העוקב יעלה במקרה הממוצע ) log2(nצעדים. במקרה הגרוע ידרוש איתור העוקב סדר גודל של nצעדים) ,אתם מוזמנים לצייר לעצמכם עץ בו זה המצב( .אחרי שאיתרנו את העוקב ,דורשת פעולת המחיקה רק מספר קבוע )ועל כן זניח( של צעדים .לכן מחיקת ערך מעץ דורשת במקרה הממוצע ) lg2(nצעדים ,ובמקרה הגרוע nצעדים. שוב אנו רואים כי עץ חיפוש שקול )מבחינת כמות העבודה הנדרשת( למערך ממוין ַבמקרה הגרוע ,אך עדיף על-פני מערך ממוין ַבמקרה הממוצע. 14.12.3חיפוש נתון עתה נשאל את עצמנו כמה עולה חיפוש נתון בכל אחד משני מבני הנתונים .כבר אמרנו כי במערך ממוין ידרוש החיפוש ) log2(nצעדים הן במקרה הגרוע והן במקרה הממוצע .לעומת זאת בעץ ידרוש החיפוש ) log2(nצעדים במקרה הממוצע ,אך n צעדים במקרה הגרוע )בו העץ התנוון לכדי רשימה(. לסיכום ,במקרה הממוצע )או בעץ 'שיודע לאזן את עצמו'( :עץ יעיל יותר ממערך בהוספה ובמחיקה )שתי הפעולות עולות ) log2(nצעדים בעץ ,לעומת nצעדים במערך( ,ושקול למערך בחיפוש )שתי הפעולות עולות ) log2(nצעדים בשני מבני הנתונים( .במקרה גרוע :מערך ועץ שקולים בהוספה ומחיקה )שתי הפעולות עולות n צעדים בשני מבני הנתונים( ,ומערך יעיל יותר מעץ בחיפוש )אשר עולה )log2(n צעדים במערך ,לעומת nצעדים בעץ(. == מכאן ואילך לא עודכן ==2010-11 14.13האם עץ בינארי מלא עץ בינארי נקרא מלא אם: .Iלכל צומת פנימי יש שני בנים ,וכן: .IIכל העלים מצויים באותה רמה. מבין שלושת העצים שהצגנו בציור האחרון )בסעיף הקודם( ,רק האמצעי הוא עץ בינארי מלא .כפי שראינו העץ הימני אינו מקיים את התנאי א' ,ובעץ השמאלי לא כל העלים מצויים באותה רמה. 144 עתה ברצוננו לכתוב פונקציה אשר מקבלת מצביע לעץ בינארי ,ומחזירה ערך בולאני המעיד האם העץ מלא .נציג שתי גרסות שונות לפונקציה .הגרסה הראשונה תהא מימוש ישיר של ההגדרה .ראשית נציג אותה: { )bool is_fool(const struct Node * const on_the_hill ; bool ok ; unsigned int the_depth ; ) if (on_the_hill == NULL) return( true ; )ok = non_leaf_2_sons(on_the_hill ; ) if (! ok) return( false )the_depth = depth(on_the_hill ; )ok = all_leaves_at_depth(on_the_hill, depth, 1 ; )return(ok } את הפונקציות non_leaf_2_sonsו depth -כבר פגשנו .נותר לנו עוד לכתוב את הפונקציה .all_leaves_at_depthפונקציה זאת מקבלת שלושה פרמטרים: .Iמצביע לשורשו של )תת( עץ בינארי. .IIעומקו של העץ המקורי )השלם( עבורו נערכת הבדיקה. .IIIהעומק בעץ המקורי בו מצוי שורש תת-העץ הנוכחי .מכיוון ששורש העץ השלם מצוי בעומק של אחד ,אזי בקריאה הראשונה לפונקציה מועבר לה הערך .1כל קריאה רקורסיבית לפונקציה ,עם בניו של השורש הנוכחי ,תעביר ערך גדול באחד מהערך שהועבר לה. אנו מניחים כי לא יקראו לפונקציה עם עץ ריק .על-כן בפונקציה ,if_foolבמידה והעץ ריק אנו מחזירים את הערך trueמיידית ,ולכן איננו מזמנים את הפונקציה .all_leaves_at_depthגם בפונקציה all_leaves_at_depthאנו מקפידים לקרוא רקורסיבית רק עבור בן שאינו ריק. נציג את הפונקציה: bool all_leaves_at_depth(const struct Node * const root, { )unsigned int tree_depth, unsigned int curr_depth ; bool ok = true )if (root->_left == NULL && root-> _right == NULL ; ) return( curr_depth == tree_depth )if (root->_left != NULL ok = all_leaves_at_depth(root->_left, )// (- ; )tree_depth, curr_depth +1 ; )if (!ok) return(false )if (root-> _right != NULL ok = all_leaves_at_depth(root-> _right, )// (+ ; )tree_depth, curr_depth +1 ; )return(ok 145 } הסבר הפונקציה :במידה והצומת הנוכחי הוא עלה )ערכם של המצביעים _ leftו- _ rightבצומת הוא (NULLאזי יש להחזיר את הערך trueבמידה ו- ,curr_depth==tree_depthכלומר במידה והעלה הנוכחי מצוי ַברמה שינה עומק העץ .פקודת ה ,return -ראשית ,תעריך את הביטוי הבולאני: ,curr_depth==tree_depthושנית ,תחזיר את ערכו של הביטוי; ולכן יוחזר הערך המתאים. אם הבן השמאלי אינו ריק אנו קוראים עבורו .במידה ומוחזר ערך falseהעותק הנוכחי מחזיר מייד ערך זה .שימו לב כי אם הבן השמאלי הוא ריק) ,ולכן לא נערכת קריאה רקורסיבית( ,אזי תודות לכך ש ok -אותחל לערך trueהתנאי (!ok) :לא יתקיים ,ולא יוחזר הערך falseבאופן נחפז .באותו אופן ,אם הבן הימני אינו ריק אזי נערכת קריאה עבורו ,ומוחזר הערך שחזר מהקריאה הרקורסיבית .אם הבן הימני ריק ,אזי לא תיערך קריאה רקורסיבית עבורו ,ובמקרה זה יוחזר הערך true שחזר מהקריאה עם הבן השמאלי) .בדקו לעצמכם מדוע אם הבן הימני ריק אזי אנו מובטחים ש) :א( נערכה קריאה עם הבן השמאלי ,וכן) :ב( קריאה זאת החזירה את הערך trueלתוך המשתנה .(ok מכיוון שמדובר בפונקציה לא טריוויאלית נציג שתי דוגמות הרצה. בדוגמה הראשונה מצב הזיכרון טרם הקריאה לפונקציה is_foolהוא: root (of = )main הפונקציה מקבלת עותק של המצביע לשורש העץ .לכן מצב הזיכרון בעקבות זימון הפונקציה הוא: root (of = )main =on_the_hill 146 הפונקציה מתחילה להתבצע .ערכו של המצביע on_the_hillאינו .NULL הפונקציה non_leaf_2_sonsמחזירה את הערך trueלתוך המשתנה ,okועל- כן פקודת ה return -העוקבת לקריאה לפונקציה אינה מתבצעת .בשלב הבא מוכנס למשתנה the_depthעומקו של העץ ,שהוא שתיים .עתה מזמנת is_fool את הפונקציה . all_leaves_at_depthהיא מעבירה לה את שורש העץ ,את עומקו של העץ ,כלומר את הערך שתיים ,ואת הקבוע .1נציג את מצב הזיכרון: root (of = )main =on_the_hill =root tree_depth=2 curr_depth=1 התנאי בפקודת ה if -הראשונה אינו מתקיים ,שכן לשורש הנוכחי יש שני בנים שאינם ריקים .התנאי )(root->_left != NULLמתקיים ,ולכן אנו קוראים רקורסיבית עם בנו השמאלי של השורש .שימו לב לערכם של הפרמטרים ,כפי שמופיע בציור .כתובת החזרה של הקריאה הרקורסיבית המזומנת היא ההשמה ).(- נציג את מצב הזיכרון: root (of = )main =on_the_hill =root tree_depth=2 curr_depth=1 return=is_fool =root tree_depth=2 curr_depth=2 )return= (- 147 העותק השני של all_leaves_at_depthמתחיל להתבצע .הפעם התנאי: )(root->_left == NULL && root-> _right == NULLמתקיים ,ולכן מוחזרת תוצאת ההשוואה . curr_depth == tree_depthמכיוון שערכם של שני הפרמטרים הוא 2מוחזר הערך .trueערך זה חוזר לעותק הראשון של ,all_leaves_at_depthלמשתנה .okבשלב זה העותק הראשון 'מופשר מקפאונו' .התנאי ) (!okאינו מתקיים ,ולכן אנו מתקדמים לפקודת הif - הבודקת האם ) .(root-> _right != NULLהתנאי בפקודה מתקיים ,ולכן אנו מזמנים שוב את .all_leaves_at_depthהפעם כתובת החזרה היא ההשמה )(+ בעותק הראשון של הפונקציה .נציג את מצב הזיכרון ,בפרט את רשומת ההפעלה של הפונקציה הנקראת: root (of = )main =on_the_hill =root tree_depth=2 curr_depth=1 return=is_fool =root tree_depth=2 curr_depth=2 )return= (+ העותק הנוכחי של הפונקציה מתחיל להתבצע .התנאי: )(root->_left == NULL && root-> _right == NULLמתקיים ,ולכן מוחזרת תוצאת ההשוואה . curr_depth == tree_depthמכיוון שערכם של שני הפרמטרים הוא 2מוחזר הערך .trueערך זה חוזר לעותק הראשון של ,all_leaves_at_depthלמשתנה okבהשמה ) .(+ערכו של ,okכלומר הערך ,trueמוחזר על-ידי העותק הראשון )שנקרא על-ידי .(is_foolבשלב זה is_foolיכולה להחזיר את הערך trueכראוי. עתה נראה כיצד תתנהל ריצה של is_foolעל העץ הבא )עליו מצביע rootשל התכנית הראשית(: מצב הזיכרון עם הקריאה לפונקציה is_foolהוא: 148 root (of = )main =on_the_hill כפי שראינו בסעיף הקודם ,הפונקציה non_leaf_2_sonsתחזיר את הערך ,trueכלומר לא היא שתעיד על כך שהעץ אינו מלא .הפונקציה depthתחזיר את הערך 3לתוך המשתנה .the_depthבשלב הבא אנו מזמנים את הפונקציה ,all_leaves_at_depthוהיא שאמורה להחזיר את הערך ,falseאשר יוחזר על-ידי .is_foolנבחן עתה את פעולתה של .all_leaves_at_depthבקריאה הראשונה לה מועברים לה ארגומנטים כמתואר בציור: root (of = )main =on_the_hill =root the_depth= 3 curr_depth= 1 return=is_fool העותק הראשון של all_leaves_at_depthמתחיל להתבצע .התנאי ) (root->_left == NULL && root-> _right == NULLאינו מתקיים. בפקודה הבאה ,התנאי ) (root->_left != NULLמתקיים ,ולכן מזומנת קריאה רקורסיבית .ערכי הארגומנטים המועברים לקריאה הרקורסיבית הם כפי שמופיע בציור: root (of = )main =on_the_hill =root the_depth= 3 curr_depth= 1 return=is_fool =root the_depth= 3 curr_depth= 2 )return=(- 149 העותק השני של all_leaves_at_depthמתחיל להתבצע .עת התנאי ) (root->_left == NULL && root-> _right == NULLמתקיים ,ולכן הערך המוחזר על-ידי הקריאה השניה ל all_leaves_at_depth -הוא ערכו של הביטוי הבולאני . ( curr_depth == tree_depth ) :מכיוון שערכם של curr_depthו the_depth -שונֵה ,מוחזר הערך .falseערך זה מוכנס בהשמה ) (-למשתנה okשל העותק הראשון של .all_leaves_at_depthמכאן העותק הראשון של הפונקציה מתקדם לפקודה , if (!ok)… :אשר גורמת לו להחזיר את הערך .falseערך זה מוחזר לפונקציה is_foolכראוי; והיא על-כן מחזירה בתורה את הערך .false עד כאן הצגנו גרסה ראשונה של הפונקציה הבודקת האם עץ בינארי מלא .הגרסה הראשונה הינה מימוש ישיר של ההגדרה .עתה נרצה להציג גרסה שניה ,שלא תסתמך ישירות על ההגדרה ,אלא על אובזרבציה פשוטה .נשים לב כי בעץ מלא בעומק 1קיים צומת יחיד; בעץ מלא שעומקו 2קיימים 3צמתים ,בעץ מלא שעומקו 3קיימים 7צמתים ,ובאופן כללי בעץ מלא שעומקו nקיימים 2n -1צמתים .ומכאן נוכל לענות על השאלה האם עץ בינארי מלא באופן הבא: { )bool simple_is_full(const struct Node * const root ; unsigned int the_depth, num_of_nodes ; )the_depth = depth(root ; )num_of_nodes = count_nodes(root ; ) return( power(2, the_depth) –1 == num_of_nodes } מה עשינו? נאתר את עומקו של העץ) ,באמצעות פונקציה שכתבנו בעבר(; נספור את מספר הצמתים בעץ )באמצעות פונקציה שכתבנו בעבר(; נחזיר את תוצאת ההשוואה בין מספר הצמתים בעץ ל :שתיים בחזקת עומק העץ ,מינוס אחד) .את הפונקציה powerהמעלה מספר טבעי אחד בחזקת מספר טבעי שני יהיה עלינו לממש(. 14.14האם בכל הצמתים בעץ השוכנים באותה רמה מצוי אותו ערך עתה ברצוננו לכתוב פונקציה אשר מקבלת מצביע לעץ בינארי )שאינו עץ חיפוש(, ומחזירה את הערך trueאם ורק אם בכל אחת מהרמות בעץ יש אותו ערך בכל הצמתים .לדוגמה ,על הפונקציה להחזיר את הערך trueעל העץ הימני ,אך לא על השמאלי: 7 5 2 7 5 2 5 2 2 150 5 2 1 14.14.1הפונקציהequal_vals_at_level : הפונקציה שנכתוב תכלול לולאה אשר תעבור על רמות העץ בזו אחר זו ,החל מהרמה מספר אחת )שורש העץ( ,ועד רמת העלים .עבור כל רמה ראשית נשלוף ערך כלשהו המצוי באותה רמה ,ושנית נבדוק האם בכל הצמתים באותה רמה מצוי ערך המפרה את הדרישות נחזיר מיידית את הערך .falseנציג ֵ זה .במידה וגילינו רמה את הפונקציה: )bool equal_vals_at_level(const struct Node * const root { ; unsigend int a_depth ; int val ; bool ok { )for (a_depth= 1; a_depth <= depth(root); a_depth ++ ; )ok = get_val_from_level(root, a_depth, 1, val ; )ok = check_level(root, a_depth, val, 1 ; ) if (!ok) return( false } ; )return(true } הפונקציה פשוטה למדי ,היא כתובה על-פי עקרונות התכנות המודולרי ,ועצוב מעלה-מטה .שימו לב כי עבור עץ ריק יוחזר הערך trueבלי שהפונקציה תכנס ללולאה אף לא פעם אחת )שכן התנאי a_depth <= depth(root) :לא יתקיים כבר בפעם הראשונה שנגיע ללולאה ,וזאת משום שעומקו של עץ ריק הוא אפס(. 14.14.2הפונקציהget_val_from_level : נִ פנה עתה להצגת הפונקציות בהן equal_vals_at_levelעושה שימוש. לפונקציה get_val_from_levelארבעה פרמטרים: .Iפרמטר ערך באמצעותו מועבר לה מצביע לשורש העץ ממנו עליה לשלוף את הערך הרצוי. .IIפרמטר ערך המציין מאיזו רמה בעץ השלם יש לשלוף את הערך. .IIIפרמטר ערך באמצעותו מועבר ַלפונקציה העומק בעץ השלם בו מצוי הצומת שמצביע אליו מועבר לה בקריאה הנוכחית. .IVפרמטר משתנה לתוכו יוכנס ערך כלשהו מהרמה המבוקשת בעץ המבוקש. הפונקציה מחזירה ערך בולאני המורה האם היא הצליחה לאתר ערך כלשהו ברמה המבוקשת .הערך הבולאני המוחזר ישרת אותנו בקריאות הרקורסיביות תקרא לעצמה .מכיוון שבקריאה הראשונה לפונקציה מתוך ְ שהפונקציה equal_vals_at_levelאנו מובטחים שמועבר עומק תקין ,כלומר כזה שאינו גדול מעומק העץ השלם ,אזי בערך המוחזר על-ידי הפונקציה get_val_from_levelל equal_vals_at_level -איננו עושים שימוש. נציג עתה את קוד הפונקציה: bool get_val_from_level(const struct Node * const root, unsigned int wanted_depth, unsigned int curr_depth, 151 { )int &val ; bool success ; ) if (root == NULL) return( false { )if (wanted_depth == curr_depth ; val = root -> data ; ) return( true } success = get_val_from_level(root->_left, )// (- wanted_depth, curr_depth +1, ; )val ; ) if (success) return( true success = get_val_from_level(root->_left, )// (+ wanted_depth, curr_depth +1, ; )val ; ) return( success } הסבר :ראשית ,אם העץ ריק ,סימן שההתקדמות הייתה על ענף בו לא מצוי ערך ברמה המבוקשת ,ועל-כן הפונקציה מחזירה את הערך .falseשנית ,אם הגענו לרמה המקיימת ש(wanted_depth == curr_depth) :אזי אנו ַבעומק המבוקש; על-כן יש להכניס לפרמטר המשתנה valאת הערך המצוי בצומת זה ,ויש להחזיר את הערך ,trueכדי לציין שמצאנו ערך בעומק המבוקש .לבסוף ,אם העץ אינו ריק ,וכן עדיין לא הגענו ַלעומק המבוקש ,אזי אנו תחילה קוראים רקורסיבית עם הבן השמאלי .במידה והקריאה הרקורסיבית מחזירה את הערך ,trueסימן שהיא הצליחה לאתר ערך בעומק המבוקש )בבן השמאלי( ,ולכן הקריאה הנוכחית יכולה לסיים ,ולהחזיר את הערך .trueאם הקריאה עם הבן השמאלי לא הצליחה לאתר ערך בעומק המבוקש )ועל כן החזירה את הערך ,(falseאזי אנו קוראים ֵ רקורסיבית עם הבן הימני ,ומחזירים את האיתות המוחזר מהקריאה הרקורסיבית. 152 נדגים הרצה של הפונקציה על העץ הבא: 7 8 5 6 נניח כי הפונקציה מתבקשת להחזיר ערך המצוי ברמה שלוש בעץ .מצב הזיכרון עם הקריאה לפונקציה הוא הבא: )root (of main = 7 8 5 root (of )…equal_vals = =val return= main 6 = root wanted_depth= 3 curr_depth= 1 =val return=equal.. שימו לב לערכם של הפרמטרים השונים :הפרמטר rootמצביע על שורש העץ; wanted_depthמציין את הרמה ממנה יש לשלוף ערך ,כלומר curr_depth ;3 מציין את העומק בעץ בו אנו מצויים עתה ,כלומר val ;1הוא פרמטר משתנה ולכן שלחנו חץ לארגומנט המתאים; כתובת החזרה של העותק הראשון של get_val_from_levelהיא הפונקציה .equal_vals_at_level הפונקציה get_val_from_levelמתחילה להתבצע .ערכו של rootאינו .NULL גם התנאי ) (wanted_depth == curr_depthאינו מתקיים ,ועל-כן אנו מתקדמים לפקודה אשר מזמנת קריאה רקורסיבית לפונקציה עם הבן השמאלי של השורש הנוכחי .כתובת החזרה של העותק השני של הפונקציה היא ההשמה )(- בעותק הראשון .נציג את מצב המחסנית עם הקריאה לעותק השני: 153 )root (of main = 7 8 5 root (of )…equal_vals = =val return= main 6 = root wanted_depth= 3 curr_depth= 1 =val return=equal.. = root wanted_depth= 3 curr_depth= 2 =val )return= (- אני מסב את תשומת לבכם לערכם של הפרמטרים השונים בקריאה השניה לפונקציה. הקריאה השניה מתחילה להתבצע .התנאים בשתי פקודות ה if -הראשונות אינם מתקיימים ,על כן הפונקציה קוראת רקורסיבית עם הבן השמאלי של הצומת אשר מצביע אליו מועבר לה בפרמטר .rootהבן השמאלי דנן ערכו ,NULLועל כן הקריאה הרקורסיבית השלישית מחזירה מיידית את הערך falseלמשתנה successשל הקריאה הרקורסיבית השניה .עתה הקריאה הרקורסיבית השניה קוראת עם בנה הימני .גם ערכו של מצביע זה הוא ,NULLועל כן גם הקריאה עמו מחזירה את הערך .falseלקריאה הרקורסיבית השניה לא נותר עוד דבר לעשותו, ואין לה בררה אלא להחזיר את הערך falseהמעיד שמכיווּן זה בעץ לא הצלחנו לשלוף ערך כלשהו מעומק שלוש בעץ .הערך המוחזר על-ידי הקריאה הרקורסיבית השניה מוכנס בהשמה ) (-למשתנה successשל הקריאה הרקורסיבית הראשונה. הקריאה הרקורסיבית הראשונה ניסתה ,עד כה ,לשלוף ערך מעומק שלוש בעץ על- ידי התקדמות לבנה השמאלי; הניסיון נכשל .על-כן עתה הקריאה הרקורסיבית הראשונה תנסה לשלוף את הערך מבנה הימני .הקריאה הראשונה מתקדמת להשמה ) (+ומזמנת קריאה רקורסיבית נוספת .נציג את מצב הזיכרון: 154 root (of main) = root (of equal_vals…) = val= return= main 7 8 5 6 root = wanted_depth= 3 curr_depth= 1 val= return=equal.. root = wanted_depth= 3 curr_depth= 2 val= return= (+) התנאים בשתי.נציין את הקריאה הרקורסיבית הנוכחית כקריאה מספר חמש כן הקריאה- על,פקודות התנאי הראשונות בקריאה מספר חמש אינם מתקיימים .(6 מספר חמש קוראת רקורסיבית עם בנה השמאלי )כלומר עם מצביע לצומת של : נציג את מצב הזיכרון.נסמן את הקריאה המזומנת כקריאה מספר שש root (of main) = root (of equal_vals…) = val= return= main 7 8 5 6 root = wanted_depth= 3 curr_depth= 1 val= return=equal.. root = wanted_depth= 3 curr_depth= 2 val= return= (+) root = wanted_depth= 3 curr_depth= 3 val= return= (-) 155 הקריאה מספר שש מתחילה להתבצע .התנאי )(root == NULLאינו מתקיים; אך התנאי (tree_depth == curr_depth) :מתקיים מאוד .על כן הפונקציה מכניסה לתוך הפרמטר המשתנה valאת ערכו של ,root-> _dataכלומר את הערך שש .מכיוון ש val -הוא פרמטר משתנה אנו עוקבים אחר החצים כל הדרך אל הבנק עד למשתנה valשל הפונקציה ,equal_vals_at_levelושם שמים אחר העותק מספר שש מחזיר את הערך .trueערך זה חוזר בהשמה (- את הערךַ . ) למשתנה successשל העותק מספר חמש .בזאת מסתיים גם העותק מספר חמש תוך שהוא מחזיר את ערכו של successלהשמה ) (+של העותק הראשון .עתה העותק הראשון יכול להחזיר את הערך trueהמצוי במשתנה successשלו לפונקציה equal_vals_at_levelאשר זימנה את העותק הראשון של .get_val_from_levelבזאת השלמנו את המשימה של שליפת ערך כשלהו מעומק שלוש בעץ. 14.14.3הפונקציהcheck_level : עתה עלינו לכתוב את הפונקציה השניה בה equal_vals_at_levelעושה שימוש ,את הפונקציה .check_levelפונקציה זאת מקבלת ארבעה פרמטרי ערך: .Iמצביע לשורש )תת(-העץ המועבר לה. .IIהעומק אותו עליה לבדוק) .להזכירכם ,על הפונקציה לבדוק האם בכל הצמתים ַבעומק הרצוי קיים אותו ערך(. .IIIהערך שאמור להימצא בכל הצמתים בעומק הרצוי. .IVהעומק בעץ השלם בו מצוי הצומת שמצביע אליו מועבר בפרמטר הראשון. בקריאה הראשונה ל) ,check_level -זו המזומנת על-ידי equal_vals_at_levelיועבר שורש העץ השלם ,והערך ,1שכן שורש העץ מצוי בעומק אחד( .במהלך הבדיקה יהיה על הפונקציה להתקדם באמצעות קריאות רקורסיביות לעומק הרצוי .כל קריאה רקורסיבית תעביר לקריאה שהיא מזמנת את עומקו בעץ השלם של הצומת המועבר בפרמטר הראשון(. נציג את הפונקציה :check_level bool check_level( const struct Node * const root, unsigned int wanted_depth, int val , { )unsigned int curr_depth ; ) if (root == NULL) return( true )if (curr_depth == wanted_depth ; ) return( root-> _data == val return( check_level(root->_left, wanted_depth, val, )curr_depth +1 && check_level(root-> _right, wanted_depth, val, )curr_depth +1 ; ) } הסבר :אם הגענו לתת-עץ ריק לא גילינו הפרה של הדרישה שבכל הצמתים בעומק הרצוי ) (wanted_depthיהיה הערך המבוקש ) ,(valולכן אנו מחזירים את הערך .trueבמילים אחרות ,אם הגענו לתת-עץ ריק משמע הענף הנוכחי מסתיים בעומק קטן מ ,wanted_depth -ועל כן בו אין צומת המצוי בעומק ,wanted_depth 156 בפרט אין צומת שמפר את הדרישה שבכל הצמתים בעומק המבוקש יהיה את הערך ְ .val מנגד ,אם ) (curr_depth == wanted_depthאזי הגענו ַלעומק בו אנו מתעניינים ,ועל-כן יש להחזיר את הערך trueאם ורק אם בצומת קיים הערך .val פקודת ה return -ראשית ,משווה את הערך המצוי בצומת עם הערך המבוקש, ושנית ,מחזירה את תוצאת ההשוואה. לבסוף ,אם אף אחד משני התנאים הקודמים לא התקיים אזי מחד טרם הגענו לעומק הדרוש ,ומאידך יש עדיין לאן להתקדם על הענף עליו אנו מתקדמים ,לכן יש לקרוא רקורסיבית עם שני בניו של הצומת הנוכחי ,ולהחזיר את תוצאת הבדיקה המוחזרת על-ידם .הקוד הבא עושה זאת: return( check_level(root->_left, wanted_depth, val, )curr_depth +1 && check_level(root-> _right, wanted_depth, val, )curr_depth +1 ; ) נסביר מדוע וכיצד :הקוד כולל פקודת returnאשר מחזירה את תוצאת הקריאה הרקורסיבית עם הבן השמאלי וגם תוצאת הקריאה הרקורסיבית עם הבן הימני. אם לפחות אחת משתי הקריאות תחזיר את הערך falseאזי פקודת הreturn - תחזיר את הערך ,falseואם שתי הקריאות תחזרנה את הערך trueאזי פקודת ה return -תחזיר את הערך .true 14.14.4זמן הריצה של equal_vals_at_level כמה עבודה מבצעת הפונקציה equal_vals_at_levelעת היא בודקת עץ כלשהו? נניח כי על הפונקציה לבדוק עץ בעומק ַ .nבמקרה הגרוע ,כלומר ַבמקרה בו על הפונקציה לבצע את מירב העבודה ,העץ הינו מלא ,ולכן כולל 2n –1צמתים, וכמו כן בצמתים המצויים בכל רמה ורמה יש אותו ערך )ולכן הבדיקה אינה נעצרת באמצע התהליך ,אלא ִמתמצה( .עבוּר כל אחת ואחת מרמות העץ ,החל ברמה מספר אחת ועד הרמה מספר nמבצעת הפונקציה שתי פעולות: .Iהיא שולפת ערך המצוי בעומק הרצוי )נסמן עומק זה באות .(d .IIהיא בודקת את הצמתים ברמה מספר .d עבוּר כל אחת משתי הפעולות על פונקציה לסרוק כל צמתי העץ ברמות ,1..d כלומר לסרוק כ 2d -צמתים. כאמור ,את הבדיקה הנ"ל יש לבצע עבור ,d=1,…,nולכן כמות העבודה המתבצעת היא בשיעור . 21+…+2n :הביטוי שקיבלנו הוא סכומו של טור הנדסי בן nאיברים, שהאיבר הראשון בו ) (a1ערכו שתיים ,האיבר האחרון ערכו ,2nוהיחס בין כל זוג איברים עוקבים ) (qהוא שתיים .סכומו של טור זה מתקבל על-ידי הנוסחהs= : ) . a1*(qn –1)/(q –1יישומה של הנוסחה למקרה שלנו ייתן את הביטוי: ) , 2*(2n –1)/(2 –1עבור nשהינו עומק העץ .מספר הצמתים בעץ הוא, להזכירכם ,2n ,כלומר כמות העבודה המתבצעת על-ידי הפונקציה לינארית בכמות הצמתים. מה ,לעומת זאת ,יקרה אם העץ יתנוון לכדי רשימה משורשרת )כלומר לכל צומת יהיה רק בן יחיד(? במקרה זה בעץ שעומקו nיש nצמתים .עתה כדי לבדוק את הצומת )היחיד( מעומק dיש לסרוק dצמתים ,וזאת יש לבצע עבור ,d=1,…,n 157 כלומר כמות העבודה הינה בסדר גודל של ,1+2+…+n :כלומר בסדר גודל של ,n2 עבור nשמציין את עומקו של העץ ,ואת מספר הצמתים בעץ. על-כן במקרה הגרוע הפונקציה שכתבנו תרוץ בזמן ריבועי במספר הצמתים .לא נוכיח כאן כי במקרה הממוצע כמות העבודה שהפונקציה תבצע הינה כמו במקרה הראשון שראינו )בו העץ מלא( כלומר לינארית במספר הצמתים. 14.14.5פתרון חלופי לבעיה את הבעיה המוצגת בסעיף זה )האם בכל הצמתים באותה רמה בעץ מצוי אותו ערך( ניתן לפתור בצורה שונה ,פשוטה יותר .נציג כאן את קווי המתאר של הפתרון החלופי: d .Iאתר את עומק העץ ,שיסומן כ) .d -כזכור ,בעץ יש לכל היותר 2 -1צמתים(. .IIהקצה מערך בגודל 2d –1תאים שלמים. .IIIשכן את עץ במערך ,כפי שמתואר בסעיף .13.2 i i+1 .IVסרוק את המערך סדרתית .התאים מספר ) 2 ,…,2 -1עבור (i=1,…,d-1 במערך ,שאינם ריקים ,מכילים ערכים המצויים באותה רמה בעץ ,ועל-כן בדוק האם הם שווים. 14.15האם כל הערכים בעץ שונים זה מזה עתה ברצוננו לכתוב פונקציה בשם uniqueאשר מקבלת מצביע לעץ בינארי )שאינו דווקא עץ חיפוש( ומחזירה את הערך trueאם ורק אם כל הערכים בעץ שונים זה מזה ,במילים אחרות :אם לא קיימים בעץ שני צמתים שיש בהם אותו ערך. תמ ֵצא בצומת node הפונקציה uniqueתסרוק את כל צמתי העץ .עת הפונקציה ַ כלשהו ,בו מצוי הערך ,vיהיה עליה לקרוא לפונקציה count_valאשר תספור כמה פעמים מופיע הערך vבעץ בשלמותו .אם כל ערך מופיע בעץ רק פעם יחידה, אזי הפונקציה count_valתחזיר את הערך אחד )שכן היא תספור גם את המופע הנוכחי של .(vלכן אם count_valמחזירה ערך גדול מאחד מצאנו הפרה של הדרישה ,וניתן לסיים מיידית )את הרצתה של (uniqueתוך החזרת הערך .false קל יחסית להבין כי uniqueתקבל מצביע )בשם (curr_nodeלצומת כלשהו בעץ, והרקורסיה תתבצע על ,curr_nodeכלומר ַבקריאות הרקורסיביות השונות ַלפונקציה ישתנה ערכו של .curr_nodeהנקודה העדינה בפונקציה uniqueהיא שעל מנת ש unique -תוכל להעביר ל count_val -את שורש העץ השלם היא צריכה לקבל ְפרט למצביע curr_nodeמצביע לשורש העץ השלם )שיקרא .(tree_rootעל מצביע זה לא תיערך רקורסיה ,והוא יועבר כמות שהוא בקריאות הרקורסיביות השונות ל ,unique -וזאת על-מנת שכל קריאה רקורסיבית תוכל להעבירו ל.count_val - נציג ,ראשית ,את :unique bool unique(const struct Node * const curr_node, { )const struct Node * const tree_root ; if (curr_node == NULL) return true )if (count_val(tree_root, curr_node-> _data) > 1 158 ; return false ; ) && )return( unique(curr_node->_left, tree_root )unique(curr_node-> _right, tree_root } הסבר הפונקציה :ראשית ,אם תת-העץ אליו הגענו ריק אזי אין בו ערך המופיע בעץ יותר מפעם יחידה ,ועל-כן ניתן להחזיר את הערך .trueשנית ,אם תת-העץ אליו הגענו אינו ריק ,אזי יש לספור כמה פעמים מופיע בעץ השלם הערך >curr_node- ,_dataואת זאת אנו עושים על-ידי קריאה ל count_val -אשר מקבלת מצביע לשורש העץ השלם וערך רצוי .אם הפונקציה מחזירה ערך גדול מאחד ,אזי ניתן להסיק כי הערך המצוי בצומת הנוכחי אינו ייחודי ,ועל-כן יש להחזיר את הערך .falseלבסוף ,אם הערך בצומת הנוכחי התגלה כייחודי ,אזי יש להחזיר את תוצאת הקריאה הרקורסיבית לבן השמאלי ,וגם תוצאת הקריאה הרקורסיבית לבן המזוּמנות מקבלת את הבן הימני .שימו לב כי כל אחת מהקריאות הרקורסיביות ְ המתאים ,ואת ְ tree_rootכמות שהוא ,כלומר את שורש העץ השלם. שאלות למחשבה והשלמות נדרשות לסעיף זה: .Iכתבו את הפונקציה )הפשוטה למדי( .count_val .IIכמה עבודה מבצעת ) uniqueבמקרה הגרוע ביותר( עת עליה לבדוק עץ בן n צמתים? .IIIכתבו את uniqueעבור עץ חיפוש. כמה עבודה מבצעת uniqueעל עץ חיפוש? 159 14.16תרגילים 13.14.1תרגיל ראשון :עץ radix בהינתן אוסף מחרוזות המורכבות משתי אותיות בלבד a,bנרצה להחזיקן במבנה נתונים שיאפשר לנו להשיב ביעילות על 'שאילתות שייכות' ,כלומר ,בהינתן מחרוזת נרצה לדעת האם היא מופיעה באוסף המחרוזות הנתון ,או לא מופיעה. לדוגמא עבור אוסף המחרוזות } {a,abb,ba,baa,babbאנו עשויים להישאל האם המחרוזת baaמופיעה באוסף? )כן( האם המחרוזת babמופיעה בו? )לא( האם המחרוזת abbaמופיעה בו? )לא( וכדומה. דרך אפשרית לממש את המטרה היא להחזיק את האוסף הנתון בעץ בינרי הנקרא :radix treeהעץ יבנה ,על-סמך אוסף המחרוזות באופן הבא: .Iראשית ,בנה עץ הכולל שורש בלבד )לא עץ ריק(. אחר ,עבוֹר על כל מחרוזות האוסף ,בזו אחר זו ,והוסף אותן לעץ באופן הבא: ַ .II אחר אות ,משמאל לימין. התחל משורש העץ .קרא את המחרוזת הנוכחית אות ַ בכל צעד אם התו הבא הוא aרד לבן השמאלי של הקודקוד הנוכחי ,אם התו נדרשת לרדת לבן ַ הבא הוא bרד לבן הימני של הקודקוד הנוכחי .בכל שלב בו שאינו קיים – צור אותו .אם המחרוזת הסתימה זה עתה – סמן את הקודקוד בו היא הסתימה. העץ שיווצר עבור הדוגמה לעיל: + - + - + + + בהינתן מחרוזת ,הסיקו כיצד תשתמשו בעץ )בלבד( על-מנת להשיב על שאילתת שייכות )לא להגשה(. ִ .Iכתבו את ההגדרות הדרושות למימוש העץ הנ"ל )כלומר אילו מערכים-struct ,ים או מבני נתונים אחרים יידרשו למימוש עץ כנ"ל(. 160 .IIכתבו את הפונקציה addהמוסיפה מחרוזת לעץ נתון .הפונקציה מקבלת כקלט מצביע לשורש העץ ,ואת המחרוזת המבוקשת) .הניחו כי העץ מכיל לכל הפחות שורש .אתם רשאים להוסיף ַלפונקציה פרמטרים נוספים על-פי שיקול דעתכם(. .IIIכתבו את הפונקציה findהמחפשת מחרוזת בעץ נתון )כנ"ל( .הפונקציה מקבלת כקלט מצביע לשורש העץ ,ואת המחרוזת המבוקשת ,ומחזירה ערך אמת ) (true/falseמתאים) .אתם רשאים להוסיף לפונקציה פרמטרים נוספים על-פי שיקול דעתכם(. .IVנסמן ב n :את מספר המחרוזות באוסף ,וב m :את אורך המחרוזת הארוכה ביותר האפשרית )באוסף ,או בשאילתת השייכות( .הניחו כי פעולת השוואה של זוג תווים )זה עם זה( אורכת יחידת זמן אחת )וכל שאר הפעולות זניחות(. .1חשבו והסבירו את זמן הריצה של שאילתת שייכות על העץ. .2כנ"ל לגבי חיפוש במערך דו ממדי ממוין הגדול דיו להחזיק את כל המחרוזות באוסף. ה .בחרו את אחת משתי הפונקציות שכתבתם – וכתבו אותה שוב :אם בחרתם במימוש איטרטיבי ,השתמשו כעת ברקורסיה ,ולהפך. הערה :בכל המקרים ניתן להניח קלט תקין. 13.14.2תרגיל שני :מציאת קוטר של עץ בתכנית מוגדר: struct Node { ; int _data ; struct Node *_left, * _right } נגדיר קוטר של עץ בינארי כמספר הקשתות על המסלול הארוך ביותר בין שני צמתים בעץ. הקוטר של העץ הריק יוגדר להיות ,1-וקוטרו של עץ הכולל צומת יחיד יוגדר להיות .0 דוגמה :קוטרם של שני העצים הבאים הוא :5 רמז :שימו לב כי בכל עץ קיים צומת )הצבוע בדוגמות שלנו באפור( כך ששני הצמתים המצויים בשני קצותיו של הקוטר )שני הצמתים הצבועים בשחור( הינם שניהם צאצאיו של אותו צומת. כתבו פונקציה המקבלת מצביע לעץ בינארי )מטיפוס * (struct Nodeומחזירה את קוטרו )יש לכתוב כל פונקצית עזר בה הפונקציה שלכם משתמשת(. 161 13.14.3תרגיל שלישי :מה עושה התכנית? נגדיר struct Node { ;int _number ;struct Node *_left, * _right } נתונה הפונקציה הבאה: )void secret (struct Node *t, float &x, int &n { ;float x1, x2 ;int n1, n2 )if (t == NULL { ;x= 0 ;n= 0 } else { ;)secret (t->_left, x1, n1 ;)secret (t-> _right, x2, n2 ;n= n1 +n2 +1 ;x= (t->number + x1*n1 + x2*n2) / n } } .Iאם מריצים את הפונקציה הזו על עץ בינרי ,מהם הערכים שיוחזרו בפרמטרים xו?n- ;)secret (t-> _right, x2, n2 .IIאם במקום השורה ;x2 = 0; n2 = 0 יופיעו שתי הפקודות: מהם הערכים שיוחזרו אז? 13.14.4תרגיל רביעי :בדיקה האם עץ א' משוכן בעץ ב' נגדיר: struct Node { ;int _data ; struct Node *_left, *_right } נאמר שעץ בינארי T1משוכן בעץ בינארי שני T2אם ב T2 -קיים צומת N המהווה שורש של תת עץ )של (T2שהינו זהה לחלוטין ל.T1 - לדוגמא העץ: 2 162 1 3 משוכן בימני מבין שלושת העצים הבאים ,אך לא בשנים האחרים: )הערה :מצביעים שערכם NULLהושמטו מכל ארבעת הציורים( 5 5 2 7 3 2 7 1 5 7 1 4 2 3 1 4 כתבו את הפונקציה: )bool nested(struct Node *small, struct Node *large הפונקציה תחזיר את הערך אמת אם העץ עליו מצביע הפרמטר smallמשוכן בעץ עליו מצביע הפרמטר ,largeושקר אחרת. 163 13.14.5תרגיל חמישי :תת-עץ מרבי בו השורש גדול מצאצאיו כתבו פונקציה המקבלת מצביע rootלשורשו של עץ בינארי ,ומחזירה מצביע root_largerלשורש תת-העץ הגדול ביותר )כלומר לתת-העץ שכולל את מספר הגדול ביותר של צמתים( ,המקיים שעבור כל צומת בתת-העץ עליו מצביע ,root_larger הערכים המצויים בצאצאיו של הצומת קטנים או שווים מהערך המצוי ַבצומת עצמו. במידה וקיימים כמה תתי-עצים כנ"ל יש להחזיר מצביע לאחד מהם. לדוגמה ,עבור העץ הבא: 4 5 7 0 6 3 2 4 יוחזר מצביע לתת-העץ ששורשו הוא הצומת של שבע. 164 13.14.6תרגיל שישי :בדיקה האם תת-עץ שמאלי מחלק כל שורש ,וכל שורש מחלק תת-עץ ימני כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי ומחזירה את הערך trueאם ורק אם כל צומת nבעץ מקיים שהערכים בכל הצמתים בתת-העץ עליו מצביע בנו השמאלי של nמחלקים את הערך שבצומת ,nבעוד הערכים בכל הצמתים בתת-העץ עליו מצביע בנו הימני של nמחולקים על-ידי הערך שבצומת .n לדוגמה ,על העץ הבא יוחזר הערך :true 16 8 64 64 32 4 8 4 13.14.7תרגיל שביעי :בדיקה האם עץ סימטרי הגדרה :עץ בינארי יקרא סימטרי אם בנו השמאלי הוא תמונת ראי של בנו הימני. כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי ,ומחזירה את הערך trueאם ורק אם העץ סימטרי. לדוגמה ,העץ הבא סימטרי: 16 8 8 4 7 7 5 4 5 9 9 165 13.14.8תרגיל שמיני :איתור הערך המזערי בעץ כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי שאינו ריק ,ומחזירה את הערך המזערי בעץ. 13.14.9תרגיל תשיעי :עץ ביטוי )עבור ביטוי אריתמטי( בתרגיל זה נעסוק בביטויים אריתמטיים כדוגמת. 5, (5+3), (2*(5+3)) : הגדרה :ביטוי אריתמטי מורכב מ: .Iמספר טבעי .או .IIסוגריים המכילים בתוכם (1) :ביטוי אריתמטי (2) ,אופרטור )שעשוי להיות אחד מהבאים (3) ,(% ,/ ,* ,- ,+ :ביטוי אריתמטי. ראשית ,בדקו לעצמכם ששלושת הביטויים הנ"ל עונים על ההגדרה. התכנית שנכתוב תבצע את הפעולות הבאות: .Iהתכנית תקרא מהקלט ביטוי אריתמטי ותבנה ממנו עץ ביטוי על-פי אלגוריתם שיתואר בהמשך. .IIהתכנית תעריך את הביטוי תוך שימוש בעץ הביטוי שנבנה. התכנית תשתמש בפונקציה שנהוג לקרוא בשם ) tokenizerואשר תיכתב ,כמובן ,על- ידכם( .באחריותה של פונקצית ה tokenizer -יהיה לקרוא את הקלט ,ולהחזיר ,בכל פעם שהיא נקראת ,את האסימון ) (tokenהבא בקלט .אסימון הוא מרכיב אטומי כלשהו בקלט .בדוגמה שלנו האסימונים האפשריים הינם :סוגר שמאלי ,סוגר ימני, אחד מחמשת האופרטורים ,מספר טבעי .כדי להחזיר את האסימון הדרוש נשתמש במבנה שיוגדר באופן הבא לערך: { struct op_op bool oprnd_oprtr ; // which of the other 2 fields is ; char oprnd // in use ; unsigned int oprtr ; } אלגוריתם לבניית עץ ביטוי: קרא שוב ושוב ל tokenizer -עד קריאת הביטוי השלם .על-פי הערך המוחזר על-ידי ה tokenizer -בצע: .Iאם אותר מספר אזי החזר מצביע לשורש עץ הכולל את המספר שנקרא בלבד. .IIאם הוחזר סוגר פותח )סוגר שמאלי( אזי: .1קרא רקורסיבית לאלגוריתם ,והכנס את הערך המוחזר למצביע שיהיה בן שמאלי של הצומת שמצביע אליו תחזיר. .2קרא ל tokenizer -שיחזיר את האופרטור שישכון בשורש העץ שמצביע אליו תחזיר. .3קרא רקורסיבית לאלגוריתם ,והכנס את הערך המוחזר למצביע שיהיה בן ימני של הצומת שמצביע אליו תחזיר. .4קרא ל tokenizer -שיחזיר את הסוגר הימני הסוגר את הסוגר השמאלי שנקרא בתחילת סעיף ב'. .5החזר מצביע לעץ שמבנהו תואר בסעיפים 1עד .3 166 לדוגמה :עבור הביטוי ))(2*(5+3יבנה עץ הבא: * + 3 2 5 בהינתן עץ ביטוי אתם מוזמנים לחשוב בעצמכם כיצד ניתן להעריכו ,כדי לקבל את ערכו של הביטוי .בדוגמה שלנו ערכו של הביטוי הוא . (2*(5+3))=16 13.14.10תרגיל עשירי :חישוב מספר עצי החיפוש בני nצמתים כתבו פונקציה המקבלת מספר טבעי ,nומחזירה את מספר עצי החיפוש הבינאריים השונים בני nצמתים ,עם nערכים שונים .1..n דוגמה: .Iעבור n=0יוחזר הערך אחד ,שכן יש רק עץ חיפוש בינארי ריק יחיד. .IIעבור n=1יוחזר הערך אחד ,שכן יש רק עץ חיפוש בינארי יחיד המכיל צומת אחד .IIIעבור n=2יוחזר הערך שתיים ,שכן קיימים שני עצי חיפוש המכילים שני ערכים שונים: .IVעבור n=3יוחזר הערך חמש. רמז: .Iכל אחד מ n -הערכים עשוי להיות בשורש העץ ,וכל שורש כזה מגדיר ,כמובן ,עץ שונה. .IIבעקבות ההחלטה כי בשורש ישכון הערך ,xנקבע גם גודלו של הבן השמאלי, והבן הימני של השורש. 13.14.11תרגיל מספר אחד-עשר :איתור הצומת העמוק ביותר בעץ כתבו פונקציה המקבלת מצביע לעץ בינארי ,ומחזירה מצביע לצומת העמוק ביותר בעץ ,וכן את עומקו של צומת זה. 167 13.14.12תרגיל מספר שניים-עשר :בדיקה האם עץ כמעט מלא עץ בינארי נקרא כמעט מלא אם כל עליו נמצאים ברמה dאו ברמה ,d-1ואם לכל צומת המקיים שיש לו צאצא ימני ברמה ,iיש גם צאצא שמאלי ברמה זאת. לדוגמה :השמאלי במין שני העצים הבאים הוא עץ כמעט מלא ,בעוד הימני אינו. * הסיבה לכך שהימני אינו כמעט מלא היא שלצומת המסומן בכוכב יש צאצא ימני בעומק ארבע ,אך אין לו בן שמאלי בעומק זה. כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי ,ומחזירה את הערך trueאם ורק אם העץ כמעט מלא. 13.14.13תרגיל מספר שלושה-העשר :האם כל תת-עץ מכיל מספר זוגי של צמתים כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי .על הפונקציה להחזיר את הערך trueאם ורק אם כל תת-עץ מכיל מספר זוגי של צמתים ,וזאת מבלי לספור את מספר הצמתים שבכל תת-עץ. 13.14.14תרגיל מספר ארבעה-עשר :שמירת עומקם של עלי העץ במערך כתבו פונקציה המקבלת מצביע לשורשו של עץ בינארי ,וכן מערך חד-ממדי של משתנים בולאניים ,עליו ידוע כי תאיו אותחלו לערך .false בתום ביצוע הפונקציה יכיל התא מספר iבמערך את הערך trueאם ורק אם בעץ קיים עלה בעומק ) iעבור .(i = 0…N 168 13.14.15תרגיל מספר חמישה-עשר :חיוט של עץ עץ בינארי נקרא מחויט אם כל צומת בו מכיל גם מצביע לצומת שמצוי באותה רמה כמו הצומת הנוכחי ,והינו השכן הימני של הצומת הנוכחי בעץ. דוגמה לעץ מחויט: החצים המקווקווים הם שמדגימים את החיוט. כדי להחזיק עת מחויט נזדקק למבנה מעט שונה מזה המשמש אותנו בדרך כלל ,על- כן נגדיר: { struct T_node ; int _data ; T_node *_left, * _right, *_brother ; } כתבו פונקציה המקבלת מצביע מטיפוס * t_nodeלשורשו של עץ בינארי שנבנה left,בכל צומת מחזיקים ערכים אך טרם חוייט )המצביעים __right תקינים ,אך ערכו של המצביע brotherבכל צומת הוא .(NULLעל הפונקציה לחייט את העץ. רמזים לפתרון: א .הגדירו מבנה: { struct List_of_p_2_t_node ; struct T_node *the_p ; struct List_of_p_2_t_node *_next ; } .IIIבקרו בעץ רמה אחר רמה .בכל רמה בקרו כרגיל משמאל לימין .עת אתם מבקרים ַבצמתים מרמה lכלשהי: מטיפוס מאיברים המורכבת משורשרת רשימה ְבנו .1 .List_of_p_2_t_nodeכל תא ַברשימה יחזיק בראש וראשונה מצביע לאיבר כלשהו ברמה lבעץ ,וכן מצביע לאיבר הבא ברשימה המשורשרת. יתוַסף בסופה. את הרשימה המשורשרת בנו ְכתור ,כלומר כל איבר חדש ְ השתמשו ברשימה המשורשרת כדי לחייט את העץ. .2 169 עודכן 5/2010 15מצביעים לפונקציות בפרקים הקודמים ראינו כי מצביעים עשויים להורות על נתונים השמורים במערכים ,רשימות או עצים .אמרנו כי למעשה עת משתנה מטיפוס מצביע מורה על משתנה כלשהו הוא )המצביע( מכיל את כתובתו של המשתנה עליו המצביע מורה; במילים אחרות ,עת אנו מציירים חץ הנשלח מהמצביע לאובייקט עליו המצביע מורה ,אנו מדגימים בכך בצורה מוחשית ,וקלה יותר להבנה ,כי המצביע מכיל את הכתובת המתאימה .בפרק הראשון אמרנו כי אחד ממאפייני מחשבי פון-נויימן, בהם אנו עושים שימוש ,הוא שזיכרון יחיד מכיל הן תכניות ,והן נתונים עליהם פועלות התכניות .הוספנו כי לכל תא בזיכרון יש כתובת; משמע הן לתא המכיל נתונים יש כתובת ,והן לתא המכיל קטע תכנית .אם זה המצב ,אזי כפי שמצביע עשוי להצביע על נתון ,הוא יכול להצביע על קטע תכנית .בפרק זה נכיר את ההשתמעויות של אפשרות זאת. 15.1דוגמה פשוטה ראשונה למצביע לפונקציה נניח שברצוננו לכתוב תכנית אשר) :א( קוראת מהמשתמש האם ברצונו לקבל את המינימום או המקסימום בין זוגות המספרים שהוא יזין בהמשך) ,ב( קוראת מהמשתמש סדרת מספרים ,ועבור כל זוג מציגה את המינימום או המקסימום בין שני המספרים שהוזנו ,על-פי הבקשה שהוזנה קודם לכן .אנו יכולים לכתוב את קטע התכנית באופן הבא: int min_max, // 0 => min, 1 => max ; num1, num2 ; cin >> min_max ; cin >> num1 >> num2 { )while (num1 != 0 || num2 != 0 )if (min_max == 0 ; )cout << min(num1, num2 ; )else cout << max(num1, num2 ; cin >> num1 >> num2 } הגדרת הפונקציות min, maxפשוטה ביותר: { )int min(int num1, int num2 ; ) return( num1 < num2 ? num1 : num2 } { )int max(int num1, int num2 ; ) return( num1 > num2 ? num1 : num2 } בפרק זה נרצה להציג דרך חלופית ,אלגנטית יותר ,לכתיבת קטע הקוד .הדרך החלופית תסתמך על שימוש במצביע לפונקציה. בשפת Cאנו רשאים להגדיר משתנה: ; )int (*func_ptr)(int, int 170 נסביר :הגדרנו משתנה בשם ) func_ptrוזהו כמובן שם שאנו בחרנו( .המשתנה הינו מצביע )ולכן מופיעה כוכבית לפני שמו( אשר מסוגל להורות על פונקציה המקבלת שני ערכים שלמים )ולכן בסוגריים המופיעים אחרי שם המצביע כתבנו ,(int, intומחזירה ערך שלם )ולכן לפני שם המשתנה כתבנו .(intכלומר כפי שמצביע 'רגיל' לא מצביע על כל טיפוס נתונים שהוא ,אלא עלינו להגדירו כמצביע ל- intאו מצביע ל ,struct Node -ולא ניתן לערבב בין סוגי מצביעים שונים )מצביע ל int -לא יוכל להצביע על ,(struct Nodeכך גם מצביע לפונ' לא יוכל להצביע על כל פונ' שהיא ,אלא בהגדרתו עלינו לקבוע על אילו סוגים של פונ' המצביע ידע להצביע :על פונ' שבהכרח מקבלות א' ,ב' ,ג' ,ומחזירות ד'. באופן דומה כדי להגדיר מצביע בשם fpשידע להורות על פונקציות אשר מקבלות מצביע ל ,float -ופרמטר הפניה מטיפוס תו ,ומחזירות ערך בולאני נכתוב: ; )& bool (*fp)(float *, char נניח ,אם כן ,כי הגדרנו משתנה func_ptrכנ"ל ,וכן משתנים שלמים . num2, min_maxקטע הקוד המשופר שנציג הינו: num1, ; cin >> min >> max )if (min_max == 0 ; func_ptr = min else ; func_ptr = max ; cin >> num1 >> num2 { )while (num1 != 0 || num2 != 0 ; )cout << func_ptr(num1, num2 ; cin >> num1 >> num2 } נסביר :ההשמה ; func_ptr = minמכניסה ַלמצביע func_ptrאת כתובתה של הפונקציה ) minכפי שהופיעה קודם( .במילים אחרות ,ההשמה הנ"ל גורמת למצביע להצביע על הפונקציה .minאחרי שהפננו את המצביע להורות על הפונקציה הפקודה: את לכתוב הלולאה, בגוף יכולים, אנו ;) cout << func_ptr(num1, num2ופקודה זאת מציגה את הערך שמחזירה הפונקציה minעת היא מזומנת עם . num1, num2כלומר func_ptrהפך, בעקבות ביצוע ההשמה ,לשם נרדף לפונקציה ,minובכל מקום בו מופיע שם המשתנה func_prtמבין המחשב כי אנו מזמנים את הפונקציה .min אעיר כי במקום לכתוב func_ptr = min ; :ניתן ,ויש האומרים שגם עדיף, לכתוב func_ptr = &min ; :כדי להדגיש את השימוש במצביעים .הן אם ביצענו את ההשמה באופן הראשון ,ובין אם ביצענו אותה באופן השני ,את זימון הפונ' עליה מצביע func_ptrנוכל עשות באופןfunc_ptr(num1, num2) ; : )כפי שהופיע בדוגמה מעל( או באופן . (*func_ptr)(num1, num2) ; :כמובן שכדאי להיות עקביים ואם השתמשנו ב & -להשתמש גם ב ,* -ולהיפך ,אך מבחינת הקומפיילר זה לא הכרחי. 171 15.2דוגמה שניה למצביע לפונקציה הדוגמה הקודמת שראינו נועדה לשם הכרות ראשונית עם המצביעים לפונקציות. ככזאת ,הייתה זאת דוגמה פשוטה ומעט מאולצת של שימוש במצביעים לפונקציות .עתה נרצה להציג דוגמה טובה יותר לחשיבותו של הכלי. נניח כי בתכנית כלשהי אנו מטפלים בנתוני תלמידים .לשם כך אנו מגדירים את המבנה: { struct Stud ;]char _last[NAME_LEN], _first[NAME_LEN ; bool _gender ; unsigned int _shoe_num ; ]unsigned int _grades[MAX_GRADE ; } כפי שניתן לראות מהגדרת ה ,struct -עבור כל תלמיד ברצוננו לשמור את שמו הפרטי ) ,(_firstשם משפחתו ) ,(_lastמינו ) ,(_genderמספר נעליו ) ,(_shoe_numומערך של ציונים ).(_grades אחר ,בתכנית הראשית ,נגדיר מערך של נתוני תלמידים: ַ ; ]stud my_class[CLASS_SIZE נניח כי התכנית קראה נתונים לתוך המערך ,ועתה המשתמש יכול לבחור האם ברצונו למיין את המערך: א .על-פי שם המשפחה והשם הפרטי. ב .על-פי מספר הנעליים. ג .על-פי ממוצע הציונים. לשדות על-פיהם אנו ממיינים את הרשומות נקרא מפתח המיון. כדי לציין מהו מפתח המיון הרצוי נגדיר טיפוס: enum sort_by_t {LAST_FIRST_SRT, SHOE_NUM_SRT, ; }AVG_GRADE_SRT נניח כי תכניתנו כוללת פונקציה read_sort_keyאשר מחזירה אחד משלושת הערכים הנ"ל ,על-פי בקשתו של המשתמש. עתה ,כדי למיין את המערך )מקטן לגדול( עלינו להשוות בין הרשומות בו על-פי השדות הרצויים )כלומר על-פי מפתח המיון( .לדוגמה ,אם יש למיין על-פי שם משפחה ושם פרטי אזי המבנה של cohen yosiצריך להופיע במערך לפני המבנה של ;levi danaאולם אם יש למיין את התלמידים על-פי מספר הנעליים אזי יוסי כֹהן שמספר נעליו 49צריך להופיע אחרי דנה לוי שמספר נעליה הוא ;19ואם יש למיין את הנתונים על-פי ממוצע הציונים אזי מי משניהם שממוצע ציוניו גבוה יותר צריך להופיע אחרי מי שממוצע ציוניו נמוך יותר. 15.2.1אפשרות א' לכתיבת פונקצית המיון :שימוש בדגל בלבד לשם הפשטות נניח כי ברצוננו למיין את המערך תוך שימוש במיון בועות )אם כי העקרונות שנציג ישימים גם ַלמיונים היעילים יותר( .האפשרות הראשונה ,והפחות מוצלחת ,לכתיבת פונקצית המיון תסתמך על הרעיון הבא :פונקצית המיון תקבל ְפרט למערך התלמידים פרמטר נוסף ,באמצעותו נאותת לפונקציה על-פי אילו שדות 172 פי מפתח- הפונקציה תשווה בין כל זוג רשומות במערך על.עליה למיין את המערך . ותחליף ביניהן אם הן מפרות את סדר המיון,המיון :הקריאה לפונקצית המיון מהתכנית הראשית תעשה בהקשר כדוגמת הבא sort_by_t key = read_sort_key() ; bubble_sort(my_class, key) ; מזמנים את הפונקציה אשר קוראת ְ ַבפקודה הראשונה מבין השתיים אנו ,פי אילו שדות ברצונו למיין את רשומות המערך; בפקודה השניה-מהמשתמש על , אנו מעבירים ַלפונקציה את המערך שעליה למיין.אנו מזמנים את פונקצית המיון : פונקצית המיון תראה.ואת מציין מפתח המיון הרצוי void bubble_sort( stud my_class[], sort_by_t key) { for (int round = 0; round < CLASS_SIZE -1; round++) for (int i =0; i < CLASS_SIZE – round –1; i++) { bool need_to_swap = false ; // should class[i], class[i+1] // be swapped switch (key) { // check if class[i], class[i+1] // violate the correct order // according to the sorting key case LAST_FIST: if ( ( strcmp(my_class[i]._last, my_class[i+1]._last)> 0) || ( strcmp(my_class[i]._last, my_class[i+1]._last)==0 && strcmp(my_class[i]._first, my_class[i+1]._first)> 0)) need_to_swap = true ; break ; case SHOE_NUM: need_to_swap = my_class[i]._shoe_num > my_class[i+1]._shoe_num ; break ; case AVG_GRADE: need_to_swap = avg(my_class[i]._grades) > avg(my_class[i+1].grades) ; break ; } if (need_to_swap) swap(my_class[i], my_class[i+1]) ; } } unsigned int avg(const unsigned int grades[MAX_GRADES]) { int sum = 0 ; for (int i=0; i < MAX_GRADES; i++) sum += grades[i] ; return( sum / MAX_GRADES ) ; } void swap( stud &s1, stud &s2) { stud temp = s1 ; s1 = s2 ; s2 = temp ; } 173 הסבר הפונקציה :bubbble_sortכדרכו של מיון בועות ,הפונקציה כוללת לולאה כפולה .בכל סיבוב בלולאה הפנימית מחליפים או לא מחליפים בין שתי רשומות סמוכות .קטע הקוד הכולל את פקודת ה ,switch -והמהווה את עיקר הפונקציה, בודק האם על-פי מפתח המיון יש צורך להחליף בין שתי הרשומות הסמוכות, ] my_class[iו .my_class[i+1] -ההחלטה האם יש להחליף בין שתי הרשומות או לא נשמרת ַבמשתנה הבולאני .need_to_swapבמידה וערכו של המשתנה הוא trueאזי אנו מחליפים בין שתי הרשומות. הסתייגותנו מקוד זה היא שאם בעתיד נרצה למיין את הרשומות על-פי מפתח מיון אחר )למשל נרצה למיין את הרשומות בסדר יורד של השמות ]מגדול לקטן[ ,או נרצה למיינן לפי שדה המין ,דווקא( אזי יהיה עלינו לשוב לפונ' המיון ולעדכנה .מצב זה אינו רצוי :היינו מעדיפים לכתוב את פונ' המיון פעם אחת ולתמיד ,ולא להצטרך לשנותה גם עת נוצר הצורך למיין את המערך בסידור שונה כלשהו. 15.2.2אפשרות ב' לכתיבת פונקצית המיון :שימוש בפונקציות השוואה ובדגל עתה נציג אפשרות אלגנטית יותר לכתיבת פונקצית המיון .עבור אפשרות זאת נגדיר פונקציַת השוואה תשווה בין זוג רשומות ְ שלוש פונקציות השוואה בין רשומות .כל על-פי מפתח מיון מסוים .פונקציות ההשוואה תכתבנה על-פי אותו עיקרון כמו ,strlenכלומר אם ערכו של הפרמטר הראשון קטן )על-פי מפתח המיון( מערכו של הפרמטר השני אזי יוחזר ערך שלילי; אם ,לעומת זאת ,שתי הרשומות שוות )מבחינת ערכו של מפתח המיון( אזי יוחזר ערך אפס; ולבסוף ,אם הרשומה השניה גדולה מהראשונה אזי יוחזר ערך חיובי .נציג ,ראשית ,את פונקציות ההשוואה: int comp_by_last_first(const struct Stud &s1, { )const sturct Stud &s2 ; )int temp = strcmp(s1._last, s2._last ; if (temp != 0) return temp ; )return strcmp(s1._first, s2._first } //-----------------------------------------------int comp_by_shoe_num(const struct Stud &s1, { )const sturct Stud &s2 )if (s1._shoe_num < s2._shoe_num ; return –1 )if (s1._shoe_num == s2._shoe_num ; return 0 )if (s1._shoe_num > s2._shoe_num ; return 1 } //-----------------------------------------------int comp_by_avg_grade(const struct Stud &s1, { )const sturct Stud &s2 unsigned int avg1 = avg(s1._grades), ; )avg2 = avg(s2._grades ; is (avg1 > avg2) return –1 174 ; if (avg1 == avg2) return 0 ; if (avg1 < avg2) return 1 } הסבר :נסביר לדוגמה את ,comp_by_last_firstשתי הפונקציות האחרות דומות לה למדי .ראשית ,אנו משווים בין שני שמות המשפחה ,כפי שהם שמורים ב- . s1._last, s2._lastאת תוצאת ההשוואה אנו שומרים במשתנה העזר .tempבמידה והשם הראשון קטן מהשני )כלומר strcmpמחזירה ערך שלילי( ,או שהשם השני גדול מהראשון )ואז ערכו של tempחיובי( אזי כבר שם המשפחה קובע את יחס הסדר בין שתי הרשומות ,וניתן להחזיר את ערכו של .tempאם ,לעומת זאת ,ערכו של tempהוא אפס ,סימן ששני שמות המשפחה זהים ,ועל כן יש להחזיר את תוצאת ההשוואה בין שני השמות הפרטיים של זוג התלמידים; וזאת אנו עושים. הערה נוספת :מדוע הגדרנו את הפרמטרים לפונקציות ההשוואה באופןconst : . struct Stud &s1מדוע לא העברנו את המבנים כפרמטרי ערך? מדוע נזקקנו לשימוש במילת המפתח ?constהסיבה לכך היא שמבנה עשוי להיות גדול )למשל אם מערך הציונים בו גדול( ,במקרה כזה העברתו כפרמטר ערך תחייב השקעה של זמן ,והקצאה של זיכרון ,שכן על המחשב יהיה להעתיק את ערכו של הארגומנט על שטח הזיכרון המוקצה לפרמטר .כדי לחסוך בזמן ובזיכרון נוהגים להעביר מבנים כפרמטרים משתנים ,ואז נשלח רק 'חץ' מהפרמטר ַלארגומנט המתאים ,ואין צורך בהשקעת זמן וזיכרון מרובים .עתה משהפכנו את הפרמטר לפרמטר משתנה יש תפגע בנתונים המועברים חשש שהפונקציה ,אם תיכתב על-ידי מתכנת לא אחראיְ , לה; כדי למנוע זאת הוספנו לפני טיפוס הפרמטר את הקביעה שהפרמטר הינו קבוע, ולא ניתן לשנותו. את פונקצית המיון נוכל לכתוב תוך שימוש בפונקציות ההשוואה באופן יותר נקי: { )void bubble_sort(struct Stud my_class[], sort_by_t key )for (int round = 0; round < CLASS_SIZE -1; round++ )for (int i =0; i < CLASS_SIZE – round –1; i++ { ]bool need_to_swap = false ; // should class[i], class[i+1 // be swapped ]switch (key) { // check if class[i], class[i+1 // violate the correct order // according to the sorting key case LAST_FIST_SRT: if ( comp_by_last_first(my_class[i], )my_class[i+1]) > 0 ; need_to_swap = true ; break case SHOE_NUM_SRT: )if ( comp_by_shoe_num(my_class[i], my_class[i+1]) > 0 ; need_to_swap = true ; break case AVG_GRADE_SRT: )if ( comp_by_avg_grade(my_class[i], my_class[i+1])> 0 ; need_to_swap = true ; break } )if (need_to_swap ; )]swap(my_class[i], my_class[i+1 } 175 } פונקציַת המיון הנוכחית שונה מקודמתה בפקודת הַ .switch -בפונקציה ְ הסבר: הנוכחית ,על-פי ערכו של הדגל ,keyאנו מזמנים את פונקצית ההשוואה הדרושה, ומעדכנים את ערכו של המשתנה need_to_swapעל-פי הערך שמחזירה פונקצית ההשוואה .באופן זה פונקצית המיון הפכה מסודרת יותר ,במילים אחרות רגולרית יותר .התוצאה היא שבמידה ובעתיד נרצה למיין את התלמידים על-פי מפתח מיון שונה קל יהיה לנו יותר לעדכן את פונקצית המיון. 15.2.3אפשרות ג' לכתיבת פונקצית המיון :שימוש במצביע לפונקציה עתה נציג את האפשרות האלגנטית ביותר להשלמת המשימה .אפשרות זאת תשתמש במצביע לפונקציה. בתכנית הראשית נגדיר את המשתנה: int (*comp_func_ptr)(const struct Stud &, ; )& const struct Stud כלומר הגדרנו מצביע לפונקציה .המצביע ידע להצביע על פונקציות שמקבלות שני מבנים מטיפוס Studכפרמטרי הפניה קבועים ,ומחזירות ערך שלם .במילים פשוטות ,המצביע ידע להצביע על כל אחת משלוש פונקציות ההשוואה שכתבנו. פרט לכך תכיל התכנית אותן הגדרות כפי שהצגנו קודם לכן. התכנית הראשית תראה עתה לערך כך: ; )read_data(my_class ; )(sort_by key = read_sort_order { )switch (key case LAST_FIRST_SRT : ;comp_func_ptr = comp_by_last_first ; break case SHOE_NUM_SRT : ; comp_func_ptr = comp_by_shoe_num ; break case GRADES_SRT : ; comp_func_ptr = comp_by_avg_grade ; break } ; )bubble_sort(my_class, comp_func_ptr מה עשינו עד כה )בתכנית הראשית(? את בחירת פונקצית ההשוואה העברנו לחלוטין לתכנית הראשית .כפי שתיראו בהמשך ,פונקצית המיון פשוט תפעיל את פונקצית ההשוואה שמועברת לה ) ַבפרמטר השני( ,היא אינה מתעניינת כלל בפונקצית ההשוואה .מבחינת פונקצית המיון ,פונקצית ההשוואה הפכה להיות קופסה שחורה שרק מחזירה ערך רצוי ,ועל-פי הערך המוחזר פונקצית המיון מחליפה ,או לא מחליפה ,ביו רשומות .אם בעתיד נרצה למיין את הרשומות על-פי מפתח מיון שונה, לא נאלץ לשנות כלל את פונקצית המיון ,וזו תכונה רצויה ביותר של פונקצית מיון אשר אמורה להיות כללית ככל שניתן ,ולמיין כל מה שמעבירים לה. 176 נציג את פונקצית המיון: void bubble_sort( struct Stud my_class[], int (*comp_func_ptr)(const struct Stud &, { ))& const struct Stud )for (int round = 0; round < CLASS_SIZE -1; round++ )for (int i =0; i < CLASS_SIZE – round –1; i++ { )if (comp_func_ptr(my_class[i], my_class[i+1]) > 0 ; )]swap(my_class[i], my_class[i+1 } } הסבר :פונקצית המיון מקבלת שני פרמטרים :מערך של מבנים אותו עליה למיין, ומצביע לפונקציה )אשר מקבלת שני מבנים מסוג studכפרמטרים משתנים קבועים ,ומחזירה ערך שלם( .פונקצית המיון מתנהלת כמו כל מיון בועות :היא כוללת לולאה כפולה .בכל סיבוב בלולאה אנו משווים בין שני תאים סמוכים ַבמערך ובמידת הצורך מחליפים ביניהם .כיצד אנו יודעים האם יש להחליף בין שני התאים הסמוכים או לא? על-פי מה שמחזירה לנו פונקצית ההשוואה המועברת בפרמטר השני :אם היא מחזירה ערך חיובי ,סימן שעל פי מפתח המיון הרשומה ] my_class[iגדולה מהרשומה ] ,my_class[i+1ועל-כן יש להחליף בין הרשומות .באחריות מי שקורא לפונקצית המיון להעביר לה פונקצית השוואה מתאימה .פשוט ,נקי ואלגנטי ,האין זאת? בדוגמה האחרונה ראינו כי את המשתנה comp_func_ptrאנו מפנים להצביע על כגון: השמה בעזרת הרצויה הפונ' ; . comp_func_ptr = comp_by_last_firstאעיר כי ,למרות שהדבר עשוי באופן: גם ההשמה את לכתוב ניתן מפתיע, להיראות ; comp_func_ptr = &comp_by_last_firstכלומר הוספנו & לפני שם הפונ' .יש המעדיפים צורת כתיבה זאת המדגישה שאנו עוסקים כאן במצביעים. הפרוטוטיפ של )( bubble_sortנותר זהה בשני המקרים .בלי תלות באופן בו ביצענו את ההשמה הקודמת ,בגוף הפונ' )( bubble_sortעת אנו מזמנים את פונ' ההשוואה הדרושה ,אנו יכולים לעשות זאת באחת משתי צורות הכתיבה: )] comp_func_ptr(my_class[i], my_class[i+1כפי שמופיע בדוגמה מעל ,או (*comp_func_ptr)(my_class[i], my_class[i+1]) :כדי להדגיש שמדובר במצביע )לפונ'(. כאשר פונ' fמקבלת פרמטר pfשהינו מצביע לפונ' כלשהי ,ניתן בעת הזימון של f להעביר לה בארגומנט המתאים ערך ,NULLכלומר לא להעביר שום פונ' שהיא. כמובן שאז אל ל f -לזמן את הפונ' .pfייתכן כי במצב בו מועבר ל f -הערך NULL היא תזמן פונ' קבועה כלשהי )לא כזו שמועברת לה כפרמטר(. המונח callback functionמתאר פונ' המזומנת באמצעות מצביע אליה שמועבר לפונ' שניה; לפיכך הדוגמה האחרונה שכתבנו הינה .callback function 15.3הדפסת מערך תוך שימוש במצביע לפונ' אציג עתה דוגמה נוספת לשימוש במצביע לפונ' .הדוגמה ,כמו גם זו שמופיעה בסעיף הבא ,אינה מחדשת דבר מעבר למה שכבר ראינו .הדוגמות רק מציגות שוב את אותו נושא. נניח שבכמה מקומות בתכנית עלינו להדפיס מערך של מספרים שלמים .אולם במקומות השונים יש להדפיס את נתוני המערך באופן מעט שונה: א .במקום אחד יש להפרידם באמצעות פסיקים. 177 ב .במקום אחר יש להדפיסם כך שלפני כל נתון יופיע התו < אם הוא גדול מקודמו, יופיע התו = אם הוא שווה לקודמו ,ויופיע התו > אם הוא קטן מקודמו. ג .במקום שלישי בתכנית יש להדפיס לצד כל נתון גם את מספר התא במערך בו הוא מצוי. על כן נרצה פונ' הדפסה אחת ,אשר מפעילה בכל שלושת המקרים אותו מנגנון בקרה :ריצה על-פני המערך ,אך מזמנת בכל אחד משלושת המקרים פונ' אחרת להדפסת הערך המצוי בתא שיש להדפיס בכל סיבוב בלולאה. נכתוב את הפונ' באופן דומה לגרסה המתקדמת ביותר של פונ' המיון כפי שראינו בסעיף הקודם ,כלומר פונ' הדפסת המערך תקבל מצביע לפונ' אשר יודעת להדפיס תא בודד במערך. נכין לנו ,על כן ,שלוש פונ' בעלות אותו פרוטוטיפ ,שכל אחת מהן מדפיסה תא במערך על פי הדרישות שהצבנו קודם לכן: { )void print_cell_comma(const int arr[], int index ; " cout << arr[index] << ", } //---------------------------------------------------{ )void print_cell_sign(const int arr[], int index ; ' ' = char sign )if (index > 0 )]if (arr[index] > arr[index -1 ; '>' = char )]else if (arr[index] == arr[index -1 ; '=' = sign else '<' = sign ; " " << ]cout << sign << arr[index } //---------------------------------------------------{ )void print_cell_index(const int arr[], int index ; " cout << index << "=" << arr[index] << ", } שלוש הפונ' מקבלות את המערך ,ואת מספר התא הרצוי .כל אחת מהן מדפיסה את ערכו של התא המבוקש בפורמט שהגדרנו. עתה נכתוב את פונ' הדפסת המערך ,אשר עושה שימוש בפונ' הדפסת התא שהועברה לה: void print_arr(const int arr[], ))void (*print_cell_func)(const int*, int { )for (int i=0; i < N ; i++ ; )print_cell_func(arr, i } 178 נסביר :הפונ' print_arrמקבלת את המערך שעליה להדפיס )באמצעות הפרמטר: ][ ,(const int arrומצביע לפונ' הדפסת תא בודד במערך .הפרמטר השני של הפונ' print_arrהוא מצביע לפונ' .שמו של הפרמטר השני הוא .print_cell_funcהפרמטר מצביע על פונקציות שמקבלות מצביע קבוע למערך הביטוי: לנו מורה )וזאת שלם וערך ) ) ((const int*, intומחזירות כלום )וזאת מורה לנו המילה voidשלפני שם הפרמטר ,והכוכבית שלצדו (. הפונ' רצה סידרתית על תאי המערך ,ועבור כל תא מזמנת את הפונ' שהועברה לה כפרמטר )ושמורה בפרמטר (print_cell_funcומעבירה לאותה פונ' את המערך השלם ,ואת מספר התא הרצוי. בהנחה שבתכנית הוגדר מערך: ; ]int int_arr[N והוזנו לתוכו נתונים ,נוכל לזמן את פונ' ההדפסה בכל אחד משלושת האופנים הבאים: ; )print_arr(int_arr, print_cell_comma ; )print_arr(int_arr, print_cell_sign ; )print_arr(int_arr, print_cell_index בכל אחד משלושת הזימונים אנו מעבירים לפונ' את המערך ,ומצביע לפונ' שונה להדפסת תא במערך. 15.4מציאת הערך המזערי של פונ' בעזרת מצביע לפונ' נניח כי בתכנית הוגדרו מספר פונ' ממשיות של משתנה אחד )במובן המתמטי של המילה( ,כלומר פונ' המקבלות ערך ממשי xומחזירות את ערך ה y -הממשי המתאים לו .לדוגמה: { )double f1(int x ; return 3*x + 2 } //--------------------------------{ )double f2(int x ; return 3*x*x -5*x + 4 } //--------------------------------{ )double f3(int x ; )return 3*sin(x) – 2*cos(x } //--------------------------------- במקומות שונים בתכנית ברצונו למצוא את ערך המינימום של הפונקציות הנ"ל בקטעים שונים של הישר .לדוגמה :ברצוננו למצוא את המינימום של f1בקטע ] , [-3, 2או שברצוננו למצוא את המינימום של f3בקטע ] ,[0, 5וכן הלאה. כמו כן ,נרצה לפתור את הבעיה לא בשיטות אנליטיות ,ע"י גזירת כל אחת מהפונ' ובדיקת הנגזרת ,אלא בשיטות חישוביות :בהינתן פונ' fוקטע שבין x0ל ,x1 -נרצה 179 לחשב את ערכה של fב N -נקודות בקטע )עבור Nשהינו קבוע של התכנית(: הנקודות: x0, x0 +(x1-x0)/N, x0 +(x1-x0)/N*2, ... , x1 וכך לאתר את המינימום של הפונ'. נכתוב ,על כן ,פונ' )שתקרא (find_minהמקבלת מצביע לפונ' כדוגמת שלוש הפונ' שהגדרנו מעל )פרמטר זה יקרא ,(func_ptrושני ערכים ממשיים המציינים קטע של הישר ) .(x0, x1הפונ' find_minתזמן את N func_ptrפעמים ,עם N נקודות בקטע )כפי שתואר מעל( ,ותחזיר את הערך המזערי שהוחזר ע"י func_ptrבכל Nהקריאות .הקוד: double find_min( double (*func_ptr)(double), )double x0, double x1 { double min = func_ptr(x0), ; delta = (x1 – x0)/N )for (double x = x0 + delta; x <= x1; x+= delta )if (func_ptr(x) < min ; )min = func_ptr(x ; return min } עתה, כדי למצוא את המינימיום של f3 בקטע ]1 [0, נזמן: ).find_min(f3, 0, 1 למותר לציין שאם בעתיד נגדיר בתכנית פונ' ממשית נוספת במשתנה יחיד כלשהי, ונרצה לאתר גם את המינימום שלה בקטע כלשהו ,נוכל לעשות זאת בנקל בלי שנצטרך לשנות את find_minשכתבנו עתה. 15.5תרגילים 15.3.1תרגיל מספר אחד :האם פונקציה אחת גדולה יותר משניה בתחום כלשהו נניח שבתכנית מסוימת קיימות מספר פונקציות שכל אחת מהן מקבלת שני מספרים שלמים )נסמנם xו (y-ומחזירה מספר שלם )נסמנו .(zעליכם לכתוב פונקציה greaterהמקבלת שישה פרמטרים: א .זוג פונקציות כנ"ל )נסמנן f1ו.(f2- ב .זוג ערכי גבול x0ו x1-עבור .x ג .זוג ערכי גבול y0ו y1-עבור .y על הפונקציה greaterלקבוע האם עבור כל ערך xכך ש ,x0 ≤ x ≤ x1-ועבור כל ערך של yכך ש f1 y0 ≤ y ≤ y1-הינה 'גדולה יותר' מ f2-במובן ש f1-מחזירה ערך גדול יותר מהערך שמחזירה f2עבור זוג הערכים .השגרה greaterתבצע את משימתה באופן הבא N :פעמים תגריל greaterזוג ערכים xוַ y-בתחום הרצוי ,ותקרא ל f1-ול- f2עבור זוג הערכים .במידה ובכל Nהפעמים החזירה f1ערך גדול יותר מכפי שהחזירה ,f2תסיק greaterש f1-אכן גדולה מ f2-בתחום הנ"ל ותחזיר את הערך ,trueאם לפחות באחד המקרים לא החזירה f1ערך גדול יותר מזה שהוחזר על-ידי f2תחזיר greaterאת הערך .false 180 15.3.2תרגיל מספר שתיים :מציאת שורש של פונקציה נניח כי בתכנית כלשהי הוגדרו מספר פונקציות של משתנה ממשי יחיד )כלומר כאלה המקבלות ארגומנט ממשי יחיד ,ומחזירות ערך ממשי יחיד( .כתבו פונקציה המקבלת: א .מצביע fלפונקציה כנ"ל. ב .ערך ממשי x0המקיים כי ערכה של fשלילי בנקודה .x0 ג .ערך ממשי x1המקיים כי ערכה של fחיובי בנקודה .x1 על הפונקציה שתכתבו לאתר שורש של .f הפונקציה תפעל באופן הבא: א .במידה ו f(x0) -אינו שלילי יוחזר ערך קטן מ.x0 - ב .במידה ו f(x1) -אינו חיובי יוחזר ערך גדול מ.x1 - ג .חזור על תהליך חיפוש בינארי: .1קבע.mid = (x0+x1)/2 : .2אם ערכו המוחלט של ) f(midקטן מ EPSILON -אזי החזר את הערך .mid .3אם f(mid)>0אזי עדכן. x1 = mid : .4אחרת עדכן. x0 = mid : 181 עודכן 5/2010 .16מצביעים גנריים )* (void עת דנו במצביעים עד כה הדגשנו שמצביע הינו משתנה המכיל כתובת של משתנה אחר .אולם אם נדייק אזי המצביע מכיל לא רק כתובת אלא גם את טיפוס הנתונים עליהם הוא מצביע ,זו הסיבה שאנו מבחינים בין * intל ,double * -לדוגמה; וזו גם הסיבה שעת אנו כותבים cout << *ip :יודע המחשב כמה בתים בזיכרון עליו להציג, וכיצד יש לפרש את הבתים הללו )כמספר שלם? כמספר ממשי? כמחרוזת?( .מאותה סיבה ,עת אנו כותבים ip++יודע המחשב כיצד לקדם את המצביע כך שהוא יצביע על האיבר הבא במערך ,בין אם מדובר במערך של תווים )בו כל תא צורך בית יחיד(, ובין אם מדובר במערך של מבנים )בו כל תא עשוי להיות גדול(. בפרק זה נדון במצביעים גנריים ) (generic pointersכלומר כלליים ,כאלה שמסוגלים להצביע לכל טיפוס של נתונים מצד אחד ,אולם טיפוס הנתונים המוצבע על-ידם אינו ידוע להם מצד שני .במלים אחרות ,המצביע הגנרי כולל רק כתובת של תא בזיכרון ,ולא כולל את טיפוס הנתונים השמור בבית זה )ובבתים העוקבים לו, ומרכיבים יחד נתון בודד מטיפוס כלשהו( .על כן את טיפוס הנתונים יהיה עלינו לשמור ,כך או אחרת בנפרד. המצביעים הגנריים נותנים לנו אפשרות לטפל בטיפוסי נתונים רב-צורניים ) ,(polymorphic data typeכלומר לכתוב קוד שיטפל במגוון של טיפוסי נתונים .אולם, וזו נקודה שמאוד חשוב להפנים ,כדי לטפל בטיפוסי הנתונים השונים יהיה עלינו ראשית להמיר את המצביע הגנרי )חזרה( לכדי מצביע מטיפוס מסוים .אבהיר :נניח משתנה ,או פרמטר , void *p :אזי pיכול להצביע הן על משתנים מטיפוס ,intהן על משתנים מטיפוס ,doubleוהן על משתנים מכל טיפוס שהוא .אולם ניסיון לבצע פעולות כגון cout << *p; :או p++; :לא יתקמפל ,בדיוק מהסיבה שהמחשב אינו יודע איזה סוג נתון עליו להדפיס עת יש פניה ל ,*p :ובאיזה שיעור יש להסיט עת pעת מבצעים . p++ :לכן ,כך או אחרת ,כדי להשתמש ַבמצביע ניאלץ ראשית להמירו חזרה לטיפוס מסוים כלשהו ,ורק אז לעשות בו שימוש; כלומר לכתוב: ; cout << * (int *) pאו ; p= (int *)p + 1; :כלומר יהי עלינו לשמור 'בצד' גם מידע אודות טיפוס הנתונים עליהם מצביע pעתה. כדרכנו ,נתחיל מדוגמה פשוטה .נניח כי בתכניתנו הוגדרו המערכים: ; ]int iarr[N1 ; ]double darr[N2 ואולי גם מערכים נוספים שתאיהם מטיפוסים שונים. נניח כי ברצוננו לכתוב קטע קוד הקורא נתונים למערכים השונים .בכלים שעמדו לרשותנו עד-היום נאלצנו לכתוב פונ' נפרדת עבור כל אחד משני המערכים השונים; עתה נרצה ,בהדרגה ,לצמצם כפילות הזאת. 16.1דוגמה ראשונה :פונ' המשתמשת במצביע גנרי אציג תחילה אפשרות פשוטה יותר ,ומוצלחת פחות ,המשתמשת במצביע גנרי ,וע"י כך מאפשרת לנו לכתוב פונ' יחידה לקריאת נתוני שני המערכים .על-פי גישה זאת, נכתוב פונ' המקבלת מצביע גנרי void *arrאשר יוכל להורות על כל אחד משני המערכים .בזאת צעדנו צעד קדימה ,במובן זה שתהיה לנו פונ' אחת ,שתקבל את המערך בין אם מדובר במערך של שלמים ,ובין אם מדובר מערך של ממשיים )ובין 182 אם מדובר במערך של תאים מכל סוג שהוא( .פרמטר נוסף שיהיה מטיפוס char יורה לפונ' על איזה טיפוס למעשה מצביע הפרמטר ,arrכלומר לאיזה טיפוס יש לעשות castלפרמטר .arrולבסוף ,אם לא כל המערכים מכילים אותו מספר תאים, אזי פרמטר שלישי יחזיק את גודל המערך )כלומר כמה תאים הוא כולל(. //-----------------------------------------------------{ )void read_data(void *arr, char type, int arr_size )for (int i = 0; i < arr_size; i++ { )switch (type ; ]case 'i' : std::cin >> ((int *) arr)[i ; break ; )case 'd' : std::cin >> ((double *) arr) +i ; break default: ; "std::cerr << "Error: wrong type\n ; )exit(1 } } קוד הפונ' פשוט למדי :הפונ' מנהלת לולאה בה היא קוראת את הנתונים תא אחר תא ,בכל סיבוב בלולאה פקודת ה switch -בודקת מהו טיפוס המערך ,ועל-פי ערכו של הפרמטר typeעושה castלמצביע arrלטיפוס המתאים .אחרי שהמצביע הומר לטיפוס רצוי ,לדוגמה (int *) arr :ניתן להוסיף לו את iבתור ההסטה הרצויה ,כלומר התא המתאים במערך ,ומכיוון שעתה כבר מוגדר שהמערך הוא מערך של שלמים גם ברור מה צריך להיות שיעור ההסטה בבתים כדי להגיע לתא מספר .iלכן הפעולה ((int *) arr) +i :מחזירה לנו מצביע לתא מספר i במערך .לבסוף פניה ל *(((int *) arr) +i) :פונה לאותו תא ,ועל כןcin : ;) >> *(((int *) arr) +iקוראת נתונים לתא המתאים ְבמערך של שלמים .לחילופין ,ניתן לקרוא לתא רצוי במערך בעזרת הכתיבה הפשוטה יותר: ] ; ((int *) arr)[iעתה ,המרנו את ) arrרק לצורך ביצוע הפעולה הנוכחית, לא באופן גורף!( לכדי * ,intולכן אנו יכולים לפנות לתא מספר iבמערך עליו arrמצביע. זימון הפונ' מהתכנית הראשית יעשה באופן הבא: ; )read_data(iarr, 'i', N1 ; )read_data(darr, 'd', N2 מגבלתה של הפונ' שכתבנו היא שכדי להכלילה ,כך שהיא תהיה מסוגלת לקרוא נתונים גם לתוך מערך של תווים עלינו לחזור לפונ' ,ולשנות את הקוד שלה :להוסיף caseנוסף בפקודת ה ,switch -ולכך איננו ששים :היינו רוצים לכתוב אחת ולתמיד פונ' אשר תדע לקרוא נתונים לתוך כל מערך שהוא ,בלי שנצטרך לשנותה כל פעם שנרצה להכלילה לטיפוס נתונים נוסף .נרצה שהשינויים ,או הרחבות ,יבוצעו מחוץ לפונ' ,ע"י מי שקורא לפונ'. 16.2דוגמה שניה ,משופרת :פונ' המשתמשת במצביע גנרי עתה נציג גרסה שניה של פונ' המסוגלת לקרוא נתונים למערכים ממגוון טיפוסים. כמו בגרסה הראשונה ,גם בגרסה השניה הפונ' תקבל מצביע גנרי void *arrאשר יורה על המערך הרצוי .אולם בגרסה זאת ,בניגוד לקודמתה ,לא נעביר פרמטר שמציין את טיפוס המערך ,אלא נעביר מצביעים לזוג פונ': א .פונ' get_p2_wanted_cellהמקבלת את המצביע הגנרי ומספר של תא רצוי במערך ,ומחזירה מצביע לאותו תא. 183 ב .פונ' read_into_wanted_cellהמקבלת מצביע גנרי לתא רצוי במערך, וקוראת נתונים לאותו תא אחרי שהיא ממירה את המצביע הגנרי לטיפוס רצוי. באחריות מי שירצה לזמן את הפונ' read_dataיהיה לספק גם שתי פונ' פשוטות אלה .הפונ' read_dataתעשה בהן שימוש :בכל סיבוב בלולאה היא תזמן את הפונק' get_p2_wanted_cellתעביר לה את המצביע לתחילת המערך ומספר תא רצוי ,ותקבל חזרה מצביע גנרי אך לתא הרצוי; מצביע זה תעביר read_data לפונק' ) read_into_wanted_cellאותה היא קיבלה כפרמטר( .הפונ' read_into_wanted_cellאותה סיפק מי שקרא ל ,read_data -כלומר מי שיודע מהו טיפוסו האמיתי של המערך ,arrתבצע למצביע הגנרי לתא הרצוי המרת טיפוס לטיפוס 'האמיתי' ,ותקרא נתונים לתא המתאים. נציג עתה את הקוד של :read_data //------------------------------------------------------void read_data(void *arr, void *(*get_p2_wanted_cell)(void *, int), { ) )* void (*read_into_wanted_cell)(void { )for (int i = 0; i < N; i++ ; )void *cell = get_p2_wanted_cell(arr, i ; )read_into_wanted_cell(cell } } נסביר :הפונ' מקבלת שלושה פרמטרים: א .הפרמטר void *arr :הינו מצביע גנרי המורה על התא מספר אפס במערך. ב .הפרמטר) void *(*get_p2_wanted_cell)(void *, int) :שמו הוא (get_p2_wanted_cellהוא מטיפוס מצביע לפונ' המקבלת שני פרמטרים: ) (void *, intהמציינים את המצביע לתחילת המערך ומספר תא רצוי; הפונ' מחזירה * :voidמצביע )גנרי( לתא המבוקש. )* ) void (*read_into_wanted_cell)(voidשמו הוא ג .הפרמטר: (read_into_wanted_cellהוא מצביע לפונ' המקבלת מצביע גנרי מסוג * ,voidומחזירה .voidהפונ' עושה למצביע המרת טיפוס לטיפוס מסויים כלשהו ,וקוראת נתונים לתוך התא עליו המצביע מורה. גוף הפונ' כולל לולאה פשוטה :בכל סיבוב בלולאה נקראים נתונים לתוך תא יחיד במערך )הפונ' מניחה שבמערך Nתאים( .הדבר נעשה ע"י שראשית מזמנים את הפונ' שהועברה כפרמטר ,get_p2_wanted_cellושנית את הפונ' )שהועברה כפרמטר(.read_into_wanted_cell : יופיה של הפונ' ,לעומת זאת שהצגנו בסעיף הקודם ,הוא שהיא מסוגלת לקרוא ערכים לתוך כל מערך שהוא )בן Nתאים( ,ובלבד שהיא תקבל את הפרמטרים הדרושים :מצביע לתחילת המערך ,מצביע לפונ' שיודעת להמיר מצביע גנרי +מספר תא במצביע לאותו תא ,ומצביע לפונ' שבהינתן תא יודעת לקרוא לתוכו נתונים. למעשה read_dataשלנו מספקת את הבקרה על התהליך :את הלולאה ,ומזמנת את הפונ' הספציפיות לכל טיפוס וטיפוס. כיצד ינהג מי שרוצה לזמן את הפונ' ?read_data נניח שבתכנית הוגדר: ; ]int iarr[N כדי שניתן יהיה להשתמש בפונ' read_dataעלינו להגדיר עוד זוג פונ': 184 { )void *get_wanted_int_cell(void *p, int i ; )return (void *)((int *)p + i } //------------------------------------------------------{ )void read_into_wanted_int_cell(void *p ; cin >> *(int *) p } הראשונה בין השתיים מחזירה מצביע גנרי לתא רצוי במערך ,והשניה מקבלת מצביע גנרי ,ממירה אותו למצביע ל int -וקוראת נתונים לתא עליו המצביע מורה. אחרי כתיבת שתי הפונ' הללו נוכל לזמן את :read_data read_data(iarr,get_wanted_int_cell, ; )read_into_wanted_int_cell נסביר :אנו מעבירים ל read_data -את המערך ,ואת שתי הפונ' הדרושות לה. עתה נניח כי הגדרנו בתכניתנו: { struct Stud ; unsigned _id ; ]char _name[MAX_NAME_LEN ; } ; ]struct Stud studs[N כיצד נוכל להשתמש בפונ' read_dataשלנו על-מנת לקרוא נתונים גם למערך ?studs ראשית ,כאמור עלינו לכתוב את שתי פונ' העזר: האחת שמחזירה מצביע לתא רצוי במערך { )void *get_wanted_Stud_cell(void *p, int i ; )return (void *)(( struct Stud *)p + i } והשניה שקוראת נתונים לתוך תא רצוי: //------------------------------------------------------{ )void read_into_wanted_Stud_cell(void *p ; cin >> (( struct Stud *) p) -> _id >> )cin >> setw(MAX_NAME_LEN ; (( struct Stud *) p) -> _name } עתה נוכל לזמן את :read_data read_data(studs, get_wanted_Stud_cell, ; )read_into_wanted_Stud_cell כמובן שכדי להרחיב את read_dataכך שהיא תקרא נתונים גם למערך של תלמידים לא שינינו כלל את גוף הפונ'. עת תלמדו תכנות מונחה עצמים תגלו שאת הפונ' הללו ,לטיפול במבנה הנתונים struct Studניתן לכלול בתוך הגדרת ה ,struct -וכך לקבל 'עצם' )(object הכולל הן נתונים ,והן את הפונ' לטיפול בנתונים )בגישת התכנות מונחה העצמים קוראים לפונ' אלה בדרך-כלל בשם 'שיטות' .(methodsעת העצם מועבר כפרמטר לפונ' מועברים הן הנתונים והן השיטות כאחת. 185 16.3דוגמה שלישית :פונ' מיון גנרית כפי שציינו בדוגמה הקודמת ,תכונתה של הפונ' הגנרית הינה שהיא מספקת את מנגנון הבקרה )בדוגמה הקודמת זו הייתה לולאה פשוטה( ,ומשתמשת בפונ' המועברות לה כדי למממש את הפעולות עצמן .בפונ' פשוטה כגון read_dataמנגנון הבקרה הוא פשוט למדי ,ועל-כן התועלת משימוש בפונ' גנרית מוגבלת .על-כן עתה נציג משימה מורכבת יותר :מיון; בה תהליך הבקרה מורכב יותר ,וככזה השמוש בפונ' גנרית נראה מוצדק יותר. לשם הפשטות אציג מימוש של מיון בועות ,אולם לצורך ענייננו אין לכך כל חשיבות, ומימוש של פונ' מיון יעילות יותר נעשה באופן דומה לגמרי. כפי שאנו זוכרים)?( מיון בועות מריץ לולאה כפולה .בכל סיבוב בלולאה הוא משווה בין שני תאים סמוכים במערך ,ובמידת הצורך )במידה והם מפרים את סדר המיון הרצוי( האלג' מחליף בין ערכי התאים .על-כן ,פונ' המיון הגנרית שלנו תקבל את הפרמטרים הבאים: א .המערך אותו עליה למיין ).(void *arr, ב .פונ' המקבלת מצביע לתחילת המערך ומספר של תא רצוי ,ומחזירה מצביע ַלתא הקודמת. בדוגמה ראינו כבר כזו )פונ' )( void *(*get_p2_wanted_cell)(void *, int ג .פונ' המקבלת מצביעים לשני תאים )סמוכים( במערך ,משווה בין שני התאים במערך ,ומחזירה האם שני התאים בהתאמה עם הסדר הרצוי או לא. ))* ( bool (*cmp)(void *, void ד .פונ' המקבלת מצביעים לזוג תאים )סמוכים( במערך ומחליפה בין ערכיהם )* .void (*swap)(void *, void נציג את קוד הפונ ואחר נסבירו: //------------------------------------------------------void bubble_sort(void *arr, void *(*get_p2_wanted_cell)(void *, int), bool (*cmp)(void *, void *), { ))* void (*swap)(void *, void )for (int round = 0; round < N -1; round++ { )for (int place = 0; place < N -round -1; place++ if (!cmp(get_p2_wanted_cell(arr, place), ) ) )get_p2_wanted_cell(arr, place +1 swap(get_p2_wanted_cell(arr, place), ; ) )get_p2_wanted_cell(arr, place +1 } } הפונ' מנהלת את הלולאה הכפולה 'הרגילה' המורצת על-ידי מיון בועות .בכל סיבוב בלולאה היא מזמנת את פונ' ההשוואה ) cmpאשר הועברה לה כפרמטר( על שני תאי המערך ] arr[place], arr[place+1אשר מצביעים אליהם מחזירה לה הפונ' .get_p2_wanted_cellבמידה ופונ' ההשוואה מורה כי זוג התאים מפרים את הסדר )כלומר )] ( if (!cmp(arr[place], arr[place+1אזי יש להחליף בין ערכי התאים ,וזה נעשה ע"י הפונ' )שהועברה כפרמטר( swapאשר מקבלת מצביעים לשני התאים ,ומחליפה בין ערכיהם. 186 על-כן ,כדי להשתמש בפונ' bubble_sortהנ"ל יש צורך לממש את הפונ' הדרושות .נדגים כיצד נעשה הדבר ,ראשית עבור מערך של מספרים שלמים. נניח שהגדנו: ; ]int idata[N וקראנו נתונים למערך. נציג את הפונ' אותן יש לממש כדי לזמן את פונ' מיון הבועות .ראשית ,את הפונ' ) void *get_wanted_int_cell(void *p, int iכבר ראינו ,והיא שתועבר כארגומנט שני ל.bubble_sort - עתה אציג את פונ' ההשוואה ,אותה נעביר כארגומנט שלישי: //------------------------------------------------------{ )bool cmp_int( void *p1, void *p2 ; return *(int *)p1 <= *(int *)p2 } הפונ' ממירה את שני המצביעים הגנריים שהועברו לה לכדי מצביעים על ,intואז מחזירה את תוצאת ההשוואה בין השלם עליו מצביע הפרמטר הראשון ,למספר השלם עליו מצביע הפרמטר השני. פונ' ההחלפה ,המועברת כארגומנט הרביעי והאחרון ל bubble_sort -הינה: //------------------------------------------------------{ )void swap_int(void *p1, void *p2 ; int temp = *(int *) p1 ; *(int *) p1 = *(int *)p2 ; *(int *) p2 = temp } גם פונ' זו מקבלת שני מצביעים גנריים .היא ממירה אותם למצביעים ל,int - ומכאן מעבירה ערכים בין תאי הזיכרון כפי שאנו כבר מכירים מימים ימימה. עתה ,כשבאמתחתנו שלוש פונ' עזר הדרושות נוכל לזמן את bubble_sortכדי למיין את המערך :idata bubble_sort(iarr, get_wanted_int_cell, cmp_int, ; )swap_int נניח שעתה ברצוננו למיין את המערך ,אולם כל שאנו רוצים הוא שהמספרים השליליים ישכנו בתחילתו ,והחיוביים )או האי שליליים( בסופו .מה יהיה בדיוק הסדר בין השליליים לבין עצמם ,או בין החיוביים לבין עצמם לא משנה לנו .כדי להשיג מיון זה עלינו רק לכתוב פונ' השוואה חדשה .פונ' ההשוואה תקבע ששני שליליים הם בהתאמה לסדר הדרוש ,ואין צורך להחליף ביניהם ,כך גם שני חיוביים ,וכך גם שלילי וחיובי .הזוג היחיד שמפר את הסדר הוא עת מספר חיובי מופיע לפני מספר שלילי ,ולכן רק במקרה זה על פונ' ההשוואה להחזיר ערך .falseהפונ' תכתב לפיכך: //------------------------------------------------------{ )bool cmp_neg_pos( void *p1, void *p2 )if (*(int *)p1 >= 0 && *(int *)p2 < 0 ; return false ; return true } עתה נוכל למיין את המערך שוב ,בלי שנכניס כל שינוי בפונ' המיון עצמה ,ע"י שנעביר ל bubble_sort -את cmp_neg_posכפונ' ההשוואה בה יש לעשות שימוש: 187 bubble_sort(iarr, get_wanted_int_cell, cmp_neg_pos, ; )swap_int באופן דומה ,כדי ש bubble_sort -תוכל למיין מערך של ) struct Studכפי שהוצג בסעיף הקודם( ,עלינו למממש את שלוש פונ' העזר הדרושות על משתנים מטיפוס זה .את הפונ' get_wanted_Stud_cellכבר מימשנו בסעיף הקודם. את הפונ' cmp_struct_Studנממש עתה .נניח שברצוננו למיין את התלמידים על-פי מספר הזהות )שדה ה ,(id -ובין שני תלמידים להם אותו מספר זהות )ולרגע נניח שהדבר אפשרי( ,נקבע את הסדר על-פי שדה השם ) .(nameעל כן פונ' ההשוואה בין שני תלמידים תהיה: { )bool cmp_stuct_Stud( void *p1, void *p2 אם מספר הזהות של הראשון קטן משל השני החזר :true < if ((struct Stud *)p1 -> _id ) (struct Stud *)p2 -> _id ; return true אם מספרי הזהות שלהם שווים ,ושמו של הראשון קטן משמו של השני אזי החזר :true == _id && _id *)p1 -> _name, )*)p2 -> _name) <= 0 >if ((struct Stud *)p1 - >(struct Stud *)p2 - strcmp((struct Stud (struct Stud ; return true בכל מקרה אחר החזר :false ; return false } הפונ' swap_struct_Studדומה מאוד לזו המחליפה שלמים: //------------------------------------------------------{ )void swap_struct_Stud(void *p1, void *p2 ; struct Stud temp = *( struct Stud *) p1 ; *( struct Stud *) p1 = *( struct Stud *)p2 ; *( struct Stud *) p2 = temp } עתה נוכל לזמן את :bubble_sort bubble_sort(studs, get_wanted_ struct_Stud _cell, cmp_struct_Stud, ; )swap_struct_Stud כדי לא להעמיס על הדוגמות הקודמות הסרתי מהן את כל נושא הפרמטרים הקבועים ) ,(constמעשה ,שכמובן ,לא ראוי לעשות .הפונ' המחזירה מצביע לתא במערך ,כמו גם זו המשווה בין שני תאי מערך ,יכולה וצריכה להיכתב תוך שימוש בפרמטרים קבועים .על-כן ,לסיום ,אציג את הפרוטוטיפים של הפונ' השונות כשהן כתובות כהלכה ,ועם פרמטרים קבועים: //------------------------------------------------------שתי הפונ' המחזירות תא במערך מקבלות :const void *p ; )void *get_wanted_int_cell(const void *p, int i ; )void *get_wanted_double_cell(const void *p, int i בהתאמה ,גם read_dataמקבלת כפרמטר שני פונ' המקבלת מצביע שהינו קבוע: void read_data(void *arr, 188 )void *(*get_p2_wanted_cell (const void *, int), ; ) )* void (*read_into_wanted_cell)(void ; )void read_into_wanted_int_cell(void *p ; )void read_into_wanted_double_cell(void *p פונ' ההשוואה יכולות לקבל מצביע קבוע: ; )bool cmp_int(const void *p1,const void *p2 ; )bool cmp_double(const void *p1,const void *p2 ; )void swap_int(void *p1, void *p2 ; )void swap_double(void *p1, void *p2 הפרוטוטיפ של מיון בועות משקף את הפרוטוטיפים של הפונ' שהוא מקבל: void bubble_sort(void *arr, )void *(*get_p2_wanted_cell (const void *, int), bool (*cmp)(const void *,const void *), ; ))* void (*swap)(void *, void 16.4דוגמה שלישית :בניית רשימה מקושרת ממוינת גנרית נניח שבתכנית הגדרנו מבנה בעזרתו נשמור נתוני תלמיד בודד )מספר הזהות שלו, שמו ,ומינו( ,ומבנה שני בעזרתו נשמור נתוני קורס בודד )קוד הקורס ,שמו ,ושנת הלימודים לה הוא מיועד(: { struct Stud ; unsigned long _id ; ]char _name[MAX_NAME_LEN ; enum gender_t _gender ; } { struct Course ; unsigned int _course_code ; ]char _name[MAX_NAME_LEN ; char _for_year ; } נשים לב כי עת הגדרנו מבנים אלה לא נתנו דעתנו לשאלה האם נרצה לאחסן את נתוני התלמידים או הקורסים במערך ,ברשימה מקושרת ,בעץ ,או בכל מבנה נתונים אחר .מבני הנתונים כוללים רק את המידע אותו ברצוננו לאחסן .במילים אחרות, המבנים אינם כוללים מצביעים שיתמכו ברשימה או בעץ. עתה נניח כי החלטנו שבתכנית כלשהי ברצוננו לבנות רשימה מקושרת ממוינת אחת של תלמידים )שתמוין על-פי מספר הזהות( ,ושניה של קורסים )שתמוין על-פי שם הקורס( .אדגיש כי כל רשימה תכיל מבנים מסוג יחיד )לא נכניס לרשימה אחת הן תלמידים והן קורסים(. את המשימה של בניית הרשימה הממוינת נרצה לממש באמצעות פונ' גנרית ,אשר תממש עבורנו את הלולאות הדרושות ,תוך שהיא עושה שימוש בפונ' שנממש עבור 189 כל אחד ואחד ממבני הנתונים ,בפרט פונ' השוואה בין שני תלמידים ,ופונ' השוואה בין שני קורסים. 190 לשם ביצוע המשימה נגדיר מבנה שלישי: { struct List_node ; void *_item ; struct List_node *_next ; } מבנה זה הוא שיתמוך בקיומה של רשימה מקושרת .נסביר :כל איבר מסוג List_nodeכולל מצביע למבנה של תלמיד בודד או קורס בודד )ולכן זהו מצביע מסוג * ,voidעל מנת שהוא יוכל להצביע על כל מבנה שהוא ,ומצביע שני לאיבר הבא ברשימה ,איבר שהוא תיד ובהכרח מסוג .List_nodeמבחינה ציורית הרשימה נראית: _next _next _next _next _item _item _item _item תלמיד או קורס בודד תלמיד או קורס בודד תלמיד או קורס בודד תלמיד או קורס בודד נסביר :בשורה העליונה בציור מופיעים איברים מטיפוס List_nodeהכוללים שני מצביעים בכל איבר .את המצביעים מסוג * List_nodeציירתי בקו שלם .בשורה מתחת מופיעים האיברים הכוללים את הנתונים שיש לשמור )נתוני תלמידים או לחילופין קורסים( .כל איבר מסוג List_nodeמצביע )עם מצביע שמצויר בקו מקווקו( על איבר בודד של תלמיד או של קורס .אברי התלמידים\קורסים כמובן שאינם משורשרים זה לזה ישירות ,הרי אין בהם כלל מצביעים ,רק שדות של נתונים. כדי להשלים את המשימה יהיה עלינו לכתוב שלוש פונ' עבור ,struct Stud ושלוש מקבילות עבור .struct Nodeשלוש הפונ' יבצעו: א .הקצאת איבר של תלמיד\קורס והחזרת מצביע אליו )מצביע זה נכניס למרכיב _itemבקופסה מסוג .List_noe ב .קריאת נתוני תלמיד\קורס בודד )לתוך איבר עליו מורה מצביע גנרי ,כלומר כזה מטיפוס * .(void ג .השוואה בין שני תלמידים )או בין שני קורסים( עליהם מורים מצביעים גנריים. בנוסף להן אציג גם פונ' הדפסת נתוני תלמיד בודד .לפונ' זו נזדקק עת נרצה להציג את הרשימות הממוינות שבנינו. אציג את שלוש הפונ' עבור ,struct Studאלה של קורס דומות לגמרי: פונ' זו //-----------------------------------------------------------מקצה איבר מסוג struct Studומחזירה מצביע אליו { )(void* alloc_stud ; struct Stud *p = new (std::nothrow) struct Stud { )if (p == NULL ; "cerr << "cannot allocate a new sturct Stud\n ; )exit(EXIT_FAILURE } ; return p } //------------------------------------------------------------ פונ' זו מקבלת מצביע גנרי ,אשר אנו מובטחים כי מצביע לאיבר של תלמיד ,וקוראת את נתוני התלמיד: 191 { )void read_stud(void *p ; int temp ; cin >> ((struct Stud *) p) -> _id ; cin >> setw(MAX_NAME_LEN) >> ((struct Stud *) p) -> _name ; cin >> temp ? )((struct Stud *) p) -> _gender = ((temp == 0 ;)FEMALE_GNDR : MALE_GNDR } //------------------------------------------------------------ פונ' זו מקבלת מצביע גנרי ,אשר אנו מובטחים שמצביע אל מבנה של תלמיד, ומציגה את נתוני התלמיד )נזדקק לה לשם הצגת הרשימות שנבנה(: { )void print_stud(const void *p ; " " << cout << ((struct Stud *) p) -> _id ; cout << ((struct Stud *) p) -> _name ? cout << (((struct Stud *) p) -> _gender == FEMALE_GNDR ; )""F" : "M ; cout << endl } //------------------------------------------------------------ פונ' זו מקבלת זוג מצביעים גנריים ,אשר אנו מובטחים שמצביעים על מבנים של תלמידים )כל אחד על מבנה יחיד( ,ומחזירה את תוצאת ההשוואה בין שני המבנים. שרירותית ,ולשם הפשטות בחרתי להשוות בין שני תלמידים על-פי מספר הזהות שלהם ,אולם לכך אין ,כמובן ,חשיבות: { )bool cmp_stud(const void *p1, const void *p2 =< return ((struct Stud *) p1) -> _id ; ((struct Stud *) p2) -> _id } עתה ,כשבצקלוננו הפונ' הפשוטות הללו נוכל לכתוב את הפונ' הגנרית .build_sorted_listהפונ' תקבל מצביעים לשלוש פונ' :פונ' הקצאת איבר מהטיפוס הרצוי ,פונ' קיראת נתונים לתוך איבר מהטיפוס הרצוי ,ופונ' להשוואה בין שני איברים מהטיפוס הרצוי .הפונ' תבנה רשימה מקושרת ממוינת ,ותחזיר מצביע לראש הרשימה .מצביע מטיפוס * .List_nodeאציג את קוד הפונ': //-----------------------------------------------------------(struct List_node *build_sorted_list void* alloc_struct() , void read_struct(void *p) , { ) )bool cmp_struct(const void *p1, const void *p2 ; struct List_node *head = NULL ; 'char another_one = 'y { )'while (another_one == 'y ; )(void *p = alloc_struct ; )read_struct(p ; )insert(head, p, cmp_struct ; " ?cout << "insert another item ; cin >> another_one } ; return head } נסביר את הפונ' :כפי שאנו רואים מכתורת הפונ' היא אכן מקבלת מצביעים לשלוש פונ' :האחת מקצה איבר ומחזירה מצביע גנרי אליו ,השניה מקבלת מצביע גנרי וקוראת נתונים לתוך האיבר )תוך שהיא ,כמובן ,מבצעת המרת טיפוס למצביע הגנרי לכדי הטיפוס המתאים( ,והשלישית משווה בין איברים כדי לקבוע את מקומם ברשימה הממוינת. 192 הפונ' מגדירה מצביע struct List_node *head = NULL ; :אשר יורה על אברי הרשימה .היא מנהלת לולאה בה כל עוד המשתמש מעוניין להוסיף איברים נוספים לרשימה אותה הוא בונה עתה :א .מזמנים את הפונ' להקצאת איבר בודד) ,ועליו מצביע המשתנה (pב .קוראים נתונים לתוך אותו איבר )זה שעליו מצביע ,(pג. מכניסים את האיבר הבודד )עליו מצביע (pלרשימה השלמה הנבנית )עליה מצביע (headבמקום המתאים )תוך שימוש בפונ' ההשוואה(. נפנה עתה להצגת הפונ' insertאשר מוסיפה איבר בודד לרשימה: //-----------------------------------------------------------void insert(struct List_node *&head, void *p, { ) )bool cmp_struct(const void *p1, const void *p2 = struct List_node *new_item ; new (std::nothrow) struct List_node { )if (new_item == NULL ; "cerr << "Cannot allocate a new List_node\n ; )exit(EXIT_FAILURE } ; new_item -> _item = p אם הרשימה ריקה ,זה האיבר הראשון המוסף לה: { )if (head == NULL ; head = new_item ; head -> _next = NULL } אם האיבר החדש צריך להיות מוסף בראש הרשימה )פונ' ההשוואה מורה לנו שהוא קטן מהאיבר שבראש הרשימה(: { ))else if (cmp_struct(new_item -> _item, head -> _item ; new_item -> _next = head ; head = new_item } אם האיבר החדש צריך להיות מוסף באמצע או בסוף הרשימה: else { struct List_node *rear = head, ; *front = head->_next && while (front != NULL { ))cmp_struct(front->_item, new_item->_item ; rear = front ; front = front -> _next } ; new_item -> _next = front ; rear -> _next = new_item } } נסביר את הפונ' :הפונ' מקבלת שלושה פרמטרים :הראשון head ,מצביע על הרשימה הנבנית .הוא מועבר כפרמטר הפניה שכן הפונ' עשויה לשנות את ערכו )בעת הוספת איבר ראשון ,או עת מוסף יבר חדש שקטן מהאיבר המצוי בראש הרשימה(. המצביע הוא מסוג * List_nodeשכן אברי הרשימה הם כולם מסוג זה .בכל איבר יהיה מצביע גנרי שיצביע על תלמיד בודד או על קורס בודד ,ואותו מצביע )השדה _itemהוא מצביע גנרי( .הפרמטר השני ) pשל הפונ'( ,מצביע על האיבר הבודד אותו יש להוסיף לרשימה )איבר זה עשוי להיות מסוג struct Studאו מסוג struct Courseולכן המצביע הוא גנרי( .הפרמטר השלישי הוא פונ' השוואה בין שני איברים ,בעזרתה ממוינת הרשימה. 193 קוד הפונ' דומה לזה שראינו עת בנינו רשימה מקושרת ממוינת 'רגילה' .המשתנה new_itemיצביע על האיבר שיוּסף ַלרשימה )איבר מסוג ,(List_nodeעל כן ראשית אנו מקצים לו זיכרון ,ושנית ְ מפנים את המצביע _itemלהצביע על נתוני התלמיד\קורס החדש .עתה עלינו לשלב את האיבר עליו מצביע ַ new_itemברשימה עליה מצביע ,headולשם כך אנו מבחינים בין שלושה מקרים: נפנה את head א .אם הרשימה ריקה ,כלומר זהו האיבר הראשון המוסף לה ,אזי ְ להצביע כמו .new_item ב .אם על-פי פונ' ההשוואה שהועברה לנו עולה כי האיבר הנוכחי קטן מזה שניצב בראש הרשימה ,אזי יש להוסיף את האיבר הנוכחי בראש הרשימה ,וזה מה שאנו עושים. ג .במקרה הכללי :אנו מתקדמים על-פני הרשימה עם זוג מצביעים rear, frontעד אשר frontמצביע על איבר גדול יותר מהאיבר שיש להוסיף .עתה )אחרי שגמרנו להתקדם( ,אנו משלבים את האיבר החדש ברשימה בין האיבר עליו מצביע rearלאיבר עליו מצביע .front התכנית הראשית תראה: //-----------------------------------------------------------{ )(int main ; struct List_node *studs ; struct List_node *courses ; )studs = build_sorted_list(alloc_stud, read_stud, cmp_stud courses = build_sorted_list(alloc_course, read_course, ; )cmp_course ; )print_list(studs, print_stud ; )print_list(courses, print_course ; return EXIT_SUCCESS } את מימוש הפונ' print_listאני משאיר לכם כתרגיל. 194 עודכן 5/2010 .17חלוקת תכנית לקבצים עת התכניות שאנו כותבים גדלות קורה לא אחת שאנו מעוניינים לחלק את התכנית לכמה קבצים שיקומפלו יחד לכדי תכנית אחת שלמה )לעתים נאמר :לכדי אפליקציה אחת( .סיבות שונות מביאות אותנו לחלוקת התכנית לכמה קבצים: א .מתכנתים שונים יכולים לעבוד במקביל על חלקים שונים בתכנית; כל חלק ייכתב בקובץ נפרד. ב .פונ' אשר מבצעות משימה דומה )למשל פונ' המבצעות משימות מתמטיות שונות בתכניתנו( יושמו בקובץ יחיד .עת נזדקק גם בתכנית אחרת למשימות אלה ,נוכל לשלב את הקובץ ַבתכנית האחרת בנקל )בלי שנצטרך לבצע 'העתק\הדבק' מקובץ לקובץ ַלפונ' הדרושות לנו(. ג .עת מתגלית שגיאה בחלק כלשהו של התכנית ,או עת אנו רוצים לשנות חלק כלשהו בתכנית איננו צריכים לקמפל את כל התכנית )דבר שעלול לקחת זמן רב אם התכנית כוללת אלפים רבים של שורות קוד( ,אלא אנו מקמפלים )במובן הצר והמדויק של המילה( רק את הקובץ שמכיל את הקוד שהשתנה ,ואחר אנו כורכים )עושים (linkingלכלל הקבצים המקומפלים )אלה שלא שונו ,וזה ששונה( לכדי תכנית שלמה חדשה. לסיכום :מגוון של סיבות מביאות אותנו לרצות לחלק את תכניתנו למספר קבצים. בפרק זה נלמד כיצד לעשות זאת. 17.1הנחיית הקדם מהדר #define אתחיל בסטייה לנושא מעט שונה ,אשר נזדקק לו בהמשך ,עת נתקדם עם המשימה של חלוקת התכנית לקבצים. עת דנו בתרגומה של התכנית משפה עילית לשפת מכונה אמרנו שלמרות שבשפה חופשית אנו קוראים לתהליך קומפילציה )או הידור( הרי ליתר דיוק התהליך מתחלק לשני שלבים מרכזיים :שלב הקומפילציה ,בו הקומפיילר מתרגם את התכנית משפה עילית לשפת מכונה )אלא אם הוא מצא בה שגיאות תחביריות ,ואז הוא מודיע עליהן ,ולא מתרגם את התכנית( ,ושלב הלינקינג )או הכריכה( בה נוספים לתכנית חלקי קוד נוספים ,בפרט של פונ' סיפריה שונות אותן זימנו בתכניתנו )כדוגמת cin, sqrt :וכולי( כך שמתקבלת תכנית שלמה ,ניתנת להרצה. עתה נוסיף ונפרט ,ונאמר שגם שלב ההידור מתחלק לשני שלבים :שלב ה'-קדם מהדר' ) ,(pre-processorושלב ההידור המרכזי .בסעיף זה נתאר חלק מפעולותיו של קדם המהדר. קדם המהדר סורק את קובץ התכנית שלנו .עת הוא נתקל בפקודה המתחילה בסימן הסולמית ) (#הוא מבצע אותה ,ובכך משנֵה את הקובץ שלנו ,באופן אותו נתאר מייד) .אם נדייק ,השינוי אינו מבוצע על קובץ המקור ,קובץ המקור אינו משתנה, השינוי הוא בכך שהפלט של קדם המהדר כולל את קובץ המקור שלנו ,אחרי שינויים(. לדוגמה :אם בתכנית שלנו נכלול את השורה: 17 195 #define MAX אזי הפקודה ) #defineכמו כל ההנחיות המתחילות ב (# -הינה פקודה המופנית לקדם המהדר .הפקודה מורה לקדם המהדר שבהמשך התכנית ,בכל מקום בו תופיע המחרוזת MAXיש להמירה במספר .17הפקודה גם מדגימה לנו את אופן פעולתו של קדם המהדר באופן כללי :קדם המהדר הוא רכיב 'טיפש' אשר יודע לבצע פעולות המרה של מחרוזות .מבחינה עניינית ,הפקודה הנ"ל היא הדרך המקובלת בשפת C )בניגוד לשפת (C++להגדיר קבועים .דרך זו נחשבת לפחות מוצלחת או רצויה מהדרך המקובלת ב C++ -שכן אין בה אפשרות לבדוק טיפוסים—המהדר עצמו ,עת מטפל בתכנית ,מוצא את המספר הקבוע ) .17כרגיל ,אנו נוהגים לכתוב קבועים באות גדולה(. אם נדייק אזי הפקודה #defineתביא להמרת המחרוזת שמופיעה אחריה בשארית השורה .מסיבה זאת אנו יכולים לכתוב גם את פקודות ה#define - הבאות: { } #define BEGIN #define END #define THEN אם בהמשך התכנית שלנו יופיע הקוד: if (a < b) THEN BEGIN a = b; b = 0; END if (a == 0) THEN BEGIN ; END BEGIN ; END אזי בעקבות פעולת הקדם מעבד יתקבל הקוד: } ;if (a < b) { a = b; b = 0 } ; { } ; { )if (a == 0 נסביר :כל מופע של המחרוזת BEGINמומר במחרוזת { ,באופן דומה כל מופע של ENDמוחלף בשארית השורה ,כלומר ב , } :ולבסוף ,כל מופע של THENמוחלף בכלום ,במילים פשוטות ,נמחק מהקוד. שימו לב שהקדם מעבד מבצע פעולה על מחרוזות ,במובן זה ניתן להפעילו גם על קובץ שאינו כולל קוד ,והוא יבצע את פעולות ההמרה המתבקשות ממנו. במהדר g++נוכל לבקש לראות את פלט קדם ההידור של התכנית שלנו מבלי שהמהדר ימשיך ביתר תהליך הקומפילציה ,בעזרת הפקודה: g++ -E my_prog.cc הדגל -Eמורה ל g++ -לבצע רק את שלב הקדם מהדר. הפלט של קדם המהדר יוצג על-גבי המסך. נציג עתה את פעולת הקדם מעבד הקרויה 'מאקרו' .המאקרו מגדיר מספר פעולות שיש לבצע עת 'מזמנים' אותו ,ליתר דיוק עת 'פורשים' אותו .לדוגמה המאקרו: ; #define ZERO num1 = 0; num2 = 0; num3 = 0 אומר שכל פעם שנכתוב בהמשך התכנית את המילה ZEROהיא תוחלף על-ידי קדם המהדר בשלוש פעולות ההשמהnum1 = 0; num2 = 0; num3 = 0 ; : כלומר נוכל בתחילת הקוד שלנו להגדיר את המאקרו הנ"ל )באמצעות פעולת ה- ,(#defineובהמשך לכתוב כל פעם ZEROואז בקוד ישתלו שלוש ההשמות הנ"ל. לפני שנפנה לדוגמה נוספת נציג ונסביר את הקוד הבא: ; num1 = num1 + num2 ; num2 = num1 – num2 ; num1 = num1 – num2 196 קוד זה מהווה דוגמה טובה ַלדרך בה נקל לכתוב קוד פשוט בדרך קשה להבנה .אם תסמלצו את הקוד תגלו שהוא מחליף בין ערכי num1ו num2 -בלי להשתמש במשתנה עזר. עתה נציג את המאקרו הבא: ;#define SWAP(_A, _B) _A = _A +_B; _B= _A- _B; _A = _A - _B זהו מאקרו 'מתקדם' ,כזה המקבל פרמטרים _A :ו . _B -בהמשך התכנית ,אחרי הגדרת המאקרו נוכל לכתוב , SWAP(num1, num2) :והקדם מהדר ימיר כתיבה זאת בשלוש הפקודות: ;num1 = num1 +num2; num2= num1- num2; num1 = num1 - num2 תוך שהוא ממיר כל _Aב ,num1 -וכל _Bב .num2 -התוצאה היא מעין קריאה לפונ' ,אולם זו לא באמת קריאה לפונ' עם הסתעפות לקוד הפונ' ,ותוך בדיקה של המהדר שהזימון הינו תקין; כל שנעשה פה הוא שהמחרוזת ) SWAP(num1, num2מומרת על-ידי קדם המעבד בשלוש ההשמות: ;num1 = num1 +num2; num2= num1- num2; num1 = num1 - num2 אגב ,שימו לב שאחרי שם המאקרו איננו כותבים נקודה-פסיק ,שכן המאקרו אינו פקודה בתכנית שיש לסיים בנקודה-פסיק ,הוא הנחיה לקדם-מהדר. נביט עתה במאקרו פשוט אחר: #define DOUBLE(_A) _A+_A אם נכתוב בהמשך התכנית: ; )cout << DOUBLE(num1 אזי המאקרו ייפרש ונקבל שהמהדר יקמפל את הקוד: ; cout << num1 + num1 אך מה יקרה אם נכתוב: ; )cout << num2 * DOUBLE(num1 אזי תוצאת פרישת המאקרו תהיה: ; cout << num2 * num1 + num1 ובשל סדר הפעולות האריתמטיות קרוב לוודאי שלא זה מה שהמתכנת רצה להשיג. הפתרון הוא להגדיר את המאקרו באופן: )#define DOUBLE(_A) (_A+_A תוצאת פרישת המאקרו: ; )cout << num2 * DOUBLE(num1 תהיה: ; )cout << num2 * (num1 + num1 מין הסתם ,התוצאה הרצויה. לבסוף ,אנו רשאים להגדיר את המאקרו: \ )#define SWAP(_TYPE, _A, _B } ;{_TYPE temp = _A; _A= _B; _B = temp ראשית ,אעיר שהגדרת מאקרו צריכה לשכון על שורה יחידה ,עת המאקרו גדול מדי אנו רשאים לחלקו למספר שורות ,אך בסוף כל שורה עלינו לכתוב את התו (\) backslashשמורה למהדר להתייחס לשורה הבאה כאילו היא המשכה של השורה הנוכחית .כאילו לא שברנו שורה. אם בהמשך הקוד נכתוב: )SWAP(int, num1, num2 אזי תוצאת הפרישה של המאקרו תהיה: } ;{int temp = num1; num1= num2; num2 = temp 197 הפרמטר _TYPEבמאקרו מוחלף במילה ,intהפרמטרים _A, _Bמוחלפים ב: num1, num2בהתאמה ,והמאקרו יוצר בלוק ,בו הוא מגדיר משתנה עזר בשם tempובעזרתו מחליף בין ערכי .num1, num2 17.2הנחיית הקדם מהדר #ifdef הנחיית המהדר #ifdef :שולטת על קימפולו או אי קימפולו של קטע קוד כלשהו. נראה דוגמה קטנה: #ifdef SYMBOL1 ; "!cout << "Wow ; num = 0 #endif עת הקדם מהדר יגיע לקטע קוד זה הוא ישאל את עצמו :האם כבר הוגדר לי )באמצעות פקודת (#defineהסימן ) SYMBOL1ולא משנה איזה ערך הוא מייצג(? במידה והתשובה לשאלה היא כן אזי שתי הפקודות: ; "!cout << "Wow ; num = 0 תכללנה בפלט של קדם-המהדר ,כלומר תקומפלנה על-ידי המהדר עצמו .אם לעומת זאת הסימן SYMBOL1לא הוגדר עד כה אזי שתי הפקודות הללו )ובמקרה הכללי עד ה (#endif -לא תשלחנה לפלט ,ועל כן לא תקומפלנה ,כלומר יתקבל מצב כאילו הן לא נכללו כלל בתכנית שלנו. נראה דוגמה שנייה .נניח את הקוד: #define A { )(int main ; int num = 0 #ifdef A ; num = 1 #endif #ifdef B ; num = 2 #endif ; cout << num ; return EXIT_SUCCESS } ונשאל :מה יהיה פלט התכנית? התשובה :הפלט יהיה ,1 :שכן ההשמה: ; num = 1תיכלל בקוד המקומפל ,אך ההשמה num= 2 ; :לא תיכלל .אם, לעומת זאת ,נוסיף לצד ההנחיה #define A :את ההנחיה) #define B :לפני או אחרי ההנחיה ,#define Aאין לכך חשיבות( ,אזי פלט התכנית יהיה .2 נציג דוגמה בה הנחיית המהדר עשויה להיות שימושית .נביט בקטע הקוד: )for (int div = 2; div <= sqrt(num); d++ 198 { #ifdef DEBUG ; cout << num << div << num%div << endl #endif )if (num % div == 0 ; break } קטע קוד זה נכלל בפונ' הבודקת האם המספר numהינו ראשוני או פריק. חשיבותו לצורך ענייננו הנוכחי היא שבמידה והסימן DEBUGהוגדר כבר ַל ְקדם המהדר אזי בכל סיבוב בלולאה יבוצעו שתי פקודות :פקודת פלט )העוזרת לנו לבחון את ערכי המשתנים ,ועל-ידי כך את הקורה בלולאה( ,ופקודת התנאי .אם ,לעומת זאת ,הסימן DEBUGלא הוגדר עד כה לקדם המהדר ,אזי בכל סיבוב בלולאה תבוצע רק פקודת התנאי ,פקודת הפלט לא תיכלל כלל בקוד שיקומפל. וכיצד הדבר ישמש אותנו? דרך אחת היא שבזמן העבודה על התכנית ,עת יש לבדוק את תקינותה ,נוסיף לפני הקוד הנ"ל את השורה: #define DEBUG ובכך אנו מגדירים את הסימן ) DEBUGוקובעים שיש להחליפו בשום דבר ,אולם לכך אין חשיבות לצורך העניין( ,ולכן גם פקודת הפלט תופיע בקוד שלנו .בתום העבודה על התכנית נסיר את השורה #define DEBUG :ועל כן בתכנית הגמורה כבר לא תופיע פקודת הפלט. דרך חלופית ,פשוטה יותר ,היא במהלך העבודה על התכנית לקמפלה ַבאופן: g++ -Wall –DDEBUG my_prog.cc הדגל -DDEBUG :כולל ראשית את הסימן –Dובעקבותיו את המחרוזת .DEBUG מרכיב זה בפקודת ההידור שקול בדיוק לכתיבתַ #define DEBUG :בקוד; כלומר הוא מגדיר לקדם מהדר את הסימן .DEBUGהתוצאה תהיה כמו בדרך שתוארה קודם :קוד התכנית יכלול גם את פקודת הפלט .לעומת זאת ,עת נסיים לעבוד על התכנית ,נקמפלה 'כרגיל': g++ -Wall my_prog.cc בפקודת הידור זו איננו מגדירים את הסמל ,DEBUGולכן קוד התכנית לא יכלול את פקודת הפלט. לפקודה #ifdefיש 'אחות תאומה' והיא פקודת ה #ifndef :אשר שואלת את השאלה ההפוכה :האם לא הוגדר הסימן המופיע בהמשך השורה? רק אם הסימן לא הוגדר קטע הקוד המופיע בין ההנחיה הנ"ל לבין ה #endif -יקומפל. בהמשך ,עת נציג כיצד מחלקים את תכנית למספר קבצים ,נראה שימושים נוספים, מובהקים יותר להנחיות קדם המהדר. #ifdef, #ifndef : 17.3הנחיית הקדם מהדר #include ההנחיה #include "my_file" :גורמת לקדם המהדר להכניס לקובץ בו מופיע ההנחיהַ ,במקום בו מופיעה ההנחיה ,את הקובץ my_fileאשר אמור לשכון במדריך הנוכחי )אחרת יש לציין נתיב מלא לקובץ(. נראה דוגמה: נניח את ההנחות הבאות: הקובץ f1כולל את השורה )ורק אותה ,כלומר זהו כל הקובץ(: 199 I am f1 הקובץ f2כולל את השורות )ורק אותן(: I am f2 1 "#include "f1 I am f2 2 הקובץ f3כולל את השורות: I am f3 1 "#include "f1 "#include "f2 I am f3 2 כיצד יראה פלט קדם ההידור של הקובץ :f3 ראשית ,השורה I am f3 1 :תופיע בפלט קדם ההידור ,כמות שהיא. שנית ,השורה #include "f1" :תגרום להכנסת הקובץ f1כמות שהוא, כלומר תוסיף לפלט קדם ההידור את השורה.I am f1 : שלישית ,השורה #include "f2" :תגרום להכללת הקובץ f2בפלט ,ולכן תופיע בפלט קדם ההידור השורה ,I am f2 1 :אחריה תופעל פעולת ההכללה של f1 כפי שמופיעה ב ,f2 -ולכן תופיע בפלט השורה ,I am f1 :ולבסוף תופיע השורהI : .am f2 2 רביעית תופיע בפלט קדם ההידור השורה.I am f3 2 : התוצאה הכוללת תהיה: 1 1 2 2 200 f3 f1 f2 f1 f2 f3 am am am am am am I I I I I I עתה נניח את ההנחות הבאות: הקובץ f1כולל את ההגדרה: { struct S1 ; double _x ; } הקובץ f2כולל את השורות הבאות: "#include "f1 { struct S2 ; struct S1 s1, s2 ; } הקובץ f3כולל את הקוד: "#include "f1 "#include "f2 כיצד יראה פלט קדם ההידור של הקובץ ?f3 ראשית יוכלל בו f1ולכן בפלט יופיע: { struct S1 ; double _x ; } שנית ,יוכלל בו ' f2שמביא איתו' שוב את f1ולכן לפלט יתווסף: { ; _x { ; S1 s1, s2 struct S1 double ; } struct S2 struct ; } כלומר הפלט יכלול את ההגדרה }…{ struct S1פעמיים. אם פלט זה אמור לעבור לשלב ההידור המרכזי ,כפי שצפוי שיקרה ,אזי המהדר 'יצעק' עלינו שאנו מגדירים את struct S1פעמיים )גם אם בשתי הפעמים זו בדיוק אותה הגדרה(. כיצד נמנע מתקלה בלתי רצויה זאת? התשובה היא :בעזרת פקודות .#ifdef נסביר :את הקובץ f1נשנה באופן הבא: #ifndef STRUCT_S1 #define STRUCT_S1 { struct S1 ; double _x ; } #endif אנו אומרים ש':אנו מגנים על המבנה s1מפני הכללה כפולה'.. נסביר מה הועילו חכמים בתקנתם :כיצד יראה עתה פלט קדם ההידור של הקובץ :f3ראשית ,ההנחיה " #include "f1תביא להכללת הקובץ f1בפלט .עת קדם המהדר יעבור על f1הוא ישאל את עצמו :האם לא הוגדר לי עדיין )בריצתי הנוכחית( הסימן ? STRUCT_S1והתשובה תהיה' :כן .עד כה לא הוגדר לקדם המהדר הסימן .'STRUCT_S1על כן השורות שעד ה #endif -תשלחנה לפלט ,וכן הסימן STRUCT_S1כבר יוגדר לקדם המהדר. עת נתקדם בקובץ f3לשורה השנייה #include "f2" :נפנה לכלול את הקובץ .f2בקובץ f2נמצא את ההנחיה ,#include f1 :ולכן קדם המהדר יפנה לקובץ .f1הוא ישאל את עצמו שוב :האם לא הוגדר לי עדיין )בריצתי הנוכחית( הסימן 201 ? STRUCT_S1הפעם התשובה תהיה 'לא .כבר הוגדר לי הסימן .'STRUCT_S1על כן כל הקוד עד ה #endif -לא ישלח לפלט ,בפרט הגדרת ה struct S1 -לא תופיע פעמיים בפלט קדם ההידור .קדם המהדר יחזור לקובץ ,f2ויוסיף את הגדרת המבנה struct S2לפלט שלו. התוצאה המרכזית ,כאמור ,היא שבתוצאת קדם ההידור ,המבנה S1מוגדר פעם יחידה ,ועל כן המהדר כבר לא יתלונן על הגדרה כפולה של המבנה .נחזור לנושא בהמשך ,עם תכנית שלמה. כמה מכם ודאי שאלו את עצמם מה הקשר בין הפקודה: >#include <iostream #includeכפי שמוסברת בסעיף שאנו כותבים בכל תכנית שלנו ,לבין הנחיית ה- זה? התשובה היא ,כמובן ,שמדובר באותו דבר .ההבדל היחיד הוא שאת הקובץ iostreamלא אנחנו יצרנו ,ובהתאמה הוא אינו שוכן בתיקיה שלנו .סוגרי הזווית )> <( שעוטפים את שם הקובץ מורים לקדם המהדר שקובץ זה עליו לחפש לא במדריך הנוכחי ,אלא במדריך )המוכר לו( הכולל את כל קובצי הinclude - הסטנדרטיים .מעבר לכך ,הקובץ iostreamהוא קובץ הכולל הצהרות חיוניות, אשר הקדם מהדר 'דוחף' בקובץ שלנו ,בדיוק כפי שתיארנו את פעולת ה.include - ההצהרות המופיעות בקובץ iostreamגורמות למהדר להכיר את פקודות הספרייה הסטנדרטית cin, cout, ... :ולא 'לצעוק' עלינו עת אנו כוללים פקודות אלה )שאינן חלק משפת c++הבסיסית( בתכניתנו. בהתאמה ,אנו יכולים לצור קובץ בשם . my_includes.hבהזדמנות זאת אעיר שהסיומת .hשמציינת headerאו 'כותר' בעברית ,היא סיומת מקובלת לקבצים הכוללים הגדרות שונות; קבצים אשר עושים להם ,includeולא מקמפלים אותם )כלומר לעולם לא נכתוב g++ -Wall my_file.h :עבור my_file.hשהינו קובץ כותר ,הכולל הצהרות מועילות שונות (.הקובץ my_include.hעשוי להראות: ><iostream ><iomanip ><cmath ><cstdlib #include #include #include #include ; using std::cin ; using std::cout ; using std::endl בכל תכנית שלנו נוכל לעשות includeלקובץ זה ,והדבר יחסוך לנו את הצורך לכתוב שוב ושוב ,בראש כל תכנית ,את השורות הללו .אעיר שאני לא בטוח שאני ממליץ על גישה זאת ,שכן לבצע includeבכל מקרה ,לקבצים בהם אין לנו צורך מעמיס על המהדר ,ומאריך את משך ההידור; גודלה של התכנית הנוצרת בסופו של דבר ,התכנית בשפת מכונה ,לא יגדל ,אולם גם הכבדה שלא לצורך על המהדר אינה דבר ראוי .לכן ,למרות שהרעיון שהצגתי כאן הינו אפשרי ותקין ,אני לא בטוח שהוא גם מומלץ. 17.4חלוקת תכנית לקבצים ,דוגמה א' 17.4.1הקבצים שנכללים בתכנית נדון בתכנית הפשוטה הבאה: 202 //---------- include and using sections -------#include <iostream> using std::cin ; using std::cout ; //-------------- prototypes section -----------void read_int(int &n) ; void print_int(int n) ; int add_int(int n1, int n2) ; int sub_int(int n1, int n2) ; //-------------- main -----------int main() { int num1, num2, result ; read_int(num1) ; read_int(num2) ; result = add_int(num1, num2) ; print_int(result) ; result = sub_int(num1, num2) ; print_int(result) ; return(EXIT_SUCCESS) ; } //-----------------------------------void read_int(int &n) { cin >> n ; } //-----------------------------------void print_int(int n) { cout << n << " " ; } //-----------------------------------int add_int(int n1, int n2) { return n1+n2 ; } //-----------------------------------void sub_int(int n1, int n2) { Return n1 – n2 ; } //------------------------------------ 203 מדובר בתכנית קצרה ופשוטה ,אולם לצורך דיוננו 'נשחק בכאילו'—כאילו מדובר בתכנית גדולה ,ועל כן ברצוננו לחלקה למספר קבצים .נתחיל בכך שנחלק את התכנית לשלושה קבצים: א .קובץ שייקרא ex17g.ccויכלול את התכנית הראשית. ב .קובץ שייקרא io.ccויכלול את הפונ' העוסקות בקלט פלט ) read_intו- (print_int ג .קובץ שייקרא arithmetic.ccויכלול את הפונ' העוסקות באריתמטיקה ) add_intו.(sub_int - החלוקה מלמדת אתכם על האופן בו אנו מחלקים תכנית לקבצים :את אוסף הפונ' העוסקות בהיבט מסוים אנו מרכזים בקובץ אחד. הקובץ , io.ccכאמור ,יכלול את שתי הפונ' read_intו .sub_int -פונ' אלה עושות שימוש בפקודות cin, coutועל כן יש צורך לכלול בקובץ גם את פקודות ה include -וה using -הדרושות .על כן תכולתו של io.ccהינה: >#include <iostream ; using std::cin ; using std::cout //-----------------------------------)void read_int(int &n { ; cin >> n } //-----------------------------------)void print_int(int n { ; " " << cout << n } //------------------------------------ הקובץ arithmetic.ccיכיל את שתי הפונ' האריתמטיות ,פונ' אלה אינן עושות שימוש בפונ' ספריה כלשהן ,ולכן בקובץ זה אין צורך בכל פקודת includeאו .usingתכולתו של arithmetic.ccהינה לפיכך: //-----------------------------------)int add_int(int n1, int n2 { ; return n1+n2 } //-----------------------------------)int sub_int(int n1, int n2 { ; return n1 – n2 } //------------------------------------ בקובץ ex17g.ccשוכנת התכנית הראשית .התכנית הראשית אינה מזמנת פונ' ספריה ,אולם משתמשת ב EXIT_SUCCESS -המוגדר ב .cstdlib -התכנית הראשית מזמנת את ארבע הפונ' שכתבנו ,ועל כן ,על מנת שהמהדר לא 'יצעק' עלינו 204 בעת הידורה ,יש לכלול בה את ההצהרה על הפרוטוטיפים של פונ' הללו .תכולתו של ex17g.ccהינה לפיכך: >#include <cstdlib //-------------- prototypes section -----------; )void read_int(int &n ; )void print_int(int n ; )int add_int(int n1, int n2 ; )int sub_int(int n1, int n2 //-------------- main -----------)(int main { int num1, num2, ; result ; )read_int(num1 ; )read_int(num2 ; )result = add_int(num1, num2 ; )print_int(result ; )result = sub_int(num1, num2 ; )print_int(result ; )return(EXIT_SUCCESS } 17.4.2הידור התכנית עד כאן הכנו את שלושת הקבצים .עתה נשאל :כיצד 'נתפור' את שלושת הקבצים לכדי תכנית שלמה אחת .נציג מספר דרכים לעשות זאת בעזרת המהדר g++ )בסביבת יוניקס(. אפשרות א' ,הפשוטה ביותר ,היא לכתוב: g++ -Wall ex17g.cc io.cc arithmetic.cc –o ex17g בשיטת הידור זאת אנו מציינים בפקודת ההידור את שלושת הקבצים .המהדר יעבור על שלושתם יקמפלם ,ויכרוך אותם לכדי תכנית ניתנת להרצה אחת ,אשר על- פי הוראתנו תשכון בקובץ .ex17g חסרונה של גישה זאת הוא שאם עתה עלינו לשנות רק את אחד הקבצים אנו מקמפלים מחדש גם את שני הקבצים האחרים ,ובכך משקיעים זמן ועבודה מיותרים של המהדר .היינו מעדיפים שאם רק אחד הקבצים השתנה אזי נהדר )במובן הצר של המילה( רק את הקובץ שהשתנה ,ואז נכרוך )נבצע לינקינג( של שלושת הקבצים לכדי executableחדש. כדי להשיג מטרה רצויה זאת ננקוט בגישה הבאה ,שהינה אפשרות ב' לקימפול התכנית .לפי שיטה זאת אנו ,ראשית ,מקמפלים )במובן הצר והמדויק של המילה( כל אחד משלושת הקבצים ,ומקבלים את תרגום הקובץ לשפת מכונה; אנו אומרים שאנו מקבלים קובץ .objectבשלב השני אנו כורכים את שלושת קובצי ה- 205 objectלכדי תכנית שלמה ניתנת להרצה ) g++ .(executableיעשה עבורנו הן את עבודת הקימפול ,והן את עבודת הלינקינג ,והדבר יתבצע באופן הבא :הפקודה: g++ -Wall –c io.cc מפעילה את g++ומורה לו ,באמצעות הדגל –cלהסתפק רק בקומפילציה )במובן הצר של המילה( ,כלומר רק לתרגם את התכנית לשפת מכונה ,בלי להמשיך לשלב הלינקינג ,ולנסות לייצר תכנית ניתנת להרצה )דבר שלא ניתן לעשות ,למשל מפני שאין לנו בקובץ זה כל תכנית ראשית(. באופן דומה ,נקמפל את שני הקבצים האחרים: g++ -Wall –c arithmetic.cc g++ -Wall –c ex17g.cc io.o, שלוש פקודות הידור אלה ייצרו שלושה קובצי :object code ) arithmetic.o, ex17g.oהמהדר בעצמו יודע שעת יש רק לקמפל קובץ, ולייצר ממנו קובץ objectיש לתת לקובץ ה object -אותו שם כמו לקבוץ המקור ,עם סיומת .(o את שלושת קובצי ה object -נכרוך יחד לכדי תכנית שלמה ניתנת להרצה באופן הבא: g++ io.o arithmetic.o ex17g.o –o ex17g שוב אנו עושים שימוש ב ,g++ -אולם הפעם כלינקר )ולא כקומפיילר( .אנו מורים לו לכרוך את שלושת הקבצים io.o arithmetic.o ex17g.o :לכדי תכנית שלמה ,אשר תשכון בקובץ) ex17g :הסיומת –o ex17gמורה שזה יהיה שמו של הקובץ שיכיל את התכנית הניתנת להרצה .לו היינו משמיטים את סיומת ,הייתה התכנית הניתנת להרצה שוכנת תמיד בקובץ ,בעל השם המוזר(.a.out , אם עתה אנו משנים את אחד מקובצי המקור ,אזי נקמפל שוב )במובן הצר של המילה( רק קובץ זה ,ואחר-כך נכרוך יחדיו שוב את שלושת קובצי ה .object -בכך נחסוך לנו את הצורך לקמפל שוב את כל שלושת הקבצים ,עת רק אחד מהם השתנה. עת הפרויקט שלנו כולל שלושה קבצים קטנים ,קל יחסית לעקוב אחר השינויים שאנו עורכים בכל קובץ ,ולקמפל בדיוק את מה שצריך )לא פחות ,ולא יותר( .אולם עת האפליקציה )התכנית( שלנו כוללת מספר גדול של קבצים אזי יקשה עלינו לזכור אילו קבצים השתנו ,ולפיכך איזה קבצים יש לקמפל .כאן באה לעזרתנו פקודת ה- Shellשל יוניקס הנקראת .make פקודת ה make -עושה שימוש בקובץ הנקרא ) makefileזה בדיוק צריך להיות שמו של הקובץ; ללא סיומת( .הקובץ makefileמתאר ,לפקודת ה ,make -את תהליך ההידור שעליה לבצע .אציג כאן צורה בסיסית ביותר לבניית קובץ ה- ,makefileהמתעניינים מוזמנים ,כמובן ,להרחיב את ידיעותיהם בנושא. 206 תכולת הקובץ היא: ex17g: ex17g.o io.o arithmetic.o g++ ex17g.o io.o arithmetic.o –o ex17g "echo "a new ex17g was created ex17g ex17g.o: ex17g.cc g++ -Wall –c ex17g.cc io.o: io.cc g++ -Wall –c io.cc arithmetic.o: arithmetic.cc g++ -Wall –c arithmetic.cc אדגיש כי בשורות שאינן מתחילות בעמודה הראשונה )כדוגמת השורה השנייה, השלישית ,הרביעית ,השישית וכולי( יש להקיש על מקש ה tab -בתחילת השורה )ולא על מקשי רווח(. נסביר את הקובץ :כאמור ,הקובץ הנ"ל הינו הקלט לפקודת ה .make -הפקודה עוברת על הקובץ משורתו הראשונה ב'-רברס' .היא מתחילה בקובץ ex17gכפי שמופיע משמאל לנקודותיים בשורה הראשונה .מימין לנקודותיים מתוארים )שלושת( הקבצים בהם קובץ זה תלוי—אם אחד הקבצים הללו עודכן במועד מאוחר יותר מהמועד בו עודכן ex17gאזי יש לבצע את הפקודות המופיעות בשלוש השורות הבאות )שכל אחת מהן מתחילה ,כאמור ,ב .(tab -שלוש הפקודות הן ראשית לינקינג של שלושת קובצי ה object -לכדי תכנית ניתנת להרצה ,ומעבר לכך ,כדי לסבר את העין ,הוספנו שיש לעשות עוד שני דברים :לשלוח למסך פלט האומר ,a new ex17g was createdולסיום גם להריץ את התכנית .בד"כ, קובץ ה makefile -לא יכיל הרצה של התכנית ,וגם הודעה למסך ספק אם יש צורך להוציא. כאמור ,פקודת ה make -מתקדמת על הקובץ מראשיתו כלפי סופו :אחרי שהיא גילתה ש ex17g :תלוי ב ex17g.o io.o arithmetic.o :היא עוברת לבדוק עבור כל אחד ואחד משלושת הקבצים הללו במי הוא תלוי .כך היא מגלה ,למשל, שהקובץ io.oתלוי ב) io.cc -זאת מורה לה השורה (io.o: io.cc :ועל כן אם io.ccמאוחר משל io.oאזי יש לבצע: תאריך עדכונו של .g++ -Wall –c io.cc 207 לכן פקודת ה make -ראשית תהדר את io.ccותייצר io.oחדש ,ורק שנית תכרוך את io.oהחדש עם קובצי האובג'קט האחרים לכדי executableחדש. כלומר ,למרות שבקובץ פקודת הלינקינג מופיעה בשורה קודמת לפקודת ההידור, ַבפועל ,בשל ההתקדמות 'ברברס' ,פקודת ההידור תבוצע לפני פקודת הכריכה. פקודת ה make -למעשה מייצרת מעין 'עץ תלויות' שבמקרה שלנו נראה באופן הבא: ex17g g++ ex17g.o io.o arithmetic.o –o ex17g arithmetic.o g++ -Wall –c arithmetic.cc arithmetic.c ex17g.o o.io g++ -Wall –c io.cc io.c g++ -Wall –c ex17g.cc ex17g.c שורש העץ הוא התכנית שיש לייצר ,כפי שמופיעה משמאל לנקודותיים בשורה הראשונה .חץ מצומת אחד לשני מעיד שהצומת השני תלוי בצומת הראשון ,ואם הצומת הראשון עדכני יותר אזי יש לעדכן גם את הצומת השני .הפעולה שיש לבצע כתובה לצד החץ או החיצים .פקודת ה make -בונה את העץ מהשורש כלפי העלים, אך מבצעת את הפעולות מהעלים כלפי השורש )כפי שהגיוני לעשות(. 17.4.3שימוש בקובצי כותר )(header נמשיך בחלוקת התכנית שלנו לקבצים .כאמור ,אחת המטרות של חלוקת התכנית לקבצים היא שאם בהמשך נרצה לכתוב תכנית הזקוקה לפונקציות המתמטיות שכתבנו ) (add_int, sub_intנוכל לצרף פונקציות אלה בנקל לאותה תכנית ,ע"י שנכרוך את הקובץ add_sub.oעם קובצי האובג'קט האחרים שירכיבו את אותה תכנית .נניח שבאותה תכנית קיים גם הקובץ .prog.ccנניח שבקובץ זה ברצוננו לזמן את הפונ' .add_int, sub_int :על כן עלינו להצהיר על הפרוטוטיפ שלהן באותו קובץ. כדי להקל עלינו בכך ,נהוג לצרף לכל קובץ מקור ,הכולל הגדרה של פונ' )כדוגמת (add_sub.ccגם קובץ כותר אשר כולל הצהרות על הפרוטוטיפים של הפונקציות הנכללות בקובץ המקור) .נהוג ששמו של קובץ הכותר מסתיים ב, .h :האות hהיא עבור.(header : הדוגמה שלנו ,כוללת את קובץ המקור :io.cc >#include <iostream ; using std::cin 208 using std::cout ; //-----------------------------------void read_int(int &n) { cin >> n ; } //-----------------------------------void print_int(int n) { cout << n << " " ; } //------------------------------------ : שיראהio.h עתה נכלול בתכנית גם את קובץ הכותר void read_int(int &n) ; void print_int(int n) ; עתה נוסיף.arithmetic.cc התכנית שלנו כוללת את קובץ המקור,באופן דומה :arithmetic.h לתכנית את קובץ הכותר int add_int(int n1, int n2) ; int sub_int(int n1, int n2) ; כבר לא תמנה את הפרוטוטיפים של ארבע הפונ' הללו באופן,התכנית הראשית : התכנית הראשית תראה. אלא תכליל את קובצי הכותר המתאימים,מפורש #include <cstdlib> #include "io.h" #include "arithmetic.h" //-------------- main -----------int main() { int num1, num2, result ; read_int(num1) ; read_int(num2) ; result = add_int(num1, num2) ; print_int(result) ; result = sub_int(num1, num2) ; print_int(result) ; return(EXIT_SUCCESS) ; } ( יתבצעmake באופן ישיר )שלא באמצעות,הידור התכנית במתכונתה הנוכחית :בדיוק כמו קודם; דבר לא משתנה g++ -Wall –c io.cc g++ -Wall –c arithmetic.cc g++ -Wall –c ex17g.cc: 209 g++ io.o arithmetic.o ex17g.o –o ex17g עת אנו משתמשים בפקודת ה make -יש לתקן את קובץ ה makefile -כך שהוא יורה שגם עת חל שינוי בקובץ כותר כלשהו יש לקמפל את קובצי המקור אליהם קובץ הכותר מתייחס )למשל ,אם חל שינוי ב io.h -אזי יש לקמפל את ,(io.cc וכן יש לקמפל את הקבצים בהם נכלל קובץ הכותר המתאים )בדוגמה שלנו ,עת חל שינוי ב io.h -יש לקמפל גם את ex17g.ccהכולל אותו( .לכן קובץ ה- makefileיראה: ex17g: ex17g.o io.o arithmetic.o g++ ex17g.o io.o arithmetic.o –o ex17g "echo "a new ex17g was created ex17g ex17g.o: ex17g.cc io.h arithmetic.h g++ -Wall –c ex17g.cc io.o: io.cc io.h g++ -Wall –c io.cc arithmetic.o: arithmetic.cc arithmetic.h g++ -Wall –c arithmetic.cc נסביר :שינוי ב io.h -מעיד על שינוי בפרוטוטיפים של הפונ' הממומשות ב- ,io.cעל כן יש לקמפל קובץ זה מחדש .באופן דומה ,שינוי בקובץ זה אשר נכלל בקובץ ex17g.ccמשמעו ,מבחינת המהדר ,שינוי בקובץ ,ex17g.ccולכן גם את הקובץ האחרון יש לקמפל .כמובן שקימפול כל אחד מהקבצים הללו יביא גם לכריכה מחדש של כלל קובצי האובג'קט לכדי תכנית ניתנת להרצה חדשה. 17.5חלוקת תכנית לקבצים ,דוגמה ב' :תרומתה של הנחיית המהדר ifndef נניח כי עתה עלינו לכתוב תכנית המטפלת בנקודות ,ובמלבנים אשר צלעותיהם מקבילות לצירים .לשם כך ,בפרט עלינו להגדיר את המבנים הבאים: { struct Point ; double _x, _y } נקודה אנו מייצגים באמצעות הקואורדינאטות שלה ,ומלבן באמצעות פינתו השמאלית העליונה ,ופינתו הימנית התחתונה: { struct Rectangle ; struct Point _top_left, _bottom_right } כמו כן ,יש להגדיר פונ' שונות אשר פועלות על נקודות ועל מלבנים. מעבר לאלה תהייה לנו תכנית ראשית אשר תשתמש בנקודות ובמלבנים. במצב זה התכנית שלנו תורכב מחמשת הקבצים הבאים: א .קובץ כותר בשם Point.hאשר כולל את הגדרת המבנה ,Pointואת ההצהרה על הפונ' הפועלות על נקודות. ב .קובץ מקור בשם Point.ccהכולל את מימוש הפונ' אשר עליהן הצהרנו ב: .Point.hמכיוון שהפונ' עושות שימוש בהגדרת המבנה Pointאזי בקובץ זה נכלול )בין היתר( את הקובץ Point.h 210 ג .קובץ כותר בשם Rectangle.hהכולל את הגדרת המבנה ,Rectangleואת ההצהרה על הפונ' הפועלות על מלבנים .מכיוון שהגדרת מלבן ,כפי שראינו ,מסתמכת על הגדרת נקודה אזי קובץ זה יכלול את הקובץ .Point.h ד .קובץ מקור בשם Rectangle.ccהכולל את מימוש הפונ' עליהן הצהרנו ב: .Rectangle.hמכיוון שהפונ' עושות שימוש בהגדרת המבנה Rectangleאזי בקובץ זה נכלול )בין היתר( את הקובץ .Rectangle.h ה .קובץ בשם my_prog.ccבו תשכון התכנית הראשית .מכיוון שהתכנית הראשית תגדיר משתנים מהטיפוסים Point, Rectangleאזי עלינו לכלול בקובץ my_prog.cc את Point.hואת .Rectangle.hנעיר כאן שמישהו עשוי לטעון ,ובצדק ,שאין צורך לכלול בקובץ זה גם את Point.hשכן Rectangle.hכבר מכליל בתוכו קובץ זה .זה נכון ,אולם פעמים רבות מתכנתים שונים כותבים חלקים שונים בתכנית; )כפי שעוד נרחיב בהמשך( המתכנת שמקודד את התכנית הראשית עשוי שלא לדעת שמלבן מיוצג באמצעות שתי נקודות ,ועל כן ש Rectangle.h -כבר הכליל את .Point.hהמתכנת שכותב את התכנית הראשית יודע שהוא משתמש הן בנקודות והן במלבנים ,ועל כן הוא ,באופן טבעי ,מכליל את שני קובצי הכותר .בבעיות שהדבר גורם ,והפתרונות להן ,נדון בהמשך) .למעשה הבעיות והפתרונות כבר נסקרו עד דנו בהנחיית המהדר (.#ifndef נציג עתה את הקבצים השונים .אתחיל בכך שאציג את הקבצים כפי שמתכנת שלמד את החומר עד כה ,אך לא הפנים את נושא ה ,#ifndef -היה כותב אותם .אחר כך אסביר את בעיית הקומפילציה שמתעוררת עת כותבים את התכנית באופן זה, ולבסוף אסביר מה יש להוסיף לתכנית על מנת להתגבר על הבעיה. 17.5.1הקובץ Point.h כפי שאמרנו ,נניח שאנו מגדירים נקודה באופן הבא: { struct Point ; double _x, _y } עוד נניח שעל נקודה אנו מגדירים את הפעולות הבאות: א .קריאת נקודה מהמשתמש. ב .הדפסת נקודה לפלט הסטנדרטי.. ג .החזרת הרביע בו מצויה הנקודה. ד .החזרת המרחק בין שתי נקודות. כל אחת ,כמובן ,תמומש באמצעות פונ'. לכן הקובץ Point.hיראה באופן הבא: { struct Point ; double _x, _y } ; )void read_point(struct Point &p ; )void print_point(const struct Point &p ; )int quarter((const struct Point &p double distance(const struct Point & p1, ; )const struct Point &p2 אעיר שעת פונ' אמורה רק להשתמש בנקודה כלשהי ,אך לא לשנותה ,אנו מעבירים את הפרמטר כפרמטר הפניה קבוע ) .(const struct Point &pאנו לא מעבירים את המבנה כפרמטר ערך כדי לחסוך את הזמן והזיכרון הדרושים לשם העתקת המבנה, 211 העתקה שהייתה נדרשת לו הפרמטר היה פרמטר ערך .דנו בכך בהקשר לש מבנים, ולא ארחיב על כך כאן. 17.5.2הקובץ Point.cc הקובץ Point.ccכולל ,כאמור ,את המימוש של הפונ' המטפלות בנקודות ,פונ' עליהם הצהרנו בקובץ .Point. hעל כן הוא יכלול למשל את הפונ': )void read_point(struct Point &p { ; cin >> p. _x >> p._y } //-----------------------------------)void print_point(const struct Point &p { ; ')' << cout << '(' << p._x << ", " << p._y } על מנת שקימפולו של קובץ זה יעבור בהצלחה על הקומפיילר להכיר את הפרמטר של הפונ' ,מטיפוס struct Pointהגדרת המבנה Pointמצויה בקובץ Point.hעל-כן יש לכלול קובץ זה בקובץ .Point.ccמכיוון שהפונ' read_pointכוללת פקודת , cinוהפונ' print_pointכוללת פקודת ,cout אזי יש לכלול גם את iostreamבקובץ .הכללתו יכולה להתבצע מפורשות בקובץ Point.ccאו בקובץ הכותר Point.hהמוכלל ב .Point.cc -כמובן ,במידה ורוצים בכך יש צורך בפקודות .using באותו אופן ,נממש בקובץ Point.ccאת הפונ' האחרות. כפי שתיארתי את קובץ הכותר נתקן לכדי: >#include <iostream >#include <cmath ; using std::cin ; using std::cout { struct Point ; double _x, _y } ; )void read_point(struct Point &p ; )void print_point(const struct Point &p ; )int quarter((const struct Point &p double distance(const struct Point & p1, ; )const struct Point &p2 הקובץ Point.ccיראה: "#include "Point.h //-----------------------------)void read_point(struct Point &p { 212 cin >> p. _x >> p._y ; } //-----------------------------void print_point(const struct Point &p) { cout << '(' << p._x << ", " << p._y << ')' ; } //---------------------------------... implementation of the other two functions Rectangle.h הקובץ17.5.3 נניח שעבור מלבן אנו.עתה נטפל במלבן באופן דומה לדרך בה נהגנו עם נקודה :מגדירים את הפעולות . קריאת נתונים מלבן מהמשתמש.א . הדפסת נתוני מלבן למשתמש.ב . חישוב והחזרת שטח מלבן.ג . חישוב והחזרת היקף מלבן.ד . בדיקה האם נקודה מצויה בתוך מלבן.ה :קובץ הכותר ייראה #include <iostream> #include "Point.h" using std::cin ; using std::cin ; struct Rectangle { struct Point _top_left, _bottom_right ; } void read_rectangle(struct Rectangle &r) ; void print_rectangle(const struct Rectangle &r) ; double rectangle_area(const struct Rectangle &r) ; double rectangle_circumference( const struct Rectangle &r) ; bool is_point_in_rect(const struct Point &p, const struct Rectangle &r) ; שכן מלבן מוגדרת תוך שימושPoint.h אתRectangle.h אנו מכלילים בקובץ בעת שהוא מקמפל אתstruct Point ועל כן על הקומפיילר להכיר את,בנקודה )למרות שהוא מוכלל כבר ע"יiostream אנו מכלילים גם את.הקוד הקשור למלבן ( מתוך הנחה שהמתכנת שכותב את הקוד הקשור למלבן הוא מתכנתPoint.h 213 והוא אינו יודע מה כלל עמיתו בקוד,אחר מזה שכותב את הקוד הקשור לנקודה הוא מכלילוiostream - מכיוון שהמתכנת שכותב את קוד של מלבן זקוק ל.שלו .בקובץ הכותר שלו בעצמו Rectangle.cc הקובץ17.5.4 :Point.cc - יהיה דומה במבנהו לRectangle.cc הקובץ #include "rectangle.h" //----------------------------------------------void read_rectangle(struct Rectangle &r) { read_point(r._top_left) ; read_point(r._bottom_right) ; } //----------------------------------------------void print_rectangle(const struct Rectangle &r) { cout << "[ " ' print_point(r._top_left) ; cout << " – " ; print_point(r._bottom_right) ; cout << " ]" ; } //----------------------------------------------... the other three functions my_prog.cc הקובץ17.5.5 אשר עושה שימוש בנקודות, יכיל את התכנית הראשיתmy_prog.cc הקובץ תחת ההנחה שהמתכנת שכתב אותו אינו יודע מה הכלילו עמיתיו, על כן.ובמלבנים Point.h, קובץ זה צריך לכלול בתוכו את שני קובצי הכותר,בקבצים שהם כתבו .פי הצורך- על, ואולי קובצי כותר אחרים,Rectangle.h :הקובץ יראה באופן הבא #include <iostream> #include "Point.h" #include "Rectangle.h" using std::cout ; //----------------------------------------int main() { struct Point p1, p2 ; struct Rectangle r ; read_point(p1) ; read_point(p2) ; 214 ; )read_rectangle(r )if (distance(p1, p2) == 0 ; "cout << "It is the same point\n ))if (is_point_in_rect(p1, r ; "cout << "p1 is in r\n ... } 17.5.6בעיות ההידור המתעוררות והתיקונים הדרושים בעזרת #ifndef נניח שעתה ברצוננו להדר )לקמפל( את התכנית על מנת לקבל קובץ ניתן להרצה. בפרט יהיה עלינו להדר )במובן הצר של המילה( את הקובץ .my_prog.ccפקודת ההידור היא: g++ -Wall –c my_prog.cc נבחן מה יקרה בתהליך ההידור ,ואיזה בעיה תתעורר :כזכור לנו ,ההידור מתחיל בהפעלת קדם המהדר ,אשר 'שותל' את הקבצים המוכללים במקום בו הם מוכללים .על-כן: א .ראשית' ,ישתול' קדם המהדר את iostreamבקובץ שלנו; ב .שנית ,הוא ישתול את ,Point.hועל-כן הגדרת struct Pointתוכנס לקובץ ,my_prog.ccוטוב שכך ,שכן אחרת המהדר )המרכזי( בשלב ההידור העיקרי לא יכיר את ,struct Pointו'-יצעק' עלינו ,כלומר יוציא הודעת שגיאה. ג .שלישית ,הוא ייגש 'לשתול' את Rectangle.hבקובץ הנוכחי Rectangle.h .כולל בתוכו את ההנחיה " #include "Point.hמה שיגרום לקדם מהדר 'לשתול' שוב את Point.hבקובץ שלנו ,ואחר-כך 'לשתול' את יתר הקובץ Rectangle.hבקובץ my_prog.cc התוצאה :הגדרת struct Pointמופיעה בקובץ my_prog.ccפעמיים .אומנם בשתי הפעמים זו בדיוק אותה הגדרה ,אך ,כאמור ,היא מצויה בקובץ פעמיים .עת המהדר )המרכזי( ייתקל פעמיים בהגדרת המבנה הוא 'יצעק' עלינו שזו שגיאה; וזה כמובן רע מאוד. כיצד נתקן את התקלה ,ונאפשר לתכניתנו להתקמפל כהלכה? הכלי שיעזור לנו בכך הוא הנחיית הקדם מהדר .#ifndef 215 : ואחר אסביר את התיקון,אציג את קובצי הכותר המעודכנים The file: Point.h #ifndef POINT #define POINT struct Point { double _x, _y ; } void read_point(struct Point &p) ; void print_point(const struct Point &p) ; int quarter((const struct Point &p) ; double distance(const struct Point & p1, const struct Point &p2) ; #endif The file: Rectangle.h #ifndef RECTANGLE #define RECTANGLE #include <iostream> #include "Point.h" using std::cin ; using std::cin ; struct Rectangle { struct Point _top_left, _bottom_right ; } void read_rectangle(struct Rectangle &r) ; void print_rectangle(const struct Rectangle &r) ; double rectangle_area(const struct Rectangle &r) ; double rectangle_circumference( const struct Rectangle &r) ; bool is_point_in_rect(const struct Point &p, const struct Rectangle &r) ; #endif . #ifndef : 'הגנו' על כל אחד מקובצי הכותר באמצעות השאלה:כפי שניתן לראות עת הוא עובר על, תוך שאנו חוזרים על פעולתו של קדם המהדר,אסביר ביתר פירוט : עבור הקבצים המתוקניםmy_prog.cc הקובץ כאן לא.(my_prog.cc) בקובץ שלנוiostream 'ישתול' קדם המהדר את, ראשית.א .ifndef בה לא הוספנו עדיין את ההנחיות,חל כל שינוי יחסית לדוגמה הקודמת : כלומר,#ifndef POINT : בפרט הוא יישאל.Point.h הוא ייגש לשתול את, שנית.ב ? התשובה תהיהPOINT לא הוגדר לי הסימן, בהרצה נוכחית שלי,האם עד כה ייכלל, כלומר בדוגמה שלנו עד סוף הקובץ,endif - ועל כן כל מה שמופיע עד ה,כן struct הגדרת, בפרט.(בפלט של קדם המהדר )ולכן יקומפל ע"י המהדר המרכזי מגדיר#define POINT : בשורה, מעבר לכך.my_prog.cc תוכנס לקובץPoint .לעצמו קדם המהדר סימן זה 216 ג .שלישית ,הוא ייגש 'לשתול' את Rectangle.hבקובץ הנוכחי Rectangle.h .כולל בתוכו את ההנחיה " #include "Point.hמה שיגרום לקדם מהדר לפנות שוב ל- Point.hעל מנת להכלילו בקובץ .my_prog.ccשוב ישאל עצמו קדם המהדר :האם עד כה ,בהרצה נוכחית שלי ,לא הוגדר לי הסימן ? POINTהפעם ,התשובה תהיה לא ,שכן בסעיף ב' שמעל כבר הגדיר לעצמו קדם המהדר את הסימן הזה ,על כן כל מה שמופיע עד ה #endif -לא 'יישתל' בקובץ שלנו ,בפרט הגדרת struct Point לא תופיע פעמיים ,וההידור יעבור בהצלחה )לכל הפחות מהיבט זה( .לא התייחסתי בדברי לשאלה המקבילה שקדם המהדר שואל: #ifndef ? RECTANGLEשכן הדיון בה זהה לזה שערכנו עבור .POINTבתכנית הנוכחית הגנה זאת מפני הכללה כפולה של הגדרת struct Rectangleאינה חיונית ,ולכן בתכנית הנוכחית יכולנו לוותר על ההנחיות#ifndef RECTANGLE, #define : . RECTANGLE, #endifאנו כותבים אותן לטובת מקרה בן בעתיד מישהו עשוי לכלול בתכניתו את rectangle.hיותר מפעם אחת. 17.5.7קובץ ה makefile -לפרויקט קובץ ה makefile -לבניית תכנית ניתנת להרצה עבור הקבצים שלנו דומה בעיקרון לזה שראינו בדוגמה הקודמת: my_prog: my_prog.o Point.o Rectangle.o g++ my_prog.o Point.o Rectangle.o –o my_prog my_prog.o: my_prog.cc Point.h Rectangle.h g++ -Wall –c my_prog.cc Rectangle.o: Rectangle.cc Rectangle.h Point.h g++ -Wall –c Rectangle.cc Point.o: Point.cc Point.h g++ -Wall –c Point.cc נסביר: Rectangle.oתלוי כמובן ב .Rectangle.cc :מכיוון ש Rectangle.cc :כולל בתוכו את שני קובצי הכותר Rectangle.h Point.hאזי לצורך הקומפילציה )המרכזית( שני קבצים אלה הם חלק מ Rectangle.cc :ולכן Rectangle.oתלוי גם בהם .דין דומה חל על .my_prog.oכמובן שה ,executable -הקובץ ,my_progתלוי בשלושת קובצי ה- objectולכן אם אחד מהם עדכני יותר ממנו יש לבצע לינקינג מחדש לשלושת הקבצים על מנת לקבל תכנית ניתנת להרצה חדשה. 17.6קבועים גלובליים נניח שאנו כותבים תכנית המורכבת מכמה קבצים .לשם הפשטות נניח ששמות הקבצים הם .f1.cc, f2.ccעוד נניח כי ברצוננו להגדיר קבועים בהם נעשה שימוש. אם נגדיר בכל אחד משני הקבצים את הקבועים: ; const int N = 1 אזי הקבוע Nיוכר רק בקובץ בו הוא מוגדר .לכן ,מבחינת תקינות התכנית ,נוכל להגדיר בקובץ f1.ccאת הקבוע: ; const int N = 1 ובקובץ f2.ccאת הקבוע: ; const int N = 2 217 ומבחינת הקומפיילר לא תהיה בכך כל בעיה )מבחינת הסגנון התכנותי זה עלול להיות מבלבל ,אך זו סוגיה אחרת( .כמובן בכל קובץ עת יתעניינו בערכו של N יתקבל הערך שהוגדר באותו קובץ. אולם ישנם מצבים בהם ברצוננו שאותו קבוע ,אשר יוגדר פעם אחת בלבד ,יוכר בקבצים שונים .כיצד נשיג אפקט זה? התשובה היא שבקובץ מקור אחד בלבד )קובץ , .ccלא קובץ כותר( נגדירו באופן: ; extern const int N = 1 בקובצי המקור האחרים ,או בקובץ כותר אותו מכללים בקובצי מקור אחרים נצהיר עליו באופן: ; extern const int N כלומר בלי לקבוע את ערכו. מילת המפתח externבקובץ בו הקבוע הוגדר ,ונקבע לו ערך ,אומרת שעליו להיות מוכר בכל הקבצים המרכיבים את התכנית )ולא רק בקובץ בו הוא מוגדר ,כפי שקורה עת לא כותבים את מילת המפתח .(extern ההצהרה extern const int N ; :מורה למהדר שהקבוע הוגדר ,ונקבע לו ערך ,בקובץ אחר ,וללינקר היא מורה שעליו לאתר את ערכו של הקבוע עת הוא כורך את קובצי האובג'קט השונים לכדי תכנית שלמה אחת. נראה דוגמה קטנה ביותר: הקובץ f1.ccהינו: >#include <iostream ; extern const int N = 1 ; )(void f { )(int main ; " " << std::cout << N ; )(f ; return 0 } הקובץ f2.ccהינו: >#include <iostream ; extern const int N { )(void f ; std::cout << N } 218 17.7מבוא לתכנות מונחה עצמים :עקרון הכימוס בדוגמה האחרונה ראינו תכנית אשר משתמשת בנקודות ובמלבנים .לטיפוסי משתנים של מבנים )כדוגמת נקודות ומלבנים( אנו קוראים לעתים אובייקטים .אחד העקרונות התכנותיים המקובלים ביותר ,עת תכנית משתמשת באובייקט ,כדוגמת נקודה או מלבן ,הוא עיקרון הכימוס ) .(encapsulationעקרון הכימוס קובע שהתכנית הראשית כלל לא צריכה לדעת כיצד המתכנת שהגדיר את struct Pointבחר לייצג נקודה .האם הוא עושה זאת באמצעות קואורדינאטת ה x -וה y -של הנקודה )כפי שאכן עשינו בדגומה מעל( ,כלומר בייצוג קרטזי? או שמא הוא בחר לייצג נקודה באמצעות המרחק שלה מראשית הצירים )מה שקרוי הרדיוס של הנקודה( ,והזווית בין הישר המחבר אותה לראשית הצירים ,לבין הכיוון החיובי של ציר ה) x -מה שקרוי זווית אלפה של הנקודה( ,כלומר בייצוג קוטבי? לפי עקרון הכימוס ,התכנית הראשית ,אמורה לקבל אוסף של פעולות על נקודה ולהשתמש בהן ,ורק בהן ,עת היא מגדירה משתנים מטיפוס נקודה .כלומר ,התכנית הראשית לא תוכל לבצע פעולה כגון) p. _x = 0; :עבור נקודה ,(pשכן התכנית הראשית כלל לא אמורה לדעת שנקודה מיוצגת באמצעות קואורדינאטת ה x -שלה .כיצד אם כן תוכל התכנית הראשית לבצע פעולה פשוטה ובסיסית זאת? התשובה היא :בעזרת פונקציות שהמתכנת שכתב את struct Pointהעמיד לרשות התכנית הראשית ,למשל באמצעות פעולה . set_x(p, 0) :באותו אופן ,בתכנית הראשית נוכל לכתוב set_radius(p, 1) :בלי להתעניין כיצד בפועל מיוצגת נקודה; וזאת ,כאמור ,מתוך הנחה שהמתכנת שכתב את הקוד של נקודה העמיד לרשותנו את שתי הפונ' הללו. באופן דומה ,התכנית הראשית לא תוכל לכלול פקודה ,cout << p._x :אלא היא תכלול פקודה cout << get_x(p) :או פקודה cout << get_radius(x) :ובאחריות המתכנת שכתב את הקוד של נקודה להעמיד לרשות התכנית הראשית את שתי הפונ' הללו. על כן ,סביר להניח שעבור נקודה ,שנניח שהוגדרה כפי שהדגמנו קודם: { struct Point ; double _x, _y } תסופקנה לנו ,מעבר לפעולות שראינו )החזרת הרבע בו מצויה נקודה ,החזרת מרחק בין שתי נקודות( גם הפעולות: )void set_x(struct Point &p, double x } ; { p. _x = x )void set_y(struct Point &p, double y } ;{ p. _x = y void set_radius_and_alpha(struct Point &p, )double r, double alpha ;{ p. _x = r / cos(alpha p._y = r/ sin(alpa} ; : )double get_x(const struct Point &p } ; { return p._x )double get_y(const struct Point &p } ; { return p._y 219 )double get_raduis(const struct Point &p } ; ){ return sqrt(p._x*p._x + p._y*p._y )double get_alpha(const struct Point &p } ; ){ return atan(y/x התכנית הראשית ,תוכל להגדיר משתנה: ; struct Point p כדי להציב את הנקודה בראשית הצירים תבצע התכנית הראשית: ; )set_x(p, 0 ; )set_y(p, 0 וכדי לבדוק האם הנקודה נמצאת על הישר y = xתשאל התכנית הראשית: ))if (get_x(p) == get_y(p ... על דרך השלילה ,התכנית הראשית לא תוכל לבצע את הפעולות הבאות: ; p._x = 0 )if (p._x == p._y ... 17.8יצירת ספריה סטאטית נחזור לשני זוגות הקבצים ששימשו אותנו בדוגמה הראשונה בה חילקנו את תכניתנו לקבצים. io.h, io.cc, arithmetic.h, arithmetic.cc : נניח שאנו סוברים כי הפונ' שכתבנו בקבצים אלה מאוד שימושיות ,ועל כן נרצה להשתמש בהן בתכניות רבות שנכתוב בעתיד .כדי לייעל את תהליך הקומפילציה של אותן תכניות נרצה לצור משני הקבצים הללו ספריה ) ,(libraryולא רק לכרוך את שני הקבצים הללו עם תכניות עתידיות שנכתוב ,כפי שעשינו בדוגמות הקודמות. יצירת ספריה עדיפה על-פני כריכת הקבצים io.o arithmetic.oעם התכנית המזמנת את הפונ' הנכללות באותם קבצים שכן היא מקצרת את זמן ההידור. בתכניות שלכם ,ההידוק לוקח הרף עין ,אולם בתכניות גדולות ההידור עלול לקחת זמן רב )סדר גודל של דקות ואפילו שעות( ,ועל כן המוטיבציה לקצרו רבה. הסיבה לכך שמשך ההידור מתקצר היא שהלינקר צריך לפתוח פחות קבצים )קובץ ספריה יחיד ,במקום שני קובצי אובג'קט( .לקובץ הספרייה יש בדרך כלל אינדקס, ולכן נקל לאתר בו פונ' אותה יש לכרוך עם התכנית. בסעיף זה נלמד כיצד יוצרים ספריה סטאטית .תכונתה של ספריה סטאטית היא שבזמן הכריכה הלינקר שולף מהספרייה את קטעי הקוד הדרושים ,ומצרף אותם לקובץ הניתן להרצה שהוא יוצר .עת התכנית רצה כבר אין יותר פניה לקובץ הספרייה ,ולכן ,למשל ,ניתן להעביר את הקובץ הניתן להרצה למחשב אחר ,בלי להעביר איתו את קובץ הספרייה. יצירת הספרייה מתחילה ,כמובן בקימפול שני הקבצים ,וקבלת קובצי אובג'קט: g++ -Wall –c add_sub.cc g++ -Wall –c io.cc 220 עתה ,כדי לצור משני קובצי האובג'קט הללו קובץ ספריה ,נשתמש בפקודת ה- Shellשל יוניקס הקרויה ar) .arהוא קיצור של archiveאו קובץ ארכיב(. אופן השימוש בפקודה: ar rc libadd_sub_io.a add_sub.o io.o נסביר: א .כאמור הפקודה אותה אנו מפעילים היא הפקודה .ar ב .זוג האותיות rcהנכתבות מייד אחרי שם הפקודה הן קיצור שלreaplce : createוהן מורות למערכת ההפעלה לייצר את הספרייה אם היא אינה קיימת, או להחליף את הספרייה הקיימת אם כבר הייתה אחת כזאת. ג .המרכיב השלישי libadd_sub_io.a :מציין את שם הספרייה שתווצר .שם ספרייה חייב להתחיל בשלוש האותיות ,libולהסתיים בזוג התווים) .a :התו aעומד בשביל.(archive : ד .המשך הפקודה מציין את שמתו הקבצים שנכלול בספרייה .במקרה שלנו אלה הקבצים .add_sub.o io.o קובץ הספרייה שנוצר הוא חסר אינדקס .אינדקס בקובץ ספריה ,כמו אינדקס המוכר לכם מספר ,עוזר לאתר במהירות פונ' רצויה בקובץ הספרייה )כפי שהוא עוזר לאתר במהירות נושא רצוי בספר( .כדי לייצר אינדקס לקובץ הספרייה )אינדקס שיכלל בקובץ הספרייה עצמו( ניתן להריץ את פקודת ה ar -באופן הבא: ar rcs lib_add_sub_io.a add_sub.o io.o הדגל s :מורה שיש לייצר את הספרייה עם אינדקס. לחילופין :ניתן להוסיף לספריה הקיימת אינדקס באמצעות הפקודה: ranlib libadd_sub_io.a הפקודהnm –s libadd_sub_io.a : מציגה לכם את תכולתו של האינדקס שנוצר. 17.9יצירת ספריה דינאמית )משותפת( עת יצרנו ספריה סטאטית הלינקר ,בשלב הכריכה ,צירף לתכנית שלנו את הפונקציות אותן זימנו מתוך הספרייה הסטאטית ,כך שהתקבל קובץ ניתן להרצה ) (executableשכבר אינו תלוי בספרייה .לדוגמה :אם נעביר קובץ זה למחשב אחר )כמובן אחד עם אותה שפת מכונה( ,התכנית תוכל לרוץ ַבמחשב האחר ,גם אם לא נעביר ַלמחשב האחר את קובץ הספרייה .צדו השני של המטבע :אם במחשב שלנו רצות תכניות רבות שנכרכו באופן סטאטי עם אותה פונ' ספרייה )לדוגמה: ,(cinאזי כל אחת מהן גדלה בנפחה בשיעור הנפח של אותה פונ' ספרייה .מכיוון שהזיכרון הראשי הוא משאב מוגבל ,אזי יש טעם לפגם בכך שאנו מגדילים כל אחת ואחת מהתכניות באותו קוד )לדוגמה :בפונ' .(cinעל כן ,במצבים בהם פונ' הספרייה נכרכת לתכניות רבות ,נעדיף לכרכה באופן שקוד הפונ' לא ייכרך עם כל קובץ הרצה בנפרד ,נעדיף שקוד הפונ' יוחזק פעם יחידה בזיכרון ,עבור כלל התכניות הזקוקות לקוד זה .ספרייה דינאמית היא הכלי שיעזור לנו להשיג זאת. עת אנו יוצרים ספרייה דינאמית ,במלים אחרות ספרייה משותפת ,הלינקר ,בשלב הכריכה ,לא מצרף את קוד הפונ' הדרושה לקובץ הניתן להרצה שלנו ,אלא שותל בקובץ רק בדל ) ,(stubכלומר מעין 'קצה חוט' ,אשר יאפשר ,בעת ריצת התכנית, לזמן פונ' שאינה חלק מקובץ התכנית שלנו ,פונ' שמצויה בספרייה דינאמית אשר שוכנת בזיכרון פעם יחידה עבור כל התכניות שמריצות אותו קוד .לדוגמה :בהנחה ש cin -שוכנת בספרייה דינאמית ,הקוד של cinנמצא בזיכרון פעם יחידה בלבד. 221 עת תכנית א' מזמנת את הפונ' פונים להריץ את קוד הפונ' ,כפי שמצוי בזיכרון )גם אם לא בקובץ התכנית הניתנת להרצה של תכנית א'(; עת תכנית ב' מזמנת את הפונ' פונים לאותו קוד ,אשר כאמור מצוי בזיכרון פעם יחידה .התוצאה :מצד אחד קובץ התכנית )ה (executable -שלנו כבר לא עצמאי ,הוא חייב שבזיכרון יהיה גם קובץ הספרייה עת התכנית רצה; אולם מצד שני ,גם אם תכניות רבות הרצות במקביל, ומצויות בזיכרון המחשב ,כוללות את הפונ' ,הפונ' נמצאת בזיכרון רק פעם אחת, וכך נחסך בזבוז של הזיכרון )בזבוז שנגרם עת אותה פונ' מצויה בזיכרון פעמים רבות ,כחלק מתכניות רבות(. לשימוש בספרייה דינאמית יש עוד מחיר :טעינת התכנית לזיכרון לשם ריצתה, ובהמשך הסתעפות לפונ' שאינה חלק מהתכנית ,הינם איטיים יותר. כדי לצור מזוג הקבצים שראינו בעבר ספרייה דינאמית נקמפלם באופן: g++ -Wall –c –fPIC add_sub.cc g++ -Wall –c –fPIC add_sub.cc פקודה ההידור כולל את הדגל המוכר לנו –cהמורה שאין לייצר תכנית ניתנת להרצה ,אלא רק קובץ אובג'קט ,ומעברל ו את הדגל )החדש לנו( . -fPIC :דגל זה הוא שמאפשר לכלול את האובג'קט בספרייה דינאמית כפי שמייד נייצר. את הספרייה עצמה נייצר באמצעות הפקודה: g++ -shared -o libadd_sub_io.so add_sub.o io.o הספרייה תקרא libadd_sub_io.so :שמה בהכרח יתחיל ב lib :וייגמר ב ,so :היא תכלול את הקבצים כמתואר ,ותהיה משותפת so ) .הוא קיצור של (shared object את התכנית הראשית נהדר ,כרגיל ,באופן: g++ -Wall –c main.cc פקודת הכריכה תהייה: g++ main.o -L. -ladd_sub_io –o my_prog הדגל -Lמורה היכן יש לחפש ספריות ,והמיקום הוא . :כלומר גם במדריך הנוכחי )מעבר למקום בו בד"כ הכורך יודע לחפש את הספריות הסטנדרטיות( .הדגל –l )האות אנגלית Lקטנה( מורה שיש לכלול את הספרייה libadd_sub_io.so :כאשר את הרישא ) (libוהסיפא ) (.soאין כותבים. במידה ויש לנו הן ספריה סטאטית ,libadd_sub_io.aוהן ספריה דינאמית: libadd_sub_io.soיעדיף הכורך לכרוך את תכניתנו עם הספריה הדינאמית. כדי להריץ את התכנית יש ,ראשית ,להורות לטוען הדינמי היכן הוא יימצא את הספרייה הדינמית שלנו .בהנחה שאנו עובדים ביוניקס על ה tcsh :Shell -נעשה זאת על-ידי: setenv LD_LIBRARY_PATH /cs/teach/yoramb/intro2cs2/dividing_program/example1/dynamic_lib כלומר :אנו מכניסים למשתנה LD_LIBRARY_PATHאת הנתיב המלא לספריה שלנו .כזכור ,פקודת ה pwd :Shell -תוכל לתת לנו נתיב זה )בהנחה שאנו במדריך בו מצויה הספרייה(. הרצת התכנית היא כרגיל ,בדוגמה מעל. my_prog : 222 במידה ואנו מתעניינים בכך ,הפקודה ldd a.out :תלמד אותנו היכן מצויות הספריות בהן תוכניתנו עושה שימוש ,בפרט הספרייה שיצרנו 223
© Copyright 2024