Atminties paskirstymas c naujas. Dinaminis atminties paskirstymas C. Dinaminis atminties paskirstymas vienmačiams masyvams

Atradome dinaminio atminties paskirstymo galimybes. Ką tai reiškia? Tai reiškia, kad naudojant dinaminį atminties paskirstymą, atmintis rezervuojama ne kompiliavimo, o programos vykdymo etape. Ir tai suteikia mums galimybę efektyviau paskirstyti atmintį, daugiausia masyvams. Naudojant dinaminį atminties paskirstymą, mums nereikia iš anksto nustatyti masyvo dydžio, juolab, kad ne visada žinoma, kokio dydžio turi būti masyvas. Toliau pažiūrėkime, kaip galima paskirstyti atmintį.

Atminties paskirstymas C (malloc funkcija)

Funkcija malloc() yra apibrėžta stdlib.h antraštės faile ir naudojama rodyklėms inicijuoti reikiamą atminties kiekį. Atmintis skiriama iš RAM sektoriaus, prieinamo visoms šiame įrenginyje veikiančioms programoms. Argumentas yra atminties baitų, kuriuos reikia paskirstyti, skaičius; funkcija grąžina žymeklį į atmintyje skirtą bloką. Funkcija malloc () veikia kaip ir bet kuri kita funkcija, nieko naujo.

Kadangi skirtingi duomenų tipai turi skirtingus atminties reikalavimus, turime išmokti gauti skirtingų duomenų tipų baitų dydį. Pavyzdžiui, mums reikia atminties dalies int tipo reikšmių masyvei – tai yra vienas atminties dydis, o jei reikia skirti atmintį tokio paties dydžio, bet tipo char masyvei, tai yra skirtingo dydžio. Todėl reikia kažkaip apskaičiuoti atminties dydį. Tai galima padaryti naudojant sizeof() operaciją, kuri paima išraišką ir grąžina jos dydį. Pavyzdžiui, sizeof(int) grąžins baitų skaičių, reikalingą int reikšmei išsaugoti. Pažiūrėkime į pavyzdį:

#įtraukti int *ptrVar = malloc(dydis(int));

Šiame pavyzdyje, in 3 eilutė PtrVar žymekliui priskiriamas adresas atminties vietai, kurios dydis atitinka int duomenų tipą. Automatiškai ši atminties sritis tampa neprieinama kitoms programoms. Tai reiškia, kad po to, kai skirta atmintis tampa nereikalinga, ji turi būti aiškiai atlaisvinta. Jei atmintis nėra aiškiai atlaisvinta, pasibaigus programai, operacinės sistemos atmintis nebus atlaisvinta, tai vadinama atminties nutekėjimu. Taip pat galite nustatyti skirtos atminties, kurią reikia skirti, dydį perleisdami nulinę žymeklį. Štai pavyzdys:

Int *ptrVar = malloc(dydis(*ptrVar));

Kas čia vyksta? Operacija sizeof(*ptrVar) įvertins žymeklio nurodytos atminties vietos dydį. Kadangi ptrVar yra int atminties vietos rodyklė, sizeof() grąžins sveikojo skaičiaus dydį. Tai yra iš tikrųjų, remiantis pirmąja rodyklės apibrėžimo dalimi, apskaičiuojamas antrosios dalies dydis. Tai kam mums to reikia? To gali prireikti, jei staiga reikia pakeisti žymeklio apibrėžimą, pvz., int , kad jis būtų plūduriuojantis ir tada mums nereikia keisti duomenų tipo dviejose rodyklės apibrėžimo dalyse. Užteks, jei pakeisime pirmąją dalį:

Float *ptrVar = malloc(dydis(*ptrVar));

Kaip matote, šis žymėjimas turi vieną labai stiprų tašką – neturėtume kviesti funkcijos malloc() naudodami sizeof(float) . Vietoj to, mes perdavėme rodyklę į float tipą į malloc(), tokiu atveju paskirstytos atminties dydis bus automatiškai nustatytas!

Tai ypač naudinga, jei reikia skirti atmintį toli nuo žymeklio apibrėžimo:

Plūduriuoti *ptrVar; /*. . . šimtas kodo eilučių */ . . . ptrVar = malloc(dydis(*ptrVar));

Jei naudotumėte atminties paskirstymo konstrukciją su operacija sizeof(), tai kode tektų rasti rodyklės apibrėžimą, pasižiūrėti jo duomenų tipą ir tik tada būtų galima teisingai paskirstyti atmintį.

Atlaisvinama skirta atmintis

Atmintis atlaisvinama naudojant funkciją free(). Štai pavyzdys:

Nemokama(ptrVar);

Atlaisvinus atmintį, gera praktika yra iš naujo nustatyti žymeklį į nulį, tai yra, priskirti *ptrVar = 0 . Jei žymekliui priskiriate 0, rodyklė tampa niekine, kitaip tariant, ji neberodo nieko. Atlaisvinus atmintį, visada žymekliui priskirkite 0, kitu atveju, net ir atlaisvinus atmintį, žymeklis vis tiek nukreipia į ją, vadinasi galite netyčia pakenkti kitoms programoms, kurios gali naudoti šią atmintį, bet apie tai net nieko nežinote tai sužinosite ir pamanysite, kad programa veikia tinkamai.

P.S.: Visi, kurie domisi vaizdo įrašų redagavimu, gali susidomėti šia „Windows 7“ vaizdo įrašų redagavimo priemone. Vaizdo įrašų rengyklė vadinasi Movavi, gal kas jau yra susipažinęs ar net dirbo. Naudodami šią programą rusų kalba galite lengvai pridėti vaizdo įrašą iš savo fotoaparato, pagerinti kokybę ir pritaikyti gražius vaizdo efektus.

C++ kalboje, kaip ir daugelyje kitų kalbų, atmintis gali būti paskirstoma statiškai (atmintis paskirstoma prieš programos vykdymą ir atlaisvinama pasibaigus programai) arba dinamiškai (atmintis paskirstoma ir atlaisvinama programos vykdymo metu).

Statinis atminties paskirstymas atliekamas visiems globaliems ir vietiniams kintamiesiems, kurie turi aiškias deklaracijas programoje (nenaudojant rodyklių). Šiuo atveju atminties paskirstymo mechanizmą lemia kintamojo aprašymo vieta programoje ir atminties klasės specifikacija aprašyme. Kintamojo tipas lemia skiriamos atminties srities dydį, tačiau atminties paskirstymo mechanizmas nuo tipo nepriklauso. Yra du pagrindiniai statinės atminties paskirstymo mechanizmai.

· Atmintis kiekvienam visuotiniam ir statiniam (deklaruojamam su statiniu specifikatoriumi) kintamiesiems paskirstoma prieš pradedant programos vykdymą pagal tipo aprašymą. Nuo programos vykdymo pradžios iki pabaigos šie kintamieji yra susieti su jiems skirta atminties sritimi. Taigi jie galioja visame pasaulyje, tačiau jų matomumas skiriasi.

· Vietiniams kintamiesiems, deklaruojamiems bloko viduje ir be specifikacijos statinis atmintis paskirstoma kitaip. Prieš pradedant vykdyti programą (kai ji įkeliama), yra skiriama gana didelė atminties sritis, vadinama krūva(kartais vartojami terminai programų krūva arba skambučių kamino kad būtų galima atskirti krūvą kaip abstrakčių duomenų tipą). Stacko dydis priklauso nuo kūrimo aplinkos, pavyzdžiui, MS Visual C++, pagal nutylėjimą dėkui yra skiriamas 1 megabaitas (šią reikšmę galima pritaikyti). Programos vykdymo metu, įvedant tam tikrą bloką, bloke lokalizuotiems kintamiesiems (pagal jų tipo aprašymą) yra skiriama atmintis, o išėjus iš bloko ši atmintis atlaisvinama. Šie procesai atliekami automatiškai, todėl vietiniai kintamieji C++ kalboje dažnai vadinami automatinis.

Kai funkcija iškviečiama, krūvoje paskirstoma atmintis jos vietiniams kintamiesiems, parametrams (parametrų reikšmė arba adresas dedamas ant krūvos), funkcijos rezultatui ir išsaugant grįžimo tašką - adresą programoje, kur turite grįžti, kai funkcija bus baigta. Kai funkcija išjungiama, visi su ja susiję duomenys pašalinami iš kamino.

Sąvokos „stekas“ vartojimą paaiškinti nesunku – taikant priimtą atminties paskirstymo ir atskyrimo metodą, pirmiausia iš jo pašalinami kintamieji, kurie yra paskutiniai krūvoje (tai yra kintamieji, lokalizuoti giliausiame įdėtame bloke). Tai yra, atminties paskirstymas ir išleidimas vyksta pagal LIFO principą (LAST IN – FIRST OUT, last in – first out). Tai yra kamino veikimo principas. Kitame skyriuje pažvelgsime į steką kaip į dinamišką duomenų struktūrą ir galimą jos įgyvendinimą.



Daugeliu atvejų statiškai paskirstyta atmintis ją išnaudoja neefektyviai (tai ypač pasakytina apie didelius masyvus), nes statiškai paskirstyta atminties sritis ne visada iš tikrųjų užpildoma duomenimis. Todėl C++, kaip ir daugelyje kalbų, yra patogių priemonių dinamiškai generuoti kintamuosius. Dinaminio atminties paskirstymo esmė yra ta, kad atmintis yra paskirstoma (užfiksuojama) paprašius iš programos ir taip pat atlaisvinama pagal užklausą. Šiuo atveju atminties dydis gali būti nustatomas pagal kintamojo tipą arba aiškiai nurodytas užklausoje. Tokie kintamieji vadinami dinamiškas. Galimybė kurti ir naudoti dinaminius kintamuosius yra glaudžiai susijusi su rodyklės mechanizmu.

Apibendrinant visa tai, kas išdėstyta aukščiau, galime įsivaizduoti tokią atminties paskirstymo schemą programos vykdymo metu (2.1 pav.). Paveikslėlyje esančių sričių vieta viena kitos atžvilgiu yra gana savavališka, nes Operacinė sistema rūpinasi atminties paskirstymo detalėmis.

2.1 pav. – atminties paskirstymo diagrama

Baigdami šį skyrių, palieskime vieną skaudžią problemą dirbant su kaminu – jo perpildymo galimybę (ši avarinė situacija dažniausiai vadinama Stack Overflow). Priežastis, dėl kurios kilo problema, yra aiški – ribotas atminties kiekis, kuris skiriamas kaminui įkeliant programą. Labiausiai tikėtinos dėklo perpildymo situacijos yra dideli lokalūs masyvai ir gilus rekursinių funkcijų iškvietimų įdėjimas (dažniausiai tai atsitinka, kai rekursyvios funkcijos programuojamos netiksliai, pavyzdžiui, pamirštama kokia nors terminalo šaka).



