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

ملخص

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

أهداف التعلم

  1. تعريف struct بحقول وقيم ابتدائية، وإنشاء سجلات بالتهيئة بالأقواس المعقوصة، وبالمهيِّئات المعيَّنة أيضًا.
  2. كتابة دوال تستقبل سجلات وتعيدها، مع اختيار طريقة التمرير.
  3. ترتيب متجه من السجلات والبحث فيه بحسب حقل باستعمال std::ranges::sort وstd::ranges::stable_sort وstd::ranges::find مع إسقاط، والتمييز بين ترتيب مستقر وترتيب غير مستقر.
  4. نمذجة بيانات مسألة بالسجلات وتعليل اختيار الحقول والأنواع.

تجميع البيانات

المتجهات المتوازية

لنفترض أن برنامجًا عليه أن يحفظ، لكل طالب في مجموعة، اسمه ودرجته وعمره. بما رأيناه حتى درس الجداول، فالحل المتاح ثلاثة متجهات، واحد لكل سمة، مع اصطلاح أن الفهرس ii يدل على الشخص نفسه في الثلاثة: names[i] وgrades[i] وages[i]. تُسمّى المتجهات المتوازية. ولا يصلح الجدول، لأن خاناته كلها يجب أن تكون من النوع نفسه، والاسم ليس عددًا.

برنامج بلغة C++
#include <algorithm>#include <cstddef>#include <print>#include <string>#include <vector>​int main(){    const std::vector<std::string> names{"آنا", "لويس", "إيفا"};    std::vector<int> grades{6, 4, 7};    const std::vector<int> ages{19, 22, 20};    std::ranges::sort(grades);    for (std::size_t i{0}; i < names.size(); ++i) {        std::println("{}: الدرجة {}، العمر {}", names[i], grades[i], ages[i]);    }}
المخرجات
آنا: الدرجة 4، العمر 19لويس: الدرجة 6، العمر 22إيفا: الدرجة 7، العمر 20

أراد البرنامج أن يرتّب الطلاب بحسب الدرجة، فرتّب الدرجات وحدها. وهو يُترجم دون تحذيرات، ولا يجد كاشف أخطاء الذاكرة، AddressSanitizer، ولا كاشف السلوك غير المعرَّف، UndefinedBehaviorSanitizer، ما يبلّغان عنه؛ ومع ذلك فمخرجاته خاطئة: كانت لآنا الدرجة 6 وتظهر بالدرجة 4. فالاصطلاح الذي كان يربط المتجهات الثلاثة غير مكتوب في أي موضع يستطيع المترجم قراءته، فلا شيء يمنع خرقه. ومن ذلك تنتج هشاشة المتجهات المتوازية: كل عملية تنقل عنصرًا أو تضيفه أو تحذفه يجب أن تتكرر، بالطريقة نفسها وبالترتيب نفسه، فيها جميعًا؛ ويكفي نسيان واحد منها لإفساد البيانات في صمت.

مثالصفوف تختل محاذاتها وبطاقات تتحرك كاملة

ثلاثة متجهات متوازية في مقابل متجه من البطاقات، في كل بطاقة البيانات الثلاث لشخص واحد.

البرهان

  1. العمود 0 يصف شخصًا واحدًا: names[0] وgrades[0] وages[0] هي اسم آنا ودرجتها وعمرها. والرابط بينها هو الفهرس وحده.

  2. يعيد std::ranges::sort(grades) ترتيب صف واحد فقط. والصفان الآخران لا يعلمان بذلك: العمود 0 يجمع الآن اسم آنا ودرجة لويس وعمر آنا، وهي بيانات لا تخص أحدًا.

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

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

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