Pointer

Menggunakan pointer satu item, banyak item, dan pointer opsional.

Daftar isi

Pointer adalah nilai yang menyimpan alamat memori dari nilai lain. Topik ini membahas cara mengambil dan membaca alamat, mutasi lewat pointer, pointer ke elemen array, tiga bentuk pointer yang tersedia pada Zig, serta pointer opsional ?*T yang menjadi tipe kembalian lazim untuk fungsi pencarian. Setelah topik ini dibaca, pilihan bentuk pointer dapat disesuaikan dengan kebutuhan datanya, dan penukaran dua nilai dapat ditulis tanpa salinan tambahan.

Konsep#

Pointer menyimpan alamat memori#

Variabel lokal berdomisili pada memori stack, yaitu wilayah memori yang dibuat dan dibebaskan secara otomatis mengikuti pemanggilan fungsi. Alamat variabel tersebut dapat diambil dengan operator &, sehingga &nilai menghasilkan alamat tempat nilai berada. Tipe hasilnya mengikuti variabel yang dirujuk, yaitu *u8 untuk variabel u8 yang dideklarasikan var dan *const u8 untuk yang dideklarasikan const. Tanda bintang pada tipe tersebut dibaca “pointer ke”, sehingga *u8 adalah pointer ke satu nilai u8.

Membaca atau mengubah nilai pada alamat yang ditunjuk disebut dereference, dan ditulis dengan titik diikuti tanda bintang, misalnya p.*. Ekspresi p.* berperan seperti variabel biasa, sehingga pembacaannya menghasilkan nilai di alamat tersebut dan penugasan p.* = 10 menulis nilai baru ke alamat yang sama. Mutasi ini hanya sah bila pointer bertipe *T, bukan *const T, dan bila memori yang dirujuk masih hidup.

Manfaat mutasi lewat pointer terlihat pada fungsi yang harus mengubah variabel milik pemanggilnya. Parameter Zig diteruskan sebagai nilai, sehingga penugasan pada parameter biasa tidak pernah sampai ke variabel asalnya. Fungsi tukar pada berkas berikut menerima dua parameter *u8, lalu menukar isi kedua alamat tersebut.

zig tukar-nilai.zig
const std = @import("std");

fn tukar(a: *u8, b: *u8) void {
    const sementara = a.*;
    a.* = b.*;
    b.* = sementara;
}

pub fn main() void {
    var nilai_a: u8 = 64;
    var nilai_b: u8 = 91;
    tukar(&nilai_a, &nilai_b);
    std.debug.print("nilai_a = {d}, nilai_b = {d}\n", .{ nilai_a, nilai_b });
}
text Keluaran
nilai_a = 91, nilai_b = 64

Pemanggilan tukar(&nilai_a, &nilai_b) mengirimkan alamat kedua variabel, bukan salinan nilainya. Di dalam fungsi, a.* dan b.* menunjuk langsung ke nilai_a dan nilai_b milik main, sehingga penugasan di tubuh tukar benar-benar menukar keduanya. Nilai lama a.* disimpan lebih dahulu pada sementara agar tidak tertimpa sebelum sempat dipindahkan.

bash
# mengompilasi dan menjalankan program
zig run tukar-nilai.zig
text Keluaran
nilai_a = 91, nilai_b = 64

Pointer ke elemen array#

Operator & juga dapat diarahkan ke satu elemen array. Ekspresi &nilai[2] menghasilkan *u8 yang menunjuk elemen ketiga, dan mutasi lewat p.* mengubah elemen aslinya, bukan salinannya. Bentuk ini berguna ketika satu elemen tertentu harus diperbarui tanpa menyalin seluruh array, misalnya memperbaiki satu nilai pengukuran yang salah catat.

zig Contoh: mutasi satu elemen lewat pointer
// tidak-dijalankan
var nilai: [4]u8 = .{ 70, 85, 90, 65 };
const p = &nilai[2];
p.* = 95;
// nilai[2] kini 95, sedangkan tiga elemen lainnya tidak tersentuh

Perulangan for menyediakan cara yang setara untuk memperoleh pointer ke setiap elemen. Bentuk for (daftar) |*item| menangkap alamat elemen pada setiap iterasi, sehingga item.* dapat ditulis untuk mengubah elemen aslinya. Capture semacam ini menjadi dasar fungsi pencarian pada bagian pointer opsional, yaitu alamat barang yang ditemukan dikembalikan kepada pemanggil dalam bentuk ?*T.

Contoh Lengkap#

Pointer satu item dan banyak item#

Zig membedakan pointer satu item *T dari pointer banyak item [*]T. Pointer satu item menunjuk tepat satu nilai, sedangkan pointer banyak item menunjuk rangkaian nilai yang jumlahnya tidak disimpan pada pointer itu sendiri. Pengindeksan seperti p_banyak[0] tetap sah, tetapi Zig tidak mengetahui batas akhirnya, sehingga tidak ada pemeriksaan batas yang dapat dijalankan. Karena itu, pointer banyak item biasanya dikonversi menjadi slice []T yang membawa panjangnya sebelum diproses lebih jauh.

Bentuk ketiga adalah pointer sentinel [*:0]T, yaitu pointer banyak item yang akhir datanya ditandai nilai khusus, umumnya 0. Tipe optional beserta pembukaan nilainya sudah dibahas pada topik Optional, termasuk pembeda terhadap pointer sentinel, dan tipe string pada Zig memakai pola yang sama untuk menandai akhir teksnya. Array dan string literal dapat dikonversi langsung ke pointer banyak item, sehingga &data dari var data = [_]u8{ 10, 20, 30 } sah disimpan pada variabel bertipe [*]u8.

zig Contoh: tiga bentuk pointer
// tidak-dijalankan
var nilai: u8 = 100;
const p_satu: *u8 = &nilai;

var data = [_]u8{ 10, 20, 30 };
const p_banyak: [*]u8 = &data;

const p_teks: [*:0]const u8 = "halo";

Aturan pemilihannya dapat diringkas sebagai berikut. Gunakan *T ketika tepat satu nilai dirujuk, gunakan slice []T ketika rangkaian nilai diproses, dan simpan [*]T untuk batas antarmuka yang rendah, misalnya data yang datang dari pustaka C atau perangkat keras. Pointer sentinel dipakai ketika datanya memang berakhir dengan penanda, seperti string yang mewarisi konvensi C.

Pointer opsional ?*T#

Pointer opsional ?*T menyimpan alamat *T atau null, dan bentuk ini lazim menjadi tipe kembalian fungsi pencarian. Ukurannya sama persis dengan *T karena null diwakili alamat nol yang tidak pernah menjadi alamat sah. Keunggulannya terletak pada mutasi, sebab nilai yang ditangkap dari if (opsi) |p| bertipe *T, sehingga p.* langsung dapat ditulis tanpa salinan tambahan.

zig cari-barang.zig
const std = @import("std");

const Barang = struct {
    kode: u32,
    jumlah: u32,
};

fn cari(daftar: []Barang, kode: u32) ?*Barang {
    for (daftar) |*item| {
        if (item.kode == kode) {
            return item;
        }
    }
    return null;
}

pub fn main() void {
    var gudang = [_]Barang{
        .{ .kode = 101, .jumlah = 24 },
        .{ .kode = 205, .jumlah = 7 },
        .{ .kode = 310, .jumlah = 15 },
    };

    if (cari(&gudang, 205)) |item| {
        item.jumlah += 6;
        std.debug.print("kode 205 kini berisi {d} unit\n", .{item.jumlah});
    }
    if (cari(&gudang, 999)) |item| {
        std.debug.print("kode 999 berisi {d} unit\n", .{item.jumlah});
    } else {
        std.debug.print("kode 999 tidak terdaftar\n", .{});
    }
}
text Keluaran
kode 205 kini berisi 13 unit
kode 999 tidak terdaftar

Fungsi cari menelusuri gudang dengan capture |*item|, lalu mengembalikan alamat barang yang kodenya cocok, atau null setelah seluruh daftar dilewati. Pemanggil pertama menangkap alamat barang kode 205 sebagai item bertipe *Barang, kemudian item.jumlah += 6 mengubah data di dalam array gudang itu sendiri. Pemanggil kedua jatuh ke cabang else karena kode 999 tidak terdaftar, dan program mencetak pesannya tanpa menyentuh data apa pun.

bash
# mengompilasi dan menjalankan program
zig run cari-barang.zig
text Keluaran
kode 205 kini berisi 13 unit
kode 999 tidak terdaftar

Kesalahan umum#

  1. p.* dibaca pada pointer opsional yang ternyata null. Program berhenti dengan panic penggunaan nilai null pada mode Debug. Buka nilainya dengan orelse atau capture if terlebih dahulu, sebagaimana dilakukan fungsi cari di atas.
  2. Panjang diharapkan dari [*]T. Pointer banyak item tidak menyimpan jumlah elemennya, sehingga perulangan atasnya tanpa batas adalah kekeliruan. Konversikan menjadi slice, atau teruskan panjangnya sebagai parameter terpisah.
  3. Penugasan p.* = 10 ditulis pada pointer bertipe *const T. Kompilator menolaknya karena tipe tersebut menjamin isi yang dirujuk tidak berubah. Ambil alamat dari variabel yang dideklarasikan var bila mutasi memang diperlukan.

Ringkasan#

  • Pointer menyimpan alamat memori, operator & mengambil alamat, dan p.* membaca atau menulis nilai pada alamat tersebut.
  • *T menunjuk satu nilai, [*]T menunjuk rangkaian tanpa panjang, dan [*:0]T menambahkan penanda akhir data.
  • Pointer ke elemen array diperoleh dari &nilai[i] atau capture |*item|, dan mutasinya mengubah elemen aslinya.
  • ?*T menyatakan alamat atau null, berukuran sama dengan *T, dan mutasinya tersedia setelah nilai ditangkap dengan capture if.

Referensi#