Menu
flag Ar iconالعربيةdown icon

المؤشّرات والمصفوفات في لغة C: التحوّل وarr[i] = *(arr+i) وفخّ sizeof

في لغة C يتحوّل اسم المصفوفة إلى مؤشّر إلى عنصرها الأول في كل موضع تقريبًا تستخدمه فيه. تشرح هذه الصفحة ذلك التحوّل، ولماذا تكون arr[i] حرفيًا *(arr+i)، ولماذا يجب أن يسافر حجم المصفوفة منفصلًا إلى الدوال، وكيف يختلف المؤشّر إلى مصفوفة عن مصفوفة المؤشّرات.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

المصفوفات والمؤشّرات شيئان مختلفان في C يتصرّفان على النحو نفسه في معظم الأحيان. و"معظم الأحيان" تلك هي ما يجعل الموضوع زلقًا: فالشيفرة التي تعامل مصفوفة كمؤشّر تعمل عادةً، حتى يأتي يوم تهبط فيه في أحد الموضعين اللذين يهمّ فيهما الفرق فتكون الإجابة خاطئة بصمت.

ترسم هذه الصفحة الخطّ بدقة.

تحوّل المصفوفة: القاعدة الواحدة وراء كل شيء

في كل تعبير تقريبًا، يُحوَّل اسم المصفوفة تلقائيًا إلى مؤشّر إلى عنصرها الأول. ويُسمّى ذلك التحوّل. فتصبح arr هي &arr[0]، من النوع int *.

يُطبَع اسم المصفوفة وعنوان عنصرها الأول متطابقين. وذلك هو التحوّل وهو يعمل، وهو سبب إمكان إسناد مصفوفة إلى مؤشّر دون &.

وهناك ثلاثة مواضع بالضبط لا يحدث فيها التحوّل: كمعامل لـ sizeof، وكمعامل لـ &، وعند تهيئة مصفوفة محارف من ثابت نصّي. وفي كل ما عداها، افترض التحوّل.

arr[i] هي حرفيًا *(arr + i)

يعرّف معيار C عامل الفهرسة بدلالة حساب المؤشّرات: فـ a[b] تعني *(a + b). والفهرسة ليست خاصية منفصلة - بل اختصار.

وتتبع ذلك نتيجتان. أولًا، يمكن فهرسة مؤشّر: فـ p[2] هي *(p + 2)، ولهذا تستطيع الدوال التي تستقبل مؤشّرًا أن تستخدم صياغة arr[i] المألوفة. وثانيًا، تُترجَم 2[arr] - فـ *(2 + arr) هو العنوان نفسه الذي تعنيه *(arr + 2). لا يكتب أحد ذلك خارج اختبار، لكنه يحسم مسألة ما إذا كانت الفهرسة "فعلًا" حسابَ مؤشّرات.

تمرير مصفوفة إلى دالة

بسبب التحوّل، لا تستقبل الدالة مصفوفة أبدًا. بل تستقبل مؤشّرًا.

ثلاثة أمور تستحق الاستخراج من ذلك المثال.

الحجم في int arr[100] توثيق، لا أكثر. فالمترجم يعيد كتابة الوسيط إلى int *arr ولا يفحص إطلاقًا أنك مرّرت مئة من أي شيء.

يجب أن يسافر الطول منفصلًا. فلدى الدالة عنوان ولا فكرة لها عن مدى امتداد البيانات. ولهذا تأخذ كل دالة C تقريبًا تستقبل مصفوفة عددًا: memcpy(dst, src, n)، وfread(ptr, size, count, f)، وqsort(base, nmemb, size, cmp). والسلاسل النصية استثناء فقط لأنها تحمل علامة نهايتها الخاصة، وهي '\0'.

تمرير مؤشّر رخيص والبيانات مشتركة. فلا تُنسَخ المصفوفة، ويستطيع بذلك الدالة تعديل عناصر المستدعي - وهو غالبًا ما تريده بالضبط:

ضع على الوسائط للقراءة فقط النوع const int * كما تفعل print_all. فذلك يوثّق القصد ويتيح للمترجم التقاط كتابة عرضية.

فخّ sizeof

إليك الخطأ الذي يوقع بكل مبرمج C مرة. sizeof أحد الموضعين اللذين لا يحدث فيهما التحوّل - فهي تعطي حجم المصفوفة الحقيقي حيث تُعرَّف المصفوفة، وحجم مؤشّر في أي موضع تحوّلت فيه المصفوفة.

في main، تكون sizeof data أربعين بايتًا ويُحسَب الطول بعشرة. وداخل inspect، تكون sizeof arr حجم مؤشّر - 8 على جهاز 64 بتًا - ويخرج "الطول" اثنين. تبدو الشيفرة متطابقة وهي خاطئة بهدوء.

والقاعدة التي تتبع ذلك: احسب طول مصفوفة فقط في النطاق الذي عُرِّفت فيه المصفوفة، ومرّره من هناك.

& مصفوفة ليست النوع نفسه

الموضع الآخر الذي لا يحدث فيه التحوّل هو العامل &. فـ &arr مؤشّر إلى المصفوفة كاملة، لا إلى عنصرها الأول. ويحمل الاثنان العنوان نفسه؛ والفرق هو ما تعنيه "أضف 1".

عنوان بداية واحد وخطوة مختلفة. فـ arr من النوع int * وتخطو بعنصر واحد؛ و&arr من النوع int (*)[5] وتخطو بالمصفوفة كاملة. وهذه هي الآلية التي تجعل تمرير صفّ من مصفوفة ثنائية الأبعاد ممكنًا، وهو ما تغطّيه المصفوفات متعدّدة الأبعاد.

مؤشّر إلى مصفوفة مقابل مصفوفة مؤشّرات

تصريحان يبدوان متشابهين ويعنيان شيئين متضادّين:

int *p[5];     // array of 5 pointers to int
int (*q)[5];   // pointer to an array of 5 ints

أولوية [] أعلى من *، فتُحلَّل int *p[5] بمعنى "p مصفوفة، من مؤشّرات". أما الأقواس في int (*q)[5] فتجبر * على الارتباط أولًا: "q مؤشّر، إلى مصفوفة".

int *p[5]                     int (*q)[5]

 p[0] -> [ int ]               q -> [ int | int | int | int | int ]
 p[1] -> [ int ]                     one contiguous block of 5
 p[2] -> [ int ]
 p[3] -> [ int ]
 p[4] -> [ int ]
 five separate addresses

ومصفوفة المؤشّرات هي الشائعة، وقد استخدمتها سلفًا: فـ char *argv[] مصفوفة من مؤشّرات إلى سلاسل نصية، واحد لكل وسيط من وسائط سطر الأوامر. وهي أيضًا كيف تحتفظ بقائمة من سلاسل نصية مختلفة الأطوال دون إهدار مساحة.

لاحظ (*q)[0]: راجِع المؤشّر لتحصل على المصفوفة، ثم افهرسها. والأقواس مطلوبة مجددًا، لسبب الأولوية نفسه.

أين تختلف المصفوفات والمؤشّرات فعلًا

احفظ هذا الجدول القصير فيزول اللبس:

المصفوفة int arr[10]المؤشّر int *p
ما هو10 أعداد int موزّعة في الذاكرةمتغيّر واحد يحمل عنوانًا
sizeof40 - الكتلة كاملة8 - المؤشّر فقط
قابل للإسنادلا: arr = x; خطأنعم: p = x; مقبول
& تعطيint (*)[10]int **
من أين تأتي الذاكرةتُعرَّف مع المصفوفةمن حيث توجّهه

اسم المصفوفة ليس متغيّرًا يحمل عنوانًا - بل هو التخزين، ويُحسَب العنوان عند الطلب. ولهذا لا تستطيع الإسناد إليه.

الأسئلة الشائعة

هل المصفوفات والمؤشّرات شيء واحد في لغة C؟

لا، لكن الخلط بينهما سهل لأن اسم المصفوفة يتحوّل إلى مؤشّر إلى عنصرها الأول في كل تعبير تقريبًا. فالمصفوفة نفسها كتلة من العناصر بحجم معروف؛ والمؤشّر متغيّر واحد يحمل عنوانًا واحدًا. وsizeof و& هما الموضعان اللذان يظهر فيهما الفرق.

لماذا تعطي sizeof(arr) إجابة خاطئة داخل دالة؟

لأن الوسيط ليس مصفوفة. فـ void f(int arr[]) تُعاد كتابتها بصمت إلى void f(int *arr)، فتقيس sizeof(arr) مؤشّرًا (8 بايتات على معظم الأجهزة) لا المصفوفة الأصلية. مرّر الطول كوسيط منفصل.

ما الفرق بين int *p[5] وint (*p)[5]؟

int *p[5] مصفوفة من 5 مؤشّرات إلى int. وint (*p)[5] مؤشّر واحد إلى مصفوفة من 5 أعداد int. فالأقواس تربط * بالاسم أولًا؛ وبدونها تفوز [] لأن أولويتها أعلى.

هل arr[i] هي فعلًا نفسها *(arr + i)؟

نعم - يعرّف المعيار a[b] بأنها *(a + b). ولهذا أيضًا تُترجَم i[arr] الغريبة المظهر وتعمل: فالجمع تبديلي، إذًا *(i + arr) هو العنصر نفسه. لا تكتبها أبدًا في شيفرة حقيقية، لكنها تثبت القاعدة.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن