format specifier אומר ל-printf איך להפוך ערך לטקסט, ול-scanf איך להפוך טקסט לערך. זה החלק היחיד ב-C שבו הקומפיילר לא יכול לעזור לכם כברירת מחדל, כי טיפוסי הארגומנטים בלתי נראים לפונקציה, ולכן העמוד הזה הוא הטבלה שכדאי לבדוק מולה.
הצורה הכללית של מגדיר היא:
%[flags][width][.precision][length]conversion
רק ה-% ואות ההמרה הם חובה. %-8.2lf משתמש בכל חמשת החלקים.
טבלת ההמרות
| מגדיר | משמעות | ארגומנט ל-printf | ארגומנט ל-scanf |
|---|---|---|---|
%d | מספר שלם עשרוני עם סימן | int | int * |
%i | מספר שלם עם סימן | int (כמו %d) | int * (מקבל גם 0x ואוקטלי עם 0) |
%u | עשרוני בלי סימן | unsigned int | unsigned int * |
%f | נקודה צפה עשרונית | double | float * |
%F | כמו %f, עם INF/NAN באותיות גדולות | double | - |
%e / %E | כתיב מדעי | double | float * |
%g / %G | הקצר מבין %f / %e | double | float * |
%a / %A | נקודה צפה הקסדצימלית | double | float * |
%c | תו בודד | int (char מקודם) | char * |
%s | מחרוזת | char * (מסתיימת ב-'\0') | char * (buffer) |
%p | כתובת של מצביע | void * | void ** |
%x / %X | הקסדצימלי בלי סימן | unsigned int | unsigned int * |
%o | אוקטלי בלי סימן | unsigned int | unsigned int * |
%n | שומר את מספר התווים שנכתבו עד כה | int * | int * |
%% | % מילולי | אין | מתאים ל-% מילולי |
%[...] | scanset (רק ב-scanf) | - | char * |
שתי רשומות ראויות לאזהרה. %n כותב לזיכרון במקום להדפיס, והוא המנגנון שמאחורי פגיעות ה-format string: לעולם אל תתנו לטקסט של משתמש להגיע למחרוזת פורמט. ו-%p דורש void *; העברה ישירה של int * היא לא מוגדרת, למרות שכל קומפיילר אמיתי מדפיס את הכתובת בכל מקרה.
מגדירי אורך
מגדיר האורך בא בין הדיוק לאות ההמרה, והוא אומר מה הגודל האמיתי של הארגומנט.
המרות עם סימן (d i) ובלי סימן (u x o) מקבלות טיפוסים שונים: %lu מצפה ל-unsigned long, לא ל-long, וטעות בזה היא התנהגות לא מוגדרת, לא פספוס קוסמטי.
| מגדיר | עם d i (printf / scanf) | עם u x o (printf / scanf) |
|---|---|---|
hh | int (מ-signed char) / signed char * | unsigned int (מ-unsigned char) / unsigned char * |
h | int (מ-short) / short * | unsigned int (מ-unsigned short) / unsigned short * |
| (אין) | int / int * | unsigned int / unsigned int * |
l | long / long * | unsigned long / unsigned long * |
ll | long long / long long * | unsigned long long / unsigned long long * |
z | טיפוס גודל עם סימן / מצביע אליו | size_t / size_t * |
j | intmax_t / intmax_t * | uintmax_t / uintmax_t * |
t | ptrdiff_t / ptrdiff_t * | המקביל בלי סימן של ptrdiff_t |
ועבור משפחות הנקודה הצפה והתווים:
| מגדיר | עם | טיפוס ב-printf | טיפוס ב-scanf |
|---|---|---|---|
| (אין) | f e g a | double | float * |
l | f e g a | double (מתקבל, בלי השפעה) | double * |
L | f e g a | long double | long double * |
l | c s | wint_t / wchar_t * | wchar_t * |
הצירופים שבאמת תקלידו:
את %zu כדאי לזכור בעל פה. sizeof, strlen וכל גודל בספרייה הסטנדרטית מחזירים size_t, שהוא 8 בתים במערכת של 64 ביט, בעוד ש-int הוא 4. printf("%d", strlen(s)) הוא באג אמיתי שנראה עובד בחלק מהפלטפורמות: השתמשו ב-%zu.
המגדירים hh ו-h קיימים בעיקר בשביל scanf, שבו הגודל של היעד חשוב. ב-printf, short מקודם ל-int לפני הקריאה, כך ש-%d היה מדפיס אותו נכון בכל מקרה; %hd רק מתעד את הכוונה.
טיפוסים ברוחב קבוע
עבור טיפוסים מ-<stdint.h> כמו int32_t, המגדירים הניידים הם מאקרו מ-<inttypes.h>:
PRId32 מתרחב לאותיות הנכונות עבור הפלטפורמה שלכם, ומחרוזות מילוליות צמודות מתחברות, ולכן הפורמט מפוצל לשלושה חלקים. זה מכוער; החלופה, המרה ל-long long ושימוש ב-%lld, לעתים קרובות קריאה יותר ותמיד נכונה.
דגלים
דגלים באים מיד אחרי ה-% ומשנים את התצוגה. הם חלים רק על printf.
| דגל | השפעה | דוגמה | פלט |
|---|---|---|---|
- | ריפוד אחרי הערך במקום לפניו | %-6d| על 42 | 42 | |
+ | תמיד להדפיס סימן עבור ערכים עם סימן | %+d על 42 | +42 |
| רווח | להדפיס רווח במקום שבו היה + | % d על 42 | 42 |
0 | ריפוד באפסים במקום ברווחים | %06d על 42 | 000042 |
# | צורה חלופית: 0x עבור %x, 0 עבור %o, שמירת הנקודה עבור %g | %#x על 255 | 0xff |
אפשר לשלב דגלים: %-+8.2f הוא רוחב מינימלי 8, שתי ספרות אחרי הנקודה, תמיד עם סימן, ריפוד אחרי המספר. - גובר על 0 אם כותבים את שניהם.
רוחב ודיוק
רוחב הוא גודל שדה מינימלי: הפלט מרופד כדי להגיע אליו, ואף פעם לא נחתך כדי להיכנס בו. דיוק (אחרי נקודה) אומר משהו שונה לכל משפחה:
| המרה | משמעות הדיוק |
|---|---|
%f %e | ספרות אחרי הנקודה העשרונית (ברירת מחדל 6) |
%g | סך הספרות המשמעותיות (ברירת מחדל 6) |
%s | מספר תווים מרבי להדפסה |
%d %i %u %x %o | מספר ספרות מינימלי, מרופד באפסים |
* במקום אחד המספרים לוקח אותו מארגומנט int, שנקרא לפני הערך עצמו. כך בונים טבלאות שרוחבי העמודות שלהן מחושבים בזמן ריצה.
ב-scanf לרוחב יש תפקיד אחר וחשוב הרבה יותר: הוא מגביל כמה קלט ההמרה תצרוך, וזה הדבר היחיד שעומד בין %s לבין buffer overflow.
איפה printf ו-scanf שונים
הם נראים דומים ולא מסכימים בארבעה מקומות. כל אחד מהם הוא מקור חי לבאגים:
| מצב | printf | scanf |
|---|---|---|
ערך double | %f (float מקודם ל-double) | %lf: %f פירושו float * |
| רוחב | גודל שדה מינימלי, מרפד | קלט מרבי שנצרך, חותך |
| ארגומנטים | ערכים | מצביעים: &x, או שם של מערך |
| רווח בפורמט | מדפיס רווח | מדלג על כל רצף של תווי רווח |
נסו 2.5 hello. הפורמט שקורא אותו והפורמט שמדפיס אותו משתמשים באותיות שונות עבור אותו double, וה-15 ב-%15s מגביל את הקלט ל-15 תווים ועוד תו הסיום, בדיוק הגודל של word.
ה-scanset של scanf
%[...] קורא קבוצה של תווים במקום מילה שמופרדת ברווחים. זו הדרך לקרוא שורה שלמה עם scanf, אם כי fgets נשאר הכלי הטוב יותר:
[^\n] פירושו "כל תו חוץ משורה חדשה". %[0-9] יקרא רק ספרות, %[abc] רק את שלוש האותיות האלה. בניגוד ל-%s, scanset לא מדלג על רווחים מובילים, כך ששורה חדשה תועה ב-buffer גורמת לו לא להתאים לכלום.
טעות היא התנהגות לא מוגדרת
printf לא יכול לראות את הטיפוס האמיתי של הארגומנט שלכם. הוא קורא כמה בתים שהמגדיר רומז, ומפרש אותם כמו שהמגדיר אומר. לכן אי התאמה היא לא תקלת עיצוב: זו קריאה של זיכרון כאילו היה משהו אחר:
printf("%d\n", 3.5); /* מדפיס זבל: קורא את הביטים של double כ-int */
printf("%s\n", 42); /* מתייחס ל-42 ככתובת: קריסה */
printf("%d\n", strlen(s)); /* size_t נקרא כ-int: שגוי ב-64 ביט */
printf("%f\n", 3); /* int נקרא כ-double: זבל */
ההגנה עולה דגל אחד. קמפלו עם -Wall ו-GCC יבדוק כל מחרוזת פורמט מילולית מול הארגומנטים שלה:
gcc -Wall -Wextra program.c -o program
הוא מדווח על כל אחת מארבע השורות שלמעלה בזמן קומפילציה. זו האזהרה בעלת הערך הגבוה ביותר ב-C, והיא מופעלת כברירת מחדל בעורך שלמעלה.
לצד המעשי של השימוש בהם, טבלאות מיושרות, הדפסה קריאה של מספרים עשרוניים וקריאת קלט בלי הבאג הקלאסי של השורה החדשה, ראו את העמודים על printf ו-scanf.
שאלות נפוצות
מה המשמעות של %d ב-C?
%d הוא מגדיר ההמרה עבור מספר שלם עשרוני עם סימן, כלומר int. ב-printf הוא מדפיס את הארגומנט כספרות בבסיס 10; ב-scanf הוא קורא ספרות בבסיס 10 לתוך int *. %i אומר אותו דבר ב-printf, אבל ב-scanf %i מקבל גם הקסדצימלי עם 0x ואוקטלי עם אפס מוביל.
מה ההבדל בין %f ל-%lf ב-C?
ב-printf אין הבדל מעשי: ארגומנטים מסוג float מקודמים ל-double, כך ש-%f מדפיס את שניהם, ו-C99 מקבלת את %lf כמילה נרדפת. ב-scanf ההבדל קריטי: %f כותב לתוך float * (4 בתים) ו-%lf לתוך double * (8 בתים). שימוש ב-%f עבור double משחית זיכרון.
איך מדפיסים size_t או long long ב-C?
%zu עבור size_t (מה ש-sizeof ו-strlen מחזירים) ו-%lld / %llu עבור long long / unsigned long long. שימוש ב-%d עבורם הוא התנהגות לא מוגדרת: במקרה זה עובד במערכות של 32 ביט ומתנהג לא נכון במערכות של 64 ביט, ולכן הבאג שורד כל כך הרבה זמן.
מה קורה אם משתמשים ב-format specifier הלא נכון ב-C?
זו התנהגות לא מוגדרת, לא שגיאת עיגול. printf קורא את הארגומנט כמו שהמגדיר טוען שהוא, ולכן printf("%d", 3.5) מדפיס זבל ו-printf("%s", 42) הולך אחרי המספר 42 כאילו היה כתובת ובדרך כלל קורס. קמפלו עם -Wall, שגורם ל-GCC לבדוק מחרוזות פורמט מול הארגומנטים.