C (שפת תכנות)

שפת C היא שפת תכנות הכוללת מנגנוני בקרת זרימה ומבני נתונים פשוטים, ומאפשרת ניצול מרבי של יכולות המחשב, בדומה לשפת סף. שפת C היא אחת השפות היעילות והמהירות בתעשייה, ומשמשת כיום בעיקר לכתיבת מערכות בהן זמן הביצוע הוא גורם קריטי, או כאלה שדורשות אינטראקציה עם מערכות חומרה. רוב מערכות ההפעלה הנפוצות כתובות בעיקר בשפת C, וכן רוב הדפדפנים הנפוצים. שפת C היא אחת משפות התכנות הנפוצות בעולם. קיימים מהדרים לשפת C עבור כמעט כל סוגי המחשבים ומערכות ההפעלה. שפת C השפיעה על שפות תכנות רבות, ובמיוחד על C++‎, שנכתבה בתחילה כקדם-מעבד עבור C, וכיום היא כמעט מכילה את שפת C. רוב שפות התכנות הנפוצות כיום כוללות מאפיינים תחביריים המזכירים את שפת C. שפת C תוכננה ופותחה לראשונה בין שנת 1969 לראשית שנות ה-70 (עד 1973). השפה נוצרה על ידי דניס ריצ'י, במעבדות חברת בל על מערכות ההפעלה UNIX שרצו על מחשבי PDP-11. מהר מאוד היא התפשטה ויצאה לאור ויצאו מספר גרסאות שונות לשפה אשר נבדלו בהבדלים קטנים זו מזו. הצורך בשפה עלה לשם כתיבת מערכת הפעלה. באותה עת הייתה קיימת שפת B, ובאמצעותה נכתבה שפת C בתחילה. מקור השם של השפה הוא באותה שפת B, תוך התקדמות אות אחת באלפבית, או האות הבאה אחרי B בשמה של BCPL שפת התכנות שקדמה לשפת B. שפת C היא שפה בעלת תקן פתוח המנוהל על ידי ANSI ו-ISO. העדכון האחרון לתקן, שיצא בשנת 2018 ומכונה "C18" או "C17", מכיל רק הבהרות ותיקונים טכניים לתקן הקודם משנת 2011 המכונה "C11". התקן הראשון לשפה הונהג על ידי ANSI ב-1989 (נהוג להתייחס אליו כ-"ANSI C" או "C89") ואומץ על ידי ISO ב-1990 (בשינויים מינוריים, ולתקן הזה מתייחסים כ-"C90"). תקן נוסף לשפה הוגדר בשנת 1999, ונקרא "C99". מפתח השפה דניס ריצ'י כתב יחד עם בריאן קרניגהאן את הספר "The C Programming Language", המתעד את השפה ומהווה תקן נוסף, ראשון ולא רשמי, לשפה. מתייחסים לתקן הזה לפעמים כ-"K&R" (ראשי התיבות של שמות המשפחה של מחבריו). מהדורה שנייה של הספר מתייחסת לתקן C89. לדוגמה, לפי תקן ANSI, שפת C חייבת לתמוך בתכונות הבאות: עד 31 פרמטרים לפונקציה. עד 509 תווים בשורת קוד. עד 32 רמות קינון קוד. הערך המקסימלי למשתנה מסוג long int, לא יעלה על 2,147,483,647. שפת C היא שפה פרוצדורלית, בעלת טיפוסיות סטטית, חלשה, ולא בטוחה. עיקרון מנחה בשפה הוא עקרון היעילות, ובכך היא מצטיינת. כל פעולה בשפה מתבטאת בפעולה בודדת או במספר מועט של פעולות בשפת סף. משום כך אין בה סוגי מבנים מורכבים כגון מחרוזות או בדיקות סמויות בגישה למערכים ומערכת הטיפוסים שלה לא בטוחה. מסיבה זו היא גם כוללת לא מעט אפשרויות לשגיאות שהמהדר (Compiler) לא יאתר, כגון פנייה לאיבר שלא קיים במערך או פנייה לערך לא חוקי בזיכרון וכדומה. הנחת העבודה של מתכנני השפה (והתקנים המאוחרים יותר) היא כי "המתכנת מבין מה הוא עושה". סביבות עבודה לשפת C כיום כוללות מהדר, כלי לניפוי שגיאות (מציאת תקלות, באגים), וכן אפשרות לערוך את הקוד בחבילת תוכנה אחת. שפת C היא שפה ה"מחזיקה את עצמה" (self hosting). כלומר, קיימים מהדרים לשפת C הכתובים אף הם בשפת C (לכך ניתן להגיע בתהליך הנקרא Bootstrapping). בשפת C ישנם מצבים רבים שאינם מוגדרים בשפה, לרוב באופן מפורש. בין אלה ניתן למנות: ניסיון לשנות תוכן של ביטוי-מחרוזת, תוצאה של חלוקה באפס, הצבעה אל אזור מחוץ למערך (הרחוק יותר מאיבר אחד אחרי האיבר האחרון), שימוש במשתנה שאיננו מאותחל, ושינוי כפול של משתנה בביטוי בודד (למשל x=++x). במקרים אלה, המהדר רשאי לפעול כרצונו - להדר את התוכנית כולה בכל דרך שנראית לו, אף אם אין לה כל קשר לכוונה של המתכנת שכתב את הקוד, ואף אין דרישה לעקביות בהידור. כל זאת, במטרה לאפשר לכתוב למהדרים לקמפל תוכניות מוגדרות היטב בצורה יעילה, בכל סוגי המערכות. ישנם מקרים המוגדרים לפי-מימוש. במקרים אלה, המהדר רשאי לפעול כאוות נפשו, אך יש לתעד את דרך הפעולה שנבחרה. מקרים אחרים מוגדרים בשפה, אך באופן חלש: סדר החישוב של ביטויים בקריאה לפונקציה איננו מוגדר, אך כל הביטויים צריכים להיות מחושבים לפני ביצוע הקריאה עצמה. משתנה בשפת C הוא שם המוענק לאזור זיכרון מסוים. בשפת C, לכל משתנה ישנו טיפוס. למשל אם מוגדר משתנה מטיפוס מספר שלם בשם "x", אז "x" יהיה כינוי לאזור בזיכרון המכיל מספר שאת ערכו תוכל התוכנית לשנות או לקרוא. לדוגמה: int x; // משתנה זה יאותחל רק אם הוא גלובלי .int מטיפוס x הגדרה של משתנה בשם int y = 5; // משתנה זה יאותחל לערך 5 .int מטיפוס y הגדרה של משתנה בשם extern int z; // המוגדר בחלק אחר בתוכנית .int מטיפוס z הצהרה על קיומו של משתנה גלובלי בשם למשתנים בשפת C ישנו ערך הניתן לשינוי. ישנן ארבע קטגוריות של משתנים: משתנים מקומיים (לוקליים). משתנים אלה שייכים לפונקציה, ומוגדרים אך ורק בבלוק הקוד הנוכחי - בין סוגריים מסולסלים. ברגע שהתוכנית יוצאת מהבלוק שבו הוגדרו (למשל בין קריאות שונות לפונקציה), הם אינם קיימים יותר. לא ניתן, למשל, להתייחס מפונקציה אחת למשתנה שהוגדר בפונקציה אחרת. משתנים מסוג זה אינם מאותחלים על ידי המהדר, אלא במפורש על ידי המתכנת, או כלל לא. אם לא אותחל, הערך התחלתי של המשתנה הוא נתון שרירותי כלשהו (ערך התחלתי כזה מכונה לעיתים זבל. הוא אינו מוגדר, אך לרוב יכיל את מה שהכיל האזור הזה בזיכרון לפני היווצרות המשתנה). משתנים מקומיים סטטיים. אל משתנים אלה ניתן לגשת רק מתוך הפונקציה והבלוק שבהם הם הוגדרו, אך אורך החיים שלהם הוא כאורך חיי התוכנית - הם שומרים על ערכם בין קריאות שונות לפונקציה, למשל. משתנים גלובליים. אלה מוגדרים בכל הקבצים שבהם קיימת הצהרה עליהם. ניתן להגדיר משתנה גלובלי רק בקובץ בודד; על מנת לגשת אליו מקובץ נפרד יש להצהיר על קיומו. משתנים גלובליים סטטיים. אלה מוגדרים אך ורק בתוך קובץ יחיד, ולא ניתן להתייחס אליהם מקבצים אחרים. טיפוסי הנתונים הבסיסיים בשפה הם מספרים שלמים (הנקראים char, short, int, long, long long לפי גודלם) וכן טיפוסים בעלי ייצוג נקודה צפה עבור מספרים ממשיים (float, double, long double). מצביע הוא משתנה המכיל כתובת בזיכרון המחשב - כתובת של משתנה או של פונקציה. משמש בין היתר ליצירה של מבני נתונים דינמיים. לדוגמה, מצביע למשתנה מסוג short, שהוא משתנה המייצג מספר שלם: short *x; בתקן ANSI לא קיים טיפוס נתונים בוליאני (המכיל ערכי אמת\שקר). אך כל משתנה שלם עם ערך 0 נחשב כשקר, וכל ערך אחר היה אמת. בתקן C99 נוסף טיפוס בוליאני מיוחד הקרוי _Bool. במקרה ואין בעיות תאימות לקוד ישן, ניתן להשתמש בספרייה <stdbool.h> על מנת להשתמש בשם המקובל bool. מערך בשפת C הוא אוסף של משתנים מאותו טיפוס המסודרים ברצף בזיכרון. ניתן לרוב להתייחס למערך כאילו הוא מצביע קבוע אל האיבר הראשון שלו. כך פונקציה הצריכה מערך כפרמטר, תבקש מצביע; אם יועבר אליה שם של מערך, הוא "ידעך למצביע" וכך יועבר כפרמטר. מספרו הסידורי (האינדקס) של התא הראשון במערך בשפת C הוא 0, ולכן האיבר האחרון במערך a בן חמישה איברים יהיה [a[4‏ (גישה לאיבר מתבצעת על ידי רישום מספרו בתוך סוגריים מרובעים. [a[4‏ שקול בשפה ל-(a + 4)*, כאשר a יכול להיות מצביע או מערך). דוגמה להגדרת מערך של מספרים מסוג נקודה צפה (float): float n[5]; לחלופין, ניתן להגדיר מערך על ידי אתחול ערכיו. נגדיר מערך של מספרים מסוג double, המייצג מספר נקודה צפה עם מספר כפול של ביטים: double n[] = {3.14, 2.71, 0.8, 5, 32}; בשפת C עצמה אין הגדרה של מחרוזת. השפה מאפשרת לכתוב ביטויים בצורה "some string", אך אלה מתורגמים למערך של char, המסתיים בערך 0 (אין הכוונה לתו המייצג את סימן האפס אלא לבית ריק - Null byte, שערכו בטבלת ASCII הוא 0). שלוש השורות הבאות שקולות לחלוטין מבחינת השפה: char name[] = "John"; char name[] = {'J', 'o', 'h', 'n', '\0'}; char name[] = {'J', 'o', 'h', 'n', 0}; מבחינת הספרייה הסטנדרטית של השפה, מחרוזת היא כל מערך של char, המסתיים בערך אפס (שניתן לכתוב אותו ישירות בקוד כ-'0\'). בספרייה זאת מוגדרות פונקציות המטפלות במחרוזות - מוצאות את אורכן, מוצאות תו כלשהו או תת-מחרוזת בתוכן, וכן פעולות נוספות. רשומה (מכונה גם מבנה) היא טיפוס המגדיר N-יה סדורה של משתנים, שניתן להגדירם בבת אחת, כאשר כל אחד מהמשתנים הוא עצמאי. בניגוד למערך, לא כל המשתנים צריכים להיות מאותו הסוג. רשומה מאפשרת לאחד מספר משתנים העוסקים באותו פריט מידע לתוך משתנה יחיד. דוגמה לרשומה (או מבנה) המתארת שם וגיל של אדם, המכילה שני שדות: struct person { char *name; int age; }; struct person yossi; השפה מאפשרת לתת שמות חדשים לטיפוסים באמצעות המילה typedef. למשל: typedef struct person tPerson; tPerson moshe; מגדיר משתנה בשם moshe מהסוג struct person. פקודות בשפת C מתבצעות אחת אחרי השנייה, לפי הסדר, אלא אם כן מדובר במבנה בקרת זרימה כלשהו, המשנה את זרימת התוכנית. מבנה הבקרה הפשוט והנפוץ ביותר הוא פקודת if. במבנה זה נבדק ערכו של תנאי מסוים, ומתבצע סט של פקודות אם התנאי נכון (ערך האמת שלו חיובי): if (3 < 5) printf("three is lower than five"); אם מתכוונים לבצע יותר מפקודה אחת כאשר התנאי מתקיים, יש להשתמש בסוגריים מסולסלים: if (number_of_apples < 10) { printf("I am hungry!"); printf("Give me more apples!"); } לפקודת תנאי ישנה גרסה מורחבת, המאפשרת להגדיר רצף של פקודות שיתבצע במקרה שהתנאי איננו מתקיים. לשם כך משתמשים במילה השמורה else ("אחרת", באנגלית): if (number_of_apples < 10) printf("I am hungry"); else printf("I am not hungry!"); צורת כתיבה נוספת לקטע קוד הכולל בלוק if ו-else עם הוראה בודדת היא בעזרת שימוש באופרטור טרנרי (Ternary), כך שבחלק הראשון מוגדר התנאי, בחלק השני מוגדרות הפקודות לביצוע במקרה שהתנאי מתקיים, ובחלק השלישי הפקודות לביצוע במקרה שהתנאי לא מתקיים: number_of_apples < 10 ? printf("I am hungry") : printf("I am not hungry"); השימוש באופרטור טרנרי אינו נפוץ ואינו מומלץ מפאת קושי הבנתו בקריאת הקוד, אך יתרון שהוא מספק הוא קיצור הקוד לשורה בודדת. פקודת תנאי מאפשרת להגדיר קטע קוד שיבוצע פעם אחת, או לא יבוצע כלל. לעיתים קרובות יש צורך להגדיר קטע קוד שיבוצע מספר כלשהו של פעמים - פעם אחת, עשרים, או בכלל לא. לצורך כך משתמשים בלולאות. ב־C קיימים שלושה סוגי לולאות: לולאות while שמבוססות על בדיקת תנאי להמשך הלולאה לפני ביצוע הקוד, לולאות do-while שדומות ללולאות while, אך התנאי נבדק בסוף הלולאה, ולולאות for, שבנוסף לבדיקת התנאי גם משלבות בצורה טבעית משתני עזר שיפקחו על ריצת הלולאה (למשל, ספירת מספר הפעמים שהלולאה חזרה על עצמה). לולאת for: for (unsigned int i = 0; i < 5; ++i) { /* הכנס כאן קוד... */ } לולאת while עם בקרה בכניסה: while (/* הכנס כאן תנאי */) { /* הכנס כאן קוד... */ } לולאת do-while עם בקרה ביציאה: do { /* הכנס כאן קוד... */ } while (/* הכנס כאן תנאי */); שפת C כוללת אפשרות להורות למעבד לקפוץ ישירות לשורה, לה הוגדר שם (label) בעזרת פקודת goto. בקוד להלן, הפקודה המסומנת "2" לא תתבצע כלל: printf("a"); /* 1 */ goto out; printf("b"); /* 2 */ out: printf("c"); /* 3 */ ניתן לקפוץ באמצעות goto אך ורק בתוך הפונקציה הנוכחית. באמצעות פקודת goto ותנאי if ניתן לממש כל מבנה בקרה שהוא. עם זאת, שימוש בפקודת goto איננו נחשב לסגנון תכנות מומלץ, בדרך כלל. יש המחריגים מכלל זה פעולות עבורן אין מבנה בקרה מתאים המובנה בשפה, כגון קפיצה החוצה מלולאה בתוך לולאה (במקרה כזה פקודת break בודדת איננה מספיקה) קפיצה אל קוד המבצע שחרור משאבים מימוש של אוטומט סופי דטרמיניסטי על מנת לעבור למקום מחוץ לפונקציה, יש להשתמש בפונקציה setjmp בתור label ובפונקציה longjmp עבור המעבר. פונקציה היא המבנה הבסיסי של השפה שבו יכולות להתבצע פקודות ומבני בקרה. אוסף של פקודות שיכול (אך לא חייב) לקבל אוסף של ערכים כקלט (ערכים אלה, והמשתנים המחזיקים אותם, נקראים "פרמטרים" או "ארגומנטים"), ולהחזיר ערך יחיד כפלט (ערך זה נקרא "ערך החזרה"). שינוי בערכי משתני הקלט בפונקציה לא ישפיע על ערכיהם בחלקים אחרים בתוכנית, שכן הם מועברים תמיד על פי ערך (by value). דוגמה לפונקציה המקבלת שני מספרים ומחזירה את ערכו של הגדול מביניהם: int max(int a, int b) { if (a > b) return a; else return b; } בהינתן הגדרה של פונקציה כזאת, ניתן להשתמש בה על מנת לקבל את המקסימום מבין שני מספרים בצורה קלה. למשל, הפקודה הבאה: printf("%d", max(5, 3)); תדפיס את המספר 5, כי הוא המספר הגבוה יותר מבין המספרים 3 ו-5. בשונה מפונקציות מתמטיות, פונקציות בשפת C יכולות לבצע פעולות שמשנות את המצב של המחשב. לדוגמה, הפונקציה printf מהדוגמה הקודמת היא פונקציה שמבצעת הדפסה למסך (או לאמצעי פלט אחר). מודול בשפת C הוא יחידה עצמאית של תוכנה, המהודרת אל קובץ עצמאי. קובץ כזה לא ניתן להריץ באופן ישיר, אך ניתן להשתמש בו בקבצים אחרים. קובצי h. שמכילים הגדרות של מודולים מאפשרים למהדר להבין מתי משתמשים בתוכן של מודול חיצוני ומתי משתמשים בתוכן של הקובץ הנוכחי. בכל תוכנה יש מודול אחד ראשי שמכיל את הפונקציה main שבה מתחילה התוכנית. אוסף של פקודות המאוגדות ומהודרות יחד למקום אחד המאפשר לנו להשתמש בהן ללא צורך להדר אותן עבור כל תוכנה מחדש נקרא ספרייה. בשפת C יש צורך לבצע פקודת include# לקובץ שמכיל את ההצהרות של פונקציות הספרייה בשביל להשתמש בה. לדוגמה, הכללת הספרייה string, המכילה פונקציות לטיפול במחרוזות: #include <string.h> לפני תרגום התוכנית לשפת מכונה, תוכנית הנקראת קדם-מעבד (pre-processor) יוצרת קובץ חדש הכולל בנקודה בה מוכרזת ההכללה את תוכן הקובץ המוכלל, ואז מועבר הקובץ להידור לשפת מכונה. שפת C היא שפה קטנה למדי. היא אינה מכילה פקודות הקושרות אותה למערכת הפעלה מסוימת או לתחום תוכנה מסוים. מסיבה זו, פקודות ספציפיות שכאלה (למשל לקלט ופלט, לחישובים מתמטיים, לעבודה עם מחרוזות ולהקצאות זיכרון דינמיות) נמצאות בספריות סטנדרטיות, שאינן חלק מתחביר השפה אך עונות לקונבנציה מוכרת וקבועה ומהוות חלק מהתקן הרשמי שלה. ספריות אלה מסופקות עם המהדר עבור מערכת הפעלה וחומרה מסוימות, והתוכנה מקושרת (linked) עימן על פי צורך והגדרת כותב הקוד. ניתן בשפת C להוסיף הערות להסברת שלבי התוכנית. ההערות אינן משפיעות על ריצת התוכנית עצמה, אך הן מסייעות להבנתה בידי מתכנתים אחרים או בזמן מאוחר יותר. הערות בשפת C נכתבות בין זוג הסימונים /* */. /* Multiline Comment */ בתקן C99 נוספה האפשרות לסמן הערה של שורה אחת על ידי // (שני לוכסנים סמוכים), המגדירה את כל הטקסט שמופיע אחריה בשורה כהערה. מאפיין זה נלקח משפת ++C. // end of line comment תוכנית פשוטה זו מדפיסה את המחרוזת "Hello, World!‎" לפלט הסטנדרטי. #include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; } מונחים בתוכנה Brian Kernighan, Dennis Ritchie, The C Programming Language 1st, Prentice Hall 1978; ISBN 0-13-110163-3. Pre-ANSI C. 2nd, Prentice Hall 1988; ISBN 0-13-110362-8. ANSI C. ISO/IEC 9899. The official C:1999 standard, along with technical corrigenda and a rationale. ISO/IEC 9899:TC2 Peter van der Linden, Expert C Programming. ISO 2011 Standard, draft אתר האינטרנט הרשמי של C אתר האינטרנט הרשמי של C מילים לזכרו של דניס ריצ'י ועל שפת C "שפת התיכנות המשפיעה ביותר, ובהרבה, בהיסטוריה" (באנגלית) The Development of the C Language (אורכב 31.01.2020 בארכיון Wayback Machine) קצת על ההיסטוריה של C - מתוך קורס שניתן באוניברסיטה העברית C, באתר אנציקלופדיה בריטניקה (באנגלית) TIOBE Index for June 2024, Tiobe, ‏יוני 2024 (באנגלית) (ארכיון)