المسارات
/
C++
C++
/
المنهج
/
الملفات الرأسية
ال

الملفات الرأسية في C++

تمرينان

نبذة عن الملفات الرأسية

في C++، غالبًا ما تُفصل التصريحات عن التعريفات. تُجمَّع التصريحات في ما يُعرف بملفات الترويسة، بينما توضع التنفيذات المقابلة في الملفات المصدرية. يمكنك اعتبار ملفات الترويسة بمثابة API. يخبرك ملف الترويسة بما تقدمه قاعدة الكود دون الخوض في تفاصيل كيف يتحقق ذلك.

الترويسة والمصدر

امتداد الملفات الأكثر شيوعًا لملفات الترويسة هو .h. تستخدم بعض المشاريع .hpp أو تتجاهل الامتداد تمامًا.

توجد التعريفات في ملف .cpp منفصل. ولجمع الأجزاء معًا، يبدأ الملف المصدر بـ_تضمين_ ملف الترويسة المقابل.

إذا أردت كتابة مكتبة تُسمى "quick_math" تقدم دالة "super_root" تريد استخدامها كثيرًا، فستبدو الملفات هكذا:

// A file named quick_math.h
#pragma once

namespace quick_math {

double super_root(double x, int n);

}
// A file named quick_math.cpp
#include <cmath>

#include "quick_math.h"

double quick_math::super_root(double x, int n) {
    while (n) {
        x = std::sqrt(x), --n;
    }
    return x;
}

إذا احتجت إلى تضمين ترويسة لا يحتاجها إلا التنفيذ، فإن سطر #include المقابل لا يلزم إلا في الملف المصدر. وكل ما يُضمَّن في الترويسة يكون متاحًا أيضًا في ملف .cpp، مثل مكتبة string في المثال أدناه. انتبه: علامة ; مطلوبة بعد التصريح في ملف الترويسة، ولكن ليس بعد التعريف في الملف المصدر.

Note

تبدأ العديد من تمارين C++ على Exercism بملفين شبه فارغين: الترويسة والمصدر. عليك فحص ملف *_test.cpp لمعرفة أسماء الدوال وفضاءات الأسماء المتوقعة كي تحل التمرين.

الأصناف والترويسات

قد تصبح الأصناف معقدة جدًا، وقد تكون علاقتها بتقسيم الترويسة والمصدر مربكة. أحد التخطيطات الممكنة هو إبقاء جميع تفاصيل التنفيذ في الملف المصدر وجميع التصريحات ومتغيرات الأعضاء في الترويسة:

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0);
    void give_water();
    std::string get_name();
    int get_size();
    void start_next_day();
};
}  // namespace robots
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"

robots::Flower::Flower(std::string name, int size) {
    this->name = "Robotica " + name;
    this->size = size;
}

void robots::Flower::start_next_day() {
    if (!needs_water) ++size;
    needs_water = true;
}

std::string robots::Flower::get_name() { return name; }

int robots::Flower::get_size() { return size; }

عندما تُستخدم الترويسة كملخص عام لـ API، فهي المكان الذي يبحث فيه المرء عن معلومات مثل القيم الافتراضية. ولذلك تُعالَج القيمة الافتراضية للمعامل size في البنّاء ضمن الترويسة، لا في التنفيذ. وتُسبق التعريفات في الملف المصدر بفضاء الأسماء robots ونوع الصنف Flower.

وهناك خيار تخطيط آخر هو مكتبة ترويسة فقط، لا تحتوي على ملف .cpp إطلاقًا:

// A file named robot_flower.h
#pragma once

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0) {
        this->name = "Robotica " + name;
        this->size = size;
    }
    void give_water() { needs_water = false; }
    std::string get_name() { return name; }
    int get_size() { return size; }
    void start_next_day() {
        if (!needs_water) ++size;
        needs_water = true;
    }
};
}  // namespace robots

قد تستخدم المشاريع مزيجًا من هذه التخطيطات، وهناك نقاش كبير حول ما قد يكون الأنسب لكل حالة استخدام.

حراس التضمين

ربما لاحظت سطر #pragma once في ملف الترويسة في المثال أعلاه. يُسمى هذا حارس تضمين، وهو يضمن أن يُضمَّن محتوى الملف مرة واحدة فقط أثناء الترجمة لتجنب الأخطاء. وهناك صيغة أخرى أكثر تعقيدًا لحارس التضمين تبدأ بـ #ifndef وتنتهي بـ #endif، وهي مفصّلة أدناه.

Advanced

التصريحات المسبقة

يُقيَّم كود C++ بشكل تسلسلي. إذا أردت استخدام دالة، فيجب أن تكون معروفة للمترجم في لحظة استخدامها. أحيانًا لا يكون من الممكن إيجاد ترتيب خطي لتنظيم التعريفات في الكود المصدري. ألقِ نظرة على المثال أدناه:

int myFunction(int n) {
    if (n < 10) {
        return n;
    } else {
        return myOtherFunction(n / 10);
    }
}

int myOtherFunction(int m) { return myFunction(m / 2); }

عند تعريف myFunction، لا يعرف المترجم شيئًا عن myOtherFunction بعد. وللأسف، لا يمكن حل مشكلة المرجعية الدائرية بتبديل الترتيب.

يقدم C++ التصريحات المسبقة لتُعلم المترجم بـ myFunction و myOtherFunction قبل تعريفهما. يفترض المترجم أن التعريف سيأتي في نقطة لاحقة بعد التصريح. يوضح المثال التالي كيفية استخدام تصريح مسبق للدوال.

int myFunction(int n);       // Forward declaration of myFunction
int myOtherFunction(int m);  // Forward declaration of myOtherFunction

// Definition of myFunction
int myFunction(int n) {
    if (n < 0) {
        return 0;
    } else {
        return myOtherFunction(n - 2);
    }
}

// Definition of myOtherFunction
int myOtherFunction(int m) { return myFunction(m / 2); }

حراس التضمين عبر ifndef

لا يهم أن يُضمَّن الملف نفسه عدة مرات داخل مشروع واحد. ينبغي ألا تحتوي ملفات الترويسة على تعريفات. ولا يمكن أن يحتوي المشروع بأكمله على التعريف نفسه أكثر من مرة. ويُسمى هذا "قاعدة التعريف الواحد". وسيفرضها المترجم.

من السهل تجنب التضمينات المتعددة غير المقصودة للتعريف نفسه باستخدام حراس التضمين. وهي تُبنى بإجراء خاص خلال مراحل الترجمة. الهدف هو تضمين الملف فقط إذا لم يكن متغير معين قد ضُبط، ثم ضبطه بمجرد تضمين الملف. وغالبًا ما يُشتق اسم هذا المتغير من اسم الملف. وهناك طريقة أخرى تتمثل في توليد UUID لتقليل خطر استخدامه مرتين بالخطأ. ويمكن رؤية الصياغة أدناه مع MY_HEADER_FILE_H كمتغير.

#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H

// file content

#endif

تكمن مشكلة #pragma once في أن توجيهات pragma ليست جزءًا رسميًا من لغة C++، وأن تنفيذها يختلف من مترجم إلى آخر. لقد تحولت مشاريع كبيرة كثيرة إلى طريقة pragma الأبسط، لكن قلة منها ما تزال حذرة.

تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة

تعلّم الملفات الرأسية