שפת C/פונקציות

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

הצורך בפונקציות עריכה

נניח שאנו כותבים תוכנית קטנה להמרת מעלות מ-Celsius ל-Fahrenheit (ראה גם כאן וכאן). התכנית תתחיל בכך שתדפיס את התרגום למעלות Fahrenheit של מעלות ה-Celsius בערכים 0, 4, 8, ..., 40, ולאחר מכן תבקש מהמשתמש מעלה ב-Celsius, ותדפיס את ערכו ב-Fahrenheit. נרשום את התוכנית כך:

#include <stdio.h>


int main()
{
  int c = 0;
  int f = 0;

  for (c = 0; c <= 40; c += 4)  
  {
    f = 1.8 * c + 32;
  
    printf("%d in Celsius is %d in Fahrenheit\n", c, f);
  }

  printf("Enter degrees in Celsius: ");
  scanf("%d", &c);
  
  f = 1.8 * c + 32;
  
  printf("This is %d in Fahrenheit\n", f);
  
  return 0;
}

נוכל לשים לב שהשורה

f = 1.8 * c + 32;

מופיעה פעמיים בתוכנית. זהו דבר בעייתי:

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

ככל שהתוכנית ארוכה ומסובכת יותר, הבעייתיות בדברים כאלה גדלה.

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

float celsius_to_fahrenheit(float celsius)
{
  float f = 1.8 * celsius + 32;
  return f;
}

הגדרת פונקציה עריכה

על מנת להגדיר פונקציה, יש לכתוב:

<return_type> <function_name>(<parameters>)
{
  <body>
}

כאשר:

  • return_type הוא סוג הערך המוחזר מהפונקציה, כלומר טיפוס המשתנה המוחזר מהפונקציה (אם לא רוצים שהפונקציה תחזיר אף משתנה, כותבים void כטיפוס המשתנה).
  • function_name הוא שם הפונקציה.
  • parameters הם ארגומנטים, כלומר משתנים שערכם נקבע מחוץ לפונקציה.
  • body הוא הפקודות המתבצעות כשהפונקציה נקראת.


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

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

return <value>;

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

return;

דבר שיגרום ליציאה מהפונקציה.

דוגמאות עריכה

פונקציה עם ערך מוחזר עריכה

הנה הפונקציה הממירה מספר נקודה-צפה המתאר טמפרטורה ב-Celsius לטמפרטורה ב-Fahrenheit:

float celsius_to_fahrenheit(float celsius)
{
  return 1.8 * celsius + 32;
}

הפונקציה מקבלת משתנה מסוג מספר נקודה-צפה ששמו celsius, ומחזירה מספר נקודה-צפה.

פונקציה בלי ערך מוחזר עריכה

הנה פונקציה המדפיסה את התרגום למעלות Fahrenheit של מעלות ה-Celsius בערכים 0, 4, 8, ..., 40:

void print_conversion_table()
{
  int c, f;

  for (c = 0; c <= 40; c += 4)  
  {
    f = 1.8 * c + 32;
  
    printf("%d in Celsius is %d in Fahrenheit\n", c, f);
  }
}

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


 

כדאי לדעת:

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

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

נניח שהחלטנו לשאול את המשתמש האם להדפיס את טבלת ההמרות, ואם המשתמש יקליד את התו 'n', לא נדפיס כלום.. נוכל לכתוב זאת כך:

void print_conversion_table_if_needed()
{
  int c, f;  
  char reply;
  
  printf("Print out conversion table?");
  scanf("%c", &reply);
  if (reply == 'n')
    return;
  for (c = 0; c <= 40; c += 4)  
  {
    f = 1.8 * c + 32;
  
    printf("%d in Celsius is %d in Fahrenheit\n", c, f);
  }
}

בפונקציה הקודמת, נשים לב לשורות:

  if (reply == 'n')
    return;

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

קריאה לפונקציה עריכה

קריאה לפונקציה נכתבת כך:

<function_name>(<values>);

כאשר function_name היא שם הפונקציה, ו-values הם הערכים שיש להשים למשתניה. אם הפונקציה אינה מקבלת ארגומנטים, פשוט רושמים כך:

<function_name>();

.

להלן דוגמה לקריאה לפונקציה celsius_to_fahrenheit:

#include <stdio.h>

float celsius_to_fahrenheit(float celsius)
{
  return 1.8 * celsius + 32;
}


int main()
{
  int f;
  
  f = celsius_to_fahrenheit(3);
  
  printf("%d", f);
  
  return 0;
}

השורה

f = celsius_to_fahreneit(3);

קוראת לפונקציה עם הערך 3. כעת הפונקציה מתחילה לפעול, ובתוך הפוקנציה, המשתנה celsius הוא בעל הערך 3. כשהפונקציה מגיעה לשורה

  return 1.8 * celsius + 32;

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

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

#include <stdio.h>

float celsius_to_fahrenheit(float celsius)
{
  return 1.8 * celsius + 32;
}


int main()
{
  int c, f;

  for (c = 0; c <= 40; c += 4)  
  {
    f = celsius_to_fahrenheit(c);
  
    printf("%d in Celsius is %d in Fahrenheit\n", c, f);
  }

  printf("Enter degrees in Clesius: ");
  scanf("%d", &c);
  
  f = celsius_to_fahrenheit(c);
  
  printf("This is %d in Fahrenheit\n", f);
  
  return 0;
}

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

#include <stdio.h>

float celsius_to_fahrenheit(float celsius)
{
  return 1.8 * celsius + 32;
}


int main()
{
  int c;

  for (c = 0; c <= 40; c += 4)  
    printf("%d in Celsius is %d in Fahrenheit\n", c, celsius_to_fahrenheit(c));

  printf("Enter degrees in Clesius: ");
  scanf("%d", &c);
  
  printf("This is %d in Fahrenheit\n", celsius_to_fahrenheit(c));
  
  return 0;
}

פונקציות שכבר ראינו עריכה

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


הפונקציה main עריכה

כל תוכנית בשפת C חייבת לכלול את הפונקציה main. זוהי הפונקציה הראשונה שמורצת כאשר מורצת התוכנית, וכאשר מסתיימת הרצתה, מסתיימת הרצת התוכנית.

אנו ראינו אותה בגרסה הזו:

int main()
{
  <body>
}

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


 

שימו לב:

לעתים אפשר לראות את הפונקציה main מוגדרת כך:

()void main כלומר, בגרסה שאינה מחזירה ערך. מדובר בשגיאה.


 

כדאי לדעת:

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

פונקציות פלט וקלט עריכה

בפלט וקלט ראינו כבר את הפונקציות printf ו- scanf.

הצהרה על פונקציות עריכה

נתבונן בתוכנית הבאה:

#include <stdio.h>


int main()
{
    int a, b;
    
    printf("Enter two numbers:\n");
    
    scanf("%d %d", &a, &b);
    print_bigger( a, b );
    
    return 0;
}


void print_bigger(int x, int y)
{
  if (x>y) 
    printf("%d",x);
  else 
    printf("%d",y);
}

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

    print_bigger( a, b );

תיקרא הפונקציה print_bigger, ולאחר שתסתיים הקריאה לפונקציה, תחזור התוכנית ל-main.

על אף שהכל נראה בסדר, המהדר יודיע שבתוכנית יש שגיאה. כאשר המהדר מגיע לשורה הקוראת ל-print_bigger, הוא עדיין לא יודע שיש פונקציה כזאת - היא מוגדרת מאוחר יותר בקובץ. המהדר יתלונן שאין פונקציה כזו. לדוגמה, המהדר gcc מתלונן כך:

main.c: In function main:
main.c:11: warning: implicit declaration of function print_bigger

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

פתרון מקובל אחר, הוא להשאיר את הסדר כפי שהוא, אך להצהיר על הפונקציה print_bigger לפני הפונקציה main, כך שהמהדר ידע על קיומה ועל האופן שבו היא צריכה להקרא. הצהרה כזאת (declaration בלעז) מתבצעת על ידי כתיבת האב-טיפוס (prototype בלעז) של הפונקציה, כלומר: הטיפוס המוחזר, שם הפונקציה וטיפוסי הפרמטרים, עם נקודה-פסיק בסוף. במקרה שלנו, לדוגמה, ההצהרה תראה כך:

void print_bigger(int x, int y);

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

#include <stdio.h>

/* This is a declaration. */
void print_bigger(int x, int y);


int main()
{
    int a, b;
    
    printf("Enter two numbers:\n");
    
    scanf("%d %d", &a, &b);
    print_bigger( a, b );
    
    return 0;
}


/* And here is the definition. */
void print_bigger(int x, int y)
{
  if (x>y) 
    printf("%d",x);
  else 
    printf("%d",y);
}

פונקציות רקורסיביות עריכה

 

שקלו לדלג על נושא זה

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



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

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

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

unsigned long factorial(unsigned int n)
{
  unsigned long fact = 1;
  unsigned int i;

  for(i = 1; i <= n; ++i)
   fact *= i;

  return fact;
}

ולהלן פונקציה רקורסיבית לחישוב עצרת:

unsigned long factorial(unsigned int n)
{
  if(n == 0)
    return 1;
    
  return n * factorial(n - 1);
}

או בצורה קצרה יותר:

unsigned long factorial(unsigned int n)
{
  return n == 0? 1 : n * factorial(n - 1);
}

מעט על פונקציות והנדסת תוכנה עריכה

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

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

נשתמש בקוד שראינו בצורך בפונקציות כדוגמה (למרות שזהו קוד פשוט מאד). ראשית נתבונן בפונקציה main:

#include <stdio.h>


int main()
{
  int c, f;

  for (c = 0; c <= 40; c += 4)  
  {
    f = 1.8 * c + 32;
  
    printf("%d in Celsius is %d in Fahrenheit\n", c, f);
  }

  printf("Enter degrees in Clesius: ");
  scanf("%d", &c);
  
  f = 1.8 * c + 32;
  
  printf("This is %d in Fahrenheit\n", f);
  
  return 0;
}

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

#include <stdio.h>


void print_init_conversion_table();
void handle_conversion_query();


int main()
{
  print_init_conversion_table();
  handle_conversion_query();
  
  return 0;
}


void print_init_conversion_table()
{
  int c, f;

  for (c = 0; c <= 40; c += 4)  
  {
    f = 1.8 * c + 32;
  
    printf("%d in Celsius is %d in Fahrenheit\n", c, f);
  }
}


void handle_conversion_query()
{
  int c, f;

  printf("Enter degrees in Clesius: ");
  scanf("%d", &c);
  
  f = 1.8 * c + 32;
  
  printf("This is %d in Fahrenheit\n", f);
}

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

#include <stdio.h>


float celsius_to_fahrenheit(int celsius);
void print_init_conversion_table();
void handle_conversion_query();


int main()
{
  print_init_conversion_table();
  handle_conversion_query();
  
  return 0;
}


void print_init_conversion_table()
{
  int c, f;

  for (c = 0; c <= 40; c += 4)  
    printf("%d in Celsius is %d in Fahrenheit\n", c, (int)celsius_to_fahrenheit(c));
}


void handle_conversion_query()
{
  int c;

  printf("Enter degrees in Clesius: ");
  scanf("%d", &c);
  
  printf("This is %d in Fahrenheit\n", (int)celsius_to_fahrenheit(c));
}


float celsius_to_fahrenheit(int celsius)
{
  return 1.8 * celsius + 32;
}

איכות הקוד כעת טובה יותר:

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

במעט על מבנים והנדסת תוכנה נדבר עוד על עניינים אלה בהקשר של מבנים.


הפרק הקודם:
לולאות
פונקציות
תרגילים
הפרק הבא:
מערכים