Menu

printf ב-C: מחרוזות פורמט, specifiers והדפסת מספרים עשרוניים

איך printf באמת עובדת ב-C: המודל של מחרוזת הפורמט, ה-specifiers שמשתמשים בהם כל יום, רוחב ודיוק לפלט מיושר, הדפסה הגיונית של float, הערך המוחזר, ולמה printf(user_input) היא פרצת אבטחה.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

printf היא הפונקציה הראשונה שכל מתכנת C לומד, וזו שרובם אף פעם לא קראו את התיעוד שלה עד הסוף. שווה להקדיש לה עשר דקות, כי מחרוזת הפורמט שלה היא שפה קטנה בפני עצמה, וכי specifier שגוי הוא לא סתם שגיאת הקלדה, הוא undefined behavior.

היא נמצאת ב-<stdio.h>, ולכן כל תוכנית שמשתמשת בה מתחילה ב-#include <stdio.h>.

המודל של מחרוזת הפורמט

printf עוברת על מחרוזת הפורמט תו אחר תו. כל מה שאינו % מודפס בדיוק כפי שנכתב. % פותח conversion specification, שצורך את הארגומנט הבא ומדפיס אותו בצורה המבוקשת.

הארגומנטים מותאמים ל-specifiers אך ורק לפי המיקום: ה-% הראשון לוקח את count, השני לוקח את name, וכן הלאה. ל-printf אין שום דרך לבדוק שהעברתם את מה שהבטחתם, כי היא לא רואה את הטיפוסים, ולכן מחרוזת הפורמט היא חוזה שאתם חייבים לכבד. בגלל זה כדאי להפעיל את -Wall: GCC קורא את מחרוזת הפורמט הליטרלית בזמן הקומפילציה ומזהיר כשארגומנט לא מתאים.

שום דבר לא מכריח ירידת שורה. printf("Hello") משאירה את הסמן אחרי ה-o, ולכן כמעט כל מחרוזת פורמט מסתיימת ב-\n.

ה-specifiers שמשתמשים בהם כל יום

חמישה מכסים את רוב הקוד:

Specifierמדפיסקריאה לדוגמהפלט
%dint עם סימןprintf("%d", -42)-42
%fdoubleprintf("%f", 2.5)2.500000
%sמחרוזת (char *)printf("%s", "hi")hi
%cתו בודדprintf("%c", 'A')A
%pכתובת של מצביעprintf("%p", (void*)&x)0x7ffd...

בקטע הזה מסתתרים שני כללים. % ליטרלי כותבים כ-%%: % בודד בסוף מחרוזת פורמט הוא undefined behavior. ו-%p דורש void *, לכן בצעו cast למצביע. העברה ישירה של int * היא מבחינה טכנית undefined, גם אם בפועל היא מודפסת בסדר.

הטבלה המלאה, עם כל specifier, כל length modifier ועמודת ה-scanf, נמצאת בעמוד format specifiers. העמוד הזה נשאר עם אלה שבאמת משתמשים בהם.

רוחב, דיוק ויישור

בין ה-% לבין האות אפשר לשים דגלים, רוחב ודיוק. כך הופכים לולאה לטבלה.

הרוחב הוא מינימום, אף פעם לא מקסימום: %3d עם הערך 123456 מדפיס את כל שש הספרות. לדיוק יש משמעות שונה לכל טיפוס: ספרות אחרי הנקודה העשרונית עבור %f, מספר תווים מקסימלי עבור %s, ומספר ספרות מינימלי עבור %d.

את שניהם אפשר לספק בזמן ריצה עם *, שקורא ארגומנט מסוג int:

שינוי של width מסדר מחדש את כל הטבלה. זו הדרך הסטנדרטית להדפיס עמודות מיושרות ב-C, בלי צורך בפונקציית עזר לריפוד מחרוזות.

הדפסה הגיונית של מספרים עשרוניים

%f בלי דיוק תמיד מדפיס שש ספרות אחרי הנקודה, ולעיתים רחוקות זה מה שמישהו רוצה:

שלוש אפשרויות, וכלל אצבע לכל אחת:

  • %.Nf לכסף, למדידות ולכל דבר עם מספר טבעי של ספרות עשרוניות. %.2f הוא מה שתרצו עבור מטבע.
  • %e לכתיב מדעי, תמיד בצורה d.dddddde±dd.
  • %g כשלא יודעים מראש את סדר הגודל. הוא בוחר בין %f ל-%e, לפי מה שקצר יותר, ומוריד אפסים בסוף. זו ברירת המחדל השפויה ביותר לרישום ערך שעוד לא ראיתם.

מלכודת אחת שכדאי לומר במפורש: אין %lf ב-printf. ארגומנטים מסוג float מקודמים ל-double עוד לפני ש-printf רואה אותם, ולכן %f מטפל בשניהם, ו-%lf בעל משמעות רק ב-scanf, שם הגודל של היעד חשוב. (C99 אכן מקבל %lf ב-printf כמילה נרדפת, אבל ההרגל לכתוב אותו גורם לאנשים לכתוב %f ב-scanf, ושם זה באמת נשבר.)

