Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Studies

מערכים ופונקציות

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

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

#include <stdio.h>
void fn(int *arr) {
    printf("First element: %d", arr[0]);
}
int main() {
    int arr[] = {1, 2, 3};
    fn(arr);
}

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

#include <stdio.h>
int calc_avg(int *grades, int grades_len) {
    int sum = 0;
    for (int i = 0; i < grades_len; i++) {
        sum += grades[i];
    }
    printf("sizeof grades: %zu\n", sizeof(grades));
    return sum / grades_len;
}
#define NUM_GRADES 5
int main() {
    int grades[NUM_GRADES] = {90, 45, 32, 100, 93};
    printf("Average is: %d", calc_avg(grades, NUM_GRADES));
}

נשים לב שאנחנו חייבים לתת לפונקציה את אורך המערך במידה ונרצה לבצע איטרציות מעליו - אחרת, אין לה דרך לדעת כמה איברים יש בו - מצביע הוא כתובת בזכרון ולא מעבר לכך. כמו כן, כאשר נדפיס את גודל המשתנה grades, נקבל 8 בתים - גודל של מצביע סטנדרטי במערכת 64bit.

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

#define NUM_GRADES 5
int calc_avg(int grades[], int grades_len);
// or like this:
int calc_avg(int grades[NUM_GRADES], int grades_len);
int main() {
    int grades[NUM_GRADES] = {90, 45, 32, 100, 93};
    printf("Average is: %d", calc_avg(grades, NUM_GRADES));
}

הסינטקס שונה קצת, אבל המשמעות זהה - הקומפיילר מזהה את grades בתוך מצביע לאיבר כלשהו, שאנחנו לאחר מכן עושים בו שימוש כמערך. במקרה זה אנחנו אומרים שהמערך דועך למצביע (array decay to pointer). הסיבה שנגיד שהוא דועך היא מכיוון שהפונקציה מפרשת את המערך כמצביע, לא ניתן לבצע sizeof ולקבל את גודל המערך כמו שראינו בעבר:

int calc_avg(int grades[], int grades_len) {
    // rest of code
    printf("sizeof(grades) = %d\n", sizeof(grades)); // 8! sizeof(ptr)
}
#define NUM_GRADES 5
int main() {
    int grades[NUM_GRADES] = {90, 45, 32, 100, 93};
    printf("Average is: %d", calc_avg(grades, NUM_GRADES));
}

המערך “מאבד” את המידע על גודלו מכיוון שהוא מועבר לפונקציה כמצביע. לכן, צורות הכתיבה עם [] בהעברה לפונקציה אינן מומלצות - כתיבה בצורה int fn(int* arr, int len) היא המועדפת ובה נשתמש בקורס.

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

int main() {
    int array[4][3];
    array[1][2] = 0;
}

כך שתמונת הזכרון נראת כך:

כאשר נזכיר שה-“רווחים” הקטנים שציירנו בין המערכים אינם באמת קיימים.

מה למעשה אנחנו מבקשים מהקומפיילר לעשות?

  1. הקצה עבורינו מערך של 4 איברים, בו כל איבר הוא מערך של 3 משתני int.
  2. גש למערך השני מתוך החמישה ובתוכו לאיבר השלישי.
  3. כתוב לתוכו 0.

נפענח את שלב 2. בשביל להגיע למערך השני, הקומפיילר צריך לדעת כמה איברים יש בכל מערך - הוא יודע את המידע הזה מההקצאה, 3 איברים. לכן הוא מפענח את הפקודה array[1][2] בתור - התקדם 3 איברים ואז 2 איברים וכך תגיע לאיבר הרצוי.

באופן כללי, עבור מערך דו-מימדי int arr[N][M], גישה לאיבר arr[i][j] תפורש באופן הבא כאריתמטיקת מצביעים:

arr + M * i + j

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

void fn(int array[][]) {
    array[1][2] = 0;
}
int main() {
    int array[4][3];
    fn(array);
}

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

void fn(int array[][3]) {
    array[1][2] = 0;
}
int main() {
    int array[4][3];
    fn(array);
}

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