Neden C ++ 'da soyut bir sınıf için sanal bir yıkıcı bildirmeliyim?


165

C ++ temel sınıflar için sanal yıkıcılar bildirmek için iyi bir uygulama olduğunu biliyorum, ama virtualarabirim olarak işlev soyut sınıflar için bile yıkıcılar bildirmek her zaman önemli mi? Lütfen bazı nedenleri ve nedenlerini belirtin.

Yanıtlar:


196

Bir arayüz için daha da önemlidir. Sınıfınızın herhangi bir kullanıcısı, somut uygulamaya bir işaretçi değil, muhtemelen arabirime bir işaretçi tutacaktır. Silmeye geldiklerinde, yıkıcı sanal değilse, türetilmiş sınıfın yıkıcısını değil, arabirimin yıkıcısını (veya belirtmediyseniz derleyici tarafından sağlanan varsayılanı) çağırırlar. Anlık bellek sızıntısı.

Örneğin

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}

4
delete ptanımlanmamış davranışı başlatır. Araması garanti edilmez Interface::~Interface.
Mankarse

@Mankarse: Neyin tanımlanmamasına neden olduğunu açıklayabilir misiniz? Eğer Derived kendi yıkıcısını uygulamadıysa, hala tanımlanmamış bir davranış olur mu?
Mart'ta Ponkadoodle

14
@Wallacoloo: özellikleri nedeniyle herhangi tanımlanmamış [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Derived'in dolaylı olarak oluşturulmuş bir yıkıcı kullanması hâlâ tanımsız olacaktır.
Mankarse

37

Sorunuzun cevabı genellikle, ancak her zaman değil. Soyut sınıfınız istemcilere bir işaretçide silme çağrısını yasaklarsa (veya belgelerinde böyle diyorsa), sanal bir yıkıcı bildirmemekte özgürsünüz.

İmha ediciyi korumalı hale getirerek istemcilerin bir işaretçide silme çağrısını yasaklayabilirsiniz. Bu şekilde çalışırken, sanal bir yıkıcıyı atlamak tamamen güvenli ve makul.

Sonunda sanal bir yöntem tablosu ile sonuçlanmayacak ve müşterilerinize bir işaretçi aracılığıyla silinemez olma niyetinizi bildireceksiniz, böylece bu durumlarda sanal olarak beyan etmemeniz için gerçekten nedeniniz var.

[Bu makaledeki 4. maddeye bakın: http://www.gotw.ca/publications/mill18.htm ]


Yanıtınızı çalıştırmanın anahtarı "silme işleminin yapılmadığı" dır. Genellikle arayüz olarak tasarlanmış soyut bir temel sınıfınız varsa, arayüz sınıfında silme çağrılır.
John Dibling

Yukarıda John'un işaret ettiği gibi, önerdiğiniz şey oldukça tehlikelidir. Arayüzünüzün istemcilerinin yalnızca temel türü bilen bir nesneyi asla yok etmeyeceği varsayımına güveniyorsunuz. Sanal değilse, soyut sınıfın dtor'unu korumaktır.
Michel

Michel, ben de öyle dedim :) "Bunu yaparsanız, yıkıcınızı korursunuz. Bunu yaparsanız, istemciler bu arayüze bir işaretçi kullanarak silemezler." ve aslında istemcilere güvenmiyor, ama istemcilere "yapamazsınız ..." diyerek bunu zorlaması gerekiyor. Herhangi bir tehlike görmüyorum
Johannes Schaub - litb

Şimdi cevabımın zayıf ifadesini düzelttim. istemcilere güvenmediğini açıkça belirtiyor. aslında bir şey yapmak istemcilere güvenmek zaten yol olduğunu açık olduğunu düşündüm. teşekkürler :)
Johannes Schaub - litb

2
Temel sınıfa bir işaretçiyi silerken yanlışlıkla yanlış yıkıcıyı çağırmak sorununun diğer "çıkış yolu" olan korunan yıkıcılardan bahsetmek için +1.
j_random_hacker

23

Biraz araştırma yapmaya karar verdim ve cevaplarınızı özetlemeye çalıştım. Aşağıdaki sorular, ne tür bir yıkıcıya ihtiyacınız olduğuna karar vermenize yardımcı olacaktır:

  1. Sınıfınız temel sınıf olarak kullanılmak üzere mi tasarlandı?
    • Hayır: Sınıfın her bir nesnesindeki v-işaretleyiciden kaçınmak için herkese açık sanal olmayan yıkıcı bildiriniz * .
    • Evet: Sonraki soruyu okuyun.
  2. Temel sınıfınız soyut mu? (yani herhangi bir sanal saf yöntem?)
    • Hayır: Sınıf hiyerarşinizi yeniden tasarlayarak temel sınıfınızı soyut yapmaya çalışın
    • Evet: Sonraki soruyu okuyun.
  3. Bir temel işaretçi aracılığıyla polimorfik silme işlemine izin vermek istiyor musunuz?
    • Hayır: İstenmeyen kullanımları önlemek için korumalı sanal yıkıcı beyan edin.
    • Evet: Herkese açık sanal yıkıcı beyan et (bu durumda ek yük yok).

Umarım bu yardımcı olur.

* C ++ 'da bir sınıfı nihai (alt sınıf dışı) olarak işaretlemenin bir yolu olmadığını belirtmek önemlidir, bu nedenle yıkıcıyı sanal olmayan ve herkese açık bir şekilde beyan etmeye karar vermeniz durumunda, diğer programcılarınızı karşı uyarmayı unutmayın. sınıfından türetmek.

Referanslar:


11
Bu cevap kısmen modası geçmiş, şimdi C ++ 'da son bir anahtar kelime var.
Étienne

10

Evet her zaman önemlidir. Türetilmiş sınıflar, bellek tahsis edebilir veya nesne yok edildiğinde temizlenmesi gereken diğer kaynaklara başvuruda bulunabilir. Arabirimlerinize / soyut sınıflarınıza sanal yıkıcılar vermezseniz, türetilmiş bir sınıf örneğini temel sınıf tanıtıcısı ile her sildiğinizde, türetilmiş sınıfınızın yıkıcısı çağrılmaz.

Böylece, bellek sızıntıları potansiyelini açıyorsunuz

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted

Doğru, aslında bu örnekte, sadece bellek sızıntısı değil, aynı zamanda çökmesi de olabilir: - /
Evan Teran

7

Her zaman gerekli değildir , ancak iyi bir uygulama olduğunu düşünüyorum. Yaptığı şey, türetilmiş bir nesnenin temel tipteki bir işaretçi ile güvenli bir şekilde silinmesine izin vermesidir.

Yani mesela:

Base *p = new Derived;
// use p as you see fit
delete p;

Basesanal bir yıkıcı yoksa , kötü biçimlendirilmiş , çünkü nesneyi a Base *.


boost :: shared_pointer <Base> p (yeni Derived) gibi görünmesi için boost :: shared_pointer p (yeni Derived) öğesini düzeltmek istemiyor musunuz; ? belki ppl cevabınızı anlayacak ve oy verecek
Johannes Schaub - litb

DÜZENLEME: Litb önerildiği gibi açılı ayraçları görünür yapmak için birkaç parça "kodlandı".
j_random_hacker

@EvanTeran: Yanıtın asıl gönderildiğinden bu durumun değişip değişmediğinden emin değilim ( boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm adresindeki Boost belgeleri olabilir), ancak bu doğru değil Bu günlerde shared_ptrnesneyi sanki a. silmeye çalışacaktır Base *- yarattığınız şeyin türünü hatırlar. Referans verilen bağlantıya, özellikle de "Yıkıcı, T'nin sanal bir yıkıcıya sahip olmasa veya geçersiz olsa bile orijinal tipiyle birlikte aynı işaretçiyle silme çağrısı" der.
Stuart Golodetz

@StuartGolodetz: Hmm, haklı olabilirsin, ama dürüstçe emin değilim. Sanal yıkıcı eksikliğinden dolayı bu bağlamda hala hasta olabilir . İçine bakmaya değer.
Evan Teran

@EvanTeran: Yararlı olması durumunda - stackoverflow.com/questions/3899790/shared-ptr-magic .
Stuart Golodetz

5

Bu sadece iyi bir uygulama değil. Herhangi bir sınıf hiyerarşisi için kural # 1'dir.

  1. C ++ içindeki bir hiyerarşinin en temel sınıfının sanal bir yıkıcıya sahip olması gerekir

Şimdi Neden için. Tipik hayvan hiyerarşisini ele alalım. Sanal yıkıcılar, diğer herhangi bir yöntem çağrısında olduğu gibi sanal dağıtımdan geçer. Aşağıdaki örneği ele alalım.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Hayvan'ın soyut bir sınıf olduğunu varsayalım. C ++ 'ın çağırmak için uygun yıkıcı bilmenin tek yolu sanal yöntem dağıtımıdır. Yıkıcı sanal değilse, Animal'in yıkıcısını çağırır ve türetilmiş sınıflardaki herhangi bir nesneyi yok etmez.

Yıkıcıyı temel sınıfta sanal yapmanın nedeni, seçimi türetilmiş sınıflardan kaldırmasıdır. Yıkıcıları varsayılan olarak sanal hale gelir.


2
Ben çoğunlukla çünkü size katılıyorum genellikle bir temel sınıf işaretçi / referans kullanarak Türetilmiş nesneye başvurmak için mümkün istiyorum bir hiyerarşi tanımlarken. Ancak bu her zaman böyle değildir ve diğer durumlarda, bunun yerine temel sınıf dtor'un korunmasını sağlamak yeterli olabilir.
j_random_hacker

@j_random_hacker onu koruma altına almak sizi yanlış dahili silmelerden korumaz
JaredPar

1
@JaredPar: Bu doğru, ama en azından kendi kodunuzda sorumlu olabilirsiniz - zor olan şey, müşteri kodunun kodunuzun patlamasına neden olamayacağından emin olmaktır . (Benzer şekilde, bir veri üyesini özel yapmak dahili kodun o
üyeyle

@j_random_hacker, bir blog gönderisiyle yanıt verdiğim için üzgünüm, ancak bu senaryoya gerçekten uyuyor. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar

@JaredPar: Mükemmel mesaj, özellikle perakende koddaki sözleşmeleri kontrol etme konusunda% 100 katılıyorum. Demek istediğim, sanal bir dtor'a ihtiyacınız olmadığını bildiğiniz durumlar var. Örnek: şablon dağıtımı için etiket sınıfları. Onlar 0 boyutu var, sadece uzmanlık belirtmek için miras kullanın.
j_random_hacker

3

Cevap basit, sanal olması gerekiyor, aksi takdirde temel sınıf tam bir polimorfik sınıf olmaz.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

Yukarıdaki silme işlemini tercih edersiniz, ancak taban sınıfın yıkıcısı sanal değilse, yalnızca taban sınıfın yıkıcısı çağrılır ve türetilmiş sınıftaki tüm veriler geri alınmaya devam eder.

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.