2015/08/09

تطبيق خوارزمية Depth First Search لرسم متاهة

السلام عليكم ورحمة الله وبركاته
تعتبر خوارزمية Depth First Search أحد الخوارزميات للمرور على جميع النقاط المتصلة connected nodes في مخطط Graph ما ..
لا أريد التكرار فالموضوع عليه الكثير من الشروحات الواضحة على youTube  كما أن صفحة wikipedia فيها شرح ممتاز (وفيديو ملحق لإنشاء متاهة أيضاً :) )
ولكن لتبسيط الأمر .. تخيل أن لدينا شجرة كثيرة الفروع , حتى أن فروعها متداخلة , يعني يمكن أن يتقاطع فرعان ويندمجا !
تخيل أن هذه شجرة :
https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Minimum_spanning_tree.svg/2000px-Minimum_spanning_tree.svg.png
هذه الأشجاء التخيلية التي تحوي فروعاً وتقاطعات تُسمّى Graphs (مخططات )
المهم ..
تخيل أننا نريد المرور على جميع فروع الشجرة . سنبدأ من الجذر ونصعد .. ثم سيتفرع الجذع الى عدة فروع .. وكل فرع سيتفرع لاحقاً وهكذا ..
هناك عدة طرق يمكننا من خلالها ضمان المرور على جميع الفروع .(المرور على جميع الفروع والتقاطعات(العقد) يُسمّى traversing )
إحداها طريقة Depth first Search
تحتاج لتطبيقها إلى مكدّس ومعرفة بسيطة في الحلقات والشروط .
خوارزمية DFS :
0-نضع الجذر في المكدس
ونبدأ الحلقة طالما أن المكدس غير فارغ :
1- نقوم بوضع علامة على العنصر في قمة المكدس للدلالة على انه قد تمت زيارته
2- نقوم باختبار وجود عناصر مرتبطة بالعنصر الحالي(قمة المكدس ) وهل هي صالحة للزيارة (تكون صالحة للزيارة إن كانت مرتبطة بالعنصر ولم تتم زيارتها من قبل )
3- كل عنصر يحقق الشرط في الخطوة 2 نضعه في المكدس
4- إن لم يحقق أي عنصر الشرط في الخطوة 2 نقوم بإخراج العنصر الحالي من المكدس
5- ان كان المكدس غير فارغ نذهب للخطوة 1

لنحول الخوارزمية إلى كود
نحتاج إلى آلية تكديس وسنستعمل std::stack, وآلية loop ويمكن أن نستعمل for  ,
وسنحتاج آلية لإعادة القيام بالعملية على العقدة الجديدة (يمكن أن نستعمل العودية recursion ولكن سنطبق اليوم بواسطة حلقة while)
كما أننا نحتاج وجود المخطط وبه العقد المتصلة (النقاط المتصلة) وسنستعمل ببساطة مصفوفة من بعدين , كل عنصرين متجاورين فيها يكونان مرتبطين
مثال للمصفوفة :
1 2 3
4 5 6
7 8 9
نقول أن 1 مع 4 و 2 مرتبطة , وكذلك :  9 مع 6 و 8 وهكذا
(يمكن لك أن تعتبر وجود ارتباطات بالمائل مثل 5 و 9 ان أردت )
يمكننا وضع علامة على النقطة التي تمت زيارتها ببساطة بتغيير قيمة المصفوفة مثلاً من 0 إلى 1
أخيراً  : نلاحظ أن الخطوة 4 لم تعد تحتاج إلى for loop  لأن العقد المرتبطة في حالة المصفوفة هي 4 كحد أقصى لذلك سنكتبها يدوياً

لنبدأ ..على بركة الله
تحويل الخوارزمية إلى كود :
أولا ودوماً أولاً : تطبيق بنية الـGraph
ببساطة مصفوفة يمكن ان تحوي 0 أو 1 .. bool :)
bool graph[30][30]={false};
القيمة false تعني أننا لم نزر أي عقدة بعد .
ثانياً : كيفية الامساك بعقدة ما ..
في حالتنا عن طريق الموضع في المصفوفة
سنكتب  struct بسيط يُمثّل الموضع
struct coord{
    int x;
    int y;
    coord(int x1,int y1){
        x=x1;
        y=y1;
    }
};
ولا ننسى آلية التكديس
stack <coord> s;
والآن  الحلقة التي سنعمل بداخلها الخطوات من 1 إلى 5 , وبها سنتابع بقية العمل , ولكن قبل الدخول إليها علينا دفع الجذر إلى المكدس (أو أي عقدة نرغب في البدء منها )
while(!s.empty())
{
 
}
سنقوم بملء التابع السابق كما توضّح الخوارزمية

1-عملية وضع علامة على الفرع الذي تمت زيارته
        graph[s.top().y][s.top().x]=true;
2-اختبار صلاحية زيارة جميع العقد المرتبطة , وبعد انتهاء العقد المرتبطة ( أو عدم وجودها فالأمر سيان ) أخرج العقدة الحالية من stack
سنختبر كل جهة على حدة كما يلي:
//look at left
        if(valid(s.top().y,s.top().x-1)){
            s.push(coord(s.top().y,s.top().x-1));
        }
        //look at right
        else if(valid(s.top().y,s.top().x+1)){
            s.push(coord(s.top().y,s.top().x+1));
        }
        //look up
        else if(valid(s.top().y-1,s.top().x)){
            s.push(coord(s.top().y-1,s.top().x));
        }
        //look down
        else if(valid(s.top().y+1,s.top().x)){
            s.push(coord(s.top().y+1,s.top().x));
        }
        else{
            s.pop();
        }
الكود بصيغته النهائية :
الكود :
أولاً : البنى والتوابع المساعدة
struct coord{
    int x;
    int y;
    coord(int y1,int x1){
        x=x1;
        y=y1;
    }
};
const int X=10,Y=10;
bool graph[Y][X]={false};
stack <coord> s;
bool valid(int y,int x){
    if(x<X&&x>=0)
        if(y<Y&&y>=0)
            if(graph[y][x]==false)
                return true;
    return false;
}
والتنفيذ في الدالةmain
int main()
{
    //push the current node
    s.push(coord(0,0));

    while(!s.empty())
    {
        graph[s.top().y][s.top().x]=true;

        //look at left
        if(valid(s.top().y,s.top().x-1)){
            s.push(coord(s.top().y,s.top().x-1));
        }
        //look at right
        else if(valid(s.top().y,s.top().x+1)){
            s.push(coord(s.top().y,s.top().x+1));
        }
        //look up
        else if(valid(s.top().y-1,s.top().x)){
            s.push(coord(s.top().y-1,s.top().x));
        }
        //look down
        else if(valid(s.top().y+1,s.top().x)){
            s.push(coord(s.top().y+1,s.top().x));
        }
        else{
            s.pop();
        }
    }

    return 0;
}
كي نتابع عملية السير سنكتب تابع بسيط لإظهار المخطط بعد كل تغيير
هذا مثال
void printGraph()
{
/*
Put here any implementation to return the pointer to top left
*/    

    cout << s.size() << " " << s.top().x <<" " <<s.top().y<< endl;
    for(int i=0;i<Y;i++)
    {
        for(int j=0;j<X;j++)
        {
            cout<<(graph[i][j]?'#':' ');
        }
        cout <<endl;
    }
/*
Put here any implementation to Sleep for 10-100 milli second
*/    
}
في ويندوز سأستعمل تابعين من الـ API
void printGraph()
{
    COORD topLeft={0,0};
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),topLeft);
    cout << s.size() << " " << s.top().x <<" " <<s.top().y<< "     " << endl;
    for(int i=0;i<Y;i++)
    {
        for(int j=0;j<X;j++)
        {
            cout<<(graph[i][j]?' ':'#');
        }
        cout <<endl;
    }
    Sleep(30);
}
ثم ضع استدعاء التابع داخل حلقة while الخاصة بالخوارزمية
جرب الكود التالي  في ويندوز (++C)
#include<stack>
#include<iostream>
#include<windows.h>
using std::stack;
using std::cout;
using std::endl;

struct coord{
    int x;
    int y;
    coord(int y1,int x1){
        x=x1;
        y=y1;
    }
};
const int X=10,Y=10;
bool graph[Y][X]={false};
stack <coord> s;
void printGraph()
{
    COORD topLeft={0,0};
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),topLeft);
    cout << s.size() << " " << s.top().x <<" " <<s.top().y<< "     " << endl;
    for(int i=0;i<Y;i++)
    {
        for(int j=0;j<X;j++)
        {
            cout<<(graph[i][j]?' ':'#');
        }
        cout <<endl;
    }
    Sleep(30);
}
bool valid(int y,int x){
    if(x<X&&x>=0)
        if(y<Y&&y>=0)
            if(graph[y][x]==false)
                return true;
    return false;
}

int main()
{

    //put flag on the visited node
    int x=0,y=0;

    //push the current node
    s.push(coord(x,y));

    while(!s.empty())
    {
        printGraph();
        graph[s.top().y][s.top().x]=true;

        //look at left
        if(valid(s.top().y,s.top().x-1)){
            s.push(coord(s.top().y,s.top().x-1));
        }
        //look at right
        else if(valid(s.top().y,s.top().x+1)){
            s.push(coord(s.top().y,s.top().x+1));
        }
        //look up
        else if(valid(s.top().y-1,s.top().x)){
            s.push(coord(s.top().y-1,s.top().x));
        }
        //look down
        else if(valid(s.top().y+1,s.top().x)){
            s.push(coord(s.top().y+1,s.top().x));
        }
        else{
            s.pop();
        }
    }
    return 0;
}

تجدر الإشارة إلى فكرة هامّة جداً ..
يعتمد المعالج في استدعاء التوابع على مكدّس خاص بالاستدعاءات
ويمكننا الاستغناء عن مكدسنا std::stack والاستعانة بالمكدس الخاص بالاستعداءات
وذلك عن طريق وضع العملية في تابع بدلاً من while , وبدلاً من عملية push سنقوم باستدعاء التابع مرة أخرى , وبمجرد انتهاء التابع أو عمل return  سيتم عمل pop للقيمة الحالية
انظر الكود التالي(أبسط من السابق)
#include<iostream>
#include<windows.h>
using std::cout;
using std::endl;

const int X=10,Y=10;
bool graph[Y][X]={false};
void printGraph()
{
    COORD topLeft={0,0};
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),topLeft);
    for(int i=0;i<Y;i++)
    {
        for(int j=0;j<X;j++)
        {
            cout<<(graph[i][j]?' ':'#');
        }
        cout <<endl;
    }
    Sleep(30);
}
bool valid(int y,int x){
    if(x<X&&x>=0)
        if(y<Y&&y>=0)
            if(graph[y][x]==false)
                return true;
    return false;
}
void function(int y,int x){
    printGraph();
    graph[y][x]=true;

    //look at left
    if(valid(y,x-1)){
        //s.push(coord(s.top().y,s.top().x-1));
        function(y,x-1);
    }
    //look at right
    if(valid(y,x+1)){
        //s.push(coord(s.top().y,s.top().x+1));
        function(y,x+1);
    }
    //look up
    if(valid(y-1,x)){
        //s.push(coord(s.top().y-1,s.top().x));
        function(y-1,x);
    }
    //look down
    if(valid(y+1,x)){
        //s.push(coord(s.top().y+1,s.top().x));
        function(y+1,x);
    }
//        s.pop();
        return ;
}
int main()
{
    function(0,0);
    return 0;
}
ولكننا خسرنا ميزة تتبع المكدس فلم يعد بإمكاننا مثلاً كتابة
cout << s.size() << " " << s.top().x <<" " <<s.top().y<< "     " << endl;
إذا جربت الكود , فستلاحظ أنه يسير بطريقة عادية ليمر على جميع عناصر المصفوفة , ولكن جرب كتابة
 function(5,5);
وسيبدأ من المنتصف , وعندها ستلاحظ سلوكاً غير متوقع (ربما) في المرور على جميع العناصر .

والآن إلى إنشاء المتاهة:
ببساطة سنتحرك خطوتين بدلاً من خطوة واحدة , وبذلك سنترك فراغات تُشكّل الحوائط !
#include<cstdio>
#include<windows.h>

const int X=21,Y=21;
bool graph[Y][X]={false};
void printGraph()
{
    COORD topLeft={0,0};
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),topLeft);
    for(int i=0;i<Y;i++)
    {
        for(int j=0;j<X;j++)
        {
            putchar(graph[i][j]?' ':'#');
        }
        putchar('\n');
    }
}
bool valid(int y,int x){
    if(x<X&&x>=0)
        if(y<Y&&y>=0)
            if(graph[y][x]==false)
                return true;
    return false;
}
void function(int y,int x){
    printGraph();

    //look at left
    if(valid(y,x-2)){
        //s.push(coord(s.top().y,s.top().x-1));
        graph[y][x-1]=true;
        graph[y][x-2]=true;
        function(y,x-2);
    }
    //look at right
    if(valid(y,x+2)){
        //s.push(coord(s.top().y,s.top().x+1));
        graph[y][x+1]=true;
        graph[y][x+2]=true;
        function(y,x+2);
    }
    //look up
    if(valid(y-2,x)){
        //s.push(coord(s.top().y-1,s.top().x));
        graph[y-1][x]=true;
        graph[y-2][x]=true;
        function(y-2,x);
    }
    //look down
    if(valid(y+2,x)){
        //s.push(coord(s.top().y+1,s.top().x));
        graph[y+1][x]=true;
        graph[y+2][x]=true;
        function(y+2,x);
    }
//        s.pop();
        return ;
}
int main()
{
    function(5,5);
    Sleep(100000);
    return 0;
}
جرب الكود , وستلاحظ أن المتاهة سهلة جداً  للحل
ولجعلها صعبة وعشوائية سنغير فقط طريقة الرؤية للكود ! ماذا يعني هذا ؟ يعني أن نجعل اختبارات (اليسارواليمين .. ) غير ثابته , فمثلاً يمكن أن نختبر المرور للأسفل قبل اليمين وهكذا ..
لاحظ اختلافات الكود :
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<windows.h>

const int X=31,Y=31;
bool graph[Y][X]={false};
void printGraph()
{
    COORD topLeft={0,0};
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),topLeft);
    for(int i=0;i<Y;i++)
    {
        for(int j=0;j<X;j++)
        {
            putchar(graph[i][j]?' ':'#');
        }
        putchar('\n');
    }
}
bool valid(int y,int x){
    if(x<X&&x>=0)
        if(y<Y&&y>=0)
            if(graph[y][x]==false)
                return true;
    return false;
}
void function(int y,int x){
    printGraph();
    for(int i=0;i<10;i++){//to ensure passing all valid moves
        int dx=0,dy=0;
        while(dx^dy==0){
            dy=rand()%2;//zero or one
            dx=rand()%2;//zero or one
        }
        if(rand()%2==1)
            dx*=-1,
            dy*=-1;
        if(valid(y+2*dy,x+2*dx)){
            //s.push(coord(s.top().y,s.top().x-1));
            graph[y+dy][x+dx]=true;
            graph[y+2*dy][x+2*dx]=true;
            function(y+2*dy,x+2*dx);
        }
    }
        return ;
}
int main()
{
    srand(time(0));
    function(5,5);
    Sleep(100000);
    return 0;
{
في الختام أود لفت الانتباه إلى أنه من الأمور الهامة جداً عند دراسة الخوارزميات عدم خلط الخورازمية بالتطبيق ,
مثلاً فتطبيق رسم المتاهة هو أحد التطبيقات لخوارزمية DFS وليس هو الخوارزمية , وقد وضعته كمثال رسومي جيد لبيان جمال الخوارزمية ليس أكثر .

والله ولي التوفيق
===================كاتب المقال: مصطفى 36a2==================

تعرف على دالة الترتيب السريع qsort

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

سنتعامل اليوم مع دالة الترتيب السريع qsort وهي التي تعتبر أسرع طرق الترتيب ..
الترتيب QuickSort والذي يأخذ( n*log(n عملية في الحالة المتوسطة .. لترتيب مصفوفة من n عنصراً .. ( هذه القيمة تسمى بالتعقيد الزمني Time Complexity وهي ليست عدد العمليات تماما  )
يجدر بك الاطلاع على خوارزمية الترتيب السريع لمعرفة كيفية عملها ... ولكن هذا غير ضروري الآن ..

تحتاج إلى معرفة أولية بالمؤشرات والدوال لفهم جيد .. كما انه من المهم جدا المامك بعملية تحويل الانواع cast

لنتعرف الآن على الدالة qsort
تأخذ هذا الدالة أربع وسطاء هي بالترتيب :
1-  مؤشر من نوع void يشير الى أول عنصر في المصفوفة (أي إلى المصفوفة ) .
2- عدد صحيح يحدد عدد عناصر المصفوفة .
3- عدد صحيح يحدد حجم العنصر الواحد في المصفوفة .
4- مؤشر إلى دالة مقارنة عنصرين ... صفاتها :
          1- تعيد عدداً صحيحاً يدل على ناتج مقارنة العنصرين الممررين كوسطاء
          2- تأخذ وسيطين من نوع const void *  أي أنهما مؤشران ثابتان على العنصرين الذين ستتم مقارنتهما .


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


  هذا كل ما تحتاجه لمعرفة استعمال الدالة ..

توقيع الدالة معقد بعض الشيء ولكن يمكننا تعديله قليلا لنقرأه كما يلي :
void qsort(void * _Base,
    int _NumOfElements, int _SizeOfElements,
       int (* _FuncCompare)(const void *, const void *));
(قمت باستبدال بعض الاسماء مثل size_t إلى int وحذف بعض المعرفات مثل__cdecl للتسهيل )
_______________________

لاستعمال الدالة .. يكفي تعريف دالة المقارنة فقط لا غير ثم يمكننا استعمال الدالة فورا ..

مثال يوضح طريقة الاستعمال :
نبدأ دوما مع دالة المقارنة .. حافظ على تعريف الدالة كما هو ..
int compare(const void*a,const void*b)
{
    int A=*((int*)a);
    int B=*((int*)b);
    if ( A < B )return -1;
    else if(A==B)return 0;
    else return 1;
}
يمكننا ان نجعل الترتيب تصاعديا أو تنازليا بتغيير الشروط ..
 ويمكن جعل الدالة أعقد لتقوم بمقارنة عناصر من كائن مع عناصر كائن آخر بطريقة تحددها انت ..
ليس هناك حدود لاستخدام الدالة طالما أنك تخافظ على القيم المعادة وتعريف الدالة كما هو ..

وهذا برنامج يوضح استخدام الدالة .. بأبسط ما يمكن ..
#include<cstdio>
int main()
{
    int a[10]={8,5,7,1,2,47,6,9,2,3};
    qsort ((void *)a, 10, sizeof(int), compare);
    for(int i=0;i<10;i++)
        printf("%i\n",a[i]);
}
لاحظ سهولة الاستخدام

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


كيف يمكنك اغناء المشاركة ...
1- يمكنك كتابة برنامج يوضح ترتيب مصفوفة من struct ما .. يحوي اسماً ورقما ويقوم بالترتيب حسب الاسماء وعند تشابه الاسم يرتب حيب الرقم
2- يمكنك كتابة مثال آخر على استخدام الدالة ..
3- يمكنك تقديم روابط تشرح qsort من مصادر أخرى .. بشكل أفضل
4- يمكنك كتابة موضوع عن دوال أخرى للترتيب أو البحث مثل bsearch  مثلاً


كيف يمكنك الاساءة الى المشاركة ..
1-أن ترد بكلمة شكر فقط ..
2- أن تسأل عن أمر غير متعلق بالموضوع ..


أخيرا .. نسيت أن أذكر أن qsort موجودة في المكتبة cstdlib .. :)
والسلام عليكم ورحمة الله وبركاته
=================كاتب المقال: مصطفى 36a2 ==================

تعرّف على التابع std::sort


السلام عليكم ورحمة الله ويركاته

هدف هذا المقال هو التعرف على تابع الترتيب الجاهز في مكتبة STL التي توفرها ++C , والذي يتم تطوير الـimplimentation بداخله نسخةً بعد نسخة , فقد بدأت بالترتيب السريع quick sort مع أول ظهور لها (إذ كانت تماثل تابع qsort في cstdlib ) أما الآن فهي تعتمد خوارزميات ترتيب أفضل , وأكثر ثباتاً ( ليست عشوائية , وتحقق(n*log (n في الحالة الأسوأ ) , آخر نسخة أعرفها تستخدم intro sort كمرحلة أولى ثم insertion sort كمرحلة ثانية .

فهرس المقالة :
1-  تعريف التابع sort
2- وسطاء التابع
2.5- عملية المقارنة
3- Member < Operator

4- Global < Operator
5- تمرير الوسيط الثالث كمؤشر إلى تابع
6- تمرير الوسيط الثالث كــ function object
7- تمرير الوسيط الثالث كعبارة lambda

التابع sort :
ما يهمنا هنا هو استخدام هذا التابع , والمعرّف كقالب template بالشكلين التاليين :
template <class RandomAccessIterator>
void sort ( RandomAccessIterator first, RandomAccessIterator last );

template <class RandomAccessIterator, class Compare>
void sort ( RandomAccessIterator first, RandomAccessIterator last, Compare comp );

التعريف الأول يستخدم االعملية <  لمقارنة العناصر , أما الثاني فيستخدم تابع خاص للمقارنة .

وسطاء التابع :
الوسيط الأول: مؤشر وصول عشوائي لأول عنصر في القائمة المراد ترتيبها , مثلاً لو أردنا ترتيب مصفوفة فهو عنوان أول عنصر , أما لو أردنا ترتيب vector فهو الـ iterator الذي تعيده الدالة ()begin
الوسيط الثاني: مؤشر وصول عشوائي للعنصر بعد الأخير القائمة المراد ترتيبها .مثلاً لو أردنا ترتيب مصفوفة فهو عنوان أول عنصر + عدد العناصر , أما لو أردنا ترتيب vector فهو الـ iterator الذي تعيده الدالة ()end
الوسيط الثالث : هو مؤشر إلى تابع , أو functor سيتم شرحه بالتفصيل لاحقاً في هذا المقال .
مثال على ترتيب مصفوفة أعداد صحيحة :
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    int a[]={9,7,5, 4,1,8,  2,3,4,  5};
    sort(a,a+10);
    for(int i=0;i<10;i++){
        cout<<a[i]<<" ";
    }
    return 0;
}
مثال على ترتيب vector :
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main()
{
    vector<int>a={9,7,5, 4,1,8,  2,3,4,  5};
    sort(a.begin(),a.end());
    for(int i=0;i<10;i++){
        cout<<a[i]<<" ";
    }
    return 0;
}

عملية المقارنة :
أثناء القيام بعملية الترتيب , تتم مقارنة العناصر .
في لغة C يوجد التابع qsort الذي يحتاج وسيطاً هو تابع مقارنة , يعيد 1 في حال كان الوسيط الأول أكبر من الثاني , و -1 في حال أصغر , و 0 في حال التساوي .
أما في ++C , فالتابع sort  يقوم باختبار بولياني bool يعيد true أو false

Member < Operator :
ففي النسخة الأولى من التابع (ذات الوسيطين ) يقوم تلقائياً باختبار(if(a<b أي أنه يستخدم العملية (أصغر > )  , فلو كنت تريد ترتيب قائمة من نوع جديد , يجب أن يحوي هذا النوع بداخله على تعريف للعملية > .
مثال : لدينا نوع جديد اسمه book وأردنا تعريف عملية المقارنة > بحسب تاريخ الكتاب , ثم اسم مؤلفه ,
#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
struct book{
    int date;// 2000bc --> 2014 ac
    string author;
    book(int d,string a):date(d),author(a){
    }
    bool operator<(const book second){
        if(date<second.date)
            return true;
        else if(date==second.date){
            if(author<=second.author)
                return true;
            else
                return false;
        }else
            return false;
    }
    void print(){
        cout<<"author "<<author<<" date:"<<date<<endl;
    }
};
int main()
{
    vector<book>a;
    a.push_back(book(1999,"mohammad"));
    a.push_back(book(2014,"mostafa"));
    a.push_back(book(2010,"ahmad"));
    a.push_back(book(1988,"mohammad"));
    sort(a.begin(),a.end());
    for(unsigned int i=0;i<a.size();i++){
        a[i].print();
    }
    return 0;
}
/*
( ملاحظة : الكود لا يعمل على Code::blocks لسبب لا أعرفه , تمت تجربته بنجاح على VS2008 )
*/
هنا استعملنا العملية كـ member function , وكملاحظة سريعة : يمكننا استخدام أي من التعاريف التالية للوسيط :
    bool operator<(const book second);
    bool operator<(const book &second);
    bool operator<(book second);
    bool operator<(book &second);
ولكن تختلف بغعاليتها , أما من ناحية الصلاحية فهي صالحة .

Global < Operator:
كما يمكننا تعريف عملية > بدون وضعها داخل فئة معينة , أي تعريقها كــ  Global Operator كما يلي :
#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
struct book{
    int date;// 2000bc --> 2014 ac
    string author;
    book(int d,string a):date(d),author(a){
    }
    void print(){
        cout<<"author "<<author<<" date:"<<date<<endl;
    }
};
bool operator<(const book&first,const book&second){
        if(first.date<second.date)
            return true;
        else if(first.date==second.date){
            if(first.author<=second.author)
                return true;
            else
                return false;
        }else
            return false;
    }
int main()
{
    vector<book>a;
    a.push_back(book(1999,"mohammad"));
    a.push_back(book(2014,"mostafa"));
    a.push_back(book(2010,"ahmad"));
    a.push_back(book(1988,"mohammad"));
    sort(a.begin(),a.end());
    for(unsigned int i=0;i<a.size();i++){
        a[i].print();
    }
    return 0;
}
 

إن مسألة تعريف عملية الأصغر, مسألة مثيرة للاهتمام , فلا يمكنك ترتيب قائمة إن لم تكن تستطيع تحديد أي العناصر تريدها أن تكون في بداية القائمة وأيها في نهاية القائمة .
وعلى هذا المبدأ , يتعمد (التعريف الثاني للتابع ) على الوسيط الثالث compare
وهو تابع يأخذ وسيطين (من نوع العناصر في القائمة) ويعيد قيمة بوليانية boolean تكون true إن كان الأول أصغر من الثاني وfalse إن كان أكبر أو يساوي .
وبذلك يمكننا خداع الدالة بحيث تتعامل مع العملية > ونكون فعلياً نتعامل معها على أنها < لكي نعكس الترتيب (تصاعدي أو تنازلي )
ولكن للأسف , لا يمكننا إعادة تعريف عملية > للأنواع الأساسية , مثل int . فالكود التالي يعتبر خاطئاً
bool operator<(const int&a,const int&b){
    /*some operations*/
}
لذلك سنلجأ إلى كتابة تابع compare يقوم بالمهمة , وهو تماماً كفكرة عملية الـ > , يعيد قيمة بوليانية boolean ويأخذ وسيطين هما القيمتان المراد مقارنتهما .

تمرير الوسيط الثالث كمؤشر إلى تابع :
لفهم عمل التابع يجب فهم كيفية التعامل مع القيمة المعادة منه :
عندما يعيد هذا التابع true فإنه يضمن لك أن يكون الوسيط الأول قبل الوسيط الثاني ( لننس فكرة الأصغر والأكبر ) , فلو كنت تريد أن يكون العنصر الأصغر في البداية والأكبر في النهاية (ترتيب تصاعدي ) عليك إعادة true في حال كان الوسيط الأول أصغر من الثاني , أما إن كنت تريد الترتيب التنازلي فعليك إعادة true  إن كان الوسيط الأول أكبر من الوسيط الثاني , وبذلك سيضمن لك التابع أن الوسيط الأول سيستقر قبل الثاني في المصفوفة .
لمعرفة هل العددان متساويان يتم استدعاء التابع مرّتين بحيث تكون الثانية هي نقس الأولى ولكن بعكس ترتيب الوسطاء ,لذلك على التابع أن يحقق هذه العمليات المنطقية , فلا يجوز أن يعيد true مهما كانت الوسطاء مثلاً ..
المثال التالي يوضح كيفية تمرير (مؤشر إلى تابع) أو ببساطة (تابع) كوسيط ثالث للدالة sort
#include <iostream>
#include <algorithm>
using namespace std;

bool compare(const int&a,const int&b){
    if(a<b)
        return true;
    else return false;
}
int main()
{
    int a[]={9, 7,6,4,   8,2,5, 4,3,1};
    sort(a,a+10,compare);
    for(unsigned int i=0;i<10;i++){
        cout<<a[i]<<" ";
    }
    return 0;
}
الكود السابق يقوم بالترتيب التصاعدي العادي , أما التالي فهو (بتغيير محتوى تابع المقارنة ) يقوم بالترتيب التنازلي (سأضع الدالة compare  فقط )
bool compare(const int&a,const int&b){
    if(b<a)
        return true;
    else return false;
}
لاحظوا أننا لو كتبنا الدالة compare كما يلي :
bool compare(const int&a,const int&b){
    return true;
}
سيتم وضع المصفوفة بشكل عشوائي لأن عملية الترتيب لن تتم بأي شكل منطقي .

تمرير الوسيط الثالث كــ function object :
يمكن تمرير الوسيط كـ functor أي كـ struct تم تعريف العملية () بداخله , وهذه الطريقة مفيدة في تسريع الكود , حيث يمكنها استغلال خاصية الـ inlining في الكود
#include <iostream>
#include <algorithm>
using namespace std;

struct test{
    bool operator()(const int&a,const int&b){
        if(a>b)
            return true;
        else return false;
    }
};
int main()
{
    int a[]={9, 7,6,4,   8,2,5, 4,3,1};
    sort(a,a+10,test());
    for(unsigned int i=0;i<10;i++){
        cout<<a[i]<<" ";
    }
    return 0;
}

أحد هذه الfunctor الجاهزة هو greater نستخدمه للترتيب التنازلي كـ functor جاهز :
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    int a[]={9, 7,6,4,   8,2,5, 4,3,1};
    sort(a,a+10,greater<int>());
    for(unsigned int i=0;i<10;i++){
        cout<<a[i]<<" ";
    }
    return 0;
}

تمرير الوسيط الثالث كعبارة lambda  (خاص بـ C++11 وما بعد ) :
بالعودة إلى مؤشر التابع , من الأمور الممتعة في C++11 تعابير lambda , التي تسمح بتعريق التابع في وقت استخدامه ,وأحياناً يكون استخدامه مفيداً جداً في توضيح الكود ( بحيث نضع طريقة المقارنة في نفس مكان استدعاء الترتيب )
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    int a[]={9, 7,6,4,   8,2,5, 4,3,1};
    sort(a,a+10,[](const int&a,const int&b){if(a>b)return true;else return false;});
    for(unsigned int i=0;i<10;i++){
        cout<<a[i]<<" ";
    }
    return 0;
}

هنا نصل إلى نهاية المقالة , أرجو أن أكون قد نقلت معظم الطرق التي تستخدم بها المقارنة في هذه الدالة , وأتمنى انه قد أصبح بإمكانك البدء باستخدام التابع std::sort دون أن تجد صعوبة في تحديد الطريقة التي تريد بها ترتيب العناصر .


المصادر :
STL Sort Comparison Function
cpp-reference
كيف تكتب تابع compare
مثال على ترتيب vector يحوي vector


والله ولي التوفيق
==========كاتب المقال : مصطفى 36a2 ==========

عادات تكتسبها من لغات البرمجة

السلام عليكم ،

الموضوع " غير مفيد علمياً " ، لكن لتقطيع الوقت فقط ..

عندما يكون لديك مجموعة زملاء ، وكل زميل " متشرّب ثقافة لغة ما " ، فإنك سترى " العجب العجاب " ، منه من ناحية أسلوبه في كتابة " البرنامج " .

لدينا أربعة زملاء :

- زميل لغته البرمجية الأولى PHP .
- زميل لغته البرمجية الأولىJava .
- زميل لغته البرمجية الأولى Visual Basic .
- زميل لغته البرمجية الأولى++C .


و لدينا هذه المشكلة :
مطلوب كتابة برنامج بسيط لإضافة وحذف و عرض مجموعة Students لدى كل واحد عدة خصائص من ضمنها Courses التي يدرسها ، و طبعاً كل Course له خصائص من الكتب المقترحة لهذا الكورس .

مبرمج PHP:
حسب تجربتي مع مبرمجي PHP ( زملاء ) ، لديهم تقديس عجيب للمصفوفات. كل شيء عندهم يمثلونه بمصفوفة ، لذلك إذا كان زميلك مبرمج PHP قح ، فسترى هذه السطور في برنامجه

<?php
function get_all_student(){
 //dummy data
 $student_id = 1;
 $course_id  = 2;
 $book_id = 3;
 
 $data['students'][$student_id]['student_name'] = $params['student_name'];
 $data['students']['courses'][$course_id]['course_name'] = $params['course_name'];;
 $data['students']['courses'][$course_id]['books'][$book_id]['book_name'] = $params['book_name'];
 
 return $data;
}

?>
صورة

ثقافة المصفوفات متأصلة في مبرمجي PHP ، وبالرغم من أن PHP 5 تدعم OOP بشكل ممتاز ( وقبلها PHP 4 ) ! ولكن كثير منهم لايستخدمها ، لدرجة أن هناك " دوال خاصة " بعملية تحويل الكائن إلى مصفوفة و العكس لإشباع رغبة هذه الطائفة ! هذه الثقافة يبدو أنها بسبب ترسبات قديمة ، كالتالي :

- المصفوفات في PHP ليست مجرد مصفوفات ، بل من الممكن أن تكون map يحتوي على key و value .. لذلك وبسبب هذه الميزة ، تجد مبرمج مبتدئ في php يستخدم maps دون أن يشعر ، بينما لو بحثت عن مبرمجي Java و Cpp ، فلن تجد إلا المبرمجين " الأقوياء " ، من يستخدم maps و في ظروف ضيقة جداً .

- php عندما ظهرت في نسخها الأولى لم تكن تدعم البرمجة الكائنية OOP ، وبالتالي كانوا يقنعون بعضهم أن المصفوفات تقوم بالأمر على أكمل وجه . .

طبعاً لن تشاهد الكثير من OOP في برنامج زميلك ، ولن تشاهد شيء اسمه Exceptions ( عندهم die شيء مقدس ومن أجل اكتشاف الأخطاء قد تشاهد var_dump ) ، ولن تشاهد function overloading لأن php لاتدعمها صورة ، لديهم طرق ملتوية باستخدام functions خاصة .

مبرمج Java :
زميلنا في Java سيحوّل كل شيء إلى Class ليبدو الوضع النهائي هكذا :
class Student{
 private Vector<Course> courses;
 private String name;
 
}

class Course {
 private Vector<Book> books;
 private String name;
}

class Book {
 private String name;
}


public static void main(){

 try{
  Student s = new Student();
  .
  .
  s.add()
 }
 catch(AddStudentException e){
 
 }
 
 catch(SqlDuplicatedKeyException e){
 }
 catch(SqlConnectionException e)
 {
 }
 .
 .
 .
 .
 ..... :(
}

طريقة كتابة الكود " منطقية " ، لأنها في النهاية عبارة عن كائنات ، لكن سيرفع ضغطك شيئان اثنان :

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

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


مبرمج Visual Basic :

هذا لا أطيقه ولا أطيق العمل معه ( معلش :-) ) ، بينما نحن نتشاور حول حل المشكلة تتفاجأ باقتراحاته :

- إنشاء TextBox في Form .
- ربط Form مع Form .
- استخدام الأداة Sql للاتصال بقاعدة البيانات عن طريق Wizard .

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

مبرمج ++C :
تقريباً هو أفضلهم ، فهو قد يستخدم OOP إن احتاج لها ، أو يستخدم Procedures إن رأى أنها كافية ، وقد يستفيد من STL ممزوجة مع Templates ( حيث أسلوب برمجة مختلف) ... لذلك سترتاح معه ، حيث سيختار ما يحتاج لأنه تأثر بثقافة ++C حيث أنها multi-paradigm .
لكن بعد أن نكمل بناء " منطق برنامجنا " مع زميلنا ، سنتفاجأ أنه يستخدم Command-line كواجهة لبرنامجه ، فثقافة GUI هي ما تنقص زميلنا صورة .


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

الدراسات التسويقية, السلاح الخفي (قصص واقعية)

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

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

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

القصة الأولى: شيبسي ولايز

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

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

القصة الثانية: علب العصير (التي تحتوي على شفاطات)

عندما قامت شركات العصير بإنزال هذا المنتج إلى الأسواق فقد اكتسح السوق تماماً وهنا أتكلم عن المصرية وهذا لأكثر من سبب:
1) المنتج فكرته كانت جديدة وجذابة وسهلة
2) استطاع هذا المنتج أن يأخذ قطاع كبير من السوق من شركات المياه الغازية

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

القصة الثالثة: سيارات الفولفو الشهيرة

عندما أُنتجت هذه السيارة اكتسحت الأسواق الأمريكية والأوروبية وشرق أسيا بجدارة فائقة وقد كانت دعاياها التسويقية تعتمد على شعار السيارة "أأمن سيارة في العالم". بعد ذلك قررت شركة السيارة أن تطرحها في أسواق أمريكا اللاتينية بنفس الشعار وخسرت خسائر فادحة! قررت الشركة عمل دراسة تسويقية لسوق أمريكا اللاتينية فوجدوا أن المواطن في الدول النامية لا يحتاج إلى "أأمن سيارة في العالم" بل يحتاج إلى السيارة التي تعيش معه ويستطيع أن يتوارثها جيل بعد جيل. فقررت النزول بحملة تسويقية في تلك البلاد بشعار "السيارة الأكثر دواما في العالم The most durable car in the
world" وبفضل هذه الدراسة وهذه الخطوة اكتسحت السيارة الأسواق اللاتينية بجدارة.


القصة الرابعة: بريل وفيروز ضد بيبسي وكوكاكولا

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

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

القصة الخامسة: الطعم الجديد لكوكاكولا

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

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

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

جزاكم الله خيراً
==========كاتب المقال: "سلفي وأفتخر"============

2015/08/01

الإجراءات الفرعية Procedures في الأسمبلي

السلام عليكم ورحمة الله وبركاته
تتمّة لكود الأستاذ Z3r0n3 في هذه المشاركة
http://arabteam2000-...ر/#entry1107066
أثناء تطبيق استخدام Procedure حدثت معي بعض الأخطاء , ورغبت أن ألخصها للحذر من الوقوع فيها .. ثم رأيت  أنه يمكن استغلالها لكتابة كود بطريقة مبتكرة :)
فهرس المقال :
1- مقدمة عن الإجراء الفرعي وكيفية استخدامه
2- مخاطر الاستخدام الخاطئ للإجراء الفرعي
3- طرق مبتكرة للاستغلال الخاطئ لـ call  و ret

مقدمة عن الإجراءات الفرعية :
الإجراء الفرعي عملياً هو مجرد عملية قفز إلى موضع معين من الكود في الذاكرة وتنفيذ الخطوات حتى الوصول إلى التعليمة ret

ويمكن الاستعاضة عنه تقريباً بتعليمة jmp للقفز إلى بداية الإجراء وتعليمة jmp  أخرى للعودة إلى مكان الاستدعاء .. ولكن استخدام call واستدعاء procedure أسهل بكثير
لا يوجد تعريف للإجراء بشكل صريح داخل الكود , ولكنه يحتوي على تعليمة ret في نهايته غالبا .
مثال على استخدامه :
13DC:0100 mov ah,2
13DC:0102 mov dl,30
13DC:0104 int 21
13DC:0106 call 10D
13DC:0109 int 20
13DC:010B nop
13DC:010C nop
13DC:010D mov dl,31
13DC:010F int 21
13DC:0111 mov dl,32
13DC:0113 int 21
13DC:0115 ret
حيث استخدمنا call نعتبر التعليمات بين العنوان 10D وبين الوصول إلى ret إجراءا فرعيا ..

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

المخاطر تأتي من استخدام الخطوات السابقة بشكل مختلف كما يلي :

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

الخطر الثاني : إذا لم نكتب التعليمة ret فسيتم متابعة تنفيذ الكود المخزّن بعد انتهاء الإجراء ..
ولن نرجع إلى الكود الأصلي (الذي قام بالاستدعاء ) إلى في حال واجهنا ret مصادفة .. وطبعا هذا سيؤدي لتنفيذ تعليمات غير مرغوبة

الخطر الثالث : قد نقوم بإعطاء أي قيمة كوسيط للتعليمة ret مما قد يؤدي بنا إلى تجاوز حجم المكدّس كله والانتقال إلى قمته فوراً ..(مثلا لو كان مؤشر المكدس على FFFC وكان الوسيط 20 مثلاً .. فسننتقل إلى 001E )

والآن : ما رأيك في استخدام المخاطر السابقة في زيادة التحكم بالكود :)

----- يمكننا الآن تغيير قيمة المؤشر ip كما نريد بالطريقة التالية :
1- ندفع القيمة الجديدة في المكدس
2- ننفذ التعليمة ret
سيتم تحميل آخر قيمة في المكدّس إلى المسجّل IP ( مؤشر التعليمات ) مباشرة بفضل ret

انظر إلى المثال التالي :
13DC:0100 mov ax,100
13DC:0103 push ax
13DC:0104 ret
المثال السابق يستخدم push  ثم ret  للقفز .. لاحظ أننا نحدد العنوان الحقيقي عوضا عن تحديد الفرق بين عنوان jmp والعنوان المراد الوصول له في الحالة المعتادة .

كما يمكننا استخدام call للقفز مع التضحية بخانة أو اثنتين من المكدّس ..
انظر المثال التالي :
13DC:0100 mov ah,2
13DC:0102 mov dl,41
13DC:0104 int 21
13DC:0106 call 100
الكود السابق يدخل في حلقة لا نهائية من طباعة A ولكنه يخرج بعد امتلاء المكدس بالعنوان 106 :)

يمكننا مثلاً أن نستعمل قيمة العنوان في عملياتنا وذلك كما يلي :
13DC:0100 pop dx
13DC:0101 mov ah,2
13DC:0103 int 21
13DC:0105 nop
13DC:0106 nop
13DC:0107 nop
13DC:0108 nop
13DC:0109 call 100
جعلنا الكود الموجود في 100 إجراء فرعيا يقوم بسحب الIP من المكدس وطباعة المحرف الموافق له
(انتبه : الكود في حلقة لا نهائية ولكنه مجرد توضيح )

