← Kursa Dön
📄 Text · 15 min

Kalıtım Temelleri

Yazılım geliştirirken sıfırdan her şeyi yazmak zorunda değilsin. Zaten var olan bir sınıfın özelliklerini alıp, üzerine yeni şeyler ekleyebilirsin. İşte buna kalıtım (inheritance) diyoruz.

Bu ders, C++'ta kalıtımın ne olduğunu, nasıl çalıştığını ve farklı türlerini sıfırdan öğretecek.


Kalıtım Nedir?

Kalıtımı anlamanın en kolay yolu aile ağacı analojisi. Bir çocuk, anne-babasından bazı özellikleri miras alır: göz rengi, boy, saç tipi... Ama çocuğun kendine has özellikleri de vardır. Aynı mantık yazılımda da geçerli.

C++'ta bir sınıf (class), başka bir sınıftan türeyebilir. Türeyen sınıf, üst sınıfın özelliklerini ve davranışlarını otomatik olarak devralır. Üzerine kendi özelliklerini ekler.

İki temel terim var:

  • Base class (temel sınıf / üst sınıf / parent): Miras veren sınıf

  • Derived class (türetilmiş sınıf / alt sınıf / child): Miras alan sınıf

class Animal {
public:
    std::string name;
    
    void eat() {
        std::cout << name << " is eating.\n";
    }
};

class Dog : public Animal {
public:
    void bark() {
        std::cout << name << " says: Woof!\n";
    }
};

Burada Dog, Animal'dan türüyor. Dog nesnesi hem eat() fonksiyonunu hem de bark() fonksiyonunu kullanabilir. Ama Animal nesnesi bark() yapamaz — çünkü her hayvan havlamaz.

int main() {
    Dog myDog;
    myDog.name = "Rex";
    myDog.eat();   // Animal'dan miras
    myDog.bark();  // Dog'a özel
}

Çıktı:

Rex is eating.
Rex says: Woof!

Kalıtımın amacı kod tekrarını önlemek ve mantıksal hiyerarşi kurmak. 10 farklı hayvan sınıfı yazacaksan, hepsine ayrı ayrı eat() yazmak yerine bir kere Animal'a yaz, hepsi miras alsın.


Public, Protected ve Private Kalıtım

Kalıtım yaparken : işaretinden sonra bir erişim belirteci (access specifier) yazıyorsun. Bu, base class'ın üyelerinin derived class'ta nasıl görüneceğini belirler.

Public Kalıtım

En yaygın kullanılan tür. "is-a" ilişkisini ifade eder: "Dog is an Animal."

class Dog : public Animal {
    // Animal'ın public üyeleri → Dog'da public kalır
    // Animal'ın protected üyeleri → Dog'da protected kalır
    // Animal'ın private üyeleri → Dog'da erişilemez (ama var!)
};

Public kalıtımda base class'ın public arayüzü aynen korunur. Dışarıdan myDog.eat() diyebilirsin.

Protected Kalıtım

Çok nadir kullanılır. Base class'ın public üyeleri derived class'ta protected olur.

class Dog : protected Animal {
    // Animal'ın public üyeleri → Dog'da protected olur
    // Animal'ın protected üyeleri → Dog'da protected kalır
};

Bu durumda myDog.eat() dışarıdan çağrılamaz. Sadece Dog'un kendi fonksiyonları içinden erişilebilir.

Private Kalıtım

Base class'ın tüm public ve protected üyeleri derived class'ta private olur.

class Dog : private Animal {
    // Animal'ın public üyeleri → Dog'da private olur
    // Animal'ın protected üyeleri → Dog'da private olur
};

Private kalıtım "is-a" ilişkisi değil, "implemented-in-terms-of" ilişkisini ifade eder. Yani "Dog, Animal kullanılarak implemente edildi" anlamına gelir. Pratikte çoğunlukla composition (bileşim) tercih edilir.

Özet Tablo

Base Class Üyesipublic kalıtımprotected kalıtımprivate kalıtım
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateerişilemezerişilemezerişilemez

💡 Pratik kural: Neredeyse her zaman public kalıtım kullan. Diğerlerini kullanman gerekiyorsa, muhtemelen kalıtım yerine composition düşünmelisin.


Protected Üyeler

private üyelere sadece o sınıfın kendi fonksiyonları erişebilir. public üyelere herkes erişebilir. Peki ya sadece türeyen sınıfların erişebilmesini istiyorsan?

İşte burada protected devreye giriyor.

class Vehicle {
protected:
    int speed;
    
public:
    Vehicle() : speed(0) {}
    
    void showSpeed() {
        std::cout << "Speed: " << speed << " km/h\n";
    }
};

class Car : public Vehicle {
public:
    void accelerate() {
        speed += 20;  // OK — protected üyeye derived class erişebilir
    }
};

int main() {
    Car myCar;
    myCar.accelerate();
    myCar.showSpeed();    // Speed: 20 km/h
    // myCar.speed = 100; // HATA — dışarıdan erişilemez
}

speed değişkeni protected olduğu için Car sınıfı içinden erişilebiliyor ama main() fonksiyonundan doğrudan erişilemiyor.

Ne Zaman Protected Kullanmalı?

Protected üyeler, derived class'ların iç mekanizmaya erişmesi gerektiğinde kullanılır. Ama dikkatli ol — çok fazla protected üye, sınıflar arasında sıkı bağlılık (tight coupling) yaratır.

⚠️ Dikkat: Protected üyeleri değiştirdiğinde, tüm derived class'ları da etkilemiş olursun. Bu yüzden mümkünse protected üye sayısını az tut. Gerekmedikçe private tercih et ve public getter/setter fonksiyonları sun.


Base Class Constructor Çağırma

Derived class'ın constructor'ı çalışmadan önce base class'ın constructor'ı çalışır. Bu mantıklı: bir evin temelini atmadan duvarlarını öremezsin.

Varsayılan (Default) Constructor

Eğer bir şey belirtmezsen, derleyici base class'ın default constructor'ını çağırır:

class Animal {
public:
    Animal() {
        std::cout << "Animal created\n";
    }
};

class Dog : public Animal {
public:
    Dog() {
        std::cout << "Dog created\n";
    }
};

int main() {
    Dog d;
    // Çıktı:
    // Animal created
    // Dog created
}

Önce Animal(), sonra Dog() çalışır. Yıkım (destruction) sırası ise tam tersi: önce Dog, sonra Animal.

Parametreli Constructor Çağırma

Base class'ın parametreli bir constructor'ı varsa, derived class'tan onu açıkça çağırman gerekir. Bunu member initializer list ile yaparsın:

class Animal {
    std::string name;
    int age;
    
public:
    Animal(const std::string& n, int a) : name(n), age(a) {
        std::cout << "Animal: " << name << ", age " << age << "\n";
    }
    
    std::string getName() const { return name; }
};

class Dog : public Animal {
    std::string breed;
    
public:
    Dog(const std::string& n, int a, const std::string& b)
        : Animal(n, a), breed(b) {  // Base constructor çağrısı
        std::cout << "Dog breed: " << breed << "\n";
    }
};

int main() {
    Dog rex("Rex", 5, "German Shepherd");
    std::cout << rex.getName() << "\n";
}

Çıktı:

Animal: Rex, age 5
Dog breed: German Shepherd
Rex

Dog'un constructor'ı, Animal(n, a) ile base class constructor'ını çağırıyor. Bu çağrı her zaman initializer list'te olmalı — constructor gövdesinin içinde yazamazsın.

⚠️ Dikkat: Base class'ın default constructor'ı yoksa ve derived class'ta base constructor'ı açıkça çağırmazsan, derleme hatası alırsın.


Kalıtım Hiyerarşisi

Kalıtım tek seviyeyle sınırlı değil. Bir sınıftan türeyen sınıftan da yeni sınıflar türeyebilir. Buna kalıtım hiyerarşisi (inheritance hierarchy) denir.

       Shape
      /     \
  Circle   Rectangle
              |
           Square
class Shape {
protected:
    std::string color;
    
public:
    Shape(const std::string& c) : color(c) {}
    
    void showColor() const {
        std::cout << "Color: " << color << "\n";
    }
};

class Rectangle : public Shape {
protected:
    double width, height;
    
public:
    Rectangle(const std::string& c, double w, double h)
        : Shape(c), width(w), height(h) {}
    
    double area() const {
        return width * height;
    }
};

class Square : public Rectangle {
public:
    Square(const std::string& c, double side)
        : Rectangle(c, side, side) {}
    // area() zaten Rectangle'dan miras
};

Square, Rectangle'dan türüyor, Rectangle da Shape'ten. Bu yüzden Square nesnesi hem showColor() hem area() fonksiyonunu kullanabilir.

int main() {
    Square sq("Red", 5.0);
    sq.showColor();   // Shape'ten miras
    std::cout << "Area: " << sq.area() << "\n";  // Rectangle'dan miras
}

Çıktı:

Color: Red
Area: 25

Hiyerarşi Tasarım İpuçları

Kalıtım hiyerarşisi kurarken şunlara dikkat et:

"is-a" testini uygula. Her türetme "X is a Y" cümlesini doğru kurmalı. "Square is a Rectangle" ✅. "Car is an Engine" ❌ — bu composition olmalı.

Derin hiyerarşilerden kaçın. 3-4 seviyeden derin hiyerarşiler anlaşılması ve bakımı zor kodlara yol açar. Derin hiyerarşi yerine composition tercih et.

Ortak davranışı yukarı taşı. Birden fazla derived class aynı kodu tekrarlıyorsa, o kodu base class'a taşı.


Kalıtımda Fonksiyon Gizleme (Name Hiding)

Derived class'ta base class'taki bir fonksiyonla aynı isimde bir fonksiyon tanımlarsan, base class'ın versiyonu gizlenir (hide):

class Base {
public:
    void greet() {
        std::cout << "Hello from Base\n";
    }
    
    void greet(int times) {
        for (int i = 0; i < times; i++)
            std::cout << "Hello!\n";
    }
};

class Derived : public Base {
public:
    void greet() {  // Base::greet() ve Base::greet(int) gizlenir!
        std::cout << "Hello from Derived\n";
    }
};

int main() {
    Derived d;
    d.greet();       // OK — Derived::greet()
    // d.greet(3);   // HATA — Base::greet(int) gizlendi!
    d.Base::greet(3); // OK — açıkça Base versiyonunu çağır
}

Derived class'ta greet() tanımladığın anda, base class'ın tüm greet overload'ları gizlenir. Sadece aynı imzalı olan değil, hepsi.

Bunu engellemek için using bildirimi kullanabilirsin:

class Derived : public Base {
public:
    using Base::greet;  // Base'in tüm greet overload'larını görünür yap
    
    void greet() {
        std::cout << "Hello from Derived\n";
    }
};

Artık d.greet(3) çalışır çünkü using Base::greet ile base class'ın overload'larını geri getirdin.

💡 İpucu: Name hiding, kalıtımda en çok kafa karıştıran konulardan biridir. Bir fonksiyonu override ederken base class'ın diğer overload'larını kaybetmek istemiyorsan using kullan.


Constructor ve Destructor Çalışma Sırası

Kalıtımda nesnelerin oluşturulma ve yıkılma sırası önemli:

class A {
public:
    A()  { std::cout << "A constructed\n"; }
    ~A() { std::cout << "A destroyed\n"; }
};

class B : public A {
public:
    B()  { std::cout << "B constructed\n"; }
    ~B() { std::cout << "B destroyed\n"; }
};

class C : public B {
public:
    C()  { std::cout << "C constructed\n"; }
    ~C() { std::cout << "C destroyed\n"; }
};

int main() {
    C obj;
}

Çıktı:

A constructed
B constructed
C constructed
C destroyed
B destroyed
A destroyed

Oluşturma: Yukarıdan aşağıya (base → derived). Önce temel atılır, sonra üst katlar.

Yıkım: Aşağıdan yukarıya (derived → base). Önce üst katlar söküle, sonra temel.

Bu sıra garanti altındadır. C++ standardı bunu zorunlu kılar.


Kalıtım ve Bellek Düzeni

Bir derived class nesnesi, base class'ın tüm üyelerini fiziksel olarak içerir. Bellekte şöyle görünür:

Dog nesnesi:
┌─────────────────────┐
│  Animal kısmı       │
│  ├─ name            │
│  └─ age             │
├─────────────────────┤
│  Dog'a özel kısım   │
│  └─ breed           │
└─────────────────────┘

Bu yüzden bir Dog* pointer'ını Animal* pointer'ına dönüştürmek güvenlidir — başlangıç adresi aynıdır. Bu özellik polimorfizm'in temelini oluşturur (bir sonraki derste göreceğiz).


Object Slicing

Derived class nesnesini base class değer olarak atarsan, derived kısmı kesilir:

class Animal {
public:
    std::string name;
    void speak() { std::cout << "...\n"; }
};

class Dog : public Animal {
public:
    std::string breed;
    void speak() { std::cout << "Woof!\n"; }
};

int main() {
    Dog rex;
    rex.name = "Rex";
    rex.breed = "Labrador";
    
    Animal a = rex;  // Object slicing!
    // a.breed → YOK, kesildi
    a.speak();       // "..." — Animal::speak() çalışır
}

rex'i Animal tipinde bir değişkene kopyaladığında, breed bilgisi kaybolur ve speak() çağrısı Animal'ınkini kullanır.

⚠️ Dikkat: Object slicing, kalıtımda en yaygın hatalardan biridir. Polimorfik davranış istiyorsan pointer veya referans kullan, değer kopyalama yapma.

Animal& ref = rex;  // Slicing YOK — referans
ref.speak();        // Hâlâ Animal::speak() çalışır (virtual olmadığı için)
                    // virtual ile düzeltilir — sonraki ders!

Ne Zaman Kalıtım Kullanmalı?

Kalıtım güçlü bir araç ama her derde deva değil. Şu durumlarda kullan:

  1. "is-a" ilişkisi varsa: Dog is an Animal ✅

  2. Ortak davranış paylaşılıyorsa: Birçok sınıf aynı fonksiyonları kullanıyor

  3. Polimorfizm gerekiyorsa: Farklı tipleri aynı arayüz üzerinden kullanmak istiyorsan

Şu durumlarda kullanma:

  1. "has-a" ilişkisi varsa: Car has an Engine → Composition kullan

  2. Sadece kod tekrarını önlemek istiyorsan → Composition veya utility fonksiyonlar

  3. Base class değişebilir ve derived class'ları etkilemesini istemiyorsan

💡 Altın kural: "Prefer composition over inheritance." Emin değilsen composition ile başla. Kalıtım sonradan eklemek, kaldırmaktan daha kolaydır.


Inherited Constructors (C++11)

C++11'den itibaren base class'ın constructor'larını using ile doğrudan devralabilirsin:

class Base {
public:
    Base(int x) { std::cout << "Base(int): " << x << "\n"; }
    Base(int x, int y) { std::cout << "Base(int,int): " << x << ", " << y << "\n"; }
    Base(const std::string& s) { std::cout << "Base(string): " << s << "\n"; }
};

class Derived : public Base {
public:
    using Base::Base;  // Base'in TÜM constructor'larını devral
    
    // Derived'a özel constructor'lar da ekleyebilirsin
    Derived() : Base(0) {
        std::cout << "Derived default\n";
    }
};

int main() {
    Derived d1(42);         // Base(int): 42
    Derived d2(1, 2);       // Base(int,int): 1, 2
    Derived d3("hello");    // Base(string): hello
    Derived d4;             // Base(int): 0 + Derived default
}

Bu özellik özellikle base class'ta birçok constructor overload'u varsa ve hepsini tek tek derived class'ta tekrar yazmak istemiyorsan kullanışlı.

Ama dikkatli ol: inherited constructor'lar, derived class'ın kendi üye değişkenlerini initialize etmez. Eğer derived class'ta ek üyeler varsa, default member initializer kullan:

class Derived : public Base {
    int extra = 0;  // Default member initializer
public:
    using Base::Base;  // extra, default değerle (0) initialize edilir
};

Pratik Örnek: Çalışan Hiyerarşisi

Gerçek dünya senaryosuyla kalıtımı pekiştirelim. Bir şirketin çalışan yönetim sistemi:

#include <iostream>
#include <string>
#include <vector>

class Employee {
protected:
    std::string name;
    int id;
    double baseSalary;
    
public:
    Employee(const std::string& n, int i, double salary)
        : name(n), id(i), baseSalary(salary) {}
    
    std::string getName() const { return name; }
    int getId() const { return id; }
    
    double calculatePay() const {
        return baseSalary;
    }
    
    void printInfo() const {
        std::cout << "ID: " << id 
                  << " | Name: " << name 
                  << " | Salary: $" << calculatePay() << "\n";
    }
};

class Manager : public Employee {
    double bonus;
    int teamSize;
    
public:
    Manager(const std::string& n, int i, double salary, 
            double b, int team)
        : Employee(n, i, salary), bonus(b), teamSize(team) {}
    
    double calculatePay() const {
        return baseSalary + bonus;
    }
    
    void printInfo() const {
        std::cout << "ID: " << id 
                  << " | Manager: " << name 
                  << " | Salary: $" << calculatePay()
                  << " | Team: " << teamSize << " people\n";
    }
};

class Intern : public Employee {
    std::string university;
    int durationMonths;
    
public:
    Intern(const std::string& n, int i, double salary,
           const std::string& uni, int months)
        : Employee(n, i, salary), university(uni), durationMonths(months) {}
    
    void printInfo() const {
        std::cout << "ID: " << id 
                  << " | Intern: " << name 
                  << " | From: " << university
                  << " | Duration: " << durationMonths << " months\n";
    }
};

int main() {
    Employee emp("Ali", 101, 5000);
    Manager mgr("Ayşe", 102, 8000, 3000, 5);
    Intern intern("Can", 103, 2000, "ITU", 6);
    
    emp.printInfo();
    mgr.printInfo();
    intern.printInfo();
}

Çıktı:

ID: 101 | Name: Ali | Salary: $5000
ID: 102 | Manager: Ayşe | Salary: $11000 | Team: 5 people
ID: 103 | Intern: Can | From: ITU | Duration: 6 months

Her çalışan tipi Employee'dan ortak bilgileri (name, id, baseSalary) miras alıyor. Ama her birinin kendine has verileri ve davranışları var. Bu, kalıtımın doğru kullanım senaryosu: "Manager is an Employee", "Intern is an Employee".


Composition vs Inheritance Karşılaştırma

Bu konuyu gerçekten anlamak önemli çünkü en çok yapılan tasarım hatasının kaynağı burasıdır.

Yanlış: Engine'i Kalıtımla Kullanmak

// KÖTÜ — "Car is an Engine" mantıksız!
class Engine {
public:
    void start() { std::cout << "Engine started\n"; }
    void stop() { std::cout << "Engine stopped\n"; }
    int getHorsepower() const { return 150; }
};

class Car : public Engine {  // ❌ Car is NOT an Engine
public:
    void drive() {
        start();
        std::cout << "Driving...\n";
    }
};

Bu çalışır ama mantıksal olarak yanlıştır. Car bir Engine değildir. Ayrıca dışarıdan myCar.getHorsepower() çağrılabilir — bu, Engine'in iç detaylarını Car'ın arayüzüne sızdırır.

Doğru: Engine'i Composition ile Kullanmak

// İYİ — "Car has an Engine"
class Engine {
public:
    void start() { std::cout << "Engine started\n"; }
    void stop() { std::cout << "Engine stopped\n"; }
    int getHorsepower() const { return 150; }
};

class Car {
    Engine engine;   // Composition — "has-a" ilişkisi
    std::string model;
    
public:
    Car(const std::string& m) : model(m) {}
    
    void start() {
        engine.start();
        std::cout << model << " is ready to go!\n";
    }
    
    void drive() {
        std::cout << model << " is driving with " 
                  << engine.getHorsepower() << "hp\n";
    }
};

int main() {
    Car car("BMW 320i");
    car.start();
    car.drive();
}

Composition ile Car, Engine'i kullanır ama onun arayüzünü dışarıya açmaz. İstediğin fonksiyonları seçerek sunarsın. Bu daha temiz, daha esnek ve daha bakımı kolay bir tasarımdır.

Karar Rehberi

Kendine şu soruları sor:

SoruEvet →Hayır →
X bir Y midir? (is-a)KalıtımComposition
X'in Y'nin tüm arayüzünü sunması mantıklı mı?KalıtımComposition
Y değişirse X de değişmeli mi?KalıtımComposition
X'in birden fazla Y'si olabilir mi?Composition(İkisi de olabilir)

Kalıtım ve Atama Operatörü

Derived class nesneleri arasında atama yaparken dikkatli ol:

class Base {
public:
    int x;
    Base(int val) : x(val) {}
};

class Derived : public Base {
public:
    int y;
    Derived(int a, int b) : Base(a), y(b) {}
};

int main() {
    Derived d1(1, 2);
    Derived d2(3, 4);
    
    d1 = d2;  // OK — hem x hem y kopyalanır
    
    Base& baseRef = d1;
    Base base(10);
    baseRef = base;  // DİKKAT! Sadece Base kısmı (x) atanır, y değişmez!
}

Son atamada d1.x 10 olur ama d1.y hâlâ 4'tür. Bu partial assignment (kısmi atama) sorunu, polimorfik hiyerarşilerde beklenmedik davranışlara yol açabilir.


struct vs class Kalıtımda

C++'ta struct ve class arasında tek fark varsayılan erişim belirtecidir:

struct Base {};

struct Derived : Base {};     // public kalıtım (struct varsayılanı)
class Derived2 : Base {};     // private kalıtım (class varsayılanı)
class Derived3 : public Base {};  // public kalıtım (açıkça belirtilmiş)

struct ile kalıtım yaparsan varsayılan public, class ile yaparsan varsayılan private. Ama her zaman açıkça yazman en iyi pratiktir — varsayılanlara güvenme.


Özet

  • Kalıtım, bir sınıfın başka bir sınıfın özelliklerini ve davranışlarını devralması demektir. Base class miras verir, derived class miras alır.

  • public kalıtım en yaygın türdür ve "is-a" ilişkisini ifade eder. Protected ve private kalıtım nadiren kullanılır.

  • protected üyeler, derived class'lardan erişilebilir ama dışarıdan erişilemez — private ile public arasında bir orta yol.

  • Base class constructor, derived class constructor'ından önce çalışır. Parametreli constructor'ı initializer list ile çağırırsın.

  • Object slicing, derived nesneyi base değere kopyalarken derived kısmın kesilmesidir — pointer veya referans kullanarak önlenir.

  • Kalıtım güçlüdür ama her yerde kullanılmamalı — "is-a" ilişkisi yoksa composition tercih et. "Prefer composition over inheritance" altın kuralını unutma.