Smart-World Surf

יחידה 3: פונקציות ומבנה תוכנית

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

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

הגדרת פונקציות וקריאה

פונקציות הן הדרך המרכזית לארגן קוד ב-C. הן מאפשרות לפרק בעיות גדולות לבעיות קטנות יותר, למנוע כפילות קוד (DRY - Don't Repeat Yourself) ולשפר את קריאות הקוד ותחזוקתו.

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

מבנה פונקציה בסיסי:

  • סוג ערך מוחזר (Return Type): הטיפוס של הערך שהפונקציה מחזירה (למשל int, double, void). אם הפונקציה אינה מחזירה ערך, נשתמש ב-void.
  • שם הפונקציה (Function Name): מזהה ייחודי לפונקציה.
  • רשימת פרמטרים (Parameter List): משתנים המקבלים ערכים מהקריאה לפונקציה. כל פרמטר מוגדר עם טיפוס ושם (למשל int x, char c).
  • גוף הפונקציה (Function Body): בלוק הקוד המכיל את ההוראות שהפונקציה מבצעת, מוקף בסוגריים מסולסלים {}.

לפני שניתן להשתמש בפונקציה, יש להצהיר עליה (Function Declaration או Prototype) ולממש אותה (Function Definition). ההצהרה מאפשרת לקומפיילר לדעת על קיומה של הפונקציה ועל חתימתה (signature) לפני שהוא נתקל בקריאה אליה.


// הצהרה על פונקציה (Function Prototype)
int add(int a, int b); 

int main() {
    int result = add(5, 3); // קריאה לפונקציה
    return 0;
}

// מימוש הפונקציה (Function Definition)
int add(int a, int b) {
    return a + b;
}

העברת פרמטרים: לפי ערך ולפי כתובת

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

העברה לפי ערך (Call by Value)

העברה לפי ערך (Call by Value): מנגנון העברת פרמטרים שבו מועתק ערך הארגומנט לפרמטר הפונקציה. שינויים בתוך הפונקציה אינם משפיעים על הארגומנט המקורי.
כאשר מעבירים פרמטר לפי ערך, נוצר עותק של ערך הארגומנט המקורי ונמסר לפונקציה. הפונקציה עובדת על העותק הזה. כל שינוי בפרמטר בתוך הפונקציה לא ישפיע על המשתנה המקורי שמחוץ לפונקציה. זוהי שיטת ברירת המחדל עבור טיפוסים פרימיטיביים (int, char, float וכו').

העברה לפי כתובת (Call by Reference)

העברה לפי כתובת (Call by Reference): מנגנון העברת פרמטרים שבו מועברת כתובת הזיכרון של הארגומנט לפרמטר הפונקציה (מצביע). מאפשר לפונקציה לשנות את ערכו המקורי של הארגומנט.
כאשר מעבירים פרמטר לפי כתובת, אנו מעבירים מצביע (pointer) לכתובת הזיכרון של הארגומנט המקורי. הפונקציה מקבלת את הכתובת ויכולה לגשת ישירות לערך המקורי ולשנות אותו באמצעות אופרטור ה-dereference (*). זה חיוני כאשר רוצים שפונקציה תשנה מספר ערכים, או כאשר מעבירים מבני נתונים גדולים כדי למנוע העתקה יקרה.

דוגמה: פונקציית החלפה (Swap)


void swap_by_value(int a, int b) { // לא יעבוד כמצופה
    int temp = a;
    a = b;
    b = temp;
}

void swap_by_reference(int *a, int *b) { // יעבוד
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    swap_by_value(x, y); // x ו-y נשארים 10, 20
    printf("After swap_by_value: x=%d, y=%d\n", x, y);

    x = 10, y = 20;
    swap_by_reference(&x, &y); // x ו-y יהפכו ל-20, 10
    printf("After swap_by_reference: x=%d, y=%d\n", x, y);
    return 0;
}

פונקציית main וארגומנטים משורת הפקודה

פונקציית main: נקודת הכניסה לכל תוכנית C; ממנה מתחילה ריצת התוכנית.

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

  • int argc: קיצור של "argument count", מייצג את מספר הארגומנטים שהועברו לתוכנית, כולל שם התוכנית עצמה.
  • char *argv[]: קיצור של "argument vector", זהו מערך של מצביעים למחרוזות (char*). כל מחרוזת במערך היא ארגומנט שהועבר. argv[0] הוא תמיד שם התוכנית.

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Number of arguments: %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("Argument %d: %s\n", i, argv[i]);
    }
    return 0;
}

אם נריץ את התוכנית הזו כך: ./myprogram hello world 123, הפלט יהיה:


Number of arguments: 4
Argument 0: ./myprogram
Argument 1: hello
Argument 2: world
Argument 3: 123

רקורסיה

רקורסיה (Recursion): טכניקת תכנות שבה פונקציה קוראת לעצמה, ישירות או בעקיפין, כדי לפתור בעיה על ידי פירוקה לבעיות קטנות יותר מאותו סוג.

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

  • תנאי עצירה (Base Case): תנאי שבו הפונקציה מפסיקה לקרוא לעצמה ומחזירה ערך ישיר. זהו המקרה הפשוט ביותר של הבעיה. ללא תנאי עצירה, הפונקציה תיכנס ללולאה אינסופית (stack overflow).
  • צעד רקורסיבי (Recursive Step): הפונקציה קוראת לעצמה עם קלט "קטן" יותר או "פשוט" יותר, המתקרב בהדרגה לתנאי העצירה.

דוגמה: חישוב עצרת (Factorial)


long long factorial(int n) {
    // תנאי עצירה: עצרת של 0 או 1 היא 1
    if (n == 0 || n == 1) {
        return 1;
    }
    // צעד רקורסיבי: n * factorial(n-1)
    return n * factorial(n - 1);
}

int main() {
    printf("Factorial of 5: %lld\n", factorial(5)); // פלט: 120
    return 0;
}
העברת מערכים לפונקציות: כאשר מעבירים מערך לפונקציה ב-C, המערך "מתנוון" למצביע לכתובת האיבר הראשון שלו. המשמעות היא שהעברה היא תמיד "לפי כתובת" (או ליתר דיוק, העתק של המצביע לכתובת הראשונה). הפונקציה מקבלת מצביע, ולא עותק של המערך כולו. לכן, שינויים באיברי המערך בתוך הפונקציה ישפיעו על המערך המקורי. יש לזכור להעביר גם את גודל המערך כפרמטר נפרד, שכן הפונקציה אינה "יודעת" את גודלו המקורי של המערך מתוך המצביע בלבד. זו נקודה קריטית למבחן!

שאלות לדיון

  • הסבר מתי נעדיף להשתמש בהעברת פרמטרים לפי ערך ומתי לפי כתובת. תן דוגמה לכל מקרה.
  • מהו תפקידם של argc ו-argv בפונקציית main? כיצד ניתן להשתמש בהם בתוכנית C?
  • הגדר רקורסיה והסבר את שני המרכיבים החיוניים לכל פונקציה רקורסיבית. מדוע רקורסיה עלולה להיות מסוכנת אם אינה מתוכננת כראוי?
  • כיצד שונה העברת מערך לפונקציה מהעברת משתנה מסוג int? מהן ההשלכות של הבדל זה?

נקודות לתשובת מודל

  • העברת פרמטרים: לפי ערך כאשר רוצים שהפונקציה תעבוד על עותק ולא תשנה את המקור (למשל, פונקציית חישוב); לפי כתובת כאשר רוצים שהפונקציה תשנה את המקור (למשל, פונקציית swap) או כאשר מעבירים מבנים גדולים כדי למנוע העתקה יקרה.
  • argc, argv: argc הוא מספר הארגומנטים כולל שם התוכנית, argv הוא מערך של מחרוזות (מצביעים ל-char) המכיל את הארגומנטים משורת הפקודה. שימושי לקונפיגורציה או קלט חיצוני לתוכנית.
  • רקורסיה: פונקציה הקוראת לעצמה. מרכיבים: תנאי עצירה (Base Case) וצעד רקורסיבי (Recursive Step). סכנות: Stack Overflow (אם אין תנאי עצירה או שהוא לא מושג), חוסר יעילות (חישובים חוזרים ללא memoization).
  • העברת מערך: מערך מתנוון למצביע לאיבר הראשון שלו. העברה היא "לפי כתובת" (של המצביע). שינויים בתוך הפונקציה משפיעים על המערך המקורי. חובה להעביר את גודל המערך כפרמטר נפרד, שכן המצביע לבדו אינו נושא מידע על גודל המערך המקורי.
מצאתם טעות או שחסר משהו?
→ הקודמת
יסודות C: אופרטורים ובקרת זרימה
הבאה ←
מערכים ומחרוזות