Professional Documents
Culture Documents
נושא 2- מערכים, מצביעים ומחרוזות
נושא 2- מערכים, מצביעים ומחרוזות
מערך -רעיונית כמו שאנחנו מכירים מג'אווה .גם האתחול של מערכים יכול להתבצע באופן זהה לזה שבג'אווה.
חשוב לזכור שבמערך ,כל הערכים שמורים ברצף בזיכרון ,ולכן קל לגשת אליהם.
נשים לב שישנן מספר דרכים אפשריות לאתחל מערך:
בשורה הראשונה זה הדבר הטריוויאלי ,מקצים גודל למערך ואת הערכים שנמצאים בו .בשורה השנייה רואים שמגדירים
את הערך של המערך ומייחסים רק ערך אחד בסוגריים המסולסלים -ו ב כ ך מ ק ב ע י ם א ת כ ל א י ב ר י ה מ ע ר ך ל ה י ו ת מ ס פ ר ז ה .
בדוגמא השלישית אנחנו רואים שאם אנחנו שמים את הערכים במערך מתי שיוצרים אותו ,לא חייב להקצות לו גודל כי
הקומפיילר כבר רואה כמה ערכים יש בו.
בשורה הרביעית רואים דוגמא שמגדירים מערך של אותיות על ידי מעבר מחרוזת .מעך זה יכיל את כל תווי המחרוזת פלוס
התו המיוחד שדיברנו עליו בשיעור הקודם שמייצג ס י ו ם מ ח ר ו ז ת .
מצביעים -Pointers
אופרטור & -אופרטור אונרי שעובד רק על ארגומנט אחד .התפקיד שלו לקחת כלשהו משתנה ולהחזיר את הכתובת שלו
בזיכרון.
נשים לב שהזהות של הזיכרון שתוחזר לא כל כך מעניינת אותנו ,אבל ניתן באמצעות כתובת זו לעשות כל מיני שימושים
ש י ע ז ר ו ל נ ו ל ב צ ע פ ע ו ל ו ת ב ת ו כ נ י ת .ב ע צ ם ה מ ש ת נ ה pאמנם מכיל כתובת ,אבל באופן רעיוני ניתן להתייחס אליו כפשוט מצביע
קונקרטי לאותו משתנה -aדרכו ניתן לעשות שימוש בערך הקונקרטי של משתנה .a
משתנה מצביע הוא משתנה שמכיל כתובת של משתנה אחר .נצהיר על משתנה מצביע על ידי כתיבת * בתחילת השם של
המשתנה.
למשל אם יש לנו משתנה אינטג'ר של מספר בשם ,aנרצה להגדיר למשל int *pשזה יהיה משתנה שיכיל את הכתובת של
אותו משתנה .pנשים לב שכאשר אנחנו מגדירים את המשתנה *pהטיפוס שנרשום לפני יהיה הטיפוס של המשתנה שאובייקט
זה יצביע אליו .כלומר ,למרות שב *p -בפועל תמיד יהיה כלשהו מספר של כתובת בינארית ,הטיפוס שלו נקבע לפי האובייקט
שעליו משתנה זה יצביע.
מקרה פרטי הוא אם נגדיר פוינטר כ 0 -או NULLואז זה כמו להגיד פוינטר שמצביע לכלום.
בעצם המשתנה המצביע נותן גישה יותר ישירה לזיכרון של משתנים שונים ,בניגוד לבשפות תכנות אחרות בהם אנחנו
מתייחסים למשתנה ככלשהו אובייקט סגור.
אופרטור * )כאשר מופיע כאופרטור אונרי( -אם נפעיל את האופרטור הזה על אובייקט הצבעה ,pאנחנו נשלוף את הערך של
המשתנה עצמו שאליו מצביע ,aוכך למשל נוכל לשייך ערך זה למשתנה נוסף.
בדוגמא הנ"ל למשל אם יהיה לנו כלשהו משתנה מספרי aבעל ערך ,1ונרצה לבצע השמה למשתנה חדש bכך שיכיל את אותו
הערך של משתנה ,aנעשה את זה בצורה הזאת -נגדיר את pכמשתנה Pointerונשים בו את הכתובת של המשתנה ,aובכך
הוא יהיה משתנה מצביע שמכיל את כתובתו .כעת בעזרת אופרטור * ,נוכל לבצע השמה למשתנה bשיכיל את הערך של
ה מ ש ת נ ה ע ל י ו מ צ ב י ע .p
נשים לב להבדל חשוב – כאשר אנחנו רושמים *pבחלק ההצהרה של משתנה pזה אומר שאנחנו מגדירים את משתנה p
להיות כלשהו פוינטר .לעומת זאת ,כאשר אנחנו משתמשים ב *-בחלק ההשמה של הביטוי )צד ימין( כמו בשורה האחרונה
של התמונה למעלה ,אנחנו משתמשים בו לא כמשהו שמהווה אינדיקציה אלא כאופרטור ממש שמבצע את הפעולה של לשים
במשתנה bאת הערך של המשתנה המקורי ש p-מצביע עליו )במקרה הזה .(a
מתי זה שימושי?
ראינו שבשפת Cהעברת ארגומנטים לפונקציה נעשית .By Valueכלומר כאשר אנחנו מכניסים משתנים כארגומנטים של
פונקציה נוצר "עותק" זמני של משתנים אלו כמשתנים לוקאלים של הפונקציה ,כך שמתי שנעשה פעולות על משתנים
לוקאלים אלו בתוך בלוק הפונקציה זה לא ישפיע על הערך של המשתנים המקוריים שהכנסנו לפונקציה אלא רק על הנוכחות
הזמנית שלהם בתוך בלוק זה ,ואם נרצה לעשות שימוש בערכים של משתנים זמניים אלו נצטרך להעיף אותם החוצה על ידי
פקודת .Return
נראה כעת שבעזרת אותם פויינטרים ,נוכל להגדיר פונקציות כך שהעברת הארגומנטים תתבצע ,By Referenceכלומר כאשר
נכניס משתנים לפונקציה כל הפעולות שיתבצעו עליהם בבלוק הפונקציה יתבצעו ממש על ערכי המשתנים המקוריים
שהעברנו ,ולא על עותקים זמניים לוקאלים.
בפריים ה Main -הגדרנו משתנים מספריים iו .j -בנפרד הגדרנו את הפונקציה ,Swapואז מה שהכנסנו לפונקציה זו
כ א ר ג ו מ נ ט י ם ז ה א ת ה כ ת ו ב ו ת ש ל ה א ו ב י י ק ט י ם ה מ ס פ ר י י ם iו ,j -ע ל י ד י ה א ו פ ר ט ו ר ש ל & .
נשים לב כעת איך מוגדרת הפונקציה -Swapהארגומנטים איתה הפונקציה תעבוד הם ,*p, *qכלומר שניהם הוגדרו
כפוינטרים .כאשר הכנסנו לארגומנטים של הפונקציה את הכתובות של משתנים iו ,j -זה כאילו רשמנו
𝑝 = & 𝑖, 𝑗 & = 𝑞
כלומר במילים אחרות המשתנה pמכיל את הכתובת של המשתנה iמבחוץ ואילו המשתנה qמכיל את הכתובת של משתנה
jמבחוץ .העברה זו של פרמטרים היא ,By Valueכמו תמיד בפונקציות ,כלומר נוצרו משתנים לוקאליים זמניים pוq -
המכילים את הכתובות הנ"ל.
לעומת זאת ,נשים לב שבשורת הגדרת הפונקציה הגדרנו את pו q -להיות פויינטרים .כלומר ,מה שבאמת נעבוד איתו
בפונקציה זה אותם *p, *qשהם האובייקטים מבחוץ שפוינטרים אלו מצביעים עליהם.
לכן לאחר שיצרנו משתנה זמני ,tmpבשורה הראשונה שמנו במשתנה זה את הערך שהפוינטר *pמצביע עליו ,שזה בעצם ערך
המשתנה iמבחוץ .לאחר מכן בשורה השנייה מה שעשינו זה לשים בערך המשתנה שפוינטר *pמצביע עליו )כלומר ישירות
ערך iהמקורי מבחוץ( את הערך שפוינטר *qמצביע עליו ,שזה ערך משתנה jישירות מבחוץ .לאחר מכן באופן זהה שמנו
בתוך המשתנה שפוינטר qמצביע עליו )כלומר משתנה jהמקורי( את הערך המספרי שיש במשתנה .tmp
בעצם ניתן לראות שעל ידי מניפולציות עם פוינטרים ,הצלחנו בגוף הפונקציה לשנות ישירות את ערכי המשתנים iו j-מהפריים
החיצוני ,מבלי צורך להחזיר משהו חדש ע"י פקודת Returnבפונקציה.
מבנה המערך-
כמו שהזכרנו מקודם ,מה שמאפיין מערך של עצמים זה שכל איברי המערך נמצאים צמודים זה לזה בזיכרון .כך למשל אם
מערך זה הוא מערך של ) Integersכלומר שכל עצם בו מיוצג בד"כ על ידי 4בתים( ,זה אומר שהמערך ממוקדם בכלשהו
כתובת בזיכרון ,כל שהאיבר הראשון הוא בכתובת מסוימת ,האיבר השני 4בתים אחריו ,השלישי 4בתים אחריו וכך הלאה.
נשים לב שכאשר אנחנו מאתחלים מערך )למשל רושמים ] 𝑎[100לצורך העניין( ,האובייקט aעצמו יהיה ,בדומה למה שראינו
בפוינטרים ,משתנה שמכיל את הכתובת לתחילת המערך -כלומר את הכתובת בזיכרון איפה שמתחיל האיבר הראשון במערך.
באופן זה ,מערך הוא סוג של פוינטר ,ועל ידי א ר י ת מ ט י ק ה ש ל פ ו י נ ט ר י ם נ י ת ן ל ג ש ת ל א י ב ר י ם ש ו נ י ם ב מ ע ר ך .
אמרנו למשל שהמשתנה aלאחר אתחול מערך מכיל את הכתובת של תחילת המערך אותו משתנה זה מייצג .אם יהיה לנו
משתנה פוינטר ,*pול p -עצמו )זה שאחראי על אכסון הכתובת של אובייקט ההצבעה( נשייך את הערך של משתנה ,aבעצם
גם משתנה pיכיל את כתובת תחילת המערך שמייצג משתנה .aבאופן זה ,כאשר נשתמש בפוינטר באמת כמצביע )ע"י כתיבת
-(*pהאובייקט שיוחזר לנו ושהפוינטר יצביע עליו בפועל הוא למעשה האובייקט הראשון במערך ,כלומר כמו שהכרנו בג'אווה
כ.𝑎[0] -
כעת ,אם נעשה למשל ) ,∗ (𝑝 + 1נשים לב שמה שבתוך הסוגריים ,הביטוי ,𝑝 + 1בעצם מכיל את הכתובת לאיבר הבא
בתור במערך ,האיבר באינדקס אחד ] .𝑎[1לכן ,כאשר הפעלנו את אופרטור ה *-על ביטוי זה ,האובייקט שהפוינטר הנ"ל
יצביע עליו ,ומה שיוחזר לנו זה למעשה האיבר הראשון במערך .aכלומר ,באמצעות פעולות אריתמטיות על הכתובת
ש מ א ו כ ס נ ת ב מ ש ת נ ה ,pנ ו כ ל ל ה ש ת מ ש ב ת כ ו נ ה ש ל ו כ א ו ב י י ק ט מ צ ב י ע ע ל מ נ ת ל ג ש ת ל א י ב ר י ם ש ו נ י ם ב מ ע ר ך .
כלומר ,אם נרשום את הדברים הבאים הכל יהיה שקול:
נשים לב לעיקרון חשוב פה -למרות שאם למשל מדובר במערך של Integersכל איבר תופס 4בתים בזיכרון ,וכך למשל האיבר
באינדקס ה 1-נמצא בכתובת של 4בתים לאחר תחילת המערך )ולא ,(1עדיין את אריתמטיקת הפויינטרים ביצענו על ידי
הוספת כמה אינדקסים נרצה ללכת ,כלומר אם עשינו למשל 𝑝 + 5אנחנו יודעים שניגש לאיבר החמישי במערך )ולא היינו
צריכים לעשות ממש 𝑝 + 20שלוקח בחשבון את הכתובות המדויקות(.
הסיבה לכך היא שהקומפיילר יודע כמה בתים אמורים לתפוס המשתנים של המערך ,ולכן עושה את ההמרה הזאת בעצמו.
זו הסיבה שבעצם ,גם בעת אתחול המערך ובמיוחד בעת אתחול הפוינטר ,pחשוב להצהיר מה סוג האובייקט שנרצה ש*p -
יצביע עליו בסופו של דבר .למשל אם אמרנו שהמצביע יהיה ל Int-אז כשנעשה +1הקומפיילר ידע ללכת 4בתים קדימה
בזיכרון ,ולעומת זאת אם אמרנו שהמצביע יהיה ל Double-הקומפיילר יניח שמדובר במערך של מספרים מסוג Double
שמיוצגים ע"י 8בתים כל אחד ,ולכן יזוז 8כתובות קדימה בזיכרון כשנעשה .+1
הקצאת זיכרון דינמית למערך -נזכור כי בעת אתחול מערך אנחנו חייבים להגדיר מראש מספר קבוע שיהיה מספר האיברים
ה מ ק ס י מ ל י ש י כ י ל ה מ ע ר ך .מ ס פ ר ז ה ח י י ב ל ה י ו ת מ ס פ ר מ פ ו ר ש ,ו ל א י כ ו ל ל ה י ו ת ל מ ש ל כ ל ש ה ו מ ש ת נ ה ,nג ם א ם מ א ו כ ס ן ב ו
מספר טבעי.
בעזרת מצביעים ,ניתן לבצע סוג של מניפולציה בעזרתה מתאפשר לנו לעשות הקצאה דינמית של זיכרון למערך לאחר האתחול
שלו.
בעזרת פונקציית )𝑚 𝑐𝑎𝑙𝑙𝑜𝑐(𝑛,אנחנו יכולים לאתחל גם כן מערך ,בדומה לאתחול הטריוויאלי של ]𝑛[𝑎 .אם נכתוב = 𝑝
)𝑚 𝑐𝑎𝑙𝑙𝑜𝑐(𝑛,זה אומר שעבור משתנה מצביע *pיוקצו nתאים בזיכרון באורך mבתים כל אחד .כלומר להקצות עבור p
מספר מסוים של תאים רצופים בזיכרון עם גודל מוגדר מראש שזה בדיוק שקול ללהגדיר מערך -אז בעצם מה שעשינו בפקודה
זו זה להגדיר מערך חדש ,שמשתנה pיצביע אל הכתובת שלו בזיכרון ,והוא צריך להכיל nעצמים לכל היותר ,כך שלכל עצם
י ו ק צ ו mתאים בזיכרון.
נזכור שבסוף פקודה זו pיכיל את כתובת תחילת המערך שאותחל בזמן ש *p -יצביע לנו בפועל על המערך עצמו.
בעצם מה שדינמי בטכניקה זו ,זה שבזמן כתיבת התוכנית ניתן לרשום ב calloc -שאנחנו רוצים להקצות למשל ,nכלשהו
משתנה ,תאים של זיכרון עבור המערך המבוקש ,ואז בזמן הריצה זה יבדוק מה הערך הנוכחי של nולפיו יקבע כמה מקומות
א נ ח נ ו ר ו צ י ם ל ה ק צ ו ת ב מ ע ר ך ה ח ד ש .ראינו שאם אנחנו מקצים מערך בצורה הרגילה ,אנחנו חייבים להכניס מספר מפורש
עוד בזמן כתיבת התוכנית שזה מספר התאים שיוקצו למערך ,מבלי לקחת בחשבון דברים שיכולים לקרות במהלך ריצת
התוכנית.
חשוב לזכור שלאחר שסיימנו את השימוש במערך שאתחלנו בצורה כזו ,יש לשחרר את הזיכרון שהשתמשנו בו ע"י פקודת
)𝑝(𝑒𝑒𝑟𝑓 .נרשום את פקודה זו בעצם אחרי שסיימנו לגמרי להשתמש במערך זה.
נסתכל למשל על הדוגמא הבאה בה אנחנו רוצים להחליף בין ההצבעות של שני
פוינטרים -הפוינטר !𝑝 מבציע על אובייקט aוהפוינטר "𝑝 מצביע על אובייקט
,bונרצה ש 𝑝! -יצביע על aבזמן ש 𝑝" -על .b
נכתוב את פונקציית ההחלפה באופן הבא ,כך שנשים לב שמה שהעברנו
כארגומנטים לפונקציה זה כאמור הכתובות של המצביעים "𝑝 .𝑝! ,
כעת כאשר מגדירים את הפונקציה ,חשוב להגדיר בהצהרה
שלה שהארגומנטים שלה יהיו מצביעים למצביעים ,על ידי
**.
נניח כי הכתובת של aהיא " "50ושל bהיא "."70
הפוינטרים המקוריים "𝑝 𝑝! ,מצביעים על אובייקטים אלו.
נניח כי הכתובת של !𝑝 היא " "100ושל "𝑝 היא " "200שאלו
הארגומנטים שהכנסנו לפונקציית ה .swap -נשים לב כי
כיוון 𝑝! -מצביע ל a -אז הערך שמאוכסן בו זה הכתובת של ("50") aבזמן שבאופן דומה מה שמאוכסן פיזית ב 𝑝" -זה "."70
בשורה הראשונה אנחנו יוצרים משתנה מצביע חדש בשם *tempועושים לו השמה למה שמצביע עליו !𝑝 של הפונקציה .נזכור
כי בתוך הפונקציה ,אותו !𝑝 הוא מצביע למצביע כלומר הכנסנו את הכתובת של המצביע !𝑝 מבחוץ ולכן אובייקט זה בתוך
הפונקציה מצביע למצביע !𝑝 מבחוץ שמכיל את הערך " ."50לכן הערך שמשתנה tempמכיל הוא "."50
לאחר מכן בשורה השנייה ,אנחנו שמים במה שמצביע אליו !𝑝 בפריים הפונקציה )שזה !𝑝 החיצוני( את מה שמצביע אליו "𝑝
של הפונקציה )שזה "𝑝 החיצוני ,שמכיל " ("70ולכן כעת ,במשתנה !𝑝 המקורי מבחוץ ,יהיה "."70
לאחר מכן אנחנו באופן דומה שמים במה שמצביע אליו "𝑝 בפריים הפונקציה )שזה בעצם "𝑝 המקורי מבחוץ( את הערך
שמאוכסן ב ,temp -וכעת "𝑝 המקורי מכיל את המספר "."50
סה"כ קיבלנו שהמשתנה המצביע המקורי !𝑝 מכיל את הכתובת " ,"70כלומר כמצביע *p1מצביע לאובייקט bשכתובתו
" ."70באופן דומה כעת המצביע *p2המקורי מצביע לאובייקט aשכתובתו " ,"50ובכך החלפנו באמת בין הערכים שכל פוינטר
מצביע אליהם.
מחרוזות
רעיונית ניתן להסתכל על מחרוזת כמערך של -Charאוסף של מספר מסוים של אובייקטים מסוג זה הנמצאים רצוף זה לצד
זה בזיכרון.
תזכורת :גם אם לא רואים זאת באמת במחרוזת ,בזיכרון המחשב כל מחרוזת
נגמרת בתו נוסף " -"\0שהוא זה שעוזר לקומפיילר להבין שנגמרה המחרוזת,
ובנקודה זו הוא צריך להפסיק לקרוא אותה .לכן למשל אם נסתכל על המחרוזת
” ,“abcזו למעשה מחרוזת שמכילה בתוכה 4תווים.
מסיבה זו ,ניתן ליצור משתנה של מחרוזת על ידי אתחול של מערך של ,charsכלומר ליצור פוינטר *pולייחס לו את המחרוזת
המבוקשת .נשים לב שבדומה למה שראינו במערכים ,ערך של משתנה pב ה ק ש ר ז ה י ה י ה ה כ ת ו ב ת ש ל ת ח י ל ת ה מ ח ר ו ז ת
בזיכרון .לכן למשל אם נעשה ,𝑝 + 1מספר זה יכיל את הכתובת של האות הבאה במחרוזת .כיוון שהקומפיילר יודע לסיים
את קריאת המחרוזת ב" -"\0אם למשל נדפיס את המחרוזת החל מתו זה היא תודפס מנקודה זו ותפסיק עדיין בסיומה
המקורי ,שכן שם מצוי " "\0מבחינת המחשב.
נזכור שאם יש לנו כלשהו מצביע 𝑝 ∗ למחרוזת ,המתשנה 𝑝 עצמו יכיל את הכתובת לאיבר הראשון של המחרוזת ואילו )𝑝 ∗(
כמכלול יהיה אובייקט שמצביע על כתובת זו ובעצם מ י י צ ג ת א ת ה מ ח ר ו ז ת ה ב א .א ם נ ר צ ה ל ט י י ל ל א ו ר ך ה מ ח ר ו ז ת ע ל
האותיות השונות שיש שם ,נוכל להשתמש ב Syntax-הבא?∗ 𝑝 + + :
מה זה אומר?
בעצם האופרטור של ה ++עובד על הערך המספרי של 𝑝 עצמו ,שזו הכתובת .בכך שאנחנו מגדילים את הכתובת ״ב1-״ )זה
ל א ב א מ ת ,1כ י ה ר א י נ ו ש א ם ה ג ד ר נ ו ל מ ש ל א ת ה ט י פ ו ס ש ל ה מ צ ב י ע 𝑝 ∗ ל ה י ו ת מ צ ב י ע ל מ ח ר ו ז ת א ז ה ג ד ל ה ש כ ז ו ת ג ד י ל א ת
הכתובת 𝑝 בזיכרון בדיוק כמה שצריך כדי לעבור לתו הבא( ,המשתנה 𝑝 יכיל כעת את הכתובת לאיבר הבא בתור במחרוזת,
ואז כאשר יפעל עליו אופרטור ∗ ה ו א י צ ב י ע ב א ו פ ן א ק ט י ב י ל מ ה ש נ מ צ א ב ז י כ ר ו ן ה ז ה ,ש ז ה ה ת ו ה ב א ב מ ח ר ו ז ת .
ישנם מספר פונקציות מובנות חשובות לשימוש במחרוזות הנמצאות בספריה " ,"string.hכמו:
כך שהראשונה משרשרת בין מחרוזות )המחרוזת s1תכיל את המחרוזת המשורשרת החדשה בזמן שהמחרוזת s2לא תשתנה
כלל( ,השנייה משווה בין שתי מחרוזות – תחזיר −1אם המחרוזת ש s1-מצביע אליה קטנה לקסיקוגרפית מהמחרוזת שs2 -
מצביע אליה ,תחזיר 0במידה והן שווה ו 1 -במידה והמחרוזת ש s1-מצביע אליה גדולה מהמחרוזת ש s2 -מצביע אליה,
השלישית מעתיקה את תוכן מחרוזת s2למחרוזת ש s1 -מצביעה עליה והרביעית מחזירה את אורך המחרוזת שהכנסנו.