You are on page 1of 19

‫© דניאל פונכץ ‪0202‬‬

‫‪medium.com/@danielfonkaz‬‬

‫מצביעים בשפת ‪C‬‬

‫הקדמה – מודל הזכרון בתוכנית‬


‫זכרון המחשב מורכב מבתים (‪ .)Bytes‬בהפשטה‪ ,‬ניתן לחשוב על בתים כאבני הבניין של‬
‫הזכרון‪ ,‬ועל זכרון המחשב כעל רצף ארוך של בתים‪ .‬לכל בית יש כתובת‪ .‬הכתובות‬
‫עוקבות זו לזו‪ .‬אם נחשוב על רצף הבתים בזכרון המחשב כמתחיל מצד שמאל ונמשך‬
‫לכיוון ימין‪ ,‬אז כתובתו של כל בית תהיה גדולה ב‪ 1-‬מהכתובת של הבית שמשמאלו‪-‬‬
‫כלומר לפניו‪ .‬בנוסף לכתובת‪ ,‬לכל בית קיים הערך הנשמר בו‪ .‬נוכל לחשוב על ערך זה‬
‫כמספר בטווח מסוים‪ .‬נעסוק בטווחים בהמשך המאמר‪.‬‬

‫תמונת זכרון אפשרית של בלוק (אוסף בתים) החל מהבית בכתובת ‪ .5‬הערך בתוך כל בית שרירותי‪,‬‬
‫אבל הכתובות של הבתים בבלוק בהכרח תהינה עוקבות‪.‬‬

‫מהו משתנה?‬
‫משתנה הוא שם וטיפוס שהמתכנת קובע עבור בלוק של בתים‪ .‬כאשר מוגדר משתנה‬
‫בתוכנית‪ ,‬מוקצה עבורו בלוק של בתים החל מכתובת מסוימת בזכרון‪ .‬כתובת ההחחלה‬
‫אינה בשליטת המתכנת‪ ,‬אבל מובטח שהבתים יוקצו ברצף ‪ -‬כלומר כבלוק‪.‬‬
‫שם ‪ -‬כיצד בוחר המתכנת לכנות את המשתנה‪.‬‬
‫טיפוס ‪ -‬כמה בתים יתפוס המשתנה‪ ,‬ומה סוג הערכים שיאוחסנו בו‪.‬‬

‫טיפוסי משתנים בשפת ‪C‬‬


‫להלן רשימה של הטיפוסים העיקריים הקיימים בשפה‪ ,‬גודלם (בבתים) וסוג הערכים‬
‫שמאוחסנים בהם‪.‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫הערה ‪ :1‬גודל הטיפוסים בבתים ומכאן טווח הערכים שלהם תלוי בסביבה בה עוברת‬
‫התוכנית קומפילציה‪ .‬מוצגת כאן האפשרות הנפוצה שלרוב נלמדת ‪.‬‬
‫הערה ‪ :0‬הערה ‪ 1‬נכונה לכל הטיפוסים למעט‪ char, float, double :‬אשר גודלם קבוע בכל‬
‫הסביבות ‪ -‬כי הוא מוגדר בתקן השפה‪.‬‬

‫‪( char‬גודל‪ 1 :‬בית) ‪ -‬משמש לאחסון מספרים שלמים בטווח ‪.-101–101‬‬


‫‪( short‬גודל‪ 0 :‬בית) ‪ -‬משמש לאחסון מספרים שלמים בטווח ‪.-20,121–20,121‬‬
‫‪( int‬גודל‪ 4 :‬בית) ‪ -‬משמש לאחסון מספרים שלמים בטווח ‪.-³¹0– ³¹0 - 1‬‬
‫‪( long‬גודל‪ 4 :‬בית) ‪ -‬משמש לאחסון מספרים שלמים בטווח ‪( .-³¹0– ³¹0 - 1‬הערה‪:‬‬
‫לעיתים נלמד שגודלו של ‪ long‬הוא כגודלו של ‪ ,long long‬כלומר ‪ 1‬בית‪ ,‬והטווח בהתאם‪).‬‬
‫‪( long long‬גודל‪ 1 :‬בית) ‪ -‬משמש לאחסון מספרים שלמים בטווח ‪.-¹⁶0– ¹⁶0 - 1‬‬
‫‪( float‬גודל‪ 4 :‬בית) ‪ -‬משמש לאחסון מספרים ממשיים ברמת דיוק של כ‪ 1-‬ספרות‬
‫עשרוניות סה'כ‪.‬‬
‫‪( double‬גודל‪ 1 :‬בית) ‪ -‬משמש לאחסון מספרים ממשיים ברמת דיוק של כ‪ 15-‬ספרות‬
‫עשרוניות סה'כ‪.‬‬

‫מרשימה זו אפשר ללמוד כמה דברים‪:‬‬


‫מספר הבתים שמוקצה לכל טיפוס קובע את טווח הערכים שלו ‪ -‬ככל שמספר‬ ‫‪.I‬‬
‫הבתים גדול יותר‪ ,‬טווח הערכים רחב יותר‪.‬‬
‫ניתן להשתמש באותו בלוק של בתים לייצוג טיפוסים שונים בזכרון‪:‬‬ ‫‪.II‬‬
‫‪ - float‬תופס ‪ 4‬בתים ומשמש לייצוג מספרים ממשיים‪ ,‬עם נקודה עשרונית‪.‬‬ ‫‪-‬‬
‫‪ - int‬תופס גם הוא ‪ 4‬בתים אך משמש לייצוג מספרים שלמים‪.‬‬ ‫‪-‬‬

‫תרשים הזכרון בתוכנית לדוגמה‬


‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫לשם חידוד ההבנה‪ ,‬נציג כעת תוכנית לדוגמה ולאחריה יוצג תרשים זכרון המחשב‬
‫המתאר אותה‪:‬‬
‫)(‪int main‬‬
‫{‬
‫;‪int i‬‬
‫;‪char c‬‬
‫;‪short s = 30‬‬
‫;‪float f = 1.25f‬‬
‫;‪char c1‬‬
‫;‪return 0‬‬
‫}‬

‫תרשים תמונת הזכרון המתאים לתוכנית לעיל‪ ,‬בשיטת ‪( little-endian‬ראו הערה ‪ ,)1‬בהנחה‬
‫שהמשתנים מוקצים ברצף בזכרון החל מהכתובת ‪( 10‬ראו הערה ‪ .)2‬שמות המשתנים בראש הציור‪,‬‬
‫החצים מסמנים את הבתים שמוקצים לכל משתנה‪ ,‬כתובת כל בית מופיע מתחתיו‪ ,‬וערכו של המשתנה‬
‫מופיע מתחת לחץ המתאים‪.‬‬

‫ניתן להבחין שהמשתנים מוקצים ברצף בתוכנית‪ ,‬כאשר לכל משתנה מוקצים מספר בתים‬
‫בהתאם לטיפוס שלו‪.‬‬
‫הערה חשובה ‪ :1‬קיימות שתי שיטות ניהול זכרון‪ :‬בשיטת ‪ little-endian‬עבור כל משתנה‬
‫נשמר בכתובת הנמוכה הבית הכי פחות משמעותי (‪ - )least significant byte‬הבתים‬
‫נשמרים על פי משמעותם בסדר עולה‪ ,‬ואילו בשיטת ‪ big-endian‬עבור כל משתנה נשמר‬
‫בכתובת הנמוכה הבית הכי משמעותי (‪ - )most significant byte‬הבתים נשמרים על פי‬
‫משמעותם בסדר יורד‪ .‬במדריך זה נעבוד עם שיטת ניהול הזכרון הכי נפוצה‪ ,‬שהיא ‪little-‬‬
‫‪.endian‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫הערה חשובה ‪ :0‬לא מובטח שהמשתנים בתוכנית יוקצו החל מכתובת מסוימת‪ ,‬בפרט ‪.10‬‬
‫בנוסף‪ ,‬לא מובטח שהמשתנים בתוכנית יוקצו ברצף בזכרון (כלומר משתנה אחד יופיע‬
‫בזכרון מיד לאחר משתנה שני)‪.‬‬
‫מה שכן ניתן לדעת בוודאות על תמונת הזכרון‪ ,‬מתואר להלן‪:‬‬
‫לכל משתנה‪ ,‬הבתים המרכיבים אותו מכילים יחד את ערכו‪ s .‬למשל מורכב משני‬ ‫‪.I‬‬
‫בתים‪ ,‬ולכן שני הבתים יחד יכילו את הערך ‪ .22‬בנוסף‪ ,‬כל הבתים של משתנה‬
‫מסוים יופיעו בהכרח ברצף בזכרון‪ .‬למשל‪ i ,‬מורכב מ‪ 4-‬בתים‪ ,‬ואם הבית הראשון‬
‫מופיע בכתובת ‪ , 10‬שלושת הבתים הנותרים יופיעו בהכרח בכתובות ‪, 14 ,12‬‬
‫‪.15‬‬
‫הערכים מיוצגים בתוך הבתים בצורה בינארית‪ ,‬כאשר כל בית מכיל ‪ 1‬ביט‪.‬‬ ‫‪.II‬‬
‫במאמר זה לא נדון ביצוג הבינארי‪ ,‬ששמור לדיון אחר‪.‬‬
‫המשתנים שאינם מאותחלים בערך מסוים‪ ,‬נותרים 'בלתי מאותחלים' וערכם‬ ‫‪.III‬‬
‫מסומן בתרשים בסימן שאלה‪ .‬בפועל‪ ,‬המשתנים מאותחלים ב‪'-‬ערך זבל' ‪ -‬הערך‬
‫שהיה במקרה בזכרון כאשר נטענה התוכנית והוקצו הבתים עבור המשתנים‪.‬‬
‫ערך זה אינו ידוע ולכן נתייחס למשתנה כ 'בלתי מאותחל' עד אשר יאותחל‪.‬‬
‫אם שואלים מה כתובתו של כל בית התשובה ברורה ‪ -‬הכתובת מצוינת מתחת‬ ‫‪.IV‬‬
‫לבית‪ .‬אבל מה אם נשאל מה כתובתו של משתנה כלשהו? אם המשתנה מורכב‬
‫מבית בודד התשובה היא פשוט כתובתו של הבית הבודד‪ .‬אבל אם המשתנה‬
‫מורכב מכמה בתים‪ ,‬כתובתו של המשתנה היא הכתובת של הבית הראשון שלו ‪-‬‬
‫זה עם הכתובת הנמוכה ביותר‪ .‬בדוגמה זו‪ ,‬כתובתו של המשתנה ‪ i‬היא ‪,10‬‬
‫כתובתו של ‪ s‬היא ‪ ,11‬וכתובתו של ‪ c1‬היא ‪.02‬‬

‫מצביעים – הגדרה ושימוש בסיסי‬


‫תזכורת ‪ :‬אם משתנה מורכב מכמה בתים‪ ,‬כתובתו של המשתנה היא הכתובת של הבית‬
‫הראשון שלו‪.‬‬
‫אם ברצוננו לדעת מה הכתובת שקיבל משתנה מסוים במהלך ריצת התוכנית ניתן לעשות‬
‫זאת באמצעות אופרטור &‪ ,‬שמשמעותו ‪ -‬הבא את הכתובת‪.‬‬
‫נתבונן בקטע הקוד הבא שמדפיס את כתובתו של משתנה בתוכנית‪:‬‬
‫)(‪int main‬‬
‫{‬
‫;‪int a = 5‬‬

‫;)‪printf("%u %p", &a, &a‬‬


‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫;‪return 0‬‬
‫}‬

‫כתובתו של המשתנה מודפסת פעמיים‪.‬‬


‫‪ — %p‬מדפיס את ערך הכתובת בבסיס ‪ ,12‬זו הדרך המקובלת להתייחס לכתובות‪.‬‬
‫‪ — %u‬מדפיס את אותה הכתובת כמספר א‪-‬שלילי בבסיס ‪ — 12‬פחות מקובל אך נוסף‬
‫כאן לשם הבהירות‪.‬‬
‫סביר שתתקבל כתובת אחרת בכל פעם שנריץ את התוכנית‪ .‬בכל מקרה‪ ,‬ההדפסה היא‬
‫רק לשם ההדגמה‪ .‬לא נזדקק לדעת מה הערך המספרי של כתובתו של משתנה בתוכנית‪,‬‬
‫ולא יהיה לכך שימוש‪ .‬עם זאת‪ ,‬כן נעשה שימוש בכתובות לשם איתחול מצביעים‪.‬‬

‫הגדרת מצביעים‬
‫מצביע הוא משתנה שמכיל כתובת‪ .‬כעקרון מצביע יכול להכיל כל כתובת שהיא‪ .‬עם זאת‪,‬‬
‫לרוב הוא יכיל את כתובתו של‪:‬‬
‫משתנה מטיפוס פשוט ‪.int, double, char -‬‬ ‫‪.I‬‬
‫משתנה מטיפוס מבנה כלשהו ‪.struct -‬‬ ‫‪.II‬‬
‫האיבר הראשון במערך מטיפוס כלשהו ‪.. int[], double[], char[] -‬‬ ‫‪.III‬‬

‫כאמור מצביע הוא משתנה שמכיל כתובת‪ ,‬שהיא מספר שלם‪ .‬לכן ניתן היה עקרונית‬
‫להשתמש במשתנה מטיפוס מספר שלם (כמו ‪ )int‬עם טווח גדול מספיק על מנת שישמש‬
‫כמצביע‪ .‬בפועל‪ ,‬לא משתמשים בטיפוס מספר שלם‪ ,‬אלא קיימים טיפוסים שונים של‬
‫מצביעים‪ .‬כל המצביעים מכילים כתובת‪ ,‬אך הם נבדלים ביניהם בסוג הכתובת שהם‬
‫עשוים להכיל‪.‬‬
‫להלן מספר דוגמאות‪:‬‬
‫‪ - int* a‬משתנה מצביע שמכיל את כתובתו של מספר שלם מסוג ‪.int‬‬ ‫‪-‬‬
‫‪ - char* c‬משתנה שמכיל את כתובתו של מספר שלם מסוג ‪.char‬‬ ‫‪-‬‬
‫‪ - struct student* ptr‬משתנה שמכיל את כתובתו של מבנה בשם ‪.student‬‬ ‫‪-‬‬
‫שימו לב לכוכבית — היא מגדירה שהמשתנה הוא מצביע לטיפוס‪ ,‬ולא משתנה רגיל‬
‫מהטיפוס‪:‬‬
‫‪ - int* a‬משתנה שמכיל את הכתובת של מספר שלם‪ ,‬לעומת‪:‬‬
0202 ‫© דניאל פונכץ‬
medium.com/@danielfonkaz

.‫ משתנה שמכיל מספר שלם‬- int a


:‫להלן קטע קוד המדגים הגדרה של מספר מצביעים‬
#include <stdio.h>

struct student {
int id;
int grade;
};

int main() {
// nonpointer variables
int a = 10, b;
struct student stud = { 100000001, 95 };
char c;

// pointer variables
int* ptr1 = &a;
struct student* ptr2 = &stud;
char* ptr3 = &c;
double* ptr4 = NULL; // initialized to NULL - points to "nothing"

// uninitialized - contains a garbage value


float* ptr5;
short* ptr6;

// definition of short array containing 3 elements -


// s_arr contains the address of the first array element
short s_arr[3];

// set ptr1 to point to a different int


ptr1 = &b;
ptr6 = &s_arr[1];

return 0;
}

:‫נניח את ההנחות הבאות‬


.‫ בתים‬4 int ‫גודל הטיפוס‬ -
.‫ בית‬1 char ‫גודל הטיפוס‬ -
.‫ בתים‬0 short ‫גודל הטיפוס‬ -
.‫ בתים‬1 struct Student ‫ולכן גודל הטיפוס‬
:‫וכן‬
.122 ‫ הוקצה החל מהכתובת‬a -
.152 ‫ הוקצה החל מהכתובת‬b -
.022 ‫ הוקצה החל מהכתובת‬stud -
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫‪ c‬הוקצה החל מהכתובת ‪.052‬‬ ‫‪-‬‬


‫‪ s_arr‬הוקצה החל מהכתובת ‪.222‬‬ ‫‪-‬‬
‫(הכתובות הן שרירותיות)‬
‫אז בסוף התוכנית המצביעים יכילו את הערכים הבאים‪:‬‬
‫‪ptr1 = 150‬‬ ‫‪-‬‬
‫‪ptr2 = 200‬‬ ‫‪-‬‬
‫‪ptr3 = 250‬‬ ‫‪-‬‬
‫‪ptr4 = 0‬‬ ‫‪-‬‬
‫? = ‪ptr5‬‬ ‫‪-‬‬
‫‪s_arr = 300‬‬ ‫‪-‬‬
‫‪ptr_6 = 302‬‬ ‫‪-‬‬

‫שימו לב לדגשים הבאים‪:‬‬


‫‪ NULL‬הינו קבוע שמוגדר בסיפריה הסטנדרטית‪ .‬ערכו לרוב ‪ .2‬הוא מייצג מצב‬ ‫‪.I‬‬
‫שהמצביע אינו מצביע לשום מקום — כלומר ‘ריק’‪ .‬שימו לב שזה שונה ממצביע‬
‫בלתי מאותחל שמכיל ערך זבל‪ ,‬ומכאן ‪.ptr4 = 0‬‬
‫‪ ptr5‬נותר בלתי מאותחל עד סוף התוכנית‪ .‬יש בו ערך זבל כלשהו — ערך‬ ‫‪.II‬‬
‫שהיה במקרה בזכרון בעת טעינת התוכנית‪ .‬ערך זה אינו מייצג כתובת‬
‫אמיתית‪ ,‬ולכן המשתנה אינו מצביע למקום מוקצה בזכרון‪.‬‬
‫‪ s_arr‬הינו מערך של שלושה ‪ .short‬הוא מכיל את הכתובת של האיבר‬ ‫‪.III‬‬
‫הראשון במערך‪ ,‬כלומר ‪ .222‬על אף שניתן לומר שמשתנה מסוג מערך הוא‬
‫סוג של מצביע‪ ,‬הוא גם שונה במידה מסוימת ממצביע רגיל (‘חופשי’)‪ ,‬בכך‬
‫למשל שלא ניתן לשנות את הצבעתו‪ ,‬כלומר לא ניתן להציב ערך חדש‬
‫במשתנה כך שיצביע למקום אחר (הסבר מפורט בהמשך המדריך)‪.‬‬
‫לעומת משתנה מסוג מערך‪ ,‬משתנה מצביע רגיל ניתן לשינוי במקום הצבעתו‪,‬‬ ‫‪.IV‬‬
‫על כן ההשמה‪ ptr1 = &b :‬הינה חוקית‪ ,‬ותגרום למצביע להצביע (כלומר להכיל‬
‫את הכתובת) של משתנה אחר‪.‬‬
‫‪ s_arr‬מכיל את הכתובת של האיבר הראשון במערך‪ ,‬בדוגמה זו ‪ .222‬על כן‪,‬‬ ‫‪.V‬‬
‫מאחר וכל איבר במערך תופס ‪ 0‬בתים‪ ,‬ומובטח שאיברים במערך מוקצים‬
‫ברצף בזכרון‪ ,‬כתובתו של האיבר השני במערך (האיבר באינדקס ‪ )1‬תהיה‬
‫‪ ,220‬ועל כן ההשמה ]‪ ptr6 = &s_arr[1‬תזין את הכתובת ‪ 220‬במצביע‪.‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫שימוש במצביעים‬
‫מצביעים מאפשרים גישה לערכם של המשתנים שעליהם הם מצביעים‪ .‬ניתן להשתמש‬
‫במצביעים לשתי מטרות‪:‬‬
‫שינוי ערך של המשתנה המוצבע (גישה לכתיבה)‪.‬‬ ‫‪-‬‬
‫קריאת ערך המשתנה המוצבע (גישה לקריאה)‪.‬‬ ‫‪-‬‬
‫מבצעים זאת תוך שימוש באופרטור * ‪ operator* -‬שמשמעותו‪ :‬קח את הכתובת שמכיל‬
‫המצביע‪ ,‬גש לכתובת זו בזכרון‪ ,‬ועשה שימוש בערך שיש שם‪.‬‬
‫נדגים זאת בתוכנית הבאה‪:‬‬
‫>‪#include <stdio.h‬‬

‫{ )(‪int main‬‬
‫;‪int a = 5‬‬
‫;} '‪char arr[3] = { 'a', 'b', 'c‬‬
‫;]‪char* c_ptr = &arr[1‬‬
‫;‪int* i_ptr = &a‬‬

‫‪*c_ptr = 't'; // write access‬‬


‫‪printf("%d", *i_ptr); // read access - prints: 5. same as printing a‬‬
‫‪int val = *i_ptr; // read access - assigns 5 to val‬‬
‫‪printf(" %c", *c_ptr); // read access - prints: t‬‬
‫‪printf(" %c", arr[1]); // will also print t‬‬

‫;‪return 0‬‬
‫}‬

