← Kursa Dön
📄 Text · 15 min

String İşlemleri Derinlemesine

Daha önceki derslerde std::string'in temellerini görmüştük — tanımlama, birleştirme, karşılaştırma. Bu derste string'in gelişmiş yeteneklerini keşfedeceğiz: arama, parçalama, dönüştürme ve hatta düzenli ifadeler (regex).

String işlemlerini bir İsviçre çakısı gibi düşün. Basit işler için bıçağı açarsın, ama ihtiyaç olunca makas, tornavida, tirbuşon da var. Bu derste çakının tüm aletlerini öğreneceğiz.


find — String İçinde Arama

find fonksiyonu, bir alt string'in (substring) ilk geçtiği pozisyonu döndürür. Bulamazsa string::npos döner.

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

int main() {
    string metin = "Merhaba dünya, merhaba C++";

    // İlk geçtiği yer
    size_t pos = metin.find("merhaba");
    if (pos != string::npos) {
        cout << "'merhaba' bulundu, pozisyon: " << pos << endl;  // 15
    }

    // Büyük/küçük harf duyarlı!
    pos = metin.find("Merhaba");
    cout << "'Merhaba' pozisyon: " << pos << endl;  // 0

    // Tek karakter arama
    pos = metin.find('+');
    cout << "'+' pozisyon: " << pos << endl;  // 24

    // Belirli pozisyondan itibaren arama
    pos = metin.find("a", 5);  // 5. indeksten itibaren 'a' ara
    cout << "'a' (5'ten sonra) pozisyon: " << pos << endl;

    // Bulunamama durumu
    pos = metin.find("Python");
    if (pos == string::npos) {
        cout << "'Python' bulunamadı" << endl;
    }

    return 0;
}

rfind — Sondan Arama

rfind, string'in sonundan başa doğru arar ve son eşleşmenin pozisyonunu döndürür:

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

int main() {
    string yol = "/home/kullanici/belgeler/rapor.txt";

    // Son '/' karakterini bul
    size_t pos = yol.rfind('/');
    cout << "Son '/' pozisyon: " << pos << endl;

    // Dosya adını çıkar
    string dosya_adi = yol.substr(pos + 1);
    cout << "Dosya: " << dosya_adi << endl;  // rapor.txt

    // Uzantıyı bul
    pos = yol.rfind('.');
    string uzanti = yol.substr(pos);
    cout << "Uzantı: " << uzanti << endl;  // .txt

    return 0;
}

find_first_of ve find_last_of

Bu fonksiyonlar bir karakter kümesinden herhangi birinin ilk veya son geçtiği pozisyonu bulur:

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

int main() {
    string metin = "Hello, World! 123";

    // İlk rakamı bul
    size_t pos = metin.find_first_of("0123456789");
    cout << "İlk rakam pozisyon: " << pos << endl;  // 14

    // İlk sesli harfi bul
    pos = metin.find_first_of("aeiouAEIOU");
    cout << "İlk sesli harf pozisyon: " << pos << endl;  // 1 (e)

    // Son noktalama işaretini bul
    pos = metin.find_last_of(",!.");
    cout << "Son noktalama pozisyon: " << pos << endl;  // 12 (!)

    // Karakter kümesinde OLMAYAN ilk karakter
    pos = metin.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
    cout << "Harf olmayan ilk karakter pozisyon: " << pos << endl;  // 5 (,)

    return 0;
}

substr — String Parçalama

substr(pos, len) belirtilen pozisyondan itibaren len karakter uzunluğunda bir alt string döndürür:

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

int main() {
    string tarih = "2024-03-15";

    // Yıl, ay, gün çıkarma
    string yil = tarih.substr(0, 4);    // 0'dan 4 karakter
    string ay  = tarih.substr(5, 2);    // 5'ten 2 karakter
    string gun = tarih.substr(8, 2);    // 8'den 2 karakter

    cout << "Yıl: " << yil << endl;    // 2024
    cout << "Ay: " << ay << endl;       // 03
    cout << "Gün: " << gun << endl;     // 15

    // Uzunluk belirtmezsen sonuna kadar alır
    string kalan = tarih.substr(5);     // 5'ten sonuna kadar
    cout << "Kalan: " << kalan << endl;  // 03-15

    return 0;
}

Pratik: String'i Ayraçla Parçalama (Split)

C++'ta hazır bir split fonksiyonu yok ama find ve substr ile yazabiliriz:

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

vector<string> parcala(const string& metin, char ayrac) {
    vector<string> parcalar;
    size_t baslangic = 0;
    size_t pos;

    while ((pos = metin.find(ayrac, baslangic)) != string::npos) {
        parcalar.push_back(metin.substr(baslangic, pos - baslangic));
        baslangic = pos + 1;
    }
    parcalar.push_back(metin.substr(baslangic));  // son parça

    return parcalar;
}

int main() {
    string csv = "Ali,85,Matematik,Geçti";

    vector<string> alanlar = parcala(csv, ',');
    for (const auto& alan : alanlar) {
        cout << "[" << alan << "]" << endl;
    }

    return 0;
}

Çıktı:

[Ali]
[85]
[Matematik]
[Geçti]

replace — Değiştirme

replace(pos, len, yeni_string) belirtilen aralığı yeni string ile değiştirir:

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

int main() {
    string metin = "Merhaba dünya";

    // 8. pozisyondan 5 karakter ("dünya") yerine "C++" koy
    metin.replace(8, 5, "C++");
    cout << metin << endl;  // Merhaba C++

    return 0;
}

Tüm Geçişleri Değiştirme

replace tek seferde bir yeri değiştirir. Tüm geçişleri değiştirmek için döngü kullanmalısın:

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

string hepsini_degistir(string metin, const string& eski, const string& yeni) {
    size_t pos = 0;
    while ((pos = metin.find(eski, pos)) != string::npos) {
        metin.replace(pos, eski.length(), yeni);
        pos += yeni.length();  // sonsuz döngüden kaçın
    }
    return metin;
}

int main() {
    string metin = "elma ve elma ve elma";

    string sonuc = hepsini_degistir(metin, "elma", "armut");
    cout << sonuc << endl;  // armut ve armut ve armut

    return 0;
}

💡 İpucu: pos += yeni.length() satırı kritik. Bu olmadan, eğer yeni string eski string'i içeriyorsa ("a" → "aa" gibi) sonsuz döngüye girersin.


Tür Dönüşümleri: stoi, stod, to_string

String'den Sayıya

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

int main() {
    // String -> int
    string s1 = "42";
    int sayi = stoi(s1);
    cout << sayi + 8 << endl;  // 50

    // String -> long
    string s2 = "1234567890123";
    long buyuk = stol(s2);

    // String -> double
    string s3 = "3.14159";
    double pi = stod(s3);
    cout << pi * 2 << endl;  // 6.28318

    // String -> float
    string s4 = "2.71";
    float e = stof(s4);

    // Hata durumu
    try {
        int x = stoi("merhaba");  // dönüştürülemez!
    } catch (const invalid_argument& e) {
        cout << "Hata: " << e.what() << endl;
    }

    try {
        int y = stoi("99999999999999999");  // çok büyük!
    } catch (const out_of_range& e) {
        cout << "Taşma: " << e.what() << endl;
    }

    return 0;
}

Sayıdan String'e

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

int main() {
    int sayi = 42;
    double pi = 3.14159;

    string s1 = to_string(sayi);    // "42"
    string s2 = to_string(pi);      // "3.141590" (6 ondalık)

    cout << "Sonuç: " + s1 + " ve " + s2 << endl;

    // Birleştirme kolaylığı
    int yas = 25;
    string mesaj = "Yaşım " + to_string(yas) + " ve öğreniyorum!";
    cout << mesaj << endl;

    return 0;
}

⚠️ Dikkat: stoi ve stod dönüşüm yapılamayınca exception fırlatır. Kullanıcı girdisiyle çalışıyorsan try-catch ile sarmala.


string_view — Kopyalamadan Bakma (C++17)

std::string_view bir string'e sahip olmayan (non-owning), sadece "bakan" bir türdür. String'i kopyalamadan okuma yapabilirsin. Bu özellikle fonksiyon parametrelerinde büyük avantaj sağlar.

Bunu bir vitrin camı gibi düşün. Mağazadaki ürünlere bakabilirsin ama dokunup değiştiremezsin. Ve vitrin camını taşımak, mağazanın tamamını taşımaktan çok daha hafif.

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

// Eski yol: const string& — string olmayan şeylerde geçici string oluşturur
void eski_yazdir(const string& s) {
    cout << s << endl;
}

// Yeni yol: string_view — kopyalama yok, her türlü string'i kabul eder
void yeni_yazdir(string_view sv) {
    cout << sv << endl;
}

int main() {
    string str = "Merhaba dünya";
    const char* cstr = "C-style string";

    // string_view her ikisini de kabul eder, kopyalama yapmadan
    yeni_yazdir(str);
    yeni_yazdir(cstr);
    yeni_yazdir("literal string");

    // string_view üzerinde arama yapılabilir
    string_view sv = "Merhaba dünya";
    cout << "Boyut: " << sv.size() << endl;
    cout << "İlk 7: " << sv.substr(0, 7) << endl;

    // Ama değiştirilemez!
    // sv[0] = 'X';  // HATA!

    return 0;
}

Ne Zaman Kullanılmalı?

// Fonksiyon sadece okuyacaksa → string_view
void analiz_et(string_view metin) {
    cout << "Uzunluk: " << metin.size() << endl;
    auto pos = metin.find("C++");
    if (pos != string_view::npos) {
        cout << "C++ bulundu!" << endl;
    }
}

// Fonksiyon string'i saklayacaksa → const string& veya string
void kaydet(string metin) {
    // metin'in sahipliğini alıyoruz
    // veritabanına yaz, vektöre ekle vs.
}

⚠️ Dikkat: string_view'ın baktığı string yok olursa, string_view dangling (havada kalan) referans olur. Asla yerel string'den oluşturulmuş string_view'ı fonksiyondan döndürme!

// TEHLİKE!
string_view tehlike() {
    string yerel = "geçici";
    return yerel;  // yerel yok olur, string_view havada kalır!
}

Basit Regex Kullanımı

Regex (regular expression — düzenli ifade), metin içinde kalıp eşleme yapmak için kullanılan güçlü bir araçtır. C++11'den itibaren <regex> kütüphanesi ile kullanılabilir.

regex_match — Tam Eşleşme

Tüm string'in kalıpla eşleşip eşleşmediğini kontrol eder:

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

int main() {
    // E-posta doğrulama (basit)
    regex email_kalip(R"(\w+@\w+\.\w+)");

    string email1 = "ali@example.com";
    string email2 = "hatali-email";

    cout << email1 << ": "
         << (regex_match(email1, email_kalip) ? "Geçerli" : "Geçersiz")
         << endl;  // Geçerli

    cout << email2 << ": "
         << (regex_match(email2, email_kalip) ? "Geçerli" : "Geçersiz")
         << endl;  // Geçersiz

    // Telefon numarası doğrulama
    regex tel_kalip(R"(\d{3}-\d{3}-\d{4})");

    cout << "555-123-4567: "
         << (regex_match(string("555-123-4567"), tel_kalip) ? "Geçerli" : "Geçersiz")
         << endl;  // Geçerli

    return 0;
}

regex_search — Kısmi Eşleşme

String içinde kalıbı arar (tam eşleşme şartı yok):

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

int main() {
    string metin = "Fiyat: 42.99 TL ve KDV: 8.50 TL";
    regex sayi_kalip(R"(\d+\.\d+)");
    smatch eslesme;

    // İlk eşleşmeyi bul
    if (regex_search(metin, eslesme, sayi_kalip)) {
        cout << "Bulunan: " << eslesme[0] << endl;  // 42.99
    }

    // Tüm eşleşmeleri bul
    string kalan = metin;
    while (regex_search(kalan, eslesme, sayi_kalip)) {
        cout << "Eşleşme: " << eslesme[0] << endl;
        kalan = eslesme.suffix().str();
    }

    return 0;
}

regex_replace — Kalıpla Değiştirme

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

int main() {
    string metin = "Tarih: 2024-03-15 ve 2024-12-25";
    regex tarih_kalip(R"((\d{4})-(\d{2})-(\d{2}))");

    // YYYY-MM-DD → DD/MM/YYYY formatına çevir
    string sonuc = regex_replace(metin, tarih_kalip, "$3/$2/$1");
    cout << sonuc << endl;
    // Tarih: 15/03/2024 ve 25/12/2024

    // Hassas bilgileri gizle
    string log = "Kullanıcı IP: 192.168.1.100 bağlandı";
    regex ip_kalip(R"(\d+\.\d+\.\d+\.\d+)");
    cout << regex_replace(log, ip_kalip, "***.***.***. ***") << endl;

    return 0;
}

Temel Regex Söz Dizimi

KalıpAnlamÖrnek
\dRakam (0-9)\d{3} → 3 rakam
\wHarf, rakam veya _\w+ → bir kelime
\sBoşluk karakteri\s+ → bir veya daha fazla boşluk
.Herhangi bir karaktera.b → "a_b", "a1b" vs.
+Bir veya daha fazla\d+ → en az bir rakam
*Sıfır veya daha fazla\d* → rakam olabilir de olmayabilir de
?Sıfır veya bircolou?r → "color" veya "colour"
{n}Tam n kez\d{4} → tam 4 rakam
[abc]a, b veya c[aeiou] → herhangi bir sesli harf
^String başı^Merhaba → "Merhaba" ile başlayan
$String sonu\.txt$ → ".txt" ile biten

💡 İpucu: C++'ta regex yazarken raw string literal (R"(...)") kullan. Böylece ters slash (\) için çift yazmana gerek kalmaz. R"(\d+)" yazmak, "\\d+" yazmaktan çok daha okunaklı.

⚠️ Dikkat: C++'ın regex kütüphanesi bazı implementasyonlarda yavaş olabilir. Performans kritikse (milyonlarca eşleşme) harici kütüphaneler (RE2 gibi) düşünülebilir. Basit doğrulamalar için gayet yeterli.


Pratik Örnek: Basit Metin İşlemci

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

// Baş ve sondaki boşlukları temizle (trim)
string trim(const string& s) {
    size_t baslangic = s.find_first_not_of(" \t\n\r");
    if (baslangic == string::npos) return "";
    size_t bitis = s.find_last_not_of(" \t\n\r");
    return s.substr(baslangic, bitis - baslangic + 1);
}

// Küçük harfe çevir
string kucuk_harf(string s) {
    transform(s.begin(), s.end(), s.begin(), ::tolower);
    return s;
}

// Büyük harfe çevir
string buyuk_harf(string s) {
    transform(s.begin(), s.end(), s.begin(), ::toupper);
    return s;
}

// Kelime say
int kelime_say(const string& s) {
    int sayac = 0;
    bool kelimede = false;
    for (char c : s) {
        if (c != ' ' && !kelimede) {
            sayac++;
            kelimede = true;
        } else if (c == ' ') {
            kelimede = false;
        }
    }
    return sayac;
}

int main() {
    string metin = "   Merhaba Dünya ve C++   ";

    cout << "Orijinal: [" << metin << "]" << endl;
    cout << "Trim: [" << trim(metin) << "]" << endl;
    cout << "Küçük: " << kucuk_harf(trim(metin)) << endl;
    cout << "Büyük: " << buyuk_harf(trim(metin)) << endl;
    cout << "Kelime sayısı: " << kelime_say(trim(metin)) << endl;

    return 0;
}

Pratik Örnek: URL Parser

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

int main() {
    string url = "https://www.example.com:8080/api/users?id=42";

    // Protokolü çıkar
    size_t pos = url.find("://");
    string protokol = url.substr(0, pos);
    cout << "Protokol: " << protokol << endl;  // https

    // Host kısmını çıkar
    size_t host_baslangic = pos + 3;
    size_t host_bitis = url.find_first_of(":/?", host_baslangic);
    string host = url.substr(host_baslangic, host_bitis - host_baslangic);
    cout << "Host: " << host << endl;  // www.example.com

    // Port çıkar (varsa)
    if (url[host_bitis] == ':') {
        size_t port_bitis = url.find_first_of("/?", host_bitis + 1);
        string port = url.substr(host_bitis + 1, port_bitis - host_bitis - 1);
        cout << "Port: " << port << endl;  // 8080
    }

    // Path çıkar
    size_t path_baslangic = url.find('/', host_baslangic);
    size_t query_baslangic = url.find('?');
    string path = url.substr(path_baslangic,
                             query_baslangic - path_baslangic);
    cout << "Path: " << path << endl;  // /api/users

    // Query string çıkar
    if (query_baslangic != string::npos) {
        string query = url.substr(query_baslangic + 1);
        cout << "Query: " << query << endl;  // id=42
    }

    return 0;
}

Özet

  • find ve rfind ile string içinde baştan ve sondan arama yapılır; bulunamazsa string::npos döner. find_first_of bir karakter kümesinden ilk eşleşeni bulur.

  • substr(pos, len) ile string'in belirli bir bölümü çıkarılır; C++'ta hazır split yoktur ama find + substr ile yazılabilir.

  • replace belirtilen aralığı yeni string ile değiştirir; tüm geçişleri değiştirmek için döngü gerekir.

  • stoi/stod string'den sayıya, to_string sayıdan string'e dönüşüm yapar. Dönüşüm başarısız olursa exception fırlatılır.

  • string_view (C++17) string'e kopyalamadan bakan, hafif bir türdür. Fonksiyon parametrelerinde const string& yerine kullanılabilir ama dangling referans riskine dikkat edilmelidir.

  • Regex (<regex>) ile kalıp eşleme yapılır: regex_match tam eşleşme, regex_search kısmi arama, regex_replace kalıpla değiştirme sağlar.