C++ da havolalar va ko’rsatkichlar, xotira menejerligini o’rganamiz.

C++ da havolalar va ko’rsatkichlar, xotira menejerligini o’rganamiz.

C++ da havolalar va ko'rsatkichlar, xotira menejerligini o'rganamiz.

Maqolada smart pointers — aqlli ko’rsatkichlar, ularning ishlash prinsiplari, ularning umumiy metodlari haqida so’z boradi. C++ da havolalar va ko’rsatkichlar, xotira menejerligini o’rganamiz maqolasining davomi.

Aqlli ko’rsatkichlar haqida.

Dastur(bunda bitta threadga ega process(jarayon) nazarda tutilyapti) ishini boshlaganda, u uchun alohida stek ajratiladi, va bu stek hajmi kichikroq bo’ladi. Stek to’lib qolishi stackoverflow xatoligini keltirib chiqarishi mumkin, agar bunday xatolik ehtimoli bo’lsa, ma’lumotlar heapga joylashtirilib xotira manzili stekka kiritib qo’yiladi — shu kiritilgan manzil ko’rsatkich hisoblanadi. Heapga qiymatni joylar ekanmiz, undan ma’lumotlarni «tozalab» tashlash ham bizning zimmamizga yuklaniladi. Har bir obyektlar to’g’ri va faqat bir martadan o’chirilishi, o’chirilishni tekshirish kabi ishlar low level(quyi) hisoblanib, bu narsa kodning tozaligiga ta’sir qiladi, natijada hatto sodda logikani tushunish qiyin bo’lgan abrakadabralarga aylantirib qo’yishi ham mumkin. Lekin… Baribir ko’rsatkichlar bilan ishlashga to’g’ri kelib qolsa-chi? Biz esa heapdagi obyektlarni o’chirishni iloji boricha asosiy logikadan uzoqroqda hal qilmoqchimiz. Aqlli ko’rsatkichlar mana shu qora ishlardan bizni himoya qilish uchun o’ylab topilgan.

Ishlash prinsipi.

Keling oddiy holat uchun obyektning xotiradagi «hayot sikli»(lifecycle) ni ko’ramiz:

int main(){

{//5 qiymati stekka kiritilyapti
int a = 5;
}//o'chirib tashlandi

}

Bu yerda bitta muhim, hal qiluvchi qoida bor bo’lib, aqlli ko’rsatkichlar ushbu qoida yordamida ishlaydi. Aytgancha, siz uchun yaxshi yangilik — qoida o’rganib olish uchun juda oson:

Stekka kiritilgan obyektlar out-of-scope(sodda qilib aytganda { va } oralig’idan chiqib ketgan) holda avtomatik ravishda o’chiriladi.

Ko’rsatkich bilan ishlaganimizda esa heapdagi manzilni ko’rsatib turgan ko’rsatkich o’chirilib, manzildagi obyekt xotiradan joy egallab turaverar edi. Bunda biz o’sha obyektga murojaat qilish uchun uning xotiradagi manzilini ko’rsatib turgan ko’rsatkichni yo’qotardik(bu holat memory leakage deyilad). Voila! Biz stekka joylangan har qanday obyekt out-of-scope bo’lganda avtomatik ravishda o’chirilishini bilgan holda o’zimiz uchun kichkina class yozib olishimiz mumkin. G’oya esa sodda — ko’rsatkichni o’zimiz yozgan obyektga «o’raymiz», va obyekt destruktor qismida ko’rsatkichni o’chirib tashlaymiz:

template
class ScopedPointer{
    T* m_ptr;
public:
    ScopedPointer(T* ptr)
    {
        m_ptr = ptr;
    }
    T* operator->(){
        return m_ptr;
    }
    ~ScopedPointer(){
        delete m_ptr;
    }
};

yozilgan classni testlash uchun Test class:

class Test{
    int val;
public:
    Test(int data)
    {
        cout 

Endi yuqoridagi ScopedPointer dan foydalanib ko'ramiz:

{
    ScopedPointer ptr(new Test(100));
    cout getVal() 

Kodni yurgazib ko'rganimizda ekranda Test obyekti yaratilganligi, undagi val qiymat va oxirida Test obyekti o'chirilganligi haqida xabarni ko'rishimiz mumkin. O'zim ham bitta proyektimda shunga o'xshagan class yozgandim ko'rsatkich uchun. Mendagi holatda bir funksiya qaytargan ko'rsatkichni olib, uni kerakli funksiyaga yo'naltirish kerak bo'lardi. U ishlatib bo'lingach, yana shunaqa ko'rsatkich kelib qolsa eskisini o'chirib yuborib, yangi kelgan ko'rsatkichni yana ushlab qolish kerak edi. Bunga yuqoridagiga o'xshash class yozgandim, eski obyektni o'chirib yangisni ushlab qolishni = operatorini qayta yuklash orqali hal qilganman.(balkim yaxshi g'oya bo'lmagandir, lekin ish bergandi :))

Albatta, yuqorida yozganimiz ScopedPointer classning o'ziga yarasha kamchiliklari ham bor(bu haqida hozir to'xtalib o'tmayman)! Aqlli ko'rsatkichlar nafaqat C++ standartida, balki boost kutubxonasida ham tanishtirilgan. Biz C++ ning standart kutubxonalaridagi aqlli ko'rsatkichlarni ko'ramiz.

Eslatma:
C++ dagi aqlli ko'rsatkichlar kutubxonasidagi std nomlar fazosida joylashgan.

std::unique_ptr;

Odatiy holatlar uchun qo'llaniladigan ushbu aqlli ko'rsatkich C++ 11 dan boshlab standartga kiritilgan. Uning ishlash prinsipi biz yuqorida yozgan ScopedPointer ga o'xshab ketadi, faqat ishlash tizimi kengaytirilgan:

std::unique_ptr data(new Test(100));
cout getVal() 

Ushbu aqlli ko'rsatkichni o'ziga hos xususiyatlari ham bor. Masalan, u ushlab turgan ko'rsatkichni boshqa unique_ptr larga shunday berib qo'ymaydi, chunki unikal ko'rsatkichning egasi unikal bo'lishi kerak:

std::unique_ptr data(new Test(100));
//xatolik: call to deleted constructor
std::unique_ptr ptr2 = data;

Xatolik unique_ptr dagi copy constructor o'chirib qo'yilganligi tufayli kelib chiqyapti. Agar o'chirib qo'yilmaganda, yuqoridagi kodda data va ptr2 o'zlarining destruktorida bitta Test obyektini o'chirishga urinishlari hisobiga noma'lum xatolik kelib chiqishi mumkin. Biz shu holatni ScopedPointer class da hisobga olib ketmagandik. Hechqisi yo'q, unga quyidagi kodni qo'shib qo'yamiz 🙂

ScopedPtr(ScopedPtr& x) = delete;

endi biz ham copy constructor ni yopib qo'ydik.

Yuqoridagi kodda data ni shundoqligicha ptr2 ga berolmadik. Bu holatda biz move() funksiyasidan foydalanishimiz mumkin:

std::unique_ptr data(new Test(100));
std::unique_ptr ptr2 = std::move(data);
std::cout 

unique_ptr dagi ko'rsatkich ko'rsatib turgan manzil unique_ptr::get() metodi yordamida olinadi. Yuqoridagi kodni yurgazib ko'rganingizda, data ko'rsatib turgan manzil 0 ekanligini ko'rasiz. Sababi ko'rsatkichning egasi unikal bo'lishi kerak, bu holatda biz ko'rsatkichga egalikni ptr2 ga berib yubordik. move() funksiyasi move constructor ga asoslanib ishlaydi.

std::shared_ptr

shared_ptr C++ 11 dan boshlab kirib keldi, lekin Boost kutubxonasida oldinroq paydo bo'lgan. unique_ptr dan katta va muhim farqi:

Xotiradagi bitta obyektga bir nechta std::shared_ptr lar egalik qilishi mumkin.

shared_ptr ham yetarlicha aqlli, agar bitta obyektga egalik qilayotgan sheriklari bo'lsa u o'zining destruktor qismida o'sha obyektni o'chirib yubormaydi. Qancha sheriklari borligini bilish uchun o'zining ichki qismida sanagichi bo'lib, o'zi o'chib ketayotganda o'sha sanagichni bitta orqaga surib qo'yadi. Va bu sanagich sheriklarida ham bir xil bo'ladi. Qachonki sheriklaridan oxirgisi o'chirilayotgandagina egalik qilib turilgan obyekt xotiradan o'chiriladi. Sheriklarining qanchaligini bilish uchun sanagich ishlatilishi reference counting(obyektga bo'lgan havola, ko'rsatkich, egaliklarni sanab ketish) texnikasi deyiladi.

std::shared_ptr data(new Test(100));
std::shared_ptr ptr2 = data;

std::cout 

c da havolalar va korsatkichlar xotira menejerligini organamiz 65e901df8b653

Kodni yurgazib ko'rgach, ko'rsatkichga 2 ta shared_ptr: data va ptr2 egalik qilayotganini ko'rasiz. ptr2 = data; data = ptr2 qilib sanagichni oshiraman, keyin kod yurganda egalik qilishlar soni ortgan bo'ladi deb o'ylasangiz adashasiz, shared_ptr avval aytganimizdek aqlli ko'rsatkich ;)

shared_ptr ni quyidagicha ishlatish noma'lum xatolikni keltirib chiqarishi mumkin:

auto e1 = new Test(100);
std::shared_ptr shared_e1(e1);
std::shared_ptr shared_e2(e1);

bu holatda ikkala shared_ptr lar ham bitta e1 ga egalik qilyapti, lekin ular bundan bexabar holda o'zlarining sanagichlarini 1 qilib olishadi. Nega bu kod muammoli bo'lishi mumkinligini o'zingizga qoldirib, keyingi aqlli ko'rsatkich bilan tanishtiray.

std::weak_ptr;

weak_ptr C++ 11 dan boshlab standartga kiritilgan, lekin Boost kutubxonasida bundan avvalroq paydo bo'lgan. shared_ptr lar egalik qilayotgan obyektga weak_ptr ham egalik qilishi mumkin, lekin uning egaligi shared_ptr lardagi sanagichga ta'sir qilmaydi(uning qiymatini oshirmaydi/kamaytirmaydi). Ya'ni agar o'sha obyektga egalik qilayotgan barcha shared_ptr lar o'chirilsa, o'sha obyekt ham xotiradan o'chiriladi, hatto weak_ptr unga egalik qilib turgan bo'lsa ham. Bunday holatda weak_ptr::lock() metodi yordamida weak_ptr egalik qilayotgan obyekt hali ham aktualligini tekshirib ko'rishimiz mumkin, metod shared_ptr qaytaradi:

std::shared_ptr data(new Test(100));
std::shared_ptr ptr2 = data;
//obyektga 2 ta shared_ptr egalik qilyapti

std::weak_ptr wk = ptr2;
std::shared_ptr temp = wk.lock();
std::cout 

weak_ptr larni shared_ptr lar bilan birga ishlatganda, weak_ptr ko'rsatib turgan ko'rsatkich null emasligini tekshirib olish kerak bo'ladi. Tekshirishni weak_ptr::expired() yoki weak_ptr::lock() metodlari bilan amalga oshirishimiz mumkin. Bu ikkala metodni nima farqi bor? expired bool tipidagi natija qaytaradi(ko'rsatkich nullptr bo'lsa true, aks holda false), lock esa weak_ptr ko'rsatib turgan obyektni shared_ptr ga o'rab qaytaradi. Agar obyekt multi-thread da ishlatilinayotgan bo'lsa, lock metodi yordamida shared_ptr olib keyin shared_ptr null emasligini tekshirish havfsizroq hisoblanadi.

if(!wk.expired())
{
    //bu yerda boshqa thread da barcha share_ptr lar
    //o'chib ketgan bo'lsa, wk dagi obyekt ham o'chgan bo'ladi
    //bu esa noma'lum xatolikka olib kelishi mumkin.
}

weak_ptr larni nimaga ishlatsak bo'ladi? Deylik sizda 3 ta shared_ptr bir-biriga bog'langan. Bu holatda out-of-scope bo'lganda ham ular xotiradan o'chib ketmaydi(nega o'chib ketmasligi haqida o'ylab ko'ring). Shu holatni yozayotganda bitta mem hayolga kelib qoldi 😉

c da havolalar va korsatkichlar xotira menejerligini organamiz 65e901dfe6b4b

Shunday hollarda ulardan birini weak_ptr qilish orqali avtomatik o'chishini ta'minlash mumkin.

c da havolalar va korsatkichlar xotira menejerligini organamiz 65e901e07be79

Agar treeda siklik holat bo'lsa, u graf bo'lib qoladi. Bunday hollarda parent to child bog'lanishni shared_ptr, child to parent bog'lanishni weak_ptr bilan qilishimiz mumkin.

std::auto_ptr;

auto_ptr tilga qolgan aqlli ko'rsatkichlarga nisbatan ertaroq - C++ 98 da kirib kelgan, C++ 11 ga kelib esa eskirgan, C++ 17 dan boshlab tildan chiqarib yuborilgan. U deyarli unique_ptr ga o'xshaydi, lekin u paytlarda hali tilda move constructor degan tushunchalar bo'lmagan. unique_ptr o'zining move constructor qismida nima ishlarni qilsa, auto_ptr ham aynan shu ishlarni copy constructor da qiladi.

std::auto_ptr p1(new int(42)); 
std::auto_ptr p2 = p1;
//obyektga egalik huquqi p2 ga o'tdi, p1 esa bo'shab qoldi

Agar sizda unique_ptr ni ishlatishga imkon bo'lsa, yaxshisi auto_ptr ni ishlatmaganingiz ma'qul, chunki u unique_ptr ga qaraganda eskiroq hisoblanadi.

Kichik hulosalar:

  • unique_ptr nazariy jihatdan o'zi ushlab turgan ko'rsatkichga yakka o'zi egalik qilishi kerak. Undagi egalikni boshqa aqlli ko'rsatkichga move() yordamida o'tkazish mumkin.
  • Texnik jihatdan unique_ptr xotiradan oddiy ko'rsatkich egallaganchalik joyni egallaydi, lekin oddiy ko'rsatkichga nisbatan qo'shimcha qulayliklari bor.
  • shared_ptr o'zi egalik qilayotgan obyektga yana nechta shared_ptr egalik qilayotganini bilish uchun sanagich ishlatadi.
  • shared_ptr ni ulashilmaydigan obyektlarga egalik qilishda foydalanish vaqt va resurs tomonidan qimmatga tushishi mumkin, chunki u o'zida ko'rsatkich bilan birga reference counting uchun ham xotiradan joy egallaydi.
  • shared_ptr::use_count() metodi yordamida obyektga nechta shared_ptr egalik qilayotganini bilish mumkin.
  • shared_ptr lar bilan ishlashda ehtiyot bo'lish kerak, chunki ularni ko'p ishlatish koddagi murakkablikni orttirib yuboradi. Agar shared_ptr lar bir-birlariga bevosita/bilvosita bog'lanib qolgan bo'lsa, unda ulardan ba'zilari o'chmay qolishi mumkin. Bunday aylana bo'lib bog'lanib qolish holatlari bo'lsa, ba'zi shared_ptr lar o'rniga weak_ptr larni ishlatish kerak.
  • unique_ptr ishlatish mumkin bo'lgan hollarda auto_ptr ishlatmagan ma'qul.

Aqlli ko'rsatkichlar bo'yicha gaplashadiganimiz shulardan iborat edi. Mavzuga doir ko'p ma'lumotlar aytilmay o'tib ketilgan bo'lishi mumkin, sizdan faqat shu manba bilan kifoyalanib qolmasdan mavzu bo'yicha izlanishingizni so'rab qolaman. Agar biror savol/maqola bo'yicha qo'shimcha ma'lumot kiritmoqchi bo'lsangiz, https://t.me/cppuz guruhida muhokama qilishingiz mumkin.

Foydalanilgan manbalar:

  1. https://www.geeksforgeeks.org/auto_ptr-unique_ptr-shared_ptr-weak_ptr-2/
  2. https://www.fluentcpp.com/2018/12/25/free-ebook-smart-pointers/

Manba:

Umumiy Dasturlash
C++ da havolalar va ko’rsatkichlar, xotira menejerligini o’rganamiz.