C: المؤشرات والمصفوفات

المصفوفات والمؤشرات كوجهين لنفس الشخص — من زوايا مختلفة تظهر بأشكال مختلفة، لكن في الأساس تشيران إلى نفس كتلة الذاكرة المتجاورة.

1. اسم المصفوفة هو عنوان العنصر الأول

في معظم التعبيرات، يتحول اسم المصفوفة تلقائيًا إلى مؤشر إلى العنصر الأول:

C
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;

printf("%p\n", (void *)arr);
printf("%p\n", (void *)&arr[0]);
printf("%p\n", (void *)p);

العناوين الثلاثة متطابقة. arr و &arr[0] متكافئان — كلاهما عنوان العنصر الأول.

هناك استثناءات: sizeof(arr) يُرجع حجم المصفوفة بالكامل، وليس حجم المؤشر. &arr هو مؤشر إلى المصفوفة بالكامل من نوع int (*)[5]، وخطوته حجم المصفوفة بالكامل.

C
printf("%zu\n", sizeof(arr));
printf("%zu\n", sizeof(p));

في نظام 64 بت، المخرجان 20 (5 × 4 بايت) و8 (حجم المؤشر)، على التوالي.


2. اجتياز المصفوفات بالمؤشرات

بما أن المؤشر يمكن أن يشير إلى عناصر المصفوفة، يمكنك اجتياز المصفوفة بالكامل باستخدام حساب المؤشرات:

C
int arr[] = {10, 20, 30, 40, 50};
int *p;
int len = sizeof(arr) / sizeof(arr[0]);

for (p = arr; p < arr + len; p++) {
    printf("%d ", *p);
}
printf("\n");

arr + len يشير إلى الموضع بعد العنصر الأخير مباشرة، كحارس لإنهاء الحلقة. هذا نمط C شائع جدًا.

يمكنك أيضًا استخدام تدوين الدليل:

C
int i;
for (i = 0; i < len; i++) {
    printf("%d ", *(arr + i));
}

أو:

TEXT 📖 للعرض فقط
int *p = arr;
int i;
for (i = 0; i < len; i++) {
    printf("%d ", p[i]);
}
💡 نصيحة: p[i] و *(p + i) متكافئتان تمامًا. مؤثر الدليل هو سكر نحوي لحساب المؤشرات.


3. حساب المؤشرات

تدعم المؤشرات مجموعة محدودة من العمليات الحسابية:

(1) الجمع والطرح مع الأعداد الصحيحة

C
int arr[] = {10, 20, 30, 40, 50};
int *p = arr + 2;

printf("%d\n", *p);
printf("%d\n", *(p + 1));
printf("%d\n", *(p - 1));

المخرجات: 30، 40، 20. p+1 يتحرك للأمام بمقدار int واحد، و p-1 يتحرك للخلف بمقدار int واحد.

(2) طرح المؤشرات

طرح مؤشرين داخل نفس المصفوفة يُرجع عدد العناصر بينهما:

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

المخرجات: 2، لأن p يشير إلى arr[2] و q يشير إلى arr[4]، بفرق عنصرين.

(3) مقارنة المؤشرات

يمكن مقارنة المؤشرات داخل نفس المصفوفة باستخدام < و <= و > و >= لتحديد المواضع النسبية:

C
int *start = arr;
int *end = arr + len;
while (start < end) {
    printf("%d ", *start);
    start++;
}
⚠️ ملاحظة: حساب المؤشرات له معنى فقط داخل نفس المصفوفة. طرح أو مقارنة مؤشرات من مصفوفات مختلفة هو سلوك غير معرّف.

(4) ملخص خطوة المؤشر

النوع الخطوة (بايتات تحرك p+1)
char * 1
int * 4
double * 8
int (*)[5] 20

الخطوة = sizeof(النوع المُشار إليه)


4. تكافؤ المؤشرات وأدلة المصفوفات

صورتا الوصول التاليتان متكافئتان تمامًا:

التعبير الشكل المكافئ
arr[i] *(arr + i)
&arr[i] arr + i
p[i] *(p + i)
&p[i] p + i

المترجم يحوّل عمليات الدليل إلى حساب مؤشرات. arr[3] و 3[arr] كلاهما قانونيان في C (لأن *(arr+3) == *(3+arr))، رغم أن لا أحد يكتب الأخير.

لكن المؤشرات والمصفوفات لها اختلافات جوهرية:

C
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

