Abstract Sınıflar ve Pure Virtual
Önceki derste virtual fonksiyonları gördük. Bazen base class'taki bir fonksiyonun varsayılan bir implementasyonu olmasını istemezsin. Hatta mantıken olmamalıdır bile. Örneğin Shape sınıfının draw() fonksiyonu ne çizsin? Daire mi? Kare mi? Hiçbir şey — çünkü "Shape" soyut bir kavram.
İşte tam burada abstract sınıflar ve pure virtual fonksiyonlar devreye girer.
Pure Virtual Function Nedir?
Bir virtual fonksiyonu pure (saf) yapmak için sonuna = 0 eklersin:
class Shape {
public:
virtual void draw() const = 0; // Pure virtual — gövde yok
virtual ~Shape() = default;
};= 0 derleyiciye şunu söyler: "Bu fonksiyonun burada implementasyonu yok. Bu sınıftan türeyen her sınıf kendi versiyonunu yazmak zorunda."
Bunu bir sözleşme (contract) gibi düşün: "Shape'ten türüyorsan, draw() fonksiyonunu implemente etmelisin. Nasıl çizeceğini sen bilirsin, ama çizmek zorundasın."
Abstract Class Nedir?
En az bir pure virtual fonksiyonu olan sınıf abstract class'tır. Abstract class'tan nesne oluşturamazsın:
class Shape {
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual ~Shape() = default;
};
int main() {
// Shape s; // HATA! Abstract class'tan nesne oluşturulamaz
// Shape* s = new Shape; // HATA! Aynı sebep
}Analoji: "Hayvan" diye bir canlı yoktur. Kedi var, köpek var, kuş var — ama soyut "hayvan" diye bir şey bulamazsın doğada. "Hayvan" bir kavram, somut bir varlık değil. Abstract class'lar da böyle — kavramı tanımlarlar ama somut bir nesne olamazlar.
Abstract vs Concrete Class
| Özellik | Abstract Class | Concrete Class |
|---|---|---|
| Pure virtual fonksiyon | En az 1 var | Yok (hepsi implemente) |
| Nesne oluşturma | ❌ Oluşturulamaz | ✅ Oluşturulabilir |
| Pointer/referans | ✅ Kullanılabilir | ✅ Kullanılabilir |
| Amaç | Arayüz tanımlama | Gerçek implementasyon |
class Shape { // ABSTRACT
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape { // CONCRETE
double radius;
public:
Circle(double r) : radius(r) {}
void draw() const override {
std::cout << "Drawing circle\n";
}
double area() const override {
return 3.14159 * radius * radius;
}
};Shape abstract — nesne oluşturulamaz. Circle concrete — tüm pure virtual fonksiyonları implemente ettiği için nesne oluşturulabilir.
Tüm Pure Virtual'ları İmplemente Etme Zorunluluğu
Derived class, base class'ın tüm pure virtual fonksiyonlarını implemente etmezse, kendisi de abstract kalır:
class Shape {
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual ~Shape() = default;
};
class PartialShape : public Shape {
public:
void draw() const override {
std::cout << "Drawing partial shape\n";
}
// area() implemente edilmedi → PartialShape de abstract!
};
// PartialShape p; // HATA! Hâlâ abstractPartialShape sadece draw()'u implemente etti, area()'yı yapmadı. Bu yüzden PartialShape de abstract kalmaya devam eder. Ondan türeyen bir sınıf area()'yı da implemente ederse, o sınıf concrete olur.
Pure Virtual Fonksiyona Gövde Vermek
İlginç bir detay: pure virtual fonksiyona gövde verebilirsin. Bu yaygın bilinen bir "sürpriz":
class Animal {
public:
virtual void speak() const = 0; // Hâlâ pure virtual
virtual ~Animal() = default;
};
// Gövde sınıf dışında tanımlanır
void Animal::speak() const {
std::cout << "Default animal sound\n";
}
class Dog : public Animal {
public:
void speak() const override {
Animal::speak(); // Base versiyonu çağırabilir
std::cout << "Woof!\n";
}
};Fonksiyon hâlâ pure virtual (yani Animal hâlâ abstract), ama derived class'lar Animal::speak() şeklinde bu varsayılan implementasyonu çağırabilir. Bu, "zorunlu override ama varsayılan davranış da mevcut" senaryosu için kullanılır.
💡 Kullanım alanı: Pure virtual destructor'a gövde vermek yaygındır. Destructor pure virtual olmalı ama aynı zamanda bir gövdeye de ihtiyaç duyar (çünkü yıkım zincirinde çağrılır).
Interface Benzeri Abstract Class'lar
C++'ta Java veya C#'taki gibi ayrı bir interface anahtar kelimesi yoktur. Ama aynı şeyi sadece pure virtual fonksiyonlardan oluşan abstract class'lar ile elde edersin:
class IDrawable {
public:
virtual void draw() const = 0;
virtual ~IDrawable() = default;
};
class IResizable {
public:
virtual void resize(double factor) = 0;
virtual ~IResizable() = default;
};
class ISerializable {
public:
virtual std::string serialize() const = 0;
virtual void deserialize(const std::string& data) = 0;
virtual ~ISerializable() = default;
};Bu sınıflar sadece "ne yapılmalı" tanımlar, "nasıl yapılmalı" tanımlamaz. Bir sınıf bunlardan birden fazlasını implemente edebilir:
class Widget : public IDrawable, public IResizable {
double width, height;
public:
Widget(double w, double h) : width(w), height(h) {}
void draw() const override {
std::cout << "Drawing widget " << width << "x" << height << "\n";
}
void resize(double factor) override {
width *= factor;
height *= factor;
}
};Interface İsimlendirme Kuralı
C++ topluluğunda interface'lerin başına I harfi koymak yaygın bir konvansiyondur: IDrawable, ISerializable, IPrintable. Bu zorunlu değil ama kodun okunabilirliğini artırır. Bazı projeler Drawable, Serializable gibi suffix-based isimlendirme tercih eder — tutarlı olduğun sürece hangisini kullanacağın sana kalmış.
Interface Kalıtımı
Interface'ler birbirinden de türeyebilir:
class IReadable {
public:
virtual std::string read() const = 0;
virtual ~IReadable() = default;
};
class IWritable {
public:
virtual void write(const std::string& data) = 0;
virtual ~IWritable() = default;
};
// Birleşik interface — hem okuma hem yazma
class IReadWritable : public IReadable, public IWritable {
// Hâlâ abstract — pure virtual'lar devralındı
};
class File : public IReadWritable {
std::string content;
public:
std::string read() const override { return content; }
void write(const std::string& data) override { content = data; }
};💡 Best practice: Interface class'larında veri üyesi (member variable) tutma. Sadece pure virtual fonksiyonlar ve virtual destructor olsun. Bu, gerçek anlamda bir "sözleşme" tanımlar.
Pratik Örnek: Loglama Sistemi
Abstract class'ların gerçek hayatta nasıl kullanıldığını görelim:
#include <iostream>
#include <string>
#include <fstream>
#include <chrono>
#include <ctime>
class ILogger {
public:
virtual void log(const std::string& message) = 0;
virtual void error(const std::string& message) = 0;
virtual ~ILogger() = default;
};
class ConsoleLogger : public ILogger {
public:
void log(const std::string& message) override {
std::cout << "[INFO] " << message << "\n";
}
void error(const std::string& message) override {
std::cerr << "[ERROR] " << message << "\n";
}
};
class FileLogger : public ILogger {
std::ofstream file;
public:
FileLogger(const std::string& filename)
: file(filename, std::ios::app) {}
void log(const std::string& message) override {
file << "[INFO] " << message << "\n";
}
void error(const std::string& message) override {
file << "[ERROR] " << message << "\n";
}
};Kullanım — logger tipi runtime'da belirlenebilir:
void processData(ILogger& logger) {
logger.log("Processing started");
// ... iş mantığı ...
logger.log("Processing complete");
}
int main() {
ConsoleLogger console;
FileLogger fileLog("app.log");
processData(console); // Konsola yazar
processData(fileLog); // Dosyaya yazar
}processData fonksiyonu logger'ın tipini bilmiyor. Sadece ILogger arayüzünü biliyor. Yarın DatabaseLogger veya NetworkLogger yazsan, processData fonksiyonunu hiç değiştirmene gerek yok.
Abstract Class ile Ortak Kod Paylaşımı
Saf interface'ler güzel ama bazen derived class'lar arasında ortak kod da paylaşmak istersin. Abstract class'lar buna da izin verir:
class Shape {
protected:
std::string color;
public:
Shape(const std::string& c) : color(c) {}
// Pure virtual — her alt sınıf implemente etmeli
virtual void draw() const = 0;
virtual double area() const = 0;
// Ortak implementasyon — tüm şekiller aynı şekilde kullanır
void setColor(const std::string& c) {
color = c;
}
std::string getColor() const {
return color;
}
void printInfo() const {
std::cout << "Shape: " << getColor()
<< ", Area: " << area() << "\n";
}
virtual ~Shape() = default;
};Shape abstract (çünkü draw() ve area() pure virtual) ama setColor(), getColor() ve printInfo() gibi ortak fonksiyonları da barındırıyor. Derived class'lar sadece draw() ve area()'yı implemente eder, diğerlerini bedavadan alır.
class Circle : public Shape {
double radius;
public:
Circle(const std::string& c, double r)
: Shape(c), radius(r) {}
void draw() const override {
std::cout << "Drawing " << color << " circle\n";
}
double area() const override {
return 3.14159 * radius * radius;
}
};
int main() {
Circle c("Red", 5.0);
c.printInfo(); // "Shape: Red, Area: 78.5398"
}Bu, pure interface ile concrete class arasında bir orta yol: abstract class hem sözleşme tanımlar hem de ortak implementasyon sunar.
Template Method Pattern
Abstract class'larla çok kullanılan bir tasarım deseni: Template Method Pattern. Bir algoritmanın iskeletini base class'ta tanımlarsın, belirli adımları derived class'lara bırakırsın:
class DataProcessor {
public:
// Template method — algoritma iskeleti
void process() {
loadData();
validate();
transform();
save();
}
virtual ~DataProcessor() = default;
protected:
virtual void loadData() = 0; // Alt sınıf implemente eder
virtual void validate() = 0; // Alt sınıf implemente eder
virtual void transform() = 0; // Alt sınıf implemente eder
void save() { // Ortak implementasyon
std::cout << "Data saved.\n";
}
};
class CsvProcessor : public DataProcessor {
protected:
void loadData() override {
std::cout << "Loading CSV file...\n";
}
void validate() override {
std::cout << "Validating CSV format...\n";
}
void transform() override {
std::cout << "Parsing CSV rows...\n";
}
};
class JsonProcessor : public DataProcessor {
protected:
void loadData() override {
std::cout << "Loading JSON file...\n";
}
void validate() override {
std::cout << "Validating JSON schema...\n";
}
void transform() override {
std::cout << "Parsing JSON objects...\n";
}
};int main() {
CsvProcessor csv;
csv.process();
// Loading CSV file...
// Validating CSV format...
// Parsing CSV rows...
// Data saved.
}process() fonksiyonu her zaman aynı sırada çalışır. Ama her adımın nasıl yapılacağı derived class'a bırakılmış. Yeni bir veri formatı eklemek istersen sadece yeni bir sınıf yazarsın.
⚠️ Dikkat: Abstract class'larda çok fazla pure virtual fonksiyon tanımlamaktan kaçın. 10-15 fonksiyonluk bir abstract class, derived class yazarken eziyet olur. İnce ve odaklı arayüzler tasarla (Interface Segregation Principle).
Abstract Class Pointer ve Referans Kullanımı
Abstract class'tan nesne oluşturamasan da, pointer ve referans olarak kullanabilirsin:
void drawShape(const Shape& shape) { // ✅ Referans — OK
shape.draw();
}
void drawAll(const std::vector<Shape*>& shapes) { // ✅ Pointer — OK
for (const auto* s : shapes) {
s->draw();
}
}Bu, polimorfizmin temelidir. Abstract class, derived nesneleri tutan "ortak tip" görevi görür.
Modern C++'ta smart pointer'lar ile:
#include <vector>
#include <memory>
void drawAll(const std::vector<std::unique_ptr<Shape>>& shapes) {
for (const auto& s : shapes) {
s->draw();
std::cout << " Area: " << s->area() << "\n";
}
}
int main() {
std::vector<std::unique_ptr<Shape>> canvas;
canvas.push_back(std::make_unique<Circle>("Blue", 5.0));
canvas.push_back(std::make_unique<Circle>("Red", 3.0));
drawAll(canvas);
// Bellek yönetimi otomatik — scope dışına çıkınca temizlenir
}Abstract Class ve Akıllı İşaretçiler
Abstract class nesneleri her zaman heap'te (dinamik bellekte) oluşturulur çünkü polimorfik kullanım pointer/referans gerektirir. Modern C++'ta bunları std::unique_ptr ile yönetmek best practice'dir:
#include <iostream>
#include <memory>
#include <vector>
class Shape {
public:
virtual double area() const = 0;
virtual std::string name() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() const override { return 3.14159 * r * r; }
std::string name() const override { return "Circle"; }
};
class Square : public Shape {
double side;
public:
Square(double s) : side(s) {}
double area() const override { return side * side; }
std::string name() const override { return "Square"; }
};
// Factory function — abstract class döndürür
std::unique_ptr<Shape> createShape(const std::string& type, double size) {
if (type == "circle") return std::make_unique<Circle>(size);
if (type == "square") return std::make_unique<Square>(size);
return nullptr;
}
int main() {
auto shapes = std::vector<std::unique_ptr<Shape>>{};
shapes.push_back(createShape("circle", 5.0));
shapes.push_back(createShape("square", 4.0));
for (const auto& s : shapes) {
if (s) {
std::cout << s->name() << ": area = " << s->area() << "\n";
}
}
}Factory pattern ile abstract class birlikte kullanıldığında, nesne oluşturma mantığı tek bir yere toplanır. Yeni şekil eklemek sadece factory fonksiyonunu güncellemek demektir.
Ne Zaman Abstract Class, Ne Zaman Concrete Base Class?
| Durum | Tercih |
|---|---|
| Base class'ın kendi başına anlamı var mı? | Evet → Concrete, Hayır → Abstract |
| Tüm derived class'lar belirli fonksiyonları implemente etmeli mi? | Evet → Pure virtual kullan |
| Base class nesnesi oluşturulmasını engellemek istiyor musun? | Evet → Abstract yap |
| Ortak kod var mı? | Evet → Abstract class (pure virtual + ortak fonksiyonlar) |
| Sadece arayüz mü tanımlamak istiyorsun? | Evet → Pure interface (sadece pure virtual) |
Genel kural: Eğer base class'ın tek başına bir "şey" olarak mantığı yoksa, abstract yap. "Hayvan" tek başına bir canlı değil → abstract. "Kedi" tek başına bir canlı → concrete.
Callback ve Strategy Pattern
Abstract class'ların bir diğer güçlü kullanımı: Strategy Pattern. Bir algoritmanın davranışını çalışma zamanında değiştirmek istediğinde, stratejiyi abstract class olarak tanımlarsın:
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
// Sıralama stratejisi — abstract
class ISortStrategy {
public:
virtual void sort(std::vector<int>& data) = 0;
virtual std::string name() const = 0;
virtual ~ISortStrategy() = default;
};
class BubbleSort : public ISortStrategy {
public:
void sort(std::vector<int>& data) override {
for (size_t i = 0; i < data.size(); i++)
for (size_t j = 0; j < data.size() - 1 - i; j++)
if (data[j] > data[j + 1])
std::swap(data[j], data[j + 1]);
}
std::string name() const override { return "BubbleSort"; }
};
class StdSort : public ISortStrategy {
public:
void sort(std::vector<int>& data) override {
std::sort(data.begin(), data.end());
}
std::string name() const override { return "std::sort"; }
};
class Sorter {
std::unique_ptr<ISortStrategy> strategy;
public:
void setStrategy(std::unique_ptr<ISortStrategy> s) {
strategy = std::move(s);
}
void sort(std::vector<int>& data) {
if (strategy) {
std::cout << "Sorting with " << strategy->name() << "\n";
strategy->sort(data);
}
}
};
int main() {
std::vector<int> data = {5, 2, 8, 1, 9};
Sorter sorter;
sorter.setStrategy(std::make_unique<BubbleSort>());
sorter.sort(data);
data = {5, 2, 8, 1, 9};
sorter.setStrategy(std::make_unique<StdSort>());
sorter.sort(data);
}Stratejiyi çalışma zamanında değiştirebiliyorsun. Yeni bir sıralama algoritması eklemek istersen sadece yeni bir sınıf yaz — Sorter sınıfını değiştirmene gerek yok.
Abstract Class Hataları ve Dikkat Noktaları
Abstract class'larla çalışırken sık yapılan hatalar:
Hata 1: Abstract class'tan nesne oluşturmaya çalışmak
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
// Shape s; // HATA!
// std::vector<Shape> shapes; // HATA! (vector değer tutar)
std::vector<Shape*> shapes; // OK — pointer tutar
std::vector<std::unique_ptr<Shape>> shapes2; // OK — smart pointerHata 2: Pure virtual fonksiyonu implemente etmeyi unutmak
class Shape {
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual ~Shape() = default;
};
class MyShape : public Shape {
public:
void draw() const override { /* ... */ }
// area() implemente edilmedi!
};
// MyShape s; // HATA! MyShape hâlâ abstractDerleyici bu hatayı yakalayacaktır — ama hata mesajı bazen kafa karıştırıcı olabilir. "Cannot instantiate abstract class" görüyorsan, hangi pure virtual fonksiyonun eksik olduğuna bak.
Hata 3: Virtual destructor koymayı unutmak
class ILogger {
public:
virtual void log(const std::string& msg) = 0;
// virtual ~ILogger() = default; // BU OLMALI!
};
// Polimorfik delete → undefined behavior!
ILogger* logger = new FileLogger("app.log");
delete logger; // FileLogger'ın destructor'ı çalışmayabilir!⚠️ Kural: Abstract class tanımlıyorsan, virtual destructor EKLEMEYI UNUTMA. En basit yol:
virtual ~ClassName() = default;
Abstract Class vs Concept (C++20)
C++20 ile gelen Concepts, bazı durumlarda abstract class'ların yerini alabilir:
// Abstract class yaklaşımı
class IPrintable {
public:
virtual void print() const = 0;
virtual ~IPrintable() = default;
};
void showItem(const IPrintable& item) {
item.print(); // Runtime polimorfizm
}// Concept yaklaşımı (C++20)
template<typename T>
concept Printable = requires(const T& t) {
{ t.print() } -> std::same_as<void>;
};
void showItem(const Printable auto& item) {
item.print(); // Compile-time polimorfizm — vtable yok!
}Concept yaklaşımı derleme zamanında çözülür, vtable maliyeti yoktur. Ama çalışma zamanında tip değişikliği gerekiyorsa (farklı tipleri aynı koleksiyonda tutmak), abstract class hâlâ gereklidir.
İkisi birbirinin rakibi değil — farklı senaryolarda kullanılır:
Abstract class: Farklı tipleri aynı container'da tutmak, runtime tip değişimi
Concept: Derleme zamanı tip kısıtlaması, sıfır maliyet
Özet
Pure virtual fonksiyon (
= 0), derived class'ların zorunlu olarak implemente etmesi gereken fonksiyondur — base class'ta gövdesi olmaz.Abstract class, en az bir pure virtual fonksiyonu olan sınıftır ve nesne oluşturulamaz — sadece pointer/referans olarak kullanılabilir.
Tüm pure virtual fonksiyonları implemente eden derived class concrete olur. İmplemente etmeyen derived class da abstract kalır.
Interface benzeri class'lar sadece pure virtual fonksiyonlar ve virtual destructor içerir — C++'ta
interfaceanahtar kelimesi yoktur ama aynı etkiyi sağlar.Abstract class'lar hem sözleşme (pure virtual) hem de ortak kod (normal fonksiyonlar) barındırabilir — bu onları pure interface'lerden daha esnek kılar.
Template Method ve Strategy Pattern gibi tasarım desenleri abstract class'ları etkin kullanır — gerçek projelerde en sık karşılaşacağın kullanım alanlarıdır.
AI Asistan
Sorularını yanıtlamaya hazır