• 0
Abboodd

المؤشرات الذكية... نظرة أوسع

سؤال

قبل أن تقرأ:
يعد بحث المؤشرات الذكية من البحوث المتقدمة في لغة سي بلس بلس، لذلك يفضل قبل أن تقرأ هذا الموضوع أن تكون على مستوى فوق المتوسط في هذه اللغة، وأن تكون مرتاحاً في التعامل مع جوانبها المختلفة.
سأحاول في هذا البحث أن أشرح المؤشرات الذكية، فوائدها، أنواعها من حيث التصميم والاستعمال، وما يتوفر منها في مكتبات STL وBOOST إن تيسر
هذا البحث مجهود شخصي، قمت فيه بمراجعة بعض المراجع التي سأعددها بعد انتهائه، وبالتالي أرجو أن تصلّحوا لي إن أخطأت في أي أمر (لغوي أو معنوي
أو شرحي) ليصل الموضوع إلى المستوى الأفضل.
سأقسم الموضوع إلى مراحل لسهولة القراءة، وأيضاً لكي لا أصاب بالملل فأتوقف عن تتمته.
إن أفدت في هذا فأرجو أن تدعوا لي أن يهديني الله ويقومني لأن هذا أكثر ما أحتاجه..
 
مقدمة:
المؤشرات الذكية؟ هل يعني هذا أن هناك مؤشرات ذكية وأخرى وغبية؟
ليس تماماً، لكن لنقل إن المؤشرات الذكية كما يقال باللهجة السورية "بتعرف تدبر راسها"، فهي تتميز بقدرتها على إدارة أمور الذاكرة بنفسها كما سنرى، أما المؤشرات العادية، فهي "بتغرق بشبر مَي" تحتاج إلى أن يقوم المبرمج بنفسه بإدارة أمورها من حذف ونسخ وغير ذلك.
لا أقصد هنا بالطبع الهجوم على المؤشرات العادية، فهي كما سنرى تشكل البنية التحتية للمؤشرات الذكية، لكن ما أقصده أن المؤشرات الذكية هي أدوات تقوم بأتمتة بعض العمليات الخاصة بالمؤشرات، وتريح المبرمج بالتالي من القيام بهذه العمليات بشكل يدوي.
 
المؤشرات الذكية؟ لماذا؟
ببساطة... لأن الأطباء العصبيين والنفسيين مكلفون جداً....
ومن المكلف على الشركة أن ترسل لهم كل حين مبرمج سي بلس بلس مسكيناً قد تلفت أعصابه بعد ساعات وربما أيام من البحث والتنقيب عن خطأ سببه أحد المؤشرات التي يستعملها....
إن السبب الذي ينفر الكثيرين من سي بلس بلس، هو مشاكل المؤشرات فيها. فعلى الرغم من المرونة الهائلة التي توفرها المؤشرات، إلا أن قلة الخبرة والانتباه في استخدامها قد تؤدي إلى مشاكل عديدة، تحتاج إلى ساعات وساعات من البحث والـDebugging لحلها.
 
من مشاكل المؤشرات في سي بلس بلس:
  •  يجب أن يقابل كل كلمة new، كلمة delete. وإلا فإن البرنامج سيستهلك مساحات من الذاكرة أكبر بكثير مما يحتاجه، فيما يعرف بـMemory Leakage، أو تسرب الذاكرة.
  •  مشكلة Shallow Copying، أو النسخ السطحي، والتي تحدث بكثرة في الكلاسات التي تحوي في داخلها مؤشرات. أحياناً تكون هذه المشكلة واضحة، وأحياناً أخرى تكون أقل وضوحاً، كالمثال التالي:

 

#include <vector>using namespace std;class Example{public:	int* p;		Example(): p(new int) {}	~Example()	{		delete p;	}};int main(){	vector<Example> a;	a.push_back(Example());	return 0;}

 

 

عندما تشغل هذا الكود على Microsoft Visual Studio، سيرمي لك استثناء، وإذا تتبعت المشكلة بالـDebugger، ستجد أنها تحدث عندما نقوم بعمل delete للمؤشر الموجود في Example، لكن ما الذي حدث؟
الذي حدث أننا عندما أعطينا push_back وسيطاً، هو الـDefault Constructor الخاص بـExample، تم إنشاء كائن مؤقت من Example وتهيئته، ثم تم نسخه إلى العنصر الأول في الـvector واستدعي الـDestructor الخاص به عندما انتهت push_back، وبالتالي فقد تم تحرير الذاكرة التي حجزها المؤشر، وحدثت مشكلة النسخ السطحي، دون أن ننتبه لها، مما سبب حدوث الخطأ. فعند انتهاء main، سيتم تدمير عناصر الـvector، وسيستدعى الـDestructor مرة أخرى، ليقوم بدوره باستدعاء delete على المؤشر نفسه مرة أخرى... بالطبع لحل الخطأ نقوم بعمل نسخ عميق Deep Copy، يجنبنا هذه المشكلة:
#include <vector>using namespace std;class Example{public:	int* p;		Example(): p(new int) {}	Example(const Example& other)	{		p= new int;		*p= *(other.p);	}	~Example()	{		delete p;	}};int main(){	vector<Example> a;	a.push_back(Example());	return 0;}

 

 

 
  • مشكلة المؤشرات الجامحة Stray Pointers، التي تم تحرير الذاكرة التي حجزتها (بواسطة delete)، لكن لم يتم تصفيرها (إسناد القيمة NULL أو 0 إليها)، والتي تحدث غالباً عند حدوث الـShallow Copy، الكود السابق هو مثال عليها، حيث تم حذف المؤشر، ومن ثم تمت محاولة إعادة حذفه مرة أخرى، مما سبب الخطأ.
 
  • مثال آخر على الـMemory Leak، كما هو من مقالة: 
Smart Pointers - What, Why, Which?
 
void foo(){    MyClass* p(new MyClass);    p->DoSomething();    delete p;}

 

 

 
لنفترض أن استدعاء DoSomthing سبب رمي استثناء ما. فإن السطر 
delete p;

 

 

لن ينفذ، وبالتالي لن يستدعى الـDestructor الخاص بـMyClass، وبناء على ما يفعل هذا الـDestructor، ستكون النتائج....
حيث أن هذا الـDestructor قد يحرر موارد عديدة في الذاكرة ويسند قيماً، ويعلم برامج أخرى ووو، كل هذا لن يحدث لأن التنفيذ لم يصل إلى جملة delete...
 
هناك مشاكل أخرى كثيرة تظهر عند التعامل مع المؤشرات. ولا ندّعي، أن أياً من هذه المشاكل لا يمكن حلها، ولكنها قد تكلف الشركة أطناناً من القهوة وفواتيراً من الأطباء العصبيين!
 
إذاً... ما الحل؟
الحل المباشر.... الحذر في التعامل ومعرفة ما تقوم به في كل لحظة. لكن هذا غير كافٍ، فمن طبيعة الإنسان الخطأ. أيضاً... لماذا لا يكون لديك أداة تريحك من عناء المتابعة والتدقيق؟؟
هنا تأتي المؤشرات الذكية
 
فما هي المؤشرات الذكية؟؟؟؟
تخيل أنك تلعب لعبة كمبيوتر، أنت فيها قائد مجموعة سرية، تقوم بإجراء المهمات الصعبة في الكواكب الأخرى بتكليف من حكومة الأرض.
لكل مهمة، ترسل واحداً من عملائك السريين، بعد أن تعطيه درعاً يساعده على القيام بالمهمة... هناك دروع مختلفة، فهناك درع الإخفاء، ودرع تسريع الحركة، ودرع الحماية من الحرارات المرتفعة.. إلخ
بالطبع كلاعب محترف، لن تقوم بإرسال العميل إلى كوكب قريب من الشمس إلا بالدرع الذي يقي من الحرارة المرتفعة، ولن تعطيه درع الإخفاء إذا كان لدى الكوكب الآخر حساسات حرارية تلتقط أجسام البشر. وبالتأكيد، فإن درع السرعة هو الأنسب عندما يكون الوقت ضيقاً.
 
ما المقصود من هذا كله؟ 
إن العميل هو المؤشر العادي الذي نعرفه، والمهمة السرية هي ما سيقوم به هذا المؤشر من حجز وتحرير للذاكرة وغير ذلك.
أما الدرع، فهو المؤشر الذكي، فالمؤشر الذكي لا يعدو عن كونه "غلافاً" يحيط بالمؤشر ليساعده على القيام بمهامه، وكما رأينا، فإن للمؤشر الذكي أنواعاً، نختار المناسب منها حسب المهام التي نريد من المؤشر القيام بها.
 
المؤشر الذكي هو Class يحوي بداخله مؤشراً عادياً، ومتغيرات أخرى ربما، كما يحوي بداخله العديد من الـFunctions التي تدير التعامل مع المؤشر الداخلي.
 
ما فوائد هذه المؤشرات الذكية؟
فوائدها كثيرة، نحصي بعضاً منها:
  • إدارة الذاكرة... فبعض أنواع المؤشرات الذكية تحرص على أن لا يشير إلى المنطقة الواحدة من الذاكرة أكثر من مؤشر واحد، وعندما ينتهي عمر هذه المؤشرات فإنها تقوم تلقائياً بتحرير الذاكرة.
    نوع آخر من المؤشرات يسمح لأكثر من مؤشر أن يشيروا إلى المكان ذاته في الذاكرة، ولا يحرره إلا عندما ينتهي عمر آخر مؤشر كان يشير إليه.
    إذاً، فإن هذه المؤشرات تقوم بتقليل احتمالية حدوث الـBugs الخاصة بالذاكرة، كما تقوم بالـGarbage Collection لتجنب الـMemory Leakage.
  • التوفير في استهلاك الذاكرة... فلنفترض أننا قمنا بالتأشير بأحد المؤشرات إلى كائن كبير جداً، ثم أردنا نسخه، فإن هذا يعني أنه سيصبح لدينا نسختان من هذا الكائن في الذاكرة، وعندما ننسخه عشر مرات فسيصبح لدينا عشر نسخ...
    أحد أنواع المؤشرات الذكية، يقوم بحل مشكلة كهذه بطريقة "ذكية"، فهو كما سنرى يقوم عند النسخ بجعل كل المؤشرات تشير إلى الكائن نفسه في الذاكرة، لكن عندما يحاول أحد هذه المؤشرات إجراء أي تعديل على الكائن، يقوم بإنشاء نسخة جديدة لهذا المؤشر ثم يجري التعديلات على النسخة، بينما تبقى المؤشرات الأخرى كلها تشير إلى النسخة الأصلية، مما يوفر الكثير من الذاكرة.
ويمكننا القول إن فوائد المؤشرات الذكية غير محدودة، فأي مشكلة يواجهها المرء مع المؤشرات، يمكن أن يصمم لها مؤشراً ذكياً جديداً يقوم بحلها، ومن يدري... ربما يقوم أحد قارئي هذا المقال بتصميم مؤشر جديد يحل مشكلة أدت في يوم من الأيام إلى ارتفاع كبير في نسبة الصلع في العالم!!
 
في الردود القادمة، سأقوم إن شاء الله بشرح الأنواع المشهورة من المؤشرات الذكية، وكيفية تصميمها واستعمال الجاهز منها، أرجو أن يكون في ذلك الفائدة....
 
لكن أمهلوني بعض الوقت :)
 
والسلام عليكم ورحمة الله....
6

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه

1 إجابات على هذا السؤال .

  • 0

المؤشر التلقائي Auto Pointer:

كما ذكرت في المرة السابقة، هناك أنواع متعددة للمؤشرات الذكية، لكل منها وظيفة محددة نستعمله من أجلها، ولتكون بدايتنا مريحة، سنبدأ بأبسط أنواع المؤشرات الذكية، وهو المؤشر التلقائي auto_ptr. السؤال الأول الذي يتبادر إلى الذهن.... لماذا تلقائي؟ لأنه ببساطة، يقوم بشكل تلقائي، بتحرير الذاكرة عندما ينتهي عمله، وبالتالي لن تحتاج إلى القلق من مشاكل تسرب الذاكرة –في الحالات البسيطة- عند استعمال هذا المؤشر.

مثال بسيط على الأمر، لنفترض أن لدينا الدالة التالية:

 

 

void testing(){                int* p= new int;                               //Some code                delete p;}

 

 

يبدو هذا الكود آمناً، لكن ماذا يجري إذا خرجت الدالة testing قبل وصولها إلى السطر الأخير؟ ربما تم رمي استثناء أو صادفت جملة return على الطريق... وبالتالي فإن الذاكرة التي حجزها p لن تحرر مما سيؤدي إلى تسرب في الذاكرة.

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

 

 

#include <memory>using namespace std;void testing(){       auto_ptr<int> p(new int);       //Some code}

 

 

إذاً، فإن الـauto_ptr هو عبارة عن template class نحدد له في الـtemplate parameter نوع البيانات التي نريد الإشارة إليها بالمؤشر. وهو ككائن عادي، يستدعى الـdestructor الخاص به بشكل تلقائي عند الخروج من الدالة بأي طريقة، حيث يقوم هذا الـdestructor بتحرير الذاكرة بالشكل المناسب.

إذاً، فلم يعد هناك خوف من ألا نصل إلى جملة delete، لأن الـdestructor الخاص بالـauto_ptr هو من يستدعيها ولا مفر من الوصول إليه عند انتهاء الدالة.

الجميل أيضاً في هذا المؤشر الذكي، أنه من الممكن استعمال معاملات المؤشرات العادية عليه كمعامل الـ* ومعامل <-، مثلاً:

 

 

#include <iostream>#include <memory>using namespace std;void testing();class SomeClass{public:       void SomeMethod()       {              cout<< "Wow!! It's working!!\n";       }};int main(){       testing();       return 0;}void testing(){       auto_ptr<int> p1(new int);       *p1= 5;       cout<< *p1<< endl;       auto_ptr<SomeClass> p2(new SomeClass);       p2->SomeMethod();}

 

 

هناك خصائص عديدة أخرى يتميز بها المؤشر التلقائي، سنشرحها من خلال صنع مؤشر تلقائي خاص بنا، ولنسمه، AT_auto_ptr.

 

 

template <class T>class AT_auto_ptr{private:       T* raw_ptr;public:AT_auto_ptr(): raw_ptr(0) {}       AT_auto_ptr(T* ptr): raw_ptr(ptr) {}       ~AT_auto_ptr()       {              delete raw_ptr;       }};

 

 

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

ما يفعله هذا الكلاس هنا ببساطة هو إعطاء قيمة للمؤشر بداخله عند إنشائه، وتحرير الذاكرة عند تدميره.

تجدر الإشارة هنا إلى أن المؤشر التلقائي يجب ألّا يشير إلّا إلى ذاكرة في الـheap، أي تم حجزها بشكل ديناميكي باستخدام new وما شابهها. لأنه في حال حاول الإشارة إلى ذاكرة في الـstack فإنه لن يكون قادراً على تحريرها عند استدعاء الـdestructor. إلا إن كانت هناك طريقة لمعرفة إن كانت الذاكرة المشار إليها تابعة للـstack أو الـheap، فعندها ليجاوبنا من عنده علم بذلك  :) 

 

سنقوم الآن بتزويد الكلاس بمعاملات المؤشرات الأساسية:

 

 

template <class T>class AT_auto_ptr{private:       T* raw_ptr;public:       AT_auto_ptr(): raw_ptr(0) {}       AT_auto_ptr(T* ptr): raw_ptr(ptr) {}       ~AT_auto_ptr()       {              delete raw_ptr;       }       T& operator * () const       {              return *raw_ptr;       }       T* operator -> () const       {              return raw_ptr;       }};

 

لاحظ أن الـreturn type الخاص بالمعامل * هو من النوع T& وليس T وذلك لتجنب القيام بعملية نسخ زائدة ليس منها فائدة.

 

سؤال جوهري يجب طرحه الآن، ماذا يحدث عندما نحاول نسخ مؤشر تلقائي إلى مؤشر تلقائي آخر؟

هذا بالطبع يعتمد على تصميمنا، لكن التصميم المتفق عليه يجعل المؤشر التلقائي يقوم عند نسخه بعملية transfer-of-ownership أو نقل الملكية، حيث يتخلى المؤشر الأصلي عن ملكيته لموقع الذاكرة، ويعطيه للمؤشر الجديد، بينما يسند قيمة مؤشره الداخلي إلى NULL.... يا له من محب للإيثار!!

هذه العملية تحمينا من أن يكون هناك أكثر من مؤشر واحد يشير لنفس الموقع في الذاكرة، كما أنها مفيدة عند إرجاع مؤشر من دالة معينة، حيث يمكننا أن نقوم بـ:

 

auto_ptr<int> testing(){	auto_ptr<int> temp(new int);	*temp= 5;	return temp;}

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

 

لكن يجب الانتباه أن عملية نقل الملكية لا تقوم بعمل Deep Copy، لأن المؤشر الذكي المنسوخ منه يفقد ملكيته تماماً لموقع الذاكرة.. وبالتالي، فإن هناك أماكن يجب ألا يستخدم فيها المؤشر التلقائي، سنتحدث عنها لاحقاً. لكن أولاً، كيف نقوم بعملية نقل الملكية؟

قبل أن نكتب كود النسخ، سنكتب أولاً دالة release، ما تقوم به هذه الدالة هو إسناد قيمة NULL إلى المؤشر الخام الموجود داخل المؤشر التلقائي، ثم تقوم بإعادة موقع الذاكرة الذي كان يشار إليه في هذا المؤشر من قبل، إذاً فهذه الدالة تقوم بـ"التخلي" عن موقع الذاكرة الخاص بها لتعطيه لمن يقوم بالتقاط القيمة المرجعة من الدالة، كالتالي:

   

 

    T* release()       {              T* temp= raw_ptr;              raw_ptr= 0;              return temp;       }

 

الآن يمكنني أن أستعمل هذه الدالة لأقوم بعملية نقل الملكية عند استدعاء copy constructor أو معامل النسخ العادي، فيصبح الكود:

 

template <class T>class AT_auto_ptr{private:       T* raw_ptr;public:       AT_auto_ptr(): raw_ptr(0) {}       AT_auto_ptr(T* ptr): raw_ptr(ptr) {}       AT_auto_ptr(AT_auto_ptr<T>& other): raw_ptr(other.release()) {}       ~AT_auto_ptr()       {              delete raw_ptr;       }       T& operator * () const       {              return *raw_ptr;       }       T* operator -> () const       {              return raw_ptr;       }             AT_auto_ptr& operator = (AT_auto_ptr<T>& other)       {              if (this != &other)              {                     delete raw_ptr;                     raw_ptr= other.release();              }              return (*this);       }       T* release()       {              T* temp= raw_ptr;              raw_ptr= 0;              return temp;       }};

 

لاحظ أننا في المعامل = قمنا أولاً بتحرير الذاكرة التي يقوم المؤشر بحجزها قبل إسناد القيمة الجديدة إليه، وذلك لتجنب حدوث تسرب في الذاكرة.

 

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

       

 

auto_ptr<int> p(new int);             //Some code       p.reset(new int);

 

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

 

 

void reset (T* ptr)       {              if (ptr != raw_ptr)              {                     delete raw_ptr;              }                           raw_ptr= ptr;       }

 

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

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

 

 

T* get()       {              return raw_ptr;       }

 

بالطبع إذا استخدمت هذه الدالة لأغراض شريرة، كأن تقوم بعمل:

 

 

delete p.get();

 

فـ"ذنبك على جنبك"  :P 

 

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

قد ترغب في استعمال المؤشر التلقائي كـmember داخل Class، وهذا أمر جميل حيث أن الذاكرة التي يقوم بحجزها المؤشر ستحرر عند انتهاء حياة الـobject، لكن يجب الانتباه من عمليات النسخ، لأن عملية نقل الملكية "ستقتل" المؤشر الذكي المنسوخ منه، لذلك، يجب أن نقوم بعمل نوع آخر من الـdeep copy...

 

class SomeClass{public:       AT_auto_ptr<int> a;       SomeClass(): a(new int) {}       SomeClass& operator = (const SomeClass& other)       {              a.reset(new int);              *a= *(other.a);              return (*this);       }};int main(){       SomeClass first;       *(first.a)= 5;       SomeClass second;       second= first;       cout<< first.a.get()<< endl;       cout<< second.a.get()<< endl;       cout<< *(first.a)<< endl;       cout<< *(second.a)<< endl;       return 0;}

 

هذه المشكلة قد حلت إذاً باستخدام أسلوب مشابه للـDeep Copy، لكن مشكلة أساسية تحد من قدرات المؤشر التلقائي، ألا وهي أنه من غير الممكن استعماله داخل STL Containers، حيث أن هذه الحاويات تقوم بعمليات نسخ متعددة، دون الاهتمام بكون العناصر داخلها من نوع المؤشر التلقائي، وبالتالي فإن عملية نقل الملكية ستودي إلى جعل عدد كبير من المؤشرات فارغة تشير إلى NULL، ويمكن مشاهدة ذلك بوضوح في دالة sort الموجودة في STL والتي تعتمد على خوارزمية Quick Sort، هذه الخوارزمية تقوم بنسخ أحد العناصر ووضعه في محور في كل مرة وترتيب العناصر من حوله، وفي النهاية يتم حذف هذا المحور، مما سيؤدي إلى أن تصبح كل المؤشرات في الـContainer تشير إلى NULL.... بالمختصر المفيد، لا تستخدم المؤشر التلقائي في STL Containers...

بعد التفكير... جرب أن تستخدمه، لن تخسر شيئاً، لكنك لن تستطيع أصلاً أن تستخدمه لأنه تمت برمجته بطريقة تؤدي إلى صدور Compile Error عند استعماله مع دوال STL كـinsert وغيرها.... فلا مشكلة  :D 

 

 

كتلخيص....

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

 

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

 

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

atautoptr.h

تم تعديل بواسطه Abboodd
3

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه

من فضلك سجل دخول لتتمكن من التعليق

ستتمكن من اضافه تعليقات بعد التسجيل



سجل دخولك الان

  • يستعرض القسم حالياً   0 members

    لا يوجد أعضاء مسجلين يشاهدون هذه الصفحة .