Pointerlar
Pointerlar C tilidagi qiyin bo'lgan mavzulardan biri hisoblanadi. Dastlab o'rganishda hamma qiynaladi. Shuning uchun tushunishmayapman deb o'ylasangiz bilingki bu tabiiy hol.
Umid qilamanki, bu maqoladan keyin bu mavzuda sizda muammo qolmaydi.
O'zgaruvchi (variable)
Dastlab o'zgaruvchi tushunchasini yodga olaylik. Bundan keyin variable deb ketaman sababi, dasturchilar orasida birov o'zgaruvchi demaydi.
Hop, xullas, noldan tushuntiraman, tassavvur qiling sizda bitta quti bor va qutini ustida yozuvi bor. Pastdagi rasmdagi kabi:
Bu qutiga xohlagan vaqtingizda turlicha narsalarni sola olasiz. Lekin, qutilar ham turli narsalar uchun mo'ljallangan bo'ladi to'grimi, masalan, setkali yoki to'rli qutiga siz don mahsulotlari masalan, guruch sola olmaysiz, solsangiz to'kilib ketadi.
Dasturlashda variable bu qutiga o'xshaydi, variableni nomi (name) bu quti ustidagi yozuv kabidir. A qutini ichidagi narsa esa bu variableni valuesi (qiymati, iltimos bundan keyin value deyishga odatlaning) hisoblanadi. Va aytganimdek qutilar malum bir narsalarni saqlash uchun mo'ljallangani kabi, dasturlashda ham variablelarni biz malum bir turdagi malumotlarni saqlash uchun ishlata olamiz xolos (C tilida). Masalan, butun sonlarni saqlagan variablemizda keyichalik kasr sonlarni saqlay olmaymiz.
Bu qutilarga biz har xil narsalarni har xil vaqtda sola olganimiz kabi, o'zgaruvchilar (variable) ni qiymatini(valuesini) ham xohlagan vaqtda o'zgartirsak bo'ladi.
Misol uchun:
- Sizda tuz solinadigan quti bor va unga siz tuz quti deb yozib qo'ygansiz
- Dastlab tuz qutini to'ldirdingiz, lekin bir haftadan keyin qarasangiz qutida yarim tuz qoladi.
Variablelar ham shunday vaziyatga ulardagi saqlangan qiymatlar o'zgarishi mumkin.
C tilida variable yaratish uchun data type variable nomi yoziladi:
int a = 9; // bu yerda a variable nomi va 9 esa o'zgaruvchining qiymati (valuesi)
Pointer tushunchasi
Ho'p biz bildikki, variablelar xotiradagi nomlangan joylar bo'lib biz u joylarda qiymat (malumot, masalan son) saqlay olarkanmiz.
Pointerlar esa bular ham o'zgaruvchilar (variablelar) faqat bular faqatgani qiymat sifatida xotira addresini saqlay olishadi.
Tushuntiraman, demak pointerlar ham qutilar faqat biz bu qutilarda xotira addresslarinigina saqlay olarkanmiz xolos. Rasmda tassavvur qilishiniz uchun address qutini tasvirlangan:
Demak, yanayam soddalashtirsam, pointer bu quti va bu qutini ichida boshqa quti qayerda joylashganligi addresi bor bo'ladi.
Masalan, sizda bitta mashina tuzatish uchun kerak bo'ladigan klyuchlar (uskunalar) saqlaydigan qutingiz bor. Va siz bu qutini qayerga qo'yganingizni unutib qo'ymaslik uchun uni manzili yozilgan qog'ozni oshxonadagi tokchada turgan qutiga qo'yib qo'ydingiz. Shunda o'sha address yozilgan qog'oz solingan qutini biz pointer deb atasak bo'ladi.
Dasturlash tilida aytadigan bo'lsak, pointer bu boshqa o'zgaruvchilarnining xotiradagi addresslarini o'zida saqlovchi o'zgaruvchidir.
Kod ham yozaylik a?
int a = 9; // bu o'zgaruvchi
int *pointerA; // bu o'zgaruvchi addresi saqlash uchun mo'ljallangan quti yani pointer variable, hozir bo'm bo'sh turibdi yani qiymati null
pointerA = &a; // bu yerda biz pointerA nomli pointerga a nomli o'zgaruvchimizni xotiradagi addresini beryapmiz. Demak hozir qutimizda a ni addressi bor.
Yuqoridagi koddan bir nechta narsalarni bilib olsak bo'ladi.
- Pointer yaratish uchun biz bizning pointer qanday malumot turini saqlayotgan variableni xotiradagi addresiga ishora qilishini yozishimiz kerakligi va "*" belgisi, keyin pointerga nom berishimiz kerak ekan.
- "&" belgisini o'zgaruvchi nomi oldidan qo'ysak o'sha o'zgaruvchini xotiradagi addressini olib berarkan
- Biz
pointerA = &a
deyish orqali, mening pointerA nomli pointer o'zgaruvchimga a variableni xotiradagi addressini bergin deyapmiz.
Pointerlarga qiymat berish
Pointerlar har qanday qiymatni olavermaydi, yani biz pointer variablega ixtiyoriy qiymatni yoki sonni tenglab keta olmaymiz. Pointerlar faqatgina boshqa o'zgaruvchining xotiradagi addressi, 0 yoki NULL qiymatlarni olishi mumkin.
Qiziqayotgandursiz NULL nima ekan deb? Qisqach malumot berib ketaman, mavzudan chetlashmasligimiz uchun, agar biz pointerga NULL qiymat bersak, bu bizning pointer xotiradan hech qanday yaroqli addressga ishora qilmayotganligini bildiradi.
Pointerga 0 berilganda ham xuddi NULL dek mano anglatadi yani pointerimizda hech qanday yaroqli address yo'q.
Endi keling bir qancha holatlarni ko'rib chiqaylik:
int a = 10;
int *ptrA = &a; // pointerga xotira addressi berildi, bu to'g'ri
int *ptrB = 0; // pointerga 0 yani yaroqsiz address berildi, bu ham to'gri
int *ptrC = NULL // pointerga NULL berildi bu ham to'g'ri
int d = 13;
int e = 14;
int *ptrD = &d;
int *ptrE = &e;
ptrD = ptrE; // bu ham to'gri chunki ptrE o'zida address saqlaydi va biz ptrD ga ptrE ishora qilib turgan addressni beryapmiz
int f = 10;
int *ptrF;
ptrF = &f; // bu ham to'gri (etibor bering biz yulduzcha qo'ymadik
//sababi yuqoridagi holatlarda pointer elon qilayotgan vaqtimizda qiymat berayotgandik
//hozir esa pointer ozgaruvchi yaratilgandan keyin bermoqdamiz, bunda * shartmas)
Yuqorida tog'ri holatlarni ko'rdik, endi xato ya'ni kompiler bizga xato qaytaradigan compile qila olmaydigan vaziyatlarni ham ko'raylik:
int *ptrA = 12; // bu xato, pointerga son berib bo'lmaydi
int a = 10;
int *ptrB = a; // bu ham xato, bu vaziyatda ham pointerga a ning qiymati berilmoqda addressi emas
Hozir biz pointerda boshqa o'zgaruvchilarning xotiradagi addressini saqlashni o'rgandik. Keling endi, qanday qilib bu pointerdan foydalanishimiz mumkinligini ko'rib chiqamiz.
- Pointer yordamida o'sha o'zgaruvchini xotiradagi addressida saqlanayotgan qiymatni olsak bo'ladi, buning uchun pointer oldiga "*" beligisini qo'yish kifoya. Bu belgi indirection operator deb ham ataladi.
int a = 9;
int *ptrA = &a;
printf("a ning xotiradagi addressi: %d", ptrA);
printf("a ning qiymati: %d", *ptrA); // shu yerda biz ptrAdagi addressni xotirada saqlayotgan qiymatini olyapmiz
- Pointer yordamida o'sha o'zgaruvchini xotiradagi addressida saqlanayotgan qiymatni o'zgartirsak bo'ladi:
int a = 9; // o'zgaruvchi
int *ptrA = &a; // a ni pointeri
*ptrA = 13; // bu yerda a ni qiymatini 13 ga o'zgartirdik
printf("a ning qiymati: %d", *ptrA); // shu yerda biz ptrA dagi addressni xotirada saqlayotgan qiymatini olyapmiz
Xulosa qilsak, ixtiyoriy pointerni boshqa o'zgaruvchini xotira addressini saqlashligini bildik, endi ana shu addressda nima turgani ko'rish uchun, olish uchun yoki o'zgartirish uchun o'sha pointer oldiga "*" belgisini qo'yishimiz kerak ekan.
Pointerlarni funksiyalarga argument sifatiada berish
Biz odatda dastur tuzayotganimizda dasturimizni funksiyalarga bo'lib bir qancha qulayliklarga erishamiz. Bu haqida boshqa maqolada yozaman, aks holda o'tlab ketib qolamiz :).
Ho'p, funksiya bilan ishlayotganda odatda funksiyalar parametrlar qabul qiladi to'g'rimi, masalan, quyidagi funksiyani ko'raylik:
int add(int a, int b){
return a + b;
}
Bu funksiyani vazifasi 2 ta sonni bir-biriga qo'shib natijasini qaytarish. Shunda biz a va b larni funksiya parametri sifatida berib yuboryapmiz. Qachondir funksiyani chqairganimizda, biz unga parametr sifatida bergan qiymatlar o'zi bormaydi, o'rniga kopiyasi yaratilib o'shalarni yuboriladi. :::infoBu dasturlashda pass by value ya'ni qiymatni uzatish deyiladi. :::
int main(){
int a = 10;
int b = 13;
int natija = add(a,b);
return 0;
}
Yuqoridagi kodda a va b larni kopiyasi xotirada yaratiladi va add funksiyasi foydalanishi uchun beriladi. Bu nimaga sabab bo'ladi? Ha topdingiz, xotiradan unumsiz foydalanish ya'ni 2 ta o'zgaruvchimizni kopiyasi paydo bo'lib ko'proq joy egalashlashga sabab bo'ladi. Yanachi? Ha yana topdingiz, biz original yani bizni misolda main functionni ichidagi a va b larni add funksiyasi ichida qiymatini o'zgartira olmaymiz, sababi funksiyaga ular emas balki ularni kopiyasi yuborilgan.
Buni optimallashtirish uchun yechim, o'zgaruvchilarni o'zini emas balki xotiradagi addressini berib yuborishdir. Buni qanday qilamiz, albatta pointerlar yordamida.
Ho'p, funksiyaga pointerni parametr sifatida berishni afzalliklarini bildik, endi qanday qilamiz buni C da deyotganlarga:
int add(int *a, int *b){
return *a + *b;
}
int main(){
int a = 2;
int b = 9
int natija = add(&a, &b); // funskiya pointer qabul qilgani uchun biz pointer qabul qila oladigan qiymat yani addressni beryapmiz
return 0;
}
Funksiya parametri sifatida array berish
Array nima edi? Aha, qisqacha aytaman arrayga o'tlab ketmaslik uchun, array bu malum bir turdagi data typelar to'plami. Boshqa savol beraman, array xotirada qanaqa saqlanadi? Qoyil, topdingiz, array elementlari xotirada ketma-ket joylashgan bloklarda saqlanadi. Rasmga qarang:
Yuqoridagi rasmda array elementlarini ketma ket joylashganini va elemntlari indekslari noldan boshlanganini ko'rish mumkin. Yuqorisida esa xotiradagi addressi tasvirlangan.
Ho'p, manga nima qizig'i bor arrayni, axir pointerlarni o'rganyapmanku deyapsizmi? Mayli aytaman, quyidagi kodga qarang:
int raqamlar[4] = {1,2,3,4};
Yuqorida array o'zgaruvchisi yaratilishi va qiymat berilishi ko'rsatilgan. Shu yerda array nomi - bu arrayning birinchi elementini xotiradagi addresiga ishora qiladi, boshqacha qilib aytganda, u pointerdir. Demak, quyidagi kod to'g'ri bo'ladi:
int raqamlar[4] = {1,2,3,4};
int *ptrA = raqamlar; // raqamlar array birinch elementi 1 ni xotirada saqlangan addressini beradi
Arraylarni funksiyaga quyidagicha parametr sifatida bersak bo'ladi:
// bu yerda qanday qilib parametr sifatida berishimiz ko'rsatilgan
void qanaqadurArrayOladiganFunksiya(int nums[]){
// nimalardur yozilgan
}
int main(){
int nums[4] = {1,2,3,4};
qanaqadurArrayOladiganFunksiya(nums); // demak arrayni berayotganda shunchaki nomini berish kifoya qilarkan
return 0;
}
Ho'p, tepadagi konsepsiyaparimizga qaytaylik, pass by value bu qiymatni o'zi yuborilishi va funksiya ichida o'zgartirish originali o'zgarishiga sabab bolmasligi bo'lsa pass by reference bu aksi, funksiya ichida o'zgartirsak orginali ham o'zgarishidir.
Pass by reference ga erishish uchun C da pointerdan foydalanamiz dedim. Array nomini esa array birinchi elementiga pointer dedim, demak bundan chiqdi, funksiyaga arraylarimiz pass by reference shaklida yuboriladi. Yani funksiya ichidagi arrayga qilingan o'zgairhslar funksiya tashqarisida ham ko'rinadi. Masalan:
void kvadratgaOshir(int nums[]){
for(int i=0; i<4; i++){
nums[i] = nums[i]*nums[i];
}
}
int main() {
int nums[4] = {1,2,3,4};
kvadratgaOshir(nums);
for(int i=0; i<4; i++){
printf("%d ", nums[i]);
}
// output: 1 4 9 16
return 0;
}
Ushbu kodda kvadratgaOshir funksiyasi chaqirilgandan keyin bizni arrayga main functionda kirsak uni qiymatlari o'zgargan bo'ladi.
Pointer arifmetikasi
Pointerlar mavzusida array bilan aloqadorligini o'rganayotganimizda pointer arifmetikasi degan tushunchaga duch kelamiz. Bu nima degani? Bu pointerga boshqa son qo'shilganda yoki ayrilganda nima bo'lishidir. Buni alohida o'rganilishini sababi, pointer bu xotira addresi yani u odatiy son emas, shuning uchun unga qo'shish va ayrish boshqacha ishlaydi.
Gapni qisqa qilay, qo'shish qanday bo'ladi: Tasavvur qiling sizda int tipidagi pointer bor. Uni print qilib chiqarsangiz quyidagi narsa ekranga chiqsin.
int a = 19;
int *ptrA = &a;
printf("%d", ptrA); // output: 192456420
Agar biz mana shu ptrA pointerimizga 1 sonini qo'shsak, u bizda chiqqan output 192456420 ga 1 qo'shmaydi. U pointerni tipini bizda hozir int xotiradan olgan hajmi qancha bo'lsa bizni 1 sonimizni shunga ko'paytrib keyin qo'shadi ya'ni 1*4 (byte):
int a = 19;
int *ptrA = &a;
printf("%d", ptrA); // output: 192456420
printf("%d", ptrA+1); // output: 192456424
printf("%d", ptrA+2); // output: 192456428
printf("%d", ptrA+3); // output: 192456432
Bu bizga nimaga kerak? Array xotirada qanaqa joylashishi yodingizdami, array xotiradan ketma ket joylarni oladi, masalan:
int nums[3] = {1,2,3};
printf("%d", &nums[0]); // output: 256980102
printf("%d", &nums[1]); // output: 256980106
printf("%d", &nums[2]); // output: 256980110
Va arrayni nomi o'sha arraydagi birinchi elementni xotiradagi addresiga ishora qiladi, boshqacha qilib aytganda array nomini, bizni misolada nums ni biz shu arrayning birinchi elementiga pointer deb atasak bo'ladi, quyida arrayning birinchi elementini ham, array nomini o'zini ham chiqarganimizda bir xil narsa ekranga chiqadi:
int nums[3] = {1,2,3};
printf("%d", &nums[0]); // output: 256980102
printf("%d", nums); // output: 256980102
Pointer bilan arrayni bog'liqligiga keladigan bo'lsak, array nomini biz birinchi elementiga pointer deb atadik. Ho'p pointer arifmetikasiga ko'ra biz agar shu array nomiga 1 sonini qo'shadigan bo'lsak, u xotira addresiga 4 bayt qo'shadi, a yangi addresda esa arrayni ikkinchi elementi turgan edi.
int nums[3] = {1,2,3};
printf("%d", &nums[0]); // output: 256980102
printf("%d", &nums[1]); // output: 256980106
printf("%d", &nums[2]); // output: 256980110
printf("%d", nums); // output: 256980102
printf("%d", nums + 1); // output: 256980106
Demak biz pointer arifmetikasi yordamida arrayning istalgan elementiga kira olaman, bu xuddi arrayni indekslari kabi orqali olishga o'xshaydi. Agar nums+2 arrayni 2-indeksdagi xotiradagi addresiga ishora qilsa demak, ushbu addresda qanaqa qiymat borligini ko'rish uchun pointer ni oldiga * belgisini qo'ygan kabi ish tutishimiz kerak:
int nums[3] = {1,2,3};
printf("%d", nums); // output: 256980102
printf("%d", nums + 1); // output: 256980106
printf("%d", *nums); // output: 1
printf("%d", *(nums + 1)); // output: 2
Quyidagi kod pointer orqali arrayni barcha elementlarini chop etishga misol bo'la oladi:
int nums[3] = {1,2,3};
for(int i = 0; i<3; i++){
printf("%d ", *(nums + 1));
}
//output: 1 2 3
Const keywordining foydasi
const keywordini vazifasi nima edi, ha alabtta, biz agar o'zgaruvchini conts keyowordi bilan elon qilsak uni qiymatini keyinchalik o'zgartira olmaymiz, yani variableni konstanta qilib qo'yadi. agar o'zgartirsak, compiler error berardi.
Biz tepada pointerni foydalari haqida gaplashganda 2 ta foydasini aytib o'tdik, biri xotiradan unumdor foydalanish, ikkinchisi pass by reference ni ta'minlash edi.
Endi, men xotiradan unumli foydalanmoqchiman funksiyaga pointer berib yuborib, lekin man yana shuni xohlaymanki, man shu pointerni qiymatini funksiya ichida o'zgartira olmay. Bu holat dasturlashda read only yani faqat o'qish uchun foydalanish deyiladi.
Bu xususiyatga biz const keywordi yrdamida erishamiz, agar biz pointerni funksiyaga const keywordi bilan bersak, bu pointer qiymatini funksiya ichida o'zgartirmoqchi bo'lganimizida compiler xato beradi. Kodda yaxshiroq tushunasizlar:
void change(const int *ptrA){ // pointer oldidan const keyowrdi bilan berish
*ptrA = 12;
}
int main() {
int a = 10;
int ptrA = &a;
change(ptrA);
return 0;
}
Ushbu kodni run qilsak, compile error olamiz, sababi shu biz const keyword bilan pointer oldik, bu degani buni qiymatini o'zgartira olmaymiz.
Arraylarni ham const keywordi bilan bersak, funksiya ichida o'zgartira olmaymiz. Quyidagi kod ham compile error beradi:
void change(const int *nums){ // pointer oldidan const keyowrdi bilan berish
*nums = 12;
}
int main() {
int nums[3] = {1,2,3};
change(nums);
return 0;
}
Pointerga oid bilish kerak bo'lgan ma'lumotlar shular edi. Umid qilamanki, foydali bo'ldi. O'ziyam vikipediya bo'lib ketdi 😁.
Quyida ushbu maqola bo'yicha tayyorlangan video: