Përditësimi i fundit: 27.05.2017

Treguesit janë objekte vlerat e të cilave janë adresat e objekteve të tjera (ndryshore, konstante, tregues) ose funksione. Treguesit janë një komponent integral për menaxhimin e kujtesës në gjuhën C.

Për të përcaktuar një tregues, duhet të specifikoni llojin e objektit tek i cili tregon treguesi dhe yllin *. Për shembull, le të përcaktojmë një tregues për një objekt të tipit int:

Ndërsa treguesi nuk i referohet asnjë objekti. Tani le t'i caktojmë adresën e ndryshores:

Int x = 10; // definoni një variabël int *p; // përcaktoni treguesin p = // treguesi merr adresën e ndryshores

Një tregues ruan adresën e një objekti në kujtesën e kompjuterit. Dhe për të marrë adresën, operacioni & zbatohet në ndryshore. Ky operacion zbatohet vetëm për objektet që ruhen në memorien e kompjuterit, domethënë për variablat dhe elementët e grupit.

Ajo që është e rëndësishme është që ndryshorja x të jetë e tipit int, dhe treguesi që tregon adresën e tij është gjithashtu i tipit int. Kjo do të thotë, duhet të ketë një përputhje sipas llojit.

Cila është saktësisht adresa e ndryshores x? Për të printuar vlerën e një treguesi, mund të përdorni specifikuesin special %p:

#përfshi int main(void) ( int x = 10; int *p; p = printf ("%p \n", p); // 0060FEA8 kthen 0; )

Në rastin tim, adresa e makinës së ndryshores x është 0060FEA8. Por në çdo rast individual adresa mund të jetë e ndryshme. Adresa në fakt përfaqëson një vlerë të plotë të shprehur në format heksadecimal.

Kjo do të thotë, në kujtesën e kompjuterit ekziston një adresë 0x0060FEA8, ku ndodhet ndryshorja x. Meqenëse x përfaqëson një int, ai do të zërë 4 bajt të ardhshëm në shumicën e arkitekturave (madhësia e kujtesës për një int mund të ndryshojë në arkitektura specifike). Kështu, një ndryshore e tipit int do të zërë në mënyrë sekuenciale qelizat e memories me adresat 0x0060FEA8, 0x0060FEA9, 0x0060FEAA, 0x0060FEAB.

Dhe treguesi p do t'i referohet adresës ku ndodhet ndryshorja x, domethënë adresa 0x0060FEA8.

Por meqenëse treguesi ruan një adresë, ne mund ta përdorim këtë adresë për të marrë vlerën e ruajtur atje, domethënë vlerën e ndryshores x. Për ta bërë këtë, përdorni operacionin * ose operacionin e çreferencimit, domethënë operacionin që përdoret kur përcaktoni një tregues. Rezultati i këtij operacioni është gjithmonë objekti i drejtuar nga treguesi. Le të zbatojmë këtë operacion dhe të marrim vlerën e ndryshores x:

#përfshi int main(void) ( int x = 10; int *p; p = printf ("Adresa = %p \n", p); printf ("x = %d \n", *p); kthimi 0; )

Dalja e konsolës:

Adresa = 0060FEA8 x = 10

Duke përdorur vlerën që rezulton nga operacioni i dereferencimit, ne mund ta caktojmë atë në një variabël tjetër:

Int x = 10; int *p = int y = *p; printf("x = %d \n", y); // 10

Dhe gjithashtu duke përdorur një tregues, ne mund të ndryshojmë vlerën në adresën që është ruajtur në tregues:

Int x = 10; int *p = *p = 45; printf("x = %d \n", x); // 45

Meqenëse ndryshorja x ndodhet në adresën e treguar nga treguesi, vlera e saj do të ndryshojë në përputhje me rrethanat.

Le të krijojmë disa tregues të tjerë:

#përfshi int main(void) ( char c = "N"; int d = 10; short s = 2; char *pc = // merrni adresën e ndryshores c të tipit char int *pd = // merrni adresën e ndryshores d të tipit int short *ps = // merrni adresën e ndryshores s të tipit short printf("Variable c: adresa=%p \t vlera=%c \n", pc, *pc); printf("Variable d: adresa=%p \t vlera=% d \n", pd, *pd); printf("Variable s: adresa=%p \t vlera=%hd \n", ps, *ps); kthej 0; )

Në rastin tim, unë do të marr daljen e mëposhtme të konsolës:

Variabli c: adresa=0060FEA3 vlera=N Variabli d: adresa=0060FE9C vlera=10 Ndryshorja s: adresa=0060FE9A vlera=2

Ju mund të shihni nga adresat se variablat shpesh ndodhen afër në memorie, por jo domosdoshmërisht në rendin në të cilin janë përcaktuar në tekstin e programit.

Përditësimi i fundit: 27.05.2017

Treguesit në gjuhën C mbështesin një sërë operacionesh: caktimin, marrjen e adresës së një treguesi, marrjen e një vlere nga një tregues, disa operacione aritmetike dhe operacione krahasimi.

Detyrë

Një treguesi mund t'i caktohet ose adresa e një objekti të të njëjtit lloj, vlera e një treguesi tjetër ose një konstante NULL.

Caktimi i një adrese për një tregues është diskutuar tashmë në temën e mëparshme. Për të marrë adresën e një objekti, përdorni funksionin &:

Int a = 10; int *pa = // pointeri pa ruan adresën e ndryshores a

Për më tepër, treguesi dhe ndryshorja duhet të kenë të njëjtin lloj, në këtë rast int.

Caktimi i një treguesi në një tregues tjetër:

#përfshi int main(void) ( int a = 10; int b = 2; int *pa = int *pb = printf("Variable a: adresa=%p \t vlera=%d \n", pa, *pa); printf("Variable b: adresa=%p \t vlera=%d \n", pb, *pb; pa = pb; // tani treguesi pa ruan adresën e ndryshores b printf("Variabla b: adresa= %p \ t vlera=%d \n", pa, *pa); kthej 0;)

Kur një tregues i caktohet një treguesi tjetër, treguesi i parë në të vërtetë fillon gjithashtu të tregojë në të njëjtën adresë që tregon treguesi i dytë.

Nëse nuk duam që treguesi të tregojë një adresë specifike, mund t'i caktojmë një vlerë null të kushtëzuar duke përdorur konstantën NULL, e cila përcaktohet në skedarin e kokës stdio.h:

Int *pa = NULL;

Dereferencimi i treguesit

Operacioni i çreferencimit të një treguesi në formën *pointer_name ju lejon të merrni një objekt në adresën që ruhet në tregues.

#përfshi int main(void) ( int a = 10; int *pa = int *pb = pa; *pa = 25; printf("Vlera në treguesin pa: %d \n", *pa); // 25 printf(" Vlera në treguesin pb: %d \n", *pb); // 25 printf("Vlera e ndryshores a: %d \n", a); // 25 kthen 0; )

Përmes shprehjes *pa mund të marrim vlerën në adresën që ruhet në treguesin pa, dhe përmes një shprehjeje si *pa = value mund të fusim një vlerë të re në këtë adresë.

Dhe meqenëse në këtë rast treguesi pa tregon variablin a, atëherë kur vlera në adresën e treguar nga treguesi ndryshon, vlera e ndryshores a do të ndryshojë gjithashtu.

Adresa e treguesit

Një tregues ruan adresën e një ndryshoreje dhe nga kjo adresë mund të marrim vlerën e asaj ndryshore. Por përveç kësaj, një tregues, si çdo variabël, në vetvete ka një adresë ku ndodhet në memorie. Kjo adresë mund të merret gjithashtu duke përdorur funksionin &:

Int a = 10; int *pa = printf("adresa e pointerit=%p \n", &pa); // adresa e treguesit printf("adresa e ruajtur në pointer=%p \n", pa); // adresa që ruhet në pointer është adresa e ndryshores a printf("value on pointer=%d \n", *pa); // vlera në adresën në tregues - vlera e ndryshores a

Operacionet Krahasuese

Operacionet e krahasimit > , >= , mund të aplikohen te treguesit< , <= ,== , != . Операции сравнения применяются только к указателям одного типа и константе NULL . Для сравнения используются номера адресов:

Int a = 10; int b = 20; int *pa = int *pb = if(pa > pb) printf("pa (%p) është më e madhe se pb (%p) \n", pa, pb); else printf("pa (%p) është më pak ose e barabartë pb (%p) \n", pa, pb);

Dalja e konsolës në rastin tim:

Pa (0060FEA4) është më i madh se pb (0060FEA0)

Cast

Ndonjëherë ju duhet të caktoni një tregues të një lloji në vlerën e një treguesi të një lloji tjetër. Në këtë rast, duhet të kryeni një operacion të konvertimit të tipit:

Char c = "N"; char *pc = int *pd = (int *)pc; printf("pc=%p \n", pc); printf("pd=%p\n", pd);

Një tregues është një variabël vlera e të cilit është adresa e një lokacioni memorie. Kjo do të thotë, treguesi i referohet një blloku të dhënash nga një zonë memorie dhe në fillimin e saj. Një tregues mund t'i referohet një ndryshoreje ose një funksioni. Për ta bërë këtë, ju duhet të dini adresën e ndryshores ose funksionit. Pra, për të gjetur adresën e një ndryshoreje specifike në C++, ekziston një operacion unar për marrjen e adresës & . Ky operacion rimerr adresën e variablave të deklaruar në mënyrë që ta caktojë atë në një tregues.

Treguesit përdoren për të kaluar të dhënat me referencë, gjë që përshpejton shumë procesin e përpunimit të këtyre të dhënave (nëse sasia e të dhënave është e madhe), pasi ato nuk kanë nevojë të kopjohen, si kur kalohen sipas vlerës, domethënë duke përdorur variablin emri. Treguesit përdoren kryesisht për të organizuar shpërndarjen dinamike të memories, për shembull, kur deklaroni një grup, nuk do të duhet të kufizohet në madhësi. Në fund të fundit, programuesi nuk mund të dijë paraprakisht se çfarë madhësie i nevojitet një përdoruesi të caktuar; në këtë rast, përdoret shpërndarja dinamike e memories për grupin. Çdo tregues duhet të deklarohet përpara përdorimit, ashtu si çdo variabël.

//deklarata e treguesit /*lloji i të dhënave*/ * /*emri i treguesit*/;

Parimi i deklarimit të pointerëve është i njëjtë me parimin e deklarimit të variablave. I vetmi ndryshim është se emri paraprihet nga një yll *. Vizualisht, treguesit ndryshojnë nga variablat me vetëm një simbol. Gjatë deklarimit të pointerëve, kompajleri shpërndan disa bajt memorie, në varësi të llojit të të dhënave të alokuara për të ruajtur disa informacione në memorie. Për të marrë vlerën e shkruar në një zonë të cilës i referohet treguesi, duhet të përdorni funksionin e çreferencimit të treguesit *. Është e nevojshme të vendosim një yll përpara emrit dhe do të kemi akses në vlerën e treguesit. Le të zhvillojmë një program që do të përdorë tregues.

// pointer1.cpp: Përcakton pikën hyrëse për aplikacionin e konsolës. #include "stdafx.h" #include << "&var = " << &var << endl;// адрес переменной var содержащийся в памяти, извлечённый операцией взятия адреса cout << "ptrvar = " << ptrvar << endl;// адрес переменной var, является значением указателя ptrvar cout << "var = " << var << endl; // значение в переменной var cout << "*ptrvar = " << *ptrvar << endl; // вывод значения содержащегося в переменной var через указатель, операцией разименования указателя system("pause"); return 0; }

// kodi Kodi::Blloqe

// Kodi i Dev-C++

// pointer1.cpp: Përcakton pikën hyrëse për aplikacionin e konsolës. #përfshi duke përdorur hapësirën e emrave std; int main(int argc, char* argv) ( int var = 123; // inicializoni variablin var me numrin 123 int *ptrvar = // pointer në variablin var (i caktuar adresën e variablës treguesit) cout<< "&var = " << &var << endl;// адрес переменной var содержащийся в памяти, извлечённый операцией взятия адреса cout << "ptrvar = " << ptrvar << endl;// адрес переменной var, является значением указателя ptrvar cout << "var = " << var << endl; // значение в переменной var cout << "*ptrvar = " << *ptrvar << endl; // вывод значения содержащегося в переменной var через указатель, операцией разименования указателя return 0; }

