מבוא לתכנות ולמדעי המחשב בשפת C/פונקציות

תבנית:ניווט מבוא


פונקציות ותכנות פרוצדוראלי

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

מוטיבציה

עריכה

נניח שאנחנו כותבים תוכנה שצריכה לחשב מספר סכומים חלקיים מהסוג: <m>1^2+2^2+...+n^2</m>

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

#include <stdio.h>

int main() {
    int s,i;

    s = 0; 
    for(i=1; i<=6; ++i)
	s += i*i; 
    printf("The sum 1^2+2^2+3^2+...+6^2 is %d\n",s);

    s = 0; 
    for(i=1; i<=9; ++i)
	s += i*i; 
    printf("The sum 1^2+2^2+3^2+...+9^2 is %d\n",s);

    s = 0; 
    for(i=1; i<=5; ++i)
	s += i*i; 
    printf("The sum 1^2+2^2+3^2+...+5^2 is %d\n",s);

    return 0;
}

פלט:

The sum 1^2+2^2+3^2+...+6^2 is 91
The sum 1^2+2^2+3^2+...+9^2 is 285
The sum 1^2+2^2+3^2+...+5^2 is 55

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

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

#include <stdio.h>

int squareSum(int n) {
    int i,sum = 0; 
    for(i=1; i <= n; ++i)
	sum += i*i; 
    return sum; 
}

int main() {
    int sum;

    sum = squareSum(6); 
    printf("The sum 1^2+2^2+3^2+...+6^2 is %d\n",sum);

    printf("The sum 1^2+2^2+3^2+...+9^2 is %d\n",squareSum(9));

    printf("The sum 1^2+2^2+3^2+...+5^2 is %d\n",squareSum(5));

    return 0;
}

(הפלט זהה לזה של הקוד הקודם)

הסבר: נתבונן בהגדרת הפונקציה:

int squareSum(int n) {
    int i,s = 0; 
    for(i=1; i <= n; ++i)
	sum += i*i; 
    return sum; 
}

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

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

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

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

מימוש פונקציית חזקה

עריכה

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


#include <stdio.h>

// computes a to the power b
int myPow(int a, int n) {
    int i,ret = 1; 
    for(i=0; i < n; ++i)
	ret *= a; 
    return ret; 
}

int main() {
    printf("2^8 = %d\n",myPow(2,8));

    int i; 
    for(i=3; i<=10; ++i) 
	printf("%d^6 = %d\n",i,myPow(i,6));

    printf("(2^3)^4 = %d\n",myPow(myPow(2,3),4));

    return 0;
}

פלט:

2^8 = 256
3^6 = 729
4^6 = 4096
5^6 = 15625
6^6 = 46656
7^6 = 117649
8^6 = 262144
9^6 = 531441
10^6 = 1000000
(2^3)^4 = 4096

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

2 + myPow(x + 1, 8)

הוא ביטוי חוקי.

פונקציה שמבצעת פעולות

עריכה

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

לדוגמה:


#include <stdio.h>

double charSequence(char c, int n) {
    int i; 
    for(i=0; i<n; ++i)
	printf("%c",c); 
    printf("\n"); 
    return 17.7; 
}

int main() {
    charSequence('*',8); 
    charSequence('=',20); 
    charSequence('+',14); 
    double d = charSequence('#',14); 
    printf("%lf\n",d); 

    return 0;
}

פלט:

********
====================
++++++++++++++
##############
17.700000

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

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

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

void charSequence(char c, int n) {
    int i; 
    for(i=0; i<n; ++i)
	printf("%c",c); 
    printf("\n"); 
}

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

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

#include <stdio.h>

void doSomething() {
    printf("Ha!\n"); 
}

int main() {
    doSomething(); 
    doSomething(); 
    return 0;
}

הפלט יהיה פעמיים !Ha.

הפונקציה main

עריכה

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

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

ב GNU/Linux יש אפשרות להדפיס את הערך הזה. נקח לדוגמה את הקוד הפשוט הבא:

int main() {
    return 17;
}

נניח שקימפלנו אותו וקובץ ההרצה נקראה check. כעת, ניתן לכתוב מה shell:

./check 
echo $?

ואז נקבל כתשובה - 17. כזכור, echo היא פשוט פקודת ההדפסה של ה shell. ?$ מחזיק את ערך ה exit status של התוכנית שהורצה. בגלל שהערך שהחזרנו מ main היה 17, זה מה שקיבלנו.

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

פרמטר כמשתנה לוקאלי והעברה by value

עריכה

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

#include <stdio.h> 

void foo(int n) {
    while (n>0) {
	printf("%d, ",n); 
	--n; 
    }
    printf("\n"); 
}

int main() {
    int a = 3;
    foo(a); 
    printf("%d\n",a); 

    return 0;
}

פלט:

3, 2, 1, 
3

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

שימו לב שמבחינת המשמעות, a ו n הם משתנים נפרדים לחלוטין. היינו יכולים לשנות את שמו של המשתנה a להיות גם הוא n ועדיין התוכנית היתה מתנהגת באופן זהה. עדיין היה מדובר בשני משתנים נפרדים - n של main (לשעבר a) ו n של foo:

<#include <stdio.h> 

void foo(int n) {
    while (n>0) {
	printf("%d, ",n); 
	--n; 
    }
    printf("\n"); 
}

int main() {
    int n = 3;
    foo(n); 
    printf("%d\n",n); 

    return 0;
}

(פלט זהה לקוד הקודם)

פונקציות קוראות לפונקציות

עריכה

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

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

#include <stdio.h> 

void foo3() {
    printf("I'm foo3\n"); 
}

void foo2() {
    printf("I'm foo2\n"); 
    foo3(); 
}

void foo1() {
    printf("I'm foo1\n"); 
    foo2();
    foo3(); 
}

int main() {
    foo1(); 

    return 0;
}

פלט:

I'm foo1
I'm foo2
I'm foo3
I'm foo3

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

עריכה

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

#include <stdio.h> 

int a; 
double ar[10]; 

void foo() {
	printf("in foo: %d, %lf\n",a,ar[7]); 
	++a;
	ar[7] = 33;  
}

int main() {
	a = 17; 
	ar[7] = 3; 
	foo(); 
	printf("in main: %d, %lf\n",a,ar[7]); 
	return 0; 
}

פלט:

in foo: 17, 3.000000
in main: 18, 33.000000

כפי שאפשר לראות, גם main וגם foo מתייחסות לאותם שני משתנים גלובליים. בתחילה main משימה בהם את הערכים 17 ו 3 ואז foo מדפיסה אותם. לאחר מכן, foo משנה את הערכים שלהם ל 18 ו 33 ו main מדפיסה אותם.

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