قضت لغة C أول 27 عامًا من عمرها دون نوع منطقي. وهذا ليس سهوًا - بل قرار تصميمي له عواقب تصادفها في يومك الأول، وعلّة شهيرة واحدة تنبع منه مباشرةً.
الصفر خطأ، وكل ما عداه صحيح
لا توجد قيمة خاصة للصحّة أو الخطأ في قواعد الشروط في C. فـ if وwhile وfor و&& و|| و! تطرح جميعها سؤالًا واحدًا بالضبط: هل هذه القيمة صفر؟
الأعداد السالبة صحيحة. والكسور صحيحة. والمؤشّر غير الفارغ صحيح. الصفر وحده - بأي نوع - هو الخطأ.
ولهذا تمتلئ شيفرة C بصيغ اصطلاحية تبدو موجزة إلى حدّ الغموض قبل أن تعرف القاعدة:
if (count) /* إذا كانت count غير صفرية */
if (!count) /* إذا كانت count صفرًا */
if (p) /* إذا لم تكن p مساوية لـ NULL */
if (!strcmp(a, b)) /* إذا تساوت السلسلتان - تُعيد strcmp القيمة 0 عند التطابق */
هذه الأخيرة توقع كثيرين. تُعيد strcmp القيمة 0 عند تطابق السلسلتين، فتُقرأ "متساويتان" على أنها "ليس" داخل if. وكتابة if (strcmp(a, b) == 0) أوضح وتفعل الشيء نفسه بالضبط.
المقارنات تُنتج int
المقارنة في C ليست نوعًا خاصًا من التعابير. فهي تُقيَّم إلى int عادي قيمته 1 أو 0، ويمكنك طباعتها أو تخزينها أو إجراء حساب بها:
يعمل العامل ! بالطريقة نفسها: فهو يُنتج 1 إن كان معامله صفرًا و0 فيما عدا ذلك. لذا فإن !!x صيغة اصطلاحية كلاسيكية تُطبّع أي قيمة إلى 0 أو 1 بالضبط.
bool وtrue وfalse (معيار C99)
أضاف C99 نوعًا منطقيًا حقيقيًا اسمه _Bool، وترويسةً تمنحه اسمًا مقروءًا:
<stdbool.h> ترويسة صغيرة جدًا: فهي تعرّف bool بأنه _Bool، وtrue بأنه 1، وfalse بأنه 0. هذه هي الترويسة بأكملها. لكن استخدامها يجعل القصد مرئيًا بطريقة لا يبلغها int flag = 1; أبدًا.
ولـ _Bool سلوك واحد مميّز فعلًا: فهو يخزّن 0 أو 1 فقط. إسناد أي قيمة غير صفرية يُحوَّل إلى 1 بالضبط:
ولا يزال لا يوجد %b في printf للقيم المنطقية - اطبعها بـ %d، أو حوّلها إلى نص بنفسك:
وفي C23 صار الأمر أبسط: أصبحت bool وtrue وfalse كلمات مفتاحية حقيقية، فلم يعد التضمين مطلوبًا. لكن معظم الشيفرات اليوم ما زالت تستهدف C17، فأبقِ على #include <stdbool.h>.
إعادة قيمة منطقية من دالة
هنا يُثبت bool جدارته بأوضح صورة:
نوع الإرجاع bool يخبر المستدعي بمعنى القيمة. أما نوع إرجاع int من دالة اسمها check_file فقد يكون قيمة منطقية أو عدّادًا أو رمز خطأ - وفي أعراف مكتبات C يكون غالبًا الاحتمال الثالث، حيث يعني 0 النجاح. تسمية النوع تزيل التخمين.
علّة = مقابل ==
لأن الإسناد تعبير يُنتج القيمة المُسنَدة، ولأن أي قيمة غير صفرية صحيحة، فإن هذا يُترجَم بنجاح:
حدث خطآن في آن واحد. أسند الشرط 5 إلى x فدمّر قيمتها، ثم فحص 5، وهي غير صفرية، فنُفِّذ الفرع. ولا يوجد في البرنامج خطأ ولا تحذير افتراضيًا.
والأسوأ أن النسخة التي فيها 0 لا تُنفّذ فرعها أبدًا:
if (found = 0) { /* لا يُنفَّذ أبدًا - و found صارت الآن 0 */ }
ثلاثة خطوط دفاع، مرتّبة حسب فائدتها:
ترجم مع التحذيرات. يلتقطها -Wall:
warning: suggest parentheses around assignment used as truth value
الوصول إلى صفر تحذيرات هو الحلّ الحقيقي هنا، وتكلفته راية واحدة.
اكتب الثابت أولًا. "شرط يودا" يحوّل الخطأ المطبعي إلى خطأ ترجمة:
if (5 == x) /* صحيح */
if (5 = x) /* خطأ: ليس قيمة يسارية - المترجم يوقفك */
بعض الفرق تحبّ هذا الأسلوب وبعضها يراه غير مقروء؛ وكلا الموقفين قابل للدفاع عنه، لكنه ينجح فعلًا.
كن متعمّدًا حين تقصده. الإسناد داخل شرط مفيد فعلًا عند قراءة الإدخال:
int c;
while ((c = getchar()) != EOF) { ... }
الأقواس الإضافية حول c = getchar() مطلوبة لأولوية العوامل، وهي تشير أيضًا إلى أن الإسناد مقصود. هذه هي الصيغة الاصطلاحية - فإن لم يكن إسنادك داخل الشرط يشبهها، فهو على الأرجح خطأ مطبعي.
أخطاء منطقية شائعة
المقارنة بـ true. if (flag == true) يعمل مع stdbool.h، لكنه ينكسر لحظة كون flag من نوع int تحمل 42، لأن 42 == 1 خطأ. اكتب if (flag).
المقارنات المتسلسلة. if (0 < x < 10) محقّق دائمًا: فـ 0 < x يُنتج 0 أو 1، وكلاهما أصغر من 10. اكتب if (x > 0 && x < 10).
العوامل البتّية بدل المنطقية. & و| ليسا && و||. فهما لا يجريان تقييمًا مختصرًا، وهما يدمجان البتّات:
كلا المعاملين "صحيح"، ومع ذلك فإن a & b يساوي 0. واستخدام & حيث قصدت && يُنتج شرطًا خاطئًا لقيم معيّنة فقط، وهو أصعب أنواع العلل عثورًا.
مقارنة أعداد الفاصلة العائمة بالتساوي. if (0.1 + 0.2 == 0.3) خطأ. قارن مقابل هامش تسامح بدلًا من ذلك - انظر أنواع البيانات لمعرفة السبب.
الأسئلة الشائعة
هل يوجد نوع منطقي في لغة C؟
منذ C99، نعم: _Bool نوع مدمج، وتضمين <stdbool.h> يمنحك التسميات الأودّ bool وtrue وfalse. قبل C99 لم يكن هناك نوع منطقي إطلاقًا - كانت البرامج تستخدم int مع 0 للخطأ و1 للصحّة، وهو أسلوب ما زال يعمل وما زال شائعًا.
ما الذي يُعدّ صحيحًا في لغة C؟
أي قيمة غير صفرية. 1 و-5 و0.01 والمؤشّر غير الفارغ كلها صحيحة في شرط؛ والخطأ محصور في 0 و0.0 والمؤشّر الفارغ. لا يوجد فحص منطقي منفصل - if (x) يفحص ببساطة ما إذا كانت x غير صفرية.
كيف أستخدم bool في لغة C؟
أضف #include <stdbool.h> وعندها يعمل bool ready = true; كما تتوقّع. وخلف الكواليس bool هو _Bool، الذي يخزّن 0 أو 1 فقط - فإسناد أي قيمة غير صفرية إليه يخزّن 1 بالضبط.
ما الفرق بين = و== في لغة C؟
= تُسنِد، و== تقارن. if (x = 5) يُسنِد 5 إلى x ثم يفحص 5، وهي غير صفرية، فيُنفَّذ الفرع دائمًا. أما if (x == 5) فيفحص التساوي. الأولى تُترجَم دون خطأ، ولهذا وُجد كل من -Wall وعادة كتابة if (5 == x).