Setiap nilai yang kita buat di program Rust — angka, teks, hingga data yang lebih besar — harus menempati ruang di memori komputer selama program berjalan. Berbeda dari banyak bahasa yang menyerahkan urusan memori sepenuhnya kepada garbage collector, Rust justru menjadikan cara data disimpan sebagai fondasi seluruh sistem keamanan memorinya. Sebelum kita menyelami ownership pada Chapter berikutnya, kita perlu memahami dua wilayah tempat Rust menempatkan data: stack dan heap. Pilihan wilayah menentukan kecepatan alokasi, fleksibilitas ukuran data, dan siapa yang bertanggung jawab membersihkan data ketika data tersebut tidak lagi dipakai.

Dua Wilayah Memori dalam Program Rust #

Saat sebuah program Rust berjalan, sistem operasi menyediakan dua wilayah memori utama yang karakter kerjanya sangat berbeda. Wilayah pertama bernama stack, yaitu area memori yang bekerja seperti tumpukan piring: data masuk dan keluar mengikuti pola terakhir-masuk-pertama-keluar (LIFO). Wilayah kedua bernama heap, yaitu area memori besar yang fleksibel, tetapi menuntut pengelolaan lebih cermat. Rust menempatkan setiap nilai ke salah satu wilayah tersebut berdasarkan sifat nilainya, bukan berdasarkan keinginan kita saat menulis kode.

Stack yang Rapi dan Cepat #

Stack hanya menerima data yang ukurannya tetap dan sudah diketahui pada saat kompilasi. Contohnya tipe i32 yang selalu menempati 4 byte, f64 yang selalu 8 byte, serta bool dan char yang ukurannya tidak pernah berubah. Berkat sifat tetap tersebut, menambah data ke stack hanya berarti menggeser penunjuk stack ke posisi berikutnya lalu menulis data di sana — operasi yang sangat murah. Ketika data tidak lagi dibutuhkan, data tersebut cukup dilepas (pop) dari tumpukan, dan penunjuk stack mundur dengan sendirinya. Tidak ada proses pencarian ruang kosong dan tidak ada pembukuan tambahan.

Heap yang Fleksibel namun Berbiaya #

Heap menampung data yang ukurannya tidak diketahui saat kompilasi atau ukurannya bisa berubah saat program berjalan — misalnya isi sebuah teks yang bertambah panjang karena input pengguna. Alokasi di heap bekerja seperti menitipkan barang di gudang: program meminta allocator mencari ruang kosong yang cukup besar, menandai ruang tersebut sebagai terpakai, lalu menerima alamat lokasi barang itu sebagai balasan. Alamat inilah yang disebut pointer. Seluruh proses tersebut lebih lambat daripada alokasi stack, dan ruang yang sudah tidak dipakai harus dikembalikan dengan benar agar tidak terjadi kebocoran memori. Di titik inilah nanti konsep ownership memainkan perannya.

Perhatikan ringkasan perbedaan kedua wilayah berikut:

AspekStackHeap
Kecepatan alokasiSangat cepat, cukup menggeser penunjuk stackLebih lambat, perlu mencari ruang kosong
Syarat ukuran dataTetap dan diketahui saat kompilasiBisa berubah saat program berjalan
Pola pengelolaanLIFO, otomatis rapiDikelola oleh allocator
PembersihanOtomatis saat scope berakhirDijatuhkan (drop) saat pemiliknya keluar scope

Alokasi Stack untuk Tipe dengan Ukuran Tetap #

Mari kita mulai dari wilayah yang paling sederhana. Variabel bertipe integer seperti i32 hidup sepenuhnya di stack: nilai 120 benar-benar tersimpan di wilayah stack, dan variabel hanya berlaku di dalam scope tempat variabel tersebut dideklarasikan. Scope ditentukan oleh pasangan kurung kurawal { }.

rust
fn main() {
    let stock = 120;

    {
        let discount = 15;
        println!("Stok: {}, diskon: {}%", stock, discount);
    }

    println!("Stok: {}", stock);
}

Output yang Dihasilkan:

text
Stok: 120, diskon: 15%
Stok: 120

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 2 (let stock = 120;): Rust menyimpan nilai 120 milik variabel stock di stack. Ukuran i32 sudah tetap sejak kompilasi, sehingga tidak ada permintaan ke allocator.
  2. Baris 4 ({): kurung kurawal membuka scope baru di dalam main. Semua variabel yang dideklarasikan setelah titik ini hanya berlaku sampai scope ini ditutup.
  3. Baris 5 (let discount = 15;): variabel discount ditumpuk di stack, di atas data stock.
  4. Baris 6 (println! ...): kedua variabel masih hidup, sehingga keduanya dapat dicetak bersamaan.
  5. Baris 7 (}): scope ditutup. Variabel discount dilepas dari stack secara otomatis; sejak titik ini discount tidak dapat diakses lagi. Variabel stock tidak terpengaruh karena scope-nya masih berlanjut.
  6. Baris 9 (println! ...): stock masih sah dipakai, terbukti dari baris kedua pada output. Sebaliknya, discount sudah lenyap.

Karakter inilah yang membuat stack murah: umur setiap nilainya dikunci oleh struktur kurung kurawal, dan pembersihannya berlangsung otomatis tanpa kita menulis satu baris kode pembersihan pun.

Alokasi Heap untuk Data yang Bisa Bertumbuh #

Sekarang kita masuk ke wilayah yang lebih menarik. Tipe String harus menyimpan isi teksnya di heap karena isi teks bisa bertambah atau berkurang selama program berjalan. Yang menarik, variabel String itu sendiri tetap berupa data kecil di stack yang berisi tiga informasi: pointer ke alamat isi teks di heap, panjang teks (length), dan kapasitas ruang yang sudah dipesan (capacity).

Kita bisa mengintip panjang dan kapasitas sebuah String lewat method len() dan capacity():

rust
fn main() {
    let title = String::from("Belajar Rust");
    println!("Isi: {}", title);
    println!("Panjang (len): {}", title.len());
    println!("Kapasitas (capacity): {}", title.capacity());

    let mut message = String::from("Halo");
    println!("Awal: len = {}, capacity = {}", message.len(), message.capacity());
    message.push_str(", dunia");
    println!("Setelah push_str: len = {}, capacity = {}", message.len(), message.capacity());
    message.push('!');
    println!("Setelah push: len = {}, capacity = {}", message.len(), message.capacity());
}

Output yang Dihasilkan:

text
Isi: Belajar Rust
Panjang (len): 12
Kapasitas (capacity): 12
Awal: len = 4, capacity = 4
Setelah push_str: len = 11, capacity = 11
Setelah push: len = 12, capacity = 22

Mari kita bedah kode di atas langkah demi langkah:

  1. Baris 2 (let title = ...): String::from("Belajar Rust") meminta allocator menyiapkan ruang di heap untuk 12 byte teks. Nilai kembalian berupa tiga data di stack — pointer, len 12, dan capacity 12.
  2. Baris 3–4 (println! ...): len() melaporkan jumlah byte teks yang benar-benar terpakai, sedangkan capacity() melaporkan jumlah byte yang sudah dipesan. Karena teks dibuat dalam satu kali pemesanan, keduanya sama besar.
  3. Baris 6 (let mut message = ...): teks "Halo" hanya 4 byte, jadi heap dipesan sebesar 4 byte dan len serta capacity sama-sama 4. Kata kunci mut wajib karena isi teks akan diubah.
  4. Baris 8 (message.push_str(", dunia");): teks baru butuh 11 byte total, padahal kapasitas lama hanya 4. Rust meminta allocator menyiapkan ruang yang lebih besar, memindahkan isi lama, lalu menempelkan ", dunia" — hasilnya len dan capacity sama-sama 11.
  5. Baris 10 (message.push('!');): satu karakter lagi membuat kebutuhan naik menjadi 12 byte. Allocator kembali memesan ruang baru, tetapi Rust sengaja memesan lebih banyak — 22 byte — supaya penambahan karakter berikutnya tidak mengulang proses pindah rumah. Inilah alasan capacity (22) bisa lebih besar daripada len (12).

Stack dan Heap Bekerja dalam Satu Program #

Dalam program nyata, kedua wilayah bekerja berdampingan pada saat yang sama. Perhatikan contoh singkat berikut yang mencampur tipe stack (i32) dan tipe heap (String):

rust
fn main() {
    let mut number = 5;
    number = number + 10;

    let mut name = String::from("Budi");
    name.push_str(" Santoso");

    println!("number: {}", number);
    println!("name: {}", name);
}

Output yang Dihasilkan:

text
number: 15
name: Budi Santoso

Mari kita amati pembagian kerjanya:

  1. Baris 2–3 (let mut number ...): seluruh kehidupan nilai number terjadi di stack. Operasi number + 10 membaca nilai lama, menghitung 15, lalu menimpa isi slot stack milik number.
  2. Baris 5 (let mut name = ...): variabel name menerima tiga data kecil di stack (pointer, len 4, capacity 4), sementara teks "Budi" sendiri tinggal di heap.
  3. Baris 6 (name.push_str(" Santoso");): isi teks bertambah dari 4 byte menjadi 11 byte. Proses realokasi terjadi di heap; data pengelola di stack ikut diperbarui agar tetap menjelaskan isi heap dengan akurat.
  4. Baris 8–9 (println! ...): kedua nilai dicetak dengan cara yang sama persis dari sisi pemanggilan, meskipun tempat tinggal keduanya berbeda dunia.

Ringkasan #

Stack adalah wilayah memori cepat untuk data berukuran tetap yang hidupnya mengikuti scope, sedangkan heap adalah wilayah fleksibel untuk data yang bisa bertumbuh dengan biaya alokasi lebih mahal. Sebuah String menempati keduanya sekaligus: data pengelola di stack dan isi teks di heap. Pertanyaan berikutnya menjadi penting: ketika data pengelola di stack itu disalin atau dipindahkan, apa yang terjadi pada isi teks di heap, dan siapa yang berhak membersihkannya? Pertanyaan itulah yang dijawab oleh konsep ownership pada Chapter berikutnya.