القائمة الرئيسية

الصفحات

المراجع والمؤشرات في السي بلاس بلاس

C++ المراجع

مفهوم المراجع C++

المراجع ( References ) هي العناوين التي يتم إعطائها لأي شيء (مثل المتغيرات, المصفوفات و الكائنات) يتم تعريفه في الذاكرة عند تشغيل البرنامج.
عناوين الأشياء التي يتم تخصيص مساحة لها في الذاكرة, يتم وضعها بأسلوب Hexadecimal حيث تجد أغلب العنواين تحتوي على أرقام و أحرف كالتالي 0xd5ef87c.

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

إمكانية الوصول للأشياء الموجودة في الذاكرة هو أهم ما يميز لغة C++ عن باقي اللغات التي لا يمكن فيها ذلك كلغة جافا و لغة بايثون.

الآن, للوصول إلى عناوين الأشياء الموجودة في الذاكرة نستخدم العامل & الذي يقال له Address Operator.

طباعة عناوين الأشياء الموجودة في الذاكرة في C++

لطباعة عنوان المساحة المخصصة لأي متغير تم تعريفه في الذاكرة نضع & قبل إسمه كما سنرى في المثال التالي.

مثال

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// و قيمته 5 x هنا قمنا بتعريف متغير إسمه
		int x = 5;

		// في الذاكرة x هنا قمنا بطباعة عنوان المساحة التي تم تخصيصها للمتغير
		cout << "Address of x in memory: " << &x;

		return 0;
		}
	  

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

		Address of x in memory: 0x61fe1c
	  

الصورة التالية تظهر كيف تم تخزين عنوان و قيمة المتغير x في الذاكرة.

ربط متغيرين على نفس العنوان الموجود في الذاكرة في C++

إذا قمت بتعريف متغير و تريد الوصول له بشكل مباشر بإسم مختلف, يمكنك تعريف متغير آخر و تجعله يشير لعنوانه في الذاكرة كما في المثال التالي.

المثال الأول

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// و قيمته x هنا قمنا بتعريف متغير إسمه
		int x = 5;

		// في الذاكرة x يشير بنفس عنوان المتغير y هنا قمنا بتعريف متغير إسمه
		int &y = x;

		// x هي نفسها قيمة المتغير y و لاحظ كيف أن قيمة المتغير .y و x هنا قمنا بطباعة قيمة المتغيرين
		cout << "x = " << x << endl;
		cout << "y = " << y;

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		x = 5
		y = 5
	  

الصورة التالية تظهر كيف أن المتغير y يشير لنفس قيمة المتغير x في الذاكرة.


عندما تقوم بربط متغيرين على نفس القيمة الموجود في الذاكرة, تصبح قادر على تغييرها من أي واحد منهما.
في المثال التالي قمنا بتعريف المتغير x ثم قمنا بتعريف متغير إسمه y و إعطائه نفس قيمة x.
بعدها قمنا بتغيير القيمة المشتركة بين المتغيرين x و y عن طريق المتغير y.
في الأخير قمنا بعرض القيمة التي يحتويها x و القيمة التي يحتويها y للتأكد ما إن كانت تغيرت بالنسبة لكلاهما أم لا.

المثال الثاني

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// و قيمته x هنا قمنا بتعريف متغير إسمه
		int x = 5;

		// في الذاكرة x يشير بنفس عنوان المتغير y هنا قمنا بتعريف متغير إسمه
		int &y = x;

		// x و التي هي نفسها قيمة المتغير y هنا قمنا بتغيير قيمة المتغير
		y = 7;

		// للتأكد ما إن كانا يحتويان على نفس القيمة الجديدة و التي هي 7 y و x هنا قمنا بطباعة قيمة المتغيرين
		cout << "x = " << x << endl;
		cout << "y = " << y;

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		x = 7
		y = 7
	  

ملاحظات و أمثلة عملية حول استخدام المراجع في C++

القواعد التالية مهمة جداً عند التعامل مع المراجع و يجب أن تحفظها حتى لا تقع في أخطاء بسببها:

  • عندما ننشئ مرجع لشيء فنحن كأننا نضيف إسم آخر ( Alias ) لهذا الشيء و لا يمكن إلغاؤه.

  • عند تعريف المرجع, يجب مباشرةً تحديد عنوان الشيء الذي يشير لقيمته في الذاكرة.

  • بعد تعريف المرجع, لا يمكن تغيير الشيء الذي يشير لقيمته.


