بسم الله الرحمن الرحيم
اللهم علمنا ما ينفعنا وانفعنا بما علمتنا وزدنا علماً وإخلاصاً في الدين.
جزى الله خيراً الإخوة الذين أشاروا إلى احتمال وجود خطأ في ردي على أحد المواضيع عن التمرير بواسطة المرجع، مما جعلني أقوم بشيء من البحث في الموضوع، أوصلني –بعون الله- للتالي:
وسائط الدالة على ما نعلم، تمرر إليها بإحدى الطريقتين: التمرير بواسطة القيمة، والتمرير بواسطة المرجع.
يأخذ التمرير بواسطة القيمة الشكل التالي كمثال:
int MyFunc(int x)
عند تمرير أي قيمة إلى هذه الدالة، فإنها تأخذها وتنسخها إلى متغير آخر محلي فيها (أي موجود على المكدس Stack الخاص بها)، وتقوم بالعمليات على هذه النسخة.
مثال:
#include <iostream>
using namespace std;
void MyFunc(long x);
int main()
{
//We'll first pass a normal variable.
int a= 5;
cout<< "In main\n";
cout<< "The value is: "<< a<< "\tThe address is: "<< &a<< endl;
MyFunc(a); //We pass the value of the variable
MyFunc((long)&a); //We pass the address of the variable
cout<< endl<< endl;
//Now, we're passing a pointer.
long* p= new long(15);
cout<< "In main\n";
cout<< "The value is: "<< *p<< "\tThe address on the heap is: "<< p;
cout<< "\nThe address of the pointer itself is: "<< &p<< endl; //Note that the address of the pointer differs from the address it's pointing to.
MyFunc(*p); //We pass the value in the address at which points the pointer.
MyFunc((long) p); //We pass the value of the pointer (The address on the heap).
MyFunc((long) &p); //We pass the address of the pointer.
cout<< endl<< endl;
//Finally, we're passing a reference.
int& r= a;
cout<< "In main\n";
cout<< "The value is: "<< r<< "\tThe address is: "<< &r<< endl; //Note that the address of the reference is the same of the variable it referes to.
MyFunc(r); //We pass the value of the reference.
MyFunc((long)&r); //We pass the address of the reference (which is the same of the Variable a).
cout<< endl<< endl;
return 0;
}
void MyFunc(long x)
{
cout<< "In myFunc\n";
cout<< "The value is: "<< x<< "\tThe address is: "<< &x<< endl;
}يمكنك أن تلاحظ كيف قامت الدالة في كل مرة بنسخ القيمة المرسلة إليها والتعامل معها بشكل منفصل تماماً عن المتغير المرسل، والدليل هو أن عنوان المتغير x في الدالة هو عنوان مختلف عن عنوان المتغير المرسل إليها.
بالطبع، دائماً يقولون لنا إننا لا نستطيع تغيير قيمة المتغير المرسل إلى دالة تأخذ وسائطها بالقيمة...
في الحقيقة، يمكننا أن نكسر هذه القاعدة (نوعاً ما)...
ملاحظة: يجب الانتباه إلى أن الطريقة التالية غير عملية وغير صحيحة في الاستعمال، حيث أننا سنقوم بمعاملة متغير int كأنه مؤشر، مع أن لكل واحد منهما صفات خاصة وأسلوباً مختلفاً في حفظ البيانات، والهدف من استعمالها هو التوضيح فقط.
ليكن لدينا المتغير التالي:
int a= 5;
يمكننا أن نضع عنوانه في متغير عادي آخر:
unsigned long b= (unsigned long) &a;
وهذا صحيح تماماً، فإذا كتبنا:
bool IsItReal= b == (unsigned long)&a; cout<< IsItReal<< endl;
ستكون النتيجة صحيحة.
ويمكننا أن نذهب أبعد من ذلك لنكتب:
IsItReal= *(int*)b == a; cout<< IsItReal<< endl;
وستكون النتيجة صحيحة كذلك... مع أن b متغير عادي وليس مؤشراً (طبعاً مع الاضطرار إلى القيام ببعض التحويلات Casting).
ماذا نستنج من ذلك؟؟؟
لتكن لدينا الشفرة التالية:
#include <iostream>
using namespace std;
void Changer(unsigned long x);
int main()
{
int a= 5;
cout<< "In main:"<< endl;
cout<< "The value is: "<< a<< "\tThe address is: "<< &a<< endl;
Changer((unsigned long)&a); //We pass the variable's address.
cout<< "In main:"<< endl;
cout<< "The value is: "<< a<< "\tThe address is: "<< &a<< endl;
return 0;
}
void Changer(unsigned long x)
{
cout<< "In changer:"<< endl;
cout<< "The value is: "<< x<< "\tThe address is: "<< &x<< endl;
*(int *)x= 7;
}استطعنا التعديل على المتغير الأصلي، مع أننا نمرر بالقيمة (هذه الطريقة غير محبذة، يفضل على كل حال استعمال المؤشرات، ولكنها لمجرد التوضيح)، هذا يأخذنا إلى النقطة التالية:
في الأساس، التمرير بواسطة المؤشر، تمرير بواسطة القيمة أيضاً!
ماذا يحدث عندما نمرر قيمة إلى دالة كهذه:
void MyFunc(int* Ptr)
سيتم التعامل مع القيمة الممررة على أنها عنوان، سيؤخذ هذا العنوان، وينسخ أيضاً، ويوضع في المؤشر المحلي Ptr.
وإليك الدليل :) :
#include <iostream>
using namespace std;
void MyFunc(int* Ptr);
int main()
{
//With a variable:
int x= 5;
cout<< "In main:"<< endl;
cout<< "The value is: "<< x<< endl;
cout<< "The address is: "<< &x<< endl;
MyFunc(&x); //Passing the address of the variable.
cout<< endl<< endl;
//With a pointer:
int* p= new int(10);
cout<< "In main:"<< endl;
cout<< "The value is: "<< *p<< endl;
cout<< "The address of the value is: "<< p<< endl;
cout<< "The address of the pointer is: "<< &p<< endl;
MyFunc(p); //Passing the value of the pointer (which is a memory address).
MyFunc((int*) &p); //Passing the address of the pointer (note that it's the address of the pointer itself, not the address it points to).
cout<< endl<< endl;
//With a reference:
int& r= x;
cout<< "In main:"<< endl;
cout<< "The value is: "<< r<< endl;
cout<< "The address is: "<< &r<< endl;
MyFunc(&r); //Passing the address of the reference, which is the same of x.
cout<< endl<< endl;
return 0;
}
void MyFunc(int* Ptr)
{
cout<< "In MyFunc:"<< endl;
cout<< "The value is: "<< *Ptr<< endl;
cout<< "The address of the value is:"<< Ptr<< endl;
cout<< "The address of the pointer is: "<< &Ptr<< endl;
}إذا دققت جيداً، ستجد أن عنوان المتغير في الدالة MyFunc، يختلف عن عنوان المؤشر المرسل إليه، وهذا يعني أنه ليس المؤشر ذاته، بل هو نسخة أخرى خاصة بـMyFunc، تحوي القيمة ذاتها.
لكن، بما أنه يعد تمريراً بالقيمة، كيف نستطيع استخدامه لتغيير قيمة المتغير المرسل؟
بكل بساطة، لأن القيمة التي تم تمريرها والتي يحويها المؤشر عادة هي عنوان، وبالتالي، يمكننا استعمال هذا العنوان للوصول إلى المتغير الأصلي.
أما إذا أردنا الوصول إلى المؤشر نفسه لتغيير قيمته (أي تغيير العنوان الذي يشير إليه)، فيمكننا إما تمرير مؤشر إلى مؤشر، أو مرجع إلى مؤشر:
#include <iostream>
using namespace std;
void PointerManipulator(int** Ptr);
int main()
{
int a= 5;
int* p= &a;
PointerManipulator(&p);
*p= 10;
cout<< a<< endl;
return 0;
}
void PointerManipulator(int** Ptr)
{
*Ptr= new int; //We change the value of the pointer to pointer (which is the address to which points the inner pointer (p)).
}لاحظ أن قيمة a لن تتغير، ستبقى 5، لأن p تم تغيير العنوان الذي يشير إليه، ولم يعد عنوان a.
على كل حال، يمكنك أن تلاحظ وتجرب، لتجد أن عنوان p قد تم نسخه ووضعه في Ptr، لكننا استطعنا الوصول إليه، لأن القيمة الموجودة في Ptr هي عنوان p، والقيمة الموجودة في p هي عنوان a (قبل تعديلها)، وقيمة a هي الـ5. فبالتالي نستنتج أن التمرير لا زال يتم بالقيمة، لكن القيمة هنا كانت عنوان المؤشر p.
إذاً، ماذا نفعل إذا أردنا تمرير المتغير نفسه؟؟
حينها نمرر بالمرجع، لماذا؟
لأن المرجع هو عبارة اسم مستعار Alias للمتغير الذي يمثله، وهذا يعني أن للمرجع القيمة ذاتها، والعنوان في الذاكرة ذاته، وكل ما في الأمر أنه اسم آخر للمتغير نفسه.
فحين نمرر بالمرجع:
void MyFunc(int& Ref)
سيتم وضع المتغير كما هو في الدالة (ولكن باسم آخر هو Ref)، والتعامل معه مباشرة.
وإليك الشفرة التالية:
#include <iostream>
using namespace std;
void MyFunc(int& Ref);
int main()
{
//With variables:
int x= 5;
cout<< "In main:"<< endl;
cout<< "The value is: "<< x<< endl;
cout<< "The address is: "<< &x<< endl;
MyFunc(x); //Passing the address of the variable.
cout<< endl<< endl;
//With a pointer:
int* p= new int(10);
cout<< "In main:"<< endl;
cout<< "The value is: "<< *p<< endl;
cout<< "The address of the value is: "<< p<< endl;
cout<< "The address of the pointer is: "<< &p<< endl;
MyFunc(*p); //Passing the value in the address.
MyFunc((int&) p); //Passing the value of the pointer (which is a memory address).
cout<< endl<< endl;
//With a reference:
int& r= x;
cout<< "In main:"<< endl;
cout<< "The value is: "<< r<< endl;
cout<< "The address is: "<< &r<< endl;
MyFunc(r); //Passing the address of the reference, which is the same of x.
cout<< endl<< endl;
return 0;
}
void MyFunc(int& Ref)
{
cout<< "In MyFunc:"<< endl;
cout<< "The value is: "<< Ref<< endl;
cout<< "The address is: "<< &Ref<< endl;
}يمكنك أن تلاحظ عند تنفيذ الشفرة أن عنوان Ref هو عنوان المتغير المرسل تماماً، وبهذا تحقق الأمر لأول مرة، بأن لم يتم إنشاء متغير جديد.
ويمكنك تمرير مرجع إلى مؤشر أيضاً:
#include <iostream>
using namespace std;
void PointerManipulator(int*& Ptr);
int main()
{
int a= 5;
int* p= &a;
PointerManipulator(p);
*p= 10;
cout<< a<< endl;
return 0;
}
void PointerManipulator(int*& Ptr)
{
Ptr= new int; //We change the value of Ptr (The address to which it points).
}يمكنك أن ترى كيف تم تغيير العنوان الذي يشير إليه p، بحيث لم يعد يشير إلى a.
لاحظ أن النوع يقرأ من اليمين إلى اليسار، فالتالي:
int*& r= a;
هو مرجع إلى مؤشر إلى عدد صحيح.
تجدر بنا الإشارة في النهاية، إلى أن C++ ترينا أن المتغير نفسه يمرر إلى الدالة عند تمريره بواسطة المرجع، لكن إن كان لديك علم بلغة التجميع Assembly ونظرت إلى الشفرة الخاصة بدالة يتم التمرير إليها بالمرجع، ستجد أن عنوان الوسيط يختلف عن عنوان المتغير المرسل إلى الدالة، لكن كليهما يتم تسجيله على المسجل نفسه.
إذاً، للوسيط الممرر بالمرجع عنوان خاص به، فكيف نصل إليه في C++؟
تحتاج هذه الطريقة إلى وجود وسيط آخر يمرر إلى الدالة بالقيمة، فبافتراض أن النظام الذي نعمل عليه x86، ونظام الاستدعاء في الدوال يتم بطريقة cdecl أو stdcall، حيث عند تمرير الوسائط إلى مكدس الدالة Stack، يتم تمريرها من اليمين إلى اليسار بحيث لو كانت لدينا الدالة:
int MyFunc(int& r, int a)
سيتم تخزين a في المكدس أولاً ثم يتم تخزين r، وبالتالي يكون عنواناهما متتاليين حيث إن كان عنوان a هو x، فسيكون عنوان r هو x – 1، فلنجرب المثال التالي لنتحقق:
#include <iostream>
using namespace std;
void MyFunc(int& r, int a);
int main()
{
int x= 5;
int b= 10; //The only use of b is to pass it to MyFunc by value.
cout<< x<< endl;
MyFunc(x, b);
cout<< x<< endl;
return 0;
}
void MyFunc(int& r, int a)
{
int* p= &a - 1;
**(int**)p= 10; //We have to cast it into a pointer to pointer to reach to value of r.
}يمكنك أن تلاحظ كيف وصلنا إلى r من خلال a، وقمنا بتغيير قيمة المتغير الذي يشير إليه r، اضطررنا إلى تحويل المؤشر إلى "مؤشر إلى مؤشر"، وذلك لأن p يشير إلى عنوان r، والذي يشير إلى عنوان x، وهكذا استطعنا الوصول إلى قيمة x وتغييرها.
والحمد لله رب العالمين
جزيل الشكر للأستاذين محمد علاء الدين وخالد الشايع على إثرائهما للموضوع بالكثير من المعلومات والتوضيحات، والتي تم تعديل جزء كبير منه على أثرها، لذلك، لتحصل على المزيد من الفائدة، قم بقراءة الردود التالية للموضوع.
ورجائي لك أيها القارئ بعد قراءة هذا البحث الصغير، أن تسهم في تصليح أي خطأ شكلي أو معنوي فيه، وسأكون لذلك شاكراً ومسروراً.
وأن تدلي بملاحظاتك وإضافاتك على هذا الموضوع.
وجزاك الله كل خير :)


