C ++ yıkıcı ne zaman çağrılır?


118

Temel Soru: Bir program ne zaman C ++ 'da bir sınıf' yıkıcı yöntemini çağırır? Bir nesne kapsam dışına çıktığında veya bir nesneye maruz kaldığında çağrıldığı söylendi.delete

Daha spesifik sorular:

1) Nesne bir işaretçi aracılığıyla oluşturulmuşsa ve bu işaretçi daha sonra silinmişse veya işaret etmesi için yeni bir adres verilmişse, işaret ettiği nesne yıkıcısını çağırıyor mu (başka hiçbir şeyin onu işaret etmediğini varsayarsak)?

2) 1. soruyu takiben, bir nesnenin kapsam dışına çıktığı zamanı tanımlayan şey (bir nesnenin belirli bir {blok} 'dan ne zaman ayrıldığı ile ilgili değil). Diğer bir deyişle, bağlantılı listedeki bir nesneye yıkıcı ne zaman çağrılır?

3) Bir yıkıcıyı manuel olarak çağırmak ister miydiniz?


3
Özel sorularınız bile çok geniş. "Bu işaretçi daha sonra silinir" ve "işaret etmek için yeni bir adres verilir" oldukça farklıdır. Daha fazla arama yapın (bunlardan bazıları yanıtlanmıştır) ve bulamadığınız kısımlar için ayrı sorular sorun.
Matthew Flaschen

Yanıtlar:


74

1) Nesne bir işaretçi aracılığıyla oluşturulmuşsa ve bu işaretçi daha sonra silinmişse veya işaret etmesi için yeni bir adres verilmişse, işaret ettiği nesne yıkıcısını çağırıyor mu (başka hiçbir şeyin onu işaret etmediğini varsayarsak)?

İşaretçilerin türüne bağlıdır. Örneğin, akıllı işaretçiler genellikle nesnelerini silindiğinde siler. Sıradan işaretçiler değil. Aynısı, bir işaretçinin farklı bir nesneyi göstermesi için yapıldığında da geçerlidir. Bazı akıllı işaretçiler eski nesneyi yok eder veya daha fazla referansı yoksa yok eder. Sıradan işaretçilerin böyle bir zekası yoktur. Sadece bir adres tutarlar ve özellikle bunu yaparak gösterdikleri nesneler üzerinde işlem yapmanıza izin verirler.

2) 1. soruyu takiben, bir nesnenin kapsam dışına çıktığı zamanı tanımlayan şey (bir nesnenin belirli bir {blok} 'dan ne zaman ayrıldığı ile ilgili değil). Diğer bir deyişle, bağlantılı listedeki bir nesneye yıkıcı ne zaman çağrılır?

Bu, bağlantılı listenin uygulanmasına bağlıdır. Tipik koleksiyonlar, yok edildiklerinde içerdikleri tüm nesneleri yok ederler.

Bu nedenle, bağlantılı bir işaretçiler listesi tipik olarak işaretçileri yok eder, ancak işaret ettikleri nesneleri yok eder. (Doğru olabilir. Başka işaretçilerin referansları olabilirler.) Bununla birlikte, özellikle işaretçileri içermek üzere tasarlanmış bağlantılı bir liste, nesneleri kendi yok ederek silebilir.

Akıllı işaretçilerin bağlantılı bir listesi, işaretçiler silindiğinde nesneleri otomatik olarak silebilir veya daha fazla referansları yoksa bunu yapabilir. İstediğinizi yapan parçaları seçmek size kalmış.

3) Bir yıkıcıyı manuel olarak çağırmak ister miydiniz?

Elbette. Örnek olarak, bir nesneyi aynı türden başka bir nesneyle değiştirmek, ancak belleği yeniden ayırmak için boşaltmak istemiyorsanız verilebilir. Eski nesneyi yerinde yok edebilir ve yerine yenisini inşa edebilirsiniz. (Ancak, genellikle bu kötü bir fikirdir.)

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}

2
Son örneklerinizin bir işlev ilan ettiğini sanıyordum? Bu "en can sıkıcı çözümleme" nin bir örneğidir. (Daha önemsiz olan diğer bir nokta da new Foo()büyük 'F' ile kastettiğinizi tahmin ettiğimdir .)
Stuart Golodetz

1
Bence Foo myfoo("foo")Most Vexing Parse değil, ama char * foo = "foo"; Foo myfoo(foo);öyle.
Cosine

Aptalca bir soru olabilir ama daha delete myFooönce çağrılmamalı Foo *myFoo = new Foo("foo");mı? Yoksa yeni oluşturulan nesneyi silersiniz, değil mi?
Matheus Rocha

Çizgiden myFooönce yok Foo *myFoo = new Foo("foo");. Bu çizgi, myFoovar olanı gölgeleyen yepyeni bir değişken yaratır . Ancak bu durumda, myFooyukarıdakiler ifsona eren kapsam dahilinde olduğundan mevcut bir tane yoktur .
David Schwartz

1
@galactikuh "Akıllı işaretçi", bir nesneye işaretçi gibi davranan, ancak aynı zamanda o nesnenin ömrünü yönetmeyi kolaylaştıran özelliklere sahip bir şeydir.
David Schwartz

20

Diğerleri zaten diğer sorunları ele aldı, bu yüzden sadece bir noktaya bakacağım: bir nesneyi manuel olarak silmek istiyor musunuz?

Cevap Evet. @DavidSchwartz bir örnek verdi, ancak oldukça sıra dışı bir örnek. Pek çok C ++ programcısının her zaman kullandığı şeyin altını çizen bir örnek vereceğim: std::vector(vestd::dequePek çok pek kullanılmasa da).

Çoğu kişinin bildiği gibi, std::vectormevcut tahsisinin tutabileceğinden daha fazla öğe eklerseniz / eklediğinizde daha büyük bir bellek bloğu ayıracaktır. Ancak bunu yaptığında, şu anda vektörde olandan daha fazla nesneyi tutabilen bir bellek bloğuna sahiptir .

Bunu yönetmek için, vectorkapakların altında yapılan şey , nesne aracılığıyla ham bellek tahsis etmektir Allocator(aksi belirtilmedikçe, kullandığı anlamına gelir ::operator new). Ardından, push_backöğesine bir öğe eklemek için (örneğin) vectorkullandığınızda, dahili olarak vektör placement new, bellek alanının (daha önce) kullanılmamış kısmında bir öğe oluşturmak için a kullanır .

Şimdi, erasevektörden bir öğe olursanız / olursanız ne olur ? Sadece kullanamaz delete- bu tüm bellek bloğunu serbest bırakır; diğerlerini yok etmeden veya kontrol ettiği bellek bloklarından herhangi birini serbest bırakmadan o bellekteki bir nesneyi yok etmesi gerekir (örneğin, erasebir vektörden 5 öğe, ardından hemen push_back5 öğe daha alırsanız , vektörün yeniden tahsis edilmeyeceği garanti edilir. bunu yaptığınızda hafıza.

Bunu yapmak için vektör, kullanarak değil , açıkça yıkıcıyı çağırarak bellekteki nesneleri doğrudan yok eder delete.

Muhtemelen, bir başkası kabaca bir vectoryaptığı gibi bitişik depolamayı kullanarak bir kap yazacak olsaydı (veya std::dequegerçekten yaptığı gibi bunun bir çeşidini ), neredeyse kesinlikle aynı tekniği kullanmak istersiniz.

Örneğin, dairesel bir halka tampon için nasıl kod yazabileceğinizi düşünelim.

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

Standart konteynerlerin aksine, bu operator newve operator deletedoğrudan kullanır . Gerçek kullanım için, muhtemelen bir ayırıcı sınıf kullanmak istersiniz, ancak şu an için dikkatinizi dağıtmak için katkıda bulunmaktan daha fazlasını yapacaktır (IMO, neyse).


9
  1. İle bir nesne oluşturduğunuzda new, aramaktan sorumlusunuz delete. İle bir nesne oluşturduğunuzda make_shared, sonuç shared_ptr, sayım ve arama yapmaktan sorumludur.delete kullanım sayacı sıfıra gittiğinde .
  2. Kapsam dışına çıkmak, bir bloktan çıkmak anlamına gelir. Bu amaç olduğunu varsayarak, yıkıcı çağrıldığında olduğu değil tahsisnew (yani bir yığın nesnesi olduğu) .
  3. Bir yıkıcıyı açıkça çağırmanız gereken tek zaman, nesneyi bir yerleşimlenew ayırdığınız zamandır .

1
Açıkça düz işaretçiler için olmasa da, referans sayımı (paylaşılan_tr) vardır.
Pubby

1
@Pubby: İyi nokta, iyi uygulamayı destekleyelim. Cevap düzenlendi.
MSalters

6

1) Nesneler 'işaretçiler aracılığıyla' oluşturulmaz. 'Yeni' olduğunuz herhangi bir nesneye atanmış bir işaretçi var. Bunu kastettiğiniz varsayılırsa, işaretçide 'sil' olarak adlandırırsanız, işaretçinin başvurduğu nesneyi gerçekten siler (ve yok ediciyi çağırır). İşaretçiyi başka bir nesneye atarsanız, bir bellek sızıntısı olacaktır; C ++ 'daki hiçbir şey sizin için çöpünüzü toplamaz.

