המדריך השלם
 

מצביעים (pointers)

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

לדוגמא:
int *pi - בהצהרה זו אנו אומרים 2 דברים: הראשון כי המשתנה pi הינו משתנה מסוג מצביע לשלם, והשני (*pi) הוא הערך המוצבע על ידי pi הוא שלם.
כלומר אם נסתכל בתוכנו של pi נראה כי מכיל כתובת לזיכרון, ו pi)*) הוא התוכן של הכתובת שמוכלת ב pi .

ניתן ליצור משתנים מסוג מצביע לכל טיפוסי הנתונים שהזכרנו עד כה: מצביע לשלם, לממשי וכו' ואף לטיפוסים מורכבים יותר: למערך ומבנה.

משתנה מסוג מצביע תופס 2 בתים בזיכרון (לשם אחסון ערך כתובת).
ניתן לבצע הקצאת זיכרון על ידי פקודת malloc (הקצאת זיכרון דינאמית - נסביר בהמשך ביתר פירוט) , למשל מעוניינים לבצע הקצאת זיכרון למשתנה pi נרשום:
pi = malloc(2);

פקודה זו מחזירה ערך של כתובת של בלוק בגודל 2 בתים (גודל של int).
בלוק הזיכרון מוקצה מתוך אזור זיכרון הנקרא ערימה (heap).

נניח כי הכתובת המוחזרת על ידי פקודת new היא FFF4 והתוכן של כתובת זו הוא 14, אזי ערכו של pi הוא FFF4 וערכו של (*pi) הוא 14.
כשאנו נרצה לקבל הקצאת זיכרון אנו נרצה לשנות את הערך של הכתובת שקיבלנו (שכן ערכה חסרת משמעות לגבינו) נעשה זאת על ידי הצבה בערך המוצבע כך:

pi = 20* - פנינו לכתובת של p ושינינו את ערכה.

בפרקים קודמים הסברנו כי האופרטור & מציין כתובת של משתנה ולכן:
אם נרצה לפנות לכתובת של pi נרשום: pi& .
אם נרצה לפנות לערכו נרשום: pi.
אם נרצה לפנות לערך המוצבע נרשום: pi*.

לא ניתן לבצע הצבת כתובת לאחר הצהרת על משתנה מצביע כך:

int *pi;
pi = FFF4; (לא תקין).


אך אם יש בידינו משנה שכבר הוקצה בעבורו מקום בזיכרון ניתן להציב את כתובתו במצביע כך:
int x;
int *pi;
pi = &x;


אם הכתובת של x היא B206 אזי pi יכיל את כתובת זו.

זיכרון דינמי

ישנן שתי שיטות לביצוע הקצאת זיכרון: הקצאת זיכרון סטטית והקצאת זיכרון דינאמית.

הקצאת זיכרון סטטית - המהדר קובע את דרישות האחסון על פי הצהרת המשתנים (כמובן בזמן ההידור).
הקצאת זיכרון דינאמית - הקצאת מקום נעשה בזמן ביצוע התוכנית. (על יד פקודת malloc).

מכיוון שהקצאת הזיכרון נעשתה בזמן ביצוע התוכנית יש לדאוג לשחרר את הזיכרון ,נעשה זאת על ידי פקודת free (name_pointer).

חשוב מאוד לשחרר את הזיכרון בעבור משתנים שההקצאה עבורם הייתה דינאמית, אם לא נעשה זאת המקום בזיכרון יהיה תפוס ולא נוכל לשנות ולהשתמש בו שוב. מצב זה נקרא דליפת זיכרון - במצב זה מוקצה זיכרון שוב ושוב ואם אין משחררים מגיעים למצב שאין עוד מקום בזיכרון להקצאות נוספות הנדרשות על ידי התוכנית. פונקציה זו נמצאת בספרייה alloc.h.
משתנה שבעבורו הייתה הקצאת זיכרון סטטית המהדר דואג לשחרור הזיכרון .

נסכם את מה שהסברנו עד כה: מצביע הינו משתנה שמצביע למשתנה אחר, משמע שהמצביע תוכנו הוא כתובת של משתנה אחר. המצביע לא מכיל ערך במובן המקובל אלא מכיל כתובת של משתנה המכיל ערך סטנדרטי.
מכיוון שמצביע מכיל כתובת ולא ערך הוא מחולק ל- 2: המצביע המכיל כתובת וכתובת מוצבעת המכילה ערך.

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