هنا وضعنا لك عدة أمثلة حول الأخطاء التي قد تقع فيها عند التعامل مع المراجع حتى تتعلم كيف تتجنبها.

الخطأ الأول

في المثال التالي قمنا بتعريف x كمرجع بدون أن نحدد إسم المتغير الذي سيكون بمثابة مرجع له.

	  int &x;
	

هذا الكود سيسبب ظهور الخطأ التالي و الذي يعني أنه لم يتم إعطاء x قيمة أولية.
القيمة الأولية هنا تعني إسم المتغير الذي سيكون x يمثابة مرجع له.

error: 'x' declared as reference but not initialized


الخطأ الثاني

المثال التالي قمنا بتعريف متغيرين هما x و y و وضعنا قيمة أولية في كل منهما.
بعدها قمنا بتعريف مرجع للمتغير x إسمه z.
بعدها حاولنا جعل المرجع z يصبح بمثابة مرجع للمتغير y.

	  int x = 5;
	  int y = 7;

	  int &z = x;   // إلى هنا لا يوجد أي مشكلة
	  &z = y;       // يشير لقيمة متغير آخر غير الذي تم تعريفه من أجله في الأساس z هنا سيحدث مشكلة لأننا حاولنا جعل المرجع
	

بما هذا الكود سيسبب ظهور الخطأ التالي عند تشغيل الكود لأنه لا يمكن تغيير المتغير الذي يشير إليه المرجع z بعد أن يتم تعريفه.

error: lvalue required as left operand of assignment


المثال التالي يعلمك كيف تقوم بتمرير الشيء الموجود في الذاكرة بشكل مباشر لدالة.
هنا قمنا بتعريف دالة إسمها swap, عند استدءعاها نمرر لها متغيرين فتقوم بتبديل قيمهما.

مثال

main.cpp
		#include <iostream>

		using namespace std;

		// و ستقوم هي بإحضار قيمهما من الذاكرة b و a عند إستدعاءها نمرر لها عنوان متغيّرين مكان الباراميترين swap هنا قمنا بتعريف دالة إسمها
		void swap(int &a, int &b)
		{
		// الموجودين في الذاكرة b و a هنا ستقوم الدالة بتيديل قيم المتغيرين
		int temp = a;
		a = b;
		b = temp;
		}

		int main()
		{
		// و قيمته 5 y و قيمته 3 و متغير إسمه x هنا قمنا بتعريف متغير إسمه
		int x = 3;
		int y = 5;

		// لها حتى تبدل قيمهما y و x و تمرير المتغيرين swap() هنا قمنا باستدعاء الدالة
		swap(x, y);

		// ما إن كانت قيمهما قد تبدلت أم لا y و x هنا قمنا بطباعة قيمة المتغيرين
		cout << "x = " << x << endl;
		cout << "y = " << y;

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		x = 5
		y = 3
	  

C++ المؤشرات

مفهوم المؤشرات C++

في الدرس السابق تعرفنا على المراجع ( References ) و تعلمنا كيف أنها تستخدم بهدف جعلنا قادرين على الوصول إلى الأشياء المعرفة في الذاكرة بشكل مباشر.
المؤشرات ( Pointers ) تستخدم أيضاً للوصول لأي شيء يتم تعريفه في الذاكرة و هي تعطينا المزيد من المزايا في التحكم.


الفرق بين المرجع و المؤشر

الفرق الأساسي بينهما هو أن المؤشر يقوم بحجز مساحة في الذاكرة لتخزين عنوان الشيء الذي يؤشر إليه.
بالإضافة إلى ذلك, فإن المؤشر يمكنه الإشارة لأي شيء موجود في الذاكرة في أي وقت و لست مجبراً على تحديد الشيء الذي يشير إليه لحظة إنشاءه.

في هذا الدرس ستفهم الفرق بين المراجع و المؤشرات بالنسبة للذاكرة و ستتعرف على المزايا الإضافية التي توفرها المؤشرات.

تعريف مؤشر في C++

لتعريف مؤشر جديد نستخدم الرمز * مع الإشارة إلى أن نوع المؤشر يجب أن يكون نفس نوع الشيء الذي سيشير له في الذاكرة.

إذا اردنا تعريف مؤشر نوعه int و إسمه x فعندنا ثلاث خيارات كالتالي.

	  // الأسلوب الأول و الذي يعتبر الأكثر استخداماً
	  int* x;

	  // الأسلوب الثاني
	  int *x;

	  // الأسلوب الثالث
	  int * x;
	


الآن, لتعريف مؤشر و جعله يشير لقيمة شيئ موجود في الذاكرة, يجب أن نقوم بتمرير عنوان هذا الشيء كقيمة للمؤشر و عندها سيصبح المؤشر قادر على الوصول لقيمته و عنوانه كما سنرى في المثال التالي.

مثال

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// "Arabic" و قيمته language هنا قمنا بتعريف متغير إسمه
		string language = "Arabic";

		// في الذاكرة language هنا قمنا بتعريف مؤشر لعنوان المتغير
		string* ptr = &language;

		// language هنا قمنا بطباعة قيمة المتغير
		cout << "language = " << language << endl;

		// في الذاكرة language هنا قمنا بطباعة عنوان المتغير
		cout << "Address of language = " << &language << endl;

		// في الذاكرة ptr هنا قمنا بطباعة عنوان المؤشر
		cout << "Address of ptr in memory = " << &ptr << endl;

		// language و التي هي عنوان المتغير ptr هنا قمنا بطباعة القيمة الموجودة في المؤشر
		cout << "Value of ptr in memory = " << ptr << endl;

		// language في الذاكرة و التي هي قيمة المتغير ptr هنا قمنا بطباعة القيمة التي يشير إليها عنوان المؤشر
		cout << "Value that ptr point to = " << *ptr;

		return 0;
		}
	  

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.

		language = Arabic
		Address of language = 0x61fde0
		Address of ptr in memory = 0x61fdd8
		Value of ptr in memory = 0x61fde0
		Value that ptr point to = Arabic
	  

