Neden 'yeni' kullanımı bellek sızıntılarına neden oluyor?


131

Önce C # öğrendim ve şimdi C ++ ile başlıyorum. Anladığım kadarıyla, newC ++ 'daki operatör C #' teki ile aynı değil.

Bu örnek kodda bellek sızıntısının nedenini açıklayabilir misiniz?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

Yanıtlar:


464

Ne oluyor

Yazdığınızda , otomatik depolama süresiT t; olan türde bir nesne oluşturuyorsunuz . Kapsam dışına çıktığında otomatik olarak temizlenecektir.T

Eğer yazarken new T()sen türünde bir nesne oluştururken Tile dinamik depolama süresi . Otomatik olarak temizlenmeyecek.

temizleme olmadan yeni

deleteTemizlemek için ona bir işaretçi iletmeniz gerekir :

silerek yeniler

Ancak, ikinci örneğiniz daha kötü: işaretçinin referansını kaldırıyorsunuz ve nesnenin bir kopyasını oluşturuyorsunuz. Bu şekilde ile oluşturulan nesnenin işaretçisini kaybedersiniz new, böylece isteseniz bile onu asla silemezsiniz!

deref ile newing

Ne yapmalısın

Otomatik saklama süresini tercih etmelisiniz. Yeni bir nesneye ihtiyacınız var, sadece şunu yazın:

A a; // a new object of type A
B b; // a new object of type B

Dinamik depolama süresine ihtiyacınız varsa, işaretçiyi, tahsis edilen nesneye, onu otomatik olarak silen bir otomatik depolama süresi nesnesinde saklayın.

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

automatic_pointer ile yenileştirme

Bu, çok açıklayıcı olmayan RAII ( Resource Acquisition Is Initialization ) adıyla anılan yaygın bir deyimdir . Temizlenmesi gereken bir kaynak aldığınızda, onu otomatik depolama süresi olan bir nesneye yapıştırırsınız, böylece temizleme konusunda endişelenmenize gerek kalmaz. Bu, bellek, açık dosyalar, ağ bağlantıları veya istediğiniz her şey gibi herhangi bir kaynak için geçerlidir.

Bu automatic_pointerşey zaten çeşitli şekillerde var, ben sadece bir örnek vermek için verdim. Adlı standart kitaplıkta çok benzer bir sınıf bulunmaktadır std::unique_ptr.

Ayrıca adında eski bir tane (C ++ 11 öncesi) var, auto_ptrancak garip bir kopyalama davranışına sahip olduğu için artık kullanımdan kaldırıldı.

Ve sonra std::shared_ptr, aynı nesneye birden çok işaretçiye izin veren ve yalnızca son işaretçi yok edildiğinde onu temizleyen daha akıllıca örnekler var .


4
@ user1131997: Buna başka bir soru sorduğunuza sevindim. Gördüğünüz gibi yorumlarda açıklamak çok kolay değil :)
R. Martinho Fernandes

@ R.MartinhoFernandes: mükemmel cevap. Sadece bir soru. Neden operatör * () işlevinde referansa göre döndürmeyi kullandınız?
Destructor

@Destructor geç cevap: D. Referans olarak geri dönmek, noktayı değiştirmenize izin verir, böylece, örneğin *p += 2normal bir işaretçi ile yaptığınız gibi yapabilirsiniz . Referans olarak dönmediyse, normal bir işaretçinin davranışını taklit etmezdi, buradaki niyet budur.
R. Martinho Fernandes

"İmleci, tahsis edilen nesneye, onu otomatik olarak silen otomatik bir depolama süresi nesnesinde saklamanızı" önerdiğiniz için çok teşekkür ederiz. Keşke kodlayıcıların herhangi bir C ++ 'yı derleyebilmeleri için bu kalıbı öğrenmelerini istemenin bir yolu olsaydı!
Andy

35

Adım adım açıklama:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

Yani bunun sonunda, yığın üzerinde işaretçisi olmayan bir nesne var, bu yüzden silmek imkansız.

Diğer örnek:

A *object1 = new A();

yalnızca deleteayrılan belleği unutursanız bir bellek sızıntısıdır :

delete object1;

C ++ 'da otomatik depolamalı nesneler, yığın üzerinde oluşturulanlar otomatik olarak atılan nesneler ve dinamik depolamalı nesneler yığın üzerinde, ayırdığınız newve kendinizi serbest bırakmanız gereken nesneler vardır delete. (bunların hepsi kabaca belirtilmiştir)

deleteTahsis edilen her nesne için bir tane almanız gerektiğini düşünün new.

DÜZENLE

Bir düşünün, object2bir hafıza sızıntısı olması gerekmiyor.

Aşağıdaki kod sadece bir noktaya değinmek içindir, bu kötü bir fikir, böyle bir kodu asla sevme:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

Bu durumda, otherreferansla aktarıldığı için, tam olarak işaret edilen nesne olacaktır new B(). Bu nedenle, &otherişaretçiyi kullanarak adresini almak ve silmek hafızayı boşaltacaktır.

Ama bunu yeterince vurgulayamam, bunu yapma. Sadece bir noktaya değinmek için burada.


2
Ben de aynı şeyi düşünüyordum: Sızdırmamak için hackleyebiliriz ama bunu yapmak istemezsiniz. Nesne1'in de sızması gerekmez, çünkü kurucusu kendisini bir noktada silecek bir tür veri yapısına ekleyebilir.
CashCow

2
Bu "bunu yapmak mümkün ama yapma" cevaplarını yazmak her zaman ÇOK caziptir! :-) Duyguları biliyorum
Kos

11

İki "nesne" verildiğinde:

obj a;
obj b;

Hafızada aynı yeri işgal etmezler. Diğer bir deyişle,&a != &b

Birinin değerini diğerine atamak konumlarını değiştirmez, ancak içeriğini değiştirir:

obj a;
obj b = a;
//a == b, but &a != &b

Sezgisel olarak, işaretçi "nesneler" aynı şekilde çalışır:

obj *a;
obj *b = a;
//a == b, but &a != &b

Şimdi örneğinize bakalım:

A *object1 = new A();

Bu değerini atayan new A()için object1. Değer bir işaretçi, yani object1 == new A()ama &object1 != &(new A()). (Bu örneğin geçerli bir kod olmadığını, sadece açıklama amaçlı olduğunu unutmayın)

İşaretçinin değeri korunduğu için, işaret ettiği belleği serbest bırakabiliriz: delete object1;Kuralımıza göre, bu delete (new A());, sızıntısı olmayan ile aynı şekilde davranır .


İkinci örnek için, işaret edilen nesneyi kopyalıyorsunuz. Değer, gerçek işaretçi değil, o nesnenin içeriğidir. Diğer her durumda olduğu gibi &object2 != &*(new A()).

B object2 = *(new B());

Ayrılan belleğin göstericisini kaybettik ve bu yüzden onu serbest bırakamayız. delete &object2;işe yarayacakmış gibi görünebilir, ancak &object2 != &*(new A())eşdeğer değildir delete (new A())ve o kadar da geçersizdir.


9

C # ve Java'da, herhangi bir sınıfın bir örneğini oluşturmak için new kullanırsınız ve daha sonra onu yok etme konusunda endişelenmenize gerek kalmaz.

C ++ ayrıca bir nesne oluşturan "new" anahtar kelimesine de sahiptir, ancak Java veya C # 'dan farklı olarak, bir nesne oluşturmanın tek yolu bu değildir.

C ++, bir nesne oluşturmak için iki mekanizmaya sahiptir:

  • otomatik
  • dinamik

Otomatik oluşturma ile nesneyi kapsamlı bir ortamda oluşturursunuz: - bir işlevde veya - bir sınıfın (veya yapının) bir üyesi olarak.

Bir işlevde bunu şu şekilde yaratırsınız:

int func()
{
   A a;
   B b( 1, 2 );
}

Bir sınıf içinde normalde şu şekilde oluşturursunuz:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

İlk durumda, osiloskop bloğundan çıkıldığında nesneler otomatik olarak yok edilir. Bu, bir işlev içinde bir işlev veya bir kapsam bloğu olabilir.

İkinci durumda, b nesnesi, üye olduğu A durumu ile birlikte yok edilir.

Nesnenin ömrünü kontrol etmeniz gerektiğinde nesneler yeni ile tahsis edilir ve daha sonra onu yok etmek için silinmesi gerekir. RAII olarak bilinen teknikle, nesneyi oluşturduğunuz noktada otomatik bir nesnenin içine koyarak silinmesini hallediyor ve bu otomatik nesnenin yıkıcısının devreye girmesini bekliyorsunuz.

Bu tür bir nesne, bir "silme" mantığını çağıran, ancak yalnızca nesneyi paylaşan paylaşılan_tr'nin tüm örnekleri yok edildiğinde paylaşılan bir paylaşılan_tr'dir.

Genel olarak, kodunuzun yeniye birçok çağrısı olabilirken, sınırlı sayıda silme çağrınız olmalıdır ve bunların akıllı işaretleyicilere yerleştirilen yıkıcılardan veya "silici" nesnelerden çağrıldığından her zaman emin olmalısınız.

Yıkıcılarınız da asla istisnalar atmamalıdır.

Bunu yaparsanız, birkaç bellek sızıntınız olur.


4
Daha fazlası var automaticve dynamic. Ayrıca var static.
Mooing Duck

9
B object2 = *(new B());

Bu hat, sızıntının sebebidir. Bunu biraz ayıralım ..

object2, 1 adresinde depolanan B tipi bir değişkendir (Evet, burada rasgele sayılar seçiyorum). Sağ tarafta, yeni bir B veya B tipi bir nesneye işaretçi istediniz. Program bunu size memnuniyetle verir ve yeni B'nizi adres 2'ye atar ve ayrıca adres 3'te bir işaretçi oluşturur. Şimdi, adres 2'deki verilere erişmenin tek yolu adres 3'teki işaretçi aracılığıyladır. Daha sonra *, işaretçinin işaret ettiği veriyi (adres 2'deki veri) almak için işaretçiye başvuruda bulunmadınız. Bu, bu verinin etkin bir şekilde bir kopyasını oluşturur ve onu adres 1'de atanan nesne2'ye atar. Unutmayın, bu bir KOPYA, orijinal değil.

Şimdi, sorun şu:

Aslında bu işaretçiyi kullanabileceğiniz hiçbir yerde saklamadınız! Bu atama bittiğinde, işaretçi (adres2'ye erişmek için kullandığınız adres3'teki bellek) kapsam dışında ve erişiminizin ötesindedir! Bundan sonra silme işlemini çağıramazsınız ve bu nedenle adres2'deki belleği temizleyemezsiniz. Elinizde kalan, adres1'deki adres2'deki verilerin bir kopyasıdır. Aynı şeylerden ikisi hafızada oturuyor. Birine erişebilir, diğerine erişemezsiniz (çünkü ona giden yolu kaybetmişsinizdir). Bu yüzden bu bir hafıza sızıntısı.

C # geçmişinizden gelmenizi, C ++ 'daki işaretçilerin nasıl çalıştığına dair çok şey okumanızı öneririm. İleri düzey bir konudur ve kavraması biraz zaman alabilir, ancak kullanımları sizin için çok değerli olacaktır.


8

Bunu kolaylaştıracaksa, bilgisayar belleğini bir otel gibi düşünün ve programlar, ihtiyaç duydukları zaman oda kiralayan müşterilerdir.

Bu otelin çalışma şekli, bir oda ayırtmanız ve kapı görevlisine ne zaman çıkacağınızı söylemenizdir.

Kitapları bir oda programlar ve kapı görevlisine haber vermeden ayrılırsanız, kapı görevlisi odanın hala kullanımda olduğunu düşünecek ve başka kimsenin kullanmasına izin vermeyecektir. Bu durumda bir oda sızıntısı var.

Programınız bellek ayırır ve onu silmezse (yalnızca kullanmayı bırakırsa), bilgisayar belleğin hala kullanımda olduğunu düşünür ve başka kimsenin kullanmasına izin vermez. Bu bir hafıza sızıntısı.

Bu tam bir benzetme değil ama yardımcı olabilir.


5
Bu benzetmeyi çok beğendim, mükemmel değil, ama kesinlikle yeni olan insanlara bellek sızıntılarını açıklamanın iyi bir yolu!
AdamM

1
Bunu bir İK kızına bellek sızıntılarını açıklamak için Londra'daki Bloomberg'de kıdemli bir mühendisle yaptığım röportajda kullandım. Bu röportajı bitirdim çünkü bellek sızıntılarını (ve iş parçacığı sorunlarını) programcı olmayan birine anlayabileceği şekilde açıklayabildim.
Stefan

7

Oluştururken object2size yeni oluşturulan nesnenin bir kopyasını oluşturursunuz, ama aynı zamanda (asla atanmış) işaretçisi kaybediyoruz (şimdiye sonradan silmek için bir yolu yoktur). Bundan kaçınmak için object2bir referans vermeniz gerekir .


3
Bir nesneyi silmek için bir referansın adresini almak inanılmaz derecede kötü bir uygulamadır. Akıllı bir işaretçi kullanın.
Tom Whittock

3
İnanılmaz derecede kötü bir uygulama, ha? Akıllı işaretçilerin perde arkasında ne kullandığını düşünüyorsunuz?
Blindy

3
@Blindy akıllı işaretçiler (en azından düzgün bir şekilde uygulanmış olanlar) doğrudan işaretçileri kullanır.
Luchian Grigore

2
Tamamen dürüst olmak gerekirse, Fikir o kadar da harika değil, değil mi? Aslında, OP'de denenen modelin nerede işe yarayacağından bile emin değilim.
Mario

7

Bir noktada newoperatörü kullanarak ayırdığınız hafızayı, deleteoperatöre bu hafızaya bir işaretçi ileterek boşaltmazsanız, bir hafıza sızıntısı yaratırsınız.

Yukarıdaki iki durumunuzda:

A *object1 = new A();

Burada deletehafızayı boşaltmak için kullanmıyorsunuz , bu nedenle object1işaretçiniz kapsam dışına çıkarsa hafıza sızıntısı yaşarsınız çünkü işaretçiyi kaybedersiniz ve bu nedenle deleteoperatörü kullanamazsınız .

Ve burada

B object2 = *(new B());

tarafından döndürülen işaretçiyi atıyorsunuz new B()ve bu nedenle deletebelleğin serbest bırakılması için bu işaretçiyi asla geçiremezsiniz . Dolayısıyla başka bir bellek sızıntısı.


7

Hemen sızan bu satır:

B object2 = *(new B());

Burada B, yığın üzerinde yeni bir nesne oluşturuyorsunuz, ardından yığın üzerinde bir kopya oluşturuyorsunuz. Yığın üzerinde tahsis edilmiş olana artık erişilemez ve bu nedenle sızıntıya neden olur.

Bu satır hemen sızdırmaz:

A *object1 = new A();

Asla eğer bir sızıntı olur deleted object1olsa.


4
Dinamik / otomatik depolamayı açıklarken lütfen yığın / yığın kullanmayın.
Pubby

2
@Pubby neden kullanmıyorsun? Dinamik / otomatik depolama nedeniyle yığın değil her zaman yığın mı? İşte bu yüzden stack / heap hakkında detay vermeye gerek yok, haklı mıyım?

4
@ user1131997 Yığın / yığın uygulama ayrıntılarıdır. Bilmeleri önemlidir, ancak bu soruyla alakaları yoktur.
Pubby

2
Hmm, buna ayrı bir cevap istiyorum, yani benimkiyle aynı ama yığın / yığını en iyi düşündüğünüz şeyle değiştiriyorum. Bunu nasıl açıklamayı tercih edeceğinizi öğrenmek isterim.
mattjgalloway
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.