הערה:
בפונקצית ההדפסה יש להשתמש בתבנית ההמרה p% בכדי להדפיס כתובת.

דוגמא 1:

#include <stdio.h>
#include <conio.h>

void main()
{
    int i, j;
    int *pi; // a pointer to an integer

    printf("The address of i is: %p\n\r",&i);
    pi = &i;
    printf("The value of pi is: %p\n\r", pi);
    *pi = 5;
    printf("The value of i is: %d\n\r", i);
    j = i;
    printf("The value of j is: %d\n\r", j);
    printf("The value of *pi is: %d", *pi);
}
העבר את העכבר מעל הדוגמא כדי לראות הסבר מפורט


הערה: כשמצהירים על מצביע p אין הוא מצביע על ערך "חוקי", חובה להציב ערך כתובת לפני שמשתמשים בערך המוצבע ( *p).
לדוגמא:
int *pi;
*pi = 100; (לא חוקי)


פעולות אלו ייצרו טעויות העלולות לגרום לקריסת התוכנית.
הדרך הנכונה היא:
int *pi;
int x;
pi = &x;
*pi = 5;


דוגמא 2:

#include <stdio.h>
#include <conio.h>
#include <alloc.h>

void main()
{
    int i = 10;
    int *pi;

    printf ("&i= %p i= %d \n\r", &i, i);
    printf ("&pi= %p pi= %p *pi= %d\n\r", &pi, pi, *pi);

    pi = &i;
    printf ("&pi= %p pi= %p *pi= %d\n\r", &pi, pi, *pi);

    (*pi)++;
    printf ("&i= %p i= %d\n\r", &i, i);
    printf ("&pi= %p pi= %p *pi= %d\n\r", &pi, pi, *pi);
    pi = (int*)malloc(sizeof(int*));
    *pi = 100;
    printf ("&i= %p i= %d\n\r", &i, i);
    printf ("&pi= %p pi= %p *pi= %d", &pi, pi, *pi);

    free(pi);
}
העבר את העכבר מעל הדוגמא כדי לראות הסבר מפורט


מצביע כפרמטר של פונקציה

נפתח בדוגמא:

דוגמא 3:

#include <stdio.h>
#include <conio.h>

void swap (int *pi, int *pj)
{
    int temp;
    printf ("&ip= %p pi= %p *pi= %d\n\r", &pi, pi, *pi);
    printf ("&jp= %p pj= %p *pj= %d\n\r", &pj, pj, *pj);

    temp = *pi;
    *pi = *pj;
    *pj = temp;
    printf ("\n\rAfter swapping there values:\n\n\r");
    printf ("&ip= %p pi= %p *pi= %d\n\r", &pi, pi, *pi);
    printf ("&jp= %p pj= %p *pj= %d\n\r", &pj, pj, *pj);
}

void main()
{
    int i = 10;
    int j = 20;
    swap (&i, &j);
}
העבר את העכבר מעל הדוגמא כדי לראות הסבר מפורט


הערה לדוגמא: אם הפרמטרים של פונקצית swap לא היו מצביעים לא היינו מקבלים את אותה התוצאה: הערכים (בלבד) של iו- j היו נשלחים לפונקציה וערכי i ו-j המקוריים היו נשארים כמו שהם.

הערה:
מומלץ לעקוב אחר כל הכתובות של המשתנים ואחר השינויים.

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

דוגמא 4:

#include <stdio.h>
#include <conio.h>
#include <alloc.h>

void fun1 (int *p)
{
    printf ("In fun1: &p= %p p= %p *p= %d\n\r", &p, p, *p);
}

void fun2 (int *p)
{
    printf ("In fun2: &p= %p p= %p *p= %d\n\r", &p, p, *p);
}

void fun3 (int p)
{
    printf ("In fun3: &p= %p p= %d\n\r", &p, p);
}

void main()
{
    int *p = (int*)malloc (sizeof(int));
    *p = 100;
    printf ("In main: &p= %p p= %p *p= %d\n\r", &p, p, *p);
    fun1(p);
    fun2(& *p);
    fun3(*p);

    free(p);
}
:פונקציות fun1 ו fun2 זהות מכיוון ש& p* משמעו הכתובת של המשתנה p* וזהו בדיוק p.
כל השאר מתקיים כפי שהסברנו למעלה.


הרחבה של הדוגמא הקודמת:

דוגמא 5:

#include <stdio.h>
#include <conio.h>
#include <alloc.h>

void fun1 (int *p)
{
    int i = 10;
    p = &i;
    printf ("In fun1: &p= %p p= %p *p= %d\n\r", &p, p, *p);
}

void fun2 (int *p)
{
    *p = 1;
    printf ("In fun2: &p= %p p= %p *p= %d\n\r", &p, p, *p);
}

void fun3 (int p)
{
    p = 2;
    printf ("In fun3: &p= %p p= %d\n\r", &p, p);
}

void fun4 (int* p)
{
    int i = 5;
    *p = i;
    printf ("In fun2: &p= %p p= %p *p= %d\n\r", &p, p, *p);
}

void main()
{
    int *p = (int*)malloc(sizeof(int));
    *p = 8;
    printf ("In main: &p= %p p= %p *p= %d\n\r", &p, p, *p);
    fun1(p);
    fun2(& *p);
    fun3(*p);
    fun4(&*p);
    printf ("In main: &p= %p p= %p *p= %d\n\r", &p, p, *p);
    free(p);
}


כתובת של מערך

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

int arr_i[5];
arr_i 2000 2002 2004 2006 2008
2000 arr_i[4] arr_i[3] arr_i[2] arr_i[1] arr_i[0]


כל התאים הם מסוג שלם , שם המערך arr_i יהיה משתנה מצביע מסוג שלם ומכיל את הכתובת של האיבר הראשון במערך (2000).

ניתן לגשת לאיבר הראשון במערך באופן הבא:
*(arr_i)
ולאיבר השני:
*(arr_i+1)
לאיבר האחרון:
*(arr_i+4)
נכליל זאת ,על מנת לגשת לכתובת של a[i]:
*(a+i-1).
הסבר:
כשאר כותבים arr_i+1 הכוונה לכתובת הבאה אחרי הכתובת של arr_i ומכיוון שהמערך שלפנינו הוא מסוג שלם הקפיצות יהיו של שני בתים (בניגוד לאינטואיציה הקובעת שהקפיצות יהיו בבית אחד). העניין נעוץ באריתמטיקה של מצביעים: לדוגמא אם p הוא מצביע מסוג ממשי אזי הפעולה p++ תקדם את p ב 4 בתים וזאת מכיוון שגודלו בזיכרון של ממשי הוא 4.
(ניתן לבצע גם הגדלה מאוחרת ומוקדמת של מצביעים , חיסור וחיבור בשלם).

דוגמא 6:

#include <stdio.h>
#include <conio.h>

void main()
{
    int i = 0;
    float arr_f[] = {1.1, 2.2, 3.3, 4.4, 5.5};

    for ( ; i < 5; i++)
       printf ("array[%d] = %f\n\r",i , *(arr_f+i));
}


דוגמא 7:

#include <stdio.h>
#include <conio.h>
#include <string.h>

void main()
{
    char *string = "Hello Student";
    puts (string+1);
    puts (++string);
    puts (string-1);
    puts (string+2);
}
העבר את העכבר מעל הדוגמא כדי לראות הסבר מפורט


פקודת typedef

ניתן להגדיר מחדש כל טיפוס על ידי פקודת typedef .

תבנית:
typedef character new_character;

יחליף את הטיפוסcharacter בטיפוס החדש new_charecter - כלומר אם נגדיר משתנה מהטיפוס החדש הוא יתייחס אליו כמו בהתייחסותו אל משתנה שהוגדר כטיפוס הישן. לדוגמא:

typedef int* pint;
pint p;
משמע: p הוא משתנה מצביע לשלם מכיוון שהגדרנו מצביע לשלם כ- pint- המהדר מחליף כל מופע של pint ב *int .

האופרטור sizeof

האופרטור sizeof מחזיר את גודלו של הטיפוס בבתים.