‫דגשים לתוכנית לעיל‪:‬‬


‫‪ c_ptr‬מכיל את כתובתו של האיבר השני במערך התווים‪ ,‬כלומר מצביע אליו‪.‬‬ ‫‪.I‬‬
‫‪ i_ptr‬מכיל את כתובתו ולכן מצביע אל המשתנה ‪.a‬‬
‫בהשמה הבאה‪ *c_ptr = ‘t’ :‬נלקחת הכתובת שבמצביע‪ ,‬כלומר הכתובת של‬ ‫‪.II‬‬
‫האיבר השני במערך‪ ,‬מתבצעת גישה לזכרון בכתובת זו‪ ,‬מגיעים לערך שקיים‬
‫שם ‪ -‬זה האיבר השני במערך התווים‪ ,‬ומשנים אותו כך שיכיל את הערך '‪.'t‬‬
‫בשורה הבאה‪ printf(“%d”, *i_ptr) :‬נלקחת הכתובת במצביע‪ ,‬כלומר הכתובת‬ ‫‪.III‬‬
‫של המשתנה‪ ,‬מתבצעת גישה לזכרון בכתובת זו‪ ,‬מגיעים לערך שקיים שם ‪ -‬זה‬
‫הערך של המשתנה‪ ,‬ומדפיסים אותו‪.‬‬
‫בשורות הבאות‪ printf(“ %c”, *c_ptr); printf(“ %c”, arr[1]) :‬מודפס אותו‬ ‫‪.IV‬‬
‫הערך מאחר והמצביע מכיל את הכתובת של האיבר השני במערך‪.‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫שימושים מעשיים במצביעים‬


‫שינוי ערך של משתנה שהוגדר מחוץ לפונקציה‬

‫נביט בקטע הקוד הבא‪:‬‬


‫>‪#include <stdio.h‬‬

‫)‪void resetNum(int a‬‬


‫{‬
‫;‪a = 0‬‬
‫}‬

‫)(‪int main‬‬
‫{‬
‫;‪int num = 5‬‬

‫;)‪resetNum(num‬‬
‫;)‪printf("%d", num‬‬

‫;‪return 0‬‬
‫}‬

‫מה יהיה פלט התוכנית? במבט ראשון ניתן אולי לחשוב שהפלט יהיה ‪ .2‬הפונקציה‬
‫מקבלת כפרמטר מספר שלם‪ ,‬ומציבה בו ‪ — 2‬ולכן ערכו של המשתנה בפונקציה ‪main‬‬
‫יעודכן להיות ‪ 2‬וההדפסה בהתאם‪ .‬אך לא כך הדבר — ערכו של המשתנה בפונקציה‬
‫‪ main‬נותר כשהיה‪ ,‬כלומר ‪ num = 5‬ומכאן יודפס בהתאם ‪.5‬‬
‫הסיבה לכך היא שפונקציה מקבלת ערך — ולא משתנה‪ .‬הערך שמועבר לפונקציה מוצב‬
‫בפרמטר‪ ,‬שהוא משתנה מקומי חדש שנוצר בפונקציה ‪ -‬ומכאן מכונה העתק‪.‬‬
‫;)‪ - resetNum(num‬בשורה זו מועבר הערך של המשתנה שמוגדר בפונקציה ‪,main‬‬
‫כלומר ‪.5‬‬
‫)‪ - void resetNum(int a‬בכניסה לפונקציה נוצר משתנה חדש ‪ -‬הפרמטר‪ ,‬והוא מקבל‬
‫את הערך שהועבר בקריאה לפונקציה‪ ,‬במקרה זה ‪.5‬‬
‫;‪ - a = 0‬בשורה זו משתנה ערכו של הפרמטר להיות ‪ .2‬ערכו של המשתנה שמוגדר‬
‫בפונקציה ‪ main‬נותר ללא שינוי‪.‬‬
‫כעת נתבונן בדוגמת קוד נוספת‪ ,‬בה עולה אותה הבעיה‪ ,‬הפעם בשימוש ב‪:struct-‬‬

‫>‪#include <stdio.h‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫>‪#include <string.h‬‬
‫>‪#include <stdio.h‬‬
‫‪#define MAX_NAME_SIZE 64‬‬

‫{ ‪struct student‬‬
‫;]‪char name[MAX_NAME_SIZE‬‬
‫;‪unsigned int grade‬‬
‫;}‬

‫)‪void readStudent(struct student stud‬‬


‫{‬
‫;)" ‪printf("Please enter student name (one word) and grade:‬‬
‫;)‪scanf("%s %d", stud.name, &stud.grade‬‬
‫}‬