Norint geriau suprasti krūvos perpildymo problemą, rekomenduojame atlikti šį paprastą eksperimentą. Funkcijoje pagrindinis paskelbti sveikųjų skaičių masyvą su, tarkime, milijono elementų dydžiu. Programa bus kompiliuojama, tačiau ją paleidus įvyks dėklo perpildymo klaida. Dabar pridėkite specifikaciją prie masyvo aprašymo pradžios statinis(arba išimkite masyvo deklaraciją iš funkcijos pagrindinis) – programa veiks!

Čia nėra nieko stebuklingo - tiesiog dabar masyvas dedamas ne ant krūvos, o globalių ir statinių kintamųjų srityje. Šios srities atminties dydį nustato kompiliatorius - jei programa sukompiliuota, tada ji veiks.

Tačiau, kaip taisyklė, programoje nereikia deklaruoti statiškai sugeneruotų didžiulių dydžių masyvų. Daugeliu atvejų dinamiškas atminties paskirstymas tokiems duomenims bus efektyvesnis ir lankstesnis būdas.

Norint efektyviai naudoti kompiuterio atmintį, būtinas dinaminis atminties paskirstymas. Pavyzdžiui, mes parašėme kažkokią programą, kuri apdoroja masyvą. Rašant šią programą reikėjo deklaruoti masyvą, tai yra suteikti jam fiksuotą dydį (pvz., nuo 0 iki 100 elementų). Tada ši programa nebus universali, nes gali apdoroti ne daugiau kaip 100 elementų masyvą. Ką daryti, jei mums reikia tik 20 elementų, o atmintyje vietos skirta 100 elementų, nes masyvo deklaracija buvo statiška, o toks atminties panaudojimas itin neefektyvus.

C++ kalboje naujos ir trynimo operacijos naudojamos dinamiškai paskirstyti kompiuterio atmintį. Nauja operacija paskirsto atmintį iš laisvos atminties srities, o trynimo operacija atlaisvina skirtą atmintį. Paskirstyta atmintis ją panaudojus turi būti atlaisvinta, todėl naujos ir trynimo operacijos naudojamos poromis. Net jei aiškiai neatlaisvinsite atminties, ji bus atlaisvinta OS išteklių, kai programa baigsis. Vis tiek rekomenduoju nepamiršti trynimo operacijos.

// operacijos new int naudojimo pavyzdys *ptrvalue = new int; //kur ptrvalue yra žymeklis į paskirtą int tipo atminties sritį //new yra laisvos atminties skyrimo sukurtam objektui operacija.

Nauja operacija sukuria nurodyto tipo objektą, paskiria jam atmintį ir grąžina tinkamo tipo rodyklę į nurodytą atminties vietą. Jei atminties paskirstyti negalima, pavyzdžiui, jei nėra laisvų sričių, tada grąžinamas nulinis rodyklė, tai yra, rodyklė grąžins reikšmę 0. Atminties paskirstymas galimas bet kokio tipo duomenims: tarpt, plūdė,dvigubai,char ir tt

// trynimo operacijos naudojimo pavyzdys: delete ptrvalue; // kur ptrvalue yra žymeklis į paskirtą int tipo atminties sritį // trynimas yra atminties atleidimo operacija

Sukurkime programą, kurioje bus sukurtas dinaminis kintamasis.

// new_delete.cpp: apibrėžia konsolės programos įėjimo tašką. #include "stdafx.h" #include << "ptrvalue = "<< *ptrvalue << endl; ištrinti ptrvalue; // atminties sistemos atlaisvinimas ("pause"); return 0; }

// kodas Code::Blocks

// Dev-C++ kodas

// new_delete.cpp: apibrėžia konsolės programos įėjimo tašką. #įtraukti naudojant vardų sritį std; int main(int argc, char* argv) ( int *ptrvalue = new int; // dinaminis atminties paskirstymas objektui, kurio tipas yra int *ptrvalue = 9; // objekto inicijavimas naudojant žymeklį //int *ptrvalue = new int ( 9); inicijavimas gali būti atliktas iš karto, kai deklaruojamas dinaminis cout objektas<< "ptrvalue = "<< *ptrvalue << endl; ištrinti ptrvalue; // atminties išlaisvinimas return 0; )

B linija 10 rodomas būdas deklaruoti ir inicijuoti dinaminį objektą su devyniais; viskas, ką jums reikia padaryti, tai nurodyti reikšmę skliausteliuose po duomenų tipo. Programos rezultatas parodytas 1 pav.

Ptrvalue = 9 Norėdami tęsti, paspauskite bet kurį klavišą. . .

1 paveikslas – dinaminis kintamasis

Dinaminių masyvų kūrimas

Kaip minėta anksčiau, masyvai taip pat gali būti dinamiški. Dažniausiai naujos ir trynimo operacijos naudojamos dinaminiams masyvams kurti, o ne dinaminiams kintamiesiems kurti. Pažiūrėkime į kodo fragmentą, skirtą vienmačiui dinaminei masyvai sukurti.

// vienmačio 10 elementų dinaminio masyvo deklaravimas: float *ptrarray = new float ; // kur ptrarray yra žymeklis į paskirtą atminties sritį realiųjų skaičių masyvei float tipo // laužtiniuose skliaustuose nurodome masyvo dydį

Kai dinaminis masyvas tapo nereikalingas, turite atlaisvinti jam skirtą atminties sritį.

// atlaisvinama atmintis, skirta vienmačiui dinaminei masyvai: ištrinti ptrarray;

Po trynimo operatoriaus dedami laužtiniai skliaustai, rodantys, kad atleidžiama vienmačiam masyvei skirta atminties dalis. Sukurkime programą, kurioje sukursime vienmatį dinaminį masyvą, užpildytą atsitiktiniais skaičiais.

// new_delete_array.cpp: apibrėžia konsolės programos įėjimo tašką. #įtraukti"stdafx.h" #include !} // antraštės faile // antraštės faile < 10; count++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 cout << "array = "; for (int count = 0; count < 10; count++) cout << setprecision(2) << ptrarray << " "; delete ptrarray; // высвобождение памяти cout << endl; system("pause"); return 0; }

// kodas Code::Blocks

// Dev-C++ kodas

// new_delete_array.cpp: apibrėžia konsolės programos įėjimo tašką. #įtraukti // antraštės faile yra laiko() funkcijos #include prototipas // antraštės faile yra funkcijos setprecision() prototipas #include #įtraukti naudojant vardų sritį std; int main(int argc, char* argv) ( srand(time(0)); // atsitiktinių skaičių generavimas float *ptrarray = new float ; // sukuriamas dinaminis realiųjų skaičių masyvas su dešimčia elementų, skirtas (int count = 0; skaičiuoti< 10; count++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 cout << "array = "; for (int count = 0; count < 10; count++) cout << setprecision(2) << ptrarray << " "; delete ptrarray; // высвобождение памяти cout << endl; system("pause"); return 0; }

Sukurtas vienmatis dinaminis masyvas užpildomas atsitiktiniais realiaisiais skaičiais, gautais naudojant atsitiktinių skaičių generavimo funkcijas, ir generuojami skaičiai intervale nuo 1 iki 10, intervalas nustatomas taip - rand() % 10 + 1 . Norint gauti atsitiktinius realiuosius skaičius, atliekama padalijimo operacija, naudojant eksplicitinį vardiklio tipą - float((rand() % 10 + 1)) . Norėdami parodyti tik du skaitmenis po kablelio, naudojame funkciją setprecision(2). , šios funkcijos prototipas yra antraštės faile . Funkcija laikas(0) atsitiktinių skaičių generatorių pasėja laikinąja reikšme, taip atkartodama skaičių atsiradimo atsitiktinumą (žr. 2 pav.).

Masyvas = 0,8 0,25 0,86 0,5 2,2 10 1,2 0,33 0,89 3,5 Norėdami tęsti, paspauskite bet kurį klavišą. . .

2 pav. Dinaminis masyvas C++

Baigus darbą su masyvu, jis ištrinamas ir taip atlaisvinama jo saugojimui skirta atmintis.

Išmokome kurti ir dirbti su vienmačiais dinaminiais masyvais. Dabar pažiūrėkime į kodo dalį, kuri parodo, kaip deklaruoti dvimatį dinaminį masyvą.

// 10 elementų dvimačio dinaminio masyvo deklaravimas: float **ptrarray = new float* ; // dvi masyvo eilutės, skirtos (int count = 0; count< 2; count++) ptrarray = new float ; // и пять столбцов // где ptrarray – массив указателей на выделенный участок памяти под массив вещественных чисел типа float

Pirma, deklaruojamas antros eilės rodyklės plaukiojantis **ptrarray, kuris nurodo plūduriuojančių* rodyklių masyvą, kur masyvo dydis yra du . Po to cikle for kiekviena masyvo eilutė deklaruojama 2 eilutė atmintis skirta penkiems elementams. Rezultatas yra dvimatis dinaminis masyvas ptrarray. Apsvarstykite pavyzdį, kaip atlaisvinti atmintį, skirtą dvimačiui dinaminei masyvai.

// atlaisvinama atmintis, skirta dvimačiui dinaminei masyvei: for (int count = 0; count< 2; count++) delete ptrarray; // где 2 – количество строк в массиве

Dvimačio dinaminio masyvo deklaravimas ir ištrynimas atliekamas naudojant kilpą, kaip parodyta aukščiau, turite suprasti ir atsiminti, kaip tai daroma. Sukurkime programą, kurioje sukursime dvimatį dinaminį masyvą.

// new_delete_array2.cpp: apibrėžia konsolės programos įėjimo tašką. #include "stdafx.h" #include #įtraukti #įtraukti < 2; count++) ptrarray = new float ; // и пять столбцов // заполнение массива for (int count_row = 0; count_row < 2; count_row++) for (int count_column = 0; count_column < 5; count_column++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 // вывод массива for (int count_row = 0; count_row < 2; count_row++) { for (int count_column = 0; count_column < 5; count_column++) cout << setw(4) <

// kodas Code::Blocks

// Dev-C++ kodas