תבנית:
sizeof (name_of_charecter) לדוגמא:
sizeof (float) - מחזיר 4. אילו הינו כותבים:
float f
sizeof(f) - (4) הינו מקבלים אותו פלט .


הכללת הקובץ alloc.h:
ספרייה זו מכילה פונקציות בעבור פעולות של הקצאה ושחרור זיכרון.
כבר דיברנו על הפקודה free לשחרור זיכרון שהוקצה באופן דינאמי.

הקצאה דינמית - malloc

כעת נכיר את הפונקציה להקצאת זיכרון בזמן ביצוע malloc - לשם כך יש צורך להגדיר מצביע למערך ולציין את גודל המערך בבתים.
כפי שהסברנו מצביע למערך יכיל את הכתובת של האיבר הראשון במערך.
גודלו של המערך הוא: גודל הטיפוס*מספר איברים במערך לכן מערך של שלמים בגודל 5 יכיל 10 בתים (כי כל שלם תופס 2 בתים).

התבנית לשימוש בפונקציה malloc():
variable_pointer = (pointer_type) malloc (size_of_memory);

דוגמא:
int size, *p_list;
printf("Enter the number of elements:");
scanf("%d", &size);
p_list = (int*)malloc (size * sizeof(int));


הקצאת זיכרון בעזרת הפונקציה calloc

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

התבנית לשימוש בפונקציה calloc():
variable_pointer = (casting) calloc (num_of_elements, size_of_element);

יש צורך ב casting לפני הפקודה calloc (כמו בפונקציה (malloc לפי סוג הטיפוס שבעבורו מקצים זיכרון.

דוגמא:
int size, *arr_p;
printf("Enter the number of elements:");
scanf("%d", &size);
arr_p = (int*)calloc (size, sizeof(int));


הערה:
כאשר מקצים זיכרון הערך המוחזר על ידי פונקצית הקצאת זיכרון היא כתובת.
יש מקרים שבעבורם הקצאת הזיכרון נכשלת והערך המוחזר על ידי הפונקציה היא null ומכיוון שלא קיבלנו כתובת לא ניתן להשתמש במצביע כמצביע "חוקי" ונרצה לסיים את התוכנית. לכן לאחר הקצאת זיכרון תמיד צריך לבדוק אם ההקצאה הצליחה.

דוגמא 8:

#include <stdio.h>
#include <stdio.h>
#include <stdio.h>

void main()
{
    long *l_list;
    l_list =(long*) malloc (5*sizeof(long));
    if (l_list == NULL)
    {
       printf ("Failed to allocate memory");
       return;
    }
    printf ("The address of l_list is: %p", &l_list);
    free(l_list);
}


שינוי גודל מערך דינאמי בזמן ביצוע

מערך דינאמי הוא בעצם מערך שניתן לשנות את גודלו. מצהירים על מצביע בגודל מסוים (מספר איברים כרצוני) על ידי הפונקציות שהזכרנו malloc או calloc, כעת נרצה לשנות את גודלו של המערך בזמן ביצוע התוכנית לשל כך נשתמש בפונקציה realloc.

התבנית לשימוש בפונקציה realloc():
variable_pointer = (casting) realloc (variable_pointer, size_of_memory);

דוגמא 9:

#include <stdio.h>
#include <alloc.h>
#include <stdlib.h>
#include <conio.h>

void main()
{
    int i, leng = 5;
    int *i_arr = (int*) calloc (leng, sizeof(int));

    if (i_arr == NULL)
    {
       printf ("Failed to allocate memory");
       return;
    }

    for (i = 0; i < leng; i++)
       i_arr[i] = i*i;

    for(i = 0; i < leng; i++)
       printf("%d ", i_arr[i]);

    leng += leng;
    i_arr = (int*)realloc (i_arr, sizeof(int));

    if (i_arr == NULL)
    {
       printf ("Failed to allocate memory");
       return;
    }
    printf("\n\rChanging the size of i_arr to %d:\n\r", leng);

    for (i = leng/2; i < leng; i++)
       i_arr[i] = i*i;

    printf("The num i_arr is:\n\r");
    for(i = 0; i < leng; i++)
       printf("%d ", i_arr[i]);
}



מבחן
© איתן 2003. כל הזכויות שמורות למערכת המידע איתן.