كل متغيّر في برنامجك يعيش في موضع ما من الذاكرة، ولكل "موضع" رقم - هو عنوانه. والمؤشّر ليس إلا متغيّرًا يخزّن واحدًا من تلك الأرقام. تلك هي الفكرة بأكملها. وكل ما عداها في هذه الصفحة صياغة ونتائج.
وسبب أهمية المؤشّرات البالغة في C هو أن C تسلّم كل دالة نسخة من وسائطها. فالدالة التي أُعطيت العدد 7 تستطيع تغيير نسختها كما تشاء؛ ومتغيّر المستدعي لا يتحرّك أبدًا. أعطِ الدالة عنوانًا بدلًا من ذلك، فتستطيع أن تمتدّ وتغيّر الأصل. والمصفوفات والسلاسل النصية والذاكرة الديناميكية وكل بنية بيانات ستبنيها يومًا في C تقوم على تلك القدرة الواحدة.
الذاكرة والعناوين ولماذا لكل شيء عنوان
حين تكتب int score = 42;، يحجز المترجم بضعة بايتات من الذاكرة ويمنحها اسمًا تستطيع استخدامه. ولتلك البايتات أيضًا عنوان رقمي. تصوّر الذاكرة شارعًا طويلًا جدًا من البيوت المرقّمة: فـ score هي الأسرة الساكنة في البيت رقم 0x7ffd4c2a - الاسم لك، والرقم للآلة.
ويطلب العامل & ذلك الرقم:
شغّله. العناوين الثلاثة مختلفة لأن المتغيّرات الثلاثة تشغل بايتات مختلفة، وستتغيّر من تشغيل إلى آخر - وهذا طبيعي، وهو سبب ألّا تكتب عنوانًا ثابتًا في الشيفرة أبدًا. ولاحظ التحويل (void *): فـ %p محدَّد ليأخذ void *، وتمرير أي نوع مؤشّر آخر سلوك غير معرَّف تقنيًا. اجعل التحويل عادة.
وكون العناوين غير قابلة للتنبّؤ ليس مشكلة، لأنك لا تكاد تهتمّ أبدًا بما هو العنوان. بل تهتمّ بأنك تملكه.
تعريف مؤشّر
يسمّي تعريف المؤشّر النوعَ الذي يشير إليه:
int *p; // تستطيع p حمل عنوان int
double *q; // تستطيع q حمل عنوان double
char *name; // تستطيع name حمل عنوان char
اقرأ int *p; هكذا: "*p من نوع int" - فمراجعة p تعطيك عددًا صحيحًا. وتلك القراءة تصمد أمام كل تعريف معقّد قد ترميك به C لاحقًا.
وموضع * اختيار أسلوبي يتجاهله المترجم: فـ int* p وint *p وint * p كلها تعرّف الشيء نفسه. وتكتب معظم شيفرة C int *p، ولذلك سبب عملي:
int* a, b; // a مؤشّر إلى int؛ وb عدد int عادي، لا مؤشّر
int *a, *b; // كلاهما مؤشّران - فـ * ترتبط بالاسم لا بالنوع
ترتبط * بالمعرِّف لا بالنوع. وكتابتها بجوار الاسم تُبقي ذلك مرئيًا.
& و*: العاملان
عاملان يؤدّيان كل العمل، وهما نقيضان تمامًا.
&x- عنوان. ينتج العنوان الذي تعيش فيهx.*p- مراجعة. يذهب إلى العنوان فيpويعطيك الكائن الساكن هناك.
السطران الأخيران هما كل المقصود من المؤشّرات. فلم يُسنَد شيء إلى age مباشرةً، ومع ذلك تغيّرت age - لأن *p = 31 تعني "خزّن 31 في العنوان الذي تحمله p"، وذلك العنوان هو عنوان age.
وهذه هي الصورة في الذاكرة:
p age
+------------+ +----------+
| 0x7ffd1c40 | --------> | 31 |
+------------+ +----------+
at 0x7ffd1c38 at 0x7ffd1c40
p -> the address 0x7ffd1c40
*p -> the int stored there, 31
&age -> 0x7ffd1c40, the same address p holds
&p -> 0x7ffd1c38, where p itself lives
لاحظ السطر الأخير: المؤشّر نفسه متغيّر في موضع ما من الذاكرة، فله عنوانه الخاص أيضًا. وهذا ليس سؤالًا ملغّمًا، بل مجرّد اتّساق - وهو كيفية عمل المؤشّرات إلى المؤشّرات (int **).
ولأن & و* يلغي كلٌّ منهما الآخر، فإن *&age ليست إلا age، و&*p ليست إلا p.
لماذا تهمّ أنواع المؤشّرات
يخزّن المؤشّر عنوانًا، والعنوان ليس إلا رقمًا - فلماذا يصرّ المترجم على معرفة ما إذا كان يشير إلى int أم double؟ لسببين:
- كم بايتًا يقرأ. فمراجعة
int *تقرأ 4 بايتات (عادةً)؛ ومراجعةdouble *تقرأ 8. وبدون النوع لا معنى لـ*p. - كيف يفسّر تلك البايتات. فنمط البتات نفسه عدد ما كـ
intوعدد مختلف كليًا كـfloat.
أما حجم المؤشّر نفسه فلا يعتمد على ما يشير إليه - فالعنوان عنوان:
على جهاز 64 بتًا تكون المؤشّرات الثلاثة كلها 8 بايتات، بينما ما تشير إليه 4 و8 و1. والنوع المشار إليه هو ما يعطي *p وحساب المؤشّرات أي معنى.
والإسناد عبر أنواع مؤشّرات مختلفة خطأ سيشتكي منه المترجم، ولسبب وجيه - فـ pi = &d; كانت ستجهّزك لقراءة 4 بايتات من double وتسمية النتيجة عددًا صحيحًا.
الدافع النموذجي: swap()
إليك الدالة التي تلجأ إليها كل دورة في C، لأنها تفشل بالضبط بالطريقة التي تشرح المؤشّرات.
تبلّغ الدالة بأنها بدّلتهما - وتبلّغ main بأنه لم يحدث شيء. فقد استقبلت broken_swap نسخًا من x وy. وبدّلت نسختيها بإتقان، ثم زالت تلك النسختان حين عادت الدالة.
سلّمها العناوين بدلًا من ذلك:
الآن ما تزال a وb نسختين - نسختين من عنوانين - لكن نسخة العنوان تشير إلى المتغيّر نفسه الذي أشار إليه الأصل. فـ *a هي x، من أي موضع استُدعيت الدالة.
وهذا هو النمط وراء كل دالة C تعدّل بيانات مستدعيها، ووراء وسائط الدوال عمومًا: لا يوجد في C تمرير بالمرجع، فتمرّر مؤشّرًا وتراجعه. ولهذا أيضًا تحتاج scanf إلى &:
int n;
scanf("%d", &n); // scanf must be able to write into n, so it needs n's address
إرجاع أكثر من قيمة
تُرجِع دالة C قيمة واحدة. والمؤشّرات هي كيفية الالتفاف حول ذلك: مرّر عناوين للنتائج الإضافية.
تحمل القيمة المُرجَعة الحالة وتحمل المؤشّرات النتائج. وذلك الفصل موجود في كل مكان في مكتبة C القياسية نفسها وفي واجهات أنظمة التشغيل.
مؤشّرات إلى مؤشّرات
ولأن المؤشّر متغيّر، فيمكنك أخذ عنوانه، ونوع ذلك العنوان هو "مؤشّر إلى مؤشّر إلى int":
pp p value
+------+ +------+ +------+
| &p | ----> | &val | -----> | 8 |
+------+ +------+ +------+
وستلتقي بـ ** فعليًا حين تحتاج دالة إلى تغيير مؤشّر يحمله المستدعي - مثل مخصِّص يضبط مؤشّر المستدعي على ذاكرة جديدة - وفي char *argv[] في وسائط سطر الأوامر.
أربعة أخطاء تستحق المعرفة قبل ارتكابها
استخدام مؤشّر لم يُوجَّه إلى أي مكان قط. المؤشّر غير المهيّأ يحمل البايتات التي كانت هناك سلفًا. ومراجعته تقرأ عنوانًا عشوائيًا أو تكتب فيه.
int *p; // p holds garbage
*p = 10; // undefined behavior - likely a crash
هيّئ كل مؤشّر، بعنوان حقيقي أو بـ NULL. انظر المؤشّرات الفارغة للانضباط المحيط بذلك.
الخلط بين *p وp. فـ p = 5; تضبط المؤشّر على العنوان 5 (وهذا هراء)؛ و*p = 5; تخزّن 5 حيث يشير. وسيحذّر المترجم من الأولى، والتحذيرات تستحق القراءة.
نسيان & في scanf. فـ scanf("%d", n) تمرّر قيمة n كعنوان. وتُترجَم مع تحذير ثم تكتب في أي ذاكرة يسمّيها ذلك الرقم.
إرجاع عنوان متغيّر محلّي. فالمتغيّر المحلّي يزول لحظة عودة الدالة؛ والعنوان إيصال لعقار مهدوم.
int *bad(void) {
int local = 42;
return &local; // the caller gets a dangling pointer
}
وإن احتجت إلى ذاكرة تعمّر أطول من الاستدعاء، فخصّصها - وذلك ما وُجدت الذاكرة الديناميكية من أجله.
إلى أين بعد ذلك
لا تتوقّف المؤشّرات عند "حمل عنوان واحد". فالإضافة إلى مؤشّر تخطو به عبر الذاكرة بوحدات نوعه، وهو موضوع حساب المؤشّرات؛ واسم المصفوفة في C يتحوّل إلى مؤشّر إلى عنصرها الأول، ولهذا تكون المؤشّرات والمصفوفات فعليًا الموضوع نفسه بمجرّد معرفتك الاثنين. اقرأ هذين تاليًا، فتكفّ بقية C عن أن تبدو سحرًا.
الأسئلة الشائعة
ما المؤشّر في لغة C؟
المؤشّر متغيّر قيمته عنوان في الذاكرة - أي موقع كائن آخر في الذاكرة. فـ int *p = &age; تجعل p تحمل عنوان age، و*p عندئذٍ تقرأ المتغيّر age أو تكتب فيه عبر ذلك العنوان.
ما الفرق بين * و& في لغة C؟
&x عامل العنوان: ينتج العنوان الذي تعيش فيه x. و*p عامل المراجعة: يذهب إلى العنوان المخزَّن في p ويعطيك الكائن الموجود هناك. وكلٌّ منهما يلغي الآخر، فـ *&x ليست إلا x.
لماذا تظهر * مرتين - في التعريف وعند استخدام المؤشّر؟
هما استخدامان مختلفان للرمز نفسه. ففي int *p; تكون * جزءًا من النوع، بمعنى "p مؤشّر إلى int". وفي *p = 5; تكون * عامل المراجعة، بمعنى "خزّن 5 في العنوان الذي تحمله p". وقراءة التعريف على أنه "*p من نوع int" تجعل الاثنين مفهومين.
لماذا أحتاج إلى المؤشّرات في لغة C؟
تمرّر C كل وسيط بالقيمة، فلا تستطيع دالة تغيير متغيّر المستدعي إلا إذا سُلِّمت العنوان. وتتيح لك المؤشّرات أيضًا المرور على المصفوفات بكفاءة، وإرجاع أكثر من نتيجة، وبناء بنى مترابطة، واستخدام ذاكرة مخصَّصة في وقت التشغيل بـ malloc.