الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

شيء من التشريح لعملية تمرير الوسائط إلى دالة

بدأه Abboodd في 27 يونيو 2010 · 31 رد · 4,388 مشاهدة · في المواضيع والدروس
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

بسم الله الرحمن الرحيم

اللهم علمنا ما ينفعنا وانفعنا بما علمتنا وزدنا علماً وإخلاصاً في الدين.

جزى الله خيراً الإخوة الذين أشاروا إلى احتمال وجود خطأ في ردي على أحد المواضيع عن التمرير بواسطة المرجع، مما جعلني أقوم بشيء من البحث في الموضوع، أوصلني –بعون الله- للتالي:

وسائط الدالة على ما نعلم، تمرر إليها بإحدى الطريقتين: التمرير بواسطة القيمة، والتمرير بواسطة المرجع.

يأخذ التمرير بواسطة القيمة الشكل التالي كمثال:

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 وتغييرها.

والحمد لله رب العالمين

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

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

وأن تدلي بملاحظاتك وإضافاتك على هذا الموضوع.

وجزاك الله كل خير :)

تم تعديل هذه المشاركة بواسطة محمد علاء الدين في 2 يوليو 2010 في 16:11 — السبب: تم إجراء تعديلات على الموضوع بناءا على رغب

6

هذه الجملة خاطئة  :wacko: 


#2

شكرا ً لك .. هناك أمور كانت غامضة بالنسبة لي و الآن توضحت

و لكن بقي لي سؤال سألته من قبل في الموضوع السابق

اقتباس
إذا أرسلت بالمؤشر، أي بوضع *، فعندها أنت ترسل موقع أو عنوان Address المتغير، والدالة ستتعامل مع المؤشر على أنه عنوان للمتغير الأصلي، وبالتالي يمكنك التغيير في المتغير، وذلك بواسطة المؤشر الذي يشير إليه.

بس بعد اذنك سؤال ..

هو بيتعامل مع المتغير الأصلي عندما نغير بقيمة المؤشر داخل الدالة

لكن عندما نغير عنوان المؤشر داخل الدالة فإنه سينسخ المتغير الأصلي و لن يتغير المتغير الأصلي حينها

صح هالكلام؟

و شكرا ً لك

شكرا ً لك مرة أُخرى

#3

عندما نغير عنوان المؤشر داخل الدالة، لن ينسخ المتغير الأصلي، لكنه ببساطة سيصبح يشير إلى مكان آخر في الذاكرة، هو العنوان الذي تم التغيير إليه، وعندها لن يعود بإمكانه تغيير قيمة المتغير الأصلي.

والله أعلم

أرجو أن يكون هذا ما تقصده.

إن لم يكن، فوضح سؤالك أكثر من فضلك

بالتوفيق

1

هذه الجملة خاطئة  :wacko: 


#4

السلام عليكم

شكرا لك أخي على الشرح و التشريح المميز. هذا من اهم الامور التي يجب معرفتها في التعامل مع الدوال.

#5
aohammed كتب:

إذا أرسلت بالمؤشر، أي بوضع *، فعندها أنت ترسل موقع أو عنوان Address المتغير، والدالة ستتعامل مع المؤشر على أنه عنوان للمتغير الأصلي، وبالتالي يمكنك التغيير في المتغير، وذلك بواسطة المؤشر الذي يشير إليه.

بس بعد اذنك سؤال ..

هو بيتعامل مع المتغير الأصلي عندما نغير بقيمة المؤشر داخل الدالة

لكن عندما نغير عنوان المؤشر داخل الدالة فإنه سينسخ المتغير الأصلي و لن يتغير المتغير الأصلي حينها

صح هالكلام؟

و شكرا ً لك

شكرا ً لك مرة أُخرى

الرمز "*" يسمى dereference operator وعند وضعه امام المؤشر و تمريره لدالة فإن هذه الاخيرة ستقوم بالتغييرات على القيمة التي يشير اليها المؤشر و ليس عنوان القيمة في الذاكرة.

أما اذا قمت بتغيير عنوان المؤشر داخل الدالة فانك ستجعل هذا المؤشر يشير الى قيمة اخرى مغايرة (متغير اخر). لكن هذه العملية خطيرة لانك لا تعرف الى ماذا يشير هذا العنوان وربما قد تتسبب في تغيير قيمة خاصة بالنظام او باحد التطبيقات الاخرى .

