nullptr و Dangling Pointers — المؤشرات والذاكرة
nullptr والمؤشرات المعلّقة وأخطاء الذاكرة
أخطاء الذاكرة هي أخطر أخطاء C++: لا يكتشفها المترجم، وقد لا تظهر في كل تشغيل، وتسبب ثغرات أمنية. معرفة أسمائها وأسبابها نصف الحل.
nullptr
في C++ الحديثة استخدم nullptr للمؤشر الفارغ. ستجد في الكود القديم NULL أو 0؛ nullptr أوضح وأكثر أماناً مع التحميل الزائد للدوال.
أشهر الأخطاء
| الخطأ | ماذا يحدث |
|---|---|
| إشارة إلى مؤشر فارغ | *p حيث p = nullptr: توقف فوري (segmentation fault) |
| مؤشر غير مهيأ | int* p; *p = 5; يكتب في عنوان عشوائي |
| مؤشر معلّق (dangling) | يشير لذاكرة حُررت أو لمتغير انتهى نطاقه |
| استخدام بعد التحرير (use-after-free) | القراءة أو الكتابة عبر مؤشر بعد delete |
| تحرير مزدوج (double free) | delete مرتين لنفس الذاكرة |
| تسرب (leak) | نسيان delete |
كيف ينشأ المؤشر المعلّق؟
int* p = new int(5);
int* q = p; // مؤشران لنفس الذاكرة
delete p;
p = nullptr; // p آمن الآن...
*q = 10; // ...لكن q ما زال يشير للذاكرة المحررة!جعل p يساوي nullptr لا يحمي المؤشرات الأخرى. المشكلة الأساسية: من يملك هذه الذاكرة ومسؤول عن تحريرها؟ المؤشرات الذكية تجيب عن هذا السؤال (الدرس القادم).
السلوك غير المعرّف (Undefined Behavior)
هذه الأخطاء لا تعني "توقف البرنامج" بالضرورة. قد يعمل البرنامج "بشكل صحيح" على جهازك، ويطبع قيمة خاطئة على جهاز آخر، أو يتوقف بعد ساعة. لا تثق بأن غياب التوقف دليل على السلامة.
أدوات الاكتشاف
- AddressSanitizer: ترجم بـ -fsanitize=address -g (g++ أو clang)، فيتوقف البرنامج عند أول خطأ ذاكرة ويطبع مكانه بالضبط.
- Valgrind (لينكس): يشغّل البرنامج ويبلغ عن التسرب والوصول غير الصالح.
- تفعيل التحذيرات -Wall -Wextra.
عادات تحميك
1. هيّئ كل مؤشر (بعنوان صالح أو nullptr).
2. افحص المؤشرات التي قد تكون فارغة قبل استخدامها.
3. لا ترجع عناوين متغيرات محلية.
4. فضّل المراجع، و vector، والمؤشرات الذكية على new/delete.
تمرين
الكود في المثال يوضح كل خطأ بأمان (دون تنفيذ السلوك غير المعرّف فعلياً). اقرأه، ثم اكتب دالة safeAverage(const int* arr, int size) تتعامل مع المؤشر الفارغ والحجم صفر.
#include <iostream>
using namespace std;
// دالة آمنة تتحقق من المؤشر
void safeDelete(int*& ptr) {
if (ptr != nullptr) {
delete ptr;
ptr = nullptr;
cout << "تم التحرير بأمان" << endl;
} else {
cout << "المؤشر فارغ أصلاً" << endl;
}
}
void processData(int* data) {
if (data == nullptr) {
cout << "خطأ: المؤشر فارغ!" << endl;
return;
}
cout << "القيمة: " << *data << endl;
}
int main() {
// ===== Wild Pointer (تجنب!) =====
// int* wild; // خطير! قيمة عشوائية
// cout << *wild; // سلوك غير محدد!
// الطريقة الصحيحة
int* safe = nullptr; // آمن ✓
// ===== التحقق من nullptr =====
if (safe == nullptr) {
cout << "المؤشر فارغ - تهيئة..." << endl;
safe = new int(42);
}
cout << "القيمة: " << *safe << endl;
// ===== Dangling Pointer =====
int* ptr = new int(100);
cout << "\nقبل delete: " << *ptr << endl;
delete ptr;
// cout << *ptr; // خطير! Dangling pointer!
ptr = nullptr; // الحل!
// ===== التحقق قبل الاستخدام =====
if (ptr != nullptr) {
cout << *ptr << endl;
} else {
cout << "المؤشر فارغ (آمن)" << endl;
}
// ===== استخدام دالة آمنة =====
int* num = new int(200);
safeDelete(num); // يحرر ويجعله nullptr
safeDelete(num); // آمن - يطبع رسالة
// ===== Memory Leak (تجنب!) =====
// int* leak = new int(50);
// leak = new int(60); // فقدنا الأولى!
// الطريقة الصحيحة
int* noLeak = new int(50);
delete noLeak; // حرر الأولى
noLeak = new int(60); // ثم اعطه الجديدة
delete noLeak;
// ===== دالة مع تحقق =====
int value = 300;
processData(&value); // يعمل
processData(nullptr); // يطبع رسالة خطأ
// ===== تنظيف نهائي =====
safeDelete(safe);
cout << "\n✓ البرنامج انتهى بدون تسريب!" << endl;
return 0;
}اختبر فهمك