يحمل المؤشّر دائمًا قيمةً ما. وحين لا يكون لديه كائن يشير إليه بعد - قبل إسناده، أو بعد تحرير ما كان يشير إليه، أو حين لا يكون لدى دالة ما تُرجِعه - يحتاج إلى قيمة تعني بلا لبس "لا شيء هنا". تلك القيمة هي المؤشّر الفارغ، وNULL هو الاسم الذي تكتبه.
والانضباط المحيط به صغير ويسدّد ثمنه باستمرار: اضبط المؤشّرات على NULL حين لا هدف لها، وافحص NULL قبل المراجعة، وأرجِع NULL حين لا تملك نتيجة.
ما NULL فعليًا
NULL ماكرو معرَّف في <stddef.h> وتجلبه أيضًا <stdio.h> و<stdlib.h> و<string.h> وغيرها. وهو يتمدّد إلى ثابت مؤشّر فارغ - عادةً ((void*)0) أو 0 مجرّدًا.
ويضمن معيار C الخاصية التي تهمّ: المؤشّر الفارغ لا يساوي أي مؤشّر إلى كائن أو دالة فعلية. ومؤشّران فارغان من النوع نفسه يتساويان دائمًا.
تطبع معظم الأنظمة (nil) أو 0x0 للمؤشّر الفارغ - أي العنوان صفر. وذلك تطبيق شائع لا قاعدة؛ فالمعيار لا يعد أبدًا بأن البتات أصفار. بل يعد بالمقارنة، فاكتب p == NULL ولا تكتب أبدًا ما يعتمد على التمثيل.
مراجعة NULL تنهار
هذا هو سبب كل فحص فراغ في الوجود:
int *p = NULL;
printf("%d\n", *p); // undefined behavior - almost certainly a crash
على ويندوز وmacOS ولينكس تُترك الصفحة الأولى من فضاء العناوين غير مخطَّطة عن قصد، فيعترض المعالج الوصول ويقتل نظام التشغيل البرنامج. وسترى:
Segmentation fault (core dumped) # Linux
zsh: segmentation fault ./program # macOS
ذلك الانهيار الفوري ميزة. فهو يحوّل خطأً منطقيًا إلى فشل صاخب عند السطر الذي سبّبه بالضبط، بدلًا من إفساد صامت تكتشفه بعد ثلاثة أسابيع. انظر خطأ التجزئة لمعرفة كيف تقرأ الانهيار وتجد السطر.
ولهذا فإن "المؤشّر الفارغ" ليس استثناءً تلتقطه في C. فلا توجد آلية للتعافي؛ لا يوجد إلا الفحص المسبق.
افحص ما قد يفشل
تُشير الدوال التي قد تفشل إلى ذلك بإرجاع NULL. والفحص ليس تأدّبًا اختياريًا - فالانهيار هو البديل.
المكتبة القياسية مليئة بهذه: malloc وcalloc وrealloc وfopen وstrchr وstrstr وgetenv وbsearch. وكل واحدة منها تجيب بـ "لا نتيجة" بـ NULL. انظر الذاكرة الديناميكية لقصة التخصيص كاملة.
واصطلاحان للفحص، وكلاهما شائع:
if (p == NULL) { /* handle */ } // explicit - preferred for clarity
if (!p) { /* handle */ } // terse - relies on NULL being falsy
يُفحَص المؤشّر الفارغ على أنه خاطئ في شرط والمؤشّر الصالح على أنه صحيح، فكلاهما يعمل. والصيغة الصريحة أفضل قراءةً حين يمسح غريبٌ شيفرتَك بحثًا عن أخطاء.
NULL مقابل 0 مقابل غير المهيّأ
ثلاثة مواقف متمايزة يخلط المبتدئون بينها:
int *a = NULL; "points at nothing" - testable, safe to check
int *b; uninitialized - holds garbage, NOT testable
int *c = &x; points at a real object
والخطير هو b. فبايتاته هي ما تُرك في خانة المكدّس تلك، وقد تكون أصفارًا (فتبدو سليمة) وقد تكون عنوانًا معقول المظهر من استدعاء سابق. ولا يوجد فحص يميّزه عن مؤشّر صالح، ويتغيّر السلوك بين بناء التنقيح وبناء الإصدار.
هيّئ كل مؤشّر. فإن لم يكن لديك عنوان بعد، فإن NULL هو العنوان. وترجم بـ -Wall -Wextra فينبّهك المترجم إلى كثير من الاستخدامات غير المهيّأة.
وعن 0: في سياق مؤشّر يكون الثابت الصحيح 0 هو ثابت المؤشّر الفارغ، فـ p = 0; شيفرة C صالحة. ومع ذلك فضّل NULL. فهو يشير إلى القصد، ويهمّ في الاستدعاءات متغيّرة الوسائط حيث لا يستطيع المترجم التحويل نيابةً عنك:
execl("/bin/ls", "ls", 0); // risky - may pass an int where a pointer is expected
execl("/bin/ls", "ls", (char *)NULL); // correct
وتضيف C23 الكلمة المفتاحية nullptr بنوعها الخاص الذي يتجنّب هذا الصنف من الالتباس كليًا؛ ويبقى NULL صحيحًا وقابلًا للنقل في كل مكان.
أنماط دفاعية
احرس في رأس كل دالة تأخذ مؤشّرات.
اضبط على NULL بعد التحرير. لا تغيّر free مؤشّرك - بل تحرّر الذاكرة فقط. والمؤشّر القديم المتروك مؤشّر معلّق، واستخدامه سلوك غير معرَّف كثيرًا ما لا ينهار فورًا.
حقيقتان داخل ذلك المثال تستحقّان الحفظ. free(NULL) معرَّفة بألّا تفعل شيئًا، فلا تحتاج شيفرة التنظيف إلى حراستها أبدًا. وضبط p = NULL بعد free يحوّل الاستخدام بعد التحرير - الذي قد يفسد البيانات بصمت - إلى انهيار فوري قابل للتنقيح.
أرجِع NULL بمعنى "لا نتيجة"، وقُل ذلك.
// returns a pointer to the matching element, or NULL if there is none
int *find(int *arr, int n, int target);
وثّق ذلك في التعليق فوق الدالة. فالمستدعي الذي يعلم أن NULL محتمل يكتب الفحص؛ ومن لا يعلم لن يفعل.
قائمة تحقّق سريعة
- هيّئ كل مؤشّر، بعنوان حقيقي أو بـ
NULL. - افحص القيمة المُرجَعة من
mallocوcallocوreallocوfopenقبل استخدامها. - افحص وسائط المؤشّرات في رأس أي دالة قد تُستدعى بإدخال سيّئ.
- اضبط المؤشّرات على
NULLفورfree. - اكتب
NULLلا0كلما كان المقصود مؤشّرًا. - ابنِ بـ
-Wall -Wextra، وحين تستطيع شغّل تحت مُعقِّم:gcc -fsanitize=address,undefinedيلتقط مراجعات الفراغ بتقرير دقيق.
الأسئلة الشائعة
ما المؤشّر الفارغ في لغة C؟
مؤشّر يحمل ثابت المؤشّر الفارغ - قيمة مضمون أنها لا تساوي عنوان أي كائن حقيقي. وهو الطريقة المعيارية لقول "هذا المؤشّر لا يشير إلى شيء بعد"، وNULL هو الماكرو الخاص به، المعرَّف في <stddef.h> وفي عدة ترويسات أخرى.
ماذا يحدث إذا راجعت مؤشّرًا فارغًا في لغة C؟
هذا سلوك غير معرَّف. وعمليًا، على أنظمة المكاتب والخوادم، ينهار البرنامج فورًا بـخطأ تجزئة لأن العنوان صفر يُترك غير مخطَّط عن قصد. أما على الأنظمة المضمَّنة بلا حماية ذاكرة فقد يقرأ شيئًا أو يفسده بصمت بدلًا من ذلك، وهذا أسوأ بكثير.
هل NULL هو نفسه 0 في لغة C؟
في سياقات المؤشّرات نعم: فالثابت الصحيح 0 هو ثابت المؤشّر الفارغ، فتعمل p = 0; وp == NULL كلتاهما. لكن استخدم NULL للمؤشّرات و0 للأعداد - فذلك يخبر القارئ أيّهما قصدت، ويبقي الاستدعاءات متغيّرة الوسائط آمنة، حيث قد يُمرَّر 0 المجرّد كعدد صحيح لا كمؤشّر.
ما الفرق بين المؤشّر الفارغ والمؤشّر غير المهيّأ؟
المؤشّر الفارغ يشير قطعًا إلى لا شيء، ويمكنك فحصه. أما المؤشّر غير المهيّأ فيحمل ما صادف أن كان في تلك الذاكرة من بايتات - وربما عنوانًا يبدو صالحًا - فلا يوجد فحص يستطيع التقاطه. هيّئ المؤشّرات دائمًا بـ NULL حين لا يكون لديك أفضل من ذلك.