المؤشرات Pointers
المؤشرات ( Pointers ) في لغة سي, سوف نتعرَّف اليوم على خاصية مهمة جداً في لغة سي و هذه الخاصية تميز هذه اللغه عن غيرها من اللغات. لكونها تُمثِّل المبرمج من تنظيم الذاكرة بشكل اكبر و افضل أثناء تنفيذ البرنامج مقارنة مع اللغات الأُخرى. و هو ما جعل لغة سي لغة مُتقدمة يمكن إستخدامها في برمجة نُظُم التشغيل أو البرمجيات المتطورة الأٌخرى, تُسمى هذه الخاصية بالمؤشرات Pointers.
تخزين القيم الموجودة في الذاكرة
يُمكنك تخيل الذاكرة على أجهزة الكمبيوتر على أنها مصفوفة كبيرة تحتوي على بيانات مُعينة بحجم 1 بايت. لو قٌلنا أن جهاز معين سعة ذاكرة الوصول العشوائية RAM تكون 2GB و الغيغا تعني مليار فهذا يعني أن ذاكرة الوصول العشوائية. لهذا الجهاز يُمكن تخزين 2 مليار بايت. وطبعاً البيانات الموجودة في الذاكرة قد تكون من تخزين إما من نظام التشغيل لها OS أو من قبل برنامج معين يتم تنفيذه.
كيف يتم الوصول إلى مكان مُعين في الذاكرة و تخزين القيم فيه أو إسترجاعها؟
إن لكل موقع في الذاكرة عنوان Address مُعيَّن يُستخدم للوصول إلى هذا الموقع في أي وقت, مثلاً نُركز بالشكل الآتي
في الشكل السابق نُلاحظ أنَّ الموقع صاحب العنوان 0 يحتوي على القيمة 23 و الموقع صاحب العنوان 1 يحتوي على القيمة 26 و الموقع صاحب العنوان 2 يحتوي على القيمة 27 و هكذا......الخ. لنتذكَّر معاً ما تعلّمناه سابقاً عن كيفية تخزين المُتغيرات في الذاكرة, لنُلقي نظرة للشكل الآتي
عند تعريفنا لمُتغير ما يتم حجز مكان معيَّن له في الذاكرة خاص في هذا المتغير, و يبدر الذكر هُنا أن نوع البيانات int و هو من حجم 2 بايت. فإنه سيحتل مكانين فقط في الذاكرة و ليس مكان واحد, لكن بغرض التبسيط هنا في هذا الدرس سنفترض أنه مكان واحد فقط حتى الآن. إن قُمنا بإستدعاء هذا المتغير من خلال إسمه فإن القيمة المخزَّنة فيه هي التي سيتم إرجاعها إلينا
int x = 23; printf("%d", x);
ففي الكود السابق int x يُساوي 23 ثم طبعنا الـ x و سوف يطبع لنا 23, و لكن كيف لنا بإسترجاع. عنوان هذا الموقع, موقع المتغير x الذي تم تخزين القيمة فيه بدلاً من القيمة نفسها؟ يتم ذلك بمجرد إضافة علامة & قبل إسم المتغير
int x = 23; printf("%p", &x);
و طبعاً علامة & تختلف عن علامة & المستخدمة في العمليات المنطقية فهي ذات معنى آخر هنا, و تم تغيير d إلى p يعني. أننا نُريد مكان هذا المتغير و الحرف p إختصار للكلمة pointer و يعني المكان الذي يعيش به المتغير.
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int x = 23; printf("%p", &x); return 0; }
أنشأنا مُتغير عددي int إسمه x و قيمته 23 ثم طلبنا طباعة مكان تواجد أو أين يسكُن هذا المتغير في الذاكرة. عبر %p ثم وضعنا إشارة & قبل إسم المتغير, و عند تشغيل هذا البرنامج سنحصل على النتيجة.
0x7fffc36209ec
لقد طبع لنا المكان 0x7fffc3fd286c حيث يسكُن فيه المٌتغير x في هذا المكان و هو من نوع hexadecimal أي الست عشري. أي أن الأرقام تكون من 0 حتى F و بداية الموقع كان 0x و هو ليس من ضمن الموقع. لهذا المتغير, يُمكن التعرف على النظام الست عشري بمشاهدة الفيديو الآتي
تخزين عناوين المتغيرات في المؤشرات لغة C
تعلَّمنا قبل قليل عن كيفية طباعة عنوان مُتغير ما, لكن كيف لنا أن نستخدم هذا العنوان عدا عن مجرد طباعته. فهل يُمكننا تخزين هذا العنوان لإستخدامه لآحقاً كما في المتغيرات العادية؟ نعم هنا يأتي دور المؤشرات وهي عبارة عن أنواع بيانات تقوم بالتأشير على عنوان مُعين في الذاكرة. و تختلف عن طريقة تعريف المُتغير العادي لكونها تحتوي على علامة النجمة بجانب إسم المتغير.
فهذه النجمة تدل على أن هذا المُتغير مخصص لتخزين العناوين و ليس القيم أي بخلاف المُتغير العادي الذي يُستخدَم لتخزين قيم. و في الصورة السابقة لدينا int x يساوي 9 و x هي متغير عادي يملك قيمة عادية من نوع int أما int*pointer يساوي 1 فهو مؤشر. يقوم بالتأشير على العنوان 1 و لا يُمكننا كتابة المؤشر بهذا الشكل و سوف نرى هذا لآحقاً. نظراً لكون المبرمج لا يرى ذاكرة الوصول العشوائية في جهاز الكمبيوتر و لا يعلَم ما هي البيانات المخزَّنة. فيها و أين هي مخزنة, فمن غير العملي تحديد العنوان بصورة مباشرة كأنه قيمة عادية.
int x = 23; int *pointer = &x; printf("%p", pointer);
فلو أردنا تخزين عنوان المُتغير x في المؤشر المُسمى pointer نقوم بتعريف المؤشر بوضع إشارة النجمه * قبل إسمه و من ثم نقوم بتحديد قيمته و هي عنوان المُتغير x حيث نستخدم إشارة & لإسترجاع عنوان هذا المتغير.
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int x = 26; printf("X value = %d\n", x); printf("X address = %p\n", &x); return 0; }
قمنا بإنشاء مُتغير عددي و إسمه x و قيمته هي 26 ثم طلبنا طباعة المُتغير كما هي في العملية الأولى. و بعملية الطباعة الثانية قُمنا بطلب مكان سكن هذا المٌتغير في الذاكرة, و عند تشغيل. هذا البرنامج سنحصل على نتيجتين كما يلي
X value = 26 X address = 0x7ffde9397b4c
السطر الأول هي قمية المتغير كقيمة عادية, أما السطر الثاني هو مكان تواجد هذا المُتغير في الذاكرة العشوائية RAM.
تخزين عنوان ذاكرة في متغير بواسطة pointer لغة C
يُمكننا أن نقوم بكتابة مكان تواجد المتغير في الذاكرة بمتغير آخر و هذا ما سوف نتعرف عليه بالمثال الآتي
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int x = 26; printf("X value = %d\n", x); printf("X address = %p\n", &x); int *xPointer = &x; printf("X Pointer = %p", xPointer); return 0; }
قمنا بإنشاء مُتغير عددي و إسمه x و قيمته هي 26 ثم طلبنا طباعة المُتغير كما هي في العملية الأولى. وبعملية الطباعة الثانية قُمنا بطلب مكان سكن هذا المٌتغير في الذاكرة, ثم قُمنا بإنشاء متغير إسمه Pointer. ووضعنا قبل الإسم علامة النجمة ثم كتبنا أن قيمة هذا المؤشر هي x ووضعنا علامة & قبل إسم المتغير. ثم طلبنا طباعة هذا المتغير كمتغير عادي و عند تشغيل هذا الكود ستكون النتيجة هي
X value = 26 X address = 0x7ffffb1fc864 X Pointer = 0x7ffffb1fc864
كما لآحظنا في البداية طبع لنا قيمة المتغير كقيمة عادية و هي 26 ثم طبع لنا مكان تواجد هذا المتغير. في الذاكرة و أخيراً قُمنا بتخزين مكان المتغير في متغير عادي.
تعديل القيم بواسطة المؤشرات لغة C
تعديل القيم بواسطة المؤشرات, حتى الآن تعلّمنا كيفية تعريف المؤشرات Pointers و إسترجاع القيمة الموجودة في العناوين التي تُشير إليها.
لكن هل يُمكننا تعديل هذه القيم وليس مجرد إسترجاعها؟
في آخر مثال في الدرس السابق قُمنا بإنشاء متغير x=26 و قُمنا بطباعة المُتغير x و كانت قيمته في البدايه هي 26. ثم قُمنا بطباعة العنوان الخاص في x و كان العنوان هو 0x7ffffb1fc864 من النوع hexadecimal ثم قُمنا بتخزين هذا العنوان. ولتخزين عنوان مُتغير يجب إنشاء مُتغير من نوع pointer و لا يمكن إنشاء مُتغير عادي كما في x.
ثم قُمنا بطباعة قيمة هذا المتغير و هذا المتغير يحمل قيمة عنوان لذلك لا نَضَع d لأننا لا نطبَع رقماً عُشرياً. بل نطبَع عنواناً و العنوان هذا من النوع الست عشري و ليس من الأرقام العادية من 0 إلى 9, ثم قُمنا بالوصول إلى القيمة الموجودة داخل العنوان من خلال النجمة. وقلنا أنَّ هناك فرق عند النجمة عند التعريف و النجمة عند الإستخدام, النجمة عند التعريف تعني أننا نُعرِّف مُتغير من نوع pointer. و أما النجمة عند الإستخدام فتعني أننا نُريد الوصول إلى القيمة التي تَسكُن في هذا العنوان لهذا الـ pointer.
مثال
سنعتمد على المثال الذي كان بنهاية الدرس السابق, سنقوم اولاً بتعديل قيمة x لنرى النتيجة
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int x = 26; printf("X value = %d\n", x); printf("X address = %p\n", &x); int *xPointer = &x; printf("X Pointer = %p\n", xPointer); x = 23; printf("The value inside the title %p = %d\n", xPointer, *xPointer); return 0; }
نُلاحظ أن x في البداية كانت تُساوي 26 و عندما غيرّنا قيمة x إلى 23, تغيرت قيمة x في دالة الطباعة الأخيرة عند *xPointer عندما إستدعيناها للطباعة. عندما قُمنا بطباعة القيمة التي بداخل العنوان *xPointer تم طباعة قيمة x الجديدة و النتيجة هي
X value = 26 X address = 0x7fff0283d794 X Pointer = 0x7fff0283d794 The value inside the title 0x7fff0283d794 = 23
يُمكن ايضاً تغيير قيمة x من خلال الـ Pointer فنحن لدينا وصول كامل من خلال العنوان على المتغير الذي يسكُن هذا العنوان. مثلاً لدينا عنوان منزل يُمكننا تغيير ما بداخل هذا المنزل, لطالما لدينا العنوان من خلال الوصول إلى ما بداخل هذا المنزل.
مثال
و بكل بساطة نقوم بكتابة *xPointer كما تمت طباعتها بالأسفل و نقوم بتغيير قيمتها كما يلي
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int x = 26; printf("X value = %d\n", x); printf("X address = %p\n", &x); int *xPointer = &x; printf("X Pointer = %p\n", xPointer); *xPointer = 1995; printf("The value inside the title %p = %d\n", xPointer, *xPointer); return 0; }
بإعتمادنا على المثال السابق لقد قُمنا بتغيير قيمة x من خلال الـ Pointer و غيّرنا القيمة إلى 1995. ثم قُمنا بالطباعة و طبع لنا مكان سَكَن هذا المتغير من الذاكرة و يساوي القيمة الجديدة و هي 1995. وعند تشغيل الكود سنجد النتيجة الآتية
X value = 26 X address = 0x7ffc84dd0814 X Pointer = 0x7ffc84dd0814 The value inside the title 0x7ffc84dd0814 = 1995
إلى هنا تحدثنا عن كل شيئ يتعلق بأساسيات المؤشرات و من ذلك يُمكننا أن نرى أنها ذات أهمية كبيرة جداً. و من خلالها يستطيع المبرمج الوصول إلى الذاكرة و تنظيمها كيفما يشاء, و هذا امر ضروري لكتابة البرامج المُتقدمة كأنظمة التشغيل OS و برامج التعريف Drivers فضلاً عن البرامج الأُخرى التي تتطلب كفاءة عالية في إدارة الذاكرة. و يَسَعُنا القول أن المؤشرات هي إحدى الأمور التي مَنَحَت لغة سي قوتها بحيث كانت و لا زالت اكثر لغات البرمجة و طلباً على الإطلاق.
مثال
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int x = 7, *cs = &x; printf("%d\n", *cs * *cs); return 0; }
هنا قُمنا بإنشاء متغير من نوع integer و أسميناه x و قيمته هي 7 و انشأنا Pointer معه في نفس السطر. وهذا المؤشر سيقوم بالتأشير على int و نُلاحظ أن إسم هذا الـ Pointer هو *cs و النجمة وضعناها ملاصقة لإسم هذا المؤشر يعني أن هذا المتغير هو مؤشر من نوع int و قيمته تساوي عنوان x. و كلمة عنوان إختصرناها برمز & للدلالة على أن هذا هو العنوان يقع في x و قُمنا بالطباعة و كتبنا *cs الأولى تعني الوصول إلى القيمة التي تقع داخل العنوان *cs. ووضعنا نجمه ايضاً أن يكون العملية عملية ضرب ثم كتبنا *cs مثل كأننا نقول x ضرب x و عند تشغيل الكود نتيجته ستكون 49 كما يلي
49
مثال
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int x = 7, *cs = &x; printf("%d\n", *&x); return 0; }
قُمنا بإنشاء متغير من نوع integer و أسميناه x و قيمته هي 7 و انشأنا Pointer معه في نفس السطر وهذا المؤشر سيقوم بالتأشير على int. ونُلاحظ أن إسم هذا الـ Pointer هو *cs و النجمة وضعناها ملاصقة لإسم هذا المؤشر يعني أن هذا المتغير هو مؤشر من نوع int و قيمته تساوي عنوان x. وكلمة عنوان إختصرناها برمز & للدلالة على ان هذا هو العنوان يقع في x ثم قمنا بطباعة *&x تعني قم بفتح ما بداخل العنوان. و اُنظر ما هي القيمة التي بداخل هذا العنوان x و عندما نرى النتيجة ستكون كما يلي
7
عندما نرى * بعدها & فإن النجمة تلغي مفعول الـ &, فإن قمنا هنا بالتبديل بين & و النجمة &*x سوف يعطينا خطأ. لا يمكن وضع نجمة قبل الـ x ﻷن x هو متغير من نوع int و عند وضع النجمة قبل المتغير مباشرتاً تعني أن المتغير. الذي يأتي بعد النجمة هو من نوع Pointer أي منزل, مكان, عنوان, عندما نضع نجمه يعني انظر إلى داخل هذا العنوان.
العمليات الحسابية على المؤشرات لغة سي
العمليات الحسابية Pointer Arithmetic, هل تعلم أن لغة سي تسمح لنا بإجراء العمليات الحسابية على المؤشرات Pointers. سنتعرف على كيفية عمل ذلك من خلال الكود, الآن ما عليكم سوى إنشاء مشروع جديد في المحرر لديكم و إتباعي على الكود الآتي.
مثال
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int array[4] = {20, 30, 40, 50}; printf("array[0] = %d\n", array[0]); printf("array[1] = %d\n", array[1]); printf("array[2] = %d\n", array[2]); printf("array[3] = %d\n", array[3]); return 0; }
أنشأنا مصفوفة عددية نوع int و إسمها array و بها اربع عناصر و هي ( 20, 30, 40, 50 ). وقُمنا بطباعة هذه القيم على الشاشة بواسطة تحديدها عبر الأندكس الخاص بكل عنصر. ستكون نافذة الإخراج بالشكل
array[0] = 20 array[1] = 30 array[2] = 40 array[3] = 50
نُلاحظ أنه طَبَعَ لنا جميع القيم الموجودة في داخل هذه المصفوفة.
طباعة العناوين للقيم Print addresses
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int array[4] = {20, 30, 40, 50}; printf("array[0] = %p\n", &array[0]); printf("array[1] = %p\n", &array[1]); printf("array[2] = %p\n", &array[2]); printf("array[3] = %p\n", &array[3]); return 0; }
أنشأنا مصفوفة عددية نوع int و إسمها array و بها اربع عناصر و هي ( 20, 30, 40, 50 ). وقُمنا بطباعة عناوين هذه القيم على الشاشة بواسطة تحديد العناوين عبر الأندكس الخاص بكل عنصر. بوضع علامة & قبل إسم المصفوفة للدلالة أنه نريد عنوان و إستخدام الحرف p. ستكون نافذة الإخراج بالشكل
array[0] = 0x7ffe9e957190 array[1] = 0x7ffe9e957194 array[2] = 0x7ffe9e957198 array[3] = 0x7ffe9e95719c
نُلاحظ في اول عنوان 0x7ffe9e957190 بنهايته أن نقطة البداية كانت 0. ثم النقطة الثانية كانت عند 4 لأن حجم int كما قُلنا في الدرس السابق هو 4 بايت و هذا يعني أن كل عنصر. سوف يكون على بُعد 4 بايت من العنصر الذي قبله و عند النقطة الثالثة كان 8 و الرابعة c أي12 بالنظام الست عشري.
إستخدام المؤشرات للوصول للعناوين
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int array[4] = {10, 20, 30, 40}; int *DataCs = &array[0]; printf("address array[0] = %p\n", DataCs); printf("address array[1] = %p\n", DataCs + 1); printf("address array[2] = %p\n", DataCs + 2); printf("address array[3] = %p\n", DataCs + 3); return 0; }
أنشأنا مصفوفة عددية نوع int و إسمها array و بها اربع عناصر و هي ( 20, 30, 40, 50 ). و عرّفنا متغير إسمة DataCs و عرّفناه على أنه مؤشر ووضعنا نجمة قبلهُ و قُمنا بالتأشير على المُتغير الأول الذي في الأسفل. أي جعلنا هذا المؤشر يحمل قيمة العنصر الأول, و قُمنا بطباعة هذه القيم على الشاشة.
array[0] = 0x7ffc9cf3bd40 array[1] = 0x7ffc9cf3bd44 array[2] = 0x7ffc9cf3bd48 array[3] = 0x7ffc9cf3bd4c
لقد قُمنا بالوصول إلى العناصر من دون إستعمال المصفوفة و إستعمال المؤشر على المصفوفة.
إستخدام المؤشرات للوصول للعناصر
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int array[4] = {10, 20, 30, 40}; int *DataCs = &array[0]; printf("address array[0] = %d\n", *DataCs); printf("address array[1] = %d\n", *DataCs + 1); printf("address array[2] = %d\n", *DataCs + 2); printf("address array[3] = %d\n", *DataCs + 3); return 0; }
عرّفنا متغير إسمة DataCs و عرّفناه على أنه مؤشر ووضعنا نجمة قبلهُ و قُمنا بالتأشير على المُتغير الأول الذي في الأسفل. أي جعلنا هذا المؤشر يحمل قيمة العنصر الأول, ثم في الأسفل عند القيمة الثانية وضعنا +1 و عند القيمة الثالثة +2 و القيمة الرابعة +3. هذا و عندما شغلنا الكود قد عمل بشكل طبيعي لكن يوجد خطأ, سنشرح الخطأ بعد صورة نافذة الإخراج الآتية
address array[0] = 10 address array[1] = 11 address array[2] = 12 address array[3] = 13
نُلاحظ أن القيمة الأولى ثابتة 10 أما التي تليها تأتي متسلسلة له 11 و 12 و 13. و هذا يعني أنه لدينا العنوان الأول array[0] هو 10 ثم في الأسفل زِدنا على العنوان 1 في DataCs + 1 يعني أنه نحن قُمنا بالوصول للعنوان DataCs. و زودنا عليه 1 فاصبحت القيمة 11 ثم بنفس الأمر لباقي القيم. و للوصول للقيم بدون زيادة 1 عليها نقوم بوضع اقواس كما يلي
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { int array[4] = {10, 20, 30, 40}; int *DataCs = &array[0]; printf("address array[0] = %d\n", *DataCs); printf("address array[1] = %d\n", *(DataCs + 1)); printf("address array[2] = %d\n", *(DataCs + 2)); printf("address array[3] = %d\n", *(DataCs + 3)); return 0; }
و عند تنفيذ هذا الكود سوف يطبع لنا عناصر المصفوفة و تم الوصول إليهم. عن طريق إستخدام المؤشرات, و نافذة الإخراج للكود هي
address array[0] = 10 address array[1] = 20 address array[2] = 30 address array[3] = 40