לכל משתנה ב-C יש טיפוס, שנבחר כשמצהירים עליו וקבוע לכל אורך חייו. הטיפוס קובע שלושה דברים: כמה בתים המשתנה תופס, איך מפרשים את הבתים האלה, ואילו פעולות הגיוניות עליו.
רשימת הטיפוסים של C קצרה. המורכבות נמצאת במגדירים.
ארבעת טיפוסי הבסיס
charמחזיק בית אחד. משתמשים בו לתווים בודדים ('A'), לבתים של מחרוזת, ולפעמים כמספר שלם זעיר.intהוא סוס העבודה של המספרים השלמים. מוני לולאות, גדלים, מזהים.floatו-doubleמחזיקים מספרים ממשיים. ל-doubleיש בערך פי שניים דיוק.voidהוא במובן מסוים הטיפוס הרביעי, אבל פירושו "אין ערך": אי אפשר להצהיר על משתנהvoid, רק להשתמש בו כטיפוס החזרה של פונקציה, כרשימת פרמטרים ריקה או כמצביע בלי טיפוס.
שימו לב לסיומת f ב-3.14f. בלעדיה הערך המילולי הוא double, והשמתו ל-float מאבדת דיוק בשקט. חלק מהקומפיילרים מזהירים על זה.
מגדירים: short, long, unsigned
טיפוסי השלמים הבסיסיים מגיעים עם מגדירים שמשנים את הגודל או את הסימן.
כל טיפוס שלם צריך מציין פורמט משלו: %hd ל-short, %d ל-int, %ld ל-long, %lld ל-long long, %u ל-unsigned. שימוש במציין הלא נכון הוא התנהגות לא מוגדרת, לא שגיאת עיגול.
unsigned מסיר את ביט הסימן ומכפיל את הטווח החיובי. unsigned int מחזיק 0 עד בערך 4.3 מיליארד במקום ממינוס 2.1 עד פלוס 2.1 מיליארד. זו הבחירה הנכונה לדברים שבאמת לא יכולים להיות שליליים, גדלים, ספירות של בתים, תבניות ביטים, ומלכודת לכל דבר שעלול לרדת מתחת לאפס בחיסור.
signed היא ברירת המחדל עבור int, short, long ו-long long, כך שכמעט אף פעם לא כותבים אותה. המקום היחיד שבו זה משנה הוא char: האם char רגיל הוא עם סימן או בלי תלוי מימוש, אז כתבו signed char או unsigned char כשהסימן משנה.
גדלים: מה מקבלים בפועל
תקן C מגדיר מינימום, לא גדלים מדויקים. בכל מכונת Linux, macOS או Windows מודרנית של 64 ביט תראו את זה:
| טיפוס | גודל טיפוסי | טווח טיפוסי |
|---|---|---|
char | בית אחד | מינוס 128 עד 127 (או 0 עד 255) |
short | 2 בתים | מינוס 32,768 עד 32,767 |
int | 4 בתים | מינוס 2,147,483,648 עד 2,147,483,647 |
long | 8 בתים (4 ב-Windows) | בערך ±9.2 קווינטיליון |
long long | 8 בתים | בערך ±9.2 קווינטיליון |
float | 4 בתים | כ-7 ספרות משמעותיות |
double | 8 בתים | כ-15 ספרות משמעותיות |
long double | 16 בתים (משתנה) | יותר מ-double |
שורת ה-long היא זו שתופסת אנשים: היא 8 בתים ב-Linux וב-macOS, ו-4 בתים ב-Windows של 64 ביט. קוד שמניח ש-long יכול להחזיק ערך של 64 ביט אינו נייד. השתמשו ב-long long, או בטיפוסים ברוחב מדויק מ-stdint.h (int32_t, uint64_t) כשהגודל הוא חלק מהדרישה.
sizeof: שאלו את הקומפיילר
לעולם אל תנחשו גודל, מדדו אותו:
sizeof הוא אופרטור, לא פונקציה, והוא מחושב בזמן קומפילציה. הוא מחזיר ערך מטיפוס size_t, שמודפס עם %zu.
sizeof(char) מובטח להיות בדיוק 1: זו ההגדרה של בית ב-C. כל השאר נמדד ביחס אליו.
limits.h ו-float.h
הטווחים המדויקים של הקומפיילר שלכם זמינים כקבועים עם שמות:
אלה הערכים שמולם משווים כשצריך לדעת אם פעולה תגלוש. בדיקת if (a > INT_MAX - b) לפני חישוב a + b היא הדרך לזהות גלישה לפני שהיא קורית, וזה חשוב, כי אי אפשר לזהות גלישה של ערך עם סימן אחרי המעשה.
גלישת מספרים שלמים
מה שקורה כשערך חורג מהטווח של הטיפוס שלו תלוי לגמרי בסימן.
גלישה של ערך בלי סימן מוגדרת: הערך מתגלגל מודולו 2^N.
המקרה השני הוא מקור אמיתי לבאגים. לולאה כמו for (unsigned i = n - 1; i >= 0; i--) לעולם לא נגמרת, כי ערך בלי סימן הוא תמיד >= 0.
גלישה של ערך עם סימן היא התנהגות לא מוגדרת. לא "מתגלגל", אלא לא מוגדר. הקומפיילר רשאי להניח שהיא אף פעם לא קורית ולבצע אופטימיזציה על סמך זה, כלומר בדיקת גלישה שנכתבה אחרי המעשה עלולה להימחק:
int sum = a + b;
if (sum < a) { /* the compiler may remove this entirely */ }
בדקו לפני, בעזרת הגבולות:
if (b > 0 && a > INT_MAX - b) {
/* a + b would overflow - handle it */
}
דיוק של נקודה צפה
float ו-double מאחסנים מספרים בבינארי, ולרוב השברים העשרוניים אין צורה בינארית מדויקת, בדיוק כמו של-1/3 אין צורה עשרונית מדויקת.
הכלל שנובע מזה: לעולם אל תשוו ערכי נקודה צפה עם ==. השוו במקום זה את ההפרש המוחלט מול סבולת קטנה.
ולעולם אל תשתמשו בנקודה צפה לכסף. אחסנו אגורות כמספר שלם; long long של אגורות מדויק, בעוד ש-double של שקלים לא.
בחירת טיפוס
רשימת החלטות קצרה שמכסה את רוב הקוד:
- מספרים שלמים:
int, אלא אם יש לכם סיבה. זה הטיפוס שהמעבד מטפל בו ביעילות הגבוהה ביותר, והטיפוס שכל כללי החשבון בנויים סביבו. - כל דבר גדול מ-2 מיליארד:
long long, אוint64_tמ-stdint.h. - גדלים, אורכים ואינדקסים של מערכים מ-
sizeofאו מ-strlen:size_t. הוא בלי סימן ומובטח להיות גדול מספיק לכל אובייקט. - מספרים עשרוניים:
double. השתמשו ב-floatרק כדי לחצות את הזיכרון במערכים גדולים או בחומרה משובצת בלי יחידה לדיוק כפול. - תווים בודדים ובתים גולמיים:
charלטקסט,unsigned charלנתונים בינאריים. - אמת/שקר:
boolמ-stdbool.h; ראו ערכים בוליאניים ב-C. - רוחב ביטים מדויק (פורמטים של קבצים, פרוטוקולי רשת, רגיסטרים של חומרה):
stdint.h, כלומרuint8_t,int16_t,uint32_tוכן הלאה.
ערבוב טיפוסים
כשמשלבים שני טיפוסים שונים בביטוי אחד, C ממירה אותם מאחורי הגב שלכם לפני החישוב. זה בדרך כלל מועיל ולפעמים הרסני:
הראשון הוא חילוק שלמים: שני האופרנדים הם int, כך שהתוצאה היא int והשבר נזרק. השלישי גרוע יותר: השוואה של ערך עם סימן לערך בלי סימן ממירה את זה שעם סימן לבלי סימן, והופכת את -1 למספר חיובי ענק.
כללי ההמרה האלה, ואיך לקחת עליהם שליטה עם המרות מפורשות, הם הנושא של המרת טיפוסים ב-C.
שאלות נפוצות
מה הם טיפוסי הנתונים הבסיסיים ב-C?
ארבעה טיפוסי בסיס: char לתווים בודדים ולבתים, int למספרים שלמים, float ו-double למספרים עשרוניים. מגדירים משנים את הגודל והסימן שלהם, short, long, long long, signed ו-unsigned, וכך נוצרת הקבוצה המלאה.
כמה בתים יש ב-int ב-C?
כמעט תמיד 4 בתים (32 ביט) במערכות שולחניות ושרתים מודרניים, וזה נותן טווח של בערך מינוס 2.1 מיליארד עד 2.1 מיליארד. התקן מבטיח רק לפחות 2 בתים, ובמיקרו-בקרים של 16 ביט באמת משתמשים ב-2. השתמשו ב-sizeof(int) אם צריך לדעת בוודאות.
מה ההבדל בין float ל-double ב-C?
float הוא 4 בתים עם בערך 7 ספרות עשרוניות משמעותיות; double הוא 8 בתים עם בערך 15. double היא ברירת המחדל לערכים מילוליים של נקודה צפה ולפונקציות מתמטיות, ואלא אם אתם מאחסנים מיליוני ערכים או מכוונים לשבב משובץ, double היא הבחירה הנכונה.
מה קורה כש-int גולש ב-C?
עבור int עם סימן, גלישה היא התנהגות לא מוגדרת: הקומפיילר עשוי להתגלגל, לרוות או להעלים את הבדיקה לגמרי באופטימיזציה. עבור int בלי סימן היא מוגדרת לגמרי: הערך מתגלגל מודולו 2^N, כך ש-UINT_MAX + 1 הוא 0. לעולם אל תסמכו על גלישה של ערך עם סימן.