שפת C/משתנים

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי

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

מהם משתנים?[עריכה]

לעתים קרובות אפשר לראות בקוד C קטעי קוד מהצורה:

int grade = 80;

קטע זה מגדיר את קיומו של משתנה ששמו grade, וטיפוסו שלם (int); המשתנה מאותחל לערך 80. כעת, במקום להשתמש במספר 80, נוכל להשתמש במשתנה grade.

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

טיפוסים[עריכה]

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

הגדרה: בתים

המחשב מארגן מידע בבתים שהם יחידות הזיכרון הבסיסיות של המחשב. בכל מחשב יכול בית להכיל אחת מ-256 אפשרויות.

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

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

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

טיפוסים שלמים[עריכה]

טיפוסים אלו נועדו לאחסן מספרים שלמים.

סווג עיקרי[עריכה]

יש שני סוגי שלמים עיקריים:

  • char - טיפוס הנועד לשמירת תווים או מספרים קטנים
  • short int, int, long int - טיפוסים שנועדו לאחסן מספרים שלמים גדולים יותר, בהתאמה

בתרשים הבא, לדוגמה, אפשר לראות שני משתנים. האחד, grade, בנוי משני בתים, ומכיל את המספר 80. השני, c, מורכב מבית אחד, ומכיל את התו 'a':

דוגמאות למשתנים.
דוגמאות למשתנים.

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

שפת C ידועה בקצרנותה. אפשר לכתוב long כקיצור ל-long int, ואפשר לכתוב short כקיצור ל-short int.

ציון סימן[עריכה]

כל הטיפוסים יכולים להכיל הן מספרים חיוביים והן מספרים שליליים. הטווח הוא סימטרי. כך, לדוגמה, במחשב שבו שלם (int) יכול להכיל 65,536 אפשרויות, הוא יוכל להכיל את כל המספרים השלמים החל מ-‎-32,768 ועד ל-32,767. לעתים יודעים מראש שמשתנה יוכל לקבל מספרים לא-שליליים בלבד. כך, לדוגמה, אם משתנה אמור להכיל משכורת בשקלים, אין טעם לחשוב על אפשרויות שליליות. השפה מאפשרת לקבוע האם טיפוס שלם יכול לקבל ערכים שליליים או לא. אם נציין שטיפוס לא יכול לקבל מספרים שליליים, נוכל לשמור בו מספרים יותר גדולים. כך, לדוגמה, נוכל לשמור במשתנה מספרים עד 65,535.

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

signed int temperature;

מצהיר על משתנה שלם temperature שיכול לקבל גם ערכים שליליים (שלמים כמובן). מצד שני, לדוגמה:

unsigned int salary;

מצהיר על משתנה שלם salary שיכול לקבל ערכים לא-שליליים בלבד (שלמים כמובן).

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

int temperature;
int salary;

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

טיפוסי נקודה צפה[עריכה]

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

  • float – טיפוס רציונאלי בעל יכולת דיוק בינונית
  • double ו-long double – טיפוסים בעלי יכולת דיוק גבוהה וגבוהה במיוחד.

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


עכשיו תורכם:

דרגת קושי:

☆ ☆ ★ ★

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

  • double
  • int
  • char
  • unsigned long
  • unsigned int


כנראה unsigned long או unsigned int. להלן השיקולים:

  • הטיפוס double כנראה אינו מתאים, מפני שמספר אנשים הינו שלם.
  • הטיפוס int עשוי להתאים, אך מספר אנשים לרוב אינו שלילי, ואילו טיפוס זה מאפשר גם ערכים שליליים.
  • הטיפוס char מכיל ערכים שליליים (שפחות רלוונטיים למקרה זה), וערכים חיוביים קטנים יחסית, ולכן הטיפוס עשוי לא להתאים.
  • הטיפוס unsigned long וunsigned int שניהם מכילים ערכים שלמים ולא שליליים בלבד, ולכן כל אחד מהם מתאים. הראשון מאפשרת ערכים גדולים יותר, אך תופסת יותר מקום, יחסית לשני.


קבועים[עריכה]

כל מספר קבוע שמופיע בקוד שפת C הוא בעל טיפוס כלשהו. לדוגמה, אם מופיע בקוד 80, אז זהו קבוע מטיפוס שלם (int).

הצהרה על משתנים[עריכה]

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

צורת ההצהרה[עריכה]

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

<type> <name> = 0;

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

int x;

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

int x, y;
float grade1, grade2, grade_average;

שמות משתנים[עריכה]

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

  • אין להשתמש במילים שמורות (מילות מפתח המוגדרות בשפה) עבור שם של משתנה. כך, לדוגמה, אי אפשר לתת למשתנה את השם int. תוכל לראות את רשימת המילים השמורות בנספח מילים שמורות.
  • שם המשתנה יכול להכיל רק אותיות אנגליות (גדולות וקטנות), מספרים, וקו תחתון. שם המשתנה חייב להתחיל באות או קו תחתון. כך, לדוגמה, option2 הוא שם חוקי למשתנה, אך 2option איננו.
  • לכל משתנה חייב להיות שם ייחודי (נרחיב על כך בטווח ההכרה של משתנים). השפה מבדילה בין אותיות אנגליות גדולות לקטנות, ולכן השם Foo נבדל מ-foo.

השורות הבאות הן טעות:

/* char is not a valid name for a variable! */
int char;

int x;
/* This variable was already declared. */
int x;

/* A variable name cannot start with 3. */
int 3db;

עבודה עם משתנים[עריכה]

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

השמה[עריכה]

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

int a, b;

a = 3;

b = 7 + 8;

הדבר ידוע בשם השמה.

