في عالم الحوسبة، تُشكِّل البرمجيات (Software) و العتاد (Hardware) طبقتين مختلفتين من التجريد الأولى تعتمد على لغات سهلة القراءة، بينما الثانية تعمل بتفاصيل فيزيائية معقدة. هنا يأتي دور المُترجم ليغلق هذه الفجوة، مُحققًا التوافق بين البرمجة المرنة وكفاءة العتاد، مما يسمح للمطورين بتحسين الأداء، واستهلاك الطاقة، وغيرها من المعايير الحيوية وفقًا لاحتياجات التطبيق
يُعد المُترجم مصطلحًا شاملاً يشمل المُصرِّف (Compiler)، والمُفسِّر (Interpreter)، والمُجمِّع (Assembler)، حيث يعمل كجسرٍ يربط بين مستويات اللغات الحاسوبية المختلفة:
- اللغات عالية المستوى (مثلpython) القريبة من فهم البشر.
- اللغات متوسطة المستوى (مثل Bytecode).
- اللغات منخفضة المستوى (مثل لغة التجميع Assembly و البيناري كود)
.
اللغات المترجمة Compiled والمُفسّرة interpreted
غالبًا ما يُوصف لغات البرمجة بأنها إما Compiled أو interpreted. تعني كلمة "Compiled" أن البرامج تُترجم إلى لغة الآلة ثم تُنفّذ بواسطة المعالج؛ بينما تعني كلمة "interpreted" أن البرنامج يقراء وينفذ بواسطة مُفسّر برمجي. عادةً ما تُعتبر لغة C لغة مترجمة ، بينما تُعتبر لغة Python لغة مُفسّرة. لكن التمييز ليس واضحًا دائمًا.
أولًا، يُمكن أن تكون العديد من اللغات إما مترجمة أو مُفسّرة. على سبيل المثال، هناك مُفسّرات C و مترجمات Python. ثانيًا، هناك لغات مثل Java تستخدم نهجًا هجينًا، حيث تُجمّع البرامج إلى لغة وسيطة ثم تُشغّل البرنامج المُترجم في مُفسّر.
تستخدم Java لغة وسيطة تُسمى Java bytecode، وهي تُشبه لغة الآلة، ولكن يتم تنفيذها بواسطة مُفسّر برمجي، وهو مشغل افتراضي يدعى Java virtual machine (JVM).
لذا فإن الترجمة أو التفسير ليس سمة جوهرية للغة؛ ومع ذلك، هناك بعض الاختلافات العامة بين اللغات المجمعة والمفسرة.
الفرق بين الأنواع الساكنة (Static Types) والأنواع الديناميكية (Dynamic Types) في لغات البرمجة
تختلف اللغات البرمجية في طريقة تعاملها مع أنواع البيانات (Data Types)، حيث تنقسم إلى لغات تستخدم الأنواع الساكنة (Static Typing) مثلC وJava وأخرى تعتمد علىالأنواع الديناميكية (Dynamic Typing) مثل Python وJavaScript .الفارق الرئيسي بينهما هو توقيت تحديد نوع المتغير:
- في اللغات الساكنة يُحدد نوع المتغير أثناء وقت التصريف (Compile Time)، مما يسمح للمُصرِّف (Compiler) بفحص الأخطاء قبل تشغيل البرنامج. على سبيل المثال، في لغة C، تُصرَّح أنواع المتغيرات و قيمها بشكل صريح:
هنا، يُضمن أن `x` و`y` عددان صحيحان، ويتم التحقق من صحة العملية الحسابية أثناء التصريف.
- أما في اللغات ديناميكية ، لا يُعرف نوع المتغير إلا أثناء وقت التشغيل (Runtime). مثلًا، الدالة التالية في Python تقبل أي نوع يدعم عملية الجمع:
مميزات الأنواع الساكنة
- الكشف المبكر عن الأخطاء: يتم اكتشاف المشكلات قبل تشغيل البرنامج، حتى في الأجزاء غير المُنفَّذة.
- تحسين الأداء: تجنُّب فحص الأنواع أثناء التشغيل يزيد سرعة التنفيذ.
- توفير الذاكرة: أسماء المتغيرات تُحذف بعد التصريف، ويُستعاض عنها بعناوين الذاكرة فقط.
مميزات الأنواع الديناميكية
- المرونة: لا تتطلب تعريف الأنواع مسبقًا، مما يسهل البرمجة السريعة.
- إمكانية التعديل أثناء التشغيل: كما في وظيفة `locals()` في Python التي تُظهر أسماء المتغيرات وقيمها أثناء التنفيذ.
باختصار، تُفضل اللغات ساكنة الأنواع في المشاريع الكبيرة التي تحتاج إلى موثوقية وكفاءة، بينما تتفوق اللغات ديناميكية الأنواع في البرمجة السريعة والنمذجة الأولية.
كيف يعمل المصرّف (Compiler)؟ فهم عملية الترجمة خطوة بخطوة
كمبرمج، يجب أن يكون لديك نموذج ذهني لما يحدث أثناء التجميع. إذا فهمت العملية، فسيساعدك ذلك على تفسير رسائل الخطأ، وتصحيح أخطاء الكود، وتجنب الأخطاء الشائعة.
مراحل التصريف الرئيسية:
المعالجة المسبقة (Preprocessing):
تُعد لغة C واحدة من عدة لغات تتضمن توجيهات المعالجة المسبقة التي تُفعّل قبل تجميع البرنامج. على سبيل المثال، يُؤدي التوجيه #include إلى إدراج الكود المصدري من ملف آخر في موقع التوجيه.
التحليل النحوي (Parsing):
أثناء التحليل، يقرأ المُجمِّع الكود المصدري ويُنشئ تمثيلًا داخليًا للبرنامج، يُسمى شجرة التركيب المجردة (Abstract Syntax Tree - AST). عادةً ما تكون الأخطاء المُكتشفة خلال هذه الخطوة أخطاءً في بناء الجملة syntax errors.
الفحص الساكن (Static Checking)
يتحقق المترجم مما إذا كانت المتغيرات والقيم من النوع الصحيح، وما إذا كانت الدوال تُستدعى بالعدد والنوع الصحيحين من الوسائط، وما إلى ذلك. تُسمى الأخطاء المُكتشفة خلال هذه الخطوة أخطاء دلالية ساكنة (Static Semantic Errors).
توليد الكود (Code Generation):
يقرأ المترجم التمثيل الداخلي للبرنامج ويُولِّد شيفرة الآلة أو بيناري كود.
الربط (Linking):
إذا كان البرنامج يستخدم قيمًا ووظائف مُعرَّفة في مكتبة، فيجب على المترجم العثور على المكتبة المناسبة وتضمين الشيفرة المطلوبة
التحسين (Optimization):
في عدة مراحل من العملية، يُمكن للمُترجم تحويل البرنامج لتوليد شيفرة تعمل بشكل أسرع أو تستهلك مساحة أقل.
عملية الترجمة باستخدام GCC: من الكود إلى التنفيذ
عند استخدام مترجم GCC (مترجم جنو للغة C) لبناء برنامج، يمر الكود بجميع مراحل التصريف تلقائيًا لينتج ملفًا قابلًا للتنفيذ. لنأخذ مثالًا على برنامج C بسيط يطبع "Hello World":
خطوات الترجمة والتنفيذ:
- حفظ الكود المصدري: يتم حفظ الكود أعلاه في ملف باسم `hello.c`.
- الترجمة باستخدام GCC : عند تنفيذ الأمر التالي في الطرفية:
- - معالجة التوجيهات المسبقة (مثل `#include`)
- - تحليل الكود النحوي والدلالي
- - توليد كود الآلة
- - ربط المكتبات الضرورية (مثل مكتبة `stdio` لوظيفة `printf`)
- - إنشاء ملف تنفيذي افتراضي باسم `a.out` (اختصارًا لـ "Assembler Output")
حيث تشير `./` إلى أن الملف موجود في المجلد الحالي.
4. تخصيص اسم للملف التنفيذي (اختياري)
يُفضل استخدام العلم `-o` لتحديد اسم واضح للملف التنفيذي بدلًا من `a.out`:
ما يحدث خلف الكواليس:
- يتم ربط المكتبة القياسية لـ C (`libc`) تلقائيًا لدعم وظائف مثل `printf`.
- بدون تحديد اسم المخرج (`-o`)، يُستخدم الاسم الافتراضي `a.out` لأسباب تاريخية (تعود إلى أنظمة يونكس القديمة).
نصيحة للمطورين:
- لتتبع مراحل التصريف خطوة بخطوة، يمكن استخدام أوامر مثل `-E` (لرؤية مخرجات المعالج المسبق) أو `-S` (لإنشاء كود تجميع).
فهم رسائل الأخطاء في عملية الترجمة والتنفيذ
عند مواجهة أخطاء البرمجة في لغة C، يمكّنك فهم مراحل التصريف من تفسير رسائل الخطأ بدقة. تبدأ الأخطاء المحتملة من مرحلة المعالجة المسبقة، حيث تظهر أخطاء مثل عدم وجود ملفات مدرجة، كما في رسالة:"fatal error: stdioo.h: No such file or directory"