C: المؤشرات — الأساسيات

المؤشرات كعناوين الشوارع — العنوان بحد ذاته ليس المنزل، لكن من خلاله يمكنك إيجاد ذلك المنزل بالضبط. أتقن العناوين، وأتقن أقوى ميزة في C.

1. ما هو المؤشر؟

كل بايت في الذاكرة له رقم فريد يُسمّى عنوانًا. متغير المؤشر هو متغير مصمم خصيصًا لتخزين عنوان.

C
int x = 42;
int *p = &x;

المؤشرات نفسها تشغل ذاكرة. في نظام 32 بت، المؤشر 4 بايت؛ في نظام 64 بت، 8 بايت، بغض النظر عن النوع الذي يشير إليه.


2. مؤثر أخذ العنوان &

يُستخدم & للحصول على عنوان متغير:

C
int a = 10;
printf("Value of a: %d\n", a);
printf("Address of a: %p\n", (void *)&a);

مُحدِّد التنسيق %p يطبع عنوانًا؛ يجب تحويل الوسيط إلى void *.

ما يمكنك أخذ عنوانه:

ما لا يمكنك أخذ عنوانه:


3. مؤثر إلغاء الإشارة *

يُستخدم * للوصول إلى البيانات التي يشير إليها المؤشر:

C
int x = 42;
int *p = &x;

printf("%d\n", *p);
*p = 100;
printf("%d\n", x);

المخرج الأول 42، والثاني 100. *p = 100 تعدّل x من خلال المؤشر.

💡 نصيحة: * في التصريح تعني "هذا مؤشر"؛ في التعبير تعني "إلغاء الإشارة" (الحصول على القيمة المُشار إليها). المعنى يعتمد على السياق.

إلغاء إشارة مؤشر غير مهيَّأ هو خطأ جسيم:

C
int *p;
*p = 10;

قيمة p عشوائية؛ الكتابة في عنوان عشوائي قد تُتلف بيانات أخرى أو تُنهي البرنامج فورًا.


4. المؤشر NULL

NULL هو قيمة مؤشر خاصة تعني "لا يشير إلى أي مكان". مُعرَّف في عدة ملفات رأس منها <stddef.h>، وقيمته عادة 0.

TEXT 📖 للعرض فقط
int *p = NULL;

if (p == NULL) {
    printf("Pointer does not point to a valid address\n");
}

إلغاء إشارة مؤشر NULL يسبب خطأ تقسيم ويُنهي البرنامج. لذا، تحقق دائمًا مما إذا كان المؤشر NULL قبل استخدامه:

C
void safe_print(int *p) {
    if (p != NULL) {
        printf("%d\n", *p);
    }
}
⚠️ ملاحظة: إذا لم يُسنَد للمؤشر قيمة فورًا بعد تعريفه، هيّئه دائمًا إلى NULL. المؤشر "الطليق" غير المهيَّأ له قيمة غير محددة وهو أخطر من مؤشر NULL لأنه لا يمكن كشفه بفحص NULL.


5. أنواع المؤشرات والخطوة

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

C
int a = 10;
double b = 3.14;

int *pi = &a;
double *pd = &b;

خطوة المؤشر تساوي حجم النوع المُشار إليه:

C
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 التالي.

نتائج طرح المؤشرات تُقاس أيضًا بالعناصر:

TEXT 📖 للعرض فقط
int *q = &arr[4];
printf("%ld\n", (long)(q - p));

المخرجات: 4، أي أن 4 عناصر int تفصل بين المؤشرين.

💡 نصيحة: لا يمكن إسناد مؤشرات من أنواع مختلفة مباشرة. int *p = &d; (حيث d من نوع double) ستنتج تحذير مترجم لأن عدم تطابق النوع يسبب قراءة عدد خاطئ من البايتات عند إلغاء الإشارة.

▶ مثال

مبادلة قيم متغيرين باستخدام المؤشرات:

C
#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;
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Before swap: x=10, y=20
After swap: x=20, y=10

هنا، تُمرَّن العناوين إلى الدالة، التي تعدّل المتغيرات الأصلية من خلال إلغاء الإشارة. هذه هي التقنية الكلاسيكية لـ "محاكاة التمرير بالمرجع" — معامل الدالة هو مؤشر int *، والاستدعاء يمرّر &x.


6. المؤشرات و const

دمج const مع المؤشرات له معنيان:

(1) مؤشر إلى ثابت

TEXT 📖 للعرض فقط
const int *p = &x;
*p = 20;

غير قانوني. لا يمكنك تعديل البيانات من خلال p، لكن p نفسه يمكنه الإشارة إلى متغير آخر:

C
p = &y;

قانوني.

(2) مؤشر ثابت

TEXT 📖 للعرض فقط
int * const p = &x;
p = &y;

غير قانوني. لا يمكن لـ p تغيير ما يشير إليه، لكن يمكنك تعديل البيانات من خلال p:

C
*p = 20;

قانوني.

حيلة حفظ: const يسار * تعدّل البيانات؛ const يمين * تعدّل المؤشر نفسه.

▶ مثال

جمع مصفوفة باستخدام اجتياز المؤشر:

C
#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;
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Sum: 25

المعامل const int *arr يشير إلى أن الدالة لن تعدّل محتويات المصفوفة. المؤشر end يحدد موضع التوقف، متجنبًا الحاجة للأدلة.


❓ أسئلة شائعة

س كم بايت يشغل متغير المؤشر؟
ج يعتمد على عرض عنوان النظام: 4 بايت في أنظمة 32 بت، و8 بايت في أنظمة 64 بت، بغض النظر عن النوع المُشار إليه.
س هل تنتمي * في int *p إلى النوع أم إلى المتغير؟
ج تنتمي إلى المتغير. int* p, q; تصرّح p كمؤشر int و q كـ int عادي. يُنصح بتصريح مؤشر واحد فقط في كل سطر.
س ما الفرق بين NULL والمؤشر غير المهيَّأ؟
ج NULL قيمة محددة (0) يمكن فحصها؛ المؤشر غير المهيَّأ له قيمة عشوائية ولا يمكن كشفه بشكل موثوق، مما يجعله أخطر.
س هل إلغاء إشارة مؤشر NULL يُنهي البرنامج دائمًا؟
ج ليس مضمونًا. العنوان 0 قد يكون عنوانًا صالحًا في بعض الأنظمة المدمجة. لكن في أنظمة التشغيل السائدة، الوصول إلى العنوان 0 يُطلق خطأ تقسيم.

📖 ملخص

📝 تمارين

  1. اكتب دالة void divide(int a, int b, int *quotient, int *remainder) تُرجع الحاصل والباقي عبر معاملات مؤشر.
  2. اكتب برنامجًا يعرّف متغير int ومتغير double، ويشير إليهما بـ int * و double * على التوالي، ويطبع عناوينهما وقيمهما، ويراقب فرق الخطوة.
  3. اكتب دالة int *find_max(int *arr, int len) تُرجع مؤشرًا إلى أكبر عنصر في المصفوفة.
Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%