سؤال

أريد أن أعرف عيوب scanf().

في العديد من المواقع، قرأت ذلك باستخدام scanf قد تسبب الفيضان المخزن المؤقت. ما هو سبب هذا؟ هل هناك أي عيوب أخرى مع scanf?

هل كانت مفيدة؟

المحلول

مشاكل SCANF (بحد أدنى):

  • استخدام %s للحصول على سلسلة من المستخدم، مما يؤدي إلى احتمال أن تكون السلسلة أطول من المخزن المؤقت الخاص بك، مما تسبب في تجاوز الفائض.
  • إمكانية فحص فشل ترك مؤشر الملف الخاص بك في موقع غير محدد.

أنا جيد جدا باستخدام fgets لقراءة الخطوط الكاملة حتى تتمكن من الحد من مقدار قراءة البيانات. إذا كنت قد حصلت على المخزن المؤقت 1K، وقرأت خطا فيه fgets يمكنك معرفة ما إذا كان السطر طويلا جدا بحقيقة أنه لا يوجد حرف جديد إنهاء (سطر أخير من الملف دون أي خط جديد على الرغم من).

ثم يمكنك تقديم شكوى إلى المستخدم، أو تخصيص مساحة أكبر لبقية السطر (بشكل مستمر إذا لزم الأمر حتى يكون لديك مساحة كافية). في كلتا الحالتين، ليس هناك خطر من تجاوز الفائض المخزن المؤقت.

بمجرد قراءة الخط في، أنت أعرف أنك وضعت في السطر التالي حتى لا توجد مشكلة هناك. يمكنك ذلك بعد ذلك sscanf سلسلة الخاص بك إلى محتوى قلبك دون الحاجة إلى حفظ واستعادة مؤشر الملفات لإعادة القراءة.

إليك مقتطف من التعليمات البرمجية التي كثيرا ما تستخدمها لضمان عدم وجود فائض مؤقت عند مطالبة المستخدم بالمعلومات.

يمكن ضبطه بسهولة لاستخدام ملف آخر غير المدخلات القياسية إذا لزم الأمر ويمكنك أيضا أن تخصيص المخزن المؤقت الخاص به (والاستمرار في زيادة ذلك حتى يكون كبيرا بما فيه الكفاية) قبل إعطاء ذلك مرة أخرى إلى المتصل (على الرغم من أن المتصل سيكون مسؤولا لتحريرها، بالطبع).

#include <stdio.h>
#include <string.h>

#define OK         0
#define NO_INPUT   1
#define TOO_LONG   2
#define SMALL_BUFF 3
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Size zero or one cannot store enough, so don't even
    // try - we need space for at least newline and terminator.
    if (sz < 2)
        return SMALL_BUFF;

    // Output prompt.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }

    // Get line with buffer overrun protection.
    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.
    size_t lastPos = strlen(buff) - 1;
    if (buff[lastPos] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[lastPos] = '\0';
    return OK;
}

واختبار سائق لذلك:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        // Extra NL since my system doesn't output that on EOF.
        printf ("\nNo input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long [%s]\n", buff);
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

أخيرا، يتم تشغيل اختبار لإظهاره في العمل:

$ ./tstprg
Enter string>[CTRL-D]
No input

$ ./tstprg
Enter string> a
OK [a]

$ ./tstprg
Enter string> hello
OK [hello]

$ ./tstprg
Enter string> hello there
Input too long [hello the]

$ ./tstprg
Enter string> i am pax
OK [i am pax]

نصائح أخرى

يبدو أن معظم الإجابات حتى الآن التركيز على مشكلة تجاوز سلسلة المخزن المؤقت. في الواقع، محطات التنسيق التي يمكن استخدامها مع scanf وظائف تدعم صريح عرض الحقل الإعداد، الذي يحد من الحد الأقصى لحجم الإدخال ومنع تجاوز الفضاء المخزن المؤقت. هذا يجعل الاتهامات الشعبية من مخاطر تجاوز سلسلة العازلة الموجودة في scanf لا أساس له من الصحة تقريبا. مدعيا ذلك scanf مماثلة بطريقة أو بأخرى ل gets في الاحترام غير صحيح تماما. هناك فرق نوعي كبير بين scanf و gets: scanf هل تزويد المستخدم بميزات تمنع String-Buffer-Overflow، في حين gets لا.

يمكن للمرء أن يجادل هذه scanf من الصعب استخدام الميزات، نظرا لأن عرض الحقل يجب دمجه في سلسلة التنسيق (لا توجد وسيلة لتمريرها من خلال حجة المتغيرات، كما يمكن القيام به في printf). هذا صحيح في الواقع. scanf هو بالفعل سيئة إلى حد ما في هذا الصدد. ولكن مع ذلك أي مطالبات ذلك scanf هو بطريقة أو بأخرى مكسورة بشكل يائس فيما يتعلق بالسلامة الفائقة من سلسلة المخاطر العازلة هي وهمية تماما وعادة ما يصنعها المبرمجين كسول.

المشكلة الحقيقية مع scanf لديه طبيعة مختلفة تماما، على الرغم من أنها أيضا حول تجاوزت. وبعد متي scanf يتم استخدام الدالة لتحويل التمثيلات العشرية للأرقام إلى قيم أنواع حسابية، لا توفر أي حماية من تجاوزات الحساب. إذا حدث تجاوز، scanf تنتج سلوك غير محدد. لهذا السبب، فإن الطريقة الصحيحة الوحيدة لأداء التحويل في مكتبة C القياسية هي الوظائف من strto... الأسرة.

لذلك، لتلخيص ما سبق، والمشكلة مع scanf هو أنه من الصعب (وإن كان ذلك ممكنا) لاستخدام بشكل صحيح وأمان مع المخازن المؤقتة السلسلة. ومن المستحيل استخدامه بأمان للمدخلات الحسابية. هذه الأخيرة هي المشكلة الحقيقية. السابق هو مجرد إزعاج.

ملاحظة ما ورد أعلاه في المقصود أن تكون حول عائلة بأكملها scanf وظائف (بما في ذلك أيضا fscanf و sscanf). مع scanf على وجه التحديد، المشكلة الواضحة هي أن فكرة استخدام وظيفة منسقة بدقة للقراءة المحتملة تفاعلي المدخلات مشكوك فيها إلى حد ما.

من comp.lang.c التعليمات: لماذا يقول الجميع عدم استخدام SCANF؟ ماذا يجب أن تستخدم بدلا من ذلك؟

scanf لديه عدد من المشاكل - انظر الأسئلة 12.17, 12.18 أ, ، و 12.19. وبعد أيضا، لها %s التنسيق لديه نفس المشكلة التي gets() لديه (انظر السؤال 12.23) - من الصعب ضمان أن يكون المخزن المؤقت الاستقبال لن يتفوق. هامش

بشكل عام، scanf تم تصميمه لإدخال منظم نسبيا ومنسق (اسمه في الواقع مشتق من "المسح المنسق"). إذا كنت تولي اهتماما، فسيخبرك ما إذا كان قد نجح أو فشل، لكن يمكن أن يخبرك فقط أين فشلت، وليس على الإطلاق كيف أو لماذا. لديك فرصة ضئيلة للغاية للقيام بأي استعادة خطأ.

بعد إدخال المستخدم التفاعلي هو الإدخال الأقل هيكلة هناك. تسمح واجهة مستخدم مصممة جيدا بإمكانية كتابة المستخدم فقط حول أي شيء - لا مجرد رسائل أو علامات الترقيم عندما كان من المتوقع حدوث الأرقام، ولكن أيضا أحرف أكثر أو أقل مما كانت متوقعة أو لا توجد أحرف على الإطلاق (بمعنى آخر, ، فقط مفتاح العودة)، أو أو أي وقت سابق لأوانه، أو أي شيء. من المستحيل تقريبا التعامل بأمان مع كل هذه المشاكل المحتملة عند استخدامها scanf; ؛ من الأسهل بكثير قراءة الخطوط بأكملها (مع fgets أو ما شابه)، ثم تفسيرها، إما باستخدام sscanf أو بعض التقنيات الأخرى. (وظائف مثل strtol, strtok, ، و atoi غالبا ما تكون مفيدة؛ انظر أيضا الأسئلة 12.16 و 13.6.) إذا كنت تستخدم أي scanf البديل، تأكد من التحقق من قيمة الإرجاع للتأكد من العثور على العدد المتوقع للعناصر. أيضا، إذا كنت تستخدم %s, ، تأكد من الحذر من تجاوز تجاوز المخزن المؤقت.

ملاحظة، بالمناسبة، أن انتقادات scanf ليست بالضرورة لوائح الاتهام fscanf و sscanf. scanf يقرأ من stdin, ، والتي عادة ما تكون لوحة مفاتيح تفاعلية، وبالتالي فهي أقل مقيدة، مما يؤدي إلى معظم المشاكل. عند وجود ملف بيانات بتنسيق معروف، من ناحية أخرى، قد يكون من المناسب قراءته fscanf. وبعد انها مناسبة تماما لتحليل السلاسل مع sscanf (طالما تم فحص قيمة الإرجاع)، لأنه من السهل استعادة التحكم، أعد تشغيل المسح، وتجاهل المدخلات إذا لم تتطابق، إلخ.

روابط إضافية:

المراجع: K & R2 SEC. 7.4 ص. 159.

من الصعب جدا الحصول عليها scanf للقيام الشيء الذي تريده. بالتأكيد، يمكنك، ولكن أشياء مثل scanf("%s", buf); هي خطيرة gets(buf);, ، كما قال الجميع.

كمثال، ما يفعله Paxdiablo في وظيفته لقراءة يمكن القيام به مع شيء مثل:

scanf("%10[^\n]%*[^\n]", buf));
getchar();

ما سبق قراءة خط، تخزن أول 10 أحرف غير موجودة في buf, ، ثم تجاهل كل شيء حتى (بما في ذلك) جديد. لذلك، يمكن كتابة وظيفة paxdiablo باستخدام scanf الطريقة التالية:

#include <stdio.h>

enum read_status {
    OK,
    NO_INPUT,
    TOO_LONG
};

static int get_line(const char *prompt, char *buf, size_t sz)
{
    char fmt[40];
    int i;
    int nscanned;

    printf("%s", prompt);
    fflush(stdout);

    sprintf(fmt, "%%%zu[^\n]%%*[^\n]%%n", sz-1);
    /* read at most sz-1 characters on, discarding the rest */
    i = scanf(fmt, buf, &nscanned);
    if (i > 0) {
        getchar();
        if (nscanned >= sz) {
            return TOO_LONG;
        } else {
            return OK;
        }
    } else {
        return NO_INPUT;
    }
}

int main(void)
{
    char buf[10+1];
    int rc;

    while ((rc = get_line("Enter string> ", buf, sizeof buf)) != NO_INPUT) {
        if (rc == TOO_LONG) {
            printf("Input too long: ");
        }
        printf("->%s<-\n", buf);
    }
    return 0;
}

واحدة من المشاكل الأخرى مع scanf هو سلوكها في حالة الفائض. على سبيل المثال، عند قراءة int:

int i;
scanf("%d", &i);

لا يمكن استخدام ما سبق بأمان في حالة تجاوز الفائض. حتى بالنسبة للحالة الأولى، فإن قراءة السلسلة أكثر بساطة للقيام بها fgets بدلا من مع scanf.

نعم كلامك صحيح. هناك عيب الأمن الرئيسي في scanf الأسرة(scanf,sscanf, fscanf.. esp عند قراءة سلسلة، لأنهم لا يأخذون طول المخزن المؤقت (الذي يقرأون فيه) في الاعتبار.

مثال:

char buf[3];
sscanf("abcdef","%s",buf);

من الواضح أن المخزن المؤقت buf يمكن أن تعقد ماكس 3 شار. لكن ال sscanf سيحاول وضع "abcdef" في ذلك تسبب تجاوز المخزن المؤقت.

مشاكل لدي مع *scanf() الأسرة:

  • إمكانية تجاوز تجاوز المخزن المؤقت مع٪ S و٪ [محطات التحويل. نعم، يمكنك تحديد عرض حقل أقصى، ولكن على عكس printf(), ، لا يمكنك أن تجعلها حجة في scanf() يتصل؛ يجب أن تكون صياغة في محدد التحويل.
  • إمكانات الفائض الحسابي مع٪ D،٪ I، إلخ.
  • القدرة المحدودة على الكشف عن وإدخال المدخلات المشكلة بشدة. على سبيل المثال، "12W4" ليس عددا صحيحا صالحا، ولكن scanf("%d", &value); سوف يحول بنجاح وتعيين 12 إلى value, ، ترك "W4" عالقة في مجرى الإدخال لإثارة قراءة المستقبل. من الناحية المثالية يجب رفض سلسلة الإدخال بأكملها، ولكن scanf() لا يمنحك آلية سهلة للقيام بذلك.

إذا كنت تعرف المدخلات الخاصة بك ستكون دائما تشكلت بشكل جيد مع سلاسل ثابتة والقيم العددية التي لا تغازل الفائض، ثم scanf() هي أداة رائعة. إذا كنت تتعامل مع الإدخال التفاعلي أو المدخلات غير المضمونة لتكون تشكيلها جيدا، فاستخدم شيئا آخر.

العديد من الإجابات هنا تناقش المشكلات السائلة المحتملة باستخدام scanf("%s", buf), ، ولكن أحدث مواصفات posix أكثر أو أقل يحل هذه المشكلة عن طريق توفير m حرف تخصيص الاحالة التي يمكن استخدامها في محددات التنسيق c, s, ، و [ التنسيقات. وهذا سيسمح scanf لتخصيص أكبر قدر ممكن من الذاكرة مع malloc (لذلك يجب إطلاق سراحه لاحقا free).

مثال على استخدامه:

char *buf;
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char.

// use buf

free(buf);

يرى هنا. وبعد عيوب هذا النهج هو أنه إضافة حديثة نسبيا لمواصفات POSIX وغير محددة في مواصفات C على الإطلاق، لذلك لا تزال غير قابلة للذات غير قابلة للتحقيق الآن.

هناك مشكلة واحدة كبيرة مع scanf- مثل الوظائف - عدم وجود أي نوع السلامة. هذا هو، يمكنك رمز هذا:

int i;
scanf("%10s", &i);

الجحيم، حتى هذا هو "جيد":

scanf("%10s", i);

انها أسوأ من printf- مثل الوظائف، ل scanf تتوقع مؤشر، لذلك الحادث أكثر احتمالا.

بالتأكيد، هناك بعض الداما المعاشية بالتنسيق هناك، ولكن تلك ليست مثالية وجيدة، فهي ليست جزءا من اللغة أو المكتبة القياسية.

ميزة scanf بمجرد تعلم كيفية استخدام الأداة، كما يجب أن تفعل دائما في C، فإنه يحتوي على USECASS مفيدة للغاية. يمكنك تعلم كيفية استخدام scanf والأصدقاء من خلال القراءة والتفاهم الدليل. وبعد إذا لم تتمكن من الوصول إلى هذا الدليل دون مشاكل فهم خطيرة، فمن المحتمل أن يشير ذلك إلى أنك لا تعرف ج جيدا.


scanf والأصدقاء عانوا من خيارات التصميم المؤسفة التي جعلتها صعبة (وأحيانا مستحيل) استخدامها بشكل صحيح دون قراءة الوثائق، كما أظهرت إجابات أخرى. يحدث هذا في جميع أنحاء C، لسوء الحظ، لذلك إذا كنت لتقديم المشورة ضد استخدام scanf ثم ربما ننصح ضد استخدام C.

يبدو أن أحد أكبر عيوب عيوب بحتة سمعة مكت مكاسب; ؛ كما هو الحال مع العديد من الميزات المفيدة ل C، يجب أن نكون على اطلاع جيد قبل استخدامه. المفتاح هو إدراك أنه كما هو الحال مع بقية C، يبدو موجزا والصاصية، ولكن يمكن أن يكون مضللا مهما. هذا منتظم في ج؛ من السهل على المبتدئين كتابة التعليمات البرمجية التي يعتقدون أنها منطقية وقد تعمل لهم في البداية، ولكن لا معنى لها ويمكن أن تفشل كارثيا.

على سبيل المثال، يتوقع الشائعة غير المبللة %s مندوب سيؤدي إلى ذلك خط ليتم قراءتها، وبينما قد يبدو ذلك بديهيا فإنه ليس صحيحا بالضرورة. انها أكثر ملاءمة لوصف الحقل اقرأ كما كلمة. وبعد قراءة الدليل ينصح بشدة بكل وظيفة.

ماذا سيكون أي استجابة لهذا السؤال دون ذكر عدم وجود سلامة وخطر الفائض المخزن المؤقت؟ كما كنا مغطاة بالفعل، ج C ليست لغة آمنة، وسوف تسمح لنا بقطع الزوايا، وربما لتطبيق تحسين على حساب صحة أو أكثر عرضة لأننا مكبراتين كسولين. وبالتالي، عندما نعرف أن النظام لن يتلقى أبدا سلسلة أكبر من عدد ثابت من البايتات، فإننا نظمنا بالقدرة على إعلان صفيف هذا الحجم والتحقق من حدود الحدود. لا أرى هذا حقا كقوة سقوط؛ إنه خيار. مرة أخرى، فإن قراءة الدليل ينصح بشدة وسيكشفنا عن هذا الخيار إلينا.

المبرمجين كسول ليسوا الوحيدون scanf. وبعد ليس من غير المألوف أن نرى الناس يحاولون القراءة float أو double القيم باستخدام %d, ، علي سبيل المثال. عادة ما يكونون مخطئين في الاعتقاد بأن التنفيذ سيعمل نوعا من التحويلات وراء الكواليس، مما سيؤدي إلى معنى لأن التحويلات المماثلة تحدث طوال بقية اللغة، لكن هذا ليس هو الحال هنا. كما قلت باكرا، scanf والأصدقاء (وبالفعل بقية ج) خادعة؛ يبدو أنها موجزة والاعتبني لكنها ليست كذلك.

المبرمجين عديمي الخبرة لا يجبرون على النظر في نجاح العملية. وبعد لنفترض أن المستخدم يدخل شيئا غير رقمي تماما عندما قلنا scanf لقراءة وتحويل سلسلة من الأرقام العشرية باستخدام %d. وبعد الطريقة الوحيدة التي يمكننا فيها اعتراض مثل هذه البيانات الخاطئة هي التحقق من قيمة الإرجاع، وكم مرة نزعج التحقق من قيمة الإرجاع؟

يشبه إلى حد كبير fgets, ، متي scanf والأصدقاء فشلوا في قراءة ما قالوه للقراءة، سيتم ترك الدفق في حالة غير عادية؛- في حالة ما اذا fgets, إذا لم يكن هناك مساحة كافية لتخزين سطر كامل، فيمكن أن يتم علاج بقية الخط غير المقروءة بشكل خاطئ كما لو أنها سطر جديد عندما لا يكون كذلك. - في حالة ما اذا scanf والأصدقاء، فشل التحويل كما هو موثق أعلاه، يتم ترك البيانات الخاطئة غير مقروءة على الدفق ويمكن علاجها عن طريق الخطأ كما لو أنها جزء من حقل مختلف.

ليس من الأسهل استخدامها scanf والأصدقاء من استخدام fgets. وبعد إذا تحققنا للنجاح من خلال البحث عن '\n' عندما نستخدم fgets أو عن طريق فحص قيمة العودة عندما نستخدم scanf والأصدقاء، ونجد أننا نقرأ خط غير مكتمل باستخدام fgets أو فشل في قراءة حقل باستخدام scanf, ، ثم نواجه نفس الواقع: نحن على الأرجح تجاهل المدخلات (عادة ما يصل حتى و بما في ذلك Newline التالي)! yuuuuuuck!

لسوء الحظ، scanf كلاهما يجعل من الصعب في وقت واحد (غير بديهية) وسهلة (أقل ضغطات المفاتيح) لتجاهل المدخلات بهذه الطريقة. في مواجهة هذا الواقع من تجاهل إدخال المستخدم، حاول البعض scanf("%*[^\n]%*c");, ، لا يدرك أن %*[^\n] سوف يفشل المندوبون عندما يواجهوا شيئا سوى خطوط نيوسرية، وبالتالي ستظل نيولين غادر على الدفق.

التكيف الطفيف، عن طريق فصل مندوبي الشكلين ونرى بعض النجاح هنا: scanf("%*[^\n]"); getchar();. وبعد حاول القيام بذلك مع عدد قليل جدا من المفاتيح باستخدام أداة أخرى؛)

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top