C++ ile Nesne Yönelimli Programlama

C++ ile Nesne Yönelimli Programlama

C++ sınıfları, nesne ömrü, referanslar, kaynak yönetimi, aşırı yükleme, kalıtım, çok biçimlilik, akışlar ve standart kütüphane üzerine ayrıntılı ders notları.

C++ kapsamı nesne yönelimli programlama ve kaynak yönetimine odaklanır. C++11 sonrasında yerleşen kavramlar; sınıf, nesne ömrü, kalıtım, çok biçimlilik ve standart kütüphane eksenindeki temel yapı ile birlikte ele alınır.

Ünite 1: C++'a Geçiş

C ve C++ ilişkisi

C++ birçok C sözdizimi öğesini paylaşır ancak iki dil birebir uyumlu değildir.

Geçerli C kodu her zaman geçerli C++ değildir.

Örnek farklar:

  • tür dönüşümü kuralları,
  • void * dönüşümleri,
  • karakter sabitlerinin türü,
  • anahtar sözcükler,
  • designated initialization ayrıntıları,
  • değişken uzunluklu diziler,
  • standart kütüphane kullanımı.

Bu nedenle .c dosyasını yalnız .cpp olarak yeniden adlandırmak sistematik bir C++ geçiş yöntemi değildir.

C++ programının temel yapısı

#include <iostream>

int main()
{
    std::cout << "Merhaba\
";
}

C++'ta main dönüş türü yine inttir.

return 0; satırı main sonunda yazılmasa da başarılı sonlandırma kabul edilir, ancak açık yazımı öğretici olabilir.

namespace

Standart C++ kütüphanesinin isimleri:

std::

ad alanındadır.

Örnek:

std::cout
std::string
std::vector

Genel başlık dosyalarında:

using namespace std;

kullanmak ad çakışmalarını büyütebildiği için önerilmez.

C++ başlatma biçimleri

int a = 10;
int b(10);
int c{10};

C++ farklı initialization biçimlerine sahiptir.

Süslü parantezli initialization daraltıcı dönüşümleri engelleyebilir:

int x{3.14};   // hata

Bu nedenle modern C++ kodunda {} sık kullanılır.

auto

auto count = 10;
auto ratio = 2.5;

auto derleyicinin türü initializer'dan çıkarmasını sağlar.

Türü gizlemek amacıyla değil, açıkça çıkarılabildiği veya karmaşık türün tekrarının gereksiz olduğu yerlerde kullanılması uygundur.

const ve constexpr

const nesnenin ilgili erişim yoluyla değiştirilememesini sağlar.

const int limit = 100;

constexpr ise sabit ifade bağlamında kullanılabilen değer veya işlevleri tanımlamak için kullanılır:

constexpr int square(int x)
{
    return x * x;
}

nullptr

C++11 ve sonrasında null pointer için:

nullptr

tercih edilir.

int *p = nullptr;

0 veya NULL kullanımındaki bazı overload belirsizliklerini ortadan kaldırır.

Giriş ve çıkış

int value{};
std::cin >> value;
std::cout << value << '\
';

Metin satırı:

std::string line;
std::getline(std::cin, line);

ile okunabilir.

std::string

C++'ta metin için standart sınıf:

std::string

kullanılır.

std::string first = "Ali";
std::string last = "Koker";
std::string full = first + " " + last;

C karakter dizileri gerektiğinde kullanılabilir, ancak genel uygulama kodunda std::string bellek yönetimini ve birçok işlemi daha güvenli hale getirir.

std::vector

Dinamik dizi için:

std::vector<int> values;

kullanılabilir.

values.push_back(10);
values.push_back(20);

Bellek yönetimini otomatik yapar.

Ham:

new int[n]

yerine sahiplik gereksinimi yalnız dinamik dizi ise çoğunlukla std::vector daha uygundur.

Ünite 2: Nesneye Yönelik Programlama

Nesne ve sınıf

Sınıf:

  • veri,
  • bu veri üzerinde çalışan işlevler,
  • erişim kuralları,
  • nesne yaşam döngüsü

için bir tür tanımıdır.

Örnek:

class Counter {
public:
    void increment()
    {
        ++value_;
    }

    int value() const
    {
        return value_;
    }

private:
    int value_{};
};

Nesne:

Counter c;

sınıfın örneğidir.

Soyutlama

Soyutlama, kullanıcının gerekli davranışı görmesini, gereksiz gerçekleştirim ayrıntılarından bağımsız kalmasını sağlar.

Örneğin:

account.withdraw(100);

çağrısının kullanıcı tarafı:

  • bakiyenin hangi veri yapısında tutulduğunu,
  • işlem kaydının nasıl saklandığını,
  • doğrulamanın nasıl yapıldığını

bilmek zorunda değildir.

İyi soyutlama "hangi iş yapılabilir?" sorusunu açık, "nasıl yapılıyor?" ayrıntısını ise gerektiği kadar gizli tutar.

Kapsülleme

Kapsülleme yalnız alanları private yapmak değildir.

Asıl amaç:

  • sınıfın geçerli durum koşullarını korumak,
  • dış kullanımı kontrollü arabirim üzerinden yapmak,
  • iç gerçekleştirimi değiştirebilmektir.

Örnek:

class BankAccount {
public:
    explicit BankAccount(double initial)
        : balance_{initial}
    {
    }

    bool withdraw(double amount)
    {
        if (amount < 0 || amount > balance_) {
            return false;
        }

        balance_ -= amount;
        return true;
    }

    double balance() const
    {
        return balance_;
    }

private:
    double balance_{};
};

Dış kod:

account.balance_ = -1000000;

gibi sınıfın kuralını bozan doğrudan değişiklik yapamaz.

public, private ve protected

public üyeler sınıfın dış arabirimidir.

private üyeler sınıf dışından doğrudan kullanılamaz.

protected üyeler sınıf ve türetilmiş sınıflardan erişilebilir.

Veri alanlarını varsayılan olarak private tutmak, davranışı üye işlevlerle yönetmek yaygın tasarım ilkesidir.

struct ve class

C++'ta struct ile class yetenek bakımından büyük ölçüde aynıdır.

Temel fark varsayılan erişimdir:

struct -> public
class  -> private

Ayrıca struct için varsayılan kalıtım public, class için privatedır.

Basit veri taşıyan türlerde struct, invariant ve davranış içeren kapsüllenmiş türlerde class kullanımı yaygın bir gelenektir.

Kurucu

Constructor nesnenin başlangıç durumunu kurar.

class Point {
public:
    Point(int x, int y)
        : x_{x}, y_{y}
    {
    }

private:
    int x_;
    int y_;
};

Üye initializer list:

: x_{x}, y_{y}

üyelerin gövdeden önce doğrudan başlatılmasını sağlar.

Varsayılan kurucu

class Point {
public:
    Point() = default;
};

derleyici tarafından üretilebilen davranışı açıkça talep eder.

explicit

Tek argümanlı kurucularda istenmeyen örtük dönüşümleri engellemek için:

explicit Distance(double meters);

kullanılabilir.

Yıkıcı

Destructor:

~Resource()
{
    ...
}

nesnenin yaşamı bittiğinde çalışır.

Modern C++'ta destructor'ın temel önemi RAII ile kaynak serbest bırakmadır.

RAII

Resource Acquisition Is Initialization, kaynağın nesne yaşam süresine bağlanmasıdır.

Örnek kaynaklar:

  • dinamik bellek,
  • dosya,
  • mutex,
  • socket,
  • işletim sistemi handle'ı.
{
    std::ifstream input{"data.txt"};
    // ...
}

blok sonunda input nesnesinin destructor'ı dosyayı kapatır.

Bu yaklaşım istisna dahil bütün çıkış yollarında kaynak yönetimini güvenilir hale getirir.

Bir nesneyi diğerine atama

Point a{1, 2};
Point b{3, 4};

b = a;

sınıfın copy assignment işlemini çağırır.

Kullanıcı özel bir kopyalama semantiği tanımlamazsa uygun koşullarda derleyici üye bazlı kopyalama üretir.

Kopya kurucusu

Type(const Type& other);

mevcut nesneden yeni nesne oluşturur.

Taşıma semantiği

Modern C++'ta kaynak sahipliğini kopyalamadan aktarabilmek için move semantics bulunur.

Type(Type&& other);
Type& operator=(Type&& other);

Bu özellikle:

  • dinamik kaynak sahipliği,
  • büyük container'lar,
  • geçici nesneler

için önemlidir.

Rule of Zero

Sınıf kaynak yönetimini doğrudan ham işaretçilerle yapmak yerine:

  • std::vector,
  • std::string,
  • std::unique_ptr,
  • standart kaynak yöneticileri

kullanıyorsa çoğu durumda destructor, copy ve move işlevlerini elle yazmaya gerek kalmaz.

Bu Rule of Zero yaklaşımıdır ve modern C++ için tercih edilir.

Ünite 3: C++ Referansları, Nesne İşaretçileri ve Bellek Yönetimi

Referans

C++ reference:

int x = 10;
int& ref = x;

başlangıçta bir nesneye bağlanır.

ref = 20;

x değerini değiştirir.

C++ referansı C'deki işaretçi ile aynı dil özelliği değildir.

Değer ile parametre

void f(int x);

nesnenin değeri fonksiyon parametresine aktarılır.

const referans

Büyük nesneleri kopyalamadan salt okunur geçirmek için:

void print(const std::string& text);

kullanılabilir.

Referans ile değiştirme

void increment(int& value)
{
    ++value;
}

çağıranın nesnesini doğrudan değiştirebilir.

İşaretçi ve referans seçimi

Kabaca:

  • null olabilme anlamlıysa işaretçi,
  • "bu nesne mutlaka vardır" ilişkisi varsa referans

uygun olabilir.

Ancak sahiplik, yaşam süresi ve API sözleşmesi de seçime dahildir.

this

Statik olmayan üye işlev içinde:

this

mevcut nesneyi gösterir.

class Point {
public:
    Point& setX(int x)
    {
        this->x_ = x;
        return *this;
    }

private:
    int x_{};
};

Birçok durumda this-> açıkça yazılmak zorunda değildir.

new ve delete

Tek nesne:

int *p = new int{42};
delete p;

Dizi:

int *a = new int[100];
delete[] a;

Doğru eşleşme gerekir:

new     -> delete
new[]   -> delete[]

Ham new/delete neden sınırlandırılmalı?

Manuel sahiplik:

  • leak,
  • double delete,
  • istisna sırasında kaynak kaybı,
  • karmaşık yaşam süresi

sorunlarına açıktır.

Modern C++'ta:

std::vector<int>
std::string
std::unique_ptr<T>
std::shared_ptr<T>

gibi RAII türleri tercih edilir.

unique_ptr

Tek sahiplik:

auto p = std::make_unique<MyType>();

p scope dışına çıktığında nesne otomatik silinir.

Kopyalanamaz, taşınabilir.

shared_ptr

Paylaşılan sahiplik:

auto p = std::make_shared<MyType>();

referans sayımı kullanır.

Her nesne ilişkisi için shared_ptr kullanmak doğru değildir. Sahiplik gerçekten paylaşılıyorsa kullanılmalıdır.

Karşılıklı güçlü referanslar döngü oluşturabilir. Gerekli yerde:

std::weak_ptr

kullanılır.

Nesne dizileri

std::array<Point, 10> fixed_points;

sabit boyut için uygundur.

Dinamik boyut:

std::vector<Point> points;

ile daha doğal yönetilir.

Ham:

Point *p = new Point[n];

modern genel uygulama kodunda çoğunlukla gerekli değildir.

Ünite 4: Fonksiyon ve İşleç Aşırı Yükleme

Fonksiyon aşırı yükleme

Aynı ad farklı parametre listeleriyle kullanılabilir:

int abs_value(int x);
double abs_value(double x);

Derleyici çağrıdaki argüman türlerine göre uygun overload'u seçer.

Yalnız dönüş türünün değişmesi yeterli değildir:

int f(int);
double f(int);   // geçersiz

Varsayılan argüman

void log(std::string_view text, int level = 1);

çağrılar:

log("başladı");
log("hata", 3);

biçiminde olabilir.

Varsayılan argümanlar parametre listesinin uygun sağ tarafında düzenlenmelidir.

Kurucu aşırı yükleme

class Point {
public:
    Point() = default;

    Point(int x, int y)
        : x_{x}, y_{y}
    {
    }

private:
    int x_{};
    int y_{};
};

aynı sınıfı farklı başlangıç verileriyle kurmayı sağlar.

Delege eden kurucu

class Point {
public:
    Point()
        : Point{0, 0}
    {
    }

    Point(int x, int y)
        : x_{x}, y_{y}
    {
    }

private:
    int x_;
    int y_;
};

başlatma mantığının tekrarını azaltır.

İşleç aşırı yükleme

Kullanıcı tanımlı türler için birçok işlecin davranışı tanımlanabilir.

Örnek:

class Vector2 {
public:
    Vector2(double x, double y)
        : x_{x}, y_{y}
    {
    }

    Vector2 operator+(const Vector2& other) const
    {
        return {x_ + other.x_, y_ + other.y_};
    }

private:
    double x_;
    double y_;
};

Kullanım:

Vector2 c = a + b;

İşleç aşırı yüklemede tasarım

İşlecin anlamı kullanıcı beklentisine yakın kalmalıdır.

Örneğin:

a + b

ifadesinin a nesnesini sürpriz biçimde silmesi veya dosyayı kapatması kötü tasarımdır.

Aşırı yükleme sözdizim kolaylığı sağlar, anlam karmaşasını haklı çıkarmaz.

Karşılaştırma

Modern C++'ta eşitlik ve sıralama işleçleri uygun biçimde tanımlanabilir.

C++20 ile üç yönlü karşılaştırma:

operator<=>

bazı türlerde karşılaştırma işlemlerini sadeleştirebilir.

Artırma işleci

Prefix:

T& operator++();

Postfix:

T operator++(int);

dummy int parametresi iki overload'u ayırır.

Ünite 5: Kalıtım ve Çok Biçimlilik

Kalıtım

Bir sınıf başka bir sınıfın arabirim ve davranışının bir bölümünü devralabilir.

class Shape {
    ...
};

class Circle : public Shape {
    ...
};

Public kalıtım genel olarak:

Circle bir Shape'dir

ilişkisini modellemelidir.

Sırf kod tekrarını azaltmak için kalıtım kullanmak uygun olmayabilir. Bileşim çoğu durumda daha esnek çözüm sağlar.

public kalıtım

Temel sınıfın:

  • public üyeleri türetilmiş sınıfta public,
  • protected üyeleri protected

olarak kalır.

private üyelere türetilmiş sınıf doğrudan erişemez.

protected kalıtım

Public ve protected üyeler türetilmiş sınıfta protected hale gelir.

Dış API'de "is-a" ilişkisini sakladığı için genel modellemede daha seyrek kullanılır.

private kalıtım

Temel sınıfın public ve protected üyeleri türetilmiş sınıfta private olur.

Bu daha çok gerçekleştirim tekniği olarak görülebilir. Çoğu durumda composition daha açık olabilir.

Kurucu sırası

Türetilmiş nesne oluşturulurken genel sıra:

temel sınıf
↓
üye nesneler
↓
türetilmiş sınıf

Yıkım ters sırada olur.

Çoklu kalıtım

C++ birden fazla temel sınıftan kalıtımı destekler:

class C : public A, public B {
};

Bu olanak güçlüdür fakat:

  • ad belirsizliği,
  • diamond problemi,
  • karmaşık yaşam döngüsü

oluşturabilir.

Arayüz benzeri küçük soyut temel sınıfların birleştirilmesi daha kontrollü kullanım alanıdır.

Diamond problemi

    A
   / \
  B   C
   \ /
    D

B ve C ayrı ayrı A'dan türetilirse D içinde iki ayrı A alt nesnesi bulunabilir.

Virtual inheritance:

class B : virtual public A {};
class C : virtual public A {};

tek ortak A alt nesnesi kullanılmasını sağlayabilir.

Sanal fonksiyon

Çalışma zamanı çok biçimliliği için:

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

Türetilmiş sınıf:

class Circle : public Shape {
public:
    explicit Circle(double r)
        : r_{r}
    {
    }

    double area() const override
    {
        return 3.141592653589793 * r_ * r_;
    }

private:
    double r_;
};

override

double area() const override;

derleyicinin gerçekten bir temel sınıf sanal işlevinin override edildiğini doğrulamasını sağlar.

Yanlış imza gibi hataları erken yakalar.

virtual destructor

Polimorfik temel sınıf nesnesi temel sınıf işaretçisi üzerinden silinecekse destructor sanal olmalıdır:

virtual ~Base() = default;

Aksi halde davranış hatalı olabilir.

Saf sanal fonksiyon

virtual void draw() = 0;

sınıfı soyut hale getirir.

Soyut sınıftan doğrudan nesne oluşturulamaz.

Dinamik dispatch

Shape& s = circle;
s.draw();

çağrısı nesnenin gerçek dinamik türüne göre uygun override'ı çalıştırır.

Bu çalışma zamanı polymorphismdir.

Statik çok biçimlilik

C++'ta templates ve overload'lar derleme zamanı çok biçimliliği sağlar.

Örnek:

template <typename T>
T max_value(T a, T b)
{
    return a > b ? a : b;
}

Bu yapı sanal çağrıdan farklıdır. Uygun kod tür başına derleme sırasında oluşturulur.

Ünite 6: C++ Giriş Çıkış ve Dosyalar

Akımlar

C++ I/O sistemi akış soyutlamasına dayanır.

Temel akımlar:

std::cin
std::cout
std::cerr
std::clog

Çıktı

std::cout << "Değer: " << value << '\
';

'\ ' yalnız yeni satır ekler.

std::endl

yeni satırın yanında akışı flush eder. Her satırda gereksiz std::endl kullanmak performansı azaltabilir.

Girdi

int value{};

if (std::cin >> value) {
    ...
}

Akış durumunu denetlemek hatalı girdiyi yönetmek için önemlidir.

Biçimlendirme

#include <iomanip>

std::cout
    << std::fixed
    << std::setprecision(2)
    << value;

Dosya yazma

#include <fstream>

std::ofstream out{"data.txt"};

if (!out) {
    // hata
}

out << 10 << ' ' << 20 << '\
';

Dosya nesnesi RAII sayesinde kapsam sonunda kapanır.

Dosya okuma

std::ifstream in{"data.txt"};

int a{};
int b{};

if (in >> a >> b) {
    ...
}

Satır okuma

std::string line;

while (std::getline(in, line)) {
    ...
}

İkili dosyalar

std::ofstream out{
    "data.bin",
    std::ios::binary
};

Ham nesne belleğini doğrudan kalıcı biçim olarak yazmak C++ için de taşınabilir değildir.

Özellikle sınıflar için:

  • vtable işaretçileri,
  • dolgulama,
  • işaretçiler,
  • endian,
  • ABI

nedeniyle açık serileştirme gerekir.

Rastgele erişim

stream.seekg(position);
stream.tellg();
stream.seekp(position);
stream.tellp();

ile dosya konumu yönetilebilir.

Ünite 7: C++ Standart Kütüphanesi

Standart Şablon Kütüphanesi yaklaşımı

C++ standart kütüphanesinde container, iterator ve algoritmalar birlikte çalışır.

Kavramsal model:

Container
   ↓ iterator range
Algorithm

Bu ayrım veri yapısı ile algoritmanın gevşek bağlı kalmasını sağlar.

vector

std::vector<int> values{1, 2, 3};

özellikleri:

  • ardışık bellek,
  • rastgele erişim O(1),
  • sona ekleme amortize O(1),
  • ortada ekleme/silme O(n).
values.push_back(4);
values.pop_back();

std::vector, genel amaçlı değişken boyutlu ardışık veri için çoğu zaman varsayılan container seçimidir.

array

Sabit boyutlu dizi:

std::array<int, 4> values{1, 2, 3, 4};

C dizisine benzer ardışık saklama sağlar fakat:

  • size,
  • iterator,
  • assignment

gibi standart container olanaklarını sunar.

list

std::list<int>

çift yönlü bağlantılı listedir.

Bilinen konumda ekleme ve silme sabit zamanlı olabilir.

Ancak:

  • rastgele erişim yoktur,
  • her elemanda bağlantı maliyeti vardır,
  • önbellek locality düşüktür.

Bu nedenle std::list, std::vector yerine otomatik olarak daha verimli değildir.

deque

std::deque<int>

iki uçtan büyüyebilen sıra yapısıdır.

Başta ve sonda ekleme/silme verimlidir.

Bellek yerleşimi vector gibi tek parça ardışık olmak zorunda değildir.

stack

std::stack<int> stack;

LIFO adaptörüdür.

stack.push(10);
stack.top();
stack.pop();

kuyruk

std::queue<int> queue;

FIFO adaptörüdür.

priority_kuyruk (queue)

std::priority_queue<int>

varsayılan olarak en büyük elemanı tepeye getiren öncelik kuyruğudur.

Öbek tabanlı gerçekleştirim yaygındır.

associative containers

std::set
std::map
std::multiset
std::multimap

sıralı ilişkisel container'lardır.

Yaygın gerçekleştirim dengeli ağaçtır.

Tipik arama:

O(log n)

unordered containers

std::unordered_set
std::unordered_map

hash tablosu yaklaşımına dayanır.

Ortalama arama:

O(1)

olabilir.

Kötü durum garantisi aynı değildir.

Iterator

Iterator container elemanları üzerinde konum ve dolaşım soyutlamasıdır.

Örnek:

for (auto it = values.begin(); it != values.end(); ++it) {
    std::cout << *it << '\
';
}

Modern C++'ta range-for daha sade olabilir:

for (const auto& value : values) {
    std::cout << value << '\
';
}

Iterator kategorileri

Klasik kategoriler:

  • girdi,
  • output,
  • forward,
  • bidirectional,
  • random access,
  • contiguous

olarak düşünülür.

Her algoritma her iterator türüyle çalışamaz.

Örneğin rastgele erişim gerektiren algoritma std::list iterator'ıyla kullanılamaz.

find

auto it = std::find(values.begin(), values.end(), target);

dizilim içinde doğrusal arama yapar.

Bulunamazsa:

it == values.end()

olur.

sort

std::sort(values.begin(), values.end());

random-access iterator gerektirir.

Standart karmaşıklık:

O(n log n)

karşılaştırma düzeyindedir.

count

auto n = std::count(
    values.begin(),
    values.end(),
    target
);

eşleşen eleman sayısını verir.

Alt dizi araması:

std::search(
    sequence.begin(),
    sequence.end(),
    pattern.begin(),
    pattern.end()
);

ile yapılabilir.

transform

std::transform(
    input.begin(),
    input.end(),
    output.begin(),
    operation
);

her elemana dönüşüm uygular.

Modern C++'ta lambda ile:

std::transform(
    values.begin(),
    values.end(),
    result.begin(),
    [](int x) { return x * x; }
);

kullanımı doğaldır.

Lambda

auto square = [](int x) {
    return x * x;
};

adı olmayan fonksiyon nesnesi oluşturur.

Algoritmalarla kullanımı:

std::sort(
    people.begin(),
    people.end(),
    [](const Person& a, const Person& b) {
        return a.age < b.age;
    }
);

ranges

C++20 ve sonrasında ranges kütüphanesi algoritma ve görünüm işlemlerini geliştirir.

Örneğin:

std::ranges::sort(values);

iterator çiftini açıkça yazma ihtiyacını azaltabilir.

Views:

  • filtreleme,
  • dönüştürme,
  • tembel veri işleme

için kullanılabilir.

Dersin klasik STL container-iterator-algorithm modeli ranges yaklaşımının da temelini oluşturur.

Ünite 8: Modern C++ Kaynak ve Tür Güvenliği

Raw pointer sahiplik değildir

Ham işaretçi:

T*

tek başına nesnenin sahibinin kim olduğunu belirtmez.

Modern tasarımda ayrım açık olmalıdır:

  • sahiplik,
  • gözlem,
  • geçici erişim.

Tek sahiplik:

std::unique_ptr<T>

Paylaşılan sahiplik:

std::shared_ptr<T>

Sahip olmayan erişim:

T*
T&
std::span<T>

gibi türlerle ifade edilebilir.

std::span

Ardışık veri aralığını sahiplenmeden temsil eder:

void process(std::span<const int> values)
{
    for (int x : values) {
        ...
    }
}

Bu sayede dizi ve std::vector gibi farklı ardışık kaynaklar ortak arabirimle kullanılabilir.

std::string_view

Sahip olmayan metin görünümü:

void print(std::string_view text);

gereksiz std::string kopyalarını azaltabilir.

Ancak gösterdiği karakterlerin yaşam süresini uzatmaz. Dangling string_view oluşturulmamalıdır.

Istisna

C++ hata aktarımı için istisna mekanizması sağlar:

throw std::runtime_error{"hata"};

yakalama:

try {
    ...
} catch (const std::exception& e) {
    ...
}

RAII, istisna kullanılsa da kaynakların doğru serbest bırakılabilmesini sağlar.

noexcept

Bir fonksiyon:

void swap(Type& other) noexcept;

ile istisna fırlatmayacağını bildirebilir.

Özellikle move operation'larda noexcept, standart container optimizasyonlarını etkileyebilir.

Undefined behavior

Hem C hem C++'ta bazı hatalar "hata kodu üretir" biçiminde tanımlanmaz. Davranış tamamen tanımsız olabilir.

Örnekler:

  • dizi sınırı dışı erişim,
  • serbest bırakılmış belleğe erişim,
  • null işaretçiyi dereference etme,
  • signed integer overflow,
  • bazı yaşam süresi ihlalleri.

Undefined behavior, yalnız beklenmedik çıktı değil, derleyicinin program hakkında çok farklı optimizasyon varsayımları yapabilmesi anlamına gelir.

Derleyici uyarıları

Geliştirme sırasında yüksek uyarı düzeyi kullanılmalıdır.

GCC/Clang için örnek:

-Wall
-Wextra
-Wpedantic
-Wconversion

Her uyarı mutlak hata değildir, fakat sessiz tür dönüşümleri ve olası kusurları görünür hale getirir.

Sanitizer

Geliştirme ve testte:

AddressSanitizer
UndefinedBehaviorSanitizer
ThreadSanitizer

gibi araçlar:

  • bellek taşması,
  • use-after-free,
  • undefined behavior,
  • veri yarışı

gibi hataları bulmada çok değerlidir.

C ve C++ için güvenli varsayımlar

C'de:

  • boyutları açık taşı,
  • gets kullanma,
  • scanf ve dosya işlemlerinin dönüşlerini denetle,
  • dinamik belleğin sahipliğini açık tut,
  • malloc çarpımında taşmayı değerlendir,
  • işaretçi yaşam süresini takip et.

C++'ta bunlara ek olarak:

  • raw new/delete yerine RAII,
  • C karakter dizisi yerine uygun olduğunda std::string,
  • dinamik dizi yerine std::vector,
  • null için nullptr,
  • sahiplik için smart pointer,
  • kopya/move davranışında Rule of Zero,
  • polimorfik temelde virtual destructor

tercih edilmelidir.

Ünite 9: Şablonlar, Concepts ve Jenerik Programlama

C++'ta jenerik programlama yalnız "aynı fonksiyonu farklı türlerle çalıştırmak" değildir. Amaç, tür bilgisini derleme zamanında koruyarak tekrar kullanılabilir algoritmalar yazmak ve yanlış tür kullanımını mümkün olduğunca erken reddetmektir.

Fonksiyon ve sınıf şablonları

Fonksiyon şablonu, parametre türünü derleme zamanında belirler:

template <class T>
T maxValue(const T& a, const T& b) {
    return b < a ? a : b;
}

Derleyici gerekli türler için somutlaştırma (instantiation) yapar. Bu model çalışma zamanı tür sorgusuna ihtiyaç duymadan sıfır maliyetli soyutlama sağlayabilir; fakat çok sayıda somutlaştırma ikili boyutunu ve derleme süresini artırabilir.

Sınıf şablonları aynı yaklaşımı veri yapısına taşır. std::vector<T>, std::optional<T> ve akıllı işaretçiler bunun yaygın örnekleridir.

Kısıtlar ve concept

Şablonun yalnız sözdizimsel olarak geçerli olması yeterli değildir; algoritmanın türden beklediği semantik işlemler de ifade edilmelidir. C++20 ile concept ve requires, şablon kısıtlarını dil düzeyinde görünür hale getirir:

template <class T>
concept Addable = requires(T a, T b) {
    a + b;
};

template <Addable T>
T add(const T& a, const T& b) {
    return a + b;
}

concept, kalıtım hiyerarşisinin yerine geçen bir çalışma zamanı arabirimi değildir. Derleme zamanı gereksinimidir ve aşırı yükleme çözümüne katılır.

constexpr, consteval ve derleme zamanı hesaplama

constexpr, bir işlevin veya değerin uygun bağlamda derleme zamanında hesaplanabilmesine izin verir; her çağrının zorunlu olarak derleme zamanında çalışacağı anlamına gelmez. consteval işlev ise çağrının sabit ifade olarak değerlendirilmesini zorunlu kılar.

Derleme zamanı hesaplama, çalışma zamanı maliyetini azaltabilir; buna karşılık derleme süresi, hata iletilerinin karmaşıklığı ve ikili boyutu açısından maliyet üretebilir. Bu nedenle yalnız "compile-time daha hızlıdır" gibi tek eksenli bir değerlendirme doğru değildir.

İletme referansları ve std::forward

Bir şablonda T&& biçimindeki parametre, tür çıkarımı yapılıyorsa iletme referansı (forwarding reference) olabilir. std::forward, argümanın lvalue/rvalue niteliğini iç çağrıya koruyarak iletmeyi sağlar.

Bu mekanizma generic fabrika ve sarmalayıcılarda gereksiz kopyayı önleyebilir; ancak yanlış kullanıldığında nesne ömrünü ve aşırı yükleme çözümünü okumayı zorlaştırır. Sahiplik aktarımı ile yalnız değer kategorisini koruma aynı kavram değildir.

Şablon görünürlüğü ve ikili sınırlar

Şablon tanımı genellikle somutlaştırmanın yapıldığı çeviri biriminde görünür olmalıdır. Bu nedenle şablonların başlık dosyalarında tanımlanması yaygındır. Açık somutlaştırma (explicit instantiation) kullanılarak belirli türler için kod üretimi ayrı bir çeviri birimine taşınabilir.

Jenerik tasarım, ABI sınırları açısından normal sanal arabirimlerden farklıdır. Kütüphane arayüzünde hangi türlerin public sözleşmeye taşındığı, derleme bağımlılığı ve ikili uyumluluk birlikte düşünülmelidir.

Standart sürümü ve taslak özellikler

Eylül 2026 itibarıyla yayımlanmış güncel C++ uluslararası standardı ISO/IEC 14882:2024'tür. Bir sonraki revizyon ISO içinde Draft International Standard (DIS) aşamasındadır. Bu nedenle taslakta görülen bir özelliğin yayımlanmış standartta bulunduğu varsayılmamalı; ders ve üretim kodunda "yayımlanmış standart" ile "geliştirilmekte olan taslak" açıkça ayrılmalıdır.

Ünite 10: Eşzamanlılık ve C++ Bellek Modeli

C++ eşzamanlılık modeli yalnız iş parçacığı başlatma API'lerinden ibaret değildir. Bir programın doğru olması için nesne ömrü, veri yarışı, görünürlük ve senkronizasyon ilişkileri birlikte değerlendirilmelidir.

Eşzamanlılık ve paralellik

Eşzamanlılık (concurrency) birden fazla işin zaman içinde ilerleyebilmesidir. Paralellik (parallelism) bu işlerin aynı anda birden fazla yürütme kaynağında çalışmasıdır. Tek çekirdekte eşzamanlı fakat paralel olmayan bir program mümkündür.

Bu ayrım performans tasarımında önemlidir: daha fazla iş parçacığı her zaman daha yüksek throughput anlamına gelmez. Kilit çekişmesi, önbellek etkileri ve zamanlayıcı maliyeti ölçeklenmeyi sınırlayabilir.

Veri yarışı ve tanımsız davranış

İki iş parçacığı aynı bellek konumuna eşzamanlı erişiyor, erişimlerden en az biri yazma yapıyor ve aralarında uygun senkronizasyon yoksa veri yarışı oluşur. C++ bellek modelinde veri yarışı genel olarak tanımsız davranıştır.

volatile bu problemi çözmez. volatile, belirli gözlemlenebilir erişimlerin derleyici tarafından yok edilmemesi gibi amaçlara hizmet eder; iş parçacıkları arası atomiklik veya happens-before ilişkisi sağlamaz.

Mutex ve RAII

Paylaşılan değişmezleri korumanın temel yolu mutex kullanmaktır. Kilidin nesne ömrüne bağlanması istisna ve erken dönüş durumlarında kilidin unutulmasını önler:

std::mutex m;

void update() {
    std::lock_guard<std::mutex> lock(m);
    // korunan durum
}

Birden çok mutex birlikte alınacaksa kilit sırası kaynaklı deadlock riski değerlendirilmelidir. std::scoped_lock birden fazla kilidi birlikte yönetmek için tasarlanmıştır.

Koşul değişkenleri

Koşul değişkeni olay beklemek için kullanılır. Bekleyen iş parçacığı sahte uyanma yaşayabileceği ve durum bekleme sırasında değişebileceği için koşul her uyanmada yeniden sınanmalıdır:

cv.wait(lock, [&] { return ready; });

Bildirim bir "mesaj" değildir; paylaşılan durum ile koşul değişkeninin birlikte tasarlanması gerekir.

Atomikler ve bellek sırası

std::atomic<T>, uygun T türleri için yarışsız atomik işlemler sağlar. Varsayılan sıralama ardışık tutarlılıktır (memory_order_seq_cst) ve güçlü bir zihinsel model sunar.

Acquire/release veya relaxed sıralamalar daha zayıf garantiler verir ve bazı düşük seviyeli tasarımlarda maliyeti azaltabilir; fakat yanlış bir zayıflatma nadiren yalnız performans hatası üretir, doğrudan doğruluk hatasına dönüşebilir. Ölçümle gösterilmiş bir gereksinim olmadan bellek sırasını zayıflatmak güvenli varsayım değildir.

happens-before ilişkisi

Bir iş parçacığındaki yazmanın diğer iş parçacığında güvenilir biçimde görülmesi için yalnız fiziksel olarak "önce çalışmış" olması yeterli değildir. Dilin tanımladığı senkronizasyon ilişkileri üzerinden bir happens-before bağı kurulmalıdır.

Mutex kilitleme/açma, uygun atomik release/acquire işlemleri ve iş parçacığı yaşam döngüsü işlemleri bu ilişkinin kurulmasına katkı sağlayabilir. Donanımın önbellek tutarlılığı tek başına C++ programının veri yarışı içermediğini kanıtlamaz.

İptal, kapanış ve yaşam döngüsü

Uzun yaşayan iş parçacıklarında yalnız başlatma değil kapanış protokolü de tasarlanmalıdır. std::jthread, scope sonunda join davranışı ve std::stop_token ile işbirlikçi durdurma için bir yapı sağlar.

Güvenilir kapanışta şu sorular açık olmalıdır: işi kim sonlandırır, bekleyen iş parçacığı nasıl uyandırılır, kuyrukta kalan işler ne olur, paylaşılan nesneler hangi sırada yok edilir ve hata durumunda join garantisi nasıl korunur?

Performans: çekişme, false sharing ve belirlenimlilik

Aynı mutex üzerinde artan çekişme throughput'u düşürür. Farklı atomik değişkenlerin aynı önbellek satırını paylaşması false sharing oluşturarak mantıksal olarak bağımsız iş parçacıklarını fiziksel olarak birbirine bağlayabilir.

Performans incelemesinde yalnız CPU kullanımına değil kuyruk uzunluğu, kilit bekleme süresi, cache miss, context switch ve kuyruk gecikmesi gibi ölçülere de bakılmalıdır. Düşük gecikmeli sistemlerde ortalama süre kadar p95/p99 gibi kuyruk gecikmesi ve en kötü davranış da önem taşır.

C ve C++ Arasındaki Temel Farklar

  • Konu: Temel yaklaşım; C: Yordamsal ve sistem programlama; C++: Çok paradigmalı
  • Konu: Sınıf; C: Yok; C++: Var
  • Konu: Kalıtım; C: Yok; C++: Var
  • Konu: Sanal fonksiyon; C: Yok; C++: Var
  • Konu: Fonksiyon aşırı yükleme; C: Yok; C++: Var
  • Konu: İşleç aşırı yükleme; C: Yok; C++: Var
  • Konu: Reference türü; C: Yok; C++: Var
  • Konu: Template; C: Yok; C++: Var
  • Konu: Istisna; C: Yok; C++: Var
  • Konu: Namespace; C: Yok; C++: Var
  • Konu: Dinamik bellek; C: malloc/free; C++: RAII, container, smart pointer, new/delete
  • Konu: Metin; C: char dizisi; C++: std::string ve C dizileri
  • Konu: Dinamik dizi; C: Manuel bellek; C++: std::vector
  • Konu: Jenerik programlama; C: Makro / _Generic gibi olanaklar; C++: Template ve concepts
  • Konu: Standart algoritmalar; C: Daha sınırlı; C++: Geniş algorithms/ranges kütüphanesi

Bu tablo dillerden birinin diğerinden genel olarak "daha iyi" olduğu anlamına gelmez.

C:

  • ABI sadeliği,
  • küçük çalışma zamanı,
  • gömülü sistemler,
  • işletim sistemi çekirdeği,
  • donanım arabirimi,
  • taşınabilir C API

için güçlüdür.

C++:

  • büyük performans duyarlı uygulamalar,
  • kaynak güvenliği,
  • yüksek düzey soyutlama,
  • generic programming,
  • oyun motorları,
  • gerçek zamanlı sistemler,
  • masaüstü uygulamaları,
  • yüksek performanslı kütüphaneler

için geniş araçlar sunar.

Değer Semantiği, RAII ve Sahipliği Birlikte Tasarlamak

Modern C++'ta güvenilir tasarımın merkezi yalnız sınıf/kalıtım değildir; nesne ömrü ve sahipliğin tür sistemiyle görünür kılınmasıdır. Resource Acquisition Is Initialization (RAII), kaynak edinimini nesne ömrüne bağlayarak normal dönüş ve exception sırasında aynı temizleme kuralının çalışmasını sağlar.

Rule of Zero ve Rule of Five

Bir sınıfın kaynağı std::vector, std::string, std::unique_ptr gibi RAII türleriyle tutulabiliyorsa çoğu zaman destructor, copy/move constructor ve assignment operatörlerini elle yazmaya gerek yoktur; bu Rule of Zero yaklaşımıdır.

Sınıf doğrudan özel bir kaynağı yönetmek zorundaysa copy/move davranışının tamamı birlikte düşünülmelidir. Kopyalama kaynağı çoğaltacak mı, yasak mı olacak; taşıma sonrası kaynak nesnenin geçerli fakat tanımlı bir durumda kalması nasıl sağlanacak? Bu sorular bağımsız değildir.

unique_ptr, shared_ptr ve çıplak pointer

std::unique_ptr<T> tekil sahipliği, std::shared_ptr<T> paylaşılan referans sayımlı sahipliği ifade eder. shared_ptr her pointer için güvenli varsayılan değildir; ek tahsis/referans sayımı maliyeti ve döngüsel sahiplik riski taşır. std::weak_ptr paylaşılan sahiplik döngüsünü kırmak için kullanılabilir.

Çıplak pointer (T*) modern C++'ta hâlâ geçerli bir araçtır; özellikle sahiplik taşımayan, nullable gözlem ilişkisini ifade edebilir. Sorun pointer'ın varlığı değil, sahiplik semantiğinin belirsizliğidir.

Copy ve move semantiği

Move işlemi çoğu zaman pahalı kaynağın sahipliğini yeni nesneye aktarır; nesnenin bitlerini zorunlu olarak kopyalamadan “sihirli hız” sağlamaz. Taşınmış nesne geçerli fakat içeriği kullanım sözleşmesine bağlı bir durumda kalır. std::move da taşıma yapmaz; nesneyi move overload seçimine uygun bir value category'ye dönüştürür.

Container'lar yeniden ayırma sırasında güçlü exception garantisini korumak için noexcept move constructor bilgisinden yararlanabilir. Bu nedenle noexcept yalnız stil değil, generic container davranışına etki eden bir kontrattır.

Polymorphism ve destructor

Bir nesne base-class pointer üzerinden silinecekse base destructor'ın uygun biçimde virtual olması gerekir. Aksi hâlde derived kısmın destructor'ının çalışmaması tanımsız davranışa yol açabilir. Buna karşılık her sınıfa refleks olarak virtual destructor eklemek de gereksiz vtable/semantik değişiklik yaratır; gereksinim polymorphic deletion modelinden gelir.

Composition ve inheritance

