المؤشرات في لغة C: دليل شامل لفهمها وإتقانها للمبرمجين

دقائق القراءة: 33

مقدمة إلى المؤشرات في لغة C: مفتاح القوة والمرونة

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

أولاً: المفاهيم الأساسية للمؤشرات

1. ما هي المؤشرات (Pointers) بالضبط؟

قبل الغوص في تعريف المؤشرات، دعنا نفهم ما يحدث عند كتابة سطر برمجي بسيط مثل:

int digit = 42;

رسم توضيحي لمتغير digit بقيمة 42 في كتلة ذاكرة بعنوان 24650

عند تنفيذ هذا السطر، يقوم المترجم بحجز كتلة من الذاكرة لتخزين قيمة من نوع int. يُطلق على هذه الكتلة اسم digit، والقيمة المخزنة فيها هي 42. لتتبع هذه الكتلة، يتم تعيين عنوان أو رقم موقع لها في الذاكرة (على سبيل المثال، 24650). قيمة هذا العنوان ليست ثابتة أو مهمة بحد ذاتها، فهي قيمة عشوائية يحددها نظام التشغيل.

يمكننا الوصول إلى هذا العنوان باستخدام عامل التشغيل & (علامة العطف)، والذي يُعرف بعامل “عنوان الـ” (address of operator). لنرى كيف:

printf("The address of digit = %d.", &digit);
/* قد يطبع: "The address of digit = 24650." */

وللحصول على قيمة المتغير digit من خلال عنوانه، نستخدم عامل تشغيل آخر هو * (علامة النجمة)، والذي يُعرف بعامل “الوصول غير المباشر” أو “إلغاء المرجع” (indirection or dereferencing operator) أو “القيمة عند العنوان” (value at address operator).

printf("The value of digit = %d.", *(&digit));
/* قد يطبع: "The value of digit = 42." */

2. تعريف المؤشرات وطرق الإعلان عنها

يمكن تخزين عنوان متغير في متغير آخر يُعرف بـ “متغير المؤشر” (pointer variable). الصيغة العامة لتخزين عنوان متغير في مؤشر هي كالتالي:

dataType *pointerVariableName = &variableName;

بالنسبة لمتغيرنا digit، يمكن كتابة ذلك على النحو التالي:

int *addressOfDigit = &digit;

أو يمكن فصل الإعلان عن التعيين:

int *addressOfDigit;
addressOfDigit = &digit;

رسم متحرك يوضح المؤشر addressOfDigit يشير إلى عنوان المتغير digit

يمكن قراءة هذا الإعلان على النحو التالي: “addressOfDigit هو مؤشر لنوع int (عدد صحيح) ويخزن عنوان المتغير digit“.

نقاط مهمة للفهم:

  • dataType (نوع البيانات): يجب أن نحدد للمترجم نوع البيانات للمتغير الذي سنقوم بتخزين عنوانه. في مثالنا، كان int هو نوع بيانات digit. هذا لا يعني أن addressOfDigit سيخزن قيمة من نوع int، بل يعني أنه مؤشر يمكنه تخزين عنوان متغير من نوع int فقط.
int variable1;
int variable2;
char variable3;
int *addressOfVariables;

هنا، يمكننا تعيين عنوان variable1 و variable2 للمؤشر addressOfVariables، ولكن لا يمكننا تعيين عنوان variable3 لأنه من نوع char. سنحتاج إلى مؤشر من نوع char لتخزين عنوانه.

  • * (علامة النجمة): متغير المؤشر هو متغير خاص يُستخدم لتخزين عنوان متغير آخر. لتمييزه عن المتغيرات العادية التي لا تخزن عناوين، نستخدم * كرمز في الإعلان.

يمكننا استخدام متغير المؤشر addressOfDigit لطباعة عنوان وقيمة digit كما يلي:

printf("The address of digit = %d.\n", addressOfDigit);
/* قد يطبع: "The address of digit = 24650." */
printf("The value of digit = %d.\n", *addressOfDigit);
/* قد يطبع: "The value of digit = 42." */

هنا، يمكن قراءة *addressOfDigit على أنه “القيمة الموجودة في العنوان المخزن في addressOfDigit“.

لاحظ أننا استخدمنا %d كمحدد تنسيق لـ addressOfDigit. هذا ليس صحيحًا تمامًا. المحدد الصحيح هو %p، الذي يعرض العنوان كقيمة سداسية عشرية. على الرغم من أن عنوان الذاكرة يمكن عرضه كأعداد صحيحة أو قيم ثمانية، إلا أن استخدام %d ليس الطريقة الصحيحة تمامًا وقد يؤدي إلى ظهور تحذير من المترجم.

int num = 5;
int *p = #
printf("Address using %%p = %p\n", p);
printf("Address using %%d = %d\n", p);
printf("Address using %%o = %o\n", p);

الناتج وفقًا لمعظم المترجمات سيكون مشابهًا لما يلي:

Address using %p = 000000000061FE00
Address using %d = 6422016
Address using %o = 30377000

التحذير الذي يظهر عند استخدام %d هو: “warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘int *’“.

3. أنواع خاصة من المؤشرات

المؤشر الجامح (Wild Pointer)

char *alphabetAddress; /* مؤشر غير مهيأ أو جامح */
char alphabet = 'a';
alphabetAddress = &alphabet; /* الآن، لم يعد مؤشرًا جامحًا */

عندما أعلنا عن مؤشر الحرف alphabetAddress، لم نقم بتهيئته. تُعرف هذه المؤشرات بالمؤشرات الجامحة (wild pointers). إنها تخزن قيمة غير مرغوب فيها (عنوان ذاكرة) لبايت لا نعرف ما إذا كان محجوزًا أم لا (تذكر int digit = 42;، حيث حجزنا عنوان ذاكرة عند الإعلان). إذا قمنا بإلغاء مرجع مؤشر جامح وقمنا بتعيين قيمة لعنوان الذاكرة الذي يشير إليه، فسيؤدي ذلك إلى سلوك غير متوقع، حيث سنكتب بيانات في كتلة ذاكرة قد تكون حرة أو محجوزة.

المؤشر الفارغ (Null Pointer)

للتأكد من عدم وجود مؤشر جامح، يمكننا تهيئة المؤشر بقيمة NULL، مما يجعله مؤشرًا فارغًا (null pointer).

char *alphabetAddress = NULL; /* مؤشر فارغ */

المؤشر الفارغ لا يشير إلى أي شيء، أو يشير إلى عنوان ذاكرة لا يمكن للمستخدمين الوصول إليه.

المؤشر العام (Void Pointer)

يمكن استخدام المؤشر العام (void pointer) للإشارة إلى متغير من أي نوع بيانات. يمكن إعادة استخدامه للإشارة إلى أي نوع بيانات نريده. يتم الإعلان عنه كالتالي:

void *pointerVariableName = NULL;

نظرًا لطبيعته العامة جدًا، يُعرف أيضًا بالمؤشر العام (generic pointer). ومع مرونته، يجلب المؤشر العام بعض القيود. لا يمكن إلغاء مرجعه مثل أي مؤشر آخر. يجب إجراء تحويل نوع مناسب (typecasting).

void *pointer = NULL;
int number = 54;
char alphabet = 'z';

pointer = &number;
// printf("The value of number = ", *pointer); /* خطأ في الترجمة */
/* الطريقة الصحيحة */
printf("The value of number = %d\n", *(int *)pointer); /* يطبع "The value of number = 54" */

pointer = &alphabet;
// printf("The value of alphabet = ", *pointer); /* خطأ في الترجمة */
printf("The value of alphabet = %c\n", *(char *)pointer); /* يطبع "The value of alphabet = z" */

وبالمثل، تتطلب المؤشرات العامة تحويلًا للنوع لإجراء العمليات الحسابية عليها. تُعد المؤشرات العامة ذات فائدة كبيرة في لغة C. فدوال المكتبة malloc() و calloc() التي تقوم بتخصيص الذاكرة ديناميكيًا تُرجع مؤشرات عامة. ودالة qsort()، وهي دالة فرز مدمجة في C، تأخذ دالة كوسيطة تأخذ بدورها مؤشرات عامة كوسيطات.

المؤشر المتدلي (Dangling Pointer)

المؤشر المتدلي (dangling pointer) يشير إلى عنوان ذاكرة كان يستخدم لتخزين متغير ما. بما أن العنوان الذي يشير إليه لم يعد محجوزًا، فإن استخدامه سيؤدي إلى نتائج غير متوقعة.

int main(){
    int *ptr;
    ptr = (int *) malloc(sizeof(int));
    *ptr = 1;
    printf("%d\n", *ptr); /* يطبع 1 */
    free(ptr); /* إلغاء تخصيص الذاكرة */
    *ptr = 5;
    printf("%d\n", *ptr); /* قد يطبع 5 أو قيمة غير متوقعة */
    return 0;
}

على الرغم من أن الذاكرة قد تم إلغاء تخصيصها بواسطة free(ptr)، إلا أن المؤشر إلى العدد الصحيح ptr لا يزال يشير إلى عنوان الذاكرة غير المحجوز هذا.

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

نعلم الآن أن المؤشرات ليست كأي متغير آخر. إنها لا تخزن أي قيمة سوى عنوان كتل الذاكرة. لذلك يجب أن يكون واضحًا أن جميع العمليات الحسابية لن تكون صالحة معها. هل سيكون ضرب أو قسمة مؤشرين (لهما عناوين) منطقيًا؟

للمؤشرات عمليات صالحة قليلة ولكنها مفيدة للغاية:

  • التعيين (Assignment): يمكنك تعيين قيمة مؤشر لآخر فقط إذا كانا من نفس النوع (ما لم يتم تحويل نوعهما أو كان أحدهما void *).
int ManU = 1;
int *addressOfManU = &ManU;
int *anotherAddressOfManU = NULL;
anotherAddressOfManU = addressOfManU; /* صالح */
// double *wrongAddressOfManU = addressOfManU; /* غير صالح - خطأ في الترجمة */
  • الجمع والطرح مع الأعداد الصحيحة (Addition and Subtraction with Integers): يمكنك فقط إضافة أو طرح أعداد صحيحة من المؤشرات.
int myArray[] = {3, 6, 9, 12, 15};
int *pointerToMyArray = &myArray[0];
pointerToMyArray += 3; /* صالح */
// pointerToMyArray *= 3; /* غير صالح */

عندما تضيف (أو تطرح) عددًا صحيحًا (ولنقل n) إلى مؤشر، فإنك لا تضيف (أو تطرح) n بايت فعليًا إلى قيمة المؤشر. بل تقوم فعليًا بإضافة (أو طرح) n-ضعف حجم نوع البيانات للمتغير الذي يتم الإشارة إليه.

int number = 5; /* لنفترض أن عنوان number هو 100 */
int *ptr = &number;
int *newAddress = ptr + 3; /* مكافئ لـ ptr + 3 * sizeof(int) */

القيمة المخزنة في newAddress لن تكون 103، بل ستكون 112 (إذا كان حجم int هو 4 بايت).

  • طرح ومقارنة المؤشرات (Subtraction and Comparison of Pointers): يكون طرح ومقارنة المؤشرات صالحًا فقط إذا كان كلاهما عضوين في نفس المصفوفة. يعطي طرح المؤشرات عدد العناصر التي تفصل بينهما.
int myArray[] = {3, 6, 9, 12, 15};
int sixthMultiple = 18;
int *pointer1 = &myArray[0];
int *pointer2 = &myArray[1];
int *pointer6 = &sixthMultiple;

/* تعابير صالحة */
if (pointer1 == pointer2) { /* مقارنة */ }
int diff = pointer2 - pointer1; /* طرح يعطي عدد العناصر (1) */

/* تعابير غير صالحة */
// if(pointer1 == pointer6) { /* مقارنة غير صالحة لأنها ليست من نفس المصفوفة */ }
// pointer2 - pointer6; /* طرح غير صالح */
  • التعيين والمقارنة مع NULL: يمكنك تعيين أو مقارنة مؤشر بـ NULL.

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

ثانياً: المؤشرات والمصفوفات والسلاسل النصية

1. العلاقة بين المؤشرات والمصفوفات: لماذا هي مهمة؟

في لغة C، توجد علاقة قوية جدًا بين المؤشرات والمصفوفات. السبب في وجوب مناقشتهما معًا هو أن ما يمكنك تحقيقه باستخدام تدوين المصفوفة (arrayName[index]) يمكن تحقيقه أيضًا باستخدام المؤشرات، ولكن عادةً ما يكون أسرع.

2. المؤشرات والمصفوفات أحادية البعد

دعنا نلقي نظرة على ما يحدث عندما نكتب int myArray[5];. يتم إنشاء خمس كتل متتالية من الذاكرة بدءًا من myArray[0] وحتى myArray[4] بقيم غير مرغوب فيها. كل كتلة من هذه الكتل بحجم 4 بايت (إذا كان int بحجم 4 بايت). وبالتالي، إذا كان عنوان myArray[0] هو 100 (على سبيل المثال)، فإن عناوين الكتل المتبقية ستكون 104، 108، 112، و 116. لنلقِ نظرة على الكود التالي:

