9-bo‘lim

RAII - resurs bilan bog'langan umr

C++ ning markaziy g'oyasi - resursni obyekt umriga bog'lash; nima uchun u istisnolar bilan ham ishlaydi.

🕑 14 daqiqa o‘qish 📄 869 so‘z 👁 2 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo: tozalash unutiladi
  2. Yechim: destruktor
  3. Istisno bilan sinov
  4. Standart kutubxonadagi RAII
  5. Muteks - RAII ning klassik misoli
  6. O'z RAII sinfingizni yozish
  7. Qamrov qo'riqchisi
  8. Nima uchun bu C++ ga xos
  9. Xulosa

Bu darslikning eng muhim bo'limi.

RAII (Resource Acquisition Is Initialization) - C++ ni boshqa tillardan ajratib turadigan g'oya. U shunday deydi:

Resurs konstruktorda olinadi va destruktorda qaytariladi.

Muammo: tozalash unutiladi #

C++
#include <print>
#include <string>
#include <vector>
#include <stdexcept>
#include <memory>
#include <mutex>
#include <fstream>
#include <chrono>
C++
void xatoUsul(bool xatoChiqsin)
{
    int* buffer = new int[1000];        // resurs olindi
    buffer[0] = 42;

    if (xatoChiqsin) {
        std::println("  erta chiqish - delete BAJARILMADI");
        return;                          // XOTIRA SIZIB CHIQDI
    }

    std::println("  normal yo'l - buffer[0] = {}", buffer[0]);
    delete[] buffer;                     // faqat shu yo'lda bajariladi
}

int main()
{
    std::println("1. normal holat:");
    xatoUsul(false);

    std::println("2. erta chiqish:");
    xatoUsul(true);

    return 0;
}
Natija
1. normal holat:
  normal yo'l - buffer[0] = 42
2. erta chiqish:
  erta chiqish - delete BAJARILMADI
Erta chiqish - eng ko'p uchraydigan sizish sababi

delete[] yozilgan, lekin u bajarilmadi.

Bu kodda faqat bitta return bor. Real funksiyada esa:

Chiqish yo'lidelete bajariladimi
Normal returnHa
Shartli erta returnFaqat qo'shsangiz
break, continueFaqat qo'shsangiz
IstisnoHech qachon
std::terminateYo'q

To'rtinchi qator hal qiluvchi: istisno chiqsa, delete gacha yetib borilmaydi. Va istisno istalgan funksiyadan chiqishi mumkin - hatto new dan ham.

Har chiqish yo'liga delete qo'shish esa amalda imkonsiz.

Yechim: destruktor #

C++
class Buffer
{
    int* malumot;
    std::size_t hajm;

public:
    // KONSTRUKTOR - resurs olinadi
    explicit Buffer(std::size_t n)
        : malumot(new int[n]), hajm(n)
    {
        std::println("  [Buffer yaratildi: {} element]", hajm);
    }

    // DESTRUKTOR - resurs qaytariladi
    ~Buffer()
    {
        delete[] malumot;
        std::println("  [Buffer yo'q qilindi: {} element]", hajm);
    }

    int& operator[](std::size_t i) { return malumot[i]; }
    std::size_t olcham() const { return hajm; }
};

void togriUsul(bool xatoChiqsin)
{
    Buffer buffer{1000};
    buffer[0] = 42;

    if (xatoChiqsin) {
        std::println("  erta chiqish");
        return;                      // destruktor BARIBIR chaqiriladi
    }

    std::println("  normal yo'l - buffer[0] = {}", buffer[0]);
}

int main()
{
    std::println("1. normal holat:");
    togriUsul(false);

    std::println("");
    std::println("2. erta chiqish:");
    togriUsul(true);

    return 0;
}
Natija
1. normal holat:
  [Buffer yaratildi: 1000 element]
  normal yo'l - buffer[0] = 42
  [Buffer yo'q qilindi: 1000 element]

2. erta chiqish:
  [Buffer yaratildi: 1000 element]
  erta chiqish
  [Buffer yo'q qilindi: 1000 element]
Destruktor kafolatlangan

Obyekt qamrovdan chiqishi bilan destruktor chaqiriladi. Bu shartsiz kafolat:

Chiqish yo'liDestruktor
Normal returnChaqiriladi
Erta returnChaqiriladi
break, continueChaqiriladi
IstisnoChaqiriladi
Qamrov oxiriChaqiriladi

Yagona istisno - std::abort yoki std::exit (ular dasturni darhol to'xtatadi).

Bu C++ ning axlat yig'uvchisi bo'lgan tillardan asosiy farqi: u yerda tozalash qachondir bo'ladi, C++ da esa aniq shu yerda.

Istisno bilan sinov #

C++
class Kuzatuvchi
{
    std::string nom;

public:
    explicit Kuzatuvchi(std::string n) : nom(std::move(n))
    {
        std::println("  + {} yaratildi", nom);
    }

    ~Kuzatuvchi()
    {
        std::println("  - {} yo'q qilindi", nom);
    }
};

void ichkiFunksiya()
{
    Kuzatuvchi c{"ichki"};
    std::println("  istisno tashlanmoqda...");
    throw std::runtime_error("nimadir buzildi");
}

void ortadagiFunksiya()
{
    Kuzatuvchi b{"o'rtadagi"};
    ichkiFunksiya();
    std::println("  BU QATOR HECH QACHON BAJARILMAYDI");
}

int main()
{
    try {
        Kuzatuvchi a{"tashqi"};
        ortadagiFunksiya();
    } catch (const std::runtime_error& x) {
        std::println("");
        std::println("ushlandi: {}", x.what());
    }

    std::println("");
    std::println("Uchala obyekt ham TESKARI tartibda yo'q qilindi.");

    return 0;
}
Natija
  + tashqi yaratildi
  + o'rtadagi yaratildi
  + ichki yaratildi
  istisno tashlanmoqda...
  - ichki yo'q qilindi
  - o'rtadagi yo'q qilindi
  - tashqi yo'q qilindi

ushlandi: nimadir buzildi

Uchala obyekt ham TESKARI tartibda yo'q qilindi.
Stekni yechish (stack unwinding)

Istisno tashlanganda C++ chaqiruvlar stekini orqaga qaytadi va yo'lidagi har bir mahalliy obyektning destruktorini chaqiradi.

Tartib - yaratilishning teskarisi: oxirgi yaratilgan birinchi yo'q qilinadi.

Bu RAII ni ishonchli qiladi: qanday chiqilsa ham, resurslar qaytariladi.

Muhim shart: destruktor istisno tashlamasligi kerak. Agar stekni yechish paytida destruktordan istisno chiqsa, dastur std::terminate bilan tugaydi.

Shuning uchun destruktorlarni noexcept deb belgilash odat (bu standart holat).

RAII - resurs obyekt umriga bog'lanadi Qo'lda boshqarish int* p = new int[1000]; if (shart) return; ← sizadi risk(); ← istisno: sizadi delete[] p; ← faqat bitta yo'lda Har chiqish yo'liga delete kerak Istisnolar bilan bu imkonsiz Kodning yarmi tozalash bo'lib qoladi RAII Buffer b{1000}; ← konstruktor oladi if (shart) return; ← tozalanadi risk(); ← tozalanadi // delete YO'Q ← destruktor qaytaradi Tozalash BITTA joyda Har chiqish yo'lida ishlaydi Unutish mumkin emas RAII faqat xotira uchun emas Fayl deskriptori · tarmoq ulanishi · muteks · baza tranzaksiyasi · grafik resurs · vaqtinchalik fayl Har qanday "olinadi va qaytariladi" narsa RAII obyektiga o'ralishi mumkin. Asosiy qoida Har resursning bitta egasi bo'lsin. Egasi yo'q qilinganda resurs ham qaytarilsin.
Destruktor - C++ dagi yagona kafolatlangan tozalash mexanizmi

Standart kutubxonadagi RAII #

C++
int main()
{
    // 1. Konteynerlar
    {
        std::vector<int> v(1000);
        std::println("vector: xotira ajratildi, {} element", v.size());
    }
    std::println("vector: qamrovdan chiqdi - xotira qaytarildi");

    // 2. Satrlar
    {
        std::string s(10000, 'x');
        std::println("");
        std::println("string: {} belgi", s.size());
    }
    std::println("string: qaytarildi");

    // 3. Aqlli ko'rsatkichlar
    {
        auto p = std::make_unique<int>(42);
        std::println("");
        std::println("unique_ptr: qiymat {}", *p);
    }
    std::println("unique_ptr: delete avtomatik bajarildi");

    // 4. Fayl oqimlari
    {
        std::ofstream fayl{"/tmp/sr_raii_sinov.txt"};
        fayl << "RAII sinovi\n";
        std::println("");
        std::println("ofstream: fayl ochildi va yozildi");
    }
    std::println("ofstream: fayl yopildi (close() chaqirilmadi)");

    return 0;
}
Natija
vector: xotira ajratildi, 1000 element
vector: qamrovdan chiqdi - xotira qaytarildi

string: 10000 belgi
string: qaytarildi

unique_ptr: qiymat 42
unique_ptr: delete avtomatik bajarildi

ofstream: fayl ochildi va yozildi
ofstream: fayl yopildi (close() chaqirilmadi)
Standart kutubxonaning hammasi RAII
SinfNima boshqaradi
std::vector, std::stringDinamik xotira
std::unique_ptr, std::shared_ptrBitta obyekt
std::fstreamFayl deskriptori
std::lock_guard, std::unique_lockMuteks
std::jthread (C++20)Oqim
std::scoped_lockBir necha muteks

Shuning uchun zamonaviy C++ kodida new, delete, fclose, unlock deyarli uchramaydi.

Ular kutubxona ichida - va u yerda bir marta to'g'ri yozilgan.

Muteks - RAII ning klassik misoli #

C++
std::mutex qulf;
int umumiyHisob = 0;

void xatoUsul()
{
    qulf.lock();

    ++umumiyHisob;

    if (umumiyHisob > 100) {
        return;               // QULF OCHILMADI - boshqa oqimlar muzlaydi
    }

    qulf.unlock();
}

void togriUsul()
{
    std::lock_guard<std::mutex> saqlovchi{qulf};

    ++umumiyHisob;

    if (umumiyHisob > 100) {
        return;               // destruktor qulfni ochadi
    }

    // unlock() kerak emas
}

int main()
{
    togriUsul();
    togriUsul();
    togriUsul();

    std::println("hisob: {}", umumiyHisob);
    std::println("");
    std::println("lock_guard qanday chiqilsa ham qulfni ochadi:");
    std::println("  normal return  - ochadi");
    std::println("  erta return    - ochadi");
    std::println("  istisno        - ochadi");
    std::println("");
    std::println("Qo'lda lock/unlock da ularning hammasini");
    std::println("hisobga olish kerak - va bittasi unutiladi.");

    return 0;
}
Natija
hisob: 3

lock_guard qanday chiqilsa ham qulfni ochadi:
  normal return  - ochadi
  erta return    - ochadi
  istisno        - ochadi

Qo'lda lock/unlock da ularning hammasini
hisobga olish kerak - va bittasi unutiladi.

O'z RAII sinfingizni yozish #

C++
// Vaqtni o'lchaydigan RAII obyekt
class Xronometr
{
    std::string nom;
    std::chrono::steady_clock::time_point boshi;

public:
    explicit Xronometr(std::string n)
        : nom(std::move(n)), boshi(std::chrono::steady_clock::now())
    {
        std::println("  [{} boshlandi]", nom);
    }

    ~Xronometr()
    {
        auto oxiri = std::chrono::steady_clock::now();
        auto vaqt = std::chrono::duration_cast<std::chrono::microseconds>(
                        oxiri - boshi).count();

        // Aniq vaqt mashinaga bog'liq - shuning uchun faqat holatni yozamiz
        std::println("  [{} tugadi, vaqt o'lchandi: {}]",
                     nom, vaqt >= 0 ? "ha" : "yo'q");
    }
};

void ogirIsh()
{
    Xronometr x{"og'ir ish"};

    std::vector<int> v;
    v.reserve(100000);
    for (int i = 0; i < 100000; ++i) {
        v.push_back(i * i);
    }

    std::println("  {} element hisoblandi", v.size());
}

int main()
{
    std::println("funksiyaga kiramiz:");
    ogirIsh();
    std::println("funksiyadan chiqdik");

    return 0;
}
Natija
funksiyaga kiramiz:
  [og'ir ish boshlandi]
  100000 element hisoblandi
  [og'ir ish tugadi, vaqt o'lchandi: ha]
funksiyadan chiqdik
RAII obyektini yozish shabloni
KOD
class Resurs
{
    Tur* tutqich;

public:
    explicit Resurs(Parametr p) : tutqich(ol(p))
    {
        if (!tutqich) {
            throw std::runtime_error("olib bo'lmadi");
        }
    }

    ~Resurs() { if (tutqich) qaytar(tutqich); }

    // Nusxalashni taqiqlaymiz yoki to'g'ri yozamiz (12-bo'lim)
    Resurs(const Resurs&) = delete;
    Resurs& operator=(const Resurs&) = delete;

    // Ko'chirishga ruxsat beramiz
    Resurs(Resurs&& b) noexcept : tutqich(b.tutqich) { b.tutqich = nullptr; }
};

Oxirgi uchta qator muhim: resursni boshqaradigan sinf nusxalanishi mumkin emas (aks holda ikki obyekt bitta resursni qaytarardi).

Buni 12-bo'limda batafsil ko'ramiz.

Qamrov qo'riqchisi #

C++
// Har qanday amalni qamrov oxirida bajaradi
template <typename Amal>
class QamrovQoriqchisi
{
    Amal amal;
    bool faol = true;

public:
    explicit QamrovQoriqchisi(Amal a) : amal(std::move(a)) {}

    ~QamrovQoriqchisi()
    {
        if (faol) {
            amal();
        }
    }

    void bekorQil() { faol = false; }

    QamrovQoriqchisi(const QamrovQoriqchisi&) = delete;
    QamrovQoriqchisi& operator=(const QamrovQoriqchisi&) = delete;
};

void tranzaksiya(bool muvaffaqiyat)
{
    std::println("  tranzaksiya boshlandi");

    QamrovQoriqchisi qoriqchi{[] {
        std::println("  >> ORQAGA QAYTARILDI");
    }};

    // ... ish bajariladi ...

    if (!muvaffaqiyat) {
        std::println("  xato yuz berdi");
        return;               // qo'riqchi ishlaydi
    }

    std::println("  hammasi yaxshi");
    qoriqchi.bekorQil();      // orqaga qaytarish kerak emas
    std::println("  >> TASDIQLANDI");
}

int main()
{
    std::println("1. muvaffaqiyatli:");
    tranzaksiya(true);

    std::println("");
    std::println("2. xato bilan:");
    tranzaksiya(false);

    return 0;
}
Natija
1. muvaffaqiyatli:
  tranzaksiya boshlandi
  hammasi yaxshi
  >> TASDIQLANDI

2. xato bilan:
  tranzaksiya boshlandi
  xato yuz berdi
  >> ORQAGA QAYTARILDI
Qamrov qo'riqchisi - eng moslashuvchan RAII

Bu naqsh istalgan tozalashni qamrov oxiriga bog'laydi - alohida sinf yozmasdan.

Amaliy qo'llanishlar:

VaziyatNima qilinadi
Baza tranzaksiyasiMuvaffaqiyat bo'lmasa - orqaga qaytarish
Vaqtinchalik faylChiqishda o'chirish
Global holatEski qiymatni tiklash
C kutubxonasi tutqichiclose, free chaqirish
Jurnal"funksiyadan chiqildi" yozish

Ba'zi kutubxonalarda tayyor shakli bor (Boost, GSL da finally), lekin uni o'zingiz yozish ham oson - yuqorida 20 qator.

Shuni ham eslang: destruktor istisno tashlamasin. Qo'riqchi ichidagi amal ham xavfsiz bo'lishi kerak.

Nima uchun bu C++ ga xos #

C++
int main()
{
    std::println("{:<14} {:<26} {}", "TIL", "TOZALASH", "QACHON");
    std::println("{}", std::string(66, '-'));
    std::println("{:<14} {:<26} {}", "C", "qo'lda free/close",
                 "yozsangiz");
    std::println("{:<14} {:<26} {}", "C++", "destruktor",
                 "ANIQ, qamrov oxirida");
    std::println("{:<14} {:<26} {}", "Java", "axlat yig'uvchi",
                 "qachondir");
    std::println("{:<14} {:<26} {}", "Python", "havolalar sanog'i",
                 "odatda darhol");
    std::println("{:<14} {:<26} {}", "Go", "axlat yig'uvchi + defer",
                 "funksiya oxirida");
    std::println("{:<14} {:<26} {}", "Rust", "Drop",
                 "ANIQ, qamrov oxirida");

    std::println("");
    std::println("Faqat C++ va Rust da tozalash vaqti ANIQ ma'lum.");
    std::println("Axlat yig'uvchi xotirani boshqaradi, lekin");
    std::println("fayl va qulflarni emas - ular uchun alohida");
    std::println("mexanizm kerak (try-with-resources, with, defer).");

    return 0;
}
Natija
TIL            TOZALASH                   QACHON
------------------------------------------------------------------
C              qo'lda free/close          yozsangiz
C++            destruktor                 ANIQ, qamrov oxirida
Java           axlat yig'uvchi            qachondir
Python         havolalar sanog'i          odatda darhol
Go             axlat yig'uvchi + defer    funksiya oxirida
Rust           Drop                       ANIQ, qamrov oxirida

Faqat C++ va Rust da tozalash vaqti ANIQ ma'lum.
Axlat yig'uvchi xotirani boshqaradi, lekin
fayl va qulflarni emas - ular uchun alohida
mexanizm kerak (try-with-resources, with, defer).
RAII buzilishining uchta yo'li

RAII kuchli, lekin uni buzish mumkin:

1. Xom new ni saqlash.

KOD
Resurs* p = new Resurs{};    // egasi yo'q

Yechim: std::unique_ptr (10-bo'lim).

2. Nusxalashni noto'g'ri yozish.

Ikki obyekt bitta resursga ega bo'lsa, destruktor ikki marta chaqiriladi. Yechim: 12-bo'lim.

3. Halqa havolalar.

shared_ptr bir-biriga ishora qilsa, hech biri yo'q qilinmaydi. Yechim: weak_ptr (10-bo'lim).

Uchalasi ham keyingi bo'limlarda ko'riladi.

Amaliy topshiriq
  1. new bilan xotira oling va erta return qo'shing.
  2. Xuddi shu ishni destruktorli sinf bilan qayta yozing.
  3. Uch darajali funksiya chaqiruvida istisno tashlang.
  4. Destruktorlar qanday tartibda chaqirilganini kuzating.
  5. vector, string, unique_ptr va ofstream qamrovdan chiqishini ko'ring.
  6. Qo'lda lock/unlock yozing va erta return qo'shing.
  7. Uni std::lock_guard bilan qayta yozing.
  8. Vaqt o'lchaydigan RAII sinf yozing.
  9. Qamrov qo'riqchisini yozing va uni bekor qilishni sinang.
  10. RAII buzilishining uchta yo'lini o'z so'zlaringiz bilan yozing.

Xulosa #

  • RAII: resurs konstruktorda olinadi, destruktorda qaytariladi.
  • Destruktor qamrovdan chiqishda kafolatlangan tarzda chaqiriladi.
  • Bu erta return, break va istisno holatlarida ham ishlaydi.
  • Istisno tashlanganda stek yechiladi va barcha destruktor chaqiriladi.
  • Destruktorlar yaratilishning teskari tartibida ishlaydi.
  • Destruktor istisno tashlamasligi kerak - aks holda dastur to'xtaydi.
  • Standart kutubxonaning hammasi RAII: konteynerlar, oqimlar, qulflar.
  • Shuning uchun zamonaviy kodda new/delete deyarli uchramaydi.
  • Qamrov qo'riqchisi istalgan tozalashni qamrov oxiriga bog'laydi.
  • Tozalash vaqti C++ da aniq ma'lum - axlat yig'uvchili tillarda esa yo'q.

Keyingi bo'limda RAII ning eng ko'p ishlatiladigan shakli - aqlli ko'rsatkichlar bilan tanishamiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.