‫)(‪int main‬‬
‫{‬
‫‪struct student stud = { "", 0 }; // initialized‬‬
‫;)‪readStudent(stud‬‬
‫;)‪printf("Name: %s, grade: %d", stud.name, stud.grade‬‬
‫;‪return 0‬‬
‫}‬

‫מה יהיה פלט התוכנית? על פניו בפונקציה ‪ main‬מאותחל מבנה עם ערכי ברירת מחדל‬
‫(מחרוזת ריקה בשם ו‪ 2-‬בציון)‪ .‬המבנה נשלח כפרמטר לפונקציה שקולטת מהמשתמש‬
‫את פרטי הסטודנט ומכניסה אותם למבנה‪ .‬מכאן צפוי אולי שפלט התוכנית יהיה הפרטים‬
‫שהזין המשתמש למבנה — אך עם זאת מודפסים ערכי האיתחול — מחרוזת ריקה ו‪-‬‬
‫‪.0‬‬
‫הסיבה ל כך‪ ,‬בדומה לדוגמת הקוד הקודמת‪ ,‬היא שבכניסה לפונקציה נוצר משתנה חדש‬
‫— הפרמטר ‪ ,struct student stud‬שלתוכו מועתק הערך שהועבר כפרמטר‪ ,‬כלומר‬
‫המבנה‪ .‬הפונקציה קולטת את הקלט שהזין המשתמש לתוך הפרמטר‪ ,‬ואילו המשתנה‬
‫בפונקציה ‪ main‬נותר ללא שינוי‪.‬‬

‫הפתרון – שימוש במצביעים‬


‫נעדכן את שני קטעי הקוד הקודמים‪ ,‬כך שיניבו את התוצאה הרצויה‪ .‬לצורך כך נעשה‬
‫שימוש במצביעים‪ ,‬שאחד מהשימושים העיקריים שלהם הוא שינוי ערכו של משתנה‬
‫שהוגדר מחוץ לפונקציה — תוך המנעות מבעית ה’העתק’ שעלתה בדוגמאות לעיל‪.‬‬
‫להלן קטע הקוד הראשון מעודכן כך שיעשה שימוש במצביעים‪:‬‬

‫>‪#include <stdio.h‬‬
0202 ‫© דניאל פונכץ‬
medium.com/@danielfonkaz

void resetNum(int* ptr)


{
*ptr = 0;
}

int main()
{
int num = 5;

resetNum(&num);
printf("%d", num);

return 0;
}

.‫ פרמטר הפונקציה מוגדר כמצביע למספר שלם‬,‫ הפעם‬- int* ptr


‫ במקום‬,‫ בפונקציה הראשית נשלחת הכתובת של המשתנה‬,‫ ואכן‬- resetNum(&num);
.‫ערכו של המשתנה שנשלח בגרסה הקודמת‬
‫ כלומר‬,‫ בתוך הפונקציה מתבצעת גישה לזכרון המוצבע על ידי הפרמטר‬- *ptr = 0;
.2-‫ ומשנים את ערכו ל‬,‫ניגשים למשתנה שמוגדר בפונקציה הראשית‬
‫ אך במקרה‬,‫שימו לב שגם במקרה זה בכניסה לפונקציה נוצר משתנה חדש — הפרמטר‬
.main ‫ ומוצבת בו הכתובת שהועברה מהפונקציה‬,‫זה הפרמטר שנוצר הוא מסוג מצביע‬
:‫להלן קטע הקוד השני מעודכן כך שיעשה שימוש במצביעים‬

#include <stdio.h>
#include <string.h>
#include <stdio.h>
#define MAX_NAME_SIZE 64

struct student {
char name[MAX_NAME_SIZE];
unsigned int grade;
};

void readStudent(struct student* stud)


{
printf("Please enter student name (one word) and grade: ");
scanf("%s %d", stud->name, &stud->grade); // same as scanf("%s %d",
(*stud).name, &(*stud).grade)
}

int main()
{
struct student stud = { "", 0 }; // initiailized
readStudent(&stud);
printf("Name: %s, grade: %d", stud.name, stud.grade);
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫;‪return 0‬‬
‫}‬

‫‪ - struct student* stud‬הפעם‪ ,‬פרמטר הפונקציה מוגדר כמצביע למבנה סטודנט‪.‬‬


‫;)‪ - readStudent(&stud‬ואכן‪ ,‬בפונקציה הראשית נשלחת הכתובת של משתנה המבנה‪,‬‬
‫במקום שיועתק ערך המבנה עצמו לפטרמטר‪ .‬בתוך הפונקציה מתבצעת גישה לזכרון‬
‫המוצבע על ידי הפרמטר‪ .‬עם זאת‪ ,‬במקרה זה לאחר הגישה לזכרון מתקבל מבנה — בו‬
‫יש לגשת לשדה מסוים‪.‬‬
‫)‪ - scanf(“%s %d”, (*stud).name, (*stud).grade‬בצורת כתיבה ארוכה זו (מופיעה‬
‫בהערה בקוד) ניגשים קודם לזכרון המוצבע‪ ,‬ומתקבל מבנה הסטודנט‪ ,‬ואז במבנה‬
‫הסטודנט ניגשים לשדה מסוים בו באמצעות אופרטור הנקודה‪.‬‬
‫)‪ - scanf(“%s %d”, stud->name, &stud->grade‬בצורת כתיבה מקוצרת זו עושים‬
‫שימוש באופרטור חץ‪ .‬אופרטור זה הוא בסך הכל קיצור (“סוכר תחבירי”)‪ ,‬ושקול לגמרי‬
‫להפעלה של אופרטור כוכבית ואז אופרטור נקודה‪:‬‬
‫‪(*stud).name ← → stud->name‬‬
‫‪&(*stud).grade ← → &stud->grade‬‬

‫החזרת מספר ערכים באמצעות משתני פלט‬


‫בטכניקה המתוארת לעיל ניתן להשתמש כדי לבצע החזרה של מספר ערכים מקריאה‬
‫לפונקציה ‪ .‬כידוע כל פונקציה מחזירה לכל היותר ערך אחד בסיום פעולתה‪ .‬מה אם נרצה‬
‫להחזיר מספר ערכים? פתרון אפשרי הוא לעטוף את הערכים במבנה ולהחזיר העתק‬
‫שלו ‪ ,‬או לחלופין להחזיר מערך (במידה והערכים שנרצה להחזיר מאותו טיפוס)‪ .‬עם זאת‪,‬‬
‫קיים פתרון נוסף‪ ,‬שהוא לעיתים קרובות מסו רבל פחות — שימוש במשתנה פלט‪ .‬משתנה‬
‫פלט הוא למעשה מצביע‪ ,‬שמכיל כתובת של משתנה שהוגדר מחוץ לפונקציה‪ .‬הפונקציה‬
‫משנה את המשתנה המוצבע על ידו‪ ,‬ובכך ממומשת פונקציונליות דומה לזו של החזרת‬
‫ערך רגילה — אך בשיטה זו ניתן להגדיר מספר משתני פלט (וכן לעשות שימוש גם בערך‬
‫החזרה)‪ ,‬ומכאן ניתן "להחזיר" מספר ערכים מהפונקציה‪.‬‬
‫נדגים שימוש בשיטה זו בקטע הקוד הבא‪:‬‬
‫>‪#include <stdio.h‬‬
‫>‪#include <stdlib.h‬‬

‫)‪int* read_array_from_user(int* arr_size_ptr‬‬


‫{‬
‫;‪int* arr‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫;)"‪printf("Please enter number of elements for array:\n‬‬


‫;)‪scanf("%d", arr_size_ptr‬‬

‫;)‪arr = (int*)malloc(sizeof(int) * *arr_size_ptr‬‬

‫‪if (!arr) // allocation failure‬‬


‫{‬
‫;‪*arr_size_ptr = -1‬‬
‫;‪return NULL‬‬
‫}‬

‫;)‪printf("Please enter %d array elements:\n", *arr_size_ptr‬‬

‫)‪for (int i = 0; i < *arr_size_ptr; ++i‬‬


‫{‬
‫;)]‪scanf("%d", &arr[i‬‬
‫}‬

‫;‪return arr‬‬
‫}‬

