Pointer yang tidak menunjuk ke objek mana pun harus menyimpan nilai khusus nullptr, dan pemeriksaan terhadap nilai itu menyelamatkan program dari kerusakan mendadak. Topik ini membahas hubungan nullptr dengan NULL dan literal 0, dua jebakan yang paling sering muncul, yaitu pointer liar dan dangling pointer, serta kebiasaan menetralkan pointer setelah digunakan.
nullptr: nilai kosong yang bertipe#
Literal nullptr yang diperkenalkan C++11 menyatakan pointer yang tidak menunjuk ke mana pun dan bertipe khusus. Sebelum C++11, konvensinya memakai makro NULL atau literal 0, dan ketiganya menghasilkan nilai yang sama ketika dibandingkan. Meskipun hasil perbandingannya sama, nullptr lebih disarankan karena bertipe pointer, sehingga tidak dapat tertukar dengan bilangan bulat pada pemanggilan fungsi yang di-overload.
Program berikut membandingkan ketiga bentuk nilai kosong tersebut.
#include <iostream>
#include <cstddef>
int main() {
int* a = nullptr;
int* b = NULL;
int* c = 0;
std::cout << std::boolalpha;
std::cout << "nullptr == NULL : " << (a == b) << '\n';
std::cout << "nullptr == 0 : " << (a == c) << '\n';
std::cout << "NULL == 0 : " << (b == c) << '\n';
std::cout << "sizeof(nullptr) : " << sizeof(nullptr) << " byte\n";
return 0;
}nullptr == NULL : true
nullptr == 0 : true
NULL == 0 : true
sizeof(nullptr) : 8 byteKetiga bentuk tersebut sebanding, dan sizeof(nullptr) yang berukuran sama dengan satu alamat menegaskan bahwa nullptr adalah nilai pointer sungguhan, bukan sekadar angka. Pembedaan terpentingnya muncul pada fungsi yang di-overload untuk int dan pointer. Pemanggilan dengan literal 0 atau makro NULL dapat mengarah ke versi int, sedangkan nullptr selalu mengarah ke versi pointer, sehingga maksud pemanggilnya tidak mungkin salah tafsir.
Makro NULL sendiri tidak memiliki bentuk yang seragam. Header pustaka standar boleh mendefinisikannya sebagai 0, 0L, atau nullptr itu sendiri, sehingga pemakaiannya pada konteks non-pointer tetap mungkin lolos tanpa peringatan. Literal nullptr menghapus keraguan tersebut karena hanya cocok untuk pointer.
Pola pemeriksaan berikut adalah kontrak dasar penggunaan pointer.
#include <iostream>
int main() {
int nilai = 7;
int* p = &nilai;
int* kosong = nullptr;
if (p != nullptr) {
std::cout << "p menunjuk ke nilai " << *p << '\n';
}
if (kosong == nullptr) {
std::cout << "kosong tidak menunjuk ke objek mana pun\n";
}
return 0;
}p menunjuk ke nilai 7
kosong tidak menunjuk ke objek mana punSebelum di-dereference, pointer diperiksa terhadap nullptr, dan pemeriksaan if (kosong == nullptr) berjalan tanpa menyentuh memori apa pun. Pemeriksaan ini murah, tetapi hanya melindungi bila pointer memang diberi nullptr saat tidak menunjuk ke mana pun. Syarat itulah yang membuat dua jebakan berikut berbahaya, karena keduanya menyangkut pointer yang keadaannya tidak dapat diperiksa.
Karena pointer kosong dikonversi menjadi false dalam kondisi boolean, percabangan if (p) setara dengan if (p != nullptr), sedangkan if (!p) menguji pointer kosong. Bentuk singkat ini sangat umum pada kode C dan C++, sedangkan bentuk eksplisit dengan nullptr menyatakan niatnya lebih terbaca bagi pembaca baru. Keduanya setara dari sisi perilaku, sehingga pilihan gaya cukup dijaga konsisten.
Pointer liar#
Pointer liar adalah pointer yang dideklarasikan tanpa inisialisasi, sehingga isinya adalah alamat sisa yang kebetulan ada di memori. Dereference terhadapnya tidak dapat diprediksi, mulai dari berjalan tanpa tanda kesalahan, merusak data lain, sampai menghentikan program. Compiler modern umumnya memberi peringatan pada pemakaian variabel yang belum diinisialisasi, tetapi peringatan hanyalah jaring pengaman, bukan pengganti kebiasaan menginisialisasi.
// tidak-dijalankan (alamat sisa tidak dapat diprediksi dan dereference-nya berbahaya)
int main() {
int* p; // dideklarasikan tanpa inisialisasi
*p = 5; // menulis ke alamat yang tidak diketahui
}Pencegahannya sederhana dan berlaku universal, yaitu memberi nilai pada setiap pointer sejak deklarasinya, baik alamat variabel yang sah maupun nullptr. Nilai nullptr membuat keadaan kosong dinyatakan secara eksplisit dan dapat diperiksa dengan percabangan biasa. Pointer tanpa inisialisasi justru menyimpan keadaan yang tidak dapat diperiksa sama sekali, dan itulah yang menjadikannya lebih berbahaya daripada pointer yang jelas-jelas kosong.
Peringatan compiler membantu menangkap kebiasaan ini sejak dini. Flag -Wall beserta -Wextra pada GCC mengaktifkan peringatan pemakaian variabel yang belum diinisialisasi, dan kebiasaan yang baik adalah mengompilasi dengan kedua flag itu secara rutin. Peringatan bukan jaminan lengkap, karena tidak semua alur dapat ditelusuri compiler, tetapi biayanya nyaris nol dibandingkan manfaatnya.
Dangling pointer#
Dangling pointer adalah pointer yang masih menyimpan alamat objek yang sudah tidak hidup. Kasus yang paling umum adalah mengembalikan alamat variabel lokal dari fungsi, karena variabel lokal lenyap saat fungsinya berakhir. Pointer tersebut tampak bernilai wajar, tetapi alamatnya sudah tidak sah, dan membacanya termasuk undefined behavior.
// tidak-dijalankan (mengembalikan alamat variabel lokal adalah undefined behavior)
int* buatNilai() {
int x = 42;
return &x; // x lenyap saat fungsi berakhir
}Sumber kedua adalah objek dinamis, yaitu pointer yang tetap menyimpan alamat lama setelah delete dijalankan. Kedua kasus sama pada intinya, yaitu masa hidup objek lebih pendek daripada masa hidup alamat yang menyimpannya. Kesamaan itu memudahkan pencegahannya, karena kuncinya berada pada kebiasaan menetralkan pointer segera setelah objeknya tidak ada lagi.
Kebalikannya juga perlu dipahami, yaitu alamat yang sah sepanjang masa hidup objeknya. Variabel global dan static hidup sepanjang program, sehingga alamatnya boleh dikembalikan dari fungsi dengan aman. Objek dinamis hidup sampai delete-nya dijalankan, sehingga alamatnya juga sah lebih lama, tetapi pelepasannya menjadi tanggung jawab program, sebagaimana diurai pada topik new dan delete.
Kebiasaan menetralkan pointer#
Setelah delete dijalankan, pointer yang bersangkutan sebaiknya langsung diberi nullptr. Pointer yang sudah dinetralkan mudah diperiksa, sehingga pola if (p != nullptr) kembali bermakna dan penggunaan yang tidak disengaja langsung tertangkap. Kebiasaan kecil ini tidak menutup seluruh kemungkinan bug memori, tetapi mengubah kelas kesalahan yang sulit dideteksi menjadi kesalahan yang terdeteksi pada pemeriksaan pertama.
Kebiasaan ini juga berlaku pada komunikasi antar fungsi. Fungsi yang mengembalikan pointer dapat mengembalikan nullptr untuk menyatakan kegagalan, dan pemanggilnya memeriksa hasilnya sebelum digunakan. Rantai kecil ini, yaitu mengembalikan nullptr saat gagal, memeriksa sebelum dereference, dan menetralkan setelah pelepasan, membentuk kontrak yang jelas antara pemilik dan pengguna pointer.
Ringkasan#
nullptradalah nilai pointer kosong yang bertipe, sedangkanNULLdan0adalah bentuk warisan yang dapat tertukar dengan bilangan bulat saat fungsi di-overload.- Pointer liar adalah pointer tanpa inisialisasi, dan pencegahannya adalah memberi nilai pada setiap pointer sejak deklarasinya.
- Dangling pointer masih menyimpan alamat objek yang sudah tidak hidup, dan membacanya termasuk undefined behavior.
- Pointer yang sudah dilepas sebaiknya langsung diberi
nullptr, sedangkandeleteterhadapnullptraman dijalankan.
Referensi#
- nullptr — cppreference.com — Definisi literal nullptr dan tipe khususnya.
- Null pointers — LearnCpp — Perbedaan nullptr, NULL, dan 0 beserta jebakannya.
- Warning Options — GCC — Flag -Wall dan -Wextra yang mengaktifkan peringatan variabel belum diinisialisasi.
- Pointer declaration — cppreference.com — Nilai pointer kosong dan operasi yang menyebabkan undefined behavior.