C ממירה ערכים בין טיפוסים כל הזמן. חלק מההמרות אתם כותבים בעצמכם עם cast, ואת רובן הקומפיילר מבצע בשקט לפי כללים שלא אתם בחרתם. לדעת מה מהן זה ההבדל בין "למה הממוצע שלי תמיד 3?" לבין קוד שעושה את מה שהוא אומר.
המרה מובלעת
בכל פעם שערך מטיפוס אחד פוגש הקשר שמצפה לטיפוס אחר, C ממירה אותו:
המרות שלא יכולות לאבד מידע (int ל-double, char ל-int, short ל-long) הן המרות מרחיבות ותמיד בטוחות. המרות בכיוון ההפוך הן מצמצמות ועלולות לאבד נתונים: ה-3.9 שלמעלה הפך ל-3 בלי אזהרה, אלא אם תבקשו אחת עם -Wconversion.
ההמרות האריתמטיות הרגילות
כשלאופרטור בינארי יש אופרנדים מטיפוסים שונים, C ממירה אותם לטיפוס משותף לפני שהיא מבצעת את העבודה. הסולם, מלמטה למעלה:
- כל דבר קטן מ-
int(char,short,_Bool) מקודם ל-int. זה קידום מספרים שלמים (integer promotion), והוא קורה ראשון, תמיד. - אם אחד הצדדים הוא
long double, השני הופך ל-long double. - אחרת, אם אחד מהם
double, השני הופך ל-double. - אחרת, אם אחד מהם
float, השני הופך ל-float. - אחרת, בין טיפוסים שלמים, זה עם הדרגה הגבוהה יותר מנצח, ואם הדרגות שוות, unsigned מנצח.
הכלל האחרון הוא זה שגורם לבאגים אמיתיים. השאר אינטואיטיביים.
קידום מספרים שלמים הוא הסיבה שחשבון עם char לא גולש כמו שאולי הייתם מצפים, וגם הסיבה ששמירת התוצאה בחזרה ב-char כן גולשת.
Cast מפורש
cast הוא טיפוס היעד בתוך סוגריים:
(double)x
(int)3.9
(char)65
(unsigned int)n
הוא חל על הביטוי שבא מיד אחריו, והוא נקשר חזק מאוד: חזק יותר מ-*, / או +.
השורה הראשונה מחלקת כמספרים שלמים (ומקבלת 3) ואז ממירה את 3 ל-3.0, מאוחר מדי. השנייה ממירה את total לדיוק שמאפשר 3.5 לפני החילוק, כך שהאופרטור / רואה double ו-int, מקדם את ה-int ומבצע חילוק עשרוני.
מספיק לעשות cast לאופרנד אחד. ההמרות האריתמטיות הרגילות מטפלות בשני.
תיקון חילוק של מספרים שלמים
זו הסיבה הנפוצה ביותר לכתוב cast ב-C:
שורת האחוזים מלמדת: passed / n הוא 3 / 5, שזה 0 במספרים שלמים, ו-0 * 100 הוא 0. כפל לפני החילוק (100 * passed / n) מתקן את זה גם בלי cast, כי 300 / 5 מדויק, אבל זה עובד רק כשהמספרים משתפים פעולה. ה-cast הוא התיקון האמין.
קטיעה, לא עיגול
cast של ערך עשרוני למספר שלם זורק את השבר. הוא קוטע לכיוון אפס, ולא מעגל:
אם אתם בונים את זה על המכונה שלכם, זכרו ש-math.h צריך -lm בזמן הקישור ב-Linux.
סכנה נוספת: המרה של ערך עשרוני גדול מדי לטיפוס השלם היא undefined behavior, ולא גלגול. (int)1e20 יכול להפיק כל דבר. בדקו את הטווח לפני ה-cast כשהערך אינו בשליטתכם.
char ו-int
char ב-C הוא מספר שלם קטן שמחזיק קוד של תו. המרה בין השניים היא עבודה יומיומית:
digit - '0' הוא הניב הסטנדרטי להפיכת תו של ספרה לערך שלה, והוא עובד כי מובטח שעשרת תווי הספרות רציפים. עבור אותיות, העדיפו את toupper() ו-tolower() מתוך ctype.h על פני החשבון עם + 32: ההיסט הוא עובדה של ASCII, לא הבטחה של C.
מלכודת קשורה: פונקציות ב-ctype.h כמו isdigit ו-toupper מקבלות int שחייב להיות EOF או ערך שאפשר לייצג כ-unsigned char. העברה של char רגיל שהוא שלילי (אפשרי, כי char רגיל עשוי להיות signed) היא undefined. עשו cast: isdigit((unsigned char)c).
מלכודת signed/unsigned
שלב 5 בסולם ההמרות, unsigned מנצח בשוויון, מייצר את ההשוואה המפתיעה ביותר של C:
-1 מומר ל-unsigned int, שמפרש מחדש את תבנית הביטים שלו כ-4,294,967,295. זה גדול מ-1, ולכן ההשוואה היא false.
אותה המרה גורמת ללולאות לרוץ לנצח:
/* באג: i הוא unsigned, ולכן i >= 0 תמיד אמת. כש-i הוא 0, i-- מתגלגל. */
for (size_t i = n - 1; i >= 0; i--) { ... }
והיא גורמת לבדיקות אורך להיכשל:
/* באג: strlen מחזירה size_t (unsigned). אם המחרוזת קצרה
מ-5, len - 5 מתגלגל למספר ענק והבדיקה עוברת. */
if (strlen(s) - 5 > 0) { ... }
כתבו מחדש כ-if (strlen(s) > 5), והחיסור לא קורה אף פעם.
ההגנות: שמרו ספירות ואינדקסים בסוג סימן אחד לכל האורך, קמפלו עם -Wsign-compare (שכלול ב--Wextra), וכשאתם חייבים לערבב, עשו cast מפורש אחרי שווידאתם שהערך לא יכול להיות שלילי.
Cast של מצביעים
casts ממירים גם בין טיפוסי מצביעים, וכאן יש בהם סיכון אמיתי, כי הם משנים את האופן שבו הזיכרון מתפרש, לא את הבתים עצמם.
במכונת little-endian זה מדפיס 01 00 00 00. בדיקה של ייצוג האובייקט דרך unsigned char * היא אחד מה-casts המעטים של מצביעים שהתקן מאשר במפורש.
רוב ה-casts האחרים של מצביעים לא מאושרים. קריאה של int דרך float * מפרה את כלל ה-strict aliasing והיא undefined behavior, למרות שהיא מתקמפלת. כדי לפרש בתים מחדש, השתמשו ב-memcpy.
שתי מוסכמות ששווה להכיר. void * מומר אל כל טיפוס של מצביע לאובייקט וממנו בלי cast ב-C, ולכן לא כדאי לעשות cast לתוצאה של malloc:
int *arr = malloc(n * sizeof *arr); /* C נכונה */
int *arr = (int *)malloc(n * sizeof *arr); /* מיותר, ומסתיר header חסר */
ה-cast נדרש ב-C++, ולכן הוא מופיע בכל כך הרבה קוד. ב-C הוא יכול להסתיר את הטעות של שכחת <stdlib.h>.
ו-printf("%p", ...) מצפה ל-void *, כך שארגומנטים של מצביעים שם באמת צריכים cast: printf("%p", (void *)p).
כש-cast הוא התשובה הלא נכונה
cast משתיק את הקומפיילר. לפעמים הקומפיילר צדק.
long big = 5000000000L;
int small = (int)big; /* ה-cast מסתיר איבוד נתונים אמיתי */
אם הערך באמת נכנס, ה-cast מתעד שבדקתם. אם הוא עלול לא להיכנס, ה-cast הפך אזהרה לתשובה שגויה ושקטה. לפני שכותבים cast, שאלו אם התיקון הוא דווקא לשנות את הטיפוס של משתנה: double במקום int, size_t במקום int, long long במקום long. cast הוא הכלי הנכון בעיקר כששני טיפוסים נכונים חייבים להיפגש לפעולה אחת, כמו ב-(double)sum / count.
שאלות נפוצות
איך עושים cast ב-C?
שמים את טיפוס היעד בסוגריים לפני הערך: (double)x, (int)3.9, (char)65. ה-cast חל על הביטוי שבא מיד אחריו, כך ש-(double)a / b ממיר קודם את a ואז מחלק, בזמן ש-(double)(a / b) מחלק כמספרים שלמים וממיר את התוצאה.
איך ממירים int ל-float ב-C?
השמה עושה את זה באופן מובלע: double d = 5; שומר 5.0. בתוך ביטוי צריך לעיתים קרובות cast מפורש: (double)total / count מכריח חילוק עשרוני במקום חילוק של מספרים שלמים.
מה קורה כשעושים cast מ-float ל-int ב-C?
החלק השברי נזרק: קטיעה לכיוון אפס, אף פעם לא עיגול. (int)3.9 הוא 3, ו-(int)-3.9 הוא -3. כדי לעגל, הוסיפו 0.5 לפני ה-cast עבור מספרים חיוביים, או השתמשו ב-round(), floor() או ceil() מתוך math.h.
למה השוואה בין int עם סימן ל-int בלי סימן נותנת תשובה שגויה?
ההמרות האריתמטיות הרגילות של C ממירות את הערך עם הסימן ל-unsigned, ולכן -1 < 1u הוא false: -1 הופך למספר חיובי ענק. שמרו ספירות וגדלים בסוג סימן אחד, או עשו cast מפורש אחרי שבדקתם שהערך לא יכול להיות שלילי.