Mengapa petunjuk merupakan faktor utama kebingungan bagi banyak mahasiswa baru, dan bahkan lama, di C atau C ++? Apakah ada alat atau proses pemikiran yang membantu Anda memahami bagaimana pointer bekerja di variabel, fungsi, dan di luar level?
Apa saja hal-hal praktik yang baik yang dapat dilakukan untuk membawa seseorang ke level, "Ah-hah, saya mengerti," tanpa membuat mereka terjebak dalam konsep keseluruhan? Pada dasarnya, bor skenario seperti.
Jawaban:
Pointer adalah konsep yang bagi banyak orang dapat membingungkan pada awalnya, khususnya ketika harus menyalin nilai pointer di sekitar dan masih merujuk pada blok memori yang sama.
Saya telah menemukan bahwa analogi terbaik adalah dengan menganggap pointer sebagai selembar kertas dengan alamat rumah di atasnya, dan memori memblokir referensi sebagai rumah yang sebenarnya. Segala macam operasi dengan demikian dapat dengan mudah dijelaskan.
Saya telah menambahkan beberapa kode Delphi di bawah ini, dan beberapa komentar yang sesuai. Saya memilih Delphi karena bahasa pemrograman utama saya yang lain, C #, tidak menunjukkan hal-hal seperti kebocoran memori dengan cara yang sama.
Jika Anda hanya ingin mempelajari konsep pointer tingkat tinggi, maka Anda harus mengabaikan bagian yang berlabel "Tata letak memori" dalam penjelasan di bawah ini. Mereka dimaksudkan untuk memberikan contoh seperti apa ingatan itu setelah operasi, tetapi sifatnya lebih rendah. Namun, untuk menjelaskan secara akurat bagaimana buffer overruns benar-benar bekerja, penting agar saya menambahkan diagram ini.
Penafian: Untuk semua maksud dan tujuan, penjelasan ini dan contoh tata letak memori sangat disederhanakan. Ada lebih banyak overhead dan lebih banyak detail yang perlu Anda ketahui jika Anda perlu berurusan dengan memori pada tingkat rendah. Namun, untuk maksud menjelaskan memori dan pointer, itu cukup akurat.
Mari kita asumsikan kelas THouse yang digunakan di bawah terlihat seperti ini:
Ketika Anda menginisialisasi objek rumah, nama yang diberikan kepada konstruktor disalin ke bidang pribadi FName. Ada alasannya itu didefinisikan sebagai array ukuran tetap.
Dalam memori, akan ada beberapa overhead yang terkait dengan alokasi rumah, saya akan menggambarkan ini di bawah ini seperti ini:
Area "tttt" adalah overhead, biasanya akan ada lebih dari ini untuk berbagai jenis runtime dan bahasa, seperti 8 atau 12 byte. Sangat penting bahwa nilai apa pun yang disimpan di area ini tidak akan pernah diubah oleh apa pun selain pengalokasi memori atau rutinitas sistem inti, atau Anda berisiko menabrak program.
Alokasikan memori
Dapatkan seorang wirausahawan untuk membangun rumah Anda, dan memberi Anda alamatnya ke rumah itu. Berbeda dengan dunia nyata, alokasi memori tidak dapat diberitahu di mana harus mengalokasikan, tetapi akan menemukan tempat yang cocok dengan ruang yang cukup, dan melaporkan kembali alamat ke memori yang dialokasikan.
Dengan kata lain, wirausahawan akan memilih tempat.
Tata letak memori:
Simpan variabel dengan alamat
Tuliskan alamatnya ke rumah baru Anda di atas selembar kertas. Makalah ini akan berfungsi sebagai referensi Anda ke rumah Anda. Tanpa selembar kertas ini, Anda tersesat, dan tidak dapat menemukan rumah, kecuali Anda sudah ada di dalamnya.
Tata letak memori:
Salin nilai penunjuk
Tulis saja alamatnya di selembar kertas baru. Anda sekarang memiliki dua lembar kertas yang akan membawa Anda ke rumah yang sama, bukan dua rumah terpisah. Setiap upaya untuk mengikuti alamat dari satu kertas dan mengatur ulang furnitur di rumah itu akan membuatnya tampak bahwa rumah lain telah dimodifikasi dengan cara yang sama, kecuali Anda dapat secara eksplisit mendeteksi bahwa itu sebenarnya hanya satu rumah.
Catatan Ini biasanya konsep yang saya punya masalah paling menjelaskan kepada orang, dua pointer tidak berarti dua objek atau blok memori.
Membebaskan memori
Hancurkan rumah. Anda kemudian dapat menggunakan kembali kertas untuk alamat baru jika Anda inginkan, atau menghapusnya untuk melupakan alamat ke rumah yang sudah tidak ada.
Di sini saya pertama kali membangun rumah, dan mendapatkan alamatnya. Lalu saya melakukan sesuatu ke rumah (gunakan, ... kode, dibiarkan sebagai latihan untuk pembaca), dan kemudian saya membebaskannya. Terakhir saya menghapus alamat dari variabel saya.
Tata letak memori:
Pointer menggantung
Anda memberi tahu pengusaha Anda untuk menghancurkan rumah, tetapi Anda lupa untuk menghapus alamat dari selembar kertas Anda. Ketika nanti Anda melihat selembar kertas, Anda lupa bahwa rumah itu tidak ada lagi, dan pergi mengunjunginya, dengan hasil yang gagal (lihat juga bagian tentang referensi yang tidak valid di bawah).
Menggunakan
h
setelah panggilan ke.Free
mungkin berhasil, tapi itu hanya keberuntungan belaka. Kemungkinan besar itu akan gagal, di tempat pelanggan, di tengah operasi kritis.Seperti yang Anda lihat, ia masih menunjuk ke sisa-sisa data dalam memori, tetapi karena itu mungkin tidak lengkap, menggunakannya seperti sebelumnya mungkin gagal.
Kebocoran memori
Anda kehilangan selembar kertas dan tidak dapat menemukan rumah. Rumah itu masih berdiri di suatu tempat, dan ketika nanti Anda ingin membangun rumah baru, Anda tidak dapat menggunakan kembali tempat itu.
Di sini kita menimpa isi
h
variabel dengan alamat rumah baru, tetapi yang lama masih berdiri ... di suatu tempat. Setelah kode ini, tidak ada cara untuk mencapai rumah itu, dan itu akan tetap berdiri. Dengan kata lain, memori yang dialokasikan akan tetap dialokasikan hingga aplikasi ditutup, pada titik mana sistem operasi akan merobohkannya.Tata letak memori setelah alokasi pertama:
Tata letak memori setelah alokasi kedua:
Cara yang lebih umum untuk mendapatkan metode ini adalah lupa untuk membebaskan sesuatu, alih-alih menimpa seperti di atas. Dalam istilah Delphi, ini akan terjadi dengan metode berikut:
Setelah metode ini dieksekusi, tidak ada tempat dalam variabel kami bahwa alamat ke rumah ada, tetapi rumah itu masih ada di luar sana.
Tata letak memori:
Seperti yang Anda lihat, data lama dibiarkan utuh dalam memori, dan tidak akan digunakan kembali oleh pengalokasi memori. Pengalokasi melacak area memori mana yang telah digunakan, dan tidak akan menggunakannya kembali kecuali Anda membebaskannya.
Membebaskan memori tetapi menyimpan referensi (sekarang tidak valid)
Hancurkan rumah, hapus salah satu dari selembar kertas tetapi Anda juga memiliki selembar kertas dengan alamat lama, ketika Anda pergi ke alamat, Anda tidak akan menemukan rumah, tetapi Anda mungkin menemukan sesuatu yang menyerupai reruntuhan dari satu.
Mungkin Anda bahkan akan menemukan rumah, tetapi bukan rumah yang semula Anda berikan alamatnya, dan dengan demikian segala upaya untuk menggunakannya seolah milik Anda mungkin gagal total.
Kadang-kadang Anda bahkan mungkin menemukan bahwa alamat tetangga memiliki rumah yang agak besar di atasnya yang menempati tiga alamat (Main Street 1-3), dan alamat Anda pergi ke tengah rumah. Setiap upaya untuk memperlakukan bagian dari rumah 3-alamat yang besar itu sebagai satu rumah kecil mungkin juga gagal total.
Di sini rumah itu dirobohkan, melalui referensi di
h1
, dan sementarah1
dibersihkan juga,h2
masih memiliki alamat lama, ketinggalan zaman. Akses ke rumah yang tidak lagi berdiri mungkin atau mungkin tidak berfungsi.Ini adalah variasi dari penunjuk menggantung di atas. Lihat tata letak memorinya.
Buffer dibanjiri
Anda memindahkan lebih banyak barang ke rumah daripada yang bisa Anda muat, tumpah ke rumah atau halaman tetangga. Ketika pemilik rumah tetangga nanti pulang, dia akan menemukan segala hal yang akan dia anggap miliknya.
Ini adalah alasan saya memilih array ukuran tetap. Untuk mengatur panggung, asumsikan bahwa rumah kedua yang kita alokasikan akan, untuk beberapa alasan, ditempatkan sebelum yang pertama di memori. Dengan kata lain, rumah kedua akan memiliki alamat yang lebih rendah daripada yang pertama. Juga, mereka dialokasikan tepat di sebelah satu sama lain.
Jadi, kode ini:
Tata letak memori setelah alokasi pertama:
Tata letak memori setelah alokasi kedua:
Bagian yang paling sering menyebabkan kerusakan adalah ketika Anda menimpa bagian penting dari data yang Anda simpan yang benar-benar tidak boleh diubah secara acak. Misalnya itu mungkin tidak menjadi masalah bahwa bagian dari nama h1-house telah diubah, dalam hal menabrak program, tetapi menimpa overhead objek kemungkinan besar akan crash ketika Anda mencoba menggunakan objek yang rusak, seperti yang akan menimpa tautan yang disimpan ke objek lain di objek.
Daftar tertaut
Ketika Anda mengikuti alamat pada selembar kertas, Anda sampai di sebuah rumah, dan di rumah itu ada selembar kertas lain dengan alamat baru di atasnya, untuk rumah berikutnya dalam rantai, dan seterusnya.
Di sini kami membuat tautan dari rumah kami ke kabin kami. Kita dapat mengikuti rantai sampai sebuah rumah tidak memiliki
NextHouse
referensi, yang berarti itu adalah yang terakhir. Untuk mengunjungi semua rumah kami, kami dapat menggunakan kode berikut:Tata letak memori (menambahkan NextHouse sebagai tautan dalam objek, dicatat dengan empat LLLL dalam diagram di bawah ini):
Dalam istilah dasar, apa itu alamat memori?
Alamat memori pada dasarnya hanya berupa angka. Jika Anda menganggap memori sebagai array besar byte, byte pertama memiliki alamat 0, yang berikutnya alamat 1 dan seterusnya ke atas. Ini disederhanakan, tetapi cukup baik.
Jadi tata letak memori ini:
Mungkin memiliki dua alamat ini (paling kiri - adalah alamat 0):
Yang artinya daftar tertaut kami di atas mungkin benar-benar terlihat seperti ini:
Biasanya menyimpan alamat yang "tidak menunjuk ke mana-mana" sebagai alamat-nol.
Dalam istilah dasar, apa itu pointer?
Pointer hanyalah variabel yang menyimpan alamat memori. Anda biasanya dapat meminta bahasa pemrograman untuk memberikan nomornya, tetapi sebagian besar bahasa pemrograman dan runtime mencoba menyembunyikan fakta bahwa ada angka di bawahnya, hanya karena angka itu sendiri tidak benar-benar memiliki arti bagi Anda. Yang terbaik adalah menganggap pointer sebagai kotak hitam, yaitu. Anda tidak benar-benar tahu atau peduli tentang bagaimana itu sebenarnya dilaksanakan, asalkan berfungsi.
sumber
Di kelas Comp Sci pertama saya, kami melakukan latihan berikut. Memang, ini adalah ruang kuliah dengan sekitar 200 siswa di dalamnya ...
Profesor menulis di papan tulis:
int john;
John berdiri
Profesor menulis:
int *sally = &john;
Sally berdiri, menunjuk ke john
Profesor:
int *bill = sally;
Bill berdiri, menunjuk John
Profesor:
int sam;
Sam berdiri
Profesor:
bill = &sam;
Bill sekarang menunjuk ke Sam.
Saya pikir Anda mendapatkan idenya. Saya pikir kami menghabiskan sekitar satu jam untuk melakukan ini, sampai kami membahas dasar-dasar penugasan pointer.
sumber
Sebuah analogi yang saya temukan bermanfaat untuk menjelaskan pointer adalah hyperlink. Sebagian besar orang dapat memahami bahwa tautan pada halaman web 'menunjuk' ke halaman lain di internet, dan jika Anda dapat menyalin & menempelkan hyperlink itu, maka keduanya akan menunjuk ke halaman web asli yang sama. Jika Anda pergi dan mengedit halaman asli itu, maka ikuti salah satu dari tautan tersebut (petunjuk) Anda akan mendapatkan halaman baru yang diperbarui.
sumber
int *a = b
tidak membuat dua salinan*b
).Pointer alasan tampaknya membingungkan begitu banyak orang adalah bahwa mereka kebanyakan datang dengan sedikit atau tanpa latar belakang dalam arsitektur komputer. Karena banyak yang tampaknya tidak memiliki gagasan tentang bagaimana komputer (mesin) sebenarnya diimplementasikan - bekerja di C / C ++ tampaknya asing.
Latihan adalah meminta mereka untuk mengimplementasikan mesin virtual sederhana berbasis bytecode (dalam bahasa apa pun yang mereka pilih, python berfungsi dengan baik untuk ini) dengan set instruksi yang difokuskan pada operasi pointer (memuat, menyimpan, menangani langsung / tidak langsung). Kemudian minta mereka untuk menulis program sederhana untuk set instruksi itu.
Apa pun yang membutuhkan sedikit lebih dari penambahan sederhana akan melibatkan pointer dan mereka pasti akan mendapatkannya.
sumber
Konsep placeholder untuk nilai - variabel - peta ke sesuatu yang kita ajarkan di sekolah - aljabar. Tidak ada paralel yang ada yang dapat Anda gambar tanpa memahami bagaimana memori diletakkan secara fisik di dalam komputer, dan tidak ada yang berpikir tentang hal semacam ini sampai mereka berurusan dengan hal-hal tingkat rendah - pada tingkat komunikasi C / C ++ / byte .
Kotak alamat. Saya ingat ketika saya belajar memprogram BASIC menjadi mikrokomputer, ada buku-buku cantik dengan permainan di dalamnya, dan kadang-kadang Anda harus menyodok nilai ke alamat tertentu. Mereka memiliki gambar sekelompok kotak, secara bertahap diberi label 0, 1, 2 ... dan dijelaskan bahwa hanya satu hal kecil (satu byte) yang dapat masuk ke dalam kotak-kotak ini, dan ada banyak dari mereka - beberapa komputer punya sebanyak 65535! Mereka bersebelahan, dan mereka semua memiliki alamat.
Untuk latihan? Buat struct:
Contoh yang sama seperti di atas, kecuali dalam C:
Keluaran:
Mungkin itu menjelaskan beberapa dasar melalui contoh?
sumber
Alasan saya mengalami kesulitan memahami pointer, pada awalnya, adalah bahwa banyak penjelasan termasuk banyak omong kosong tentang melewati referensi. Semua ini dilakukan membingungkan masalah. Saat Anda menggunakan parameter pointer, Anda masih memberikan nilai; tetapi nilai kebetulan merupakan alamat alih-alih, katakanlah, int.
Orang lain telah ditautkan dengan tutorial ini, tetapi saya dapat menyoroti saat ketika saya mulai memahami petunjuk:
Tutorial tentang Pointer dan Array di C: Bab 3 - Pointer dan String
Saat saya membaca kata-kata ini, awan membelah dan seberkas sinar matahari menyelimuti saya dengan pemahaman pointer.
Bahkan jika Anda seorang pengembang VB .NET atau C # (seperti saya) dan tidak pernah menggunakan kode yang tidak aman, masih layak untuk memahami cara kerja pointer, atau Anda tidak akan mengerti cara kerja referensi objek. Kemudian Anda akan memiliki gagasan umum tapi keliru bahwa meneruskan referensi objek ke metode akan menyalin objek.
sumber
Saya menemukan "Tutorial tentang Pointer dan Array dalam C" Ted Jensen sumber yang bagus untuk belajar tentang pointer. Ini dibagi menjadi 10 pelajaran, dimulai dengan penjelasan tentang apa pointer (dan apa gunanya) dan diakhiri dengan pointer fungsi. http://home.netcom.com/~tjensen/ptr/cpoint.htm
Beranjak dari sana, Beej's Guide to Network Programming mengajarkan Unix sockets API, dari mana Anda dapat mulai melakukan hal-hal yang sangat menyenangkan. http://beej.us/guide/bgnet/
sumber
Kompleksitas petunjuk melampaui apa yang dapat dengan mudah kita ajarkan. Mintalah siswa menunjuk satu sama lain dan menggunakan selembar kertas dengan alamat rumah adalah alat pembelajaran yang hebat. Mereka melakukan pekerjaan yang bagus untuk memperkenalkan konsep-konsep dasar. Memang, mempelajari konsep dasar sangat penting untuk berhasil menggunakan petunjuk. Namun, dalam kode produksi, adalah umum untuk masuk ke skenario yang jauh lebih kompleks daripada demonstrasi sederhana ini dapat merangkum.
Saya telah terlibat dengan sistem di mana kami memiliki struktur yang menunjuk ke struktur lain yang menunjuk ke struktur lain. Beberapa dari struktur itu juga berisi struktur yang disematkan (bukan petunjuk ke struktur tambahan). Di sinilah pointer menjadi sangat membingungkan. Jika Anda memiliki beberapa tingkat tipuan, dan Anda mulai berakhir dengan kode seperti ini:
itu bisa membingungkan sangat cepat (bayangkan lebih banyak baris, dan berpotensi lebih banyak level). Lemparkan array pointer, dan pointer node ke node (pohon, daftar terkait) dan masih lebih buruk lagi. Saya telah melihat beberapa pengembang yang sangat baik tersesat begitu mereka mulai bekerja pada sistem seperti itu, bahkan pengembang yang memahami dasar-dasarnya dengan sangat baik.
Struktur kompleks pointer tidak selalu menunjukkan pengkodean yang buruk, (meskipun mereka bisa). Komposisi adalah bagian penting dari pemrograman berorientasi objek yang baik, dan dalam bahasa dengan pointer mentah, itu pasti akan mengarah pada tipuan multi-layer. Lebih jauh, sistem sering perlu menggunakan perpustakaan pihak ketiga dengan struktur yang tidak cocok satu sama lain dalam gaya atau teknik. Dalam situasi seperti itu, kompleksitas secara alami akan muncul (walaupun tentu saja, kita harus berjuang semaksimal mungkin).
Saya pikir hal terbaik yang dapat dilakukan perguruan tinggi untuk membantu siswa mempelajari petunjuk adalah menggunakan demonstrasi yang baik, dikombinasikan dengan proyek yang membutuhkan penggunaan pointer. Satu proyek yang sulit akan melakukan lebih banyak untuk pemahaman pointer daripada seribu demonstrasi. Demonstrasi dapat membuat Anda memiliki pemahaman yang dangkal, tetapi untuk memahami secara mendalam, Anda harus benar-benar menggunakannya.
sumber
Saya pikir saya akan menambahkan analogi ke daftar ini yang saya temukan sangat membantu ketika menjelaskan petunjuk (kembali pada hari itu) sebagai Tutor Ilmu Komputer; pertama, mari:
Atur panggung :
Pertimbangkan tempat parkir dengan 3 ruang, ruang-ruang ini diberi nomor:
Di satu sisi, ini seperti lokasi memori, mereka berurutan dan berdekatan .. semacam array. Saat ini tidak ada mobil di dalamnya sehingga seperti array kosong (
parking_lot[3] = {0}
).Tambahkan data
Tempat parkir tidak pernah kosong untuk waktu yang lama ... jika tidak ada gunanya dan tidak akan ada yang membangun. Jadi katakanlah ketika hari bergerak di tempat parkir dipenuhi dengan 3 mobil, mobil biru, mobil merah, dan mobil hijau:
Mobil-mobil ini adalah semua jenis yang sama (mobil) jadi salah satu cara untuk memikirkan ini adalah bahwa mobil kami adalah semacam data (mengatakan sebuah
int
) tetapi mereka memiliki nilai yang berbeda (blue
,red
,green
; yang bisa menjadi warnaenum
)Masukkan pointer
Sekarang jika saya membawa Anda ke tempat parkir ini, dan meminta Anda untuk menemukan saya mobil biru, Anda mengulurkan satu jari dan menggunakannya untuk menunjuk ke mobil biru di tempat 1. Ini seperti mengambil pointer dan menugaskannya ke alamat memori (
int *finger = parking_lot
)Jari Anda (penunjuk) bukan jawaban untuk pertanyaan saya. Melihat di jari Anda memberitahu saya apa-apa, tapi kalau saya melihat di mana Anda jari Anda adalah menunjuk ke (dereferencing pointer), saya dapat menemukan mobil (data) yang saya cari.
Menugaskan kembali pointer
Sekarang saya dapat meminta Anda untuk mencari mobil merah dan Anda dapat mengarahkan ulang jari Anda ke mobil baru. Sekarang pointer Anda (yang sama seperti sebelumnya) menunjukkan kepada saya data baru (tempat parkir di mana mobil merah dapat ditemukan) dari jenis yang sama (mobil).
Pointer tidak berubah secara fisik, itu masih jari Anda , hanya data yang saya tunjukkan berubah. (alamat "tempat parkir")
Pointer ganda (atau pointer ke pointer)
Ini bekerja dengan lebih dari satu pointer juga. Saya bisa bertanya di mana penunjuknya, yang menunjuk ke mobil merah dan Anda bisa menggunakan tangan Anda yang lain dan menunjuk dengan satu jari ke jari pertama. (ini seperti
int **finger_two = &finger
)Sekarang jika saya ingin tahu di mana mobil biru itu, saya bisa mengikuti arah jari pertama ke jari kedua, ke mobil (data).
Pointer yang menggantung
Sekarang katakanlah Anda merasa sangat seperti patung, dan Anda ingin memegang tangan Anda ke arah mobil merah tanpa batas. Bagaimana jika mobil merah itu pergi?
Pointer Anda masih menunjuk ke tempat mobil merah itu tetapi sekarang tidak lagi. Katakanlah mobil baru masuk ke sana ... mobil oranye. Sekarang jika saya bertanya lagi, "di mana mobil merah", Anda masih menunjuk ke sana, tapi sekarang Anda salah. Itu bukan mobil merah, itu oranye.
Aritmatika petunjuk
Ok, jadi Anda masih menunjuk ke tempat parkir kedua (sekarang ditempati oleh mobil Orange)
Yah saya punya pertanyaan baru sekarang ... Saya ingin tahu warna mobil di tempat parkir berikutnya . Anda dapat melihat Anda menunjuk ke titik 2, jadi Anda hanya menambahkan 1 dan menunjuk titik berikutnya. (
finger+1
), sekarang karena saya ingin tahu data apa yang ada di sana, Anda harus memeriksa tempat itu (bukan hanya jari) sehingga Anda dapat menghormati pointer (*(finger+1)
) untuk melihat ada mobil hijau yang ada di sana (data di lokasi itu) )sumber
"without getting them bogged down in the overall concept"
sebagai pemahaman tingkat tinggi. Dan untuk titik Anda:"I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"
- Anda akan sangat terkejut betapa banyak orang tidak mengerti pointer bahkan ke level iniSaya tidak berpikir pointer sebagai konsep yang sangat rumit - kebanyakan model mental siswa memetakan sesuatu seperti ini dan beberapa sketsa kotak cepat dapat membantu.
Kesulitannya, setidaknya yang pernah saya alami di masa lalu dan melihat orang lain berurusan, adalah bahwa manajemen pointer di C / C ++ dapat berbelit-belit secara tidak perlu.
sumber
Contoh tutorial dengan seperangkat diagram yang baik sangat membantu dengan pemahaman pointer .
Joel Spolsky membuat beberapa poin bagus tentang memahami petunjuk dalam Guerrilla Guide to Interviewing article-nya:
sumber
Masalah dengan pointer bukanlah konsepnya. Ini adalah eksekusi dan bahasa yang terlibat. Kebingungan tambahan terjadi ketika guru berasumsi bahwa itu adalah KONSEP dari pointer yang sulit, dan bukan jargon, atau kekacauan C dan C ++ yang membuat konsep. Begitu banyak upaya yang dilakukan untuk menjelaskan konsep ini (seperti dalam jawaban yang diterima untuk pertanyaan ini) dan itu cukup sia-sia untuk seseorang seperti saya, karena saya sudah mengerti semua itu. Itu hanya menjelaskan bagian yang salah dari masalah.
Untuk memberi Anda ide dari mana saya berasal, saya seseorang yang mengerti pointer dengan sangat baik, dan saya bisa menggunakannya dengan kompeten dalam bahasa assembler. Karena dalam bahasa assembler mereka tidak disebut sebagai pointer. Mereka disebut sebagai alamat. Ketika datang ke pemrograman dan menggunakan pointer di C, saya membuat banyak kesalahan dan benar-benar bingung. Saya masih belum menyelesaikan ini. Biarkan saya memberi Anda sebuah contoh.
Ketika sebuah api berkata:
apa yang dia inginkan?
itu bisa ingin:
nomor yang mewakili alamat ke buffer
(Untuk memberikan itu, apakah saya katakan
doIt(mybuffer)
, ataudoIt(*myBuffer)
?)nomor yang mewakili alamat ke alamat ke buffer
(apakah itu
doIt(&mybuffer)
ataudoIt(mybuffer)
ataudoIt(*mybuffer)
?)nomor yang mewakili alamat ke alamat ke alamat ke buffer
(mungkin itu
doIt(&mybuffer)
. atau apakah itudoIt(&&mybuffer)
? atau bahkandoIt(&&&mybuffer)
)dan seterusnya, dan bahasa yang terlibat tidak menjelaskannya karena melibatkan kata "pointer" dan "referensi" yang tidak memiliki banyak makna dan kejelasan bagi saya seperti "x menyimpan alamat untuk y" dan " fungsi ini membutuhkan alamat ke y ". Jawabannya juga tergantung pada apa "mybuffer" itu, dan apa yang ingin dilakukan dengan itu. Bahasa ini tidak mendukung level sarang yang dijumpai dalam praktik. Seperti ketika saya harus menyerahkan "pointer" ke fungsi yang membuat buffer baru, dan memodifikasi pointer ke titik di lokasi baru buffer. Apakah itu benar-benar ingin pointer, atau pointer ke pointer, jadi ia tahu ke mana harus pergi untuk mengubah isi dari pointer. Sebagian besar waktu saya hanya perlu menebak apa yang dimaksud dengan "
"Pointer" terlalu kelebihan. Apakah penunjuk alamat ke nilai? atau apakah itu variabel yang menyimpan alamat ke suatu nilai. Ketika suatu fungsi menginginkan sebuah pointer, apakah ia menginginkan alamat yang dipegang oleh variabel pointer, atau apakah ia menginginkan alamat tersebut ke variabel pointer? Saya bingung.
sumber
double *(*(*fn)(int))(char)
, maka hasil evaluasi*(*(*fn)(42))('x')
akan menjadidouble
. Anda dapat menghapus lapisan evaluasi untuk memahami apa yang harus menjadi tipe perantara.(*(*fn)(42))('x')
itu?x
) di mana, jika Anda mengevaluasi*x
, Anda mendapatkan dua kali lipat.fn
ada dan lebih dalam hal apa yang dapat Anda lakukan denganfn
Saya pikir hambatan utama untuk memahami petunjuk adalah guru yang buruk.
Hampir semua orang diajarkan kebohongan tentang petunjuk: Bahwa mereka tidak lebih dari alamat memori , atau bahwa mereka memungkinkan Anda untuk menunjuk ke lokasi yang sewenang - wenang .
Dan tentu saja mereka sulit dimengerti, berbahaya dan semi-magis.
Tidak ada yang benar. Pointer sebenarnya adalah konsep yang cukup sederhana, selama Anda tetap berpegang pada apa yang dikatakan bahasa C ++ tentang mereka dan tidak menanamkannya dengan atribut yang "biasanya" ternyata bekerja dalam praktik, tetapi tetap tidak dijamin oleh bahasa , dan jadi bukan bagian dari konsep aktual pointer.
Saya mencoba menulis penjelasan tentang ini beberapa bulan yang lalu di posting blog ini - semoga akan membantu seseorang.
(Catatan, sebelum ada orang yang menyinggung saya, ya, standar C ++ mengatakan bahwa pointer mewakili alamat memori. Tetapi tidak mengatakan bahwa "pointer adalah alamat memori, dan tidak ada yang lain selain alamat memori dan dapat digunakan atau dipikirkan secara bergantian dengan memori). alamat ". Perbedaannya penting)
sumber
Saya pikir apa yang membuat pointer sulit untuk dipelajari adalah bahwa sampai pointer Anda merasa nyaman dengan gagasan bahwa "di lokasi memori ini adalah sekumpulan bit yang mewakili int, double, karakter, apa pun".
Ketika Anda pertama kali melihat pointer, Anda tidak benar-benar mendapatkan apa yang ada di lokasi memori itu. "Apa maksudmu, itu menyimpan alamat ?"
Saya tidak setuju dengan anggapan bahwa "Anda mendapatkannya atau tidak".
Mereka menjadi lebih mudah dipahami ketika Anda mulai menemukan kegunaan nyata untuk mereka (seperti tidak melewatkan struktur besar ke dalam fungsi).
sumber
Alasannya sangat sulit untuk dipahami bukan karena itu konsep yang sulit tetapi karena sintaksinya tidak konsisten .
Anda pertama kali mengetahui bahwa bagian paling kiri dari suatu kreasi variabel menentukan jenis variabel. Deklarasi pointer tidak berfungsi seperti ini di C dan C ++. Sebaliknya mereka mengatakan bahwa variabel menunjuk pada tipe di sebelah kiri. Dalam hal ini:
*
mypointer menunjuk ke int.Saya tidak sepenuhnya memahami pointer sampai saya mencoba menggunakannya dalam C # (dengan tidak aman), mereka bekerja dengan cara yang sama persis tetapi dengan sintaksis yang logis dan konsisten. Pointer adalah tipe itu sendiri. Di sini mypointer adalah penunjuk ke int.
Bahkan jangan mulai dengan fungsi pointer ...
sumber
int *p;
memiliki makna yang sederhana:*p
adalah bilangan bulat.int *p, **pp
berarti:*p
dan**pp
bilangan bulat.*p
dan**pp
yang tidak bilangan bulat, karena Anda tidak pernah dijalankanp
ataupp
atau*pp
ke titik apa pun. Saya mengerti mengapa beberapa orang lebih suka bertahan dengan tata bahasa yang satu ini, terutama karena beberapa kasus tepi dan kasus kompleks mengharuskan Anda untuk melakukannya (meskipun, masih, Anda dapat dengan mudah mengatasi hal itu dalam semua kasus yang saya sadari) ... tetapi saya tidak berpikir kasus-kasus itu lebih penting daripada fakta bahwa mengajarkan keselarasan benar menyesatkan pemula. Belum lagi jenis jelek! :)Saya bisa bekerja dengan pointer ketika saya hanya tahu C ++. Saya agak tahu apa yang harus dilakukan dalam beberapa kasus dan apa yang tidak boleh dilakukan dari percobaan / kesalahan. Tetapi hal yang memberi saya pemahaman penuh adalah bahasa assembly. Jika Anda melakukan debug tingkat instruksi serius dengan program bahasa assembly yang Anda tulis, Anda harus dapat memahami banyak hal.
sumber
Saya suka analogi alamat rumah, tetapi saya selalu berpikir alamatnya adalah kotak surat itu sendiri. Dengan cara ini Anda dapat memvisualisasikan konsep dereferencing pointer (membuka kotak surat).
Misalnya mengikuti daftar tertaut: 1) mulai dengan kertas Anda dengan alamat 2) Buka alamat di atas kertas 3) Buka kotak surat untuk menemukan selembar kertas baru dengan alamat berikutnya di atasnya
Dalam daftar tertaut linier, kotak pesan terakhir tidak ada di dalamnya (akhir daftar). Dalam daftar tertaut melingkar, kotak surat terakhir memiliki alamat kotak surat pertama di dalamnya.
Perhatikan bahwa langkah 3 adalah tempat dereference terjadi dan di mana Anda akan crash atau salah ketika alamat tidak valid. Dengan asumsi Anda bisa berjalan ke kotak surat dari alamat yang tidak valid, bayangkan bahwa ada lubang hitam atau sesuatu di sana yang mengubah dunia luar :)
sumber
Saya pikir alasan utama orang mengalami masalah adalah karena umumnya tidak diajarkan secara menarik dan menarik. Saya ingin melihat seorang dosen mendapatkan 10 sukarelawan dari kerumunan dan memberi mereka masing-masing 1 meter penggaris, membuat mereka berdiri di sekitar konfigurasi tertentu dan menggunakan para penguasa untuk saling menunjuk. Kemudian tunjukkan aritmetika penunjuk dengan menggerakkan orang di sekitar (dan di mana mereka mengarahkan penggaris mereka). Ini akan menjadi cara yang sederhana namun efektif (dan yang paling berkesan) untuk menunjukkan konsep tanpa terlalu macet di mekanik.
Setelah Anda mendapatkan C dan C ++ sepertinya semakin sulit bagi sebagian orang. Saya tidak yakin apakah ini karena mereka akhirnya menempatkan teori bahwa mereka tidak memahami praktik dengan benar atau karena manipulasi pointer secara inheren lebih sulit dalam bahasa tersebut. Saya tidak dapat mengingat transisi saya sendiri dengan baik, tetapi saya tahu petunjuk dalam Pascal dan kemudian pindah ke C dan benar-benar tersesat.
sumber
Saya tidak berpikir bahwa pointer itu sendiri membingungkan. Kebanyakan orang dapat memahami konsep tersebut. Sekarang, berapa banyak petunjuk yang dapat Anda pikirkan atau berapa tingkat tipuan yang nyaman bagi Anda. Tidak perlu terlalu banyak untuk menempatkan orang pada batas. Fakta bahwa mereka dapat diubah secara tidak sengaja oleh bug di program Anda juga dapat membuat mereka sangat sulit untuk di-debug ketika ada kesalahan dalam kode Anda.
sumber
Saya pikir itu mungkin sebenarnya masalah sintaksis. Sintaks C / C ++ untuk pointer tampaknya tidak konsisten dan lebih kompleks dari yang seharusnya.
Ironisnya, hal yang benar-benar membantu saya untuk memahami pointer menemukan konsep iterator di c + + Standard Template Library . Ini ironis karena saya hanya bisa berasumsi bahwa iterator dikandung sebagai generalisasi dari pointer.
Terkadang Anda tidak bisa melihat hutan sampai Anda belajar mengabaikan pohon.
sumber
(*p)
dilakukan(p->)
, dan dengan demikian kita akan memilikip->->x
alih - alih ambigu*p->x
a->b
hanya berarti(*a).b
.* p->x
berarti* ((*a).b)
sedangkan*p -> x
berarti(*(*p)) -> x
. Campuran operator prefix dan postfix menyebabkan penguraian ambigu.1+2 * 3
seharusnya 9.Kebingungan datang dari beberapa lapisan abstraksi yang digabungkan menjadi satu dalam konsep "pointer". Programmer tidak menjadi bingung oleh referensi biasa dalam Java / Python, tetapi pointer berbeda karena mereka mengekspos karakteristik arsitektur memori yang mendasarinya.
Merupakan prinsip yang baik untuk memisahkan lapisan abstraksi dengan bersih, dan petunjuk tidak melakukannya.
sumber
foo[i]
berarti pergi ke tempat tertentu, bergerak maju jarak tertentu, dan melihat apa yang ada di sana. Yang menyulitkan adalah lapisan abstraksi ekstra yang jauh lebih rumit yang ditambahkan oleh Standar semata-mata untuk kepentingan kompiler, tetapi memodelkan berbagai hal dengan cara yang tidak sesuai untuk kebutuhan programmer dan kebutuhan kompiler sama.Cara saya suka menjelaskannya adalah dalam hal array dan indeks - orang mungkin tidak terbiasa dengan pointer, tetapi mereka umumnya tahu apa itu indeks.
Jadi saya katakan bayangkan RAM adalah sebuah array (dan Anda hanya memiliki 10-byte RAM):
Kemudian pointer ke suatu variabel benar-benar hanya indeks (byte pertama) dari variabel dalam RAM.
Jadi jika Anda memiliki pointer / indeks
unsigned char index = 2
, maka nilainya jelas elemen ketiga, atau angka 4. Sebuah pointer ke pointer adalah tempat Anda mengambil nomor itu dan menggunakannya sebagai indeks itu sendiri, sepertiRAM[RAM[index]]
.Saya akan menggambar array pada daftar kertas, dan hanya menggunakannya untuk menunjukkan hal-hal seperti banyak pointer yang menunjuk ke memori yang sama, aritmatika pointer, pointer ke pointer, dan sebagainya.
sumber
Nomor kotak pos.
Itu adalah sepotong informasi yang memungkinkan Anda mengakses sesuatu yang lain.
(Dan jika Anda melakukan hitung pada nomor kotak pos, Anda mungkin memiliki masalah, karena hurufnya berada di kotak yang salah. Dan jika seseorang pindah ke negara bagian lain - tanpa alamat penerusan - maka Anda memiliki penunjuk yang menggantung. sisi lain - jika kantor pos meneruskan surat, maka Anda memiliki pointer ke pointer.)
sumber
Bukan cara yang buruk untuk menangkapnya, melalui iterator .. tetapi terus mencari Anda akan melihat Alexandrescu mulai mengeluh tentang mereka.
Banyak ex-C ++ devs (yang tidak pernah mengerti bahwa iterator adalah pointer modern sebelum membuang bahasa) melompat ke C # dan masih percaya mereka memiliki iterator yang layak.
Hmm, masalahnya adalah bahwa semua iterator berada dalam peluang penuh pada apa yang ingin dicapai oleh platform runtime (Java / CLR): penggunaan yang baru, sederhana, semua orang adalah dev. Yang bisa bagus, tetapi mereka mengatakannya sekali dalam buku ungu dan mereka mengatakannya bahkan sebelum dan sebelum C:
Tipuan.
Konsep yang sangat kuat tetapi tidak pernah demikian jika Anda melakukannya sepenuhnya .. Iterator berguna karena mereka membantu dengan abstraksi algoritma, contoh lain. Dan waktu kompilasi adalah tempat untuk suatu algoritma, sangat sederhana. Anda tahu kode + data, atau dalam bahasa lain C #:
IEnumerable + LINQ + Massive Framework = 300MB runtime penalti tipu daya buruk, menyeret aplikasi melalui banyak contoh tipe referensi ..
"Le Pointer murah."
sumber
Beberapa jawaban di atas telah menyatakan bahwa "pointer tidak terlalu sulit", tetapi belum langsung ditujukan ke mana "pointer sulit!" datang dari. Beberapa tahun yang lalu saya mengajari siswa CS tahun pertama (hanya satu tahun, karena saya jelas mengisapnya) dan jelas bagi saya bahwa ide pointer tidak sulit. Yang sulit adalah memahami mengapa dan kapan Anda menginginkan pointer .
Saya tidak berpikir Anda bisa menceraikan pertanyaan itu - mengapa dan kapan menggunakan pointer - dari menjelaskan masalah rekayasa perangkat lunak yang lebih luas. Mengapa setiap variabel tidak boleh menjadi variabel global, dan mengapa seseorang harus memfaktorkan kode yang sama ke dalam fungsi (bahwa, dapatkan ini, gunakan pointer untuk mengkhususkan perilaku mereka ke situs panggilan mereka).
sumber
Saya tidak melihat apa yang begitu membingungkan tentang pointer. Mereka menunjuk ke lokasi di memori, yaitu menyimpan alamat memori. Dalam C / C ++ Anda dapat menentukan jenis titik penunjuk. Sebagai contoh:
Mengatakan bahwa my_int_pointer berisi alamat ke lokasi yang berisi int.
Masalah dengan pointer adalah bahwa mereka menunjuk ke suatu lokasi dalam memori, sehingga mudah untuk dilacak ke beberapa lokasi Anda tidak boleh masuk. Sebagai bukti lihat banyak lubang keamanan dalam aplikasi C / C ++ dari buffer overflow (incrementing pointer melewati batas yang dialokasikan).
sumber
Hanya untuk membingungkan hal-hal sedikit lebih, kadang-kadang Anda harus bekerja dengan pegangan, bukan pointer. Pegangan adalah pointer ke pointer, sehingga ujung belakang dapat memindahkan benda-benda dalam memori untuk mendefrag tumpukan. Jika pointer berubah di pertengahan rutin, hasilnya tidak dapat diprediksi, jadi pertama-tama Anda harus mengunci pegangan untuk memastikan tidak ada yang terjadi di mana pun.
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 membicarakannya sedikit lebih masuk akal daripada saya. :-)
sumber
Setiap pemula C / C ++ memiliki masalah yang sama dan masalah itu terjadi bukan karena "pointer sulit dipelajari" tetapi "siapa dan bagaimana hal itu dijelaskan". Beberapa peserta didik mengumpulkannya secara lisan beberapa secara visual dan cara terbaik untuk menjelaskannya adalah dengan menggunakan contoh "melatih" (cocok untuk contoh verbal dan visual).
Di mana "lokomotif" adalah sebuah penunjuk yang tidak dapat menahan apa pun dan "gerobak" adalah apa yang "ditarik oleh lokomotif" (atau arahkan ke). Setelah itu, Anda dapat mengklasifikasikan "gerobak" itu sendiri, dapatkah binatang, tumbuhan atau manusia (atau campurannya).
sumber