▶ مثال

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

C
#include <stdio.h>

void reverse(int *arr, int len) {
    int *left = arr;
    int *right = arr + len - 1;

    while (left < right) {
        int temp = *left;
        *left = *right;
        *right = temp;
        left++;
        right--;
    }
}

int main(void) {
    int data[] = {1, 2, 3, 4, 5, 6, 7};
    int len = sizeof(data) / sizeof(data[0]);
    int i;

    reverse(data, len);

    for (i = 0; i < len; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");
    return 0;
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
7 6 5 4 3 2 1

تقنية المؤشر المزدوج: left يتحرك من البداية نحو النهاية، و right يتحرك من النهاية نحو البداية، مبادلاً العناصر التي يشيران إليها حتى يلتقيا.


5. طبيعة المصفوفات كمعاملات دوال

عند تمرير مصفوفة إلى دالة، يمرر المترجم عنوان العنصر الأول فقط؛ تُفقد معلومات طول المصفوفة. هذا هو "انحلال المصفوفة إلى مؤشر":

C
void func(int arr[]) {
}

المعامل int arr[] يكافئ int *arr. داخل الدالة:

C
void print_array(int *arr, int len) {
    int i;
    for (i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

أربع تواقيع متكافئة:

C
void func(int *arr, int len);
void func(int arr[], int len);
void func(int arr[5], int len);
void func(int arr[100], int len);

الرقم بين الأقواس المربعة يتجاهله المترجم؛ إنه مجرد إشارة إلى "هذا مؤشر".

▶ مثال

إزالة التكرار من مصفوفة مرتّبة باستخدام المؤشرات:

C
#include <stdio.h>

int unique(int *arr, int len) {
    if (len == 0) return 0;

    int *dst = arr;
    int *src = arr + 1;
    int *end = arr + len;

    while (src < end) {
        if (*src != *dst) {
            dst++;
            *dst = *src;
        }
        src++;
    }

    return (int)(dst - arr + 1);
}

int main(void) {
    int data[] = {1, 1, 2, 2, 2, 3, 4, 4, 5};
    int len = sizeof(data) / sizeof(data[0]);
    int new_len = unique(data, len);
    int i;

    for (i = 0; i < new_len; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");
    return 0;
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
1 2 3 4 5

dst يتتبع موضع الكتابة للبيانات غير المكررة؛ src يمسح المصفوفة الأصلية. الدالة تُرجع الطول الجديد. هذه خوارزمية إزالة تكرار في المكان لا تحتاج مصفوفة إضافية.


❓ أسئلة شائعة

س هل يمكن إسناد اسم مصفوفة؟
ج لا. arr = p; غير قانوني; اسم المصفوفة هو مؤشر ثابت. لكن p = arr; قانوني لأن متغير المؤشر يمكن إسناده.
س ما الفرق بين arr و &arr؟
ج arr هو عنوان العنصر الأول، من نوع int *، وخطوته sizeof(int)؛ &arr هو عنوان المصفوفة بالكامل، من نوع int (*)[5]، وخطوته sizeof(int[5]). القيمة العددية واحدة، لكن الأنواع مختلفة.
س لماذا لا يمكنني استخدام sizeof على مصفوفة داخل دالة؟
ج عند تمرير المصفوفة كمعامل، تنحل إلى مؤشر، و sizeof يعطي حجم المؤشر (4 أو 8)، وليس حجم المصفوفة. يجب تمرير الطول بشكل منفصل.
س هل p[-1] قانوني؟
ج نحويًا قانوني، يكافئ *(p-1). إذا كان p يشير إلى وسط مصفوفة، فإن p[-1] يصل إلى العنصر السابق بأمان. لكن الوصول خارج حدود المصفوفة سلوك غير معرّف.

📖 ملخص

📝 تمارين

  1. اكتب دالة تُدير مصفوفة لليسار بموضع واحد (تنقل العنصر الأول إلى النهاية) باستخدام المؤشرات، بدون مصفوفة إضافية.
  2. اكتب دالة int *find(int *arr, int len, int target) تُرجع مؤشرًا إلى أول عنصر يساوي target، أو NULL إذا لم يُوجد.
  3. بمعلومية مصفوفة مرتّبة، نفّذ بحثًا ثنائيًا باستخدام المؤشرات. أرجع مؤشرًا إلى العنصر الموجود، أو NULL إذا لم يُوجد.
Web-Tutorial.com

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

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

100%