scanf هي مرآة printf: الترويسة نفسها، وسلاسل تنسيق متشابهة المظهر، واتجاه معاكس. تقرأ نصًا من الإدخال القياسي، وتحوّله وفق التنسيق، وتخزّن النتائج عبر المؤشّرات التي تعطيها إياها.
وهي أيضًا الدالة التي تُنتج مبتدئين حائرين أكثر من أي دالة أخرى في C، لأن ثلاثة من سلوكياتها مفاجئة وواحدًا منها ثغرة أمنية. تغطّي هذه الصفحة الأربعة كلها.
الشكل الأساسي ولماذا &
اكتب عددًا واضغط Enter لتشغيل هذا. و&age ليست زينة. فـ C تمرّر كل وسيط بالقيمة، فلو كتبت scanf("%d", age) لاستقبلت الدالة نسخة من قيمة age الحالية (المهملة) ولما وجدت سبيلًا إلى المتغيّر نفسه. أما &age فتمرّر عنوان المتغيّر، الذي تكتب scanf عبره. انظر المؤشّرات للصورة الكاملة.
وحذف & أشيع خطأ في scanf على الإطلاق. ويلتقطه المترجم مع -Wall (format '%d' expects argument of type 'int *')؛ وبدون تفعيل التحذيرات يُترجَم ويفسد الذاكرة في عنوان عشوائي ما.
قراءة عدة قيم
يستطيع استدعاء واحد قراءة عدة قيم. والمسافات البيضاء في الإدخال - المسافات وعلامات الجدولة والأسطر الجديدة - تفصل بينها، وتتخطّى المحدّدات العددية أي قدر منها.
والمسافات الحرفية في "%d %d %d" زائدة فعليًا - فـ %d تتخطّى المسافات البيضاء الأولى سلفًا - لكنها تجعل التنسيق مقروءًا. أما ما ليس زائدًا فهو فحص القيمة المُرجَعة، المغطّى أدناه.
ويجب أن تطابق المحارف غير البيضاء في التنسيق الإدخالَ تمامًا، وهذا كيف تقرأ إدخالًا مهيكلًا:
تطالب : في التنسيق بـ : في الإدخال. اكتب 9:30 فيُحلَّل؛ واكتب 9 30 فيفشل بعد قراءة 9.
افحص القيمة المُرجَعة دائمًا
تُرجِع scanf عدد العناصر المُسنَدة بنجاح - لا عدد المحارف ولا راية نجاح. وتُرجِع EOF إن انتهى الإدخال قبل تحويل أي شيء.
اكتب hello عند المحثّ. تُرجِع scanf القيمة 0، وتُترك n دون مساس - ما تزال تحمل ما بدأت به من قمامة - ويبقى النصّ المخالف جالسًا في تيار الإدخال. وذلك الجزء الأخير هو ما يحوّل إدخالًا سيّئًا واحدًا إلى حلقة لا نهائية:
/* BUG: spins forever on non-numeric input */
while (scanf("%d", &n) != 1) {
printf("Try again: ");
}
لا تستهلك scanf النصّ الذي عجزت عن تحويله إطلاقًا، فيفشل الاستدعاء التالي على المحارف نفسها إلى الأبد. وللتعافي عليك إهمال بقية السطر بنفسك:
وفحص EOF مهمّ: فحين ينتهي تيار الإدخال (يضغط المستخدم Ctrl+D، أو ينفد ملفّ مُمرَّر)، تُرجع scanf القيمة EOF دائمًا بعد ذلك، فحلقة تفحص != 1 وحدها تعيد المحاولة إلى ما لا نهاية. فعامِل «إدخال غير صالح» و«لا مزيد من الإدخال» بوصفهما حالتين مختلفتين. وبوجود ذلك، تستحق الدالة المساعِدة discard_line الاحتفاظ بها في أي برنامج يستخدم scanf على إدخال بشري.
فيضان المخزن في %s
تقرأ %s محارف في مصفوفة char. ولاحظ غياب & - فاسم المصفوفة يتحوّل سلفًا إلى مؤشّر إلى عنصرها الأول، فستكون &name من النوع الخاطئ.
والمشكلة أن %s المجرّدة لا فكرة لديها عن حجم مصفوفتك:
char name[10];
scanf("%s", name); /* DANGEROUS: types 40 characters, writes 41 bytes */
لا شيء يوقف الكتابة عند نهاية المصفوفة. فهي تمضي إلى ما يليها في الذاكرة - متغيّرات أخرى، أو عنوان العودة - وهذا هو فيضان المخزن الكلاسيكي، وتاريخيًا أكثر أصناف الأخطاء استغلالًا في C. والحلّ عرض بين % وs، أقلّ بواحد من حجم المصفوفة لترك مساحة للمنهي '\0':
ويجب كتابة العرض ثابتًا داخل سلسلة التنسيق، ما يجعل إبقاءه متزامنًا مع حجم المصفوفة مزعجًا - وهذا سبب إضافي لاستخدام معظم شيفرة C الإنتاجية fgets بدلًا منها.
والمفاجأة الثانية في %s: أنها تتوقّف عند أول مسافة بيضاء. اكتب Ada Lovelace فتحمل name القيمة Ada؛ وتنتظر Lovelace في التيار للقراءة التالية. فـ %s تقرأ كلمة لا سطرًا.
خطأ السطر الجديد المتبقّي
هذه هي حالة "scanf لا تعمل" التي يصطدم بها الجميع. فالمحدّدات العددية تتخطّى المسافات البيضاء الأولى؛ و%c لا تفعل.
وإليك ما يحدث بدون تلك المسافة. تكتب 30 وتضغط Enter. فتستهلك %d الرقمين 3 و0 وتتوقّف عند السطر الجديد، تاركةً '\n' في المخزن. ثم يقرأ الاستدعاء التالي scanf("%c", &initial) ذلك السطر الجديد كالمحرف ويعود فورًا - فيظهر المحثّ ويُتخطّى في اللحظة نفسها.
والمسافة في سلسلة تنسيق scanf تعني "تخطَّ أي قدر من المسافات البيضاء هنا"، فـ " %c" تخطو فوق السطر الجديد الشارد وتنتظر محرفًا حقيقيًا. والمسافة لازمة فقط قبل %c و%[؛ وكل محدّد آخر يتخطّى المسافات البيضاء بنفسه.
قراءة الأعداد العشرية والمحارف
المحدّدات مشتركة في معظمها مع printf، مع فرق مهمّ واحد: تحتاج scanf إلى معرفة حجم الوجهة بالضبط، لأنها تكتب عبر مؤشّر.
جرّب 1.5 2.25 7 x.
تقرأ %f قيمة float، وتقرأ %lf قيمة double. وفي printf يُطبَع الاثنان بالطريقة نفسها لأن قيم float تُرقّى إلى double قبل الاستدعاء - لكن في scanf لا ترقية، واستخدام %f مع double * يكتب أربعة بايتات في متغيّر من ثمانية بايتات، فيتركه فاسدًا. و%c هنا لا تحتاج مسافة أولى لأن %d التي قبلها توقّفت سلفًا عند مسافة لا عند سطر جديد. (الجدول الكامل في محدّدات التنسيق.)
fgets: البديل المتين
لأي شيء يكتبه شخص، اقرأ السطر كاملًا وحلّله بعد ذلك. تأخذ fgets حجم المخزن كوسيط، فلا يمكنها الفيضان فيزيائيًا، وحين يتّسع السطر تستهلك السطر الجديد أيضًا، فلا يبقى شيء يربك القراءة التالية. (وسطر أطول من المخزن يترك بقيّته غير مقروءة - وغياب '\n' فيما حصلت عليه هو كيف تكتشف ذلك.)
ثلاث نقاط عن ذلك النمط:
sizeof lineتمرّر الحجم الحقيقي، فتغيير حجم المصفوفة لا يحتاج أي تعديل آخر - بخلاف الثابت%19s.- تحتفظ
fgetsبالسطر الجديد إن اتّسع السطر، ولهذا يوجد اقتطاعstrcspn. وهذا أيضًا كيف تكتشف سطرًا أطول من المخزن: لا'\n'فيما حصلت عليه. - حلّل بعد ذلك.
strtolللأعداد الصحيحة (وتستطيع أيضًا إخبارك بموضع توقّف التحليل)، وstrtodللفاصلة العائمة، أوsscanf(line, "%d %d", &a, &b)لاستخدام تنسيق scanf على سلسلة تملكها سلفًا بأمان.
وsscanf تستحق ذكرًا خاصًا: فهي scanf تقرأ من سلسلة نصية بدل تيار الإدخال، وتتكامل تمامًا مع fgets. فتحصل على تحليل scanf المريح بلا أي من مشكلات تيارها.
أخطاء شائعة
- نسيان
&على وسيط ليس مصفوفة. ترجم بـ-Wall. %sبلا عرض. فيضان مخزن ينتظر إدخالًا طويلًا.- تجاهل القيمة المُرجَعة. ثم قراءة متغيّر لم تُسنِده
scanfقط. - التكرار على
scanfفاشلة دون إهمال الإدخال. حلقة لا نهائية. %cبلا مسافة أولى بعد قراءة عدد. خطأ المحثّ المتخطّى.- استخدام
%fلأجلdouble. إفساد صامت؛ فـscanfتحتاج%lf. - توقّع أن تقرأ
%sسطرًا. إنها تقرأ كلمة واحدة محدّدة بمسافة بيضاء.
الأسئلة الشائعة
لماذا تحتاج scanf إلى & في لغة C؟
لأن C تمرّر الوسائط بالقيمة. وscanf يجب أن تغيّر متغيّرك، فتحتاج إلى عنوان المتغيّر لا نسخة من قيمته: scanf("%d", &age). والاستثناء الوحيد مصفوفة المحارف، التي اسمها عنوان أصلًا - فـ scanf("%19s", name) لا تأخذ &.
لماذا تتخطّى scanf إدخالي لمحرف أو سلسلة نصية؟
لأن استدعاء scanf("%d", ...) سابقًا ترك السطر الجديد الناتج عن مفتاح Enter في مخزن الإدخال. فـ %d و%f تتخطّيان المسافات البيضاء الأولى، لكن %c لا تفعل، فتقرأ ذلك السطر الجديد بدلًا من الانتظار. والحلّ مسافة في التنسيق - scanf(" %c", &ch) - تخبر scanf بتخطّي المسافات البيضاء أولًا.
كيف أقرأ سلسلة نصية بـ scanf بأمان في لغة C؟
امنح %s عرضًا أقصى أقلّ بواحد من حجم مخزنك: char name[20]; scanf("%19s", name);. فبدون العرض تكتب %s بقدر ما يكتب المستخدم من محارف وتتجاوز نهاية المصفوفة بسرور. وتذكّر أيضًا أن %s تتوقّف عند أول مسافة بيضاء، فلا تستطيع قراءة اسم كامل فيه مسافة.
هل أستخدم scanf أم fgets في لغة C؟
استخدم fgets لأي شيء يكتبه إنسان. فهي تقرأ سطرًا كاملًا في مخزن محدّد الحجم، ولا يمكن أن تفيض، وتستهلك السطر الجديد أيضًا حين يتّسع السطر، فتزول أخطاء الإدخال المختلط الكلاسيكية. وحلّل السطر بعد ذلك بـ sscanf أو strtol. وscanf مقبولة للتمارين الصغيرة وللإدخال الآلي حسن التكوين.