C++: مكرّرات STL
في الدرسين 34-35 تعلّمنا عن حاويات وخوارزميات STL.
الآن، سنتعمّق في "الصمغ" الذي يربط STL — المكرّرات.
فهم المكرّرات هو المفتاح لفهم فلسفة تصميم STL حقًا.
1. نظرة عامة على المكرّرات
(1) 1.1 ما هي المكرّرات؟
المكرّرات هي مفهوم أساسي في STL تربط الحاويات بالخوارزميات.
تشبيه من الحياة الواقعية:
- الحاوية = مستودع
- الخوارزمية = عامل
- المكرّر = إذن الاستلام في يد العامل (يُخبر العامل أي رف يذهب إليه)
(2) 1.2 العمليات الأساسية للمكرّرات
تدعم جميع المكرّرات العمليات التالية:
| العملية | الوصف | مثال |
|---|---|---|
*it |
فك المرجع | int x = *it; |
it++ |
الانتقال للعنصر التالي | ++it; |
it-- |
الرجوع للعنصر السابق | --it; |
it1 == it2 |
مقارنة المساواة | if (it1 == it2) |
it1 != it2 |
مقارنة عدم المساواة | while (it != end()) |
2. فئات المكرّرات
(1) 2.1 أنواع المكرّرات الخمسة
تُعرّف STL خمسة أنواع من المكرّرات، من الأضعف إلى الأقوى:
| نوع المكرّر | القدرات | الحاوية الممثّلة |
|---|---|---|
| مكرّر الإدخال | قراءة فقط، أحادي الاتجاه | istream_iterator |
| مكرّر الإخراج | كتابة فقط، أحادي الاتجاه | ostream_iterator |
| مكرّر أمامي | قراءة/كتابة، أحادي الاتجاه | forward_list |
| مكرّر ثنائي الاتجاه | قراءة/كتابة، ثنائي الاتجاه | list, set, map |
| مكرّر الوصول العشوائي | قراءة/كتابة، وصول عشوائي | vector, deque, array |
(2) 2.2 مقارنة قدرات المكرّرات
مكرّر الإدخال ← الأضعف
↓
مكرّر أمامي
↓
مكرّر ثنائي الاتجاه
↓
مكرّر الوصول العشوائي ← الأقوى
كلما زادت القدرة، زادت العمليات المدعومة:
| العملية | الإدخال | أمامي | ثنائي الاتجاه | وصول عشوائي |
|---|---|---|---|---|
فك المرجع * |
✅ | ✅ | ✅ | ✅ |
التقديم ++ |
✅ | ✅ | ✅ | ✅ |
التأخير -- |
❌ | ❌ | ✅ | ✅ |
وصول عشوائي [] |
❌ | ❌ | ❌ | ✅ |
حساب + - |
❌ | ❌ | ❌ | ✅ |
(3) 2.3 مثال: مكرّرات لحاويات مختلفة
#include <iostream>
#include <vector>
#include <list>
#include <forward_list>
int main() {
std::vector`<int>` v = {1, 2, 3};
std::list`<int>` l = {1, 2, 3};
std::forward_list`<int>` fl = {1, 2, 3};
// vector: مكرّر الوصول العشوائي
auto it_v = v.begin();
std::cout << it_v[2] << std::endl; // الوصول العشوائي يعمل
// list: مكرّر ثنائي الاتجاه
auto it_l = l.begin();
++it_l; // يمكن التقديم
--it_l; // يمكن التأخير
// it_l[2]; // ❌ خطأ! list لا يدعم الوصول العشوائي
return 0;
}
المخرجات:
3
3. بطلان المكرّرات
(1) 3.1 ما هو بطلان المكرّرات؟
يحدث بطلان المكرّر عندما تُصبح عملية على الحاوية موضع المكرّر غير صالح.
الأسباب الشائعة:
- إعادة تخصيص الذاكرة (
push_backفيvector) - حذف عنصر (
erase)
(2) 3.2 قواعد بطلان المكرّرات حسب الحاوية
| الحاوية | العملية | البطلان |
|---|---|---|
vector |
push_back |
قد يبطل (عندما يحدث إعادة تخصيص) |
vector |
erase |
تُبطَل المكرّرات عند العنصر المحذوف وبعده |
list |
push_back |
لا يبطل |
list |
erase |
يُبطَل فقط مكرّر العنصر المحذوف |
map/set |
erase |
يُبطَل فقط مكرّر العنصر المحذوف |
(3) 3.3 مثال: بطلان مكرّر vector (الصعوبة ⭐⭐)
▶ مثال 1: استخدام حاويات STL (الصعوبة ⭐)
#include <iostream>
#include <vector>
int main() {
std::vector`<int>` v = {1, 2, 3, 4, 5};
auto it = v.begin();
std::cout << "*it = " << *it << std::endl; // المخرج: 1
// قد يسبب push_back إعادة تخصيص، مما يبطل المكرّرات
v.push_back(6);
// ❌ خطير! قد يكون it مبطَلاً
// std::cout << "*it = " << *it << std::endl; // سلوك غير معرّف
// ✅ الطريقة الصحيحة: إعادة الحصول على المكرّر
it = v.begin();
std::cout << "*it = " << *it << std::endl; // المخرج: 1
return 0;
}
المخرجات:
*it = 1
*it = 1
(4) 3.4 إزالة العناصر بأمان
الطريقة الخاطئة:
for (auto it = v.begin(); it != v.end(); ++it) {
### ▶ مثال 2: تطبيق ميزات C++ الحديثة (الصعوبة ⭐)
if (*it % 2 == 0) {
v.erase(it); // ❌ it مبطَل!
}
}
الطريقة الصحيحة:
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // ✅ erase يُرجع المكرّر الصالح التالي
} else {
++it;
}
}
المخرجات:
(مخرجات البرنامج)
4. المكرّرات العكسية
(1) 4.1 ما هي المكرّرات العكسية؟
المكرّرات العكسية تجتاز الحاوية من النهاية إلى البداية.
مثال: الإخراج العكسي (الصعوبة ⭐)
#include <iostream>
#include <vector>
int main() {
std::vector`<int>` v = {1, 2, 3, 4, 5};
// استخدام مكرّر عكسي
for (auto it = v.rbegin(); it != v.rend(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
النتيجة:
5 4 3 2 1
5. مكرّرات الإدراج
(1) 5.1 ما هي مكرّرات الإدراج؟
مكرّرات الإدراج هي مكرّرات إخراج تُستخدم لإدراج عناصر في حاوية.
ثلاثة أنواع من مكرّرات الإدراج:
| المكرّر | الوظيفة | مثال |
|---|---|---|
back_inserter |
إدراج في الخلف | std::back_inserter(v) |
front_inserter |
إدراج في الأمام | std::front_inserter(l) |
inserter |
إدراج في موضع محدد | std::inserter(v, v.begin()) |
(2) 5.2 مثال: النسخ باستخدام back_inserter (الصعوبة ⭐⭐)
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
int main() {
std::vector`<int>` src = {1, 2, 3, 4, 5};
std::vector`<int>` dst; // حاوية فارغة
// ❌ خطأ! dst لا يملك مساحة كافية
// std::copy(src.begin(), src.end(), dst.begin());
// ✅ صحيح! استخدم back_inserter للتوسيع التلقائي
std::copy(src.begin(), src.end(), std::back_inserter(dst));
std::cout << "نتيجة النسخ: ";
for (int x : dst) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
المخرجات:
1 2 3 4 5
6. مكرّرات التدفق
(1) 6.1 مكرّرات تدفق الإدخال
الوظيفة: قراءة البيانات من تدفق إدخال.
مثال: القراءة من المدخلات القياسية (الصعوبة ⭐⭐)
#include <iostream>
#include <vector>
#include <iterator>
int main() {
std::vector`<int>` v;
std::cout << "أدخل بعض الأرقام (Ctrl+Z للإنهاء):" << std::endl;
// القراءة من المدخلات القياسية
std::copy(std::istream_iterator`<int>`(std::cin),
std::istream_iterator`<int>`(),
std::back_inserter(v));
std::cout << "أدخلت: ";
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
(2) 6.2 مكرّرات تدفق الإخراج
الوظيفة: كتابة البيانات إلى تدفق إخراج.
مثال: الإخراج إلى ملف (الصعوبة ⭐⭐)
#include <iostream>
#include <vector>
#include <iterator>
#include <fstream>
int main() {
std::vector`<int>` v = {1, 2, 3, 4, 5};
// الإخراج إلى المخرجات القياسية
std::copy(v.begin(), v.end(),
std::ostream_iterator`<int>`(std::cout, " "));
std::cout << std::endl;
// الإخراج إلى ملف
std::ofstream file("output.txt");
std::copy(v.begin(), v.end(),
std::ostream_iterator`<int>`(file, "\n"));
file.close();
return 0;
}
❓ أسئلة شائعة
list هو قائمة مرتبطة — العناصر غير متجاورة في الذاكرة، لذا لا يمكن الوصول إليها مباشرة بالفهرس.erase لتحديث المكرّرات 3. فضّل استخدام الخوارزميات على معالجة المكرّرات يدويًاس: ما هو const_iterator؟ ج:
const_iteratorهو مكرّر للقراءة فقط لا يمكنه تعديل قيم العناصر.
std::vector`<int>` v = {1, 2, 3};
std::vector`<int>`::const_iterator it = v.cbegin();
// *it = 10; // ❌ خطأ! لا يمكن التعديل
▶ مثال 3: الاجتياز بالمكرّرات (الصعوبة ⭐)
#include <iostream>
#include <vector>
int main() {
std::vector`<int>` v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
المخرجات:
1 2 3 4 5
* لفك المرجع، و++ للتحرك، و!= لمقارنة المواضع.
| الموضوع | النقاط الرئيسية |
|---|---|
| فئات المكرّرات | الإدخال → أمامي → ثنائي الاتجاه → وصول عشوائي |
| بطلان المكرّرات | vector قد يُبطِل، list لا يُبطِل |
| المكرّرات العكسية | rbegin()/rend() |
| مكرّرات الإدراج | back_inserter وغيرها |
| مكرّرات التدفق | تربط STL بعمليات الإدخال/الإخراج |
📖 ملخص
- المكرّرات: كائنات شبيهة بالمؤشرات تُستخدم لاجتياز عناصر الحاوية
- فئات المكرّرات: إدخال/إخراج/أمامي/ثنائي الاتجاه/وصول عشوائي
- begin()/end(): الحصول على مكرّرات بداية ونهاية الحاوية
- بطلان المكرّرات: الإدراج/الحذف قد يُبطِل المكرّرات
📝 تمارين
-
أساسي (الصعوبة ⭐): استخدم المكرّرات لاجتياز
vector<int>وإخراج جميع العناصر. افعل ذلك باستخدام begin/end وحلقة for النطاقية. -
متوسط (الصعوبة ⭐⭐): استخدم المكرّرات العكسية rbegin/rend لاجتياز vector بشكل عكسي وإخراجه. لاحظ الفرق عن الاجتياز الأمامي.
-
متقدم (الصعوبة ⭐⭐⭐): نفّذ مكرّرًا مخصصًا يغلّف نطاقًا صحيحًا (مثلاً من 1 إلى 10)، يدعم عاملَي
++و*.
- المكرّرات هي الجسر بين الحاويات والخوارزميات
- خمسة أنواع من المكرّرات: إدخال/إخراج/أمامي/ثنائي الاتجاه/وصول عشوائي
- حلقات for النطاقية تُنفَّذ باستخدام المكرّرات في الخلفية
- بطلان المكرّرات: بعض المكرّرات تصبح غير قابلة للاستخدام بعد الإدراج/الحذف
- const_iterator يوفّر وصول القراءة فقط للعناصر
الدرس التالي: كائنات دوال STL (#38)