الشبكة العربية لمطوري الألعاب

خبير  أحمد عزالدين مشاركة 1

السلام عليكم ورحمة الله
الكل منا يعرف ان المؤشرات هي من
الاسباب الرئيسية لحدوث تسريب في الذاكرة خصوصا في اللغات التى
توفر وصول مباشر للذاكرة مثل ال ++c
ونعرف ان التسريب يحدث اذا تم انشاء
ذاكرة عشوائية ولم نعمل لها release بعد الانتهاء من التعامل معها
سواء حدث ذلك بسبب lost pointer او بسبب انتهاء الاجراء التي انشئت
فيه دون ان نحذفها

ونعلم ان تراكم التسريب في الذاكرة
يسبب نقص لموارد ذاكرة الكمبيوتر
void fun()

{
    int* dynArr = new int[50];
    // use your dynamic array here
    // we should delete this created
dynamic memory after finishing
    // working with it
    delete []dynArr;   // comment this, to cause memory-leak

}

 

بالطبع هناك كثير من الاسباب التي
تسبب حدوث تسريب للذاكرة
ومن اهم هذه الاسباب واكثرها شيوعا هو
انشاء ذاكرة ديناميكية واستخدامها
لكن وقبل الوصول للكود الذي يحذفها قد
يحدث اي bug or error مما
يسبب تسريب
ايضا مع انه تم كتابة الكود الذي يحذف
الذاكرة التي انشأناها ولكن هذا الكود لم يتم استدعائه

ونستطيع ان نحاول التقليل من حدوث
التسريب دوما بالتعود علي استخدام الاساليب البرمجية التي
توفرها لنا اللغة مثل assert او حتي بالفحص دائما عن قيمة المؤشر
قبل استخدامه وكذلك باستخدام اسلوب
ال exception
handling الذي توفره لنا لغة السي بلس بلس
بالاسلوب  try and catch

ولكن ما اود ان اقدمه هنا هو احد
الاشياء التي قرأت عنها في كتاب
وهو ما يسمي بالـauto pointer وهو اسلوب مثالي يمكننا بالتعامل مع
الذاكرة مباشرة
ويقوم دائما بحذف الذاكرة العشوائية
التي انشأناها عن طريقه حتي لو نسينا ذلك او ضاع منا عنوان المؤشر

هذا النص مأخوذ من كتاب C++ How To Program
انظرو النص النجليزي لمزيد من التوضيح:
 A common programming
practice is to allocate dynamic memory, assign the address of that memory to a
pointer, use the pointer to manipulate the memory and deallocate the memory
with delete when the memory is no longer needed. If an exception occurs after
successful memory allocation but before the delete statement executes, a memory
leak could occur. The C++ standard provides class template auto_ptr in header file
to deal with this situation.
An object of class auto_ptr maintains a pointer to
dynamically allocated memory. When an auto_ptr object destructor is called (for
example, when an auto_ptr object goes out of scope), it performs a delete
operation on its pointer data member. Class     template auto_ptr
provides overloaded operators * and -> so that an auto_ptr object can be
used just as a regular pointer variable is.
1  // Fig. 16.8: Integer.h 2 
// Integer class definition. 3 4  class Integer 5  { 6  public: 7    
Integer( int i = 0 ); // Integer default constructor 8     ~Integer(); // Integer
destructor 9     void setInteger( int i ); //
functions to set Integer10     int getInteger() const; //
function to return Integer11  private:12     int value;13  }; // end class Integer

1  // Fig.
16.9: Integer.cpp 2  // Integer member function definition. 3  #include  4  using std::cout; 5  using std::endl; 6 7  #include "Integer.h" 8 9  // Integer default constructor10  Integer::Integer( int i )11     : value( i )12  {13     cout << "Constructor
for Integer " << value << endl;14  } // end Integer constructor1516  // Integer destructor17  Integer::~Integer()18  {19     cout << "Destructor
for Integer " << value << endl;20  } // end Integer destructor2122  // set Integer value23  void Integer::setInteger( int i )24  {25     value = i;26  } // end function setInteger2728  // return Integer value29  int Integer::getInteger() const30  {31     return value;32  } // end function getInteger

1  // Fig.
16.10: Fig16_10.cpp 2  // Demonstrating auto_ptr. 3  #include  4  using std::cout; 5  using std::endl; 6 7  #include     8  using std::auto_ptr; // auto_ptr class
definition 910  #include "Integer.h"1112  // use auto_ptr to manipulate Integer object13  int main()14  {15     cout << "Creating an
auto_ptr object that points to an Integer\n";1617     // "aim" auto_ptr at
Integer
object                 
18     auto_ptr< Integer >
ptrToInteger( new Integer( 7 ) );1920     cout << "\nUsing the
auto_ptr to manipulate the Integer\n";21     ptrToInteger->setInteger( 99
); // use auto_ptr to set Integer value2223     // use auto_ptr to get Integer
value24     cout << "Integer after
setInteger: " << ( *ptrToInteger ).getInteger()25     return 0;26  } // end main

  
Now
after we display the result of this code execution, we found:
     Creating an auto_ptr object that
points to an Integer
     Constructor for Integer 7
 
     Using the auto_ptr to manipulate
the Integer
     Integer after setInteger: 99
 
     Terminating program
     Destructor for Integer 99
   
You saw that we uses the auto_ptr overloaded ->
operator to invoke function setInteger on the Integer object pointed to by ptrToInteger.
Line 24 uses the auto_ptr overloaded * operator to dereference ptrToInteger,
then uses the dot (.)            
operator to invoke function getInteger on the Integer object pointed to by ptrToInteger.
Like a regular pointer, an auto_ptr's -> and * overloaded operators can be
used to access the object to which the auto_ptr points.
 Because ptrToInteger is a local automatic variable in main,
ptrToInteger is destroyed when main terminates. The auto_ptr destructor forces
a delete of the Integer object pointed to by ptrToInteger, which in turn calls
the Integer class destructor. The memory that Integer occupies is released,
regardless of how control leaves the block (e.g., by a return statement or by
an exception). Most importantly, using this technique can prevent memory leaks.
For example, suppose a function returns a pointer aimed at some object.
Unfortunately, the function caller that receives this pointer might not delete
the object, thus resulting in a memory leak. However, if the function returns
an auto_ptr to the object, the object will be deleted   automatically when the auto_ptr object's
destructor gets called.

An auto_ptr can pass ownership of the dynamic memory it
manages via its overloaded assignment operator or copy constructor. The last auto_ptr
object that maintains the pointer to the dynamic memory will delete the memory.
This makes auto_ptr an         ideal mechanism
for returning dynamically allocated memory to client code. When the auto_ptr
goes out of scope in the client code, the auto_ptr's destructor deletes the
dynamic memory.
 
    NOTE:
An auto_ptr
has restrictions on certain operations.
For
example, an auto_ptr cannot point to an array or a standard-container class.


I hope that this sample may help any one of
you    and i welcome any feedback from you.

أحمد عزالدين
طالب دراسات عليا
جامعة كالجري

خبير  سعيد بسيوني مشاركة 2

بتاريخ 06/ربيع الأول/1429 04:25 ص، قطب ahmed ezz حاجبيه بشدة وهو يقول:

ونستطيع ان نحاول التقليل من حدوث
التسريب دوما بالتعود علي استخدام الاساليب البرمجية التي
توفرها لنا اللغة مثل assert او حتي بالفحص دائما عن قيمة المؤشر
قبل استخدامه وكذلك باستخدام اسلوب
ال exception
handling الذي توفره لنا لغة السي بلس بلس
بالاسلوب  try and catch

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



int *array = new int[15];
try
{
	if (!ReadArrayFromFile(fileName))
		return false;
}
catch (...)
{
	return false;
}
finally
{
	delete [] array;
	array = 0;
}
 
في هزا المثال نرى أننا نحجز مصفوفة بحجم معين من العناصر
بعد كده ندخل في بلوك try فيه تعليمات قابل انها تفشل وترمي استثناء exception او انها ترجع كود خطأ (0 مثلا)
وقتها حنرجع القيمة false وبس قبل ما نخرج من البلوك دي حيتم تنفيذ الكود ضمن الـ finally (أخيراً) واللي بيقوم بتحرير الذاكرة فوراً مهما حدث سواء نجحت العملية أو فشلت

خبير  أحمد عزالدين مشاركة 3

السلام عليكم اخي الكريم
 
انا اقصد انك دوما يمكنك استخدام الماكرو assert والذي يفيد في حال كان المشروع في طور التطوير debug
بحيث يمكنك دوما استخدامه ليفحص لك قيم المؤشرات بحيث يعطيك رسالة خطأ اذا حاولت استخدام هذا المتغير دون ان تكون
جعلته يشير لشئ ما
لانه من المعروف ان بداية اخطاء الذاكرة ومن ضمنها التسريب يكون سببها في البداية استخدام متغيرات من نوع مؤشرات غير صالحة اي لا تشير لنوع
معين وموجود من البيانات في الذاكرة كما انها تسبب حدوث bug في الكود وهو ما يؤدي لوقف تنفيذ البرنامج وبالتالي قد لا يتم تنفيذ
الكود التالي والمسئول عن تحرير الذاكرة
واستخدام assert بسيط جدا كالاتي:
 
اولا قم بعمل include للملف التالي
لنفرض ان لديك دالة تستقبل متغير مؤشر لمصفوفة سيتم ملئها لابد ان يكون هذا المؤشر صالحا قبل استخدامه
وللتاكد من عدم استخدام المؤشر ان لم يكن صالحا وبدلا من ذلك ننبه الي وجود الخطأ نستخدم الدالة assert والتي تستقبل كبارامتر
لها المتغير الذي يمثل المؤشر فاذا كان هذا المتغير صالح للاستخدام فان الدالة assert لا تسبب توقف التنفيذ
اما اذا كان المؤشر غير صالح للاستخدام كان يكون NULL مثلا فان الـ assert تسبب وقف تنفيذ البرنامج وتعرض مربع يوضح لك السطر الذي به المشكلة


#include 
#include 
#include 
using namespace std;
void PrintArray(int arr[], int size)
{
   assert( arr != 0 );    // check for null array pointer
   assert( size>=1 );   // insure that size is at least 1 or more elements
   for(int i=0 ;i   {
      cout << arr[i] << " ";
   }
   cout << endl;
}
void main()
{
    int *array = 0;
    PrintArray(array, 10);
}
هذا المثال ماخوذ من كتاب ال Game Institute c++ module 1
 
ارجو ان تكون قد وصلت الفكرة

أحمد عزالدين
طالب دراسات عليا
جامعة كالجري

خبير مدير وسام البهنسي مشاركة 4

في 13 مارس 2008 11:00 ص، قال ahmed ezz بهدوء وتؤدة:

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

الاستخدام الأساسي لماكرو الـ assert يكمن في التحقق من أن الأمور تسير كما هو متوقع في البرنامج. مثلاً، أن تقوم ببعض الحسابات، وفي النهاية يجب أن يكون الناتج صفراً مثلاً، وإلا فإن هناك خطأ في الخوارزمية ويجب إصلاحه. عندها يمكننا أن نضع الـ assert.
 

float distance = sqrt(getPlayerDistanceSquared());
assert(distance >= 0.0f);
 
في هذا المثال نحن ننفذ عملية حسابية تعيد لنا مربع المسافة بين اللاعب وشيء ما (لا أدري ما هو)، ونريد حساب الجذر التربيعي للقيمة لنحصل على المسافة النهائية. هنا يمكننا وضع assert للتحقق من أن القيمة المعادة غير سلبية، وإلا فإن تابع الجذر التربيعي يكون قد أخطأ في حساباته، ويجب إصلاحه. لاحظ أن الـ assert سيعطل عمل البرنامج بشكل فوري ولا يفضل متابعة التنفيذ إن كنت مستخدماً اعتيادياً للبرنامج.


 

في 13 مارس 2008 11:00 ص، عقد ahmed ezz حاجبيه بتفكير وقال:

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

ليس هكذا تماماً، وإنما في الحقيقة استخدام المتغيرات الغير مهيئة uninitialized قد يؤدي إلى واحدة من أحقر (عفواً للكلمة) المشاكل التي قد تواجه المبرمج الذي يعمل في ++C، وهي مشكلة الكتابة فوق ذاكرة خارج النطاق المقصود. وهذا الخطأ أيضاً قد يحدث حتى في حالة القيم المهيئة بشكل غير سليم. خذ معي المثال الآتي:



#include 
#include 
 
void main(void)
{
   int iTitleLen = strlen("Prince of Persia");
   int iCompanyLen = strlen("Broderbund");
 
   char *pszGameTitle = new char[iTitleLen];
   char *pszGameCompany = new char[iCompanyLen];
   char *pszFullName = new char[iTitleLen+iCompanyLen+3];

 
   strcpy(pszGameCompany,"Broderbund");
   strcpy(pszGameTitle,"Prince of Persia");
   sprintf(pszFullName,"%s : %s", pszGameCompany, pszGameTitle);
   puts(pszFullName);
 
   delete [] pszGameTitle;
   delete [] pszGameCompany;
   delete [] pszFullName;
}
 
الكود كما يظهر بريء تماماً ولا يحوي أية مشاكل. لدينا اسم الشركة واسم اللعبة، ونريد أن نضعهما سوية في عبارة واحدة نعرضها للمستخدم (كما نرى في سطر التعليمة sprintf). يمكنك تنفيذ هذا الكود والاستمتاع بالجنون الذي سيظهر لك بدلاً من النتيجة المتوقعة. والسبب؟
 
عندما حجزنا المصفوفات pszGameTitle و pszGameCompany و pszFullName فإننا حجزناها بطول يطابق الطول المتوقع والمحسوب من عدد المحارف في العبارات التي ستوضع في هذه المصفوفات. لكننا نسينا هنا أن سلسلة المحارف دائماً تنتهي بمحرف النهاية NULL ( أو  '0\' )، وهذا أمر مضمر لا يدخل ضمن حسابات strlen. وهكذا فإن النداء الثاني للإجراء strcpy سيقوم بنسخ النص Prince of Persia على المصفوفة pszGameTitle، ويتجاوز حدود المصفوفة بعنصر واحد إضافي ويكتب القيمة NULL لينهي السلسلة... هذه القيمة ستكتب على كتلة الذاكرة التي ستلي الكتلة المحجوزة لـ pszGameTitle، واحتمال كبير أن تكون كتلة pszGameCompany هي التالية، لذا سنفقد أول محرف من كلمة Broderbund ليصبح صفراً.
 
كل هذا سيحدث والبرنامج صامت لا يعترض، لأنك في أغلب الأحيان أنت تتجاوز الكتل إلى كتل أخرى محجوزة أيضاً. وهكذا ينتج لدينا حالة من الحالات التي تجعل اللعبة تعمل في بعض الأحيان وفي أحيان أخرى تفشل في العمل على نفس الجهاز.
 
في مثل هذه الحالة فإن استخدام الماكرو assert لن يفيدك كثيراً كمبرمج عادي، إلا أنه يفيد كثيراً للمبرمج الذي يكتب إجراءات الحجز والتحرير ذاتها (new و delete). إذ أنه يستطيع معرفة إن تم تجاوز حدود كتلة ذاكرة عن طريق الخطأ، وسيستخدم الماكرو assert ليطلق لك خطأ ينبهك عن حدوث ذلك. وهذه الصورة هي مثال عما سينتج لك من تنفيذ البرنامج أعلاه ضمن إعدادات Debug:




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

وسام البهنسي
مبرمج في إنفيديا وإنفريمز

خبير  أحمد عزالدين مشاركة 5

السلام عليكم

شكرا اخى وسام على التوضيح

اتمنى لو تستطيع ان تساعدنا بأى من الطرق التى تساعد المبرمجين على التعامل مع اخطاء الذاكرة خصوصا
والاخطاء عموما

يعنى لو هناك اي ادوات يمكنها ان تساعدنا فى تتبع واكتشاف الاخطاء خصوصا اخطاء الذاكرة والتسريب
فنرجو ذكر امثلة على تلك البرامج

بالمناسبة انا كنت قد قرأت وتعاملت مع برنامج PIX المرفق مع مكتبة ال DirectX SDK ونتمنى لو يستطيع احد
الاخوة الافاضل ذكر المزيد عنها وانا ان شاء الله سأبدأ فى شرح ما أعرفه عن هذه الاداة قريبا

شكرا والسلام عليكم

أحمد عزالدين
طالب دراسات عليا
جامعة كالجري

خبير  سلوان الهلالي مشاركة 6

السلام عليكم,
 
إحدى المزايا المهمة التي توفرها لغة ++C هي إمكانية عمل override لـ new و delete بنوعيها الإعتيادي والخاص بالمصفوفات ("[]"), يمكن إستخدام هذه الميزة لعمل نظام إدارة ذاكرة كامل يستطيع تتبع كل بايت يقوم الكود بحجزه من الذاكرة الديناميكية, ليس فقط هذا بل يمكن كذلك كتابة دوال new و delete مختلفة لكل كائن, ووضع خوارزميات إدارة خاصة تأخذ بنظر الإعتبار طبيعة تعامل الكائن مع الذاكرة...
 
مثلاً لو كان لديك كائن معين يحتاج إلى حجز مجموعة كبيرة من العناصر من الذاكرة الديناميكية وكل من العناصر يحتاج ذاكرة صغيرة الحجم نسبياً (لنقل مثلاً إن الكائن يقوم بحجز وتحرير عشرات العناصر لكل إطار) فإستخدام new و delete القياسية قد لا يكون فكرة جيدة, إذ سيكون هنالك overhead ممكن أن يؤثر بشكل جدّي على الأداء, كما إن تتبع هكذا عملية بطرق إعتيادية للتأكد من عدم تسرب الذاكرة سيكون ملئ بالأفخاخ والمشاكل, عن طريق كتابة نسختك الخاصة من new و delete للكائن المذكور, تستطيع إستخدام نظام حجز ذاكرة ذكي لنقل إنه سيقوم بحجز ذاكرة تكفي لعدد من العناصر كلما إحتاج ذاكرة أكثر, إضافة العناصر بعد ذلك سيكون شيئاً بسيط جداً, مجرد إعادة مؤشر للذاكرة, لأن الذاكرة محجوزة أصلاً.
 
بإستخدام هذه الطريقة من الممكن تتبع الذاكرة بالتفصيل الدقيق ومعرفة أين يذهب كل بايت, كذلك فإنك ستتخلص من الـ overhead الذي يأتي مع new و delete القياسية, وطبعاً زيادة الأداء, مقارنة مع خوارزمية حجز عمياء.
 
ولكن هنالك بعض المشاكل, مثلاً إذا إستخدمت مكتبات خارجية (لم تكتبها أنت) فإنك لن تستطيع تتبع عمليات حجز وتحرير الذاكرة التي تحصل في داخل تلك المكتبات, كذلك فإن الطريقة المذكورة لن تستطيع متابعة الذاكرة التي سيتم حجزها وتحريرها بإستخدام دوال C القياسية malloc و free, طبعاً يمكن إيجاد طرق للإلتفاف حول هذه المشكلة ;-)
 
والآن بعد مقدمة نظرية مزعجة نوعاً ما, للحصول على معلومات عملية, هنالك مقالة قديمة تتكلم عن نظام لإدارة الذاكرة مشابه لما ذكرته:
http://flipcode.com/archives/How_To_Find_Memory_Leaks.shtml
 
أتمنى لك التوفيق :-)