C: إدارة الذاكرة الديناميكية
الذاكرة الديناميكية كاستئجار شقة — تطلبها عندما تحتاجها (malloc)، وتُخلّيها عندما تنتهي (free). إذا لم تُخلّها أبدًا، فستستمر في احتلال الموارد — وهذا تسرب للذاكرة.
1. لماذا الذاكرة الديناميكية
المتغيرات المحلية على المكدس تختفي عند عودة الدالة، وأحجام المصفوفات يجب تحديدها وقت الترجمة. عندما يُعرف حجم البيانات فقط وقت التشغيل، أو تحتاج إلى بقاء البيانات عبر استدعاءات الدوال، تحتاج إلى ذاكرة ديناميكية على الكومة.
int n;
scanf("%d", &n);
int arr[n];
malloc.
2. malloc و free
تُخصّص malloc عددًا محددًا من البايتات على الكومة وتُرجع void *. يجب عليك free الذاكرة عند الانتهاء.
int *p = (int *)malloc(sizeof(int) * 5);
if (p == NULL) {
printf("Memory allocation failed\n");
return 1;
}
for (int i = 0; i < 5; i++) {
p[i] = i * 10;
}
for (int i = 0; i < 5; i++) {
printf("%d ", p[i]);
}
free(p);
p = NULL;
0 10 20 30 40
malloc لا تُهيّئ الذاكرة — محتواها قيم مهملة. تحقق دائمًا من القيمة المُرجعة مقابل NULL بعد التخصيص. ضع المؤشر إلى NULL بعد free هو ممارسة جيدة لمنع المؤشرات المعلقة.
3. calloc
تُخصّص calloc ذاكرة وتهيّئ كل بت إلى 0. وسائطها هما عدد العناصر وحجم كل عنصر.
int *p = (int *)calloc(5, sizeof(int));
if (p) {
for (int i = 0; i < 5; i++) {
printf("%d ", p[i]);
}
free(p);
}
0 0 0 0 0
مقارنة malloc و calloc:
| الدالة | التهيئة | الوسائط |
|---|---|---|
malloc |
بدون تهيئة | إجمالي البايتات |
calloc |
تصفير | عدد العناصر، حجم كل عنصر |
4. realloc
تُغيّر realloc حجم الذاكرة المُخصصة سابقًا — يمكنها التوسيع أو التقليص.
int *p = (int *)malloc(sizeof(int) * 3);
p[0] = 10; p[1] = 20; p[2] = 30;
int *tmp = (int *)realloc(p, sizeof(int) * 5);
if (tmp) {
p = tmp;
p[3] = 40;
p[4] = 50;
for (int i = 0; i < 5; i++) {
printf("%d ", p[i]);
}
free(p);
}
10 20 30 40 50
realloc عنوانًا جديدًا (تُنسخ البيانات الأصلية تلقائيًا) أو نفس العنوان. لا تكتب أبدًا p = realloc(p, ...) — إذا فشلت وأرجعت NULL، فُقد المؤشر الأصلي. استخدم متغيرًا مؤقتًا لاستقبال النتيجة.
5. مصفوفة ديناميكية أحادية البُعد
▶ مثال
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n;
printf("Enter number of elements: ");
scanf("%d", &n);
int *arr = (int *)calloc(n, sizeof(int));
if (!arr) return 1;
for (int i = 0; i < n; i++) {
arr[i] = (i + 1) * (i + 1);
}
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
arr = NULL;
return 0;
}
Enter number of elements: 5
1 4 9 16 25
6. مصفوفة ديناميكية ثنائية البُعد
هناك طريقتان شائعتان لتنفيذ المصفوفات الديناميكية ثنائية البُعد.
(1) الطريقة 1: مصفوفة المؤشرات (كل صف مُخصص بشكل مستقل)
int rows = 3, cols = 4;
int **matrix = (int **)malloc(sizeof(int *) * rows);
for (int i = 0; i < rows; i++) {
matrix[i] = (int *)calloc(cols, sizeof(int));
}
matrix[1][2] = 99;
printf("%d\n", matrix[1][2]);
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
99
(2) الطريقة 2: ذاكرة متجاورة (تخصيص واحد)
int rows = 3, cols = 4;
int *buf = (int *)calloc(rows * cols, sizeof(int));
int **matrix = (int **)malloc(sizeof(int *) * rows);
for (int i = 0; i < rows; i++) {
matrix[i] = buf + i * cols;
}
matrix[2][3] = 77;
printf("%d\n", matrix[2][3]);
free(matrix);
free(buf);
77
▶ مثال
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int rows = 3, cols = 4;
int **m = (int **)malloc(sizeof(int *) * rows);
int *buf = (int *)calloc(rows * cols, sizeof(int));
for (int i = 0; i < rows; i++) {
m[i] = buf + i * cols;
}
int val = 1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
m[i][j] = val++;
}
}
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%3d", m[i][j]);
}
printf("\n");
}
free(m);
free(buf);
return 0;
}
1 2 3 4
5 6 7 8
9 10 11 12
7. أخطاء الذاكرة الديناميكية الشائعة
(1) نسيان free — تسرب الذاكرة
void leak(void) {
int *p = (int *)malloc(sizeof(int) * 100);
}
عند انتهاء الدالة، يختفي المتغير المحلي p، لكن الذاكرة المئة عدد صحيح على الكومة لا تزال محتلة ولا يمكن تحريرها أبدًا.
(2) التحرير المزدوج
int *p = (int *)malloc(sizeof(int));
free(p);
free(p);
free على نفس الذاكرة مرتين هو سلوك غير مُعرّف وقد يُعطّل البرنامج. ضع المؤشر إلى NULL فورًا بعد free — استدعاء free(NULL) آمن.
(3) استخدام ذاكرة مُحرّرة
int *p = (int *)malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);
(4) الوصول خارج الحدود
int *p = (int *)malloc(sizeof(int) * 5);
p[5] = 100;
خُصصت 5 عناصر بفهارس من 0 إلى 4، لذا p[5] خارج الحدود.
8. مقدمة في valgrind
Valgrind هو أداة فحص ذاكرة على لينكس يمكنها كشف تسريبات الذاكرة والوصول خارج الحدود والقراءات غير المُهيأة.
gcc -g -o myapp myapp.c
valgrind --leak-check=full ./myapp
مخرجات نموذجية:
==12345== HEAP SUMMARY:
==12345== in use at exit: 400 bytes in 1 blocks
==12345== total heap usage: 2 allocs, 1 frees, 800 bytes allocated
==12345==
==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x10915E: leak (myapp.c:5)
❓ أسئلة شائعة
📖 ملخص
malloc(size)تُخصّص بدون تهيئة،calloc(n, size)تُخصّص وتُصفّر،realloc(ptr, size)تُغيّر الحجم- للمصفوفات الديناميكية ثنائية البُعد، يُنصح بنهج "تخصيص متجاور واحد + مصفوفة مؤشرات لرسم الصفوف"
- يجب أن يقترن malloc دائمًا بـ free — نسيان free يسبب تسرب الذاكرة
- الأخطاء الشائعة: التسريبات، التحرير المزدوج، المؤشرات المعلقة، الوصول خارج الحدود
- valgrind يمكنها كشف مشاكل الذاكرة تلقائيًا — استخدمها بانتظام أثناء التطوير
📝 تمارين
- اكتب برنامجًا: يُدخل المستخدم n، خُصّص ديناميكيًا n أعداد صحيحة، اقرأ n أعداد، رتّبها ثم اطبعها، ثم حرّر الذاكرة
- اكتب دوال مصفوفة ديناميكية ثنائية البُعد:
int **create_matrix(int rows, int cols)وvoid free_matrix(int **m, int rows). في main، أنشئ مصفوفة 4×5، عيّن قيمًا واطبعها - أنشئ تسرب ذاكرة متعمدًا في كودك، ثم استخدم valgrind (أو Dr. Memory) لكشفه وافحص المخرجات