
نحن ندعوك لمحاولة العثور على خطأ في وظيفة بسيطة للغاية من مشروع قائد غنو منتصف الليل. لماذا؟ تماما مثل ذلك. انه مضحك وممتع. على الرغم من لا ، كذبنا. مرة أخرى ، نود أن نظهر خطأً يجده الشخص بصعوبة في عملية مراجعة الكود ، ولكن يمكنه بسهولة العثور على محلل الكود الثابت PVS-Studio.
لقد أرسلنا مؤخرًا خطابًا يسأل عن سبب قيام المحلل بإنشاء تحذير على وظيفة
EatWhitespace ، والتي يرد
رمزها أدناه. في الواقع السؤال ليس بهذه البساطة. حاول معرفة ما هو الخطأ في هذا الكود بنفسك.
static int EatWhitespace (FILE * InFile) { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); }
كما ترون ، وظيفة
EatWhitespace صغيرة جدًا. حتى التعليق على الوظيفة يشغل مساحة أكبر من نص الوظيفة نفسها :). الآن بعض التفاصيل.
وصف وظيفة
Getc :
int getc ( FILE * stream );
تقوم الدالة بإرجاع الحرف المشار إليه بواسطة المؤشر الداخلي لموضع ملف الدفق المحدد. ثم ينتقل المؤشر إلى الحرف التالي. إذا تم الوصول إلى نهاية الملف في وقت الاستدعاء للدفق ،
فتُرجع الدالة
EOF وتعيين مؤشر نهاية الملف لهذا الدفق. في حالة حدوث خطأ في القراءة ، تقوم الدالة بإرجاع قيمة EOF وتعيين مؤشر خطأ للدفق المحدد (ferror).
وصف وظيفة
isspace :
int isspace( int ch );
تقوم الدالة بالتحقق مما إذا كان الحرف هو مسافة بيضاء وفقًا لتصنيف الإعدادات المحلية الحالية. في الإعدادات المحلية القياسية ، تكون الأحرف التالية مسافة بيضاء:
- مساحة (0x20 ، ``) ؛
- تغيير الصفحة (0x0c ، '\ f') ؛
- سطر التغذية LF (0x0a ، '\ n') ؛
- حرف الإرجاع CR (0x0d ، '\ r') ؛
- علامة تبويب أفقية (0x09 ، '\ t') ؛
- علامة تبويب رأسية (0x0b ، '\ v').
قيمة الإرجاع قيمة غير صفرية ، إذا كان الحرف هو مسافة بيضاء ، صفر خلاف ذلك.
يجب أن
تتخطى الدالة
EatWhitespace جميع الأحرف التي تم اعتبارها بيضاء ، باستثناء سطر السطر '\ n'. قد يكون سبب آخر لإيقاف القراءة من ملف هو الوصول إلى نهاية الملف (EOF).
والآن ، ومع معرفة كل هذا ، حاول العثور على خطأ!
لمنع القارئ من أن لا ينظر بشكل مفاجئ في الإجابة ، أضف زوجًا من حيدات الانتظار.

الشكل 1. الوقت للبحث عن خطأ. سوف حيدات الانتظار.هل ما زلت لا ترى الخطأ؟
الشيء هو أننا خدعنا القراء حول
isspace . هاها هذه ليست ميزة قياسية على الإطلاق ، ولكن ماكرو محلية الصنع. نعم ، نحن بلا لوم وجعلنا في حيرة من أمرك.

الشكل 2. وحيد القرن يعطي القراء انطباعا خاطئا عن ما هو isspace .في الواقع ، بالطبع ، نحن وحدنا يونيكورن ليسوا مسؤولين. ساهم مؤلفو مشروع GNU Midnight Commander في الارتباك من خلال اتخاذ قرار بإنشاء تطبيق
الإصدار الخاص بهم في ملف
charset.h :
#ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t')
بإنشاء مثل هذا الماكرو ، أربك بعض المطورين المطورين الآخرين. تتم كتابة التعليمة البرمجية على افتراض أن
isspace هي وظيفة قياسية تراعي إرجاع الأحرف (0x0d ، '\ r') كأحد أحرف المسافة البيضاء.
يعتبر الماكرو المطبق فقط المسافات وعلامات التبويب كأحرف بيضاء. دعنا نستبدل الماكرو ونرى ما سيحدث.
for (c = getc (InFile); ((c)==' ' || (c) == '\t') && ('\n' != c); c = getc (InFile))
تعبير subpresspress ('\ n'! = C) متكرر (متكرر) لأن نتيجته ستكون دائمًا صحيحة. يحذر محلل PVS-Studio من هذا الأمر ، مع إعطاء تحذير:
V560 جزء من التعبير الشرطي صحيح دائمًا: ('\ n'! = C). params.c 136.
من أجل الوضوح ، دعنا نحلل 3 خيارات لتطوير الأحداث:
- يتم الوصول إلى نهاية الملف. نهاية الملف (EOF) ليست مساحة أو علامة تبويب. لا يتم حساب التعبير الفرعي ('\ n'! = C) بسبب تقييم الدائرة القصيرة . توقف الدورة.
- تتم قراءة أي حرف ليس مسافة أو علامة تبويب. لا يتم حساب التعبير الفرعي ('\ n'! = C) بسبب تقييم الدائرة القصيرة. توقف الدورة.
- قراءة حرف مسافة أو علامة تبويب أفقية. يتم حساب التعبير الفرعي ('\ n'! = C) ، لكن النتيجة ستكون صحيحة دائمًا.
بمعنى آخر ، الكود الذي تمت مراجعته يعادل هذا:
for (c = getc (InFile); c==' ' || c == '\t'; c = getc (InFile))
وجدنا أن الكود لا يعمل بالشكل المطلوب. دعونا نرى الآن ما هي عواقب ذلك.
توقع المبرمج الذي كتب استدعاء
isspace في
نص الدالة
EatWhitespace أن يتم استدعاء دالة قياسية. لهذا السبب أضاف الشرط الذي يجب أن لا يعتبر سطر التغذية LF ('\ n') حرف مسافة بيضاء.
لذلك ، خطط المبرمج أنه بالإضافة إلى المساحات وعلامات التبويب الأفقية ، سيتم تخطي أحرف مثل تغيير الصفحة وعلامة التبويب العمودية.
تجدر الإشارة إلى أنه تم التخطيط لتخطي حرف إرجاع حرف CR (0x0d ، '\ r'). هذا لا يحدث وتتوقف الدورة عندما تصادف هذا الرمز. سيؤدي ذلك إلى مفاجآت غير سارة إذا كان فاصل الأسطر في الملف هو تسلسل CR + LF المستخدم في بعض الأنظمة غير التابعة لنظام UNIX ، مثل Microsoft Windows.
بالنسبة لأولئك الذين يرغبون في معرفة المزيد عن الأسباب التاريخية لاستخدام LF أو CR + LF كفواصل أسطر ، فيما يلي مقالة Wikipedia "
تغذية السطر ".
من
المفترض أن تقوم وظيفة
EatWhitespace بمعالجة الملفات بنفس الطريقة ، حيث يتم استخدام LF و CR + LF كفاصل. في حالة CR + LF ، هذا ليس كذلك. بمعنى آخر ، إذا كان الملف الخاص بك قد جاء من عالم Windows ، فأنت خارج الحظ :).
ربما لا يكون هذا خطأً خطيرًا ، خاصةً لأن GNU Midnight Commander شائع في أنظمة التشغيل المشابهة لـ UNIX ، حيث يتم استخدام حرف LF (0x0a ، '\ n') لترجمة سطر. ومع ذلك ، وبسبب هذه التافهات ، تنشأ مشاكل مزعجة مختلفة من عدم توافق البيانات المعدة في أنظمة Linux و Windows.
الخطأ الموصوف مثير للاهتمام لأنه يكاد يكون من المستحيل اكتشافه من خلال مراجعة الكود الكلاسيكي. لا يمكن لجميع مطوري المشاريع معرفة تعقيدات الماكرو ، ومن السهل جدًا نسيانهم. هذا مثال جيد حيث يُكمل تحليل الكود الثابت مراجعات الكود وتقنيات اكتشاف الأخطاء الأخرى.
التغلب على الوظائف القياسية هو ممارسة سيئة. بالمناسبة ، في الآونة الأخيرة في مقال "
الحب تحليل رمز ثابت " تم النظر في حالة مماثلة مع الماكرو
# تحديد sprintf std :: printf .
سيكون الحل الأفضل هو إعطاء الماكرو اسمًا فريدًا ، على سبيل المثال ،
is_space_or_tab . عندها سيكون الارتباك مستحيلاً.
ربما كان السبب وراء إنشاء الماكرو هو التشغيل البطيء لوظيفة
isspace القياسية
، وقام المبرمج بإنشاء إصدار أسرع ، وهو ما يكفي لحل جميع المهام الضرورية. لكن لا يزال ، هذا القرار خاطئ. سيكون أكثر
موثوقية لتعريف
isspace بطريقة تحصل على التعليمات البرمجية غير المترجمة. ولتنفيذ الوظيفة اللازمة في ماكرو باسم فريد.
شكرا لاهتمامكم نحن ندعوك
لتنزيل وتجربة محلل PVS-Studio لاختبار مشاريعك. بالإضافة إلى ذلك ، نذكرك أن المحلل أضاف مؤخرًا دعمًا بلغة جافا.

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