İşaretçiler, özellikle işaretçi değerlerinin etrafına kopyalanması ve aynı bellek bloğuna başvurulması söz konusu olduğunda, birçokları için başta kafa karıştırıcı olabilen bir kavramdır.
En iyi benzetmenin, işaretçiyi üzerinde bir ev adresi olan bir kağıt parçası olarak ve gerçek ev olarak başvurduğu bellek bloğunu düşünmek olduğunu buldum. Böylece her türlü işlem kolayca açıklanabilir.
Aşağıda bazı Delphi kodları ve uygun olduğunda bazı yorumlar ekledim. Delphi'yi seçtim, çünkü diğer ana programlama dilim C #, bellek sızıntıları gibi şeyleri aynı şekilde sergilemiyor.
Yalnızca üst düzey işaretçi kavramını öğrenmek istiyorsanız, aşağıdaki açıklamada "Bellek düzeni" etiketli parçaları görmezden gelmelisiniz. Operasyonlardan sonra hafızanın nasıl görünebileceğine dair örnekler vermeye yöneliktirler, ancak doğada daha düşük seviyelidirler. Ancak, arabellek taşmalarının gerçekte nasıl çalıştığını doğru bir şekilde açıklamak için, bu diyagramları eklemem önemliydi.
Yasal Uyarı: Tüm niyetler ve amaçlar için, bu açıklama ve örnek bellek düzenleri büyük ölçüde basitleştirilmiştir. Hafıza ile düşük seviyede işlem yapmanız gerekip gerekmediğini bilmeniz gereken daha fazla yük ve daha fazla ayrıntı var. Bununla birlikte, hafızayı ve işaretçileri açıklamak için yeterince doğrudur.
Aşağıda kullanılan THouse sınıfının aşağıdaki gibi olduğunu varsayalım:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
House nesnesini başlattığınızda, kurucuya verilen ad FName özel alanına kopyalanır. Sabit boyutlu bir dizi olarak tanımlanmasının bir nedeni vardır.
Hafızada, ev tahsisi ile ilişkili bazı ek yükler olacak, bunu aşağıdaki şekilde göstereceğim:
--- [ttttNNNNNNNNNN] ---
^ ^
| |
| + - FName dizisi
|
+ - tepegöz
"Tttt" alanı tepegözdür, genellikle 8 veya 12 bayt gibi çeşitli çalışma türleri ve diller için bundan daha fazla olacaktır. Bu alanda depolanan değerlerin hiçbir zaman bellek ayırıcı veya çekirdek sistem rutinleri dışında hiçbir şeyle değişmemesi ya da programı çökme riskiniz olması zorunludur.
Bellek ayırma
Evinizi inşa etmek için bir girişimci alın ve size evin adresini verin. Gerçek dünyanın aksine, bellek tahsisine nereye tahsis edileceği söylenemez, ancak yeterli alana sahip uygun bir yer bulur ve adresi tahsis edilen belleğe geri bildirir.
Başka bir deyişle, girişimci yeri seçecektir.
THouse.Create('My house');
Bellek düzeni:
--- [ttttNNNNNNNNNN] ---
1234 Evim
Adresle birlikte bir değişken tutma
Adresi yeni evinize bir parça kağıda yazın. Bu makale evinize referans olarak hizmet edecektir. Bu kağıt parçası olmadan, kaybolursunuz ve zaten içinde değilseniz, evi bulamazsınız.
var
h: THouse;
begin
h := THouse.Create('My house');
...
Bellek düzeni:
h
v
--- [ttttNNNNNNNNNN] ---
1234 Evim
İşaretçi değerini kopyala
Adresi yeni bir kağıda yazmanız yeterlidir. Şimdi sizi aynı eve götürecek iki ayrı kağıdınız var, iki ayrı eve değil. Adresi bir kağıttan takip etme ve o evdeki mobilyaları yeniden düzenleme girişimleri, aslında sadece bir ev olduğunu açıkça belirleyemediğiniz sürece , diğer evin aynı şekilde değiştirilmiş görünmesini sağlayacaktır .
Not Bu genellikle insanlara en çok açıkladığım kavramdır, iki işaretçi iki nesne veya bellek bloğu anlamına gelmez.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
v
--- [ttttNNNNNNNNNN] ---
1234 Evim
^
h2
Hafızada yer açma
Evi yık. Daha sonra isterseniz yeni bir adres için kağıdı yeniden kullanabilir veya artık mevcut olmayan evin adresini unutmak için temizleyebilirsiniz.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
Burada önce evi inşa ediyorum ve adresini ele alıyorum. Sonra eve bir şey yapıyorum (kullan, ... kodunu, okuyucu için bir egzersiz olarak bıraktım) ve sonra serbest bırakıyorum. Son olarak adresi değişkenimden silerim.
Bellek düzeni:
h <- +
v + - ücretsiz önce
--- [ttttNNNNNNNNNN] --- |
1234 Evim <- +
h (şimdi hiçbir yere işaret etmiyor) <- +
+ - bedavadan sonra
---------------------- | (not, bellek yine de olabilir
xx34Evim <- + bazı veriler içeriyor)
Sarkan işaretçiler
Girişimcinize evi yok etmesini söylersiniz, ancak adresi kağıdınızdan silmeyi unutursunuz. Daha sonra kağıda baktığınızda, evin artık orada olmadığını ve başarısız sonuçlarla ziyarete gittiğini unuttunuz (ayrıca aşağıdaki geçersiz referans hakkındaki bölüme bakın).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
Kullanılması h
için çağrısından sonra .Free
kudreti çalışmalarında, ama bu sadece saf şans. Büyük olasılıkla, bir müşterinin bulunduğu yerde, kritik bir operasyonun ortasında başarısız olacaktır.
h <- +
v + - ücretsiz önce
--- [ttttNNNNNNNNNN] --- |
1234 Evim <- +
h <- +
v + - ücretsiz olduktan sonra
---------------------- |
Evim <- +
Gördüğünüz gibi, h hala bellekteki verilerin kalıntılarını gösterir, ancak tamamlanamayacağı için, daha önce olduğu gibi kullanmak başarısız olabilir.
Bellek sızıntısı
Kağıt parçasını kaybediyorsunuz ve evi bulamıyorsunuz. Ev hala bir yerde duruyor ve daha sonra yeni bir ev inşa etmek istediğinizde, o noktayı tekrar kullanamazsınız.
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;
Burada, h
değişkenin içeriğinin üzerine yeni bir evin adresi yazdık , ancak eski olan hala duruyor ... bir yerde. Bu koddan sonra, o eve ulaşmanın bir yolu yoktur ve ayakta kalacaktır. Başka bir deyişle, tahsis edilen bellek, uygulama kapanana kadar tahsis edilir ve bu noktada işletim sistemi onu yıkar.
İlk ayırmadan sonra bellek düzeni:
h
v
--- [ttttNNNNNNNNNN] ---
1234 Evim
İkinci ayırmadan sonra bellek düzeni:
h
v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 Evim 5678 Evim
Bu yöntemi elde etmenin daha yaygın bir yolu, yukarıdaki gibi üzerine yazmak yerine bir şeyi serbest bırakmayı unutmaktır. Delphi açısından, bu aşağıdaki yöntemle gerçekleşir:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
Bu yöntem yürütüldükten sonra, değişkenlerimizde evin adresinin var olduğu bir yer yoktur, ancak ev hala dışarıdadır.
Bellek düzeni:
h <- +
v + - işaretçiyi kaybetmeden önce
--- [ttttNNNNNNNNNN] --- |
1234 Evim <- +
h (şimdi hiçbir yere işaret etmiyor) <- +
+ - işaretçiyi kaybettikten sonra
--- [ttttNNNNNNNNNN] --- |
1234 Evim <- +
Gördüğünüz gibi, eski veriler bellekte bozulmadan kalır ve bellek ayırıcı tarafından tekrar kullanılmaz. Ayırıcı, hangi bellek alanlarının kullanıldığını takip eder ve serbest bırakmadıkça bunları yeniden kullanmaz.
Belleği boşaltmak ancak (şimdi geçersiz) bir referans tutmak
Evi yıkın, kağıt parçalarından birini silin, ancak üzerinde eski adresi olan başka bir kağıt parçanız var, adrese gittiğinizde bir ev bulamazsınız, ancak harabeye benzeyen bir şey bulabilirsiniz. biri.
Belki bir ev bile bulacaksınız, ama aslında adresin verildiği ev değil ve bu nedenle size aitmiş gibi kullanma girişimleri korkunç bir şekilde başarısız olabilir.
Bazen komşu bir adresin üzerinde üç adresi kaplayan oldukça büyük bir ev olduğunu bile görebilirsiniz (Ana Cadde 1-3) ve adresiniz evin ortasına gider. Büyük 3 adresli evin bu bölümünü tek bir küçük ev olarak ele alma girişimleri de korkunç bir şekilde başarısız olabilir.
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?
Burada ev, referans yoluyla yıkıldı h1
ve aynı h1
zamanda temizlendi, h2
hala eski, güncel olmayan adrese sahip. Artık ayakta olmayan eve erişim işe yarayabilir veya çalışmayabilir.
Bu, yukarıdaki sarkan işaretçinin bir varyasyonudur. Bellek düzenine bakın.
Arabellek taşması
Evin içine, sığabileceklerinden daha fazla şey taşıyorsunuz, komşuların evine ya da bahçesine dökülüyorsunuz. Bu komşu evin sahibi daha sonra eve geldiğinde, kendi düşüneceği her şeyi bulacaktır.
Bu yüzden sabit boyutlu bir dizi seçtim. Sahneyi ayarlamak için, tahsis ettiğimiz ikinci evin, bir nedenden dolayı, hafızadaki ilk evin önüne yerleştirileceğini varsayalım. Başka bir deyişle, ikinci evin ilk evden daha düşük bir adresi olacaktır. Ayrıca, yan yana yerleştirilmişlerdir.
Böylece, bu kod:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
İlk ayırmadan sonra bellek düzeni:
h1
v
----------------------- [ttttNNNNNNNNNN]
5678 Evim
İkinci ayırmadan sonra bellek düzeni:
h2 h1
vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
1234 Diğer evim biraz
^ --- + - ^
|
+ - üzerine yazılmıştır
En sık çökmeye neden olan kısım, sakladığınız verilerin gerçekten rastgele değiştirilmesi gerekmeyen önemli kısımlarının üzerine yazmanızdır. Örneğin, h1-house adının bölümlerinin programın çökmesi açısından değiştirilmesi bir sorun olmayabilir, ancak kırık nesneyi kullanmaya çalıştığınızda nesnenin üzerine yazılması büyük olasılıkla çökecektir nesnedeki diğer nesnelere depolanan bağlantıların üzerine yazma.
Bağlantılı listeler
Bir kağıt parçasındaki bir adresi takip ettiğinizde, bir eve gidersiniz ve o evde, zincirdeki bir sonraki ev için, üzerinde yeni bir adres bulunan başka bir kağıt parçası vardır.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
Burada evimizden kabinimize bir bağlantı oluşturuyoruz. Bir evin NextHouse
referansı olmayana kadar zinciri takip edebiliriz , yani sonuncusu. Tüm evlerimizi ziyaret etmek için aşağıdaki kodu kullanabiliriz:
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;
Bellek düzeni (aşağıdaki şemada dört LLLL ile not edilen nesneye bağlantı olarak NextHouse eklenmiştir):
h1 h2
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
Ev + 5678
| ^ |
+ -------- + * (bağlantı yok)
Temel olarak, bellek adresi nedir?
Bellek adresi temel olarak sadece bir sayıdır. Belleği büyük bir bayt dizisi olarak düşünüyorsanız, ilk baytın adresi 0, diğeri adres 1'dir. Bu basitleştirilmiş, ancak yeterince iyi.
Bu bellek düzeni:
h1 h2
vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 Evim 5678 Evim
Bu iki adres olabilir (en soldaki - 0 adresidir):
Bu, yukarıdaki bağlantılı listemizin gerçekte şöyle görünebileceği anlamına gelir:
h1 (= 4) h2 (= 28)
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
Ana Sayfa 0028 5678
| ^ |
+ -------- + * (bağlantı yok)
"Hiçbir yere işaret etmeyen" bir adresi sıfır adres olarak kaydetmek tipiktir.
Temel anlamda, işaretçi nedir?
İşaretçi, bir bellek adresini tutan bir değişkendir. Programlama dilinden size numarasını vermesini isteyebilirsiniz, ancak çoğu programlama dili ve çalışma zamanı, altında bir sayı olduğu gerçeğini gizlemeye çalışır, çünkü sayının kendisi sizin için gerçekten bir anlamı yoktur. İşaretçiyi kara bir kutu olarak düşünmek en iyisidir, yani. tam olarak nasıl uygulandığını bilmiyor veya önemsemiyorsunuz, tıpkı çalıştığı sürece.