‫)(‪int main‬‬
‫{‬
‫;‪int* arr‬‬
‫;‪int size‬‬

‫;)‪arr = read_array_from_user(&size‬‬

‫‪// do something with array ..‬‬

‫;)‪free(arr‬‬

‫;‪return 0‬‬
‫}‬

‫בדוגמה זו מוצגת פונקציה הקולטת מערך שלמים מהמשתמש‪ .‬על הפונקציה להחזיר שני‬
‫ערכים — את המערך עצמו ואת גודלו‪ .‬לכן היא עושה שימוש במשתנה פלט — המערך‬
‫מוחזר בהוראת החזרה כרגיל‪ ,‬וגודל המערך מוחזר תוך שימוש במשתנה הפלט‪ .‬ניתן‬
‫היה גם לממש אחרת — שהמערך יוחזר במשתנה הפלט‪ ,‬ואילו גודלו יוחזר בהוראת‬
‫החזרה — או לחלופין להחזיר את שני הערכים בשני משתני פלט‪ ,‬ולא להשתמש בערך‬
‫החזרה (טיפוס ההחזרה יהיה ‪.)void‬‬

‫המנעות מהעתקה של ערך שהועבר כפרמטר‬


‫נתבונן בקטע הקוד הבא‪:‬‬
‫>‪#include <stdio.h‬‬
‫>‪#include <string.h‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫>‪#include <stdio.h‬‬
‫‪#define MAX_NAME_SIZE 64‬‬

‫{ ‪struct student‬‬
‫;]‪char name[MAX_NAME_SIZE‬‬
‫;‪unsigned int grade‬‬
‫;}‬

‫)‪void printStudent(struct student stud‬‬


‫{‬
‫;)‪printf("Name: %s, grade: %d", stud.name, stud.grade‬‬
‫}‬

‫)(‪int main‬‬
‫{‬
‫;} ‪struct student stud = { "Yossi", 92‬‬
‫;)‪printStudent(stud‬‬
‫;‪return 0‬‬
‫}‬

‫לכאורה קטע הקוד תקין‪ .‬במקרה זה על הפונקציה להדפיס את נתוני הסטודנט‪ ,‬והיא‬
‫איננה מעונינת לשנות את ערכי המשתנה שמוגדר מחוצה לה‪ ,‬בפונקציה ‪ — main‬ומכאן‬
‫אין בעיה שבפונקציה מוגדר הפרמטר ‪ stud‬כערך ‪( struct student‬כלומר‪ ,‬לא כמצביע)‪,‬‬
‫ונוצר משתנה חדש בכניסה לפונקציה אליו מועתקים ערכי המבנה שהועבר מהפונקציה‬
‫הראשית — כלומר נוצר העתק‪.‬‬
‫ברמת הפלט אכן אין בעיה בקוד הנ’ל‪ ,‬אך קיימת בו בעיה של יעילות — בכל פעם‬
‫שקוראים לפונקציה ומעבירים לה כפרמטר מבנה‪ ,‬הוא מועתק למשתנה ההעתק שנוצר‬
‫— וזה עולה בזמן ריצה‪.‬‬
‫כאשר מדובר במבנים בעלי משקל קטן יחסית‪ ,‬כמו ‪ struct student‬במקרה הנ’ל (כ‪21-‬‬
‫בתים) — תקורה זו אינה מאוד משמעותית‪ .‬אך אם מדובר במבנים גדולים יותר‪ ,‬או‬
‫לחלוטין מתבצעות קריאות רבות לפונקציה בפרק זמן קצר‪ ,‬תקורת זמן הריצה עשויה‬
‫להיות משמעותית‪.‬‬
‫כדי להמנע מתקורה זו‪ ,‬נהוג שפונקציות מקבלות פרמטר מסוג מבנה שגודלו מעל לזה‬
‫של משתנה פרימיטיבי (כלומר‪ ,‬מעל לכ‪ 1-‬בתים) כמצביע ולא כערך – במקרה זה‬
‫*‪ .struct student‬באופן זה אומנם נוצר משתנה חדש — אך הוא מסוג מצביע‪ ,‬ששוקל‬
‫מעט יחסית (‪ 1‬בתים ברוב הסביבות)‪ .‬כלומר‪ ,‬רק הכתובת מועתקת‪ ,‬וכך התקורה קטנה‬
‫משמעותית‪ .‬אם הפונקציה אינה מתכוונת לשנות את המבנה המוצבע‪ ,‬היא תקבל אותו‬
‫כמצביע ‪.const‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫להלן קטע הקוד מעודכן כך שיעשה שימוש במצביע בכדי להמנע מהעתקה של‬
‫המבנה‪:‬‬
‫>‪#include <stdio.h‬‬
‫>‪#include <string.h‬‬
‫>‪#include <stdio.h‬‬
‫‪#define MAX_NAME_SIZE 64‬‬

‫{ ‪struct student‬‬
‫;]‪char name[MAX_NAME_SIZE‬‬
‫;‪unsigned int grade‬‬
‫;}‬

‫)‪void printStudent(const struct student* stud‬‬


‫{‬
‫;)‪printf("Name: %s, grade: %d", stud->name, stud->grade‬‬
‫}‬

‫)(‪int main‬‬
‫{‬
‫;} ‪struct student stud = { "Yossi", 92‬‬
‫;)‪printStudent(&stud‬‬
‫;‪return 0‬‬
‫}‬

‫פלט הקוד זהה לזה של התוכנית לעיל‪ ,‬אך נחסכת העתקה יקרה יחסית‪.‬‬

‫מצביעים‪ ,‬מערכים והקצאות דינאמיות‬


‫בחלק זה נדון בקשר בין מצביעים למערכים ובשימוש במצביעים בהקצאה דינאמית‪.‬‬

‫מערכים בשפת סי ממומשים באמצעות מצביעים‬


‫שפות תכנות רבות תומכות במבנה נתונים המכונה מערך‪ .‬תכונותיו של מבנה נתונים זה‬
‫הן כדלהלן‪:‬‬
‫מחזיק נתונים מאותו הסוג‪ ,‬כלומר ניתן ליצור מערך של מספרים שלמים‪,‬‬ ‫‪-‬‬
‫מערך של מספרים ממשיים‪ ,‬מערך של מחרוזות וכו‪.‬‬
‫סדרתי (כלומר הנתונים במערך שמורים בסדר מסוים — ניתן לקבוע מי‬ ‫‪-‬‬
‫הראשון‪ ,‬מי השני וכך הלאה)‪.‬‬
‫מאפשר גישה אקראית לנתונים השונים (כלומר ניתן לגשת לערכו של נתון‬ ‫‪-‬‬
‫שרירותי במערך ולכתוב אליו או לקרוא את ערכו)‪.‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫מבנה הנתונים מערך ממומש בשפת ‪ C‬באופן הבא‪:‬‬


‫עבור הנתונים במערך מוקצה גוש בתים רציף בזכרון בגודל מסוים (למשל‪ ,‬אם‬ ‫‪-‬‬
‫מעונינים במערך של ‪ 2‬מספרים שלמים‪ ,‬וגודלו של כל מספר שלם הוא ‪4‬‬
‫בתים‪ ,‬עבור המערך מוקצים בסה’כ ‪ 10‬בתים ברצף בזכרון)‪.‬‬
‫מערך נקבע בגודל מסוים שיש לצין בעת יצירתו‪ .‬עם זאת‪ ,‬מערך דינאמי ניתן‬ ‫‪-‬‬
‫להגדיל ולהקטין באמצעות הקצאה מחדש‪.‬‬
‫הנתונים במערך ממוספרים‪ .‬כל נתון במערך מכונה איבר‪ ,‬וניתן לו מספר‬ ‫‪-‬‬
‫סידורי (אינדקס) המתאר את המיקום שלו‪ ,‬כאשר לאיבר הראשון במערך‬
‫ניתן האינדקס ‪ ,0‬לאיבר השני במערך ניתן האינדקס ‪ 1‬וכך הלאה‪.‬‬
‫להלן קטע קוד המדגים יצירה של שני סוגי מערכים ‪ -‬מערך מוקצה מקומית ומערך‬
‫מוקצה דינאמית‪:‬‬

‫)(‪int main‬‬
‫{‬
‫;]‪int arr[3‬‬
‫;)‪short* sr = (short*)malloc(sizeof(short) * 3‬‬

‫‪if (sr == NULL) // dynamic memory allocation failure‬‬


‫{‬
‫;)‪exit(1‬‬
‫}‬
‫‪// ...‬‬

‫;)‪free(sr‬‬
‫;‪return 0‬‬
‫}‬

‫מבחינת הקצאת הזכרון‪,‬קיימים כמה הבדלים בין מערך מוקצה מקומית למערך מוקצה‬
‫דינאמית‪:‬‬
‫הזכרון של מערך מקומי מוקצה על מחסנית זמן הריצה‪ ,‬ואילו הזכרון של‬ ‫‪-‬‬
‫מערך דינאמי מוקצה בערימה‪.‬‬
‫על פי כללי התחביר בשפת ‪ ,C‬מערך מקומי חייב להיות מוקצה בגודל קבוע‬ ‫‪-‬‬
‫ואילו מערך דינאמי יכול להיות מוקצה בגודל קבוע או שאינו קבוע — למשל‬
‫גודל שתלוי בערכו של משתנה‪.‬‬
‫הקצאת הזכרון של מערך דינאמי עשויה להכשל‪ ,‬ואילו הקצאה של מערך‬ ‫‪-‬‬
‫מקומי מובטח שתצליח (ליתר דיוק‪ ,‬מובטח שהקצאה מקומית תצליח אם‬
‫נותר די מקום במחסנית‪ .‬אחרת‪ ,‬התוכנית תיפול‪ .‬מאחר שלרוב מקצים‬
‫מערכים מקומיים קטנים מתעלמים בדרך כלל מאפשרות זו)‪.‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

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


‫ממערכת הפעלה‪ ,‬לעומת הקצאה מקומית‪ ,‬בה נעשה שימוש בזכרון המחסנית‬
‫שמוקצה כבר בעת טעינת התוכנית‪.‬‬

‫הן בהקצאה מקומית והן בהקצאה דינאמית‪ ,‬המשתנה שמייצג את המערך מכיל את‬
‫הכתובת של האיבר הראשון‪ .‬מכאן‪ ,‬ניתן לומר שבשני המקרים טיפוס משתנה המערך‬
‫הוא מצביע* (ראו הערה בהמשך)‪ ,‬אשר מצביע לאיבר הראשון במערך‪.‬‬

‫עם זאת‪ ,‬קיימים מספר הבדלים חשובים בין מצביע למערך מקומי* לבין מצביע למערך‬
‫דינאמי‪:‬‬
‫מצביע למערך מקומי הוא מקובע — לא ניתן לשנות את מיקום הצבעתו לאחר‬ ‫‪-‬‬
‫היצירה‪ ,‬הוא לעד יצביע לאיבר הראשון במערך‪ .‬לעומת זאת‪ ,‬מצביע למערך‬
‫דינאמי ניתן לשינוי במקום הצבעתו — ניתן לשנות את הכתובת אותה הוא‬
‫מכיל כך שהצבעתו תשתנה‪.‬‬
‫‪ — sizeof‬אופרטור זה מחזיר את הגודל בבתים של משתנה‪ .‬אם יופעל על‬ ‫‪-‬‬
‫מצביע למערך מקומי יתקבל הגודל הכולל של המערך בבתים — בדוגמה‬
‫לעיל למשל היה מתקבל ‪( 10‬שלושה מספרים שלמים במערך‪ ,‬כל אחד בגודל‬
‫‪ 4‬בתים)‪ .‬לעומת זאת‪ ,‬אם יופעל על מצביע למערך דינאמי יתקבל הגודל של‬
‫המצביע עצמו — ‪ 1‬ברוב הסביבות‪ ,‬ללא תלות בגודל המערך‪.‬‬

‫(*) ליתר דיוק‪ ,‬במקרה של מערך מקומי‪ ,‬טיפוס המשתנה אינו מצביע‪ ,‬אלא טיפוס מערך‪,‬‬
‫אשר ברוב ההקשרים עובר המרה אוטומטית למצביע‪ ,‬ולכן מתנהג בפועל כמו מצביע‪.‬‬
‫במקרים בהם לא מתבצעת המרה אוטומטית כאמור‪ ,‬מערך מקומי מתנהג בצורה שונה‬
‫מעט ממצביע‪ ,‬כפי שמתואר בנקודות לעיל‪ .‬לשם הפשטות נתייחס גם למשתנה מערך‬
‫מקומי כמצביע לאיבר הראשון במערך‪.‬‬

‫גישה לאיבר במערך נעשת תוך שימוש במצביע ובאריתמטיקה‬


‫ניתן לעשות שימוש במערכים בשפה תוך התעלמות מהיותם מצביעים‪ ,‬באמצעות תחביר‬
‫סוגריים מרובעים‪ .‬למשל‪ ,‬בקטע הקוד הבא‪:‬‬
‫>‪#include <stdlib.h‬‬
‫>‪#include <stdio.h‬‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫)(‪int main‬‬
‫{‬
‫;]‪int arr[3‬‬
‫;)‪int* d_arr = (int*)malloc(sizeof(int) * 3‬‬

‫;)‪if (!d_arr) exit(1‬‬

‫‪// write access‬‬


‫;‪arr[0] = 7‬‬
‫;‪d_arr[1] = 4‬‬

‫‪// read access‬‬


‫;)]‪printf("%d %d\n", arr[0], d_arr[1‬‬

‫;)‪free(d_arr‬‬
‫;‪return 0‬‬
‫}‬

‫בקטע הקוד ניגשים ומשנים אבר במערך (גישת כתיבה) וכן קוראים את ערכו של אבר‬
‫במערך ומדפיסים אותו‪ .‬הן במערך המקומי והן במערך הדינאמי אין לכאורה שימוש‬
‫בהיותם מצביעים — נעשה שימוש באופרטור סוגריים מרובעים לצורך גישה לאיברי‬
‫המערכים‪.‬‬
‫את הת וכנית לעיל היה ניתן לכתוב גם תוך שימוש באריתמטיקה של מצביעים —‬
‫ובעובדה שמשתנה מערך הוא למעשה מצביע לאיבר הראשון במערך אותו הוא מייצג‪:‬‬

‫>‪#include <stdlib.h‬‬
‫>‪#include <stdio.h‬‬

‫)(‪int main‬‬
‫{‬
‫;]‪int arr[3‬‬
‫;)‪int* d_arr = (int*)malloc(sizeof(int) * 3‬‬

‫;)‪if (!d_arr) exit(1‬‬

‫‪// write access‬‬


‫‪*(arr + 0) = 7; // could also use *arr = 0‬‬
‫;‪*(d_arr + 1) = 4‬‬

‫‪// read access‬‬


‫;))‪printf("%d %d\n", *(arr + 0), *(d_arr + 1‬‬

‫;)‪free(d_arr‬‬
‫;‪return 0‬‬
‫}‬

‫;‪ - *(arr + 0) = 7‬בשורה זו נלקח ערכו של משתנה המערך‪ ,‬כלומר כתובת‬ ‫‪-‬‬
‫האיבר הראשון במערך‪ ,‬נוסף לו הערך ‪ ,2‬מה שמותיר אותו ללא שינוי‪ .‬לאחר‬
‫© דניאל פונכץ ‪0202‬‬
‫‪medium.com/@danielfonkaz‬‬

‫מכן ניגשים לערך בכתובת זו‪ ,‬כלומר לאיבר הראשון במערך‪ ,‬ומשנים את‬
‫ערכו ל‪.1-‬‬
‫;‪ - *(d_arr + 1) = 4‬באופן דומה גם בשורה זו נלקח ערכו של משתנה המערך‪,‬‬ ‫‪-‬‬
‫כלומר כתובת האיבר הראשון‪ ,‬אבל מוסף לו ‪ — 1‬כלומר דילוג אחד קדימה‪.‬‬
‫גודל כל דילוג הוא לצורך הענין ‪ 4‬בתים‪ ,‬מאחר ומדובר במערך של מספרים‬
‫שלמים‪ .‬לכן‪ ,‬נוסף בפועל לכתובת האיבר הראשון הערך ‪ ,4‬והכתובת‬
‫החדשה המתקבלת היא כתובת האיבר השני במערך — אליו ניגשים ומשנים‬
‫את ערכו ל‪.4-‬‬
‫;))‪ - printf(“%d %d\n”, *(arr + 0), *(d_arr + 1‬באופן דומה לשורות לעיל‪,‬‬ ‫‪-‬‬
‫גם כאן ניגשים לאיבר ה‪ 0-‬ולאיבר ה‪ 1-‬במערכים המתאימים — אך בשורה‬
‫זו מודפס ערכם‪ ,‬ללא שינויו‪.‬‬
‫אז מה ההבדל בין תחביר סוגריים מרובעים לבין תחביר אריתמטיקה של מצביעים?‬
‫למעשה אין הבדל ‪ .‬כאשר ניגשים לאיברי המערך בצורה ה”רגילה” (ללא שימוש‬
‫במצביעים) מאחורי הקלעים נעשה למעשה שימוש במצביע לאיבר הראשון (שהוא‬
‫כאמור משתנה המערך) כדי לגשת לאיבר המתאים‪ .‬כלומר‪ ,‬סוגריים מרובעים הם‬
‫למעשה “סוכר תחבירי” שנועד לאפשר עבודה נוחה יותר‪ ,‬ומתורגמים בפועל על ידי‬
‫הקומפיילר לתחביר אריתמטיקה של מצביעים‪.‬‬
‫‪ arr[0] = 7‬מתורגם ל← ‪*(arr + 0) = 7‬‬ ‫‪-‬‬
‫‪ arr[1] = 7‬מתורגם ל← ‪*(arr + 1) = 7‬‬ ‫‪-‬‬
‫ובאופן כללי‪,‬‬
‫]‪ arr[i‬מתורגם ל← )‪*(arr + i‬‬ ‫‪-‬‬
‫ש תי צורות הכתיבה שקולות ולכן למתכנת החופש לעבוד בכל אחת מהן‪ .‬ככלל‪ ,‬מומלץ‬
‫לעבוד בתחביר סוגריים מרובעים כאשר עובדים עם מצביעים המייצגים מערכים‪,‬‬
‫ובתחביר אריתמטיקה של מצביעים כאשר עובדים עם מצביעים שאינם מייצגים‬
‫מערכים ‪ ,‬מאחר וצורת עבודה זו קריאה יותר ופחות מסורבלת‪ .‬עם זאת‪ ,‬במקרים‬
‫מסוימים (למשל מערכים גנריים) כן מקובל לעבוד עם מערכים בצורת אריתמטיקה של‬
‫מצביעים‪.‬‬

You might also like