سلسلة ++c للمحترفين الدرس 11: دخل وخرج الملفات (File I/O) في Cpp


محمد الميداوي

تستخدم لغة C++‎ مجاري التدفق لإدارة دخل وخرج الملفات، إذ تستخدم:

  • std::istream لقراءة النص.
  • std::ostream لكتابة النص.
  • std::streambuf لقراءة أو كتابة المحارف.
  • يستخدم الدَّخل المُنسّق (Formatted input) العاملَ‏‏ ‎operator>>‎.
  • يستخدم الخرج المنسّق العامل ‎operator<<‎.

تستخدم المجاري ‎std::locale‎ مثلًا للحصول على تفاصيل التنسيق والترجمة بين الترميزات الخارجية والترميز الداخلي.

الكتابة في الملفات

هناك عدة طرق للكتابة في ملف، لعل أسهلها هو استخدام مجرى خرج الملفّات ‎ofstream‎ مع عامل الإدارج في المجرى (‎<<‎)، انظر:

std::ofstream os("foo.txt");
if(os.is_open()){
os << "Hello World!";
}

تستطيع استخدام الدالة التابعة لمجرى خرج الملفّات ‎write()‎ بدلًا من ‎<<‎، انظر المثال التالي حيث سنكتب ثلاثة محارف من data:

std::ofstream os("foo.txt");
if (os.is_open()) {
    char data[] = "Foo";
    os.write(data, 3);
}

يجب أن تتحقق دائمًا بعد الكتابة في المجرى من راية حالة الخطأ badbit، إذ أنها تشير إلى نجاح العملية أو فشلها، وتستطيع ذلك باستدعاء دالة تابع مجرى خَرْج الملف ‎bad()‎، انظر:

os << "Hello Badbit!"; // قد تفشل هذه العملية لسبب ما
if (os.bad())
// فشل في الكتابة

فتح ملف

تُفتح الملفات بنفس الطّريقة في جميع مجاري الملفات الثلاث (‎ifstream‎، ‎ofstream‎، و‎fstream‎). يمكنك فتح الملف مباشرة في المُنشئ (constructor)، انظر المثال التالي حيث نفتح الملف foo.txt للقراءة فقط، ثم للكتابة فقط، ثم للقراءة والكتابة:

std::ifstream ifs("foo.txt"); 
std::ofstream ofs("foo.txt");
std::fstream iofs("foo.txt"); 

أو يمكنك استخدام دالة تابع مجرى الملف ‎open()‎ لتنفيذ نفس الشيء:

std::ifstream ifs;
ifs.open("bar.txt"); // للقراءة فقط  "bar.txt" فتح الملف
std::ofstream ofs;
ofs.open("bar.txt"); // للكتابة فقط  "bar.txt" فتح الملف
std::fstream iofs;
iofs.open("bar.txt"); // للقراءة والكتابة "bar.txt" فتح الملف

يجب عليك التحقّق دائمًا ممّا إذا كان الملف قد فُتِح بنجاح (حتى أثناء الكتابة)، قد تفشل عملية الفتح لعدة أسباب، انظر بعضها فيما يلي:

  • عدم وجود الملف
  • صلاحيات الوصول (access rights) غير صالحة.
  • الملف قيد الاستخدام في الوقت الراهن.
  • وقوع خطأ في القرص.
  • فصل القرص من الحاسوب.

يمكن إجراء عملية التحقّق على النحو التالي:

// 'foo.txt' محاولة قراءة
std::ifstream ifs("fooo.txt"); // خطأ في الكتابة، لا يمكن فتح الملف

// التحقق مما إذا كان الملف قد فُتِح بنجاح
if (!ifs.is_open()) {
    // لم يُفتح الملف بعد، اتخاذ الإجراءات المناسبة
    throw CustomException(ifs, "الملف لم يُفتح");
}

إن كان المسار يحتوي على شرطة مائلة عكسية \ كما هو الحال في نظام Windows، فيجب أن تهربها بشكل سليم. انظر المثال التالي لفتح الملف foo.txt في ويندوز:

std::ifstream ifs("c:\\\\folder\\\\foo.txt"); // تهريب الشرطة المائلة العكسية

الإصدار ≥ C++‎ 11

أو يمكنك استخدام القيم مصنفة النوع الخام (raw literal)، انظر:

std::ifstream ifs(R"(c:\\folder\\foo.txt)"); // استخدام  قيم مصنفة النوع خام

أو استخدم شرطة مائلة / بدلاً من ذلك:

std::ifstream ifs("c:/folder/foo.txt");

الإصدار ≥ C++‎ 11

إن أردت أن تفتح ملفًّا يحتوي على محارف من غير ASCII في مسارٍ في Windows، فيمكنك حاليًا استخدام المَحرَف العام غير القياسي (non-standard wide character) في المسار، انظر المثال التالي حيث نضع كلمة "مثال" بالبلغارية في مسار الملف:

std::ifstream ifs(LR"(пример\\foo.txt)"); // استخدام محرف عام مع سلسلة نصية خام

القراءة من ملف

هناك عدة طرق لقراءة البيانات من ملف، فإن كنت تعرف تنسيق (format) البيانات فيمكنك استخدام عامل الاستخراج من المجرى - stream extraction operator‏‏ - (‎>>‎). دعنا نفرض أنّ لديك ملفّا يُسمّى foo.txt يحتوي على البيانات التالية:

John Doe 25 4 6 1987
Jane Doe 15 5 24 1976

فعندها يمكنك استخدام الشّيفرة أدناه لقراءة تلك البيانات من الملف، لاحظ أننا سنستخرج الاسم الأول firstname واسم العائلة lastname والعمر age وشهر الولادة bmonth ويوم الولادة bday وعام الولادة byear. أيضًا، انتبه إلى أن ‎>>‎ تعيد القيمة false إن بلغت نهاية الملف أو لم تتوافق بيانات الدخل مع نوع المتغير، فلا يمكن استخراج نص foo إلى متغير int مثلًا.

// تعريف المتغيرات
std::ifstream is("foo.txt");
std::string firstname, lastname;
int age, bmonth, bday, byear;
while (is >> firstname >> lastname >> age >> bmonth >> bday >> byear)
// معالجة البيانات المقروءة

يستخرج العامل ‎>>‎ كل المحارف ويتوقف إذا وجد حرفًا لا يمكنه تخزينه أو محرفًا خاصًّا (special character):

  • بالنسبة للسًلاسل النصية، يتوقًف العامل عند المسافة الفارغة ()، أو عند السًطر الجديد (‎\n‎).
  • بالنًسبة للأعداد، يتوقف العامل عند المحارف غير الرقمية.

هذا يعني أنّ النُّسخ التالية من ملف foo.txt ستُقرأ بنجاح من قبل الشّيفرة السابقة:

John
Doe 25
4 6 1987
Jane
Doe
15 5
24
1976

يعيدُ عاملُ ‎>>‎ المجرى المُمرّرَ إليه، لهذا يمكن سَلْسَلةُ هذا العامل من أجل قراءة البيانات على التوالي. كذلك من الممكن استخدام المجرى كتعبير بولياني (كما هو مُوضّح في حلقة ‎while‎ في الشّيفرة السابقة)، ذلك أن أصناف المجرى بها عامل تحويل للنوع ‎bool‎.

سيعيد العامل ‎bool()‎ القيمة ‎true‎ طالما أنّ المجرى لا يحتوي على أخطاء، أما إن تغيّرت حالة المجرى إلى حالة خطأ (على سبيل المثال، إذا لم يكن من الممكن استخراج مزيد من البيانات)، فسوف يعيد العاملُ ‎bool()‎ القيمة‎false‎، وعليه فإن حلقة ‎while‎ في الشّيفرة السابقة ستُنهى بعد قراءة كامل الملف.

إذا أردت قراءة كامل الملف كسلسلة نصّية، فيمكنك استخدام الشّيفرة التالية:

// 'foo.txt' فتح
std::ifstream is("foo.txt");
std::string whole_file;

// الذهاب إلى نهاية الملف
is.seekg(0, std::ios::end);

// تخصيص ذاكرة للملف
whole_file.reserve(is.tellg());

// الذهاب إلى بداية الملف
is.seekg(0, std::ios::beg);

