İçeriğe geç
academia.sh

Ders 05 / 11

Kapsayıcılar

map'in operator[] ile okumasının anahtar yoksa eleman eklediğinin, find ve contains'in kaynağı aynen bıraktığının ölçümü.

Yayın Güncelleme

Bu derste

  • std::map'te operator[] ile okumanın anahtar yoksa haritaya sıfırla kurulmuş bir eleman eklediğini ölçer
  • find ve contains'in aynı haritayı aynen bıraktığını gösterir
  • Sıralı, ilişkisel ve sırasız kapsayıcılar arasında karmaşıklığa göre seçim yapar
  • const bir haritada operator[] çağrısının neden derlenmediğini açıklar
İçindekiler

Önceki ders Kopyalanamaz ve Taşınamaz Tipler’de bir sınıfın kendi taşıyan yapıcısını yazarak taşınmış kaynağa hangi boş durumu bırakacağına karar verdiğini gösterdi — Tutamac’ın numarası -1 oluyordu, çünkü taşıyan yapıcı öyle yazılmıştı. Bu ders standart kütüphanenin hazır kapsayıcılarına geçiyor ve aynı soruyu bu kez bir okuma işlemine soruyor: stok["armut"] yazmak, bir değeri okumak gibi görünüyor. Ama "armut" anahtarı haritada yoksa bu satır kaynağa ne bırakıyor?

operator[] ile Okumak Aslında Yazmaktır

std::map<std::string, int>::operator[], anahtar haritada zaten varsa ona bağlı değeri döndürür. Anahtar yoksa farklı bir şey yapar: değeri sıfırla kurulmuş yeni bir eleman ekler ve o elemana bir referans döndürür. Bu, imzasından çıkarılamayan bir yan etkidir — [] köşeli parantezi çoğu programcıya “sorgula” der, ama map için anlamı “varsa getir, yoksa kur”dur.

// kapsayici-okuma.cpp — operator[] ile okuma haritaya eleman ekler mi
#include <cstdio>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stok{{"elma", 4}};
    std::size_t once = stok.size();

    int adet = stok["armut"];
    (void)adet;

    std::printf("eleman sayisi degisti mi %s\n",
        (stok.size() != once) ? "evet" : "hayir");
    std::printf("armut degeri %d\n", stok["armut"]);
    return 0;
}
eleman sayisi degisti mi evet
armut degeri 0

KT1. stok["armut"] çağrısı, "armut" haritada yokken haritayı değiştirdi: eleman sayısı bir arttı ve yeni elemanın değeri int’in varsayılan değeri olan 0 oldu. adet değişkenine atanan değer bir okuma gibi görünse de, o okumanın kendisi haritaya yazdı. Bu ifadenin sınıfı değişti — kaynak hâlâ bir değer taşıyor ve standart bu değeri söylüyor (boş harita değil, bir elemanlı harita), ama değer işlemden öncekinden farklı.

find ve contains: Aynı Soruyu Eklemeden Sormak

Bir anahtarın haritada olup olmadığını öğrenmenin operator[]’i çağırmayan iki yolu vardır. find, anahtarı bulursa o elemana bir yineleyici, bulamazsa end() döndürür — hiçbir durumda eleman eklemez. contains, C++20 ile gelen daha doğrudan yol; yalnız bir bool döndürür.

// kapsayici-sorgu.cpp — find ve contains haritayi degistirir mi
#include <cstdio>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stok{{"elma", 4}};
    auto once = stok;

    auto it = stok.find("armut");
    bool var = stok.contains("kiraz");

    std::printf("find sonucu %s\n", (it == stok.end()) ? "bulunamadi" : "bulundu");
    std::printf("contains sonucu %s\n", var ? "evet" : "hayir");
    std::printf("harita degisti mi %s\n", (stok != once) ? "evet" : "hayir");
    return 0;
}
find sonucu bulunamadi
contains sonucu hayir
harita degisti mi hayir

KT2. find("armut") ve contains("kiraz") ikisi de haritada olmayan anahtarları sorguladı ve ikisi de haritayı aynen bıraktı: stok, işlem öncesindeki kopyasıyla hâlâ eşit. find’ın döndürdüğü end() yineleyicisi “burada yok” demenin bir yolu, eklemenin bir tetikleyicisi değil. Bir haritada bir anahtarın varlığını yalnız denetlemek isteyen kod operator[] yerine find ya da contains kullanmalı — aksi hâlde sorgu, sorguladığı yapıyı değiştirir.

Kapsayıcı Aileleri ve Seçim Ölçütü

Standart kütüphane kapsayıcıları üç aileye ayrılır. Sıralı kapsayıcılar (vector, deque, list, array) elemanın konumunu eklenme sırasından alır — burada “sıralı” büyüklüğe göre dizilmiş anlamına gelmez, elemanın yeri programcının onu koyduğu yerdir. vector bitişik bellekte durur ve sona ekleme ortalama sabit zamanlıdır; sona erişim indeksle sabit zamanlıdır. list çift yönlü bağlı bir listedir; ortadan ekleme sabit zamanlıdır ama indeksle erişim yoktur, baştan gezilir.

İlişkisel kapsayıcılar (map, set) elemanları bir anahtara göre sıralı tutar; genellikle dengeli bir ikili arama ağacıyla gerçeklenir ve arama, ekleme, silme O(log n) karmaşıklıktadır. Sırasız kapsayıcılar (unordered_map, unordered_set) elemanları bir özet tablosunda tutar; ortalama durumda arama ve ekleme O(1)’dir, ama en kötü durumda bütün elemanlar aynı kovaya düşerse O(n)’e düşer. Seçim, hangi işlemin sık yapılacağına bağlıdır: sık sıralı gezinme gerekiyorsa map, yalnız hızlı arama gerekiyor ve sıra önemsizse unordered_map tercih edilir.

// aile-secimi.cpp — sirali, iliskisel ve sirasiz kapsayicidan okuma
#include <cstdio>
#include <map>
#include <unordered_map>
#include <vector>

int main() {
    std::vector<int> sirali{30, 10, 20};
    std::map<int, int> iliskisel{{1, 10}, {2, 20}};
    std::unordered_map<int, int> sirasiz{{1, 10}, {2, 20}};

    std::printf("vector ilk eleman %d\n", sirali.front());
    std::printf("map en kucuk anahtar %d\n", iliskisel.begin()->first);
    std::printf("unordered_map arama %d\n", sirasiz.at(1));
    return 0;
}
vector ilk eleman 30
map en kucuk anahtar 1
unordered_map arama 10

KT3. Bu üç okuma da bir kapsayıcıyı aynen bıraktı: front(), yineleyici üzerinden first okuması ve at() hiçbiri kaynağa yazmadı. unordered_map’in elemanlarını hangi sırada tuttuğu burada hiç sorgulanmadı ve sorgulanmayacak — sırasız kapsayıcının dolaşma sırası gerçeklemeye bağlıdır ve bu kursta hiç basılmaz. map’te begin()->first her koşumda 1 verir çünkü map anahtara göre sıralıdır; aynı çağrı bir unordered_map üzerinde yapılsaydı sonuç güvenilir olmazdı.

at(): Sınır Denetimi Olan Erişim

operator[]’in bir sorunu daha var: const bir haritada kullanılamaz, çünkü anahtar yoksa eleman ekleyebilme ihtimali onu değiştirici bir işlem yapar. at() bu sorunu ortadan kaldırır — anahtar yoksa eleman eklemez, bunun yerine std::out_of_range fırlatır ve const bir haritada da kullanılabilir.

// at-erisimi.cpp — at() sinir disi anahtarda ne yapar
#include <cstdio>
#include <map>
#include <stdexcept>
#include <string>

int main() {
    const std::map<std::string, int> stok{{"elma", 4}};

    try {
        int deger = stok.at("armut");
        (void)deger;
        std::printf("yakalandi mi hayir\n");
    } catch (const std::out_of_range&) {
        std::printf("yakalandi mi evet\n");
    }

    std::printf("eleman sayisi %zu\n", stok.size());
    return 0;
}
yakalandi mi evet
eleman sayisi 1

KT4. stok.at("armut") bir istisna fırlattı ve bu ders yalnız yakalandığını bastı — istisnanın what() metnini basmadı, çünkü o metnin kesin biçimi kütüphane uygulamasına bağlıdır ve dersin ölçtüğü şey değil. Fırlatmadan sonra stok’un eleman sayısı hâlâ 1: harita aynen kaldı. Bu, at()’i const bir kapsayıcı üzerinde güvenle kullanılabilir kılan şey — sınır dışı bir anahtar isteği kaynağı hiç değiştirmiyor, yalnız bunu bir istisnayla bildiriyor.

insert ve emplace: Var Olan Anahtara Dokunmama Kuralı

operator[] yalnız anahtar yoksa eleman ekleyip döndürmüyor; anahtar zaten varsa değeri değiştiriyor. insert ve emplace bunun tam tersi bir garanti taşır: anahtar zaten varsa hiçbir şeyi değiştirmezler ve mevcut elemana dokunmadıklarını bir bool ile bildirirler.

// insert-emplace.cpp — anahtar zaten varken insert kaynagi degistirir mi
#include <cstdio>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stok{{"elma", 4}};

    auto sonuc = stok.insert({"elma", 99});
    bool eklendi = sonuc.second;

    std::printf("eklendi mi %s\n", eklendi ? "evet" : "hayir");
    std::printf("elma degeri %d\n", stok.at("elma"));
    return 0;
}
eklendi mi hayir
elma degeri 4

KT5. insert({"elma", 99}) çağrısı "elma" anahtarı haritada zaten varken çalıştı ve sonuc.second false döndü — hiçbir ekleme olmadı. elma değeri hâlâ 4: harita bu çağrıdan sonra aynen kaldı, 99 değeri hiçbir zaman yazılmadı. Bu, operator[] ile taban çizgide bir fark yaratır: stok["elma"] = 99 yazılsaydı değer koşulsuz üzerine yazılır ve sınıf değişti olurdu. insert’in bu geri çevirme davranışı, bir haritayı yalnız yoksa doldurmak isteyen kodun operator[] yerine insert ya da emplace seçmesinin gerekçesidir — emplace aynı garantiyi taşır, farkı elemanı bir geçici nesne kurmadan doğrudan kapsayıcının belleğinde inşa etmesidir.

erase’in Karmaşıklığı Kapsayıcıya Göre Değişir

Bir elemanı silmek her kapsayıcıda aynı işi yapmaz. std::vector::erase silinen konumdan sonraki her elemanı bir konum sola kaydırır — bu, silinen konum başa ne kadar yakınsa o kadar çok eleman taşınacağı anlamına gelir ve karmaşıklığı O(n)dir. std::list::erase bitişik bellek tutmadığı için kaydırma yapmaz; yalnız silinen düğümün komşularının bağlantısını günceller ve karmaşıklığı O(1)dir. std::map::erase ise bir anahtara göre arama yapıp ardından ağaçtaki bir düğümü kaldırır, toplamda O(log n).

// erase-karmasikligi.cpp — vector erase kalan elemanlari kaydirir mi
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> sirali{10, 20, 30, 40};

    sirali.erase(sirali.begin());

    std::printf("eleman sayisi %zu\n", sirali.size());
    std::printf("ilk eleman %d\n", sirali.front());
    return 0;
}
eleman sayisi 3
ilk eleman 20

KT6. sirali.erase(sirali.begin()) ilk elemanı sildi ve kalan üç eleman bir konum sola kaydı — front() artık eskiden ikinci sırada duran 20’yi veriyor. Kaynak bu işlemden sonra değişti: yalnız eleman sayısı bir azalmadı, kalan elemanların bellekteki konumu da değişti. Bir std::list üzerinde aynı çağrı yapılsaydı sonuç sınıf olarak yine değişti olurdu — eleman sayısı yine bir azalırdı — ama kaydırma hiç gerçekleşmezdi; kalmayan elemanların adresleri sabit kalırdı. Bu dersin sınıflandırması (aynen/değişti/boş/belirsiz) bir işlemin ne kadar iş yaptığını değil, kaynağı ne durumda bıraktığını ölçtüğü için, vector ve list üzerindeki bu iki erase çağrısı aynı sınıfa düşer; karmaşıklık farkı ayrı bir eksendir ve seçim yaparken karmaşıklık ile sınıf birlikte değerlendirilir, biri diğerinin yerine geçmez.

Sınır Dışı İndeksleme: Tanımsız Davranış ve Tanımlı Karşılığı

std::vector::operator[], verdiği indeksin kapsayıcının sınırları içinde olduğunu hiç denetlemez. sirali[10] çağrısı sirali’nin boyutu 3 olduğunda tanımsız davranıştır: çalışma zamanı bir çökme üretebilir, çöp bir değer döndürebilir ya da hiçbir hata vermeden “çalışabilir” — üçü de eşit derecede olasıdır ve bu kurs hiçbirini göstermez, çünkü tanımsız davranışın çıktısı standarda göre tanımlı değildir ve doğrulanamaz. std::vector::at() aynı isteğe tanımlı bir karşılık verir: indeks sınır dışıysa std::out_of_range fırlatır, sınır içindeyse operator[] ile aynı elemana erişir. Fark performanstadır — at() her çağrıda bir sınır denetimi yapar, operator[] yapmaz — ama bu fark yalnız sınır içindeki erişimlerde görünür bir maliyettir; sınır dışı bir erişimde operator[]’in “maliyetsiz” olması, kaynağın ne durumda kaldığının artık tanımsız olması pahasınadır. Bir indeksin geçerliliğinden emin olunamayan her yerde at() tercih edilir; indeksin geçerliliği çağıran tarafından zaten garanti edildiği sıkı döngülerde operator[]’in denetimsiz hızı kabul edilir.

Sınırlayıcı Ölçüm

const std::map<std::string, int>& stok üzerinde stok["armut"] yazmak derlenmez: operator[]’in const olmayan bir aşırı yüklemesi vardır ve const bir nesne üzerinde çağrılamaz, çünkü işlecin gövdesi anahtar yoksa eleman ekleyebilir — bu, const sözleşmesiyle çelişir. Aşağıdaki blok bu satırı yorum olarak taşır ve onun yerine derlenen sürümü, const bir haritanın yalnız find, at ve contains ile okunabildiğini gösterir:

// const-harita.cpp — const haritada operator[] neden kullanilamaz
#include <cstdio>
#include <map>
#include <string>

int main() {
    const std::map<std::string, int> stok{{"elma", 4}};

    // Asagidaki satir derlenmez, cunku operator[] const olmayan bir islev:
    // int adet = stok["armut"];

    auto it = stok.find("elma");
    std::printf("elma bulundu mu %s\n", (it != stok.end()) ? "evet" : "hayir");
    std::printf("eleman sayisi %zu\n", stok.size());
    return 0;
}
elma bulundu mu evet
eleman sayisi 1

Derleyicinin bu satırı reddetmesi bir kısıtlama değil, bir güvencedir: const bir haritanın yalnız sorgulanabileceğini, hiçbir okumanın onu değiştiremeyeceğini derleme anında zorunlu kılar. Bu dersin ölçtüğü işlem — değiştirilebilir bir haritada operator[] ile okuma — bu güvenceyi taşımaz; kaynağa yazan yol açıktır ve derleyici onu kapatmaz.

Özet

  • std::map’te operator[] ile okuma, anahtar yoksa haritaya sıfırla kurulmuş bir eleman ekler; bu dersin ölçtüğü işlemin sınıfı değişti.
  • find ve contains, anahtarı ararken haritaya hiç yazmaz; kaynak aynen kalır.
  • Sıralı (vector, deque, list, array), ilişkisel (map, set) ve sırasız (unordered_map, unordered_set) kapsayıcılar farklı karmaşıklık garantileri taşır (erase örneğin vector’de O(n), list’te O(1), map’te O(log n)); ama bu fark sınıflandırmayı etkilemez, hepsi kaynağı değişti durumuna taşır.
  • insert ve emplace, anahtar zaten varsa hiçbir şey yazmaz — kaynak aynen kalır; operator[] ile yazmak ise koşulsuz üzerine yazar ve sınıf değişti olur.
  • at(), anahtar yoksa istisna fırlatır ve haritayı hiç değiştirmez, bu yüzden const bir kapsayıcı üzerinde de kullanılabilir; buna karşın const bir haritada operator[] çağrısı derlenmez — derleyici, kaynağı değiştirebilecek bir yolu const sözleşmesiyle baştan kapatır.
  • vector::operator[] sınır dışı bir indekste tanımsız davranışa girer; vector::at() aynı isteğe tanımlı bir istisna fırlatarak karşılık verir — ikisi de kaynağı okumaktan öteye gitmez, fark yalnız sınır dışı isteğe verilen yanıtın tanımlı olup olmadığındadır.

Sonraki Adım

Bu ders bir kapsayıcının bütününe ne olduğunu ölçtü. Sonraki ders sınırı bir kapsayıcının içindeki bir konuma indiriyor: bir yineleyici, işaret ettiği elemanı silme işleminden sonra hâlâ okunabilir mi, yoksa o konuma dokunmak tanımsız davranışa mı giriyor?

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat