Muallif
June 4, 2024
102Maqolada 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:
cpp
Копировать код
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<typename T> 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 << "Test obyekti yaratildi!\n"; val = data; } int getVal(){return val;} ~Test(){ cout << "Test obyekti o'chirildi\n"; } };
Endi yuqoridagi ScopedPointer dan foydalanib ko'ramiz:
{ ScopedPointer<Test> ptr(new Test(100)); cout << "Test obyektining datasi: " << ptr->getVal() << endl; }
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 <memory> kutubxonasidagi std nomlar fazosida joylashgan.
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<Test> data(new Test(100)); cout << data->getVal() << endl;
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<Test> data(new Test(100)); //xatolik: call to deleted constructor std::unique_ptr<Test> 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<Test> data(new Test(100)); std::unique_ptr<Test> ptr2 = std::move(data); std::cout << "data ushlab turgan ko'rsatkich manzili: " << data.get() << "\nptr2 ushlab turgan ko'rsatkich manzili: " << ptr2.get() << std::endl;
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.
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<Test> data(new Test(100)); std::shared_ptr<Test> ptr2 = data; std::cout << "data ushlab turgan ko'rsatkich manzili: " << data.get() << std::endl << "ptr2 ushlab turgan ko'rsatkich manzili: " << ptr2.get() << std::endl; std::cout << "ko'rsatkichga egalik soni: " << ptr2.use_count() << std::endl;
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<Test> shared_e1(e1); std::shared_ptr<Test> 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.
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<T> qaytaradi:
std::shared_ptr<Test> data(new Test(100)); std::shared_ptr<Test> ptr2 = data; //obyektga 2 ta shared_ptr egalik qilyapti std::weak_ptr<Test> wk = ptr2; std::shared_ptr<Test> temp = wk.lock(); std::cout << "Obyektga egalik qilayotgan shared_ptrlar: " << temp.use_count() << std::endl; //Obyektga egalik qilayotgan shared_ptrlar: 3 //shared_ptr::reset metodi yordamida shared_ptr larni //obyektga egalik qilishdan mahrum qilamiz: data.reset(); ptr2.reset(); temp.reset(); //obyektga oxirgi egalik qilib turgan temp //bo'shatildi, obyekt esa xotiradan o'chirildi temp = wk.lock(); if(temp) std::cout << "weak_ptr ko'rsatib turgan manzil hali ham aktual!\n"; //if dagi shart bajarilmaydi
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 ;)
Shunday hollarda ulardan birini weak_ptr qilish orqali avtomatik o'chishini ta'minlash mumkin.
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.
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<int> p1(new int(42)); std::auto_ptr<int> 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:
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.