الدرس1 / 2

الستاك والهيب

قراءة

اللي هتفهمه

  • الفرق العملي بين الستاك والهيب، ومين بيقرر القيمة تروح فين.
  • ليه String بتاخد مكانين في الذاكرة في نفس الوقت.
  • إزاي الكومبايلر بيعرف إمتى يحرّر الذاكرة من غير GC ومن غير ما تكتب حاجة.

الشرح

كل الدروس اللي فاتت كانت بتشتغل على قيم حجمها معروف: i32 أربع بايتات، bool بايت واحد، char أربع بايتات. الحجم ده معروف وإنت بتكتب الكود، قبل ما البرنامج يشتغل أصلاً.

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

الحلين دول ليهم مكانين مختلفين تماماً في الذاكرة.

الستاك ترتيبه بسيط جداً: كل حاجة بتتحط فوق اللي قبلها، وبتتشال بنفس الترتيب بالعكس. عشان كده هو سريع — إضافة قيمة يعني تحريك مؤشر واحد. بس الترتيب البسيط ده ليه شرط: كل حاجة على الستاك لازم يكون حجمها معروف وقت الترجمة.

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

كل دالة بتشتغل في Rust بتاخد شريط على الستاك اسمه frame، وكل متغيراتها المحلية بتقعد فيه. لما الدالة تخلص، الـ frame بيتشال كله مرة واحدة.

طيب String بقى، اللي حجمه مش معروف — بيقعد فين؟ في الاتنين.

المرئي

قيمة على الستاك، وقيمة على الهيب

fn main() {    let x = 5;    let s = String::from("hi");    println!("{x} {s}");}

البرنامج بدأ. الـ frame بتاع main اتفتح على الستاك وهو لسه فاضي — ده الشريط اللي هيتحط فيه كل متغير محلي في الدالة دي.

الخطوة 1 / 5

استخدم الأسهم في لوحة المفاتيح، أو اسحب بإصبعك على الشاشة.

الكود

جرّب ده بنفسك — كل رقم في الشرح فوق طالع من الأرقام اللي البرنامج ده بيطبعها:

fn main() {
    let x = 5;
    let s = String::from("hi");

    println!("x = {x}");
    println!("s = {s}, len = {}, capacity = {}", s.len(), s.capacity());
    println!("size of x on the stack  = {} bytes", std::mem::size_of_val(&x));
    println!("size of s on the stack  = {} bytes", std::mem::size_of_val(&s));
    println!("bytes s owns on the heap = {}", s.capacity());
}
افتح في Playground

الخرج، من تشغيل حقيقي بـ rustc 1.97.1:

x = 5
s = hi, len = 2, capacity = 2
size of x on the stack  = 4 bytes
size of s on the stack  = 24 bytes
bytes s owns on the heap = 2

خد بالك من السطر التالت والرابع. x بياخد 4 بايت على الستاك، وs بياخد 24 — مع إن النص اللي جواه حرفين بس. الـ 24 دي مش النص، دي التلات حقول: العنوان، والطول، والسعة. كل واحد فيهم 8 بايت على جهاز 64-bit.

ليه 8 بايت للطول؟ الطول 2 بس!

الطول نوعه usize — النوع اللي حجمه بحجم عنوان الذاكرة على الجهاز. على جهاز 64-bit ده 8 بايت. الرقم المخزّن دلوقتي هو 2، بس المساحة المحجوزة تكفي أي طول ممكن النص يوصله. لو الحقل كان أصغر، كان فيه حد أقصى لطول النصوص في اللغة.

ليه بيشتغل كده

السؤال اللي فتحنا بيه المسار كله كان: مين المسؤول عن إن الذاكرة ترجع؟

هنا بتشوف الإجابة بتتنفذ. x مش محتاج مسؤول — هو جوه الـ frame، ولما الدالة تخلص الـ frame بيتشال والقيمة بتروح معاه. مفيش شغل إضافي.

s مختلف. البايتات اللي على الهيب مش جوه الـ frame، فشيل الـ frame مش بيحررها. لازم يبقى فيه أمر صريح بتحريرها، والأمر ده لازم يتنفذ مرة واحدة بالظبط: لو اتنفذ صفر مرات ده تسريب ذاكرة، ولو اتنفذ مرتين ده تحرير مزدوج — وهو من أخطر الأخطاء في C.

الكومبايلر بيحل ده بإنه يتتبع مين المالك. s هو المالك، وآخر مكان s موجود فيه هو آخر الدالة، فالكومبايلر بيحط أمر التحرير هناك وهو بيترجم. البرنامج اللي بيطلع مفيهوش أي فحص وقت التشغيل — القرار اتاخد قبل ما البرنامج يبدأ.

عشان ده يفضل مضمون، لازم يكون فيه مالك واحد بس في كل لحظة. دي القاعدة اللي الدرس الجاي كله عنها.

تمرين

من غير ما تشغّل حاجة، قول كل قيمة من دول بتقعد فين — الستاك، الهيب، ولا الاتنين:

  1. let n: u64 = 900;
  2. let name = String::from("Sara");
  3. let flag = true;
  4. let letter = 'م';
  5. let text = "Sara"; — خد بالك ده مالوش نوع String
الحل
  1. الستاك. u64 حجمه معروف — 8 بايت — والقيمة قاعدة في الـ frame نفسه.

  2. الاتنين. تلات حقول (24 بايت) على الستاك، وحروف Sara على الهيب.

  3. الستاك. بايت واحد.

  4. الستاك. char في Rust دايماً 4 بايت، مهما كان الحرف — الحرف ده عربي أو لاتيني ملوش فرق. جرّب std::mem::size_of::<char>() وهيطلعلك 4.

  5. ولا واحد فيهم بالمعنى اللي فوق. نوعه &str، والحروف نفسها مكتوبة جوه الملف التنفيذي وقت الترجمة. اللي بيتحط على الستاك حقلين بس — عنوان وطول، 16 بايت — والحروف مش على الهيب أصلاً، هي جزء من البرنامج نفسه.

    الفرق بين String و&str ده بيسبب أول خطأ بيقابل معظم الناس، وليه صفحة كاملة في String و&str مش نفس الحاجة.

مشاكل شائعة

النقاش

لو عندك سؤال أو لقيت غلطة في الصفحة دي، اكتبها فيالنقاشات على GitHub. كل صفحة ليها موضوعها الخاص.