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

حساب المؤشّرات في لغة C: الجمع والطرح والمرور على الذاكرة

إضافة 1 إلى مؤشّر لا تضيف بايتًا واحدًا - بل تنتقل إلى الكائن التالي من ذلك النوع. تغطّي هذه الصفحة ptr+1، والزيادة والإنقاص، وفروق المؤشّرات، والمقارنات، والمرور على مصفوفة بمؤشّر، وقاعدة الموضع التالي للنهاية.

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

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

وذلك القرار التصميمي الواحد هو سبب كون المرور على مصفوفة بمؤشّر طبيعيًا في C كالمرور عليها بفهرس.

إضافة 1 تتحرّك بكائن واحد لا ببايت واحد

خُذ مؤشّرًا، وأضف 1، واطبع العنوانين:

يقفز int * أربعة بايتات، ويقفز double * ثمانية، ويقفز char * واحدًا - ويهبط كلٌّ منها بالضبط حيث يبدأ الكائن التالي من نوعه. والقاعدة بسيطة:

ptr + n  ==  (address in ptr) + n * sizeof(*ptr)

وهذا بالضبط سبب أن يكون للمؤشّر نوع أصلًا. فالعنوان المجرّد بلا نوع مرفق لن يعرف كم يبعد "التالي".

المرور على مصفوفة بمؤشّر

والآن المكسب. تقع عناصر المصفوفة في ذاكرة متتالية، فخطو مؤشّر يزورها بالترتيب:

تطبع الحلقتان الأعداد الخمسة نفسها. وفي الثانية، تتحوّل scores إلى مؤشّر إلى عنصرها الأول، وتتقدّم p++ بمقدار int واحد، وتقرأ *p العنصر الموجود هناك. وبتوزيعه في الذاكرة:

 scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
|   88    |   92    |   75    |   60    |   100   |
+---------+---------+---------+---------+---------+
 ^         ^                                       ^
 p         p+1                                     scores+5
 (start)   (4 bytes later)                         (one past the end)

والعلاقة بين الصيغتين دقيقة وتستحق الحفظ: فـ scores[i] معرَّفة بأنها *(scores + i). والمزيد عن ذلك في المؤشّرات والمصفوفات.

الزيادة والإنقاص وفخّ *p++

تعمل ++ و-- على المؤشّرات بالقياس نفسه.

والتعبير الذي يربك الجميع هو *p++. فعامل ++ اللاحق يرتبط أوثق من *، فيعني "خُذ p الحالية، وقدّم p، ثم راجِع القيمة القديمة":

ثلاثة تعابير بثلاثة معانٍ مختلفة:

التعبيرالمؤشّر بعدهاالقيمة الناتجة
*p++تقدّمالعنصر الذي قبل الانتقال
*++pتقدّمالعنصر الذي بعد الانتقال
(*p)++دون تغييرالقيمة القديمة، ويُزاد العنصر

و*p++ أسلوب اصطلاحي في C - فهي كيف تُكتب نسخ السلاسل النصية والمخازن - لكن اكتب الأقواس حين تقصد أي شيء آخر.

طرح مؤشّرين

طرح مؤشّر من آخر يعطي عدد العناصر بينهما، لا عدد البايتات:

نوع النتيجة هو ptrdiff_t، وهو نوع صحيح موقّع من <stddef.h>، يُطبَع بـ %td. وطرح مؤشّرين إلى مصفوفتين مختلفتين ينتج نتيجة غير معرَّفة - فالمعيار لا يعرّف العملية إلا داخل كائن واحد.

ويمكنك أيضًا طرح عدد صحيح من مؤشّر (فـ p - 3 تتراجع ثلاثة عناصر)، لكنك لا تستطيع جمع مؤشّرين. فـ "مجموع عنوانين" لا يسمّي شيئًا.

مقارنة المؤشّرات

تعمل عوامل المقارنة على مؤشّرات داخل المصفوفة نفسها، وتعني بالضبط ما يوحي به توزيع الذاكرة: فـ p < q محقّقة حين تعنون p عنصرًا أسبق.

front < back هي كل قصة إنهاء الحلقة، وهي صحيحة لأن كلا المؤشّرين يبقى داخل مصفوفة واحدة. و== و!= هما أيضًا الطريقة الطبيعية لفحص حلقة مقابل علامة نهايتها، وهو ما يقودنا إلى القاعدة التي تجعل كل هذا مشروعًا.

قاعدة الموضع التالي للنهاية

تسمح C صراحةً بتكوين مؤشّر إلى الموضع الذي يلي بموضع واحد آخرَ عنصر في مصفوفة. ويمكنك حسابه وتخزينه والمقارنة به. لكن لا يمكنك مراجعته.

int arr[4];

 arr[0]   arr[1]   arr[2]   arr[3]   (no element)
+------+ +------+ +------+ +------+  +- - - -+
|      | |      | |      | |      |  |       |
+------+ +------+ +------+ +------+  +- - - -+
 ^                                    ^
 arr                                  arr + 4
 legal to use                         legal to FORM and COMPARE
                                      never legal to dereference

وتلك القاعدة هي ما يجعل شكل الحلقة المعياري صالحًا:

for (int *p = arr; p != arr + n; p++) {
    /* ... */
}

ففي التكرار الأخير تصبح p مساوية لـ arr + n، فتفشل المقارنة وتخرج الحلقة - دون قراءة ذلك الموضع إطلاقًا.

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

int *bad1 = arr + n + 1;   // undefined - too far past
int *bad2 = arr - 1;       // undefined - before the beginning

والثاني يهمّ عمليًا: فالحلقة العكسية المكتوبة for (int *p = arr + n - 1; p >= arr; p--) تحسب arr - 1 عند إنقاصها الأخير. وهي تعمل على كل مترجم شائع وتبقى غير معرَّفة رسميًا. والحلقة العكسية النظيفة تتجنّب ذلك:

مؤشّرات void والحسابات التي لا تستطيع إجراءها

يحمل void * عنوانًا بلا نوع مرفق، وهو كيف تبقى malloc وqsort عامّتين. ولأنه لا يوجد حجم عنصر، فإن حساب المؤشّرات على void * غير مسموح به في المعيار - حوّل إلى نوع محدّد (أو إلى char * للعمل على مستوى البايت) أولًا.

void *v = buffer;
// v + 1;              // not standard C - no size to scale by
char *b = v;
b + 1;                 // fine - one byte forward

ويقبل GCC وClang حسابات void * كامتداد يعاملها كـ char *، فالشيفرة المعتمِدة عليها تُترجَم حتى اليوم الذي تُبنى فيه في بيئة أكثر صرامة. ترجم بـ -std=c17 -pedantic إن أردت أن يُقال لك.

ما الذي يشتريه لك هذا

حساب المؤشّرات ليس حيلة تحسين تلجأ إليها أحيانًا - بل هو الآلية تحت فهرسة المصفوفات ومعالجة السلاسل النصية وكل مرور على مخزن في المكتبة القياسية. فـ strlen مؤشّر متقدّم إلى '\0' المُنهي ومطروح منه البداية. وmemcpy مؤشّران يخطوان معًا بالتزامن.

تاليًا، انظر كيف يعمل التكافؤ بين المصفوفات والمؤشّرات فعليًا، بما في ذلك فخّ sizeof الذي يوقع بالجميع أول مرة يمرّرون فيها مصفوفة إلى دالة: المؤشّرات والمصفوفات.

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

ماذا تفعل ptr + 1 في لغة C؟

تنقل المؤشّر إلى الأمام بمقدار كائن واحد، لا بايت واحد. فلمؤشّر int * على جهاز نموذجي هذا 4 بايتات؛ ولمؤشّر double * هو 8. ويضرب المترجم في sizeof(*ptr) نيابةً عنك، ولهذا يهمّ نوع المؤشّر.

هل يمكن طرح مؤشّرين في لغة C؟

نعم، حين يشير كلاهما إلى المصفوفة نفسها. فـ end - start تعطي عدد العناصر بينهما، من النوع ptrdiff_t (اطبعه بـ %td). أما طرح مؤشّرين إلى كائنين غير مترابطين فسلوك غير معرَّف.

هل يجوز الإشارة إلى موضع واحد بعد نهاية مصفوفة؟

نعم - فتكوين مؤشّر إلى عنصر واحد بعد الأخير مسموح صراحةً لتعمل حلقات مثل for (int *p = arr; p != arr + n; p++). ويمكنك حساب ذلك المؤشّر ومقارنته، لكن مراجعته سلوك غير معرَّف.

لماذا ليست *p++ مطابقة لـ (*p)++؟

*p++ تزيد المؤشّر وتراجع القيمة القديمة - فتقرأ العنصر الحالي ثم تتقدّم. أما (*p)++ فتترك المؤشّر وشأنه وتزيد القيمة التي يشير إليها. وعامل ++ اللاحق يرتبط أوثق من *، فالأقواس هي ما يغيّر المعنى.

Coddy programming languages illustration

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

ابدأ الآن