7.1 البرمجة بلغة C/C++ كتاب لينكس الشامل >>

7.1 البرمجة بلغة C/C++

الفهرس

7.1.1 مقدمة

كنت أعتقد (قبل أن أعرف لينكس) أن لغة التجميع asm هي أقوى لغة برمجة موجودة لأنها تتعامل مع التركيب الأولي للحاسوب مباشرة دون لف ودوران أي أن كل تعليمة بها تتحول إلى تعليمة واحدة بلغة الآلة فبالتالي هي أفضل من كل لغات البرمجة ولكنها صعبة وبدائية لهذا غير شائعة ولكن هذا الكلام كان نابع من تجرربتي لمصنفات C مثل TC و Borland C و Microsoft VC التي تحتوي على تحسين زائف Fake Optimization (تجده في القوائم ولكنه خيار غير مطبق) ولكن في لينكس وبالتحديد باستعمال GNU C Compiler المعادلة تنقلب فهذا الأخير يحتوي على تحسين حقيقي Real Optimization وذو فعالية عالية فقد تجعل برنامج يقل إلى نصف حجمه وتزداد سرعته أضعافاً كثير وذلك لأنه يعرف كل تعليمات لغة الآلة ويعرف الكثير من طرق التسريع والإختصار فهو يقوم إن شئت بإعادة كتابة برنامج (بلغة الآلة أو asm) ويعطيك خوارزمية غير التي استعملتها أنت ولكن تقوم بنفس الشيء الذي تريده عنها تكون لغة C أفضل لغة على هذا الكوكب في هذا العصر فاذا أضفنا إلى هذا أنها لغة توفر وظائف عالية المستوى مثل printf ووظائف منخفضة المستوى مثل fread و أنواع متغيرات عالية المستوى مثل complex و time و في نفس الوقت أنواع منخفضة المستوى مثل المؤشرات pointers وهي لغة المستوى العالي الوحيدة التي يمكنها كتابة أنظمة تشغيل وهي اللغة التي يستعملها Linus ومكتباتها لا تعد ولا تحصى

tipتلميح

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

tipتلميح

أقول إن أفضل لغة هي 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 في ويندوز. من الجيد أن تكون المكتبات التي تتعلمها مفتوحة المصدر لأنك إذا قمتت باستدعاء وظيفة معينة من يضمن لك بأنها ستعمل ما طلب منها فقط وأن المعاملات التي تصل إليها (قد تتكون كلمة سر مثلاً) لن ترسل إلى مكان اضافي دون علمك ، يجب أن تكون المكتبات مجانية لأنك لا تستطيع أن تقول لزبائنك اشتروا الإصدار الفلاني منها لأنك بذلك تفقد بعض الزبائن .

7.1.2 مصنف GNU C Compiler

اما أن تستخدم برنامج 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

7.1.3 البرنامج الأول

/* 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;
}

warningتحذير

أكثر خطأ شيوعا هو أن تنسى الفاصلة المنقوطة

warningتحذير

لا يوجد ; بعد كل ما يبدأ ب # مثل #include ولا بعد الحاصرة {} التي تحدد وحدة block من الكود

warningتحذير

إن مصنف gnu للغة C يقبل تعليقات C++ ان لم تطلب منه أن يطبق المعايير بحذافيها ولكن المعايير تمنع ذلك

7.1.4 أنواع المتغيرات

على عكس اللغات التفسيرية يجب أن تحدد نوع المتغير والأنواع الأساسية هي
char هو رمز واحد "محرف" يحجز واحد بايت فقط
int رقم صحيح عادي يحجز 4 بايت
float عدد نسبي (يمكن أن يحتوي كسور) وهو أحادي الدقة يحجز 4 بايت
وهناك أنواع اضافية هي
short رقم صحيح بنصف الطول يحجز 2 بايت
long رقم صحيح مضاعف الطول يحجز 8 بايت
double عدد نسبي (يمكن أن يحتوي كسور) وهو مضاعف الدقة يحجز 8 بايت
long double عدد نسبي (يمكن أن يحتوي كسور) وهو مضاعف الدقة يحجز 10 بايت
وهناك محددات قبلية نتخدمها مع char و int هي signed وهي تلقائية تفترض إذا لم تذكرها وتعني أن يحجز نصف الأعداد التي يمكنه تمثيلها لتمثيل الأعداد السالبة أما unsigned فيمكنها تمثيل ضعف العدد من الأرقام ولكن فقط غير سالبة فمثلا نقول unsigned int و unsigned char وهذه الأخيرة تمثل الرموز بشيفرة ASCII وتعطيها أرقام من صفر إلى 255 أما بدون unsigned ستكون من -128 إلى 127 ، ويمكنك أن تضع حرف u بعد الرقم لتدل على أنه غير سالب وهذا مهم عند التعامل بالنظام الست-عشري مثلا 0xffffu تعني 65535 أما 0xffff تعني -1 ولدلالة على أن الرقم كبير يمكن أن نضع l أي long مثلا 0x10000ul وللعلم مدى ال short هو من -32768 إلى 32767

tipتلميح

في مصنفات 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 فهي غير معروفة

7.1.5 الثوابت الرقمية في سي

وكما لاحظت نستخدم نفس أسلوب اللغات السابقة أي الرقم الذي يبدأ بصفر هو بالنظام الثماني والذي يبدأ ب 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

7.1.6 العمليات في C

العمليات الحسابية هي الجمع + والطرح - ناتجهما هو من نفس نوع المعاملات أي جمع صحيحين يعطي صحيح وهكذا والضرب * والقسمة / وهي ضعف نوع المعاملات أي ضرب صحيحين هو صحيح مضاعف 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 كما هو الحال في الحاسوب

والعمليات الثنائية و عملية "و" الثنائية & وعملية "أو" الثنائية | وعملية "أم" الثنائية ^ التي تسمى "أو الاستثنائية" (سين أم صاد ؛أحدهما وليس كلاهما) وأخيراً عملية النفي الثنائي ~ وهي عملية سابقة (أي تأتي قبل معاملها) لمعامل واحد (وليس معاملين كسابقاتها) الواحد يصبح صفراً والصفر واحداً . وهي ليست عمليات منطقية (أي لا نستخدمها بالجمل الشرطية) بل عمليات على المنازل الثنائية في الرقم مثلاً 23 "و" 25 هي 10111bin & 11001bin = 10001bin = 17dec يقابلها بلغة سي printf("%d",23 & 25 ); التي تعطي 17 ، يجب الإنتباه في عميات النفي الثنائي إلى أن الأصفار على اليسار لها قيمة لأنها ستقلب إلى واحدات أما عدد هذه الأصفار فيعتمد على نوع المتغير لنأخذ هذا المثال ~(char)23dec=~00010111bin = 11101000bin = 232dec في سي نقول unsigned char i=23; printf("%d", ~i ); فيعطينا 232 . وهذا الجدول يوضح العمليات الثنائية

&01
000
101
|01
001
111
^01
001
110

من العميات الثنائية هي عميات الإزاحة لليسار مثلاً 23dec<<2dec= 10111bin << 2dec = 1011100bin = 92dec في سي نعبر عن ذلك printf("%d", 23 << 2); أو لليمين 23dec>>2dec= 10111bin >> 2dec = 101bin = 5dec في سي نعبر عن ذلك printf("%d", 23 >> 2); في الحالتين يحذف الفائض

tipتلميح

عملية الإزاحة لليسار (للأعداد الصحيحة) بمقدار واحد هي الضرب في 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 ، ولهما أهمية في التعامل مع المنظومات لاحقاً.

warningتحذير

لاتخلط بين تعريف متغير ليكون مؤشراً int *ptr; وبين عملية تحويل المؤشر إلى قيمة i=*ptr; الأولى تكون في جملة تعريف (على اليسار من المساواة فيها إن وجدت) والثانية تكون في أي جملة عادية

warningتحذير

لاتخلط بين تعريف متغير ليكون اسماً مستعاراً 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 هل عرفت لماذا يمكن لسي أن تكون طلاسم.

7.1.7 العمليات المختصرة

توفر لنا سي اسلوب العميات المختصرة فإذا أردت أن تقوم بعملية ثنائية (تأخذ معاملين مثل الجمع) ووضع الناتج في المعامل الأول كأن تجمع 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 واحد. وللعلم القبلية أسرع من البعدية

tipتلميح

الهدف الأساسي من العمليات المختصرة ليس توفير بعض النقرات على لوحة المفاتيح ولا تقليل من حجم برنامج سي بل الهدف هو تسريع البرنامج لأن المعالج يقوم بتلك العمليات بشكل أسرع مثلاً عملية i=i+1; هي بلغة التجميع mov AX,i; mov BX,1; add AX,BX; mov i,AX; ولكن ++i; هي mov AX,i; inc AX; mov i,AX;

7.1.8 الاختصارات Macros

من بين الأشياء التي تجعل عملك أسهل هو استخدام الاختصارات 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))
الأول يعرف اختصار فارغ والثاني يخبر cpp بأن يستبدل كلمة PRINT ب printf فإذا قررت في المستقبل استعمال وظيفة اخرى مثل g_printf مكان printf فقط غير هذا السطر التالية تعرف رقم اسمه MAX_NUMBER ربما نستخدمه ليمثل الحد الأعلى فإذا رغبت في زيادنه في كل البرنامج عدل هذا السطر فقط والأخيرة تعرف اختصار اسمه ADD يأخذ معاملين 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))
لاحظ استخدام "\" التي تعني المتابعة على السطر الذي يليه.

7.1.9 التفاعل مع المستخدم

بطريقة مشابهة لطريقة الكتابة في 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;
}
لأن المؤشران ptr1 و ptr2 لم يتم تحديد موقعهما فهما يشيران للموقع NULL وهو ليس مكان في الذاكرة أي لا يجوز أن تخزن فيه والصواب هو أن تجعلهما يشيران لموقع في الذاكرة مثل موقع i و j انظر البرنامج بعد التصحيح
/* 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;
}

7.1.10 الجمل الشرطية والتفرعات

كما لاحظنا يبدأ تنفيذ البرنامج من 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;
}
جملة "if" لن تظهر أبداً أما الجملة بعد "else" ستظهر دائماً لأن الصفر يعني غير متحقق

من تلك العمليات عمليات المقارنة مثلاً فحص المساواة "==" و فحص عدم المساواة "!=" وأكبر من ">" وأصغر من "<" أكبر من أو يساوي "<=" و أصغر من أو يساوي ">=" ويمكن استعمالها مع العمليات المنطقية الأساسية وهي "و المنطقية" "&&" و "أوالمنطقية" "||" وكلها عمليات ثنائية بينية (تأتي وسط معاملين) و النفي المنطقي "!" وهي عملية سابقة احادية تأتي قبل ما تنفيه. هذا مثال بسيط :

/* 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;
}
المثال التالي يوضح برنامج يسألك ادخال 3 أرقام ويخبرك فيما إذا كان الرقم الثاني محصور بين الرقمين الأول والأخير أي بطريقة رياضية 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;
}

7.1.11 الأولويات

الجدول سيكمل لاحقاً

الأعلى أولوية
() [] الأقواس وعناصر المنظومةltr
++ -- unary+ unary- ! (type) الزيادة والنقصان (القبلية والبعدية) وتغيير الإشارة و النفي المنطقي وتحديد النوعrtl
* / % الضرب والقسمة والباقيltr
+ - الجمع والطرحltr
<< >> السحب ولإضفة في سي++ltr
< <= > >= المتبايات أقل أقل أو يساوي أكبر أكبر أو يساويltr
== != فحص المساواة وعدمهاltr
&& "و" المنطقيةltr
|| "أو" المنطقيةltr
()?():() الشرطrtl
= += -= *= /= %= الإحلال والإحلال المختصرrtl
, الفاصلة تعيد آخر ناتجltr
الأقل أولوية

7.1.12 الحلقات التكرارية

للقيام بعمل أكثر من مرة يمكننا أن نستخدم الحلقات التكرارية باستعمال 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);
}

أما الصيغة العامة لعبارة for فهي

for([INIT] ; [COND] ; [UPDATE])
	LOOPBODY;
حيث INIT هي العبارة التي ينفذها قبل الدخول في الحلقة و COND هو شرط الإستمرار في الحلقة ويتم فحصة قبل الدخول في كل دورة و UPDATE هي العبارة التي يتم تنفيذها بعد الإنتهاء من كل دورة والعبارات INIT و COND و UPDATE كلها اختيارية أي أن التالية صحيحة for(;;) و for(i=0;;) for(;i<5;) و for(;;++i) وإذا أردت القيام بأكثر من عملية افصل بينها بفاصلة for(i=0,j=0;i<5;++i,j+=5) ويمكن تعريف المتغير داخل وحدة for هذا مثال for(int i=0;i<5;++i) { int j; }

For loops

يجب أن تنتبه إلى أن 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 أو أي تركيب آخر

warningتحذير

لا تعرف شيئاً داخل حلقة 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");
}
لقد بدأنا بقيم 10 ل i ثم فحصنا هل هي أكبر من 0؟ نعم؛ لهذا تابع، اكتب قيمة i وسطر جديد ثم اطرح منها واحد وافحص هل هي أكبر من الصفر ... وهكذا إلى أن نصل إلى أن قيمة 1 هل هي أكبر من صفر نعم اطبعها ثم أنقص منها واحد هل أصبحت أكبر من الصفر؟ لا؛ أنهي الحلقة التكرارية وقد طبعنا الأرقام من 10 إلى 1 وخرجنا عندما أصبحت قيمة i تساوي صفراً (لا تحقق الشرط) دون أن نطبع الصفر ثم تابعنا تنفيذ البرنامج

لاحظ يمكن أن نغير البرنامج ليصبح

/* 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.");
}
هذا البرنامج يحسب الوسط لعينة دون أن نعرف عددها يسألك عن العلامة الأولى فالثانية إلى أن تدخل له -1 لتخرج من الحلقة عندها نحسب الوسط ويمكن أن نستعمل while
/* 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.");
}
لابد أن الجزء الغامض بالنسبة لك هو لماذا بدأت في الأول n بسالب واحد وفي الثاني بصفر ولماذا طرحنا واحد من s و n في الثاني. جواب السؤال يكمن في تتبع البرنامج وملاحظات الآثار الجانبية ( وهي عبارة نكتبها لتؤدي مهمة فيستلزم القيام بعمليات بينية معينة لذلك للوصول للنتيجة تلك العمليات قد يغفل عنها المبرمج ) لقد وضعنا s التي تمثل المجموع بصفر لأننا ننوي جمع أرقام فوقه ولا نريد أن تؤثر قيمته الاستهلالية عليها فلو كانت واحد لكان s يحمل المجموع وواحد فالصفر هنا المحايد الجمعي. ووضع n بسالب واحد لأن العدد الأخير في الحلقة ليس رقماً نريده وإنما رمز للخروج في المثال الثاني عالجنا المشكلة بطرح واحد بعد الخروج من الحلقة. وإليك هذا المثال الذي يحسب مضروب رقم (حاصل ضرب الأرقام من الواحد إلى ذلك الرقم)
/* 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 مثلاً
/* goto.c: */
#include<stdio.h>
int main() {
	int i;
	goto lastline;
	printf("%d This will never print\n",i);
lastline:
	return 0;
}
ولكن يجب أن لا تستعمل هذه الطريقة فهي تصعب تتبع البرنامج وتعتبر ضعف في المبرمج وهي من مخلفات اللغات القديمة

7.1.13 المنظومات

المنظومة 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; نستخدم الحاصرتان ثم القيم تفصل بينها فاصلة

warningتحذير

لا يجوز أن تكتب في عنصر خارج المنظومة مثلاً عند حجز منظومة بحجم عشرة 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);
}
ولكن يجب أن تنتبه أنه لا يجوز حجز منظومة ساكنة(أو عامة) بحجم غير ثابت أي أن يكون الرقم بين القوسين المربعين عند تحديد حجم المنظومة متغيراً بل ثابتاً (انظر static variables ) أي أن هذا المثال خطأ
/*  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() {
 /* ... */
}
ولكن في مصنفات سي الحديثة ذلك ممكن للمنظومات اللتي تكون ضمن وظيفة مثل main وتحجز مساحتها من المكدس الخاص بالوظيفة كما في المثال اللاحق ولكن هذا ممنوع في مواصفات ANSI/ISO
/*  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);
}
ولأن تلك المساحة من الذاكرة مهمة يفضل تجنب عمل هكذا منظومات لأننا نفضل وضع التعريف في البداية ثم نعرف الحجم (لاحظ أن تعريف a جاء بعد استدعاء وظيفتين) ولأننا قد نرغب في تعديل حجمها (زيادةً أو نقصان) فيما بعد وهذا غير ممكن ، والأفضل أن تحجز ديناميكياً (تحدد وقت التنفيذ ويمكن تقليصها أو مدها فيما بعد) ولحجز مساحة معينة ديناميكياً استعمل وظائف مكتبة سي القياسية مثل 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 و delete لحجز وتحرير الذاكرة اضافة إلى وظائف المكتبات القياسية ولكن لا يجوز الخلط بينهما كحجز ب new و تحرير ب free ولا يمكن تغيير حجم المنظومة المحجوزة ب new وطريقة استعمالها هي بكتابة new ثم النوع ثم أقواس مربعة بينها الحجم ولتحريرها نكتب delete ثم أقواس مربعة ثم اسم المنظومة أي المؤشر على أولها
/* 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;
}
ويمكننا استخدام وظيفة qsort من مكتبة سي القياسية لترتيب المنظومات

7.1.14 السلاسل النصية

من أكثر الأمور تعقيداً في سي و سي++ هو التعامل مع السلاسل النصية ويمكنك القول إذا كان برنامج يهدف للتعامل مع السلاسل النصية بشكل كبير استعمل 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); ثم تضع قيمتها باستعمال وظائف مكتبة سي القياسية

tipتلميح

بعض اللغات مثل Basic ومشتقاتها تكون السلسلة النصية عبارة عن رقم يمثل الطول ثم منظومة المحارف

warningتحذير

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

يمكنك طباعة السلسلة النصية ب %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 التي يكون أول معامل لها هو السلسلة والثاني هو الحد الأقصى للحجم

7.1.15 المنظومات متعددة الأبعاد

لنفرض أنك تريد منظومة عناصرها من نوع سلسلة نصية مثلاً منظومة عناصرها {"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
ولحجز هذه المصفوفة في وقت التنفيذ نقوم أولاً بحجز المنظومة الكبرى التي عناصرها 3 مؤشرات إلى أعداد صحيحة أي 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;
}
لنكتب برنامج لجمع مصفوفتين

7.1.16 معاملات وظيفة main

اكتب البرامج التالي وسمه 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 ولاحظ الفرق

7.1.17 الوظائف والنماذج

حتى الآن كان معظم تعاملنا يتم من خلال الكلمات المفتاحية والقليل من الوظائف من بين آلاف الوظائف التي توفرها مكتبة سي القياسية ولكن كيف تعمل وظيفة خاصة بك لكي تستعملها فهي توفر عليك تكرار كتابة الأجزاء المكررة في برنامج ربما باختلاف بسيط هنا وهناك ؟ الأجزاء المختلفة في كل مرة نسميها معاملات تمرر للوظيفة في كل مرة يتم استدعائها فيها وعند الإنتهاء يمكن أن تعيد هذه الوظيفة قيمة قد تكون ناتج العمليات التي قامت بها وفي الغالب تكون اشارة إذا نجحت العملية أم لا. لقد كتبنا وظيفة قبل الآن، إنها وظيفة 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;
}
ولكن إذا أردت استدعاء الوظيفة في مكان ما قبل الوظيفة نفسها فعليك أن تخبر سي أن هذه الوظيفة موجودة وأنها تأخذ الصيغة الفلانية (نوع المعاملات وعددها ونوع الناتج) وهو يكون ببساطة بذكر نموذج الوظيفة function prototype قبل استدعائها وهو ذكر نوع الناتج و اسم الوظيفة وأقواس بينها أنواع المعاملات (وإن شئت أسماء للدلالة على المعنى) ثم فاصلة منقوطة كما في المثال التالي
/* 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;
}
ويفضل أن يكون هذه النماذج فوق ال main في بداية البرنامج. ويمكن اعطاء بعض المعاملات قيمة تلقائية Default value يأخذها المتغير في حال لم تعطى ويكون ذلك في متن الوظيفة أو في نموذجها ويفضل في نموذجها وذلك بكذ القيمة هكذا 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);
}
هذا البرنامج لن يعمل كما يجب لأن استدعاؤه يعني حجز متغير جديد باسم i ثم وضع قيمته بقيمة a ثم زيادة الأول i بمقدار واحد فتصبح قيمة i هي 2 ثم العودة وتحرير هذا المتغير مع بقاء قيمة 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);
}
ما حدث عند استدعاء هذه الوظيفة يعني عمل متغير جديد من نوع مؤشر لعدد صحيح ووضع قيمته بعنوان 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);
}
عند استدعاء الوظيفة تحجز متغير جديد i من نوع اسم مستعار ل a وتقوم بزيادة قيمة i ولكن لأن i ليس سوى اسم آخر a فإن هذا يتسبب في زيادة a. هذه الطريقة غير مستعملة في مكتبة سي القياسية.

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

7.1.18 الوظائف ذاتية الاستدعاء

يمكن لوظائف سي أن تستدعي وظائف أخرى للقيام بعملها ويمكنها أيضا أن تستدعي نفسها (بمعاملات أخرى مثلاً) ولكن حتى نضمن أنها تعمل يجب أن نوفر حالة مؤكدة الحدوث بأن عملية الاستدعاء هذه ستتوقف عند مرحلة معينة قبل أن يطفح المكدس وإلا فإن البرنامج سيقتل أو سيعلق الجهاز وإذا أمكن الاستغناء عن هذه الطريقة باستعمال الحلقات التكرارية فهذا أفضل لأن الأخيرة أسرع ولا تستنزف الذاكرة لنأخذ مثال حساب المضروب كمثال فمضروب أي رقم يساوي الرقم ضرب مضروب الرقم الذي قبله علماً أن مضروب الصفر 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;
}
ولكن البرنامج الذي كتبناه قبلاً باستعمال for أفضل ،ولكن أحياناً تكون كتابة البرنامج بالاستدعاء الذاتي أسهل بكثير مقارنة بالحلقات التكرارية وغالباً ما تكون طبيعة البرنامج على شكل إذا أردنا أن نحل هذا المسألة عند سين يجب أن نعرف كيف نحلها عند مرحلة قبل سين ولنحل تلك المرحلة علينا أن نعرفف مرحلة قبلها حتى نصل لمرحلة حلها معروف مثل مسألة برج هانوي وهي أحجية قديمة (لعبة ذهنية) عبارة عن ثلاث قواعد(مرقمة 1 2 3) فوق احدها (لنقل رقم واحد) عمود من قطع كبيرة فأصغر فأصغر على شكل طوابق برج المطلوب نقل هذا البرج من المكان الذي هو فيه إلى أحد القواعد الأخرى الخالية (لنقل رقم 2) بشرط أن تحمل في كل مرة طابق واحد وتضعه فوق قطعة أكبر منه (لا يجوز أن تأتي قطع كبيرة فوق صغيرة لأن البرج سينهار) هنا نستخدم القاعدة الأخرى كمكان مؤقت ، المطلوب كتابة برنامج يسألك عن عدد الطوابق ثم يكتب لك الحل على كل حرك من قطعة من قاعدة رقم كذا إلى كذا. حل المسألة لعدد غير محدد من الطوابق معقد جداً ولكن لو فكرنا بكتابة وظيفة تحل عملية نقل n قطعة من مكان from إلى مكان to، لا تزال المسألة عامة جداً ، ولكن كيف تنقل قطة واحدة من مكان from إلى to ؟ ببساطة حركها من from إلى to. حسنا كيف تحرك قطعتين من from إلى to ؟ حرك قعطة من from إلى المكان المؤقت temp (ليس from وليس to بل الثالث) ثم حرك قطعة من from إلى to ثم ضع القطعة الصغيرة الموجودة في المكان المؤقت فوق to. يمكننا القول أن نقل n قطعة من from إلى to يكون بنقل n-1 قطعة من from إلى temp بطريقة ما ثم نقل قطعة واحدة من from إلى to ثم أخذ ال n-1 قطعة من temp إلى to
/* 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
يمكننا من خلال استعمال static لتحديد المتغيرات التي لا يلزم منها سوى نسخة واحدة في الوظائف لكي نجعل

7.1.19 الوظائف لا محدودة المعاملات

إذا تأملت وظيفة 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 لها لكي تحرر ما حجزته. يمكنك أن تكتب وظيفة تحسب القيمة العظمى لمعاملاتها كتمرين.

7.1.20 تراكيب وصنوف البيانات

يمكنك في سي تعريف أنواع بيانات جديدة 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
حيث في هذا المثال نفحص إذا كان المصنف من عيار 16 بت أو 32 بت ونعرف 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;
تسمى المتغيرات x و y بالبيانات الأعضاء member data يتم الوصول لها بوضع نقطة بعد اسم المتغير مثلاً p1.x أو -> عند التعامل مع مؤشرات لمتغير التركيب ptr->x مثلاً printf("%f",p1.x); ويمكن أن يكون مؤشر للتركيب عضواً فيه ولكن لا يجوز أن يكون التركيب عضواً لنفسه لأن المؤشر من جميع الأنواع هو عنوان في الذاكرة ويمكن معرفة كم بايت يحتاج دون معرفة حجم ما يشير له مثلاً
typedef struct {
	ListNode *next;
	int value;
} ListNode;
tipتلميح

يفضل تمرير التراكيب للوظائف المختلفة على شكل مؤشر لتوفير الذاكرة

يمكن الاستفادة من ميزة تسمى حقل البتات bit feild حيث نخصص لكل متغير حيز من البتات بحيث تتوزع على المتغيرات المختلفة داخل التركيب دون اشتراط أن تكون من مضاعفات الثمانية
typedef struct {
	unsigned int m:23;
	unsigned int b:8;
	unsigned int s:1;
} MyFloat;
حيث في هذا المثال نخصص 23 بت ل m و 8 ل b و بت ل s وتوفر سي إمكانية وضع أكثر من متغير على نفس المساحة من الذاكرة بواسطة union مثلاً
typedef union {
	unsigned char u;
	signed char s;
} MyChar;
في هذا المثال يشترك u مع s في الذاكرة بحيث إذا غيرت من قيمة أي منها ستتغير قيمة الآخر هذا مثال كامل يوضح ذلك
#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;
}
فإذا أدخلت قيمة أقل من 128 فإنك ستجد القيمتان متساويتان دائماً أما إذا أدخلت قيمة أكبر من ذلك فإن قيمة العضو s ستكون s=u-256 لأنهما تأخذان نفس المووقع في الذاكرة ولكن عند الوصول لها من s تفسر على أنها تحتوي بت الإشارة أما عند استعمال u فهي لا تحتوي بت الإشارة

7.1.21 هذه ليست سي++ ؟! أين iostream و cout ؟

على عكس الكثير من الكتب أجد مكتبة سي الكلاسيكية أسهل وأكثر مرونة من مكتبة سي++ ربما لأني تعلمتها قبل فترة طويلة وتعودت عليها، أو ربما لأني لن أشعر بالغربة عند استعمال لغات أخرى تم عملها بحيث تشبه مكتبة سي القياسية مثل 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 البرمجة الموجهة للكائنات


<< السابق كتاب لينكس الشامل التالي >>