מספרים שלמים ברוחבים אחרים

%d מיועד ל-int. טיפוסים גדולים יותר או unsigned צריכים length modifier, ואי התאמה ביניהם היא undefined behavior ולא שגיאת עיגול:

%zu עבור size_t הוא זה שאנשים טועים בו הכי הרבה: sizeof מחזיר size_t, לא int, ולכן printf("%d", sizeof x) הוא באג שבמקרה עובד במערכות 32 ביט ומתנהג לא נכון במערכות 64 ביט.

הערך המוחזר

printf מחזירה את מספר התווים שכתבה, או מספר שלילי במקרה של כישלון. בדרך כלל מתעלמים ממנו, ולרוב זה בסדר, אבל הוא באמת שימושי כשצריך לדעת כמה רחב היה הפלט:

הפונקציות הקרובות sprintf ו-snprintf כותבות לתוך buffer במקום למסך, ומחזירות את האורך שהפיקו. תמיד העדיפו את snprintf, שמקבלת את גודל ה-buffer ולא יכולה לגלוש ממנו:

snprintf מחזירה את האורך שהמחרוזת המלאה הייתה צריכה, ולכן ערך מוחזר ששווה לגודל ה-buffer או גדול ממנו מראה שהפלט נקטע.

פגיעות מחרוזת הפורמט

זו בעיית האבטחה היחידה ב-printf, והיא חמורה. לעולם אל תעבירו כמחרוזת הפורמט נתונים שלא אתם כתבתם:

/* מסוכן: לעולם אל תעשו את זה */
printf(user_input);

/* נכון */
printf("%s", user_input);

אם user_input מכיל %s, הפונקציה printf קוראת ארגומנט שמעולם לא הועבר והולכת אחרי כל זבל שנראה כמו מצביע שהיא מוצאת, ובדרך כלל קורסת. %x שחוזר על עצמו שופך את תוכן ה-stack למסך, ושם עלולים להיות סיסמאות או מפתחות. ו-%n, שכותב את מספר התווים לתוך ארגומנט, הפך את זה היסטורית להשחתת זיכרון שרירותית ולהרצת קוד מרחוק.

הכלל מוחלט ולא עולה כלום: מחרוזת הפורמט היא תמיד ליטרל שאתם כתבתם. כל דבר משתנה נכנס כארגומנט. -Wformat-security של GCC מסמן את הטעות, והוא כלול ב--Wall -Wextra.

Buffering: למה פלט מופיע לפעמים באיחור

printf לא כותבת לטרמינל מיד. הפלט עובר buffering: הוא נשפך כשה-buffer מתמלא, כשמופיעה ירידת שורה בטרמינל, או כשהתוכנית מסתיימת כרגיל. שתי השלכות:

  • תוכנית שקורסת עלולה לאבד פלט ש"כבר הודפס". אם אתם מדבגים עם printf וההודעה האחרונה לא מופיעה אף פעם, הקריסה קרתה אחרי השורה הזו, לא לפניה. הוסיפו fflush(stdout) או שלחו את פלט הדיבוג ל-stderr, שאין לו buffer.
  • fprintf(stderr, ...) היא הדרך הנכונה להדפיס שגיאות. היא בלי buffer, והיא הולכת ל-stream נפרד שמשתמשים יכולים להפנות בנפרד מהפלט הרגיל.

שאלות נפוצות

איך printf עובדת ב-C?

printf מקבלת מחרוזת פורמט וסורקת אותה תו אחר תו. תווים רגילים מודפסים כמו שהם, וכל % פותח specification של המרה שצורכת את אחד הארגומנטים הבאים ומדפיסה אותו בצורה המבוקשת. printf("%d apples\n", 5) מדפיסה 5 apples וירידת שורה.

איך מדפיסים float עם 2 ספרות אחרי הנקודה ב-C?

משתמשים בדיוק (precision): printf("%.2f\n", 3.14159) מדפיסה 3.14. המספר שאחרי הנקודה קובע כמה ספרות יופיעו אחרי הנקודה העשרונית. %f בלי דיוק תמיד מדפיס 6 ספרות, ולכן float בלי עיצוב נראה כל כך רועש.

למה printf מדפיסה זבל או קורסת?

כמעט תמיד בגלל אי התאמה בין ה-specifier לארגומנט. printf("%d", 3.5) או printf("%s", 42) הם undefined behavior: printf לא רואה את הטיפוסים האמיתיים, היא סומכת על מחרוזת הפורמט. קמפלו עם -Wall, וכך GCC יבדוק את הפורמט מול הארגומנטים.

למה printf(user_input) מסוכן?

אם הטקסט של המשתמש מכיל רצפי %, הפונקציה printf תציית להם ותקרא ארגומנטים שמעולם לא הועברו: היא תדליף זיכרון מה-stack, ועם %n אפילו תכתוב לזיכרון. זו פגיעות מחרוזת הפורמט הקלאסית. תמיד כתבו במקום זה printf("%s", user_input).

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל