Bagaimana Pointer ke Struktur Bekerja di C

3

Anda dapat menunjuk ke hampir semua hal di C. Bahasa ini memungkinkan Anda membuat pointer untuk tipe primitif, array, fungsi, dan tipe yang ditentukan pengguna. Namun petunjuk mengenai struktur adalah hal yang menarik. Hal ini sangat umum terjadi. Jika Anda menulis kode C, kemungkinan besar Anda akan sering menemukannya.

Berikut adalah contoh dasarnya. Ini menunjukkan bagaimana Anda mendefinisikan suatu struktur dan kemudian membuat penunjuk ke sana.

Cuplikan ini melakukan lebih dari sekedar menetapkan alamat memori. Ini membangun hubungan antara variabel ptr dan struct p1. Pointer menyimpan alamat struktur. Ini memungkinkan Anda untuk mengakses anggota x dan y secara tidak langsung. Ini adalah pola mendasar. Anda melihatnya dalam daftar tertaut, alokasi memori dinamis, dan argumen fungsi.

Mengapa Menggunakan Pointer pada Struktur?

Menyalin seluruh struktur itu mahal. Jika struct Anda berisi banyak data, meneruskannya berdasarkan nilai ke suatu fungsi berarti kompiler membuat salinan lengkap. Ini membuang-buang waktu dan memori. Menggunakan pointer menghindari penyalinan. Anda malah memberikan alamatnya. Fungsi ini dapat mengubah data asli tanpa overhead.

Ada juga pertanyaan tentang ukuran dinamis. Array memiliki ukuran tetap pada waktu kompilasi. Struktur tidak serta merta menyelesaikan masalah dinamis dengan sendirinya. Namun saat Anda menggabungkan struct dengan malloc, Anda mendapatkan penyimpanan data yang fleksibel. Anda dapat mengalokasikan struktur di heap. Penunjuk ke struktur tersebut memungkinkan Anda mengatur masa pakainya secara manual.

Mengakses Anggota melalui Pointer

Setelah Anda memiliki penunjuk, Anda perlu mengakses data di dalamnya. Anda tidak dapat menggunakan operator titik. Ekspresi ptr.x akan gagal dikompilasi. ptr adalah alamat, bukan struktur itu sendiri. Anda harus melakukan dereferensi terlebih dahulu.

Ada dua cara untuk melakukan ini. Yang pertama adalah verbose.

Tanda kurung wajib ada di sini. Operator titik memiliki prioritas lebih tinggi dibandingkan operator dereferensi. Tanpa mereka, kompiler mencoba mengakses ptr.x, namun tidak ada.

Cara kedua lebih bersih. C menyediakan operator panah ->. Ini menggabungkan dereferensi dan akses anggota menjadi satu langkah.

Sintaks ini standar. Ini adalah apa yang akan Anda lihat di basis kode nyata. Bunyinya secara alami adalah “pergi ke objek yang ditunjuk oleh ptr dan dapatkan anggota x -nya.” Ini ringkas. Ini mengurangi kekacauan visual.

Kesalahan Umum

Petunjuk nol adalah sebuah risiko. Jika Anda mendeklarasikan sebuah pointer tetapi tidak menginisialisasinya, itu menunjuk ke sampah. Mengakses ptr->x pada penunjuk nol menyebabkan kesalahan segmentasi. Anda harus memeriksa null sebelum menggunakan pointer.

Pemeriksaan ini bukan opsional dalam kode yang kuat. Ini mencegah crash.

Masalah lainnya adalah kebocoran memori. Jika Anda mengalokasikan memori dengan malloc dan kehilangan penunjuknya, memori tersebut tetap disimpan hingga program berakhir. Anda perlu melacak setiap alokasi. gratis() adalah tanggung jawab Anda. Lupakan saja

Pointer ke struktur di C sering kali membuat pengembang tersandung yang merasa nyaman dengan pointer dasar tetapi bingung dengan sintaksisnya. Pertimbangkan definisi rekaman sederhana untuk profil pengguna.

struktur typedef {
nama karakter[21];
kota char[21];
status karakter[3];
} Rek;

typedef Rec *RecPointer;

Di sini, RecPointer hanyalah sebuah alias untuk penunjuk ke struktur Rec. Saat Anda mendeklarasikan variabel jenis ini, Anda berurusan dengan alamat memori, bukan data itu sendiri.

RecPointer r;

Variabel r menempati empat byte pada sistem 32-bit (atau delapan byte pada 64-bit). Itu hanya sebuah penunjuk. Itu tidak memuat nama, kota, atau negara bagian. Untuk menyimpan data aktual, Anda harus mengalokasikan memori pada heap.

r = (RecPointer)malloc(sizeof(Rec));

Panggilan malloc ini memiliki cadangan 45 byte. Yaitu 21 byte untuk nama, 21 untuk kota, dan 3 untuk negara bagian, ditambah satu byte untuk padding atau penyelarasan untuk memenuhi batas memori. Sekarang r menunjuk ke blok memori valid yang berperilaku persis seperti struktur Rec.

Mengakses Anggota melalui Dereferensi

Untuk berinteraksi dengan data, Anda harus melakukan dereferensi penunjuk. Di sinilah kesalahan prioritas muncul. Anda dapat mengakses anggota menggunakan notasi titik standar, tetapi Anda harus membungkus dereferensi dalam tanda kurung.

strcpy((r).nama, “Leigh”);
strcpy((
r).kota, “Raleigh”);
strcpy((*r).status, “NC”);

Perhatikan sintaksnya. (*r).nama benar. Jika Anda menulis *r.name, kompilasi gagal. Mengapa? Karena operator titik mempunyai prioritas lebih tinggi dibandingkan operator dereferensi. Kompiler menafsirkan *r.name sebagai *(r.name). Karena r adalah sebuah pointer, r.name adalah sintaksis yang tidak valid, dan ekspresi diciutkan. Tanda kurung memaksa dereferensi *r terjadi terlebih dahulu, menghasilkan struktur, dan kemudian operator titik mengakses bidang nama.

Mengetik itu membosankan. Kelihatannya berantakan. Ini mengundang kesalahan.

Notasi Panah

C menyediakan cara yang lebih bersih untuk menangani ini. Operator panah -> adalah gula sintaksis untuk (*pointer).member. Ini bukanlah mekanisme yang berbeda. Bukan operator baru yang mengubah cara memori diakses. Ini hanyalah cara singkat untuk menulis dereferensi dan akses anggota dalam satu langkah.

strcpy(r->nama, “Leigh”);

Ini identik dengan strcpy((*r).name, "Leigh"). Ini menghemat dua karakter. Ini menghilangkan kebutuhan akan tanda kurung bersarang. Ini adalah cara standar sebagian besar pengembang C berinteraksi dengan penunjuk struktur.

Implikasi Manajemen Memori

Panggilan gratis(r) adalah wajib. Memori dialokasikan dari heap. Kalau tidak dilepas nanti bocor. Pointer r itu sendiri, variabel empat byte yang menyimpan alamat, bersifat lokal pada frame tumpukan atau lingkup global, namun data yang ditunjuknya berada di tempat lain.

Saat Anda menggunakan r->nama, Anda memodifikasi data pada alamat yang disimpan di r. Penunjuk r tetap tidak berubah. Alamatnya bisa

Mengalokasikan memori untuk array dengan cepat adalah inti dari pemrograman C, namun memerlukan pemahaman bagaimana pointer berinteraksi dengan blok memori mentah. Saat Anda membutuhkan array berukuran tetap yang tidak diketahui pada waktu kompilasi, alokasi tumpukan standar tidak akan mencukupinya. Anda harus meraih tumpukan itu.

Cuplikan kode di bawah menunjukkan satu pola umum:

Di sini, malloc menyediakan ruang untuk sepuluh bilangan bulat. Pemeran ke (int *) memastikan penunjuk cocok dengan tipe yang diharapkan, meskipun kompiler C modern sering memperingatkan terhadap pemeran eksplisit untuk void*. Loop kemudian menginisialisasi setiap elemen ke nol menggunakan notasi subskrip. Terakhir, gratis melepaskan memori kembali ke sistem.

Tapi ini bukan satu-satunya cara untuk menulisnya.

Anda dapat memperoleh hasil yang sama persis dengan menukar p[i] dengan aritmatika penunjuk:

Mengapa ini penting? Karena p[i] hanyalah gula sintaksis untuk *(p+i). Kompiler memperlakukannya dengan cara yang sama. Jika Anda bekerja dengan sistem tertanam atau menulis loop ketat yang setiap siklusnya penting, mengetahui bahwa ini dapat dipertukarkan akan membantu Anda membaca kode lama—dan menulis kode Anda sendiri tanpa kebingungan.

Namun, masih ada jebakan halus.

Jika Anda mendeklarasikan pointer ke tipe array secara langsung—seperti int (*p)[10] —Anda berhadapan dengan monster yang sama sekali berbeda. Pointer itu menunjuk ke seluruh array, bukan hanya elemen pertama. Menambahnya akan memindahkan penunjuk berdasarkan ukuran seluruh array, bukan satu bilangan bulat. Kebanyakan pengembang tetap menggunakan int * karena lebih sederhana dan fleksibel.

Kapan Menggunakan Pendekatan Yang Mana

Memilih antara notasi subskrip dan aritmatika penunjuk eksplisit sering kali bergantung pada keterbacaan dan maksud.

  • Gunakan p[i] bila Anda ingin menekankan akses berbasis indeks. Ini lebih jelas bagi sebagian besar pembaca.
  • Gunakan *(p+i) saat Anda melakukan manipulasi memori tingkat rendah atau perlu menghindari kebiasaan peluruhan array dalam ekspresi kompleks.

Kedua pendekatan tersebut memerlukan manajemen memori yang cermat. Lupakan gratis, dan Anda bocor. Sebut saja terlalu dini, dan Anda akan menemukan perilaku tidak terdefinisi. Dan meskipun malloc sangat jelas di sini, selalu verifikasi bahwa nilai yang dikembalikan bukan NULL sebelum melakukan dereferensi.

“Penunjuk ke array sangat berguna, namun membutuhkan kedisiplinan. Satu kesalahan saja maka Anda akan membaca sampah—atau lebih buruk lagi, merusak memori variabel lain.”

Dalam praktiknya, sebagian besar pengembang jarang perlu menulis aritmatika penunjuk mentah untuk array sederhana. Perpustakaan seperti std::vector di C++ atau abstraksi tingkat tinggi dalam bahasa lain menangani ini secara otomatis. Tapi di C? Anda sendirian.

Dan itulah mengapa memahami mekanisme itu penting. Bukan hanya untuk lulus wawancara atau menulis buku teks, tapi ketika kode rusak pada jam 2 pagi dan Anda perlu tahu persis apa yang terjadi di memori.

Saat Anda mendeklarasikan pointer ke array integer, Anda tidak membuat sesuatu yang eksotik. Itu hanyalah penunjuk standar ke int. Keajaiban terjadi dengan malloc. Anda mengalokasikan satu blok memori yang cukup besar untuk berapa pun bilangan bulat yang Anda perlukan. Pointer kemudian menargetkan elemen pertama dari blok itu.

C tidak peduli bagaimana Anda mengaksesnya. Anda dapat menggunakan tanda kurung siku seperti p[5] atau menggunakan aritmatika penunjuk seperti *(p + 5). Kompiler memperlakukannya sebagai identik. Fleksibilitas inilah yang menyebabkan array dinamis sangat berguna untuk string. Anda tidak dapat menebak ukurannya. Anda mengalokasikan penyimpanan yang cukup untuk panjang string ditambah terminator nol.

Array Pointer vs. Array Struktur

Mengapa menggunakan array pointer ketika Anda hanya bisa menggunakan array struct? Ruang angkasa. Atau lebih tepatnya, kekurangannya.

Pertimbangkan struktur Rec dengan tiga array karakter masing-masing 81 byte. Itu adalah 243 byte per catatan. Jika Anda mendeklarasikan Rec Records[10], Anda langsung mencadangkan 2.430 byte di memori. Semuanya. Meskipun Anda hanya menggunakan satu catatan.

Serangkaian pointer mengubah matematika.

Array a sendiri hanya menampung 10 pointer. Pada sistem 64-bit, itu berarti 80 byte. Itu adalah sebagian kecil dari memori yang diperlukan untuk struktur penuh. Memori untuk catatan sebenarnya tetap tidak terpakai sampai Anda membutuhkannya.

Anda dapat mengalokasikan satu catatan sesuai permintaan.

Pola ini memecahkan masalah intensif memori dengan menunda alokasi. Anda hanya membayar untuk apa yang Anda gunakan. Ketika Anda selesai dengan rekaman, Anda memanggil gratis. Pointer menjadi referensi yang menggantung, namun memori dikembalikan ke sistem.

Struktur yang Berisi Pointer

Struktur dapat menyimpan petunjuk. Hal ini memungkinkan Anda untuk menggabungkan data berukuran tetap dengan data berukuran variabel dalam objek yang sama.

Ambil entri buku alamat. Nama, kota, dan nomor telepon mungkin memiliki panjang maksimum yang wajar. Namun, sebuah komentar bisa berupa apa saja, mulai dari satu kata hingga novel. Anda tidak ingin membuang-buang ruang dengan mengalokasikan buffer yang besar untuk setiap entri kalau-kalau ada orang yang menulis komentar panjang.

Struktur Addr itu sendiri berukuran kecil

Cara Bidang Komentar Menangani Catatan Kosong vs. Terisi

Tidak semua catatan dalam database membawa komentar. Jika suatu bidang dibiarkan kosong, bidang tersebut tidak akan kosong. Itu memegang sebuah penunjuk. Secara khusus, penunjuk 4-byte yang tidak menunjuk pada substansi apa pun. Sistem memperlakukan ketidakhadiran ini sebagai keadaan yang valid. Catatannya masih lengkap. Metadatanya utuh.

Namun apa yang terjadi jika pengguna benar-benar mengetik sesuatu?

Alokasinya berubah. Basis data tidak menyediakan buffer tetap untuk komentar-komentar ini. Itu tidak menebak panjang maksimum dan mengisi sisanya dengan byte nol. Itu akan membuang-buang ruang. Sebaliknya, sistem menghitung panjang string yang tepat. Kemudian ia mengalokasikan byte sebanyak itu.

Alokasi dinamis ini efisien. Ini mencegah fragmentasi dari kembung. Catatan singkat membutuhkan beberapa byte. Esai yang panjang membutuhkan lebih banyak. Penunjuk dalam catatan kosong menunjuk ke referensi nol, sehingga jejaknya tetap minimal. Catatan dengan konten diregangkan agar sesuai dengan data. Tidak ada yang terbuang. Tidak ada yang dipaksakan.

Apakah ini satu-satunya cara untuk menyimpan teks? Tidak. Tapi ini adalah cara cerdas untuk menyeimbangkan kecepatan dan ruang. Anda mendapatkan responsivitas header berukuran tetap dengan fleksibilitas data dengan panjang variabel. Ini adalah detail kecil. Seorang mekanik tingkat rendah. Tapi itu bertambah ketika Anda mengelola jutaan catatan. Basis data bernafas lebih mudah. Penggunaan disk tetap ramping.

Dan penggunanya? Mereka tidak pernah melihat penunjuknya. Mereka hanya melihat komentar mereka. Atau kekurangannya. Kompleksitasnya tersembunyi. Penyimpanan dioptimalkan. Hasilnya adalah sistem yang terasa ringan, bahkan ketika datanya bertambah.