مرحبا بالعالم
يطبع البرنامج التالي العبارة مرحبًا بالعالم!
في مجرى الخرج القياسي (standard output stream):
#include <iostream> int main() { std::cout << "مرحبا بالعالم!" << std::endl; }
يمكنك رؤية التجربة الحية على Coliru.
تحليل البرنامج
لنحلل كل جزء من شيفرة البرنامج بالتفصيل:
#include <iostream>
هو موجّه معالجة مسبقة (preprocessor directive)، ويتضمن محتوى iostream
، وهي ترويسة ملف C++ القياسي (standard C++ header file).
الكلمة iostream
هي ترويسة ملف مكتبة قياسية (standard library header file)، وتحتوي على تعريفات مُجريَا الدخل والخرج القياسيين (standard input and output streams). هذه التعريفات مُتضمنة في فضاء الاسم std
، كما هو موضح أدناه.
يوفر مُجريا الدخل/الخرج القياسيين (I/O) طريقة للبرامج يمكنه عبرها جلب مدخلات من مخرجات خاصة بنظام خارجي، والذي يكون طرفية في العادة.
int main() { ... }
تُعرِّف هذه الشيفرة دالة جديدة باسم main
تستدعى عند تنفيذ البرنامج. يجب أن تكون هناك دالة main
واحدة لكل برنامج C++، ويجب أن تعيد دائمًا عددًا من النوع int
. يمثل int
نوع القيمة التي تعيدها الدالة والتي تمثل رمز الخروج (exit code) الخاص بالدالة main
.
البرامج ذات رمز الخروج المساوي للقيمة 0
أو EXIT_SUCCESS
تُعد ناجحة من قبل النظام الذي ينفّذ ذلك البرنامج. ترتبط كل رموز الخروج الأخرى بأخطاء تحددها هي.
في حال عدم استخدام التعليمة return
، ستعيد الدالة main
(وبالتالي البرنامج نفسه) القيمة 0
افتراضيًّا. في هذا المثال لا نحتاج إلى كتابة ;return 0
.
جميع الدوال الأخرى، باستثناء تلك التي تُعيد النوع void
، يجب أن تُعيد قيمة بشكل صريح من النوع الذي يُفترض أن تعيده، أو لا ينبغي أن تعيد أي قيمة على الإطلاق.
std::cout << "مرحبا بالعالم!" << std::endl;
تطبع هذه التعليمة السلسلة النصية "مرحبا بالعالم!" في مجرى الخرج القياسي، وتتألف من الأجزاء التالية:
-
std
: هي فضاء اسم، و::
هو عامل تحليل النطاق (scope resolution operator) الذي يسمح بالبحث عن الكائنات بأسمائها ضمن فضاء الاسم. هناك العديد من فضاءات الاسم. هنا نستخدم::
لإظهار أننا نريد استخدامcout
من فضاء الاسمstd
. لمزيد من المعلومات، ارجع إلى المقال Scope Resolution Operator في توثيق ميكروسوفت. -
std::cout
: هو كائن مجرى الخرج القياسي (standard output)، وهو مُعرّف فيiostream
، ويُطبع أيّ شيء يُمرَّر إليه في مجرى الخرج القياسي (stdout
). -
>>
: هي، في هذا السياق، عامل الإدراج (stream insertion operator)، ويُسمى كذلك لأنه يدرج كائنًا في كائن المجرى (stream object). تُعرِّف المكتبة القياسية العامل>>
لإدراج بعض أنواع البيانات في مجاري الخرج. تدرج التعليمةstream << content
المحتوىcontent
في المجرىstream
ثم تعيد المجرى نفسه بعد تحديثه. يسمح ذلك بإجراء الإدراجات المتسلسلة، مثلًا، تطبع التعليمة std::cout << "Foo" << " Bar";
السلسة النصية "Foo Bar" في سطر الأوامر. -
"مرحبا بالعالم!": هي سلسلة نصية حرفية (character string literal). عامل الإدراج الخاص بالسلاسل النصية مُعرّف في الملف
iostream
. -
std::endl
: هو كائن خاص لمعالجة مجرى I/O، وهو مُعرّف في الملفiostream
. إنّ إدراج معالج في مجرى ما يغير حالة ذلك المجرى. يقوم معالج المجرىstd::endl
بعملين: أولًا، يدرج محرف نهاية السطر (end-of-line character)، ثم ينقل البيانات (flushes) الموجودة في المخزن المؤقت (stream buffer) الخاص بالمجرى لجعل النص يظهر في سطر الأوامر. يضمن هذا أنّ البيانات المُدرجة في المجرى ستظهر بالفعل في وحدة التحكم. (تُخزنّ بيانات المجرى عادة في مخزن مؤقت، ثم تُنقَل على دفعات، إلا إذا أمرت بنقلها فوريًا.)
هناك طريقة بديلة تُجنِّب نقل البيانات من المخزن المؤقت، وهي:
std::cout << "Hello World!\n"; // سطرًا جديدًا \n يمثل
-
الفاصلة المنقوطة ;
: تُخطر الفاصلة المنقوطة (;
) المُصرّف (compiler) بأنّ التعليمة البرمجية قد انتهت. تتطلب كل عبارات C++ وتعريفات الأصناف استخدام فاصلة منقوطة في النهاية.
التعليقات
التعليقات هي جزء من الشيفرة يتجاهلها مصرف C++. تُستخدم التعليقات لتوضيح بعض الجوانب التي قد تكون غامضة بخصوص تصميم أو طريقة عمل البرنامج.
هناك نوعان من التعليقات في C++:
التعليقات السطرية (Single-Line Comments)
تجعل الشرطتان المائِلتان //
النص الذي يليها وحتى بداية السطر الجديد تعليقًا:
int main() { // هذا تعليق سطري int a; // هذا تعليق سطري كذلك int i; // هذا أيضا تعليق سطري }
التعليقات الكُتلية (Block Comments)
يُستخدَم المحرفان /*
للإعلان عن بداية تعليق كتلي، فيما يُستخدَم المحرفان */
للإعلان عن نهاية التعليق. يُفسَّر النص الموجود بين العبارتين على أنه تعليق، حتى لو كان النص الموجود بينهما شيفرة C++ صالحة. يسمى هذا النوع من التعليقات عادة تعليقات "C-style"، لأنّ صياغة هذ النوع من التعليقات موروثة من سلف C++، أي اللغة C:
int main() { /* * هذا تعليق كتلي */ int a; }
يمكنك كتابة ما تشاء في التعليقات الكتلية لكن عندما يجد المصرّف رمز نهاية التعليق*/
، فإنه ينهي التعليق الكتلي:
int main() { /* تعليق كتلي يمتد * على عدة أسطر * وينتهي في السطر التالي */ int a; }
يمكن أيضًا أن تبدأ التعليقاتُ الكتلية وتنتهي في سطر واحد. مثلا:
void SomeFunction(/* الوسيط الأول */ int a, /* الوسيط الثاني */ int b);
أهمية التعليقات
كما هو الحال مع جميع لغات البرمجة، توفر التعليقات العديد من الفوائد، منها:
- توثيق ضمني للشيفرة البرمجية لتسهيل قراءتها وصيانتها
- شرح الغرض من الشيفرة البرمجية ودوالها
- توفير تفاصيل حول تاريخ الشيفرة أو المنطق وراءها
- وضع حقوق الطبع والنشر/التراخيص، أو ملاحظات حول المشروع، أو شكر خاص، أو التنويه بالمساهمين، وما إلى ذلك مباشرة في الشيفرة المصدرية (source code).
من جهة أخرى، فإنّ للتعليقات جانبًا سلبيًا كذلك:
- يجب تعديلها لتعكس أي تغييرات في الشيفرة
- الإفراط في إدراج التعليقات قد يؤثر سلبًا على مقروئية الشيفرة.
يمكن تقليل الحاجة إلى التعليقات عبر كتابة شيفرة واضحة ومُوثقة ذاتيًا (self-documenting). أحد الأمثلة على ذلك هو استخدام أسماء توضيحية للمُتغيرات والدوال والأنواع. وتوزيع المهام المترابطة منطقيًا في دوال منفصلة.
تعليق الشيفرة واختبارها
أثناء التطوير، يمكن أيضًا استخدام التعليقات لتعطيل أجزاء من الشيفرة بسرعة دون حذفها. غالبًا ما يكون هذا مفيدًا في مرحلة الاختبار أو التنقيح (debugging)، ولكن ينبغي محو تلك التعليقات بعد الانتهاء. يشار إلى هذا غالبًا باسم "تعليق الشيفرة" (commenting out).
بالمثل، الاحتفاظ بالإصدارات القديمة من أجزاء من الشيفرة في التعليقات لجعلها متاحة للمراجعة قد يكون مزعجًا، لأنّ ذلك يُراكم الكثير من الشيفرة غير المستخدمة، ولا يضيف قيمة تذكر موازنةً بالاطلاع على تاريخ الشيفرة عبر نظام إصدارات (versioning system).
عملية التصريف القياسية في C++
تُنتَج برامج C++ القابلة للتنفيذ عادة بواسطة المُصرّف (compiler)، وهو برنامج يترجم الشيفرة من لغة برمجة إلى شيفرة تنفيذية تفهمها الآلة وتُنفذ على الحاسوب من قبل المستخدم النهائي. استخدام المصرّف لتصريف الشيفرة يسمى عملية التصريف (compilation process).
ورثت C++ آلية التصريف من سلفها، أي اللغة C. فيما يلي قائمة توضح الخطوات الرئيسية الأربع للتصريف في C++:
-
ينسخ المعالج الأولي (preprocessor) للغة C++ محتويات كل ملفات الترويسات (header files) المتضمنة في ملف الشيفرة المصدري، ويولد «شيفرة عملية بدل» (macro code، ويعوّض الثوابت الرمزية (symbolic constants) المعرفّة باستخدام
#define
بقِيمهما. - تُصرَّف الشيفرة المصدرية الموسعة التي أُنتِجت بواسطة المعالج الأولي لـ C++ إلى لغة التجميع (assembly language) المناسبة لنظام التشغيل.
- تُصرَّف الشيفرة المجمّعة التي أُنتِجت بواسطة المصرّف إلى تعليمات مُصرّفة (object code) مناسبة لنظام التشغيل.
- تُربَط التعليمات المُصرَّفة المُولّدة من قبل المجمّع (assembler) مع ملفات التعليمات المُصرَّفة (object code files) الخاصة بدوال المكتبات المستخدمة لإنتاج الملف القابل للتنفيذ.
ملاحظة: أحيانًا تُربط بعض الشيفرات المُصرَّفة معًا، ولكن ليس لغرض إنشاء برنامج نهائي، إذ يمكن تحزيم (packaging) الشيفرة "المربوطة" لأجل استخدامها من قبل برامج أخرى. الحزم الناتجة هي ما يشير إليه مبرمجو C++ بالمكتبات (libraries).
تدمج العديد من مصرّفات C++ أو تفكّك بعض مراحل عملية التصريف لتسهيل العملية، أو لأجل التحليل الإضافي. يستخدم مُبرمجو C++ أدوات مختلفة، لكنها جميعًا تتبع المقاربة أعلاه.
الدوال
الدالة هي كتلة من الشيفرة تحتوي سلسلة من العبارات. يمكن للدوال قبول وسائط (arguments) أو قيم، ويمكن أن تعيد قيمة واحدة، أو قد لا تعيد أي قيمة. لاستخدام دالة، يجب استدعاؤها مع تمرير وسائط إليها، ثم تعيد قيمة.
لكل دالة بصمة نوعيّة (type signature)، والتي تمثل أنواع وسائطها، ونوع القيمة المعادة. الدوال مستوحاة من مفهومَي الإجراء (procedure) والدالة في الرياضيات.
ملاحظات:
- دوال C++ هي إجراءات (procedures) بالأساس، ولا تتبع بدقة قواعد الدوال الرياضية وفق مفهوم علوم الرياضيات.
- تؤدي الدوال في العادة مهام محددة ويمكن استدعاؤها من أجزاء أخرى من البرنامج. كما يجب التصريح عن الدالة وتعرِيفها قبل استدعائها في البرنامج.
- قد تُخفى تعريفات الدوال الشائعة والمهمة في ملفات أخرى مُضمّنة، فذلك يسهِّل إعادة استخدامها في البرامج. وهذا أحد الاستخدامات الشائعة لملفات الترويسات (header files).
التصريح عن دالة
التصريح عن دالة (Function Declaration) هو عملية الإعلان عن وجود دالة، مع توضيح اسمها وبصمتها النوعيّة للمصرف. ويتبع الصياغة التالية:
// الدالة التالية تقبل عددًا صحيحًا وتعيد عددًا صحيحًا int add2(int i);
في المثال أعلاه، تخبر int add2(int i)
المصرّف بالمعلومات التالية عن الدالة المُصرَّح عنها:
-
نوع القيمة المعادة هو
int
. -
اسم الدالة هو
add2
. -
عدد وسائط الدالة هو 1:
-
الوسيط الأول من النوع
int
. -
سيُشار إلى الوسيط الأول داخل الدالة بالاسم
i
.
-
الوسيط الأول من النوع
اسم الوسيط اختياري؛ إذ يمكن التصريح بالدالة كما يلي:
int add2(int); // يمكن حذف اسم الوسيط
وفقًا لقاعدة التعريف الواحد (one-definition rule)، لا يمكن التصريح بدالة ذات بصمة (signature) نوعيّة معينة أو تعريفها أكثر من مرة واحدة في كامل شيفرة C++ المرئية للمصرّف. بمعنى آخر، لا يمكن إعادة تعريف دالة ذات بصمة نوعية معيّنة إلا مرة واحدة فقط. وبالتالي، فالشيفرة التالية غير صالحة في C++ وسيطلق المصرف خطأ عند تنفيذها:
// int هي دالة من النوع add2 المصرف سيعلم أن int add2(int i); // (int) -> int // لا يمكن إعادة تعريف نفس الدالة بنفس البصمة int add2(int j); // (int) -> int
إذا لم تُعِد الدالة أي قيمة، فإنّ نوع القيمة المعادة سيُكتب void
. إذا لم تأخذ الدالة أيّ معاملات، فينبغي أن تكون قائمة المعاملات فارغة.
// الدالة التالية لا تأخذ أي وسائط، ولا تعيد أي قيمة void do_something();
لاحظ أنه ما يزال بمقدور الدالة do_something التأثير في المتغيرات التي يمكنها الوصول إليها.
استدعاء الدالة
يمكن استدعاء الدالة بعد التصريح عنها. على سبيل المثال، يستدعي البرنامج التالي الدالة add2
مع القيمة 2
داخل الدالة main
:
#include <iostream> int add2(int i); // add2 تعريف الدالة int main() { // في هذا الموضع add2(2) سيتم تقييم std::cout << add2(2) << "\n"; // وستُطبع النتيجة return 0; }
بالنظر إلى الشيفرة السابقة، نجد أنَّه ما تزال الدالة add2 تحتاج إلى توفير طريقة تنفيذ لها بالإضافة إلى تعريفها (جسم الدالة)، رغم أن تنفيذ الدالة لا يظهر مباشرة في الشيفرة، إذ يمكن جلبه من ملف آخر بربط هذا الملف مع الشيفرة التي تستدعي الدالة.
تمثل add2(2)
صياغة استدعاء دالة.
تعريف الدالة
تعريف الدالة يشبه التصريح عنها، إلا أنه يحتوي أيضًا على الشيفرة الذي ستُنفّذ عند استدعاء الدالة، هذه الشيفرة تُسمى جسم الدالة (function body). فيما يلي مثال عن تعريف الدالة add2
:
// i القيمة التي ستُمرر إلى الدالة سيشار إليها بالاسم int add2(int i) { // بين القوسين يمثل داخل نطاق أو جسم الدالة int j = i + 2; return j; }
زيادة تحميل الدوال
يمكنك إنشاء عدة دوال تشترك في نفس الاسم، ولكن مع اختلاف المعاملات، أي تختلف بصمة الدالة فقط، وهذا ما يعرف «بزيادة تحميل دالة» (Function Overloading).
// التنفيذ الأول int add2(int i) { int j = i + 2; return j; } // التنفيذ الثاني int add2(int i, int j) { int k = i + j + 2; return k; // }
كلتا الدالتين تحملان الاسم add2
، بيْد أنّ الدالة المُنفّذة تعتمد على عدد وأنواع المعاملات المُمررة في الاستدعاء. في معظم الحالات، يمكن لمصرّف C++ أن يحدد الدالة المراد استدعاؤها. لكن في بعض الحالات يجب ذكر النوع بوضوح. فسيتم تقييم الشيفرة المحتواة في التعريف الأول عند استدعاء الدالة نع معامل واحد بالشكل add2(1)
. وفي حال استدعاء الدالة مع معاملين بالشكل add2(1, 3)
، فسيتم تقييم الشيفرة المتضمنة في التعريف الثاني بدلًا من التعريف الأول.
المعاملات الافتراضية
يمكن تحديد القيم الافتراضية لمعاملات الدالة (Default Parameters) في تعريفات الدوال فقط.
// 7 هي b القيمة الافتراضية للمعامل int multiply(int a, int b = 7); // جسم الدالة int multiply(int a, int b) { return a * b; }
في هذا المثال، يمكن استدعاء multiply()
مع معامل واحد أو مُعاملين. في حال تمرير معامل واحدة فقط، فستكون القيمة الافتراضية للمعامل b
هي 7 وسيتم ضرب القيمة الممررة تلك بالمعامل الافتراضي 7 آنذاك. يجب وضع المعاملات الافتراضية في الأخير على النحو التالي:
// تعريف صحيح int multiply(int a = 10, int b = 20); // مُقدّمة int a تعريف غير صحيح لأن int multiply(int a = 10, int b);
استدعاءات خاصة للدوال - العوامل
توجد استدعاءات خاصة في C++ للدوال، وهي ذات صياغة مختلفة عن الصياغة التقليدية name_of_function(value1, value2, value3)
. المثال الأكثر شيوعا هي العوامل (operators). وهي تسلسلات أحرف خاصة تحوَّل إلى استدعاءات دوال من طرف المصرّف، من أمثلة ذلك، !
و +
و -
و *
و %
و <<
وغيرها. عادةً ما ترتبط هذه المحارف الخاصة باستخدامات غير برمجية، أو تُستخدم للتبسيط (على سبيل المثال، المحرف +
يمثل عادةً مفهوم الإضافة). تعالج C++ هذه التسلسلات؛ وتحوّل كل عامل إلى استدعاء الدالة المقابلة. على سبيل المثال، التعبير التالي:
3+3
يكافئ استدعاء الدالة التالية:
operator+(3, 3)
تبدأ جميع أسماء دوال العوامل بـ operator
. في لغة C، لا يمكن تعيين معان جديدة لأسماء دوال العوامل عبر كتابة تعريفات إضافية ذات بصمات نوعية مختلفة، في C++، هذا جائز. يشار إلى ربط تعريفات إضافية بنفس اسم الدالة باسم التحميل الزائد للعامل (operator overloading) في C++، وهو اصطلاح شائع نسبيًا، ولكنه غير عام في C++.
التصريح عن الدوال وقواعد مرئيتها
في C++، يجب التصريح عن الشيفرة أو تعريفها قبل استخدامها. على سبيل المثال، ينتج عن الشيفرة التالية خطأ في وقت التصريف:
int main() { foo(2); // (*) } void foo(int x) {}
سيطلق خطأ، لأن الدالة foo استُدعِيت في السطر (*) قبل أن تُعرّف، إذ لا ترى الدالة main هذا التعريف المتأخر للدالة foo بعد استدعائها ضمنها.
هناك طريقتان لحل هذه المشكلة: إما التصريح عن foo()
أو تعريفها قبل استخدامها في main()
. هذا مثال على ذلك:
// ووضع جسمها أولا foo التصريح عن الدالة void foo(int x) {} int main() { // الآن foo يمكن استدعاء الدالة foo(2); }
من الممكن أيضًا التصريح المسبق (forward-declaration) عن الدالة عن طريق وضع تصريح عن قالبها (prototype declaration) قبل موضع استدعائها والذي يحدد نوع القيمة المعادة واسم الدالة وعدد وسائطها وأنواعها، ثم تعريف جسمها لاحقًا:
// foo تصريح عن قالب لدالة باسم void foo(int); int main() { foo(2); // (*) } // ينبغي أن تطابق تصريح القالب أعلاه void foo(int x) { // هنا foo تعريف جسم }
أصبحت الدالة foo مرئية، لذا يمكن استدعاؤها الآن ضمن الدالة main في السطر (*) رغم أنها لم تُعرّف بعد.
يجب أن يحدد تصريح النموذج نوع القيمة المعادة (void
)، واسم الدالة (foo
)، وأنواع المتغيرات في قائمة الوسائط (int
)، لكنّ أسماء الوسائط غير ضرورية.
يمكن وضع تصريحات قوالب الدوال في ترويسة الملف:
// foo.h الملف // تصريح عن قالب دالة void foo(int);
ثم تقديم التعريف الكامل في موضع آخر:
// foo.cpp --> foo.o // foo تضمين الملف الذي يحوي قالب الدالة #include "foo.h" // foo تعريف جسم الدالة void foo(int x) { }
ثم، بمجرد تصريف الشيفرة، يمكن ربط كائن الملف (object file) المقابل foo.o
بالكائن المصرّف حيث يتم استخدامه في مرحلة الربط، main.o
:
// main.cpp --> main.o // foo تصريح عن قالب الدالة #include "foo.h" int main() { foo(2); } // (*)
استدعاء الدالة foo ممكن هنا، لأنه سبق التصريح عنها، وتعريف قالب ومتن الدالة foo مربوط عبر كائنات الملفات (object files).
يحدث الخطأ "رمز خارجي غير محلول" (unresolved external symbol) إذا صادف المصرّف تصريحَ قالب واستدعاء لدالة، دون وجود لجسمها (body). قد يكون حل هذه الأخطاء معقدًا، لأنّ المصرّف لن يبلِّغ عن الخطأ حتى مرحلة الربط النهائية، ولن يعرف السطر الذي يجب الانتقال إليه في الشيفرة لإظهار الخطأ.
المعالج الأولي
المعالج الأولي (Preprocessor) هو جزء مهم من المصرّف إذ يقوم بتعديل الشيفرة المصدرية، وحذف بعض البتات، أو تغييرها، وإضافة أشياء أخرى.
في الملفات المصدرية، يمكننا تضمين مُوجِّهات (directives) للمعالج الأولي. تخبر تلك الموجِّهات المعالج الأولي بتنفيذ إجراءات محددة. يبدأ الموجّه بالرمز #
في سطر جديد مثل:
#define ZERO 0
من أشهر مُوجهات المعالج الأولي، الموجهات التالية:
#include <something>
يأخذ هذا الموجه something
ويُدرجه في ملفك حيث يظهر الموجه. مثلا، يبدأ برنامج "مرحبا بالعالم!" بالسطر التالي:
#include <iostream>
يضيف هذا السطر الدوال والكائنات التي تتيح لك استخدام المدخلات والمخرجات القياسية. لا تحتوي اللغة C (والتي تستخدم أيضًا المُعالج الأولي) نفس القدر من ترويسات الملفات كخَلَفها C++، كما أنه يمكنك في C++ استخدام جميع ترويسات C.
المُوجِّه الثاني في الأهمية هو على الأرجح الموجّه التالي:
#define something something_else
يخبر هذا المُوجِّه المعالج الأولي بأنه يجب أن يبدِّل something
مكان كل ظهور لـ something_else
في الملف الحالي. يمكن أن يجعل هذا الموجّه بعض الأشياء تتصرف مثل دوال، لكنه مفهوم متقدم أشرنا إليه قبل قليل على أنه «شيفرة عملية بدل» (macro code)، ولن نتطرق إليه الآن.
something_else
ليس ضروريًا، ولكن في حال عدم إضافة something
، سيُحذَف كل ظهور لـ something
.
هذا مفيد جدا، إذ يمكن استخدامه مع المُوجِّهات #if
و #else
و #ifdef
. وفق الصياغة التالية:
#if something == true // شيفرة #else // شيفرة أخرى #endif #ifdef thing_that_you_want_to_know_if_is_defined // شيفرة #endif
تدرِج هذه الموجِّهات الشيفرة الموجودة في البت الصحيح، وتحذف الآخرين. يمكن استخدام هذا لتضمين أجزاء من الشيفرة حصرًا في أنظمة تشغيل معينة دون الحاجة إلى إعادة كتابة الشيفرة بالكامل.
هذه المقالة جزء من سلسلة مقالات عن C++.
ترجمة -وبتصرّف- للفصل Chapter 1: Getting started with C++ من الكتاب C++ Notes for Professionals
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.