C: المؤشرات — الأساسيات
المؤشرات كعناوين الشوارع — العنوان بحد ذاته ليس المنزل، لكن من خلاله يمكنك إيجاد ذلك المنزل بالضبط. أتقن العناوين، وأتقن أقوى ميزة في C.
1. ما هو المؤشر؟
كل بايت في الذاكرة له رقم فريد يُسمّى عنوانًا. متغير المؤشر هو متغير مصمم خصيصًا لتخزين عنوان.
int x = 42;
int *p = &x;
xهو متغير عدد صحيح قيمته 42&xهو عنوانxفي الذاكرةpهو متغير مؤشر يخزن عنوانx*pيصل إلى قيمةxمن خلال عنوانها، وهي 42
المؤشرات نفسها تشغل ذاكرة. في نظام 32 بت، المؤشر 4 بايت؛ في نظام 64 بت، 8 بايت، بغض النظر عن النوع الذي يشير إليه.
2. مؤثر أخذ العنوان &
يُستخدم & للحصول على عنوان متغير:
int a = 10;
printf("Value of a: %d\n", a);
printf("Address of a: %p\n", (void *)&a);
مُحدِّد التنسيق %p يطبع عنوانًا؛ يجب تحويل الوسيط إلى void *.
ما يمكنك أخذ عنوانه:
- المتغيرات:
&x - عناصر المصفوفات:
&arr[3] - أعضاء البنى:
&s.age
ما لا يمكنك أخذ عنوانه:
- القيم الحرفية:
&10(غير قانوني) - متغيرات register:
&r(إذا كانrمن نوعregister) - نتائج التعبيرات:
&(a+b)(غير قانوني)
3. مؤثر إلغاء الإشارة *
يُستخدم * للوصول إلى البيانات التي يشير إليها المؤشر:
int x = 42;
int *p = &x;
printf("%d\n", *p);
*p = 100;
printf("%d\n", x);
المخرج الأول 42، والثاني 100. *p = 100 تعدّل x من خلال المؤشر.
* في التصريح تعني "هذا مؤشر"؛ في التعبير تعني "إلغاء الإشارة" (الحصول على القيمة المُشار إليها). المعنى يعتمد على السياق.
إلغاء إشارة مؤشر غير مهيَّأ هو خطأ جسيم:
int *p;
*p = 10;
قيمة p عشوائية؛ الكتابة في عنوان عشوائي قد تُتلف بيانات أخرى أو تُنهي البرنامج فورًا.
4. المؤشر NULL
NULL هو قيمة مؤشر خاصة تعني "لا يشير إلى أي مكان". مُعرَّف في عدة ملفات رأس منها <stddef.h>، وقيمته عادة 0.
int *p = NULL;
if (p == NULL) {
printf("Pointer does not point to a valid address\n");
}
إلغاء إشارة مؤشر NULL يسبب خطأ تقسيم ويُنهي البرنامج. لذا، تحقق دائمًا مما إذا كان المؤشر NULL قبل استخدامه:
void safe_print(int *p) {
if (p != NULL) {
printf("%d\n", *p);
}
}
5. أنواع المؤشرات والخطوة
يحدد نوع المؤشر عدد البايتات المقرؤة عند إلغاء الإشارة، وعدد البايتات التي يتحركها المؤشر في الحساب (الخطوة).
int a = 10;
double b = 3.14;
int *pi = &a;
double *pd = &b;
*piيقرأ 4 بايت (حجم int)، و*pdيقرأ 8 بايت (حجم double)pi+1يتحرك 4 بايت إلى int التالي، وpd+1يتحرك 8 بايت إلى double التالي
خطوة المؤشر تساوي حجم النوع المُشار إليه:
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%d\n", *p);
printf("%d\n", *(p + 1));
printf("%d\n", *(p + 3));
المخرجات: 10، 20، 40. p+1 لا يضيف 1 إلى قيمة العنوان؛ بل يضيف sizeof(int) (4 بايت)، مشيرًا إلى عنصر int التالي.
نتائج طرح المؤشرات تُقاس أيضًا بالعناصر:
int *q = &arr[4];
printf("%ld\n", (long)(q - p));
المخرجات: 4، أي أن 4 عناصر int تفصل بين المؤشرين.
int *p = &d; (حيث d من نوع double) ستنتج تحذير مترجم لأن عدم تطابق النوع يسبب قراءة عدد خاطئ من البايتات عند إلغاء الإشارة.
▶ مثال
مبادلة قيم متغيرين باستخدام المؤشرات:
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main(void) {
int x = 10, y = 20;
printf("Before swap: x=%d, y=%d\n", x, y);
swap(&x, &y);
printf("After swap: x=%d, y=%d\n", x, y);
return 0;
}
Before swap: x=10, y=20
After swap: x=20, y=10
هنا، تُمرَّن العناوين إلى الدالة، التي تعدّل المتغيرات الأصلية من خلال إلغاء الإشارة. هذه هي التقنية الكلاسيكية لـ "محاكاة التمرير بالمرجع" — معامل الدالة هو مؤشر int *، والاستدعاء يمرّر &x.
6. المؤشرات و const
دمج const مع المؤشرات له معنيان:
(1) مؤشر إلى ثابت
const int *p = &x;
*p = 20;
غير قانوني. لا يمكنك تعديل البيانات من خلال p، لكن p نفسه يمكنه الإشارة إلى متغير آخر:
p = &y;
قانوني.
(2) مؤشر ثابت
int * const p = &x;
p = &y;
غير قانوني. لا يمكن لـ p تغيير ما يشير إليه، لكن يمكنك تعديل البيانات من خلال p:
*p = 20;
قانوني.
حيلة حفظ: const يسار * تعدّل البيانات؛ const يمين * تعدّل المؤشر نفسه.
▶ مثال
جمع مصفوفة باستخدام اجتياز المؤشر:
#include <stdio.h>
int array_sum(const int *arr, int len) {
int sum = 0;
const int *end = arr + len;
while (arr < end) {
sum += *arr;
arr++;
}
return sum;
}
int main(void) {
int data[] = {3, 7, 1, 9, 5};
int total = array_sum(data, 5);
printf("Sum: %d\n", total);
return 0;
}
Sum: 25
المعامل const int *arr يشير إلى أن الدالة لن تعدّل محتويات المصفوفة. المؤشر end يحدد موضع التوقف، متجنبًا الحاجة للأدلة.
❓ أسئلة شائعة
* في int *p إلى النوع أم إلى المتغير؟int* p, q; تصرّح p كمؤشر int و q كـ int عادي. يُنصح بتصريح مؤشر واحد فقط في كل سطر.📖 ملخص
- المؤشرات تخزن عناوين;
&يأخذ العنوان، و*يلغي الإشارة للحصول على القيمة - المؤشرات غير المهيَّأة هي مؤشرات طليقة؛ هيّئها دائمًا إلى NULL
- نوع المؤشر يحدد الخطوة وعدد البايتات المقرؤة عند إلغاء الإشارة
- تمرير مؤشر إلى دالة يسمح لها بتعديل متغيرات المستدعي
constمع المؤشرات له موضعان: تعديل البيانات أو تعديل المؤشر نفسه
📝 تمارين
- اكتب دالة
void divide(int a, int b, int *quotient, int *remainder)تُرجع الحاصل والباقي عبر معاملات مؤشر. - اكتب برنامجًا يعرّف متغير int ومتغير double، ويشير إليهما بـ
int *وdouble *على التوالي، ويطبع عناوينهما وقيمهما، ويراقب فرق الخطوة. - اكتب دالة
int *find_max(int *arr, int len)تُرجع مؤشرًا إلى أكبر عنصر في المصفوفة.