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.
Ushbu bo‘lim mundarijasi
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 #
#include <print>
#include <string>
#include <vector>
#include <stdexcept>
#include <memory>
#include <mutex>
#include <fstream>
#include <chrono>
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;
}
1. normal holat:
normal yo'l - buffer[0] = 42
2. erta chiqish:
erta chiqish - delete BAJARILMADI
delete[] yozilgan, lekin u bajarilmadi.
Bu kodda faqat bitta return bor. Real funksiyada esa:
| Chiqish yo'li | delete bajariladimi |
|---|---|
Normal return | Ha |
Shartli erta return | Faqat qo'shsangiz |
break, continue | Faqat qo'shsangiz |
| Istisno | Hech qachon |
std::terminate | Yo'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 #
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;
}
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]
Obyekt qamrovdan chiqishi bilan destruktor chaqiriladi. Bu shartsiz kafolat:
| Chiqish yo'li | Destruktor |
|---|---|
Normal return | Chaqiriladi |
Erta return | Chaqiriladi |
break, continue | Chaqiriladi |
| Istisno | Chaqiriladi |
| Qamrov oxiri | Chaqiriladi |
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 #
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;
}
+ 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.
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).
Standart kutubxonadagi RAII #
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;
}
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)
| Sinf | Nima boshqaradi |
|---|---|
std::vector, std::string | Dinamik xotira |
std::unique_ptr, std::shared_ptr | Bitta obyekt |
std::fstream | Fayl deskriptori |
std::lock_guard, std::unique_lock | Muteks |
std::jthread (C++20) | Oqim |
std::scoped_lock | Bir 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 #
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;
}
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 #
// 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;
}
funksiyaga kiramiz:
[og'ir ish boshlandi]
100000 element hisoblandi
[og'ir ish tugadi, vaqt o'lchandi: ha]
funksiyadan chiqdik
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 #
// 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;
}
1. muvaffaqiyatli:
tranzaksiya boshlandi
hammasi yaxshi
>> TASDIQLANDI
2. xato bilan:
tranzaksiya boshlandi
xato yuz berdi
>> ORQAGA QAYTARILDI
Bu naqsh istalgan tozalashni qamrov oxiriga bog'laydi - alohida sinf yozmasdan.
Amaliy qo'llanishlar:
| Vaziyat | Nima qilinadi |
|---|---|
| Baza tranzaksiyasi | Muvaffaqiyat bo'lmasa - orqaga qaytarish |
| Vaqtinchalik fayl | Chiqishda o'chirish |
| Global holat | Eski qiymatni tiklash |
| C kutubxonasi tutqichi | close, 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 #
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;
}
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 kuchli, lekin uni buzish mumkin:
1. Xom new ni saqlash.
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.
newbilan xotira oling va ertareturnqo'shing.- Xuddi shu ishni destruktorli sinf bilan qayta yozing.
- Uch darajali funksiya chaqiruvida istisno tashlang.
- Destruktorlar qanday tartibda chaqirilganini kuzating.
vector,string,unique_ptrvaofstreamqamrovdan chiqishini ko'ring.- Qo'lda
lock/unlockyozing va ertareturnqo'shing. - Uni
std::lock_guardbilan qayta yozing. - Vaqt o'lchaydigan RAII sinf yozing.
- Qamrov qo'riqchisini yozing va uni bekor qilishni sinang.
- 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,breakva 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/deletedeyarli 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.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
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.