المؤشرات في C: العناوين و malloc و free
المؤشرات (Pointers) أشهر موضوع في C، وأكثر ما يخيف المبتدئين. لكن فكرتها بسيطة جداً: المؤشر متغير يخزن عنوان متغير آخر في الذاكرة. استخدمتها سريعاً في درس الدوال لتبديل قيمتين. في هذا الدرس ستفهمها بعمق: كيف تعمل، وما علاقتها بالمصفوفات، وكيف تحجز بها ذاكرة جديدة أثناء تشغيل البرنامج.
الذاكرة شارع طويل
Section titled “الذاكرة شارع طويل”تخيّل ذاكرة الكمبيوتر شارعاً طويلاً من البيوت، لكل بيت رقم (العنوان)، وبداخله ساكن (القيمة). عندما تكتب int age = 15; يحجز البرنامج بيتاً ويضع فيه الرقم 15.
- العلامة
&قبل اسم المتغير تعطيك عنوان بيته. - المؤشر ورقة مكتوب عليها رقم بيت. لا تحمل الساكن نفسه، بل تدلك على مكانه.
int age = 15;int *p = &age;
printf("%d\n", age);printf("%d\n", *p);
*p = 16;printf("%d\n", age);151516int *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));000000A5F1BFF8DC8العنوان رقم بالنظام الست عشري، ويتغير من تشغيل لآخر. وحجم المؤشر 8 بايت على الأجهزة الحديثة (64-bit) مهما كان نوع ما يشير إليه.
المؤشر الفارغ NULL
Section titled “المؤشر الفارغ NULL”مؤشر لم تعطه عنواناً بعد يحتوي قيمة عشوائية، واستخدامه كارثة. لذلك نعطيه القيمة الخاصة 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.
المؤشرات والمصفوفات
Section titled “المؤشرات والمصفوفات”هنا سر من أسرار 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);10203020int *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والآن تفهم ما قرأته في درس المصفوفات: عند تمرير مصفوفة لدالة تصلها نسخة من العنوان لا نسخة من العناصر، ولذلك تفقد الدالة معرفة الحجم، ولذلك أيضاً يتغير الأصل إذا عدّلته.
إرجاع أكثر من قيمة
Section titled “إرجاع أكثر من قيمة”الدالة في 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 25malloc(count * sizeof(int)): احجز مساحة تكفي 5 أعداد صحيحة.- افحص دائماً أن الناتج ليس
NULL، فالحجز قد يفشل إذا امتلأت الذاكرة. - تتعامل مع المساحة المحجوزة كأنها مصفوفة عادية:
squares[i]. free(squares): أعد الذاكرة للنظام عندما تنتهي منها.squares = NULL: عادة جيدة بعد التحرير، حتى لا تستخدم المؤشر بالخطأ.
الأمر مثل استعارة كتاب من المكتبة: malloc تستعير، و free تُرجع. من يستعير ولا يُرجع يُفرغ المكتبة، وهذا ما يسمى تسرب الذاكرة (Memory Leak).
جرّب بنفسك
Section titled “جرّب بنفسك”- عرّف متغيراً
double price = 19.5ومؤشراً إليه، ثم ضاعف السعر عبر المؤشر واطبع المتغير الأصلي. - اكتب دالة
void resetAll(int *arr, int size)تجعل كل عناصر المصفوفة صفراً، وتأكد من النتيجة فيmain. - اكتب دالة
void divide(int a, int b, int *quotient, int *remainder)تحسب ناتج القسمة وباقيها. - تحدٍّ: اطلب من المستخدم عدد الدرجات، واحجز لها مصفوفة بـ
malloc، واقرأها بـscanf، ثم اطبع المتوسط وحرّر الذاكرة.
أخطاء شائعة
Section titled “أخطاء شائعة”- مؤشر غير مهيأ:
int *p; *p = 5;يكتب في عنوان عشوائي. أعطِ المؤشر عنواناً صالحاً أوNULLعند تعريفه. - نسيان free: كل
mallocيقابلهاfreeواحدة. نسيانها تسرب ذاكرة، وتكرارها مرتين لنفس المؤشر خطأ خطير أيضاً. - الاستخدام بعد التحرير: القراءة من مؤشر بعد
freeتعطي بيانات عشوائية. اجعلهNULLبعد التحرير. - نسيان
&في scanf:scanf("%d", age)تمرر القيمة لا العنوان، فينهار البرنامج. الصحيح&age.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
إذا كان int x = 7; int *p = &x; فماذا تفعل العبارة *p = 9;؟
فك الإشارة يذهب إلى العنوان المخزن في p، وهو عنوان x، ويكتب فيه القيمة الجديدة.
سؤال
ما الذي يجب فعله بعد الانتهاء من ذاكرة محجوزة بـ malloc؟
الذاكرة الديناميكية تبقى محجوزة حتى تحررها بنفسك، ونسيان ذلك يسبب تسرب الذاكرة.
الخلاصة
Section titled “الخلاصة”- المؤشر متغير يخزن عنواناً:
&xعنوان المتغير، و*pالقيمة عند العنوان. - هيّئ المؤشر دائماً، وافحص
NULLقبل فك الإشارة. - اسم المصفوفة مؤشر إلى أول عنصر، و
p[i]تساوي*(p + i). - بتمرير العناوين تعدّل الدالة متغيرات المستدعي وتُرجع أكثر من نتيجة.
mallocتحجز ذاكرة وقت التشغيل، وfreeتحررها، ولكل حجز تحرير.