int prime[5] = {2, 3, 5, 7, 11};
printf("Result using &prime = %d\n", &prime);
printf("Result using prime = %d\n", prime);
printf("Result using &prime[0] = %d\n", &prime[0]);
/* الناتج المحتمل */
// Result using &prime = 6422016
// Result using prime = 6422016
// Result using &prime[0] = 6422016

إذن، &prime، prime، و &prime[0] كلها تعطي نفس العنوان، أليس كذلك؟ حسنًا، انتظر واقرأ لأنك على وشك أن تفاجأ (وربما بعض الارتباك). دعنا نحاول زيادة كل من &prime، prime، و &prime[0] بمقدار 1.

printf("Result using &prime + 1 = %d\n", &prime + 1);
printf("Result using prime + 1 = %d\n", prime + 1);
printf("Result using &prime[0] + 1 = %d\n", &prime[0] + 1);
/* الناتج المحتمل */
// Result using &prime + 1 = 6422036
// Result using prime + 1 = 6422020
// Result using &prime[0] + 1 = 6422020

انتظر! كيف أن &prime + 1 ينتج شيئًا مختلفًا عن الاثنين الآخرين؟ ولماذا لا يزال prime + 1 و &prime[0] + 1 متساويين؟ دعنا نجيب على هذه الأسئلة:

  • prime و &prime[0] كلاهما يشيران إلى العنصر رقم 0 في المصفوفة prime. وبالتالي، فإن اسم المصفوفة نفسه هو مؤشر إلى العنصر رقم 0 في المصفوفة. هنا، يشيران كلاهما إلى العنصر الأول بحجم 4 بايت. عندما تضيف 1 إليهما، فإنهما يشيران الآن إلى العنصر رقم 1 في المصفوفة. لذلك، ينتج عن هذا زيادة في العنوان بمقدار 4 بايت.
  • &prime، من ناحية أخرى، هو مؤشر إلى مصفوفة int بحجم 5 عناصر. إنه يخزن العنوان الأساسي للمصفوفة prime[5]، وهو مساوٍ لعنوان العنصر الأول. ومع ذلك، فإن الزيادة بمقدار 1 فيه تؤدي إلى عنوان بزيادة قدرها 5 × 4 = 20 بايت.

باختصار، arrayName و &arrayName[0] يشيران إلى العنصر رقم 0، بينما &arrayName يشير إلى المصفوفة بأكملها.

رسم متحرك يوضح الفرق بين &arrayName و arrayName و &arrayName[0]

يمكننا الوصول إلى عناصر المصفوفة باستخدام المتغيرات ذات الفهارس (subscripted variables) على النحو التالي:

int prime[5] = {2, 3, 5, 7, 11};
for (int i = 0; i < 5; i++) {
    printf("index = %d, address = %d, value = %d\n", i, &prime[i], prime[i]);
}

يمكننا فعل الشيء نفسه باستخدام المؤشرات، والتي تكون دائمًا أسرع من استخدام الفهارس:

int prime[5] = {2, 3, 5, 7, 11};
for (int i = 0; i < 5; i++) {
    printf("index = %d, address = %d, value = %d\n", i, prime + i, *(prime + i));
}

كلا الطريقتين تعطيان نفس الناتج:

index = 0, address = 6422016, value = 2
index = 1, address = 6422020, value = 3
index = 2, address = 6422024, value = 5
index = 3, address = 6422028, value = 7
index = 4, address = 6422032, value = 11

وبالتالي، فإن &arrayName[i] و arrayName[i] هما نفس الشيء مثل arrayName + i و *(arrayName + i) على التوالي.

3. المؤشرات والمصفوفات ثنائية البعد

المصفوفات ثنائية البعد هي في الأساس مصفوفة من المصفوفات.

int marks[5][3] = {
    {98, 76, 89},
    {81, 96, 79},
    {88, 86, 89},
    {97, 94, 99},
    {92, 81, 59}
};

هنا، يمكن اعتبار marks كمصفوفة من 5 عناصر، كل منها عبارة عن مصفوفة أحادية البعد تحتوي على 3 أعداد صحيحة. دعنا نستعرض سلسلة من الأمثلة لفهم التعبيرات المختلفة ذات الفهارس.

printf("Address of whole 2-D array = %d\n", &marks);
printf("Addition of 1 results in %d\n", &marks + 1);
/* الناتج المحتمل */
// Address of whole 2-D array = 6421984
// Addition of 1 results in 6422044

مثل المصفوفات أحادية البعد، يشير &marks إلى المصفوفة ثنائية البعد بأكملها، marks[5][3]. وبالتالي، فإن زيادته بمقدار 1 (أي 5 مصفوفات × 3 أعداد صحيحة لكل منها × 4 بايت = 60 بايت) تؤدي إلى زيادة بمقدار 60 بايت.

printf("Address of 0th array = %d\n", marks);
printf("Addition of 1 results in %d\n", marks + 1);
printf("Address of 0th array = %d\n", &marks[0]);
printf("Addition of 1 results in %d\n", &marks[0] + 1);
/* الناتج المحتمل */
// Address of 0th array = 6421984
// Addition of 1 results in 6421996
// Address of 0th array = 6421984
// Addition of 1 results in 6421996

إذا كانت marks مصفوفة أحادية البعد، لكانت marks و &marks[0] قد أشارتا إلى العنصر رقم 0. بالنسبة للمصفوفة ثنائية البعد، أصبحت العناصر الآن مصفوفات أحادية البعد. وبالتالي، فإن marks و &marks[0] يشيران إلى المصفوفة رقم 0 (العنصر)، وتؤدي إضافة 1 إلى الإشارة إلى المصفوفة رقم 1.

printf("Address of 0th element of 0th array = %d\n", marks[0]);
printf("Addition of 1 results in %d\n", marks[0] + 1);
printf("Address of 0th element of 1st array = %d\n", marks[1]);
printf("Addition of 1 results in %d\n", marks[1] + 1);
/* الناتج المحتمل */
// Address of 0th element of 0th array = 6421984
// Addition of 1 results in 6421988
// Address of 0th element of 1st array = 6421996
// Addition of 1 results in 6422000

والآن يأتي الاختلاف. بالنسبة للمصفوفة أحادية البعد، ستعطي marks[0] قيمة العنصر رقم 0. ستؤدي الزيادة بمقدار 1 إلى زيادة القيمة بمقدار 1. ولكن، في المصفوفة ثنائية البعد، تشير marks[0] إلى العنصر رقم 0 في المصفوفة رقم 0. وبالمثل، تشير marks[1] إلى العنصر رقم 0 في المصفوفة رقم 1. ستؤدي الزيادة بمقدار 1 إلى الإشارة إلى العنصر رقم 1 في المصفوفة رقم 1.

printf("Value of 0th element of 0th array = %d\n", marks[0][0]);
printf("Addition of 1 results in %d\n", marks[0][0] + 1);
/* الناتج المحتمل */
// Value of 0th element of 0th array = 98
// Addition of 1 results in 99

هذا هو الجزء الجديد. marks[i][j] يعطي قيمة العنصر j للمصفوفة i. تؤدي الزيادة فيه إلى تغيير القيمة المخزنة في marks[i][j]. الآن، دعنا نحاول كتابة marks[i][j] بدلالة المؤشرات. نعلم أن marks[i] + j سيشير إلى العنصر j للمصفوفة i من مناقشتنا السابقة. إلغاء مرجعه سيعني القيمة في ذلك العنوان. وبالتالي، فإن marks[i][j] هو نفس *(marks[i] + j). ومن مناقشتنا للمصفوفات أحادية البعد، فإن marks[i] هو نفس *(marks + i). وبالتالي، يمكن كتابة marks[i][j] على النحو التالي *(*(marks + i) + j) بدلالة المؤشرات.

إليك ملخص للتدوينات مقارنة بين المصفوفات أحادية وثنائية البعد:

التعبير مصفوفة أحادية البعد (1-D Array) مصفوفة ثنائية البعد (2-D Array)
&arrayName يشير إلى عنوان المصفوفة بأكملها؛ إضافة 1 تزيد العنوان بمقدار 1 * sizeof(arrayName) يشير إلى عنوان المصفوفة بأكملها؛ إضافة 1 تزيد العنوان بمقدار 1 * sizeof(arrayName)
arrayName يشير إلى العنصر رقم 0؛ إضافة 1 تزيد العنوان إلى العنصر رقم 1 يشير إلى العنصر رقم 0 (المصفوفة)؛ إضافة 1 تزيد العنوان إلى العنصر رقم 1 (المصفوفة)
&arrayName[i] يشير إلى العنصر i؛ إضافة 1 تزيد العنوان إلى العنصر (i+1) يشير إلى العنصر i (المصفوفة)؛ إضافة 1 تزيد العنوان إلى العنصر (i+1) (المصفوفة)
arrayName[i] يعطي قيمة العنصر i؛ إضافة 1 تزيد قيمة العنصر i يشير إلى العنصر رقم 0 للمصفوفة i؛ إضافة 1 تزيد العنوان إلى العنصر رقم 1 للمصفوفة i
arrayName[i][j] لا ينطبق يعطي قيمة العنصر j للمصفوفة i؛ إضافة 1 تزيد قيمة العنصر j للمصفوفة i
تعبير المؤشر للوصول إلى العناصر *(arrayName + i) *(*(arrayName + i) + j)

4. المؤشرات والسلاسل النصية (Strings)

السلسلة النصية هي مصفوفة أحادية البعد من الأحرف تنتهي بحرف null (\0). عندما نكتب char name[] = "Srijan";، يشغل كل حرف بايت واحدًا من الذاكرة، ويكون الحرف الأخير دائمًا \0. على غرار المصفوفات التي رأيناها، تشير name و &name[0] إلى الحرف رقم 0 في السلسلة، بينما يشير &name إلى السلسلة بأكملها. أيضًا، يمكن كتابة name[i] على النحو *(name + i).

/* سلسلة نصية */
char champions[] = "Liverpool";
printf("Pointer to whole string = %d\n", &champions);
printf("Addition of 1 results in %d\n", &champions + 1);
/* الناتج المحتمل */
// Pointer to whole string = 6421974
// Addition of 1 results in 6421984

printf("Pointer to 0th character = %d\n", &champions[0]);
printf("Addition of 1 results in %d\n", &champions[0] + 1);
/* الناتج المحتمل */
// Pointer to 0th character = 6421974
// Addition of 1 results in a pointer to 1st character 6421975

printf("Pointer to 0th character = %d\n", champions);
printf("Addition of 1 results in a pointer to 1st character %d\n", champions + 1);
/* الناتج المحتمل */
// Pointer to 0th character = 6421974
// Addition of 1 results in 6421975

printf("Value of 4th character = %c\n", champions[4]);
printf("Value of 4th character using pointers = %c\n", *(champions + 4));
/* الناتج المحتمل */
// Value of 4th character = r
// Value of 4th character using pointers = r

يمكن أيضًا الوصول إلى مصفوفة ثنائية البعد من الأحرف أو مصفوفة من السلاسل النصية والتلاعب بها كما نوقش سابقًا.

/* مصفوفة من السلاسل النصية */
char top[6][15] = {"Liverpool", "Man City", "Man United", "Chelsea", "Leicester", "Tottenham"};

printf("Pointer to 2-D array = %d\n", &top);
printf("Addition of 1 results in %d\n", &top + 1);
/* الناتج المحتمل */
// Pointer to 2-D array = 6421952
// Addition of 1 results in 6422042

printf("Pointer to 0th string = %d\n", &top[0]);
printf("Addition of 1 results in %d\n", &top[0] + 1);
/* الناتج المحتمل */
// Pointer to 0th string = 6421952
// Addition of 1 results in 6421967

printf("Pointer to 0th string = %d\n", top);
printf("Addition of 1 results in %d\n", top + 1);
/* الناتج المحتمل */
// Pointer to 0th string = 6421952
// Addition of 1 results in 6421967

printf("Pointer to 0th element of 4th string = %d\n", top[4]);
printf("Pointer to 1st element of 4th string = %d\n", top[4] + 1); /* تم تصحيح %c إلى %d لطباعة العنوان */
/* الناتج المحتمل */
// Pointer to 0th element of 4th string = 6422012
// Pointer to 1st element of 4th string = 6422013

printf("Value of 1st character in 3rd string = %c\n", top[3][1]);
printf("Same using pointers = %c\n", *(*(top + 3) + 1));
/* الناتج المحتمل */
// Value of 1st character in 3rd string = h
// Same using pointers = h

5. مصفوفة المؤشرات (Array of Pointers)

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

dataType *variableName[size];

/* أمثلة */
int *example1[5];
char *example2[8];

باتباع أسبقية العوامل، يمكن قراءة المثال الأول على النحو التالي: example1 هي مصفوفة ([]) من 5 مؤشرات إلى int. وبالمثل، example2 هي مصفوفة من 8 مؤشرات إلى char.

يمكننا تخزين المصفوفة ثنائية البعد للسلاسل النصية top باستخدام مصفوفة مؤشرات وتوفير الذاكرة أيضًا.

char *top[] = {"Liverpool", "Man City", "Man United", "Chelsea", "Leicester", "Tottenham"};

