İşaretçileri anlamanın önündeki engeller nelerdir ve bunların üstesinden gelmek için neler yapılabilir? [kapalı]


449

İşaretçiler neden C veya C ++ 'da birçok yeni ve hatta eski üniversite öğrencisi için bu kadar önde gelen bir kafa karışıklığı faktörü? İşaretçilerin değişken, işlev ve ötesinde nasıl çalıştığını anlamanıza yardımcı olan herhangi bir araç veya düşünce süreci var mı?

Birilerini genel konsepte saptırmadan "Ah-hah, anladım" seviyesine getirmek için yapılabilecek bazı iyi uygulama şeyleri nelerdir? Temel olarak, senaryolar gibi delin.


20
Bu sorunun tezi, işaretçilerin anlaşılması zor olmasıdır. Soru, işaretçilerin anlaşılması her şeyden daha zor olduğuna dair hiçbir kanıt sunmuyor.
bmargulies

14
Belki bir şey eksik (çünkü ben GCC'd dilde kod) ama her zaman bir Key-> Değer yapısı olarak bellek işaretçiler olmadığını düşündüm. Bir programda büyük miktarda veriyi dolaşmak pahalı olduğu için, yapıyı (değeri) yaratırsınız ve onun işaretçisini / başvurusunu (anahtarını) geçirirsiniz, çünkü anahtar daha büyük yapının çok daha küçük bir temsilidir. Zor kısım, yapı (değer) içerisindeki verilere girmek için daha fazla iş gerektiren iki işaretçi / referansı karşılaştırmanız gerektiğinde (anahtarları veya değerleri karşılaştırıyor musunuz).
Evan Plaice

2
@ Wolfpack'08 "Bana öyle geliyor ki adresteki bir bellek her zaman bir int olacak." - O zaman size hiçbir şeyin bir türü olmadığı anlaşılıyor , çünkü hepsi sadece hafızada bitler. "Aslında, işaretçinin tipi işaretçi noktaları var türüdür" - No, işaretçinin türüdür için işaretçi doğaldır ve açık olmalı ki - işaretçi işaret ettiği var türüne.
Jim Balter

2
Değişkenlerin (ve fonksiyonların) sadece bellek blokları olduğu ve işaretçilerin bellek adreslerini saklayan değişkenler olduğu gerçeğini kavramanın ne kadar zor olduğunu merak ettim. Bu belki çok pratik düşünce modeli soyut kavramların tüm hayranlarını etkilemeyebilir, ancak işaretçilerin nasıl çalıştığını anlamaya mükemmel bir şekilde yardımcı olur.
Christian Rau

8
Özetle, öğrenciler muhtemelen anlamıyorlar, çünkü bir bilgisayarın belleğinin ve özellikle C "bellek modeli" nin nasıl çalıştığını veya hiç çalışmadıklarını anlamıyorlar . Sıfırdan Programlama adlı bu kitap bu konular hakkında çok iyi bir ders vermektedir.
Abbafei

Yanıtlar:


745

İş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ı hiç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, hdeğ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ı h1ve aynı h1zamanda temizlendi, h2hala 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 NextHousereferansı 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):

  • h1 = 4
  • h2 = 23

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.


59
Arabellek taşması çok komik. "Komşu eve gelir, kafatasının önemsizine kaymasını engeller ve sizi unutulmaya mahk sum eder."
gtd

12
Bu kavramın güzel bir açıklaması, elbette. Konsept, işaretçiler hakkında kafa karıştırıcı bulduğum şey değil, bu yüzden bu makalenin tamamı boşa gitti.
Breton

10
Ama ne yapmak istemişlerdir için size önerilerle ilgili kafa karıştırıcı bulmak?
Lasse V. Karlsen

11
Cevabı yazdığınızdan beri bu gönderiyi birkaç kez tekrar ziyaret ettim. Kod ile açıklamanız mükemmel ve daha fazla düşünce eklemek / düzeltmek için tekrar ziyaret etmenizi takdir ediyorum. Bravo Lasse!
David McGraw

3
Tek bir metin sayfasının (ne kadar uzun olursa olsun) bellek yönetimi, referanslar, işaretçiler vb. Her nüansı özetlemesinin bir yolu yoktur. 465 kişi buna oy verdiğinden, yeterince iyi hizmet ettiğini söyleyebilirim başlangıç ​​sayfası. Öğrenecek daha çok şey var mı? Tabii, ne zaman değil?
Lasse V. Karlsen

153

İlk Comp Sci sınıfımda aşağıdaki egzersizi yaptık. Kabul edersek, burası yaklaşık 200 öğrencisi olan bir konferans salonuydu ...

Profesör tahtaya yazar: int john;

John ayağa kalkıyor

Profesör şöyle yazar: int *sally = &john;

Sally ayağa kalkar, John'u işaret eder

Profesör: int *bill = sally;

Bill ayağa kalkar, John'u işaret eder

Profesör: int sam;

Sam ayağa kalkar

Profesör: bill = &sam;

Bill şimdi Sam'i işaret ediyor.

Bence fikri anladın. İşaretçi atama temellerini geçene kadar bunu yapmak için yaklaşık bir saat harcadığımızı düşünüyorum.


5
Yanlış anladığımı sanmıyorum. Niyetim, sivri-değişkenin değerini John'dan Sam'e değiştirmekti. İnsanlarla temsil etmek biraz daha zor, çünkü her iki göstergenin değerini değiştiriyormuşsunuz gibi görünüyor.
Tryke

24
Ama kafa karıştırıcı olmasının nedeni, sanırım John'un koltuğundan kalkması ve sam'in tahmin edebileceğimiz gibi oturması gibi değil. Daha çok sam geldi ve elini john'a yapıştı ve sam programlamasını john'un vücuduna klonladı, tıpkı matristeki hugo dokuma gibi yeniden yüklendi.
Breton

59
Sam, John'un yerini alır ve John, kritik bir şeye çarpıp bir segfault yaratana kadar odanın etrafında yüzer.
just_wes

2
Şahsen bu örneği gereksiz yere karmaşık buluyorum. Profesyonelim bana bir ışığa işaret etmemi ve "eliniz ışık nesnesinin göstergesidir" dedi.
Celeritas

Bu tür örneklerle ilgili sorun, X ve X işaretçilerinin aynı olmamasıdır. Ve bu insanlarla resmedilmiyor.
Isaac Nequittepas

124

İşaretçileri açıklamak için yararlı bulduğum bir benzetme köprüdür. Çoğu kişi, bir web sayfasındaki bir bağlantının internetteki başka bir sayfayı 'işaret ettiğini' anlayabilir ve bu köprüyü kopyalayıp yapıştırabilirseniz, her ikisi de aynı orijinal web sayfasını gösterecektir. Bu orijinal sayfayı gidip düzenlerseniz, bu bağlantılardan birini (işaretçiler) takip ederek yeni güncellenen sayfayı alırsınız.


15
Bunu gerçekten beğendim. Köprüyü iki kez yazmanın iki web sitesinin görünmesini sağladığını görmek zor değildir (tıpkı int *a = biki kopyasını yapmaz gibi *b).
detly

4
Bu aslında çok sezgisel ve herkesin ilişkilendirebilmesi gereken bir şey. Bu benzetmenin parçalandığı birçok senaryo olmasına rağmen. Ancak hızlı bir giriş için harika. +1
Brian Wigginton

İki kez açılan bir sayfaya bağlantı genellikle bu web sayfasının neredeyse tamamen bağımsız iki örneğini oluşturur. Bence bir köprü belki bir kurucu için iyi bir benzetme olabilir, ama bir işaretçi değil.
Utkan Gezer

@ThoAppelsin Örneğin, statik bir html web sayfasına erişiyorsanız, sunucudaki tek bir dosyaya erişiyor olmanız gerekmez.
dramatik

5
Çok düşünüyorsun. Köprüler sunucudaki dosyalara işaret eder, bu benzetmenin kapsamıdır.
15'da dramatik

48

İşaretçilerin bu kadar çok insanı şaşırtmasının nedeni, bilgisayar mimarisinde çoğunlukla çok az veya hiç arka plana sahip olmamalarıdır. Birçoğu bilgisayarların (makinenin) gerçekte nasıl uygulandığına dair bir fikre sahip olmadığı için C / C ++ ile çalışmak yabancı görünüyor.

Matkap, işaretçi işlemlerine (yükleme, depolama, doğrudan / dolaylı adresleme) odaklanan bir talimat seti ile basit bir bayt kodu tabanlı sanal makine (seçtikleri herhangi bir dilde, python bunun için harika çalışır) uygulamalarını istemektir. O zaman bu komut seti için basit programlar yazmalarını isteyin.

Basit eklemeden biraz daha fazlasını gerektiren her şey işaretçileri içerecektir ve bunu alacağından emin olabilirler.


2
İlginç. Buna rağmen nasıl başlayacağınız hakkında bir fikrim yok. Paylaşacak kaynak var mı?
Karolis

1
Katılıyorum. Örneğin, C'den önce derleme programlamayı öğrendim ve kayıtların nasıl çalıştığını bilerek, öğrenme işaretçileri kolaydı. Aslında, çok fazla öğrenme yoktu, hepsi çok doğal geldi.
Milan Babuškov

Temel bir CPU alın, çim biçme makineleri veya bulaşık makineleri çalıştıran bir şey söyleyin ve uygulayın. Veya çok temel bir ARM veya MIPS alt kümesi. Her ikisinin de çok basit bir ISA'sı var.
Daniel Goldberg

1
Bu eğitim yaklaşımının Donald Knuth'un kendisi tarafından savunduğunu / uygulandığını belirtmekte fayda vardır. Knuth'un Bilgisayar Programlama Sanatı, basit bir varsayımsal mimariyi tanımlar ve öğrencilerden, bu mimariye yönelik varsayımsal bir montaj dilinde problemleri çözmek için çözümler uygulamalarını ister. Pratik olarak uygulanabilir hale geldiğinde, Knuth'un kitaplarını okuyan bazı öğrenciler mimarisini aslında bir VM olarak uygular (veya mevcut bir uygulamayı kullanır) ve aslında çözümlerini çalıştırırlar. IMO, zamanınız varsa öğrenmek için harika bir yoldur.
WeirdlyCheezy

4
@ Luke İşaretçileri kavrayamayan (ya da daha kesin olarak dolaylı olarak dolaylı) anlamanın bu kadar kolay olduğunu sanmıyorum. Temelde, C'deki işaretçileri anlamayan insanların, derlemeyi öğrenmeye başlayabileceklerini, bilgisayarın temel mimarisini anlayabileceklerini ve işaretçileri anlayarak C'ye dönebileceklerini varsayıyorsunuz. Bu birçok kişi için doğru olabilir, ancak bazı çalışmalara göre, bazı insanlar prensipte bile dolaylı olarak dolaylı olarak kavrayamıyor gibi görünüyor (buna hala inanmak çok zor, ama belki de "öğrencilerimle şanslıydım ").
Luaan

27

C / C ++ dilinde birçok yeni ve hatta eski, üniversite düzeyindeki öğrenciler için neden bu kadar önde gelen bir kafa karışıklığı faktörü?

Değer - değişkenler - için yer tutucu kavramı, okulda öğretilen bir şeyle eşleşir - cebir. Belleğin bir bilgisayar içinde fiziksel olarak nasıl düzenlendiğini anlamadan çizebileceğiniz mevcut bir paralel yoktur ve kimse C / C ++ / bayt iletişim düzeyinde düşük seviyeli şeylerle uğraşana kadar bu tür bir şey hakkında düşünmez. .

İşaretçilerin değişken, işlev ve ötesinde nasıl çalıştığını anlamanıza yardımcı olan herhangi bir araç veya düşünce süreci var mı?

Adres kutuları. BASIC'i mikrobilgisayarlara programlamayı öğrendiğimde, içinde oyunların bulunduğu bu güzel kitaplar olduğunu ve bazen değerleri belirli adreslere sokmak zorunda kaldığınızı hatırlıyorum. 0, 1, 2 ile kademeli olarak etiketlenmiş bir grup kutunun resmi vardı ve bu kutulara sadece küçük bir şeyin (bayt) sığabileceği açıklandı ve birçoğu vardı - bazı bilgisayarlar 65535 kadar vardı! Birbirlerinin yanındaydılar ve hepsinin bir adresi vardı.

Birilerini genel konsepte saptırmadan "Ah-hah, anladım" seviyesine getirmek için yapılabilecek bazı iyi uygulama şeyleri nelerdir? Temel olarak, senaryolar gibi delin.

Tatbikat için mi? Bir yapı yapın:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

Yukarıdaki örnekle aynı, C hariç:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

Çıktı:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

Belki de bu bazı temel örnekleri örnek olarak açıklar.


+1 "hafızanın fiziksel olarak nasıl yerleştirildiğini anlamadan". Bir montaj dili geçmişinden C'ye geldim ve işaretçiler kavramı çok doğal ve kolaydı; ve sadece daha yüksek seviyede bir dil geçmişine sahip insanların bunu anlamak için mücadele ettiğini gördüm. Sözdizimini daha da kötüleştirmek kafa karıştırıcıdır (işlev işaretçileri!), Bu nedenle kavramı ve sözdizimini aynı anda öğrenmek sorun için bir reçetedir.
Brendan

4
Bu eski bir yazı biliyorum, ancak sağlanan kod çıktısı yazıya eklenirse harika olurdu.
Josh

Evet, cebire benzer (cebirlerin "değişkenleri" değişmezken ekstra anlaşılır bir noktaya sahip olmasına rağmen). Fakat tanıdığım insanların yaklaşık yarısının cebirde pratikte bir kavrayışı yoktur. Sadece onlar için hesaplanmaz. Sonuca ulaşmak için tüm bu "denklemleri" ve reçeteleri biliyorlar, ancak bunları rastgele ve beceriksizce uyguluyorlar. Ve onları kendi amaçları için genişletemezler - bu sadece onlar için değiştirilemez, birleştirilemez bir kara kutu. Cebiri anlarsanız ve onu etkili bir şekilde kullanabiliyorsanız, programcılar arasında bile paketin çok ötesindesiniz.
Luaan

24

İlk başta, işaretçileri anlamakta zorlanmamın nedeni, birçok açıklamanın referans ile geçme konusunda çok fazla saçmalık içermesidir. Bütün bunlar sorunu karıştırıyor. Bir pointer parametresi kullandığınızda, yine de değere göre geçiyorsunuz; ancak değer bir int yerine bir adres olur.

Birisi bu eğiticiye zaten bağlanmış, ancak işaretçileri anlamaya başladığım anı vurgulayabilirim:

C'de İşaretçiler ve Diziler Üzerine Bir Eğitim: Bölüm 3 - İşaretçiler ve Dizeler

int puts(const char *s);

Şu an için, const.iletilen parametre puts()bir işaretçi, yani bir işaretçinin değeri (C'deki tüm parametreler değere göre geçtiği için) ve bir işaretçinin değeri, işaret ettiği adres veya , bir adres. Böylece puts(strA);gördüğümüz gibi yazdığımızda strA [0] adresini geçiyoruz.

Bu kelimeleri okuduğum anda bulutlar ayrıldı ve bir güneş ışığı beni işaretçi anlayışıyla kuşattı.

Bir VB .NET veya C # geliştiricisi olsanız bile (benim gibi) ve asla güvenli olmayan kod kullanmasanız bile, işaretçilerin nasıl çalıştığını anlamaya değer veya nesne referanslarının nasıl çalıştığını anlamayacaksınız. Ardından, bir yönteme nesne referansı iletmenin nesneyi kopyaladığı ile ilgili ortak ama yanlış bir fikre sahip olacaksınız.


İşaretçiye sahip olmanın ne anlama geldiğini merak ediyorsun. Karşılaştığım çoğu kod bloğunda, bir işaretçi yalnızca bildiriminde görülür.
Wolfpack'08

@ Wolfpack'08 ... ne ?? Hangi koda bakıyorsunuz ??
Kyle Strand

@KyleStrand Bir bakayım.
Wolfpack'08

19

Ted Jensen'ın "C'deki İşaretçiler ve Diziler Eğitimi" ni işaretçiler hakkında bilgi edinmek için mükemmel bir kaynak olarak buldum. İşaretçilerin ne olduğunu (ve ne için olduklarını) açıklayan ve işlev işaretçileriyle biten 10 derse ayrılmıştır. http://home.netcom.com/~tjensen/ptr/cpoint.htm

Buradan hareketle, Beej'in Ağ Programlama Kılavuzu, gerçekten eğlenceli şeyler yapmaya başlayabileceğiniz Unix soketleri API'sını öğretir. http://beej.us/guide/bgnet/


1
Ted Jensen dersini ikinci olarak verdim. İşaretçileri bir ayrıntı düzeyine ayırır, bu aşırı ayrıntılı değildir, okuduğum hiçbir kitap yok. Son derece yardımcı! :)
Dave Gallagher

12

İşaretçilerin karmaşıklığı, kolayca öğretebileceğimiz şeyin ötesine geçer. Öğrencilerin birbirlerini göstermesi ve ev adresleriyle kağıt parçaları kullanması hem harika bir öğrenme aracıdır. Temel kavramları tanıtmak için harika bir iş çıkarıyorlar. Gerçekten de, temel kavramları öğrenmek işaretçileri başarılı bir şekilde kullanmak için çok önemlidir . Bununla birlikte, üretim kodunda, bu basit gösterilerin kapsadığından çok daha karmaşık senaryolara girmek yaygındır.

Diğer yapılara işaret eden diğer yapılara işaret eden yapılara sahip olduğumuz sistemlere katıldım. Bu yapıların bazıları ayrıca (ek yapılara işaretçiler yerine) gömülü yapılar içeriyordu. Bu, işaretçilerin gerçekten kafa karıştırıcı olduğu yerdir. Birden fazla dolaylı düzeyiniz varsa ve bunun gibi bir kodla başlamaya başlarsanız:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

gerçekten hızlı bir şekilde kafa karıştırıcı olabilir (çok daha fazla satır ve potansiyel olarak daha fazla seviye hayal edin). İşaretçi dizileri ve düğüm işaretleyicilerine (ağaçlar, bağlantılı listeler) düğüm atın ve daha da kötüleşir. Bazı gerçekten iyi geliştiricilerin bu tür sistemler üzerinde çalışmaya başladıklarında, hatta temelleri gerçekten iyi anlayan geliştiriciler bile kaybolduğunu gördüm.

İşaretçilerin karmaşık yapıları da kötü kodlamayı göstermez (olabilirler). Kompozisyon, iyi bir nesne yönelimli programlamanın hayati bir parçasıdır ve ham işaretçileri olan dillerde, kaçınılmaz olarak çok katmanlı dolaylamaya yol açacaktır. Ayrıca, sistemlerin genellikle üçüncü taraf kitaplıklarını stil veya teknik olarak birbiriyle eşleşmeyen yapılarla kullanması gerekir. Bu gibi durumlarda, karmaşıklık doğal olarak ortaya çıkacaktır (kesinlikle, mümkün olduğu kadar mücadele etmeliyiz).

Üniversitelerin öğrencilerin işaretçileri öğrenmelerine yardımcı olmak için yapabileceği en iyi şey, işaretçi kullanımı gerektiren projelerle birlikte iyi gösteriler kullanmaktır. Zor bir proje, işaretçiyi anlamak için bin gösteriden daha fazlasını yapacaktır. Gösteriler size sığ bir anlayış kazandırabilir, ancak işaretçileri derinlemesine kavramak için bunları gerçekten kullanmanız gerekir.


10

Bu listeye bir bilgisayar bilimi öğretmeni olarak işaretçileri (gün içinde) açıklarken çok yararlı bulduğum bir analoji ekleyeceğimi düşündüm; ilk olarak:


Sahneyi ayarlayın :

3 boşluklu bir otoparkı düşünün, bu alanlar sayılı:

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

Bir bakıma, bu bellek konumları gibidir, sıralı ve bitişiktir .. bir diziye benzer. Şu anda aralarında araba yok, bu yüzden boş bir dizi ( parking_lot[3] = {0}) gibi.


Verileri ekleyin

Bir otopark asla uzun süre boş kalmaz ... eğer yapsaydı anlamsız olurdu ve hiç kimse inşa etmeyecekti. Diyelim ki gün hareket halindeyken 3 araba, mavi bir araba, kırmızı bir araba ve yeşil bir araba ile dolar:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

Bu arabalar Bütün bunları düşünürken bir yolu bizim arabalar verilerin çeşit (bir demek olmasıdır, böylece aynı tip (araba) vardır int) ama onlar farklı değerlere sahip ( blue, red, green; bir renk olabilir enum)


İşaretçiyi girin

Şimdi sizi bu otoparka götürürsem ve bana mavi bir araba bulmanızı istersem, bir parmağınızı uzatırsınız ve nokta 1'deki mavi bir arabayı işaret etmek için kullanırsınız. Bu bir işaretçi alıp bir bellek adresine atamak gibidir. ( int *finger = parking_lot)

Parmağınız (işaretçi) sorumun cevabı değil. Looking at parmağınızla bana bir şey söylemiyor, ama sen parmak nerede bakarsak işaret (işaretçiyi kaldırma), ben arıyordum araba (veri) bulabilirsiniz.


İşaretçiyi yeniden atama

Şimdi sizden kırmızı bir araba bulmanızı isteyebilirim ve parmağınızı yeni bir arabaya yönlendirebilirsiniz. İşaretçiniz (önceki ile aynı) bana aynı türden (araba) yeni veriler (kırmızı arabanın bulunduğu park yeri) gösteriyor.

İşaretçi fiziksel hala var, değişmedi senin parmak, benim değişti gösteriyordu sadece veri. ("park yeri" adresi)


Çift işaretçiler (veya işaretçiye bir işaretçi)

Bu, birden fazla işaretçiyle de çalışır. Kırmızı arabaya işaret eden işaretçinin nerede olduğunu sorabilirim ve diğer elinizi kullanabilir ve ilk parmağınızı parmağınızla işaret edebilirsiniz. (bu gibi int **finger_two = &finger)

Şimdi mavi arabanın nerede olduğunu bilmek istersem, ilk parmağın yönünü ikinci parmağa, araca (veri) doğru takip edebilirim.


Sarkan ibre

Şimdi diyelim ki bir heykel gibi hissediyorsunuz ve elinizi kırmızı arabaya süresiz olarak işaret etmek istiyorsunuz. Ya o kırmızı araba uzaklaşırsa?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

İşaretçiniz hala kırmızı araba nereye işaret ediyor idi ama artık yok. Diyelim ki yeni bir araba içeri giriyor ... Turuncu bir araba. Şimdi sana tekrar "kırmızı araba nerede" diye sorarsam, hala oraya işaret ediyorsun ama şimdi yanılıyorsun. Bu kırmızı bir araba değil, turuncu.


İşaretçi aritmetiği

Tamam, hala ikinci park yerini gösteriyorsunuz (şimdi Turuncu araba tarafından işgal ediliyor)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

Şimdi yeni bir sorum var ... Bir sonraki park yerinde arabanın rengini bilmek istiyorum . 2. noktayı işaret ettiğinizi görebilirsiniz, böylece sadece 1 eklersiniz ve bir sonraki noktayı işaret ediyorsunuz. ( finger+1), şimdi verilerin ne olduğunu bilmek istediğimden, o noktayı (sadece parmağınızı değil) kontrol etmelisiniz, böylece *(finger+1)orada yeşil bir araba olduğunu (o konumdaki veriler görmek için işaretçiyi ( ) )


Sadece "çift ibre" kelimesini kullanmayın. İşaretçiler herhangi bir şeye işaret edebilir, bu yüzden açıkça diğer işaretçileri işaret eden işaretçileriniz olabilir. Bunlar çift gösterici değildir.
gnasher729

Bence bu “parmakların” kendi benzetmesini devam ettirmek, her biri “bir park yeri işgal etmek” için özlüyor. İnsanların analojinizin soyutlamasının üst düzeyindeki işaretçileri anlamakta zorluk çektiğinden emin değilim, işaretçilerin bellek konumlarını işgal eden değişebilir şeyler olduğunu ve bunun ne kadar yararlı olduğunu, insanların kaçtığı anlaşılıyor.
Emmet

1
@Emmet - WRT işaretleyicilerine gidebilecek çok daha fazla şey olduğu konusunda hemfikir değilim, ama şu soruyu okuyorum: "without getting them bogged down in the overall concept"üst düzey bir anlayış olarak. Ve noktasına: "I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"- Çok kaç kişi için sürpriz olmayacağını yok bile bu seviyeye işaretçileri anlama
Mike

Araba-parmak benzetmesini bir kişiye (bir veya daha fazla parmakla - ve bunların her birinin herhangi bir yöne işaret etmesine izin verebilecek genetik bir anormallik!) Genişletmenin başka bir arabaya işaret eden araçlardan birinde (veya eğimin yanındaki boşluğa bir "başlatılmamış işaretçi" olarak işaret ettiğinde eğildi; ya da bütün bir el "sabit boyutlu [5] işaretçi dizisi" olarak bir sıra boşluğa işaret ederek ya da avuç içi "boş işaretçi" olarak kıvrıldı ASLA araba olmadığı bilinen bir yere işaret ediyor) ... 8-)
SlySven

açıklamanız takdir edilebilir ve yeni başlayanlar için iyi bir açıklama.
Yatendra Rathore

9

Bir kavram olarak işaretçilerin özellikle zor olduğunu düşünmüyorum - çoğu öğrencinin zihinsel modelleri böyle bir şeyle eşleşiyor ve bazı hızlı kutu çizimleri yardımcı olabilir.

En azından geçmişte yaşadığım ve başkalarının uğraştığını gördüğüm zorluk, C / C ++ 'da işaretçilerin yönetiminin gereksiz yere kıvrılmış olabilmesidir.


9

İyi bir diyagram kümesine sahip bir öğretici örneği, işaretçilerin anlaşılmasına büyük ölçüde yardımcı olur .

Joel Spolsky, Gerilla Röportaj Rehberinde işaretçileri anlama konusunda bazı iyi noktalara değiniyor :

Nedense çoğu insan beynin işaretçileri anlayan bir parçası olmadan doğmuş gibi görünmektedir. Bu bir yetenek şeyidir, yetenekli bir şey değildir - bazı insanların yapamayacağı karmaşık iki kat dolaylı düşünme biçimini gerektirir.


8

İşaretçilerle ilgili sorun kavram değildir. Bu, icra ve dahil olan dildir. Öğretmenler, jargonun veya kavisli karışıklık C ve C ++ kavramının değil, zor olan işaretçilerin KAVRAMI olduğunu varsaydığında ek karışıklık sonuçları ortaya çıkar. Bu nedenle, kavramı açıklamak için büyük miktarda çaba sarf ediliyor (bu soru için kabul edilen cevapta olduğu gibi) ve bu benim gibi biri için neredeyse boşa harcanıyor, çünkü zaten hepsini anlıyorum. Sadece sorunun yanlış kısmını açıklıyor.

Size nereden geldiğim hakkında bir fikir vermek için, işaretçileri mükemmel bir şekilde anlayan biriyim ve bunları montajcı dilinde yetkin bir şekilde kullanabilirim. Çünkü montajcı dilinde işaretçi olarak adlandırılmazlar. Adres olarak adlandırılırlar. C'de işaretçiler programlama ve kullanma söz konusu olduğunda, çok fazla hata yapıyorum ve gerçekten kafam karışıyor. Bunu hala çözemedim. Sana bir örnek vereyim.

Bir API derken:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

ne istiyor

isteyebilir:

bir arabellek adresini temsil eden bir sayı

(Bunu vermek için mi söylüyorum doIt(mybuffer), yoksa doIt(*myBuffer)?)

arabelleğe giden bir adrese adresi temsil eden bir sayı

(bu doIt(&mybuffer)veya doIt(mybuffer)veya doIt(*mybuffer)?)

arabellek adresine adresin adresini temsil eden bir sayı

(belki de bu doIt(&mybuffer). ya da öyle doIt(&&mybuffer)mi? hatta hatta doIt(&&&mybuffer))

ve bu şekilde devam eder ve söz konusu dil bunu netleştirmez, çünkü "x" y'nin adresini tutar "ve" bu işlev y "için bir adres gerektirir. Cevap ek olarak sadece "mybuffer" ın ne ile başlayacağına ve onunla ne yapmak istediğine bağlıdır. Dil, pratikte karşılaşılan yuvalama düzeylerini desteklemez. Yeni bir arabellek oluşturan bir işleve bir "işaretçi" vermek zorunda olduğumda ve arabellek yeni konumunu işaret etmek için işaretçiyi değiştirdiğinde olduğu gibi. İşaretçiyi veya işaretçiye bir işaretçi gerçekten istiyor mu, bu yüzden işaretçinin içeriğini değiştirmek için nereye gideceğini bilir. Çoğu zaman sadece "

"Pointer" çok fazla yüklenmiş. İşaretçi bir değerin adresi midir? veya bir adresi bir değere tutan bir değişkendir. Bir işlev işaretçi istediğinde, işaretçi değişkeninin sahip olduğu adresi ister mi, adresi işaretçi değişkenine ister mi? Kafam karıştı.


Ben şöyle açıkladı gördüm: Eğer gibi bir işaretçi bildirimi görürseniz double *(*(*fn)(int))(char), o zaman değerlendirme sonucu *(*(*fn)(42))('x')bir olacaktır double. Ara türlerin ne olması gerektiğini anlamak için değerlendirme katmanlarını çıkarabilirsiniz.
Bernd Jendrissek

@BerndJendrissek Takip ettiğimden emin değilim. O (*(*fn)(42))('x') zaman değerlendirmenin sonucu nedir ?
Breton

xdeğerlendirirseniz *x, iki katına çıkacağınız bir şey alırsınız (diyelim ) .
Bernd Jendrissek

@BerndJendrissek Bunun işaretçilerle ilgili bir şeyi açıklaması gerekiyor mu? Anlamıyorum. Ne demek istiyorsun? Bir katmanı sıyırdım ve herhangi bir ara tip hakkında yeni bir bilgi kazanmadım. Belirli bir fonksiyonun neyi kabul edeceği hakkında ne açıklıyor? Bir şeyle ne ilgisi var?
Breton

Belki bu açıklama mesajı (ve o benim ben bulmak isterdim, değil nerede ben ilk testere) daha az ne açısından düşünmek olduğunu fn olduğunu ve daha sen ne açısından yapılacak olanfn
Bernd Jendrissek

8

Bence işaretçileri anlamanın önündeki temel engel kötü öğretmenler.

Hemen hemen herkese işaretçiler hakkında yalanlar öğretiliyor: Bellek adreslerinden başka bir şey olmadıkları veya keyfi konumlara işaret etmenize izin verdikleri .

Ve elbette anlaşılması zor, tehlikeli ve yarı büyülü.

Bunların hiçbiri doğru değil. C ++ dilinin onlar hakkında söylediklerine sadık kaldığınızda ve bunları "genellikle" pratikte işe yarayan, ancak yine de dil tarafından garanti edilmeyen niteliklerle doldurmadığınız sürece , işaretçiler aslında oldukça basit kavramlardır. ve gerçek işaretçi kavramının bir parçası değildir.

Birkaç ay önce bu blog yazısında bunun bir açıklamasını yazmaya çalıştım - umarım birisine yardımcı olur.

(Not, kimse bana bilgiçlik getirmeden önce, evet, C ++ standardı işaretçilerin bellek adreslerini temsil ettiğini söyler . Ancak "işaretçiler bellek adresleri değildir ve bellek adreslerinden başka bir şey değildir ve bellek ile birbirinin yerine kullanılabilir veya düşünülebilir adresleri ".


Sonuçta, boş bir işaretçi C "değeri" sıfır olmasına rağmen bellekte sıfır adresini göstermez. Bu tamamen ayrı bir kavram ve eğer yanlış bir şekilde uğraşırsanız, sonuçta beklemediğiniz bir şeyi ele alabilir (ve kayıttan çıkarmayı). Bazı durumlarda, bu bellekte sıfır adres bile olabilir (özellikle şimdi adres alanı genellikle düzdür), ancak diğerlerinde, optimize edici bir derleyici tarafından tanımlanmamış davranış olarak göz ardı edilebilir veya belleğin ilişkilendirilmiş başka bir bölümüne erişilebilir. işaretçi türü için "sıfır" ile. Hilarity ortaya çıkar.
Luaan

Şart değil. İşaretçilerin anlamlı olması (ve diğer programlarda da hata ayıklaması) için bilgisayarı kafanızda modelleyebilmeniz gerekir. Herkes bunu yapamaz.
Thorbjørn Ravn Andersen

5

İşaretçileri öğrenmeyi zorlaştıran şeyin, işaretçiler olana kadar "bu bellek konumunda bir int, bir çift, bir karakter, ne olursa olsun" temsil eden bir dizi bit olduğu fikrinden memnun olduğunuzu düşünüyorum.

Bir işaretçiyi ilk gördüğünüzde, o bellek konumunda ne olduğunu anlamıyorsunuz. "Ne demek istiyorsun, bir adresi var mı?"

"Ya onları alırsın ya da almazsın" fikrine katılmıyorum.

Onlar için gerçek kullanımlar bulmaya başladığınızda anlaşılması kolaylaşır (büyük yapıları işlevlere geçirmemek gibi).


5

Anlamanın bu kadar zor olmasının nedeni zor bir kavram değil , sözdiziminin tutarsız olması .

   int *mypointer;

İlk olarak, bir değişken oluşturma işleminin en sol kısmının değişkenin türünü tanımladığı öğrenilir. İşaretçi bildirimi C ve C ++ 'da böyle çalışmaz. Bunun yerine değişkenin soldaki türe işaret ettiğini söylüyorlar. Bu durumda: *mypointer bir int işaret ediyor.

Ben C # (güvenli olmayan) kullanarak denedim kadar işaretçiler tamamen kavramadım, onlar aynı şekilde ama mantıklı ve tutarlı sözdizimi ile çalışır. İşaretçi bir türün kendisidir. İşte mypointer olan bir int işaretçi.

  int* mypointer;

Beni fonksiyon göstergelerine bile sokma ...


2
Aslında, her iki parçanız da C geçerlidir. Birincisinin daha yaygın olması çok yıl süren C stili meselesidir. İkincisi, C ++ 'da biraz daha yaygındır.
RBerteig

1
İkinci parça, daha karmaşık beyanlarda pek işe yaramaz. İşaretçi bildiriminin sağ tarafının, işaretçiyi soldaki atomik tip belirteci olan bir şey elde etmek için işaretçiye ne yapmanız gerektiğini gösterdiğini anladıktan sonra sözdizimi "tutarsız" değildir.
Bernd Jendrissek

2
int *p;basit bir anlamı vardır: *pbir tamsayıdır. int *p, **ppşu anlama gelir: *pve **pptamsayıdır.
Miles Rout

@MilesRout: Ama sorun tam olarak bu. *pve **ppvardır değil sen başlatıldı çünkü hiçbir zaman, tam sayılar pya da ppya da *ppbir şey noktaya kadar. Bazı insanların neden bu dilbilgisi ile uğraşmayı tercih ettiğini anlıyorum, özellikle bazı kenar vakalar ve karmaşık vakalar bunu yapmanızı gerektirdiğinden (yine de, farkında olduğum tüm durumlarda önemsiz bir şekilde çalışabilirsiniz) ... ancak bu davaların doğru hizalanmayı öğretmenin yeni başlayanlar için yanıltıcı olmasından daha önemli olduğunu düşünmüyorum. Çirkin bir şeyden bahsetmiyorum bile! :)
Orbit'te Hafiflik Yarışları

@LightnessRacesinOrbit Sağa hizalamayı öğretmek yanıltıcı olmaktan çok uzaktır. Bunu öğretmenin tek doğru yolu budur. DEĞİL öğretmek yanıltıcıdır.
Miles Rout

5

Sadece C ++ bildiğimde işaretçilerle çalışabilirdim. Bazı durumlarda ne yapacağımı ve deneme yanılma yönteminden ne yapılamayacağını biliyordum. Ama bana tam anlamayı sağlayan şey montaj dilidir. Yazdığınız bir montaj dili programı ile bazı ciddi talimat düzeyinde hata ayıklama yaparsanız, birçok şeyi anlayabilmeniz gerekir.


4

Ev adresi benzetmesini seviyorum, ancak her zaman adresin posta kutusunun kendisine ait olduğunu düşündüm. Bu şekilde, işaretçiyi silme (posta kutusunu açma) kavramını görselleştirebilirsiniz.

Örneğin, bağlantılı bir listeyi takip etmek için: 1) adresinizle kağıdınız

Doğrusal bağlantılı listede, son posta kutusunun içinde hiçbir şey yoktur (listenin sonu). Dairesel bağlı bir listede, son posta kutusunun içindeki ilk posta kutusunun adresi vardır.

3. adımın, başvurunun kaldırıldığı ve adres geçersiz olduğunda kilitleneceğiniz veya yanlış olacağınız yer olduğunu unutmayın. Geçersiz bir adresin posta kutusuna gidebileceğinizi varsayarsak, dünyayı tersine çeviren bir kara delik veya başka bir şey olduğunu hayal edin :)


Posta kutusu numarası benzetmesi ile ilgili kötü bir sorun, Dennis Ritchie tarafından icat edilen dil, baytların adresleri ve bu baytlarda depolanan değerler açısından davranışı tanımlarken, C Standardı tarafından tanımlanan dilin bir davranışı kullanmak için uygulamaları optimize etmeye davet etmesi daha karmaşık ancak modelin çeşitli yönlerini belirsiz, çelişkili ve eksik biçimlerde tanımlar.
supercat

3

İnsanların bununla ilgili sorunlarının temel sebebinin, genellikle ilginç ve ilgi çekici bir şekilde öğretilmemesidir. Bir konuşmacının kalabalıktan 10 gönüllü aldığını ve her birine 1 metre cetvel verdiklerini, belli bir konfigürasyonda durmalarını ve cetvelleri birbirlerine işaret ettiklerini görmek istiyorum. Ardından, insanları hareket ettirerek (ve yöneticilerini nereye yönlendirdiklerini) işaretçi aritmetiğini gösterin. Mekaniğe çok fazla sapmadan kavramları göstermenin basit ama etkili (ve her şeyden önce unutulmaz) bir yolu olurdu.

Bir kez C ve C ++ olsun bazı insanlar için zorlaşıyor gibi görünüyor. Bunun nihayetinde doğru bir şekilde pratik yapmadıkları teorisini koydukları için mi yoksa işaretçi manipülasyonunun bu dillerde doğal olarak daha zor olduğundan mı emin değilim. Kendi geçişimi o kadar iyi hatırlayamıyorum, ama Pascal'daki işaretçileri biliyordum ve sonra C'ye geçip tamamen kayboldum.


2

İşaretçilerin kendilerinin kafa karıştırıcı olduğunu düşünmüyorum. Çoğu insan kavramı anlayabilir. Şimdi kaç tane işaretçi hakkında düşünebiliyorsunuz ya da kaç seviyeli dolaylı indirme konusunda rahatsınız. İnsanları kenara koymak çok fazla zaman almaz. Programınızdaki hatalar tarafından yanlışlıkla değiştirilebilmeleri, kodunuzda bir şeyler ters gittiğinde hata ayıklamalarını çok zorlaştırabilir.


2

Aslında sözdizimi sorunu olabileceğini düşünüyorum. İşaretçiler için C / C ++ sözdizimi, tutarsız ve olması gerekenden daha karmaşık görünüyor.

İronik olarak, gerçekten işaretçileri anlamama yardımcı olan şey, c ++ Standart Şablon Kütüphanesi'nde bir yineleyici kavramıyla karşılaşmaktı . Bu ironik çünkü yineleyicilerin yalnızca işaretçinin bir genellemesi olarak tasarlandığını varsayabilirim.

Bazen ağaçları görmezden gelmeyi öğrenene kadar ormanı göremezsiniz.


Sorun çoğunlukla C beyanı sözdizimindedir. Ama işaretçi kullanımı olsaydı daha kolay (*p)olurdu (p->)ve bu yüzden p->->xbelirsiz yerine olurdu*p->x
MSalters

@MSalters Aman tanrım şaka yapıyorsun, değil mi? Orada herhangi bir tutarsızlık yok. a->bbasitçe (*a).b.
Miles Rout

@Miles: Gerçekten de, ve bu mantıkla * p->xaracı * ((*a).b)ise *p -> xvasıtası (*(*p)) -> x. Önek ve düzeltme sonrası işleçlerinin karıştırılması belirsiz ayrıştırma işlemine neden olur.
MSalters

@ MSalters hayır, çünkü boşluk önemsizdir. Bu 1+2 * 39 olması gerektiğini söylemek gibi .
Miles Rout

2

Karışıklık, "işaretçi" konseptinde birbirine karıştırılmış çoklu soyutlama katmanlarından gelir. Programcılar Java / Python'daki sıradan referanslarla karıştırılmazlar, ancak işaretçiler temeldeki bellek mimarisinin özelliklerini ortaya koymaları bakımından farklıdır.

Soyutlama katmanlarını temiz bir şekilde ayırmak iyi bir prensiptir ve işaretçiler bunu yapmaz.


1
İlginç olan şey, C işaretçilerin aslında altta yatan bellek mimarisinin özelliklerini göstermemesi. Java referansları ve C işaretçileri arasındaki tek fark, işaretçileri içeren karmaşık türlere sahip olabilmenizdir (örn. İnt *** veya char * ( ) (void * )), dizileri ve üyeleri yapılandırmak için işaretçiler için aritmetik, varlığı boşluk * ve dizi / işaretçi ikiliği. Bunun dışında da aynı şekilde çalışıyorlar.
17'de jpalecek

İyi bir nokta. İşaretçi aritmetiği ve o anda ilgili bellek alanından koparak soyutlamadan kurtulma tampon taşmaları olasılığıdır.
Joshua Fox

@jpalecek: İşaretçilerin davranışlarını temel alınan mimari açısından belgeleyen uygulamalarda nasıl çalıştığını anlamak oldukça kolaydır. Söylemek foo[i], belirli bir noktaya gitmek, belirli bir mesafeyi ileriye taşımak ve orada ne olduğunu görmek anlamına gelir. İşleri zorlaştıran şey, Standart tarafından yalnızca derleyicinin yararına eklenen çok daha karmaşık ekstra soyutlama katmanıdır, ancak şeyleri programcı ihtiyaçları ve derleyici ihtiyaçları için kötü bir şekilde modellemektedir.
supercat

2

Bunu açıklamaktan hoşlandığım yol, diziler ve dizinler cinsindeydi - insanlar işaretçileri bilmiyor olabilirler, ancak genellikle bir dizinin ne olduğunu biliyorlar.

Bu yüzden RAM'in bir dizi olduğunu düşünün (ve sadece 10 bayt RAM'e sahipsiniz):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

Sonra bir değişkene işaretçi gerçekten de RAM'deki bu değişkenin (ilk bayt) indeksidir.

Dolayısıyla, bir işaretçi / dizininiz unsigned char index = 2varsa, değer açıkça üçüncü öğedir veya 4 sayısıdır. Bir işaretçiye işaretçi, bu sayıyı alıp dizinin kendisi gibi kullandığınız yerdir RAM[RAM[index]].

Bir kağıt listesine bir dizi çizmek ve sadece aynı bellek, işaretçi aritmetik, işaretçi işaretçi, vb işaret eden birçok işaretçi gibi şeyler göstermek için kullanın.


1

Postane kutu numarası.

Başka bir şeye erişmenizi sağlayan bir bilgi parçasıdır.

