Menguasai Dasar-Dasar Pointer dalam C dan C++

Pembaharuan Terakhir: 12/02/2025
  • Pointer mewakili alamat memori dan memungkinkan kontrol langsung atas di mana dan bagaimana data disimpan dan diakses.
  • Dereferensi, kebenaran konstanta, dan aritmatika penunjuk sangat penting untuk menggunakan penunjuk dengan aman bersama array, struct, dan memori dinamis.
  • Pointer memungkinkan penyampaian argumen melalui referensi, membangun struktur dinamis, dan menerapkan pengalihan bertingkat seperti pointer-ke-pointer.
  • Pemeriksaan nol, alokasi/dealokasi yang benar, dan inisialisasi yang disiplin sangat penting untuk menghindari perilaku yang tidak terdefinisi dan kerusakan.

dasar-dasar pointer

Pointer dalam C dan C++ memiliki reputasi legendaris: kuat, rumit, dan mampu membuat program Anda mogok dalam sekejap mata jika Anda ceroboh. Namun, begitu Anda benar-benar memahami apa itu pointer – yang hanyalah alamat memori – sebagian besar misteri itu mulai memudar, dan Anda memperoleh akses ke salah satu alat paling serbaguna dalam pemrograman tingkat rendah dan sistem.

Artikel ini memandu Anda langkah demi langkah dari ide dasar alamat memori dan pointer sederhana, melalui referensi, array, kelas, dan memori dinamis, hingga ke kasus penggunaan pointer-ke-pointer dan jebakan umum. Tujuannya adalah untuk membuat bekerja dengan pointer terasa alami, tidak seperti sihir hitam, sehingga Anda dapat bernalar tentang apa yang sebenarnya terjadi dalam memori saat kode Anda dijalankan.

Memahami variabel dan alamat memori

memori dan variabel

Sebelum menyentuh pointer, Anda perlu gambaran mental yang jelas tentang bagaimana variabel berada dalam memori. RAM komputer secara konseptual adalah larik byte yang panjang, setiap byte ditandai dengan alamat numerik yang unik. Saat Anda mendeklarasikan sebuah variabel, kompiler menyimpan satu atau lebih byte tersebut dan mengaitkan alamat tersebut dengan nama variabel.

Bayangkan variabel sebagai kotak berlabel yang ditempatkan di suatu tempat dalam memori: nama adalah labelnya, alamat adalah lokasi fisik di rak, dan konten adalah nilai yang disimpan di dalam kotak. Misalnya, jika Anda memiliki int pada Arduino UNO biasa, ia akan menempati 2 byte berturut-turut dalam RAM, dan kompilator mencatat alamat pasti mana yang dicadangkan untuknya.

Deklarasi suatu variabel memberi tahu kompiler jenis dan ukuran apa yang perlu dicadangkan, sedangkan definisi atau penugasan sebenarnya menyimpan nilai di lokasi yang dicadangkan itu. Misalnya, menulis int j; hanya mengumumkan variabel dan membiarkan kompiler mengalokasikan memori, sedangkan j = 10; menuliskan nilai numerik 10 ke dalam sel memori yang dimiliki j.

Secara internal, kompilator menyimpan tabel simbol tempat memetakan setiap nama variabel ke alamat memori dan jenisnya. Jika kompilator memutuskan bahwa j tinggal di alamat 2020, Anda secara konseptual dapat memikirkan situasi seperti ini: pengenal j menunjuk ke alamat 2020, dan byte pada alamat 2020 berisi representasi biner 10.

Sangat penting untuk memisahkan gagasan “di mana sesuatu disimpan” (alamatnya) dari “apa yang disimpan di sana” (nilainya). Dalam teori penyusun dan banyak buku, lokasi ini sering disebut nilai (dari “nilai lokasi”), sedangkan konten disebut sebagai nilaiPointer adalah tentang memanipulasi lokasi tersebut secara langsung.

Apa sebenarnya pointer itu?

dasar-dasar penunjuk

Pointer hanyalah sebuah variabel yang nilainya merupakan alamat memori yang menunjuk ke beberapa objek lain. Ia tidak menyimpan data itu sendiri, melainkan alamat tempat data tersebut berada. Ukuran pointer bergantung pada arsitektur mesin: pada sistem x86 32-bit biasanya 4 byte, pada sistem x86-64 64-bit biasanya 8 byte, dan pada mikrokontroler kecil seperti Arduino, sebuah alamat dapat muat dalam 2 byte.

Saat Anda mendeklarasikan sebuah pointer, Anda tidak hanya menentukan bahwa pointer tersebut menyimpan alamat, tetapi juga tipe objek yang akan ditunjuknya. Sebagai contoh, int* p mendeklarasikan pointer ke intBintang di sini adalah bagian dari tipe, bukan tanda perkalian, dan memberi tahu kompiler berapa banyak byte yang harus dibaca atau ditulis saat Anda mengaksesnya nanti *p.

Alamat operator & memberi Anda alamat objek yang ada, yang dapat Anda simpan dalam variabel penunjuk. Misalkan Anda memiliki int n = 0;; maka kode ini menyimpan alamat n menjadi sebuah penunjuk:

Contoh: int n = 0;
int* p = &n; // p now holds the address of n

Setelah sebuah pointer memegang alamat yang valid, operator dereferensi * memungkinkan Anda mengakses objek yang ada di alamat tersebut. If p adalah penunjuk ke int, kemudian *p berfungsi seperti alias untuk bilangan bulat sebenarnya yang tersimpan di memori. Misalnya:

Potongan: *p = 1; // writes 1 into n through the pointer
std::cout << *p; // reads the current value of n

Gagasan utamanya adalah bahwa bintang memiliki arti yang berbeda dalam konteks yang berbeda: ketika digunakan dalam deklarasi, ia membentuk tipe penunjuk, dan ketika digunakan dalam ekspresi, ia membuang referensi penunjuk. Membingungkan kedua peran ini merupakan salah satu kesalahan klasik pemula, jadi selalu perhatikan apakah Anda mendeklarasikan pointer atau menggunakannya untuk mengakses memori.

Pada perangkat kecil seperti Arduino, penunjuk yang tidak diinisialisasi secara eksplisit akan memiliki alamat 16-bit yang valid atau berisi sampah. Tidak ada nilai “kosong” ajaib kecuali Anda sengaja mengaturnya ke konstanta penunjuk null seperti nullptr dalam C++. Dereferensi alamat sampah semacam itu hampir pasti akan mengunci mikrokontroler Anda.

Kebenaran konstanta dan berbagai jenis pointer

Pointer berinteraksi dengan const dengan cara yang mungkin membingungkan pada awalnya, tetapi menguasai ini sangat penting untuk menulis C++ yang benar. Posisi dari const relatif terhadap bintang menentukan apakah objek yang ditunjuk, penunjuk itu sendiri, atau keduanya tidak dapat diubah.

Jika Anda mempunyai integer konstan, tipe pointer harus mencerminkan bahwa Anda hanya dapat membacanya, tidak dapat mengubahnya. Bayangkan kode ini:

Demo: auto const cn = int{0}; // cn is a constant int
int const* p = &cn; // pointer to const int

Tipe dari p ini adalah “pointer ke konstanta int”: Anda dapat membaca *p tetapi tidak dapat menetapkannya. mencoba int* p = &cn; akan menjadi kesalahan tipe, karena itu akan menjanjikan Anda dapat memodifikasi objek konstan, yang dilarang oleh bahasa tersebut.

Terkadang, objek itu sendiri tidak konstan, tetapi Anda secara sengaja menginginkan penunjuk yang hanya mengizinkan akses baca melaluinya. Dalam kasus tersebut Anda menggunakan lagi int const*:

Pemakaian: auto n = int{0}; // non-const int
int const* p = &n; // can read n via p, but not write through p

Perhatikan bahwa int const* dan const int* artinya sama persis: integer hanya dapat dibaca melalui pointer, tetapi pointer masih dapat diubah untuk menunjuk ke tempat lain. Di sisi lain, jika Anda menulis int* const p = &n;, Anda memiliki penunjuk konstan ke int non-konstan: alamat yang disimpan di p tidak dapat diubah setelah inisialisasi, tetapi nilainya *p bebas untuk bervariasi.

Anda bahkan dapat menggabungkan kedua bentuk tersebut untuk membuat penunjuk konstan ke bilangan bulat konstan: int const* const p. Hal ini memberi tahu kompiler bahwa baik alamat di p Nilai yang tersimpan di alamat tersebut juga tidak boleh diubah. Memahami variasi ini membantu Anda mengekspresikan maksud dengan sangat jelas, dan kompiler akan memastikan Anda jujur.

Penunjuk ke struktur dan kelas

Ketika sebuah pointer merujuk ke sebuah struct atau kelas, Anda biasanya ingin mengakses antarmuka publiknya: anggota data dan fungsi anggota. Dereferensi dengan * masih berfungsi, tetapi sintaksnya bisa menjadi sedikit bertele-tele, jadi C++ menyediakan operator panah -> sebagai singkatan.

Pertimbangkan sebuah contoh sederhana Student struktur dengan nilai dan metode yang menghitung rata-rata. If Student* p menyimpan alamat Student objek, Anda dapat menulis (*p).grade_2 untuk mencapai kelas dua, atau (*p).average() untuk memanggil fungsi anggota.

Operator panah menggabungkan dereferensi dan akses anggota dalam satu langkah: p->grade_2 dan p->average() berarti persis sama dengan (*p).grade_2 dan (*p).average(). Dibawah tenda, p->member hanyalah gula sintaksis untuk (*p).memberItulah sebabnya Anda hampir selalu melihat -> digunakan dalam kode dunia nyata saat menangani petunjuk ke objek.

Selama kelas tidak kelebihan beban operator* or operator-> dengan beberapa perilaku eksotis, Anda dapat mengobati p->member sebagai cara standar untuk mengakses objek di belakang pointer. Banyak kerangka kerja yang mengandalkan pembebanan berlebih pada operator-operator ini untuk penunjuk pintar, tetapi secara konseptual, maknanya tetap sama: mengikuti penunjuk dan kemudian mengakses anggotanya.

Pointer nol dan keamanan

Pointer yang saat ini tidak merujuk ke objek yang valid dikatakan null, dan dalam C++ modern cara kanonik untuk mengekspresikannya adalah dengan nullptr. Writing int* p = nullptr; secara eksplisit menyatakan bahwa p belum menunjuk ke arah yang berarti.

Menghapus referensi dari pointer null adalah perilaku yang tidak terdefinisi, yang biasanya menyebabkan kerusakan, pelanggaran akses, atau pada papan kecil, sistem yang macet. Itulah sebabnya kode yang menerima pointer sebagai parameter sering kali memeriksa apakah nilainya null sebelum menggunakannya. Jika logika Anda mengizinkan "tidak ada objek" sebagai status yang bermakna, parameter pointer sesuai, karena dapat membawa informasi "tidak ada" tersebut melalui nullptr.

Contoh idiomatis adalah fungsi yang mengonversi string gaya C (char const*) ke std::string tetapi harus menangani kasus dengan baik jika penunjuk input bernilai null. Fungsi ini memeriksa apakah pointer bukan null sebelum membangun std::stringJika nilainya null, ia akan mengembalikan string kosong, alih-alih mendereferensikan alamat yang tidak valid.

Jika parameternya wajib dan tidak boleh absen, referensi C++ biasanya merupakan pilihan yang lebih baik daripada pointer mentah. Referensi tidak dapat diubah posisinya dan tidak dimaksudkan untuk bernilai null, sehingga sistem tipe dengan jelas menyatakan ekspektasi bahwa pemanggil harus menyediakan objek yang valid. Hal ini membuat API lebih aman dan kode lebih mudah dipahami.

Pointer sebagai parameter fungsi: berdasarkan nilai vs berdasarkan referensi

Secara default, saat Anda meneruskan suatu variabel ke suatu fungsi di C atau C++, variabel tersebut diteruskan berdasarkan nilai: fungsi menerima salinan nilai argumen, bukan variabel asli. Artinya, setiap penugasan pada parameter di dalam fungsi hanya memengaruhi salinan lokal dan membiarkan variabel pemanggil tidak berubah.

Perilaku ini sering kali diinginkan – ia mengisolasi fungsi dan menghindari efek samping yang mengejutkan – tetapi terkadang Anda benar-benar menginginkan suatu fungsi untuk mengubah variabel pemanggil. Anda mungkin berpikir untuk menggunakan variabel global, tetapi seiring berkembangnya program, variabel global dengan cepat menjadi sulit dilacak dan rawan kesalahan.

Pointer menawarkan alternatif yang bersih: Anda meneruskan alamat variabel ke fungsi, dan fungsi tersebut kemudian dapat mengubah nilai pada alamat tersebut. Ini dikenal sebagai “melewati referensi melalui pointer”. Dalam C++, Anda juga dapat menggunakan parameter referensi (int&), yang seringkali lebih jelas, tetapi memahami bentuk penunjuk tetap penting.

Bayangkan sebuah fungsi double_value yang seharusnya menggandakan bilangan bulat yang ditetapkan dalam pemanggil. Dengan menggunakan antarmuka berbasis pointer, Anda akan mendeklarasikannya sebagai mengambil int*, dan memanggilnya dengan meneruskan alamat variabel Anda: double_value(&k);Di dalam fungsi, *k = *k * 2; memperbarui nilai asli melalui penunjuk.

Teknik ini juga memungkinkan suatu fungsi untuk secara efektif “mengembalikan” beberapa hasil dengan memodifikasi beberapa variabel yang alamatnya dilewatkan sebagai argumen. Alih-alih mengembalikan struct yang kompleks, Anda dapat menerima beberapa parameter pointer dan memperbarui semuanya. Dalam C++ modern, Anda biasanya lebih menyukai referensi, tupel, atau struct demi kejelasan, tetapi parameter pointer tetap umum digunakan dalam API tingkat rendah dan pustaka C.

Aritmatika pointer dan array

Salah satu aspek pointer yang paling kuat – dan berbahaya – adalah aritmatika pointer, terutama dalam konteks array. Dalam C dan C++, array disimpan sebagai blok elemen yang bersebelahan dalam memori, dan nama array dapat berubah menjadi penunjuk ke elemen pertamanya saat diteruskan ke suatu fungsi atau digunakan dalam ekspresi tertentu.

Jika Anda menyatakan char h[] = {'P','r','o','m','e','t','e','c','\n'};, kemudian h dapat diperlakukan sebagai penunjuk ke h[0]. Mengakses h[i] secara konseptual setara dengan komputasi *(h + i), Di mana h adalah alamat dasar dan i adalah offset dalam elemen (bukan dalam byte). Kompiler mengalikan i berdasarkan ukuran setiap elemen (1 byte untuk char, 4 byte untuk int, dll.) sebelum menambahkannya ke penunjuk.

Ini berarti ketika Anda melihat ekspresi seperti *(h + i), Anda melakukan aritmatika pointer klasik: Anda memajukan pointer h by i posisi dan kemudian membuang referensi dari hasilnya. Karena alasan kinerja, kompiler sangat baik dalam mengoptimalkan pola ini, itulah sebabnya array dan pointer C secara historis menjadi kombinasi yang populer untuk pekerjaan tingkat rendah.

Anda juga dapat membuat penunjuk eksplisit ke elemen pertama suatu array dan menambah penunjuk tersebut untuk menelusuri array. Misalnya, mendeklarasikan char* ptr = h; dan kemudian mencetak berulang kali *ptr++ dalam satu loop akan melintasi setiap karakter secara berurutan. Postfix ++ memajukan penunjuk setelah setiap akses, memindahkannya ke elemen array berikutnya.

Gaya kompak ini merupakan gaya bahasa C yang idiomatis, namun bisa jadi sulit dipahami bagi pemula, sehingga dalam C++ modern banyak pengembang lebih memilih bentuk yang lebih eksplisit seperti for perulangan dengan indeks atau perulangan for berbasis rentang. Namun, pemahaman tentang aritmatika pointer sangat diperlukan untuk membaca dan memelihara kode lama, serta untuk mengimplementasikan rutinitas penting bagi kinerja.

Memori dinamis, baru/hapus dan iterasi penunjuk

Pointer juga merupakan pegangan mendasar yang Anda terima saat Anda mengalokasikan objek secara dinamis pada penyimpanan bebas (sering disebut secara informal sebagai heap). Dalam C++, operator new mengembalikan pointer ke objek yang baru dialokasikan, dan delete membebaskan memori itu saat Anda tidak lagi membutuhkannya.

Sebagai contoh, Student* p = new Student{...}; menyimpan cukup memori untuk satu Student objek dan mengembalikan alamatnya. Anda kemudian menggunakan p->member untuk mengakses anggotanya atau memanggil metodenya. Ketika objek tidak lagi diperlukan, delete p; menghancurkannya dan melepaskan memori kembali ke penyimpanan bebas.

C++ juga memungkinkan pengalokasian array secara dinamis menggunakan new[], yang mengembalikan penunjuk ke elemen pertama array. Misalnya, Student* p = new Student[100]; mengalokasikan ruang untuk 100 Student objek-objek yang ditata secara bersebelahan di dalam memori, dengan p menunjuk ke elemen pada indeks 0.

Dengan menggunakan aritmatika pointer, ekspresi p + i menunjuk ke ielemen ke- dari array itu, jadi (p + 4)->grade_1 adalah setara dengan p[4].grade_1. Secara konseptual, p seperti iterator yang dimulai pada elemen pertama, dan p + i kemajuan iterator itu dengan i langkah di sepanjang susunan.

Perbedaan antara pointer juga membawa arti: jika q = p + 4;, kemudian q - p bernilai 4, yang merupakan jumlah elemen antara kedua penunjuk tersebut. Dalam hal ini, pointer mentah adalah bentuk paling sederhana dari iterator akses acak. Banyak kontainer STL menampilkan iterator yang berperilaku serupa, tetapi menyembunyikan detail pointer mentah demi keamanan dan fleksibilitas.

Meskipun mentah new/delete kuat, C++ modern sangat menganjurkan penggunaan penunjuk pintar dan RAII (Resource Acquisition Is Initialization) untuk mengelola sumber daya secara otomatis. Petunjuk cerdas seperti std::unique_ptr dan std::shared_ptr merangkum kepemilikan dan secara otomatis membebaskan memori saat tidak lagi diperlukan, mengurangi risiko kebocoran dan penghapusan ganda.

Penunjuk ke penunjuk dan ketidakterarahan yang lebih dalam

Setelah Anda merasa nyaman dengan petunjuk sederhana, Anda pasti akan menjumpai petunjuk ke petunjuk (dan terkadang bahkan tingkat ketidakterusterangan yang lebih tinggi). Secara konseptual, pointer ke pointer hanyalah variabel lain yang menyimpan alamat variabel pointer, dan tidak merujuk langsung ke int, double atau objek.

Kasus penggunaan yang paling jelas adalah mengelola array pointer yang dialokasikan secara dinamis, seperti tabel yang dibangun secara dinamis dari string yang dialokasikan secara dinamis. Dalam bahasa C biasa, contoh klasiknya adalah char** argv dalam main fungsi, yang merupakan pointer ke array string gaya C, yang masing-masingnya merupakan char*.

Skenario lain yang sering terjadi adalah ketika suatu fungsi harus mengubah petunjuk yang disediakan oleh pemanggil, bukan hanya data yang ditunjuknya. Melewati pointer ke pointer memungkinkan fungsi untuk mengubah objek mana yang dirujuk oleh pointer asli, atau menginisialisasinya dengan mengalokasikan objek baru dengan new or mallocKode pemanggil kemudian melihat nilai penunjuk yang diperbarui.

Beberapa tingkat ketidaklangsungan juga muncul secara alami dalam struktur data tertentu, terutama struktur data tertaut yang dibuat di tumpukan. Misalnya, daftar tertaut yang dibangun secara dinamis dari node yang dialokasikan dengan malloc or new dapat melibatkan petunjuk ke node, ditambah fungsi yang menerima petunjuk ke petunjuk tersebut untuk memasukkan atau menghapus elemen sembari memperbarui petunjuk kepala.

Dan tentu saja, array dinamis multi-dimensi biasanya direpresentasikan sebagai pointer ke pointer dalam antarmuka gaya C: “matriks” biasanya dimodelkan sebagai int**, di mana setiap elemen dimensi pertama adalah penunjuk ke array baris. Dalam C++ modern Anda mungkin lebih suka std::vector<std::vector<T>> atau kelas matriks khusus, tetapi tata letak penunjuk-ke-penunjuk tetap mendasar dalam API dan pengikatan tingkat rendah.

Perangkap umum dan praktik baik dengan pointer

Bekerja secara langsung dengan pointer memberi Anda kendali yang lebih rinci, tetapi juga membuka pintu bagi kesalahan-kesalahan halus dan sulit di-debug jika Anda tidak disiplin. Banyak bug legendaris dalam basis kode C dan C++ disebabkan oleh kesalahan penanganan alamat mentah, baik dengan menulis ke memori yang bukan milik Anda atau karena lupa mengelola masa pakai objek.

Salah satu kesalahan klasik adalah menulis lebih banyak byte daripada yang dapat ditampung oleh tipe target, atau salah menafsirkan tipe lokasi memori. Misalnya, jika Anda menyimpan long nilai dalam suatu int variabel atau menulis long ke lokasi yang hanya berukuran untuk int, Anda akhirnya menimpa memori yang berdekatan, yang berpotensi merusak variabel lain atau bahkan penunjuk kode.

Bahaya lainnya adalah menetapkan nilai numerik sembarangan langsung ke variabel pointer, seperti ptrNum = 7;, kecuali Anda melakukan pekerjaan sistem tingkat sangat rendah dan tahu persis apa yang ada di alamat tersebut. Untuk kode aplikasi biasa, memperlakukan bilangan bulat sebagai alamat adalah garis langsung menuju perilaku tidak terdefinisi dan kerusakan yang tidak menentu.

Lupa menginisialisasi pointer dengan benar juga berisiko: pointer yang berisi nilai tak tentu mungkin terlihat baik-baik saja tetapi bisa saja menunjuk ke mana saja dalam memori. Selalu inisialisasi pointer Anda – baik ke alamat yang valid atau ke nullptr – dan periksa sebelum melakukan dereferensi jika ada keraguan bahwa mereka mungkin nol.

Akhirnya, dengan memori dinamis, setiap bahan mentah new harus dicocokkan dengan tepat satu yang sesuai delete, dan setiap new[] dengan tepat satu delete[]. Kebocoran terjadi saat Anda kehilangan jejak memori yang dialokasikan secara dinamis tanpa menghapusnya, dan penghapusan ganda (atau menghapus memori yang bukan milik Anda) merusak struktur internal pengalokasi, yang biasanya muncul sebagai cacat yang terputus-putus dan sangat sulit direproduksi.

Jika ditangani dengan hati-hati, pointer lebih seperti alat yang tajam dan seimbang daripada sumber kekacauan yang acak: pointer memungkinkan Anda bernalar tentang tata letak memori program, merancang struktur data yang efisien, dan membangun abstraksi yang kuat di atas kontrol tingkat rendah tersebut. Saat Anda berlatih, berpindah-pindah di antara alamat, melakukan dereferensi, dan memahami bagaimana fungsi dan array berinteraksi dengan pointer menjadi hal yang alami, dan rasa takut awal berubah menjadi rasa hormat yang sehat dan banyak kegunaan praktis.

Pos terkait: