Bir yığın işaretçisinin ne olduğunu anlıyorum - ama ne için kullanılıyor?


11

Yığın işaretçisi, "LIFO" olarak adlandırdığımız veriyi depolayan yığının en üstüne işaret eder. Başkasının benzetmesini çalmak, üstüne bulaşıkları koyup aldığınız bir yığın yemek gibi. Yığın işaretçisi OTOH, yığının en üstteki "çanağına" işaret eder. En azından x86 için bu doğru.

Peki bilgisayar / program neden yığın işaretçisinin işaret ettiğini "önemsiyor"? Başka bir deyişle, yığın işaretçisine sahip olmak ve nereye hizmet edeceğini bilmek hangi amaçtır?

C programcıları tarafından anlaşılabilir bir açıklama takdir edilecektir.


Eğer olamaz çünkü bakınız Eğer yemekler yığının tepesine görebilirsiniz gibi ram içinde yığının tepesine.
tkausl


8
Sen do not bir yığının altından bir tabak alır. Sen üst üste ekleyip başkası alır üstten . Burada bir kuyruk düşünüyorsun.
Kilian Foth

@Snowman Düzenlemeniz sorunun anlamını değiştiriyor gibi görünüyor. moonman239, Snowman'ın değişikliğinin doğru olup olmadığını, özellikle "Bu yığını yapısını açıklama yerine gerçekte hangi amaca hizmet ediyor?"
8bittree

1
@ 8bittree Lütfen düzenleme açıklamasına bakın: Soruyu konu satırında belirtildiği gibi sorunun gövdesine kopyaladım. Tabii ki, her zaman bir şeyleri değiştirme olasılığına açıkım ve orijinal yazar her zaman geri dönebilir veya yayını başka bir şekilde düzenleyebilir.

Yanıtlar:


23

Bu yığın, yapısını açıklama yerine gerçekte hangi amaca hizmet ediyor?

İstediğiniz sorunun tersini not ettiğim, yığınta depolanan verilerin yapısını doğru bir şekilde açıklayan birçok cevabınız var.

Yığının hizmet ettiği amaç şudur: Yığın, koroutinleri olmayan bir dilde devam etmenin bir parçası .

Hadi onu açalım.

Devam etmek gerekirse, "programımda bundan sonra ne olacak?" Sorusunun cevabı basittir. Her programın her noktasında bir şeyler olacak. İki işlenen hesaplanacaktır, daha sonra program toplamlarını hesaplayarak devam eder ve daha sonra program toplamı bir değişkene atayarak devam eder ve sonra ... vb.

Yeniden birleşme , soyut bir kavramın somut bir uygulamasını yapmak için yüksek bir kelimedir. "Sonra ne olur?" soyut bir kavramdır; yığının ortaya konma şekli, soyut kavramın gerçekte işleri hesaplayan gerçek bir makineye nasıl dönüştürüldüğünün bir parçasıdır.

Koroutinler , nerede olduklarını hatırlayabilen, bir süre başka bir koroutine kontrol verebilen ve daha sonra kaldıkları yerden devam edebilecekleri, ancak koroutin verimlerden hemen sonra değil , devam edebilecek işlevlerdir. Bir sonraki öğe istendiğinde veya eşzamansız işlem tamamlandığında nerede olduklarını hatırlaması gereken C # 'da "geri dönüş" veya "bekle" yi düşünün. Koroutinleri veya benzer dil özelliklerine sahip diller, sürekliliği uygulamak için bir yığından daha gelişmiş veri yapıları gerektirir.

Bir yığın devamlılığı nasıl uygular? Diğer cevaplar nasıl olduğunu söylüyor. Yığın, yaşam sürelerinin geçerli yöntemin etkinleştirilmesinden daha büyük olmadığı bilinen değişkenlerin ve geçici değerlerin (1) değerlerini ve (2) en son yöntem etkinleştirme ile ilişkili devam kodunun adresini saklar. İstisna yönetimi dışındaki dillerde, yığın "hata devamı" - yani istisnai bir durum oluştuğunda programın bundan sonra ne yapacağına dair bilgileri de saklayabilir.

