C: إدارة الذاكرة الديناميكية

الذاكرة الديناميكية كاستئجار شقة — تطلبها عندما تحتاجها (malloc)، وتُخلّيها عندما تنتهي (free). إذا لم تُخلّها أبدًا، فستستمر في احتلال الموارد — وهذا تسرب للذاكرة.

1. لماذا الذاكرة الديناميكية

المتغيرات المحلية على المكدس تختفي عند عودة الدالة، وأحجام المصفوفات يجب تحديدها وقت الترجمة. عندما يُعرف حجم البيانات فقط وقت التشغيل، أو تحتاج إلى بقاء البيانات عبر استدعاءات الدوال، تحتاج إلى ذاكرة ديناميكية على الكومة.

C
int n;
scanf("%d", &n);
int arr[n];
⚠️ ملاحظة: المصفوفات متغيرة الطول (VLA) هي ميزة C99 لا تدعمها جميع المترجمات، ولا يُنصح بها للمصفوفات الكبيرة. الطريقة الصحيحة هي استخدام malloc.


2. malloc و free

تُخصّص malloc عددًا محددًا من البايتات على الكومة وتُرجع void *. يجب عليك free الذاكرة عند الانتهاء.

C
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;
TEXT 📖 للعرض فقط
0 10 20 30 40
💡 نصيحة: malloc لا تُهيّئ الذاكرة — محتواها قيم مهملة. تحقق دائمًا من القيمة المُرجعة مقابل NULL بعد التخصيص. ضع المؤشر إلى NULL بعد free هو ممارسة جيدة لمنع المؤشرات المعلقة.


3. calloc

تُخصّص calloc ذاكرة وتهيّئ كل بت إلى 0. وسائطها هما عدد العناصر وحجم كل عنصر.

TEXT 📖 للعرض فقط
int *p = (int *)calloc(5, sizeof(int));
if (p) {
    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }
    free(p);
}
TEXT 📖 للعرض فقط
0 0 0 0 0

مقارنة malloc و calloc:

الدالة التهيئة الوسائط
malloc بدون تهيئة إجمالي البايتات
calloc تصفير عدد العناصر، حجم كل عنصر

4. realloc

تُغيّر realloc حجم الذاكرة المُخصصة سابقًا — يمكنها التوسيع أو التقليص.

C
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);
}
TEXT 📖 للعرض فقط
10 20 30 40 50
⚠️ ملاحظة: قد تُرجع realloc عنوانًا جديدًا (تُنسخ البيانات الأصلية تلقائيًا) أو نفس العنوان. لا تكتب أبدًا p = realloc(p, ...) — إذا فشلت وأرجعت NULL، فُقد المؤشر الأصلي. استخدم متغيرًا مؤقتًا لاستقبال النتيجة.


5. مصفوفة ديناميكية أحادية البُعد

▶ مثال

C
#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;
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Enter number of elements: 5
1 4 9 16 25

6. مصفوفة ديناميكية ثنائية البُعد

هناك طريقتان شائعتان لتنفيذ المصفوفات الديناميكية ثنائية البُعد.

(1) الطريقة 1: مصفوفة المؤشرات (كل صف مُخصص بشكل مستقل)

C
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);
TEXT 📖 للعرض فقط
99

(2) الطريقة 2: ذاكرة متجاورة (تخصيص واحد)

TEXT 📖 للعرض فقط
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);
TEXT 📖 للعرض فقط
77
💡 نصيحة: الطريقة 2 ذاكرتها متجاورة، وهي صديقة للذاكرة المخبئية وأبسط في التحرير. الطريقة 1 تسمح لكل صف بعدد مختلف من الأعمدة (مصفوفة مسننة).

▶ مثال

C
#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;
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
  1  2  3  4
  5  6  7  8
  9 10 11 12

7. أخطاء الذاكرة الديناميكية الشائعة

(1) نسيان free — تسرب الذاكرة

C
void leak(void) {
    int *p = (int *)malloc(sizeof(int) * 100);
}

عند انتهاء الدالة، يختفي المتغير المحلي p، لكن الذاكرة المئة عدد صحيح على الكومة لا تزال محتلة ولا يمكن تحريرها أبدًا.

(2) التحرير المزدوج

C
int *p = (int *)malloc(sizeof(int));
free(p);
free(p);
⚠️ ملاحظة: استدعاء free على نفس الذاكرة مرتين هو سلوك غير مُعرّف وقد يُعطّل البرنامج. ضع المؤشر إلى NULL فورًا بعد free — استدعاء free(NULL) آمن.

(3) استخدام ذاكرة مُحرّرة

TEXT 📖 للعرض فقط
int *p = (int *)malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);
⚠️ ملاحظة: الوصول إلى الذاكرة بعد تحريرها هو مؤشر معلق — النتائج غير متوقعة.

(4) الوصول خارج الحدود

C
int *p = (int *)malloc(sizeof(int) * 5);
p[5] = 100;

خُصصت 5 عناصر بفهارس من 0 إلى 4، لذا p[5] خارج الحدود.


8. مقدمة في valgrind

Valgrind هو أداة فحص ذاكرة على لينكس يمكنها كشف تسريبات الذاكرة والوصول خارج الحدود والقراءات غير المُهيأة.

BASH
gcc -g -o myapp myapp.c
valgrind --leak-check=full ./myapp

مخرجات نموذجية:

TEXT 📖 للعرض فقط
==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)
💡 نصيحة: على ويندوز يمكنك استخدام Dr. Memory أو كومة تصحيح Visual Studio. على macOS، استخدم أداة Leaks في Instruments.


❓ أسئلة شائعة

س ما القيم الموجودة في الذاكرة التي تُرجعها malloc؟
ج غير مُعرّفة — بيانات مهملة متبقية من استخدامات سابقة. استخدم calloc إذا احتجت ذاكرة مُصفّاة، أو استدعِ memset يدويًا.
س هل يبقى للمؤشر قيمة بعد free؟
ج free تُحرّر الذاكرة فقط — لا تُغيّر قيمة المؤشر. لا يزال يحمل ذلك العنوان (مؤشر معلق). لهذا يجب وضعه إلى NULL فورًا بعد التحرير.
س هل ستفقد realloc البيانات الأصلية عند التوسيع؟
ج لا. realloc تضمن الحفاظ على البيانات الأصلية (على الأقل min(old_size, new_size) بايت). إذا انتقلت إلى عنوان جديد، تُنسخ تلقائيًا.
س هل يمكنني تحرير متغير على المكدس؟
ج قطعًا لا. free يمكنها تحرير ذاكرة الكومة المُرجعة من malloc/calloc/realloc فقط. استدعاء free على عنوان مكدس هو سلوك غير مُعرّف.

📖 ملخص

📝 تمارين

  1. اكتب برنامجًا: يُدخل المستخدم n، خُصّص ديناميكيًا n أعداد صحيحة، اقرأ n أعداد، رتّبها ثم اطبعها، ثم حرّر الذاكرة
  2. اكتب دوال مصفوفة ديناميكية ثنائية البُعد: int **create_matrix(int rows, int cols) و void free_matrix(int **m, int rows). في main، أنشئ مصفوفة 4×5، عيّن قيمًا واطبعها
  3. أنشئ تسرب ذاكرة متعمدًا في كودك، ثم استخدم valgrind (أو Dr. Memory) لكشفه وافحص المخرجات
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%