- Rādītāji attēlo atmiņas adreses un nodrošina tiešu kontroli pār to, kur un kā dati tiek glabāti un tiem piekļūst.
- Atsauces noņemšana, konstantu pareizība un rādītāja aritmētika ir būtiskas, lai droši izmantotu rādītājus ar masīviem, struktūrām un dinamisko atmiņu.
- Rādītāji ļauj nodot argumentus pēc atsauces, veidot dinamiskas struktūras un ieviest daudzlīmeņu netiešo saikni, piemēram, rādītājs-rādītājs.
- Nulles pārbaudes, pareiza piešķiršana/atdalīšana un disciplinēta inicializācija ir ļoti svarīgas, lai izvairītos no nedefinētas uzvedības un avārijām.

C un C++ rādītājiem ir leģendāra reputācija: spēcīgi, viltīgi un spējīgi acumirklī avarēt jūsu programmu, ja esat neuzmanīgs. Tomēr, tiklīdz jūs patiesi saprotat, kas ir rādītājs – tikai atmiņas adrese –, liela daļa no šī noslēpuma sāk izgaist, un jūs iegūstat piekļuvi vienam no daudzpusīgākajiem rīkiem zemā līmeņa un sistēmu programmēšanā.
Šajā rakstā soli pa solim ir aprakstīta atmiņas adrešu un vienkāršu rādītāju pamatideja, izmantojot atsauces, masīvus, klases un dinamisko atmiņu, kā arī rādītāju savstarpējās izmantošanas gadījumus un biežāk sastopamās kļūdas. Mērķis ir padarīt darbu ar rādītājiem dabisku, nevis kā tumšo maģiju, lai jūs varētu spriest par to, kas patiesībā notiek atmiņā, kad jūsu kods darbojas.
Mainīgo un atmiņas adrešu izpratne
Pirms pieskaršanās rādītājiem, ir nepieciešams skaidrs priekšstats par to, kā mainīgie atrodas atmiņā. Datora RAM konceptuāli ir garš baitu masīvs, katram baitam ir piešķirta unikāla skaitliska adrese. Deklarējot mainīgo, kompilators rezervē vienu vai vairākus no šiem baitiem un saista šo adresi ar mainīgā nosaukumu.
Iedomājieties mainīgo kā marķētu lodziņu, kas novietots kaut kur atmiņā: nosaukums ir etiķete, adrese ir fiziskā atrašanās vieta plauktā, un saturs ir vērtība, kas tiek glabāta lodziņa iekšpusē. Piemēram, ja jums ir int Tipiskā Arduino UNO tas aizņems 2 secīgus baitus RAM, un kompilators reģistrē, kuras precīzās adreses tam ir rezervētas.
Mainīgā deklarācija norāda kompilatoram, kāda veida un izmēra tas jārezervē, savukārt definīcija vai piešķiršana faktiski saglabā vērtību šajā rezervētajā vietā. Piemēram, rakstīšana int j; tikai paziņo mainīgo un ļauj kompilatoram piešķirt atmiņu, turpretī j = 10; ieraksta skaitlisko vērtību 10 atmiņas šūnās, kas pieder j.
Iekšēji kompilators uztur simbolu tabulu, kurā tas saista katru mainīgā nosaukumu ar tā atmiņas adresi un tipu. Ja kompilators nolemj, ka j dzīvo adresē 2020, jūs varat konceptuāli iztēloties situāciju šādi: identifikators j norāda uz adresi 2020, un baiti adresē 2020 satur skaitļa 10 bināro attēlojumu.
Ir ļoti svarīgi nodalīt ideju par to, "kur kaut kas tiek glabāts" (tā adrese) no idejas "kas tur tiek glabāts" (tā vērtība). Kompilatoru teorijā un daudzās grāmatās atrašanās vietu bieži sauc par lvalue (no "location value" (atrašanās vietas vērtība)), savukārt saturu sauc par rvalue . Rādītāji ir paredzēti tiešai manipulācijai ar šīm atrašanās vietām.
Kas īsti ir rādītājs?

Rādītājs ir vienkārši mainīgais, kura vērtība ir atmiņas adrese, kas norāda uz kādu citu objektu. Tas neuzglabā pašus datus, bet gan adresi, kur šie dati atrodas. Rādītāja lielums ir atkarīgs no datora arhitektūras: 32 bitu x86 sistēmās tas parasti ir 4 baiti, 64 bitu x86-64 sistēmās tas parasti ir 8 baiti, un mazos mikrokontrolleros, piemēram, Arduino, adrese var ietilpt 2 baitos.
Deklarējot rādītāju, jūs norādāt ne tikai to, ka tas saglabā adresi, bet arī objekta veidu, uz kuru tas norādīs. Piemēram, int* p deklarē rādītāju uz intŠeit zvaigznīte ir daļa no tipa, nevis reizināšanas zīme, un tā norāda kompilatoram, cik baitu lasīt vai rakstīt, kad vēlāk piekļūstat. *p.
Operatora adrese & sniedz esoša objekta adresi, kuru var saglabāt rādītāja mainīgajā. Pieņemsim, ka jums ir int n = 0;; tad šis kods saglabā adresi n rādītājā:
Piemērs: int n = 0;
int* p = &n; // p now holds the address of n
Kad rādītājam ir derīga adrese, atsauces noņemšanas operators tiek izmantots. * ļauj piekļūt objektam, kas atrodas šajā adresē. If p ir norāde uz int, Tad *p uzvedas kā atmiņā saglabātā faktiskā veselā skaitļa aizstājvārds. Piemēram:
Fragments: *p = 1; // writes 1 into n through the pointer
std::cout << *p; // reads the current value of n
Galvenā ideja ir tāda, ka zvaigznei dažādos kontekstos ir atšķirīga nozīme: deklarācijā tā veido rādītāja tipu, bet izteiksmē tā atceļ atsauci uz rādītāju. Šo divu lomu jaukšana ir viena no klasiskajām iesācēju kļūdām, tāpēc vienmēr pievērsiet uzmanību tam, vai deklarējat rādītāju vai izmantojat to, lai piekļūtu atmiņai.
Mazās ierīcēs, piemēram, Arduino, rādītājs, kas nav skaidri inicializēts, satur vai nu derīgu 16 bitu adresi, vai arī tajā ir atkritumi. Nav maģiskas “tukšas” vērtības, ja vien jūs to apzināti neiestatāt uz nulles rādītāja konstanti, piemēram, nullptr C++ valodā. Šādas atkritumu adreses dereferencēšana ir gandrīz drošs veids, kā bloķēt mikrokontrolleri.
Konstantes pareizība un dažādi rādītāju veidi
Norādes mijiedarbojas ar const veidos, kas sākumā var šķist mulsinoši, taču to apgūšana ir ļoti svarīga pareizas C++ rakstīšanai. Pozīcija const attiecībā pret zvaigzni nosaka, vai objekts, uz kuru norādīts, pats rādītājs vai abi ir nemainīgi.
Ja jums ir konstants vesels skaitlis, rādītāja tipam jāatspoguļo, ka jūs to varat tikai izlasīt, nevis modificēt. Iedomājieties šo kodu:
Demo: auto const cn = int{0}; // cn is a constant int
int const* p = &cn; // pointer to const int
Veida p šeit ir “rādītājs uz konstantu int”: jūs varat izlasīt *p bet nevar tam piešķirt. Trying int* p = &cn; būtu tipa kļūda, jo tas solītu, ka jūs varat modificēt konstantu objektu, ko valoda aizliedz.
Dažreiz pats objekts nav const, bet jūs apzināti vēlaties rādītāju, kas atļauj tikai lasīšanas piekļuvi caur to. Tādā gadījumā jūs atkal izmantojat int const*:
Lietošana: auto n = int{0}; // non-const int
int const* p = &n; // can read n via p, but not write through p
Ņemiet vērā, ka int const* un const int* nozīmē tieši to pašu: vesels skaitlis ir tikai lasāms caur rādītāju, bet rādītāju joprojām var mainīt, lai tas norādītu uz citu vietu. No otras puses, ja jūs rakstāt int* const p = &n;, Jums ir konstants rādītājs uz nekonstantu int: adrese, kas saglabāta p pēc inicializācijas nevar mainīt, bet gan vērtību *p var brīvi mainīties.
Varat pat apvienot abas formas, lai izveidotu konstantu rādītāju uz konstantu veselu skaitli: int const* const p. Tas norāda kompilatoram, ka ne adrese, p arī šajā adresē saglabātā vērtība nedrīkst mainīties. Izpratne par šīm variācijām palīdz ļoti skaidri izteikt nodomu, un kompilators nodrošinās jūsu godīgumu.
Norādes uz struktūrām un klasēm
Kad rādītājs atsaucas uz struktūru vai klasi, parasti ir nepieciešams piekļūt tās publiskajai saskarnei: datu elementiem un elementu funkcijām. Atsauces noņemšana ar * joprojām darbojas, bet sintakse var kļūt nedaudz garlaicīga, tāpēc C++ nodrošina bultiņas operatoru -> kā saīsinājums.
Apsveriet vienkāršu Student struktūra ar atzīmēm un metode, kas aprēķina vidējo vērtību. If Student* p tur adresi Student objekts, jūs varat rakstīt (*p).grade_2 lai sasniegtu otro klasi, vai (*p).average() lai izsauktu dalībnieka funkciju.
Bultas operators apvieno atsauces dereferenci un piekļuvi dalībniekiem vienā solī: p->grade_2 un p->average() nozīmē tieši to pašu, ko (*p).grade_2 un (*p).average(). Saskaņā ar kapuci, p->member ir vienkārši sintaktiskais cukurs (*p).memberTāpēc jūs gandrīz vienmēr redzēsiet -> izmanto reālās pasaules kodā, strādājot ar rādītājiem uz objektiem.
Kamēr vien klase nepārslogo operator* or operator-> ar kādu eksotisku uzvedību jūs varat izturēties pret p->member kā standarta veids, kā piekļūt objektam aiz rādītāja. Daudzi ietvari patiešām paļaujas uz šo operatoru pārslodzi viedajiem rādītājiem, taču konceptuāli tiem ir tāda pati nozīme: sekot rādītājam un pēc tam piekļūt dalībniekam.
Nulles norādes un drošība
Rādītājs, kas pašlaik neatsaucas uz derīgu objektu, tiek saukts par nulli, un mūsdienu C++ valodā kanoniskais veids, kā to izteikt, ir ar nullptr. Rakstiski int* p = nullptr; nepārprotami norāda, ka p vēl nekur jēgpilnu nenorāda.
Nulles rādītāja dereferencēšana ir nedefinēta uzvedība, kas parasti izraisa avārijas, piekļuves pārkāpumus vai mazos paneļos iesaldētu sistēmu. Tāpēc kods, kas saņem rādītāju kā parametru, bieži pirms tā izmantošanas pārbauda, vai tas ir nulle. Ja jūsu loģika pieļauj jēgpilnu stāvokli “nav objekta”, rādītāja parametrs ir piemērots, jo tas var pārnēsāt šo “prombūtnes” informāciju, izmantojot nullptr.
Idiomātisks piemērs ir funkcija, kas pārveido C stila virkni (char const*) līdz std::string bet tam ir graciozi jāapstrādā gadījums, kad ievades rādītājs ir nulle. Funkcija pirms veidošanas pārbauda, vai rādītājs nav nulle. std::stringJa tā ir nulle, tā atgriež tukšu virkni, nevis dereferencē nederīgu adresi.
Ja parametrs ir obligāts un nevar iztrūkt, C++ atsauces parasti ir labāka izvēle nekā neapstrādāti rādītāji. Atsauci nevar atjaunot, un tai nav jābūt tukšai, tāpēc tipu sistēma skaidri izsaka cerību, ka izsaucējam ir jānodrošina derīgs objekts. Tas padara API drošāku un kodu vieglāk saprotamu.
Rādītāji kā funkciju parametri: pēc vērtības pretstatā atsaucei
Pēc noklusējuma, nododot mainīgo funkcijai C vai C++ valodā, tas tiek nodots pēc vērtības: funkcija saņem argumenta vērtības kopiju, nevis sākotnējo mainīgo. Tas nozīmē, ka jebkura parametra piešķiršana funkcijas ietvaros ietekmē tikai lokālo kopiju un izsaucēja mainīgo atstāj nemainīgu.
Šāda uzvedība bieži vien ir vēlama — tā izolē funkcijas un novērš pārsteidzošas blakusparādības —, taču dažreiz tiešām ir nepieciešams, lai funkcija modificētu izsaucēja mainīgos. Jūs varētu apsvērt globālo mainīgo izmantošanu, taču, programmām augot, globālos mainīgos ātri kļūst grūti izsekot un tie ir pakļauti kļūdām.
Rādītāji piedāvā tīru alternatīvu: jūs nododat mainīgā adresi funkcijai, un funkcija pēc tam var mainīt vērtību šajā adresē. To sauc par “atsauces nodošanu caur rādītājiem”. C++ valodā var izmantot arī atsauces parametrus (int&), kas bieži vien ir vēl skaidrāki, taču rādītāja formas izpratne joprojām ir būtiska.
Iedomājieties funkciju double_value kam vajadzētu dubultot izsaucējā definēto veselo skaitli. Izmantojot uz rādītāja balstītu saskarni, jūs to deklarētu kā tādu, kas izmanto int*un izsauciet to, nododot sava mainīgā adresi: double_value(&k);Funkcijas iekšpusē *k = *k * 2; atjaunina sākotnējo vērtību, izmantojot rādītāju.
Šī metode arī ļauj funkcijai efektīvi "atgriezt" vairākus rezultātus, modificējot vairākus mainīgos, kuru adreses tika nodotas kā argumenti. Tā vietā, lai atgrieztu sarežģītu struktūru, var pieņemt vairākus rādītāja parametrus un atjaunināt tos visus. Mūsdienu C++ valodā skaidrības labad parasti dod priekšroku atsaucēm, kortežiem vai struktūrām, taču rādītāja parametri joprojām ir izplatīti zemā līmeņa API un C bibliotēkās.
Rādītāja aritmētika un masīvi
Viens no spēcīgākajiem – un bīstamākajiem – rādītāju aspektiem ir rādītāja aritmētika, īpaši masīvu kontekstā. C un C++ valodās masīvs tiek glabāts atmiņā kā blakus esošu elementu bloks, un masīva nosaukums, nododot to funkcijai vai izmantojot noteiktās izteiksmēs, var pārveidoties par rādītāju uz tā pirmo elementu.
Ja jūs deklarējat char h[] = {'P','r','o','m','e','t','e','c','\n'};, Tad h var uzskatīt par norādi uz h[0]. Piekļuve h[i] konceptuāli ir līdzvērtīgs skaitļošanai *(h + i), Kur h ir bāzes adrese un i ir nobīde elementos (nevis baitos). Kompilators reizina i pēc katra elementa lieluma (1 baits char, 4 baiti priekš intutt.) pirms pievienošanas rādītājam.
Tas nozīmē, ka, redzot tādu izteiksmi kā *(h + i), jūs veicat klasisko rādītāja aritmētiku: jūs pārvietojat rādītāju h by i pozīcijas un pēc tam atsaukt rezultāta atsauci. Veiktspējas apsvērumu dēļ kompilatori ļoti labi optimizē šo modeli, tāpēc C masīvi un rādītāji vēsturiski ir bijusi tik populāra kombinācija zemā līmeņa darbam.
Varat arī izveidot skaidru rādītāju uz masīva pirmo elementu un palielināt šo rādītāju, lai tas pārvietotos pa masīvu. Piemēram, deklarējot char* ptr = h; un pēc tam atkārtoti drukājot *ptr++ ciklā secīgi šķērsos katru rakstzīmi. Postfikss ++ pēc katras piekļuves pārvieto rādītāju uz nākamo masīva elementu.
Šis kompaktais stils ir idiomātisks C valodā, taču iesācējiem tas var šķist nesaprotams, tāpēc mūsdienu C++ valodā daudzi izstrādātāji dod priekšroku skaidrākām formām, piemēram, for cikli ar indeksiem vai uz diapazonu balstīti for cikli. Tomēr rādītāja aritmētikas izpratne ir neaizstājama, lai lasītu un uzturētu mantoto kodu, kā arī ieviestu veiktspējai kritiskas rutīnas.
Dinamiskā atmiņa, jauna/dzēsta un rādītāja iterācija
Rādītāji ir arī pamata rokturis, ko saņemat, dinamiski piešķirot objektus brīvajā krātuvē (bieži neformāli sauktā par kaudzi). C++ valodā operators new atgriež rādītāju uz jaunizveidoto objektu un delete atbrīvo atmiņu, kad tā vairs nav nepieciešama.
Piemēram, Student* p = new Student{...}; rezervē pietiekami daudz atmiņas vienam Student objektu un atgriež tā adresi. Pēc tam jūs izmantojat p->member lai piekļūtu tā dalībniekiem vai izsauktu tā metodes. Kad objekts vairs nav nepieciešams, delete p; iznīcina to un atbrīvo atmiņu atpakaļ brīvajā krātuvē.
C++ arī ļauj dinamiski sadalīt masīvus, izmantojot new[], kas atgriež rādītāju uz masīva pirmo elementu. Piemēram, Student* p = new Student[100]; atvēl vietu 100 Student atmiņā blakus izvietoti objekti ar p norāda uz elementu ar indeksu 0.
Izmantojot rādītāja aritmētiku, izteiksme p + i norāda uz išī masīva -tais elements, tātad (p + 4)->grade_1 ir līdzvērtīgs p[4].grade_1. Konceptuāli, p ir kā iterators, kas sākas ar pirmo elementu, un p + i virza šo iteratoru uz priekšu par i soļi gar masīvu.
Arī rādītāju atšķirībām ir nozīme: ja q = p + 4;, Tad q - p novērtē kā 4, kas ir elementu skaits starp šiem diviem rādītājiem. Šajā ziņā neapstrādāts rādītājs ir vienkāršākā nejaušās piekļuves iteratora forma. Daudzi STL konteineri atklāj iteratorus, kas darbojas līdzīgi, bet drošības un elastības labad slēpj neapstrādātā rādītāja informāciju.
Lai gan neapstrādāts new/delete ir jaudīgi, mūsdienu C++ stingri iesaka izmantot viedos rādītājus un RAII (Resource Acquisition Is Initialization), lai automātiski pārvaldītu resursus. Viedas norādes, piemēram, std::unique_ptr un std::shared_ptr iekapsulēt īpašumtiesības un automātiski atbrīvot atmiņu, kad tā vairs nav nepieciešama, samazinot noplūdes un dubultas dzēšanas risku.
Norādes uz norādēm un dziļāka netiešība
Kad būsiet apguvis vienkāršus rādītājus, neizbēgami sastapsies ar rādītājiem uz rādītājiem (un dažreiz pat augstākiem netiešības līmeņiem). Konceptuāli rādītājs uz rādītāju ir tikai vēl viens mainīgais, kas satur rādītāja mainīgā adresi, nevis tieši atsaucas uz veselu skaitli (int), dubulto skaitli (double) vai objektu.
Visacīmredzamākais lietošanas gadījums ir dinamiski piešķirtu rādītāju masīvu pārvaldība, piemēram, dinamiski veidota dinamiski piešķirtu virkņu tabula. Vienkāršā C valodā klasisks piemērs ir char** argv iekš main funkcija, kas ir rādītājs uz C stila virkņu masīvu, no kurām katra pati par sevi ir char*.
Vēl viens bieži sastopams scenārijs ir tad, kad funkcijai ir jāmodificē izsaucēja sniegtais rādītājs, ne tikai dati, uz kuriem tas norāda. Rādītāja nodošana rādītājam ļauj funkcijai mainīt objektu, uz kuru atsaucas sākotnējais rādītājs, vai arī inicializēt to, piešķirot jaunu objektu ar new or mallocIzsaucošais kods pēc tam redz atjaunināto rādītāja vērtību.
Vairāki netiešības līmeņi dabiski parādās arī noteiktās datu struktūrās, īpaši saistītajās datu struktūrās, kas izveidotas kaudzē. Piemēram, dinamiski veidots saistīts mezglu saraksts, kam piešķirts malloc or new var ietvert norādes uz mezgliem, kā arī funkcijas, kas saņem norādes uz šiem rādītājiem, lai ievietotu vai noņemtu elementus, vienlaikus atjauninot galveno rādītāju.
Un, protams, daudzdimensiju dinamiskie masīvi C stila saskarnēs parasti tiek attēloti kā norādes uz norādēm: “matrica” parasti tiek modelēta kā int**, kur katrs pirmās dimensijas elements ir rādītājs uz rindu masīvu. Mūsdienu C++ valodā jūs varētu dot priekšroku std::vector<std::vector<T>> vai pielāgotas matricas klases, taču rādītāja-rādītāja izkārtojumi joprojām ir būtiski zema līmeņa API un saistījumos.
Biežāk pieļautās kļūdas un laba prakse ar norādījumiem
Tiešs darbs ar rādītājiem sniedz jums precīzu kontroli, taču tas arī paver durvis uz smalkām un grūti atkļūdojamām kļūdām, ja neesat disciplinēts. Daudzas leģendāras kļūdas C un C++ kodu bāzēs rodas no neapstrādātu adrešu nepareizas apstrādes, vai nu rakstot atmiņā, kas jums nepieder, vai aizmirstot pārvaldīt objektu dzīves ilgumu.
Viena klasiska kļūda ir ierakstīt vairāk baitu, nekā mērķa tips var saturēt, vai nepareizi interpretēt atmiņas atrašanās vietas tipu. Piemēram, ja jūs uzglabājat long vērtība int mainīgais vai uzrakstiet long uz vietu, kas ir piemērota tikai int, jūs pārrakstāt blakus esošo atmiņu, potenciāli bojājot citus mainīgos vai pat koda rādītājus.
Vēl viens risks ir patvaļīgu skaitlisku vērtību piešķiršana tieši rādītāja mainīgajiem, piemēram ptrNum = 7;, ja vien neveicat ārkārtīgi zema līmeņa sistēmas darbu un precīzi nezināt, kas atrodas šajā adresē. Parastā lietojumprogrammas kodā vesela skaitļa apstrāde kā adrese ir tieša saikne ar nedefinētu uzvedību un neregulārām avārijām.
Arī rādītāju pareizas inicializācijas aizmirstība ir riskanta: rādītājs ar nenoteiktu vērtību var izskatīties labi, bet var norādīt uz jebkuru vietu atmiņā. Vienmēr inicializējiet rādītājus — vai nu uz derīgu adresi, vai uz nullptr – un pirms atsauču noņemšanas pārbaudiet tos, ja rodas šaubas, ka tie varētu būt tukši.
Visbeidzot, ar dinamisko atmiņu katra neapstrādātā new jāatbilst tieši vienam atbilstošam delete, un katru new[] ar tieši vienu delete[]. Noplūdes rodas, ja pazaudējat piekļuvi dinamiski piešķirtajai atmiņai, to neizdzēšot, un dubultā dzēšana (vai atmiņas, kas jums nepieder, dzēšana) bojā piešķirtāja iekšējās struktūras, kas parasti parādās kā periodiski un ļoti grūti reproducējami defekti.
Ja ar tiem rīkojas uzmanīgi, tie drīzāk ir ass, labi līdzsvarots rīks, nevis nejaušs haosa avots: tie ļauj spriest par programmas atmiņas izkārtojumu, izstrādāt efektīvas datu struktūras un veidot spēcīgas abstrakcijas, izmantojot zemākā līmeņa kontroli. Prakses gaitā pārvietošanās starp adresēm, atsauču noņemšana un izpratne par to, kā funkcijas un masīvi mijiedarbojas ar rādītājiem, kļūst par otro dabu, un sākotnējās bailes nomaina veselīga cieņa un daudz praktiskas lietderības.