// new_delete_array2.cpp: apibrėžia konsolės programos įėjimo tašką. #įtraukti #įtraukti #įtraukti #įtraukti naudojant vardų sritį std; int main(int argc, char* argv) ( srand(time(0)); // atsitiktinių skaičių generavimas // dinamiškai sukuriamas dvimatis realiųjų skaičių masyvas su dešimčia elementų float **ptrarray = new float* ; // dvi masyvo eilutės, skirtos (int count = 0; count< 2; count++) ptrarray = new float ; // и пять столбцов // заполнение массива for (int count_row = 0; count_row < 2; count_row++) for (int count_column = 0; count_column < 5; count_column++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 // вывод массива for (int count_row = 0; count_row < 2; count_row++) { for (int count_column = 0; count_column < 5; count_column++) cout << setw(4) <

Išvedant masyvą buvo naudojama funkcija setw(); jei prisimenate, ji išvesties duomenims skiria tam tikro dydžio vietą. Mūsų atveju kiekvienam masyvo elementui yra keturios pozicijos, tai leidžia išlyginti skirtingo ilgio skaičius išilgai stulpelių (žr. 3 pav.).

2,7 10 0,33 3 1,4 6 0,67 0,86 1,2 0,44 Norėdami tęsti, paspauskite bet kurį klavišą. . .

3 pav. Dinaminis masyvas C++

Prieš gilindamiesi į objektinį kūrimą, turime padaryti trumpą nukrypimą apie darbą su atmintimi C++ programoje. Negalėsime parašyti jokios sudėtingos programos, jei vykdymo metu negalėsime skirti atminties ir jos pasiekti.
C++ kalboje objektai gali būti paskirstyti statiškai kompiliavimo metu arba dinamiškai vykdymo metu, iškviečiant funkcijas iš standartinės bibliotekos. Pagrindinis skirtumas naudojant šiuos metodus yra jų efektyvumas ir lankstumas. Statinis paskirstymas yra efektyvesnis, nes atminties paskirstymas vyksta prieš paleidžiant programą, tačiau jis yra daug mažiau lankstus, nes turime iš anksto žinoti paskirstomo objekto tipą ir dydį. Pavyzdžiui, visai nelengva sudėti tekstinio failo turinį į statinį eilučių masyvą: turime iš anksto žinoti jo dydį. Užduotys, kurioms reikia saugoti ir apdoroti nežinomą elementų skaičių, paprastai reikalauja dinaminio atminties paskirstymo.
Iki šiol visuose mūsų pavyzdžiuose buvo naudojamas statinis atminties paskirstymas. Tarkime, apibrėžiant kintamąjį ival

Int ival = 1024;

verčia kompiliatorių atmintyje skirti pakankamai didelę sritį, kad būtų galima saugoti int tipo kintamąjį, susieti pavadinimą ival su šia sritimi ir įdėti reikšmę 1024. Visa tai daroma kompiliavimo etape, prieš paleidžiant programą.
Su ival objektu yra susietos dvi reikšmės: tikroji kintamojo reikšmė, šiuo atveju 1024, ir atminties srities, kurioje saugoma ši reikšmė, adresas. Galime nurodyti bet kurį iš šių dviejų dydžių. Kai rašome:

Int ival2 = ival + 1;

tada pasiekiame kintamajame ival esančią reikšmę: pridedame prie jo 1 ir inicijuojame ival2 kintamąjį su šia nauja reikšme 1025. Kaip galime pasiekti adresą, kuriame yra kintamasis?
C++ turi įmontuotą rodyklės tipą, kuris naudojamas objektų adresams saugoti. Norėdami paskelbti rodyklę, kurioje yra ival kintamojo adresas, turime parašyti:

Int *pintas; // rodyklė į objektą, kurio tipas int

Taip pat yra speciali adreso paėmimo operacija, žymima simboliu &. Jo rezultatas yra objekto adresas. Šis sakinys pintos žymekliui priskiria ival kintamojo adresą:

Int *pintas; pint = // pint gauna ival adreso reikšmę

Objektą, kurio adresu yra pint (mūsų atveju ival), galime pasiekti naudodami operaciją nuorodų panaikinimas, taip pat vadinama netiesioginis kreipimasis. Ši operacija pažymėta simboliu *. Štai kaip netiesiogiai pridėti jį prie „ival“ naudojant jo adresą:

*pintas = *pintas + 1; // netiesiogiai padidina ival

Ši išraiška daro lygiai tą patį kaip

Ival = ival + 1; // aiškiai padidina ival

Šiame pavyzdyje nėra jokios prasmės: žymeklio naudojimas netiesiogiai manipuliuoti ival kintamuoju yra mažiau efektyvus ir ne toks aiškus. Šį pavyzdį pateikėme tik norėdami pateikti labai paprastą rodyklių idėją. Tiesą sakant, rodyklės dažniausiai naudojamos manipuliuoti dinamiškai priskirtais objektais.
Pagrindiniai statinio ir dinaminio atminties paskirstymo skirtumai yra šie:

  • statiniai objektai atvaizduojami pavadintais kintamaisiais, o veiksmai su šiais objektais atliekami tiesiogiai naudojant jų pavadinimus. Dinaminiai objektai neturi tikrinių pavadinimų, o veiksmai su jais atliekami netiesiogiai, naudojant rodykles;
  • Kompiliatorius automatiškai paskirsto ir atlaisvina atmintį statiniams objektams. Programuotojui pačiam dėl to jaudintis nereikia. Už dinaminių objektų atminties paskirstymą ir atlaisvinimą visiškai atsako programuotojas. Tai gana sudėtinga užduotis, kurią sprendžiant lengva padaryti klaidų. Operatoriai new ir delete naudojami dinamiškai paskirstyta atmintimi valdyti.

Naujasis operatorius turi dvi formas. Pirmoji forma paskirsto atmintį vienam tam tikro tipo objektui:

Int *pint = naujas int(1024);

Čia naujas operatorius paskirsto atmintį neįvardytam int tipo objektui, inicijuoja jį reikšme 1024 ir grąžina sukurto objekto adresą. Šis adresas naudojamas pintos žymekliui inicijuoti. Visi veiksmai su tokiu neįvardytu objektu atliekami panaikinus nuorodą į šią žymeklį, nes Neįmanoma tiesiogiai manipuliuoti dinamišku objektu.
Antroji naujojo operatoriaus forma paskiria atmintį tam tikro dydžio masyvei, susidedančiam iš tam tikro tipo elementų:

Int *pia = naujas int;

Šiame pavyzdyje atmintis yra skirta keturių int elementų masyvai. Deja, ši naujojo operatoriaus forma neleidžia inicijuoti masyvo elementų.
Sumaištį sukelia tai, kad abi naujojo operatoriaus formos grąžina tą patį žymeklį, mūsų pavyzdyje tai yra sveikojo skaičiaus rodyklė. Pint ir pia yra deklaruojami lygiai taip pat, bet pint nurodo vieną int objektą, o pia nurodo pirmąjį keturių int objektų masyvo elementą.
Kai dinaminis objektas nebereikalingas, turime aiškiai atlaisvinti jam skirtą atmintį. Tai atliekama naudojant trynimo operatorių, kuris, kaip ir naujas, turi dvi formas - vienam objektui ir masyvei:

// vieno objekto atlaisvinimas trinti pint; // masyvo atlaisvinimas delete pia;

Kas nutiks, jei pamiršime atlaisvinti skirtą atmintį? Atmintis bus išeikvota, ji bus nenaudojama, bet jos negalima grąžinti į sistemą, nes neturime į ją rodyklės. Šis reiškinys gavo specialų pavadinimą atminties nutekėjimas. Galiausiai programa sugenda dėl atminties trūkumo (jei ji veikia pakankamai ilgai, žinoma). Nedidelį nuotėkį gali būti sunku aptikti, tačiau yra paslaugų, kurios gali padėti tai padaryti.
Mūsų sutrumpinta dinaminės atminties paskirstymo ir rodyklės naudojimo apžvalga tikriausiai sukėlė daugiau klausimų nei atsakė. 8.4 skirsnyje bus išsamiai aptariami su tuo susiję klausimai. Tačiau mes negalėjome išsiversti be šio nukrypimo, nes „Array“ klasė, kurią ketiname sukurti tolesniuose skyriuose, yra pagrįsta dinamiškai paskirstytos atminties naudojimu.

2.3 pratimas

Paaiškinkite skirtumą tarp keturių objektų:

a) int ival = 1024; (b) int *pi = (c) int *pi2 = naujas int(1024); d) int *pi3 = naujas int;

2.4 pratimas

Ką daro šis kodo fragmentas? Kas yra loginė klaida? (Atkreipkite dėmesį, kad indekso() operacija yra teisingai pritaikyta pia rodyklei. Šio fakto paaiškinimą rasite 3.9.2 skyriuje.)

Int *pi = naujas int(10); int *pia = naujas int;
while (*pi< 10) {
pia[*pi] = *pi; *pi = *pi + 1;
) ištrinti pi; ištrinti pia;

Darbas su dinamine atmintimi dažnai yra daugelio algoritmų kliūtis, nebent būtų naudojami specialūs triukai.

Šiame straipsnyje apžvelgsiu keletą tokių metodų. Straipsnyje pateikti pavyzdžiai skiriasi (pavyzdžiui, nuo) tuo, kad naudojamasi perkraunant naujus ir ištrinti operatorius, todėl sintaksės struktūros bus minimalistinės, o programos pertvarkymas – paprastas. Taip pat aprašomos proceso metu aptiktos spąstai (žinoma, guru, perskaitę standartą nuo viršelio iki viršelio, nenustebins).

0. Ar mums reikia rankinio darbo su atmintimi?

Pirmiausia patikrinkime, kiek išmanusis skirstytuvas gali pagreitinti atminties darbą.

Parašykime paprastus C++ ir C# testus (C# garsėja puikia atminties tvarkykle, kuri skirsto objektus į kartas, naudoja skirtingus telkinius skirtingo dydžio objektams ir pan.).

Klasės mazgas (viešas: Node* kitas; ); // ... for (int i = 0; i< 10000000; i++) { Node* v = new Node(); }

Class Node ( public Node next; ) // ... for (int l = 0; l< 10000000; l++) { var v = new Node(); }

Nepaisant viso pavyzdžio „sferinio vakuumo“ pobūdžio, laiko skirtumas buvo 10 kartų (62 ms prieš 650 ms). Be to, C# pavyzdys baigtas ir pagal geros manieros taisykles C++ turi būti ištrinti pasirinkti objektai, o tai dar labiau padidins tarpą (iki 2580 ms).

1. Objektų telkinys

Akivaizdus sprendimas yra paimti didelį atminties bloką iš OS ir padalinti jį į vienodus dydžio (Node) dydžio blokus, paskirstant atmintį paimti bloką iš telkinio, o atlaisvinant – grąžinti į baseiną. Lengviausias būdas organizuoti baseiną yra naudoti atskirai susietą sąrašą (steką).

Kadangi tikslas yra minimalus įsikišimas į programą, viskas, ką galima padaryti, yra pridėti „BlockAlloc“ mišinį į „Node“ klasę:
klasės mazgas: viešasis BlockAlloc

Visų pirma, mums reikia didelių blokų (puslapių), kuriuos paimame iš OS arba C-runtime, telkinio. Jį galima organizuoti kartu su malloc ir laisvomis funkcijomis, tačiau siekiant didesnio efektyvumo (norėdami praleisti papildomą abstrakcijos lygį), naudojame VirtualAlloc/VirtualFree. Šios funkcijos paskirsto atmintį 4K blokų kartotiniams ir taip pat rezervuoja proceso adresų erdvę 64K blokų kartotiniams. Vienu metu nurodydami įsipareigojimo ir rezervavimo parinktis, pereiname į kitą abstrakcijos lygį, rezervuojame adresų erdvę ir paskirstome atminties puslapius vienu skambučiu.

PagePool klasė

inline size_t align(dydis_t x, dydis_t a) ( return ((x-1) | (a-1)) + 1; ) //#define align(x, a) ((((x)-1) | ( (a)-1)) + 1) šabloną class PagePool ( viešas: void* GetPage() ( void* page = VirtualAlloc(NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(puslapis); grįžti puslapis; ) ~PagePool() ( for (vektorius) ::iteratorius i = puslapiai.begin(); i != puslapiai.pabaiga(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) privatus: vektorius puslapiai; );

Tada organizuojame tam tikro dydžio blokų baseiną

BlockPool klasė

šabloną klasė BlockPool: PagePool ( viešas: BlockPool() : head(NULL) ( BlockSize = align(sizeof(T), Alignment); count = PageSize / BlockSize; ) void* AllocBlock() ( // todo: lock(this) if (!head) FormatNewPage(); void* tmp = head; head = *(void**)head; return tmp; ) void FreeBlock(void* tmp) ( // todo: lock(this) *(void**)tmp = head; head = tmp; ) private: void* head; size_t BlockSize; size_t count; void FormatNewPage() ( void* tmp = GetPage(); head = tmp; for(size_t i = 0; i< count-1; i++) { void* next = (char*)tmp + BlockSize; *(void**)tmp = next; tmp = next; } *(void**)tmp = NULL; } };

komentuoti // Todo: užrakinti (tai) Vietos, kurioms reikalingas sinchronizavimas tarp gijų, yra pažymėtos (pvz., naudokite EnterCriticalSection arba boost::mutex).

Paaiškinsiu, kodėl „formatuojant“ puslapį, „FreeBlock“ abstrakcija nenaudojama blokui įtraukti į telkinį. Jei kažkas panašaus būtų parašyta

For (dydis_t i = 0; i< PageSize; i += BlockSize) FreeBlock((char*)tmp+i);

Tada puslapis, naudojant FIFO principą, būtų pažymėtas „atvirkščiai“:

Keli blokai, kurių prašoma iš telkinio iš eilės, turėtų mažėjančius adresus. Tačiau procesorius nemėgsta eiti atgal, tai sulaužo išankstinį iškvietimą ( UPD: Neaktualu šiuolaikiniams procesoriams). Jei žymėjimą darote ciklo būdu
for (dydis_t i = Puslapio dydis-(BlockSize-(PageSize%BlockSize)); i != 0; i -= BlockSize) FreeBlock...
tada žymėjimo ciklas eitų atgal į adresus.

Dabar, kai paruošiamieji darbai baigti, galime apibūdinti mixin klasę.
šabloną class BlockAlloc ( public: static void* operatorius new(size_t s) ( if (s != sizeof(T)) ( return::operator new(s); ) return pool.AllocBlock(); ) static void operator delete(void * m, dydis_t s) ( if (s != sizeof(T)) ( ::operator delete(m); ) else if (m != NULL) ( baseinas.... static void* operatorius new(size_t, void) * m) ( return m; ) // ...ir įspėjimas apie trūkstamą vietą ištrinti... static void operator delete(void*, void*) ( ) private: static BlockPool baseinas; ); šabloną BlockPool BlockAlloc ::baseinas;

Paaiškinsiu, kodėl reikalingi patikrinimai jei (s ! = dydis (T))
Kada jie dirba? Tada, kai sukuriama/ištrinama klasė, paveldėta iš bazės T.
Įpėdiniai naudos įprastą naują / ištrinti, tačiau su jais taip pat gali būti sumaišytas BlockAlloc. Tokiu būdu galime lengvai ir saugiai nustatyti, kurios klasės turėtų naudoti baseinus, nebijant ką nors sugadinti programoje. Daugkartinis paveldėjimas taip pat puikiai veikia su šiuo mišiniu.

Paruošta. Mes paveldime Node iš BlockAlloc ir iš naujo paleidžiame testą.
Bandymo laikas dabar yra 120 ms. 5 kartus greičiau. Bet C# skirstytuvas vis tiek geresnis. Tikriausiai tai ne tik susietas sąrašas. (Jei iš karto po new iškviečiame delete ir taip nešvaistome daug atminties, duomenis talpindami į talpyklą, gauname 62 ms. Keista. Lygiai taip pat kaip ir .NET CLR, tarsi atlaisvintus vietinius kintamuosius iš karto grąžina į atitinkamas baseinas, nelaukiant GC)

2. Talpykla ir spalvingas jo turinys

Ar dažnai susiduriate su klasėmis, kuriose saugoma daug įvairių vaikų objektų, kad pastarųjų gyvenimo trukmė ne ilgesnė nei tėvų?

Pavyzdžiui, tai gali būti XmlDocument klasė, užpildyta Mazgo ir Atributo klasėmis, taip pat c-stygomis (char*), paimtomis iš teksto mazguose. Arba failų ir katalogų sąrašas failų tvarkyklėje, kurie įkeliami vieną kartą, kai katalogas perskaitomas ir daugiau niekada nesikeičia.

Kaip buvo parodyta įžangoje, ištrynimas yra brangesnis nei naujas. Antrosios straipsnio dalies idėja yra skirti atmintį antriniams objektams dideliame bloke, susietame su pagrindiniu objektu. Ištrynus pirminį objektą, kaip įprasta, bus iškviesti vaikų naikintuvai, tačiau atminties grąžinti nereikės – ji bus atlaisvinta viename dideliame bloke.

Sukurkime PointerBumpAllocator klasę, kuri gali nukąsti įvairaus dydžio gabalus iš didelio bloko ir paskirti naują didelį bloką, kai senasis išsenka.

PointerBumpAllocator klasė

šabloną class PointerBumpAllocator ( viešas: PointerBumpAllocator() : free(0) ( ) void* AllocBlock(size_t block) ( // todo: lock(this) block = align(block, Alignment); if (block > free) ( free = lygiuoti (blokas, puslapio dydis); head = GetPage(free); ) void* tmp = head; head = (char*)head + blokas; nemokama - = blokas; return tmp; ) ~PointerBumpAllocator() (skirta (vektoriui) ::iteratorius i = puslapiai.begin(); i != puslapiai.pabaiga(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) privatus: void* GetPage(dydis_t dydis) ( void* page = VirtualAlloc(NULL, dydis, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(puslapis) ; grąžinti puslapį; ) vektorius puslapiai; tuščia* galva; dydis_t nemokamas; ); typedef PointerBumpAllocator<>DefaultAllocator;

Galiausiai apibūdinkime ChildObject mišinį su perkrautu nauju ir ištrinkite prieigą prie nurodyto skirstytuvo:

Šablonas struct ChildObject ( static void* operatorius new(size_t s, A& allocator) ( return allocator.AllocBlock(s); ) static void* operator new(size_t s, A* allocator) ( return allocator->AllocBlock(s); ) static void operator delete(tuščia*, dydis_t) ( ) // *1 static void operator delete(tuščia*, A*) ( ) static void operator delete(void*, A&) ( ) private: static void* operator new(size_t s) ; );

Tokiu atveju, be mišinio įtraukimo į vaikų klasę, taip pat turėsite pakoreguoti visus skambučius į naujus (arba naudoti „gamyklos“ šabloną). Naujojo operatoriaus sintaksė bus tokia:

Nauji (...parametrai operatoriui...) ChildObject (...parametrai konstruktoriui...)

Patogumui nurodžiau du naujus operatorius, kurie priima A& arba A*.
Jei skirstytuvas pridedamas prie pagrindinės klasės kaip narys, pirmoji parinktis yra patogesnė:
mazgas = new(alokatorius) XmlMazgas(mazgo pavadinimas);
Jei skirstytuvas pridedamas kaip protėvis (miksinas), antrasis yra patogesnis:
mazgas = new(this) XmlMazgas(mazgo pavadinimas);

Nėra specialios sintaksės trynimui iškviesti, kompiliatorius iškvies standartinį trynimą (pažymėtą *1), neatsižvelgiant į tai, koks naujas operatorius buvo naudojamas objektui sukurti. Tai yra, ištrynimo sintaksė yra normali:
ištrinti mazgą;

Jei ChildObject konstruktoriuje (ar jo palikuonyje) atsiranda išimtis, trynimas iškviečiamas su parašu, atitinkančiu naujojo operatoriaus, naudoto kuriant šį objektą, parašą (pirmasis parametras dydis_t bus pakeistas į void*).

Naujo operatoriaus įtraukimas į privačią sekciją apsaugo nuo iškvietimo naujam nenurodant skirstytuvo.

Štai išsamus Allocator-ChildObject poros naudojimo pavyzdys:

Pavyzdys

klasė XmlDocument: public DefaultAllocator ( viešas: ~XmlDocument() ( skirtas (vektoriui ::iteratorius i = mazgai.begin(); i != mazgai.end(); ++i) ( ištrinti (*i); ​​​​) ) void AddNode(char* turinys, char* pavadinimas) ( char* c = (char*)AllocBlock(strlen(content)+1); strcpy(c, content) ); char* n = (char*)AllocBlock(strlen(vardas)+1); strcpy(n, turinys); nodes.push_back(new(this) XmlNode(c, n)); ) klasė XmlNode: viešas ChildObject ( public: XmlNode(char* _content, char* _name) : content(_content), name(_name) ( ) private: char* content; char* name; ); privatus: vektorius mazgai; );

Išvada. Straipsnis parašytas prieš 1,5 metų smėlio dėžei, bet deja, moderatoriui nepatiko.