انتقل إلى المحتوى
Topos Uranos

ملخص

حتى الآن كان كل برنامج في الدورة يتّسع له ملف واحد. أما البرنامج ذو الألف سطر فلم يعد يُقرأ دفعة واحدة، ودواله جديرة بأن تخدم برامج أخرى. يعلّم هذا الدرس تقسيمه: ملف ترويسة يصرّح بما تقدّمه الوحدة النمطية، وملف تنفيذ يعرّفه، وmain.cpp يستعمله. ويعرض حرّاس التضمين؛ وقاعدة التعريف الواحد، التي تفسّر الخطأين الكلاسيكيين للرابط والمعنى الحديث لـ inline؛ والترجمة المنفصلة، التي لا تعيد ترجمة إلا ما تغيّر؛ ويُختم بمكتبة خاصة مصنوعة من دوال الدروس التي سبقته.

أهداف التعلم

  1. تقسيم برنامج إلى ملف ترويسة فيه التصريحات وملف تنفيذ وmain.cpp، وترجمته.
  2. كتابة حرّاس التضمين وشرح المشكلة التي يحلّونها؛ والتعرّف على #pragma once بوصفه امتدادًا تقبله جميع المترجمات لكن المعيار لا يتضمّنه.
  3. شرح قاعدة التعريف الواحد وتشخيص انتهاكاتها انطلاقًا من رسالة الرابط («multiple definition»، «undefined reference»).
  4. الترجمة المنفصلة باستعمال -c وربط الملفات الكائنية، مع شرح ما يُعاد ترجمته عند تغيّر كل ملف.

الواجهة والتنفيذ

النمطية

الدالة ذات العقد، مثل دوال درس التصميم بالدوال، تفصل أصلًا بين ما تعد به، المكتوب في تصريحها وعقدها، وبين طريقة وفائها به، المكتوبة في جسمها؛ ومن يستدعيها لا يحتاج إلا إلى الأول. وإذا كان الأمر كذلك، فلا شيء يمنع حفظ الجزأين في ملفين مختلفين. الوحدة النمطية مجموعة من الدوال (ومن الأنواع التي تستعملها) ذات غاية مشتركة؛ وواجهتها هي ما تقدّمه لمن يستعملها، وتنفيذها هو الطريقة التي تفعل بها ذلك. في C++ تُكتب الواجهة في ملف ترويسة (الامتداد .hpp، أو .h الموروث من C)، والتنفيذ في ملف .cpp يحمل الاسم نفسه.

ومن ذلك تنتج ثلاث مزايا. فهي تُفهم جزءًا جزءًا: لاستعمال الوحدة النمطية تكفي قراءة ملف ترويستها. وتُترجم جزءًا جزءًا: تغيير التنفيذ لا يُلزم بإعادة ترجمة من يستعمل الواجهة وحدها. وتُختبر جزءًا جزءًا، في مقابل كل عقد، كما سيعلّم درس اختبار البرامج. ودورة «LaTeX للكتابة الأكاديمية» تقسّم المستند الطويل بالطريقة نفسها، في درسها «المستندات الطويلة».

ثلاثة ملفات

تقدّم الوحدة النمطية للهندسة المستوية في هذا المثال ثلاث دوال؛ اثنتان منها من الدرس 5.3. ولا يفعل ملف الترويسة سوى التصريح بها، كلٌّ مع عقدها. أما الأسطر الثلاثة التي تبدأ بـ # فستُشرح في القسم الآتي؛ ويكفي الآن أن نعلم أن كل ملف ترويسة يحملها.

geometry.hpp
// geometry.hpp: واجهة الوحدة النمطية للهندسة المستوية.#ifndef GEOMETRY_HPP#define GEOMETRY_HPP​// مساحة الدائرة التي نصف قطرها radius.// الشرط المسبق: radius >= 0.// الشرط اللاحق: يُعيد مساحة الدائرة التي نصف قطرها radius.double circleArea(double radius);​// هل ثلاثة أطوال هي أضلاع مثلث.// الشرط المسبق: لا شيء.// الشرط اللاحق: يُعيد true إذا وفقط إذا وُجد مثلث أضلاعه a وb وc.bool isTriangle(double a, double b, double c);​// مساحة المثلث الذي أضلاعه a وb وc.// الشرط المسبق: isTriangle(a, b, c).// الشرط اللاحق: يُعيد مساحة المثلث الذي أضلاعه a وb وc (صيغة هيرون).double triangleArea(double a, double b, double c);​#endif

يضمّن ملف التنفيذ ملف ترويسته ويعرّف الدوال. أما ترويسات المكتبة القياسية التي يحتاج إليها (<cmath> للجذر، و<numbers> لـ π\pi) فتُضمَّن هنا لا في ملف الترويسة، لأن من يستعمل الوحدة النمطية لا يحتاج إليها.

geometry.cpp
// geometry.cpp: تنفيذ الوحدة النمطية للهندسة المستوية.#include "geometry.hpp"​#include <cmath>#include <numbers>​double circleArea(double radius){    return std::numbers::pi * radius * radius;}​bool isTriangle(double a, double b, double c){    return a > 0 && b > 0 && c > 0 && a < b + c && b < a + c && c < a + b;}​double triangleArea(double a, double b, double c){    const double s{(a + b + c) / 2};    return std::sqrt(s * (s - a) * (s - b) * (s - c));}

وأخيرًا يضمّن main.cpp ملف الترويسة ويستعمل الوحدة النمطية، دون أن يرى سطرًا واحدًا من تنفيذها:

برنامج بلغة C++
#include <print>​#include "geometry.hpp"​int main(){    std::println("دائرة نصف قطرها 2: {:.4f}", circleArea(2.0));    std::println("هل يوجد مثلث أضلاعه 3، 4، 5؟ {}", isTriangle(3.0, 4.0, 5.0));    std::println("مساحته: {}", triangleArea(3.0, 4.0, 5.0));    std::println("وبأضلاع 1، 2، 3؟ {}", isTriangle(1.0, 2.0, 3.0));}
المخرجات
دائرة نصف قطرها 2: 12.5664هل يوجد مثلث أضلاعه 3، 4، 5؟ trueمساحته: 6وبأضلاع 1، 2، 3؟ false

علامتا التنصيص في #include "geometry.hpp" تبحثان عن الملف أولًا في مجلد الملف الذي يضمّنه، وأما الزاويتان في #include <print> ففي مجلدات النظام: علامتا التنصيص للملفات الخاصة. ويضمّن الملف geometry.cpp ملف ترويسته أولًا، حتى إذا نقص ملفَ الترويسة تضمينٌ يحتاج إليه هو نفسه، ظهر الخطأ عند ترجمة الوحدة النمطية لا في ملف شخص آخر. والمثلث الذي أضلاعه 1 و2 و3 غير موجود (3=1+23 = 1 + 2)، ولذلك يُلزم الشرط المسبق لـ triangleArea بالسؤال عن ذلك مسبقًا.

للترجمة يتلقّى الأمر ملفي .cpp؛ أما ملف الترويسة فلا يُذكر، لأنه يدخل كلًّا منهما عبر #include. وتُحفظ خيارات الدورة في متغير من متغيرات الطرفية، FLAGS، حتى لا تتكرر في كل أمر؛ و$FLAGS يستعيدها. هكذا يُكتب في Linux وmacOS؛ أما في PowerShell فتُكتب المتغيرات بطريقة أخرى، ويحسن كتابة الخيارات كاملة أو الترجمة من المحرر (القسم «الترجمة المنفصلة»).

الطرفية
$ FLAGS="-std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined"$ g++ $FLAGS main.cpp geometry.cpp -o main$ ./mainدائرة نصف قطرها 2: 12.5664هل يوجد مثلث أضلاعه 3، 4، 5؟ trueمساحته: 6وبأضلاع 1، 2، 3؟ false
مثالمسار ثلاثة ملفات

يستعيد الشكل شكل درس البرامج والمترجمات ولغة C++ لهذا المشروع.

البرهان

  1. يكتب المبرمج ثلاثة ملفات: ملف ترويسة وملفي .cpp. ولا يُسلَّم إلى المترجم سوى ملفات .cpp.

  2. ينسخ المعالج المسبق geometry.hpp في كل ملف يضمّنه. فهناك إذن نسختان من التصريحات، واحدة في كل وحدة ترجمة، وهذا صحيح: يمكن التصريح بالدالة مرات بقدر ما يُراد.

  3. تُترجم كل وحدة ترجمة على حدة إلى ملفها الكائني. وعند ترجمة main.cpp لا ينظر المترجم في geometry.cpp: يكفيه التصريح للتحقق من أنواع كل استدعاء.

  4. يجمع الرابط الملفين الكائنيين والمكتبة القياسية، ويحلّ كل استدعاء في main.o لـ circleArea بالتعريف الموجود في geometry.o.

من الأخطاء الشائعة تضمين ملف التنفيذ، #include "geometry.cpp". فإذا ذُكر فوق ذلك في الأمر، صارت دواله معرَّفة مرتين وفشل الربط، بسبب القاعدة التي سيشرحها القسم «قاعدة التعريف الواحد»؛ وإذا لم يُذكر، عاد main.cpp نصًّا واحدًا يُعاد ترجمته كاملًا مع كل تغيير. ملفات .cpp تُترجم؛ وملفات الترويسة تُضمَّن.