מכללת אורט כפר-סבא תכנות מערכות בשפת C מערכים ומצביעים הקצאה דינאמית של מערכים דו-מימדיים 09.11.14 אורי וולטמן uri.weltmann@gmail.com חידה לחימום לילך ויוסי נכנסים לחדר .על ראש כל אחד מהם שמים כובע .צבע כל כובע הינו אדום או כחול ( 50%סיכוי לכל צבע) .כיון שצבעו של כל כובע יכול להיות כחול או אדום ,ייתכן שצבעי שני הכובעים שווים .ייתכן גם ,כמובן ,שצבעיהם שונים. לאחר השמת הכובעים על הראשים ,כל אחד מביט על הכובע של השני ,ורושם על פתק ניחוש של צבע הכובע שלו .הרישום הוא באותו הזמן ,ואין סימנים ביניהם .מטרתם בניחוש היא שאחד מהם ינחש נכונה. ניתן לקבוע חוק ניחוש ללילך וחוק ניחוש ליוסי ,שינחו את הניחוש של כל אחד. החוקים יכולים להיות זהים ,או שונים זה מזה ,וייתכן שבחלק מן המקרים לילך תנחש נכון ובחלק מן המקרים יוסי ינחש נכון. מהו החוק ללילך ,ומהו החוק ליוסי כך שתמיד אחד מהם ינחש נכון? מצביעים ומערכים כידוע ,מערך הוא רצף של תאים בזיכרון .מצהירים על מערך באופן הבא: ;]int A[8 21 18 25 8 7 1 3 6 כעת ,נגדיר מצביע ל:int- ונוכל לבצע השמה לתוך המצביע ,בעזרת האופרטור '&': אם נבצע את ההוראה הבאה: אז יוצג כפלט תוכנו של התא ש p-מצביע עליו (התא הראשון במערך .)Aכלומר :יוצג כפלט המספר .6 ;int *p ;]p = &A[0 ;)printf (“%d”, *p מצביעים ומערכים ניתן לומר שאם pמצביע לתא מסוים במערך (כלומר :מכיל את כתובתו בזיכרון של תא זה) ,אז p+1הוא הכתובת של האיבר הבא במערך ,מכיוון שאיברי המערך נמצאים בזיכרון ברצף. במקרה שלנו p+1 ,הוא הכתובת של תא ] ;A[1ואילו p+2הוא הכתובת של תא ].A[2 באופן כללי :הביטוי p+iמצביע לתא ה i-שאחרי .pלכן ,אם pמצביע לתא הראשון במערך )] ,(p = &A[0הרי שהביטוי ) *(p+1שקול ל- ] ,A[1והביטוי ) *(p+2שקול ל ,A[2]-ובאופן כללי ) *(p+iשקול ל.A[i]- 21 18 25 8 7 1 3 6 מצביעים ומערכים מה יבצע קטע הקוד הבא? את ההשמה השנייה ניתן היה גם לרשום.*(p+7)++ : את ההשמה הראשונה ניתן היה גם לרשום ,p = A :וזאת מפני ששם המערך מתפקד ככתובת של התא הראשון שלו. ;]int A[8 ;int *p ;]p = &A[0 ;*(p+7) += 1 21 18 25 8 7 1 3 6 סוגי מצביעים עתה ניתן להבין ,מדוע אנו עושים את ההבחנה בין מצביע ל,int- מצביע ל ,float-מצביע ל ,char-וכו'. הסיבה היא שאם נרשום ,p+1צריך לדעת בכמה בתים צריך להתקדם בזיכרון. למשל ,אם pהוא מצביע ל ,int-אז ( p+1בסביבת עבודה שבה int תופס 2בתים) זו כתובת של תא בזיכרון שנמצא 2בתים אחרי .p אם pיהיה מצביע ל ,long-אז p+1יהיה כתובת של תא בזיכרון שנמצא 4בתים אחרי .p אם pהוא מצביע ל ,char-אז p+1 זו כתובת שגדולה בבית אחד מהכתובת של .p תרגיל האם קטע התכנית הבא חוקי? מה יהיה הפלט של קטע התכנית הבא? הסבירו מדוע ניתן להחליף את השורה השנייה ל.char *p = arr : הסבירו כיצד ניתן לקצר את גוף הלולאה להוראה אחת. ;int arr[5] = {19,44}, *p ;p = arr ;*(p+1) = 6 ;]p[2] = 100 + p[4] + p[0 ;}int arr[5] = {1,2,3,4,5 ;char *p ;p = arr { )while (*p != 5 ;)printf (“%d ”, *p ;p++ } מצביעים ומערכים ההבדל בין שם של מערך לבין מצביע ,הוא שבעוד שמצביע הוא משתנה וניתן לשנות את ערכו ,שם המערך הוא כתובת קבועה. לדוגמא: >#include <stdio.h )(int main { ;int mar[100], num ;int *ptr = mar חוקי */ */ */שגיאה! */ ;ptr++ ;mar++ חוקי */ */ */שגיאה! */ ;ptr = &num ;mar = &num ;return 0 } מערך של מצביעים : אחרי התכנית הבאה, שורה אחרי שורה,עקבו #include <stdio.h> int main() { int arr[20], x, y; int *ptr[10]; ptr[0] = &x; ptr[1] = &arr[3]; ptr[2] = arr + 5; ptr[3] = &y; ptr[4] = ptr[3]; ptr[5] = &x; *ptr[5] = 17; return 0; } מערכים רב-מימדיים נניח שיש בתכנית שלנו הצהרה על מערך רב-מימדי: מה המשמעות? פירוש החלק המודגש בקו תחתי" :אנחנו מצהירים על מערך בגודל חמישה איברים ושמו ."matאבל מהו טיפוס כל איבר? ;]int mat[5][4 ;]int mat[5][4 ;]int mat[5][4 פירוש החלק המודגש בקו תחתי" :טיפוס כל איבר הוא מערך של ארבעה מספרים שלמים". מערכים רב-מימדיים את המערך הדו-מימדי matניתן למלא בנתונים ,כאילו היה מדובר בחמישה מערכים שונים ,שכל אחד מהם הוא בעצמו מערך בגודל :4 int mat[5][4] = {{13,25,16,22}, {6,2,2,19}, {4,0,3,31}, {22,-9,33,22}, ;}}{5,5,5,1 ניתן להבין זאת כאילו אתחלנו בבת אחת ,חמישה מערכים בגודל 4 תאים כל אחד: ;}{13,25,16,22 ;}{6,2,2,19 ;}{4,0,3,31 ;}{22,-9,33,22 ;}{5,5,5,1 = = = = = ]mat[0 ]mat[1 ]mat[2 ]mat[3 ]mat[4 מערכים רב-מימדיים לחלופין ,מכיוון שמערכים מאוחסנים בצורה רציפה בזיכרון ,ניתן היה לאתחל את המערך הדו-מימדי על-ידי הצבת בלוק הנתונים הבא: ;}int mat[5][4] = {13,25,16,22,6,2,2,19,4,0,3,31,22,-9,33,22,5,5,5,1 איך הקומפיילר מפרש את שם המערך ?mat באותו אופן ששם המערך פורש עבור מערך חד-מימדי – בתור הכתובת של האיבר הראשון.&mat[0][0] : איך הקומפיילר מפרש את הביטוי ?mat+1 ידוע לקומפיילר שבמערך יש 4עמודות ,ולכן mat+1 פירושו הכתובת בזיכרון הנמצאת )4*sizeof(int בתים אחרי הכתובת .matכלומר ,זוהי כתובת הזיכרון של האיבר המופיע הראשון בשורה השנייה: ].&mat[1][0 תרגיל :בהינתן ההצהרה הבאה :מה יהיה הפלט שיוצג בעקבות ההוראות הבאות int mat[5][4] = {{13,25,16,22}, {6,2,2,19}, {4,0,3,31}, {22,-9,33,22}, {5,5,5,1}}; printf printf printf printf printf printf printf printf (“%d\n”, (“%d\n”, (“%d\n”, (“%d\n”, (“%d\n”, (“%d\n”, (“%d\n”, (“%d\n”, mat[0][0]); mat); mat + 1); mat[2]); mat[2][0]); mat[2][3]); (*(mat[2] + 3)); *(*(mat + 2) + 3)); מערכים רב-מימדיים כשם שבמערכים חד-מימדיים ,הביטויים הבאים שקולים: כך במערכים דו-מימדיים ,הביטויים הבאים שקולים: ]vector[i )*(vector + i ]matrix[i][j )*(*(matrix + i) + j אילו חישובים מבצע הקומפיילר כדי לחשב את ערכו של הביטוי ) ,*(*(matrix + i) + jבהנחה ש matrix-הוגדר כמערך של ROWS שורות ו COLS-עמודות? הוא מפרש את שם המערך matrixבתור כתובת בזיכרון. הוא מוסיף לה )…( i * COLS * sizeofבתים. הוא מוסיף לכתובת שנתקבלה עוד )…( j * sizeofבתים. נשים לב שהקומפיילר נזקק למספר עמודות המערך (המימד השני). מערכים רב-מימדיים >#include <stdio.h נביט בתכנית הבאה. )]a[][4 )][][void foo (int a התכנית לא תעבור קומפילציה ,ותופיע הודעת { ;int i,j השגיאהinvalid use of array with unspecified " : ( "boundsלגבי השורה עם ה.)printf- )for (i = 0; i < 5; i++ )for (j = 0; j < 4; j++ הסיבה לכך היא שצריך לדעת את ;)]printf ("%d ", a[i][j מספר עמודות המערך הדו-מימדי } על מנת להיות מסוגלים להגיע אל כל איבר בו. )(int main אם נוסיף לכותרת הפונקציה את המימד השני ,אז { הבעיה תיפטר והתכנית תרוץ. int mat[5][4] = {{13,25,16,22}, {6,2,2,19}, במערכים רב-מימדיים מסדר גבוה {4,0,3,31}, (תלת-מימדיים ומעלה) ,צריך לציין {22,-9,33,22}, ;}}{5,5,5,1 בכותרת הפונקציה את כל המימדים ;)foo(mat למעט הראשון. ;return 0 } מצביעים ומערכים מה משמעות שתי ההגדרות הבאות: השורה הראשונה מגדירה את vectorלהיות מערך של 4שלמים, והשורה השנייה מגדירה את matrixלהיות מערך של חמישה .vector כלומר :מערך של חמישה מערכים בני ארבעה איברים. שלושת ההצהרות הבאות שקולות: ;]typedef int vector[4 ;]typedef vector matrix[5 ;matrix m ;]vector m[5 ;]int m[5][4 ומה תעשה ההצהרה הבאה? vector *p : תגדיר את pלהיות מצביע ל ,vector-קרי :מצביע למערך של ארבעה תאים מטיפוס שלם .על כן ההשמות הבאות חוקיות, p = &m[0] : . p = m מצביעים ומערכים האם ניתן לכתוב קוד השקול לשתי ההוראות הבאות ,אך מבלי להשתמש בפקודה ?typedef ;]typedef int vector[4 vector *p ניתן לעשות זאת ,אם כי הקוד שנקבל פחות קריא: נשים לב שהוראה זו אינה שקולה להוראה הבאה: ההוראה הקודמת מגדירה את pלהיות מצביע למערך של ארבעה שלמים ,ואילו ההוראה האחרונה מגדירה את pלהיות מערך של ארבעה מצביעים לשלמים. ;]int (*p)[4 ;]int *p[4 מערכים של מחרוזות מכיוון שבשפת Cמחרוזת היא מערך של תווים (המכיל את התו המיוחד ' ,)'\0הרי שמערך של מחרוזות הוא למעשה מערך דו-מימדי. נביט בהצהרה הבאה: ;}”char names[5][6] = {“Dan”,”John”,”Eden”,”Billy”,”Dena השם הארוך ביותר במערך (” ,)“Billyאורכו חמישה תווים .מדוע ,אם כך ,בחרנו להגדיר את המימד השני של המערך להיות בגודל ?6 זוהי תמונת הזיכרון שתיווצר בעקבות ההצהרה: \0 \0 \0 y \0 \0 n n l a n h e l n a o d i e D J E B D מערכים של מחרוזות ?מה יהיה הפלט של התכנית הבאה #include <stdio.h> int main() { char names[5][6] = {“Dan”,”John”,”Eden”,”Billy”,”Dena”}; int i; for (i = 0; i < 5; i++) puts(names[i]); return 0; } את מערך השמות ניתן היה להגדיר :גם כך char *names[5] = {“Dan”,”John”,”Eden”,”Billy”,”Dena”} מצביעים ומערכים כאמור ,שני הביטויים הבאים הם שקולים: בנוסף ,גם שני הביטויים הבאים שקולים: בכל מקום בו משתמשים בסימון של מערך (גישה לאיבר על-ידי סוגריים מרובעים) ,ניתן להשתמש במצביעים. ההבדל המשמעותי היחידי בין מערכים למצביעים הוא שמצביעים הם משתנים ( )variablesניתן להציב בהם כתובות שונות ולגרום להם להצביע על מקומות שונים בזיכרון (.)p = &num לעומת זאת ,מערכים הם קבועים ( )constantsואומנם ניתן לשנות את תוכן תאי המערך ,אך לא ניתן להציב בתוך מערך כתובת זיכרון חדשה ( ...arr = &arr2אסור!). ]vector[i )*(vector + i ]matrix[i][j )*(*(matrix + i) + j תרגיל :נתונים המשתנים הבאים בזיכרון המחשב int a[] = {6,5,4,3,2,1}; char *b[] = {"Trust","i","seek","and","i","find","in","you"}; char *c = "hAppY"; char *d[] = {b[a[2]], b[a[0] - a[5]]}; int e[2][3] = {{0,3,4},{1,2,7}}; int f[] = {b[3][2] - b[4][0] , a[2]+a[4] , e[1][2] - e[0][1]}; char *g = b[3]; :חשבו את ערכם של הביטויים הבאים c[1] 2[a] f[2] + *(f+1) *(f+a[5]) **(d+1) (g+1)[1] *(*(b+a[0])+e[0][0]) המשך התרגיל :מגדירים טיפוס חדש :ונתון קטע הקוד הבא struct quack { int x; double y; char *s; int a[3]; }; :חשבו את ערכם של הביטויים הבאים h.s[1] (*k).s[2] k->s[2] h.x + k->y *(f + h.x) *(h.s + k->x) struct quack h, *k; h.x = 1; h.y = 4.7; h.s = b[3]; h.a[0] = a[2]; h.a[1] = a[1]; h.a[2] = a[2]; k = &h; שקף התאוששות מהתרגיל הקשה הקצאה דינאמית למדנו בעבר כיצד ניתן להשתמש בפקודות כגון mallocאו callocעל מנת להקצות דינאמית זיכרון עבור מערך. מעבירים ל ,malloc-למשל ,פרמטר יחיד המציין את הגודל בבתים של בלוק הנתונים אותו מעוניינים להקצות ,והפונקציה mallocתקצה בזיכרון בלוק מתאים ,ותחזיר את הכתובת של תחילת הבלוק. בגרסאות ישנות של שפת ,Cלפני פרסום התקן ANSI-Cבשנת ,1989 הפונקציה mallocהייתה מחזירה משתנה מטיפוס * ,charוהיה צריך לעשות לו הסבה ( )castingבצורה מפורשת לטיפוס המבוקש .לדוגמא: ))p = (int *)malloc(20*sizeof(int כיום ,בכל הקומפיילרים שנבנו לפי תקן ,ANSI-Cהפונקציה mallocמחזירה מצביע מהסוג * ,voidשמומר אוטומטית לטיפוס המבוקש. במידה ואין מספיק זיכרון פנוי ,או שפעולת הפונקציה נכשלת מכל סיבה שהיא ,הפונקציה mallocמחזירה .NULLתמיד צריך לבדוק את הערך המוחזר על-ידי הפונקציה! הקצאה דינאמית של מערך דו-מימדי מעוניינים להקצות בצורה דינאמית מערך דו-מימדי. מהן הדרכים שעומדות לרשותנו לעשות זאת? יש 3שיטות שונות: בראשונה משתמשים אם ידוע מראש המימד השני (מס' העמודות). בשנייה משתמשים אם שני המימדים אינם ידועים ,ואין חשיבות לכך שתאי המערך יוקצו בזיכרון בצורה רציפה. בשלישית משתמשים אם שני המימדים אינם ידועים ,ויש חשיבות לכך שתאי המערך יוקצו בזיכרון בצורה רציפה. שיטה א' :אם מספר העמודות ידוע מראש אם בזמן הקומפילציה ידוע מראש מספר העמודות במערך (המימד השני של המערך הדו-מימדי) ,ניתן להקצות זיכרון באופן הבא: >#include <stdio.h >#include <stdlib.h #define COLS 5 )(int main { ;int nrows */מספר השורות במערך הדו-מימדי */ ;int i,j אינדקסים לשורה ולעמודה */ */ ;]int (*a)[COLS מצביע למערך של COLSשלמים */ */ ;)“ ?printf (“How many rows ;)scanf (“%d”, &nrows ;))a = malloc(nrows * COLS * sizeof(int ;if (a == NULL) return 1 סטודנט טען כי בתכנית נוצרת דליפת )for (i = 0; i < nrows; i++ זיכרון ( ,)memory leakשכן את )for (j = 0; j < COLS; j++ הזיכרון שהוקצה דינאמית לא שחררנו ;)]scanf (“%d“, &a[i][j בעזרת .freeהאם הוא צודק? ;return 0 } הקצאה דינאמית של מערך דו-מימדי השיטה בה נקטנו להקצאת זיכרון דינאמית של מערך דו-מימדי פועלת רק אם מספר העמודות ידוע בזמן הקומפילציה .אם מעוניינים ששני מימדי המערך ייקבעו בזמן הריצה (למשל :על-ידי קלט מהמשתמש) ,צריכים לפעול בצורה אחרת. אם מעוניינים להקצות דינאמית מערך דו-מימדי של nrowsשורות וncols- עמודות ,אפשר לנהוג בשיטה הבאה: ניצור (ע"י הקצאה דינאמית) מערך של nrowsאיברים מטיפוס 'מצביע למערך'. נעבור על כל איברי המערך החד-מימדי ,ובכל אחד מאיבריו נציב את הכתובת המתקבלת כתוצאה מהקצאה דינאמית של מערך חד-מימדי בגודל .ncols כעת נקבל מבנה דינאמי שניתן להתייחס אליו כאל מערך דו-מימדי. מצביע למערך של מצביעים למערך:'שיטה ב #include <stdio.h> #include <stdlib.h> int main() { int nrows, ncols; int i,j; int **a; a printf (“How many rows and columns? “); scanf (“%d”, &nrows); scanf (“%d”, &ncols); a = malloc(nrows * sizeof(int *)); if (a == NULL) return 1; ... מצביע למערך של מצביעים למערך:'שיטה ב ... for (i = 0; i < nrows; i++) { a[i] = malloc(ncols * sizeof(int)); if (a[i] == NULL) return 1; a } for (i = 0; i < nrows; i++) for (j = 0; j < ncols; j++) scanf (“%d”,&a[i][j]); return 0; } הקצאה דינאמית של מערך דו-מימדי )*(*(a + i) + j a ]a[i][j = הקצאה דינאמית של מערך דו-מימדי לצורך הקצאת המערך הדו-מימדי ,היה צריך להשתמש בnrows+1- קריאות ל( malloc-קריאה אחת עבור מערך חד-מימדי של מצביעים לשורות ,וקריאה אחת עבור nrowsמערכים חד-מימדיים שכל אחד מהם מייצג שורה). לאחר סיום ההקצאה הדינאמית ,אנחנו נתעלם מהמבנה המורכב שמסתתר מאחורי הקלעים ( aהוא מצביע למערך של מצביעים לתוך מערך של שלמים ,)...ונעבוד עם aכאילו היה מערך דו-מימדי רגיל: ,a[2][1] = 5וכו'. הבדל משמעותי בין המערך הדו-מימדי המתקבל כתוצאה מהקצאה דינאמית כזו ,לבין מערך דו-מימדי רגיל ,הוא שמערך דו-מימדי רגיל יושב בזיכרון בצורה רציפה (כלומר :מדובר ברצף של תאי זיכרון אחד אחרי שני). למערך דו-מימדי רגיל ,אפשר ,אם רוצים ,להתייחס כאל מערך חד-מימדי ארוך. הקצאה דינאמית של מערך דו-מימדי דרך אחרת להקצות דינאמית מערך דו-מימדי ,שתבטיח שהמערך הדו-מימדי יאוחסן בזיכרון בצורה רציפה ,היא להקצות קודם מערך חד-מימדי ארוך. לאחר מכן ,נקצה מערך חד-מימדי של מצביעים ,ונציב בכל מצביע כתובת של איבר מתוך המערך החד-מימדי (כתובתו של האיבר הראשון בכל 'שורה'). הקצאה בצורה רציפה:'שיטה ג #include <stdio.h> #include <stdlib.h> int main() { int nrows, ncols; int i,j; int **a; int *b; b printf (“How many rows and columns? “); scanf (“%d”, &nrows); scanf (“%d”, &ncols); b = malloc(nrows * ncols * sizeof(int)); if (b == NULL) return 1; ... a הקצאה בצורה רציפה:'שיטה ג ... a = malloc(nrows * sizeof(int *)); if (a == NULL) return 1; b for (i = 0; i < nrows; i++) a[i] = b + (i * ncols); a for (i = 0; i < nrows; i++) for (j = 0; j < ncols; j++) scanf (“%d”, &a[i][j]); return 0; } שיטה ג' :הקצאה בצורה רציפה ]a[i][j a )*(*(a + i) + j = הקצאה דינאמית של מערך דו-מימדי לצורך הקצאת המערך הדו-מימדי בצורה רציפה ,היה צריך להשתמש פעמיים ב:malloc- לאחר סיום ההקצאה הדינאמית ,אנחנו נתעלם מהמבנה המורכב שמסתתר מאחורי הקלעים ( aהוא מצביע למערך של מצביעים לתוך מערך של שלמים ,)...ונעבוד עם aכאילו היה מערך דו-מימדי רגיל: קריאה אחת לצורך הקצאת מערך חד-מימדי ארוך באורך .nrows*ncols קריאה שנייה לצורך הקצאת מערך של nrowsמצביעים (שכל אחד מהם יצביע על תחילת 'שורה' במערך). ,a[2][1] = 5וכו'. בשיטה זו ,ניתן לנצל את העובדה שהמערך מאוחסן בזיכרון באופן רציף ולעבד אותו גם בצורה זו ,אם מעוניינים בכך. יתרון נוסף של שיטה זו הוא שפשוט יותר לשחרר את הזיכרון שהקצנו (בעזרת .)free
© Copyright 2024