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:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
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:
--- [ttttNNNNNNNNNNNN] ---
^ ^
| |
| + - array FName
|
+ - overhead
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.
THouse.Create('My house');
Tata letak memori:
--- [ttttNNNNNNNNNNNN] ---
1234Rumahku
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.
var
h: THouse;
begin
h := THouse.Create('My house');
...
Tata letak memori:
h
v
--- [ttttNNNNNNNNNNNN] ---
1234Rumahku
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.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
v
--- [ttttNNNNNNNNNNNN] ---
1234Rumahku
^
h2
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.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
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:
h <- +
v + - sebelum gratis
--- [ttttNNNNNNNNNNN] --- |
1234Rumah saya <- +
h (sekarang tidak menunjukkan apa-apa) <- +
+ - setelah gratis
---------------------- | (catatan, memori mungkin masih
rumah xx34My <- + mengandung beberapa data)
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).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
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.
h <- +
v + - sebelum gratis
--- [ttttNNNNNNNNNNN] --- |
1234Rumah saya <- +
h <- +
v + - setelah gratis
---------------------- |
xx34My House <- +
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.
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
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:
h
v
--- [ttttNNNNNNNNNNNN] ---
1234Rumahku
Tata letak memori setelah alokasi kedua:
h
v
--- [ttttNNNNNNNNNNN] --- [ttttNNNNNNNNNNNN]
1234Rumahku 5678Rumahku
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:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
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:
h <- +
v + - sebelum kehilangan pointer
--- [ttttNNNNNNNNNNN] --- |
1234Rumah saya <- +
h (sekarang tidak menunjukkan apa-apa) <- +
+ - setelah kehilangan pointer
--- [ttttNNNNNNNNNNN] --- |
1234Rumah saya <- +
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.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
Di sini rumah itu dirobohkan, melalui referensi di h1
, dan sementara h1
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:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
Tata letak memori setelah alokasi pertama:
h1
v
----------------------- [ttttNNNNNNNNNNN]
5678 rumah saya
Tata letak memori setelah alokasi kedua:
h2 h1
ay
--- [ttttNNNNNNNNNNN] ---- [ttttNNNNNNNNNNNN]
1234 Rumahku di suatu tempat
^ --- + - ^
|
+ - ditimpa
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.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
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:
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
Tata letak memori (menambahkan NextHouse sebagai tautan dalam objek, dicatat dengan empat LLLL dalam diagram di bawah ini):
h1 h2
ay
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNNNLLLL]
1234Rumah + 5678 Kabin +
| ^ |
+ -------- + * (tanpa tautan)
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:
h1 h2
ay
--- [ttttNNNNNNNNNNN] --- [ttttNNNNNNNNNNNN]
1234Rumahku 5678Rumahku
Mungkin memiliki dua alamat ini (paling kiri - adalah alamat 0):
Yang artinya daftar tertaut kami di atas mungkin benar-benar terlihat seperti ini:
h1 (= 4) h2 (= 28)
ay
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNNNLLLL]
1234Rumah 0028 5678Kabin 0000
| ^ |
+ -------- + * (tanpa tautan)
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.