(Ve postane kutusu numaralarında aritmetik yaparsanız, bir sorun olabilir, çünkü mektup yanlış kutuya girer. Ve birisi başka bir duruma taşınırsa - yönlendirme adresi olmadan - o zaman sarkan bir işaretçiniz vardır. Öte yandan - postane postayı iletirse, bir işaretçiye bir işaretçiniz vardır.)


1

Yineleyiciler aracılığıyla onu kavramanın kötü bir yolu değil ... ama aramaya devam et Alexandrescu'nun bunlardan şikayet etmeye başladığını göreceksin.

Birçok eski C ++ geliştiricisi (yineleyicilerin dili dökmeden önce modern bir işaretçi olduğunu asla anlamadı) C # 'a atlar ve hala iyi yineleyicilere sahip olduklarına inanırlar.

Hmm, sorun şu ki, yineleyiciler tüm çalışma zamanı platformlarının (Java / CLR) elde etmeye çalıştığı şeylerle tam orantılıdır: yeni, basit, herkes-a-dev kullanımı. Bu iyi olabilir, ama mor kitapta bir kez söylediler ve C'den önce ve önce bile söylediler:

İndirection.

Çok güçlü bir konsept ama asla bunu yapmazsanız .. Yineleyiciler algoritmaların soyutlanmasına yardımcı oldukları için faydalıdır, başka bir örnek. Ve derleme zamanı bir algoritmanın yeri, çok basit. Kod + veri biliyorsunuz veya başka bir dilde C #:

IEnumerable + LINQ + Massive Framework = 300MB çalışma zamanı cezası berbat, dolaylı başvuru örnekleri yığınları aracılığıyla uygulamaları sürükleyerek ..

"Le Pointer ucuz."


4
Bunun herhangi bir şeyle ne ilgisi var?
Neil Williams

... "statik bağlantı şimdiye kadarki en iyi şey" ve "Daha önce öğrendiğimden farklı bir şeyin nasıl çalıştığını anlamıyorum" dışında ne demeye çalışıyorsunuz?
Luaan

Luaan, 2000 yılında JIT'i parçalarına ayırarak ne öğrenebileceğini bilmiyor muydun? ASM'de 2000 yılında çevrimiçi olarak gösterildiği gibi, bir atlama tablosunda, bir işaretçi tablosundan sonuçlandığı için, farklı bir şeyi anlamamanın başka bir anlamı olabilir: dikkatle okumak önemli bir beceridir, tekrar deneyin.
rama-jka toti

1

Yukarıdaki bazı cevaplar "işaretçilerin gerçekten zor olmadığını", ancak doğrudan "işaretçi zor!" gelen. Birkaç yıl önce ilk sınıf CS öğrencilerine ders verdim (sadece bir yıl boyunca, açıkça emdiğimden beri) ve bu fikrin işaretçi zor olmadığı . Zor olan, neden ve ne zaman bir işaretçi isteyeceğinizi anlamaktır .

Bu soruyu - neden ve ne zaman bir işaretçi kullanacağınız - daha geniş yazılım mühendisliği sorunlarını açıklayabileceğinizi sanmıyorum. Her değişken olmalıdır Neden değil global bir değişken olabilir ve neden kimse fonksiyonlarına benzer kodunu dikkate almanız gerekir (bu olsun, o, kullanım işaretçileri kendi çağrı sitesine davranışlarını uzmanlaşmak).


0

İşaretçiler hakkında neyin kafa karıştırıcı olduğunu görmüyorum. Bellekteki bir yeri gösterirler, yani bellek adresini saklarlar. C / C ++ 'da işaretçinin işaret ettiği türü belirleyebilirsiniz. Örneğin:

int* my_int_pointer;

My_int_pointer öğesinin int içeren bir konumun adresini içerdiğini belirtir.

İşaretçilerle ilgili sorun, bellekte bir yere işaret etmeleridir, bu nedenle bulunmamanız gereken bir yere gitmek kolaydır. C / C ++ uygulamalarındaki çok sayıda güvenlik deliğine arabellek taşmasından (işaretçiyi arttırma) kanıt olarak bakın ayrılan sınırı geçmiş).


0

Sadece işleri biraz daha karıştırmak için bazen işaretçiler yerine tutamaklarla çalışmak zorundasınız. Kulplar, işaretçilerin işaretçileridir, böylece arka uç, yığını birleştirmek için bellekteki şeyleri taşıyabilir. İşaretçi rutinin ortasında değişirse, sonuçlar tahmin edilemezdir, bu nedenle hiçbir şeyin hiçbir yere gitmediğinden emin olmak için önce kolu kilitlemeniz gerekir.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 benden biraz daha tutarlı bir şekilde bahsediyor. :-)


-1: Kulplar işaretçileri göstermez; hiçbir anlamda işaretçi değillerdir. Onları karıştırmayın.
Ian Goldby

"Onlar hiçbir anlamda işaretçi değil" - um, farklı olmaya yalvarıyorum.
SarekOfVulcan

İşaretçi bir bellek konumudur. Tanıtıcı herhangi bir benzersiz tanımlayıcıdır. Bir işaretçi olabilir, ancak bir dizideki bir dizin veya bu konu için başka bir şey olabilir. Verdiğiniz bağlantı, tutamacın bir işaretçi olduğu özel bir durumdur, ancak olması gerekmez. Ayrıca bkz. Parashift.com/c++-faq-lite/references.html#faq-8.8
Ian Goldby

Bu bağlantı, hiçbir şekilde işaretçi olmadıklarını iddia etmenizi desteklemiyor - "Örneğin, tutamaklar Fred * olabilir, burada işaret edilen Fred * işaretçileri ..." -1 adil idi.
SarekOfVulcan

0

Her C / C ++ acemi aynı soruna sahiptir ve bu sorun "işaretçileri öğrenmek zor" değil "kim ve nasıl açıklanır" nedeniyle oluşur. Bazı öğrenciler sözlü olarak görsel olarak toplarlar ve açıklamanın en iyi yolu "tren" örneğini kullanmaktır (sözlü ve görsel örnek için uygun).

Nerede "lokomotif" bir gösterici olamaz şey tutun "vagon" "lokomotif" (veya nokta) çekme çalıştığı şey budur. Sonra, "vagon" un kendisini sınıflandırabilir, hayvanları, bitkileri veya insanları (veya bunların bir karışımını) tutabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.