هناك عدد غير محدود من الأفكار التي يمكننا بها استغلال خواص ret و call  الفريدة .. وسأختم بالفكرة التالية :
ما رأيك لو نستدعي الإجراء بشكل عودي بدون ret ولكن مع وضع شرط على مؤشر المكدس :)
13DC:0100 call 103
13DC:0103 mov bx,sp
13DC:0105 cmp bx,FFCC
13DC:0108 jle 114
13DC:010A push bx
13DC:010B mov ah,2
13DC:010D mov dl,31
13DC:010F int 21
13DC:0111 call 103
13DC:0114 int 20
اقرأ الكود السابق وحاول معرفة كيف يمكنك تحديد عدد الاستدعاءات ...

فيما يلي مثال يوضّح استخدام الإجراءات .. (الكود يقوم بعملية ضرب رقمين )
;هذا الكود يوضّح عملية ضرب رقمين من الدخل وطباعتهما باستخدام العمليات
;Procedures
; ويمكنك قراءته وفهمه بسهولة بسبب القاعدة : فرّق تسد :) إنها السيطرة
.model small
.stack 100h
.data
.code;كل استدعاء يدل على وظيفته من اسمه
    call read_number
    call write_Multiplication_Sign
    call read_number
    call write_Equal_Sign
    call find_Result
    call print_Result
    call End_program
    
read_number proc
    mov ax,0100h;نقوم بمقاطعة الدخل لحرف واحد
    int 21h
    sub al,30h;نحول الحرف الى رقم
    mov ah,00h;نصفّر الجزء العلوي من المسجل حتى يختفظ المسجّل بشكل كامل بالقيمة لنتمكن من دفعه للمكدّس
    pop bx;المكدّس يحتفظ بقيمة مؤشّر التعليمات لذلك يجب أن نحافظ عليه
    push ax;نصع نتيجة الإجراء في المكدّس
    push bx;ونعيد مؤشر التعليمات الى المكدّس
    ret;ونعود إلى التعليمة التي يشير لها مؤشر التعليمات
read_number endp

write_Multiplication_Sign proc
    mov ah,02h;تخزين قيمة مقاطعة الخرج لحرف واحد
    mov dl,2Ah;تخزين قيمة الآسكي لإشارة الجمع
    int 21h;طباعة الحرف المخزّن في المسجل السابق
    ret;العودة لحيث يؤشّر مؤشر التعليمات
write_Multiplication_Sign endp

write_Equal_Sign proc;نفس الإجراء السابق ولكن مع تغيير قيمة الآسكي لتطبع إشارة المساواة
    mov ah,02h
    mov dl,3Dh
    int 21h
    ret
write_Equal_Sign endp

find_Result proc
    pop cx;نحتفظ بمؤشر التعليمات
    pop ax;نأخذ الرقم الأول من المكدّس
    pop bx;نأخذ الرقم الثاني من المكدّس
    push cx;نرجع مؤشر التعليمات
    call multiply_Ax_Bx;نستعدي إجراء ضرب هذين المسجّلين
    ret
find_Result endp

multiply_Ax_Bx proc
    mul bx;سنستخدم الضرب العادي
    ret
multiply_Ax_Bx endp

print_Result proc;ax;يقوم بطباعة النتيجة الموجودة في
    mov cl,0Ah;نخزن الرقم 10
    div cl;على 10;ax;نقسم
    ;حسب آلية عمل تعليمة القسمة;ah;وباقي القسمة في;al;ستخزّن نتيجة القسمة في
    ;al;والعشرات في;ah;الآحاد في;
    mov dx,ax;
    mov ah,02h;نضع قيمة تعليمة طباعة حرف
    
    add dh,30h;نحوّل الآحاد من رقم إلى حرف
    add dl,30h;نحول العشرات من رقم إلى حرف
    
    int 21h;نطبع العشرات
    mov dl,dh;
    int 21h;نطبع الآحاد
    ;dl;الطباعة تتم للقيمة الموجودة في
    ret
print_Result endp

End_Program proc;يقوم بتنفيذ مقاطعة الخروج من البرنامج
    mov ah,4Ch
    int 21h
    ret
End_Program endp
end
والآن سأبين أهمية "تفصيص" البرنامج لإجراءات ...
تخيّل أننا نريد تغيير الآلية الأساسية لعملية الضرب التي استخدمناها ..
يمكننا ببساطة كتابة إجراء جديد .. بدلا من القديم .. دون المساس بأجزاء أخرى من الكود .. ودون عناء البحث عن موضع عملية الضرب :
الكود القديم :
multiply_Ax_Bx proc
    mul bx
    ret
multiply_Ax_Bx endp
نريد تغييره إلى  :
multiply_Ax_Bx_2 proc
mov cx,ax
mov ax,0
mab2start:
    cmp bx,0
    je ret_
    dec bx
    add ax,cx
    jmp mab2start
ret_:
    ret
multiply_Ax_Bx_2 endp
(قمت بتغيير الاسم لهدف سأبينه لاحقا)
الكود السابق يقوم بالجمع لتنفيذ الضرب ضمن حلقة :)
ما رأيك لو أحببت القيام بالعملية بطريقة أخرى :
multiply_Ax_Bx_3 proc
mov cx,bx
mov bx,ax
mov ax,0
cmp cx,0
je ret_
mab3start:
    add ax,bx
    loop mab2start
ret_2:
    ret
multiply_Ax_Bx_3 endp
الكود السابق يستخدم loop لإجراء الحلقة ..

الآن لدينا 3 إجرائيات تقوم بنفس الوظيفة ولها نفس الواجهة (أي طريقة إدخال الوسطاء والرجوع بالنتيجة )
ولنا حرية اختيار أي منها داخل برنامجنا ... وهذا يبيّن أهمية تجزئة البرنامج ... فرّق تسُد :)

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

الرابط الأصلي

كود أسمبلي تحويل العدد من الترميز العشري إلى الترميز الثنائي

السلام عليكم ورحمة الله وبركاته
تتمّة لبرنامج الأخ Z3r0n3 في هذه المشاركة ..
البرنامج التالي يقوم بأخذ عدد بالترميز العشري وتحويله إلى الترميز الثنائي ولكن بشكل معكوس
وفق الخوارزمية التالية :
1- نقرأ رقماً من العدد (القراءة من اليسار إلى اليمين )
2- إذا كانت القيمة المقروءة هي نهاية السطر ننتقل للخطوة 4
3- نضرب العدد المحفوظ حتى الآن بـعشرة ونضيف له العدد المقروء وننتقل للخطوة 1
4- نقسم العدد على 2 ونطبع باقي القسمة ونكرر الخطوة حتى يصبح العدد صفراً .

وهذا هو الكود :
.model small
.stack 10h
.data
.code
;bx;هو الذي سيحتفظ بالرقم المدخل
GetTheNumber:
    mov ah,01h;تخزين رقم مقاطعة ادخال الحرف من المستخدم
    int 21h;
    cmp al,2Fh;مقارنة الدخل مع القيمة التي تأتي تحت الصفر مباشرة في جدول الآسكي
        jle end_;القفز إذا كانت القيمة أصغر أو تساوي .. أي أنها ليست رقما
    
    mov ch,al; نخزّن الدخل في مسجل آخر
    
    ;ضرب مسجل القاعدة بعشرة
    mov ax,bx;
    mov cl,0Ah;
    mul cl;
    mov bx,ax;
    
    ;نطرح قيمة الآسكي للرقم صفر .. فتصبح القيمة هي قيمة العدد تماما وليس قيمته في الآسكي
    sub ch,30h
    mov cl,ch
    mov ch,00h
    add bx,cx;نضيف المنزلة الجديدة إلى مسجل القاعدة الذي يخزّن القيمة الكلّية

jmp GetTheNumber
        
        end_:
printNewLine:
        mov ah,02h;قيمة المقاطعة الخاصة بطباعة حرف
        mov dl,0Dh;نضع الحرف المراد طباعته في مسجل البيانات
        int 21h;ننفذ المقاطعة ونطبع القيمة
        mov dl,0Ah
        int 21h
end printNewLine
            print:
                mov dx,bx;نضع قيمة العدد في مسجل البيانات
                and dx,1;نختبر إذا كان يقبل القسمة على اثنين
                add dx,30h;ونضيف له رقم الآسكي الخاص بالصفر فيصبح الآن يحتفظ بقيمة الآسكي للواحد أو الصفر
                int 21h;ننفذ مقاطعة طباعة الحرف الموجود في مسجل البيانات
                shr bx,1;نقسم القيمة المحفوظة على اثنين عن طريق الإزاحة
                cmp bx,0;نقارن هل صار الرقم صفرا
                    je kill;ننهي البرنامج في حال انتهاء الرقم
            jmp print;أو نكرر العملية حتى ينتهي الرقم
                    kill:
                        mov ah,4Ch;تخزين قيمة المقاطعة الخاصة بالخروج من البرنامج
                        int 21h;تنفيذ المقاطعة السابقة
end
 والكود التالي يقوم بطباعة العدد بالشكل الصحيح (وليس معكوس ) باستخدام push و pop  في المكدّس .
.model small
.stack 10h
.data
.code
;bx;هو الذي سيحتفظ بالرقم المدخل
GetTheNumber:
    mov ah,01h;تخزين رقم مقاطعة ادخال الحرف من المستخدم
    int 21h;
    cmp al,2Fh;مقارنة الدخل مع القيمة التي تأتي تحت الصفر مباشرة في جدول الآسكي
        jle end_;القفز إذا كانت القيمة أصغر أو تساوي .. أي أنها ليست رقما
    
    mov ch,al; نخزّن الدخل في مسجل آخر
    
    ;ضرب مسجل القاعدة بعشرة
    mov ax,bx;
    mov cl,0Ah;
    mul cl;
    mov bx,ax;
    
    ;نطرح قيمة الآسكي للرقم صفر .. فتصبح القيمة هي قيمة العدد تماما وليس قيمته في الآسكي
    sub ch,30h
    mov cl,ch
    mov ch,00h
    add bx,cx;نضيف المنزلة الجديدة إلى مسجل القاعدة الذي يخزّن القيمة الكلّية

jmp GetTheNumber
        
        end_:
printNewLine:
        mov ah,02h;قيمة المقاطعة الخاصة بطباعة حرف
        mov dl,0Dh;نضع الحرف المراد طباعته في مسجل البيانات
        int 21h;ننفذ المقاطعة ونطبع القيمة
        mov dl,0Ah
        int 21h
end printNewLine
                mov cl,0;نصفّر المسجل الذي سنستعمله كعدّاد
            reverse:;والآن سنقوم بعكس العدد الثنائي
                    mov dl,bl;\;
                    and dl,1 ;| نضع في المكدّس البت الأقل وزنا من العدد المحفوظ
                    push dx  ;/
                    inc cl;نزيد العداد
                    shr bx,1;نقسم العدد على اثنين
                    cmp bx,0;اذا وصلنا للصفر نذهب للطباعة
                    je print
                    jmp reverse;أو نكرر العملية
                print:
                cmp cl,0;اذا صار العداد صفر اخرج
                je kill
                mov ah,02;تخزين قيمة مقاطعة طباعة الرقم
                pop dx;نخرج من المكدّس البتّات بترتيب معاكس للإدخال
                add dx,30h ;نضيف قيمة الآسكي للصفر لنحصل على قيمة الآسكي للرقم المخزّن
                int 21h ;ننفذ مقاطعة طباعة الرقم وستطبع واحد أو صفر
                dec cl ;ننقص العداد
                jmp print
                    kill:
                        mov ah,4Ch ;مقاطعة انهاء البرنامج
                        int 21h
end
 والله وليّ التوفيق