אופרטורים
לאחר שהגדרנו מהם טיפוסים והגדרנו בעזרתם משתנים, נרצה להתחיל לבצע עליהם פעולות וחישובים. נגדיר את הפעולות הבאות:
פעולות אריתמטיות (+, -, *, /)
אופרטורים אלו מייצגים את הפעולות הקלאסיות של חיבור, חיסור, כפל וחילוק:
#include <stdio.h>
int main() {
int x = 5;
int y = 2;
float f = 3.5;
int add = x + y;
int sub = x - y;
int int_div = x / y;
float float_div = (float)x / y;
printf("add: %d\n", add);
printf("sub: %d\n", sub);
printf("int division: %d\n", int_div);
printf("float division: %.2f\n", float_div);
}
נשים לב להבדלים בין חלוקת שלמים לחלוקת מספרים עשרוניים.
השמה (=)
פעולה מהצורה x = y - זו פעולה שכמובן פגשנו כבר, אבל נגדירה כעת בצורה מסודרת. נשים לב שבניגוד למשוואות מתמטיות בהן = מייצג שוויון סימטרי בין שני צדדים, הפעולה אינה סימטרית - היא מחשבת את הביטוי שמופיע באגף ימין וכותבת אותו כערך למשתנה שמופיע מצד שמאל. צד שמאל חייב להיות משתנה בעוד שצד ימין יכול להיות כל ביטוי עם ערך מתאים. למשל הביטויים הבאים הם חוקיים:
int x = 6;
x = x + 2;
int y = x;
int z = x + y - 2;
חשוב להדגיש כי סדר הפעולות היא חישוב הביטוי אותו משימים ורק לאחר מכן כתיבת הערך למשתנה. בתחילת שורה 2 הערך של x הוא 6 - מחשבים את ערך הביטוי x + 2 שתוצאתו 8, ורק לאחר מכן כותבים אותו למשתנה x.
לעומת זאת, הביטויים הבאים אינם חוקיים:
int x = y; // y not declared yet
5 = x; // Can't assign to a constant
כדוגמא נוספת, נעקוב אחרי ערכי משתנים בדוגמא הבאה:
double x;
double y = 2.1;
x = y - 2.2;
y = x + 0.2;
| Line | x | y |
|---|---|---|
double x | garbage | not declared yet |
double y = 2.1 | garbage | 2.1 |
x = y - 2.2 | -0.1 | 2.1 |
y = x + 0.2 | -0.1 | 0.1 |
מתברר כי ביטויים בהם משתמשים בערך הנוכחי של המשתנה כחלק מחישוב הם נפוצים, ולכן השפה מציעה לנו קיצורים מהסוג הבא:
int x = 2;
x += 3; // x = x + 3
x *= -4; // x = x * (-4)
x /= 7; // x = x / 7
כמו כן, מסתבר שפעולות כמו x += 1 הן אפילו יותר נפוצות ולכן השפה מציעה גם עבורם את הקיצורים הבאים. יש כמה נקודות עדינות בשימוש באופרטורים האלו, לכן קודם נגדיר אותם ואז נראה דוגמא:
x++- אופרטור++משמאל למשתנה מגדיל את המשתנה ב-1 לפני חישוב שאר הביטוי.++x- אופרטור++מימין למשתנה קודם כל מחשב את הביטוי ולאחר מכן מגדיל את המשתנה ב-1.x--- זהה ל-x++, רק שמקטין את המשתנה ב-1 במקום להגדיל.--x- זהה ל-++x, רק שמקטין את המשתנה ב-1 במקום להגדיל.
מה הכוונה בכל סדר הפעולות שדיברנו עליו למעלה? נבין ע“י דוגמא:
int x = 5; // x is 5
int y = ++x; // x is 6, y is 6
int z = y--; // x is 6, y is 5, z is 6
נפרק את השורות לפי הסדר:
- הצהרה על משתנה ומתן ערך
- האופרטור משמאל למשתנה ולכן אנחנו קודם כל מגדילים אותו ואז מחשבים את הביטוי. אז הגדלנו את
xל-6, ואז כתבנו את התוצאה ל-yכך שערכו גם 6. - האופרטור מימין למשתנה ולכן קודם כל חישבנו את הביטוי - כתבנו ל-
zאת הערך 6 ורק לאחר מכן הקטנו אתyב-1, כך שערכו בסוף השורה 5.xללא שינוי.
מודולו %
בניגוד למה שראינו עד כה, זהו אופרטור קצת פחות מוכר ואינטואיבי אבל יש לו שימושים רבים בתכנות. אופרטור המודולו (לפעמים גם מודולוס, modulus) מוציא את השארית של חלוקת שני שלמים. לדוגמא:
int a = 17, b = 5;
int result = a % b; // result = 2
כאשר a / b ללא שארית, המודולו a % b הוא 0. אחרת, הוא מחזיר את שארית החלוקה בין שני המספרים.
דוגמא - קבלת הספרה האחרונה ממספר ע“י מודולו
בהנתן מספר int, אפשר להשתמש במודולו בשביל לקבל את הספרה האחרונה שלו:
int x = 65154;
int last_digit = x % 10; // 4
פעולות שוויון והשוואה
שוויון == ואי שוויון =!
אינטואיטיבי מאוד - בודק האם שני ערכים כלשהם שווים או שונים. ניתן להשוות בין מספרים שלמים, עשרוניים, תווים ועוד. אפשר לכתוב את התוצאה של אופרטור ההשוואה למשתנה, או להשתמש בה כתנאי במשפטים מורכבים יותר שנפגוש בהמשך:
int a = 17, b = 5, c = 17;
bool a_b_equal = a == b; // False!
bool a_c_equal = a == c;
if (a_c_equal) { // True!
// Do some stuff
}
char char1 = 'a', char2 = 'a';
if (char1 != char2) { // Use directly in comparison
// Don't do stuff
}
Warning
הזהרו - באג קלאסי הוא בלבול בין אופרטור ההשוואה
==לאופרטור ההשמה=. בדקו תמיד האם השתמשתם באופרטור שהתכוונתם אליו.
אופרטור גדול מ/קטן מ
בדומה לאופרטור השוויון, קיימים גם גרסאות סטנדרטיות לבדיקת אי שוויונות:
<ו->לבדיקת קטן/גדול<=ו-=>לבדיקת קטן שווה/גדול שווה
סוגריים ()
ולבסוף, כמו במתמטיקה רגילה, ניתן להשתמש בסוגריים () בשביל להגדיר ביטוי עם קדימות גבוהה יותר מכל ביטוי אחר. לדוגמא:
int a = 5, b = 3;
int result1 = a + (b * 2);
int x = 10, y = 4, z = 2;
int result2 = (x - y) / z;
int p = 7, q = 2;
int result3 = (p++) * (--q);
סדר פעולות
כמו בכל ביטוי מתמטי, צריך להגדיר בצורה מסודרת מהו סדר הפעולות של אופרטורים על מנת שביטויים יתנהגו בצורה נגדיר צפויה. נגדיר את הקדימות של האופרטורים שהצגנו עד כה, מגבוה לנמוך:
- אופרטורי קידום ונסיגה:
++,-- - כפל, חילוק ומודולו:
*,/,% - חיבור וחיסור:
+,- - שוויון:
==
שאלות ותרגילים:
- עבור כל אחת מהתוכניות הבאות, מה יהיה הערך של המשתנה
resultבסופן?
// Program 1
int a = 5, b = 3;
int result1 = a + b * 2;
// Program 2
int x = 10, y = 4, z = 2;
int result2 = (x - y) / z;
// Program 3
int p = 7, q = 2;
int result3 = p++ * --q;
// Program 4
int m = 8, n = 3, k = 2;
int result4 = m / n * k + m % n;
// Program 5
int aa = 3, bb = 4, cc = 5;
int result5 = (aa++ + --bb) * (cc-- - ++aa);
אופרטורים לוגים (&&, || ו-!)
נסכם את נושא האופרטורים עם אופרטורים לוגיים. יתכן ושמעתם כבר על חלק מהקונספטים שנציג בחלק הזה.
אופרטורים לוגיים הם דרך לייצר משתנים בוליאנים כקומבינציה של משתנים אחרים. לדוגמא, נניח שאנחנו רוצים להכין עוגה ורוצים לבדוק האם יש מספיק קמח ומספיק ביצים, כאשר קיים מספר קבוע כלשהו של ביצים וקמח שצריך בשביל להכין את העוגה. לא משנה מהו מספר הביצים וכמות הקמח שיש - קיימות ארבע אופציות:
- אין מספיק קמח ואין מספיק ביצים - אי אפשר להכין עוגה.
- אין מספיק קמח ויש מספיק ביצים - אי אפשר להכין עוגה.
- יש מספיק קמח ואין מספיק ביצים - אי אפשר להכין עוגה.
- יש מספיק קמח ויש מספיק ביצים - אפשר להכין עוגה!
כלומר, ניתן להכין עוגה רק אם יש מספיק ביצים וגם יש מספיק קמח. ניתן לחשוב על זה בקוד כך:
#define EGGS_NEEDED 2
#define FLOUR_NEEDED 5
#include <stdio.h>
#include <stdbool.h>
int main() {
int eggs = -1;
int flour = -1;
printf("Please enter amount of eggs and flour\n");
scanf("%d", &eggs);
scanf("%d", &flour);
bool have_enough_eggs = eggs >= EGGS_NEEDED;
bool have_enough_flour = flour >= FLOUR_NEEDED;
}
הגדרנו את שני התנאים שצריכים להתקיים בשביל שיהיה אפשר להכין עוגה. מהי קומבינציה של שני התנאים שמאפשרת להכין עוגה? אפשר לחשוב על משתנה בשם can_make_cake שהוא קומבינציה לוגית של שני התנאים הקודמים. נשים בטבלה את כל האופציות עבור שני התנאים ונרכיב מהם את המשתנה החדש:
| have_enough_eggs | have_enough_flour | can_make_cake |
|---|---|---|
| false | false | false |
| true | false | false |
| false | true | false |
| true | true | true |
למעשה ניתן להכין עוגה רק כאשר שני התנאים הם true. ליחס מהסוג הזה קוראים יחס and - התנאי להכנת עוגה הוא true רק אם גם התנאי הראשון וגם התנאי הראשון מתקיימים. אלטרנטיבית, ניתן לחשוב על כך שמספיק שתנאי אחד לא מתקיים והשני גם לא.
הערה: לטבלה מהסוג הזה קוראים טבלת אמת והיא שימושית בתחום חשוב מאוד הנקרא אלגברה בוליאנית, עליו תרחיבו בקורסי המשך.
הדרך להגדיר משתנה בוליאני שהוא and של שני משתנים אחרים נראת כך:
bool can_make_cake = have_enough_flours && have_enough_eggs;
בצורה דומה, ניתן לחשוב על עוגה שצריך או קמח או ביצים בשביל להכין אותה. מספיק שאחד המשתנים הוא אמת - הדרך להגדיר זאת נראת כך:
bool can_make_cake = have_enough_flours || have_enough_eggs;
אופרטור ה-|| נקרא אופרטור or והוא מייצג מצב בו מספיק שאחד מהתנאים יתקיים בשביל שצירופם יתקיים גם.
אופציה נוספת היא שלילה - נניח שהתוכנית שלנו נראת דווקא כך:
#define EGGS_NEEDED 2
#define FLOUR_NEEDED 5
#include <stdio.h>
#include <stdbool.h>
int main() {
int eggs = -1;
int flour = -1;
printf("Please enter amount of eggs and flour\n");
scanf("%d", &eggs);
scanf("%d", &flour);
bool not_enough_eggs = eggs < EGGS_NEEDED;
bool not_enough_flour = flour < FLOUR_NEEDED;
}
הגדרנו את התנאים שלנו הפוך. במידה ונרצה לקבל את התנאי הדואלי, ניתן להשתמש באופרטור השלילה ! כך:
bool have_enough_flour = !not_enough_flour;
אופרטור השלילה פשוט מאוד - הוא בוחן את הערך שהוא קיבל. במידה והוא קיבל ערך שאינו 0, הוא הופך אותו ל-0 - ובמידה וקיבל 0, הוא הופך אותו ל-1.
ניתן גם לשרשר אופרטורים לוגיים זה לזה - למשל כמו כאן:
#define EGGS_NEEDED 2
#define FLOUR_NEEDED 5
#include <stdio.h>
#include <stdbool.h>
int main() {
int eggs = -1;
int flour = -1;
printf("Please enter amount of eggs and flour\n");
scanf("%d", &eggs);
scanf("%d", &flour);
bool not_enough_eggs = eggs < EGGS_NEEDED;
bool not_enough_flour = flour < FLOUR_NEEDED;
bool can_make_cake = !not_enough_eggs && !not_enough_flour;
}
נסכם את טבלאות האמת של האופרטורים השונים. נסמן ב-b1 ו-b2 שני משתנים בוליאנים כלשהם ונקבל:
| b1 | b2 | b1 or b2 | b1 and b2 | !b1 | !b2 |
|---|---|---|---|---|---|
| false | false | false | false | true | true |
| true | false | true | false | false | true |
| false | true | true | false | true | false |
| true | true | true | true | false | false |
Caution
בשפה קיימים גם האופרטורים
&ו-|, הנבדלים מהאופרטורים&&ו-|. האופטורים נקראים bitwise operators והם מבצעים פעולה אחרת מהאופרטורים הלוגיים - לא נרחיב עליהם בקורס זה.
הערה: יש להזהר - קיימים בשפה גם אופרטורים & ו-|, השונים בהגדרתם מ-&& ו-|| ונקראים bitwise operators. נרחיב עליהם בהמשך.
חישוב עצל (Lazy Evaluation)
כדוגמא, נניח שאנו בונים מערכת כניסה לאתר אינטרנט. על מנת להכנס לאתר, המשתמש צריך לעמוד בתנאים הבאים:
- להכניס סיסמא נכונה.
- להיות מעל לגיל 10 או להיות בעל אישור הורים להכנס לאתר.
ניתן לייצג בקוד את התנאים כך:
#define PASSWORD 1234
#define MIN_AGE 10
int password;
int age;
bool has_parents_permit;
bool can_enter = password == PASSWORD && (age > MIN_AGE || has_parents_permit);
נבנה טבלה של המקרים האפשריים:
| password | age | permit | can enter |
|---|---|---|---|
false | false | false | false |
false | false | true | false |
false | true | false | false |
false | true | true | false |
true | false | false | false |
true | false | true | true |
true | true | false | true |
true | true | true | true |
נשים לב כשכש-password הוא false, אין שום משמעות למשתנים age ו-parents - התשובה היא תמיד false. הסיבה לכך שעבור כל משתנה בוליאני x, הביטוי x && false הוא תמיד false.
השפה תמיד שואפת להמנע מחישובים ופעולות מיותרות, ולכן משתמשת במקרה זה בקונספט הנקרא חישוב עצל - כאשר מבצעים חישוב של ביטוי לוגי, מיד כאשר ניתן לקבוע את ערכו הסופי, השפה עוצרת את החישוב וקובעת אותו. נושא זה יהיה חשוב בהמשך כאשר ביטויים לוגיים יורכבו לא רק מביטויים בוליאנים אלא מקריאות לפונקציות עם השפעות מעבר לביטוי.
בצורה דומה, מכיוון שלכל משתנה בוליאני הביטוי x x || true הוא תמיד true, ניתן לפעמים להמנע מחישובים על ידי חישוב עצל. למשל, נניח בדוגמא הקודמת שאין סיסמא לאתר - הקוד יהפוך ל-:
#define MIN_AGE 10
int age;
bool has_parents_permit;
bool can_enter = age > MIN_AGE || has_parents_permit;
אם מתקיים age > MIN_AGE, אין צורך לבדוק בכלל את המשתנה has_parents_permit.
Note
קיימים מקומות בהם משתמשים במונח short-circuit evaluation במקום ב-lazy evaluation.