C: المؤشرات والمصفوفات
المصفوفات والمؤشرات كوجهين لنفس الشخص — من زوايا مختلفة تظهر بأشكال مختلفة، لكن في الأساس تشيران إلى نفس كتلة الذاكرة المتجاورة.
1. اسم المصفوفة هو عنوان العنصر الأول
في معظم التعبيرات، يتحول اسم المصفوفة تلقائيًا إلى مؤشر إلى العنصر الأول:
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]، وخطوته حجم المصفوفة بالكامل.
printf("%zu\n", sizeof(arr));
printf("%zu\n", sizeof(p));
في نظام 64 بت، المخرجان 20 (5 × 4 بايت) و8 (حجم المؤشر)، على التوالي.
2. اجتياز المصفوفات بالمؤشرات
بما أن المؤشر يمكن أن يشير إلى عناصر المصفوفة، يمكنك اجتياز المصفوفة بالكامل باستخدام حساب المؤشرات:
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 شائع جدًا.
يمكنك أيضًا استخدام تدوين الدليل:
int i;
for (i = 0; i < len; i++) {
printf("%d ", *(arr + i));
}
أو:
int *p = arr;
int i;
for (i = 0; i < len; i++) {
printf("%d ", p[i]);
}
p[i] و *(p + i) متكافئتان تمامًا. مؤثر الدليل هو سكر نحوي لحساب المؤشرات.
3. حساب المؤشرات
تدعم المؤشرات مجموعة محدودة من العمليات الحسابية:
(1) الجمع والطرح مع الأعداد الصحيحة
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) طرح المؤشرات
طرح مؤشرين داخل نفس المصفوفة يُرجع عدد العناصر بينهما:
int *q = &arr[4];
printf("%ld\n", (long)(q - p));
المخرجات: 2، لأن p يشير إلى arr[2] و q يشير إلى arr[4]، بفرق عنصرين.
(3) مقارنة المؤشرات
يمكن مقارنة المؤشرات داخل نفس المصفوفة باستخدام < و <= و > و >= لتحديد المواضع النسبية:
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))، رغم أن لا أحد يكتب الأخير.
لكن المؤشرات والمصفوفات لها اختلافات جوهرية:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
arrمصفوفة;sizeof(arr)= 20؛ لا يمكن إعادة إسنادهاpمؤشر؛sizeof(p)= 8 (64 بت)؛ يمكن إعادة إسناده للإشارة إلى مكان آخر
▶ مثال
عكس مصفوفة باستخدام المؤشرات:
#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;
}
7 6 5 4 3 2 1
تقنية المؤشر المزدوج: left يتحرك من البداية نحو النهاية، و right يتحرك من النهاية نحو البداية، مبادلاً العناصر التي يشيران إليها حتى يلتقيا.
5. طبيعة المصفوفات كمعاملات دوال
عند تمرير مصفوفة إلى دالة، يمرر المترجم عنوان العنصر الأول فقط؛ تُفقد معلومات طول المصفوفة. هذا هو "انحلال المصفوفة إلى مؤشر":
void func(int arr[]) {
}
المعامل int arr[] يكافئ int *arr. داخل الدالة:
sizeof(arr)يعطي حجم المؤشر، وليس حجم المصفوفة- لا يمكنك استخدام
sizeofلحساب عدد العناصر - يجب تمرير الطول كمعامل منفصل
void print_array(int *arr, int len) {
int i;
for (i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
أربع تواقيع متكافئة:
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);
الرقم بين الأقواس المربعة يتجاهله المترجم؛ إنه مجرد إشارة إلى "هذا مؤشر".
▶ مثال
إزالة التكرار من مصفوفة مرتّبة باستخدام المؤشرات:
#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;
}
1 2 3 4 5
dst يتتبع موضع الكتابة للبيانات غير المكررة؛ src يمسح المصفوفة الأصلية. الدالة تُرجع الطول الجديد. هذه خوارزمية إزالة تكرار في المكان لا تحتاج مصفوفة إضافية.
❓ أسئلة شائعة
arr = p; غير قانوني; اسم المصفوفة هو مؤشر ثابت. لكن p = arr; قانوني لأن متغير المؤشر يمكن إسناده.arr و &arr؟arr هو عنوان العنصر الأول، من نوع int *، وخطوته sizeof(int)؛ &arr هو عنوان المصفوفة بالكامل، من نوع int (*)[5]، وخطوته sizeof(int[5]). القيمة العددية واحدة، لكن الأنواع مختلفة.p[-1] قانوني؟*(p-1). إذا كان p يشير إلى وسط مصفوفة، فإن p[-1] يصل إلى العنصر السابق بأمان. لكن الوصول خارج حدود المصفوفة سلوك غير معرّف.📖 ملخص
- اسم المصفوفة ينحل إلى مؤشر إلى العنصر الأول في التعبيرات، إلا مع sizeof و &
arr[i]و*(arr+i)متكافئان تمامًا؛ الأدلة سكر نحوي لحساب المؤشرات- حساب المؤشرات يعمل بوحدة الخطوة، حيث الخطوة = sizeof(النوع المُشار إليه)
- تنحل المصفوفات إلى مؤشرات عند تمريرها كمعاملات دوال؛ يجب تمرير الطول بشكل منفصل
- تقنية المؤشر المزدوج تحل بكفاءة مشكلات كالعكس وإزالة التكرار
📝 تمارين
- اكتب دالة تُدير مصفوفة لليسار بموضع واحد (تنقل العنصر الأول إلى النهاية) باستخدام المؤشرات، بدون مصفوفة إضافية.
- اكتب دالة
int *find(int *arr, int len, int target)تُرجع مؤشرًا إلى أول عنصر يساوي target، أو NULL إذا لم يُوجد. - بمعلومية مصفوفة مرتّبة، نفّذ بحثًا ثنائيًا باستخدام المؤشرات. أرجع مؤشرًا إلى العنصر الموجود، أو NULL إذا لم يُوجد.