Në rreshtin 10, treguesi ptrvar deklarohet dhe inicializohet me adresën e ndryshores var. Së pari thjesht mund të deklaroni treguesin dhe më pas ta inicializoni atë, pastaj do të kishte dy rreshta:

Int *ptrvar; // deklarimi i treguesit ptrvar = // inicializimi i treguesit

Në programim, është zakon të shtoni prefiksin ptr në emrin e një treguesi, në këtë mënyrë ju merrni një emër kuptimplotë për treguesin dhe një tregues i tillë nuk mund të ngatërrohet me një ndryshore të zakonshme. Rezultati i programit (shih Figurën 1).

&var = 0x22ff08 ptrvar = 0x22ff08 var = 123 *ptrvar = 123 Shtypni çdo tast për të vazhduar. . .

Figura 1 - Treguesit në C++

Pra, programi e tregoi këtë rreshtat 11 dhe 12 nxirrni një adresë identike, domethënë adresën e ndryshores var, e cila gjendet në treguesin ptrvar. Ndërsa operacioni i dereferencimit të treguesit *ptrvar siguron qasje në vlerën të cilës i referohet treguesi.

Treguesit mund të krahasohen jo vetëm për barazi ose pabarazi, sepse adresat mund të jenë më të vogla ose më të mëdha në raport me njëra-tjetrën. Le të zhvillojmë një program që do të krahasojë adresat e treguesve.

"stdafx.h" #include << "var1 = " << var1 << endl; cout << "var2 = " << var2 << endl; cout << "ptrvar1 = " << ptrvar1 << endl; cout << "ptrvar2 = " << ptrvar2 << endl; if (ptrvar1 > << "ptrvar1 >ptrvar2"<< endl; if (*ptrvar1 > << "*ptrvar1 >*ptrvar2"<< endl; system("pause"); return 0; }

// kodi Kodi::Blloqe

// Kodi i Dev-C++

// pointer.cpp: Përcakton pikën hyrëse për aplikacionin e konsolës. #përfshi duke përdorur namespace std; int main(int argc, char* argv) ( int var1 = 123; // inicializimi i variablës var1 me numrin 123 int var2 = 99; // inicializimi i ndryshores var2 me numrin 99 int *ptrvar1 = // treguesi në ndryshorja var1 int *ptrvar2 = / / treguesi në variablin var2 cout<< "var1 = " << var1 << endl; cout << "var2 = " << var2 << endl; cout << "ptrvar1 = " << ptrvar1 << endl; cout << "ptrvar2 = " << ptrvar2 << endl; if (ptrvar1 >ptrvar2) // krahasoni vlerat e treguesve, domethënë adresat e variablave cout<< "ptrvar1 >ptrvar2"<< endl; if (*ptrvar1 >*ptrvar2) // krahasoni vlerat e variablave të referuara nga treguesit cout<< "*ptrvar1 >*ptrvar2"<< endl; return 0; }

Rezultati i programit është paraqitur në Figurën 2.

Var1 = 123 var2 = 99 ptrvar1 = 0x22ff04 ptrvar2 = 0x22ff00 ptrvar1 > ptrvar2 *ptrvar1 > *ptrvar2 Për të vazhduar, shtypni çdo tast. . .

Figura 2 - Treguesit në C++

Në rastin e parë, ne krahasuam adresat e variablave dhe, për më tepër, adresa e ndryshores së dytë është gjithmonë më e vogël se adresa e ndryshores së parë. Sa herë që niset programi, ndahen adresa të ndryshme. Në rastin e dytë, ne krahasuam vlerat e këtyre variablave duke përdorur një operacion dereferencimi të treguesit.

Nga veprimet aritmetike, veprimet më të përdorura janë mbledhja, zbritja, shtimi dhe zvogëlimi, pasi me ndihmën e këtyre veprimeve, për shembull në vargje, llogaritet adresa e elementit pasardhës.

Treguesit për tregues

Treguesit mund të tregojnë tregues të tjerë. Në këtë rast, qelizat e memories të cilave do t'u referohen treguesit e parë nuk do të përmbajnë vlerat, por adresat e treguesve të dytë. Numri i personazheve * kur deklaron një tregues, tregon rendin e treguesit. Për të hyrë në vlerën e referuar nga treguesi, duhet të çreferencohet numri i duhur i herë. Le të zhvillojmë një program që do të kryejë disa operacione me tregues të rendit më të lartë se i pari.

