דוא"ל:
תפריט משתמש




שתף |

קורס:"שפת C"

שיעור 8: מצביעים

[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]

מתוך הספר: C - מדריך מקצועי       C - מדריך מקצועי
תוכן עניינים



הגדרת מצביע

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

לדוגמא, נתון קטע התכנית הבא:

 
void main()
{
          int x;
          float y;
          
          x=3;
          y=2.6;
}
 

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

ערך

כתובת

21340

21341

21342

x=3

21343

21344

21345

21346

y=2.6

21347

21348

21349

21350

בעמודה השמאלית מוצגות הכתובות בזיכרון באופן רציף, כל כתובת מתייחסת לבית (Byte) יחיד, כלומר לנתון בן 8 סיביות.

בעמודה הימנית מוצגים ערכי המשתנים שהוגדרו בתכנית, כאשר במערכת המסוימת שעליה הופעלה הדוגמא משתנה שלם הוא בן 4 בתים, וכך גם משתנה ממשי.

מהן כתובות המשתנים בתכנית?

    x - 21342
    y - 21346

נניח שאנו רוצים ששתי כתובות המשתנים y,x יוצבו במצביעים מתאימים xptr ו - yptr בהתאמה. ראשית צריך להגדיר אותם:

 
int    *xptr;
float *yptr;
 

על ידי האופרטור "*" הגדרנו את xptr ו- yptr מטיפוס מצביעים : xptr מצביע לשלם ו- yptr מצביע לממשי. כעת נרצה להציב להם את כתובות x  ו- y . כתובת המשתנה ניתנת ע"י אופרטור הכתובת "&":

 
xptr = &x;
yptr = &p;
 

פירושו של האופרטור & הוא "כתובתו של". ערכי המצביעים כעת:

    xptr  - 21342
    yptr - 21346


התכנית במלואה ותמונת הזיכרון מובאים בעמ' 191-192.

תרגול

קרא/י סעיף זה בספר ובצע/י את התרגיל שבעמ' 192.

גישה למשתנה דרך המצביע אליו