هذه الطريقة تستعمل لقراءة البيانات الموجودة في الذاكرة او ما يسمى ب memory dump.

#6

دائما و ابدا عند تمرير معاملات لداله يتم حجز مساحه لهذه المعاملات و الحجم يختلف بنوع المعامل و طبيعة تمريره للداله، لذا و حتى التمرير بالمرجع يتم حجز مساحه للمعامل داخل الـ stack - الأمر يتفاوت مع تفعيل الـ optimization

اقتباس

بالطبع، دائماً يقولون لنا إننا لا نستطيع تغيير قيمة المتغير المرسل إلى دالة تأخذ وسائطها بالقيمة...

في الحقيقة، يمكننا أن نكسر هذه القاعدة (نوعاً ما)...

الإسلوب الذى اتبعته فى هذه الفقره غير صحيح لانك تتعامل مع النوع int كأنه مؤشر لـ int فى حين انهم نوعين مختلفين و لكل منهم خصائص و اسلوب فى حفظ البيانات يختلف عن الأخر

اقتباس
سيتم التعامل مع القيمة الممررة على أنها عنوان، سيؤخذ هذا العنوان، وينسخ أيضاً، ويوضع في المؤشر المحلي Ptr.

هذا صحيح، فإذا لم ترد أن يتم نسخ المؤشر فيمكنك استخدام التعريف التالى

void MyFunc(int*& Ptr)

اقتباس
بكل بساطة، لأن القيمة التي تم تمريرها والتي يحويها المؤشر عادة هي عنوان، وبالتالي، يمكننا استعمال هذا العنوان للوصول إلى المتغير الأصلي.

و ما يدريك أن هذا العنوان هو عنوان المتغير الأصلى، انظر معى هذا المثال:

int main()
{
    int  T  = 100;
    int* pT = &T;

    test(pT);
}

void test(int* ptr)
{
  // ptr point to value of T not pT
}

لا يمكنك الحصول على عنوان المؤشر الذى يتم تمريره للداله إلا إذا تم تمرير المؤشر بالمرجع.

و الله ولى التوفيق

2

مدونتي: C++ Tips and Tricks

#7

جزاكم الله خيراً على التفاعل

الأستاذ علاء:

اقتباس
دائما و ابدا عند تمرير معاملات لداله يتم حجز مساحه لهذه المعاملات و الحجم يختلف بنوع المعامل و طبيعة تمريره للداله، لذا و حتى التمرير بالمرجع يتم حجز مساحه للمعامل داخل الـ stack - الأمر يتفاوت مع تفعيل الـ optimization

كيف أصل إلى عنوان الوسيط (المعامل) في الـstack في حال تم تمريره بالمرجع؟

اقتباس
الإسلوب الذى اتبعته فى هذه الفقره غير صحيح لانك تتعامل مع النوع int كأنه مؤشر لـ int فى حين انهم نوعين مختلفين و لكل منهم خصائص و اسلوب فى حفظ البيانات يختلف عن الأخر

هو بالفعل غير صحيح، ويمكنك أن تلاحظ أني اضطررت في كل حركة إلى القيام بـCasting، ولكن أردت استعماله فقط لتوضيح النقطة التي تليه والمتعلقة بالمؤشرات.

اقتباس

هذا صحيح، فإذا لم ترد أن يتم نسخ المؤشر فيمكنك استخدام التعريف التالى

جزاك الله كل خير، معلومة قيمة، لدي سؤال عليها: لماذا وضعنا المعامل * قبل &، وما النتيجة عند التبديل بينهما؟

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

جزاك الله كل خير يا أستاذ علاء، وهذا هو النوع من المشاركات الذي كنت أنتظره، أرجو أن تجاوب على الاستفسارات التي طرحتها :)

هذه الجملة خاطئة  :wacko: 


#9
اقتباس
كيف أصل إلى عنوان الوسيط (المعامل) في الـstack في حال تم تمريره بالمرجع؟

لن يمكنك إلا لو كانت الداله تحتوى على معاملين على الأقل هو احدهما و الأخر يتم تمريره بالقيمه، و قبل أن تتسأل لماذا لابد لنا ان ننزل لمستوى الأسمبلى لترى كيف يتم التمرير بالمرجع أنظر معى لهذا المثال:

الكود بإستخدام الـ c

void test(int& r);

int main()
{
	int x = 0x64;
	test(x);
}

void test(int& r)
{ r = 0x7; }

نفس الكود بإستخدام اسمبلى x86

كود الداله main

pu0 0   ebp
mov     ebp, esp
push    ecx

mov     dword ptr [ebp-4], 64
lea     eax, [ebp-4]
push    eax
call    test

add     esp, 4
xor     eax, eax
mov     esp, ebp
pop     ebp
retn

كود الداله test

push    ebp
mov     ebp, esp

mov     eax, [ebp+8]
mov     dword ptr [eax], 7

pop     ebp
retn

اتمنى ان تكون على درايه (و لو بسيطه) بلغة الأسمبلى و ذلك لأن ما انا على وشك قوله سيبدو كطلاسم إن لم تكن كذلك.

لنبدأ بشرح الكود الخاص بلغة الـ c بشئ من التفصيل: اول ما يحدث هو حجز مساحة بالـ stack مساحتها 4 بايت و اعطاء اسم لها و هو x يلى ذلك وضع القيمه 100 داخل المساحه ذات الإسم x يلي ذلك تمرير عنوان x للداله test و من ثم إستدعائها.

داخل الداله test يتم وضع الرقم 7 داخل المكان الذى تم تمريره للداله (فى حالتنا هذه المكان هو x و داخل الداله test له اسم اخر هو r).

بعد ذلك يتم الرجوع من الداله test إلى الداله main التى تنتهى يليها انتهاء البرنامج.

لنذهب الأن لكود الأسمبلى لنرى الأمور على حقيقتها و ليس كما يراد لنا أن نراها داخل السى: كود الأسمبلى للداله main و test قمت بتقسيمه إلى 3 اجزاء يفصل بينهم سطر فارغ، الجزء الأول و الأخير يطلق عليهم إسم Epilog Code و Prolog Code على التوالى و يستخدم الأول فى حفظ بيانات الإستدعاء و من الداله السابقه و ايضا حجز المساحه بالـ stack فى حين الأخير يقوم الأخير بتحرير المساحه التى حجزها الأول و إسترجاع بيانات الداله التى قامت بالإستدعاء و من ثم العوده للداله التى قامت بالإستدعاء.

الكود الموجود بالمنتصف هو كود المستخدم User Code و هو محل النقاش التالى: تبدأ الداله main بوضع القيمه 0x64 داخل الموقع [ebp-4] (يقابل x داخل الـ c) و من بعد ذلك تقوم بتحميل عنوان هذا الموقع داخل المسجل eax و بعد ذلك يتم حفظ قيمة eax (العنوان) داخل الـ stack و من ثم يتم الإستدعاء للداله test.

عندما يتم استدعاء الداله test سيتم استخدام القيم التى تم حفظها داخل الـ stack قبل الإستدعاء مباشرة كمعاملات للداله و فى حالتنا هذه لا يوجد غير واحد فقط و هو [ebp+8] (لاحظ أن ebp+8 داخل الداله test هو نفسه ebp-4 داخل الداله main و التغيير هذا يرجع إلى أن الوصول للمساحات داخل الـ stack تكون عن طريق offset address و ليس actual address و لهذا قمنا بإستخدام التعليمه lea للحصول على الموقع الفعلى)، الخطوه التاليه هى وضع القيمة التى يشير لها ebp+8 داخل eax (وضع العنوان داخل eax) و من ثم وضع القيمه 7 داخل العنوان الذى يحتوى eax عليه، بعد ذلك يتم الرجوع للداله main و التى تنتهى يليها انتهاء البرنامج.

كما ترى تم حجز مساحه بالـ stack ليتم حفظ موقع x بها و بعد ذلك تم الوصول لهذا الموقع من خلال الداله test و التى اخذت العنوان المسجل بذلك الموقع و قمات بوضع الرقم 7 داخله.

أرجو أن اكون قد وفقت فى الشرح.

نأتى الأن لنقطة معرفة الموقع: إذا كانت الداله تحتوى على معامل واحد فقط الذى يتم التمرير إليه بالمرجع فلن تستطيع الحصول على موقعه و ذلك لإنك كلما رردت الحصول عليه سيتم تحميل لك بقيمته (و هى الموقع الذى يشير إليه)، و لكن إذا كان لديك معامل واحد على الأقل يتم تمريره بالقيمه (معامل عادى او مؤشر ايا كان مؤسى الإستدعاء به) فيمكنك من خلال هذا المعامل معرفة موقع المعامل الذى تم تمريره بالمرجع، أنظر معى هذا الكود:

void test(int& r, int x = 0);

int main()
{
	int x = 0x64;
	test(x);
}

void test(int& r, int x)
{
	int* ptr = &x - 1;
	r = 0x7;
	*(*(int**)ptr) = 15;
}

دعك من كود الداله main فكل ما يهمنا الأن كود الداله test و خصوصا الكود العجيب بالسطر رقم 3 و لكن و قبل شرح الكود لابد أن تدرك جيدا أن كل الدوال بلغة السى و سى++ (داخل ويندوز و بالأخص مع مترجم VC و Intel) لها اسلوب استدعاء cdecl بشكل افتراضى و حتى إن جعلته stdcall سيعمل الكود معك بشك جيد (الـ cdecl و الـ stdcall هى اسلوب تمرير المعاملات للدوال و نظام معين يتبعه نظام التشغيل فى حجز و تحرير ذاكرة الـ stack للدوال).

يتم تمرير معاملات الداله test من داخل الداله main و يتم التمرير من اقصى اليمين لليسار لذا فى حالتنا هذه يتم تمرير الرقم صفر أولا (المعامل a) و حيث أن الـ stack يعمل بنظام LIFO يتم تخزين الرقم صفر على قمة الـ stack يلي ذلك تمرير عنوان المتغير x للداله لذا يتم دفعه للـ stack و بهذا يصبح عنوان المتغير x اولا داخل الـ stack يليه الرقم صفر.

داخل الداله test قمت بالحصول على عنوان المعامل a (ذو القيمه صفر) و بعد ذلك قمت بطرح الرقم 1 منه و هنا نتسائل لماذا واحد بالذات و لماذا الطرح؟

داخل الـ stack قيمة المعامل r هى على القمه يليها قيمة المعامل a لذا و حيث ان لدى موقع a داخل الذاكره و انا اعلم ان موقع r اعلاه لذا سأحتاج لإنقاص عدد معين من موقع المعامل a و ذلك للحصول على موقع المعامل r (هذا تفسير علامة الطرح و هو انقاص موقع a بمقدار معين للوصول لموقع r) و حيث أن العمليات الأساسيه (الجمع و الطرح و الضرب و القسمه) تتم بشكل مختلف على العنواين (حيث بالجمع يتم الزيادة بـ 4 و كذلك بالنقصان - الرقم 4 هو مساحة المؤشر فى الذاكره) لذا فبإنقاص واحد هذا يعنى انقاص 4 بايت من قيمة موقع a و بهذا يكون داخل المتغير ptr موقع المعامل r داخل الذاكره.

السطر التالى اقوم بجعل قيمة r بـ 7.

السطر التالى : المؤشر ptr قيمته هى عنوان المتغير a الموجود بالداله main و هذا يعنى أن ptr هو مؤشر و قيمته هى مؤشر لمكان بالذاكره لذا كل ما افعله هو تحويل قيمة ptr إلى مؤشر لمؤشر و من ثم اقوم بعمل derefernce لمستويان المؤشر حتى اضع الرقم 15 بالموقع النهائى الذى يشار إليه من خلاله، لذا بعد انتهاء الداله test من عملها و العوده للداله main لن تجد قيمة a بـ 100 و لا 7 و انما 15.

اقتباس
جزاك الله كل خير، معلومة قيمة، لدي سؤال عليها: لماذا وضعنا المعامل * قبل &، وما النتيجة عند التبديل بينهما؟

راجع مشاركة الأخ خالد فبها موضوع يشرح الفرق جيدا.

=====================

كل الكلام السابق هو يرتبط ارتباط وثيق بنظام التشغيل و المترجم و ليس من الـ standard فى شئ لذا لا تندهش إن جربته على Mac for Intel و لم يعمل معك :wink:

و الله ولى التوفيق

تم تعديل هذه المشاركة بواسطة محمد علاء الدين في 28 يونيو 2010 في 17:51 — السبب: تصحيح بعض الأخطاء

