تخطَّ إلى المحتوى

المؤشرات في C: العناوين و malloc و free

الدرس 7 من 10 · C

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

تخيّل ذاكرة الكمبيوتر شارعاً طويلاً من البيوت، لكل بيت رقم (العنوان)، وبداخله ساكن (القيمة). عندما تكتب int age = 15; يحجز البرنامج بيتاً ويضع فيه الرقم 15.

  • العلامة & قبل اسم المتغير تعطيك عنوان بيته.
  • المؤشر ورقة مكتوب عليها رقم بيت. لا تحمل الساكن نفسه، بل تدلك على مكانه.
int age = 15;
int *p = &age;
printf("%d\n", age);
printf("%d\n", *p);
*p = 16;
printf("%d\n", age);
الناتج
15
15
16
  • int *p: تعريف مؤشر اسمه p يشير إلى قيمة من نوع int.
  • &age: عنوان المتغير age. خزّناه في p.
  • *p: «اذهب إلى العنوان المكتوب في p وهات ما فيه». هذه العملية اسمها فك الإشارة (Dereference).
  • *p = 16: اذهب إلى العنوان وغيّر ما فيه. تغيّر age نفسه، لأن p يشير إليه.

يمكنك طباعة العنوان نفسه بالمحدد %p:

int age = 15;
int *p = &age;
printf("%p\n", (void *)p);
printf("%zu\n", sizeof(p));
ناتج محتمل
000000A5F1BFF8DC
8

العنوان رقم بالنظام الست عشري، ويتغير من تشغيل لآخر. وحجم المؤشر 8 بايت على الأجهزة الحديثة (64-bit) مهما كان نوع ما يشير إليه.

مؤشر لم تعطه عنواناً بعد يحتوي قيمة عشوائية، واستخدامه كارثة. لذلك نعطيه القيمة الخاصة NULL التي تعني «لا يشير إلى شيء»، ونفحصها قبل الاستخدام:

int *p = NULL;
if (p == NULL) {
printf("المؤشر لا يشير إلى شيء\n");
}
int score = 90;
p = &score;
if (p != NULL) {
printf("%d\n", *p);
}
الناتج
المؤشر لا يشير إلى شيء
90

فك الإشارة لمؤشر قيمته NULL يوقف البرنامج فوراً بالخطأ الشهير Segmentation fault.

هنا سر من أسرار C: اسم المصفوفة يتصرف مثل مؤشر إلى عنصرها الأول.

int nums[4] = {10, 20, 30, 40};
int *p = nums;
printf("%d\n", *p);
printf("%d\n", *(p + 1));
printf("%d\n", p[2]);
p++;
printf("%d\n", *p);
الناتج
10
20
30
20
  • int *p = nums;: بدون &، لأن nums وحدها تعني عنوان العنصر الأول.
  • p + 1: لا تضيف بايتاً واحداً، بل تقفز عنصراً كاملاً (4 بايتات لنوع int). هذا اسمه حساب المؤشرات (Pointer Arithmetic).
  • p[2] و *(p + 2) كتابتان لشيء واحد تماماً.
  • p++: حرّك المؤشر إلى العنصر التالي.

ولهذا يمكن المرور على المصفوفة بمؤشر متحرك:

int nums[4] = {10, 20, 30, 40};
int sum = 0;
for (int *p = nums; p < nums + 4; p++) {
sum += *p;
}
printf("%d\n", sum);
الناتج
100

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

الدالة في C تُرجع قيمة واحدة بـ return. بالمؤشرات تستطيع «إرجاع» عدة نتائج: تستقبل الدالة عناوين متغيرات وتكتب فيها.

void minMax(const int arr[], int size, int *min, int *max) {
*min = arr[0];
*max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] < *min) *min = arr[i];
if (arr[i] > *max) *max = arr[i];
}
}
int scores[5] = {70, 95, 82, 60, 88};
int low, high;
minMax(scores, 5, &low, &high);
printf("الأقل: %d\n", low);
printf("الأعلى: %d\n", high);
الناتج
الأقل: 60
الأعلى: 95

