Menu

scanf ב-C: קליטת קלט, כלל ה-& ולמה זה מפסיק לעבוד

איך קוראים קלט עם scanf ב-C: למה ה-& חובה, איך מטופלים רווחים, גלישת ה-buffer של %s והתיקון עם רוחב, בדיקת הערך המוחזר, באג ירידת השורה שנשארת, ו-fgets כחלופה החסינה.

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

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

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

הצורה הבסיסית, ולמה &

כדי להריץ את זה, הקלידו מספר ולחצו Enter. ה-&age הוא לא קישוט. C מעבירה כל ארגומנט by value, כך שאם הייתם כותבים scanf("%d", age) הפונקציה הייתה מקבלת עותק של הערך הנוכחי (הזבל) של age, בלי שום דרך להגיע למשתנה עצמו. &age מעביר את הכתובת של המשתנה, ו-scanf כותבת דרכה. בעמוד המצביעים תמצאו את התמונה המלאה.

השמטת ה-& היא הטעות הנפוצה ביותר עם scanf. הקומפיילר עם -Wall תופס אותה (format '%d' expects argument of type 'int *'), ובלי אזהרות מופעלות הקוד מתקמפל ומשחית זיכרון בכתובת אקראית כלשהי.

קריאה של כמה ערכים

קריאה אחת יכולה לקרוא כמה ערכים. רווחים לבנים בקלט, כלומר רווחים, טאבים וירידות שורה, מפרידים ביניהם, וה-specifiers המספריים מדלגים על כל כמות מהם.

הרווחים הליטרליים ב-"%d %d %d" בעצם מיותרים, כי %d כבר מדלג על רווחים בהתחלה, אבל הם הופכים את הפורמט לקריא. מה שלא מיותר הוא בדיקת הערך המוחזר, שמוסברת בהמשך.

תווים שאינם רווחים בפורמט חייבים להתאים לקלט בדיוק, וכך קוראים קלט מובנה:

ה-: בפורמט דורש : בקלט. הקלידו 9:30 והניתוח מצליח, הקלידו 9 30 והוא נכשל אחרי קריאת ה-9.

תמיד בדקו את הערך המוחזר

scanf מחזירה את מספר הפריטים שהושמו בהצלחה: לא את מספר התווים, ולא דגל הצלחה. היא מחזירה EOF אם הקלט נגמר לפני שמשהו הומר.

הקלידו hello בהנחיה. scanf מחזירה 0, n נשאר ללא שינוי, עדיין עם הזבל שהיה בו מההתחלה, והטקסט הבעייתי עדיין יושב ב-stream הקלט. החלק האחרון הזה הוא מה שהופך קלט שגוי אחד ללולאה אינסופית:

/* באג: מסתובבת לנצח על קלט שאינו מספרי */
while (scanf("%d", &n) != 1) {
    printf("Try again: ");
}

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

בדיקת ה-EOF חשובה: כש-stream הקלט נגמר (המשתמש לוחץ Ctrl+D, או שקובץ שהועבר בצינור נגמר), scanf מחזירה EOF מאז ולתמיד, כך שלולאה שבודקת רק != 1 מנסה שוב לנצח. טפלו ב"קלט שגוי" וב"אין יותר קלט" כשני המקרים השונים שהם. כשזה במקום, כדאי לשמור את פונקציית העזר discard_line בכל תוכנית שמשתמשת ב-scanf על קלט של בני אדם.

גלישת ה-buffer של %s

%s קורא תווים לתוך מערך של char. שימו לב שאין &: שם של מערך כבר מתדרדר למצביע לאיבר הראשון שלו, ולכן &name היה מהטיפוס הלא נכון.

הבעיה היא ש-%s רגיל לא יודע מה גודל המערך שלכם:

char name[10];
scanf("%s", name);      /* מסוכן: מקלידים 40 תווים, נכתבים 41 בתים */

שום דבר לא עוצר את הכתיבה בסוף המערך. היא ממשיכה לכל מה שנמצא אחריו בזיכרון, משתנים אחרים, כתובת החזרה, וזו גלישת ה-buffer הקלאסית, ומבחינה היסטורית סוג הבאגים המנוצל ביותר ב-C. הפתרון הוא רוחב בין ה-% ל-s, קטן באחד מגודל המערך כדי להשאיר מקום ל-'\0' שמסיים את המחרוזת:

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

ההפתעה השנייה של %s: הוא נעצר ברווח הראשון. הקלידו Ada Lovelace, ו-name יכיל Ada, בזמן ש-Lovelace מחכה ב-stream לקריאה הבאה. %s קורא מילה, לא שורה.

באג ירידת השורה שנשארת

זה ה-"scanf not working" שכולם נתקלים בו. specifiers מספריים מדלגים על רווחים בהתחלה, %c לא.

הנה מה שקורה בלי הרווח הזה. אתם מקלידים 30 ולוחצים Enter. %d צורך את ה-3 ואת ה-0 ונעצר בירידת השורה, ומשאיר '\n' ב-buffer. הקריאה הבאה, scanf("%c", &initial), קוראת את ירידת השורה הזו כתו וחוזרת מיד: ההנחיה מופיעה ומדולגת באותה נשימה.

רווח במחרוזת הפורמט של scanf פירושו "דלג כאן על כל כמות של רווחים לבנים", ולכן " %c" עובר מעל ירידת השורה התועה ומחכה לתו אמיתי. הרווח נחוץ רק לפני %c ו-%[, וכל specifier אחר מדלג על רווחים לבד.

קריאת float ו-char

ה-specifiers ברובם משותפים עם printf, עם הבדל חשוב אחד: scanf צריכה לדעת את הגודל המדויק של היעד, כי היא כותבת דרך מצביע.

נסו 1.5 2.25 7 x.

%f קורא float, ו-%lf קורא double. ב-printf שניהם מודפסים באותה דרך, כי float מקודם ל-double לפני הקריאה, אבל ב-scanf אין קידום, ושימוש ב-%f עם double * כותב ארבעה בתים לתוך משתנה של שמונה בתים ומשאיר אותו פגום. ה-%c כאן לא צריך רווח לפניו, כי ה-%d שלפניו כבר נעצר ברווח ולא בירידת שורה. (הטבלה המלאה בעמוד format specifiers.)

fgets: החלופה החסינה

לכל דבר שאדם מקליד, קראו את השורה כולה ונתחו אותה אחר כך. fgets מקבלת את גודל ה-buffer כארגומנט, ולכן היא פיזית לא יכולה לגלוש, וכשהשורה נכנסת היא צורכת גם את ירידת השורה, כך ששום דבר לא נשאר מאחור כדי לבלבל את הקריאה הבאה. (שורה ארוכה מה-buffer משאירה את הזנב שלה לא נקרא: ה-'\n' החסר במה שקיבלתם הוא הדרך לזהות את זה.)

שלוש נקודות לגבי הדפוס הזה:

  • sizeof line מעביר את הגודל האמיתי, כך ששינוי גודל המערך לא דורש שום עריכה נוספת, בניגוד לליטרל %19s.
  • fgets שומרת את ירידת השורה אם השורה נכנסה, ובגלל זה יש את החיתוך עם strcspn. כך גם מזהים שורה ארוכה מדי ל-buffer: אין '\n' במה שקיבלתם.
  • נתחו אחר כך. strtol למספרים שלמים (היא גם יכולה לספר לכם איפה הניתוח נעצר), strtod למספרים עשרוניים, או sscanf(line, "%d %d", &a, &b) כדי להשתמש בפורמט של scanf על מחרוזת שכבר מחזיקים בצורה בטוחה.

sscanf ראויה לאזכור משלה: זו scanf שקוראת ממחרוזת במקום מ-stream הקלט, והיא משתלבת מושלם עם fgets. מקבלים את הניתוח הנוח של scanf בלי אף אחת מבעיות ה-stream שלה.

טעויות נפוצות

  • שכחת & בארגומנט שאינו מערך. קמפלו עם -Wall.
  • %s בלי רוחב. גלישת buffer שמחכה לקלט ארוך.
  • התעלמות מהערך המוחזר. ואז קריאה של משתנה ש-scanf מעולם לא השימה בו ערך.
  • לולאה על scanf שנכשלה בלי לזרוק את הקלט. לולאה אינסופית.
  • %c בלי רווח לפניו אחרי קריאת מספר. באג ההנחיה שמדולגת.
  • שימוש ב-%f עבור double. השחתה שקטה, scanf צריכה %lf.
  • לצפות ש-%s יקרא שורה. הוא קורא מילה אחת שמופרדת ברווחים.

שאלות נפוצות

למה scanf צריכה & ב-C?

כי C מעבירה ארגומנטים by value. scanf חייבת לשנות את המשתנה שלכם, ולכן היא צריכה את הכתובת שלו, לא עותק של הערך: scanf("%d", &age). החריג היחיד הוא מערך של תווים, שהשם שלו כבר כתובת: scanf("%19s", name) בלי &.

למה scanf מדלגת על הקלט שלי עבור char או מחרוזת?

קריאה קודמת של scanf("%d", ...) השאירה את ירידת השורה של מקש Enter ב-buffer הקלט. %d ו-%f מדלגים על רווחים בהתחלה, אבל %c לא, ולכן הוא קורא את ירידת השורה הזו במקום לחכות. הפתרון הוא רווח בפורמט, scanf(" %c", &ch), שאומר ל-scanf לדלג קודם על רווחים.

איך קוראים מחרוזת עם scanf בצורה בטוחה ב-C?

תנו ל-%s רוחב מקסימלי שקטן באחד מה-buffer שלכם: char name[20]; scanf("%19s", name);. בלי הרוחב, %s כותב כמה תווים שהמשתמש מקליד וממשיך בשמחה אחרי סוף המערך. זכרו גם ש-%s נעצר ברווח הראשון, ולכן הוא לא יכול לקרוא שם מלא שיש בו רווח.

האם להשתמש ב-scanf או ב-fgets ב-C?

השתמשו ב-fgets לכל דבר שאדם מקליד. היא קוראת שורה שלמה לתוך buffer עם גודל ידוע, לא יכולה לגלוש, ו(כשהשורה נכנסת) צורכת גם את ירידת השורה, כך שהבאגים הקלאסיים של קלט מעורב נעלמים. נתחו את השורה אחר כך עם sscanf או strtol. scanf בסדר לתרגילים קטנים ולקלט מכונה מסודר.

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

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

להתחיל