ניתן לשנות את ערך המשתנה ע"י המצביע אליו, לדוגמא:

 
void main()
{
          int x=2;
          int *px;
          
          px=&x;
          *px=36;
{
 

ערכו של x לאחר השורה האחרונה בתכנית שווה ל- 36.  כמו כן ניתן לבצע דרך מצביע למשתנה כל פעולה שניתן לבצע על המשתנה עצמו. דוגמאות:

 
*px = *px + 5;    /*     equivalent to:  x = x + 5     */
(*px)++;             /*     equivalent to:  x++                         */
*px++ = 5;                   /*     equivalent to: *px = 5;  px++          */
 

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

האופרטור "*" מורה למהדר לגשת לערך המשתנה המוצבע ע"י המצביע xptr שהוא x. כלומר משמעות האופרטור "*" היא "הנתון המוצבע על-ידי".

לדוגמא, בהגדרת המצביע

 
int *px;
 

אנו בעצם מכריזים "הנתון המוצבע על-ידי px הוא מסוג int".

ואילו בפעולת הצבה

 
*px=36;
 

אנו מורים למהדר "הצב 36 לנתון המוצבע ע"י px".

הערה : האופרטורים & ו- * הם בעצם הפכיים. האופרטור & מחזיר את כתובת המשתנה ואילו האופרטור * מחזיר את הנתון המוצבע ע"י הכתובת הנתונה.

מצביע למצביע

ניתן להגדיר מצביע למצביע ובכך להגדיל את רמת העקיפות בתכנית. מגדירים מצביע למצביע בצורה דומה להגדרת מצביע רגיל, לדוגמא:

 
int  ** ppi;
 

באופן זה ניתן לקבל את כתובתו של משתנה מסוג מצביע, לדוגמא:

 
int  i;
int *pi = &i;
int  ** ppi = &pi;
 
**ppi  = 4; /* i = 4 */
 

השימוש במצביעים למצביעים (או ברמה עקיפה יותר) אינו שכיח בשפת C כמו השימוש במצביעים מדרגה ראשונה.

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

העברת פרמטר לפונקציה ע"י מצביע

העברת פרמטרים ע"י ערך והעברה ע"י התייחסות

כפי שכבר ראינו, העברת הפרמטרים בשפת C היא עפ"י ערך (by value).

כלומר, שינוי הפרמטרים הפורמליים בתוך הפונקציה הנקראת אינו משפיע על הפרמטרים האקטואליים מכיוון שהם העתק שלהם.

לדוגמא, נניח שמוגדרת פונקציה בשם swap להחלפה בין ערכיהם של שני משתנים שלמים:

 
/* swap between 2 integers */
void swap(int a1, int a2)
{
          int a3 = a1;
          
          a1 = a2;
          a2 = a3;
}
 

והפונקציה הקוראת לה:

 
void main()
{
          int x=5, y=3;
 
        swap(x,y);
          printf("x=%d\n", x);
          printf("y=%d\n", y);
}
 

שאלה: מה יהיה פלט התכנית?

תשובה:

 
x=5
y=3
 

כלומר ערכי המשתנים לא שונו!!!

הפונקציה swap מגדירה שני פרמטרים פורמליים a1,a2. כאשר main קוראת ל- swap מועתקים ערכי x,y למשתנים a1,a2 בהתאמה.

בסופה של swap ערכיהם של a1 ו- a2 מוחלפים, אך עם חזרת הביצוע ל- main לא מושפעים המשתנים x,y והם מודפסים ללא שינוי:

צורה זו של העברת פרמטרים נקראת העברת פרמטרים ע"י ערך (by value), זאת מכיוון שרק ערך הפרמטרים מועבר מהפונקציה הקוראת לפונקציה הנקראת.

בשפת C זוהי השיטה היחידה המקובלת. בשפות מסוימות קיימת דרך אחרת להעברת פרמטרים - העברה ע"י התייחסות (by reference/var), לדוגמא, בפסקל וב- C++.



העברת פרמטרים ע"י התייחסות - שימוש במצביעים

אחד השימושים במצביעים הוא בהעברת פרמטר לפונקציה, כאשר היא אמורה לשנות את ערכו.

הרעיון: הפונקציה הקוראת תעביר כפרמטרים את כתובות המשתנים, והפונקציה הנקראת תיגש לנתונים המוצבעים על ידם ותשנה אותם.

נחזור לדוגמא הקודמת ונגדיר את הפונקציה swap אחרת:

 
#include <stdio.h>
 
/* swap between 2 integers */
void swap(int *pa1, int *pa2)
{
          int a3 = *pa1;
          
          *pa1 = *pa2;
          *pa2 = a3;
}
 
void main()
{
          int x=5, y=3;
 
          swap(&x,&y);
          printf("x=%d\n", x);
          printf("y=%d\n", y);
}
 

והפעם הפלט הוא כנדרש:

 
x=3
y=5
 


הסבר

הפונקציה swap מקבלת כפרמטרים 2 מצביעים:

 
void swap(int *pa1, int *pa2)
 

הפונקציה מחליפה בין ערכי המשתנים המוצבעים ע"י pa1 ו- pa2, שהם המשתנים x,y המוגדרים ב- main ואשר כתובותיהם הועברו כפרמטרים:

 
swap(&x,&y);
 

בעמ' 198-199 מוצגת מחסנית הקריאות (Call Stack) לאורך מהלך התכנית. עיין/י בעמודים אלו.

דוגמא נוספת - שימוש בפונקציה swap במיון

נתייחס לפונקצית מיון המערך שראינו בפרק 7, "מערכים". השתמשנו בפונקצית sort שביצעה מיון בשיטת "מיון בועות". תוך שימוש בפונקציה swap נוכל לפשט את פעולת sort:

  - הפונקציה swap():
 
void swap(int *pa1, int *pa2)
{
          int a3 = *pa1;
          
          *pa1 = *pa2;
          *pa2 = a3;
}
 
  - הפונקציה sort():
 
void sort(int arr[], int size)
{
          int i,j;
 
          for(i=0; i < size - 1; i++) 
                   for(j=0; j < size -1 - i; j++) 
                            if(arr[j] > arr[j+1]) 
                              swap(&arr[j], &arr[j+1]);
}
 

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

 
                   swap(&arr[j], &arr[j+1]);
 

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

שאלת חזרה

הסבר/י - תוך התייחסות להעברת פרמטרים ע"י ערך וע"י התייחסות - מדוע בקריאה לפונקציה scanf יש צורך להעביר כפרמטרים את כתובות המשתנים.

כמו כן, הסבר/י מדוע בפונקציה printf אין בכך צורך.

מצביעים ומערכים

שקילות בין מערך ומצביע

בשפת C מערך הוא מצביע, כלומר, שם המערך הוא כתובת תחילתו בזיכרון. לדוגמא, נגדיר מערך שלמים קצרים בשם array:

 
short array[5] = {45,56,12,98,12};
 

תמונת הזיכרון של המערך במערכת שבה short הוא בגודל 2 בתים:

כתובת

ערך

array[]=

243890

45

243892

56

243894

12

243896

98

243898

12



array הוא בעצם מצביע לאיבר הראשון במערך. כלומר ערכו של array בדוגמא זו הוא 243890. לכן ניתן לבצע עליו פעולות מצביעים כגון:

 
*array = 0;                  /* array[0] = 0 */
*(array+2) =8/* array[2] = 8 */
 

כמו כן ניתן להגדיר מצביע מתאים ולבצע פעולות חוזרות על איברי המערך:

 
short *p = array;
for(i=0; i<5; i++)
          *(p+i) = i;
 

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

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

 
#include <stdio.h>
void convert(char *p)
{
          *p        = 'A';
          *(p+1) = 'h';
          *(p+2) = 'l';
          *(p+3) = 'a';
          *(p+4) = 'n';
}
 

הפונקציה המשתמשת מעביר ל- convert() מערך תווים:

 
void main()
{
        char s[] = {'H', 'e', 'l', 'l', 'o'};
          int i;
          
        convert(s);
          for(i=0; i<5; i++)
                   putchar(s[i]);
}
 

הפלט:

 
Ahlan
 

מכיוון שמערך הוא מצביע, שינוי איבריו בפונקציה הנקראת משפיע אצל הפונקציה הקוראת.

דוגמאות נוספות מובאות בעמ' 202.

מצביעים והקצאות זיכרון

כאשר מגדירים מערך, בדרך כלל מקצים לאיבריו מקום בזיכרון. לדוגמא ההגדרה

 
char s[5];
 

או לחילופין,

 
char s[] = {'H', 'e', 'l', 'l', 'o'};
 

יוצרת מערך ומקצה 5 עמדות בזיכרון עבור איבריו. לעומת זאת אם נגדיר מצביע

 
char *s;
 

לא מוקצות עמדות זיכרון לאיברים כלשהם.

דוגמא לבעיה:

 
#include <stdio.h>
 
void main()
{        
          char s[5];
          char *p;
 
          int i;
 
          /* read 5 chars into the array pointed to by s */
          for(i=0; i<5; i++)
                   s[i] = getchar();  /* OK */
 
          /* read 5 chars into the array pointed to by p */
          for(i=0; i<5; i++)
               p[i] = getchar();   
                                      /* Runtime error!!  no memory allocated for the elements - memory access violation */
}
 

תמונת הזיכרון:

ערך

כתובת

p=839642

s[0]

s=839644

s[1]

s[2]

s[3]

s[4]

הבעיה: כאשר התכנית מנסה לגשת למקום p[i] , היא חורגת אל תא זיכרון שאינו שמור עבור p, וזה עלול לפגוע בתוכן של משתנים אחרים בתכנית. לדוגמא ניסיון לכתוב

 
p[3] = 'r';
 

ישנה (בהנחת תמונת הזיכרון הנ"ל) את ערכו של s[1] ל- 'r' !!! אנו נראה בפרק 12 כיצד ניתן להקצות מקום בזיכרון - באופן מפורש - עבור מצביעים.

הקבוע NULL

בשפת C מוגדר קבוע בשם NULL בעל ערך 0 , שבהצבתו למצביע מציין "שום מקום". לדוגמא ההוראה

 
int  *p = NULL;
 

מגדירה מצביע בשם p ומאתחלת אותו להצביע על "שום מקום", כלומר ערכו הוא 0. ללא אתחול זה ערכו של p איננו מוגדר, כלומר הוא מכיל "זבל".

אנו נראה בפרק 12, "הקצאת זיכרון דינמית ורשימות מקושרות", שימושים להצבות NULL למצביעים.

הקבוע NULL מוגדר בקבצי הממשק של הספריות התקניות stdio.h, stdlib.h.

האם קיים בכל זאת הבדל בין מצביע ומערך?

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

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

לכן לא ניתן לשנות את ערכו, וגם לא לקבל את כתובתו.

התכנית והסברה בעמ'  204-205 ממחישים את ההבדל בין מצביע למערך.

תרגיל

בצע/י את התרגיל  בעמ'  205-206.

פעולות חשבוניות על מצביעים

פעולות חשבוניות על מצביע מבוצעות בהתאם לטיפוסו, כלומר בהתאם לטיפוס הנתון עליו הוא מצביע. ככלל, מצביע ptr לטיפוס type

<type> 

  • <ptr>;

מקודם עפ"י גודל הטיפוס type. למשל  אם p מוגדר כמצביע לממשי (float *) אזי הביטוי

 p++

יגרום לקידומו של p  ב- 4 בתים, במערכת בה ממשי הוא בגודל 4. באופן כללי מתקיים

ptr + x       שקול ל-    ptr + x*sizeof(type)

ptr - x        שקול ל-     ptr - x*sizeof(type)

לדוגמא, נניח שמוגדרים המצביעים הבאים:

 
char           *pc;
int              *pi;
double       *pd;
float           array[5];
 

ובהנחה שנתונים גדלי הטיפוסים

    sizeof(char) = 1
    sizeof(int) = 4
    sizeof(float) = 4
    sizeof(double) = 8

הביטויים הבאים שקולים:

ביטוי

פירוש בחשבון של בתים

pc++

pc = pc + 1

pi + 2

pi  + 2*4

pd--

pd = pd - 8

pc - 4

pc - 4

array + 3

array + 3*4

מצביעים לפונקציות

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

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

לדוגמא, נתונה הפונקציה

 
int func1(int x)
{
          printf("\nfunc1: x=%d",x);
          return x+1;
}
 

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

 
          func1(17)
 

או ע"י כתובתה:

 
          (*func1)(17);
 

ניתן להגדיר מצביע לפונקציה באופן הבא:

 
          int (*fp1)(int);
 

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

 
          fp1 = func1;
 

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

וכעת ניתן לקרוא לפונקציה ע"י המצביע fp1:

 
printf("\nfunc1 returned %d.", fp1(3));
 

הפלט:

 
func1: x=3
func1 returned 4.
 

הסוגריים סביב המצביע (*fp1) בהגדרת המצביע חשובות, מכיוון שבלעדיהן

 
int *fp1(int);
 

fp1 יוגדר כפונקציה המחזירה מצביע ל- int.

דוגמא נוספת -  נגדיר פונקציה שנייה, func2:

 
int func2(int x, int y)
{
          printf("\nfunc2: x=%d y=%d", x, y);
          return x+y;
}
 

נגדיר מצביע לפונקציה ונציב לו את כתובתה:

 
int (*fp2)(int,int);
fp2 = func2;
 

וכעת נקרא לפונקציה ע"י המצביע:

 
printf("\nfunc2 returned %d.", fp2(8,10));
 

והפלט:

 
func2: x=8 y=10
func2 returned 18.
 

שימושים במצביעים לפונקציות

בעמ' 208-210 מובאת דוגמת שימוש במצביעים לפונקציות באמצעות פונקצית הספרייה qsort().

הגדרת טיפוס מצביע ומערכי מצביעים לפונקציות

בגלל מורכבות ההכרזות על מצביעים לפונקציות, רצוי להגדיר טיפוס מצביע לפונקציה ע"י  typedef:

 
typedef int (*FPTR)(int) ;
 

כעת FPTR הוא מסוג מצביע לפונקציה המקבלת כפרמטר int ומחזירה int, וניתן להשתמש בו כך:

 
FPTR  fp1 =  func1;
printf("\nfunc1 returned %d.", fp1(3));
 

כמו כן ניתן להגדיר מערך מצביעים לפונקציות ע"י

 
FPTR farr[3];
 

וכעת ניתן להציב ל- farr את כתובתה של func1 ולקרוא לה:

 
farr[0] = func1;
farr[0](34);
 

סיכום

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

תרגילי סיכום

בצע/י את תרגילי הסיכום בעמ' 212.



[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]