وهذا بالضبط سبب كتابة & في scanf("%d", &age): الدالة تحتاج عنوان المتغير لتكتب فيه ما قرأته.

الذاكرة الديناميكية: malloc و free

Section titled “الذاكرة الديناميكية: malloc و free”

حجم المصفوفة العادية يُحدَّد وقت كتابة الكود. فماذا لو لم تعرف العدد إلا أثناء التشغيل؟ الدالة malloc من المكتبة stdlib.h تحجز ذاكرة بالحجم الذي تطلبه وتُرجع مؤشراً إليها:

#include <stdlib.h>
int count = 5;
int *squares = malloc(count * sizeof(int));
if (squares == NULL) {
printf("فشل حجز الذاكرة\n");
return 1;
}
for (int i = 0; i < count; i++) {
squares[i] = (i + 1) * (i + 1);
}
for (int i = 0; i < count; i++) {
printf("%d ", squares[i]);
}
printf("\n");
free(squares);
squares = NULL;
الناتج
1 4 9 16 25
  • malloc(count * sizeof(int)): احجز مساحة تكفي 5 أعداد صحيحة.
  • افحص دائماً أن الناتج ليس NULL، فالحجز قد يفشل إذا امتلأت الذاكرة.
  • تتعامل مع المساحة المحجوزة كأنها مصفوفة عادية: squares[i].
  • free(squares): أعد الذاكرة للنظام عندما تنتهي منها.
  • squares = NULL: عادة جيدة بعد التحرير، حتى لا تستخدم المؤشر بالخطأ.

الأمر مثل استعارة كتاب من المكتبة: malloc تستعير، و free تُرجع. من يستعير ولا يُرجع يُفرغ المكتبة، وهذا ما يسمى تسرب الذاكرة (Memory Leak).

  1. عرّف متغيراً double price = 19.5 ومؤشراً إليه، ثم ضاعف السعر عبر المؤشر واطبع المتغير الأصلي.
  2. اكتب دالة void resetAll(int *arr, int size) تجعل كل عناصر المصفوفة صفراً، وتأكد من النتيجة في main.
  3. اكتب دالة void divide(int a, int b, int *quotient, int *remainder) تحسب ناتج القسمة وباقيها.
  4. تحدٍّ: اطلب من المستخدم عدد الدرجات، واحجز لها مصفوفة بـ malloc، واقرأها بـ scanf، ثم اطبع المتوسط وحرّر الذاكرة.
  • مؤشر غير مهيأ: int *p; *p = 5; يكتب في عنوان عشوائي. أعطِ المؤشر عنواناً صالحاً أو NULL عند تعريفه.
  • نسيان free: كل malloc يقابلها free واحدة. نسيانها تسرب ذاكرة، وتكرارها مرتين لنفس المؤشر خطأ خطير أيضاً.
  • الاستخدام بعد التحرير: القراءة من مؤشر بعد free تعطي بيانات عشوائية. اجعله NULL بعد التحرير.
  • نسيان & في scanf: scanf("%d", age) تمرر القيمة لا العنوان، فينهار البرنامج. الصحيح &age.

سؤال

إذا كان int x = 7; int *p = &x; فماذا تفعل العبارة *p = 9;؟

سؤال

ما الذي يجب فعله بعد الانتهاء من ذاكرة محجوزة بـ malloc؟

  • المؤشر متغير يخزن عنواناً: &x عنوان المتغير، و *p القيمة عند العنوان.
  • هيّئ المؤشر دائماً، وافحص NULL قبل فك الإشارة.
  • اسم المصفوفة مؤشر إلى أول عنصر، و p[i] تساوي *(p + i).
  • بتمرير العناوين تعدّل الدالة متغيرات المستدعي وتُرجع أكثر من نتيجة.
  • malloc تحجز ذاكرة وقت التشغيل، و free تحررها، ولكل حجز تحرير.