אתחול[עריכה]

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

int a = 3, b = 7 + 8;

הדבר ידוע בשם אתחול.

פעולות חשבוניות[עריכה]

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

int main()
{
  int a = 7, b = 3, c;
  
  c = a + b;  /* now c is 10   */
  a = 5;    /* c is still 10 */
  c = a - 1;  /* now c is 4    */
  
  return 0;
}

נעסוק בכך בפעולות חשבוניות.


עכשיו תורכם:

דרגת קושי:

☆ ☆ ★ ★

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

  • 78
  • 84
  • 45
  • 97
  • 64

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

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


הפתרון
int main()
{
  /* The given data */
 unsigned int grade1 = 78, grade2 = 84, grade3 = 45, grade4 = 97, grade5=64; 
  float average;
  
  average = (grade1 + grade2 + grade3 + grade4 + grade5) / 5; /* finding the average */

  printf("Your Average Is:%f\n",average);
  return 0;
}


קלט ופלט[עריכה]

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

אורך החיים וטווח ההכרה של משתנים[עריכה]

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

רצפי פקודות[עריכה]

נתבונן בשורות הבאות:

int a, b;

a = 3;  

b = 7 + 8;

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

int a = 3;

int b = a + 8;

בלוקים[עריכה]

בלוק הוא רצף פקודות בתוך סוגריים מסולסלים. להלן בלוק בעל שתי פקודות:

{
  a = 3;  

  b = 7 + 8;  
}

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

{

}

{
  a = 3;  

  b = 7 + 8;  
}

בלוק יכול אפילו להכיל בלוק אחר:

{
  e = -3;

  {  
    a = 3;    

    b = 7 + 8;    
  }  
  
  d = 16;
}

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

משתנים לוקליים[עריכה]

משתנה לוקלי הוא משתנה המוגדר בתוך בלוק כלשהו. כאן, לדוגמה, a הוא משתנה לוקלי:

{
  int a;

  a = 3;
}

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

{
  int n = 2;
}


{
  int x;
  
  x = n + 5; /* ERROR! variable "n" is not recognized here! */
}

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

{
  int n = 2;
}

{
  int n = 3;
  int x = n + 5; /* OK! */
}

בדוגמה זו כל אחד מהבלוקים מכיל משתנה בשם n. כיוון שהם לוקליים, אין ביניהם כל קשר ולכן אין עם זה בעיה. במקרה זה, אגב, המשתנה x בבלוק השני יקבל ערך 8.


שימו לב:

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

משתנים גלובליים[עריכה]

ניתן להגדיר משתנים גם מחוץ לכל בלוק שהוא. משתנים כאלו יהיו גלובליים, ויוכרו ע"י כל קטע קוד המכיר את השם שלהם. בקוד הבא, לדוגמה, x הוא משתנה גלובלי:

char x;

int main()
{
  int n;
  
  n = 3;
  
  x = 'f';
  
  return 0;	
}

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

על מנת להגדיר משתנה גלובלי שניתן יהיה להכיר אותו אך ורק בקובץ הנוכחי, יש להשתמש במילה השמורה static.

כדאי לדעת:

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

משתנים קבועים[עריכה]

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

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



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

const <type> <name>

המהדר יאכוף זאת. לדוגמה, בקטע הקוד הבא:

const int a = 8;

/* Error: can't change the value of a const variable! */
a = 8;

המהדר יתלונן על הניסיון לשים ערך חדש ב-a.

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

int average = 0.1 * midterm_grade + 0.9 * final_grade;

if(average < 60)
...
else if(average = 60) /* This line is suspicious! */
...

אפשר לראות שאחת השורות שגוייה. השורה:

  else if(average = 60)

משימה את הערך 60 למשתנה average, ולא בודקת שוויון; מדובר בשגיאת תכנות. לו היינו מגדירים את המשתנה כקבוע, עם זאת, כך:

const int average = 0.1 * midterm_grade + 0.9 * final_grade;

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

שמות נרדפים לטיפוסים בעזרת typedef[עריכה]

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

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



שפת C מאפשרת להגדיר "שם נרדף" לטיפוסים על ידי המילה typedef. עושים זאת בצורה הבאה:

typedef <known_type> <alias>;

כאשר known_type הוא טיפוס משתנה ידוע, ו-alias הוא "שם נרדף" לו.

לדוגמה, אפשר לתת "שם נרדף" לשלם, ולהשתמש בו להצהרה על משתנים:

typedef int my_new_name_for_int;

my_new_name_for_int x = 3;

הסבה[עריכה]

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

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



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

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

ביצוע ההסבה[עריכה]

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

float x = (float) 1;

ניתן לבצע זאת גם עם משתנים:

int i = 121;
char *x = (char *) i;

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

הסבה אוטומטית וסכנת אובדן המידע[עריכה]

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

int a = 1;
float b = a;

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

#include <stdio.h>

int main() {
	double a = 7.654321;
	int b;
	printf("Before: %lf\n", a);
	b = (int) a;
	a = (double) b;
	printf("After: %lf\n", a);
	return 0;
}

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

קריאה לפונקציה עם הסבה[עריכה]

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

#include <stdio.h>
#include <math.h>

int main() {
	int a = 16;
	double b;
	b = sqrt((double) a);
	printf("Square root of %d: %d\n", a, (int) b);
	return 0;
}

הסבה לא חוקית[עריכה]

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

struct my_struct {
	int x;
};

int main() {
	int a = 16;
	struct my_struct b;
	b = (struct my_struct) a;	// Illegal
	a = (int) b;			// Illegal
	return 0;
}

משתנים סטטיים[עריכה]

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

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




פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.



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

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


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