// 'whole_file' تعيين محتوى
// إلى جميع محارف الملف
whole_file.assign(std::istreambuf_iterator<char>(is),
   std::istreambuf_iterator<char>());

توفر هذه الشيفرة مساحة للسّلسلة النصية ‎string‎ من أجل الاقتصاد في الذاكرة. أما إن أردت قراءة الملف سطرًا سطرًا، فيمكنك استخدام الدالة ‎getline()‎، انظر المثال التالي حيث تعيد هذه الدالة القيمة false إن لم يكن ثمة أسطر أخرى متبقية:

std::ifstream is("foo.txt");
for (std::string str; std::getline(is, str);) {
    // معالجة السّطر المقروء
}

إذا أردت قراءة عدد محدّد من المحارف فاستخدم دالة تابع المجرى ‎read()‎:

std::ifstream is("foo.txt");
char str[4];
// قراءة أربعة حروف من الملف
is.read(str, 4);

يجب أن تتحقق دائمًا بعد تنفيذ أمر القراءة من راية حالة الخطأ failbit إذ أنها تشير إلى نجاح العملية أو فشلها، وتستطيع ذلك باستدعاء دالة تابع مجرى خَرْج الملف ‎fail()‎، انظر:

is.read(str, 4); // قد تفشل هذه العملية لسبب ما
if (is.fail())
// فشل في القراءة!

أوضاع الفتح

يمكنك تحديد وضع الفتح عند إنشاء مجرى ملف، ووضع الفتح ما هو إلا إعداد للتّحكم في كيفيّة فتح المجرى للملف، تستطيع العثور على جميع الأوضاع في مجال اسم ‎std::ios‎. يمكن تمرير وضع الفتح كمعامل ثاني إلى منشئ مجرى الملف أو إلى تابعه ‎open()‎:

std::ofstream os("foo.txt", std::ios::out | std::ios::trunc);
std::ifstream is;
is.open("foo.txt", std::ios::in | std::ios::binary);

تجدر الإشارة إلى أنّه يجب عليك تعيين معامِل الوضع الافتراضي ‎ios::in‎ أو ‎ios::out‎ إذا أردت تعيين قيم الرايات الأخرى، لأنها لا تٌعيّن ضمنيًا من قبل أعضاء مجرى الدّخل (iostream) رغم أنّ لديها قيمة افتراضية صحيحة.

يُستخدم وضع الفتح الافتراضي أدناه إذا لم تحدِّد وضعًا للفتح:

  • ifstream - دخل
  • ofstream - خرج
  • fstream - دخل وخرج

أوضاع الفتح التي تستطيع تحديدها هي:

الوضع المعنى الغرض الوصف
app ألحِق (append) إخراج إضافة البيانات إلى نهاية الملف
binary ثنائي/بِتِّي (Binary) إخراج/إدخال الإدخال والإخراج يحدث بالبتات
in دخل (input) إدخال فتح الملف للقراءة
out خرج (output) إخراج فتح الملف للكتابة
trunc بتر (truncate) إخراج/إدخال حذف محتوى الملف عند الفتح
ate عند النهاية (at end) إدخال الذهاب إلى نهاية الملف عند الفتح

ملاحظة: في حال تعيين الوضع البتّي ‎binary‎، فستُقرأ وتُكتب البيانات تمامًا كما هي؛ أما إن لم تُحدَّد فسيكون ممكنًا ترجمة محرف السّطر الجديد ‎'\n'‎ إلى نهاية السّطر المناسبة لنظام التشغيل المستخدم. على سبيل المثال، في نظام Windows، تسلسل نهاية السطر هو CRLF ‏‏(‎"\r\n"‎).

  • الكتابة:
"\n" => "\r\n"
  • القراءة:
"\r\n" => "\n"

قراءة ملف ASCII إلى مكتبة std::string

انظر المثال التالي، سيكون محتوى file.txt موجودًا في buffer.str()‎:

std::ifstream f("file.txt");

if (f) {
    std::stringstream buffer;
    buffer << f.rdbuf();
    f.close();
}

يعيد التابع ‎rdbuf()‎ مؤشرًا إلى ‎streambuf‎، والذي يمكن إضافته إلى المخزن المؤقّت ‎buffer‎ عبر دالة التابع stringstream::operator<<‎. هناك احتمال آخر (من اقتراح سكوت مايرز):

std::ifstream f("file.txt");
if (f)
{
std::string str((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
// `str` عمليّات على
}

هذه الطّريقة لا تحتاج شيفرة كبيرة وتسمح بقراءة الملفّ مباشرة إلى أي حاوية مكتبة قياسيّة وليست قاصرة على السّلاسل النصية فقط، لكن قد تكون بطيئة إن كان الملف كبيرُا.

ملاحظة: الأقواس الإضافية حول الوسيط الأول في مُنشئ السّلسلة النصية ضرورية لمنع حدوث مشكلة في التحليل (parsing).

وأخيرًا وليس آخرًا:

std::ifstream f("file.txt");
if (f) {
    f.seekg(0, std::ios::end);
    const auto size = f.tellg();
    std::string str(size, ' ');
    f.seekg(0);
    f.read( & str[0], size);
    f.close();
    // `str` عمليات على
}

وهو أسرع خيار من بين الخيارات الثلاثة المقترحة.

الكتابة في الملفات باستخدام إعدادات محليّة غير قياسية

استخدم ‎std::locale‎ و ‎std::basic_ios::imbue()‎ إن كنت بحاجة إلى الكتابة في ملف باستخدام إعدادات محليّة غير الإعدادات الافتراضية، واسترشد بالتوجيهات الآتية:

  • يجب عليك دائمًا تطبيق إعدادات محليّة على المجرى قبل فتح الملف.
  • بمجرد أن تطعِّم المجرى بالإعدادات الجديدة، لا تغيّر الإعدادات المحليّة.

أسباب تلك التوجيهات: تحويل مجرى ملف إلى إعدادات محليّة ستكون له تداعيات غير متوقّعه إذا لم تكن الإعدادات الحاليّة مستقلّة، أو إن لم تكن تشير إلى بداية الملف.

مجاري UTF-8 وغيرها ليست مستقلة، كما أنّ مجاري الملفّات ذات الإعدادات المحليّة UTF-8 قد تحاول قراءة المِحرف BOM من الملف عند فتحه؛ لذلك فعليك أن تنتبه، فقراءة الملف بعد الفتح قد لا تكون من بدايته دائمًا.

#include <iostream>
#include <fstream>
#include <locale>

int main() {
    std::cout << "User-preferred locale setting is " <<
        std::locale("").name().c_str() << std::endl;

    // اكتب عدد عشري باستخدام الإعدادات المحليّة للمستخدم
    std::ofstream ofs1;
    ofs1.imbue(std::locale(""));
    ofs1.open("file1.txt");
    ofs1 << 78123.456 << std::endl;
    // استخدم إعدادات محليّة محددة، الأسماء تختلف من نظام لآخر
    std::ofstream ofs2;
    ofs2.imbue(std::locale("en_US.UTF-8"));
    ofs2.open("file2.txt");
    ofs2 << 78123.456 << std::endl;
    // "C" التحوّل إلى الإعدادات المحليّة لـ
    std::ofstream ofs3;
    ofs3.imbue(std::locale::classic());
    ofs3.open("file3.txt");
    ofs3 << 78123.456 << std::endl;
}

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

78,123.456
78,123.456
78123.456

على سبيل المثال، إذا كانت الإعدادات المحليّة المفضّلة هي اللغة الألمانية ومن ثم فإنها تستخدم تنسيقا مختلفًا للأرقام، فإنّ المثال سيُكتب هكذا:

78 123,456
78,123.456
78123.456

(لاحظ الفاصلة العشرية في السطر الأول).

تجنب التحقق من نهاية الملف داخل شرط الحلقة

لا تعيد ‎eof‎ القيمة true إلا بعد قراءة نهاية الملف، وهي لا تشير إلى أنّ القراءة التالية ستكون نهاية المجرى. انظر:

while (!f.eof()) {
    // هذا يبدو جيدًا
    f >> buffer;
    // صحيحة eof هنا تصبح  
    /* `buffer` استخدم */
}

يمكن أن تكتب أيضًا:

while (!f.eof()) {
    f >> buffer >> std::ws;
    if (f.fail())
        break;
    /* `buffer` استخدم */
}

لكنّ الشّيفرة:

while (f >> buffer) {
    /* `buffer` استخدم */
}

أبسط وأصحّ.

مراجع أخرى:

  • std::ws: تتجاهل المسافة البيضاء في بداية مجرى الدخل.
  • std::basic_ios::fail: تعيد true إذا حدث خطأ في المجرى المرتبط بها.

تفريغ المجرى (Flushing a stream)

تقوم مجاري الملفات -إضافة إلى العديد من أنواع المجاري الأخرى- بالتخزين المؤقت (buffering) افتراضيًا، وذلك يعني أن الكتابة في المجرى قد لا تؤدي إلى تغير الملف المقابل فورًا، بل يجب أن تفرِّغ المجرى إن أردت ذلك، إما باستدعاء التابع ‎flush()‎ أو عبر معالج المجرى ‎std::flush‎، انظر:

std::ofstream os("foo.txt");
os << "Hello World!" << std::flush;
char data[3] = "Foo";
os.write(data, 3);
os.flush();

يجمع مُعالج المجرى ‎std::endl‎ بين كتابة سطر جديد وتفريغ المجرى، انظر الشيفرة التالية حيث ينفِّذ كلا السطران نفس الشيء:

os << "Hello World!\n" << std::flush;
os << "Hello world!" << std::endl;

يحسّن التخزين المؤقّت (Buffering) أداء عمليات الكتابة في المجرى، لهذا يُفضّل أن تتجنّب التطبيقاتُ كثيرةُ الكتابةِ استخدام التفريع ما لم يكن لازمًا. لكن على العكس من ذلك، إذا لم يكن البرنامج يكثر من عمليات الدّخل والخرج (I/O)، فمن الأفضل الإكثار من التفريغ لتجنب تكدّس البيانات في كائن المجرى.

قراءة ملف في حاوية

في المثال التالي، سنستخدم ‎std::string‎ و ‎operator>>‎ لقراءة عناصر من ملف.

std::ifstream file("file3.txt");
std::vector < std::string > v;
std::string s;
while (file >> s) // الاستمرار في القراءة حتى النهاية
{
    v.push_back(s);
}

في المثال أعلاه، كرّرنا -من iterate- على الملف بقراءة "عنصر" واحد في كل مرّة باستخدام العامل <<‎. يمكن تحقيق الشّيء نفسه باستخدام ‎std::istream_iterator‎، والذي هو مكرّر دخْلٍ يقرأ "عنصرًا" واحدًا في كل مرة من المجرى. كذلك يمكن إنشاء معظم الحاويات باستخدام مُكرِّرين كما يلي:

std::ifstream file("file3.txt");
std::vector<std::string>     v(std::istream_iterator<std::string>{file},
std::istream_iterator<std::string>{});

يمكننا توسيع هذه الشّيفرة لقراءة أيّ نوع من الكائنات من خلال تمرير الكائن الذي نريد قراءته كمعامل قالب (template parameter) إلى ‎std::istream_iterator‎. وهكذا، يمكننا توسيع الشّيفرة السّابقة لقراءة الأسطر (بدلاً من الكلمات). انظر المثال التالي، لاحظ أنه لعدم وجود نوع مضمّن يقرأ الأسطر باستخدام << فإننا سنبني صنفًا مساعدًا لفعل ذلك، وسيقوم بعملية التحويل إلى سلسلة نصية (string) عند استخدامه في سياق نصي:

struct Line {
    // تخزين البيانات هنا
    std::string data;
    // تحويل الكائن إلى سلسلة نصية
    operator std::string
    const & () const {
        return data;
    }
    // قراءة سطر من المجرى
    friend std::istream & operator >> (std::istream & stream, Line & line) {
        return std::getline(stream, line.data);
    }
};

std::ifstream file("file3.txt");

// قراءة أسطر الملف إلى حاوية
std::vector<std::string>     v(std::istream_iterator<Line>{file},
                   std::istream_iterator<Line>{});

نسخ الملفات

std::ifstream  src("source_filename", std::ios::binary);
std::ofstream  dst("dest_filename",   std::ios::binary);
dst << src.rdbuf();

الإصدار ≥ C++‎ 17

الطريقة القياسيّة لنسخ ملف في C++‎ 17 هي تضمين التّرويسة ، واستخدام ‎copy_file‎، انظر:

std::fileystem::copy_file("source_filename", "dest_filename");

طُوِّرت المكتبة filesystem في البداية كوحدة ‎boost‏‏ (‎boost.filesystem‎)، تم دُمِجت بعد ذلك في التوصيف ISO C++‎ بدءًا من C++‎ 17.

إغلاق ملف

نادرًا ما يكون عليك إغلاق الملفات في C++‎، لأنّ المجرى سوف يغلق الملف المرتبط به تلقائيًا في المفكك (destructor) الخاص به. ومع ذلك يُفضّل أن تحُدّّ من عمر كائنات المجرى، حتى لا يبقى مِقبض (handle) الملفّ مفتوحًا لفترة أطول من اللازم.

على سبيل المثال، يمكنك فعل ذلك عبر وضع جميع العمليّات المُنفّذة على الملف في نطاق خاص (‎{}‎)، لاحظ أن ofstream ستكون خارج النطاق بنهاية هذه الشيفرة:

std::string
const prepared_data = prepare_data(); {
    // فتح ملف للكتابة
    std::ofstream output("foo.txt");
    // كتابة البيانات
    output << prepared_data;
}
// سيتولى المدمر إغلاق الملف

لا يكون استدعاء ‎close()‎ بشكل صريح ضروريًّا إلا إن أردت إعادة استخدام نفس كائن ‎fstream‎ لاحقًا لكنّك لم ترغب في إبقائه مفتوحًا إلى ذلك الحين، انظر المثال التالي الذي نجريه على ملف foo.txt:

// افتح الملف لأول مرة
std::ofstream output("foo.txt");

// جهِّز بعض البيانات لتكتبها في الملف
std::string
const prepared_data = prepare_data();

// اكتب البيانات إلى الملف
output << prepared_data;

// أغلق الملف
output.close();

// قد يستغرق تحضير البيانات وقتًا طويلًا
// لذا لن نفتح مجرى خرج الملف حتى نكون جاهزين للكتابة فيه
std::string
const more_prepared_data = prepare_complex_data();

// افتح الملف مرة أخرى بمجرد أن تكون جاهزًا للكتابة فيه
output.open("foo.txt");

// اكتب البيانات في الملف
output << more_prepared_data;

// أغلق الملف مرة أخرى
output.close();

قراءة بُنية struct من ملف نصّي منسق

الإصدار ≥ C++‎ 11 انظر المثال التالي، سنحدد حملًا زائدًا للعامل <<operator على أنه دالة friend تمنح امتياز الوصول إلى البيانات الخاصة للأعضاء.

struct info_type {
    std::string name;
    int age;
    float height;

       friend std::istream & operator >> (std::istream & is, info_type & info) {
        // تجاوز المسافة البيضاء
        is >> std::ws;
        std::getline(is, info.name);
        is >> info.age;
        is >> info.height;
        return is;
    }
};
void func4() {
    auto file = std::ifstream("file4.txt");
    std::vector < info_type > v;
    for (info_type info; file >> info;) // اقرأ حتى النهاية
    {
        // لن نصل إلى هنا إلا في حال نجاح عملية القراءة
        v.push_back(info);
    }
    for (auto
        const & info: v) {
        std::cout << "  name: " << info.name << '\n';
        std::cout << "   age: " << info.age << " years" << '\n';
        std::cout << "height: " << info.height << "lbs" << '\n';
        std::cout << '\n';
    }
}

ملف file4.txt

لنفرض أن البيانات التالية موجودة في ملفّ file4.txt:

Wogger Wabbit
2
6.2
Bilbo Baggins
111
81.3
Mary Poppins
29
154.8

إذًا يكون الخرج ما يلي:

name: Wogger Wabbit
age: 2 years
height: 6.2lbs
name: Bilbo Baggins
age: 111 years
height: 81.3lbs
name: Mary Poppins
age: 29 years
height: 154.8lbs

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 12: File I/O‎ من كتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن