שיעור בתכנות מערכות בשפת C מיום א` (9.11)

‫מכללת אורט כפר‪-‬סבא‬
‫תכנות מערכות‬
‫בשפת ‪C‬‬
‫מערכים ומצביעים‬
‫הקצאה דינאמית של מערכים דו‪-‬מימדיים‬
‫‪09.11.14‬‬
‫אורי וולטמן‬
‫‪[email protected]‬‬
‫חידה לחימום‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫‪‬‬
‫לילך ויוסי נכנסים לחדר‪ .‬על ראש כל אחד מהם שמים כובע‪ .‬צבע כל כובע הינו‬
‫אדום או כחול (‪ 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‬‬