الصورة التالية تظهر كيف تم تخزين عنوان و قيمة المتغير language و المؤشر ptr في الذاكرة.


من المهم جداً أن تلاحظ الطريقة التي يتم فيها إنشاء المؤشر في الذاكرة حتى تعرف كيف تتعامل معه بسهولة.

  • المؤشر ptr تم تخصيص مساحة خاصة له في الذاكرة و هذه المساحة لها عنوان و قيمة أيضاً.

  • قيمة المؤشر ptr هي عنوان المتغير lanugage في الذاكرة, و هذا يعني أن قيمة المؤشر دائماً تكون عنوان الشيء الذي يشير إليه في الذاكرة.


بالنسبة للتعامل مع المؤشر فلاحظنا التالي:

  • نكتب إسم المؤشر فقط في حال أردنا الوصول لقيمته, كمثال ptr

  • نضع & قبل إسم المؤشر في حال أردنا الوصول لعنوانه في الذاكرة, كمثال &ptr

  • نضع * قبل إسم المؤشر في حال أردنا الوصول لقيمة الشيئ الذي يشير إليه, كمثال *ptr

تحديد الشيء الذي يتم الإشارة إليه في C++

في المثال السابق لاحظنا أن المؤشر في الذاكرة يشبه كثيراً الشيء الذي يشير إليه حيث يملك قيمة و عنوان خاص في الذاكرة.
و لاحظنا أيضاً أننا في المؤشر لا نضع قيم عادية, حيث أن قيمته تكون عنوان الشيء الذي يشير إليه في الذاكرة و الذي من خلاله يمكننا الوصول لقيمته.

الآن إذا أردت جعل المؤشر يشير إلى شيء آخر موجود في الذاكرة, فكل ما عليك فعله هو تمرير عنوان الشيء الآخر له كقيمة.


في المثال التالي قمنا بتعريف متغيرين إسمهما x و y.
بعدها قمنا بتعريف مؤشر إسمه ptr جعلناه يشير للمتغير x و عرضنا التي يشير إليها.
ثم جعلنا ptr يشير للمتغير y و عرضنا القيمة التي يشير إليها.

مثال

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// قيمته 2 y قيمته 1 و متغير x هنا قمنا بتعريف متغير
		int x = 1;
		int y = 2;

		// ptr هنا قمنا بتعريف مؤشر إسمه
		int* ptr;

		// x يشير إلى عنوان المتغير ptr هنا قمنا بجعل المؤشر
		ptr = &x;

		// x و التي هي قيمة المتغير ptr هنا قمنا بطباعة قيمة المتغير الذي يشير إليه
		cout << "*ptr = " << *ptr << endl;

		// y يشير إلى عنوان المتغير ptr هنا قمنا بجعل المؤشر
		ptr = &y;

		// y من جديد و التي أصبحت قيمة المتغير ptr هنا قمنا بطباعة قيمة المتغير الذي يشير إليه
		cout << "*ptr = " << *ptr << endl;

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		*ptr = 1
		*ptr = 2
	  

الفيديو التالي يظهر كيف تم تغيير قيمة المؤشر ptr حيث تم جعله يشير لعنوان المتغير x و من ثم يشير لعنوان المتغير y.

تغيير قيمة الشيء الذي يشير إليه المؤشر في C++

في المثال التالي قمنا بتعريف متغير إسمه x قيمته 1 و مؤشر إسمه ptr يشير للمتغير x.
من خلال المؤشر ptr قمنا بتغيير قيمة المتغير x و من بعدها قمنا بطباعة قيمته للتأكد.

مثال

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// و قيمته 1 x هنا قمنا بتعريف متغير إسمه
		int x = 1;

		// في الذاكرة x و جعلناه يشير لعنوان المتغير ptr هنا قمنا بتعريف مؤشر إسمه
		int* ptr = &x;

		// إلى 5 ptr هنا قمنا بتغير قيمة المتغير الذي يشير إليه المؤشر
		*ptr = 5;

		// للتأكد ما إن كانت قد تغيرت أم لا x هنا قمنا بطباعة قيمة المتغير
		cout << "x = " << x << endl;

		// x و التي هي نفسها قيمة المتغير ptr هنا قمنا بطباعة قيمة المتغير الذي يشير إليه
		cout << "*ptr = " << *ptr;

		return 0;
		}

	  

سنحصل على النتيجة التالية عند التشغيل.

		x = 5
		*ptr = 5
	  

أمثلة حول مزايا استخدام المؤشرات في C++


المثال الأول

هنا وضعنا مثال حول كيفية إنشاء مؤشر لا يملك قيمة ( Null Pointer ) بالإضافة إلى كيفية التشييك على المؤشر لمعرفة ما إن كان يملك قيمة أم لا.

إذا أردت إنشاء مؤشر لا يملك قيمة, لا بد أن تعطيه القيمة NULL أو القيمة nullptr لكي تضمن أنه فارغ و لا يوجد فيه أي قيم إفتراضية.
عندما تمرر له إحدى هاتين القيمتين تصبح قيمته تساوي 0 و عندها يصبح بشكل قاطع لا يشير لأي شيء موجود في الذاكرة لأنه لا يمكن لعنوان شيء موجود في الذاكرة أن يكون 0 فقط.


ملاحظة

القيمة nullptr هي قيمة مخصصة في لغة C++ للتعامل مع المؤشرات.
القيمة NULL يمكن إستخدامها مع المؤشرات و أي شيء آخر نريد ضمان جعله لا يملك قيمة.


في المثال التالي قمنا بإنشاء مؤشر و طباعة القيمة الإفتراضية الموجودة فيه.

المثال الأول

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// بدون جعله يشير لقيمة شيء موجود في الذاكرة x هنا قمنا بتعريف مؤشر إسمه
		int* x;

		// في الذاكرة x هنا قمنا بطباعة عنوان الشيء الذي يشير إليه المؤشر
		cout << "x = " << x;

		return 0;
		}
	  

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.
نلاحظ أن المؤشر x ظهر فيه قيمة عشوائية.

		x = 0x10
	  

هنا وضعنا مثال حول كيفية إنشاء مؤشر لا يملك قيمة ( Null Pointer ) بالإضافة إلى كيفية التشييك على المؤشر لمعرفة ما إن كان يملك قيمة أم لا.

المثال الثاني

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// و جعله لا يشير لأي شيء موجود في الذاكرة x هنا قمنا بتعريف مؤشر إسمه
		int* ptr = NULL;

		// لا يشير لشيء موجود في الذاكرة ptr سيتم تنفيذ أمر الطباعة الموضوع هنا إذا كان المؤشر
		if (!ptr)
		{
		cout << "ptr is null";
		}
		// يشير لشيء موجود في الذاكرة ptr سيتم تنفيذ أمر الطباعة الموضوع هنا إذا كان المؤشر
		else
		{
		cout << "ptr is not null";
		}

		return 0;
		}

	  

سنحصل على النتيجة التالية عند التشغيل.

		ptr is null
	  

إذاً عند التشييك على قيمة المؤشر نكتب (!ptr) إذا أردنا تنفيذ أوامر في حال كان المؤشر فارغ.
و نكتب (ptr) إذا أردنا تنفيذ أوامر في حال كان المؤشر يشير لشيء ما.



المثال الثاني

هنا وضعنا مثال حول كيفية جعل المؤشر يشير لقيم عناصر المصفوفة باستخدام العوامل الحسابية ( Arithmetic Operator ) الأربعة -, --, +, ++.

عندما نقوم بتعريف مصفوفة, فإننا نقوم بتعريف شيء واحد و نحدد عدد عناصره.
و عندها يقوم المترجم بإنشاء جميع عناصر المصفوفة بالترتيب وراء بعضها في الذاكرة.
إذا قمت بإضافة مؤشر لتعريف المصفوفة فهذا يجعلك قادر على التنقل بين عناصرها باستخدام العوامل الحسابية -, --, +, ++.


معلومة

عندما نستخدم مؤشر للتنقل بين عناصر المصفوفة, نقوم بجعل قيمة المؤشر تساوي المصفوفة كما هي.
عندها سيتم وضع عنوان أول عنصر في المصفوفة كقيمة للمؤشر كما سترى في المثال التالي.


في المثال التالي قمنا بتعريف مصفوفة إسمها fruit تحتوي على ثلاث قيمة نصية.
بعدها قمنا بإنشاء مؤشر إسمه ptr و جعله يشير للمصفوفة.

مثال

main.cpp
		#include <iostream>

		using namespace std;

		int main()
		{
		// تحتوي على ثلاث قيم fruit هنا قمنا بتعريف مصفوفة إسمها
		string fruit[3] = {"Apple", "Banana", "Orange"};

		// fruit و جعلناه يشير لعناصر المصفوفة ptr هنا قمنا بتعريف مؤشر إسمه
		string* ptr = fruit;

		// و من الطبيعي أنها ستكون أول قيمة موجودة في المصفوفة ptr هنا قمنا بطباعة قيمة العنصر الذي يشير إليه المؤشر
		cout << *ptr << endl;

		// بهدف الإنتقال إلى العنصر التالي الموجود في المصفوفة ptr هنا قمنا بإضافة 1 على عنوان العنصر الموجود في المؤشر
		ptr++;

		// و من الطبيعي أنها ستكون ثاني قيمة موجودة في المصفوفة ptr هنا قمنا بطباعة قيمة العنصر الذي يشير إليه المؤشر
		cout << *ptr << endl;

		// بهدف الإنتقال إلى العنصر التالي الموجود في المصفوفة ptr هنا قمنا بإضافة 1 على عنوان العنصر الموجود في المؤشر
		ptr++;

		// و من الطبيعي أنها ستكون ثالث قيمة موجودة في المصفوفة ptr هنا قمنا بطباعة قيمة العنصر الذي يشير إليه المؤشر
		cout << *ptr;

		return 0;
		}
	  

سنحصل على نتيجة تشبه النتيجة التالية عند التشغيل.
نلاحظ أن المؤشر x ظهر فيه قيمة عشوائية.

		Apple
		Banana
		Orange
	  


المثال الثالث

المثال التالي مهم جداً و منه ستتعلم كيف تنشئ دالة تأخذ باراميتر عبارة عن مؤشر و كيف نقوم بتمرير قيمة للبارامتير عند استدعاءها.

المثال التالي يعلمك كيف تنشئ دالة تأخذ باراميتر عبارة عن مؤشر و كيف نقوم بتمرير قيمة للبارامتير عند استدعاءها.
هنا قمنا بتعريف دالة إسمها swap, عند استدءعاها نمرر لها متغيرين فتقوم بتبديل قيمهما.

مثال

main.cpp
		#include <iostream>

		using namespace std;

		// لأننا سنشير لهذين العنوان من خلالهما b و a عند إستدعاءها نمرر لها عنوان متغيّرين مكان الباراميترين swap هنا قمنا بتعريف دالة إسمها
		void swap(int *a, int *b)
		{
		// الموجودين في الذاكرة b و a هنا ستقوم الدالة بتيديل قيم المتغيرين
		int temp = *a;
		*a = *b;
		*b = temp;
		}

		int main()
		{
		// و قيمته 5 y و قيمته 3 و متغير إسمه x هنا قمنا بتعريف متغير إسمه
		int x = 3;
		int y = 5;

		// لها حتى تبدل قيمهما y و x و تمرير عنوان المتغيرين swap() هنا قمنا باستدعاء الدالة
		swap(&x, &y);

		// ما إن كانت قيمهما قد تبدلت أم لا y و x هنا قمنا بطباعة قيمة المتغيرين
		cout << "x = " << x << endl;
		cout << "y = " << y;

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		x = 5
		y = 3