2) Bunlar iki ayrı sorudur. Bir değişken, içinde bildirildiği yığın çerçevesi yığından çıkarıldığında kapsam dışına çıkar. Bu genellikle bir bloktan çıktığınız zamandır. Bir yığın içindeki nesneler, yığın üzerindeki işaretçileri olabilir, ancak kapsam dışına çıkmaz. Bağlantılı bir listedeki bir nesnenin bir yıkıcısının çağrılacağını özellikle hiçbir şey garanti etmez.

3) Pek değil. Tersini önerebilecek Deep Magic olabilir, ancak tipik olarak 'yeni' anahtar kelimelerinizi 'sil' anahtar kelimelerinizle eşleştirmek ve kendi kendini düzgün bir şekilde temizlediğinden emin olmak için gereken her şeyi yok edicinize koymak istersiniz. Bunu yapmazsanız, yıkıcıyı, sınıfı kullanan herkese, o nesnenin kaynaklarını manuel olarak nasıl temizlemeleri gerektiği konusunda belirli talimatlarla yorumladığınızdan emin olun.


3

3. soruya ayrıntılı bir cevap vermek gerekirse: evet, dasblinkenlight'ın gözlemlediği gibi, yıkıcıyı açıkça, özellikle yeni bir yerleşimin karşılığı olarak çağırabileceğiniz (nadir) durumlar vardır.

Bunun somut bir örneğini vermek gerekirse:

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

Bu tür bir şeyin amacı, bellek tahsisini nesne yapımından ayırmaktır.


2
  1. İşaretçiler - Normal işaretçiler RAII'yi desteklemez. Açıkça belirtilmezse delete, çöp olur. Neyse ki C ++, bunu sizin için halleden otomatik işaretleyicilere sahiptir!

  2. Kapsam - Bir değişkenin programınızda görünmez hale geldiğini düşünün . {block}Sizin de belirttiğiniz gibi, genellikle bu sondadır .

  3. Manuel imha - Bunu asla denemeyin. Bırakın dürbün ve RAII sihri sizin için yapsın.


Bir not: auto_ptr, bağlantınızda bahsedildiği için kullanımdan kaldırılmıştır.
tnecniv

std::auto_ptrC ++ 11'de kullanımdan kaldırıldı, evet. OP gerçekten C ++ 11'e sahipse, std::unique_ptrtek sahipler std::shared_ptriçin veya referans sayılan birden çok sahip için kullanmalıdır.
chrisaycock

'Manuel imha - Bunu asla denemeyin'. Sıklıkla, derleyicinin anlamadığı bir sistem çağrısı kullanarak nesne işaretçileri farklı bir iş parçacığına sıraya koyuyorum. Kapsam / otomatik / akıllı işaretleyicilere 'güvenmek', uygulamalarımın, tüketici iş parçacığı tarafından işlenmeden önce çağıran iş parçacığı tarafından silindiği için uygulamalarımın feci şekilde başarısız olmasına neden olur. Bu sorun, kapsam sınırlı ve refCounted nesneleri ve arabirimleri etkiler. Yalnızca işaretçiler ve açık silme işe yarar.
Martin James

@MartinJames Derleyicinin anlamadığı bir sistem çağrısı örneği gönderebilir misiniz? Ve kuyruğu nasıl uyguluyorsunuz? Değil std::queue<std::shared_ptr>?ben buldum pipe()çok daha kolay, kopyalama çok pahalı değilse bir üretici ve tüketici iplik yapmak eşzamanlılık arasında.
chrisaycock

myObject = yeni sınıfım (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James

1

"Yeni" yi her kullandığınızda, yani işaretçiye bir adres ekleyin veya öbek üzerinde yer talep ettiğinizde, onu "silmeniz" gerekir.
1. evet, bir şeyi sildiğinizde, yıkıcı çağrılır.
2. Bağlantılı listenin yıkıcısı çağrıldığında, nesnenin yıkıcısı çağrılır. Ancak işaretçilerse, manuel olarak silmeniz gerekir. 3. Alan "yeni" tarafından talep edildiğinde.


0

Evet, bir nesne yığın üzerindeyse kapsam dışına çıktığında veya deletebir nesneye bir işaretçiyi çağırdığınızda bir yıkıcı (dtor olarak da bilinir) çağrılır .

  1. İşaretçi aracılığıyla silinirse delete, yönlendirici çağrılacaktır. İşaretçiyi önce çağırmadan yeniden deleteatarsanız, nesne hala bir yerde bellekte mevcut olduğundan bellek sızıntısı yaşarsınız. İkinci durumda, dtor çağrılmaz.

  2. İyi bir bağlantılı liste uygulaması, liste yok edilirken listedeki tüm nesnelerin dtor'unu çağırır (çünkü onu yok etmek için bir yöntem çağırdınız veya kapsam dışına çıktı). Bu, uygulamaya bağlıdır.

  3. Bundan şüpheliyim, ama orada garip bir durum varsa şaşırmam.


1
"Eğer işaretçiyi önce delete'u çağırmadan yeniden atarsanız, nesne hala bellekte bir yerde var olduğundan bellek sızıntısı yaşarsınız." Şart değil. Başka bir işaretçi ile silinebilirdi.
Matthew Flaschen

0

Nesne bir işaretçi aracılığıyla oluşturulmamışsa (örneğin, A a1 = A ();), yıkıcı, nesne yok edildiğinde, her zaman nesnenin bulunduğu işlev tamamlandığında çağrılır. Örneğin:

void func()
{
...
A a1 = A();
...
}//finish


yıkıcı, kod "bitiş" satırına çalıştırıldığında çağrılır.

Nesne bir işaretçi aracılığıyla oluşturulursa (örneğin, A * a2 = new A ();), imleç silindiğinde yıkıcı çağrılır (a2'yi silin;) Eğer nokta kullanıcı tarafından açıkça silinmezse veya bir yeni adres silinmeden önce, hafıza sızıntısı meydana geldi. Bu bir hata.

Bağlantılı bir listede, eğer std :: list <> kullanırsak, desctructor veya bellek sızıntısı ile ilgilenmemize gerek yoktur çünkü std :: list <> bunların hepsini bizim için bitirmiştir. Kendi yazdığımız linkli bir listede desctructor'ı yazmalı ve göstericiyi açıkça silmeliyiz aksi takdirde hafıza sızıntısına neden olur.

Bir yıkıcıyı nadiren manuel olarak çağırırız. Sistemi sağlayan bir işlevdir.

Zavallı İngilizcem için üzgünüm!


Bir yıkıcıyı manuel olarak arayamayacağınız doğru değil - yapabilirsiniz (örneğin cevabımdaki koda bakın). Doğru olan, çoğu zaman yapmamanız gerektiğidir :)
Stuart Golodetz

0

Unutmayın ki bir nesnenin Oluşturucusu, bellek o nesne için tahsis edildikten hemen sonra çağrılır ve yıkıcı, o nesnenin belleğinin serbest bırakılmasından hemen önce çağrılı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.