المؤشرات Pointers في لغة البرمجة C
شرح المؤشرات Pointers في لغة البرمجة C، ماهو تعريف المؤشر Pointer، كيفية إنشاء أو الإعلان عن المؤشرات في لغة السي، نبذة عن مكونات الذاكرة، كيفية تحديد عنوان المتغير في الذاكرة وعملية الـ Deferencing، أمثلة على المؤشرات في C
تعريف المؤشر Pointer
المؤشر أو الـ Pointer في لغة البرمجة C، هو عبارة عن متغير تحتوي قيمته على عنوان متغير آخر في الذاكرة، ومن الإسم فهو يشير إلى عنوان متغير آخر.
حجم المتغير من النوع Pointer يكون ثابت أي كان حجم أو نوع البيانات التي يقوم بالإشارة إليها، ويتم تحديد الحجم الخاص بالمؤشر طبقا لمعمارية وحدة المعالجة المركزية وبالتبعية نظام التشغيل المستهدف، عادة ما يتم حجز مساحة 4-Byte في المعمارية 32-bit ويتم حجز مساحة 8-Byte في المعمارية 64-bit.
في لغة البرمجة C لا توجد الكلمة Pointer كنوع للبيانات كما هو الحال مع باقي الأنواع مثل int
أو char
، ولكن يتم إضافة رمز النجمة * مع نوع البيانات لتحديد أن هذا المتغير هو مؤشر كما سنتعرف خلال هذا المقال، وسوف تجدني أستخدم المصطلح نوع البيانات Pointer للإشارة إلى ذلك.
لفهم هذه المقالة بشكل جيد يجب أن تكون على علم بـ كيفية التعامل مع المتغيرات وأنواع البيانات في لغة البرمجة C، لذلك يفضل الإطلاع على هذه المقالة قبل التعرف على المؤشرات.
تعتبر المؤشرات من أهم المواضيع التي يجب أن تتعلمها وتفهمها جيدا حتى تصبح مبرمج محترف بلغة الـ C، ولكي تتمكن من فهم المؤشرات يجب أولا أن تتعرف على المكونات الأساسية للذاكرة.
مكونات الذاكرة العشوائية RAM
تتكون الذاكرة من عدد كبير جدا من الوحدات القابلة لتخزين البيانات الثنائية (إما صفر أو واحد) تسمى هذه الوحدات بـ بت أو bit، ونظرا لصغر حجم هذه الوحدة يتم التعامل مع الذاكرة بوحدة أكبر قليلا تسمى بـ بايت أو Byte وهي مكونة من 8 بت، وتعد وحدة البايت هي أصغر وحدة يمكن حجزها لتخزين المتغيرات في أجهزة الحاسوب.
حتى يتمكن جهاز الحاسوب من التعامل مع الذاكرة يتم تقسيم الذاكرة المتاحة في الجهاز إلى وحدات بايت، ويتم ترقيم كل وحدة برقم مسلسل يبدأ بـ صفر، ويسمى هذا الرقم بعنوان وحدة الذاكرة أو الـ Memory Address، فإذا كان جهاز الحاسوب يحتوي على ذاكرة بقدرة تخزينية واحد كيلو بايت 1KB (وهي قدرة صغيرة جدا فقط لتسهيل الفكرة) في هذه الحالة سوف تحتوي الذاكرة على 1024 بايت بما يعني وجود 1024 عنوان للذاكرة، بحيث يمثل العنوان صفر أول وحدة في الذاكرة والعنوان 1023 آخر عنوان في الذاكرة.
تقيد معمارية الحاسوب عدد عناويين الذاكرة التي يمكن التعامل معها، على سبيل المثال المعمارية 32-bit يمكنها التعامل مع عناوين للذاكرة تصل إلى 4,294,967,296 عنوان وهي تساوي 4 جيجا بايت من الذاكرة، هذا العدد من العناويين يمكن تمثيله أو تخزينه في 32 بت أو 4 بايت وهو حجم المتغير من النوع Pointer في هذه المعمارية.
يعتبر العنوان صفر في الذاكرة ذو معنى خاص، حيث يتم حجزه بواسطة نظام التشغيل وعادة ما يشار إليه بالقيمة NULL و يفضل أن يتم تخصيص هذه القيمة أثناء الإعلان عن المؤشرات.
الإعلان عن المؤشرات Declaration
يقصد بالإعلان عن المؤشرات أن يتم إنشاء متغير جديد من النوع Pointer، في هذه العملية يتم حجز مساحة في الذاكرة حجمها 4 بايت في المعمارية 32-bit أو 8 بايت في المعمارية 64-bit وعند تخصيص هذه المساحة تكون قيمتها الإفتراضية أي قيمة ثنائية عشوائية موجودة في لحظة تخصيصها للمتغير وتسمى هذه القيمة بـ Garbage أو قمامة، لذلك يفضل تهيئة المتغير الجديد بتخصيص القيمة NULL.
لكي يتم إنشاء متغير من النوع Pointer يجب تحديد نوع البيانات الخاص بالمتغير الذي سوف يتم إنشاء المؤشر ليتعامل معه، ثم يتم إستخدام رمز النجمة * ويسمى بـ Asterisk قبل إسم المتغير، والشكل التالي يمثل القاعدة العامة لإنشاء متغير من النوع Pointer
Data-Type *variable_name;
عند الإعلان عن المؤشر يمكنك كتابة رمز النجمة بأي شكل من الأشكال التالية
char * s1;char* s2;char *s3;
ولكن يجب أن تعلم أن رمز النجمة مرتبط بإسم المتغير وليس بنوع البيانات، وذلك لتحديد أن هذا المتغير هو مؤشر وليس متغير عادي، فكما تعلم أن لغة البرمجة C تمكنك من إنشاء أكثر من متغير من النفس النوع في جملة واحدة، ويمكنك إنشاء متغير عادي ومتغير مؤشر في نفس الجملة كما في الأمثلة التالية
// i_var is variable, i_ptr is Pointerint i_var, *i_ptr;// c_ptr is Pointer, c_var is variablechar *c_ptr, c_var;
من هذه الأمثلة أردت التأكيد على أن رمز النجمة مرتبط بإسم المتغير وليس بنوع البيانات، وذلك أثناء عملية الإعلان عن المتغير.
عند الإعلان عن متغير من النوع Pointer يجب تحديد نوع البيانات الخاص بالمتغير الآخر الذي سوف يشير إليه وذلك حتى يتمكن مترجم اللغة من تحديد حجم المتغير الآخر.
لضمان عدم حدوث أي أخطاء أثناء التعامل مع المؤشرات يفضل تخصيص القيمة NULL كقيمة إفتراضية أثناء عملية الإعلان عن المؤشر، وذلك لتفادي إستخدام المتغير قبل أن يتم تحديد قيمة فعلية له، فكما تعرفنا أن القيمة NULL لها معنى خاص ولذلك عند محاولة إستخدام المتغير قبل تخصيص قيمته سوف يظهر خطأ لينبهك بذلك.
كما تعرفنا أن القيمة NULL هي مساوية للعنوان صفر في الذاكرة، ولذلك فإن القيمة صفر هي القيمة الوحيدة التي يمكن تخصيصها للمؤشر أثناء الإعلان عنه، والمثال التالي يوضح كيفية تخصيص القيمة الإفتراضية للمؤشر بشكلين هما متساويين في النتيجة
int *ptr = NULL;// هذا الأمر مساوي تماما للأمر السابقint *ptr = 0;
تحديد عنوان المتغير Address Of
حتى الآن قد تعلمنا أن المؤشر يستخدم ليشير إلى عنوان متغير آخر كقيمة له، ولذلك يجب معرفة كيفية إيجاد عنوان متغير في الذاكرة ليتم تخصيص هذا العنوان كقيمة للمؤشر، ويتم ذلك في لغة البرمجة C عن طريق كتابة الرمز & قبل إسم المتغير الذي سبق الإعلان عنه، ويسمى هذا الرمز بـ Address Of Operator.
المثال التالي هو برنامج متكامل كما شرحنا في مقالة الأكواد الأساسية لكتابة برنامج بلغة البرمجة C هذا البرنامج يقوم بإنشاء متغير ثم يقوم بطباعة قيمة المتغير وطباعة عنوان المتغير عن طريق إستخدام الدالة printf، عند تشغيل الكود في المثال التالي سوف تلاحظ أن عنوان المتغير سوف يتغير في كل مرة تقوم بتشغيل الكود
#include <stdio.h>#include <stdlib.h>int main() {int var = 101;// طباعة قيمة المتغيرprintf("Value = %d\n", var);// طباعة عنوان المتغيرprintf("Address = %d\n", &var);system("pause");return 0;}
لاحظ إستخدام رمز & قبل إسم المتغير في أمر طباعة العنوان &var
.
تحديد قيمة المتغير المستخدم مع المؤشر Dereferencing
تعرفنا حتى الآن أن المؤشر يقوم بتخزين عنوان متغير آخر في الذاكرة، وبالتالي عند طباعة متغير من النوع Pointer سوف تحصل على عنوان المتغير الآخر وليس القيمة المخزنة في هذا المتغير، ولكي تتمكن من قراءة القيمة المخزنة للمتغير عن طريق المؤشر نقوم بإستخدام الرمز * قبل إسم المؤشر، ويعرف هذا الرمز في هذه العملية بـ Dereferencing أو إلغاء التأشير.
المثال التالي يوضح كيفية تخصيص قيمة متغير بداخل مؤشر وطباعة عنوان المتغير وقيمته
#include <stdio.h>#include <stdlib.h>int main() {// الإعلان عن متغير ومؤشر في أمر واحدint var, *ptr;// تخصيص القيمة 101 للمتغيرvar = 101;// المؤشر يشير إلى عنوان المتغيرptr = &var;// طباعة عنوان المتغير عن طريق طباعة المؤشرprintf("var Address = %d\n", ptr);// طباعة قيمة المتغير عن طريق إلغاء التأشير للمؤشرprintf("var Value = %d\n", *ptr);// تغيير قيمة المتغير عن طريق إستخدام المؤشر*ptr = 103;// طباعة قيمة المتغير الجديدة عن طريق طباعة المتغير نفسهprintf("var New Value = %d\n", var);system("pause");return 0;}
عند تشغيل الكود من المثال السابق سوف تحصل على قيمة مختلفة لعنوان المتغير في كل مرة، حيث في كل مرة يتم تشغيل البرنامج يتم تخصيص مساحة في الذاكرة مختلفة عن التي سبق تحديدها وبالتالي تحصل على عنوان مختلف في كل مرة، أما عند إلغاء التأشير عن المؤشر يتم طباعة القيمة 101 والتي سبق تخصيصها للمتغير var، ويمكنك تغيير قيمة المتغير نفسه عن طريق إستخدام المؤشر كما في المثال حيث تم تغيير القيمة الموجودة بداخل المتغير var إلى 103 عن طريق إستخدام إلغاء التأشير مع المؤشر، وللتأكيد أن قيمة المتغير قد تحولت إلى 103 قمنا بطباعة قيمة المتغير نفسه.
لتوضيح هذه العملية، فإن مترجم اللغة عندما يجد رمز الـ * قبل إسم المؤشر يقوم بالذهاب إلى العنوان المخزن بداخل المؤشر ليحصل على القيمة الموجودة في هذا العنوان أو تغييرها، والتي تمثل قراءة الرقم 101 في هذا المثال أو تغيير القيمة إلى 103.
أهمية إستخدام المؤشرات في لغة البرمجة C
تمتاز لغة البرمجة C بقدرتها على التعامل مع الذاكرة بشكل مباشر ما يتيح إنشاء برمجيات ذات كفاءة عالية في إستخدام الذاكرة، ويرجع الفضل بشكل كبير إلى المؤشرات، حيث يمكن أن يتم التعامل مع أي متغير أي كان حجمه بمجرد معرفة عنوانه.
وتستخدم المؤشرات بشكل كبير للتعامل مع المصفوفات، حيث تعتبر المصفوفة نوع من أنواع المؤشرات وبالتالي يتم إستخدام العمليات الحسابية على المؤشرات (Pointer Arithmetic) للتنقل بين عناصر المصفوفة كما سنتعرف عليه لاحقا.
يتم إستخدام المؤشرات لتمرير المتغيرات ذات الحجم الكبير بين الدوال، فعند تمرير أي متغير لأي دالة عادة ما يتم إعادة نسخ المتغير في المساحة المخصصة من الذاكرة للدالة (بإستثناء المصفوفات) وتسبب هذه العملية إهدار للوقت المستغرق في عملية النسخ وإهدار للذاكرة المتاحة حيث يتم نسخ المتغير والإحتفاظ بالمتغير الأصلي أي أن المتغير يوجد في الذاكرة مرتين، وتعرف عملية تمرير المؤشرات للدوال بالتمرير عن طريق المرجع أو العنوان Pass By Reference.
تعتبر المؤشرات موضوع حيوي و أساسي عند التحدث عن هياكل البيانات والتي تعرف بـ Data Structures، حيث تستخدم المؤشرات للربط بين وحدات البيانات أو كما تعرف بـ Nodes.
تستخدم المؤشرات لإنشاء متغيرات ديناميكية أو كما تعرف بـ Dynamic-Variables والتي تمكن المبرمج من إنشاء متغيرات أثناء تشغيل البرنامج عند الحاجة إليها، أو إنشاء مصفوفات بالحجم الذي يحدده مستخدم البرنامج، فكما تعلمنا أن المصفوفات في لغة البرمجة C يجب أن يتم تحديد عدد عناصرها بشكل ثابت أثناء إنشاؤها، ولكن بإستخدام المؤشرات يمكنك إنشاء مصفوفات بأي حجم يحدده المستخدم أثناء التشغيل وهو ما يعرف بـ Dynamic Arrays.
أمثلة على المؤشرات في C
المثال التالي يتم إنشاء متغيرين من النوع int
و مؤشر، ويقوم البرنامج بطباعة قيمة المتغيرين بإستخدام المؤشر
#include <stdio.h>#include <stdlib.h>int main() {int num1, num2, *ptr;num1 = 101;num2 = 102;// المؤشر يشير إلى عنوان المتغير الأولptr = &num1;// طباعة عنوان المتغير الأول عن طريق طباعة المؤشرprintf("num1 = %d\n", *ptr);// المؤشر يشير إلى عنوان المتغير الثانيptr = &num2;// طباعة عنوان المتغير الثاني عن طريق طباعة المؤشرprintf("num2 = %d\n", *ptr);system("pause");return 0;}