duke përdorur hapësirën e emrave std; int _tmain(int argc, _TCHAR* argv) ( int var = 123; // inicializoni variablin var me numrin 123 int *ptrvar = // tregues në një variabël var int **ptr_ptrvar = // tregues në një tregues në një variabli var int *** ptr_ptr_ptrvar = &ptr_ptrvar;cout<< " var\t\t= " << var << endl; cout << " *ptrvar\t= " << *ptrvar << endl; cout << " **ptr_ptrvar = " << **ptr_ptrvar << endl; // два раза разименовываем указатель, так как он второго порядка cout << " ***ptr_ptrvar = " << ***ptr_ptr_ptrvar << endl; // указатель третьего порядка cout << "\n ***ptr_ptr_ptrvar ->**ptr_ptrvar -> *ptrvar -> var -> "<< var << endl; cout << "\t " << &ptr_ptr_ptrvar<< " -> " << " " << &ptr_ptrvar << " ->" << &ptrvar << " -> " << &var << " -> " << var << endl; system("pause"); return 0; }

// kodi Kodi::Blloqe

// Kodi i Dev-C++

// pointer.cpp: Përcakton pikën hyrëse për aplikacionin e konsolës. #përfshi duke përdorur hapësirën e emrave std; int main() ( int var = 123; // inicializoni variablin var me numrin 123 int *ptrvar = // tregues për variablin var int **ptr_ptrvar = // tregues për treguesin në variablin var int *** ptr_ptr_ptrvar = &ptr_ptrvar; cout<< " var\t\t= " << var << endl; cout << " *ptrvar\t= " << *ptrvar << endl; cout << " **ptr_ptrvar = " << **ptr_ptrvar << endl; // два раза разименовываем указатель, так как он второго порядка cout << " ***ptr_ptrvar = " << ***ptr_ptr_ptrvar << endl; // указатель третьего порядка cout << "\n ***ptr_ptr_ptrvar ->**ptr_ptrvar -> *ptrvar -> var -> "<< var << endl; cout << "\t " << &ptr_ptr_ptrvar<< " -> " << " " << &ptr_ptrvar << " ->" << &ptrvar << " -> " << &var << " -> " << var << endl; return 0; }

Figura 3 tregon rezultatin e programit.

Var = 123 *ptrvar = 123 **ptr_ptrvar = 123 ***ptr_ptrvar = 123 ***ptr_ptr_ptrvar -> **ptr_ptrvar -> *ptrvar -> var -> 123 0x22ff00 -> 0x22ff2ff04 -> 0x22ff2ff04 -> 0x22ff2ff04 ->2ff04 -> 2ff Për të vazhduar, shtypni çdo buton. . .

Figura 3 - Treguesit në C++

Ky program vërteton faktin se për të marrë një vlerë, numri i referencave të treguesit duhet të përputhet me rendin e tij. Logjika e çreferencimit me n-fish është se programi përsëritet në mënyrë sekuenciale përmes adresave të të gjithë treguesve deri në variablin që përmban vlerën. Programi tregon zbatimin e një treguesi të rendit të tretë. Dhe nëse, duke përdorur një tregues të tillë (rendi i tretë) është e nevojshme të merret vlera të cilës i referohet, ndërmerren 4 hapa:

  1. sipas vlerës së treguesit të rendit të tretë, merrni adresën e treguesit të rendit të dytë;
  2. nga vlera e treguesit të rendit të dytë, merrni adresën e treguesit të rendit të parë;
  3. me vlerën e treguesit të rendit të parë, merrni adresën e ndryshores;
  4. nga adresa e një ndryshoreje, hyni në vlerën e saj.

Këto katër veprime janë paraqitur në figurën 3 (dy rreshtat e parafundit). Rreshti i sipërm tregon emrat e treguesve, dhe rreshti i poshtëm adresat e tyre.

Figura 4 tregon një diagram për çreferencimin e një treguesi të rendit të tretë nga programi i lartë. Çështja është se treguesit janë të lidhur me njëri-tjetrin përmes adresave të tyre. Për më tepër, për shembull, për treguesin ptr_ptrvar ky numër 0015FDB4 është adresa, dhe për treguesin ptr_ptr_ptrvar i njëjti numër është vlera.

Figura 4 - Treguesit në C++

Kështu bëhet zinxhiri i treguesve. Në përgjithësi, treguesit e rendit më të lartë se i pari përdoren rrallë, por ky material do të ndihmojë për të kuptuar mekanizmin se si funksionojnë treguesit.

Treguesit e funksionit

Treguesit mund t'u referohen funksioneve. Emri i funksionit, si emri i grupit, është në vetvete një tregues, domethënë ai përmban adresën hyrëse.

// deklarimi i një treguesi funksioni /*lloji i të dhënave*/ (* /*emri i treguesit*/)(/*lista e argumenteve të funksionit*/);

Ne përcaktojmë llojin e të dhënave që do të kthehet nga funksioni të cilit do t'i referohet treguesi. Simboli i treguesit dhe emri i tij janë të mbyllura në kllapa për të treguar se është një tregues dhe jo një funksion që kthen një tregues në një lloj specifik të dhënash. Pas emrit të treguesit ka kllapa; këto kllapa listojnë të gjitha argumentet, të ndara me presje, si në një deklaratë prototipi të funksionit. Argumentet trashëgohen nga funksioni të cilit do t'i referohet treguesi. Le të zhvillojmë një program që përdor një tregues funksioni. Programi duhet të gjejë GCD - pjesëtuesin më të madh të përbashkët. GCD është numri i plotë më i madh që ndan dy numra të futur nga përdoruesi pa mbetje. Numrat e hyrjes duhet gjithashtu të jenë numra të plotë.

// pointer_onfunc.cpp: Përcakton pikën hyrëse për aplikacionin e konsolës. #include "stdafx.h" #include << "Enter first number: "; cin >>a; cout<< "Enter second number: "; cin >>b; cout<< "NOD = " << ptrnod(a, b) << endl; // обращаемся к функции через указатель system("pause"); return 0; } int nod(int number1, int number2) // рекурсивная функция нахождения наибольшего общего делителя НОД { if (number2 == 0) //базовое решение return number1; return nod(number2, number1 % number2); // рекурсивное решение НОД }

// kodi Kodi::Blloqe

// Kodi i Dev-C++

// pointer_onfunc.cpp: Përcakton pikën hyrëse për aplikacionin e konsolës. #përfshi duke përdorur hapësirën e emrave std; int nod(int, int); // prototipi i funksionit të specifikuar int main(int argc, char* argv) ( int (*ptrnod)(int, int); // deklarimi i një treguesi funksioni ptrnod=nod; // cakto adresën e funksionit treguesit ptrnod int a, b; cout<< "Enter first number: "; cin >>a; cout<< "Enter second number: "; cin >>b; cout<< "NOD = " << ptrnod(a, b) << endl; // обращаемся к функции через указатель return 0; } int nod(int number1, int number2) // рекурсивная функция нахождения наибольшего общего делителя НОД { if (number2 == 0) //базовое решение return number1; return nod(number2, number1 % number2); // рекурсивное решение НОД }

Ky problem u zgjidh në mënyrë rekursive për të zvogëluar sasinë e kodit të programit në krahasim me një zgjidhje përsëritëse për të njëjtin problem. NË rreshti 9 deklarohet një tregues tek i cili rreshti 10 caktohet adresa e funksionit. Siç thamë më parë, adresa e një funksioni është thjesht emri i tij. Kjo do të thotë, ky tregues tani tregon funksionin nod(). Kur deklaroni një tregues në një funksion, mos harroni kurrë kllapat që mbyllin simbolin e treguesit dhe emrin e tij. Kur deklarojmë një tregues, ne specifikojmë në argumente të njëjtën gjë si në prototipin e funksionit të cilit i referohemi. Rezultati i programit (shih Figurën 5).

Futni numrin e parë: 16 Futni numrin e dytë: 20 NOD = 4 Për të vazhduar, shtypni çdo buton. . .

Figura 5 - Treguesit në C++

Fusim numrin e parë, pastaj të dytin dhe programi prodhon një gcd. Në figurën 5 mund të shihni se GCD për numrat 16 dhe 20 është e barabartë me katër.

Edhe nëse shumica e programuesve e kuptojnë ndryshimin midis objekteve dhe treguesve drejt tyre, ndonjëherë nuk është plotësisht e qartë se cila mënyrë për të hyrë në një objekt duhet të zgjidhet. Më poshtë jemi përpjekur t'i përgjigjemi kësaj pyetjeje.

Pyetje

Vura re që shpesh programuesit, kodi i të cilëve pashë, përdorin tregues për objekte më shpesh sesa vetë këto objekte, d.m.th., për shembull, ata përdorin konstruksionin e mëposhtëm:

Objekti *myObject = Objekti i ri;

Objekti myObject;

E njëjta gjë me metodat. Pse në vend të kësaj:

MyObject.testFunc();

duhet të shkruajmë këtë:

MyObject->testFunc();

Siç e kuptoj, kjo jep një fitim në shpejtësi, sepse ... ne aksesojmë drejtpërdrejt memorien. E drejtë? P.S. Kam kaluar nga Java.

Përgjigju

Vini re, meqë ra fjala, se në Java treguesit nuk përdoren në mënyrë eksplicite, d.m.th. Një programues nuk mund të aksesojë një objekt në kod përmes një treguesi në të. Sidoqoftë, në realitet, në Java, të gjitha llojet, përveç atyre bazë, janë lloje referimi: ato aksesohen me referencë, megjithëse është e pamundur të kalohet në mënyrë eksplicite një parametër me referencë. Dhe gjithashtu, vini re, të rejat në C++ dhe në Java ose C# janë gjëra krejtësisht të ndryshme.

Për të dhënë një ide të vogël se cilat janë treguesit në C++, këtu janë dy fragmente të ngjashme të kodit:

Objekti objekt1 = Objekti i ri(); // Objekti i ri Objekt objekt2 = Objekti i ri(); // Një tjetër objekt i ri objekt1 = objekt2; // Të dy variablat i referohen objektit që më parë ishte referuar nga objekti2 // Nëse objekti i referuar nga objekti1 ndryshon, // objekti2 gjithashtu do të ndryshojë, sepse janë i njëjti objekt

Ekuivalenti më i afërt në C++ është:

Objekti * objekt1 = Objekti i ri(); // Memoria ndahet për një objekt të ri // Kjo memorie referohet nga objekti1 Objekti * objekt2 = Objekti i ri(); // E njëjta gjë me objektin e dytë delete object1; // C++ nuk ka një sistem grumbullimi të mbeturinave, kështu që nëse kjo nuk bëhet, // programi nuk do të jetë më në gjendje t'i qaset kësaj memorie, // të paktën derisa programi të riniset // Ky quhet objekt i rrjedhjes së kujtesës1 = objekt2; // Ashtu si në Java, objekti1 tregon në të njëjtin vend si objekti2

Sidoqoftë, kjo është një gjë krejtësisht e ndryshme (C ++):

Objekti objekt1; // Objekti i ri Objekti objekt2; // Një tjetër objekt1 = objekt2; // Kopjimi i plotë i objektit2 në objekt1, // në vend që të ripërcaktohet treguesi, është një operacion shumë i shtrenjtë

Por a do të fitojmë shpejtësi duke hyrë drejtpërdrejt në kujtesë?

Në mënyrë rigoroze, kjo pyetje ndërthur dy pyetje të ndryshme. Së pari: kur duhet të përdorni alokimin dinamik të memories? Së dyti: kur duhet të përdorni tregues? Natyrisht, këtu nuk mund të bëjmë pa fjalë të përgjithshme se është gjithmonë e nevojshme të zgjedhësh mjetin më të përshtatshëm për këtë punë. Pothuajse gjithmonë ka një zbatim më të mirë sesa përdorimi i shpërndarjes dinamike manuale (shpërndarja dinamike) dhe/ose tregues të papërpunuar.

Shpërndarja dinamike

Formulimi i pyetjes paraqet dy mënyra për të krijuar një objekt. Dhe ndryshimi kryesor është jetëgjatësia e tyre (kohëzgjatja e ruajtjes) në memorien e programit. Përdorimi i Object myObject; , ju mbështeteni në zbulimin automatik gjatë gjithë jetës dhe objekti do të shkatërrohet sapo të largohet nga objekti i tij. Por Object *myObject = Objekti i ri; e mban objektin gjallë derisa ta fshini manualisht nga memoria me komandën delete. Përdorni opsionin e fundit vetëm kur është vërtet e nevojshme. Dhe për këtë arsye Gjithmonë zgjidhni të përcaktoni automatikisht jetëgjatësinë e një objekti nëse është e mundur.

Në mënyrë tipike, përcaktimi i detyruar i jetëgjatësisë përdoret në situatat e mëposhtme:

  • Ju duhet që objekti të ekzistojë edhe pasi të keni lënë objektin e tij- pikërisht ky objekt, pikërisht në këtë zonë memorie, dhe jo një kopje e tij. Nëse kjo nuk është e rëndësishme për ju (në shumicën e rasteve është), mbështetuni në përcaktimin automatik të jetëgjatësisë. Megjithatë, këtu është një shembull i një situate ku mund t'ju duhet të përdorni një objekt jashtë fushëveprimit të tij, por ju mund ta bëni këtë pa e ruajtur në mënyrë eksplicite: Duke shkruar një objekt në një vektor, ju mund të "prishni lidhjen" me vetë objektin - në fakt ajo (dhe jo një kopje e saj) do të jetë e disponueshme kur thirret nga një vektor.
  • Ju duhet të përdorni shumë memorie, të cilat mund të tejmbushin pirgun. Është mirë nëse nuk duhet të përballeni me një problem të tillë (dhe rrallë e hasni), sepse është "jashtë kompetencës" së C++, por për fat të keq, ndonjëherë ju duhet ta zgjidhni edhe këtë problem.
  • Për shembull, ju nuk e dini saktësisht madhësinë e grupit që do të duhet të përdorni. Siç e dini, në C++, vargjet kanë një madhësi fikse kur përcaktohen. Kjo mund të shkaktojë probleme, për shembull, kur lexoni hyrjen e përdoruesit. Treguesi përcakton vetëm zonën në memorie ku do të shkruhet fillimi i grupit, përafërsisht, pa kufizuar madhësinë e tij.

Nëse përdorimi i shpërndarjes dinamike është i nevojshëm, atëherë duhet ta kapsuloni atë duke përdorur një tregues inteligjent (mund të lexoni në artikullin tonë) ose një lloj tjetër që mbështet idiomën "Marrja e një burimi po inicializohet" (kontejnerët standardë e mbështesin këtë - kjo është idioma sipas cili burim: memoria e bllokut, skedari, lidhja në rrjet etj. - kur merret, inicializohet në konstruktor dhe më pas shkatërrohet me kujdes nga destruktori). Treguesit inteligjentë janë, për shembull, std::unique_ptr dhe std::shared_ptr.

Tabelat

Megjithatë, ka raste kur përdorimi i pointerëve justifikohet jo vetëm nga pikëpamja e shpërndarjes dinamike të memories, por pothuajse gjithmonë ekziston një mënyrë alternative, pa përdorimin e pointerëve, të cilën duhet ta zgjidhni. Si më parë, le të themi: gjithmonë zgjidhni alternativën, përveç nëse ka nevojë të veçantë për të përdorur tregues.

Rastet kur përdorimi i treguesve mund të konsiderohet si një opsion i mundshëm përfshijnë sa vijon:

  • Semantika referenciale. Ndonjëherë mund të jetë e nevojshme të qaseni në një objekt (pavarësisht se si ndahet memoria për të), sepse dëshironi të aksesoni funksionet në këtë objekt, dhe jo kopjen e tij - d.m.th. kur ju duhet të zbatoni kalimin me referencë. Sidoqoftë, në shumicën e rasteve, mjafton të përdorni një lidhje këtu, dhe jo një tregues, sepse për këtë janë krijuar lidhjet. Vini re se këto janë gjëra paksa të ndryshme nga ajo që u përshkrua në pikën 1 më lart. Por nëse mund të përdorni një kopje të objektit, atëherë nuk ka nevojë të përdorni një referencë (por kini parasysh se kopjimi i një objekti është një operacion i shtrenjtë).
  • Polimorfizmi. Thirrja e funksioneve brenda polimorfizmit (klasa e objektit dinamik) është e mundur duke përdorur një referencë ose tregues. Përsëri, përdorimi i referencave është i preferuar.
  • Objekt opsional. Në këtë rast, mund të përdorni nullptr për të treguar se objekti është lënë jashtë. Nëse është një argument funksioni, atëherë është më mirë ta zbatoni atë me argumente të paracaktuar ose një mbingarkesë. Përndryshe, mund të përdorni një lloj që përfshin këtë sjellje, si p.sh. boost::optional (i modifikuar në C++14 std::optional).
  • Përmirësimi i shpejtësisë së përpilimit. Mund t'ju duhet të ndani njësitë e përpilimit (njësi përpilimi). Një përdorim efektiv i pointerëve është deklarimi paraprak (sepse për të përdorur një objekt, së pari duhet ta përkufizoni atë). Kjo do t'ju lejojë të lini hapësirë ​​nga njësitë e përpilimit, të cilat mund të kenë një efekt pozitiv në përshpejtimin e kohës së përpilimit, duke reduktuar ndjeshëm kohën e shpenzuar për këtë proces.
  • Ndërveprimi me bibliotekënC ose C-si. Këtu do të duhet të përdorni tregues të papërpunuar, duke çliruar kujtesën prej tyre në momentin e fundit. Ju mund të merrni një tregues të papërpunuar nga një tregues inteligjent, për shembull, me operacionin marrë. Nëse biblioteka përdor memorie që më vonë duhet të lirohet manualisht, mund ta kornizoni destruktorin në një tregues inteligjent.

Një tregues është një variabël që përmban adresë disa objekte në kujtesën me akses të rastësishëm (RAM). Pika e përdorimit të pointerëve është adresimi indirekt i objekteve në OP, i cili ju lejon të ndryshoni dinamikisht logjikën e programit dhe të kontrolloni shpërndarjen e OP.

Aplikimet kryesore:

  • duke punuar me vargje dhe vargje;
  • akses direkt në OP;
  • duke punuar me objekte dinamike për të cilat është ndarë OP.

Përshkrimi i treguesit ka formën e përgjithshme të mëposhtme:

Shkruani *emrin;

domethënë, treguesi është gjithmonë trajton një lloj të caktuar objekti! Për shembull,

Int *px; // tregues për të dhënat e plota char *s; //tregues për të shtypur char (varg C)

Le të përshkruajmë operacionet dhe veprimet kryesore që lejohen me tregues:

1. Mbledhja/zbritja me numra:

Px++; //lëvizni treguesin px në sizeof(int) bytes përpara s--; //shkoni te karakteri i mëparshëm i rreshtit //(sipas madhësisë së (char) bajteve, jo domosdoshmërisht një)

2. Një treguesi mund t'i caktohet adresa e një objekti duke përdorur operacionin unar "&":

Int *px; int x,y; px= //tani px tregon në një qelizë memorie me // vlerë x px= //dhe tani në një qelizë me vlerë y

3. Vlera e variablit të treguar nga treguesi merret nga operacioni unar " * " ("merr vlerën"):

X=*px; //indirekt ka kryer detyrën x=y (*px)++; //tërthorazi rriti vlerën e y me 1

E rëndësishme! Për shkak të përparësisë dhe asociativitetit të operacioneve C++, veprimi

ka një kuptim krejtësisht të ndryshëm nga ai i mëparshmi. Do të thotë "merr vlerën y (*px) dhe më pas kaloni në vendndodhjen tjetër të memories (++)"

Le të deshifrojmë operatorin

Nëse px ende tregon y, do të thotë "shkruani vlerën e y në x dhe më pas kaloni në vendndodhjen e kujtesës pranë px". Kjo është pikërisht qasja në C klasike që përdoret për të skanuar vargje dhe vargje.

Këtu është një shembull që tregon këtë ndryshim të rëndësishëm deri në adresat e kujtesës. Komenti tregon vlerat dhe adresat e kujtesës të variablave x dhe y, si dhe vlerën e marrë nga treguesi px dhe adresën e kujtesës në të cilën ai tregon. Ju lutemi vini re se pas ekzekutimit të versionit të dytë të kodit, vlera e marrë nga treguesi u bë "mbeturina", pasi tregonte në një ndryshore dhe jo në elementin zero të grupit.

#përfshi int main() ( int x=0,y=1; int *px= printf ("\nx=%d në &%p, y=%d në &%p, *px=%d në &%p" ,x,&x,y,&y,*px,px); x=(*px)++; //pas ekzekutimit të parë, zëvendësojeni me x=*px++; printf ("\nx=%d në &%p , y =%d në &%p, *px=%d në &%p",x,&x,y,&y,*px,px); /* Veprimi (*px)++ x=0 në &002CFC14, y= 1 në &002CFC08, *px=1 në &002CFC08 x=1 në &002CFC14, y=2 në &002CFC08, *px=2 në &002CFC08 Veprim *px++ x=0 në &0021F774, y=1p në &0021F774, *021p në &002CFC08 &0021F768 x =1 në &0021F774, y=1 në &0021F768, *px=-858993460 në &0021F76C */ getchar(); ktheje 0; )

Këtu është një shembull i lidhjes së një treguesi me një grup statik:

Int a=(1,2,3,4,5); int *pa= për (int i=0; i<5; i++) cout

Për (int i=0; i<5; i++) cout

Këto hyrje janë absolutisht ekuivalente, sepse në C konstruksioni a[b] nuk do të thotë asgjë më shumë se *(a+b) , ku a është një objekt, b është zhvendosja nga fillimi i memories që adreson objektin. Kështu, qasja në një element të grupit a[i] mund të shkruhet gjithashtu si *(a+i) dhe caktimi i adresës së një elementi të grupit zero në një tregues mund të shkruhet në cilëndo nga 4 mënyrat.

Int *pa= int *pa=&(*(a+0)); int *pa=&(*a); int *pa=a;

E rëndësishme! Pavarësisht nga mënyra e shkrimit, ky është i njëjti operacion, dhe ky nuk është "caktimi i një grupi në një tregues", por vendosja e tij në elementin zero të grupit.

4. Krahasimi i treguesve (në vend të krahasimit të vlerave të cilave ata tregojnë) në përgjithësi mund të jetë i pasaktë!

Int x; int *px=&x, *py= nëse (*px==*py) ... //korrekt nëse (px==py) ... //e pasaktë!

Arsyeja është se adresimi i OP nuk duhet të jetë i paqartë; për shembull, në DOS, një adresë memorie mund të korrespondojë me çifte të ndryshme të pjesëve të adresës "segment" dhe "offset".

Metoda 1, me një variabël referimi C++

Ndërrimi i zbrazët (int &a, int &b) ( int c=a; a=b; b=c; ) //... int a=3,b=5; ndërroj(a,b);

Kjo metodë mund të quhet "kalimi i parametrave sipas vlerës, marrja me referencë".

Metoda 2, me tregues C

Shkëmbimi i zbrazët (int *a, int *b) ( int c=*a; *a=*b; *b=c; ) //... int a=3,b=5; këmbim (&a,&b); int *pa= shkëmbim (pa,&b);

Kalimi i parametrave sipas adresës, marrja me vlerë.

C pointers dhe vargjet

Në mënyrë tipike, treguesit përdoren për të skanuar vargjet C.

Char *s="Përshëndetje, botë";

Kjo është vendosja e një treguesi në bajtin e parë të një konstante vargu, jo kopjimi ose caktimi!

E rëndësishme!

1. Edhe nëse madhësia e karakterit është një bajt, ky varg do të marrë jo 12 (11 karaktere dhe një hapësirë), por 13 bajt memorie. Një bajt shtesë nevojitet për të ruajtur terminatorin null, një karakter me kodin 0, i shkruar si "\0" (por jo "0" - kjo është shifra 0 me kodin 48). Shumë funksione të vargut C shtojnë automatikisht një terminator null në fund të vargut që përpunohet:

Char s; strcpy(s"Përshëndetje, botë"); //Thirret funksioni standard i kopjimit të linjës //Error! Nuk ka vend për char s terminator null; //Kjo do të ishte e vërtetë!

2. Gjatësia e vargut C nuk ruhet askund; ajo mund të zbulohet vetëm nga funksioni standard strlen(s), ku s është një tregues char *. Për vargun e shkruar më sipër, vlera 12 do të kthehet, terminatori null nuk llogaritet. Në fakt, një varg C është një grup karakteresh, elementë të tipit char.

Si të kryeni operacione të tjera në vargjet e specifikuara duke përdorur treguesit char *? Për ta bërë këtë, mund t'ju duhen disa biblioteka standarde në të njëjtën kohë. Si rregull, përpiluesit e rinj C++ mund të përfshijnë si skedarë "klasikë" të përputhshëm me C-në dhe tituj nga versionet më të reja të standardit, të cilat tregohen në kllapa.

Skedari ctype.h (cctype) përmban:

1) funksionet e emërtuara është* - duke kontrolluar klasën e karaktereve (isalpha, isdigit, ...), ata të gjithë kthejnë një numër të plotë, për shembull:

Char d; nëse (është shifër(d)) ( //kodi për situatën kur d është një shifër)

Një kontroll i ngjashëm "manualisht" mund të kryhet me kod si

Nëse (d>="0" && d<="9") {

2) funksionet e emërtuara në* - konvertimi i rastit të karakterit (toupper, tolower), ata kthejnë karakterin e konvertuar. Ato mund të jenë të padobishme kur punoni me karaktere nga alfabetet kombëtare, dhe jo vetëm nga alfabeti latin.

Moduli string.h (cstring) është krijuar për të punuar me vargje të specifikuara nga një tregues dhe që mbarojnë me bajtin "\0" ("vargjet C"). Shumica e emrave të funksioneve të tij fillojnë me "str". Disa funksione (memcpy, memmove, memcmp) janë të përshtatshme për të punuar me buffer (zona memorie me një madhësi të njohur).

Shembuj për të punuar me vargje dhe tregues

1. Kopjoni një rresht

Char *s="Vargu i testimit"; char s2; strcpy(s2,s); //kopjimi i një vargu, s2 është një buffer, jo një tregues!

2. Kopjimi i një rreshti që tregon numrin e karaktereve

Char *s="Vargu i testimit"; char s2; char *t=strncpy (s2,s,strlen(s)); cout<< t;

Funksioni strncpy kopjon jo më shumë se n karaktere (n është parametri i tretë), por nuk e shkruan terminatorin null, duke rezultuar në daljen e mbeturinave në fund të vargut t. Do të ishte e saktë të shtoni sa vijon pas thirrjes strncpy:

T="\0";

pra instalimi “manual” i terminatorit null.

3. Kopjimi i një vargu në memorie të re

Char *s="12345"; char *s2=karak i ri ; strcpy(s2,s);

Këtu kemi kopjuar në mënyrë të sigurtë vargun s në memorien e re s2, duke kujtuar që të ndajmë një bajt "ekstra" për terminatorin null.

4. Këtu është zbatimi ynë i funksionit standard strcpy:

Char *strcpy_ (char *dst, char *src) ( char *r=dst; ndërsa (*src!="\0") ( *dst=*src; dst++; src++; ) *dst="\0"; ktheje r;)

Ju mund ta quani funksionin tonë, për shembull, si kjo:

Char *src="varg teksti"; char dst; strcpy_(&dst,&src);

Le të shkurtojmë tekstin e funksionit strcpy_:

Char *strcpy_ (char *dst, char *src) (char *r=dst; ndërsa (*src) *dst++=*src++; *dst="\0"; kthim r; )

5. Lidhja e vargut - funksioni strcat

Char *s="Vargu i testimit"; char *s2; char *t2=strcat (s2,strcat(s," fjalë të reja"));

Meqenëse strcat nuk shpërndan memorie, sjellja e një kodi të tillë është e paparashikueshme!

Por kjo lidhje e vargut do të funksionojë:

Char s; strcpy(s"Test string"); char s2; strcat(s"fjalë të reja"); strcpy(s2,s); char *t2=strcat (s2,s);

Kjo eshte, Duhet të ketë gjithmonë një kujtim se ku të shkruani- statike nga një tampon ose e alokuar në mënyrë dinamike.

6. Kërkoni për një karakter ose nënvarg në një varg.

Char *sym = strchr(s,t"); nëse (sym==NULL) vendos ("Nuk u gjet"); other puts(sym); //do të nxjerrë "t string" //për strrchr dalja do të ishte "tring" char *sub = strstr (s,"ring"); vendos (nën); //do të shfaqë "unazën"

7. Krahasimi i vargjeve - funksionet me modelin e emrit str*cmp - "krahasimi i vargjeve"

Char *a="abcd",*b="abce"; int r=strcmp(a,b); //r=-1, sepse karakteri "d" i paraprin karakterit "e" //Sipas kësaj, strcmp(b,a) do të kthejë 1 në këtë rast //Nëse vargjet përputhen, rezultati = 0

8. Ekzistojnë funksione të gatshme për analizimin e vargjeve - strtok, strspn, strсspn - shih manualin, paragrafët. 8.1-8.3

9. Shtypni konvertimin midis numrit dhe vargut - libraria stdlib.h (cstdlib)

Char *s="qwerty"; int i=atoi(s); //i=0, nuk krijohen përjashtime!

Nga numri në rresht:

1) itoa, ultoa - nga llojet e numrave të plotë

Char buf; int i=-31189; char *t=itoa(i,buf,36); //Në buf kemi marrë hyrjen i në s.s.

2) fcvt , gcvt , ecvt - nga llojet reale

Puna me memorie dinamike

Si rregull, përshkruhet një tregues i llojit të dëshiruar, i cili më pas shoqërohet me një zonë memorie të caktuar nga operatori i ri ose funksione të pajtueshme me C për menaxhimin e OP.

1. Përshkruani një tregues për një objekt dinamik të ardhshëm:

Int *a; //Më e besueshme int *a=NULL;

2. Përdorni operatorin e ri ose funksionet malloc dhe calloc për të ndarë RAM-in:

A = int e re;

#përfshi //stdlib.h, alloc.h në përpilues të ndryshëm //... a = (int *) malloc (sizeof(int)*10);

A = (int *) calloc (10,madhësia e (int));

Në rastin e fundit, ne ndamë 10 elementë të bajteve të madhësisë (int) dhe mbushëm "\0" me zero.

E rëndësishme! Mos i përzieni këto 2 metoda në një modul softueri ose projekt! preferohet e reja, me përjashtim të rasteve kur duhet të siguroheni që memoria të jetë e mbushur me zero bajt.

3. Kontrolloni nëse memoria është alokuar me sukses - nëse jo, treguesi është i barabartë me konstantën NULL nga biblioteka standarde (në disa përpilues null , nullptr , 0):

Nëse (a==NULL) ( //Gabimi i trajtimit "Dështoi të ndahet memoria" )

4. Puna me një varg ose varg dinamik nuk ndryshon nga rasti kur ato janë statike.

5. Kur OP i alokuar nuk është më i nevojshëm, ai duhet të lirohet:

Fshij a; //Nëse përdoret i ri pa pagesë (a); //Përpiqet të çlirojë OP, //nëse është përdorur malloc/calloc

E rëndësishme! Gjithmonë përpiquni t'i përmbaheni parimit të stivës kur shpërndani OP. Domethënë, objekti që pushtoi OP i fundit është i pari që e lëshon atë.

Shembull. Matrica dinamike e madhësisë n*m.

Konstit int n=5,m=4; int **a = i ri (int *) [n]; për (int i=0; i

Pas kësaj, mund të punoni me elementët e matricës a[i][j], për shembull, t'u caktoni atyre vlera. Ju mund të lironi kujtesën si kjo:

Për (int i=n-1; i>-1; i--) fshini a[i]; fshij a;

1. Shkruani funksionin tuaj për të punuar me një varg të specifikuar nga një tregues dhe krahasoni atë me atë standard.

2. Shkruani funksionin tuaj për të punuar me një grup dinamik njëdimensional të specifikuar nga një tregues.

3. Shkruani versionet tuaja të funksioneve për konvertimin e një vargu në një numër dhe një numri në një varg.


Mbylle