730 likes | 947 Views
קורס תכנות – סמסטר ב' תשס"ח. שיעור תשיעי: כתובות ומצביעים מבנים. www.cs.tau.ac.il/~armon/prog08b.htm. נושאי השיעור היום. כתובות ומצביעים: - תזכורת ודוגמאות נוספות - מצביעים ומערכים מערכים ופונקציות מבנים. תזכורת: כתובות ומצביעים.
E N D
קורס תכנות – סמסטר ב' תשס"ח שיעור תשיעי: כתובות ומצביעים מבנים www.cs.tau.ac.il/~armon/prog08b.htm
נושאי השיעור היום • כתובות ומצביעים: - תזכורת ודוגמאות נוספות - מצביעים ומערכים • מערכים ופונקציות • מבנים
תזכורת: כתובות ומצביעים • לכל תא בזיכרון יש מספר סידורי (כתובת). כל משתנה נשמר החל מכתובת מסויימת בזיכרון. • מצביע (פויינטר) הוא משתנה ששומר כתובת של משתנה אחר. למשל: int *the_pointer;.זה מצביע ששומר כתובת של int. • לכל טיפוס-משתנה ניתן להגדיר מצביע לשמירת כתובת של משתנה כזה: char *ptav; double *ptr1, *ptr2, *ptr3; • הפעולה & נותנת את כתובתו של משתנה, למשל &i. • הפעולה * נותנת את המשתנה שנמצא בכתובת מסויימת (כלומר ניגשת לערך של המשתנה שעליו הפויינטר מצביע). למשל: *my_pointer=10;
תזכורת: מציאת הכתובת של משתנה התוכנית הזיכרון טבלת הכתובות int main() { int i=10; } משתנה כתובת גודל 10 i 75004 7500 &i זה 7500
תזכורת: גישה לערך בכתובת מסויימת התוכנית הזיכרון טבלת הכתובות int main() { char c=‘A’; } משתנה כתובת גודל 65 c 92001 9200 *(9200) זה ערך המשתנה c, כלומר זה 65
100 my_pointer_to_int i תזכורת: גישה למשתנה על-ידי מצביע • קטע תוכנית להדגמת משמעות הפעולות האלה: int i=10; int *my_pointer_to_int; my_pointer_to_int =&i; *my_pointer_to_int=100; printf(“The value of i is now %d”, i); intהגדרת מצביע על iשמים במצביע את כתובת המשתנה שמים 100במשתנה שהפויינטר מצביע עליו יודפס המספר 100 345 כתובת 345 (למשל)
כתובות: נקודות לתשומת לב • לא ניתן לשנות כתובת של משתנה. כלומר לא ניתן לכתוב למשל: &i=5000. • ניתן לשנות את הערך שנמצא בכתובת מסויימת. • למשל: *(&c)=‘A’;. • זה כמו לכתובc=‘A’;. • מסוכן לשנות את הערך שנמצא בכתובת כלשהי שאיננה כתובת של משתנה. למשל *(4000)=123;. • אנחנו עלולים לשנות מקום בזיכרון ששייך למערכת ההפעלה, ולגרום לתוכנית לעוף. • לכן נשנה רק ערכים של משתנים שהגדרנו.
תזכורת: מצביעים ופונקציות • העברת כתובת של משתנה לפונקציה מאפשרת לה לשנות אותו אפילו שהוא לא מוגדר בה. • זו הסיבה שבהעברת מערך לפונקציה שונה ערכו המקורי (מה שמועבר הוא כתובת ההתחלה של המערך בזיכרון).
תזכורת: מצביעים ופונקציות – דוגמא • תארנו פונקציה שמקבלת כתובות של שני משתנים מסוג int, ומחליפה בין הערכים שלהם. void swap(int *first, int *second) { int temp; temp=*first; *first=*second; *second=temp; } השימוש למשל על-ידי:swap(&i, &j); first ערך המשתנה שכתובתו הועברה אל יוחלף עם ערך המשתנה שכתובתו הועברה אל second
עוד על פונקציות ומצביעים • אמרנו שהערך שמוחזר מפונקציה יכול להיות מצביע: int *address_of _the_higher_number(int *a, int*b); • אבל החזרת כתובת משתנה שהוגדר בתוך הפונקציה תגרום לשגיאה. כי המשתנים שמוגדרים בתוך הפונקציה נעלמים עם סיומה. למשל הפונקציה הבאה תגרום לשגיאה: int *give_pointer_to_zero() { int i=0; return &i; } 0 i
. . . . . . . . . . array[0] array[9] מצביעים ומערכים • הגדרת מערך מקצה בזיכרון מקומות רצופים עבור התאים שלו. • בנוסף, מוקצה משתנה ששמו הוא שם-המערך, שמכיל את כתובת התא הראשון של המערך (לא ניתן לשנות את ערכו). • לכן אפשר לגשת לתאי מערך גם ע"י חישוב הכתובת שלהם. למשל *(array+5) שקול ל- array[5]. • המחשב יודע לאיזה כתובת לגשת כשעושים את החשבון הזה, כי הוא יודע כמה מקום תופס כל תא במערך לפי הטיפוס. כתובת 500 (למשל) 500 array
מצביעים ומערכים • אי-אפשר לשנות כתובת של מערך (היא נקבעת כשהוא מוגדר). • לכן לא ניתן לכתוב array=array1, או array=0. • אפשר לשים כתובת של מערך בתוך מצביע. אפשר לכתוב: char *array_ptr; array_ptr=array; • (השורה האחרונה שקולה לשורה:array_ptr = &array[0]; ) • כלומר,משתנהמערך הוא סוג של מצביע (אבל לא להיפך). • מערך הוא למעשה מצביע שמקבל כתובת בהגדרה ולא ניתן לשנות אותה. (שמים במצביע את כתובת התא הראשון במערך)
גישה לתאי-מערך לפי הכתובת • אם הגדרנו למשל: int array[10]; int *array_ptr; array_ptr=array; אז אפשר לגשת לתאי-המערך גם לפי הכתובת שלהם, כמו משתנים אחרים.3 הפעולות הבאות עושות אותו דבר (שמות 100 בתא מס' 5 במערך): array[5]=100; *(array+5)=100; *(array_ptr+5)=100; • - המחשב יודע כמה בתים להתקדם כשאנחנו מבקשים להתקדם ב-5 תאים קדימה, כי הגדרנו שטיפוס הערכים הוא int והוא יודע כמה בתים כל int תופס (כאמור, ערכי המערך שמורים בזיכרון ברצף). (מצביע על התא הראשון במערך) גישה לתא שנמצא 5 תאים אחרי התא הראשון
גישה למערך לפי כתובת – דוגמא נוספת • נאמר שמוגדרים: int i,array[10]={0}; int *ptr; • אז 3 הקטעים הבאים עושים בדיוק אותו דבר (מדפיסים את המערך): • for (i=0; i<10; i++) printf(“%d ”, array[i]); • for (i=0; i<10; i++) printf(“%d ”, *(array+i)); • for(ptr=array; ptr<= &array[9]; ptr++) printf(“%d ”, *ptr); 1) הדפסה על-ידי גישה רגילה לתאי המערך 2) הדפסה על-ידי גישה לכל תא לפי הכתובת שלו יחסית לתא הראשון 3) הדפסה ע"י קידום מצביע מכתובת התא הראשון עד כתובת התא האחרון
מערכים ופונקציות - הסבר • אמרנו בעבר שכשמעבירים מערך לפונקציה שינויים שנעשה בפונקציה ישפיעו על המערך המקורי. • זה נובע מכך שלפונקציה מועברת הכתובת בזיכרון של המערך המקורי, והיא פועלת ישירות על ערכיו (ולא על העתק שלהם). • למשל, כפי שאמרנו, הפונקציה הבאה תאפס מערך שמועבר אליה: void zero_array(int a[ ], int size) { int i; for(i=0 ; i<size; i++) a[i]=0; }
מערכים ופונקציות - הסבר • אמרנו בעבר שכשמעבירים מערך לפונקציה שינויים שנעשה בפונקציה ישפיעו על המערך המקורי. • זה נובע מכך שלפונקציה מועברת הכתובת בזיכרון של המערך המקורי, והיא פועלת ישירות על ערכיו (ולא על העתק שלהם). • למשל, כפי שאמרנו, הפונקציה הבאה תאפס מערך שמועבר אליה: void zero_array(int *a, int size) { int i; for(i=0 ; i<size; i++) a[i]=0; } נציין שאם פונקציה מצפה לקבל מצביע, אפשר להעביר אליה מערך מהסוג הזה, כי בשני המקרים מה שמועבר הוא כתובת בזיכרון.
מערכים ופונקציות • למשל הפונקציות שהזכרנו על מחרוזות (כמו strcmp, strlen) מוגדרות למעשה עבור מצביעים מסוג char *. • באחריותנו להפעיל אותן באמת על מחרוזת (כלומר רצף תווים שמסתיים ב- '0\') ולא סתם על מצביע לתו או מערך של תווים (נקבל תוצאות שגויות אם אין את תו הסיום). • כמו-כן, כזכור, אם פונקציה מוגדרת על מערך בגודל מסויים, ניתן להעביר אליה גם מערך בגודל אחר של אותו טיפוס (כי כאמור מה שמועבר זה רק כתובת ההתחלה). • שוב, זה באחריותנו שתתבצע בפונקציה פעולה הגיונית ושהיא תדע מה גודל המערך ולא תחרוג ממנו.
"מצביע על כלום" • לפעמים נירצה לסמן שמצביע לא מצביע לשום משתנה (למשל לפני שהתחלנו להשתמש בו). • מקובל לעשות זאת ע"י זה שנכניס אליו את הכתובת 0 (שאיננה כתובת אפשרית של משתנה). • למעשה, במקום לרשום 0 מקובל לרשום NULL. זהו קבוע שערכו 0, שמוגדר ע"י #define בספריה stdlib.h. למשל: #include<stdlib.h> int *ptr=NULL; • לא לבלבל בין הכתובת NULL לבין תו סיום מחרוזת ‘\0’
דוגמא: חיפוש תו במחרוזת מקבלת מצביע לתחילת מחרוזת ותו #include<stdlib.h> char *my_strchr(char *str, char tav) { while((*str != ‘\0’) && (*str != tav)) str++; if (*str == tav) return str; return NULL } מתקדמים עד שמצאנו את התו או הגענו לסוף המחרוזת אם מצאנו מוחזר מצביע להופעה הראשונה אם לא מצאנו מחזירים NULL
דוגמא: חיפוש תו במחרוזת מקבלת מצביע לתחילת מחרוזת ותו #include<stdlib.h> char *my_strchr(char *str, char tav) { while((*str != ‘\0’) && (*str != tav)) str++; if (*str == tav) return str; return NULL; } int main() { char word[]=“HELLO”, tav=‘L’, *place; place=my_strchr(word, tav); return 0; } מתקדמים עד שמצאנו את התו או הגענו לסוף המחרוזת אם מצאנו מוחזר מצביע להופעה הראשונה אם לא מצאנו מחזירים NULL הדגמת אופן הקריאה לפונקציה הזו
מערכים ופונקציות – נקודה לתשומת לב • אם ננסה להחזיר מפונקציה מערך שהגדרנו בתוכה או מצביע על מערך כזה, אז נקבל שגיאה. int * zero_array() { int array[100]={0}; return array; } • זה מכיוון שמוחזרת כתובת של המערך, אבל כל מה שהוגדר בתוך הפונקציה נמחק כשהיא מסתיימת.
מצביעים ומערכים - סיכום • משתנה מערך הוא סוג של מצביע: הוא מכיל כתובת (של התא הראשון במערך), אבל אי-אפשר לשנות אותה. • זו הסיבה שבהעברת מערך לפונקציה המערך המקורי מושפע. • אפשר לגשת לתא במערך גם בעזרת חישוב כתובתו יחסית לכתובת התחלת המערך. למשל: *(array+5)=100; שאלות?
נקודה לתשומת-לב: איתחול מה שמוצבע • ככלל, לפני שכותבים ערך לכתובת שנמצאת במצביע, צריך שהמצביע יכיל כתובת של משתנה כלשהו שהגדרנו (אחרת תיקרה תעופה). int *ptr; *ptr=10; • בהתאם, בהגדרת מצביע לא ניתן לכתוב ערכים למקום שהוא מצביע עליו. למשל לא ניתן לרשום: int *my_array={1,2,3}; יכתוב לכתובת שנמצאת במצביע לפני שאיתחלנו אותו
דיברנו על: • כתובות בזיכרון • מצביעים והשימוש בהם • מערכים ומצביעים • מערכים ופונקציות סיכום מצביעים
שאלות נוספות? מצביעים
טיפוסי משתנים בשפת C • הכרנו במהלך הקורס את טיפוסי המשתנים הבסיסיים שניתן לייצג בשפת C: מספר שלם/ממשי, תו, מחרוזת, ומערך. • כשכתבנו תוכניות לפתרון בעיות כלשהן, יצגנו את נתוני התוכנית ע"י משתנים מהטיפוסים האלה. • כידוע, בבעיות מציאותית יכולים להיות אובייקטים מורכבים, שלא בהכרח מתאימים ליצוג ע"י מספר, תו, או אפילו מערך. • למשל בבעיות גיאומטריות מדובר על נקודות, ישרים, מצולעים, וכדומה. • מינהלת-האוניברסיטה מטפלת בפקולטות, קורסים, סטודנטים, וכו'.
טיפוסי משתנים בשפת C • כשכותבים תוכנות לפתרון בעיות מציאותיות, נוח שיהיו לנו טיפוסי משתנים שמייצגים את האובייקטים המציאותיים האלה. • למשל, לטיפול במספרים מרוכבים היינו רוצים טיפוס שייצג מספר מרוכב. • לפתרון בעיות גיאומטריות נירצה למשל טיפוס שמתאר נקודה במישור וטיפוסים שמתארים מצולעים שונים. • במערכת המידע של האוניברסיטה נירצה טיפוסי-משתנים עבור סטודנט, קורס, וכו' (שכל אחד מהם כולל כמה שדות-מידע רלוונטיים).
יצירת טיפוסי משתנים חדשים • שפת C מאפשרת לנו להגדיר טיפוסי משתנים חדשים שיתאימו לבעיה שאנחנו רוצים לפתור. • טיפוס חדש כזה נקרא "מבנה" (structure). הפקודה שתשמש אותנו לכך היא struct. • בדרך-כלל מבנה כולל כמה משתנים, שנקראים "שדות" (דוגמא בשקף הבא).
הגדרת מבנה חדש – דוגמא • נקודה במישור מיוצגת על-ידי שתי קואורדינטות, X ו- Y. הגדרת מבנה שייצג נקודה תיראה למשל כך: struct point { double x; double y; }; • ההגדרה הזו לא תופיע בתוך פונקציה, אלא בתחילת הקובץ (בד"כ מייד אחרי שורות ה- #include וה- #define). • כעת ניתן להגדיר בתוכנית משתנים מהסוג הזה, כפי שניראה בשקף הבא.
הגדרת מבנה חדש – המשך הדוגמא • דוגמא להגדרת משתנים מהטיפוס החדש: int main() { struct point P1,P2; return 0; } • בכך הגדרנו שתי נקודות (שני משתנים מסוג מבנה point), שלכל אחת מהן יש שני שדות. P2 P1 x y x y
הגדרת מבנה חדש – המשך הדוגמא ניתן לגשת לכל אחד מהשדות של מבנה ולכתוב/לקרוא אליו/ממנו. למשל: int main() { struct point P1,P2; P1.x = 6; P1.y = 7; P2.x = 4; P2.y = 2; printf(“%g\n”, P1.y); return 0; } P2 P1 x y 4 x y 6 2 7 (יודפס 7)
מבנים - נושאים מבנים: • הגדרה ואיתחול • פעולות על מבנים • מבנים ופונקציות • מבנים ומצביעים • מבנה בתוך מבנה • מערכים של מבנים
מבנים - תיאור פורמלי • הגדרת מבנה (טיפוס משתנה חדש): structשם הטיפוס החדש{ טיפוס שם-שדה; ….. טיפוס שם-שדה; } ; • הגדרת משתנה מהסוג החדש שהגדרנו: ;שם משתנה שם הטיפוס החדשstruct • פנייה לשדה של משתנה שהוא מבנה נעשית ע"י שם המשתנה.שם השדה
הגדרת מבנה חדש - הדוגמא הקודמת #include<stdio.h> struct point { double x; double y; }; int main() { struct point P1,P2; P1.x = 6; P1.y = 7; P2.x = 4; P2.y = 2; printf(“%g\n”, P1.y); return 0; } P2 P1 x y 4 x y 6 2 7
איתחול מבנים • גם משתנים מטיפוס חדש אפשר לאתחל בשורת ההגדרה. למשל בהתייחס לדוגמא הקודמת: struct point P = {6,7}; • השדה x של המשתנהP יאותחל ל-6 והשדהy ל-7. • באופן כללי, האיתחול הוא לפי סדר השדות בהגדרת המבנה, כלומר האיבר הראשון ברשימת האיתחול יכנס לשדה הראשון, השני לשני, וכן הלאה (אם האיתחול הוא חלקי, השאר יאותחל באפסים).
מבנים – הגדרה • מהם מבנים • הגדרת מבנים • איתחול מבנים • שאלות?
פעולות על מבנים • השמה בין מבנים מאותו סוגמתבצעת בצורה הרגילה: P1 = P2; • התוכן של P2 פשוט מועתק לתוך P1. (העתקה שדה-שדה) • אם אחד השדות הוא מערך אז גם הוא מועתק (שינוי שלו ב- P1 לא ישפיע על P2). • פעולות השוואה (== והאחרות) ופעולות חשבוןאינן פועלותעבור מבנים(זה לא יתקמפל). • כלומר נצטרך לכתוב פונקציות עבור הפעולות שנירצה ליישם (למשל נשווה את כל אחד מהשדות כדי לבצע השוואה).
pointדוגמא: פונקציה להשוואת מבני int equal(struct point p, struct pointq) { return ( (p.x == q.x) && (p.y == q.y)) } אם הנקודות זהות יוחזר 1, ואחרת 0
דוגמא לשימוש במבנים • נכתוב פונקציה שמחשבת מרחק בין שתי נקודות: double dist( struct point p, struct point q) { double d; d = (p.x – q.x)*(p.x – q.x) + (p.y–q.y)*(p.y-q.y); return sqrt(d); } (יופיע בתחילת הקובץ #include<math.h> כדי להשתמש בפונקציה sqrt)
דוגמת שימוש במבנים - המשך …. int main() { struct point p,q; printf(“Enter x and y coord. of the first point\n”); scanf(“%lf%lf”, &p.x,&p.y); printf(“Enter x and y coord. of the second point\n”); scanf(“%lf%lf”, &q.x,&q.y); printf(“Their distance is %g\n”, dist(p,q)); return 0; }
פונקציות ומבנים • איך מבני הנקודות מועברים לפונקציה?
פונקציות ומבנים • איך מבני הנקודות מועברים לפונקציה? • הם מועברים על ידי העתקת הערכים של השדות שלהם לפרמטרים של הפונקציה. • כמו במשתנים רגילים call by value)), כמו עבור int. • שינוי הנקודה בפונקציה לא ישנה את הנקודה ב- main. • פונקציה יכולה גם להחזיר מבנה, כמו כל משתנה אחר (גם במקרה הזה הערכים יועתקו).
פונקציות ומבנים – נקודה לתשומת-לב • אם אחד השדות של המבנה הוא מערך, ומעבירים את המבנה הזה לפונקציה, אז המערך מועתק (לא רק כתובת המערך מועברת). • כלומר אם נשנה בפונקציה מערך שהוא שדה של מבנה, אזלא תהיה לזה השפעה על המערך המקורי.
מבנים - פעולות • השמה בין מבנים מותרת (מתבצעת העתקה שדה-שדה) • פעולות השוואה ופעולות חשבון לא עובדות • ניתן להעביר מבנים לפונקציה ולהחזיר ממנה מבנה, והם מועתקים אליה וממנה, כמו טיפוס-משתנה רגיל (כמו int) • מערך בתוך מבנה מועתק בהעברה לפונקציה ובחזרה ממנה • שאלות?
מבנים – סיכום ביניים • שימוש במבנים מאפשר הגדרת טיפוסי משתנים חדשים שקרובים לעולם-הבעיה שאנחנו פותרים. • לא ניתן להשתמש בפעולות החשבון וההשוואה הרגילות על מבנים - יש להגדיר פונקציות עבורן בהתאם לצורך (רק פעולת ההשמה מוגדרת, ובה מתבצעת גם העתקת מערכים). • אפשר להשתמש במבנים כמו בטיפוסים רגילים מבחינת העברה לפונקציה (למעט העובדה שמערך בתוך מבנה מועתק ולא מועבר המערך המקורי). • כעת נדבר על מבנים ומצביעים, מערכים של מבנים, ומבנים בתוך מבנים.
מבנים ומצביעים • אם המבנה שלנו מורכב מהרבה מאוד משתנים, אז העתקת כל המבנה לתוך הפרמטרים של הפונקציה דורשת זמן וזיכרון רבים. • כמו-כן, כמו במשתנים רגילים, לפעמים נירצה לשנות יותר ממבנה אחד מתוך הפונקציה (ולא רק להחזיר מבנה אחד). • כדי לטפל בשני הנושאים האלה נוכל לשלוח לפונקציה מצביעים למבנים, כמו ששלחנו מצביעים למשתנים מסוגים אחרים. • בקריאה לפונקציה ישוכפל רק המצביע לכל מבנה, ובעזרת המצביע נוכל לשנות את המבנה המקורי.
מבנים ומצביעים בדיוק כמו טיפוסי-משתנים שהכרנו בעבר, אפשר להגדיר מצביע לטיפוס שהוא מבנה (שיכיל את כתובת ההתחלה שלו). למשל: struct point P={5,6}; struct point*ptr; ptr = &P ; P.x P P.y ptr
מצביעים ומבנים – גישה לשדות המבנה • בדוגמא הזאת, כדי להגיע לשדות של P דרך ptr שמצביע על P, אפשר להשתמש ב- * כרגיל: • (*ptr).x = 3; שקול ל- P.x = 3; • (*ptr).y = 7; שקול ל- P.y = 7; (צריך סוגריים כי אחרת לנקודה יש קדימות)
מצביעים ומבנים – גישה לשדות המבנה • בדוגמא הזאת, כדי להגיע לשדות של P דרך ptr שמצביע על P, אפשר להשתמש ב- * כרגיל: • (*ptr).x = 3; שקול ל- P.x = 3; • (*ptr).y = 7; שקול ל- P.y = 7; • אבל בדרך-כלל נשתמש לצורך זה בסימון מקוצר: חץ <- • ptr->x = 3; שקול ל-P.x = 3; • ptr->y = 7; שקול ל-P.y = 7; • כלומר משמעות החץ היא גישה לשדה במבנה שמצביעים עליו. (צריך סוגריים כי אחרת לנקודה יש קדימות)