C ++ 'da Yığın, Statik ve Yığın


160

Aradım, ama bu üç kavramı çok iyi anlamadım. Dinamik ayırmayı ne zaman (yığın içinde) kullanmam gerekir ve gerçek avantajı nedir? Statik ve yığın problemleri nelerdir? Yığına değişkenler ayırmadan bir uygulamanın tamamını yazabilir miyim?

Diğer dillerin bir "çöp toplayıcı" içerdiğini duydum, böylece bellek konusunda endişelenmenize gerek yok. Çöp toplayıcı ne yapar?

Bu çöp toplayıcıyı kullanarak yapamayacağınız hafızayı kendiniz manipüle edebilir misiniz?

Birisi bana bu beyanla şunu söyledi:

int * asafe=new int;

Bir "işaretçi işaretçi" var. Bu ne demek? Farklı:

asafe=new int;

?


Bir süre önce çok benzer bir soru soruldu: Yığın ve yığın ne ve nerede? Bu soruya, kendinize biraz ışık tutması gereken birkaç iyi cevap var.
Scott Saad

Yanıtlar:


223

Benzer bir soru soruldu, ancak statik hakkında soru sormadı.

Statik, yığın ve yığın belleğin özeti:

  • Statik değişken, global olarak erişemeseniz bile temelde global bir değişkendir. Genellikle bunun için yürütülebilir dosyada bir adres vardır. Tüm program için yalnızca bir kopya vardır. Bir işlev çağrısına (veya sınıfa) kaç kez gittiğinize (ve kaç iş parçacığına!) Sahip olursanız olun, değişken aynı bellek konumuna başvurur.

  • Yığın, dinamik olarak kullanılabilen bir grup bellektir. Bir nesne için 4kb istiyorsanız, dinamik ayırıcı yığındaki boş alan listesine bakacak, bir 4kb yığın seçecek ve size verecektir. Genel olarak, dinamik bellek ayırıcı (malloc, new, etc.) Belleğin sonunda başlar ve geriye doğru çalışır.

  • Bir yığının nasıl büyüdüğünü ve küçüldüğünü açıklamak, bu cevabın kapsamı dışındadır, ancak her zaman yalnızca sondan ekleyip kaldırdığınızı söylemeniz yeterlidir. Yığınlar genellikle yüksek başlar ve düşük adreslere kadar büyür. Yığın, ortada bir yerde dinamik ayırıcıyla karşılaştığında belleğiniz biter (ancak fiziksel belleğe ve sanal belleğe ve parçalamaya bakın). Birden çok iş parçacığı birden çok yığın gerektirir (işlem genellikle yığın için minimum bir boyut ayırır).

Her birini kullanmak istediğinizde:

  • Statikler / globaller, her zaman ihtiyaç duyacağınızı ve asla ayrılmak istemediğinizi bildiğiniz bellek için faydalıdır. (Bu arada, gömülü ortamların yalnızca statik belleğe sahip olduğu düşünülebilir ... yığın ve yığın, üçüncü bir bellek türü tarafından paylaşılan bilinen bir adres alanının bir parçasıdır: program kodu. Programlar genellikle kendi belleklerinden dinamik ayırma yaparlar. bağlı listeler gibi şeylere ihtiyaç duyduklarında statik bellek.Ama ne olursa olsun, statik belleğin kendisi (arabellek) kendisi "ayrılmış" değildir, bunun yerine bu amaç için arabellek tarafından tutulan bellekten başka nesneler ayrılır. konsol oyunları da yerleşik dinamik bellek mekanizmalarından tüm tahsisler için önceden ayarlanmış boyutlardaki tamponları kullanarak tahsis sürecini sıkı bir şekilde kontrol etmek lehine sık sık kaçacaktır.)

  • Yığın değişkenleri, işlevin kapsamda olduğu sürece (yığında bir yerde) değişkenlerin kalmasını isteyeceğinizi bildiğinizde kullanışlıdır. Yığınlar, bulundukları kod için ihtiyacınız olan ancak bu kodun dışında gerekli olmayan değişkenler için iyidir. Ayrıca, dosya gibi bir kaynağa eriştiğinizde ve koddan ayrıldığınızda kaynağın otomatik olarak kaybolmasını istiyorsanız da gerçekten iyidir.

  • Yığın ayırmalar (dinamik olarak ayrılan bellek), yukarıdakilerden daha esnek olmak istediğinizde kullanışlıdır. Sıklıkla, bir olaya yanıt vermek için bir işlev çağrılır (kullanıcı "kutu oluştur" düğmesini tıklar). Doğru yanıt, işlevden çıkıldıktan sonra uzun süre yapışması gereken yeni bir nesnenin (yeni bir Box nesnesi) tahsis edilmesini gerektirebilir, bu nedenle yığın üzerinde olamaz. Ancak, programın başında kaç kutu istediğinizi bilmiyorsunuz, bu yüzden statik olamaz.

Çöp toplama

Son zamanlarda Çöp Toplayıcıların ne kadar harika olduğu hakkında çok şey duydum, bu yüzden belki biraz muhalif bir ses yardımcı olabilir.

Çöp Toplama, performansın büyük bir sorun olmadığı zamanlar için harika bir mekanizmadır. GC'lerin daha iyi ve daha karmaşık hale geldiğini duyuyorum, ancak gerçek şu ki (kullanım durumuna bağlı olarak) bir performans cezası kabul etmek zorunda kalabilirsiniz. Ve eğer tembelseniz, yine de düzgün çalışmayabilir. En iyi zamanlarda, Çöp Toplayıcıları, daha fazla referans olmadığını anladığında belleğinizin kaybolduğunu fark ederler ( referans sayımına bakın)). Ancak, kendisine başvuran bir nesneniz varsa (muhtemelen geri dönen başka bir nesneye başvurarak), o zaman tek başına referans sayımı belleğin silinebileceğini göstermez. Bu durumda, GC'nin referans çorbanın tamamına bakması ve sadece kendileri tarafından atıfta bulunulan herhangi bir ada olup olmadığını bulması gerekir. Offhand, O (n ^ 2) bir operasyon olduğunu tahmin ediyorum, ama ne olursa olsun, performans ile hiç ilgileniyorsanız kötü olabilir. (Düzenleme: Martin B işaret makul verimli algoritmalar için O (n) olduğunu Yani performansla ilgilenen ve çöp toplama olmadan sabit zamanda ayırması çok fazla ise) O (n hala..)

Şahsen, insanların C ++ 'ın çöp toplama olmadığını söylediğini duyduğumda, zihnim bunu C ++' ın bir özelliği olarak etiketler, ama muhtemelen azınlıktayım. Muhtemelen insanların C ve C ++ 'da programlama hakkında öğrenmesi en zor şey, işaretçiler ve dinamik bellek ayırmalarını doğru bir şekilde nasıl kullanacaklarıdır. Python gibi diğer bazı diller GC olmadan korkunç olurdu, bu yüzden dilden ne istediğinize geldiğini düşünüyorum. Güvenilir performans istiyorsanız, çöp toplama olmadan C ++, Fortran'ın bu tarafında düşünebileceğim tek şey. Kullanım kolaylığı ve eğitim tekerlekleri istiyorsanız ("uygun" bellek yönetimini öğrenmenize gerek kalmadan kilitlenmenizi önlemek için), GC ile bir şey seçin. Belleği iyi yönetmeyi bilseniz bile, diğer kodu optimize etmek için harcayacağınız zamandan tasarruf etmenizi sağlar. Gerçekten çok fazla performans cezası yok, ama gerçekten güvenilir performansa (ve tam olarak ne olduğunu, kapakların altında ne zaman olduğunu bilme yeteneğine) ihtiyacınız varsa, o zaman C ++ ile yapışırdım. Şimdiye kadar duyduğum her büyük oyun motorunun C ++ (C veya montaj değilse) olmasının bir nedeni var. Python ve diğerleri, komut dosyası oluşturmak için iyidir, ancak ana oyun motoru değildir.


Orijinal soru ile gerçekten alakalı değil (veya aslında çok fazla), ancak yığının ve yığının konumlarını geriye doğru aldınız. Tipik olarak , yığın büyür ve yığın büyür (bir yığın aslında "büyümez", ancak bu çok büyük bir basitleştirme) ...
P Daddy

bu sorunun diğer soruya benzediğini hatta yinelediğini düşünmüyorum. Bu özellikle C ++ ile ilgilidir ve ne demek istediğini neredeyse kesinlikle C ++ 'da bulunan üç depolama süresidir. Statik belleğe tahsis edilmiş dinamik bir nesneye sahip olabilirsiniz, örneğin, aşırı yük op yeni.
Johannes Schaub - litb

7
Çöp toplamadaki aşağılayıcı muameleniz biraz daha az yardımcı oldu.
P Daddy

9
Genellikle çöp toplama günümüzde elle boşaltma belleğinden daha iyidir çünkü performansın başka türlü kullanılabileceği durumlarda gerçekleşebilecek bellek boşluğunun aksine yapılacak çok az iş olduğunda olur.
Georg Schölly

3
Sadece küçük bir yorum - çöp toplamada O (n ^ 2) karmaşıklığı yoktur (bu aslında performans için felaket olurdu). Bir çöp toplama döngüsü için geçen süre yığının boyutuyla orantılıdır - bkz. Hpl.hp.com/personal/Hans_Boehm/gc/complexity.html .
Martin B

54

Aşağıdakiler elbette tam olarak kesin değildir. Okuduğunuzda bir tuz tanesi ile alın :)

Bahsettiğiniz üç şey , nesnelerin ne kadar yaşayacağı ve hayata ne zaman başladığıyla ilgili olan otomatik, statik ve dinamik depolama süresidir .


Otomatik saklama süresi

Kısa süreli ve küçük veriler için otomatik depolama süresini kullanırsınız ; bu, yalnızca bazı bloklar için yerel olarak gereklidir :

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

Ömür bloktan çıkar çıkmaz biter ve nesne tanımlanır tanımlanmaz başlar. Bunlar en basit depolama süresidir ve özellikle dinamik depolama süresinden çok daha hızlıdır.


Statik saklama süresi

Statik depolama süresini, kapsamı bu tür kullanıma izin veriyorsa (ad alanı kapsamı) herhangi bir kod tarafından her zaman erişilebilen değişkenler ve ömrünü kapsamlarının dışına (yerel kapsam) genişletmesi gereken yerel değişkenler için kullanırsınız ve sınıflarındaki tüm nesneler tarafından paylaşılması gereken üye değişkenler için (sınıf kapsamı). Kullanım ömürleri bulundukları kapsama bağlıdır. Ad alanı kapsamına , yerel kapsam ve sınıf kapsamına sahip olabilirler . Her ikisi için de doğru olan, hayatları başladığında, programın sonunda yaşamın sona ermesidir . İşte iki örnek:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

Program yazdırır ababab, çünkü localAbloğunun çıkışından sonra yok olmaz. Yerel kapsamı olan nesnelerin, denetim tanımlarına ulaştığında ömür boyu başladığını söyleyebilirsiniz . Çünkü localAişlevin gövdesi girildiğinde olur. Ad alanı kapsamındaki nesneler için, ömür boyu program başlangıcında başlar . Aynı şey sınıf kapsamındaki statik nesneler için de geçerlidir:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

Gördüğünüz gibi classScopeA, sınıfının belirli nesnelerine değil, sınıfın kendisine bağlıdır. Yukarıdaki üç adın adresi aynıdır ve hepsi aynı nesneyi gösterir. Statik nesnelerin ne zaman ve nasıl başlatıldığına dair özel bir kural vardır, ancak şimdi bununla ilgilenmeyelim. Bu statik başlatma siparişi fiyasko terimi ile kastedilmektedir .


Dinamik depolama süresi

Son saklama süresi dinamiktir. Nesneleri başka bir adada yaşamak istiyorsanız ve bu referansların etrafına işaretçiler koymak istiyorsanız bunu kullanırsınız. Nesneleriniz büyükse ve yalnızca çalışma zamanında bilinen boyut dizileri oluşturmak istiyorsanız bunları da kullanırsınız . Bu esneklik nedeniyle, dinamik depolama süresine sahip nesnelerin yönetimi karmaşık ve yavaştır. Bu dinamik süreye sahip nesneler, uygun bir yeni operatör çağrısı olduğunda ömür boyu başlar :

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

Ömrü yalnızca onlar için sil komutunu aradığınızda sona erer . Bunu unutursanız, bu nesneler ömür boyu asla bitmez. Ve kullanıcı tarafından bildirilen bir kurucu tanımlayan sınıf nesnelerinin yıkıcıları çağrılmaz. Dinamik depolama süresine sahip nesneler, kullanım ömürlerinin ve ilişkili bellek kaynaklarının manuel olarak işlenmesini gerektirir. Kütüphanelerin kullanımı kolaylaşır. Akıllı bir işaretçi kullanılarak belirli nesneler için açık çöp toplama oluşturulabilir:

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

Silme çağrısını önemsemek zorunda değilsiniz: Nesneye başvuran son işaretçi kapsam dışına çıkarsa, paylaşılan ptr bunu sizin için yapar. Paylaşılan ptr otomatik depolama süresine sahiptir. Yani onun ömrü otomatik olarak yönetilir, bu onun yıkıcı dinamik nesnesine sivri silmesi gerekip gerekmediğini kontrol etmek için izin. Paylaşılan_ptr referansı için destek belgelerine bakın: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm


39

"Kısa cevap" gibi özenle söylendi:

  • statik değişken (sınıf)
    ömür boyu = program çalışma zamanı (1)
    görünürlük = erişim değiştiricileri tarafından belirlenir (özel / korumalı / genel)

  • statik değişken (global kapsam)
    ömür boyu = program çalışma zamanı (1)
    görünürlük = örneklendiği derleme birimi (2)

  • yığın değişkeni
    lifetime = sizin tarafınızdan tanımlanmış (silinecek yeni)
    görünürlük = sizin tarafınızdan tanımlanmış (işaretçiyi atadığınız her şey)

  • yığın değişkeni
    görünürlüğü = bildirimden kapsamdan çıkılana kadar
    ömür boyu = bildirimden kapsamdan çıkana kadar


(1) daha doğrusu: başlatmadan derleme biriminin (yani C / C ++ dosyası) dezenfekte edilmesine kadar. Derleme birimlerinin başlatma sırası standart tarafından tanımlanmamıştır.

(2) Dikkat: bir başlıkta statik bir değişken başlatırsanız, her derleme birimi kendi kopyasını alır.


5

Eminim ki, saraylardan biri daha kısa sürede daha iyi bir cevap bulur, ancak ana fark hız ve boyuttur.

yığın

Tahsis edilmesi çok daha hızlı. O (1) 'de yapılır, çünkü yığın çerçevesini esasen serbest olacak şekilde ayarlarken tahsis edilir. Dezavantajı, yığın alanı biterse kemikli olmasıdır. Yığın boyutunu ayarlayabilirsiniz, ancak IIRC ile oynamak için ~ 2MB var. Ayrıca, işlevden çıkar çıkmaz yığındaki her şey temizlenir. Bu yüzden daha sonra başvurmak sorunlu olabilir. (Tahsis edilen nesneleri yığınlamak için işaretçiler hatalara yol açar.)

Yığın

Tahsis edilmesi oldukça yavaş. Ama oynayabileceğiniz ve işaret edebileceğiniz GB var.

Çöp toplayıcı

Çöp toplayıcı arka planda çalışan ve belleği serbest bırakan bir koddur. Öbek üzerinde bellek ayırdığınızda, bellek sızıntısı olarak bilinen, onu boşaltmayı unutmak çok kolaydır. Zamanla, uygulamanızın tükettiği bellek çökene kadar büyür ve büyür. Bir çöp toplayıcısının artık ihtiyacınız olmayan belleği düzenli olarak boşaltması, bu hata sınıfını ortadan kaldırmaya yardımcı olur. Tabii ki bunun bir bedeli var, çünkü çöp toplayıcı işleri yavaşlatıyor.


3

Statik ve yığın problemleri nelerdir?

"Statik" ayırma ile ilgili sorun, ayırmanın derleme zamanında yapılmasıdır: veriyi çalışma zamanına kadar bilinmeyen değişken sayıda veri ayırmak için kullanamazsınız.

"Yığın" üzerine tahsis ile ilgili sorun, tahsisi yapan alt rutin en kısa sürede tahsis yok olmasıdır.

Yığında değişkenleri ayırmadan tüm bir uygulamayı yazabilir miyim?

Belki de önemsiz olmayan, normal, büyük bir uygulama (ancak "gömülü" olarak adlandırılan programlar yığın olmadan C ++ alt kümesi kullanılarak yazılabilir).

Çöp toplayıcı ne yapar?

Uygulamanızın artık referans vermediğini tespit etmek için verilerinizi ("işaretle ve tara") izlemeye devam eder. Bu uygulama için uygundur, çünkü uygulamanın verileri yeniden konumlandırması gerekmez ... ancak çöp toplayıcı hesaplama açısından pahalı olabilir.

Çöp toplayıcılar C ++ programlamanın olağan bir özelliği değildir.

Bu çöp toplayıcıyı kullanarak yapamayacağınız hafızayı kendiniz manipüle edebilir misiniz?

Deterministik bellek dağıtımı için C ++ mekanizmalarını öğrenin:

  • 'statik': asla serbest bırakılmadı
  • 'stack': "değişken kapsam dışına çıkar çıkmaz"
  • 'öbek': işaretçi silindiğinde (uygulama tarafından açıkça silindiğinde veya bir veya diğer altyordam içinde dolaylı olarak silindiğinde)

1

Yığın bellek ayırma (işlev değişkenleri, yerel değişkenler), yığınınız çok "derin" olduğunda ve yığın ayırmalarının kullanabileceği bellekte taştığınızda sorunlu olabilir. Yığın, birden çok iş parçacığından veya program yaşam döngüsü boyunca erişilmesi gereken nesneler içindir. Yığını kullanmadan tüm bir programı yazabilirsiniz.

Çöp toplayıcı olmadan belleği kolayca sızdırabilirsiniz, ancak nesneler ve bellek serbest bırakıldığında da dikte edebilirsiniz. GC çalıştığında Java ile ilgili sorunları için var ve GC özel bir iş parçacığı (başka bir şey çalıştırabilirsiniz), çünkü gerçek zamanlı bir işlem var. Performans kritikse ve sızan nesnelerin olmadığını garanti ediyorsanız, GC kullanmamak çok yardımcı olur. Aksi takdirde, uygulamanız bellek tükettiğinde hayattan nefret eder ve bir sızıntının kaynağını izlemeniz gerekir.


1

Programınız ne kadar bellek ayıracağını önceden bilmiyorsa (bu nedenle yığın değişkenlerini kullanamazsınız). Bağlantılı listeler deyin, listelerin boyutu ne olduğunu bilmeden büyüyebilir. Bu nedenle, bir öbeğe ayırmak, içine kaç öğe ekleneceğinin farkında olmadığınızda bağlantılı bir liste için mantıklıdır.


0

Bazı durumlarda GC'nin bir avantajı diğerlerinde bir sıkıntıdır; GC'ye güvenmek, bu konuda fazla düşünmemeyi teşvik eder. Teoride, 'boşta kalma' dönemine kadar veya bant genişliğini çalacağı ve uygulamanızda yanıt gecikmesine neden olacağı zamana kadar beklemesi gerekir.

Ama 'düşünmemeniz' gerekmiyor. Çok iş parçacıklı uygulamalardaki diğer her şeyde olduğu gibi, verebileceğiniz zaman da verebilirsiniz. Örneğin, .Net'te bir GC istemek mümkündür; bunu yaparak, daha az sıklıkta daha uzun süre çalışan GC yerine, daha sık olarak daha kısa süre çalışan GC'ye sahip olabilir ve bu ek yük ile ilişkili gecikmeyi yayabilirsiniz.

Ancak bu, GC'nin "oto-mat-ic olduğu için fazla düşünmek zorunda kalmayacağı" gibi görünen ana çekiciliğini yener.

GC yaygınlaşmadan önce programlamaya maruz kaldıysanız ve malloc / free ve new / delete ile rahat olsaydınız, GC'yi biraz can sıkıcı ve / veya güvensiz bulursanız bile (biri güvensiz olabilir) optimizasyon ', damalı bir geçmişi vardır.) Birçok uygulama rastgele gecikmeyi tolere eder. Ancak, rastgele gecikmenin daha az kabul edilebilir olduğu uygulamalar için, yaygın bir tepki GC ortamlarından kaçınmak ve tamamen yönetilmeyen kod yönünde ilerlemek (veya uzun süre önce ölmekte olan bir sanat, montaj dili olan tanrı yasaklamak).

Burada bir yaz öğrencim vardı, GC'ye sütten kesilen stajyer, akıllı bir çocuk; GC'nin üstünlüğü konusunda o kadar kararlıydı ki yönetilmeyen C / C ++ 'da programlama yaparken bile malloc / free new / delete modelini takip etmeyi reddetti çünkü alıntı, "bunu modern bir programlama dilinde yapmak zorunda değilsiniz." Ve bilirsin? Küçük, kısa çalışan uygulamalar için gerçekten bundan kurtulabilirsiniz, ancak uzun süre çalışan performans uygulamaları için değil.


0

Yığın, derleyici tarafından ayrılan bir bellektir, programı derlediğimizde, varsayılan olarak derleyici işletim sisteminden bir miktar bellek ayırır (ayarları IDE'nizdeki derleyici ayarlarından değiştirebiliriz) ve işletim sistemi size belleği veren bellektir. sistemdeki birçok kullanılabilir bellekte ve diğer birçok şeyde ve bellek yığınına gelmek, kopyaladıkları bir değişkeni (formel olarak ref) beyan ettiğimizde tahsis edilir. ör: infix gösterimi: c = a + b; yığın itme sağdan sola itme, b istifleme, operatör, a istifleme ve bu i, ec istifleme işlemlerinin sonucu olarak yapılır. Düzeltme öncesi gösterimde: = + cab Burada tüm değişkenler 1. yıla (sağdan sola) itilir ve sonra işlem yapılır. Derleyici tarafından ayrılan bu bellek sabittir. Bu yüzden, uygulamamıza 1MB bellek tahsis edildiğini varsayalım, 700kb bellek kullanılan değişkenlerin (dinamik olarak tahsis edilmedikçe tüm yerel değişkenler yığına itildiğini) söyleyelim, kalan 324kb bellek yığına tahsis edilir. Ve bu yığının daha az yaşam süresi vardır, fonksiyonun kapsamı sona erdiğinde bu yığınlar temizlenir.

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.