7.1 البرمجة بلغة C/C++
| كتاب لينكس الشامل | >> |
C/C++
اللغة عالية المستوى هي اللغة الأقرب إلى لغة الإنسان وأبعد عما يجري في داخل الحاسوب وهي بشكل عام أقل سرعة من اللغة المنخفضة المستوى التي تعبر عما يجري في داخل الحاسوب لذلك فهي أصعب (لأن ما يجري في داخل الحاسوب معقد) ولغة C لغة من لغات المستوى العالي ولكنها لا تخفي ما يجري في داخل الحاوب فهي بذلك تأخذ من مزايا الطرفين
أقول إن أفضل لغة هي C مع أنها لا تحتوي برمجة موجهة للكائنات
ذلك أن البرمجة الموجهة تبعدك أكثر عما يجري في داخل الحاسوب
فاذا كانت طبيعة برنامجك ليست بحاجة للبرمجة الموجهة فاستعمل C
أما إذا كنت بحاجة للبرمجة الموجهة فاستعمل C++
ودون أن تتعلم لغة جديدة فهي نفس صيغة C ونفس مكتباتها
لغة سي هي كما اللغات التي شرحناها من قبل حساسة لحالة الحرف كبير أم صغير في أسماء المتغيرات والوظائف ... إلخ مثلاً printf تختلف عن PrintF وعن PRINTF . لغة سي من حيث البساطة هي البساطة المفرطة مثل بساطة الرياضيات فهي تحتوي عدد قليل من الكلمات المفتاحية المبنية في اللغة (أي التي هي الجزء الأساسي دون اضافات) التي عليك أن تحفظها وتتعلمها ولكن هذه الكلمات المفتاحية لا يمكنها أن تقوم بالعمليات المعقدة ، بل يتم ذلك من خلال إضافات للغة تسمى مكتبات. هذه المكتبات تقوم بأي شيء يخطر ببالك ؛ هناك مكتبات للتعامل بالأرقام والتعامل مع المستخدم والتعامل مع ملفات الصوت والفيديو وأي شيء يخطر ببالك ، لاستعمال مكتبة يتم ربط برنامجك معها بأحدى طريقتين الأولى الربط المباشر الساكن Static linking أي أن تصبح المكتبة جزء من برنامج (ويصبح برنامج كبير الحجم) وهنا لن يحتاج برنامجك إلى أي ملف اضافي وسيعمل بنفسه stand alone أما الطريقة الأخرى هي طريقة الربط الديناميكي (المتحرك) أي أن يقوم برنامجك بتحميل ملفات اضافية عند تشغيله (في لينكس تكون so وفي ويندوز تكون dll ) تحتوي هذه الملفات المكتبة وتكون مشتركة بينه وبين غيره أي سنوفر المساحة بدل أن يكون لدينا كل برنامج كبير والمكتبة مرتبطة بكل واحد منها (مكررة في كل واحد) ، سيكون لدينا مكتبة واحدة والكثير من البرامج الصغيرة التي تستدعيها
للربط المباشر نحتاج ملفات بامتداد a (في ويندوز lib) وللديناميكي نحتاج ملفات a و so (في ويندوز lib و dll) هذه الملفات تحتوي المكتبة والكود الذي يحملها إلى الذاكرة ولكي نخبر لغة السي ماذا توفر المكتبة من متغيرات ووظائف وغير ذلك علينا أن نحتوي ملف يسمى header لكي تتمكن من معرفة من أي مكتبة تحضر الوظيفة الفلانية
مكتبات السي متوفرة بكم كبير يجب على المبرمج الجيد أن يفكر قبل أن يتعلم مكتبة من أهم ما يجب أن يتوفر في المكتبة التي تستحق أن تتعلمها أن تكون قياسية بمعنى أن تكون صادرة عن منظمة أو تتحكم في معايرها منظمة مثل ANSI (المعهد الأمريكي الوطني للمعاير) والتي تسمى ANSI C التي أصبحت حالياً تسمى ISO C بعد أن أضيف لها الكثير من الخيارات التي تخص لغات غير الإنجيزية والمزيد من الإعدادات الدولية طبعاً ونعرف كلنا أن ISO هي منظمة المعايير الدولية ومن المكتبات التي تصدر عن منظمات مكتبة glibc من منظمة البرامج الحرة www.fsf.org أو gnu في مثل هذه الحالة تضمن أن ماتتعلمه سيظل صالحاً لفترة طويلة أما بالنسبة للمكتبات الصادرة عن شركات فهي غالباً يتحكم بها السوق ومقاييسها هي الربح والربح والربح ففي الإصدارات الجديدة تجد دائماً الكثير من التغييرات بلا طائل مثل أن يصبح المعامل الأول مكان الثاني! وهم بالطبع لن يهتموا بتوثيق المكتبة القديمة وتعليمك الفوارق لأن هذا يستلزم كلفة إضافية بلا طائل ، أي أنك ستضطر لإعادة تعلم الإصدارات الجديدة من الأمثلة على المكتبات التجارية mfc في ويندوز. من الجيد أن تكون المكتبات التي تتعلمها مفتوحة المصدر لأنك إذا قمتت باستدعاء وظيفة معينة من يضمن لك بأنها ستعمل ما طلب منها فقط وأن المعاملات التي تصل إليها (قد تتكون كلمة سر مثلاً) لن ترسل إلى مكان اضافي دون علمك ، يجب أن تكون المكتبات مجانية لأنك لا تستطيع أن تقول لزبائنك اشتروا الإصدار الفلاني منها لأنك بذلك تفقد بعض الزبائن .
اما أن تستخدم برنامج Kdevalop وهناك تأخذ new project أو anjuta أو من سطر الآوامر كتالي لكتابة برنامج C افتح محرر النصوص المفضل لديك ثم خزن الملف بامتداد c ولتحويل الملف لبرنامج تنفيذي اكتب
bash$ gcc filename.c -o filename bash$ ./filename
./
في التوزيعات الحديثة.
وربما تحتاج لإعطاء الإذن بالتنفيذ بتغير الصلاحيات
chmod +x filename
قبل تنفيذ البرنامج ،
ولإضافة دليل للبحث فيه عن ملفات ال header الخاصة بالمكتبات نستخدم الخيار
-I dirname
واستخدام -lfile
لإضافة المكتبات الإضافية
في مثالنا تكون libfile.so
للديناميكية ما تشبه dll في ويندوز،
أو إذا كنت تريد أن يكون ملفك مستقل عن هذه الملفات (يعمل بدونها) أضف الخيار
-static
فيقوم باستيراد الوظائف من ملف libfile.a
ووضعها في برنامج
بشكل ثابت و
ولتعريف اختصار macro نستخدم -d
مثلا
-dMAX_NUMBER=10 -dOUT_PUT_DEBUG_STUB
وهي التي تقابل الكود التالي لغة C
#define MAX_NUMBER 10 #define OUT_PUT_DEBUG_STUB
-w
لتثبيط التحذيرات أو -W
لعرض المزيد من التحذيرات
ولتفعيل التحسين optimizations استعمل
-fexpensive-optimizations -O2
ولتحديد نوع المعالج وطرازه نستخدم
-mcpu=pentium
و
-march=pentium
و
-b TARGET
واذا كان الملف بلغة C++
نستخدم
g++
بدلا من gcc
/* This is a comment */ /* * This is my 1st C program * it's just a hello world * did you notice the comment * */ #include<stdio.h> void main() { printf("Hello world\n"); }
كل ما يوجد بين /*
و */
هو تعليقات للشرح ويجوز أن تكون على أكثر من سطر،
نضع في بداية برنامجنا
#include<filename.h>
مثلا #include<stdio.h>
وهو اعلان منا بأن يضمن الملف stdio.h إلى برنامجنا
وهذا الملف يحتوي على تعريفات وإعلانات عن
الوظائف الموجودة في هذه المكتبة وهي مكتبة الإدخال والإخراج القياسية
standard input/output
وهناك الآلاف (إن لم يكن ملايين) من المكتبات التي يمكننا
اضافتها مثلا math.h
ويمكننا استخدام
#include"filename.h"
ولكن في هذه الحالة لن يبحث عن الملف وإنما يأخذه من الدليل الحالي
ثم يأتي تعريف الوظيفة الرئيسية main (وهذا الاسم ليس من عندي بل هو محجوز)
يأتي بعدها الحاصرة التي تحدد وحدة من الكود
وما بداخلها هو الذي ينفذ عند تشغيل البرنامج
وتعني كلمة void بدون أي أنه لا يعيد قيمة
ولكن البرامج الجيدة تعيد صفر إذا نجحت أو تعيد رقم غير الصفر
اذا فشلت فيصبح البرنامج
/* Hello World ver 2.0 Hmmmm */ #include<stdio.h> int main() { printf("Hello world\n"); return 0; }
لاحظ return 0;
تعني أعد الرقم 0 للمنادي (وهو نظام التشغيل) أي أن البرنامج نجح
وهي ترافقت مع int main()
أي أن برنامجنا سيعيد
عدد صحيح integer
أما سطر printf
فهو المسؤول عن طباعة Hello world وسنشرحه لاحقاً
لاحظ بعد كل أمر يجب أن نضع ;
وهي تسمح لنا بوضع أكثر من أمر على سطر واحد أو تمديد الأمر على أكثر من سطر
/* Hello World ver 2.1 Hmmmm */ #include<stdio.h> int main() { printf("Hello world\n"); return 0; }
/* Hello World ver 2.2 Hahaha */ #include<stdio.h> int main() { printf ( "Hello world\n" ); return 0; }
ويمكن أن تأتي وحدة داخل أخرى وليكون برنامجنا مقروءاً أكثر نستخدم أسلوب أزاحة وحصر indenting موحد في كل البرنامج مثل طريقة K&R (مخترعي لغة ال C) أن يكون بدأية الحاصرة على نفس السطر ومتنها ال body تزيد عنها ب tab وتنتهي على نفس مستوى كما في هذا الكود لاحظ من السهولة أن تميز إلى أي وحدة block يعود كل سطر طبعا لا أتوقع أن تفهم هذا الكود الآن
/* Make your code readable */ #include<stdio.h> #include<math.h> float round(float x) { return floor(x+0.5); } int main() { /* we are in a 1st level block */ int i,j; float x=1.7; printf("Do NOT be afraid of C\n"); printf("This code tells you how C look like\n"); printf("The approx of 1.7 is %f\n" ,round(x)); for (i=3;i<=10;++i) { printf("I'm counting to %d\n",i); /* we are in a 2nd level block */ for (j=1;j<=i;++j) { /* we are in a 3rd level block */ printf("%d,",j); } printf(" and here we go\n"); } return 0; }
أكثر خطأ شيوعا هو أن تنسى الفاصلة المنقوطة
لا يوجد ; بعد كل ما يبدأ ب # مثل #include
ولا بعد الحاصرة {} التي تحدد وحدة block من الكود
إن مصنف gnu للغة C يقبل تعليقات C++
ان لم تطلب منه أن يطبق المعايير بحذافيها
ولكن المعايير تمنع ذلك
على عكس اللغات التفسيرية يجب أن تحدد نوع المتغير والأنواع الأساسية هي
char | هو رمز واحد "محرف" يحجز واحد بايت فقط |
int | رقم صحيح عادي يحجز 4 بايت |
float | عدد نسبي (يمكن أن يحتوي كسور) وهو أحادي الدقة يحجز 4 بايت |
short | رقم صحيح بنصف الطول يحجز 2 بايت |
long | رقم صحيح مضاعف الطول يحجز 8 بايت |
double | عدد نسبي (يمكن أن يحتوي كسور) وهو مضاعف الدقة يحجز 8 بايت |
long double | عدد نسبي (يمكن أن يحتوي كسور) وهو مضاعف الدقة يحجز 10 بايت |
unsigned int
و
unsigned char
وهذه الأخيرة تمثل الرموز بشيفرة ASCII
وتعطيها أرقام من صفر إلى 255 أما بدون unsigned ستكون من -128
إلى 127
،
ويمكنك أن تضع حرف u بعد الرقم لتدل على أنه غير سالب وهذا مهم
عند التعامل بالنظام الست-عشري
مثلا 0xffffu
تعني 65535
أما 0xffff
تعني -1
ولدلالة على أن الرقم كبير يمكن أن نضع l أي long
مثلا 0x10000ul
وللعلم مدى ال short هو من -32768
إلى 32767
في مصنفات C ذات ال 16-بت مثل TurboC و BorlandC 3 و مصنفات مايكروسوفت للدوس وويندوز 3.1 وما قبلها يحجز int ومشتقاته من الذاكرة النصف وتعبر عن ما تعبر عنه نصفها
ويكون حجز متغير من نوع معين بذكر النوع ثم اسم المتغير مثلاً
int i;
ويمكن تعريف أكثر من واحد دفعة واحدة بحيث نفصل بينها بفاصلة مثلاً
int i,j,k;
.
وعند حجز متغير فإنه يتخذ قيمة عشوائية (بتعبير أدق غير معروفة، ولا يمكن التنبؤ بها) وليس بالضرورة صفر لأن سي لا تضيع وقت
الجهاز في تصفيرها انظر لمخرجات
int i; printf("%d",i);
ويمكنك وضع قيمة استهلالية فيها باستعمال "="
مثلاً int i=1,j,k=15;
هنا i قيمتها واحد و k قيمتها
15 أما j فهي غير معروفة
وكما لاحظت نستخدم نفس أسلوب اللغات السابقة
أي الرقم الذي يبدأ بصفر هو بالنظام الثماني والذي يبدأ
ب 0x
يكون بالنظام الست-عشري
وغير ذلك بالعشري
و printf أول معامل لها يكون الصيغة
ويطبع ما بداخله كما هو إلا ما يبدأ ب % ويتلوه نوع
فيتم تعويضه من مايقابه من المعاملات التالية وهي
تستخدم نفس الصيغة التي شرحاها في perl عد
إلى الجدول هناك
/* numbers */ #include<stdio.h> int main() { int i,j,k; i=010; /* i=010(oct)=8(dec)*/ j=0x10; /* j=0x10(hex)=16(dec)*/ k=i+j; /* k=24 */ printf("i=%d\n",i); printf("j=%d\n",j); printf("i+j=%d(dec)=%x(hex)=%o(oct)\n",k,k,k); return 0; }
int a=5,b=11,c=10; int i,j=10,k; /* i,j ليس لهما قيمة محددة*/
يمكن للمبرمج أن يفرض قيوداً على نفسه بأن لا يغيير من قيمة
المتغير الأصلية باستعمال const
قبل نوع لمتغير كأن يقول
const float pi=22/7;
هناك إذا تم تغيير قيمة pi فإن المصنف سيولد خطأ
وتكون مفيدة إذا كان أكثر من خص يعملون على البرنامج
أو لتعريف متغيرات تمثل الثوابت العلمية مثل Pi و تسارع السقوط الحر.
من أكثر ما يعقد المبرمجين الجدد على سي هي المؤشرات pointers
وهي علامات تدل على موقع المتغير في الذاكرة يتم تعريفها
بوضع نجمة * قبل اسم المتغير مثلاً
int *j;
هنا j هو متغير من نوع مؤشر لعدد صحيح
والحقيقة أن قوة السي تنبع من هذه المؤشرات
التي تعطيك وصول مباشر للذاكرة.
في مثالنا j يشير إلى العدم اللا شيء ويسمى NULL pointer
أو المؤشر الخالي ومن الخطأ الكتابة في ذلك العنوان من الذاكرة لأنه
لا يشير إلى ذاكرة.
ما يزيد الطين بلة هو المتغير من نوع مؤشر بلا نوع
void *ptr;
وهنا هذا المؤشر الخالي بلا نوع يشير إلى لا شيء!
و NULL هو مؤشر بلا نوع قيمته صفر معرف في معظم المكتبات القياسية .
إن المؤشرات مهما كانت معقدة فهي مفيدة
وهي فلسفة سي! لا تتعقد منها لأننا نشرحها لاحقاً
هناك نوع أضيف حديثاً في C++
وألصق ب C وهو الاسم المستعار alias
وتم تعريفه بوضع & قبل اسم المتغير كما في
int i,&j=i;
هنا j هي اسم آخر ل i لذا نسمي j مرجع reference ل i،
أي أن أي تغيير يحصل في الأولى يحصل في الثانية
وليس له أي فائدة مباشرة ولكن فائدته تكمن في الوظائف
،وللعلم مكتبة C القياسية تستعمل المؤشرات ولا تستعمله،
سنتحدث عنه لاحقاً.
هناك المزيد من أنواع المتغيرات يمكن أن يعرفها المستخدم
كما يشاء بحيث تكون تركيب من هذه الأنواع مثلاً المتجه الفراغي هو (x,y,z)
هو عبارة عن ثلاث أرقام نسبية ، الرقم المركب (a+ai)
هو عبارة عن رقمين حقيقيين a و b
لعمل مثل هذه المتغيرات نستخدم struct
العمليات الحسابية هي
الجمع +
والطرح -
ناتجهما هو من نفس نوع المعاملات أي جمع صحيحين يعطي صحيح
وهكذا
والضرب *
والقسمة /
وهي ضعف نوع المعاملات أي ضرب صحيحين هو صحيح مضاعف
int * int=long int
ونلاحظ هنا أن عملية القسمة ليست تماماً كما في الرياضيات
في الرياضيات قسمة صحيح على صحيح ليس بالضروري
أن تعطي صحيح مثلاً 1/2=0.5
ولكنها في لغة سي (وغيرها) تعطي عدداً صحيحاً
وهو عبارة عن ناتج القسمة النسبي مع إلغاء الجزء غير الصحيح
أي أن 1/2=0
و 9/10=0
و -9/10=0
و -11/10=-1
وإذا لم ترد أن تكون صحيحة يجب أن تستعمل أرقام نسبية
9.0/10.0=0.9
.
نعم 9.0
تختلف عن 9
!! الأولى نسبية و الثانية صحيحة .
وإذا اختلفت أنواع المعاملات مثلاً جمع صحيح مع نسبي أو قسمتهما
يكون الناتج من النوع الأعم في المثال نسبي.
وباقي القسمة%
وهو باقي قسمة الأعداد الصحيحة أي لا نطبقه على float
وهنا باقي القسمة ليس كما يعرفه الرياضيون في نظرية الأعداد
(تشترط نظرية الأعداد في باقي قسمة a على b أن
يكون أكبر من أو يساوي صفر وأقل من القيمة المطلقة ل b
أي أن الباقي دائماً غير سالب )
بل هو كما في كل لغات البرمجة باقي قسمة القيمة المطلقة a
على القيمة المطلقة ل b مضروباً في اشارة
ناتج القسمة مثال
printf("%d",21%5);
تعطينا 1 لأن 21 على 5 هي 4 والباقي واحد
أما
printf("%d",-21%5);
و
printf("%d",21%-5);
تعطينا سالب واحد ولكنها من ناحية رياضية 4 لأن ناتج القسمة هو -6 أي أن
-21=-6*5+4
وليس
-21=-4*5-1
كما هو الحال في الحاسوب
والعمليات الثنائية
و عملية "و" الثنائية |
|
|
|
من العميات الثنائية هي عميات الإزاحة لليسار مثلاً
23dec<<2dec=
10111bin << 2dec = 1011100bin = 92dec
في سي نعبر عن ذلك
printf("%d", 23 << 2);
أو لليمين
23dec>>2dec=
10111bin >> 2dec = 101bin = 5dec
في سي نعبر عن ذلك
printf("%d", 23 >> 2);
في الحالتين يحذف الفائض
عملية الإزاحة لليسار (للأعداد الصحيحة) بمقدار واحد هي الضرب في 2
والإزاحة لليسار بمقدار 3 هي الضرب في 2 ثلاث مرات
i<<3=(((i*2)*2)*2)
أي الضرب في ثمانية
وليس في ستة فك الأقواس وانظر،
وعملية الإزاحة لليمين هي قسمة .
وأهم استخدام لها هو توفير الوقت لأن الإزاحة تأخذ وقت أقل من
الضرب والقسمة.
من العمليات على المتغيرات لدينا عملية عنوان المتغير &
مثلاً int i,*j=&i
هنا &i
تعني عنوان i في الذاكرة بمعنى أن المؤشر
j يشير إلى عنوان i في الذاكرة
وإذا لم يكن مؤر دون نوع void pointer يمكن أن نقوم بعمليات
جمع وطرح عليه مثلاً int *ptr=&i;
معنى ذلك أن ptr هو مؤشر من نوع صحيح
يشير إلى عنوان i فإذا قلنا ptr=ptr+1;
أصبح يشير إلى العدد الصحيح الذي يلي i ولأن العدد الصحيح
يحجز 4 بايت فهي أصبح تشير إلى 4 بايت بعد موقعها القديم.
وعملية تحويل المؤشر إلى القيمة المخزنة في العنوان الذي يشيرر إليه
وتكون بوضع "*" قبل المؤشر مثلاً
int i=5,*ptr=&i; printf("%d",*ptr);
ستعطينا 5 لأنها ptr يشير إلى عنوان i الذي به القيمة 5
ويمكن استخدامه على الطرف الآخر من المساواة مثلاً
int i=5,*ptr=&i; *ptr=21; printf("%d",i);
تكتب 21 لأننا غيرنا قيمة ما يشير له العنوان ptr في الذاكرة
وهو i إلى 21 أي أصبحت تلك هي قيمة i ،
ولهما أهمية في التعامل مع المنظومات لاحقاً.
لاتخلط بين تعريف متغير ليكون مؤشراً int *ptr;
وبين عملية تحويل المؤشر إلى قيمة i=*ptr;
الأولى تكون في جملة تعريف (على اليسار من المساواة فيها إن وجدت) والثانية
تكون في أي جملة عادية
لاتخلط بين تعريف متغير ليكون اسماً مستعاراً int &j=i;
وبين عملية أخذ عنوان المؤشر ptr=&i;
الأولى تكون في جملة تعريف (على اليسار من المساواة فيها إن وجدت) والثانية
تكون في أي جملة عادية
عميات التحويل هي عميات منها ما هو ضمني "implict" أي تلقائي ومنها ما يمكنك أن
تطلبه صراحة "explict" بنفسك
لقد تحدثنا عن بعضها عندما قلنا أن جمع صحيح مع نسبي
يعطي نسبي ولكن حتى يتمكن الحاسوب من عمل ذلك يجب أن يحول الصحيح
لنسبي لأن تعليمات المعالج محدودة
كما ذكرنا يقوم مصنف سي بالعملية تلقائياً
ولكن إذا أردت أن تقوم بها أو تحددها
يمكنك استعمال عمليات التحويل الصريحة وهي بسيطة جداً اذكر اسم النوع الذي تريد التحويل له بين قوسين مثلاً
x=(float)i;
أي حول i إلى نسبي وضعه في x
مثال آخر x=(float)i/(float)j;
تعني قسمة i على j قسمة نسبية ووضع الجواب في x
مثال أخير
int i=3.6;
تعني أن قيمة i هي 3 لأن هذه العبارة تحتوي تحويل ضمني
عملية الإحلال وهي ما نسميه تجاوزاً مساواة
وهي ليست المساواة الرياضية فكما لاحظت استخدمنا عبارات مثل
i=i+1;
فإذا طرحنا i من الطرفين حصلنا على أن 0=1
هذه ليست هي العملية بل هي عملية احلال أي نقل نسخة من الطرف الأيمن
في المكان الذي يحدده الطرف الأيسر
الطرف الأيمن يمكن أن يكون أي تعبير والأيسر هو اسم متغير
ولا يجوز أن يكون تعبير مثلاً من الخطأ أن تقول
i+1=i;
لأنها تعني ضع قيمة i مثلاً 5 في i+1 ولكن كيف!
أيضا لاحظ معنا هذه العبارة
int i=4,j=6; i=j; j=i;
سنحصل على أن i و j يحملون القيمة 6 وليس تبادل قيمهما
تتبع العملية خطوة خطوة ، ضع j في i تصبح قيمة
i 6 وتبقى قيمة j هي 6 ثم ضع قيمة i وهي 6 في j
تحصل على 6 في الأولى والثانية !!
ربما من غير المألوف لك أن تعرف أن عملية الإحلال "=" تعيد نتيجة كما تفعل
عملية الجمع بأن تعيد ناتج الجمع
ونتيجة الإحلال هي قيمة ما على اليمين
وهي عملية تنفذ من اليمين لليسار على عكس ما هو مألوف
مثلاً
i=j=k=17;
هذه تعني
i=( j= (k=17) ) ;
لنوضحها أكثر
ننفذ أقصى اليمين وهي تعني ضع 17 في k ناتج هذه العملية هو 17
نضعه في j ونضع الناتج في i
بل وأكثر من ذلك انظر هذا المثال
i=(j=17)+1 ;
هذه تعني ضع 17 في j و 18 في i هل عرفت لماذا يمكن لسي أن تكون طلاسم.
توفر لنا سي اسلوب العميات المختصرة
فإذا أردت أن تقوم بعملية ثنائية (تأخذ معاملين مثل الجمع) ووضع الناتج
في المعامل الأول كأن تجمع 5 للمتغير i وتضع الجواب في i
ببساطة اكتب اسم المتغير في مثالنا "i"
ثم العملية التي تريد في مثالنا "+"
ثم عملية الإحلال "=" ثم المعامل الثاني
يصبح مثالنا i+=5;
وهذه تصلح لكل العميات الثنائية التي تحدثنا عنها
مثلاً
i*=2;
أو i<<=1;
تضرب i في 2 تضع لجواب في i
وإذا كان المعامل الثاني هو وواحد يمكننا أن نستعمل ألوب أكثر اختصاراً
وهو يقوم على كتابة العملية مرتان على يسار أو يمين المتغير
مثلاً
++i;
أو i++;
وكلاهما تعني زيادة i بمقدار واحد أي نفس
i=i+1;
والفرق بينهما هو أن الأولى زيادة قبلية والثانية بعدية
أي في الأولى يتم تنفيذها قبل باقي العمليات في الجمل المركبة
مثلاً
i=++j;
تعني اجمع ل j واحد أولاً ثم ضع الناتج بعد الجمع في i
أما الأخرى يأخذ القيمة ثم ينفذ الجمع بعد كل العمليات الأخرى
مثلاً i=j++;
تعني ضع قيمة j في i ثم اجمع ل j واحد.
وللعلم القبلية أسرع من البعدية
الهدف الأساسي من العمليات المختصرة ليس توفير بعض النقرات على لوحة
المفاتيح ولا تقليل من حجم برنامج سي بل الهدف هو تسريع البرنامج
لأن المعالج يقوم بتلك العمليات بشكل أسرع
مثلاً عملية i=i+1;
هي بلغة التجميع mov AX,i; mov BX,1; add AX,BX; mov i,AX;
ولكن ++i;
هي mov AX,i; inc AX; mov i,AX;
من بين الأشياء التي تجعل عملك أسهل هو استخدام الاختصارات Macros وهي عمليات يقوم بها معالج سي الأولي C pre-processor أو cpp للاختصار أي أنها لا تنفذ ولا تأخذ من وقت المعالج وهي تعني أن تقول له استبدل كل كلمة كذا بكذا في نص البرنامج ويفضل أن تكون الاختصارات بأحرف كبيره انظر هذه الأمثلة
#define IS_IT_OK #define PRINT printf #define MAX_NUMBER (0xFF) #define PI (22.0/7.0) #define AUTHOR "Al-Sadi" #define ADD(x,y) ((x) + (y))
#define ADD(x,y) x + y int i=ADD(5,6)*ADD(2,3);
int i=5 + 6 * 2 + 3;
الذي يعني أن قيمة i هي 20 ولكن من الواضح أن المبرمج قصد 55
ولكن ما حدث هو أن عدم وجود أقواس خارجية أدت إلى ضرب 6 في 2 بدلاً من ضرب ناتجي الجمع !!
ويمكن ابطال مفعول الاختصار باستعمال #undef MY_MACRO
حيث MY_MACRO
هو الاختصار الذي تريد تثبيطه
ويمكن القيام ببعض العمليات في فترة المعالجة الأولية
كأن تقول
#if anything && defined(MY_MACRO) || anything int i=1,j=1; do_some_thing(); other_thing(); #elseif something do_other_thing(); #else int i=0,j=0; do_some_thing(); other_thing(); #endif
#ifdef MY_MACRO
لمعرفة فيما إذا كان الإختصار معرف ويمكن الاستعاظة عنه باستعمال #if defined(MY_MACRO)
ولتعرف إذا لم يكن معرف استعمال
#ifndef MY_MACRO
وكل هذه عبارة عن أوامر للمعالج الأولي cpp وليست للتنفيذ
أي لا تحتاج إلى فاصلة منقوطة ولا تحول إلى كود بلغة الآلة
شأنها شأن #include<>
تكمن فائدة هذه الاختصارات في تجنب الكثير من الكتابة وتفيد في
جعل البرنامج يدور حور الاختلافات بين الأنظمة والمصنفات
لأن كل منها تعرف بعض الإختصارات التي تدل عليها
من الشائع استخدام الفاصلة "," مع الاختصارات وهي تعمل
نفس عمل ";" ولكنها لا تنهي الجملة وتعتبر قيمتها
بقيمة آخر حد فيها مثلاً
(++i,++j,i+j)
تجمع واحد ل i وواحد j ثم تجمع i مع j
وتعيد ناتج الجمع أي أن
i=1; j=2; printf("%d",(++i,++j,i+j));
ستطبع 5
وفائدتها كما رأيت أنها تعتبر عبارة واحدة فقط أي أنك تضعها مكان تعبير واحد
هذا مثال آخر
#define foobar(x,y) \ (++(x),++(y),(x)+(y))
بطريقة مشابهة لطريقة الكتابة في printf
لإرسال المخرجات نستخدم scanf لأخذ مدخلات من المستخدم
مثلاً scanf("%d",&i);
تعطي محث ليدخل المستخدم قيمة i.
وصيغة scanf هي سلسلة نصية تحتوي الهيئة بنفس طريقة printf
ثم مجموعة من المؤشرات (عناوين في الذاكرة) حيث توضع المدخلات
ويمكن تحصيلها باستعمال عملية "&" أو يمكن أن تكون مؤشر
انظر هذا المثال
/* Example: User Input 1 */ #include<stdio.h> int main() { int i=0,j=0; printf("Enter two numbers: "); scanf("%d %d",&i,&j); printf("\nyou have entered [%d][%d]\n",i,j); return 0; }
scanf
/* Example: User Input 2 */ #include<stdio.h> int main() { int d,m,y; printf("Enter date (dd/mm/yyyy): "); scanf("%2d/%2d/%4d",&d,&m,&y); printf("you have entered %02d/%02d/%04d",d,m,y); return 0; }
/* Example: User Input with pointers */ /* THIS PROGRAM HAS AN ERROR */ #include<stdio.h> int main() { int i=0,j=0; int *ptr1,*ptr2; printf("Enter two numbers: "); scanf("%d %d",ptr1,ptr2); printf("\nyou have entered [%d][%d]\n",*ptr1,*ptr2); return 0; }
/* Example: User Input with pointers */ #include<stdio.h> int main() { int i=0,j=0; int *ptr1=&i,*ptr2=&j; printf("Enter two numbers: "); scanf("%d %d",ptr1,ptr2); printf("\nyou have entered [%d][%d]\n",*ptr1,*ptr2); return 0; }
كما لاحظنا يبدأ تنفيذ البرنامج من main سطراً سطراً
ولكن إذا كان لدينا أكثر من حالة وأردنا أن ننفذ شيئاً مختلفاً
في كل حالة نستخدم جملة الشرط "if" وهي على الصيغة
if (COND) [STATMENT1]; [ else [STATMENT2]; ]
حيث الأقواس هنا "()" اجبارية ولا يمكن ازالتها
أما الأقواس المربعة هي للتوضيح فقط ووضعها خطأ ، وما بداخلها اختياري
و COND هو الشرط الذي نريده فإذا حدث نفذ STATMENT1 وإلا STATMENT2
مثلاً
if (j!=0) k=i/j;
تعني إذا تحقق الشرط j!=0
ويعني أن j ليست صفراً كما سنتعلم؛ عندها نفذ عملية القسمة
ثم يتابع تنفيذ البرنامج أما إذا لم يتحقق فإنه سيتابع تنفيذ البرنامج.
يمكن أن نأخذ شرط بحالتين مثلاً
if (j!=0) k=i/j; else k=0;
هنا في حال عدم تحقق الشرط ينفذ
k=0;
ويمكن استبدال الجملة STATMENT1;
أو STATMENT2;
بوحدة من الجمل موضوعة ضمن حاصرات {}
مثلاً
if (j!=0) { k=i/j; printf("k=%d\n",k);} else {k=0; j++; printf("Can't devide by zero!!");}
وتصبح هذه الحاصرات إجبارية في حال كنا نريد استخدام
أكثر من جملة أما في حالة الجملة الواحدة يجوز
استعمال أي الطريقتين
يمكنك عمل شرط مركب باستعمال أكثر من جملة "if" مثلاً
/* Example: User Input with pointers */ #include<stdio.h> int main() { int i=0,j=0,r,op=0; printf("Enter two numbers: "); scanf("%d %d",&i,&j); printf("\nyou have entered [%d][%d]\n",i,j); printf("\t1) Addition(+)\n\t2) Subtraction(-)\n"); printf("Enter the opertion number: "); scanf("%d",&op); if (op==1) { printf ("\nyou select (1) addition\n"); r=i+j; } else if (op==2) { printf ("\nyou select (2) subtraction\n"); r=i-j; } else { printf ("\nwhat do you mean %d , only enter 1 or 2.\ntry again\n",op); return 1; } printf ("the result is\n",r); return 0; }
الشرط نفسه أو ما يسمى العمليات المنطقية فهي أي عملية تحسب على أنها صواب TRUE أي أن الشرط تحقق إذا كانت قيمتها غير صفرية وأما إذا كانت قيمته صفر فتحسب خطأ FALSE وينفذ جملة else انظر المثال التالي
/* Example: Conditions 1 */ #include<stdio.h> int main() { if (0) { printf ("This text will NEVER appear\n"); } else { printf ("This text will ALWAYS appear\n"); } return 0; }
من تلك العمليات عمليات المقارنة مثلاً
فحص المساواة "=="
و فحص عدم المساواة "!="
وأكبر من ">"
وأصغر من "<"
أكبر من أو يساوي "<="
و أصغر من أو يساوي ">="
ويمكن استعمالها مع العمليات المنطقية الأساسية
وهي "و المنطقية" "&&"
و "أوالمنطقية" "||"
وكلها عمليات ثنائية بينية (تأتي وسط معاملين) و النفي المنطقي "!"
وهي عملية سابقة احادية تأتي قبل ما تنفيه.
هذا مثال بسيط :
/* Example: Conditions 2 */ #include<stdio.h> int main() { if (!0) { printf ("This text will ALWAYS appear\n"); } else { printf ("This text will NEVER appear\n"); } return 0; }
a<b<c OR c<b<a
/* Example: Conditions 3 */ #include<stdio.h> int main() { int a,b,c; printf ("Enter 3 Numbers: \n"); scanf("%d %d %d",&a,&b,&c); if ( ( a<b && b>c ) || ( c<b && b>a ) ) { printf ("%d is between %d and %d \n",b,a,c); } else { printf ("%d is NOT between %d and %d \n",b,a,c); } return 0; }
يمكن اختصار الرط الذي يكون الهدف منه الحصول على نتيجة كما في هذا المثال
k= (j!=0)? (i/j) : (i/(j+1)) ;
والذي يعني اجعل قيمة k=(i/j)
إذا كانت j لا تساوي الصفر وإلا اجعل القيمة k=i/(j+1)
والصيغة العامة هي
(COND)? (EXPR1) : (EXPR2) ;
حيث COND
هو الشرط و EXPR1
هو القيمة التي يأخذها إذا تحقق وإلا يأخذ القيمة
EXPR2
ويجب أن نلاحظ أن EXPR
هي تعبير له قيمة وليس
أمر أي يمكن أن تكون على الطرف الأيمن من إشارة "="
إذا كان لديك الكثير من الحالات المنفردة وكنت تجد استعمال if مملاً يمكنك استعمال switch وهي على الصيغة
switch(EXPR) { case VAL1: do_something1(); do_something2(); do_something3(); do_something4(); break; case VAL2: do_other_thing1(); case VAL3: do_other_thing2(); break; ... default: do_default_thing(); }
EXPR
هو التعبير الذي تريد أن تتبع قيمته
فإذا كانت VAL1
ينفذ ما بعد النقطتين الرأسيتين
حتى يصل إلى كلمة break;
وإذا لاحظت في الحالة الثانية VAL2
لم نكتب له break;
وأول حدوث لها هو بعد VAL3
فإذا حدثت VAL2
نفذ ما هو موجود بعد VAL2
و VAL3
حتى أول break;
وإذا لم تحدث أي من الحالات المذكورة نفذ ما هو بعد
default:
وهنا break;
اختيارية لأنها آخر واحدة
انظر هذا المثال
/* Example: Switch */ #include<stdio.h> int main() { int i; float x=0,y=0,r=0; printf ("Enter two numbers: \n"); scanf("%d %d",&x,&y); printf("\n\t%s\n\t%s\n\t%s\n\t%s\nEnter the number of the operation", "1) Addition '+'", "2) Subtraction '-'", "3) Mult '*'", "4) Division '/'" ); scanf("%d",&i); switch(i) { case 1: r=x+y; break; case 2: r=x-y; break; case 3: r=x*y; break; case 4: if (y==0) { r=x/y; } else { printf("can't divide by zero.\a\n"); r=0; } break; default: printf("What do you mean by %d.\a\n",i); } printf("result %f\n",r); return 0; }
الجدول سيكمل لاحقاً
الأعلى أولوية | ||
---|---|---|
() [] | الأقواس وعناصر المنظومة | ltr |
++ -- unary+ unary- ! (type) | الزيادة والنقصان (القبلية والبعدية) وتغيير الإشارة و النفي المنطقي وتحديد النوع | rtl |
* / % | الضرب والقسمة والباقي | ltr |
+ - | الجمع والطرح | ltr |
<< >> | السحب ولإضفة في سي++ | ltr |
< <= > >= | المتبايات أقل أقل أو يساوي أكبر أكبر أو يساوي | ltr |
== != | فحص المساواة وعدمها | ltr |
&& | "و" المنطقية | ltr |
|| | "أو" المنطقية | ltr |
()?():() | الشرط | rtl |
= += -= *= /= %= | الإحلال والإحلال المختصر | rtl |
, | الفاصلة تعيد آخر ناتج | ltr |
الأقل أولوية |
للقيام بعمل أكثر من مرة يمكننا أن نستخدم الحلقات التكرارية
باستعمال for
و while
و do..while
وفي كل اللغات يكون هناك واحدة أشمل من واحدة ولكن في سي يمكن لأي منها أن تقوم بدور الأخرى
ولكن تحتلف في الصيغة فقط
الشكل التقليدي لعبارة for
هو
for(i=0;i<times;++i) { do_something(i); }
times
من المرات وتأخذ
i القيم من صفر إلى times-1
وإذا أردنا العد من رقم from
إلى رقم to
يمكن أن نستخدم
for(i=from;i<=to;++i) { do_something(i); }
يجب أن تنتبه إلى أن INIT
تابعة لوحدة for أي أن البرنامج التالي خطأ
#include<stdio.h> void main() { for(int i=0;i<5;++i) { printf("%d ",i); } printf("%d ",i); }
printf("%d ",i);
يحاول الوصول لمتغير خارج حدوده ،ليس هذا فحسب بل أن وحدة for تم الخروج منها
وتحرير كل المتغيرات التي تم حجزها داخلها
وهذا الكلام ينطبق على عمل أي وحدات أخرى . المتغيرات التي تنتمي لوحدة تسمى محلية local variables وهي لا ترى خارجها
لأنها تحجز في كل مرة ندخل الوحدة وتحرر في كل مرة نخرج منها
ولكنها ترى في الوحدات التي تتفرع من تلك الوحدة إذا لم تحجز الوحدة الفرعية متغيراً بنفس الاسم ،
وإذا أردت أن يراها البرنامج كاملاً عليك أن تجعلها متغيرات عامة global variables
ونضعها خارج كل الوحدات بما ما فيها حدود ال main كما يلي
// code-blocks.cpp: use g++ to compile it #include<stdio.h> int i=1,j=1,k=1; void main() { { /* هذه وحدة جديدة كل ما بها لا يرى خارجها */ /* ولكنها ترى المتغرات العامة */ printf("I can see global i,j,k.\nglobals are i=%d,j=%d,k=%d\n",i,j,k); } /* المتغيرات التي نعرفها هنا تكون تابعة لل main */ int i=2,j=2; /* هذه متغيرات مختلفة لها مواقع مختلفة */ { /* هذه وحدة جديدة أخرى */ /* لن ترى i,j العامة */ printf("I can see global k=%d\n",k); /* لن ترى i,j العامة */ printf("I can see global i=%d,j=%d\n",i,j); /* ترى i,j العائدة ل main */ /* إذا عرفت متغير جديد سوف لن ترى العام */ int i=3,j=3,k=3; printf("The k here is locel k=%d\n",k); printf("I can NOT see main i,j, the i,j here is locel i=%d,j=%d\n",i,j); printf("To see global i,j,k use C++ :: operator\nglobal i=%d,j=%d,k=%d\n",::i,::j,::k); } { /* هذه وحدة جديدة أخرى */ /* كل التغيرات في الوحدات السابقة غير مرئية هنا */ k=5; /* k هنا عامة */ int k=3; /* k هنا محلية */ } printf("global k is changed to be %d.\n",k); }
I can see global i,j,k. globals are i=1,j=1,k=1 I can see global k=1 I can see global i=2,j=2 The k here is locel k=3 I can NOT see main i,j, the i,j here is locel i=3,j=3 To see global i,j,k use C++ :: operator global i=1,j=1,k=1 global k is changed to be 5.
::
الموجودة في سي++ فقط ،
هذه العملية تجعلنا نصل إلى المتغيرات
العامة في حال كان لدينا متغير محلي له نفس الاسم.
كما أنه برنامج يهدف لتوضيح الفكرة لا أكثر ولا أقل
ويفضل أن لا تكتب شيئاً كهذا في برنامج حقيقي لأن بعض المصنفات
قد لا تفهم وجود وحدة داخل وحدة دون سبب ،هكذا دون for أو أي تركيب آخر
لا تعرف شيئاً داخل حلقة for لأن بعض المصنفات تأخذ هذا على أنه لا ينتمي لوحدة for بل للوحدة الموجود بها for في مثالنا main أي أنك ستحصل على خطأ dublicted definition في بعض المصنفات الأفضل أن تجعل الكود منظما؛ كل التعريفات فوق في البداية (بداية البرنامج تحت مؤشرات # وفي بداية الوظائف مثل main)
لنعد لموضوعنا، لعمل حلقة تعد عداً عكسياً من 10 إلى 1 انظر هذا البرنامج
/* count-down.c: */ #include<stdio.h> void main() { int i; for (i=10;i>0;--i) { printf("%d\n",i); } printf("Zero!!! lounch up\n"); }
لاحظ يمكن أن نغير البرنامج ليصبح
/* count-down.c: */ #include<stdio.h> void main() { int i=10; for (;i>0;) { printf("%d\n",i--); } printf("Zero!!! lounch up\n"); }
for(;COND;)
مكافئة ل
while(COND)
والتي تعني "طالما" أي طالما الشرط متحقق نفذ بكلمات أخرى افحص تحقق الشرط
لكي تدخل الدورة الأولى ثم افحص تحقق الشرط لتدخل التالية وهكذا ، يصبح برنامجنا كتالي
/* count-down.c: */ #include<stdio.h> void main() { int i=10; while(i>0) { printf("%d\n",i--); } printf("Zero!!! lounch up\n"); }
do{...}while(COND);
فتعني ادخل الدورة الأولى ثم افحص ثم ادخل الدورة التالية وهكذا
/* count-down.c: */ #include<stdio.h> void main() { int i=10; do { printf("%d\n",i--); }while(i>1); printf("Zero!!! lounch up\n"); }
لتسريع الحلقات التكرارية يمكننا تحديد أن المتغير المسؤول
عن الحلقة هو مسجل من مسجلات المعالج (أسرع ذاكرة على الإطلاق) وهذا
يكون بكتابة الكلمة المفتاحية register
قبل تعريف المتغير
وهي لن تضر لأنه في حال كانت كل المسجلات مشغولة
اعتبر متغير عادي
/* count-down2.c: fast countdown */ #include<stdio.h> void main() { register int i=10; do { printf("%d\n",i--); }while(i>1); printf("Zero!!! lounch up\n"); }
وتفيدان عندما لا تكون الحلقة عبارة عن عد مثلاً
/* sample-mean.c: */ #include<stdio.h> void main() { int m=0,s=0,n=-1; do { s+=m; ++n; printf("Enter your mark (or -1 to exit): "); scanf("%d",&m); }while(m!=-1); printf("the sum of %d marks is %d \n",n,s); if (n>0) printf("the mean is %f\n",(float)s/n); else printf("No samples,No mean."); }
/* sample-mean.c: */ #include<stdio.h> void main() { int m=0,s=0,n=0; while(m!=-1) { printf("Enter your mark (or -1 to exit): "); scanf("%d",&m); s+=m; ++n; } --n; --s; printf("the sum of %d marks is %d \n",n,s); if (n>0) printf("the mean is %f\n",(float)s/n); else printf("No samples,No mean."); }
/* factorial.c: calc n! */ #include<stdio.h> int main() { int f; int i,n; printf("Enter an integer number: "); scanf("%d",&n); if (n>12) { printf("What? %d! is very big",n); return 1; } else if (n<0) { printf("What negative number ! \nfactorial takes a positive number try gamma(%d-1)",n); return 1; } for (f=1,i=2;i<=n;++i) f*=i; printf("the fact of %d is %d \n",n,f); return 0; }
break;
وللقفز عن دورة
إلى الدورة التي تليها نستعمل continue;
انظر هذا المثال
/* break.c: */ #include<stdio.h> int main() { int i; for(i=0;i<15;++i) { break; /* exit the loop */ printf("%d This will never print\n",i); } for(i=0;i<15;++i) { if (i==7) continue; /* do not print 7 */ printf("%d)\n",i); } return 0; }
/* goto.c: */ #include<stdio.h> int main() { int i; goto lastline; printf("%d This will never print\n",i); lastline: return 0; }
المنظومة array هي عبارة عن مجموعة عناصرها محدودة
ومرقمة indexed set العنصر الأول فالثاني فالثالث
أي أن الترتيب فيها مهم والتكرار جائز
في سي يمكن تعريفها من أي نوع مثلاً
int array[]={1,1,2,3,5,8,13,21,34};
هي منظومة اسمها array وعدد عناصرها 9 العنصر الأول هو 1
ونسميه العنصر رقم صفر (لكي نوفر رقم)
ويمكنك أن تذكر عدد العناصر بكل اختياري
int array[9]={1,1,2,3,5,8,13,21,34};
والمنظومة هي مجرد مؤشر يشير إلى أول عنصر أي يمكن تعريفها بالشكل التالي
int *array={1,1,2,3,5,8,13,21,34};
في مثالنا لو قلنا int i=*(array+0)
فهي تشير إلى أول عنصر رقم صفر
أي أن قيمة i هي واحد و
int i=*(array+5)
فهذا معناه أن قيمة i هي 8 لأنها تعني العنصر السادس (العنصر رقم خمسة)
ويمكن استخدام الأقواس المربعة لتحديد العنصر
int i=array[5]
تعطينا 8
وهنا لا نستعمل * لأن الأقواس تعني العنصر وليس مؤشر عليه
على عكس +
لتعريف متغير منظومة نذكر النوع ثم اسم المتغير ثم الأقواس المربعة
ويمكن وضع الحجم بينها بشكل اختياري فإذا لم تحدده يفترضه بعدد العناصر على يمين المساواة
وإذا لم يكن هناك مساواة تفرض أنها مجرد مؤشر دون حجز مكان لها
وإذا حددت حجم المنظومة ولم تضع عناصر أو لم تضع عناصر كافية
افترض أنها أصفاراً وإذا وضعت الحجم أقل من عددها ستحصل على خطأ ،
وأخيراً الفاصلة المنقوطة أو مجرد تعريف مؤشر من النوع الذي تريد
ولوضع قيمة استهلالية لمنظومة
كما كنا نفعل في الثوابت الرقمة int i=5;
نستخدم الحاصرتان ثم القيم تفصل بينها فاصلة
لا يجوز أن تكتب في عنصر خارج المنظومة مثلاً
عند حجز منظومة بحجم عشرة
int a[10];
ثم الكتابة في العنصر الحادي عشر
a[10]=0;
ولكن لا تقوم سي باضاعة الوقت في عملية فحص هل
هذا تابع للمنظومة أم لا ولهذا تكون هذه مهمتك
وإذا فعلت ذلك في ويندز قد يعلق الجهاز أو البرنامج أما لينكس يقوم
بقتله كي لا يسبب مشكلة ،
كما لا يجوز أن تقرأ خارج الحدود ولكن أيضا سي
لن تمنعك وسستحصل على رقم
فائدة المنظومات هي أنها تعطينا قدرة على الوصول إلى المعلومات التي أدخلناها والعودة لها فيما بعد والوصول المباشر لها مثلاً
/* fab-ser.c: generate a series */ #include<stdio.h> #define MAX_N 20 void main() { int a[MAX_N]={1,1}; for (i=2;i<MAX_N;++i) a[i]=a[i-1]-a[i-2]; printf("the series is {"); for (i=0;i<MAX_N;++i) printf("%d ",i); printf (" } \n"); }
#include<stdio.h> #define MAX_N 10 void main() { int a[MAX_N]; for (i=0;i<MAX_N;++i) { printf("Enter the %d-th number : ",i+1); scanf("%d",&a[i]); } printf("You have entered : "); for (i=0;i<MAX_N;++i) printf("%d ",i); }
/* array-error.c: THIS FILE HAS ERRORS */ /* This is a wrong use of array */ #include<stdio.h> int n=15; int a[n]; /* YOU MAY NOT GIVE A STATIC/GLOBAL ARRAY A VARIABLE SIZE */ void main() { /* ... */ }
/* variable-sized-array.c: you should NOT do this */ /* This is NOT recomended use of array */ #include<stdio.h> void main() { int n; printf("Enter number of elements: "); scanf("%d",&n); int a[n]; /* variable sized array */ for (i=0;i<n;++i) { printf("Enter the %d-th number : ",i+1); scanf("%d",&a[i]); } printf("You have entered : "); for (i=0;i<n;++i) printf("%d ",i); }
malloc
التي تأخذ معامل هو الحجم بالبايت
يمكن استعمال الكلمة المفتاحية sizeof
لمعرفة كم بايت حجم
العنصر الذي تريد و تضربه في عدد العناصر
و realloc
لزيادة أو انقاص حجمها و free
لتحريرها
ويمكن الاعتماد على سي لتقوم بتحرير المساحة الحجوزة تلقائياً
عند اغلاق البرنامج ، ولكن استدعاء هذه الوظيفة قد يكون ضرورياً
عندما يكون لديك بيانات انتهيت منها وتريد توفير الذاكرة.
انظر هذا المثال
#include<stdio.h> #include<stdlib.h> int main() { int i,n=0; int *a; printf("Enter number of elements : "); scanf("%d",&n); if (n<2||n>46) { printf("%d elements ! be serous\n\a",n); return 1; } a=(int *)malloc( sizeof(int) * 10); if (!a) { printf("can't allocate memory\n\a"); return 1; } a[1]=a[0]=1; for (i=2;i<n;++i) a[i]=a[i-1]+a[i-2]; printf("\nhere we go { "); for (i=0;i<n;++i) printf("%d ",a[i]); printf("} \n"); free(a); return 0; }
/* new.cpp: */ #include<stdio.h> int main() { int i,n=0; int *a; printf("Enter number of elements : "); scanf("%d",&n); if (n<2||n>46) { printf("%d elements ! be serous\n\a",n); return 1; } a=new int[n]; if (!a) { printf("can't allocate memory\n\a"); return 1; } a[1]=a[0]=1; for (i=2;i<n;++i) a[i]=a[i-1]+a[i-2]; printf("\nhere we go { "); for (i=0;i<n;++i) printf("%d ",a[i]); printf("} \n"); delete [] a; return 0; }
من أكثر الأمور تعقيداً في سي و سي++ هو التعامل مع السلاسل النصية ويمكنك القول إذا كان برنامج يهدف للتعامل مع السلاسل النصية بشكل كبير استعمل perl ! ولكن سنحاول جعلها أسهل ما يكون عند الحديث عن مكتبة سي القياسية خصوصاً وظيفة sprintf.
المحرف الواحد (حرفاً أو رقماً من منزلة واحدة أو رمزاً خاصاً) الذي يحتل
مساحة واحد بايت (في شيفرة آسكي وليس في يوني-كود "UNICODE" بكلمات أخرى الإنجليزي فقط)
الذي تحدثنا عنه من قبل وأسميناه char يمكنه تمثيل الأرقام من 0 إلى 255 أو
من -128 إلى 127 وبهذا العدد القليل نمثل شيفرة آسكي
حيث تأخذ المسافة الرمز 32 وما دونها رموز تحكم
لا تطبع (مثل سطر جديد 10 و العودة لبداية السطر 13
وصفحة جديدة 12 و صافرة beep 7
ومفتاح الجدولة tab 9 ومفتاح الهروب 27 ... إلخ)
الرقم صفر يأخذ الرمز 48 ثم بقية الأرقام بالترتيب ،
ويأخذ حرف A الرمز 65 ثم تأتي الحروف بالترتيب
الأبجدي وتأخذ a الصغيرة الرمز 97 ثم باقي الحروف
أي أن الرموز من صفر إلى 127 مشتركة وثابتة في كل طرق الترميز
وكل ما هو فوق 127 (أو القيم السالبة) غير محدد ويعتمد على الترميز الحالي
في لينكس عادة ما يكون UTF8 وفي غيره ربما يكون iso8859-1 أي latin-1
وبدلاً من التعامل معها على شكل أرقام ومطالبتك بحفظها
يمكنك أيضا التعامل معهى كما هي بوضعها ضمن علامة تنصيص مفردة
مثلاً char c='A';
ويمكن القيام بعمليات حسابية عليها مثلاً
char d='A'+3;
تعني أن قيمة المتغير d هي 'D'
ويمكن طباعتها على أنها رقم printf("the ASCII code for 'F' is %d",'F');
سيكون ناتج التنفيذ هي
the ASCII code for 'F' is 70
ويمكن أن تستعملها على شكل رقم
char f=70;
و printf("this is F using 70 ascii : %c",70);
أو على شكل رقم باستعمال \ ثم الرقم عادةً بالثماني
printf("this is F using 70 ascii : \0106");
أو بالست-عشري printf("this is F using 70 ascii : \x46");
أو مباشرة char f='\0106';
و char f='\x46';
ويمكن استعمال \ مع واحدة من anrt
لتمثيل صافرة و سطر جديد و عودة لبداية السطر و جدولة
مثلاً char n='\n';
ولتمثيل \ نكتبها مرتين char n='\\';
ويجب أن تنتبه أنه لا يجوز أن نضع أكثر من واحد بايت
في المحرف مثلاً char n='ab';
ولكن \ هي حالة خاصة لا تمثل فيها نفسها بل حسب ما يكون بعدها.
ربما تتسائل ما علاقة هذا بالسلاسل النصية
الفكرة هنا أن السلسلة النصية في سي هي عبارة عن منظومة من المحارف
تنتهي بالرمز 0 من شيفرة آسكي أي أن
char *a="Linux";
تكافئ
char *a={'L','i','n','u','x','\0'};
ويمكنك تحديد حجمها char a[6]="Linux";
أو عدم تحديده
char a[]="Linux";
وإذا فكرت جيداً ستصل إلى أن حجم المنظومة يساوي طول النص + 1 ،
بسبب علامة النهاية '\0'
ويمكن أن تحجز مساحة أكبر من النص للاستخدام المستقبلي
كأن تقول
char a[10]="Linux";
هذا يعني
char *a={'L','i','n','u','x','\0','\0','\0','\0','\0'};
كما ويمكنك حجز السلسلة باستعمال malloc
أو new
مثلاً
char *a=new char[100];
أو
char *a=(char *)malloc(100);
ثم تضع قيمتها باستعمال وظائف مكتبة سي القياسية
بعض اللغات مثل Basic ومشتقاتها تكون السلسلة النصية عبارة عن رقم يمثل الطول ثم منظومة المحارف
تذكر أن السلسلة النصية عبارة عن منظومة أي مؤشر يشير لأول حرف،لا يمكنك استعمال العمليات التقليدية على أنها عمليات على سلاسل نصية لأنها عمليات على ذلك المؤشر وليس على السلسلة النصية أي أن مقارنة نصين ب == ستكون مقارنة بين مؤشرين أو عنوانين في الذاكرة وأنه لا يجوز جمع مؤشرين وأن المساواة هي أن تغير العنوان الذي يشير إليه كما ولا يمكنك تجاوز الحجم الذي حجزته من قبل
يمكنك طباعة السلسلة النصية ب
%s
في printf
مثلاً printf("%s",a);
لطباعة a التي عرفناها سابقاً
ويمكن القيام بالعمليات العادية على المؤشرات مثلاً
printf("%s",a+1);
ستطبع
inux
لأن a+1 تشير إلى العنوان الذي
يلي العنوان الذي تشير له a وهو عنوان الحرف الأول من Linux
كذلك يمكنك الوصول لكل حرف لوحده كما تتعامل مع المنظومة مثلاً
a[4]="s"; printf("%s",a);
ستحصل على Linus
لأننا غيرنا الحرف الخامس (رقم 4)
توفر مكتبة سي القياسية الكثير من الوظائف التي تسهل علينا التعامل مع السلسلة النصية وغالباً ما تكون هذه الوظائف نوعين أحدهما مباشر مثل strcpy والآخر يحتوي اسمه على حرف n مثل strncpy حيث تضع له الحد الأعلى لحجم المنظومة لكي لا يتجاوزه ويتسبب في أخطاء .
لسؤال المستخدم عن سلسلة نصية يمكنك أن تستعمل
scanf
مع وضع %s
في الهيئة ويمكنك تحديد الحد الأقصى
بعشرة مثلاً char a[11]; scanf("%10s",a);
لاحظ أننا لم نضع عملية العنوان قبل a لأن a هي مؤشر ،
ولكن scanf
تتوقف عند أو مسافة بيضاء (مسافة أو إدخال أو جدولة tab ... إلخ)
وليس عند الإدخال أي أن البرنامج التالي
/* name1.c: Enter you name */ #include<stdio.h> int main() { char a[100]; printf("Enter your name: "); /* لن تأخذ سوى الاسم الأول*/ scanf("%99s",a); printf("\nHello %s,nice to meet you\n",a); return 0; }
gets
أو
fgets
ويفضل استعمال الثانية لأنك تحدد لها الحد الأعلى لحجم النص لكي لا تسبب خطأ
/* name1.c: Enter you name */ #include<stdio.h> int main() { char a[100]; printf("Enter your name: "); gets(a); printf("\nHello %s,nice to meet you\n",a); return 0; }
fgets(a,99,stdin);
مكان gets(a);
لاحظ أن الثابت الحرفي '\n'
مثل المحرف 10 أي سطر جديد وهذا ينطبق على السلسلة أيضا
وقد استخدمناه من قبل عندما كنا نقول
printf("line1\nline2\n");
واذا أردت التعبير \ عليك كتابتها مرتين مثلاً
printf("C:\\TEMP\\");
لهذا نحب أن نستخدم / بدلاً من \ في التعبير عن المسارات
وهذا لا ينطبق على ما يدخله المستخدم بل هو مسألة داخلية في سي
أثناء البرمجة لتسهيل التعبير عن محارف التحكم غير الموجودة على لوحة المفاتيح
وغير القابلة للطباعة
أي أنه على المستخدم ادخال C:\TEMP\
إذا أرادها وليس C:\\TEMP\\
لمعرفة طول السلسلة (دون صفر النهاية) نستخدم
strlen(str)
ولنسخ سلسلة فوق أخرى (عملية الإحلال) لا يمكنك استعمال = بل
strcpy(dest,source)
الأول هو المكان الذي ستكتب فيه و الثاني هو المصدر
ولدينا strncpy(dest,source,max)
الأكثر موثوقية التي نعطيها رقماً لا تتجاوزه
ويمكن اضافة سلسلة إلى أخرى
strcat(dest,add)
و strncat(dest,add,n)
حيث سيتم اضافة add إلى نهاية dest بحيث لا يتجاوز طول dest المقدار n
ولكي نقارن بين نصين لدينا strcmp(str1,str2)
التي تعطي صفر إذا كانا متساويان و رقم سالب إذا كان
الأول أقل (يأتي أولاً أبجدياً) من الثاني وموجب إذا كان الأول أكبر
(يأتي آخراً أبجدياً)
.وكأن الأمر عبارة عن طرحهما من بعض
وللبحث عن سلسة داخل أخرى يمكن أن نستخدم
strstr(Search,For)
والتي تعيد مؤشر على أول النص الفرعي من
Search
يبدأ ب For
وتعيد NULL
في حال لم يكن For
جزءاً من Search
ويمكن طرح النص الأصلي من النتيجة لنحصل على الإزاحة
char *a="football",*b="ball"; printf("%i",strstr(a,b)-a);
ستكون النتيجة 4.
أما إذا أردت التأكد من أن النص هو عبارة عن تركيب من أحرف
معينة strspn(str,codeset)
وهي تعطي صفر إذا كانت السلسة ليست مكونة من codeset
أو طول السلسة كاملة إذا كانت مكونة منها
أو ليس من أحرف معينة strcspn(str,ccodeset)
i=strspn("dollar $"," abcdefghijklmnopqrstuvwxyz")
ستكون قيمة i هي 7
وهي أقل من طول السلسة أي أنها تحتوي على رموز من خارج المجموعة
يمكنك تحويل سلسلة لعدد باستعمال الطريقة القديمة atoi
أو atof
أي الحرف الأخير يشير إلى أنه صحيح integer أم
نسبي float
أو الطريقة الأحدث strtol
أو strtod
الأولى long
والثانية double
ولكنني أفضل استعمال sscanf
التي تأخذ الدخل من سلسة بدلاً من جهاز الإدخال
وصيغتها نفس scanf
مع اضافة معامل أول هو النص الذي نريد تحويله
sscanf("100","%d",&i);
بل ويمكن أن تقوم بأعمل أكثر تعقيداً كأن تحول نص إلى أكثر من رقم
sscanf("31/1/2004","%2d/%2d/%4d",&d,&m,&y);
وتحويل عدد إلى سلسلة يكون ب itoa
أو ftoa
ولكني أجد sprintf
أفضل بكثير وأظنك خمنت أنها
نفس printf
ولكن بدل أن تكتب في الخرج فإنها
تكتب على لسلة أي أنك تحصل في النهاية على سلسلة
وصيغتها نفس printf
غير أنها تأخذ معامل أول إضافي
هو السلسة المحجوزة مسبقاً مثلاً
sprintf(a,"%d",100);
ويمكنك أيضاً أن تحدد الهيئة أو تجعل الأمر أكثر تعقيداً
sprintf(a,"you have $%4.3f at %02d/%02d/%04d",10.5,31,1,2004);
التي تجعل قيمة a تساوي
"you have $10.5 at 31/01/2004"
ولكي نضمن أنها لن تتجاوز حداً معيناً نستعمل _snprintf
التي يكون أول معامل لها هو السلسلة والثاني هو الحد الأقصى للحجم
لنفرض أنك تريد منظومة عناصرها من نوع سلسلة نصية مثلاً
منظومة عناصرها {"Sat","Sun","Mon","Tus","Wed","Thu","Fri"}
أي أن نوع العناصر هو char *
فكيف تعرفها ؟ لا يوجد جديد في الموضوع اذكر النوع ثم أقواس
واذكر العدد إذا أردت
char *a[7]={"Sat","Sun","Mon","Tus","Wed","Thu","Fri"};
ولو كتبنا
printf("today is %s",a[i]);
سنحصل على today is Sat
إذا كانت i تساوي صفر ،
وسنحصل على today is Fri
إذا كانت i تساوي 6 ،
ولايشترط أن تكون السلاسل بنفس الطول إنما هي هنا صدفة.
ولكن ما الذي حصلنا عليه هنا ؟ منظومة عناصرها منظومات.
ولقد ذكرنا سابقاً أن نوع المنظومة يكون مؤشراً على نوع عناصرها أي
char **a
لاحظ معنا أن a
هو مؤشر يشير إلى سلسلة بكلمات أخرى هو مؤشر يشير إلى مؤشر
و *a
هو ما يشير له، أي مؤشر إلى
أول حرف في السلسلة الأولى و **a
هي
أول حرف في السلسلة الأولى أي "S"
لاحظ أيضاً أن *(a+6)
تعبر عن a[6]
أي Fri
و a[6][1]
تشير إلى الحرف الثاني في السلسلة أي 'r'
ويمكن أن نعبر عن ذلك ب *(*(a+6)+1)
ويمكننا أن نعبر عن a كتالي
char a[7][4]={"Sat","Sun","Mon","Tus","Wed","Thu","Fri"};
ولكن بهذه الطريقة يجب أن تكون كل العناصر لها نفس
الطول (الحد الأعلى للحجم)
هذا الكلام ينطبق على كل الأنواع الأخرى وليس فقط على منظومات المحارف (السلاسل) إذ يمكنك أن تعرف منظومة عناصرها منظومات رقمية بكلمات أخرى منظومة في بعدين وإذا كنا نتحدث عن أرقام عندها نسميها مصفوفة مثلاً
int a[3][4]={ {1, 2, 3, 4}, {5, 6, 7, 8}, {9,10,11,12} };
/ \ |1 2 3 4| |5 6 7 8| |9 10 11 12| \ / 3x4
int **a; a=(int **)malloc(sizeof(int*)*3);
ونتأكد من عدم حدوث خطأ
if (!a) {printf("error"); return 1;}
ثم نجعل المؤشر الأول يشير إلى منظومة من 4 أرقام صحيحة
a[0]=(int *)malloc(sizeof(int)*4);
والثاني
a[1]=(int *)malloc(sizeof(int)*4);
فالثالث a[2]=(int *)malloc(sizeof(int)*4);
ونتأكد من عدم حدوث خطأ فإذا كان هناك خطأ نلغي الجزء الذي تم
if (!(a[0]&&a[1]&&a[2])) { printf("error"); if (a[0]) free(a[0]); if (a[1]) free(a[1]); if (a[2]) free(a[2]); free(a); return 1; }
اكتب البرامج التالي وسمه arg.c
/* arg.c: argument tester*/ #include<stdio.h> int main(int argc,char **argv) { int i; printf("you have passed %d argument to main",argc); printf("and they are :\n"); for (i=0;i<argc;++i) printf("%d) %s\n",i,argv[i]); return 0; }
./arg hello
./arg hello world
ثم
ثم ./arg
ولاحظ الفرق
حتى الآن كان معظم تعاملنا يتم من خلال الكلمات المفتاحية والقليل من الوظائف من بين آلاف الوظائف التي توفرها مكتبة سي القياسية ولكن كيف تعمل وظيفة خاصة بك لكي تستعملها فهي توفر عليك تكرار كتابة الأجزاء المكررة في برنامج ربما باختلاف بسيط هنا وهناك ؟ الأجزاء المختلفة في كل مرة نسميها معاملات تمرر للوظيفة في كل مرة يتم استدعائها فيها وعند الإنتهاء يمكن أن تعيد هذه الوظيفة قيمة قد تكون ناتج العمليات التي قامت بها وفي الغالب تكون اشارة إذا نجحت العملية أم لا. لقد كتبنا وظيفة قبل الآن، إنها وظيفة main وبنفس الطريقة نكتب الوظائف الأخرى
لكتابة وظيفة حدد نوع المعاملات arguments types التي قد تحتاجها وأعطها أسماءً ونوع الناتج التي تعيده الوظيفة return type وأعط الوظيفة اسماً مثلاً وظيفة تجمع عددين صحيحين وتعيد الناتج
int add(int i,int j) { return i+j; }
void
ثم اسمها ثم أقواس داخلاها مانحتاجه من متغيرات
وهمية dummy variables سميها المعاملات
مسبوقة بنوعها ثم حاصرتان بينهما الكود الذي تنفذه
هذه الوظيفة ينتهي بإعادة الناتج return SOMETHING;
أو إذا كانت الوظيفة بلا ناتج void
يمكن أن
تنتهي بشكل اختياري ب return ;
لاستدعاء الوظيفة في أي مكان تحتها يمكنك أن
تفعل ما فعلناه مع وظائف مكتبات سي القياسية أي أن تكتب اسم الوظيفة فقط.
هل تذكر اسلوب K&R الأصلي كان يكتفي بوضع اسم المتغير دون نوعه
داخل الأقواس ويعرّف نوعها بعدها كما يلي
int add(i,j) int i,j; { return i+j; }
int a,b,c,d,e,f,g;
بدلاً من كتابة int في كل واحدة. في حال كانت المعاملات من أنواع مختلفة
int a,b,c; float d,e,f,g;
أيضاً نجد توفيراً في الكتابة! إنه مجرد اسلوب ولك الخيار.
هذا مثال تطبيقي يوضح فكرة الوظائف
/* func1.c: functions example */ #include<stdio.h> int add(int i,int j) { return i+j; } int main() { int a,b,c; c=add(1,2); /* gives 3 */ printf("1+2=%d\n",c); printf("1+2=%d\n",add(1,2)); /* can be called any where */ printf("Enter two integers"); scanf("%d %d",&a,&b); printf("%d+%d=%d",a,b,add(a,b)); return 0; }
/* func2.c: functions with prototype example */ #include<stdio.h> int add(int i,int j) ; int main() { int a,b,c; c=add(1,2); /* gives 3 */ printf("1+2=%d\n",c); printf("1+2=%d\n",add(1,2)); /* can be called any where */ printf("Enter two integers"); scanf("%d %d",&a,&b); printf("%d+%d=%d",a,b,add(a,b)); return 0; } int add(int i,int j) { return i+j; }
int add(int i,int j=0);
ويجب أن تكون المتغيرات ذات القيم التلقائية هي الأخيرة
أي لا يجوز أن تقول
int add(int i=0,int j);
ويجوز أن تكون في أكثر من متغير
int add(int i=0,int j=0);
أود أن أوضح كيف تتم عملية استدعاء الوظيفة
بشرح ما حدث في add(a,b)
من السطر 13
حيث تم ارسال a و b لها
هنا يتم حفظ الحالة التي يكون الجهاز بها بدفع معظم مسجلات المعالج إلى ذاكرة تسمى المكدس stack
ثم القفز إلى وظيفة add حيث يتم حجز متغيرين
جدد i و j من المكدس stack وهما ينتميان
لحدود الوظيفة أي لا يريان خارجها وإذا كان هناك متغيرات عامة
باسم i و j فإنها تصبح غير مرئية
وطبعاً كل متغيرات المستدعي (في حالتنا main ) والوظائف الأخرى غير مرئية أيضاً
ثم يضع قيمتيهما (i و j) بقيمة a و b على الترتيب
ثم يتم تنفيذ الوظيفة وعند الخروج
منها للعودة للمستدعي يتم تحرير الذاكرة المحجوزة ب i و j
من المكدس ، ويتم استعادة الحالة التي كان بها الجهاز
قبل استدعاء الوظيفة من المكدس وتحرير الذاكرة مثلاً ليعرف البرنامج أين وصل.
فالمكدس هو جزء من الذاكرة يتم حجزة
عند تنفيذ البرنامج ويحدد مقداره نظام التشغيل
وهو يعمل على طريقة من يدخل أخيراً يخرج أولاً ،
وعند استدعاء وظيفة تعمل على استدعاء وظيفة أخرى
يتم حجز مساحة من المكدس مرةً عند استدعاء الأولى ومرةً أخرى عند استدعاء الثانية
ثم يتم تحريرهما عند العودة أي أننا مررنا بمرحلة ذروة في استهلاك
ذاكرة المكدس لذا عليك التقليل قدر الإمكان من استدعاء الوظائف التي
تستدعي وظائف تستدعي بدورها وظائف باستعمال الحلقات التكرارية
كبدير عن الاستدعاء الهرمي
يمكن الاستدلال من ذلك أن الوظائف لا يمكنها تغيير قيمة معاملاتها أي أن البرنامج التالي لا يعمل كما يجب ، لنحاول أن نكتب وظيفة تزيد من قيمة معاملها الوحيد
/* This example does NOT work as it should */ #include<stdio.h> void inc(int i) { ++i; } void main() { int a=1; inc(a); printf("%d",a); }
/* change-arg.c: a function can change it's argument */ #include<stdio.h> void inc(int *ptr) { ++(*ptr); } void main() { int a=1; inc(&a); printf("%d",a); }
scanf
لابد أنك عرفت الآن لماذا كنا نضع & قبل معاملاتها
مصنفات سي++ ومصنفات سي الحديثة تستطيع عمل ذلك بطريقة أخرى باستعمال الاسم المستعار كما في هذا المثال
/* change-arg.cpp: a function can change it's argument */ #include<stdio.h> void inc(int &i) { ++i; } void main() { int a=1; inc(a); printf("%d",a); }
فلسفة البرمجة الهيكلية(التركيبية) تقوم على تقسيم البرنامج المعقد لعدة وظائف تقوم كل منها بعمل وظيفة محددة صغيرة وبسيطة (غبية) يسهل كتابتها ثم تكون وظيفة ال main مجرد استدعاء متسلسل لها فيما يشبه عملية تقسيم العمل في المصانع ، تجعل هذه العملية صيانة البرنامج أسهل بسبب امكانية تتبع الخطأ بسهولة بعزل المرحلة التي تسبب الخطأ وتجريب كل مرحلة لوحدها بتمرير معاملات مناسبة لها
يمكن لوظائف سي أن تستدعي وظائف أخرى للقيام بعملها
ويمكنها أيضا أن تستدعي نفسها (بمعاملات أخرى مثلاً) ولكن
حتى نضمن أنها تعمل يجب أن نوفر حالة مؤكدة الحدوث بأن عملية
الاستدعاء هذه ستتوقف عند مرحلة معينة قبل أن يطفح المكدس
وإلا فإن البرنامج سيقتل أو سيعلق الجهاز
وإذا أمكن الاستغناء عن هذه الطريقة باستعمال الحلقات التكرارية
فهذا أفضل لأن الأخيرة أسرع ولا تستنزف الذاكرة
لنأخذ مثال حساب المضروب كمثال فمضروب
أي رقم يساوي الرقم ضرب مضروب الرقم الذي قبله
علماً أن مضروب الصفر 1 نعبر عنها بطريقة رياضية 0!=1; n!=n(n-1)!;
/* rec-fact.cpp: a recursion version of factorial */ #include<stdio.h> int factorial(int i) { if (i>1) return i*factorial(i-1); else return 1; return 1; /* for dummy compilers like Borlands */ } int main() { int i; printf("Enter an integer (less than 13): "); scanf("%d",&i); if (i<0||i>12) { printf("you entered %d\nIt must be non-negative and less than 13\n",i); return 1; } printf("%d",factorial(i)); return 0; }
/* hanoi.cpp: solve hanoi puzzle */ #include<stdio.h> void hanoi(int n,int from,int to) { int temp=6-from-to; /* 1+2+3=6=from+to+temp */ if (n==1) printf("%d->%d\n",from,to); else if (n<1) {printf("error bad argument\n"); return;} else { hanoi(n-1,from,temp); printf("%d->%d\n",from,to); hanoi(n-1,temp,to); } } int main() { int i; printf("Enter a positive integer : "); scanf("%d",&i); if (i<1) { printf("you entered %d\nIt must be positive\n",i); return 1; } printf("start solving ...\n"); hanoi(i,1,2); return 0; }
هناك مشكلة أخرى تجعل من استعمال الوظائف ذاتية الاستدعاء
خياراً غير مفضلاً هو أن متغيرات جديدة يتم حجزها في كل استدعاء
لهذه الوظيفة وليس فقط المعاملات مثلاً temp في المسألة السابقة
كل استدعاء للوظيفة يحجز متغيراً جديداً هذا يجعلنا أمام نوعين من
المتغيرات ،نوع يحجز مرة واحدة عند تحميل البرنامج
ويسمى هذا النوع static
مثل المتغيرات العامة والمنظومات الثابتة (تلك التي تحدد حجمها عند تعريفها بين الأقواس المربعة) وتلك
التي يسبق تعريفها كلمة static
صراحةً وهذا
النوع يزيد من حجم البرنامج ، ونوع آخر يحجز
من المكدس عند الحاجة ويحرر عند الانتهاء منه
مثل المتغيرات التي تعرف داخل الوظائف والوحدات البرمجية
وهي التي يعتبر نوعها ضمنياً auto
.
وبالنسبة للنوع الأول static
فإنه وعلى عكس ما يظن البعض
بأن قيم الثوابت تحدد بعد حجزها
عند التنفيذ أي أن static int i=5;
تصبح في لغة الآلة احجز
مكان يتسع لعدد صحيح ثم ضع فيه 5
أو في لغة التجميع mov I,5;
حيث I هو المكان الذي تم حجزه.
ولكن هذا الكلام غير دقيق
الحقيقة أن مساحة هذا النوع من المتغيرات تكون بالنسبة
للغة الآلة (والتجميع) عبارة عن
جزء من كود البرنامج يتم القفز عنه أو لا يتم الوصول إليه
(غالباً ما يكون في نهاية البرنامج)
وعند تحميل البرنامج في الذاكرة يتم حجز تلك المنطقة كلها دفعة واحدة
(للبرنامج بما فيه من متغيرات)
وتكون قيمة تلك المنطقة تبعاً لذلك الثابت الذي نريده
أي أن الكود static int i=5; do_something(i);
يصبح
في لغة التجميع كما يلي (هذا الكود لتوضيح الفكرة فقط وهو ليس حقيقي)
jump main dd 05 00 00 00 main: CALL do_something(i) END
static variables
وهو يفترض في المتغيرات العامة و المتغيرات التي يذكر بصراحة أنها من هذا النوع باستعمال
الكلمة المفتاحية static
قبل النوع في تعريف المتغير
وفي المنظومات الثابتة تلك التي نضع مساواة وعلى يمينها قيمة استهلالية
أو تلك التي نحدد لها حجما بين الأقواس المربعة
لقد فكرت في مثال تختلف فيه النتيجة فيما لو كانت المتغيرات
static
أم لا . هذا المثال ربما معقد ليوضع هنا
لتفهمه جيداً انتظر حتى نتعرف على وظائف الاستدعاء الذاتي
#include<stdio.h> void iter(int i) { static int g=10; if (i>0) { --g; printf("%d-%d\n",i,g); iter(--i); } } void main() { iter(5); }
5-9 4-8 3-7 2-6 1-5
static
سيكون
5-9 4-9 3-9 2-9 1-9
إذا تأملت وظيفة printf
الخارقة تجد أنها تتلون كالحرباء
فتأخذ أي عدد من المعاملات بأي نوع فما هو نموذج هذه الوظيفة ؟
إذا فتحنا ملف stdio.h
نجد أنها
int printf (const char* Format, ...);
ومعنى الثلاث نقط أنها تريد تستقبل بقية معاملاتها
باستعمال مكتبة سي القياسية stdarg.h
هذا المثال يوضح وظيفة اسمها sum تأخذ معاملاً أول يحدد
عدد الأرقام التي يجب عليها جمعها ثم ذلك العدد من المعاملات
وتقوم بجمعها
/* arb-args.c: a functions takes many numbers nd add them */ #include<stdio.h> #include<stdarg.h> int sum(int n,...) { /* where n is the numbers of extra arguments */ int i,j,s=0; va_list ap; va_start(ap, n); printf("the arguments are:"); for (i=0;i<n;++i) { j=va_arg(ap, int ); printf("%d ",j); s+=j; } printf("\n"); va_end(ap); return s; } int main() { printf("the sum of 1 2 3 4 5 is %d\n",sum(5,1,2,3,4,5)); return 0; }
va_list
ليمثل قائمة المعاملات سنسميه ap وقبل أن تستعمله تستدعي va_start
وتعطيها هذا المتغير (قائمة المعاملات) وآخر معامل محدد ومعروف،
الآن في كل مرة تريد فيها أخذ معامل جديد تستدعي
va_arg
وتعطيها قائمة المعاملات
ap و نوع المعامل الذي تريد وهي بدورها ستعيد لك قيمته
وقبل الخروج استدع va_end
مع تمرير قائمة المعاملات ap
لها لكي تحرر ما حجزته. يمكنك أن تكتب وظيفة تحسب القيمة العظمى لمعاملاتها
كتمرين.
يمكنك في سي تعريف أنواع بيانات جديدة custom data type
وذلك بالكلمة المفتاحية typedef
حيث نذكر ماذا يقابل ثم الاسم الجديد مثلاً typedef unsigned int uint;
التي تعني تعريف نوع جديد من البيانات اسمه uint
الذي يعني
unsigned int
حيث يمكنك فيما يلي هذا السطر أن تكتب uint i;
على سبيل المثال.
يمكن استعمالها كما يلي
#if sizeof(int)==4 typedef short int int16; typedef int int32; #elseif sizeof(int)==2 typedef int int16; typedef long int int32; #endif
int16
و int32
ولكن ليس هذا هو الهدف من تعريف أنواع جديدة
بل أن تكون هذه الأنواع تركيب من أنواع معروفة مسبقاً
لتعريف تركيب نستخدم الكلمة المفتاحية struct
متبوعة اختيارياً باسم التركيب ثم حاصرات ثم المتن
وبعد الحاصرات يمكن اختيارياً تعريف متغير ثم فاصلة منقوطة،
المتن يتكون من تعريف للمتغيرات التي تشكل بمجموعها هذا التركيب
مثلاً لعمل تركيب اسمه MyPoint
يتكون من عددين نسبيين x,y
وتعريف متغير باسم p1
من هذا النوع
struct MyPoint { float x,y; } p1;
struct
ثم اسم التركيب
ثم اسم المتغير في مثالنا struct MyPoint p1;
وليكون الموضوع أكثر رسمية
نعرف نوع جديد من البيانات باستعمال typedef
وننحدد التركيب struct
على أنه معنى المقابل
للنوع الجديد الذي اسميناه MyPoint
ثم نعرف المتغير p1
في جملة اخرى بالطريقة المعتادة
typedef struct { float x,y; } MyPoint; MyPoint p1;
p1.x
أو ->
عند التعامل مع مؤشرات لمتغير التركيب
ptr->x
مثلاً printf("%f",p1.x);
ويمكن أن يكون مؤشر للتركيب عضواً فيه
ولكن لا يجوز أن يكون التركيب عضواً لنفسه
لأن المؤشر من جميع الأنواع هو عنوان في الذاكرة
ويمكن معرفة كم بايت يحتاج دون معرفة حجم ما يشير له مثلاً
typedef struct { ListNode *next; int value; } ListNode;
يفضل تمرير التراكيب للوظائف المختلفة على شكل مؤشر لتوفير الذاكرة
typedef struct { unsigned int m:23; unsigned int b:8; unsigned int s:1; } MyFloat;
union
مثلاً
typedef union { unsigned char u; signed char s; } MyChar;
#include<stdio.h> typedef union { unsigned char u; signed char s; } MyChar; int main() { int i; MyChar c; printf("Enter Number [0-255] : "); scanf("%d",&i); if (i<0||i>255) { printf("\nError: %d is NOT from 0 to 255\n",i); return 1; } c.u=i; printf("c.u=%d\nc.s=%d\n",c.u,c.s); return 0; }
s
ستكون
s=u-256
لأنهما تأخذان نفس المووقع في الذاكرة ولكن عند الوصول لها
من s تفسر على أنها تحتوي بت الإشارة
أما عند استعمال u فهي لا تحتوي بت الإشارة
على عكس الكثير من الكتب أجد مكتبة سي الكلاسيكية أسهل وأكثر مرونة من مكتبة سي++ ربما لأني تعلمتها قبل فترة طويلة وتعودت عليها، أو ربما لأني لن أشعر بالغربة عند استعمال لغات أخرى تم عملها بحيث تشبه مكتبة سي القياسية مثل perl و php أو حتى مكتبات أخرى توفر وظائف تشبه printf. حيث تستطيع هذه المكتبة تحديد الهيئة التي تريد مباشرة بينما تحتاج لعدة أسطر مملة في مكتبة سي++ القياسية. وحيث أن سي++ هي تطوير للغة سي فمكتبة سي القياسية موجودة في سي++ ويمكن استعمالها دون جهد إضافي حتى في البرامج ذات الطبيعة الكينونية. ولكن هذا رأي شخصي أحتفظ به لنفسي. ولأن مكتبة سي++ لها أسلوب مختلف تماماً عن مكتبة سي فإن تغطية مكتبة سي++ سيكون خارج أهداف هذا الكتاب وأكتفي بمقدمة "بسيطة" عنها في فصل قادم مع شرح مطول عن مكتبة سي القياسية. وعن البرمجة الكينونية.
من سيئات مكتبة سي القياسية
أنها ليست type safe أي أنها لا تعير اهتماماً
للتدقيق على نوع المتغير
مثلاً هذا الكود يصنف دون أخطاء ولكنه لن يعمل بصورة صحيحة
float f; scanf("%d",&f);
ومن سيئتها أيضاً صعوبة التدقيق بحثاً نقطة الخطأ
في المقابل سي++ هي type safe أي أنها تختار الوظيفة الخاصة بكل
نوع تلقائياً ولكنها أقل سرعة انظر هذا المثال
cout <<"String"<<1.5<<1;
عبارة عن استدعاء 3 وظائف هي
((cout.operator<<("String")).operator<<(1.5)).operator<<(1);
وفي كل واحدة يتم حفظ الحالة بدفع المسجلات إلى المكدس
وسحبها 3 مرات
في المقابل printf("%s%f%i","String",1.5,1);
هي استدعاء واحد.
سيكون شرح المقدمة البسيطة عن مكتبة سي++ القياسية في فصل 7.3 البرمجة الموجهة للكائنات
<< السابق | كتاب لينكس الشامل | التالي >> |