דוא"ל:
תפריט משתמש




שתף |

קורס:"שפת C"

שיעור 6: פונקציות

[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]

מתוך הספר: C - מדריך מקצועי       C - מדריך מקצועי
תוכן עניינים


פירוק אלגוריתם למשימות עצמאיות

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

לדוגמא, נתונה המשימה הבא:

כתוב/י תכנית שתדפיס את כל החזקות החד ספרתיות (המספרים 0..9) של 2 ושל 3 בטבלה.

מטרת המשימה היא להדפיס את הטבלה הבאה:

 
i    power(2,i)    power(3,i)
-    ----------    ----------    
0    1             1
1    2             3
2    4             9
3    8             27
4    16            81
5    32            243
6    64            729
7    128           2187
8    256           6561
9    512           19683
 

במבט ראשון, נראה שכדאי לפרק את המשימה ע"י לולאה:

הדפס כותרות לטבלה
בלולאה עם אינדקס i  מ- 0 עד 9 בצע:
          חשב את החזקה של 2 ב- i (res1)
          חשב את החזקה של 3 ב- i (res2)
          הדפס שורה בטבלה - i, res1  ו- res2

כעת יש לפרק את הוראות הלולאה הדורשות את חישוב החזקה: נבצע זאת ע"י לולאה פנימית העוברת על המספרים i..1 ומבצעת הכפלה של 2 או 3 כמספר החזרות.

האלגוריתם המלא:

משתנים: i,j - אינדקסים בלולאות, res1, res2 - משתנים לחישוב החזקות
אתחול:res1 <-- 1,  res2 <-- 1
הדפס כותרות לטבלה
בלולאה עם אינדקס i  מ- 0 עד 9 בצע:
          בלולאה עם אינדקס j  מ- 1 עד i  בצע:
                     res1 <-- res1*2
          בלולאה עם אינדקס j  מ- 1 עד i  בצע:
                     res2 <-- res2*3
          הדפס שורה בטבלה - i, res1  ו- res2

                       

כפי שניתן לראות, res1 ו- res2 מטופלים באופן דומה לחישוב החזקה ה- i-ית של 2 ו- 3 בהתאמה.

כל אחד מהם מוכפל j פעמים בבסיס החזקה (2 או 3). בכדי לבצע את חישוב החזקה נכון יש לאתחל את res1 ו- res2  ל- 1.

מהם חסרונות פירוק זה?

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

הגדרת משימה עצמאית ע"י פונקציה

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

בסיום החישוב הן מחזירות את התוצאה.

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

פונקציה power  לחישוב חזקה:

פרמטרים: base - בסיס החזקה,  n - מעריך החזקה
אתחול: result <-- 1
בלולאה עם אינדקס i  מ- 1 עד n  בצע:
              result <-- result * base
החזר את result

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

לדוגמא, בכדי לחשב את החזקה של 2 ב- 5 נכתוב power(2,5). אם נרצה לקבל את התוצאה במשתנה res למשל, נכתוב:

          res1 <-- power(2,5)

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

משתנים: i - אינדקס הלולאה, res1, res2 - משתני תוצאות החזקות
הדפס כותרות לטבלה
בלולאה עם אינדקס i  מ- 0 עד 9 בצע:
               res1 <-- power(2,i)
               res2 <-- power(3,i)
               הדפס שורה בטבלה - i, res1  ו- res2

מנגנון הפונקציות ב- C

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

חלוקה של התכנית לפונקציות מאפשרת פיתוח מודולרי והדרגתי של התכנית, וכן יכולת טובה יותר לנפות (debug) את השגיאות בתכנית.

תכנית בשפת C היא אוסף שטוח (ללא קינון) של פונקציות. אחת הפונקציות חייבת להיותmain( )  - זו הפונקציה שממנה מתחיל ביצוע התכנית.

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

פונקציה נקראת יכולה להמשיך ולקרוא לפונקציה נוספת ובכך ליצור שרשרת קריאות.

לדוגמא, נניח שיש לנו תכנית שבה קיימות הפונקציות h(), g(), f(), main(). main מבצעת קריאה ל- f ו- f קוראת לפונקציות g ו- h :

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

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

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

פונקציות לדוגמא שהכרנו (פונקציות סטנדרטיות ב- C): printf(), scanf(), main().

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

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

(<הצהרת פרמטרים>)  <שם-הפונקציה> < טיפוס-ערך-המוחזר>

}

<הגדרות טיפוסים, קבועים ומשתנים>    

<הוראות>      

{

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

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

 
/* file: powers.c */
#include <stdio.h>
int power (int base, int n)
{
          int result=1;
          int i;
 
          for (i=1; i<=n; ++i)
                   result = result * base;
          return result;
}
 

כעת נכתוב את הפונקציה הראשית בתכנית, main(), תוך שימוש בפונקציה power():

 
void main()
{
          int i;
          int res1, res2;
          printf("%-4s %-13s %-13s\n", "i", "power(2,i)", "power(3,i)");
          printf("%-4s %-13s %-13s\n", "-", "----------", "----------");
          for (i=0; i<10; ++i)
          {
                   res1 = power(2,i);
                   res2 = power(3,i);
                   printf("%-4d %-13d %-13d\n", i, res1, res2);
          }
}
 

פלט התכנית:

 
i    power(2,i)    power(3,i)
-    ----------    ----------    
0    1             1
1    2             3
2    4             9
3    8             27
4    16            81
5    32            243
6    64            729
7    128           2187
8    256           6561
9    512           19683
 

הסבר

  • בהגדרת הפונקציה power
 
int power (int base, int n)
{
          int result=1;
          int i;
          ...
}
 

     ציינו ש- :

  - הפונקציה מקבלת שני פרמטרים מטיפוס שלם : base ו- n.
  - הפונקציה מחזירה ערך מטיפוס שלם.
  - בתוך הפונקציה ניתן להגדיר משתנים מקומיים (i, result) לצורך חישובי ביניים.
  - גוף הפונקציה כולל את חישוב החזקה ע"י לולאת for:
 
          for (i=1; i<=n; ++i)
                   result = result * base;
 
  - התוצאה המוצבת במשתנה result מוחזרת בסוף הפונקציה ע"י ההוראה:
 
           return result ;
 
  • השימוש בפונקציה power בתוך main :
  - הפונקציה power מוגדרת ראשונה בכדי ש- main תוכל לקרוא לה. ככלל, פונקציה יכולה לקרוא רק לפונקציות שהוגדרו או הוכרזו לפניה (ראה/י להלן).
  - הקריאה לפונקציה power מתבצעת פעמיים:
 
          res1 = power(2,i);
          res2 = power(3,i);
 
    בכל קריאה מועברים 2 פרמטרים לפונקציה ומוחזר ערך התוצאה, שמוצב בפונקציה הקוראת למשתנה המתאים.

קריאה ישירה לפונקציה בהוראת ההדפסה

נכתוב את הפונקציה הראשית main שוב באופן מעט שונה:

 
void main()
{
          int i;
          printf("%-4s %-13s %-13s\n", "i", "power(2,i)", "power(3,i)");
          printf("%-4s %-13s %-13s\n", "-", "----------",      "----------");
          for (i=0; i<10; ++i)
                   printf("%-4d %-13d %-13d\n", i, power(2,i), power(3,i));
}
 

הפעם נקראת הפונקציה power בתוך הוראת ההדפסה, ותוצאתה מועברת ישירות כפרמטר לפונקציה printf לצורך הדפסה:

 
                   printf("%-4d %-13d %-13d\n", i, power(2,i), power(3,i));
 

בגרסה זו של הפונקציה main אין צורך בהגדרת המשתנים res1, res2.

הכרזה על אבטיפוס פונקציה

הכרזה על אבטיפוס פונקציה (function prototype) נדרשת כאשר היא נקראת ע"י פונקציה אחרת המופיעה לפניה בקובץ התכנית. נכתוב את התכנית שוב בסדר פונקציות שונה:

 
#include <stdio.h>
int power(int base, int n)/* prototype for the function power() */
 
void main()
{
          int i;
          printf("%-4s %-13s %-13s\n", "i", "power(2,i)", "power(3,i)");
          printf("%-4s %-13s %-13s\n", "-", "----------", "----------");
          for (i=0; i<10; ++i)
                   printf("%-4d %-13d %-13d\n", i, power(2,i), power(3,i));
}
 
int power (int base, int n)
{
          int result=1;
          int i;
 
          for (i=1; i<=n; ++i)
                   result = result * base;
          return result;
}
 

השורה

 
 int power(int base, int n)  ;
 

היא הכרזה מוקדמת על הפונקציה power הדרושה כדי ש- main תוכל לקרוא לה, הואיל  ו- power מופיעה מאוחר יותר מ- main בתכנית.

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

 
  int power(int, int) ;
 

כלומר לשמות base ו- n אין משמעות בהכרזה על האבטיפוס.

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

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

הצהרת הפרמטרים

בקטע ההצהרה על הפרמטרים אנו קובעים אילו פרמטרים יועברו לפונקציה, כלומר מאיזה טיפוס וכמה פרמטרים. פרמטרים אלה נקראים פרמטרים פורמליים.

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

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

המהדר בודק התאמה בין הפרמטרים האקטואליים לפרמטרים הפורמליים בקריאה לפונקציה.

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

דוגמא:

 
int power (int base, int n);
 
void main()
{
          int i = 3;
          float f = 2.5;
 
          int res = power(i,f);
}
 

בקריאה לפונקציה power המהדר יגלה אי-התאמה בפרמטר השני בין הטיפוס הפורמלי לטיפוס האקטואלי. 

מכיוון שקיימת המרה מרומזת מממשי לשלם הוא יבצע אותה - כלומר יקצוץ את השבר של f - ותוצאת הפונקציה תהיה power(3,2) = 9. כמו כן תתקבל אזהרה מהמהדר על ביצוע ההמרה.

לעומת זאת, אם ננסה לבצע

 
int res = power(i,"hello");
 

המהדר יודיע על שגיאה מכיוון שאין שיטת המרה ממחרוזת למספר.

הצהרה על פרמטר כקבוע ע"י const

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

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

 
int func(const int c1, const float c2);
 

כעת לא ניתן לשנות את c1,c2 בתוך הפונקציה func.

שאלה: מדוע ומתי להגדיר פרמטר כ- const ?

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

ברור שבמקרה זה הפונקציה לא אמורה לשנות את הפרמטרים. הגדרתם כ- const יכולה לאתר שגיאה אפשרית של המתכנת:

 
int power (const int base, const int n);
 

באופן עקרוני, נהוג להפריד בין מספר סוגי פרמטרים לפונקציה:

  • פרמטרי קלט - פרמטרים המהווים קלט לעיבוד בפונקציה
  • פרמטרי פלט - פרמטרים שבהם מוצבת תוצאת העיבוד (ראה/י פרק 8, "מצביעים")
  • פרמטרי קלט/פלט - פרמטרים בשימוש כפול: גם כקלט לעיבוד וגם כפלט התוצאה

בשפות מסוימות מצהירים על הפרמטר (in, out, inout) בהתאם לסוגו.

זה עוזר מאוד הן בתיעוד התוכנה והן ביכולת של המהדר להתריע בפני שגיאות תכנות.

בשפת C אופן זה של הכרזה על פרמטרים לא קיים - כחלופה בסיסית פרמטרי הקלט יכולים להיות מצוינים ע"י const.

ערך מוחזר

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

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

הערך המוחזר מוחזר ע"י ההוראה return. הוראה זו גם גורמת לסיום הביצוע של הפונקציה וחזרה לפונקציה הקוראת.

ברירת המחדל עבור טיפוס הערך המוחזר היא שלם (int).

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

 
main()
{
          ...
          return 0;
}
 

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

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

המרת הערך המוחזר

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

 
float max(float x, float y)
{
          if(x>y)
                   return x;
          else
                   return y;
}
 
void main()
{
          float  a=9.4, b=34.8;
          int i;
        i = max(a,b)/* convert float to integer implicitly*/
}
 

במקרה זה המהדר יציג הודעת אזהרה. ניתן לבצע המרה מפורשת ע"י

 
        i = (int) max(a,b)/* convert float to integer explicitly*/
 

הפונקציה main

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

נהוג לציין ש- main מחזירה ערך מטיפוס int המציין את תוצאת הביצוע של התכנית: הצלחה מצוינת ע"י החזרת 0, וכשלון ע"י החזרת מספר שונה מ- 0 (למשל 1). תכנית דוגמא:

 
#include <stdio.h>
int main()
{
          int    num1;
          float num2;
          int    n;
          printf("Please enter 2 numbers - integer and real: ");
          n = scanf("%d %f", &num1, &num2);
          if(n!=2) 
          {
                   printf("Incorrect input!");
                   return 1;
          }
          else
          {
                   printf("The numbers are: %d %.2f", num1, num2);
                   return 0;
          }
}
 

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

לדוגמא, בעמ' 142 מובא קובץ batch המפעיל את התכנית prog.exe. עיין/י בקובץ זה.

תרגול

קרא/י סעיף זה בספר ובצע/י את תר' 1-2 בעמ' 143.

משתנים ופונקציות

המשתנים ב- C נחלקים ל- 2 סוגים  עיקריים:

  • משתנה גלובלי - משתנה המוגדר מחוץ לכל פונקציה.
  • משתנה מקומי - משתנה המוגדר בתוך פונקציה מסוימת.


בהקשר לסוגי המשתנים, קיימים שני מושגים חשובים:

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

משתנים גלובליים

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

דוגמא:

 
#include <stdio.h>
int Xcm;
 
void convert();
int main()
{
          printf("Input a distance:");
          scanf("%d",&Xcm);
          convert();
          return 0;
}
void convert()
{
          float Xinch;
          Xinch= Xcm / 2.54;
          printf("%d centimeters are %6.2f inches\n",Xcm,Xinch);
}
 

Xcm הוא משתנה גלובלי ה"מוכר" לשתי הפונקציות, main ו- convert, ע"י כך שהוא מוגדר מחוץ להן

 
int Xcm;
 

הפונקציה  main קוראת את ערכו מהקלט ע"י

 
          scanf("%d",&Xcm);
 

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

רצוי להמעיט בהגדרת משתנים גלובליים בתכנית משתי סיבות עיקריות:

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

משתנים מקומיים

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

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

דוגמא:

 
#include <stdio.h>
 
void f();
int main()
{
          int i;
          for(i=0; i<4; i++)
               f();
}
 
void f()
{
          int x=0;
          int y=0;
          printf("x is %d, y is %d\n", x, y);
        ++x;
        ++y;
}
 

פלט התכנית:

 
x is 0, y is 0
x is 0, y is 0
x is 0, y is 0
x is 0, y is 0
 

כפי שרואים, x ו- y מאותחלים בכל קריאה לפונקציה f() מחדש, וקידומם בסוף הפונקציה אינו תורם דבר.

משתנה מקומי - סטטי

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

אופן ההגדרה:

 
static int x;
static float f;
 

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

למשל, אם בדוגמא הקודמת נשנה את הגדרת המשתנה  x:

 
#include <stdio.h>
 
void f();
int main()
{
          int i;
          for(i=0; i<4; i++)
                    f();
}
 
void f()
{
        static int x=0;
          int y=0;
          printf("x is %d, y is %d\n", x, y);
          ++x;
          ++y;
}
 

פלט התכנית יהיה:

 
x is 0, y is 0
x is 1, y is 0
x is 2, y is 0
x is 3, y is 0
 

משתנה מקומי בבלוק

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

דוגמא:

 
#include <stdio.h>
void main()
{
          int x=2;
 
          {        
                   int x;
                   x = 3;
          }
          printf("x=%d",x);
}
 

יודפס:

 
x=2
 

המשתנה x המוגדר בתוך הבלוק הפנימי שונה מהמשתנה x המוגדר בבלוק הפונקציה main() ולכן ערכו של x החיצוני אינו משתנה.

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

איתחול משתנים בברירת מחדל

למשתנה שלא אותחל קיים ערך ברירת מחדל, בהתאם לסוגו:

  • משתנה גלובלי מאותחל בברירת מחדל ל- 0. שלמים מאותחלים ל- 0, ממשיים ל- 0.0 ותווים מקבלים את ערך ה- ASCII 0.
  • משתנה מקומי אינו מאותחל לערך ידוע. כלומר, משתנה מקומי שהוגדר ללא איתחול הוא בעל ערך אקראי (ערך "זבל").
  • משתנה מקומי סטטי מאותחל כמו משתנה גלובלי.

סוגי משתנים - סיכום

סוג משתנה

תחום פעולה ( scope )

משך הקיום

חיצוני (גלובלי)

ממקום ההגדרה ועד סוף הקובץ (ניתן להרחבה ע"י extern, כפי שנראה בפרק 11).

כמשך קיום התכנית

מקומי

בפונקציה המגדירה בלבד (או בבלוק המגדיר)

כמשך קיום הפונקציה (או הבלוק המגדיר)

מקומי - סטטי

בפונקציה המגדירה בלבד

כמשך קיום התכנית

מבנה זיכרון התכנית

קטע הקוד וקטע הנתונים

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

  • קטע הקוד (Code segment)
  • קטע הנתונים (Data Segment)

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

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

לדוגמא, נתונה התכנית

 
prog.c
#include <stdio.h>
int  g1=8,  g2=20;
 
void main()
{
          int x;
          if(g1>g2)
                   x = g1;
          else
                   x = g2;
          printf("x=%d",x);
}
 

קובץ הביצוע הנוצר לאחר ההידור כולל את 2 מרכיבי הזיכרון:

 
                   prog.exe
 
 
g1=8
  g2=20
 
 
Data Segment
 
 
main:
  int x;
  if(g1>g2)
            x = g1;
  else
            x = g2;
  printf("x=%d",x);
  ...
 
 
Code Segment
 
הערה : קטע הקוד מכיל קוד בשפת מכונה - מטעמי פשטות הקוד בתרשים הוא בשפת המקור C.

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

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

  • קטע הנתונים - עבור אחסון הנתונים הנטענים מקובץ ה- .exe
  • קטע קוד - עבור אחסון קוד התכנית הנטען מקובץ ה- .exe
  • מחסנית הקריאות לפונקציות - עבור נתוני הפונקציות והתקשורת ביניהן (להלן).
  • ערמה - עבור הקצאות זיכרון מפורשות מהתכנית. על נושא זה נלמד בפרק 12.

מהלך יצירת התכנית וטעינתה לביצוע נראה כך:

שאלה: היכן מאוחסנים שאר נתוני התכנית - כלומר המשתנים המקומיים שבפונקציות?

תשובה: משתנים אלה נוצרים על מחסנית הקריאות.

מחסנית הקריאות

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

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

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

לדוגמא, בתכנית הבאה קיימות 2 פונקציות:

 
int  max(int x, int y)
{
          int m;
          if(x>y)
                   m=x;
          else
                   m=y;
          return m;
}
 
void main()
{
          int  res;
          int a=8, b=29;
          res = max(a,b);
}
 

בתחילת ריצת התכנית, הפונקציה main() מתבצעת. לפני הקריאה ל- max() מחסנית הקריאות כוללת רק את מסגרת הפונקציה main(). מצב המחסנית הוא:

 
res
  a=8
  b=29
 
 
main()
 
הערה : מטעמי פשטות נתעלם מכתובת החזרה הנשמרת כחלק ממסגרת הפונקציה.

לאחר הקריאה לפונקציה max() ובמשך ביצועה מצב המחסנית הוא :

 
m
  x=8
  y=29
 

max()

 
res
  a=8
  b=29
 

main()

בחזרה מהפונקציה max(), מוחזר הערך של m - שהוא המקסימום מבין x,y - על המחסנית:

 
29
 


 
res
  a=8
  b=29
 
 
main()
 

והערך המוחזר מוצב למשתנה res שבפונקציה main

 
res=29
  a=8
  b=29
 
 
main()
 

תמונת הזיכרון הכוללת

בעמ' 151-155 מובאת תוכנית דוגמא תוך הדגמת מבנה הזיכרון בכל שלב. עיין/י בעמודים אלו.

רקורסיה

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

דוגמא פשוטה לפונקציה רקורסיבית:

 
#include <stdio.h>
void main()
{
          puts("hello");
          main();
}
 

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

תהליך זה הוא אינסופי, ולכן על המסך תודפס בלולאה אינסופית המחרוזת hello:

 
hello
hello
hello
hello
hello
...
 

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

 
#include <stdio.h>
int azeret(int n)
{
          if(n<=1)
                   return 1;
          else
                   return n*azeret(n-1);
}
 
void main()
{
          printf("Azeret of 4 is : %d", azeret(4));
}
 

פלט התכנית:

 
Azeret of 4 is: 24
 


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

 
          if(n<=1)
                   return 1;
 

במידה והפרמטר לפונקציה הוא 1 או פחות מוחזר 1. אחרת, הפונקציה מחזירה את העצרת של n-1 ע"י קריאה רקורסיבית:

 
          else
                   return n*azeret(n-1);
 

התכנית קוראת ומדפיסה את ערך העצרת של 4 ע"י הקריאה

 
          printf("Azeret of 4 is : %d", azeret(4));
 

תרשים הקריאות:



משחק מגדלי הנוי

מגדלי הנוי הוא משחק עתיק המציב בפני השחקן את האתגר הבא: נתונים 3 מגדלים, A,B,C, כאשר על מגדל A מושחלות n טבעות:

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

  • בכל שלב מועברת טבעת יחידה.
  • ניתן להשתמש במגדל B להעברות ביניים. כמו כן ניתן בכל שלב להעביר טבעת מכל מגדל לכל מגדל.
  • בכל שלב, בכל מגדל חייב להישמר סדר יורד של טבעות, כלומר טבעת קטנה תהיה מעל טבעת גדולה יותר.

לדוגמא, עבור n=2, הפתרון יהיה:

  - העבר את הטבעת הקטנה מ- A ל- B
  - העבר את הטבעת הגדולה מ- A ל- C
  - העבר את הטבעת הקטנה מ- B ל- A

עבור n=3 הפתרון יהיה:

  - העבר את הטבעת הקטנה מ- A ל- C
  - העבר את הטבעת הבינונית מ- A ל- B
  - העבר את הטבעת הקטנה מ- C ל- B
  - העבר את הטבעת הגדולה מ- A ל- C
  - העבר את הטבעת הקטנה מ- B ל- A
  - העבר את הטבעת הבינונית מ- B ל- C
  - העבר את הטבעת הקטנה מ- A ל- C

שאלה: עבור n כלשהו מהו האלגוריתם לפתרון ?

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

פונקציה move:
פרמטרים:n - מספר הטבעות על המגדל המקורי
               t1 - המגדל שממנו מעבירים את הטבעות
               t2 - המגדל שדרכו מועברות הטבעות
               t3 - המגדל שאליו מועברות הטבעות
אם n=1
          העבר את הטבעת (היחידה) מ- t1  ל- t3
אחרת
          קרא ל- move  להעברת n-1 טבעות מ- t1  ל- t2  דרך t3
          העבר את הטבעת ה- n -ית (הגדולה ביותר) מ- t1  ל- t3
          קרא ל- move  להעברת n-1 הטבעות מ- t2  ל- t3 דרך t1

כלומר, הרעיון הוא לפתור בכל שלב את הבעיה עבור מספר קטן ב- 1 של מספר הטבעות, באופן רקורסיבי. קוד התכנית המממשת את האלגוריתם מובא בעמ' 160-161.



תרגול

קרא/י סעיף זה בספר ובצע/י את התרגיל שבעמ' 162.

 

סיכום

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

תרגילי סיכום

בצע/י את תר' 1-4 שבעמ' 163-164.



[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]