← Kursa Dön
📄 Text · 15 min

std::array ve std::vector

Bir önceki derste C-style dizileri gördük ve bir sürü sınırlamayla karşılaştık: boyut bilgisi kayboluyor, kopyalama yok, sınır kontrolü yok... Modern C++ bu sorunların hepsine çözüm sunuyor: std::array ve std::vector.

Bu ikisi C++'ın en çok kullanılan konteynerlerinden. std::array sabit boyutlu dizi istediğinde, std::vector ise boyutun çalışma zamanında değişmesi gerektiğinde kullanılır. İkisini de derinlemesine öğreneceğiz.


std::array — Sabit Boyutlu Modern Alternatif

std::array, C-style dizinin "akıllı versiyonu" gibi düşünebilirsin. Aynı performansı sunuyor ama ek özellikler getiriyor: boyut bilgisini biliyor, kopyalanabiliyor, karşılaştırılabiliyor.

Bunu bir kartvizitlik gibi düşün. Kartvizitliğin boyutu sabittir — 10 kart alır mesela. Ama en azından kaç kart kapasitesi olduğunu biliyor, kartları başka bir kartvizitliğe kopyalayabiliyor.

Tanımlama ve Kullanım

#include <iostream>
#include <array>
using namespace std;

int main() {
    // Temel tanımlama: array<tip, boyut>
    array<int, 5> notlar = {90, 85, 78, 92, 88};

    // Erişim — tıpkı C-style gibi
    cout << "İlk: " << notlar[0] << endl;
    cout << "Son: " << notlar[4] << endl;

    // .at() ile güvenli erişim — sınır kontrolü yapar
    cout << "İkinci: " << notlar.at(1) << endl;
    // notlar.at(10);  // HATA: std::out_of_range exception fırlatır

    // Boyut bilgisi her zaman mevcut
    cout << "Boyut: " << notlar.size() << endl;  // 5

    return 0;
}

Güvenli Erişim: [] vs .at()

#include <iostream>
#include <array>
using namespace std;

int main() {
    array<int, 3> dizi = {10, 20, 30};

    // [] — hızlı ama sınır kontrolü yok
    // dizi[5] — tanımsız davranış, hata vermez

    // .at() — sınır kontrolü yapar, hata fırlatır
    try {
        cout << dizi.at(5) << endl;
    } catch (const out_of_range& e) {
        cout << "Hata: " << e.what() << endl;
    }

    return 0;
}

Faydalı Üye Fonksiyonlar

#include <iostream>
#include <array>
using namespace std;

int main() {
    array<int, 5> dizi = {10, 20, 30, 40, 50};

    cout << "İlk eleman: " << dizi.front() << endl;   // 10
    cout << "Son eleman: " << dizi.back() << endl;     // 50
    cout << "Boyut: " << dizi.size() << endl;           // 5
    cout << "Boş mu: " << dizi.empty() << endl;         // 0 (false)

    // Kopyalama — C-style dizide bu mümkün değildi!
    array<int, 5> kopya = dizi;
    cout << "Kopya[0]: " << kopya[0] << endl;  // 10

    // Karşılaştırma — eleman eleman karşılaştırır
    if (dizi == kopya) {
        cout << "İki dizi eşit!" << endl;
    }

    // Tüm elemanları bir değerle doldur
    dizi.fill(0);
    for (int x : dizi) cout << x << " ";  // 0 0 0 0 0
    cout << endl;

    return 0;
}

💡 İpucu: std::array stack üzerinde yaşar ve C-style dizi ile aynı performansı sunar. Overhead yok. Sabit boyutlu dizi gerektiğinde tercih et.


std::vector — Dinamik Boyutlu Dizi

std::vector, C++'ın en çok kullanılan konteyneridir. Boyutu çalışma zamanında büyüyüp küçülebilir. Eleman ekle, çıkar, ortasına sok — hepsini yapabilirsin.

Bunu bir akordeon dosya gibi düşün. Kaç dosya gelirse gelsin, kendini genişletip daraltır. Dışarıdan sabit bir kutu gibi görünür ama içeride dinamik olarak büyür.

Temel Tanımlama ve Kullanım

#include <iostream>
#include <vector>
using namespace std;

int main() {
    // Boş vector
    vector<int> v1;

    // Başlangıç değerleriyle
    vector<int> v2 = {10, 20, 30, 40, 50};

    // 5 elemanlı, hepsi 0
    vector<int> v3(5);

    // 5 elemanlı, hepsi 42
    vector<int> v4(5, 42);

    // Erişim
    cout << "v2[0] = " << v2[0] << endl;       // 10
    cout << "v2.at(2) = " << v2.at(2) << endl;  // 30

    // Range-based for
    for (int x : v2) {
        cout << x << " ";
    }
    cout << endl;

    return 0;
}

push_back, pop_back, size, capacity, reserve

Eleman Ekleme ve Çıkarma

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> sayilar;

    // Eleman ekleme — sona ekler
    sayilar.push_back(10);
    sayilar.push_back(20);
    sayilar.push_back(30);

    cout << "Boyut: " << sayilar.size() << endl;  // 3

    for (int x : sayilar) cout << x << " ";
    cout << endl;  // 10 20 30

    // Son elemanı çıkar
    sayilar.pop_back();
    cout << "pop_back sonrası boyut: " << sayilar.size() << endl;  // 2

    for (int x : sayilar) cout << x << " ";
    cout << endl;  // 10 20

    return 0;
}

Size vs Capacity

Vector'ün iki önemli kavramı var:

  • size: Şu an kaç eleman var

  • capacity: Yeniden bellek ayırmadan kaç eleman alabilir

Vector büyüdükçe mevcut kapasitesi yetmezse, daha büyük bir bellek bloğu ayırır ve tüm elemanları oraya kopyalar. Bu maliyetli bir işlem.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 20; i++) {
        v.push_back(i);
        cout << "size=" << v.size()
             << " capacity=" << v.capacity() << endl;
    }

    return 0;
}

Çıktıda capacity'nin 1, 2, 4, 8, 16, 32... şeklinde ikiye katlanarak büyüdüğünü göreceksin. Bu strateji genellikle "amortized O(1)" olarak bilinir — ortalamada her ekleme sabit sürede yapılır.

reserve ile Önceden Yer Ayırma

Kaç eleman geleceğini aşağı yukarı biliyorsan, reserve ile önceden yer ayırabilirsin. Bu gereksiz kopyalamaları önler:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    v.reserve(1000);  // 1000 elemanlık yer ayır

    cout << "size: " << v.size() << endl;        // 0 — eleman yok henüz
    cout << "capacity: " << v.capacity() << endl; // 1000

    for (int i = 0; i < 1000; i++) {
        v.push_back(i);  // hiç reallocation olmaz
    }

    cout << "size: " << v.size() << endl;         // 1000
    cout << "capacity: " << v.capacity() << endl;  // 1000

    return 0;
}

💡 İpucu: Büyük bir döngüde push_back yapacaksan, öncesinde reserve çağır. Performans farkı büyük verilerde hissedilir.


emplace_back vs push_back

push_back bir eleman kopyasını vector'ün sonuna ekler. emplace_back ise elemanı doğrudan yerinde inşa eder (construct in place). Basit türlerde fark yok ama nesnelerle çalışırken emplace_back daha verimli olabilir.

#include <iostream>
#include <vector>
#include <string>
using namespace std;

int main() {
    vector<string> isimler;

    // push_back — önce string oluştur, sonra kopyala/taşı
    isimler.push_back("Ali");

    // emplace_back — doğrudan yerinde oluştur
    isimler.emplace_back("Veli");

    // Pratik fark: emplace_back constructor argümanlarını doğrudan alır
    vector<pair<string, int>> ogrenciler;

    // push_back ile — geçici pair oluşturman gerekir
    ogrenciler.push_back({"Ali", 90});

    // emplace_back ile — argümanları doğrudan geçir
    ogrenciler.emplace_back("Veli", 85);

    for (auto& [isim, not_] : ogrenciler) {
        cout << isim << ": " << not_ << endl;
    }

    return 0;
}

⚠️ Dikkat: Basit türlerde (int, double) push_back ve emplace_back arasında pratik fark yok. Karmaşık nesnelerde emplace_back tercih edilir çünkü gereksiz kopyalamayı önler.


Diğer Önemli vector Fonksiyonları

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {10, 20, 30, 40, 50};

    // İlk ve son eleman
    cout << "İlk: " << v.front() << endl;  // 10
    cout << "Son: " << v.back() << endl;    // 50

    // Boş mu?
    cout << "Boş mu: " << v.empty() << endl;  // 0

    // Belirli pozisyona ekleme — insert
    v.insert(v.begin() + 2, 25);  // indeks 2'ye 25 ekle
    // v: 10 20 25 30 40 50

    // Belirli pozisyondan silme — erase
    v.erase(v.begin() + 4);  // indeks 4'teki elemanı sil
    // v: 10 20 25 30 50

    // Tüm elemanları sil
    v.clear();
    cout << "clear sonrası size: " << v.size() << endl;  // 0

    // shrink_to_fit — fazla kapasiteyi serbest bırak
    cout << "capacity: " << v.capacity() << endl;  // muhtemelen hâlâ büyük
    v.shrink_to_fit();
    cout << "shrink sonrası capacity: " << v.capacity() << endl;  // 0

    return 0;
}

Iterator ile Gezinme

Iterator, konteynerlerin elemanlarını gezen bir "imlec" gibidir. Pointer'a benzer ama daha soyut.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {10, 20, 30, 40, 50};

    // begin() ilk elemanı, end() son elemandan bir SONRAKİNİ gösterir
    for (auto it = v.begin(); it != v.end(); ++it) {
        cout << *it << " ";  // * ile değere eriş
    }
    cout << endl;

    // Ters gezinme
    for (auto it = v.rbegin(); it != v.rend(); ++it) {
        cout << *it << " ";  // 50 40 30 20 10
    }
    cout << endl;

    // const iterator — değer değiştirmeyi engeller
    for (auto it = v.cbegin(); it != v.cend(); ++it) {
        cout << *it << " ";
        // *it = 99;  // HATA! const iterator
    }
    cout << endl;

    return 0;
}

STL Algoritmalarıyla Kullanım

Iterator'ların asıl gücü, STL algoritmaları ile kullanıldığında ortaya çıkar:

#include <iostream>
#include <vector>
#include <algorithm>  // sort, find, reverse, count
#include <numeric>    // accumulate
using namespace std;

int main() {
    vector<int> v = {50, 10, 40, 20, 30};

    // Sıralama
    sort(v.begin(), v.end());
    for (int x : v) cout << x << " ";  // 10 20 30 40 50
    cout << endl;

    // Ters sıralama
    sort(v.begin(), v.end(), greater<int>());
    for (int x : v) cout << x << " ";  // 50 40 30 20 10
    cout << endl;

    // Arama
    auto it = find(v.begin(), v.end(), 30);
    if (it != v.end()) {
        cout << "30 bulundu, indeks: " << distance(v.begin(), it) << endl;
    }

    // Toplam
    int toplam = accumulate(v.begin(), v.end(), 0);
    cout << "Toplam: " << toplam << endl;  // 150

    // Sayma
    vector<int> sayilar = {1, 2, 3, 2, 1, 2, 3};
    cout << "2'lerin sayısı: " << count(sayilar.begin(), sayilar.end(), 2) << endl;

    // Ters çevirme
    reverse(v.begin(), v.end());
    for (int x : v) cout << x << " ";
    cout << endl;

    return 0;
}

std::array Fonksiyona Geçirme

std::array'i fonksiyona geçirirken boyut bilgisi korunur — C-style'daki decay sorunu yok:

#include <iostream>
#include <array>
using namespace std;

// Template ile her boyuttaki array'i kabul et
template <size_t N>
void yazdir(const array<int, N>& dizi) {
    for (int x : dizi) {
        cout << x << " ";
    }
    cout << endl;
    cout << "Boyut: " << dizi.size() << endl;
}

// Belirli boyut için
void yazdir5(const array<int, 5>& dizi) {
    for (int x : dizi) cout << x << " ";
    cout << endl;
}

int main() {
    array<int, 5> notlar = {90, 85, 78, 92, 88};
    array<int, 3> kucuk = {1, 2, 3};

    yazdir(notlar);  // Boyut: 5
    yazdir(kucuk);   // Boyut: 3
    yazdir5(notlar);

    return 0;
}

std::vector Fonksiyona Geçirme

Vector'ü fonksiyona geçirirken referans kullan — yoksa tüm vector kopyalanır:

#include <iostream>
#include <vector>
using namespace std;

// Kötü: vector kopyalanır — yavaş
void kotuYazdir(vector<int> v) {
    for (int x : v) cout << x << " ";
    cout << endl;
}

// İyi: const referans — kopyalama yok, değiştirme yasak
void yazdir(const vector<int>& v) {
    for (int x : v) cout << x << " ";
    cout << endl;
}

// Değiştirmek istiyorsan: referans (const olmadan)
void ikiyeCarp(vector<int>& v) {
    for (int& x : v) {
        x *= 2;
    }
}

int main() {
    vector<int> notlar = {90, 85, 78, 92, 88};

    yazdir(notlar);      // 90 85 78 92 88
    ikiyeCarp(notlar);
    yazdir(notlar);      // 180 170 156 184 176

    return 0;
}

⚠️ Dikkat: vector'ü fonksiyona değer ile (by value) geçirirsen tüm elemanlar kopyalanır. Büyük vector'lerde bu ciddi performans kaybına neden olur. Her zaman const& veya & kullan.


Ne Zaman array, Ne Zaman vector?

DurumTercihNeden
Boyut derleme zamanında belli ve sabitstd::arrayStack'te yaşar, sıfır overhead
Boyut çalışma zamanında değişebilirstd::vectorDinamik büyüme/küçülme
Performans kritik, boyut sabitstd::arrayCache dostu, allocation yok
Kullanıcıdan veri alınacakstd::vectorBoyut bilinmiyor
Fonksiyondan dizi döndürmekstd::vectorKolay, güvenli
Küçük, sabit veri seti (günler, aylar vs.)std::arrayDerleyici optimizasyonları
Genel amaçlı "bilmiyorum kaç eleman gelecek"std::vectorEn esnek seçenek

Kural: Emin değilsen std::vector kullan. Yanlış gidemezsin.


Pratik Örnek: Öğrenci Not Sistemi

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
using namespace std;

int main() {
    vector<string> isimler;
    vector<int> notlar;

    // Veri girişi
    int n;
    cout << "Kaç öğrenci? ";
    cin >> n;

    for (int i = 0; i < n; i++) {
        string isim;
        int not_;
        cout << "İsim: ";
        cin >> isim;
        cout << "Not: ";
        cin >> not_;
        isimler.push_back(isim);
        notlar.push_back(not_);
    }

    // Ortalama
    double ort = accumulate(notlar.begin(), notlar.end(), 0.0) / notlar.size();
    cout << "Ortalama: " << ort << endl;

    // En yüksek not
    auto max_it = max_element(notlar.begin(), notlar.end());
    int max_idx = distance(notlar.begin(), max_it);
    cout << "En yüksek: " << isimler[max_idx] << " (" << *max_it << ")" << endl;

    return 0;
}

Özet

  • std::array sabit boyutlu, modern C-style dizi alternatifidir. Stack'te yaşar, sıfır overhead sunar, boyut bilgisini korur.

  • std::vector dinamik boyutlu dizidir; push_back ile eleman eklenir, pop_back ile çıkarılır. C++'ın en çok kullanılan konteyneridir.

  • Vector'de size mevcut eleman sayısı, capacity ise yeniden bellek ayırmadan alınabilecek eleman kapasitesidir. reserve ile önceden yer ayrılabilir.

  • emplace_back elemanı yerinde inşa eder; karmaşık nesnelerde push_back'e göre daha verimlidir.

  • Iterator'lar konteynerleri gezen imleclerdir; sort, find, accumulate gibi STL algoritmaları ile birlikte güçlü bir araç setidir.

  • Emin değilsen std::vector kullan — esnek, güvenli ve her duruma uygun.