Kalıtım bir “kod tekrarını azaltma” mekanizmasından daha güçlüdür: public inheritance is-a ve substitutability iddiası taşır. Yalnız ortak kod kullanmak için kalıtım seçmek kırılgan hiyerarşiler oluşturabilir. Composition, sahiplik ve işbirliğini çoğu zaman daha açık ifade eder.

Generic programlama ve kavramlar

Template'ler derleme zamanında türden bağımsız algoritmalar kurmayı sağlar. C++20 concepts, template parametresinin karşılaması gereken semantik/sözdizimsel gereksinimleri açıkça kısıtlayabilir. Böylece uzun substitution hataları yerine interface kontratına daha yakın diagnostics elde edilir.

Bu yaklaşım C++'ın temel tasarım fikrini özetler: performans maliyetini gizlemeden daha yüksek seviyeli soyutlama kurmak.

Sahiplik, RAII ve istisna güvenliği

Modern C++ tasarımında asıl soru "hangi sınıf hangi sınıftan kalıtım almalı?" değil, kaynağın sahibinin kim olduğudur. Bellek, dosya tanıtıcısı, kilit veya socket gibi kaynakların ömrü nesne ömrüne bağlandığında RAII, normal dönüşte de istisna yolunda da temizliği tek bir kurala indirger.

std::unique_ptr tek sahipliği, std::shared_ptr ortak sahipliği ifade eder; ancak ortak sahiplik varsayılan seçim değildir. Gereksiz shared_ptr çevrimleri yaşam süresini belirsizleştirir ve döngüsel referans riski yaratır. Sahip olmayan gözlem ilişkileri pointer/reference veya uygun view türleriyle açıkça ayrılmalıdır.

İstisna güvenliği açısından bir işlem en azından nesnenin geçerli kalmasını sağlamalıdır. Daha güçlü sözleşmede işlem başarısızsa gözlenebilir durum değişmez. Taşıma semantiği, swap tabanlı tasarım ve küçük sorumluluklu sınıflar bu sözleşmeleri kurmayı kolaylaştırır.

C++ tasarımında kanıtlanabilir sahiplik

Bir sınıf tasarımında sahiplik, yaşam süresi ve istisna davranışı açık değilse arayüzün yalnız okunabilir olması yeterli değildir. Kaynağın tek sahibi varsa std::unique_ptr, değer semantiği uygunsa doğrudan nesne, ortak sahiplik gerçekten gerekiyorsa std::shared_ptr tercihinin gerekçesi yazılabilmelidir.

Doğrulama yalnız birim testine dayanmaz. Sanitizer'lar, statik analiz, farklı optimizasyon düzeyleri ve kopyalama/taşıma yollarını kapsayan testler birlikte kullanıldığında yaşam süresi hataları daha görünür olur. Benchmark gerekiyorsa derleyici sürümü, bayraklar ve veri büyüklüğü sonuçla birlikte kaydedilir.

RAII ve istisna güvenliği için temel soru, başarısızlık sonrasında nesnenin hangi invariant'larının korunacağıdır. Bu sözleşme tanımlandığında “temiz kod” değerlendirmesi biçimden çok davranışa dayanır.

Kalıtım, Çok Biçimlilik ve Nesne Ömrü Arasındaki Sınırlar

C++'ta kalıtım yalnız kod tekrarını azaltma aracı değildir; doğru kullanıldığında bir “türdür” ilişkisini ifade eder. Bir sınıf yalnız ortak birkaç alanı paylaştığı için başka bir sınıftan türetilmemelidir. Nesnelerin yaşam döngüsü veya değişim nedenleri farklıysa bileşim çoğu zaman daha açık bir tasarım verir.

Çalışma zamanlı çok biçimlilik için taban sınıf üzerinden virtual işlev çağrısı gerekir. Ancak nesneyi taban sınıf türüne değer olarak kopyalamak slicing oluşturur ve türetilmiş bölümü kaybettirir. Bu nedenle polimorfik nesneler çoğunlukla referans veya uygun sahiplik modeli taşıyan akıllı işaretçi üzerinden kullanılır.

Taban sınıftan silinecek bir polimorfik nesnede yıkıcının sanal olması kritik bir ömür kuralıdır:

struct Base {
    virtual ~Base() = default;
    virtual void run() = 0;
};

Buradaki amaç yalnız sözdizimsel doğruluk değil, türetilmiş nesnenin kaynaklarının doğru sırayla serbest bırakılmasıdır. RAII bu ömür modelini daha geniş biçimde geneller: kaynak edinimi bir nesnenin kurulmasına, kaynak bırakma ise yıkımına bağlanır. Böylece normal dönüş, istisna veya erken çıkış aynı temizleme kuralına uyar.

Kopyalama ve taşıma semantiği de sahiplikle birlikte düşünülmelidir. Bir sınıf doğrudan kaynak yönetmiyorsa Rule of Zero çoğu zaman en güvenli tasarımdır. Kaynak yönetimi gerçekten sınıfa aitse kopyalama, taşıma ve yıkım davranışının birlikte tanımlanması gerekir.

Bu nedenle nesne yönelimli C++ sorularında yalnız public/private, virtual veya kalıtım ağacını ezberlemek yerine şu üç soruyu birlikte sormak daha açıklayıcıdır: nesnenin gerçek türü nedir, kaynağın sahibi kimdir ve nesnenin ömrü hangi kapsamda sona erer?

Kaynakça

  • ISO/IEC. ISO/IEC 14882:2014 Information Technology — Programming Languages — C++. International Organization for Standardization, 2014. https://www.iso.org/standard/64029.html
  • ISO/IEC. ISO/IEC 14882:2024 Programming Languages — C++. International Organization for Standardization, 2024. https://www.iso.org/standard/83626.html
  • ISO/IEC. ISO/IEC DIS 14882 Programming Languages — C++. Draft International Standard, 2026. https://www.iso.org/standard/91179.html
  • Meyers, S. Effective Modern C++. O'Reilly Media, 2014.
  • Stroustrup, B. A Tour of C++. Addison-Wesley, 2013.
  • Stroustrup, B. The C++ Programming Language. Addison-Wesley, 2013.
İçindekiler
Bu sayfanın QR kodu