لماذا sizeof(الهيكل) لا يساوي مجموع sizeof(الحقول)؟ تحليل كامل

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

كيف يمكن ذلك؟ مقدمة لمفارقة sizeof

\n\n

تخيل: تقوم بإنشاء هيكل بسيط في C++ بحقلين — char (1 بايت) و int (4 بايت). من المنطقي افتراض أن حجمه سيكون 5 بايت. تكتب الكود، تستدعي sizeof و... تحصل على 8 بايت. «هراء!» — تعتقد. «هل أصيب المترجم بالجنون؟»

\n\n

هذا السؤال هو واحد من أكثر الأسئلة شيوعاً على Stack Overflow (أكثر من 200,000 مشاهدة). وهو ليس عن خطأ في المترجم. إنه عن كيفية عمل المعالج والذاكرة العشوائية فعلياً على المستوى المنخفض. في هذه المقالة، سنقوم بتحليل محاذاة الهياكل في C++، مفهوم الحشو (padding)، ونتعلم كيفية توقع الحجم الفعلي للهياكل. والأهم من ذلك، سنعرف كيف نتحكم في ذلك عندما يكون ضرورياً حقاً.

\n\n

لماذا 1 + 4 ≠ 5؟ فيزياء العملية

\n\n

المعالجات الحديثة لا تقرأ الذاكرة بايتاً بايتاً. إنها تعمل بـ «كلمات» — كتل بحجم 4 أو 8 أو 16 بايت (حسب البنية). إذا كان عدد صحيح بحجم 4 بايت (int) موجوداً في عنوان غير قابل للقسمة على 4، فسيتعين على المعالج القيام بـ وصولين إلى الذاكرة بدلاً من واحد لقراءة هذا الرقم، ثم «تجميعه» من جزأين. هذا بطيء جداً.

\n\n

لتجنب ذلك، يقوم المترجم بإدراج بايتات «فارغة» (padding) بين حقول الهيكل. يقوم بمحاذاة كل حقل إلى عنوان قابل للقسمة على حجمه. وهذا ما يسمى المحاذاة الطبيعية (natural alignment).

\n\n
// مثال بسيط يفاجئ الجميع\n#include <iostream>\n\nstruct Example {\n    char c;   // 1 بايت\n    int  i;   // 4 بايتات\n};\n\nint main() {\n    std::cout << "حجم char: " << sizeof(char) << std::endl;    // 1\n    std::cout << "حجم int: "  << sizeof(int)  << std::endl;    // 4\n    std::cout << "حجم الهيكل: " << sizeof(Example) << std::endl; // 8 (!)\n    return 0;\n}
\n\n

كيف يبدو ذلك في الذاكرة؟ c يشغل البايت 0. البايتات 1، 2، 3 هي حشو (padding). و i يبدأ من البايت 4. المجموع: 1 (c) + 3 (فارغ) + 4 (i) = 8 بايتات.

\n\n

قواعد المحاذاة: كيف يتخذ المترجم القرار

\n\n

يتبع المترجم قواعد بسيطة ولكنها صارمة. بمعرفتها، ستتمكن من توقع حجم الهيكل دون تشغيل البرنامج.

\n\n
    \n
  • القاعدة 1: ترتيب الحقول محفوظ. لا يقوم المترجم بإعادة ترتيب الحقول في الذاكرة (على الرغم من أن بعض المترجمات قد تفعل ذلك عند التحسين، لكن المعيار لا يضمن ذلك).
  • \n
  • القاعدة 2: كل حقل يُحاذى إلى عنوان قابل للقسمة على حجمه. char (1 بايت) يمكن أن يكون في أي مكان. short (2 بايت) — في عناوين زوجية. int (4 بايتات) — في عناوين قابلة للقسمة على 4. double (8 بايتات) — في عناوين قابلة للقسمة على 8.
  • \n
  • القاعدة 3: يجب أن يكون حجم الهيكل بأكمله قابلاً للقسمة على محاذاة أكبر حقل. هذا ضروري لكي تعمل مصفوفات الهياكل بشكل صحيح. إذا كان أكبر متغير هو double (8 بايتات)، فسيكون حجم الهيكل قابلاً للقسمة على 8.
  • \n
\n\n

دعنا نحلل مثالاً أكثر تعقيداً:

\n\n
struct Complex {\n    char  a; // 1 بايت\n    int   b; // 4 بايتات\n    short c; // 2 بايت\n    char  d; // 1 بايت\n};\n// كم سيكون sizeof؟ لنحسب معاً.
\n\n

تحليل خطوة بخطوة:

\n
    \n
  • a (char) — البايت 0.
  • \n
  • b (int) — يجب أن يكون في عنوان قابل للقسمة على 4. العنوان التالي من هذا القبيل هو 4. إذن، الحشو: البايتات 1، 2، 3. المجموع بعد b: 8 بايتات (0-7).
  • \n
  • c (short) — يجب أن يكون في عنوان زوجي. الموضع الحالي هو 8 (زوجي). ممتاز. المجموع بعد c: 10 بايتات (0-9).
  • \n
  • d (char) — يمكن أن يكون في أي مكان. نضعه في البايت 10. المجموع بعد d: 11 بايت (0-10).
  • \n

المدونات

توصيات الكتب