Yığın size "nereden geldim?" Demediğini belirtmek için bu fırsatı değerlendireyim. - hata ayıklamada sıklıkla kullanılır. Yığın, bir sonraki adımda nereye gideceğinizi ve oraya geldiğinizde etkinleştirme değişkenlerinin değerlerinin ne olacağını gösterir . Coroutines içermeyen bir dilde, bir sonraki gideceğiniz yer neredeyse her zaman nereden geldiğinizdir, bu tür hata ayıklamayı kolaylaştırır. Ama hiç yoktur gereklilik olduğunu kontrol o kadar yapmadan kurtulmak eğer nereden geldiği hakkında derleyici mağaza bilgileri. Kuyruk çağrısı optimizasyonları, örneğin program kontrolünün nereden geldiğine ilişkin bilgileri yok eder.

Yığını neden ortak program içermeyen dillerde sürekliliği uygulamak için kullanıyoruz? Yöntemlerin eşzamanlı aktivasyonunun özelliği, "mevcut yöntemi askıya al, başka bir yöntemi aktive et, aktive edilen yöntemin sonucunu bilerek mevcut yönteme devam etme" şeklinin kendisiyle mantıksal olarak oluşturulduğunda bir aktivasyon yığını oluşturmasıdır. Bu yığın benzeri davranışı uygulayan bir veri yapısı oluşturmak çok ucuz ve kolaydır. Neden bu kadar ucuz ve kolay? Çünkü yonga setleri onlarca yıldır derleyici yazarları için bu tür bir programlamayı kolaylaştırmak için özel olarak tasarlanmıştır.


Başvurduğunuz teklifin yanlışlıkla başka bir kullanıcı tarafından yapılan bir düzenlemeye eklendiğini ve o zamandan beri düzeltildiğini ve bu yanıtı soruyu tam olarak ele almadığını unutmayın.
8bittree

2
Netliğin arttırılması için bir açıklama yapılması gerektiğinden eminim . Ben tamamen "yığın coroutines olmadan bir dilde sürdürmenin bir parçası olduğunu" emin değilim bile buna yaklaşıyor :-)

4

Yığının en temel kullanımı işlevler için dönüş adresini saklamaktır:

void a(){
    sub();
}
void b(){
    sub();
}
void sub() {
    //should i got back to a() or to b()?
}

ve C açısından bakıldığında hepsi bu. Derleyici bakış açısından:

  • tüm fonksiyon argümanları CPU kayıtları tarafından geçirilir - yeterli kayıt yoksa argümanlar yığına konur
  • fonksiyon sona erdikten sonra (çoğu) kayıtlar girmeden önce aynı değerlere sahip olmalıdır - bu nedenle kullanılan kayıtlar yığın üzerine yedeklenir

Ve OS açısından: program her zaman kesilebilir, bu yüzden sistem görevini bitirdikten sonra CPU durumunu geri yüklemeliyiz, bu yüzden her şeyi yığına depolayalım

Tüm bunlar, yığın üzerinde ne kadar öğe bulunduğunu veya gelecekte başka ne kadar öğe ekleyeceğimizi umursamadığımız için işe yarar, sadece yığın işaretçisini ne kadar hareket ettirdiğimizi bilmemiz ve bittikten sonra geri yüklememiz gerekir.


1
Ben genellikle bir optimizasyon kayıtları yerine görev için yeterli ücretsiz kayıt olan işlemciler üzerinde kullanılan olsa da, bağımsız değişkenler yığını itildi söylemek daha doğru olduğunu düşünüyorum. Bu bir nit, ama bence dillerin tarihsel olarak nasıl geliştiği daha iyi. En eski C / C ++ derleyicileri bunun için kayıt kullanmamıştır.
Gort the Robot

4

LIFO ve FIFO Karşılaştırması

LIFO, Son Giriş, İlk Çıkış anlamına gelir. Olduğu gibi, yığına yerleştirilen son öğe yığından alınan ilk öğedir.

Yemeklerin benzetmenizle ( ilk revizyonda ) tarif ettiğiniz şey, bir kuyruk veya FIFO, İlk Giren, İlk Çıkar.

İkisi arasındaki en büyük fark, LIFO / yığının aynı uçtan itmesi (takması) ve patlaması (çıkarması) ve bir FIFO / kuyruğunun zıt uçlardan yapmasıdır.

// Both:

Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]

// Stack            // Queue
Pop()               Pop()
-> [a, b]           -> [b, c]

Yığın işaretçisi

Yığının kaputunun altında neler olduğuna bir bakalım. İşte biraz bellek, her kutu bir adres:

...[ ][ ][ ][ ]...                       char* sp;
    ^- Stack Pointer (SP)

Ve şu anda boş yığının altına işaret eden bir yığın işaretçisi var (yığının büyüyüp büyüyüp büyümediği burada özellikle alakalı değil, bu yüzden bunu görmezden geleceğiz, ancak elbette gerçek dünyada hangi işlemin eklendiğini belirleyecek ve SP'den çıkarılır).

Hadi a, b, and ctekrar itelim. Soldaki grafikler, ortadaki "yüksek seviye" işlemi, sağdaki C-ish sözde kodu:

...[a][ ][ ][ ]...        Push('a')      *sp = 'a';
    ^- SP
...[a][ ][ ][ ]...                       ++sp;
       ^- SP

...[a][b][ ][ ]...        Push('b')      *sp = 'b';
       ^- SP
...[a][b][ ][ ]...                       ++sp;
          ^- SP

...[a][b][c][ ]...        Push('c')      *sp = 'c';
          ^- SP
...[a][b][c][ ]...                       ++sp;
             ^- SP

Gördüğünüz gibi, her seferinde push, bağımsız değişkeni yığın işaretçisinin işaret ettiği konuma ekler ve yığın işaretçisini sonraki konumu gösterecek şekilde ayarlar.

Şimdi başlayalım:

...[a][b][c][ ]...        Pop()          --sp;
          ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'c'
          ^- SP
...[a][b][c][ ]...        Pop()          --sp;
       ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'b'
       ^- SP

Poptersi ise push, yığın işaretçisini önceki konumu gösterecek şekilde ayarlar ve orada bulunan öğeyi kaldırır (genellikle çağıran kişiye geri döndürür pop).

Muhtemelen bunu fark ettiniz bve chala hafızadasınız. Ben sadece bunların yazım hatası olmadığından emin olmak istiyorum. Kısa süre içinde buna geri döneceğiz.

Yığın işaretçisi olmayan hayat

Bir yığın işaretçimiz yoksa ne olacağını görelim. Tekrar iterek başlayarak:

...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]...        Push(a)        ? = 'a';

Ee, hmm ... yığın işaretçimiz yoksa, işaret ettiği adrese bir şey taşıyamayız. Belki üst yerine tabana işaret eden bir işaretçi kullanabiliriz.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[a][ ][ ][ ]...        Push(a)        *bp = 'a';
    ^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]...        Push(b)        *bp = 'b';
    ^- bp

Ah oh. Yığının tabanının sabit değerini değiştiremediğimizden , aynı konuma aiterek üzerine yazdık b.

Neden kaç kez ittiğimizi takip etmiyoruz. Ayrıca, attığımız zamanları da takip etmemiz gerekecek.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);
                                         int count = 0;

...[a][ ][ ][ ]...        Push(a)        bp[count] = 'a';
    ^- bp
...[a][ ][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Push(a)        bp[count] = 'b';
    ^- bp
...[a][b][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Pop()          --count;
    ^- bp
...[a][b][ ][ ]...                       return bp[count]; //returns b
    ^- bp

İşe yarıyor, ama aslında öncekine oldukça benziyor, ancak *pointeryazımdan daha pointer[offset]az bahsetmiyorum bile ( daha fazla aritmetik yok) daha ucuz . Bu benim için bir kayıp gibi görünüyor.

Tekrar deneyelim. Dizi tabanlı bir koleksiyonun sonunu bulmak için Pascal dize stilini kullanmak yerine (koleksiyonda kaç öğenin bulunduğunu izleme), C dize stilini deneyelim (baştan sona tarayın):

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[ ][ ][ ][ ]...        Push(a)        char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       *top = 'a';
    ^- bp    ^- top

...[ ][ ][ ][ ]...        Pop()          char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       --top;
    ^- bp       ^- top                   return *top; // returns '('

Sorunu burada önceden tahmin etmiş olabilirsiniz. Başlatılmamış belleğin 0 olması garanti edilmez. Bu nedenle, en üste yerleştirmek için baktığımızda a, içinde rastgele çöp bulunan bir grup kullanılmayan bellek yerinin üzerinden atlıyoruz. Benzer şekilde, en üste tarama ayaptığımızda, nihayet yeni bir bellek konumu bulana kadar ittik ve ötesine 0geçip rastgele çöpü ondan hemen önce geri döndürüyoruz.

Bu düzeltmek için yeterince kolay, sadece yığının üst kısmının her zaman bir ile işaretlenecek şekilde güncellendiğinden emin olmak Pushve işlemleri eklememiz ve yığını böyle bir sonlandırıcıyla başlatmamız gerekiyor. Tabii ki bu aynı zamanda yığında gerçek bir değer olarak (veya sonlandırıcı olarak seçtiğimiz herhangi bir değere) sahip olamayacağımız anlamına da gelir .Pop00

Bunun da ötesinde, O (1) operasyonlarını neyin O (n) operasyonlarına değiştirdik.

TL; DR

Yığın işaretçisi, tüm eylemin gerçekleştiği yığının üstünü izler . Ondan kurtulmanın bir yolu vardır ( bp[count]ve topaslında hala yığın işaretçisidir), ancak ikisi de yığın işaretçisine sahip olmaktan daha karmaşık ve yavaş olur. Ve yığının üst kısmının nerede olduğunu bilmemek, yığını kullanamayacağınız anlamına gelir.

Not: x86'da çalışma zamanı yığınının "en altını" gösteren yığın işaretçisi, tüm çalışma zamanı yığınının baş aşağı olmasıyla ilgili bir yanlış anlama olabilir. Başka bir deyişle, yığının tabanı yüksek bir bellek adresine yerleştirilir ve yığının ucu daha düşük bellek adreslerine doğru büyür. Yığın işaretçisi yapar tüm eylem ucu yığının tabanından daha düşük bir bellek adresi olarak, sadece bu meydana istifin ucuna noktası.


2

Yığın işaretçisi (çerçeve işaretçisi ile) çağrı yığını için kullanılır ( iyi bir resmin olduğu wikipedia bağlantısını takip edin).

Çağrı yığını, dönüş adresi, yerel değişkenler ve diğer yerel verileri (özellikle kayıtların dökülen içeriği; formüller) içeren çağrı çerçeveleri içerir .

De okuyunuz kuyruk aramaları (bazı kuyruk özyinelemeli çağrılar herhangi çağrı çerçeve gerekmez), istisna işleme (gibi setjmp & longjmp , onlar aynı anda birçok yığın çerçeveleri haşhaş içerebilir), sinyalleri ve kesmeler ve devamlılık . Ayrıca bkz. Çağırma kuralları ve uygulama ikili arayüzleri (ABI'ler), özellikle x86-64 ABI (bazı resmi argümanların kayıtlar tarafından iletildiğini tanımlar).

Ayrıca, C'deki bazı basit işlevleri kodlayın, sonra gcc -Wall -O -S -fverbose-asm derlemek için kullanın ve oluşturulan .s derleyici dosyasına bakın.

Appel, çöp toplama işleminin yığın tahsisinden daha hızlı olabileceğini iddia eden eski bir 1986 makalesi yazdı ( derleyicide Devam Edici Stil kullanarak ), ancak bugünün x86 işlemcilerinde (özellikle önbellek etkileri nedeniyle) muhtemelen yanlıştır.

Arama kuralları, ABI'ler ve yığın düzeninin 32 bit i686 ve 64 bit x86-64'te farklı olduğuna dikkat edin. Ayrıca, çağrı kuralları (ve çağrı çerçevesinin tahsis edilmesinden veya patlamasından kim sorumludur) farklı dillerden farklı olabilir (örneğin C, Pascal, Ocaml, SBCL Ortak Lisp'in farklı çağrı kuralları vardır ...)

BTW, AVX gibi yeni x86 uzantıları yığın işaretçisine giderek daha büyük hizalama kısıtlamaları getiriyor (IIRC, x86-64'teki bir çağrı çerçevesi 16 bayta hizalanmak istiyor, yani iki kelime veya işaretçi).


1
X86-64 üzerinde 16 bayta hizalamanın, bir işaretçi boyutunun / hizalamasının iki katı anlamına geldiğini belirtmek isteyebilirsiniz, bu aslında bayt sayısından daha ilginçtir.
Tekilleştirici

1

Basit bir ifadeyle, program bu verileri kullandığından ve nerede bulacağınızı takip etmesi gerektiğinden önemlidir.

Bir işlevdeki yerel değişkenleri bildirirseniz, yığın depolandıkları yerdir. Ayrıca, başka bir işlevi çağırırsanız, yığın, dönüş adresini sakladığı yerdir; böylece, aradığınız işlev bittiğinde bulunduğunuz işleve geri dönebilir ve kaldığı yerden devam edebilir.

SP olmadan, bildiğimiz gibi yapılandırılmış programlama aslında imkansız olurdu. (Sahip olmamanız için etrafta çalışabilirsiniz, ancak kendi sürümünüzü uygulamayı gerektirecektir, bu yüzden çok fazla bir fark yoktur.)


1
Yığın olmadan yapılandırılmış programlamanın imkansız olacağı iddianız yanlıştır. Devamlı geçiş tarzında derlenen programlar yığın kullanmaz, ancak bunlar mükemmel derecede duyarlı programlardır.
Eric Lippert

@EricLippert: Belki de "kafasında durmayı ve kendini ters çevirmeyi" içerecek kadar "tamamen mantıklı" değerler için. ;-)
Mason Wheeler

1
İle devam geçerken , hiç bir çağrı yığını gerekmez mümkündür. Etkili bir şekilde, her çağrı dönüş yerine kuyruk çağrısı ve git. "CPS ve TCO, örtük bir işlev geri dönüşü kavramını ortadan kaldırdıklarından, bunların birleşik kullanımı, çalışma zamanı yığınına olan ihtiyacı ortadan kaldırabilir."

@MichaelT: Bir sebepten dolayı "esasen" imkansız dedim. CPS teorik olarak bunu başarabilir, ancak pratikte Eric'in konu hakkındaki bir dizi blog yayınına işaret ettiği gibi CPS'deki herhangi bir karmaşıklığın gerçek dünya kodunu yazmak çok hızlı bir şekilde zorlaşır .
Mason Wheeler

1
@MasonWheeler Eric, CPS'ye derlenen programlardan bahsediyor . Örneğin, Jon Harrop'un blogundan alıntı : In fact, some compilers don’t even use stack frames [...], and other compilers like SML/NJ convert every call into continuation style and put stack frames on the heap, splitting every segment of code between a pair of function calls in the source into its own separate function in the compiled form.Bu, "kendi [yığın] sürümünüzü uygulamaktan" farklıdır.
Doval

1

Bir x86 işlemcideki işlemci yığını için, bir bulaşık yığınının benzetmesi gerçekten yanlıştır.
Çeşitli nedenlerle (çoğunlukla tarihsel), işlemci yığını hafızanın tepesinden hafızanın altına doğru büyür, bu nedenle daha iyi bir benzetme tavandan asılı zincir bağlantıları zinciri olacaktır. Yığına bir şey iterken, en alt bağlantıya bir zincir bağlantısı eklenir.

Yığın işaretçisi zincirin en alt halkasına karşılık gelir ve işlemci tarafından bu en düşük halkanın olduğu yeri "görmek" için kullanılır, böylece bağlantılar tüm zinciri tavandan aşağıya taşımak zorunda kalmadan eklenebilir veya çıkarılabilir.

Bir anlamda, bir x86 işlemcinin içinde, yığın baş aşağıdır, ancak normal yığın terminoloji eşiği kullanılır, böylece en düşük bağlantı yığının üst kısmı olarak adlandırılır .


Yukarıda bahsettiğim zincir bağlantıları aslında bir bilgisayardaki bellek hücreleridir ve yerel değişkenleri ve bazı hesaplama sonuçlarını saklamak için kullanılırlar. Bilgisayar programları, yığının üst kısmının nerede olduğunu (yani en düşük bağlantının asıldığı yerde) ilgilenir, çünkü bir işlevin erişmesi gereken değişkenlerin büyük çoğunluğu yığın işaretçisinin başvurduğu yere yakındır ve bunlara hızlı erişim istenir.


1
The stack pointer refers to the lowest link of the chain and is used by the processor to "see" where that lowest link is, so that links can be added or removed without having to travel the entire chain from the ceiling down.Bunun iyi bir benzetme olduğundan emin değilim. Gerçekte bağlantılar asla eklenmez veya kaldırılmaz. Yığın işaretçisi daha çok bağlantılardan birini işaretlemek için kullandığınız bir miktar bant gibidir. O kaseti kaybederseniz, size kullanılan en alt bağlantı olduğu bilmesi mümkün olmaz hiç ; zinciri tavandan aşağıya doğru hareket ettirmek size yardımcı olmaz.
Doval

Yığın işaretçisi, programın / bilgisayarın bir işlevin yerel değişkenlerini bulmak için kullanabileceği bir referans noktası sağlar.
moonman239

Bu durumda, bilgisayar yerel değişkenleri nasıl bulur? Sadece aşağıdan yukarıya doğru her bellek adresini aramaya mı gidiyor?
moonman239

@ moonman239: Hayır, derlerken, derleyici her değişkenin yığın işaretçisine göre nerede depolandığını izler. İşlemci, değişkenlere doğrudan erişim sağlamak için bu nispi adreslemeyi anlar.
Bart van Ingen Schenau

1
@BartvanIngenSchenau Ah, tamam. Hiç bir şeyin ortasında olduğunuzda ve yardıma ihtiyacınız olduğunda olduğu gibi, 911'e bir dönüm noktasına göre nerede olduğunuz hakkında bir fikir verirsiniz. Yığın işaretçisi, bu durumda, genellikle en yakın "dönüm noktası" dır ve bu nedenle belki de en iyi referans noktasıdır.
moonman239

1

Bu yanıt, özellikle atıfta yığın işaretçisi mevcut iplik (yürütme) arasında .

Yordamsal programlama dillerinde, bir iş parçacığı tipik olarak aşağıdaki amaçlar için bir yığına 1 erişebilir :

  • Kontrol akışı, yani "çağrı yığını".
    • Bir işlev başka bir işlevi çağırdığında, çağrı yığını nereye geri döneceğini hatırlar.
    • Bir çağrı yığını gereklidir, çünkü "fonksiyon çağrısı" nın böyle davranmasını istiyoruz - "kaldığımız yerden devam etmek" .
    • Yürütmenin ortasında işlev çağrıları olmayan (ör. Yalnızca geçerli işlevin sonuna ulaşıldığında bir sonraki işlevi belirtmeye izin verilir) veya hiç işlev çağrısı olmayan (yalnızca goto ve koşullu atlamaları kullanarak) başka programlama stilleri de vardır. ). Bu programlama stilleri için çağrı yığını gerekmeyebilir.
  • Fonksiyon çağrısı parametreleri.
    • Bir işlev başka bir işlevi çağırdığında, parametreler yığına itilebilir.
    • Arayanın ve arayanın, arama bittiğinde parametreleri yığından kimin sorumlu olduğu ile aynı kurala uyması gerekir.
  • Bir işlev çağrısında yaşayan yerel değişkenler.
    • Arayana ait yerel bir değişkenin, bu yerel değişkene callee'ye bir işaretçi geçirilerek bir callee için erişilebilir hale getirilebileceğini unutmayın .

Not 1 : içeriği tamamen diğer iş parçacıkları tarafından okunabilir ve parçalanabilir olmasına rağmen, iş parçacığının kullanımına adanmıştır .

Montaj programlamasında, C ve C ++, her üç amaç da aynı yığın tarafından yerine getirilebilir. Diğer bazı dillerde, bazı amaçlar ayrı yığınlarla veya dinamik olarak atanan bellekle gerçekleştirilebilir.


1

İşte, yığının ne için kullanıldığının kasten aşırı basitleştirilmiş bir versiyonu.

Yığını bir dizin kartı yığını olarak düşünün. Yığın işaretçisi en üstteki kartı gösterir.

Bir işlevi çağırdığınızda:

  • Kodun adresini, işlevi bir karta çağıran satırdan hemen sonra yazar ve yığının üzerine koyarsınız. (Yani yığın işaretçisini bir arttırır ve adresi, işaret ettiği yere yazarsınız)
  • Sonra kayıtlarda bulunan değerleri bazı kartlara yazıp yığının üzerine koyarsınız. (yani, yığın işaretçisini kayıt sayısına göre artırır ve kayıt içeriğini işaret ettiği yere kopyalarsınız)
  • Sonra kazığa bir marker kartı koyun. (yani, geçerli yığın işaretçisini kaydedersiniz.)
  • Daha sonra, işlevin çağrıldığı her parametrenin değerini bir karta yazıp yığının üzerine koyarsınız. (yani, yığın işaretçisini parametre sayısına göre artırır ve parametreleri yığın işaretçisinin işaret ettiği yere yazarsınız.)
  • Daha sonra, her yerel değişken için bir kart eklersiniz, muhtemelen ilk değeri üzerine yazarsınız. (yani, yığın işaretçisini yerel değişkenlerin sayısına göre artırırsınız.)

Bu noktada, işlevdeki kod çalışır. Kod, her kartın üste göre nerede olduğunu bilmek için derlenir. Bu yüzden değişkenin xüstten üçüncü kart olduğunu (yani yığın işaretçisi - 3) ve parametrenin yüstten altıncı kart olduğunu (yani yığın işaretçisi - 6) bilir.

Bu yöntem, her bir yerel değişkenin veya parametrenin adresinin koda dönüştürülmesi gerekmediği anlamına gelir. Bunun yerine, bu veri öğelerinin tümü yığın işaretçisine göre adreslenir.

İşlev döndüğünde, ters işlem basitçe:

  • İşaret kartını arayın ve üstündeki tüm kartları atın. (yani yığın işaretçisini kaydedilen adrese ayarlayın.)
  • Kayıtları daha önce kaydedilmiş kartlardan geri yükleyin ve atın. (yani yığın işaretçisinden sabit bir değer çıkar)
  • Kodu kartın üstündeki adresten yayınlamaya başlayın ve ardından atın. (örneğin, yığın işaretçisinden 1 çıkarın.)

Yığın şimdi işlev çağrılmadan önceki durumuna geri döndü.

Bunu göz önünde bulundurduğunuzda, iki şeye dikkat edin: yerlilerin tahsisi ve yeniden konumlandırılması, yığın işaretçisine bir sayı ekleyerek veya yığın işaretçisinden bir sayı çıkararak son derece hızlı bir işlemdir. Ayrıca bunun doğal olarak özyineleme ile nasıl çalıştığını da not edin.

Bu açıklayıcı amaçlar için basitleştirilmiştir. Uygulamada, parametreler ve yerel ayarlar bir optimizasyon olarak kayıtlara konabilir ve yığın işaretçisi genellikle bir değil, makinenin kelime boyutuna göre artırılır ve azaltılır. (Birkaç şeyi adlandırmak için.)


1

Modern programlama dilleri, bildiğiniz gibi, altyordam çağrıları kavramını destekler (çoğunlukla "işlev çağrıları" olarak adlandırılır). Bunun anlamı şudur ki:

  1. Bazı kodların ortasında, programınızdaki diğer bazı işlevleri çağırabilirsiniz ;
  2. Bu işlev, nereden çağrıldığını açıkça bilmiyor;
  3. Bununla birlikte, işi bittiğinde ve returnkontrol edildiğinde , kontrol, çağrının başlatıldığı noktaya geri döner ve çağrının başlatıldığı zamanki tüm yerel değişken değerleri geçerlidir.

Bilgisayar bunu nasıl takip ediyor? Hangi işlevlerin hangi çağrıların geri dönmesini beklediğinin sürekli kaydını tutar. Bu kayıt, bir yığın-ve olduğu bu önemli bir tane olduğundan, normalde diyoruz yığını.

Ve bu çağrı / dönüş modeli çok önemli olduğu için, CPU'lar uzun zamandır özel donanım desteği sağlamak için tasarlandı. Yığın işaretçisi, CPU'larda bir donanım özelliğidir - yalnızca yığının tepesini takip etmeye adanmış ve CPU'nun bir alt rutine dallanma ve ondan geri dönme talimatları tarafından kullanılan bir kayıttır.

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.