تسريب الذاكرة ليس ذاكرة اختفت. بل ذاكرة ما تزال ملكك، وما تزال محجوزة، وفقدت القدرة على إعادتها - لأن آخر مؤشّر إليها زال. ولا شيء ينهار. فالبرنامج يواصل العمل، أثقل قليلًا في كل مرة، حتى يفشل شيء ما في النهاية في موضع غير ذي صلة.
تغطّي هذه الصفحة كيف تنشأ التسريبات، والانضباط الذي يمنع معظمها، والأداتين اللتين تجدان الباقي.
كيف يبدو التسريب
يكتب كل تكرار فوق block مؤشّرًا جديدًا. والكتلة السابقة ما تزال مخصَّصة؛ ولا متغيّر يحمل عنوانها؛ ولا يمكن تحريرها أبدًا. فثلاثة تكرارات تفقد اثني عشر كيلوبايتًا. وخادم يفعل ذلك مرة لكل طلب يفقدها إلى الأبد، بالمعدّل الذي تصل به الطلبات.
والإصلاح سطر واحد - free(block); في نهاية الجسم - لكن إدراك أين ينتمي هو المهارة الفعلية.
كيف تحدث التسريبات
1. المؤشّر الضائع
أي إسناد إلى مؤشّر ما زال يحمل المرجع الوحيد لكتلة حيّة يسرّبها.
char *name = malloc(32);
name = malloc(64); /* the first 32 bytes are now unreachable */
والحلقة أعلاه هي الخطأ نفسه مرتديًا حلقة. وكذلك إعادة إسناد حقل بنية، وكذلك اختصار realloc من calloc وrealloc:
p = realloc(p, n); /* on failure: p becomes NULL, old block orphaned */
2. العودة المبكرة
على كل مسار خروج من دالة أن يحرّر ما أخذته الدالة سلفًا. والمسار الذي يُنسى هو مسار خطأ دائمًا.
المسار السعيد صحيح ومسار الخطأ يسرّب، ولهذا يصمد هذا أمام الاختبار: فالفرع الفاشل لا يُنفَّذ تقريبًا أثناء التطوير. والإصلاح قسم تنظيف واحد يقفز إليه كل مسار:
وهذا هو الاستخدام الوحيد لـ goto الذي يوصي به مبرمجو C المخضرمون فعليًا. وهو يعمل لأن كل مؤشّر يبدأ بـ NULL وfree(NULL) لا تفعل شيئًا، فتكون كتلة خروج واحدة صحيحة مهما بلغت الدالة.
3. الملكية غير الواضحة
أدقّ التسريبات ليست أخطاء برمجية أصلًا - بل دالّتان تختلفان في تحديد مَن كانت مهمّته.
char *build_message(void); /* does the caller free this? */
void store(char *text); /* does store take ownership? */
إن أرجعت build_message ذاكرةً مخصَّصة ونسختها store، فعلى المستدعي التحرير. وإن احتفظت store بالمؤشّر، فعلى المستدعي ألّا يحرّر. ولا شيء في الشيفرة يقول أيّهما، فيُتَّخذ أحد الافتراضين مرتين - فتحصل إما على تسريب وإما على تحرير مزدوج.
والعلاج عرف يُذكَر في تعليق بجوار كل دالة تخصّص:
/* Returns a newly allocated string; the caller must free it. */
char *build_message(void);
/* Takes ownership of 'text'; it will be freed by store_free(). */
void store(char *text);
اكتب القاعدة عند الدالة لا في وثيقة تصميم. فهي أعلى عادة قيمةً في إدارة الذاكرة بلغة C.
انضباط الملكية
أربع قواعد تغطّي كل شيء تقريبًا:
- لكل تخصيص مالك واحد بالضبط - قطعة شيفرة واحدة مسؤولة عن تحريره.
- اقرن كل دالة تخصّص بأخرى تحرّر.
vec_init/vec_free، وconfig_load/config_free. فالتماثل يجعل الاستدعاء المفقود مرئيًا. - حرّر في الطبقة نفسها التي خصّصت، ما لم ينقل تعليق الدالة الملكية صراحةً.
- اضبط المؤشّر على
NULLبعد تحريره، ليتعطّل استخدام لاحق عرضي عند موضع الخطأ بدل إفساد الكومة بهدوء.
إيجاد التسريبات: valgrind
على لينكس، لا يحتاج valgrind إلى إعادة ترجمة، وإن كانت رموز التنقيح تجعل التقرير مقروءًا:
gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program
وللحلقة المسرِّبة في أعلى هذه الصفحة، ينتهي التقرير بشيء مثل:
==12345== HEAP SUMMARY:
==12345== in use at exit: 12,000 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345== by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 12,000 bytes in 3 blocks
اقرأه من الأسفل. فـ "definitely lost" تعني أنه لم يكن هناك مؤشّر إلى الكتلة عند الخروج - وهذا تسريب حقيقي. ويسمّي أثر المكدّس سطر الـ malloc التي أنشأتها، لا السطر الذي ضاعت فيه، وهذا يكفي عادةً لإيجاد free المفقودة.
وتظهر فئتان أخريان:
- indirectly lost - كتل لا يمكن بلوغها إلا عبر كتلة ضاعت هي نفسها، مثل عناصر قائمة مترابطة مسرَّبة. أصلح ما هو "definitely lost" فتختفي هذه.
- still reachable - مخصَّصة عند الخروج لكن مع مؤشّر حيّ، وهي عادةً مخزن مؤقّت عام. وليست تسريبًا بالمعنى الخطير، لكنها تستحق التحرير ليبقى التقرير فارغًا.
كما يلتقط valgrind قراءات الذاكرة غير المهيّأة والكتابات بعد نهاية كتلة، وهو غالبًا كيف تكتشف الخطأ الكامن وراء التسريب.
إيجاد التسريبات: AddressSanitizer
AddressSanitizer مدمج في GCC وClang، ويعمل أسرع بكثير من valgrind، ويعمل حيث لا يعمل valgrind (بما في ذلك macOS الحالي):
gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program
ويُطبَع تقرير التسريب عند الخروج تلقائيًا:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
#0 0x7f... in malloc
#1 0x1086... in main program.c:6
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).
كما يحوّل ASan الاستخدام بعد التحرير وفيضانات مخازن الكومة إلى إنهاءات فورية واضحة التسمية بدل إفساد غامض لاحقًا. ابنِ تشغيلات الاختبار به مفعّلًا؛ وابنِ الإصدارات به معطّلًا، فهو يكلّف ذاكرة وسرعة.
وإن لم يعمل كشف التسريبات على منصّتك، فاضبط ASAN_OPTIONS=detect_leaks=1 في البيئة قبل التشغيل.
إصلاح برنامج مسرِّب خطوة بخطوة
إليك برنامجًا صغيرًا بثلاثة تسريبات منفصلة:
يبلّغ valgrind عن ثلاثة سجلّات "definitely lost" بثلاثة أرقام أسطر مختلفة. ومُصلَحة واحدًا تلو الآخر:
الإصلاح 1 هو تعليق الملكية مجسَّدًا: فـ shout تخصّص وmain تحرّر. والإصلاح 2 يزيل التخصيص المزدوج كليًا بدل تحرير الأول - فالشيفرة الأبسط هي أيضًا الشيفرة الصحيحة. والإصلاح 3 يضيف free المفقودة عند العودة المبكرة؛ ومع تخصيصات أكثر، يتوسّع عنوان cleanup: الواحد المعروض سابقًا أفضل من تكرار التحريرات.
عادات تمنع التسريبات
- اكتب
freeفورًا بعد كتابةmalloc، ثم املأ الشيفرة بينهما. - امنح كل دالة تخصّص دالةَ تحرير مقابلة.
- اذكر الملكية في تعليق على أي دالة تُرجِع أو تأخذ مؤشّرًا خصّصته.
- استخدم كتلة خروج
cleanup:واحدة في الدوال التي تحمل عدة تخصيصات. - شغّل اختباراتك تحت
-fsanitize=addressكأمر معتاد، لا حين يبدو شيء خاطئًا فقط. - عامِل "definitely lost: 0 bytes" كجزء من تشغيل اختبار ناجح.
الأسئلة الشائعة
ما تسريب الذاكرة في لغة C؟
ذاكرة خصّصتها بـ malloc ولم تعد تستطيع تحريرها، لأن لا شيء في البرنامج ما زال يشير إليها. وتبقى الكتلة محجوزة طوال عمر العملية. وهذا ليس انهيارًا - فالبرنامج يواصل العمل، لكنه يستخدم ذاكرة أكثر في كل مرور حتى تنفد في النهاية.
كيف أجد تسريبات الذاكرة في لغة C؟
شغّل البرنامج تحت valgrind: valgrind --leak-check=full ./program. فهو يبلّغ عن كل كتلة ما تزال مخصَّصة عند الخروج مع أثر مكدّس الـ malloc التي أنشأتها. وعلى macOS أو حيث لا يتوفّر valgrind، ترجم بـ -fsanitize=address فيخرج التقرير نفسه عند الخروج.
ما أسباب تسريبات الذاكرة في لغة C؟
ثلاثة أنماط تغطّيها كلها تقريبًا: الكتابة فوق المؤشّر الوحيد إلى كتلة (بما في ذلك p = realloc(p, n) عند الفشل)، والعودة المبكرة من دالة خصّصت سلفًا، والملكية غير الواضحة - فتفترض كل من دالّتين أن الأخرى تحرّرها، فلا تحرّرها أي منهما.
هل تهمّ تسريبات الذاكرة إن كان البرنامج سينتهي على أي حال؟
لبرنامج يعمل مرة واحدة وينتهي، يستعيد نظام التشغيل كل شيء، فالأثر العملي معدوم. وهي تهمّ لأي شيء طويل التشغيل - خادم، أو حلقة لعبة، أو خدمة خلفية - حيث ينمو تسريب لكل طلب بلا حدّ. وحرّر باتّساق على أي حال: فتقرير التسريبات ضجيج يخفي التسريبات التي تهمّ فعلًا.