اذهب إلى المحتوى

المؤشرات Pointers في لغة سي C


Naser Dakhel

يشابه استخدام المؤشرات Pointers في لغة سي عملية تعلُّم قيادة الدراجة الهوائية، فعندما تصل إلى النقطة التي تعتقد أنك لن تتعلمها أبدًا، تبدأ بإتقانها، وبعد أن تتعلمها سيكون من الصعب نسيانها. لا يوجد هناك أي شيء مميّز بخصوص المؤشرات، ونعتقد أن معظم القرّاء يعرفون عنها مسبقًا، وفي الحقيقة، واحدةٌ من ميزات لغة سي هي اعتمادها الكبير على استخدام المؤشرات مقارنةً باللغات الأخرى، إضافةً إلى الأشياء الأخرى الممكن إنجازها بواسطة المؤشرات بسهولة ودون قيود إلى حدٍّ ما.

التصريح عن المؤشرات

ينبغي التصريح عن المؤشرات قبل استخدامها بصورةٍ مماثلة لأي متغير آخر تعاملنا معه مسبقًا. يتشابه التصريح عن المؤشر مع أي تصريح آخر، ولكن هناك نقطة مهمّة، إذ تدلّ الكلمة المفتاحية عند التصريح عن المؤشر في البداية، مثل int أوchar وغيرها عن نوع المتغير الذي سيشير المؤشر إليه، وليس نوع المؤشر بذات نفسه، ويشير المؤشّر على قيمة واحدة كل مرة من ذلك النوع وليس جميع القيم من النوع ذاته. إليك مثالًا يوضح التصريح عن مصفوفة ومؤشر:

int ar[5], *ip;

يصبح لدينا بعد التصريح مصفوفة ومؤشر، كما يوضح الشكل 1:

004ArrayPointer.png

شكل 1 مصفوفة ومؤشر

يوضح الرمز * الموجود أمام ip أن هذا مؤشر، وليس متغيرًا اعتياديًا، وهو مؤشرٌ من النوع pointer to int، أي يشير إلى قيمة من نوع int فقط، لكن لم تُسند له قيمة أوليّة بعد، ولا يمكننا استخدامه في هذه الحالة قبل أن نجعله يؤشّر على قيمةٍ ما. لاحظ أنه لا يمكنك تعيين قيمةٍ من نوع int فورًا، لأن القيم الصحيحة لها النوع int ونحن نريد هنا قيمةً من نوع "مؤشر إلى نوع صحيح pointer to int". لكن، ما الذي سيشير إليه ip في هذه الحالة إذا كانت التعليمة التالية صحيحة ؟

ip = 6;

قد تختلف الإجابة هنا، ولا يوجد إجابةً واحدةً عمّا قد يشير إليه ip، ولكن خلاصة الأمر أن هذا النوع من الإسناد غير صحيح في لغة سي.

إليك الطريقة الصحيحة لإسناد قيمة أولية لمؤشر ما:

int ar[5], *ip;
ip = &ar[3];

يؤشّر المؤشر في هذا المثال إلى عنصرٍ من المصفوفة ar بدليل 3، أي العنصر الرابع من المصفوفة.

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

005ArrayInitializedPointer.png

شكل 2 مصفوفة ومؤشر مهيّأ

نلاحظ أن قيمة المتغير ip مساوية لقيمة التعبير ‎&ar[3]‎، ويشير السهم إلى أن المؤشر ip يشير إلى المتغير [ar[3‏.

لكن ما الذي يعنيه العامل الأحادي &؟ يُشار إلى هذا العامل بكونه عامل "عنوان المتغير"، إذ أن المؤشرات تخزّن عنوان المتغير الذي تؤشّر عليه في معظم الأنظمة. ربّما ستواجه صعوبةً بخصوص هذا الأمر إذا كنت تفهم ما نعني هنا بالعنوان مقارنةً بالأشخاص الذين لا يفهمون هذا الأمر، إذ أن التفكير بالمؤشرات كونها عناوينًا يؤدي إلى كثيرٍ من المشاكل في الفهم.

قد تكون عملية التلاعب بعناوين معالج "أ" مستحيلةً على متحكّم آلة غسيل من نوع "ب" يستخدم عناوينًا بسعة 17-بِت عندما تكون في طور الغسيل، ويقلب ترتيب البِتات الزوجية والفردية عندما ينفد من مسحوق الغسيل. من المستبعد لأي أحد أن يستخدم لغة سي بمعمارية مشابهة لمثالنا السابق، ولكن هناك بعض الحالات الأخرى والأقل شدّة التي قد يمكن تشغيل لغة سي على معماريتها.

لكننا سنتابع استخدام الكلمة "عنوان المتغير"، لأن استخدام مصطلح أو كلمة مغايرة لذلك سيتسبب بمزيدٍ من المشكلات.

يعيد تطبيق العامل & لمعامل ما مؤشّرًا لهذا المعامل:

int i;
float f;
      /* مؤشر إلى عدد صحيح '&i' */
      /* مؤشر إلى عدد حقيقي '&f'*/

وسيشير المؤشر في كل حالة إلى الكائن الذي يوافق اسمه اسم المستخدم في التعبير.

المؤشر مفيدٌ فقط في حال وجود طريقة للوصول إلى الشيء الذي يشير إليه، وتستخدم لغة سي العامل الأحادي * لتحقيق ذلك؛ فإذا كان p من نوع "مؤشر إلى نوعٍ ما pointer to something"، فيشير التعبير ‎*p‎ إلى الشيء الذي يشير إليه ذلك المؤشر. على سبيل المثال، نتبع مايلي للوصول إلى المتغير x باستخدام المؤشر p:

#include <stdio.h>
#include <stdlib.h>
main(){
      int x, *p;

      p = &x;        //تهيئة المؤشر 
     *p = 0;         // ‪ x إسناد القيمة 0 إلى المتغير
     printf("x is %d\n", x);
      printf("*p is %d\n", *p);

      *p += 1;        /* زيادة القيمة التي يشير إليها المؤشر */
      printf("x is %d\n", x);

      (*p)++;         /* زيادة القيمة التي يشير إليها المؤشر */
      printf("x is %d\n", x);

      exit(EXIT_SUCCESS);
}

[مثال 1]

من الجدير بالذكر معرفة أن استخدام التركيب المؤلف من & و * بالشكل &* يلغي تأثير كلٍّ منهما، لأن & تعيد عنوان الكائن أي قيمة مؤشّره، و* تعني "القيمة التي يشير إليها المؤشر". لكن انتبه، فليس لبعض الأشياء مثل الثوابت أي عنوان، وبذلك لا يمكن تطبيق العامل & عليها، والتعبير 1.5& ليسَ مؤشّرًا بل تعبيرًا خاطئًا. من المثير للانتباه أيضًا إلى أن لغة سي من اللغات القليلة التي تسمح بوجود تعبير على الجانب الأيسر من عامل الإسناد. انظر مجدّدًا إلى المثال؛ إذ نصادف التعبير p* مرتين، ومن ثم التعليمة ‎(‎*p)++‎;‎ المثيرة للاهتمام، وستثير هذه التساؤلات من معظم المبتدئين حتى لو استطعت فهم أن التعليمة p = 0* تسند القيمة 0 إلى المتغير المُشار إليه بواسطة المؤشر p، وأن التعليمة p += 1* تضيف واحدًا إلى المتغير المُشار إليه بالمؤشر p، فاستخدام العامل ++ مع p* يبدو صعب الفهم قليلًا.

تستحق أسبقية التعبير ++(p*) النظر إليها بدقة، وسنناقش مزيدًا من التفاصيل بهذا الخصوص، ولكن دعونا نركّز عمّا يحدث في هذا المثال تحديدًا. تُستخدم الأقواس للتأكد بأن * تُطبَّق على p فقط، ومن ثم تحدث زيادةً بمقدار واحد على الشيء المُشار إليه بالمؤشر p، وبالنظر إلى جدول الأسبقية في مقال العوامل في لغة سي C نلاحظ أن للعاملين ++ و* الأسبقية ذاتها، ولكن العاملين يرتبطان من اليمين إلى اليسار، وبمعنى آخر تصبح العملية بالتخلص من الأقواس مكافئةً للعملية (++p)*، وبغض النظر عن معنى هذه العملية (التي سنتكلم عن معناها لاحقًا)، لا بُدّ من الحفاظ على الأقواس في هذه الحالة والانتباه إلى مواضعها الصحيحة.

لذا، وبما أن المؤشر يعطي عنوان الشيء الذي يشير إليه، فاستخدام pointer* (إذ أن pointer هو أيضًا مؤشر) يعيد الشيء بذاته مباشرةً، ولكن ما الذي نستفيد من ذلك؟ أول الأمور التي نستفيد منها هي تجاوز قيد الاستدعاء بالقيمة call-by-value عند استخدام الدوال؛ فعلى سبيل المثال تخيل دالةً تعيد قيمتين ممثلتين بأعداد صحيحة تمثّل الشهر واليوم لهذا الشهر، وأن لهذه الدالة طريقةً (غير محددة) لتحديد هذه القيم، والتحدي هنا هو إعادة قيمتين منفصلتين بنفس الوقت. إليك طريقةً لتجاوز هذه العقبة بالمثال:

#include <stdio.h>
#include <stdlib.h>
void
date(int *, int *);     /* التصريح عن الدالة */

main(){
      int month, day;
      date (&day, &month);
      printf("day is %d, month is %d\n", day, month);
      exit(EXIT_SUCCESS);
}

void
date(int *day_p, int *month_p){
      int day_ret, month_ret;
      /*حساب قيمة day و month في day_ret و month_ret*/
      *day_p = day_ret;
      *month_p = month_ret;
}

[مثال 2]

لاحظ طريقة التصريح عن date المتقدمة، التي توضح أنها دالةٌ تأخذ وسيطين من نوع "مؤشر إلى قيمة من نوع int"، وتعيد void لأن القيم التي تُمرّر بواسطة المؤشرات ليست من نوع قيمة اعتيادية. تمرِّر الدالة main المؤشرات إلى الدالة date على أنها وسطاء باستخدام المتغيرات الداخلية day_ret و month_ret، ومن ثم تأخذ قيمتهما وتسندها إلى الأماكن التي تشير إليها وسطاء الدالة (المؤشرات).

يوضح الشكل 3 ما الذي يحدث عند استدعاء الدالة date:

006dateCall.png

شكل 3 عند استدعاء الدالة date

تُمرَّر الوسطاء إلى date، ولكن المتغيرين day و month غير مهيّأين بقيمةٍ أولية ضمن الدالة main. يوضح الشكل 4 ما الذي يحدث عندما تصل الدالة date إلى تعليمة return، بفرض أن قيمة day هي "12" وقيمة month هي "5".

007dateReturn.png

شكل 4 عندما تصل الدالة date إلى تعليمة return

واحدةٌ من المزايا الرائعة التي قدّمتها لغة سي المعيارية هي السماح بالتصريح عن أنواع وسطاء دالة date مسبقًا، إذ كان نسيان أن الدالة تقبل مؤشرات وسطاءً لها وتمرير نوع آخر أمرًا شائع الحدوث. تخيل استدعاء الدالة date دون أي تصريح واضح مُسبقٍ لها على النحو التالي:

date(day, month);

لن يعرف المصرّف هنا أن الدالة تقبل المؤشرات وسطاءً لها وستُمرر قيمًا من نوع int افتراضيًا لكل من day و month، وبما أن تمرير المؤشرات والأعداد الصحيحة يجري على معظم الحواسيب بالطريقة نفسها، لذا ستُنفَّذ الدالة في هذه الحالة، ثم تعيد القيم في النهاية وتسندها إلى المكان الذي يشير إليه كلًا من day و month، إذا كانا مؤشرين. لن يعطي ذلك أي نتيجة بل وربما يتسبب بضرر مفاجئ للبيانات في مكان آخر بذاكرة الحاسوب، وتعقُّب هذا النوع من الأخطاء صعبٌ جدًا.

لحسن الحظ، يمكن أن يعرف المصرّف المعلومات الكافية عن date بالتصريح عن الدالة مسبقًا، وذلك من شأنه أن يحذره بخصوص أي أخطاء مرتكبة.

قد يفاجئك سماع أن المؤشرات لا تستخدم كثيرًا لتمكين طريقة الاستدعاء بالإشارة call-by-reference، إذ يُعدّ استخدام الاستدعاء بالقيمة call-by-value وإعادة قيمة واحدة باستخدام return كافٍ في معظم الحالات، والاستخدام الأكثر شيوعًا للمؤشرات هو الانتقال ما بين المصفوفات.

المصفوفات والمؤشرات

تملك عناصر المصفوفة عناوينًا مثل أي متغير اعتيادي آخر.

int ar[20], *ip;

ip = &ar[5];
*ip = 0;        // ‫يكافئ التعبير ar[5] = 0;‎ 

في المثال السابق، يُخزَّن عنوان العنصر ar[5]‎ في المؤشر ip، ثم تُسند القيمة صفر إلى موضع المؤشر في السطر الذي يليه. هذا الشيء غير مثير للإعجاب بحد ذاته، بل إن طريقة عمل العمليات الحسابية والمؤشر سويًّا هي التي تستدعي الاهتمام، فعلى الرغم من بساطة هذا الأمر، إلا أنه يُعد واحدًا من أساسات لغة سي المميزة.

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

*(ip+1) = 0;

تغيير قيمة ar[6]‎ إلى صفر، وهكذا. لا تعدّ هذه الطريقة تحسينًا على طرق الوصول إلى عناصر المصفوفة الاعتيادية، إليك مثالًا عن ذلك بدلًا من السابق:

int ar[20], *ip;

for(ip = &ar[0]; ip < &ar[20]; ip++)
      *ip = 0;

يوضّح المثال السابق ميزةً كلاسيكيةً للغة سي، إذ يشير المؤشر إلى بداية المصفوفة، وبينما يشير المؤشر إلى المصفوفة يمكن الوصول إلى عناصر المصفوفة واحدًا تلو الآخر بزيادة المؤشر بمقدار واحد كلّ مرة. يدعم معيار سي بعض الممارسات الموجودة وذلك بالسماح لاستخدام عنوان العنصر ar[20]‎ على الرغم من عدم وجود هذا العنصر، ويسمح لك هذا باستخدام المؤشرات لاختبار حدود المصفوفة ضمن الحلقات التكرارية كما هو الحال في المثال السابق، إذ أن ضمان عمله يمتد لعنصر واحد فقط خارج نهاية المصفوفة وليس أكثر من ذلك.

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

int ar[20], i;
for(i = 0; i < 20; i++)
      ar[i] = 0;

تحدث العمليات الحسابية ذاتها في الحلقة التكرارية كما في المثال السابق، لكن بإضافة حسابات العنوان المُجراة كل دورة في الحلقة.

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

إذا استوعبت جميع ما قرأته حتى الآن فتابع معنا، وإلّا فتجاوز هذا القسم واذهب للمقال التالي، فعلى الرغم من المعلومات المثيرة للاهتمام في الأقسام التالية إلا أنها غير ضرورية وتُعرَف برهبتها لمبرمجي لغة سي المتمرسين حتى.

في حقيقة الأمر، لا "تفهم" لغة سي مبدأ الوصول لعناصر المصفوفة باستخدام أدلتها (باستثناء نقطة التصريح عن المصفوفة)، فالتعبير x[n]‎ يُترجم بالنسبة للمصرّف على النحو التالي: (x+n)*، إذ يُحوَّل اسم المصفوفة إلى مؤشر يشير إلى العنصر الأول للمصفوفة وذلك أينما وجد اسم المصفوفة ضمن تعبير ما. يُعد هذا سببٌ من ضمن أسباب أخرى لبدء عناصر المصفوفة بالرقم صفر؛ فإذا كان x اسم المصفوفة، سيكون التعبير ‎&x[0]‎‎ مساوٍ للمصفوفة x، أي مؤشر إلى العنصر الأول من المصفوفة.

نستطيع الوصول إلى x[0]‎ باستخدام المؤشرات باستخدام التعبير ‎*(‎&x[0]‎)‎، مما يعني أن ‎*‎(&x[0] + 5‎‎)‎ مساوٍ للتعبير ‎*(x + 5)‎ وهو ذات التعبير [x[5. يفتح ذلك الأمر سيلًا من التساؤلات عن الإمكانيات الناتجة، فإذا كان التعبير x[5]‎ يُترجم إلى ‎*(x + 5)‎ والتعبير x + 5 يعطي النتيجة ذاتها للتعبير:

5 + x 

فالتعبير 5[x] مساوٍ للتعبير x[5]‎. إليك برنامجًا يُصرّف وينفذ دون أي أخطاء إن لم تصدق هذا الأمر:

#include <stdio.h>
#include <stdlib.h>
#define ARSZ 20
main(){
      int ar[ARSZ], i;
      for(i = 0; i < ARSZ; i++){
              ar[i] = i;
              i[ar]++;
              printf("ar[%d] now = %d\n", i, ar[i]);
      }

      printf("15[ar] = %d\n", 15[ar]);
      exit(EXIT_SUCCESS);
}

[مثال 3]

لتلخيص ما سبق:

  • تبدأ العناصر في أي مصفوفة من الدليل ذي الرقم صفر.
  • ليس هناك أي وجود للمصفوفات متعددة الأبعاد، بل هي في حقيقة الأمر مصفوفاتٌ تحتوي على مصفوفات.
  • تُشير المؤشرات إلى أشياء، والمؤشرات التي تشير إلى أشياء من أنواع مختلفة هي بدورها من أنواع مختلفة أيضًا ولا يوجد أي تشابه بين الأنواع المختلفة في لغة سي وأي تحويل ضمني تلقائي بينهما.
  • يمكن استخدام المؤشرات لمحاكاة استخدام الاستدعاء بالإشارة ضمن الدوال، ولكن الأمر يستغرق بعضًا من الجهد لتحقيقه.
  • تُستخدم زيادة أو نقصان قيمة مؤشر ما للتنقل بين عناصر المصفوفة.
  • يضمن المعيار أن محاولة الوصول إلى العنصر ذي الدليل "n" في مصفوفة ذات حجم "n" محاولةٌ صالحة على الرغم من عدم وجود هذا العنصر وذلك لتسهيل أمر التنقل داخل المصفوفة بزيادة قيمة المؤشر، ويكون مجال قيم مصفوفة مصرّح عنها على النحو int ar[N]‎ هو ‎&ar[0]‎ وصولًا إلى ‎&ar[N]‎ ولكن يجب عليك تفادي الوصول إلى قيمة العنصر الأخير الزائف.

الأنواع المؤهلة Qualified

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

قدم المعيار شيئان باسم مؤهلات النوع type qualifiers، إذ لم يكونا في لغة سي القديمة مسبقًا، ويمكن تطبيقهما لأي نوع مصرّحٌ عنه للتعديل من تصرفه، ومن هنا أتى اسمهما. سنتجاهل إحداهما (المدعو باسم "volatile") إلا أنه لا يمكننا تجاهل الآخر "const".

إذا سبقت الكلمة المفتاحية "const" أي تصريح، فهذا يعني أن الشيء الذي صُرّح عنه هو ثابت، ويجب عليك تجنب محاولة التغيير على قيمة الكائنات الثابتة "const" وإلا فستحصل على سلوك غير محدّد undefined behaviour، وسيحذّرك المصرّف عادةً بمنعك من هذه المحاولة إلا إذا تجاوزت هذا القيد بحيلةٍ ما.

هناك فائدتان مرجوتان من تصريح كائن ما بكونه ثابتًا "const":

  1. يشير استخدام الكلمة المفتاحية "const" إلى أن القيمة غير قابلة للتغيير، مما يجبر المصرّف على التحقق من أي تغييرات طرأت على هذه القيمة ضمن الشيفرة البرمجية، وهذا الأمر باعث للطمأنينة في حال أردت استخدام قيمٍ ثابتة، مثل المؤشرات التي تُستخدم وسطاءً في بعض الدوال. إذا احتوى التصريح عن الدالة مؤشرات تشير إلى كائنات ثابتة على أنها وسطاء، فهذا يعني أن الدالة لن تشير إلى أي كائن آخر.
  2. عندما يعلم المصرّف بأن الأشياء هي كائنات ثابتة، ينعكس ذلك إيجابيًا على قدرته برفع فاعلية الشيفرة البرمجية وسرعتها.

الثوابت عديمة الفائدة في حال لم تسند إليها أي قيمة، لن نتطرق بالتفصيل بخصوص تهيئة الثوابت (سنناقش الأمر لاحقًا)، كل ما عليك تذكره الآن هو أنه من الممكن لأي تصريح إسناد القيمة لتعبيرٍ ثابت. إليك بعض الأمثلة عن التصريح عن ثوابت:

const int x = 1;        /* ثابت x */
const float f = 3.5;    /* ثابت f*/
const char y[10];       /* y مصفوفة من 10 عناصر ذات قيم صحيحة ثابتة */
                        /* لا تفكر بخصوص تهيئة قيمها بعد */

ما يثير الاهتمام هو كون هذا المؤهل ممكن التطبيق على المؤشرات بطريقتين: إما بجعل الشيء الذي يشير إليه المؤشر ثابتًا، بحيث يصبح نوع المؤشر "مؤشر إلى ثابت"، أو بجعل المؤشر بذات نفسه ثابتًا (مؤشرًا ثابتًا)، إليك مثالًا عن ذلك:

int i;                  /* عدد صحيح اعتيادي i */
const int ci = 1;       /* عدد صحيح ثابت ci */
int *pi;                /* مؤشر إلى عدد صحيح pi */
const int *pci;         /* مؤشر إلى عدد صحيح ثابت pci */
      /* بالانتقال إلى الأمثلة الأكثر تعقيدًا */

// مؤشر ثابت يشير إلى قيمة عدد صحيح ‪‪cpi 
int *const cpi = &i;

// مؤشر ثابت يشير إلى قيمة عدد صحيح ثابتة cpci
const int *const cpci = &ci;

التصريح الأول (للمتغير i) اعتيادي، ولكن تصريح ci يوضح أنه عدد صحيح ثابت وبذلك لا يمكن التعديل على قيمته، وسيكون بلا فائدة إن لم يُهيّأ (إسناد قيمة أولية له).

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

يُعد التصريحان الأخيران أكثر التصريحات تعقيدًا ضمن المثال؛ فإذا كانت المؤشرات بذات نفسها ثابتةً، فمن غير المسموح تغيير المكان الذي تشير إليه، وبالتالي يجب تهيأتهما بقيمة أولية مثل ci. يمكن للشيء المُشار إليه بالمؤشر أن يكون ثابتًا أو غير ثابت بغض النظر عن كون المؤشر ثابتًا بدوره أم لا، وهذا يملي بعض القيود على استخدام الكائن المُشار إليه.

أخيرًا للتوضيح: ما الذي يملي وجود نوع مؤهّل؟ كان ci في المثال السابق نوعًا مؤهلًا بكل وضوح، ولكن pci لم تنطبق عليه هذه الحالة بما أن المؤشر ليس من نوع مؤهّل بل الشيء الذي يشير إليه. الأشياء الوحيدة الذي كانت ذات أنواع مؤهلة في المثال هي: ci و cpi و cpci.

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

سنتكلم عن أنواع البيانات المؤهلة بتوسعٍ أكبر لاحقًا.

عمليات المؤشرات الحسابية

سنتكلم بالتفصيل لاحقًا عن عمليات المؤشرات الحسابية، ولكننا سنكتفي الآن بإصدار مختصرٍ يفي بالغرض.

يمكنك الطرح أو المقارنة بين مؤشرين من النوع نفسه بالإضافة إلى إضافة قيمة عددية صحيحة إلى المؤشر، ويجب أن يشير كلا المؤشرين إلى المصفوفة ذاتها وإلا حصلنا على سلوك غير محدد. نتيجة طرح مؤشرين هي قيمة العناصر التي تفصل بينهما في المصفوفة، ونوع النتيجة معرفٌ بحسب التطبيق وسيكون إما "short" أو "int" أو "long"، ويوضح المثال القادم مثالًا على حساب الفرق بين مؤشرين واستخدام النتيجة، ولكن قبل أن ننتقل إلى المثال يجب أن تعرف معلومةً مهمة.

يُحوَّل اسم المصفوفة في أي تعبير إلى مؤشر يشير إلى العنصر الأول ضمن هذه المصفوفة، والحالة الوحيدة الاستثنائية هي عند استخدام اسم المصفوفة مع الكلمة المفتاحية "sizeof"، أو عند استخدام سلسلة نصية لتهيئة قيمة مصفوفة ما، أو عندما يكون اسم المصفوفة مرتبطًا بعامل "عنوان الكائن" (العامل الأحادي "&")، لكننا لم نتطرق إلى أيّ من الحالات السابقة بعد، وسنناقشها لاحقًا. إليك المثال:

#include <stdio.h>
#include <stdlib.h>
#define ARSZ 10

main(){
      float fa[ARSZ], *fp1, *fp2;

      fp1 = fp2 = fa; /* عنوان العنصر الأول */
      while(fp2 != &fa[ARSZ]){
              printf("Difference: %d\n", (int)(fp2-fp1));
              fp2++;
      }
      exit(EXIT_SUCCESS);
}

[مثال 4]

يشير المؤشر fp2 إلى عناصر المصفوفة، ويُطبع الفرق بين قيمته الحالية وقيمته الأصلية، وللتأكد من عدم تمرير النوع الخاطئ للوسيط للدالة printf تُحوَّل القيمة الناتجة عن فرق المؤشرين قسريًّا إلى int باستخدام تحويل الأنواع (int)، وهذا يجنّبنا من الأخطاء على الحواسيب التي تعيد قيمة long لهذا النوع من العمليات.

قد يعود إلينا المثال السابق بإجابات خاطئة إذا كان الفرق بين القيمتين من نوع long وكانت المصفوفة كبيرة الحجم، ونلاحظ في المثال التالي إصدارًا آمنًا من المثال السابق، إذ يُستخدم تحويل الأنواع قسريًا للسماح بوجود قيم long:

#include <stdio.h>
#define ARSZ 10

main(){
      float fa[ARSZ], *fp1, *fp2;

      fp1 = fp2 = fa; /* عنوان العنصر الأول */
      while(fp2 != &fa[ARSZ]){
              printf("Difference: %ld\n", (long)(fp2-fp1));
              fp2++;
      }
      return(0);
}

[مثال 5]

مؤشرات void و null والمؤشرات الإشكالية

لغة سي حريصة بشأن أنواع المؤشرات، ولن تسمح لك عمومًا باستخدام مؤشرات ذات أنوع مختلفة ضمن التعبير ذاته. يختلف مؤشر إلى نوع "char" عن مؤشر إلى نوع "int" ولا يمكنك -على سبيل المثال- إسناد قيمة أحدهما إلى الآخر أو المقارنة فيما بينهما أو طرحهما من بعضهما واستخدام النتيجة مثل وسيط في دالة ما، كما يمكن أيضًا تخزين النوعين في الذاكرة بصورةٍ مختلفة وأن يكونا بأطوال مختلفة.

اقتباس

لا تتماثل المؤشرات من أنواع مختلفة فيما بينها، وليس هناك أي تحويلات ضمنية بين الأنواع كما شاهدنا في الأنواع الحسابية سابقًا

لكن نريد في بعض الحالات تجاوز هذه القيود، فكيف نفعل ذلك؟

يكمن الحل هنا باستخدام أنواع خاصة، وقد قدمنا واحدًا من هذه الأنواع سابقًا ألا وهو "مؤشر إلى void"، وقُدّمت هذه الميزة مع سي المعيارية إذ افتُرض سابقًا أن المؤشر من نوع "مؤشر إلى char" كافٍ لهذه المهمة، وكان الافتراض صحيحًا إلى حدٍّ ما إلا أنه كان حلًّا غير منظّمًا، بينما قدّم الحل الجديد طريقةً أكثر أمانًا وأقل تشويشًا. لا يوجد أي استخدام للمؤشر هذا، لأن "* void" لا يشير إلى أي قيمة، لذا يحسّن هذا الأمر من سهولة قراءة الشيفرة البرمجية. يمكن أن يخزن المؤشر من النوع "* void" أي قيمة من أي مؤشر آخر، ويمكن إسناده إلى مؤشر آخر من أي نوع أيضًا، إلا أنه يجب استخدام هذا النوع من المؤشرات بحذر لأنه قد ينتهي الأمر بك ببعض الأخطاء الوخيمة، وسنناقش استخدامه الآمن مع دالة "malloc" لاحقًا.

قد تحتاج أيضًا في بعض الحالات إلى مؤشر مضمون أنه لا يشير إلى أي كائن والذي يُدعى مؤشر من نوع "null" أو مؤشر الفراغ null pointer. من الشائع في لغة سي كتابة بعض الإجراءات routines التي تعيد مؤشرات، بحيث يمكن الدلالة على فشل ذلك الإجراء بعدم قدرته على إعادة مؤشر صالح عن طريق إعادة مؤشر الفراغ. ومن الأمثلة على ذلك إجراء فحصٍ لقيم موجودة في جدول ما بحثًا عن قيمة معينة ويعيد مؤشرًا على الكائن المُطابق إن وُجدت النتيجة أو مؤشر الفراغ إن لم تُوجد أي نتيجةٍ مطابقة.

لكن كيف يمكن كتابة مؤشر فراغ؟ هناك طريقتان لفعل ذلك وكلا الطريقتين متماثلتين في النتيجة، إما باستخدام رقم صحيح ثابت بقيمة "0" أو تحويل القيمة إلى نوع "* void" باستخدام التحويل بين الأنواع، وتدعى نتيجة الطريقتين بمؤشر الفراغ الثابت null pointer constant. إذا أسندت مؤشر فراغ إلى أي مؤشر آخر أو قارنت بين مؤشر الفراغ ومؤشر آخر فسيُحوَّل مؤشر الفراغ إلى نوع المؤشر الآخر تلقائيًّا، مما سيحلّ أي مشكلة بخصوص توافقية الأنواع، ولن تُساوي القيمة التي يشير إليها ذلك المؤشر -مؤشر الفراغ- أي قيمة كائن آخر يشير إليها أي مؤشر داخل البرنامج (أي سيشير إلى قيمة فريدة).

القيم الوحيدة الممكن إسنادها للمؤشرات باستثناء القيمة "0" هي قيم المؤشرات الأخرى من نفس النوع، إلا أن الأمر الذي يجعل من لغة سي مميزة وبديلًا مفيدًا للغة التجميعية هو سماحها لك بفعل بعض الأشياء التي لا تسمح لك بها معظم لغات البرمجة الأخرى، جرّب التالي:

int *ip;
ip = (int *)6;
*ip = 0xFF;

ما نتيجة السابق؟ تُهيأ قيمة المؤشر إلى 6 (لاحظ تحويل نوع 6 من int إلى مؤشر)، وهذه عملية تُجرى على مستوى الآلة غالبًا ويكون تمثيل قيمة المؤشر بالبتات غير مشابه لما قد يكون تمثيل الرقم 6، كما تُسند القيمة الست عشرية FF بعد التهيئة إلى الكائن الذي يشير إليه المؤشر. تُكتب القيمة 0xFF على الرقم الصحيح ذو الموضع 6، إذ يعتمد الموضع 6 على تفسير الآلة له على الذاكرة.

قد تحتاج لهذا النوع من الأشياء وقد لا تحتاج إليها إطلاقًا، ولكن لغة سي تعطيك الخيار، ومن الواجب معرفتها إذ أنه من الممكن كتابتها على نحوٍ خاطئ غير مقصود مما سيتسبب بنتائج مفاجئة جدًا.

ترجمة -وبتصرف- لقسم من الفصل Arrays and Pointers من كتاب The C Book‪‏.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...