3

مدونتي: C++ Tips and Tricks

#10
اقتباس
بدأت بالغوص يا أخ عبود :P

أرجو ألا أغرق :lol:

جزاك الله خيراً على الرابط، فعلاً مفيد جداً، أنا كنت حيران في البداية لماذا وضعت إشارة المرجع على يمين إشارة المؤشر، الآن فهمت السبب، وفهمت كيف يمكنني استخدامها أيضاً بفضل الله ثم بفضلك، إذاً فللمعلومة الإضافية، يقرأ النوع من اليمين إلى اليسار:

int*&

A reference to a pointer to an integer

الأستاذ علاء:

جزاك الله كل خير، بصراحة أنا لا أفقه شيئاً في الأسمبلي، لكن شرحك جيد جداً، وأظن أنني قد فهمت الفكرة العامة، سألخصها وصحح لي إن أخطأتُ من فضلك:

في main، تتم وضع قيمة المتغير في العنوان من الذاكرة [ebp - 4]، وتستخدم main مسجلاً ليشير إليها اسمه eax وتقوم بتمريره إلى test، تقوم test بحجز عنوان في الذاكرة هو [ebp + 8]، وتشير إليه بواسطة المسجل eax نفسه، ثم تضع فيه القيمة 7.

وبالتالي، فللمتغير x والمرجع r عنوانان مختلفان في الذاكرة، لكن يستعمل مسجل واحد ليشير إلى كليهما، وهو eax.

أما بالنسبة للحصول على هذا العنوان، فيتم الوصول إليه عن طريق عنوان الوسيط الذي يليه، حيث أنه يكون تحته في المكدس stack، لأنه يعمل بطريقة LIFO، حضرتك ذكرت FIFO، لكني أظنه خطأ طباعياً، حيث أن FIFO يستعمل في الـQueue، وليس في الـStack.

يمكنني أن أستنتج من هذا أن الوسيط المرر بالقيمة لو كان على يسار الوسيط الممرر بالمرجع، سأقوم بإضافة واحد بدلاً من طرحه، لأنه سيكون فوقه في المكدس حينها. أليس كذلك؟

وفي النهاية، نستعمل المؤشر لنصل إلى القيمة التي يشير إليها المرجع r، بالطبع، إن ptr هو مؤشر عادي، لذلك سأضطر إلى تحويله إلى مؤشر إلى مؤشر، ثم أستعمل الـDereference مرتين لأصل إلى القيمة التي يشير إليها r.

تمام؟؟؟

جزاك الله كل خير وبارك لك في علمك

هذه الجملة خاطئة  :wacko: 


#11
اقتباس

الرمز "*" يسمى dereference operator وعند وضعه امام المؤشر و تمريره لدالة فإن هذه الاخيرة ستقوم بالتغييرات على القيمة التي يشير اليها المؤشر و ليس عنوان القيمة في الذاكرة.

أما اذا قمت بتغيير عنوان المؤشر داخل الدالة فانك ستجعل هذا المؤشر يشير الى قيمة اخرى مغايرة (متغير اخر). لكن هذه العملية خطيرة لانك لا تعرف الى ماذا يشير هذا العنوان وربما قد تتسبب في تغيير قيمة خاصة بالنظام او باحد التطبيقات الاخرى .

هذه الطريقة تستعمل لقراءة البيانات الموجودة في الذاكرة او ما يسمى ب memory dump.

لكن هل يطبق كلام التغيير على الـ char ??

فلقد جربت و غيرت القيمة داخل التابع و لم تتغير القيمة ؟؟؟ على عكس ما هو الوضع لو كان int

هل لآنه عندما نطبق char * فهذا يكافئ الـسلسلة المحرفية ؟

سوف أضع الكود ريثما أعود إلى البيت

و شكرا ً لكم

#12

لقد تم حل المشكلة و هنا أضع كود الحل .. شكرا ً لزميل عزيز علي ساعدني في حل هذه المشكلة

#include "stdafx.h"
#include<iostream>
using namespace std;
void f(char **name)
{
	char *n="Tala";
	*name=n;

}
void main()
{
	char *name="mohammed";
	f(&name);
	cout<<name;

}

و السبب كما أسلفت انها سلسلة محرفية

#13

اخى عبود

بجد موضوع جميل جدا

تطلب منك مجهودا عظيما

اشكرك عليه وجزاك الله خيرا

ربنا يوفقك يارب

تسلم الايادى

تقبل مرورى

Software Developer
Mahmoudkelany.com


 

#14
اقتباس
لأنه يعمل بطريقة LIFO، حضرتك ذكرت FIFO، لكني أظنه خطأ طباعياً

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

اقتباس

في main، تتم وضع قيمة المتغير في العنوان من الذاكرة [ebp - 4]، وتستخدم main مسجلاً ليشير إليها اسمه eax وتقوم بتمريره إلى test، تقوم test بحجز عنوان في الذاكرة هو [ebp + 8]، وتشير إليه بواسطة المسجل eax نفسه، ثم تضع فيه القيمة 7.

وبالتالي، فللمتغير x والمرجع r عنوانان مختلفان في الذاكرة، لكن يستعمل مسجل واحد ليشير إلى كليهما، وهو eax.

في main، تتم وضع قيمة المتغير في العنوان من الذاكرة [ebp - 4]، وتستخدم main مسجلاً اسمه eax ليحتوى بشكل مؤقت على العنوان الفعلى لـ [ebp-4] وتقوم بتمريره إلى test عن طريق حفظ قيمته داخل الـ Stack، عندما تم حفظ قيمة eax داخل الـ stack من داخل الداله main تم حجز له مساحه؛ هذه المساحه يتم الوصول لها من داخل الداله test بإستخدام [ebp+8]، بعد ذلك يتم نقل قيمة [ebp+8] داخل eax ثم يتم وضع الرقم 7 العنوان الذى يحتوى eax عليه.

اقتباس
يمكنني أن أستنتج من هذا أن الوسيط المرر بالقيمة لو كان على يسار الوسيط الممرر بالمرجع، سأقوم بإضافة واحد بدلاً من طرحه، لأنه سيكون فوقه في المكدس حينها. أليس كذلك؟

كلامك هذا صحيح إذا كان النظام الذى تعمل عليه هو (x86) و اسلوب استدعاء الداله هو cdecl او stdcall، فإذا حدث و قمت بالعمل على x64 او غيرها من الـ Architecture ستجد ان اسلوب الإستدعاء يختلف تمام الإختلاف عما عهدته.

و الله ولى التوفيق

تم تعديل هذه المشاركة بواسطة محمد علاء الدين في 28 يونيو 2010 في 18:19

مدونتي: C++ Tips and Tricks

#15

الأخ محمود: أهلاً بك، وأرجو الله أن تكون قد وفقت في امتحاناتك.

الأستاذ علاء:

جزاك الله خيراً

اقتباس

هذه المساحه يتم الوصول لها من داخل الداله test بإستخدام [ebp+8]

ما السطر الذي يقوم بهذه الجزئية؟

تم تعديل هذه المشاركة بواسطة Abboodd في 28 يونيو 2010 في 19:49

هذه الجملة خاطئة  :wacko: 


#16
اقتباس
ما السطر الذي يقوم بهذه الجزئية؟

عملية حفظ العنوان داخل الـ stack تمت مباشرة قبل استدعاء الداله test بالسطر التالى

push   eax

الوصول لهذ المساحه تتم من داخل الداله test من خلال السطر الذى يتم حفظ محتويات [ebp+8] داخل eax و هو التالى

mov   eax, [ebp+8]

و الله ولى التوفيق

تم تعديل هذه المشاركة بواسطة محمد علاء الدين في 28 يونيو 2010 في 20:23

مدونتي: C++ Tips and Tricks

#17
Abboodd كتب:

الأخ محمود: أهلاً بك، وأرجو الله أن تكون قد وفقت في امتحاناتك.

اهلاين بيك اخى عبود

الحمدلله على كل شىء

وما توفيقنا الا بالله ودعواتكم الخالصه

جزاكم الله خيرا

انا هاضايف فى منتدى الجافا شويه

وهاقبى ازوركم تانى هنا

معاكوا ان شاء الله

C++ & Java :lol: :lol:

ان شاء الله

تم تعديل هذه المشاركة بواسطة Mahmoud Kelany في 28 يونيو 2010 في 21:06

Software Developer
Mahmoudkelany.com


 

#18
void fun(int*& ref_to_ptr)
{
	ref_to_ptr = 0; // set the "passed" pointer to 0
	// if the pointer is not passed by ref,
	// then only the copy(parameter) you received is set to 0,
	// but the original pointer(outside the function) is not affected.
}

wacko.gif .. دخت ...

لست متأكد إن فهمت قصدك أخ AraK :D ، ولكن لا اذكر أني رأيت كود شبيه بهذا ؟ ولا أعرف لماذا قد أستخدمه ..

==

ولكن وجهة نظري المتواضعة ، هو أن لا ننشغل بهذه التفاصيل عن أمور أهم . فتجد مبرمج السي بلس يمضي ليلة كاملة وهو يحاول أن يرى تأثير النجوم على المتغيرات ، والنتيجة أنه سيرى " نجوم الظهر " بدلاً من ذلك :D . الا اذا كان الهدف زيادة في العلم في وقت الفراغ فهذا شيء آخر :-) .

==

+1 لعبّود وبقية الشباب :-) .

logo1.png تطبيق طمأنينة ، نسخة بيتا على أندرويد

عبدالله الشمّري - Al-Shammari

CodingAlone.com

twitter @abshammeri

abshammeri AT gmail.com

github : abshammeri

#19
aohammed كتب:

لقد تم حل المشكلة و هنا أضع كود الحل .. شكرا ً لزميل عزيز علي ساعدني في حل هذه المشكلة

#include "stdafx.h"
#include<iostream>
using namespace std;
void f(char **name)
{
	char *n="Tala";
	*name=n;

}
void main()
{
	char *name="mohammed";
	f(&name);
	cout<<name;

}

و السبب كما أسلفت انها سلسلة محرفية

السبب هو أن الجداول تمرر دائما بعنوانها.

#20

الأستاذ علاء أرجو أن تصبر عليّ قليلاً:

هل [ebp + 8] عنوان الوسيط؟

mov تقوم بإسناد قيمة ما بعد الفاصلة إلى ما قبلها، صحيح؟ لماذا أسند القيمة إلى eax مرة أخرى؟ أم أن eax الخاص بـtest مختلف عن الـeax الخاص بـmain؟

:wacko:

هذه الجملة خاطئة  :wacko: 


#21
اقتباس
هل [ebp + 8] عنوان الوسيط؟

نعم.

اقتباس
mov تقوم بإسناد قيمة ما بعد الفاصلة إلى ما قبلها، صحيح؟

نعم

اقتباس
لماذا أسند القيمة إلى eax مرة أخرى؟ أم أن eax الخاص بـtest مختلف عن الـeax الخاص بـmain؟

يمكنك اعتبار eax كمتغير مساحته 4 بايت داخل البروسسور عام على برنامجك ككل، و يستخدم ليحتفظ بقيم بشكل مؤقت لإجراء عمليات عليها.

بالنسبه لسؤالك عن اسناد القيمه لـ eax مره اخرى سببها هو أن قيمة [ebp+8] هى عنوان و انا اريد وضع القيمه داخل هذا العنوان فكيف أفعل هذا؟؟

كل ما يتم هو نقل قيمة [ebp+8] إلى eax و بالتالى eax يحتوى الأن على العنوان الذى الذى سأضع به الرقم 7 لذا اقوم بوضع القيمه بذلك العنوان بإستخدام الكود التالى

mov dword ptr [eax], 7

و الله ولي التوفيق

1

مدونتي: C++ Tips and Tricks

#22

جزاك الله خيراً، قد توضحت الفكرة الآن بشكل أفضل :)

اسمح لي بأن أقوم ببعض التعديل على المشاركة الأولى بناء على مجموعة المعلومات التي قدمتها.

وفقنا الله وإياكم :)

هذه الجملة خاطئة  :wacko: 


#23

أفعل ما يحلو لك :)

مدونتي: C++ Tips and Tricks

#24

يبدو أنه من غير الممكن تحرير الردود القديمة :(

هذه الجملة خاطئة  :wacko: 


#25

يمكننى إرسال محتويات المشاركه الأولى لك على الخاص و بعد ان تنتهى اقوم بنسخها هنا بدل الموجوده.

إن كان هذا مناسب لك أبلغنى.

و الله ولى التوفيق

مدونتي: C++ Tips and Tricks

مواضيع مشابهة