640 likes | 915 Views
מצביעים (Pointers). הגדרות בסיסיות העברה לפונקציה by reference אריתמטיקה של פוינטרים פוינטרים ומערכים הקצאה דינאמית. הוכן ע"י ד"ר דני קוטלר, המכללה האקדמית תל-חי. תזכורת: משתנה. משתנה הוא מקום שמור בזיכרון, המשמש את התוכנית לשמירת נתונים.
E N D
מצביעים (Pointers) הגדרות בסיסיות העברה לפונקציה by reference אריתמטיקה של פוינטרים פוינטרים ומערכים הקצאה דינאמית הוכן ע"י ד"ר דני קוטלר, המכללה האקדמית תל-חי מבוא למדעי המחשב - שיעור 8
תזכורת: משתנה • משתנה הוא מקום שמור בזיכרון, המשמש את התוכנית לשמירת נתונים. • לכל משתנה יש ערך מספרי, שהוא המספר הבינארי השמור במשתנה. • למשתנה יש שם המשמש לגישה אליו: קריאת הערך שלו או שינוי הערך שלו. • לכל משתנה יש כתובת בזיכרון, בה הוא שמור int x = 17; 0065FDF0 00010001 עד עכשיו לא התייחסנו לזה מבוא למדעי המחשב - שיעור 8
שם המשתנה לעומת הכתובת שלו • שם המשתנה משמש את שפת התיכנות העלית ונועד להקל על המתכנת את הגישה למשתנה • בתהליך ההידור נעזר המהדר בטבלה הקרויה symbol table שמתאימה לכל משתנה את כתובתו בזיכרון. בתרגום לשפת מכונה כל שם משתנה מוחלף בכתובת שלו. • רוב שפות התכנות העליות משתמשות רק בשמות המשתנים כדי לגשת אליהםואין להן כלים לעבודה עם כתובות. • שפת C מאפשרת עבודה עם כתובות של משתנים באמצעות מצביעים(פוינטרים). מבוא למדעי המחשב - שיעור 8
מה מאפשרת העבודה עם פוינטרים? • העברת כתובות של משתנים לפונקציות (העברה by reference) • גמישות בניהול זיכרון: • הקצאת זיכרון בזמן ריצה. • שיחרור זיכרון ברגע שאינו נחוץ. • ניהול זיכרון דורש מהמתכנת מיומנות. עבודה לא נכונה עם פוינטרים עלולה לגרום לנזקים כמו דריסת זיכרון, או דליפת זיכרון. (אל דאגה - כל המושגים האלה יובנו בהמשך) מבוא למדעי המחשב - שיעור 8
מהו פוינטר? • פוינטר הוא משתנה שהערכים שהוא מקבל הם כתובות בזיכרון. לרוב אלה כתובות של משתנים אחרים. • לכל טיפוס יש את טיפוס הפוינטר שלו: • פוינטר ל int – מיועד לכתובות של int • פוינטר ל char– מיועד לכתובות של char • פוינטר ל double– מיועד לכתובות של double • וגם: פוינטר לפוינטר ל int – מיועד לכתובות של פוינטרים ל int • (פוינטר ל int יכול להכיל רק כתובת של משתנה מטיפוס int וכן הלאה. הסיבה לכך תובן בהמשך) מבוא למדעי המחשב - שיעור 8
הגדרת פוינטר הטיפוס שעליו מצביע הפוינטר שם הפוינטר • סינטקס: • למשל: • ואפשר גם • וגם באותה פקודה עם משתנים רגילים: type*pointer_name; int*p; char *s; int*p; int* p; int* p; int*p, x; intx,*p; int רגיל int רגיל פוינטר פוינטר מבוא למדעי המחשב - שיעור 8
הצבת ערך לפוינטר • כדי להציב כתובת לתוך פוינטר נשתמש באופרטור & (אופרטור הכתובת) • בכל המקרים הנ"ל ערך הפוינטר p הוא הכתובת של המשתנה x. במקרה כזה אומרים ש p מצביע על x double x; double *p; p = &x; double x, *p=&x; double x; double *p=&x; השמה איתחול מבוא למדעי המחשב - שיעור 8
סימון מקובל • כאשר פוינטר מצביע על משתנה • נהוג לסמן זאת כך: double x; double *p; p = &x; p x מבוא למדעי המחשב - שיעור 8
השמה של ערך מפורש • טכנית, ניתן להציב ערך מספרי מפורש לתוך פוינטר: • כיוון שהכתובת המפורשת לא בהכרח פנויה לשימוש התוכנית שלנו, וקרוב לוודאי שהיא בשימוש אחר, מומלץ לא לבצע השמות מפורשות. גישה לכתובת שאינה מוקצה לנו נקראת דריסת זיכרון int*p = (int *)100; כשר, אבל מסריח! המרה לטיפוס "כתובת של int" מבוא למדעי המחשב - שיעור 8
הכתובת אפס 0 • כתובת מפורשת אחת שכן נשתמש בה היא הכתובת אפס: 0 • הכתובת 0 לא קיימת ומשתמשים בה כאשר רוצים שפוינטר לא יצביע על כתובת כלשהי (למשל באיתחול) • למשל או • NULL הוא קבוע סימבולי שלם שערכו 0 ונהוג להשתמש בו לציון הכתובת 0. אין צורך בהמרה double *p = NULL; double *p = 0; מבוא למדעי המחשב - שיעור 8
גישה למשתנה באמצעות פוינטר • האופרטור * (dereference operator)מאפשר גישה לכתובת עליה מצביע פוינטר. • נניח ש p מצביע על x: • במצב זה הביטוי *p זהה לביטוי x: p intx = 5, *p=&x; x printf("*p: %d ",*p); *p: 5 *p = 7; printf(" x: %d ",x); x: 7 מבוא למדעי המחשב - שיעור 8
האופרטור * • האופרטור * מבצע את הפעולה ההפוכה לזו של האופרטור &. • הביטוי *&x זהה לביטוי x • הביטוי &*p זהה לביטוי p • האופרטור * יהיה שימושי במיוחד כאשר פוינטר יצביע על כתובת שאין לה שם של משתנה. p * x & מבוא למדעי המחשב - שיעור 8
ההבדל בין *לאופרטור * • יש הבדל בין ה *המופיע בהגדרת פוינטר לבין האופרטור *, המוצמד לפוינטר שכבר הוגדר קודם. intx; int *p=&x; *p = 7; intx; int *p = &x; הגדרת פוינטר השמה של כתובת אופרטור * השמה של int מבוא למדעי המחשב - שיעור 8
הערה כללית על שימוש בתווים בשפת C • לתו מסויים יכולים להיות תפקידים שונים בשפה. • דוגמה: * • כפל • הגדרת פוינטר • האופרטור * • דוגמה: % • חלק ממציין פורמט בתוך מחרוזת הבקרה של printf ו scanf (למשל: “%d”) • אופרטור השארית • המהדר "מבין" למה התכוון המתכנת לפי ההקשר מבוא למדעי המחשב - שיעור 8
הדפסת הערך של פוינטר • ניתן להדפיס את הערך של פוינטר (כלומר, הכתובת) באמצעות מציין הפורמט %p • הערך של הפוינטר מודפס כמספר בבסיס 16 (הספרות 0-9 בתוספת האותיות A-F) מבוא למדעי המחשב - שיעור 8
דוגמה inta=1, b=2; int* p = &b; printf("a = %d and its address is %p\n", a, &a); printf("b = %d and its address is %p\n", b, p); printf("p(hexa) = %p p(decimal) = %d\n", p, p); printf("the address of p is %p\n", &p); a = 1 and its address is 0018FF2C b = 2 and its address is 0018FF20 p(hexa) = 0018FF20 p(decimal) = 1638176 the address of p is 0018FF14 מבוא למדעי המחשב - שיעור 8
כתובת של פוינטר • פוינטר הוא משתנה בפני עצמו וגם לו יש כתובת • כדי לטפל בכתובת של פוינטר יש צורך בפוינטר מיוחד - פוינטר לפוינטר • למשל, כדי לטפל בכתובת של פוינטר ל int יש צורך בפוינטר לפוינטר ל int. • הגדרה: intx; int *p = &x; int**q = &p; הטיפוס עליו מצביע q q הוא פוינטר מבוא למדעי המחשב - שיעור 8
הדגמה: int n=10, *p=&n, *q=(int *)200; double x=3.141592654, *r=&x, **s=&r; printf("n = %d *p = %d\n",n,*p); printf("&n = %p p = %p\n",&n,p); printf("q(hexa) = %p q(decimal) = %d\n",q,q); printf("size of p: %d\n",sizeof(p)); printf("x = %f *r = %f\n",x,*r); printf("r = %p *s = %p\n",r,*s); printf("s = %p &s = %p\n",s, &s); printf("size of r: %d, size of s: %d\n",sizeof(r),sizeof(s)); n = 10 *p = 10 &n = 0018FF2C p = 0018FF2C q(hexa) = 000000C8 q(decimal) = 200 size of p: 4 x = 3.141593 *r = 3.141593 r = 0018FF04 *s = 0018FF04 s = 0018FEF8 &s = 0018FEEC size of r: 4, size of s: 4 לכל הפוינטרים אותו גודל מבוא למדעי המחשב - שיעור 8 int ***
העברת משתנה לפונקציה void swap(int a, int b) { inttemp=a; a=b; b=temp; { int main() } inta=1, b=2; swap(a,b); printf("a=%d, b=%d\n",a,b); return0; { a=1, b=2 aו b לא השתנו שאלה: במה זה שונה מהפונקציה swap שראינו בשיעור הקודם? מבוא למדעי המחשב - שיעור 8
העברה by value • העברת ארגומנט לפונקציה היא העברה by value. • כלומר, הפונקציה מעתיקה את הערך שהועבר לה למשתנה פנימי שלה. • כל שינוי שיתבצע על המשתנה הפנימי של הפונקציה לא ישפיע על הארגומנט החיצוני • לדוגמה, בתוכנית הנ"ל יש שני משתנים שונים בשם a, אחד של הפונקציה main ואחד של הפונקציה swap (וכנ"ל עבור b) מבוא למדעי המחשב - שיעור 8
העברה by value לעומת by reference by reference by value a a main main שונים a swap swap אותו a מבוא למדעי המחשב - שיעור 8
העברה by reference • העברה by referenceהיא העברה בה הארגומנט עצמו מועבר לפונקציה (ולא העתק שלו). • דוגמה: העברה של מערך לפונקציה. • בהעברה by reference מועברת הכתובת של הארגומנט לפונקציה. • העברה של כתובת נעשית באמצעות פוינטר. מבוא למדעי המחשב - שיעור 8
העברה by reference • יתרונות • ניתן לשנות משתנה חיצוני של הפונקציה (אם רוצים בכך) • חיסכון במקום בזיכרון, כי לא נוצר עותק. • חיסרון: • עלולים בטעות לשנות משתנה חיצוני של הפונקציה (גם אם לא מעוניינים בכך) מבוא למדעי המחשב - שיעור 8
העברה by reference • העברה לפונקציה: מעבירים את הכתובת של המשתנה • פרמטר הפונקציה: פוינטר • גישה בתוך הפונקציה:האופרטור * int a=1, b=2; swap(&a, &b); p q a b void swap(int*p, int*q) { int temp = *p; *p = *q; *q = temp; { מבוא למדעי המחשב - שיעור 8
העברת כתובת לפונקציה void swap(int *p, int *q) { int temp = *p; *p = *q; *q = temp; { int main() } int a=1, b=2; swap(&a, &b); printf("a=%d, b=%d\n",a,b); return 0; { a=2, b=1 הפעם, aו bהשתנו מבוא למדעי המחשב - שיעור 8
דוגמה: bubble sort void swap(int *p, int *q) { int temp = *p; *p = *q; *q = temp; { voidbubble_sort(int a[], int size) { inti, j; for(i = size-1; i > 0; i--) for(j=0; j < i; ++j) if(a[j] > a[j+1]) swap(&a[j], &a[j+1]); } מבוא למדעי המחשב - שיעור 8
דוגמה: scanf intx; scanf("%d", &x); כדי ש scanf תוכל לשנות את x יש להעביר לה את הכתובת intx, *p=&x; scanf("%d", p); אפשר להעביר כתובת באמצעות פוינטר intx; scanf("%d", x); שאלה: מה יקרה אם נשכח את האופרטור &? מבוא למדעי המחשב - שיעור 8
אריתמטיקה של פוינטרים • שאלה: אם הערכים שפוינטרים מכילים הם כתובות, מדוע צריך פוינטר מיוחד לכל טיפוס? • תשובה א': האופרטור *. כאשר רושמים *p=5, אופן ההשמה של 5 בתוך הכתובת תלוי בטיפוס המשתנה השמור באותה כתובת • תשובה ב': אריתמטיקה של פוינטרים. פעולתם של אופרטורים אריתמטיים שפועלים על פוינטרים תלויה בטיפוס הפוינטר. מבוא למדעי המחשב - שיעור 8
האופרטור ++ intx, *p=&x; ++p; x p p p אפשר גם p++ (ההבדל ידוע) sizeof(int) char c, *p=&c; ++p; c אפשר גם p++ sizeof(char) double y, *p=&y; ++p; y אפשר גם p++ sizeof(double) מבוא למדעי המחשב - שיעור 8
האופרטור -- x intx, *p=&x; --p; p p p אפשר גם p-- (ההבדל ידוע) sizeof(int) char c, *p=&c; --p; c אפשר גם p-- sizeof(char) y double y, *p=&y; --p; sizeof(double) אפשר גם p-- מבוא למדעי המחשב - שיעור 8
האופרטור + • סינטקס: • דוגמה: • באופן דומה מוגדר גם אופרטור - pointer = pointer + int p q intx, *p=&x; int *q = p+5; x 5*sizeof(int) pointer = pointer - int מבוא למדעי המחשב - שיעור 8
האופרטור - • סינטקס: • דוגמה: int = pointer - pointer q p double x, y; double *p=&x, *q=&y; int n = p-q; x y n = (number of addresses between &y and &x) / sizeof(double) מבוא למדעי המחשב - שיעור 8
פוינטרים ומערכים • מערך הוא פוינטר קבוע: a הוא פוינטר קבוע double a[10]; הדפסת כתובת תחילת המערך a פוינטר קבוע. לא ניתן לשנות את הערך שלו מה אסור? מה מותר? a = &x; printf("%p", a); a++; ++a; *a = 1; זהה ל a[0] = 1 a--; --a; double *p = a+5; זהה ל p = &a[5] int n = p - a; מספר כתובות ה int בין a ל p מבוא למדעי המחשב - שיעור 8
פוינטרים ומערכים • מבחינת סינטקס אפשר להתייחס למערכים ופוינטרים באותה צורה a p inta[10], *p=a, n; מבוא למדעי המחשב - שיעור 8
הערה על העברת מערך לפונקציה • כיוון שמערך הוא פוינטר, כאשר מועבר מערך לפונקציה, למעשה מועברת כתובת • מכאן שהעברה של מערך לפונקציה היא העברה by reference • זאת הסיבה שכאשר מערך מועבר לפונקציה, מועבר המערך עצמו ולא העתק שלו, לכן הפונקציה יכולה לשנות את ערכי המערך המועבר אליה. מבוא למדעי המחשב - שיעור 8
דוגמה: bubble_sort כתיב מערכים כתיב פוינטרים voidbubble_sort( int a[], int n) { registerinti, j; for(i = n-1; i > 0; --i) for(j = 0; j < i; ++j) if(a[j] > a[j+1]) swap(&a[j], &a[j+1]); } void bubble_sort2( int *p , int n) { registerinti, j; for(i = n-1; i > 0; --i) for(j = 0; j < i; ++j) if(*(p+j) > *(p+j+1)) swap(p+j, p+j+1); } מבוא למדעי המחשב - שיעור 8
דוגמה: חיפוש בינארי כתיב מערכים intbinsearch(intnum,int a[],intsize) { intlow = 0; inthigh = size; intmid; while(low < high){ mid = (high + low) / 2; if(num < a[mid]) high = mid; elseif(num > a[mid]) low = mid +1; else returnmid; } return-1; } מבוא למדעי המחשב - שיעור 8
דוגמה: חיפוש בינארי החזרת הכתובת בה נמצא המספר כתיב פוינטרים int *binsearch(intnum, int *start, int*end) { int*low = start; int*high = end; int*mid; while(low < high) { mid = low + (high - low) / 2; if(num < *mid) high = mid; elseif (num > *mid) low = mid +1; else returnmid; } returnNULL; } חיפוש בין שתי כתובות אם המספר לא נמצא, מוחזרת כתובת שלא קיימת מבוא למדעי המחשב - שיעור 8
החזרת כמה ערכים מפונקציה • העברה by reference מאפשרת להחזיר תוצאת חישוב מפונקציה גם דרך אחד הפרמטרים (בנוסף לערך המוחזר הרגיל) • בצורה כזאת ניתן להחזיר יותר מערך אחד מפונקציה • דוגמה לכזאת פונקציה היא scanf. הפונקציה קוראת ערכים מהקלט לתוך הפרמטרים שמועברים אליה (כולם כתובות) וגם מחזירה את מספר הקריאות התקינות שביצעה. מבוא למדעי המחשב - שיעור 8
דוגמה: min_max • הפונקציה מקבלת מערך ומוצאת את הערך המינימלי ואת הערך המקסימלי במערך. כתובת החזרת הערך המינימלי כתובת החזרת הערך המקסימלי voidmin_max(int *a, int *pmin, int *pmax, int n) { inti; *pmin = *pmax = a[0]; for(i=1; i<n;i++) { *pmax=(a[i] > *pmax)? a[i]:*pmax; *pmin=(a[i] < *pmin)? a[i]:*pmin; } } עידכון הערך המקסימלי עידכון הערך המינימלי מבוא למדעי המחשב - שיעור 8
הקצאה דינמית • כפי שראינו בפרק על מערכים, גודל מערך צריך להיות מספר קבוע. • הסיבה לכך היא שכמות הזיכרון המוקצה לתוכנית מתוך הזיכרון ה"רגיל" (ה stack) צריכה להיות ידועה בזמן הידור התוכנית. • לכן, גודל מערך לא יכול להיות ביטוי שיש בו משתנה. למשל, הקוד הבא אינו חוקי: double a[10]; גם אם הערך של n ידוע, גודל המערך לא יכול להכיל שם של משתנה. intn=5; double a[n]; מבוא למדעי המחשב - שיעור 8
הקצאה דינמית • בחלק מהמקרים לא ניתן לדעת לפני הרצת התוכנית מה יהיה גודל המערך הנחוץ. • דוגמה: תוכנית המשקללת ציונים של תלמידים. לא ניתן לדעת מראש כמה תלמידים יהיו. • המושג הקצאה דינמית מתייחס להקצאת זיכרון בגודל הידוע רק בזמן ריצה. ההקצאה לא מתבצעת במקום בו מוקצים המשתנים הרגילים (ה stack) אלא במקום מיוחד (ה heap) מבוא למדעי המחשב - שיעור 8
מערך מוקצה דינמית • כדי להקצות מערך באופן דינמי יש צורך בפוינטר המצביע על טיפוס המערך • ברגע שידוע גודל המערך נשתמש באחת הפונקציות מהספרייה stdlib.h כדי להקצות: • malloc • calloc • realloc להקצאת מערך מטיפוס int int*p; הקצאה ראשונית שינוי גודל ההקצאה מבוא למדעי המחשב - שיעור 8
הפונקציה malloc המרה לטיפוס הפוינטר כתובת תחילת ההקצאה מספר הבתים המוקצים • סינטקס: • פרמטר: מספר שלם המציין את מספר הבתים המוקצים • ערך מוחזר: • אם ההקצאה הצליחה - כתובת תחילת הזיכרון המוקצה • אם ההקצאה לא הצליחה - NULL pointer= (cast) malloc(number of bytes); מבוא למדעי המחשב - שיעור 8
הפונקציה malloc • דוגמה: int n; int*a; printf(“Enter array size: “); scanf(“%d”, &n); if (n>0) a = (int *)malloc(n*sizeof(int)); … a הוא מעכשיו שם המערך המרה לטיפוס הפוינטר a מספר הבתים במערך מבוא למדעי המחשב - שיעור 8
הפונקציה malloc • הערה: הפרמטר של malloc יבוטא בדרך כלל כ • זאת כדי שהתוכנית תתאים למחשבים בהם גודל הטיפוסים הוא שונה מזה שעליו כתבנו את התוכנית. n * sizeof(int) מספר התאים במערך מספר הבתים בכל תא במערך מבוא למדעי המחשב - שיעור 8
המרה לטיפוס הפוינטר • הפונקציה malloc היא פונקציה כללית המתאימה להקצאה מכל טיפוס והערך שהיא מחזירה הוא כתובת מטיפוס סתמי (void *). לכן יש להמיר את הערך המוחזר לטיפוס של הפוינטר שלנו. char *s; int n, m, *a; ... s = (char *)malloc(n*sizeof(char)); a = (int *)malloc(m*sizeof(int)); מבוא למדעי המחשב - שיעור 8
הפונקציה malloc • אחרי כל הקצאה יש לבדוק אם ההקצאה הצליחה. ניסיון לגשת לזיכרון שלא הוקצה יגרום לשגיאת ריצה • a= (int*)malloc(n*sizeof(int)); • if (a==NULL) { • printf("problem allocating memory“); • exit(1); • } • for (i=0;i<n;++i) • scanf(“%d”, &a[i]); אם ההקצאה לא הצליחה תהיה כאן שגיאת ריצה מבוא למדעי המחשב - שיעור 8
הפונקציה calloc כתובת תחילת ההקצאה גודל כל תא במערך • סינטקס: • פרמטרים: • מספר שלם המציין את גודל המערך • מספר שלם המציין את מספר הבתים בכל תא במערך • ערך מוחזר: • אם ההקצאה הצליחה - כתובת תחילת הזיכרון המוקצה • אם ההקצאה לא הצליחה - NULL גודל המערך pointer= (cast) calloc(size of array, size of each cell); מבוא למדעי המחשב - שיעור 8
הפונקציה calloc • דוגמה: int n; int*a; printf(“Enter array size: “); scanf(“%d”, &n); if (n>0) a = (int*)calloc(n, sizeof(int)); … a הוא מעכשיו שם המערך המרה לטיפוס הפוינטר a גודל המערך גודל כל תא במערך מבוא למדעי המחשב - שיעור 8