المؤشر متغير أو ثابت قيمته عنوان متغير أو ثابت آخر. المؤشرات ميزة قوية وقد تكون خطيرة في لغة C. قد يكون فهم المؤشرات تحديًا.
int i = 5;
عندما نُسند قيمة إلى متغير يمكننا حينها طرح سؤالين.
من أنت؟
أنا i.
ماذا تملك؟
أملك العدد الصحيح 5.
وهناك سؤال ثالث يمكننا طرحه.
أين أنت؟
أنا عند 0x6dfed4.
انتظر، ماذا؟
المتغيرات والثوابت تخزّن قيمتها في موقع في الذاكرة. يُعرف ذلك الموقع في الذاكرة بعنوان المتغير أو الثابت.
للحصول على عنوان متغير نستخدم عامل العنوان، وهو &.
int i = 5;
int *i_address = &i;
انتظر، ماذا تعني تلك * قبل i_address؟ ألا تعني الضرب؟
نوع يتبعه * ثم معرف هو طريقة تعريف مؤشر إلى متغير من ذلك النوع.
تُعرف * بعامل الإشارة غير المباشرة لأنها تشير بشكل غير مباشر إلى القيمة التي يحمل المؤشر عنوانها.
حسنًا، لكن لماذا ننشئ متغيرًا فقط ليحمل عنوان متغير آخر؟
لماذا نهتم أصلًا بما هو عنوان المتغير؟
حسنًا، نهتم بمكان المتغيرات عندما تكون في مصفوفة.
عندما ننشئ مصفوفة هكذا
int my_ints[] = {1, 2};
قيمة my_ints هي في الواقع عنوانها، والذي يصادف أنه نفس عنوان عنصرها الأول.
سيكون عنوان عنصرها الثاني هو عنوان العنصر الأول زائد حجم int على النظام.
لرؤية هذا بشكل أفضل، جرّب تشغيل ما يلي
#include <stdio.h>
int main() {
// my_int's value is its address
int my_ints[] = {1, 2};
// since my_ints is already an address
// we don't need the address operator to make a pointer from it.
// a pointer to my_ints holds the address of my_ints
int *my_ptr = my_ints;
// the address of my_ints and the value of my_ptr are the same
printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
// the address of the first element is the same as the values in the line above
printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
// the address of the second element has increased by the size of an int
printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); //
return 0;
}
يوضح المثال السابق لماذا يبدأ فهرسة المصفوفة من 0 بدلًا من 1.
فهرس المصفوفة يضرب الفهرس في حجم النوع ويضيفه إلى عنوان بداية المصفوفة.
بما أن العنصر الأول يتماشى مع بداية المصفوفة، نضرب النوع في 0 لإضافته إلى عنوان البداية للحصول على العنصر الأول.
للحصول على العنصر الثاني نضرب النوع في 1 لإضافته إلى عنوان البداية.
إذا كان طول النوع أربعة بايتات، فسيكون عنوان العنصر الثاني أعلى بأربعة بايتات من العنصر الأول.
هذا أيضًا سبب تعريف المؤشرات على أنها من نوع معين.
إذا لم يعرف المؤشر نوع ما يشير إليه، فلن يعرف كم بايت يجب أن يتقدم عند إضافة 1 إليه.
إضافة 1 إلى مؤشر لا تعني بالضرورة أننا نضيف بايت واحد إلى العنوان.
بل تعني أننا نضيف 1 مضروبًا في طول نوعه بالبايتات.
لكن انتظر، هناك المزيد! لم تظن أننا انتهينا من *، أليس كذلك؟
الآن بعد أن فهمنا كيف تعمل العناوين مع فهرسة المصفوفة، ما الفائدة إن لم نحصل على القيمة المخزنة في عنوان؟
كيف نفعل ذلك؟
للحصول على القيمة المخزنة في عنوان نستخدم عامل الإشارة غير المباشرة، والذي قد تتذكر أنه *.
في هذه الحالة، يُشار إلى عامل الإشارة غير المباشرة أحيانًا أيضًا بعامل إلغاء الإشارة، لأنه يلغي إشارة القيمة التي يملك المؤشر عنوانها.
تسميته عامل إلغاء الإشارة عند استخدامه لإلغاء الإشارة تميّزه عن عامل الإشارة غير المباشرة المستخدم لتعريف مؤشر، لكن * في كلا الاستخدامين يُسمى تقنيًا عامل الإشارة غير المباشرة.
هيا!
هل تمزح معي؟
كم عدد استخدامات *؟!
الضرب، وتعريف مؤشر، والآن إلغاء الإشارة!
إنه مربك!
وما هو إلغاء الإشارة بالضبط، على أي حال؟
يوضح مثال كيفية استخدام عامل الإشارة غير المباشرة لإلغاء الإشارة
#include <stdio.h>
int main() {
// define the variable
int my_ints[] = {1, 2};
// define the pointer from the address of the variable
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
// prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
return 0;
}
يمكن أن يساعد استخدام الأقواس الهلالية في إزالة الغموض عن النوايا عند استخدام حساب المؤشرات. قد لا يطبع المثال التالي ما قصده المبرمج
#include <stdio.h>
int main() {
int my_ints[] = {1, 42};
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *++my_ptr is 42.
printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
// prints: my_ints[1] is 43. ++*my_ptr++ is 43.
printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);
return 0;
}
لكل من * و ++ نفس الأسبقية، لكنهما يُقيّمان من اليمين إلى اليسار.
في السطر الأول، يُزاد my_ptr أولًا إلى العنصر الثاني، ثم يُلغى الإشارة إليه.
في السطر الثاني، يُلغى الإشارة إلى my_ptr أولًا، وقد كان قد زيد إلى العنصر الثاني في السطر الأول، ثم تُزاد القيمة بعد إلغاء الإشارة.
إذا أضفنا هذا السطر إلى المثال أعلاه
// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);
في ذلك السطر، زدنا المؤشر ليتجاوز المصفوفة.
لقد تجاوزنا المصفوفة، ونشير إلى البايتات الأربعة التالية في الذاكرة، والتي يفسرها المؤشر على أنها int، لكن في الحقيقة قد تنتمي تلك البايتات إلى متغير آخر أو متغيرات أخرى.
قد تكون تلك البايتات الأربعة جزءًا من double أو قد تكون أربعة char.
المشكلة أننا لا ينبغي أن ننظر إليها حتى باستخدام مؤشرنا.
كان يمكن أن نستخدم
// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);
بما أن فهرسة المصفوفة تُنفذ باستخدام مؤشر، فإن نفس خطر تجاوز المصفوفة ينطبق.
وبما أن السلسلة النصية مصفوفة من char، يمكن استخدام المؤشرات مع السلاسل النصية أيضًا.
غالبًا ما تُعرّف السلسلة النصية باستخدام صيغة المؤشر، هكذا
char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";
سؤال واحد، مع ذلك. لماذا نضع النجمة بجانب المعرف؟ لماذا لا نضعها بجانب النوع، بما أنه مؤشر إلى ذلك النوع؟ لأن هذا
int* my_ptr1, my_ptr2_that_is_really_an_int;
لا يعرّف مؤشرين إلى int، بل يعرّف مؤشرًا إلى int، وint.
تعريف مؤشرين إلى int سيكون هكذا
int *my_ptr1, *my_ptr2;
لذلك من الجيد وضع * بالقرب من المعرف لتقليل الغموض.
أو تعريف معرف واحد فقط في كل سطر.
وإذا لم تكن قد اكتفيت من *، فمرحبًا بك في...
من الشائع رؤية مصفوفات من السلاسل النصية ممثلة هكذا
#include <stdio.h>
void print_strings(char **passed_in_strings) {
printf("%s\n", *passed_in_strings);
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char *my_strings[] = {"One string", "Two string"};
print_strings(my_strings);
return 0;
}
قد لا يكون واضحًا، لكن مصفوفة السلاسل النصية تُعرّف وتهيأ كمؤشر إلى مصفوفة من مصفوفات char.
عند تمرير مصفوفة مصفوفات char إلى print_strings()، تصفها التوقيع كمؤشر إلى مؤشر (أو مؤشرات) إلى char.
إذن كيف نعرف أن char ** هو حقًا مؤشر إلى مصفوفة من المؤشرات إلى مصفوفات char، وليس مؤشرًا إلى مؤشر إلى char واحد؟
لا نعرف.
هذا واحد من الأشياء العديدة التي قد تجعل استخدام المؤشرات مربكًا. قد يُترجم ما يلي بنجاح
#include <stdio.h>
void print_strings(char **passed_in_strings) {
// prints: A (followed by gibberish)
printf("%s\n", *passed_in_strings);
// prints gibberish
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char my_char = 'A';
char *my_ptr = &my_char;
char **my_ptr_to_ptr = &my_ptr;
print_strings(my_ptr_to_ptr);
return 0;
}
لكن print_strings() لا يُستخدم كما هو مقصود.
استخدام توقيع مختلف لا يساعد. تغيير التوقيع إلى هذا
void print_strings(char *passed_in_strings[]) {
// code snipped
}
قد يظل يُترجم ويطبع كلامًا غير مفهوم.
لكن لا قيمة حقيقية لتمرير مؤشر إلى مؤشر إلى char واحد.
على الأرجح، عندما ترى char **، فإنه يمثل مؤشرًا إلى مصفوفة من المؤشرات إلى مصفوفات char.
رأينا مؤشرًا يُمرر إلى دالة. لكن لماذا نمرر مؤشرًا إلى دالة؟
في لغة C، تُمرر الوسائط إلى الدوال حسب القيمة. هذا يعني أن القيمة تُنسخ من الوسيط، وأن ذلك النسخ محلي في الدالة. عندما تنتهي الدالة، تنتهي أيضًا القيمة المنسوخة.
انظر ماذا يحدث عندما نحاول تغيير القيم داخل دالة
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
//prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", a, b);
}
int main() {
int a = 3, b = 4;
swap(a, b);
// prints: After 'swap', a is 3 and b is 4
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
هذا لأن المتغيرات، رغم أنها تحمل الأسماء نفسها، فإن a و b في swap() ليسا نفس a و b في main().
إنهما نسختان من قيمتي a و b.
هل يمكننا تغيير swap ليعمل كما نقصد؟
نعم يمكننا!
يمكننا تمرير عنواني a و b كمؤشرات.
لكن ما الفائدة من ذلك، بما أن الدالة تنسخ المؤشرات؟
إنه مفيد، لأن العنوان المنسوخ يظل نفس العنوان.
بإلغاء الإشارة إلى العنوان يمكننا الوصول إلى القيمة الأصلية كما عُرّفت في main().
#include <stdio.h>
// pass in pointers to a and b
void swap(int *a, int *b) {
// set temp to value at a's dereferenced address
int temp = *a;
// set a's value to value at b's dereferenced address
*a = *b;
// set b's value to temp
*b = temp;
// prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}
int main() {
int a = 3, b = 4;
swap(&a, &b);
// prints: After 'swap', a is 4 and b is 3.
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
هناك الكثير من * في swap الآن، لكنها تعمل الآن كما هو مقصود.
إحدى طرق قراءة عامل الإشارة غير المباشرة هي "value at".
إذن، *a هي القيمة في a.
ما مدى نجاح هذا مع السلاسل النصية؟
char
نعلم أن قيمة متغير مصفوفة هي العنوان الذي تبدأ عنده المصفوفة.
ونعلم أنه يمكننا تعريف سلسلة نصية إما كمصفوفة char، char my_string[]، أو كمؤشر إلى char، char *.
كيف يختلفان؟
في هذا الكود نمرر وسيطًا من نوع char * إلى describe_me().
الوسيط adjective معرّف كمصفوفة char وتقبله الدالة كمؤشر إلى char.
#include <stdio.h>
#include <string.h>
// takes pointer to char
void describe_me(char *adjective){
strcpy(adjective, "evil");
// prints: I am evil.
printf("I am %s.\n", adjective);
}
int main() {
// defined as a char array
char adjective[] = "good";
describe_me(adjective);
// prints: I am really evil.
printf("I am really %s.\n", adjective);
return 0;
}
نحن قادرون على إجراء تغيير دائم في adjective، مما يوضح أن مصفوفة char المعرّفة يمكن قبولها كمؤشر إلى char.
الآن لنجرب تعريف adjective كمؤشر إلى char مباشرة.
#include <stdio.h>
#include <string.h>
void describe_me(char *adjective){
// segmentation fault on this line when program is executed
strcpy(adjective, "evil");
printf("I am %s.\n", adjective);
}
int main() {
char *adjective = "good";
describe_me(adjective);
printf("I am really %s.\n", adjective);
return 0;
}
أتتذكر كيف أن المؤشرات قوية، لكنها قد تكون خطيرة أيضًا؟
لكن لماذا لا يعمل؟
المصفوفة مؤشر، وقد عمل مؤشر مع مصفوفة char!
السبب أنه، سابقًا، كان adjective معرّفًا كمصفوفة char وهي متغير ينتمي إلى main().
الآن، adjective معرّف كسلسلة نصية حرفية، بمعنى أن chars تُكتب في ذاكرة ثابتة للقراءة فقط تنتمي إلى البرنامج بأكمله.
كلاهما مؤشران، لكنهما يشيران إلى شيئين مختلفين.
محاولة تغيير قيمة سلسلة نصية حرفية تشبه محاولة تغيير القيمة الحرفية لـ 5.
هذا غير مسموح.
إذن، المشكلة ليست حقًا أنه مؤشر.
المشكلة هي ما يشير إليه.