ستحتوي top على العناوين الأساسية لجميع الأسماء المعنية. سيتم تخزين العنوان الأساسي لـ “Liverpool” في top[0]، و “Man City” في top[1]، وهكذا. في الإعلان السابق، احتجنا إلى 90 بايت لتخزين الأسماء. هنا، نحتاج فقط إلى (58 بايت (مجموع بايتات الأسماء) + 12 بايت (البايتات المطلوبة لتخزين العنوان في المصفوفة)) أي 70 بايت. يصبح التلاعب بالسلاسل النصية أو الأعداد الصحيحة أسهل بكثير عند استخدام مصفوفة من المؤشرات. إذا حاولنا وضع “Leicester” قبل “Chelsea”، نحتاج فقط إلى تبديل قيم top[3] و top[4] كما يلي:

char *temporary;
temporary = top[3];
top[3] = top[4];
top[4] = temporary;

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

6. مؤشر إلى مصفوفة (Pointer to Array)

مثل “مؤشر إلى int” أو “مؤشر إلى char“، لدينا أيضًا مؤشر إلى مصفوفة. هذا المؤشر يشير إلى المصفوفة بأكملها بدلاً من عناصرها. هل تتذكر كيف ناقشنا أن &arrayName يشير إلى المصفوفة بأكملها؟ حسنًا، إنه مؤشر إلى مصفوفة. يمكن الإعلان عن مؤشر إلى مصفوفة على النحو التالي:

dataType (*variableName)[size];

/* أمثلة */
int (*ptr1)[5];
char (*ptr2)[15];

لاحظ الأقواس. بدونها، ستكون هذه مصفوفة من المؤشرات. يمكن قراءة المثال الأول على النحو التالي: ptr1 هو مؤشر إلى مصفوفة من 5 أعداد صحيحة (int).

int goals[] = {85, 102, 66, 69, 67};
int (*pointerToGoals)[5] = &goals;
printf("Address stored in pointerToGoals %d\n", pointerToGoals);
printf("Dereferencing it, we get %d\n", *pointerToGoals);
/* الناتج المحتمل */
// Address stored in pointerToGoals 6422016
// Dereferencing it, we get 6422016

عندما نلغي مرجع مؤشر، فإنه يعطي القيمة في ذلك العنوان. وبالمثل، عن طريق إلغاء مرجع مؤشر إلى مصفوفة، نحصل على المصفوفة، ويشير اسم المصفوفة إلى العنوان الأساسي. يمكننا التأكد من أن *pointerToGoals يعطي المصفوفة goals إذا وجدنا حجمها.

printf("Size of goals[5] = %d\n", sizeof(*pointerToGoals)); /* تم تصحيح الاستخدام */
/* الناتج المحتمل */
// Size of goals[5] = 20

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

for (int i = 0; i < 5; i++)
    printf("%d ", *(*pointerToGoals + i));
/* الناتج المحتمل */
// 85 102 66 69 67

المؤشرات والمؤشرات إلى المصفوفات مفيدة جدًا عند استخدامها مع الدوال. سنتناول ذلك في القسم التالي!

ثالثاً: المؤشرات والدوال (Functions)

1. تمرير القيم مقابل تمرير المرجع (Call by Value vs Call by Reference)

لنلقِ نظرة على البرنامج أدناه:

#include <stdio.h>

int multiply(int x, int y) {
    int z;
    z = x * y;
    return z;
}

int main(){
    int x = 3, y = 5;
    int product = multiply(x,y);
    printf("Product = %d\n", product); /* يطبع "Product = 15" */
    return 0;
}

تأخذ الدالة multiply() وسيطتين من نوع int وتُرجع ناتجهما كـ int. في استدعاء الدالة multiply(x,y)، قمنا بتمرير قيمة x و y (من الدالة main())، وهي وسيطات فعلية، إلى multiply(). يتم تمرير أو نسخ قيم الوسيطات الفعلية إلى الوسيطات الرسمية x و y (الخاصة بـ multiply()). إن x و y في multiply() تختلفان عن تلك الموجودة في main(). يمكن التحقق من ذلك عن طريق طباعة عناوينها.

#include <stdio.h>

int multiply(int x, int y) {
    printf("Address of x in multiply() = %d\n", &x);
    printf("Address of y in multiply() = %d\n", &y);
    int z;
    z = x * y;
    return z;
}

int main(){
    int x = 3, y = 5;
    printf("Address of x in main() = %d\n", &x);
    printf("Address of y in main() = %d\n", &y);
    int product = multiply(x,y);
    printf("Product = %d\n", product);
    return 0;
}
/* الناتج المحتمل */
// Address of x in main() = 6422040
// Address of y in main() = 6422036
// Address of x in multiply() = 6422000
// Address of y in multiply() = 6422008
// Product = 15

بما أننا أنشأنا قيمًا مخزنة في موقع جديد، فإن ذلك يكلفنا ذاكرة. ألن يكون أفضل لو استطعنا أداء نفس المهمة دون إهدار مساحة؟ يساعدنا تمرير المرجع (Call by Reference) في تحقيق ذلك. نقوم بتمرير عنوان أو مرجع المتغيرات إلى الدالة التي لا تنشئ نسخة. باستخدام عامل إلغاء المرجع *، يمكننا الوصول إلى القيمة المخزنة في تلك العناوين. يمكننا إعادة كتابة البرنامج أعلاه باستخدام تمرير المرجع أيضًا.

#include <stdio.h>

int multiply(int *x, int *y) {
    int z;
    z = (*x) * (*y);
    return z;
}

int main(){
    int x = 3, y = 5;
    int product = multiply(&x,&y);
    printf("Product = %d\n", product); /* يطبع "Product = 15" */
    return 0;
}

2. استخدام المؤشرات كوسيطات للدوال

في هذا القسم، سنلقي نظرة على برامج مختلفة نمرر فيها أنواع البيانات int و char والمصفوفات والسلاسل النصية كوسيطات باستخدام المؤشرات.

#include <stdio.h>

void add(float *a, float *b) {
    float c = *a + *b;
    printf("Addition gives %.2f\n", c);
}

void subtract(float *a, float *b) {
    float c = *a - *b;
    printf("Subtraction gives %.2f\n", c);
}

void multiply(float *a, float *b) {
    float c = *a * *b;
    printf("Multiplication gives %.2f\n", c);
}

void divide(float *a, float *b) {
    if (*b == 0) {
        printf("Error: Division by zero!\n");
        return;
    }
    float c = *a / *b;
    printf("Division gives %.2f\n", c);
}

int main(){
    printf("Enter two numbers :\n");
    float a,b;
    scanf("%f %f", &a, &b);
    printf("What do you want to do with the numbers?\nAdd : a\nSubtract : s\nMultiply : m\nDivide : d\n");
    char operation = '0';
    scanf(" %c", &operation);
    printf("\nOperating...\n\n");

    switch (operation) {
        case 'a' : add(&a,&b); break;
        case 's' : subtract(&a,&b); break;
        case 'm' : multiply(&a,&b); break;
        case 'd' : divide(&a,&b); break;
        default : printf("Invalid input!!!\n");
    }
    return 0;
}

لقد أنشأنا أربع دوال، add() و subtract() و multiply() و divide() لإجراء العمليات الحسابية على العددين a و b. تم تمرير عنوان a و b إلى الدوال. داخل الدالة، باستخدام *، وصلنا إلى القيم وطبعنا النتيجة. وبالمثل، يمكننا تمرير المصفوفات كوسيطات باستخدام مؤشر إلى عنصرها الأول.

#include <stdio.h>

void greatestOfAll(int *p) {
    int max = *p;
    for (int i=0; i < 5; i++){
        if (*(p+i) > max) max = *(p+i);
    }
    printf("The largest element is %d\n", max);
}

int main(){
    int myNumbers[5] = {34, 65, -456, 0, 3455};
    greatestOfAll(myNumbers); /* يطبع: "The largest element is 3455" */
    return 0;
}

بما أن اسم المصفوفة نفسه هو مؤشر إلى العنصر الأول، فإننا نرسل ذلك كوسيطة إلى الدالة greatestOfAll(). في الدالة، نتنقل عبر المصفوفة باستخدام حلقة ومؤشر.

#include <stdio.h>
#include <string.h>

void wish(char *p) {
    printf("Have a nice day, %s\n", p);
}

int main(){
    printf("Enter your name : \n");
    char name[20];
    // استخدام fgets بدلاً من gets لتجنب مشاكل الأمان
    fgets(name, sizeof(name), stdin);
    // إزالة حرف السطر الجديد إذا كان موجودًا
    name[strcspn(name, "\n")] = 0;
    wish(name);
    return 0;
}

هنا، نمرر السلسلة النصية name إلى wish() باستخدام مؤشر ونطبع الرسالة.

3. إرجاع المؤشرات من الدوال

#include <stdio.h>

int *multiply(int *a, int *b) {
    int c = *a * *b;
    return &c;
}

int main(){
    int a=3, b=5;
    int *c = multiply(&a,&b);
    printf("Product = %d\n", *c);
    return 0;
}

تأخذ الدالة multiply() مؤشرين إلى int. وتُرجع مؤشرًا إلى int أيضًا يخزن العنوان حيث يتم تخزين الناتج. من السهل جدًا الاعتقاد بأن الناتج سيكون 15. لكنه ليس كذلك! عندما يتم استدعاء multiply()، يتوقف تنفيذ main() ويتم تخصيص الذاكرة الآن لتنفيذ multiply(). بعد اكتمال تنفيذها، يتم إلغاء تخصيص الذاكرة المخصصة لـ multiply(). لذلك، على الرغم من أن c (المحلي لـ main()) يخزن عنوان الناتج، إلا أن البيانات هناك غير مضمونة نظرًا لأنه تم إلغاء تخصيص تلك الذاكرة.

فهل هذا يعني أنه لا يمكن إرجاع المؤشرات بواسطة دالة؟ لا! يمكننا القيام بأمرين. إما تخزين العنوان في قسم الـ heap أو القسم العام (global section) أو الإعلان عن المتغير ليكون static بحيث تستمر قيمه. يمكن ببساطة إنشاء المتغيرات الثابتة باستخدام الكلمة المفتاحية static قبل نوع البيانات عند الإعلان عن المتغير. لتخزين العناوين في الـ heap، يمكننا استخدام دوال المكتبة malloc() و calloc() التي تخصص الذاكرة ديناميكيًا. ستشرح البرامج التالية كلا الطريقتين. كلا الطريقتين تُرجعان الناتج 15.

#include <stdio.h>
#include <stdlib.h> /* لاستخدام malloc() */

/* استخدام malloc() */
int *multiply_dynamic(int *a, int *b) {
    int *c = malloc(sizeof(int));
    if (c == NULL) { /* التحقق من نجاح التخصيص */
        // Handle error
        return NULL;
    }
    *c = *a * *b;
    return c;
}

int main(){
    int a=3, b=5;
    int *c = multiply_dynamic(&a,&b);
    if (c != NULL) {
        printf("Product (dynamic) = %d\n", *c);
        free(c); /* تحرير الذاكرة المخصصة ديناميكيًا */
    }
    return 0;
}
#include <stdio.h>

/* استخدام الكلمة المفتاحية static */
int *multiply_static(int *a, int *b) {
    static int c;
    c = *a * *b;
    return &c;
}

int main(){
    int a=3, b=5;
    int *c = multiply_static(&a,&b);
    printf("Product (static) = %d\n", *c);
    return 0;
}

4. مؤشر إلى دالة (Pointer to Function)

مثل المؤشر إلى أنواع بيانات مختلفة، لدينا أيضًا مؤشر إلى دالة. يخزن مؤشر الدالة عنوان الدالة. على الرغم من أنه لا يشير إلى أي بيانات، إلا أنه يشير إلى التعليمات الأولى في الدالة. صيغة الإعلان عن مؤشر إلى دالة هي:

/* الإعلان عن دالة */
returnType functionName(parameterType1, parameterType2, ...);

/* الإعلان عن مؤشر إلى دالة */
returnType (*pointerName)(parameterType1, parameterType2, ...);
pointerName = &functionName; /* أو pointerName = functionName; */

المثال أدناه سيوضح الأمر بشكل أكبر.

#include <stdio.h>
#include <stdlib.h>

int *multiply(int *a, int *b) {
    int *c = malloc(sizeof(int));
    if (c == NULL) return NULL;
    *c = *a * *b;
    return c;
}

int main() {
    int a=3, b=5;
    int * (*p)(int *, int *) = &multiply; /* أو int* (*p)(int*, int*) = multiply; */
    int *c = (*p)(&a,&b); /* أو int *c = p(&a,&b); */
    if (c != NULL) {
        printf("Product = %d\n", *c);
        free(c);
    }
    return 0;
}

يمكن قراءة إعلان المؤشر p إلى الدالة multiply() على النحو التالي (باتباع أسبقية العوامل): p هو مؤشر إلى دالة تأخذ مؤشرين إلى int كوسيطين وتُرجع مؤشرًا إلى int. نظرًا لأن اسم الدالة هو أيضًا مؤشر إلى الدالة، فإن استخدام & ليس ضروريًا. كما أن إزالة * من استدعاء الدالة لا يؤثر على البرنامج.

5. مصفوفة من المؤشرات إلى الدوال

لقد رأينا بالفعل كيفية إنشاء مصفوفة من المؤشرات إلى int و char وما إلى ذلك. وبالمثل، يمكننا إنشاء مصفوفة من المؤشرات إلى الدوال. في هذه المصفوفة، سيخزن كل عنصر عنوان دالة، حيث تكون جميع الدوال من نفس النوع. أي أن لها نفس نوع وعدد الوسيطات ونوع الإرجاع. سنقوم بتعديل برنامج نوقش سابقًا في هذا القسم. سنخزن عناوين add() و subtract() و multiply() و divide() في مصفوفة ونجري استدعاء دالة من خلال الفهرس.

#include <stdio.h>

void add(float *a, float *b) {
    float c = *a + *b;
    printf("Addition gives %.2f\n", c);
}

void subtract(float *a, float *b) {
    float c = *a - *b;
    printf("Subtraction gives %.2f\n", c);
}

void multiply(float *a, float *b) {
    float c = *a * *b;
    printf("Multiplication gives %.2f\n", c);
}

void divide(float *a, float *b) {
    if (*b == 0) {
        printf("Error: Division by zero!\n");
        return;
    }
    float c = *a / *b;
    printf("Division gives %.2f\n", c);
}

int main(){
    printf("Enter two numbers :\n");
    float a,b;
    scanf("%f %f", &a, &b);
    printf("What do you want to do with the numbers?\nAdd : a\nSubtract : s\nMultiply : m\nDivide : d\n");
    char operation = '0';
    scanf(" %c", &operation);

    void (*p[])(float *, float *) = {add, subtract, multiply, divide};

    printf("\nOperating...\n\n");
    switch (operation) {
        case 'a' : p[0](&a,&b); break;
        case 's' : p[1](&a,&b); break;
        case 'm' : p[2](&a,&b); break;
        case 'd' : p[3](&a,&b); break;
        default : printf("Invalid input!!!\n");
    }
    return 0;
}

يمكن قراءة الإعلان هنا على النحو التالي: p هي مصفوفة من المؤشرات إلى دوال تأخذ مؤشرين إلى float كوسيطين وتُرجع void.

6. تمرير مؤشر إلى دالة كوسيطة (Callback Functions)

مثل أي مؤشر آخر، يمكن أيضًا تمرير مؤشرات الدوال إلى دالة أخرى، وبالتالي تُعرف بـ “دالة الاستدعاء العكسي” (callback function) أو “الدالة المستدعاة”. الدالة التي يتم تمريرها إليها تُعرف بـ “الدالة المستدعية” (calling function). طريقة أفضل للفهم هي النظر إلى qsort()، وهي دالة مدمجة في C. تُستخدم لفرز مصفوفة من الأعداد الصحيحة، السلاسل النصية، الهياكل، وهكذا. إعلان qsort() هو:

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));

تأخذ qsort() أربع وسيطات:

  1. مؤشر void إلى بداية المصفوفة.
  2. عدد العناصر.
  3. حجم كل عنصر.
  4. مؤشر دالة يأخذ مؤشرين void كوسيطين ويُرجع int.

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

#include <stdio.h>
#include <stdlib.h>

int compareIntegers(const void *a, const void *b) {
    const int *x = (const int *)a;
    const int *y = (const int *)b;
    return *x - *y;
}

int main(){
    int myArray[] = {97, 59, 2, 83, 19, 97};
    int numberOfElements = sizeof(myArray) / sizeof(int);

    printf("Before sorting - \n");
    for (int i = 0; i < numberOfElements; i++)
        printf("%d ", *(myArray + i));

    qsort(myArray, numberOfElements, sizeof(int), compareIntegers);

    printf("\n\nAfter sorting - \n");
    for (int i = 0; i < numberOfElements; i++)
        printf("%d ", *(myArray + i));
    printf("\n");
    return 0;
}
/* الناتج المحتمل */
// Before sorting -
// 97 59 2 83 19 97

// After sorting -
// 2 19 59 83 97 97

بما أن اسم الدالة هو مؤشر بحد ذاته، يمكننا كتابة compareIntegers كوسيطة رابعة.

رابعاً: المؤشرات والهياكل (Structures)

1. المؤشرات والهياكل (Structures)

مثل مؤشرات الأعداد الصحيحة، مؤشرات المصفوفات، ومؤشرات الدوال، لدينا أيضًا مؤشرات إلى الهياكل أو مؤشرات الهياكل.

struct records {
    char name[20];
    int roll;
    int marks[5];
    char gender;
};

struct records student = {"Alex", 43, {76, 98, 68, 87, 93}, 'M'};
struct records *ptrStudent = &student;

هنا، قمنا بالإعلان عن مؤشر ptrStudent من نوع struct records. لقد قمنا بتعيين عنوان student إلى ptrStudent. يخزن ptrStudent العنوان الأساسي لـ student، وهو العنوان الأساسي للعضو الأول من الهيكل. ستؤدي الزيادة بمقدار 1 إلى زيادة العنوان بمقدار sizeof(student) بايت.

printf("Address of structure = %d\n", ptrStudent);
printf("Address of member `name` = %d\n", &student.name);
printf("Increment by 1 results in %d\n", ptrStudent + 1);
/* الناتج المحتمل */
// Address of structure = 6421984
// Address of member `name` = 6421984
// Increment by 1 results in 6422032

يمكننا الوصول إلى أعضاء student باستخدام ptrStudent بطريقتين. باستخدام صديقنا القديم * أو باستخدام -> (عامل السهم). مع *، سنستمر في استخدام عامل النقطة (.)، بينما مع -> لن نحتاج إلى عامل النقطة.

printf("Name without using ptrStudent : %s\n", student.name);
printf("Name using ptrStudent and * : %s\n", (*ptrStudent).name);
printf("Name using ptrStudent and -> : %s\n", ptrStudent->name);
/* الناتج المحتمل */
// Name without using ptrStudent: Alex
// Name using ptrStudent and *: Alex
// Name using ptrStudent and ->: Alex

وبالمثل، يمكننا الوصول إلى الأعضاء الأخرى وتعديلها. لاحظ أن الأقواس ضرورية عند استخدام * لأن عامل النقطة (.) له أسبقية أعلى من *.

2. مصفوفة من الهياكل

يمكننا إنشاء مصفوفة من نوع struct records واستخدام مؤشر للوصول إلى العناصر وأعضائها.

struct records students[10];

/* مؤشر إلى العنصر الأول (الهيكل) من المصفوفة */
struct records *ptrStudents1 = &students[0]; /* يجب أن يشير إلى العنصر الأول */

/* مؤشر إلى مصفوفة من 10 هياكل records */
struct records (*ptrStudents2)[10] = &students;

لاحظ أن ptrStudents1 هو مؤشر إلى students[0] بينما ptrStudents2 هو مؤشر إلى المصفوفة بأكملها من 10 هياكل struct records. ستؤدي إضافة 1 إلى ptrStudents1 إلى الإشارة إلى students[1]. يمكننا استخدام ptrStudents1 مع حلقة للتنقل عبر العناصر وأعضائها.

// مثال على كيفية تعبئة المصفوفة أولاً (للتوضيح)
// for (int i = 0; i < 10; i++) {
//     sprintf(students[i].name, "Student%d", i);
//     students[i].roll = 100 + i;
//     students[i].gender = (i % 2 == 0) ? 'M' : 'F';
//     for (int j = 0; j < 5; j++) students[i].marks[j] = 70 + j + i;
// }

for (int i = 0; i < 10; i++)
    printf("%s, %d\n", (ptrStudents1 + i)->name, (ptrStudents1 + i)->roll);

3. تمرير مؤشر إلى هيكل كوسيطة لدالة

يمكننا أيضًا تمرير عنوان متغير هيكل إلى دالة.

#include <stdio.h>

struct records {
    char name[20];
    int roll;
    int marks[5];
    char gender;
};

void printRecords(struct records *ptr) {
    printf("Name: %s\n", ptr->name);
    printf("Roll: %d\n", ptr->roll);
    printf("Gender: %c\n", ptr->gender);
    for (int i = 0; i < 5; i++)
        printf("Marks in %dth subject: %d\n", i, ptr->marks[i]);
}

int main(){
    struct records student = {"Alex", 43, {76, 98, 68, 87, 93}, 'M'};
    printRecords(&student);
    return 0;
}
/* الناتج المحتمل */
// Name: Alex
// Roll: 43
// Gender: M
// Marks in 0th subject: 76
// Marks in 1th subject: 98
// Marks in 2th subject: 68
// Marks in 3th subject: 87
// Marks in 4th subject: 93

لاحظ أن الهيكل struct records مُعلن عنه خارج main(). هذا لضمان توفره عالميًا ويمكن لدالة printRecords() استخدامه. إذا تم تعريف الهيكل داخل main()، فسيكون نطاقه محدودًا بـ main(). يجب أيضًا الإعلان عن الهيكل قبل الإعلان عن الدالة. مثل الهياكل، يمكن أن يكون لدينا مؤشرات إلى الاتحادات (unions) ويمكن الوصول إلى الأعضاء باستخدام عامل السهم (->).

خامساً: المؤشر إلى مؤشر (Pointer to Pointer)

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

int var = 6;
int *ptr_var = &var;
printf("Address of var = %d\n", ptr_var);
printf("Address of ptr_var = %d\n", &ptr_var);
/* الناتج المحتمل */
// Address of var = 6422036
// Address of ptr_var = 6422024

لتخزين عنوان المتغير الصحيح var، لدينا المؤشر إلى int وهو ptr_var. سنحتاج إلى مؤشر آخر لتخزين عنوان ptr_var. بما أن ptr_var من نوع int *، لتخزين عنوانه، سيتعين علينا إنشاء مؤشر إلى int *. يوضح الكود أدناه كيف يمكن القيام بذلك.

int **ptr_ptrvar = &ptr_var; /* أو int* *ppvar أو int **ppvar */

يمكننا استخدام ptr_ptrvar للوصول إلى عنوان ptr_var واستخدام إلغاء المرجع المزدوج للوصول إلى var.

printf("Address of ptr_ptrvar = %d\n", ptr_ptrvar);
printf("Address of var = %d\n", *ptr_ptrvar);
printf("Value at var = %d\n", *(*ptr_ptrvar));
/* الناتج المحتمل */
// Address of ptr_ptrvar = 6422024
// Address of var = 6422036
// Value at var = 6

ليس من الضروري استخدام الأقواس عند إلغاء مرجع ptr_ptrvar. لكنها ممارسة جيدة لاستخدامها. يمكننا إنشاء مؤشر آخر ptr_ptrptrvar، والذي سيخزن عنوان ptr_ptrvar. بما أن ptr_ptrvar من نوع int**، فسيكون الإعلان عن ptr_ptrptrvar كالتالي:

int ***ptr_ptrptrvar = &ptr_ptrvar;

يمكننا مرة أخرى الوصول إلى ptr_ptrvar، ptr_var، و var باستخدام ptr_ptrptrvar.

printf("Address of ptr_ptrptrvar = %d\n", ptr_ptrptrvar);
printf("Value at ptr_ptrptrvar = %d\n", *ptr_ptrptrvar);
printf("Address of ptr_var = %d\n", *ptr_ptrptrvar);
printf("Value at ptr_var = %d\n", *(*ptr_ptrptrvar));
printf("Address of var = %d\n", *(*ptr_ptrptrvar));
printf("Value at var = %d\n", *(*(*ptr_ptrptrvar)));
/* الناتج المحتمل */
// Address of ptr_ptrptrvar = 6422016
// Value at ptr_ptrptrvar = 6422024
// Address of ptr_var = 6422024
// Value at ptr_var = 6422036
// Address of var = 6422036
// Value at var = 6

رسم متحرك يوضح المؤشر إلى مؤشر، وكيف يشير كل مؤشر إلى عنوان المؤشر الذي يليه

إذا قمنا بتغيير القيمة في أي من المؤشرات باستخدام ptr_ptrptrvar أو ptr_ptrvar، فسيتوقف المؤشر (أو المؤشرات) عن الإشارة إلى المتغير الأصلي.

الخلاصة التقنية

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

كما رأينا، تلعب المؤشرات دورًا حيويًا في تمرير الوسيطات للدوال (Call by Reference)، وإرجاع القيم من الدوال بأسلوب آمن، وحتى الإشارة إلى الدوال نفسها وإنشاء مصفوفات منها. أخيرًا، كشفنا عن قوة المؤشرات في التعامل مع الهياكل، ووصلنا إلى مفهوم المؤشر إلى مؤشر الذي يفتح مستويات جديدة من التحكم في الذاكرة. إن فهم المؤشرات ليس مجرد ميزة، بل هو حجر الزاوية لإتقان لغة C وكتابة أكواد فعالة ومحسنة.

تذكر أن الممارسة هي مفتاح الإتقان. العب بالمؤشرات، جرب الأمثلة، ولا تتردد في استكشاف المزيد من المفاهيم المتقدمة مثل تخصيص الذاكرة الديناميكي (Dynamic Memory Allocation) لتعميق فهمك. استمر في التعلم، وابقَ آمنًا.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *