שפות תכנות - C
היא שפת תכנות שפותחה בתחילת שנות השבעים על ידי דניס ריצ'י במעבדות בל בעיקר כדי לשמש כשפת הפיתוח למערכת ההפעלה UNIX
בתקופה שבה רוב תכנות המערכת נעשה בשפת אסמבלי המתאימה לארכיטקטורה של המחשב הרלוונטי - דבר שהיה מאוד מגוון בזמנו.
C היא עדיין אחת השפות הפופולאריות ביותר בעולם והיא נחשבת כנקודת הייחוס למהירות של שפות תכנות.
C גדלה להיות השפה המשפיעה ביותר על שפות התכנות המודרניות, ואפשר לראות את ההשפעה שלה, בין היתר, בשפות כמו Zig, Go, Python, PHP, JavaScript, C#, Java, C++ ועוד.
הגדרות
C היא שפה עילית פרוצדורלית בעלת טיפוסיות סטטית בה ניהול הזכרון מתבצע בצורה ידנית. בואו נסקור מה המילים האלו אומרות.
שפה עילית
בשפת C ניתן לכתוב קוד "נייד". כלומר, קוד שיהיה אפשר לקמפל ללא שינוי לארכיטקטורות מחשבים שונות, כגון אינטל ו-arm, ואפילו מערכות הפעלה שונות. אנחנו קוראים לשפה כזו "שפה עילית" כי אפשר לכתוב בה ברמה "מעל" ארכיטקטורת המחשב עליו רצה תוכנית הכתובה בשפה. בתקופה שבה פותחה C, זה היה דבר משמעותי מאוד שגרם לזה שיהיה אפשר להמיר את UNIX לשלל ארכיטקטורות שונות בקלות יחסית.
פרוצדורלית
המילה הזאת מתייחסת לפרדיגמת התכנות בשפה. שפת C היא שפה "אימפרטיבית", כלומר אבני הבניין הבסיסיות שלנו הן פקודות הגורמות למכונה "לשנות מצב", וקוד בשפה כזו יראה בדרך כלל כביצוע פעולות "צעד אחר צעד", כלומר - "תעשה את זה, ואז תעשה את זה, ואז...". שפה פרוצדורלית נותנת לנו לקבץ פקודות כאלה לפרוצדורות (או פונקציות), דבר שמוביל לקוד יותר מסודר, קריא, ושניתן להשתמש בו שוב ושוב.
טיפוסיות סטטית
בסופו של יום, נתונים במחשב מיוצגים כמספרים בינאריים. לתת לנתון מסוים טיפוס זה בעצם לתת משמעות לנתון או להסתכל עליו במשקפיים ספציפיות. לדוגמא, אם יש לי מספר בינארי 01000111, הוא יכול לתאר את המספר השלם 71 עם משקפיים של מספרים שלמים, או שהוא יכול לתאר את התו 'G' עם משקפיים של תוי ASCII, או שהוא יכול לתאר אופציות ליצירת חלון כאשר כל ספרה מייצגת "כן" או "לא" לפיצ'ר ספציפי. יש שלל משקפיים, או טיפוסים, שדרכם אנחנו יכולים להסתכל על נתונים.
ב-C, הצורה שבה אנו מסתכלים על נתונים בכל התוכנית צריכה להיות ברורה לפני שאנחנו מריצים אותה, ואם אנחנו רוצים להסתכל על נתון מסויים בצורה שונה, אנחנו צריכים "להמיר" את הטיפוס בצורה ידנית. יש מקרים בה ההמרה מתבצעת בצורה אוטומטית, כמו למשל בחיבור של מספר שלם ומספר עם נקודה עשרונית, בו המספר השלם "יהפוך" למספר עם נקודה עשרונית. בגלל מקרים כאלו, אנחנו קוראים לשפה כמו C שפה עם טיפוסיות חלשה.
ניהול זכרון ידני
אחד הפיצ'רים (או חוסר בפיצ'ר) המשמעותיים ביותר ב-C הוא שעל המתכנתים בה לנהל את הזכרון באופן ידני.
בשפות רבות, כשאנחנו מגדירים מבנה נתונים מורכב, אנחנו לא צריכים לדאוג איפה בדיוק נמצאים הביטים והבייטים שמייצגים את המבנה הזה. אנחנו פשוט מגדירים אותם, קושרים אליהם משתנים, מעבירים אותם בין פונקציות, וכשאנחנו לא צריכים אותם יותר ואין אף משתנה חי שמכיר בהם, יש מערכת אוטומטית שתנקה אחרינו ותחזיר את הזכרון שאנחנו לא צריכים יותר למערכת ההפעלה.
לא כך ב-C. כשאנחנו מגדירים מבנה נתונים מורכב ב-C, אנחנו צריכים לעשות זאת בצורה ישירה ולעקוב אחרי כל משתנה שמכיר את המבנה הזה ויכול לגשת אליו. וכאשר אנחנו לא צריכים אותו יותר, אנחנו צריכים להגיד לתוכנית בעצמנו מתי ואיפה לשחרר אותו.
בעוד ששתי השיטות טובות בדרכן, באופן כללי הגישה האוטומטית יותר נוחה לשימוש מהגישה הידנית, והגישה הידנית מובילה לביצועים טובים מהגישה האוטומטית. זהו כמובן לא תמיד המקרה - יש מקרים שבהן הגישה האוטומטית מהירה יותר, שכן הקצאת זכרון בגישה האוטומטית יכולה להיות מהירה יותר, ושחרור של הרבה מידע בזמן מרוכז יכול להיות מהיר יותר, וישנם מקרים שבהן אי אפשר להשתמש בשיטה האוטומטית בגלל מגבלות מסויימות בפלטפורמות או בגודל הזכרון, בהן ניהול ידני עובד בצורה יותר נוחה.
הגישה הידנית של C לניהול זכרון נותנת לה שליטה יותר מדוייקת במשאבים של המחשב, ולכן נראה אותה בשימוש נפוץ יותר משפות אחרות בתוכנות בהן השליטה הזו, או המהירות שמגיעה כתוצאה מהשליטה הזו, היא הכרחית. לדוגמא: מערכות הפעלה, עבודה על צ'יפים פשוטים יותר, מערכות ריצה של שפות תכנות, דרייברים לחומרה, ספריות חישוביות, ועוד.
חשוב לציין שהרבה מאוד בעיות אבטחה וטעויות כלליות נובעות מניהול זכרון בצורה ידנית. ולצד ניהול זכרון ידני ואוטומטי קיימות גישות נוספות שמנסות למצוא איזו שיטת ביניים בין השתיים, לדוגמא שיטה של reference counting שפופולארית ב-C++ באמצעות מצביעים חכמים, בה פונקציות ספרייה עוקבות אחרי הנתונים ומשחררות אותם בצורה אוטומטית אבל ללא התערבות חיצונית של מערכת ריצה, או שיטה של life time analysis בה משתמשים בשפת התכנות Rust לניתוח של התוכנה לפני ריצתה וקביעה מתי לשחרר זכרון בצורה סטטית.
איך נראית תוכנית בשפת C?
בתוכנית הבאה נראה כמה מהמאפיינים של השפה ונממש חלון חביב ובו כדור המטייל כמו שומר המסך של נגני DVD של פעם. נציג בכל חלק תחילה את הקוד, ואחריו קצת הסברים.
#include <stdlib.h>
#include "raylib.h"
אנחנו מתחילים בלהגדיר באילו ספריות אנו משתמשים. פקודות המתחילות בתו #
הן פקודות לקומפיילר. פה אנחנו מבקשים להשתמש בפונקציות
מתוך הספרייה הסטנדרטית, ובספרייה חיצונית הנקראת raylib המספקת פונקציונלית לבניית משחקים
ותוכניות גרפיות.
פקודת ה-#include
בעצם אומרת לקומפיילר לכלול את הקובץ המבוקש כחלק מהקובץ הקוד שלנו בזמן תהליך הקימפול.
בעזרת מערכת קצת רעועה ושילוב עם קונבנציות מסויימות, אנחנו יכולים לדאוג
שלא לכלול קובץ יותר מפעם אחת בתוכניתנו, והמערכת הזו היא גם סיבה שכותבים שני סוגים של קבצים כשכותבים C -
קבצי header
שהסיומת שלהם היא .h
, וקבצי קוד מקור שהסיומת שלהם היא .c
.
אני קצת מדלג על הפרטים והנתונים כאן כי זה לא בדיוק עיקר הפוסט, אבל אם זה ממש מעניין אתכם אתם יכולים גם לשלוח לי שאלה במייל על הנושא.
/* Constants */
#define FPS 60
#define SCREEN_WIDTH 640
#define SCREEN_HEIGHT 480
#define BALL_SPEED 200
#define BALL_RADIUS 50
הדבר הבא שאנחנו עושים הוא להגדיר כמה קבועים גלובליים. בכל מקום בו נרשום את השם של הקבוע, הקומפיילר יחליף את השם בערך שאחריו.
/* Data types */
typedef struct {
int x;
int y;
} V2;
typedef struct {
V2 position;
float radius;
V2 direction;
Color color;
} Ball;
כאן אנחנו מגדירים טיפוסים בהם נשתמש במהלך התוכנית. בשפת C, המילה struct
משמשת להגדרת טיפוס המכיל כמה טיפוסים שונים בתוכו.
אנחנו מגדירים טיפוס עבור וקטור דו מימדי, שאפשר להשתמש בו כדי ליצג נקודות או כיוונים, וכדור המכיל מיקום, רדיוס, כיוון וצבע.
/* Functions */
void initialize(void);
Ball* createBall(void);
void updateBall(Ball* ball);
void applyKeys(Ball* ball);
void swingBall(Ball* ball);
void swing(int* position, int* direction, int min, int max);
כאן אנחנו מגדירים את הפונקציות השונות אותן נממש למטה. הסיבה שאנחנו עושים את זה היא כי בשפת C אנחנו לא יכולים להתייחס בתחילת הקובץ לפונקציה המוגדרת בסוף הקובץ. זו לא מגבלה שנתקל בה בדרך כלל בשפות חדשות יותר.
אנחנו מגדירים פונקציות באמצעותן נאתחל חלון חדש, ניצור כדור, נעדכן את הכדור, וכו'.
המילה הראשונה בכל שורה מייצגת את הטיפוס של ערך ההחזרה, כאשר void
אומר "כלום",
השם הבא לפני הסוגריים הוא שם הפונקציה, ובתוך הסוגריים יש רשימת ארגומנטים המופרדים בפסיקים כאשר כל אחד מהארגומנטים מגדירים את הטיפוס המצופה
ומה יהיה שמו בתוך הפונקציה.
אני אדבר על המשמעות של הכוכביות שמופיעות בכמה פונקציות בקרוב.
בניגוד לשפות תכנות בעלות טיפוסיות דינמית בהן אנו בודקים טיפוסים תוך כדי ריצת התוכנית, לא נוכל להתחיל להריץ (או לקמפל) קוד הקורא לפונקציות עם ארגומנטים שלא בטיפוסים שהוגדרו (אלא אם יש המרה אוטומטית בין הטיפוסים). נקבל הודעת שגיאה במקום.
/* Main */
int main(void) {
initialize();
Ball* ball = createBall();
Color background_color = { 0x08, 0x18, 0x29, 0xff };
// Game loop
while (!WindowShouldClose()) {
// Update
updateBall(ball);
// Draw
BeginDrawing();
ClearBackground(background_color);
DrawCircle((*ball).position.x, (*ball).position.y, (*ball).radius, (*ball).color);
EndDrawing();
}
// De-initialization
free(ball);
CloseWindow();
return 0;
}
הגענו סוף סוף לחלק של הקוד. בחלק הזה אנחנו מגדירים את נקודת ההתחלה של התוכנית. בשפות סקריפט כמו פייתון ורובי, אפשר לכתוב פקודות בלי לשייך
אותן לפונקציה, אך ב-C
ושפות רבות אחרות, עלינו להגדיר פונקציה בשם
main
שממנה נתחיל להריץ את התוכנית.
בפונקציה הזו אנחנו מאתחלים את החלון שלנו, מגדירים את הנתונים בהם נשתמש, ומתחילים להריץ לולאה שתפסיק רק כשנסגור את החלון ובה אנחנו מעדכנים את המצב של התוכנית (במקרה שלנו, את המיקום של הכדור), ומציירים אותו למסך.
בסוף הלולאה, אנחנו משחררים את הזכרון שהקצאנו (אמרנו ניהול זכרון ידני לא?) ומסיימים את התוכנית.
התו "נקודה פסיק" (;
)
משמש לסימון של סוף של פקודה. והסוגריים המסולסלים משמשים לתחימה של פקודות ביחד. למשל, הפקודות שירוצו שוב ושוב
כחלק מלולאת ה-while
תחומות בסוגריים מסולסלים, וחלק ה-De-initialization קורה אחרי הלולאה.
/* Initialize window */
void initialize(void) {
SetConfigFlags(FLAG_MSAA_4X_HINT);
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "alloca.space - The C Programming Language");
SetTargetFPS(FPS);
}
בחלק הזה אנחנו יוצרים חלון חדש באמצעות ספריית raylib לפי הפרמטרים שהגדרנו למעלה.
/* Ball */
Ball* createBall(void) {
Ball* ball = (Ball*)malloc(sizeof(Ball));
Ball ball_value = {
.position = {
.x = SCREEN_WIDTH/3,
.y = SCREEN_HEIGHT/3,
},
.radius = BALL_RADIUS,
.direction = {
.x = 1,
.y = 1,
},
.color = { 0xf9, 0x92, 0x26, 0xff },
};
*ball = ball_value;
return ball;
}
בחלק הזה אנחנו מקצים זכרון עבור כדור חדש באמצעות פקודת malloc
,
שבמקום להחזיר לנו ערך כפי שקורה בשפות תכנות עם ניהול זכרון אוטומטי שבהן אין צורך לחשוב על ההבדלים בין מיקומי הזכרון,
מחזירה לנו כתובת לזכרון.
כלומר, במקום שהמשתנה ball
יחזיק ערך מסוג Ball
, הוא מחזיק כתובת המצביעה על מקום בזכרון בו נמצא המידע שמייצג את הכדור.
זו המשמעות של טיפוסים שמסתיימים בכוכבית. אלו נקראים "מצביעים". כדי לגשת לזכרון ולהתבונן בתוכנו או לשנותו, אנחנו נכתוב לפני ערכים שמייצגים מצביעים את התו
'*
'.
אם לא נכתוב כוכבית, הערך שנתבונן בו יהיה הכתובת ולא הערך. בשפת C ההבדלים הללו הם קריטיים!
void updateBall(Ball* ball) {
applyKeys(ball);
swingBall(ball);
}
void applyKeys(Ball* ball) {
if (IsKeyDown(KEY_D)) { (*ball).direction.x = 1; }
if (IsKeyDown(KEY_A)) { (*ball).direction.x = -1; }
if (IsKeyDown(KEY_S)) { (*ball).direction.y = 1; }
if (IsKeyDown(KEY_W)) { (*ball).direction.y = -1; }
}
void swingBall(Ball* ball) {
// for x
swing(&(*ball).position.x,
&(*ball).direction.x,
0 + (*ball).radius, // minimum placement for the ball center
SCREEN_WIDTH - (*ball).radius // maxmimum placement for the ball center
);
// for y
swing(&(*ball).position.y,
&(*ball).direction.y,
0 + (*ball).radius, // minimum placement for the ball center
SCREEN_HEIGHT - (*ball).radius // maxmimum placement for the ball center
);
}
void swing(int* position, int* direction, int min, int max) {
if (*position <= min) {
*position = min;
*direction = 1;
}
else if (*position >= max) {
*position = max;
*direction = -1;
}
float dt = GetFrameTime(); // a technique called delta timing
*position += *direction * (BALL_SPEED * dt);
}
בחלק האחרון שנשאר לנו אנחנו מעדכנים את המיקום והכיוון של הכדור שמשתנה בכל פריים. בין אם באמצעות אינפוט מהשחקן, או בבדיקת התנגשות עם הקירות.
שימו לב שלפונקציות עדכון אנחנו מעבירים מצביעים (משתנים המכילים כתובות זכרון) כדי שנוכל לשנות את המידע הנמצא בכתובות הזכרון,
וכשאנחנו רוצים להתייחס לערך שבתוכם אנחנו מוסיפים
*
לפניהם.
עוד דבר מעניין הוא שבעצם כל משתנה נמצא בזכרון איפשהו, ואנחנו יכולים לקבל את כתובת הזכרון בו הוא נמצא ע"י הוספת התו
'&
'
לפני שם המשתנה.
אז למעשה אנחנו רואים בקוד כמה וריאציות שונות של עבודה עם מצביעים:
if (*position <= min) {
-
בשורה הזו אנחנו משווים את הערך שנמצא בכתובת בזכרון שנמצאת במצביע
position
. *position += *direction;
-
בשורה הזו אנחנו מעדכנים את הערך שנמצא בכתובת בזכרון אליה מצביע המצביע
position
לערך הנמצא בכתובת בזכרון אליה מצביע המצביעdirection
. &(*ball).position.x
-
בשורה הזו אנחנו מבקשים לקבל את הכתובת בזכרון של הקומפוננטה
x
של המיקום של הכדור שנמצא בכתובת בזכרון אליה מצביע המצביעball
.
הרבה אנשים הגיעו למסקנה שעבור הרבה מאוד סוגים של תוכנה, לא ממש מועיל לחשוב על כתובות זכרון ואיפה ערכים מוגדרים, ולהבדיל בין משתנים המחזיקים כתובות (מצביעים) ומשתנים המחזיקים ערכים, ולכן הם מעדיפים להשתמש בשפות תכנות בהן אין צורך לחשוב על הדברים האלה בכלל. אבל ישנן סוגים של תוכנות, כמו מערכות הפעלה, דרייברים, ושאר סוגי התוכנות שהזכרתי מקודם, בהן המידע הזה, והשליטה על המידע הזה, תורמת והכרחית כדי לממש את המטרות שבשבילן הן נבנו.
הקוד בפעולה
את הקוד שכתבנו למעלה אפשר לראות בפעולה כאן:
איך ללמוד C?
C היא שפת תכנות סופר פופולארית, וב50+ השנים האחרונות נכתבו אינספור מדריכים לשפה. לצערנו, לא כולם טובים.
אני אמליץ על שני ספרים כאופציות ללמידת השפה:
- > Modern C / Jens Gustedt
- ספר שמופץ גם בצורה חינמית שלאחרונה יצאה לו גרסא חדשה המכסה את הסטנדרט האחרון, C23.
- > Effective C / Robert C. Seacord
- ספר קצת פחות חינמי מהקודם שגם לו יצאה גרסא חדשה המכסה את הסטנדרט C23.
בנוסף, למתכנתים שכבר מכירים את C בגרסא ישנה יותר, הסרטון הבא מסקר פיצ'רים חדשים שנוספו לשפה בסטנדרט החדש: Modern C and What We Can Learn From It - Luca Sas [ ACCU 2021 ].
פרוייקטים מעניינים הכתובים ב-C
הרבה מאוד תוכנות כתובות בשפת C. הנה כמה תוכנות לא מאוד גדולות ויחסית מודרניות הכתובות בשפה, במידה ותרצו להכיר ולשחק עם הקוד.
- > Uxn
- Uxn היא מעין "פלטפורמה וירטואלית" של מחשב היכולה לרוץ על מגוון רחב של מכשירים.
- > MEG4
- בדומה לUxn אבל בצורה שיותר מכוונת למשחקים, MEG4 היא "קונסולה וירטואלית".
- > chibicc
- קומפיילר (בעברית: מהדר) קטן לשפת התכנות C הכתוב עצמו בשפת C. מטרת המהדר הזה היא להיות קל לקריאה והבנה.
- > xv6
- מערכת הפעלה הפותחה למטרת לימוד מערכות הפעלה. יכולה לעבוד טוב ביחד עם ספר או קורס.
סיכום
שפת C היא שפה מאוד ישנה שעדיין בשימוש נרחב בהרבה מאוד מהתוכנות שכולנו משתמשים בהן, כאשר הבולטים מביניהם בקהילת הקוד הפתוח הם גרעין מערכת ההפעלה לינוקס, מערכת ניהול הגרסאות Git, ומסד הנתונים SQLite.
C מעניקה שליטה מדוייקת על משאבי המכונה הרבה מעבר להרבה שפות מודרניות יותר, אבל שליטה זו מלווה בקשיים וסכנות רבות שגורמים לרבים מצד אחד לזנוח אותה עבור שפות אחרות בפרוייקטים שאינם מצריכים שליטה כה מדוקדקת, ומצד שני גורמים לרבים לחפש גישות מודרניות יותר להשגת אותה שליטה עם פחות בעיות פוטנציאליות.
ועדיין, ניתן למצוא שלל תוכנות בשימוש נרחב המשתמשות בשפה ולכן שווה, לדעתי, להכירה.
רוצים להגיב? בדקו מהי שאלת הסינון בעמוד הראשי.