Menu

C++ Sanal Fonksiyonlar: Polimorfizm Açıklaması

Sanal fonksiyonlar, bir temel sınıf işaretçisinin çalışma zamanında türetilmiş sınıfın metot sürümünü çağırmasını sağlar. virtual, override, soyut sınıfları ve temel sınıfın yıkıcısının neden sanal olması gerektiğini öğrenin.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

Neden Tek Başına Kalıtım Yeterli Değil

Önceki sayfada bir sınıf hiyerarşisi oluşturmuştunuz: türetilmiş bir sınıf, temelinden üyeleri devralır. Ama bir püf nokta var. Bir metodu bir temel sınıf işaretçisi üzerinden çağırdığınızda, C++ hangi fonksiyonu çalıştıracağına nesnenin gerçek tipine değil, işaretçinin tipine göre karar verir. Yani aslında bir Dog'a işaret eden bir Animal* yine de Animal'ın metot sürümünü çağırır.

Bu neredeyse hiçbir zaman istediğiniz şey değildir. Çoğu zaman, her biri farklı bir türetilmiş nesneye işaret eden bir temel sınıf işaretçisi koleksiyonunuz olur ve her birinin kendisi gibi davranmasını istersiniz. Sanal fonksiyonlar bunu gerçekleştirir.

Nesne gerçekten bir Dog, ancak yine de a->speak() ifadesi Animal::speak()'i çalıştırdı. speak sanal olmadığı için derleyici fonksiyonu derleme zamanında statik Animal* tipinden seçti. Sanal fonksiyonların var olma nedeni tam da bu hatadır.

Bir Fonksiyonu Sanal Yapmak

Temel sınıf metoduna virtual anahtar kelimesini ekleyin. Artık çağrı, nesnenin gerçek tipine göre çalışma zamanında çözümlenir - işte bu dinamik gönderim.

Animal* üzerinde tek bir döngü, üç farklı davranış. Temel işaretçi çalışma zamanında gerçek tipi "bilir" ve buna göre gönderim yapar. Bu tek mekanizma - tek arayüz, çok sayıda uygulama - C++'ta polimorfizmin anlamıdır.

virtual'ın yalnızca temel bildirimde görünmesi gerektiğini unutmayın; bir fonksiyon sanal olduktan sonra, her türetilmiş sınıfta otomatik olarak sanal kalır. Onu türetilmiş sınıfta tekrar yazmak isteğe bağlı ve gereksizdir.

Her Zaman override Anahtar Kelimesini Kullanın

Yukarıdaki örnekte her türetilmiş metot override ile işaretlenmiştir. Kodun çalışması için isteğe bağlıdır, ancak onu zorunlu olarak ele almalısınız. override (C++11), derleyiciden gerçekten eşleşen bir imzayla bir temel sanal fonksiyonu geçersiz kıldığınızı doğrulamasını ister. İmzayı incelikle yanlış yaparsanız, sessiz bir hata yerine net bir hata alırsınız.

struct Animal {
    virtual void speak() const { }   // not: const
};

struct Dog : Animal {
    void speak() { }            // const DEĞİL - bu YENİ bir fonksiyon, geçersiz kılma değil!
    void speak() override { }   // hata: 'speak' geçersiz kılmıyor - size hemen söyler
};

override olmadan ilk speak() sorunsuz derlenir ama bir Animal* üzerinden asla çağrılmaz, çünkü imzası temelden farklıdır (const eksik). Geçersiz kılmanızın neden hiçbir şey yapmadığını merak ederek bir öğleden sonranızı harcardınız. override ile derleyici uyumsuzluğu anında yakalar. Onu geçersiz kılan her fonksiyona ekleyin.

Saf Sanal Fonksiyonlar ve Soyut Sınıflar

Bazen temel sınıfın mantıklı bir varsayılanı yoktur - genel bir "Animal" ne ses çıkarır? Bu durumda, fonksiyona = 0 atayarak onu saf sanal olarak tanımlayın. Bu, onu gövdesiz bırakır ve sınıfı tek başına örneği oluşturulamayan bir soyut sınıfa dönüştürür. Yalnızca türetilmiş sınıfların yerine getirmesi gereken bir arayüz tanımlamak için vardır.

Her somut alt sınıf area()'yı uygulamak zorundadır, aksi takdirde o da soyut kalır. C++ "arayüzleri" böyle ifade eder: yalnızca saf sanal fonksiyonlara sahip bir soyut sınıf, Java gibi dillerdeki bir arayüzün C++ karşılığıdır.

Sanal Yıkıcı Kuralı

Bu, herkesi en az bir kez tuzağa düşüren püf noktadır. Bir nesneyi bir temel sınıf işaretçisi üzerinden delete ettiğinizde, C++ bulduğu yıkıcıyı çağırır - ve o yıkıcı sanal değilse, yalnızca temel yıkıcıyı çalıştırır. Türetilmiş kısım asla yok edilmez ve sahip olduğu her şeyi sızdırır. Standart buna tanımsız davranış der.

Çözüm tek kelime: temel yıkıcıyı virtual yapın. O zaman delete p önce ~Derived'i, sonra ~Base'i çalıştırır, tam olması gerektiği gibi.

struct Base {
    virtual ~Base() { cout << "~Base\n"; }   // doğru
};
// şimdi: önce ~Derived sonra ~Base

Temel kural: Bir sınıfın herhangi bir sanal fonksiyonu olduğu an, ona bir de sanal yıkıcı verin. Bir sınıf, işaretçiler üzerinden kullanılan bir temel sınıf olarak tasarlandıysa, yıkıcısı sanal olmalıdır.

Sık Yapılan Hatalar ve Tuzaklar

Sanal fonksiyonlarla rahatladığınızda dikkat etmeniz gereken birkaç tuzak daha:

Nesne dilimleme (object slicing). Türetilmiş bir nesneyi bir temel değişkene değerle aktarır veya saklarsanız, türetilmiş kısım "dilimlenir" ve elinizde sade bir temel nesne kalır - sanal gönderim artık geçersiz kılmaya ulaşamaz. Polimorfizm için her zaman işaretçi veya referans kullanın:

Dog d;
Animal a = d;   // DİLİMLENDİ: a artık sadece bir Animal, Dog kısmı gitti
a.speak();      // sanal olmasına rağmen Animal::speak çalıştırır

Animal& ref = d;   // OK: referans gerçek tipi korur
ref.speak();       // Dog::speak çalıştırır

Yapıcılardan veya yıkıcılardan sanal fonksiyon çağırmayın. Yapım sırasında türetilmiş kısım henüz yoktur, bu nedenle sanal bir çağrı türetilmiş geçersiz kılmaya değil, mevcut sınıfın sürümüne çözümlenir - bu nadiren amaçladığınız şeydir.

Sanal gönderimin küçük bir maliyeti vardır. Her sanal çağrı, gizli bir fonksiyon işaretçileri tablosundan ("vtable") geçer, çağrı başına bir dolaylama. Ucuzdur ama bedava değildir, bu yüzden gerçekten geçersiz kılmaya ihtiyacınız olmadıkça bir fonksiyonu sanal yapmayın.

Temel sürümü bilerek çağırmak. Bir geçersiz kılma içinde temel uygulamayı Base::method() ile açıkça çağırmaya devam edebilirsiniz - türetilmiş davranış temeli değiştirmek yerine genişlettiğinde kullanışlıdır.

Sıradaki: Operatör Aşırı Yükleme

Sanal fonksiyonlar, nesnelerinizin paylaşılan bir arayüz üzerinden davranışlarını özelleştirmesini sağlar. Sonraki sayfa, nesnelerinize etki eden operatörleri nasıl özelleştireceğinizi gösterir: operatör aşırı yükleme ile kendi tiplerinize +, ==, << ve daha fazlasına yanıt vermeyi öğretebilirsiniz; böylece Vector + Vector veya cout << myObject ifadeleri yerleşik tiplerdeki kadar doğal okunur.

Sıkça Sorulan Sorular

C++'ta sanal fonksiyon nedir?

Sanal fonksiyon, bir temel sınıfta virtual anahtar kelimesiyle tanımlanan bir üye fonksiyondur; böylece onu bir temel sınıf işaretçisi veya referansı üzerinden çağırdığınızda C++, temel sürüm yerine türetilmiş sınıfın geçersiz kılma sürümünü çalıştırır. Bu çalışma zamanı seçimine dinamik gönderim (dynamic dispatch) denir ve polimorfizmin temelidir.

Sanal fonksiyon ile saf sanal fonksiyon arasındaki fark nedir?

Sanal fonksiyonun bir gövdesi vardır ve geçersiz kılınabilir. Saf sanal fonksiyon = 0 ile tanımlanır ve temel sınıfta gövdesi yoktur - her somut türetilmiş sınıfı bir uygulama sağlamaya zorlar. En az bir saf sanal fonksiyonu olan herhangi bir sınıf soyut bir sınıftır ve örneği oluşturulamaz.

C++'ta bir temel sınıfın neden sanal yıkıcıya ihtiyacı vardır?

Türetilmiş bir nesneyi bir temel sınıf işaretçisi üzerinden delete ederseniz ve temel yıkıcı sanal değilse, yalnızca temel yıkıcı çalışır - türetilmiş kısım hiçbir zaman temizlenmez, bu da kaynak sızıntısına yol açar ve tanımsız davranıştır. Polimorfik olarak kullanılması amaçlanan herhangi bir sınıfın yıkıcısını virtual yapın.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA