malloc موروثة من لغة C، وnew جزء من لغة C++ نفسها. الاثنان يحجزان ذاكرة على الكومة (heap)، لكن السؤال الحقيقي ليس "أيهما أسرع"، بل: هل تحتاج بايتات، أم تحتاج كائنًا؟
mallocتعطيك بايتات خامًا بالحجم الذي طلبته، ولا تعرف ما الذي ستضعه فيها.newتعطيك كائنًا جاهزًا: تحجز الذاكرة ثم تستدعي الباني (constructor). وdeleteتستدعي الهادم (destructor) قبل أن تحرّر.
كل الأمثلة هنا مُترجمة ومُشغّلة على GCC 13 بمعيار C++17، والأخطاء مكشوفة بـ AddressSanitizer.
الفرق في مثال واحد
#include <cstdlib>
#include <iostream>
#include <new>
#include <string>
struct User {
std::string name;
User() : name("ضيف") { std::cout << "User() استُدعي\n"; }
~User() { std::cout << "~User() استُدعي\n"; }
};
int main() {
User* a = new User; // يحجز ويستدعي الباني
std::cout << a->name << "\n";
delete a; // يستدعي الهادم ثم يحرّر
}
User() استُدعي
ضيف
~User() استُدعي
لاحظ أن new User تُرجع User* مباشرة. أما malloc فتُرجع void* وتحتاج تحويلًا صريحًا، ولا تطبع شيئًا لأن الباني لا يُستدعى أصلًا.
لماذا لا تصلح malloc للكائنات: مثال يبدو أنه يعمل
#include <cstdlib>
#include <iostream>
#include <string>
struct User { std::string name; };
int main() {
User* u = static_cast<User*>(std::malloc(sizeof(User)));
u->name = "سارة"; // name لم يُبنَ أصلًا
std::cout << u->name << "\n";
std::free(u);
}
ترجمناه وشغّلناه عاديًا فطبع سارة وانتهى بنجاح. هذا أخطر ما في الأمر. الحقل name من نوع std::string، وله باني يهيّئ مؤشّراته الداخلية. malloc لم تستدعِه، فالإسناد كتب في كائن بحالة عشوائية. ما ظهر "صحيحًا" هنا كان مصادفة في محتوى الذاكرة. نفس البرنامج مع AddressSanitizer:
g++ -std=c++17 -g -fsanitize=address bad.cpp -o bad && ./bad
==5605==ERROR: AddressSanitizer: SEGV on unknown address ...
#3 ... in main bad.cpp:9
السطر 9 هو u->name = "سارة". وحتى لو مرّ الإسناد، free لن تستدعي هادم std::string، فأي ذاكرة حجزها النص ستضيع (memory leak).
إن كنت مضطرًا لاستخدام ذاكرة من malloc (مثلًا جاءتك من مكتبة C)، فابنِ الكائن فيها صراحةً بـ placement new، واهدمه يدويًا:
void* raw = std::malloc(sizeof(User));
User* b = new (raw) User; // يبني الكائن داخل ذاكرة موجودة، ولا يحجز شيئًا
std::cout << b->name << "\n";
b->~User(); // استدعاء الهادم يدويًا
std::free(raw); // ثم التحرير بنفس نظام الحجز
هذا نادر في كود التطبيقات العادي، لكنه يوضّح أن new العادية هي فعليًا خطوتان: حجز + بناء.
عند فشل الحجز
void* p = std::malloc(huge); // تُرجع NULL، ويجب أن تفحصها بنفسك
char* q = new char[huge]; // ترمي std::bad_alloc
char* r = new (std::nothrow) char[huge]; // تُرجع nullptr بدل الاستثناء
جرّبناها بحجم 2^62 بايت (أكبر من أي ذاكرة حقيقية):
malloc: NULL
new: std::bad_alloc: std::bad_alloc
new(nothrow): nullptr
الفرق مهم في التصميم: كود malloc يجب أن يفحص كل استدعاء، وإن نسيت فستستخدم مؤشّرًا فارغًا لاحقًا. أما new فتوقف التنفيذ عند نقطة الفشل نفسها برمي استثناء.
لا تخلط بين النظامين: ثلاث تركيبات خاطئة
int* a = static_cast<int*>(std::malloc(sizeof(int)));
delete a; // ❌ malloc مع delete
int* b = new int(5);
std::free(b); // ❌ new مع free
int* c = new int[10];
delete c; // ❌ new[] مع delete (الصحيح delete[])
الثلاثة سلوك غير معرَّف. الخبر الجيد أن أدواتك تمسكها إن طلبت منها:
- وقت الترجمة: GCC 13 مع
-Wallيحذّر في الحالات الثلاث، مثلًا:
warning: 'void operator delete(void*, long unsigned int)' called on pointer returned
from a mismatched allocation function [-Wmismatched-new-delete]
- وقت التشغيل: AddressSanitizer يوقف البرنامج ويسمّي الخطأ بدقة:
ERROR: AddressSanitizer: alloc-dealloc-mismatch (malloc vs operator delete)
ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete)
القاعدة: malloc/calloc/realloc مع free، وnew مع delete، وnew[] مع delete[].
جدول المقارنة
malloc / free | new / delete | |
|---|---|---|
| ما تعطيك | بايتات خام بحجم تحسبه أنت (sizeof) | كائن مبني من النوع المطلوب |
| الباني والهادم | لا يُستدعيان | يُستدعيان تلقائيًا |
| نوع القيمة المُرجَعة | void* يحتاج تحويلًا | T* مباشرة |
| عند الفشل | تُرجع NULL | ترمي std::bad_alloc (أو nullptr مع nothrow) |
| تغيير الحجم | realloc (للبيانات البسيطة فقط) | لا يوجد؛ استخدم std::vector |
| المصفوفات | نفس الدالة | new[] وdelete[] منفصلان |
إذن، أيهما تستخدم؟
في كود C++ حديث، غالبًا لا هذا ولا ذاك مباشرة:
#include <memory>
#include <vector>
auto u = std::make_unique<User>(); // بدل new User، ولا تحتاج delete أبدًا
std::vector<int> nums(10); // بدل new int[10] وmalloc
nums.resize(20); // بدل realloc
std::make_unique تستدعي new في الداخل، لكنها تضمن استدعاء delete حين يخرج المؤشّر من نطاقه، حتى لو رُمي استثناء في المنتصف. هذه فكرة RAII المشروحة في مبدأ RAII، وأنواع المؤشّرات في المؤشّرات الذكية.
وكقاعدة قرار:
- تكتب كود C++ عاديًا؟
std::make_uniqueوstd::vectorوstd::string. لاnewولاmallocفي الغالب. - تحتاج
newصريحة؟ حين تبني بنية بيانات منخفضة المستوى بنفسك، وحتى هنا غلّف المؤشّر فيunique_ptrفورًا. - تحتاج
malloc؟ فقط على الحدود مع مكتبة C تحرّر الذاكرة بـfreeأو تتوقعها منك، ولبيانات بسيطة بلا باني.
ولأن هذه الأخطاء "تعمل" أحيانًا قبل أن تنهار، اجعل -Wall -fsanitize=address جزءًا من بناء التطوير عندك. وإن انتهى بك الأمر بانهيار، فخطوات تتبّعه في حل Segmentation fault. أكمل إدارة الذاكرة من الأساس في درس المراجع وإدارة الذاكرة ثم المؤشّرات الذكية.