اذهب إلى المحتوى

بعض البرامج البسيطة بلغة سي C: المصفوفات والعمليات الحسابية


Naser Dakhel

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

برنامج لإيجاد الأعداد الأولية

/*
*
*
* Dumb program that generates prime numbers.
*/
#include <stdio.h>
#include <stdlib.h>

main(){
    int this_number, divisor, not_prime;

    this_number = 3;

    while(this_number < 10000){
            divisor = this_number / 2;
            not_prime = 0;
            while(divisor > 1){
                    if(this_number % divisor == 0){
                            not_prime = 1;
                            divisor = 0;
                    }
                    else
                            divisor = divisor-1;
            }

            if(not_prime == 0)
                    printf("%d is a prime number\n", this_number);
            this_number = this_number + 1;
    }
    exit(EXIT_SUCCESS);
}

[مثال 2.1]

هناك الكثير من الأمور الجديدة المثيرة للاهتمام في هذا البرنامج. أوّلًا، البرنامج يعمل بغباء بعض الشيء، إذ يقسِّم العدد على جميع الأرقام الواقعة في المجال بين نصف قيمته والرقم 2 للتحقّق ما إذا كان أوليًّا أم لا، وإذا كان هناك أي قسمةٍ دون باقٍ فهذا يعني أنه ليس أوليًّا. هذه المرة الأولى التي نستخدم فيها كلًا من العامل % وعامل المساواة المُمثَّل بإشارتَي مساواة ==، إذ يتسبب هذا العامل بكثير من الأخطاء في برنامجك عادةً، نظرًا للخلط بينه وبين عامل الإسناد =، فكن حذرًا.

المشكلة في فحص المساواة هذا وأيّ فحص آخر، هي أن استبدال == بالرمز = هو تعليمة صالحة، ففي الحالة الأولى (استخدام ==) تُوازَن القيمتان وتُفحص حالة المساواة، كما في المثال التالي:

if(a == b)
while (c == d)

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

نلاحظ في مثالنا السابق أيضًا أول استخدام للتعليمة if، إذ تفحص هذه التعليمة تعبيرًا expression موجودًا داخل القوسين على نحوٍ مشابه لتعليمة while، إذ تتطلب جميع التعليمات الشرطية التي تتحكّم بتدفق البرنامج تعبيرًا محتوًى داخل قوسين بعد الكلمة المفتاحية، وفي هذه الحالة الكلمة المفتاحية هي if. تُكتب التعليمة if على النحو التالي:

if(expression)
        statement

if(expression)
        statement
else
        statement

يوضِّح المثال المبين أعلاه أن التعليمة تأتي بنموذجين، إذ يحتوي الأول على الكلمة if والثاني على الكلمة else؛ فإذا كان التعبير صحيحًا وفقًا للنموذج الأول، فستُنفَّذ مجموعة التعليمات الموجودة داخل متن if، أما إذا كان التعبير خاطئًا فلن تُنفّذ؛ بينما تنفّذ التعليمات الموجودة ضمن else وفقًا للنموذج الثاني فقط في حالة كان التعبير خاطئًا (أي تعبير if السابق لها).

يتميز استخدام تعليمة if بمشكلة شائعة، ألقِ نظرةً على المثال التالي وأجِب، هل ستُنفَّذ التعليمة statement-2 أم لا؟

if(1 > 0)
        if(1 < 0)
                statement-1
else
        statement-2

الإجابة هي نعم ستُنفَّذ التعليمة. لا تركّز تفكيرك على مسافة الإزاحة فهي مضللةٌ غالبًا، فقد تكون تعليمة else تابعةً لتعليمة if الأولى أو الثانية بحسب وصف كلٍّ منهما، لذا علينا اللجوء لقاعدةٍ ما لجعل الأمور أوضح. القاعدة ببساطة هي أن else تابعةٌ لتعليمة if الأقرب (التي تسبق else)، والتي لا تحتوي على تعليمة else مسبقًا. لنجعل البرنامج يعمل كما نريد وفق تنسيق مسافات الإزاحة، علينا إنشاء تعليمة مركبة باستخدام الأقواس المعقوصة كما يلي:

if(1 > 0){
        if(1 < 0)
                statement-1
}
else
        statement-2

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

عامل القسمة

يُشار إلى عامل القسمة بالرمز "/"، وعامل باقي القسمة بالرمز "%". تفعل عملية القسمة المتوقّع منها، إلا أن إجراء القسمة على أعداد صحيحة int ستعطي نتيجةً مقربةً باتجاه الحد الأدنى (الصفر)، إذ تعطي العملية 5/2 مثلًا النتيجة 2، وتعطي التعليمة 5/3 النتيجة 1؛ وللحصول على القيمة المقتطعة من الناتج نستخدم عامل باقي القسمة، إذ تعطينا العملية 2%5 النتيجة 1، والعملية 3%5 النتيجة 2. تعتمد إشارة باقي القسمة وناتج القسمة على المقسوم والمقسوم عليه وهي مُعرّفة في المعيار.

مثال عن تنفيذ عملية الدخل

من المفيد أن نكون قادرين على الحصول على الدخل وكتابة برامج قادرة على طباعة وعرض النتائج ضمن قوائم أو جداول في الخرج، والطريقة الأبسط لتنفيذ هذا هي عن طريق استخدام الدالة getchar، وهي الطريقة الوحيدة التي سنناقشها حاليًّا؛ إذ تقرأ هذه الدالة كلّ محرف على حدى من دخل البرنامج وتعيد قيمةً عدديّة int تمثّله، ويمكن استخدام هذه القيمة الممثلة للمحرف في طباعة المحرف ذاته في خرج البرنامج، ويمكن أيضًا موازنة هذا المحرف (القيمة) مع محارف معيّنة أو محارف قُرأت مسبقًا، إلا أن الاستخدام الأكثر منطقية هو موازنة المحرف المُدخل مع محرف معيّن.

لا تُعد موازنة قيمة المحارف ما إذا كانت أصغر أو أكبر خيارًا جيّدًا، إذ لا يوجد أي ضمان بأن قيمة a أصغر من قيمة b، مع أن ذلك الأمر محققٌ في معظم الأنظمة، ولكن الضمان الوحيد هنا الذي يقدمه لك المعيار هو أن القيمة ستكون متتابعة من 0 إلى 9. ألقِ نظرةً على المثال التالي:

#include <stdio.h>
#include <stdlib.h>
main(){
        int ch;

        ch = getchar();
        while(ch != 'a'){
                if(ch != '\n')
                        printf("ch was %c, value %d\n", ch, ch);
                ch = getchar();
        }
        exit(EXIT_SUCCESS);
}

[مثال 3.1]

هناك ملاحظتان جديرتان بالاهتمام، هما:

  • سنجد في نهاية كل سطر دخل المحرف n\ (محرف ثابت)، وهو المحرف ذاته الذي نستخدمه في دالة printf عندما نريد طباعة سطر جديد. نظام الدخل والخرج الخاص بلغة سي غير مبني على مفهوم الأسطر بل على مفهوم المحارف؛ فإذا كنت تريد التفكير بالأمر من منطلق مفهوم الأسطر، فانظر للمحرف n\ على أنه إعلان لنهاية السطر.
  • استخدام c% لطباعة المحرف بواسطة الدالة printf، إذ يسمح لنا هذا الأمر بطباعة المحرف على أنه محرف على الشاشة، بالموازنة مع استخدام d% الذي سيطبع المحرف ذاته ولكن بتمثيله العددي المُستخدم ضمن البرنامج.

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

المصفوفات البسيطة

يكون غالبًا استخدام المصفوفات arrays في لغة سي للمبتدئين بمثابة تحدٍ، إذ أن التصريح عن المصفوفات ليس صعبًا، بالأخص المصفوفات أحادية البعد one-dimensional، ولكن سبب الأخطاء هنا هو بدء الدليل index الخاص بها من الرقم 0. للتصريح عن مصفوفة مؤلفة من 5 أعداد من نوع int، نكتب:

int something[5];

تستخدم لغة سي الأقواس المعقوفة square brackets للتصريح عن المصفوفات، ولا تدعم أيّ مصفوفة لا تقع أدلتها بين 0 وما فوق. العناصر الصالحة في مثالنا هي [something[0 إلى [something[4، و [something[5 غير موجود في المصفوفة وهو عنصر غير صالح.

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

#include <stdio.h>
#include <stdlib.h>
#define ARSIZE  10
main(){
        int ch_arr[ARSIZE],count1;
        int count2, stop, lastchar;

        lastchar = 0;
        stop = 0;
        /*
         * Read characters into array.
         * Stop if end of line, or array full.
         */
        while(stop != 1){
                ch_arr[lastchar] = getchar();
                if(ch_arr[lastchar] == '\n')
                        stop = 1;
                else
                        lastchar = lastchar + 1;
                if(lastchar == ARSIZE)
                        stop = 1;
        }
        lastchar = lastchar-1;

        /*
         * Now the traditional bubble sort.
         */
        count1 = 0;
        while(count1 < lastchar){
                count2 = count1 + 1;
                while(count2 <= lastchar){
                        if(ch_arr[count1] > ch_arr[count2]){
                                /* swap */
                                int temp;
                                temp = ch_arr[count1];
                                ch_arr[count1] = ch_arr[count2];
                                ch_arr[count2] = temp;
                        }
                        count2 = count2 + 1;
                }
                count1 = count1 + 1;
        }

        count1 = 0;
        while(count1 <= lastchar){
                printf("%c\n", ch_arr[count1]);
                count1 = count1 + 1;
        }
        exit(EXIT_SUCCESS);
}

[مثال 4.1]

ستلاحظ استخدام الثابت المُعرّف ARSIZE في كل مكان ضمن المثال السابق بدلًا من كتابة حجم المصفوفة الفعلي بصورةٍ صريحة، ويمكننا بفضل ذلك تغيير العدد الأقصى من المحارف الممكن ترتيبها ضمن هذا البرنامج بتغيير سطرٍ واحدٍ منه وإعادة تصريفه. الانتباه إلى امتلاء المصفوفة هي نقطة غير مشدّد عليها ولكنها هامّة لأمان برنامجنا؛ فإذا نظرت بتمعّن للمثال، ستجد أن البرنامج يتوقف عند تمرير العنصر ذو الدليل ARSIZE-1 للمصفوفة، وذلك لأن أي مصفوفة بحجم N عنصر تحتوي العناصر من 0 إلى N-1 فقط أي ما مجموعه N عنصر.

على عكس بعض اللغات، لا تُعْلمك لغة سي أنك وصلت إلى نهاية المصفوفة، بل تُنتج ذلك بما يعرف باسم التصرف غير المحدد undefined behaviour في البرنامج، وهذا ما يتسبب ببعض الأخطاء الغامضة في برنامجك. يتجنّب المبرمجون الخبراء هذا الخطأ عن طريق اختبار البرنامج المتكرّر للتأكد من عدم حصول ذلك عند تطبيق الخوارزميّة المستخدمة، أو عن طريق فحص القيمة قبل محاولة الحصول عليها من المصفوفة. وتُعد هذه المشكلة من أبرز مصادر أخطاء وقت التشغيل run-time في لغة سي، لقد حذرتك!

خلاصة القول:

  • تبدأ المصفوفات بالدليل 0 دائمًا، ولا يمكنك تجنّب هذا الاصطلاح.
  • تحتوي المصفوفة من الحجم "N" على عناصر من الدليل "0" إلى الدليل "N-1"، والعنصر "N" غير موجود داخل المصفوفة هذه، ومحاولة الوصول إليه هو خطأ فادح.

مصطلحات

هناك نوعان من الأشياء المختلفة في البرامج المكتوبة بلغة سي، أشياء تُستخدم لتخزين القيَم، وأشياء تدعى بالدوال، وبدلًا عن الإشارة للشيئين بصورةٍ منفصلة بعبارة طويلة، نعتقد أنه من الأفضل أن نرمز إليهما بتسميةٍ واحدةٍ عامّة ألا وهي "الكائنات objects"، وسنستخدم هذه التسمية كثيرًا في الفصول القادمة، إذ يتبع الشيئان نفس القواعد إلى حدٍّ ما؛ ولكن يجدر الذكر هنا إلى أن معنى المصطلح هذا مختلف عمّا يقصده المعيار، إذ يُستخدم مصطلح "كائن" في المعيار على نحوٍ خاص لوصف منطقة من الذاكرة المحجوزة لتمثيل قيمة، والدالة مختلفة عن هذا التعريف تمامًا. يؤدي هذا لاستخدام المعيار المصطلحين على نحوٍ منفصل وغالبًا ما ستجد العبارة "… الدوال والكائنات …"؛ ونظرًا لأن هذا الاختلاف لا يؤدي لكثيرٍ من الخلط ويحسِّن قراءة النص في كثيرٍ من الحالات، فسنستمرّ في استخدام المصطلح العامّ "كائن" للدلالة على الدوال والقيَم، وسنستخدم المصطلح "دالة" و"كائن بيانات Data object" عندما نريد التمييز بين الاثنين وفقًا للحالة. لذا، قد تجد اختلافًا بسيطًا في المعنى إن كنت تقرأ المعيار.

ترجمة -وبتصرف- لقسم من الفصل Chapter 1 An Introduction to C من كتاب The C Book.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...