Sanal Fonksiyonlar ve VTable
Coddy'nin C++ Journey'sinin Nesne Yönelimli Programlama bölümünün bir parçası — ders 52 / 104.
virtual anahtar kelimesi, önceki derste gördüğümüz sorunu çözer. Bir metodu virtual olarak bildirdiğinizde, C++ hangi sürümün çağrılacağını işaretçi türüne göre değil, çalışma zamanındaki gerçek nesne türüne göre belirler.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Çıktı: Woof!Artık doğru metot çağrılıyor! override anahtar kelimesi isteğe bağlıdır ancak önerilir. Derleyiciye sanal bir fonksiyonu geçersiz kılmayı amaçladığınızı bildirir ve imzalar eşleşmediğinde hataları yakalar.
Bu nasıl çalışır? Bir sınıf sanal fonksiyonlara (virtual functions) sahip olduğunda, derleyici bir sanal tablo (vtable) oluşturur. Bu, sınıfın sanal fonksiyonlarına yönelik işaretçiler (pointers) içeren gizli bir arama tablosudur. Sanal fonksiyonlara sahip her nesne, kendi sınıfının vtable'ına işaret eden gizli bir işaretçi (vptr) saklar.
Bir işaretçi aracılığıyla sanal bir fonksiyon çağırdığınızda, program çalışma zamanında vtable içindeki doğru fonksiyon adresine bakar. Buna dinamik sevkiyat (dynamic dispatch) denir. Normal fonksiyon çağrılarına kıyasla küçük bir ek yük getirir, ancak güçlü polimorfik davranışlar sağlar.
Önemli bir kural: Eğer bir sınıf sanal fonksiyonlara sahipse ve bir temel sınıf olarak kullanılacaksa, yıkıcı fonksiyonu (destructor) da sanal olmalıdır. Bu, türetilmiş nesneler temel sınıf işaretçileri (base pointers) üzerinden silindiğinde düzgün bir temizleme işlemi yapılmasını sağlar:
class Animal {
public:
virtual ~Animal() {} // Sanal yıkıcı
virtual void speak() {}
};Görev
KolaySanal fonksiyonların ve çalışma zamanı polimorfizminin (runtime polymorphism) gücünü gösteren bir medya oynatıcı sistemi oluşturalım. Her birinin farklı şekilde oynatıldığı bir medya türleri hiyerarşisi oluşturacak ve temel sınıf işaretçileri (base class pointers) üzerinden erişildiğinde bile sanal fonksiyonların nasıl doğru davranışı sağladığını göreceksiniz.
Kodunuzu üç dosya halinde düzenleyeceksiniz:
Media.h: Oynatılabilir herhangi bir medyayı temsil eden temel birMediasınıfı tanımlayın:- Korumalı (protected) bir
std::string titleüyesi - Bir başlık alan ve bunu saklayan bir yapıcı (constructor)
- Şunu yazdıran sanal (virtual) bir
play()metodu:Playing media: <title> - Şunu yazdıran bir sanal yıkıcı (virtual destructor):
Media [<title>] destroyed
- Korumalı (protected) bir
AudioTrack.h:Mediasınıfından genel (public) olarak türetilen birAudioTracksınıfı tanımlayın:- Özel (private) bir
std::string artistüyesi - Bir başlık ve sanatçı alan, başlığı temel sınıfa ileten ve sanatçıyı saklayan bir yapıcı
- Şunu yazdırmak için
overrideanahtar kelimesini kullanarakplay()metodunu geçersiz kılın:Playing audio: <title> by <artist> - Şunu yazdıran bir yıkıcı:
AudioTrack [<title>] destroyed
- Özel (private) bir
main.cpp: Üç girişi okuyun (her biri ayrı bir satırda):- Ses parçası başlığı (string)
- Sanatçı adı (string)
- Video başlığı (string)
Doğrudan main.cpp içinde
Mediasınıfından genel olarak türetilen birVideoClipsınıfı tanımlayın:- Özel bir
int durationüyesi (saniye cinsinden) - Bir başlık ve süre alan bir yapıcı (varsayılan süre 120)
- Şunu yazdırmak için
play()metodunu geçersiz kılın:Playing video: <title> (<duration>s) - Şunu yazdıran bir yıkıcı:
VideoClip [<title>] destroyed
Bir blok kapsamı (block scope) içinde üç
Media*işaretçisinden oluşan bir dizi oluşturun. Dinamik olarak şunları atayın:- "Generic Media" başlıklı bir temel
Medianesnesi - Giriş yapılan başlık ve sanatçıya sahip bir
AudioTracknesnesi - Giriş yapılan video başlığına sahip bir
VideoClipnesnesi
Dizi üzerinde döngü kurun ve her işaretçi için
play()metodunu çağırın. Ardından tüm nesneleri ters sırada silin. Bloktan sonra şunu yazdırın:Playback complete!
Örneğin, Bohemian Rhapsody, Queen ve Nature Documentary girişleri ile:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Media* işaretçileri üzerinden play() çağrılmasının, sanal fonksiyonlar sayesinde nasıl doğru türetilmiş sınıf metodunu tetiklediğine dikkat edin. Ayrıca, sanal yıkıcının nasıl düzgün temizlik sağladığını gözlemleyin — bir temel sınıf işaretçisi üzerinden silme işlemi yapıldığında hem türetilmiş hem de temel sınıf yıkıcıları çalışır.
Kopya kağıdı
virtual anahtar kelimesi, hangi metodun çağrılacağını işaretçi türüne değil, gerçek nesne türüne göre belirleyerek çalışma zamanı polimorfizmini (çok biçimlilik) sağlar:
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Output: Woof!override anahtar kelimesi isteğe bağlıdır ancak önerilir. Derleyiciye sanal bir fonksiyonu geçersiz kılma (override) niyetinde olduğunuzu bildirir ve imzalar eşleşmezse hataları yakalar.
Sanal fonksiyonlar nasıl çalışır: Derleyici, sınıfın sanal fonksiyonlarına yönelik işaretçiler içeren bir sanal tablo (vtable) oluşturur. Sanal fonksiyonlara sahip her nesne, sınıfının vtable'ına yönelik gizli bir işaretçi (vptr) saklar. Bir işaretçi aracılığıyla sanal bir fonksiyon çağrıldığında, program çalışma zamanında vtable'daki doğru fonksiyon adresine bakar. Buna dinamik sevkiyat (dynamic dispatch) denir.
Sanal yıkıcılar: Eğer bir sınıf sanal fonksiyonlara sahipse ve bir temel sınıf olarak kullanılacaksa, yıkıcısı da sanal olmalıdır. Bu, türetilmiş nesneler temel işaretçiler aracılığıyla silindiğinde düzgün bir temizlik yapılmasını sağlar:
class Animal {
public:
virtual ~Animal() {} // Sanal yıkıcı
virtual void speak() {}
};Kendin dene
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Burada Media sınıfından genel olarak (publicly) türeyen VideoClip sınıfını tanımlayın
// - Özel (private) int duration üyesi (saniye cinsinden)
// - Yapıcı metot (constructor) başlık ve süre alır (varsayılan süre 120)
// - play() metodunu şu çıktıyı verecek şekilde geçersiz kılın (override): Playing video: <title> (<duration>s)
// - Yıkıcı metot (destructor) şunu yazdırır: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Yapıcı metodu (constructor) uygulayın
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Süreyi (duration) saklayın
}
// TODO: play() metodunu geçersiz kılın (override)
void play() override {
// TODO: Bu metodu uygulayın
}
// TODO: Yıkıcı metodu (destructor) uygulayın
~VideoClip() {
// TODO: Bu yıkıcı metodu (destructor) uygulayın
}
};
int main() {
// Girdileri oku
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Süslü parantezlerle bir blok kapsamı (block scope) oluşturun
// Bloğun içinde:
// 1. Üç adet Media* işaretçisinden (pointer) oluşan bir dizi oluşturun
// 2. Dinamik olarak bellek ayırın (allocate):
// - "Generic Media" başlıklı bir temel Media nesnesi
// - Girdi başlığı ve sanatçısı ile bir AudioTrack nesnesi
// - Girdi video başlığı ile bir VideoClip nesnesi
// 3. Dizi üzerinde döngü kurun ve her bir işaretçi için play() metodunu çağırın
// 4. Tüm nesneleri ters sırada silin (delete)
// TODO: Bloktan sonra şunu yazdırın: Playback complete!
return 0;
}
Bu ders kısa bir quiz içerir. Soruları yanıtlamak ve ilerlemeni kaydetmek için derse başla.
Nesne Yönelimli Programlama bölümündeki tüm dersler
1OOP Temelleri
Harici DosyalarC++ Build ve DerlemeBaşlık Dosyaları ve Kaynak DosyalarıAd Alanları ve KapsamC++'ta OOP'ye GirişSınıflar ve Nesneler'this' İşaretçisiMetotlar (Üye Fonksiyonlar)Öznitelikler (Veri Üyeleri)Ctor ve Dtor TemelleriÖzet - Basit Hesap Makinesi4Sınıf Özellikleri
Örnek ve Statik ÜyelerGetter ve Setter MetotlarıConst Üye FonksiyonlarMutable Anahtar KelimesiStatik Metotlar ve DeğişkenlerFriend Fonksiyonlar ve SınıflarÖzet - Banka Hesabı Yöneticisi7Kalıtım
Temel KalıtımKalıtım Erişim SeviyeleriCtor ve Dtor Çağrılma SırasıMetot Geçersiz KılmaSanal Fonksiyonlar ve VTableÇoklu KalıtımSanal KalıtımÖzet - Çalışan Hiyerarşisi10STL Genel Bakış
STL Genel Bakış ve FelsefesiSTL KonteynerleriİteratörlerSTL AlgoritmalarıFunctor'lar ve Lambda İfadeleriÖzet - Kelime Frekansı13Tasarım Kalıpları 1. Bölüm
Tasarım Kalıplarına GirişSingleton KalıbıFactory ve Abstract FactoryBuilder KalıbıObserver KalıbıStrategy Kalıbı2Bellek Yönetimi
Stack ve Heap Bellekİşaretçiler ve ReferanslarDinamik Bellek (new/delete)C++'ta Akıllı İşaretçilerC++'ta RAIIÖzet - Dinamik Dizi Yöneticisi5Kapsülleme
C++'da Erişim BelirleyicilerDerinlemesine Erişim BelirleyicilerBilgi GizlemeStruct vs Classİç İçe ve Dahili SınıflarÖzet - Öğrenci Kayıt Sistemi8Çok Biçimlilik
Derleme ve Çalışma Zamanı Çok BiçimliliğiFonksiyon Aşırı YüklemeSanal Fonksiyonlara Yeniden BakışSaf Sanal FonksiyonlarSoyut SınıflarC++'ta Arayüz TasarımıDynamic Casting ve RTTIÖzet - Şekil Hesaplayıcı3Yapıcılar ve Yıkıcılar
Varsayılan YapıcıParametreli YapıcıKopya YapıcıTaşıma YapıcısıYapıcı İlklendirme ListeleriTemsilci YapıcılarYıkıcılara Derinlemesine BakışÜç / Beş / Sıfır KuralıÖzet - String Sınıfı6Operatör Aşırı Yükleme
Operatör Aşırı Yüklemeye GirişAritmetik Operatör Aşırı YüklemeKarşılaştırma Operatörü Aşırı YüklemeStream OperatörleriAtama Operatörü Aşırı Yükleme[] ve () Operatör Aşırı YüklemeTip Dönüşüm OperatörleriÖzet - Matris Sınıfı9Şablonlar
Fonksiyon ŞablonlarıSınıf ŞablonlarıŞablon ÖzelleştirmeVariadic ŞablonlarSFINAE ve Type Traits TemelleriÖzet - Generic Konteyner