C ++ 'daki çöplere ne olur?


51

Java, bir zamanlar Stops The Stops The World olan ancak bir yığındaki çöplerle ilgilenen otomatik bir GC'ye sahiptir. Şimdi C / C ++ uygulamaları bu STW donmalarına sahip değildir, bellek kullanımları da sonsuz şekilde artmaz. Bu davranışa nasıl ulaşılır? Ölü nesneler nasıl halledilir?


38
Not: Dünyayı durdur, bazı çöp toplayıcıların bir uygulama seçimidir, ancak kesinlikle hepsi değildir. Örneğin, mutatör ile aynı anda çalışan eşzamanlı GC'ler vardır (bu, GC geliştiricilerin gerçek program dediği şeydir). IBM'in eşzamanlı bir duraksız koleksiyoncu olan açık kaynaklı JVM J9'un ticari bir sürümünü satın alabileceğinizi düşünüyorum. Azul Zing olmayan bir "pauseless" kollektörü vardır aslında pauseless ama son derece hızlı hayır vardır, böylece göze çarpan duraklamalar (kendi GC duraklar genellikle duraklama olarak görmedim bir işletim sistemi iplik bağlam anahtarı, aynı siparişi verilmiş olan) .
Jörg W Mittag

14
Kullandığım (uzun koşu) C ++ programlarının çoğu bunu zamanla unboundedly büyür bellek kullanımını var. Programları bir seferde birkaç günden fazla açık bırakma alışkanlığınız yok mu?
Jonathan Cast,

12
Modern C ++ ve yapıları ile artık belleği manuel olarak silmenize gerek olmadığını unutmayın (bazı özel optimizasyonlardan sonra olmadıkça), çünkü dinamik belleği akıllı işaretçilerden yönetebilirsiniz. Açıkçası, C ++ gelişimine biraz ekler ve biraz daha dikkatli olmanız gerekir, ancak bu tamamen farklı bir şey değil, sadece elle çağırmak yerine akıllı işaretçi yapısını kullanmayı hatırlamanız gerekir new.
Andy,

9
Çöp toplayan bir dilde bellek sızıntısı yaşanmasının hala mümkün olduğunu unutmayın. Java'yı bilmiyorum ama bellek sızıntısı maalesef .NET’in yönetilen GC dünyasında oldukça yaygın. Statik bir alan tarafından dolaylı olarak referans alınan nesneler otomatik olarak toplanmaz, olay işleyicileri çok yaygın bir sızıntı kaynağıdır ve çöp toplama işleminin belirleyici olmayan niteliği, el ile serbest kaynaklara ihtiyaç duyulmasını tamamen ortadan kaldıramaz hale getirir (IDSposable'ın Desen). Bunların tümü, doğru bir şekilde kullanılan C ++ bellek yönetimi modelinin , çöp toplama işleminden çok daha üstün olduğunu söyledi.
Cody Gray

26
What happens to garbage in C++? Genellikle yürütülebilir bir dosyada derlenmez mi?
BJ Myers,

Yanıtlar:


100

Programcı, yarattıkları nesnelerin üzerinden newsilinmesini sağlamaktan sorumludur delete. Bir nesne yaratılır, ancak son işaretçi veya referanstan önce yok edilmezse, kapsam dışına çıkarsa, çatlaklardan düşer ve bir Bellek Kaçakı olur .

Ne yazık ki, C, C ++ ve bir GC içermeyen diğer diller için, bu zamanla basitçe yığılır. Bir uygulamanın veya sistemin belleğinin bitmesine ve yeni bellek blokları tahsis edememesine neden olabilir. Bu noktada, kullanıcının İşletim Sistemi'nin kullanılan belleği geri alabilmesi için uygulamayı sonlandırması gerekir.

Bu sorunu hafifletmek için, bir programcının hayatını çok daha kolaylaştıran birçok şey var. Bunlar öncelikle kapsamın doğası tarafından desteklenmektedir .

int main()
{
    int* variableThatIsAPointer = new int;
    int variableInt = 0;

    delete variableThatIsAPointer;
}

Burada iki değişken yarattık. Kıvrımlı kaşlı ayraçlar tarafından tanımlanan Blok Kapsamında bulunurlar {}. Yürütme bu kapsamın dışına çıktığında, bu nesneler otomatik olarak silinir. Bu durumda, variableThatIsAPointeradından da anlaşılacağı gibi, bellekteki bir nesneye işaretçidir. Kapsam dışına çıktığında, işaretçi silinir, ancak işaret ettiği nesne kalır. Burada, deletebellek sızıntısı olmadığından emin olmak için kapsam dışına çıkmadan önce bu nesneyi kullanıyoruz. Ancak bu işaretçiyi başka bir yere de geçirdik ve daha sonra silinmesini bekliyorduk.

Bu kapsamın doğası, sınıfları kapsar:

class Foo
{
public:
    int bar; // Will be deleted when Foo is deleted
    int* otherBar; // Still need to call delete
}

Burada aynı prensip geçerlidir. Ne barzaman Foosilineceği konusunda endişelenmemize gerek yok . Ancak otherBar, yalnızca işaretçi silinir. Eğer otherBarbu işaret ne olursa olsun nesne için tek geçerli işaretçi, biz muhtemelen gerektiğini deletebunun içinde Foo'nin destructor. Bu RAII'nin arkasındaki itici konsept

kaynak tahsisi (edinme), yapıcı tarafından nesne oluşturma (özellikle başlatma) sırasında, kaynak tahsisi (serbest bırakma) nesne imha etme sırasında (özel olarak sonlandırma) yıkıcı tarafından yapılır. Bu nedenle kaynak, ilklendirme bittiğinde ve sonlandırma başladığında (kaynakları tutan bir sınıf değişmezdir) arasında tutulması ve yalnızca nesne canlı olduğunda tutulacağı garanti edilir. Dolayısıyla, hiçbir nesne sızıntısı yoksa, kaynak sızıntısı yoktur.

RAII aynı zamanda Akıllı İşaretçilerin arkasındaki tipik itici güçtür . C ++ Standart kütüphane olarak, bunlar std::shared_ptr, std::unique_ptrve std::weak_ptr; Her ne kadar gördüğüm ve diğer kullanmış shared_ptr/ weak_ptraynı kavramları izleyin uygulamaları. Bunlar için bir referans sayacı, belirli bir nesneye kaç tane işaretçi bulunduğunu izler ve bir deletekez daha referans alınmadığında nesneyi otomatik olarak siler.

Bunun ötesinde, her şey bir programcının kodlarının nesneleri doğru bir şekilde ele aldığından emin olmak için uygun uygulamalara ve disipline gelir.


4
üzerinden silindi delete- aradığım şey buydu. Muhteşem.
Ju Shua,

3
Yeni ve silme işlemlerinin çoğunun otomatik olarak yapılmasına izin veren c ++ ile sağlanan kapsam belirleme mekanizmalarını eklemek isteyebilirsiniz.
whatsisname, 16.06.2016

9
@whatsisname yeni ve silme otomatik değildir, birçok durumda
oluşmazlar

10
deleteOtomatik tarafından sizin için çağrılır akıllı işaretçiler bunları kullanmak eğer bir otomatik depolama kullanılamaz zaman bunları her defasında kullanmayı düşünmelisiniz böylece.
Marian Spanik

11
@JuShua Modern C ++ yazarken, aslında deleteuygulama kodunuzda olması gerekmeyeceğini (ve C ++ 14'ten itibaren, aynı şekilde new), ancak yığın nesnelerinin silinmesini sağlamak için akıllı işaretçiler ve RAII kullanmanız gerekmediğini unutmayın. std::unique_ptrtipi ve std::make_uniqueişlev doğrudan, en basit yedek vardır newve deleteuygulama kodu düzeyinde.
hyde

82

C ++ çöp toplama sistemine sahip değildir.

C ++ uygulamaları kendi atıklarını imha etmek için gereklidir.

Bunu anlamak için C ++ uygulama programcıları gereklidir.

Unuttukları zaman, sonuç "hafıza sızıntısı" olarak adlandırılır.


22
Cevabınız kesinlikle ne çöp ne de herhangi bir çöp içermediğinden emin ...
leftaroundabout

15
@leftaroundabout: Teşekkürler. Bunu bir iltifat olarak görüyorum.
John R. Strohm

1
Tamam bu çöp ücretsiz cevap aramak için bir anahtar kelime var: bellek sızıntısı. Aynı zamanda her nasılsa söz iyi olurdu newve delete.
Ruslan

4
Aynı zamanda geçerlidir @Ruslan mallocve freeya new[]ve delete[](Windows kullanıcısının gibi, ya da başka dağıtıcılar GlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, ve bellek sizin için ayrılan, ...) (örn aracılığıyla fopen).
user253751

43

C, C ++ ve Çöp Toplayıcı olmayan diğer sistemlerde, geliştiriciye dilin ve kütüphanelerinin hafızanın ne zaman yeniden kazanılabileceğini belirten olanaklar sunulur.

En temel tesis otomatik depolamadır . Çoğu zaman, dilin kendisi de öğelerin atılmasını sağlar:

int global = 0; // automatic storage

int foo(int a, int b) {
    static int local = 1; // automatic storage

    int c = a + b; // automatic storage

    return c;
}

Bu durumlarda, derleyici bu değerlerin ne zaman kullanılmayacağını bilmek ve bunlarla ilişkili depoları geri almaktan sorumludur.

Kullanırken dinamik depolama C, bellek geleneksel olarak tahsis edilir mallocve ile geri free. C ++ 'da hafıza geleneksel olarak tahsis edilir newve yeniden oluşturulur delete.

C ancak çağdaş C ++ kaçınan, çok yıllar içinde değişmedi newve deletetamamen ve (kendileri kullandıkları yerine kütüphane tesislerine dayanır newve deleteuygun şekilde):

  • Akıllı işaretçiler en meşhurlarıdır: std::unique_ptrvestd::shared_ptr
  • : ancak kaplar aslında çok daha yaygın olduğu std::string, std::vector, std::mapşeffaf, ... hepsi içten dinamik olarak tahsis yönetmek bellek

Konuşurken shared_ptr, bir risk var: eğer bir referanslar döngüsü oluşuyorsa ve kırılmıyorsa, bellek sızıntısı olabilir. Bu durumdan kaçınmak geliştiriciye kalmıştır, en basit yol shared_ptrtamamen kaçınmaktır ve ikinci en basit yöntem ise tür düzeyinde döngüleri önlemektir.

Sonuç olarak, bellek sızıntıları C ++ 'ta , yeni kullanıcılar için bile new, deleteveya kullanmaktan kaçındıkları sürece bir sorun değildirstd::shared_ptr . Bu, sabit disiplinin gerekli olduğu ve genellikle yetersiz olduğu C'den farklıdır.


Ancak, bu cevap, bellek sızıntılarının ikiz kız kardeşinden bahsetmeden tam olmaz: sarkan işaretçiler .

Bir sarkan işaretçi (veya sarkan bir referans), bir işaretçi veya ölmüş olan bir cisme yapılan referansla tutulması ile yaratılan bir tehlikedir. Örneğin:

int main() {
    std::vector<int> vec;
    vec.push_back(1);     // vec: [1]

    int& a = vec.back();

    vec.pop_back();       // vec: [], "a" is now dangling

    std::cout << a << "\n";
}

Sarkan bir işaretçi veya referans kullanarak Tanımsız Davranış . Genel olarak, neyse ki, bu acil bir kazadır; oldukça sık, ne yazık ki, bu ilk önce hafıza bozulmasına neden olur ... ve zaman zaman garip davranışlar ortaya çıkar çünkü derleyici gerçekten tuhaf bir kod yayar.

Tanımsız Davranış , programların güvenliği / doğruluğu açısından, C ve C ++ ile bugüne kadarki en büyük sorundur. Rust'u Çöp Toplayıcı'sız ve Tanımsız Davranışı olmayan bir dil için denemek isteyebilirsiniz.


17
Re: "Sarkan bir işaretçi veya referans kullanarak Tanımsız Davranış . Genel olarak, neyse ki, bu hemen bir çarpışmadır": Gerçekten mi? Bu benim deneyimimle hiç uyuşmuyor; Aksine, benim deneyimim, sarkan bir göstergenin kullanımlarının neredeyse hiçbir zaman acil bir çökmeye neden olmamasıdır . . .
Ruakh

9
Evet, bir işaretçi "sarkan" olduğu için, bir işaretçi daha önce tahsis edilen belleği bir noktada hedef almış olmalı ve bu belleğin, artık erişilemeyecek şekilde olduğundan, işlemden tamamen çıkarılmış olması muhtemel değildir, çünkü derhal yeniden kullanım için iyi bir aday ... pratikte, sarkan işaretçiler çarpmalara neden olmaz, kaosa neden olurlar.
Leushenko

2
"Sonuç olarak, bellek sızıntıları C ++ 'ta bir sorun değil," Tabii ki, her zaman berbat olmak için kitaplıklara C bağlamalar, özyinelemeli shared_ptr'ler ve hatta özyinelemeli unique_ptr'ler ve diğer durumlar vardır.
Mooing Duck

3
“C ++ 'ta bir sorun değil, yeni kullanıcılar için bile” - “ Java benzeri bir dilden veya C'den gelmeyen yeni kullanıcılar ” olarak nitelendirebilirim.
leftaroundabout

3
@leftaroundabout: o "Onlar kullanmaktan kaçınmaya sürece kabiliyeti olan new, deleteve shared_ptr"; olmadan newve shared_ptrhiçbir sızıntı böylece doğrudan sahipliğini var. Tabii ki, sarkan sivri uçlar, vb. Olması muhtemeldir ... ama korkarım ki onlardan kurtulmak için C ++ 'dan ayrılmanız gerekiyor.
Matthieu M.

27

C ++, RAII adlı bir şeye sahiptir . Temel olarak, bir yığında bırakmak yerine temizleyicinin toparlanmasına izin vermek yerine, gittiğinizde çöpün temizlendiği anlamına gelir. (beni odamda futbolu izlerken hayal et - bira kutuları içip yenilerine ihtiyaç duyduğumda, C ++ yolu boş tenekeyi dolaba götürmek için, C # yolu yere vurmaktır ve temizlikçiye gelince hizmetçinin onları almasını bekleyin).

Şimdi C ++ 'da hafızaya sızmak mümkündür, ancak bunu yapmak için normal yapıları terk etmenizi ve bir şeyler yapmak için C yoluna geri dönmenizi gerektirir - bir hafıza bloğu tahsis etmeyi ve bu bloğun herhangi bir dil yardımı olmadan nerede olduğunu takip etmenizi gerektirir. Bazı insanlar bu işaretçiyi unutur ve bu nedenle bloğu kaldıramaz.


9
Paylaşılan işaretçiler (RAII kullanan) sızıntı oluşturmanın modern bir yolunu sağlar. A ve B nesnelerinin paylaşılan işaretçilerle birbirlerine referansta bulunduklarını ve A nesnesinin veya B nesnesine başka hiçbir şeyin referans olmadığını varsayalım. Sonuç bir sızıntıdır. Bu karşılıklı başvuru, çöp toplama olan dillerde sorun değildir.
David Hammen,

@DavidHammen emin, ancak emin olmak için hemen hemen her nesneyi çaprazlama pahasına. Akıllı işaretçilere ilişkin örneğiniz, akıllı işaretçinin kapsam dışına çıkacağı ve ardından nesnelerin serbest bırakılacağı gerçeğini görmezden gelir. Bir akıllı göstericinin bir gösterici gibi olduğunu, onun değil, çoğu parametre gibi yığında geçirilen bir nesne olduğunu varsayıyorsunuz. Bu, GC dillerinde neden olduğu bellek sızıntılarından çok farklı değildir. Örneğin, bir olay işleyicisini bir UI sınıfından kaldırmak, sessizce başvuruda bulunarak sızıntı yapan ünlü.
gbjbaanb

1
Akıllı işaretçilerle örnekteki @gbjbaanb, hiçbir akıllı işaretçi hiç kapsam dışına çıkmaz, bu yüzden bir sızıntı var. Akıllı işaretçi nesnelerinin her ikisi de, sözcüksel değil dinamik bir alanda tahsis edildiğinden , her biri diğerini tahrip etmeden önce beklemeye çalışır. Akıllı işaretçilerin C ++ 'da gerçek nesneler olduğu ve sadece işaretçilerin değil, burada tam olarak sızıntıya neden olduğu gerçeğidir - yığın kapsamlarındaki kap işaretçilerine de işaret eden ek akıllı işaretçi nesneleri, kendilerini yeniden imha ettikleri zaman kendilerini yok edemezler. sıfır olmayan.
Leushenko

2
.NET yolu yere itmek değildir . Hizmetçi gelene kadar olduğu yerde tutuyor. .NET, uygulamada belleği ayırma biçimi nedeniyle (sözleşmesel değil), yığın rasgele erişimli bir yığın gibidir. Bir nevi sözleşme ve kâğıt yığını kullanmak, ve artık geçerli olmayanları atmak için arada bir zaman geçirmek gibi. Ve bunu kolaylaştırmak için, her atmadan kurtulanlar farklı bir yığına yükseltilir, böylece çoğu zaman tüm yığınları geçmekten kaçınabilirsiniz - ilk yığın yeterince büyüymezse, hizmetçi diğerlerine dokunmaz.
Luaan

@Luaan bir benzetme oldu ... Sanırım temizlikçi gelene kadar masanın üzerinde yatan kutuları bıraktığını söylersem daha mutlu olursun.
gbjbaanb

26

C ++ 'da "manuel bellek yönetimi yapmanız gerektiğini" yaygın bir yanılgı olduğu belirtilmelidir. Aslında, kodunuzda genellikle herhangi bir bellek yönetimi yapmazsınız.

Sabit boyutlu nesneler (kapsam ömrü boyunca)

Bir nesneye ihtiyaç duyduğunuz durumlarda çoğu durumda, nesne programınızda tanımlanmış bir ömre sahip olacak ve yığında yaratılacaktır. Bu, tüm yerleşik ilkel veri türleri için değil, aynı zamanda sınıf ve yapı örnekleri için de geçerlidir:

class MyObject {
    public: int x;
};

int objTest()
{
    MyObject obj;
    obj.x = 5;
    return obj.x;
}

İşlev sona erdiğinde yığın nesneleri otomatik olarak kaldırılır. Java'da nesneler her zaman yığınta oluşturulur ve bu nedenle çöp toplama gibi bazı mekanizmalar tarafından kaldırılması gerekir. Bu, yığın nesneleri için sorun değildir.

Dinamik verileri yöneten nesneler (kapsam ömrü dahilinde)

Yığın üzerinde boşluk kullanmak, sabit boyutlu nesneler için işe yarar. Dizi gibi değişken bir alana ihtiyaç duyduğunuzda, başka bir yaklaşım kullanılır: Liste, dinamik belleği sizin için yöneten sabit boyutlu bir nesneye yerleştirilir. Bu işe yarar, çünkü nesneler özel bir temizleme fonksiyonuna, yıkıcıya sahip olabilir. Nesnenin kapsam dışına çıkması ve yapıcının tam tersi olması durumunda çağrılması garanti edilir:

class MyList {        
public:
    // a fixed-size pointer to the actual memory.
    int* listOfInts; 
    // constructor: get memory
    MyList(size_t numElements) { listOfInts = new int[numElements]; }
    // destructor: free memory
    ~MyList() { delete[] listOfInts; }
};

int listTest()
{
    MyList list(1024);
    list.listOfInts[200] = 5;
    return list.listOfInts[200];
    // When MyList goes off stack here, its destructor is called and frees the memory.
}

Hafızanın kullanıldığı kodda hiçbir hafıza yönetimi yoktur. Emin olmamız gereken tek şey, yazdığımız nesnenin uygun bir yıkıcıya sahip olduğudur. Kapsamı nasıl terk edersek gidelim listTest, istisna yoluyla ya da basitçe geri dönerek yıkıcı ~MyList()çağrılacak ve herhangi bir hafızayı yönetmemize gerek kalmayacak.

( İkili NOT işlecini, yıkıcıyı~ belirtmek için kullanmanın komik bir tasarım kararı olduğunu düşünüyorum . Sayılarda kullanıldığında bitleri tersine çevirir; analojide, burada yapıcının ne yaptığını gösterir.)

Temel olarak, dinamik belleğe ihtiyaç duyan tüm C ++ nesneleri bu kapsüllemeyi kullanır. Nesnelerin kendi içerikleriyle ilgilendikleri basit fikrini ifade etmenin oldukça tuhaf bir yolu olan RAII ("kaynak edinme başlangıçtır") olarak adlandırılmıştır; Elde ettikleri şey onların temizliği.

Polimorfik nesneler ve kapsam dışı ömür

Şimdi, her iki durumda da açıkça tanımlanmış bir ömrü olan hafıza içindi: Yaşam süresi kapsamı ile aynı. Kapsamdan çıktığımızda bir nesnenin süresinin dolmasını istemiyorsak, bizim için belleği yönetebilecek üçüncü bir mekanizma vardır: akıllı bir işaretçi. Akıllı işaretçiler, çalışma zamanında türünde değişiklik gösteren, ancak ortak bir arabirime veya temel sınıfa sahip olan nesnelerinizin örnekleri olduğunda da kullanılır:

class MyDerivedObject : public MyObject {
    public: int y;
};
std::unique_ptr<MyObject> createObject()
{
    // actually creates an object of a derived class,
    // but the user doesn't need to know this.
    return std::make_unique<MyDerivedObject>();
}

int dynamicObjTest()
{
    std::unique_ptr<MyObject> obj = createObject();
    obj->x = 5;
    return obj->x;
    // At scope end, the unique_ptr automatically removes the object it contains,
    // calling its destructor if it has one.
}

std::shared_ptrNesneleri çeşitli istemciler arasında paylaşmak için başka bir akıllı işaretçi var . İçerdikleri nesneyi yalnızca son istemci kapsam dışına çıktığında silerler; bu nedenle, ne kadar müşterinin olacağı ve nesneyi ne kadar süre kullanacakları tamamen bilinmediği durumlarda kullanılabilir.

Özet olarak, gerçekten herhangi bir manuel bellek yönetimi yapmadığınızı görüyoruz. Her şey kapsüllenir ve daha sonra tamamen otomatik, kapsam tabanlı bellek yönetimi ile halledilir. Bunun yeterli olmadığı durumlarda, ham belleği içine alan akıllı işaretçiler kullanılır.

Ham işaretçileri C ++ kodunun herhangi bir yerinde kaynak sahibi olarak, inşaatçıların dışındaki ham tahsisatların ve deleteimha dışında yapılan ham çağrıların, istisnalar ortaya çıktığında yönetmek neredeyse imkansız ve genellikle güvenli bir şekilde kullanılması neredeyse imkansızdır.

En iyisi: bu her tür kaynak için işe yarar

RAII'nin en büyük yararlarından biri hafızayla sınırlı olmamasıdır. Aslında dosyalar ve soketler (açma / kapama) ve muteksler (senkronizasyon / kilit açma) gibi senkronizasyon mekanizmaları gibi kaynakları yönetmek için çok doğal bir yol sağlar. Temel olarak, edinilebilecek ve serbest bırakılması gereken her kaynak C ++ ile aynı şekilde yönetilir ve bu yönetimin hiçbiri kullanıcıya bırakılmaz. Hepsi, kurucuda edinen ve yıkıcıya salıverilen sınıflarda kapsüllenir.

Örneğin, bir muteksi kilitleyen bir işlev genellikle C ++ dilinde şöyle yazılır:

void criticalSection() {
    std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
    doSynchronizedStuff();
} // myMutex is released here automatically

Diğer diller, bunu manuel olarak yapmanızı (örneğin bir finallycümlede) zorunlu kılmak suretiyle ya da bu problemi çözen uzmanlaşmış mekanizmalar ortaya koyar, ancak özellikle zarif bir şekilde değil (genellikle daha sonra, insanların yeterli olduğunda eksiklikten muzdarip). Bu mekanizmalar vardır denemek-ile-kaynaklar Java ve kullanma C ++ 'ın de ray değer yaklaşık her ikisi de C # açıklamada,.

Yani, özetlemek gerekirse, bunların hepsi C ++ 'da RAII'nin çok yüzeysel bir ifadesiydi, ancak okuyucunun C ++' da bellek ve hatta kaynak yönetiminin genellikle "manuel" değil aslında çoğunlukla otomatik olduğunu anlamalarına yardımcı olacağını umuyorum.


7
İnsanları yanlış anlamayan ya da C ++ 'ı boyayan, gerçekte olduğundan daha zor ya da tehlikeli olan tek cevap budur.
Alexander Revo

6
BTW, ham işaretçiyi kaynak sahibi olarak kullanmak sadece kötü bir uygulama olarak kabul edilir. İşaretçinin kendisini aşması garanti edilen bir şeye işaret ederlerse, bunları kullanma konusunda yanlış bir şey yoktur.
Alexander Revo

8
Ben ikinci İskender. "C ++ 'ın otomatik bellek yönetimi yok, unut gitsin deleteve ölürsün" yanıtını 30 puanın üzerinde toplayan ve kabul edildiğini görürsem şaşırdım . Burada gerçekten C ++ kullanan var mı?
Quentin

8

Özellikle C ile ilgili olarak, dil dinamik olarak atanmış belleği yönetmek için hiçbir araç vermez. Her *allocbirinin bir freeyerde bir mukabil olduğundan emin olmaktan kesinlikle sorumlusun .

İşlerin gerçekten kötüye gittiği yer, kaynak tahsisinin yarıda başarısız olduğu; tekrar dener misiniz, geriye mi dönüp baştan başlıyorsunuz, geri mi dönüyorsunuz ve bir hatayla çıkıyor musunuz, sadece düpedüz mü yapıyorsunuz ve işletim sisteminin bununla uğraşmasına izin veriyor musunuz?

Örneğin, burada bitişik olmayan bir 2D dizisi tahsis etme işlevi. Buradaki davranış, işlemin ortasında tahsisat hatası oluşursa, her şeyi geri alır ve NULL işaretçisi kullanarak bir hata göstergesi döndürürüz:

/**
 * Allocate space for an array of arrays; returns NULL
 * on error.
 */
int **newArr( size_t rows, size_t cols )
{
  int **arr = malloc( sizeof *arr * rows );
  size_t i;

  if ( arr ) // malloc returns NULL on failure
  {
    for ( i = 0; i < rows; i++ )
    {
      arr[i] = malloc( sizeof *arr[i] * cols );
      if ( !arr[i] )
      {
        /**
         * Whoopsie; we can't allocate any more memory for some reason.
         * We can't just return NULL at this point since we'll lose access
         * to the previously allocated memory, so we branch to some cleanup
         * code to undo the allocations made so far.  
         */
        goto cleanup;
      }
    }
  }
  goto done;

/**
 * We encountered a failure midway through memory allocation,
 * so we roll back all previous allocations and return NULL.
 */
cleanup:
  while ( i )         // this is why we didn't limit the scope of i to the for loop
    free( arr[--i] ); // delete previously allocated rows
  free( arr );        // delete arr object
  arr = NULL;

done:
  return arr;
}

Bu kod, bunlarla çok çirkindirgoto , ancak, herhangi bir yapılandırılmış istisna işleme mekanizmasının yokluğunda, bu sorunla başa çıkmadan, özellikle de kaynak tahsisat kodunuz daha fazla iç içe geçmişse, bu sorunla başa çıkmak için tek yoldur. bir döngüden daha derin. Bu gotoaslında çekici bir seçenek olan çok az zamandan biri ; Aksi takdirde bir sürü bayrak ve fazladan ififade kullanıyorsunuz

Her kaynak için tahsis edilmiş tahsis edici / tahsil edici işlevler yazarak, kendiniz için hayatı kolaylaştırabilirsiniz.

Foo *newFoo( void )
{
  Foo *foo = malloc( sizeof *foo );
  if ( foo )
  {
    foo->bar = newBar();
    if ( !foo->bar ) goto cleanupBar;
    foo->bletch = newBletch(); 
    if ( !foo->bletch ) goto cleanupBletch;
    ...
  }
  goto done;

cleanupBletch:
  deleteBar( foo->bar );
  // fall through to clean up the rest

cleanupBar:
  free( foo );
  foo = NULL;

done:
  return foo;
}

void deleteFoo( Foo *f )
{
  deleteBar( f->bar );
  deleteBletch( f->bletch );
  free( f );
}

1
Bu iyi bir cevap, gotoifadelerle bile . Bu bazı bölgelerde pratik tavsiye edilir. C'deki istisnaların eşdeğerine karşı korunmak için yaygın olarak kullanılan bir şemadır. gotoİfadelerle dolu ve sızdırmayan Linux çekirdek koduna bir bakın .
David Hammen,

"sadece tamamen kurtarılmadan" -> adalet içinde, eğer C hakkında konuşmak istiyorsanız, bu muhtemelen iyi bir uygulamadır. C, ya başka bir yerden gelen bellek bloklarını ele almak ya da küçük bellek parçalarını diğer prosedürlere ayırmak, ancak tercihen her ikisini de aynı zamanda bir araya eklenmiş şekilde yapmamak için kullanılan bir dildir . C’de klasik “nesneler” kullanıyorsanız, dili güçlü yönleriyle kullanmayacaksınız.
Leushenko

İkincisi gotoyabancı. Eğer değiştirdiyse, daha okunabilir olurdu goto done;etmek return arr;ve arr=NULL;done:return arr;karşı return NULL;. Her ne kadar daha karmaşık durumlarda goto, çok sayıda s olsa da , farklı hazırlık seviyelerinde ortaya çıkmaya başlar (C ++ 'da gevşeyen istisna yığınıyla ne yapılabilir).
Ruslan

2

Bellek konularını birkaç farklı kategoride sınıflandırmayı öğrendim.

  • Bir kez damlar. Bir programın başlangıçta yalnızca 100 bayt sızdığını, ancak bir daha hiç sızdırmadığını varsayalım. Bu tek seferlik sızıntıları kovalamak ve ortadan kaldırmak güzeldir (kaçak tespit özelliği ile temiz bir rapor hazırlamayı severim) ancak zorunlu değildir. Bazen saldırıya uğraması gereken daha büyük problemler var.

  • Tekrarlanan sızıntılar. Programların ömrü boyunca tekrar tekrar denilen bir fonksiyon, hafızayı düzenli olarak büyük bir problemden sızdıran bir kullanım ömrü. Bu damlamalar programa ve muhtemelen işletim sistemine ölümüne işkence edecek.

  • Karşılıklı referanslar. A ve B nesneleri paylaşılan işaretçilerle birbirlerine referans veriyorsa, bu sınıfların tasarımında veya döngüselliği kırmak için bu sınıfları uygulayan / kullanan kodda özel bir şey yapmanız gerekir. (Bu çöplerin toplandığı diller için bir sorun değildir.)

  • Çok fazla hatırlamak. Bu, kötü çöp kuzeni / bellek sızıntısı. RAII burada yardım etmeyecek, çöp toplayamayacak. Bu herhangi bir dilde bir sorundur. Bazı aktif değişkenlerin, onu rastgele bir bellek yığınına bağlayan bir yolu varsa, bu rastgele bellek parçası çöp değildir. Bir programın unutkan hale getirilmesi, birkaç gün boyunca çalışabilmesi için oldukça zordur. Birkaç ay boyunca çalışabilen bir program yapmak (örneğin, disk başarısız olana kadar) çok, çok zor.

Uzun süredir sızıntılarla ilgili ciddi bir problem yaşamadım. C ++ 'da RAII kullanımı bu damlama ve sızıntıların giderilmesine yardımcı olur. (Ancak, paylaşılan bir işaretçilere dikkat etmek gerekir.) Çok daha önemlisi, artık kullanımı olmayan belleğe olan ilgisiz bağlantılar nedeniyle bellek kullanımı büyümeye ve büyümeye devam eden uygulamalarla ilgili problemler yaşadım.


-6

Gerektiğinde kendi çöp toplama şeklini uygulamak C ++ programcısına kalmıştır. Bunu yapmamak, 'bellek sızıntısı' olarak adlandırılan şeyle sonuçlanacaktır. 'Yüksek düzeyli' dillerin (Java gibi) çöp toplama işlemi yapması oldukça yaygındır, ancak C ve C ++ gibi 'düşük seviye' dilleri yoktur.

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.