متغيّر يحمل عنواناً
كل متغيّر يقيم في مكان ما داخل الذاكرة، في موضع مرقّم يُسمّى عنوانه. في معظم الأحيان لا يهمّك أين هو — تكتفي باستخدام اسم المتغيّر. أما المؤشر فيقلب ذلك: هو متغيّر قيمته هي عنوان. فبدلاً من أن يحمل 42، يحمل "المكان الذي خُزّنت فيه 42".
هذا التوجيه غير المباشر هو ما يمنح المؤشرات قوّتها. فبواسطته تستطيع الدوال تغيير متغيّر الطرف المُستدعي، وتربط بنى البيانات مثل القوائم المترابطة عُقَدها معاً به، و(كما سترى في الذاكرة الديناميكية) به تصل إلى الذاكرة التي تحجزها أثناء التشغيل.
الرمز & في &score هو عامل العنوان — وهو ينتج موضع score. والرمز * في *p هو عامل إلغاء المرجعية — وهو يتتبّع العنوان عائداً إلى القيمة المقيمة هناك.
العاملان: & و*
أكثر ما يحيّر المبتدئين أن * يعني شيئين مختلفين بحسب موضعه. أبقِ هذا واضحاً:
int* p; // تعريف: "p مؤشر إلى int"
p = &x; // & = العنوان: خزّن عنوان x في p
int y = *p; // * = إلغاء المرجعية: اقرأ القيمة التي يشير إليها p
*p = 99; // إلغاء المرجعية في طرف الإسناد: اكتب عبر المؤشر
في التعريف، * جزء من النوع. وفي التعبير، * يؤدّي عملاً. وبمجرّد إعداد المؤشر، يمنحك إلغاء مرجعيته صلاحية قراءة وكتابة كاملة على المتغيّر الأصلي:
لاحظ أنك لم تمسّ health باسمه قط بعد السطر الأول، ومع ذلك ظلّت قيمته تتغيّر. هذا هو جوهر الفكرة: hp اسم بديل لنفس موضع التخزين. أما المسافات (int* p أو int *p أو int*p) فهي شكلية فقط ومتطابقة بالنسبة للمترجم — ويستخدم هذا الدليل int* p.
nullptr: الإشارة إلى لا شيء
المؤشر الذي لا يشير إلى أي مكان ينبغي ضبطه على nullptr (C++11). إنها طريقة واضحة وآمنة نوعياً لقول "لا هدف بعد"، وتمنحك شيئاً تختبره قبل إلغاء المرجعية.
فضّل nullptr على ماكرو NULL القديم أو 0 المجرّد. فلأن nullptr يحمل نوع مؤشر حقيقي، لا يُقرأ خطأً قط على أنه العدد الصحيح 0 أثناء حلّ التحميل الزائد — وهو خطأ خفيّ كان النمط القديم قد يسبّبه.
مزلق — إلغاء مرجعية الفارغ. القراءة أو الكتابة عبر مؤشر فارغ (أو غير مهيّأ) سلوك غير معرّف، وغالباً ما يكون انهياراً فورياً:
int* p = nullptr;
cout << *p; // انهيار - إلغاء مرجعية الفارغ سلوك غير معرّف
احرص دائماً على الحماية بـ if (p) (أو if (p != nullptr)) قبل إلغاء مرجعية أي شيء قد يكون فارغاً.
المؤشرات والمصفوفات
اسم المصفوفة يتحلّل (decay) إلى مؤشر إلى عنصرها الأول، لذا فالمؤشرات والمصفوفات متشابكتان تشابكاً عميقاً. إضافة 1 إلى مؤشر لا تضيف بايتاً واحداً، بل تتقدّم عنصراً واحداً، وهذا ما يجعل حساب المؤشرات يعمل:
p[i] و*(p + i) تعبير واحد حرفياً — وهذا التكافؤ هو سبب فهرسة المصفوفات بدءاً من الصفر. والخطأ الكلاسيكي هنا هو تجاوز نهاية المصفوفة: فـ nums + 4 علامة صالحة لـ"موضع واحد بعد النهاية" تُقارَن بها، لكن إلغاء مرجعية *(nums + 4) يقرأ خارج الحدود. وأخطاء الانزياح بمقدار واحد (off-by-one) مع المؤشرات سبب رئيسي للانهيارات والإفساد الصامت، فكن متعمّداً في شرط التوقّف.
const والمؤشرات
يمكن أن يُطبَّق const على ما يشير إليه المؤشر، أو على المؤشر نفسه، أو على كليهما. اقرأ التعريف من نهايته إلى بدايته لفكّ شفرته:
const int* p; // مؤشر إلى const int - لا يمكن تغيير *p، يمكن إعادة توجيه p
int* const p = &x; // مؤشر const إلى int - يمكن تغيير *p، لا يمكن إعادة توجيه p
const int* const p = &x; // كلاهما مقفل
هذا مهمّ باستمرار في الشيفرة الحقيقية. فالدالة التي تَعِد بألّا تعدّل بياناتك تأخذ مؤشراً إلى const:
وسم المُشار إليه بـ const يوثّق النيّة ويتيح للمترجم منع الكتابات العَرَضية — أمان مجّاني بلا كلفة وقت تشغيل.
الفخّ الكبير: المؤشرات المعلّقة
المؤشر المعلّق يشير إلى ذاكرة لم تعد تحمل القيمة التي تتوقّعها — فإمّا أن المتغيّر خرج من نطاقه، أو أن الذاكرة حُرِّرت. وإلغاء مرجعيته سلوك غير معرّف، والأسوأ أنه كثيراً ما يبدو أنه يعمل إلى أن يتوقّف.
int* makeBad() {
int local = 5;
return &local; // خطأ: local يموت حين تعود الدالة
} // المؤشر المُعاد صار الآن معلّقاً
ما زال العنوان عدداً صالحاً، لكنه يشير إلى خانة من المكدّس (stack) أُعيد استرجاعها — وقراءته تعطي قمامة أو تنهار. ويحدث الأمر نفسه إن احتفظت بمؤشر إلى كائن في الكومة (heap) جرى عليه delete، أو إلى عنصر في vector أعاد التخصيص لاحقاً.
ثلاث قواعد تبقيك آمناً:
- لا تُعِد أبداً عنوان متغيّر محلّي. أعِد بالقيمة، أو اجعل الطرف المُستدعي مالكاً للتخزين.
- اضبط المؤشر على
nullptrبعد زوال ما يشير إليه، وتحقّق قبل الاستخدام. - لإدارة الملكية وأعمار الكائنات، الجأ إلى المؤشرات الذكية بدلاً من
new/deleteالمجرّدين — فهي تحرّر الذاكرة تلقائياً وتقلّص هذه الفئة كلّها من الأخطاء.
التالي: المراجع مقابل المؤشرات
ليست المؤشرات الوسيلة الوحيدة للإشارة إلى متغيّر آخر بشكل غير مباشر. ففي C++ أيضاً المراجع، التي تبدو مشابهة لكنها لا يمكن أن تكون فارغة، ولا يمكن إعادة ربطها، وتستخدم صياغة أنظف. وفي ما يلي سنضعهما جنباً إلى جنب في المراجع مقابل المؤشرات كي تعرف تماماً أيّ أداة تختار — ولماذا يفضّل معظم C++ الحديث المراجع متى استطاع استخدامها.
الأسئلة الشائعة
ما هو المؤشر في C++؟
المؤشر متغيّر يخزّن عنوان الذاكرة لقيمة أخرى بدلاً من القيمة نفسها. تعرّفه باستخدام * (مثل int* p)، وتأخذ عنواناً بعامل & (p = &x)، وتقرأ القيمة المُشار إليها أو تكتب فيها عبر إلغاء المرجعية بـ *p.
ما الفرق بين & و* في مؤشرات C++؟
في سياق المؤشرات، & هو عامل العنوان — فـ &x يعطيك عنوان x. أما * فيؤدي وظيفتين: في التعريف (int* p) يميّز المتغيّر كمؤشر، وفي التعبير (*p) يقوم بـإلغاء المرجعية للوصول إلى القيمة المخزّنة في ذلك العنوان.
ما هو nullptr في C++ ولماذا نستخدمه بدلاً من NULL؟
nullptr هو حرفية مؤشر فارغ آمنة نوعياً أُضيفت في C++11. ومعناها "لا يشير إلى شيء". فضّله على NULL القديم أو 0 المجرّد لأن nullptr يحمل نوع مؤشر حقيقي، فلا يُخلط أبداً مع عدد صحيح أثناء حلّ التحميل الزائد (overload resolution). تحقّق دائماً من if (p) قبل إلغاء المرجعية — فإلغاء مرجعية مؤشر فارغ سلوك غير معرّف.