כאן תמצאו סקירה מהירה של עיקרי המדריך:
נתחיל בהבדלים בין enum ל-struct וכיצד לבחור ביניהם, נבין מתי חוסכים זיכרון בעזרת union ומתי עדיף מבנה רגיל.
נצלול לניהול זיכרון ב-Stack וב-Heap, נזהה דליפות ונלמד להשתמש ב-malloc ו-free.
לאחר מכן נכיר את כוחו של הפרה-מעבד: מאקרואים, #include, #ifdef והגנות Include Guards.
נסביר את מוסכמות הקריאה cdecl ו-fastcall, ואת ההבדל בין inline למאקרו פונקציונלי.
לבסוף נדגים תהליך בנייה מלא עם Makefile וקבצי ELF, נכיר את readelf לניתוח פלט ואת Valgrind לבדיקת זיכרון, ונראה כיצד #error עוצרת הידור בתנאים לא תקינים – הכנה אידאלית למיוני גאמא סייבר.
היכרות מעמיקה עם שפת C מהווה בסיס חיוני לכל מתכנת המעוניין
לשלוט ביסודות תכנות מערכות ויישומי Embedded.
שפת C כוללת תכונות בסיסיות כמו עבודה עם משתנים מסוגים שונים,
בקרת זרימה באמצעות תנאים ולולאות, וכן מנגנונים מתקדמים יותר כמו
enum, struct, union ועיבוד
מקדים (Preprocessing).
נושאים אלה חשובים לכל מי שרוצה
להעמיק בהבנת השפה, ויכולים לסייע במיוחד לאלו החותרים לקריירה בתחום אבטחת
המידע והסייבר.
מי שמתעניין בקבלה ל גאמא סייבר יגלה כי שליטה ביכולות של שפת
C יכולה להעניק יתרון משמעותי בהמשך דרכו.
ב-C ניתן להגדיר אוסף של קבועים שלמים תחת שם אחד באמצעות
enum.
זוהי דרך להעניק שמות קריאים לערכים מספריים.
כאשר
מגדירים enum, המהדר מקצה לכל שם ערך עוקב כברירת מחדל, החל
מ-0.
עם זאת, אפשר להצמיד ערכים ספציפיים לכל איבר.
כאשר משתמשים ב-enum מקבלים קוד קריא יותר מאשר שימוש
במספרים "קסומים".
אפשר לדוגמה להגדיר מצבי פעולה (כמו
START, STOP, PAUSE) כערכים
של enum, ולהימנע מערבוב של מספרים סתמיים בקוד.
בנוסף, ניתן
להקצות ערכים באופן חלקי, כך שאם איבר אחד מוגדר ל-10, האיבר הבא יוגדר
אוטומטית ל-11 וכן הלאה.
struct בשפת C נועד לאגד מספר משתנים מסוגים
שונים תחת שם טיפוס יחיד.
כאשר מעצבים מבנה נתונים מורכב, אפשר להכניס בתוכו
משתני int, float, מצביעים ועוד.
לאחר מכן,
הגישה לשדות מתבצעת באמצעות סימון נקודה.
לדוגמה:
myStruct.someField.
struct הוא כלי יעיל לארגון נתונים הקשורים זה לזה בהקשר
לוגי.
במקום להחזיק כמה משתנים גלובליים או מקומיים מבולגנים, ניתן לארוז
אותם במבנה אחד, מה שמקל על קריאות הקוד ועל העברתו לפונקציות.
union פועל בדומה ל-struct מבחינת תחביר, אך
השוני העיקרי הוא שכל השדות של union חולקים את אותו אזור
בזיכרון.
פירוש הדבר הוא שבכל רגע נתון רק אחד מהשדות יכול להכיל ערך תקין.
אם תאחסן ערך בשדה אחד, שדה אחר עשוי להיפגע.
היתרון הבולט הוא חיסכון בזיכרון כאשר אנו יודעים שבפועל נשתמש רק בשדה אחד
בכל רגע.
לדוגמה, אפשר להגדיר union שמייצג נתון שעשוי להיות
מספר שלם או מספר ממשי, ולבחור באיזה שדה להשתמש בהתאם להקשר.
כאשר צריכים
לשמור כמה ערכים בו זמנית, עדיף struct על פני
union.
נעדיף union כאשר נרצה לחסוך במקום בזיכרון, או כאשר ברור
שבפועל לא יהיה שימוש מקביל בכל השדות.
במקרה כזה אין צורך להגדיר
struct שרוב שדותיו נותרים ריקים או לא בשימוש במקביל.
union שימושי במיוחד כאשר סוג הנתון שאנו קוראים או כותבים
עשוי להשתנות בזמן ריצה, אך רק אחד בכל פעם.
טיפוס char ברוב המערכות מיועד לאחסן תו אחד, לרוב בהתאם
לקידוד ASCII או Unicode בסיסי.
בפועל,
char יכול לשמש גם כמספר שלם קטן (8 ביט) ולהיות חלק
מחישובים.
טיפוס long מוגדר כשלם בעל גודל גדול או שווה לגודל של
int.
במערכות 32-ביט הוא לרוב יהיה בגודל 32 ביט, ובמערכות
64-ביט הוא עשוי להיות 64 ביט.
כך ניתן לייצג מספרים גדולים יותר מאשר
int רגיל.
מי שמעוניין להתקדם בשירות ביחידה 8200, מוצא לא
פעם צורך בטיפול בערכים גדולים בשפת C, במיוחד בעיבודי
נתונים מורכבים.
פקודת switch מאפשרת לבחור בלוק קוד לביצוע בהתאם לערך של
משתנה שלם או תו.
במקום שרשרת ארוכה של if-else, ניתן להגדיר
case לכל ערך רצוי, ולהוסיף default למקרה
בו אף case לא מתאים.
כך נמנעים מצב שאינו מטופל.
כאשר ערך המשתנה תואם לאחד ה-case-ים, מבוצעות ההוראות עד
לפקודת break או עד סוף ה-switch.
default אינו חובה, אך מומלץ לטפל במקרים לא צפויים.
פונקציית atoi (מוגדרת בקובץ הכותרת
stdlib.h) ממירה מחרוזת (char*) למספר שלם
int.
לדוגמה, "123" יהפוך ל-123.
שימושי במיוחד כאשר צריך
לקבל קלט טקסטואלי ולהפוך אותו לערך מספרי.
עם זאת, atoi
אינה מטפלת באופן מפורט בשגיאות בקלט; אם נדרש טיפול מורכב יותר (לדוגמה,
בפורמט לא תקין), ניתן להשתמש בפונקציות כמו strtol.
הפרה-מעבד פועל בשלב הראשון לפני הקומפילציה.
הוא מטפל בכל הוראות
ה-#include, #define, #if,
#ifdef ועוד, ומפיק קובץ מקור מורחב בו כל המאקרואים מוחלפים
ופקודות תנאי מוסרות או נכללות בהתאם.
לאחר מכן, הקוד הזה נשלח לקומפיילר
עצמו.
מאקרו הוא מנגנון החלפת טקסט פשוט.
הגדרת מאקרו ללא פרמטר יכולה להחליף
מחרוזת טקסט, בעוד מאקרו עם פרמטרים (Function-Like Macro)
יכול לדמות פונקציה.
היתרון הוא ביטול תקורת הקריאה לפונקציה – המהדר לא מבצע
קריאה אמיתית אלא מחליף את הטקסט.
חסרונו העיקרי הוא היעדר בדיקת טיפוסים
ותופעות לוואי אפשריות (למשל אם משתמשים בפרמטר במאקרו ומפעילים עליו ++).
כדי להגדיר מאקרו המשתרע על פני מספר שורות בקוד המקור, יש להשתמש בתו
Backslash בסוף כל שורה.
כך הפרה-מעבד יזהה אותן כרצף אחד.
לדוגמה:
#define MY_MACRO(x) \
do {
printf("%d\n", x);
} while (0)
כאשר רוצים להגדיר שם חדש לטיפוס, ניתן להשתמש ב-typedef, או
ב-#define.
חשוב לזכור ש-typedef הוא הוראה
לקומפיילר המייצרת טיפוס חדש עם בדיקת תחביר ובדיקת טיפוסים מלאה.
לעומת זאת,
#define פשוט מחליף טקסט ואינו מבצע שום בדיקה.
סוגריים
חסרים או הרחבות מסובכות יכולות לגרום לבאגים נסתרים.
ב-C קיימת האפשרות להגדיר פונקציה כ-inline.
המשמעות היא שהקומפיילר עשוי "לשתול" את קוד הפונקציה ישירות במקום הקריאה,
ולחסוך את עלות הקריאה לפונקציה – בדומה ל-macro.
ההבדל הוא שבפונקציית inline הקוד עובר בדיקת טיפוסים ובדיקת תחביר,
ובכך נמנעים תרחישים לא צפויים.
כמו כן, קומפיילרים מודרניים מחליטים באילו קריאות באמת לבצע
inline בהתאם לשיקולי אופטימיזציה.
פקודת #ifdef NAME בודקת האם מאקרו בשם NAME הוגדר.
לעומת זאת, #if defined(NAME) מאפשרת גם לשלב תנאים לוגיים מורכבים כגון
#if defined(NAME) && !defined(OTHER).
שתי הצורות פועלות בדומה, אך #if defined(...) גמישה יותר כשיש צורך
לנסח ביטויים מורכבים.
קובצי כותרת (header) נכללים בקוד באמצעות #include.
כדי למנוע הכללה כפולה שעשויה לגרום לשגיאות, נהוג להגדיר
Include Guards, לדוגמה:
#ifndef MY_HEADER_H
#define MY_HEADER_H ... #endif
בשנים האחרונות, מהדרים רבים תומכים ב-#pragma once, המסמן למהדר לכלול
את הקובץ רק פעם אחת ללא צורך להגדיר מאקרו ייחודי.
שימוש ב-#pragma once הופך את הקוד לנקי יותר.
כאשר מגדירים משתנה או פונקציה עם static מחוץ לכל פונקציה,
נוצרת מה שנקרא Internal Linkage.
פירוש הדבר הוא שהמשתנה או הפונקציה אינם זמינים בקבצים אחרים, גם אם יקראו להם באותו שם מבחוץ.
כך מקבלים מעין "מידור" בסיסי ברמת הקובץ.
static ברמת הגלובל שונה מ-static בתוך פונקציה, שם המשמעות היא שהמשתנה המקומי
ישמור על ערכו בין קריאות.
כאשר כותבים #include "filename", המהדר מחפש קודם כל את הקובץ באותה תיקייה
(או בספריית הפרויקט). אם לא נמצא, הוא מחפש במסלולי ברירת המחדל.
ב-#include <filename> החיפוש מתבצע תחילה בספריות המערכת (כגון
ספריות ה-Standard C Library).
כדאי לבחור בצורה הנכונה לפי מיקום הקובץ ואופיו (מערכתי או מקומי).
#error היא פקודת פרה-מעבד שיוצרת שגיאה מפורשת ומפסיקה את תהליך ההידור עם ההודעה
שנכתוב אחריה.
לדוגמה, אם דורשים מאקרו כלשהו שלא הוגדר, אפשר לכתוב:
#ifndef REQUIRED_MACRO
#error "REQUIRED_MACRO not defined!" #endif
שפת C מציעה מגוון מנגנונים בסיסיים ומתקדמים, מארגון ערכים באמצעות
enum, struct ו-union ועד שימוש יעיל בפרה-מעבד
באמצעות מאקרואים, הוראות תנאי והכללות נכונות.
ההקפדה על קוד קריא, מניעת התנגשות שמות, ושימוש נכון בהוראות inline ובדיקות טיפוסים
יכולה לשדרג משמעותית את איכות התוכנה.
כמו כן, מי שנמצא בשלבי הכנה למיונים גאמא סייבר יגלה שהתמצאות בפרטי הפרה-מעבד
והיכרות עם עבודה יעילה בשפת C תסייע לו לפתור שאלות מורכבות באמינות וביציבות.