Si të konfiguroni telefonat inteligjentë dhe PC. Portali informativ
  • në shtëpi
  • Windows 8
  • Përdorimi i fshirjes së re për të zbatuar vargje. §11 Vargje dhe tregues

Përdorimi i fshirjes së re për të zbatuar vargje. §11 Vargje dhe tregues

15.8. Operatorët e rinj dhe fshini

Si parazgjedhje, shpërndarja e një objekti të klasës nga grumbulli dhe lirimi i memories së zënë prej tij kryhen duke përdorur operatorët global new () dhe delete (), të cilët janë të përcaktuar në bibliotekën standarde C ++. (Ne i diskutuam këta operatorë në seksionin 8.4.) Por një klasë mund të zbatojë strategjinë e saj të menaxhimit të memories duke ofruar operatorë anëtarë me të njëjtin emër. Nëse përcaktohen në një klasë, thirren në vend të operatorëve globalë për të ndarë dhe liruar memorie për objektet e kësaj klase.

Le të përcaktojmë operatorët new () dhe të fshijmë () në klasën tonë Screen.

Operatori anëtar i ri () duhet të kthejë një vlerë të tipit void * dhe të marrë si parametër të parë një vlerë të tipit size_t, ku size_t është një typedef i përcaktuar në skedarin e kokës së sistemit. Ja njoftimi i tij:

void * operator new (size_t);

Kur new () përdoret për të krijuar një objekt të një lloji klase, përpiluesi kontrollon nëse një operator i tillë është përcaktuar në atë klasë. Nëse po, atëherë për të ndarë memorie për objektin, është objekti që thirret, përndryshe - operatori global new (). Për shembull, deklarata e mëposhtme

Ekrani * ps = Ekran i ri;

krijon një objekt Screen në një grumbull, dhe meqenëse kjo klasë ka një operator të ri (), quhet. Parametri size_t i operatorit inicializohet automatikisht me një vlerë të barabartë me madhësinë e ekranit në bajt.

Shtimi i një operatori të ri () në një klasë ose heqja e tij prej andej nuk pasqyrohet në kodin e përdoruesit. Thirrja për të re duket e njëjtë si për operatorin global ashtu edhe për operatorin anëtar. Nëse klasa Screen nuk do të kishte të re (), atëherë thirrja do të mbetej e saktë, vetëm operatori global do të thirrej në vend të operatorit anëtar.

Operatori i rezolucionit të shtrirjes globale mund të thërrasë globale new () edhe nëse klasa Screen përcakton versionin e saj:

Ekrani * ps = :: Ekrani i ri;

fshirja e operatorit void (void *);

Kur operandi delete është një tregues për një objekt të një lloji klase, përpiluesi kontrollon nëse operatori delete () është i përcaktuar në atë klasë. Nëse po, atëherë është ai që thirret për të liruar kujtesën, përndryshe - versioni global i operatorit. Udhëzimi tjetër

çliron memorien e përdorur nga objekti Screen i treguar nga ps. Meqenëse Screen ka një operator anëtar të fshirjes (), kjo është ajo që vlen. Një parametër operatori i tipit void * inicializohet automatikisht në ps. Shtimi i delete () në një klasë ose heqja e tij prej andej nuk ndikon në kodin e përdoruesit në asnjë mënyrë. Thirrja e fshirjes duket e njëjtë si për operatorin global ashtu edhe për operatorin anëtar. Nëse klasa Screen nuk do të kishte operatorin e vet delete (), atëherë thirrja do të mbetej e saktë, vetëm operatori global do të thirrej në vend të operatorit anëtar.

Duke përdorur operatorin e rezolucionit të shtrirjes globale, mund të telefononi fshirjen globale () edhe nëse Ekrani përcakton versionin e tij:

Në përgjithësi, operatori i fshirjes () i përdorur duhet të përputhet me operatorin e ri () me të cilin është alokuar memoria. Për shembull, nëse ps tregon një zonë memorie të caktuar nga e reja globale (), atëherë përdorni fshirjen globale () për ta çliruar atë.

Operatori delete () i përcaktuar për një lloj klase mund të përmbajë dy parametra në vend të një. Parametri i parë duhet të jetë ende i llojit void *, dhe i dyti duhet të jetë i tipit të paracaktuar size_t (mos harroni të përfshini skedarin e kokës):

// zëvendëson

// operatori void delete (void *);

Nëse ka një parametër të dytë, kompajleri e inicializon automatikisht atë me një vlerë të barabartë me madhësinë e objektit të adresuar nga parametri i parë në bajt. (Ky parametër është i rëndësishëm në një hierarki klase ku operatori delete () mund të trashëgohet nga një klasë e prejardhur. Më shumë rreth trashëgimisë diskutohet në Kapitullin 17.)

Le të shqyrtojmë zbatimin e operatorëve new () dhe delete () në klasën Screen në më shumë detaje. Strategjia jonë e ndarjes së kujtesës do të bazohet në një listë të lidhur të objekteve të Ekranit, të treguara nga anëtari i freeStore. Sa herë që thirret operatori i ri () anëtar, kthehet objekti tjetër nga lista. Kur telefononi delete (), objekti kthehet në listë. Nëse, kur krijoni një objekt të ri, lista e adresuar në freeStore është bosh, atëherë thirret operatori global new () për të marrë një bllok memorie të mjaftueshme për të ruajtur objektet screenChunk të klasës Screen.

Si screenChunk ashtu edhe freeStore janë me interes vetëm për Screen, kështu që ne do t'i bëjmë ata anëtarë privatë. Për më tepër, për të gjitha objektet e krijuara të klasës sonë, vlerat e këtyre anëtarëve duhet të jenë të njëjta, dhe për këtë arsye, ato duhet të deklarohen statike. Për të ruajtur strukturën e një liste të lidhur të objekteve të ekranit, na duhet një anëtar i tretë i radhës:

void * operator new (size_t);

fshirja e operatorit void (void *, madhësia_t);

Ekrani statik * freeStore;

static const int screenChunk;

Këtu është një zbatim i mundshëm i operatorit të ri () për klasën Screen:

#include "Screen.h"

#include cstddef

// anëtarët statikë janë inicializuar

// në skedarët burimor të programit, jo në skedarët e kokës

Ekrani * Ekrani :: freeStore = 0;

const int Screen :: screenChunk = 24;

void * Ekran :: operator i ri (madhësia_t)

nëse (! FreeStore) (

// lista e lidhur është bosh: merrni një bllok të ri

// thirret operatori global new

madhësi_t copë = screenChunk * madhësia;

reinterpret_cast Screen * (char [copë] e re);

// përfshini bllokun që rezulton në listë

p! = & freeStore [screenChunk - 1];

freeStore = freeStore-next;

Dhe këtu është zbatimi i operatorit të fshirjes ():

Ekrani i pavlefshëm :: fshirja e operatorit (void * p, madhësia_t)

// futni përsëri objektin "të fshirë",

// në listën e lirë

(Screen_static_cast * (p)) - tjetër = freeStore;

freeStore = Static_cast Screen * (p);

Operatori i ri () mund të deklarohet në klasë pa fshirjen përkatëse (). Në këtë rast, objektet lirohen duke përdorur operatorin global me të njëjtin emër. Lejohet gjithashtu të deklarohet operatori delete () pa të ri (): objektet do të krijohen duke përdorur operatorin global me të njëjtin emër. Sidoqoftë, këta operatorë zakonisht zbatohen në të njëjtën kohë, si në shembullin e mësipërm, pasi zhvilluesi i klasës zakonisht ka nevojë për të dyja.

Ata janë anëtarë statikë të klasës, edhe nëse programuesi nuk i deklaron në mënyrë eksplicite si të tillë, dhe u binden kufizimeve të zakonshme për funksione të tilla anëtarësh: ata nuk kalohen nga ky tregues, dhe për këtë arsye ata mund të kenë qasje direkt në anëtarët statikë. (Shih diskutimin e funksioneve të anëtarit statik në seksionin 13.5.) Arsyeja pse këta operatorë bëhen statikë është sepse thirren ose përpara se objekti i klasës të ndërtohet (i ri ()) ose pasi të shkatërrohet (fshij ()).

Shpërndarja e memories duke përdorur operatorin e ri (), për shembull:

Ekran * ptr = Ekran i ri (10, 20);

// Pseudokod në C ++

ptr = Ekrani :: operatori i ri (madhësia e ekranit));

Ekrani :: Ekrani (ptr, 10, 20);

Me fjalë të tjera, fillimisht operatori new () i përcaktuar në klasë thirret për të ndarë memorie për objektin, dhe më pas ky objekt inicializohet nga konstruktori. Nëse new () dështon, hidhet një përjashtim i tipit bad_alloc dhe konstruktori nuk thirret.

Lirimi i memories duke përdorur operatorin delete (), për shembull:

është e barabartë me ekzekutimin sekuencial të udhëzimeve si kjo:

// Pseudokod në C ++

Ekrani :: ~ Ekrani (ptr);

Ekrani :: fshirja e operatorit (ptr, madhësia e (* ptr));

Kështu, kur një objekt shkatërrohet, fillimisht thirret destruktori i klasës dhe më pas operatori delete () i përcaktuar në klasë për të liruar memorien. Nëse ptr është 0, atëherë nuk thirret as destruktori dhe as delete ().

15.8.1. Operatorët e rinj dhe fshini

Operatori i ri () i përcaktuar në nënseksionin e mëparshëm thirret vetëm kur memoria ndahet për një objekt të vetëm. Pra, ky udhëzim thërret new () të klasës Screen:

Ekrani * ps = Ekrani i ri (24, 80);

ndërsa më poshtë operatori global new () thirret për të ndarë memorie nga grumbulli për një grup objektesh të tipit Screen:

// Ekrani :: thirret operatori new ().

Ekran * psa = Ekran i ri;

Në klasë, ju gjithashtu mund të deklaroni operatorët e rinj () dhe fshini () për të punuar me vargje.

Operatori i ri anëtar () duhet të kthejë një vlerë void * dhe të marrë një vlerë size_t si parametër të parë. Ky është njoftimi i tij për Screen:

void * operator new (size_t);

Kur përdoret new për të krijuar një grup objektesh të një lloji klase, përpiluesi kontrollon nëse operatori i ri () është i përcaktuar në klasë. Nëse është kështu, atëherë për të ndarë memorien për grupin, është ai që quhet, përndryshe - e reja globale (). Deklarata e mëposhtme e grumbullit krijon një grup prej dhjetë objektesh të ekranit:

Ekrani * ps = Ekran i ri;

Kjo klasë ka një operator të ri () për këtë arsye thirret për të ndarë memorien. Parametri i tij size_t inicializohet automatikisht në sasinë e memories, në bajt, e nevojshme për të akomoduar dhjetë objekte të ekranit.

Edhe nëse klasa ka një operator anëtar new (), programuesi mund të thërrasë global new () për të krijuar grupin duke përdorur operatorin e rezolucionit të fushëveprimit global:

Ekrani * ps = :: Ekrani i ri;

Operatori delete (), i cili është anëtar i klasës, duhet të jetë i llojit void dhe të marrë void * si parametër të parë. Ja si duket reklama e tij për Screen:

fshirja e operatorit void (void *);

Për të fshirë një grup objektesh të klasës, delete duhet të quhet kështu:

Kur operandi delete është një tregues për një objekt të tipit të klasës, përpiluesi kontrollon nëse operatori delete () është i përcaktuar në atë klasë. Nëse po, atëherë është ai që thirret për të liruar kujtesën, përndryshe - versioni i saj global. Një parametër i tipit void * inicializohet automatikisht me vlerën e adresës së fillimit të zonës së memories në të cilën ndodhet grupi.

Edhe nëse klasa ka një operator anëtar delete (), programuesi mund të thërrasë fshirjen globale () duke përdorur operatorin e rezolucionit të fushëveprimit global:

Shtimi i operatorëve të rinj () ose fshirja () në një klasë ose heqja e tyre prej andej nuk pasqyrohet në kodin e përdoruesit: thirrjet si për operatorët globalë ashtu edhe për ata anëtarë duken njësoj.

Kur krijohet një grup, fillimisht thirret new () për të ndarë memorien e kërkuar dhe më pas çdo element inicializohet duke përdorur konstruktorin e paracaktuar. Nëse një klasë ka të paktën një konstruktor, por asnjë konstruktor të paracaktuar, atëherë thirrja e operatorit new () konsiderohet gabim. Nuk ka sintaksë për të specifikuar inicializuesit e elementeve të grupit ose argumentet e konstruktorit të klasës kur krijohet një grup në këtë mënyrë.

Kur një grup shkatërrohet, fillimisht thirret destruktori i klasës për të shkatërruar elementet, dhe më pas operatori delete () thirret për të liruar të gjithë memorien. Është e rëndësishme të përdorni sintaksë të saktë kur e bëni këtë. Nëse udhëzimet

ps tregon një grup objektesh të klasës, atëherë mungesa e kllapave katrore do të bëjë që destruktori të thirret vetëm për elementin e parë, megjithëse kujtesa do të lirohet plotësisht.

Operatori anëtar i fshirjes () mund të ketë jo një, por dy parametra, ndërsa i dyti duhet të jetë i tipit size_t:

// zëvendëson

// operatori void delete (void *);

fshirja e operatorit void (void *, madhësia_t);

Nëse parametri i dytë është i pranishëm, kompajleri e inicializon automatikisht atë me një vlerë të barabartë me sasinë e memories së ndarë për grupin në bajt.

Nga libri C ++ Referenca Manual autori Stroustrap Bjarn

R.5.3.4 Operatori delete Operatori delete shkatërron një objekt të krijuar nga new.release-expression: :: opt delete cast-expression :: opt delete cast-expression Rezultati është i tipit void. Operandi i fshirjes duhet të jetë një tregues që kthehet i ri. Efekti i aplikimit të operacionit të fshirjes

Nga libri Microsoft Visual C ++ dhe MFC. Programimi për Windows 95 dhe Windows NT autori Frolov Alexander Vyacheslavovich

Operatorë të rinj dhe të fshirë Operatori new krijon një objekt të llojit të specifikuar. Kur e bën këtë, ai shpërndan memorien e nevojshme për të ruajtur objektin dhe kthen një tregues që tregon drejt tij. Nëse për ndonjë arsye memoria nuk mund të merret, operatori kthen një vlerë null. Operatori

Nga libri Duke përdorur C ++ në mënyrë efektive. 55 mënyra të sigurta për të përmirësuar strukturën dhe kodin e programeve tuaja nga Meyers Scott

Rregulli 16: Përdorni të njëjtat forma të reja dhe fshini Çfarë nuk shkon me fragmentin e mëposhtëm Std :: string * stringArray = std i ri :: string; ... fshini stringArray; Në shikim të parë, gjithçka është në rregull - përdorimi i ri korrespondon me përdorimin fshije, por diçka nuk është plotësisht e gabuar këtu. Sjellja e programit

Nga libri Windows Script Host për Windows 2000 / XP autori Popov Andrey Vladimirovich

Kapitulli 8 Përshtatja dhe fshirja e të rejave Këto ditë, kur mjediset kompjuterike kanë mbështetje të integruar për mbledhjen e mbeturinave (si Java dhe .NET), qasja manuale e C ++ për menaxhimin e kujtesës mund të duket paksa e vjetëruar. Megjithatë, shumë zhvillues që krijojnë të kërkuar

Nga libri C ++ Standards Programming. 101 rregulla dhe rekomandime autori Alexandrescu Andrey

Metoda e fshirjes Nëse forca është false ose nuk specifikohet, metoda e fshirjes nuk do të jetë në gjendje të fshijë një drejtori vetëm për lexim. Vendosja e forcës në true do të fshijë menjëherë drejtoritë e tilla. Përdorimi i metodës Delete nuk ka rëndësi nëse specifikohet

Nga libri Flash Reference autori Ekipi i autorëve

Metoda e fshirjes Nëse forca është false ose nuk specifikohet, atëherë metoda e fshirjes nuk do të jetë në gjendje të fshijë një skedar vetëm për lexim. Vendosja e forcës në true do të lejojë që skedarët e tillë të fshihen menjëherë. Shënim Në vend të metodës Delete, mund të përdorni metodën DeleteFile.

Nga libri Firebird DATABASE DESIGNER'S GUIDE nga Borri Helen

Operatorët Relacionalë dhe Boolean Operatorët Relacionalë përdoren për të krahasuar vlerat e dy variablave. Këta operatorë, të përshkruar në tabelë. A2.11, mund të kthejë vetëm vlerat Boolean të vërtetë ose të gabuar. Tabela A2.11. Operatorët e relacionit Gjendja e operatorit, për

Nga libri Linux dhe UNIX: Programimi i guaskës. Udhëzuesi i zhvilluesit. nga Teinsley David

45. new dhe delete duhet të zhvillohen gjithmonë së bashku Përmbledhje Çdo mbingarkesë e operatorit void * new (parms) në një klasë duhet të shoqërohet nga një mbingarkesë korresponduese e operatorit të fshirjes (void *, parms), ku parms është një listë e llojeve të parametrave shtesë. (e para prej të cilave është gjithmonë std :: size_t). Gjithashtu

Nga libri i autorit Referenca SQL

fshij - Fshirja e një objekti, elementi grupi ose variabli delete (Operator) Ky operator përdoret për të fshirë një objekt, një veçori objekti, një element grupi ose variabla nga një skript Sintaksa: delete identifikuesin; Argumentet: Përshkrimi: Operatori i fshirjes shkatërron një objekt ose ndryshore, emër

Nga libri Kuptimi i SQL nga Gruber Martin

Deklarata DELETE Deklarata DELETE përdoret për të fshirë rreshta të tëra nga një tabelë. SQL nuk lejon që një deklaratë e vetme DELETE të fshijë rreshta nga më shumë se një tabelë. Një deklaratë DELETE që modifikon vetëm rreshtin aktual të kursorit quhet fshirje e pozicionuar.

Nga libri i autorit

15.8. Operatorët e rinj dhe të fshirë Si parazgjedhje, ndarja e një objekti të klasës nga grumbulli dhe lirimi i memories së zënë prej tij kryhen duke përdorur operatorët globalë new () dhe delete (), të cilët janë të përcaktuar në bibliotekën standarde C ++. (Ne i mbuluam këta operatorë në seksionin 8.4.) Por një klasë mund të zbatojë

Nga libri i autorit

15.8.1. Operatorë të rinj dhe të fshirë Operatori new (), i përcaktuar në nënseksionin e mëparshëm, thirret vetëm kur memoria ndahet për një objekt të vetëm. Pra, në këtë deklaratë, new () i klasës Screen quhet: // Screen :: operator new () Screen * ps = new Screen (24, 80); ndërsa më poshtë quhet

Siç e dini, në gjuhën C, funksionet malloc () dhe free () përdoren për të shpërndarë dhe liruar memorie në mënyrë dinamike. Megjithatë, C ++ përmban dy operatorë që e bëjnë ndarjen dhe lirimin e memories më efikase dhe më të thjeshtë. Këta operatorë janë të rinj dhe fshihen. Forma e tyre e përgjithshme është:

tregues_ndryshues = variabël_i ri;

fshij pointer_variable;

Këtu variable_pointer është një tregues i llojit variable_type. Operatori i ri shpërndan memorie për të ruajtur një vlerë të tipit variable_type dhe kthen adresën e saj. Çdo lloj i të dhënave mund të vendoset me të reja. Operatori i fshirjes liron memorien e drejtuar nga treguesi variable_pointer.

Nëse operacioni i ndarjes së memories nuk mund të kryhet, atëherë operatori i ri hedh një përjashtim të tipit xalloc. Nëse programi nuk e kap këtë përjashtim, atëherë ai do të ndërpritet. Ndërsa kjo është në rregull si parazgjedhje për programet e shkurtra, aplikacionet e botës reale zakonisht duhet të kapin përjashtimin dhe ta trajtojnë atë në mënyrë të përshtatshme. Në mënyrë që të gjurmoni këtë përjashtim, duhet të përfshini skedarin e kokës përveç.h.

Operatori i fshirjes duhet të përdoret vetëm për treguesit në memorie të alokuara duke përdorur operatorin e ri. Përdorimi i operatorit të fshirjes me lloje të tjera adresash mund të shkaktojë probleme serioze.

Ka një sërë avantazhesh për përdorimin e ri mbi malloc (). Së pari, operatori i ri llogarit automatikisht madhësinë e memories së kërkuar. Nuk ka nevojë të përdoret operatori sizeof (). Më e rëndësishmja, parandalon që sasia e gabuar e memories të shpërndahet aksidentalisht. Së dyti, operatori i ri kthen automatikisht një tregues të llojit të kërkuar, kështu që nuk ka nevojë të përdoret një operator konvertimi. Së treti, siç do të përshkruhet së shpejti, është e mundur të inicializohet një objekt duke përdorur operatorin e ri. Së fundi, është e mundur të mbingarkohet operatori i ri dhe operatori i fshirjes globalisht ose në lidhje me klasën që po krijohet.

Më poshtë është një shembull i thjeshtë i përdorimit të operatorëve të rinj dhe të fshirjes. Vini re përdorimin e një blloku provo / kap për të gjurmuar gabimet e shpërndarjes së kujtesës.

#përfshi
#përfshi
int main ()
{
int * p;
provoni (
p = int e re; // alokimi i memories për int
) kap (xalloc xa) (
cout<< "Allocation failure.\n";
kthimi 1;
}
* p = 20; // duke caktuar vlerën 20 në këtë vendndodhje memorie
cout<< *р; // демонстрация работы путем вывода значения
fshij p; // memorie e lirë
kthimi 0;
}

Ky program i cakton p adresën e një blloku memorie që është mjaftueshëm i madh për të përmbajtur një numër të plotë. Më pas kësaj memorie i caktohet një vlerë dhe shfaqet përmbajtja e memories. Më në fund, memoria e alokuar dinamikisht lirohet.

Siç u përmend, është e mundur të inicializohet memoria duke përdorur operatorin e ri. Për ta bërë këtë, duhet të specifikoni vlerën e inicializimit në kllapa pas emrit të tipit. Për shembull, në shembullin e mëposhtëm, memoria e treguar nga p është inicializuar në 99:

#përfshi
#përfshi
int main ()
{
int * p;
provoni (
p = int i ri (99); // inicializimi i 99-tës
) kap (xalloc xa) (
cout<< "Allocation failure.\n";
kthimi 1;
}
cout<< *p;
fshij p;
kthimi 0;
}

Vargjet mund të ndahen me të reja. Forma e përgjithshme për një grup njëdimensional është:

tregues_variable = variabël i ri_lloji [madhësia];

Këtu, madhësia përcakton numrin e elementeve në grup. Ekziston një kufizim i rëndësishëm për t'u mbajtur mend kur vendosni një grup: ai nuk mund të inicializohet.

Për të liruar një grup të alokuar dinamikisht, përdorni formën e mëposhtme të operatorit të fshirjes:

fshij pointer_variable;

Këtu kllapat informojnë operatorin e fshirjes për të liruar memorien e alokuar për grupin.

Programi i mëposhtëm shpërndan memorie për një grup prej 10 floats. Elementëve të grupit u caktohen vlera nga 100 në 109, dhe më pas përmbajtja e grupit shtypet në ekran:

#përfshi
#përfshi
int main ()
{
noton * p;
int i;
provoni (
p = noton i ri; // merrni elementin e dhjetë të grupit
) kap (xalloc xa) (
cout<< "Allocation failure.\n";
kthimi 1;
}
// caktimi i vlerave nga 100 në 109
për (i = 0; i<10; i + +) p[i] = 100.00 + i;
// shfaq përmbajtjen e grupit
për (i = 0; i<10; i++) cout << p[i] << " ";
fshij p; // fshini të gjithë grupin
kthimi 0;
}

C ++ mbështet tre lloje kryesore ekskrecionet (shpërndarja) memorie, me dy prej të cilave tashmë jemi njohur:

Alokimi statik i memoriesështë ekzekutuar për dhe variablat. Kujtesa ndahet një herë, kur programi niset, dhe ruhet gjatë gjithë programit.

Shpërndarja automatike e memories kryhet për dhe. Kujtesa shpërndahet kur futni bllokun që përmban këto variabla dhe lirohet kur dilni prej tij.

është tema e këtij artikulli.

Si shpërndarja e memories statike ashtu edhe ajo automatike kanë dy gjëra të përbashkëta:

Madhësia e variablës / grupit duhet të dihet në kohën e kompilimit.

Shpërndarja dhe shpërndarja e memories ndodh automatikisht (kur krijohet ose shkatërrohet një variabël).

Në shumicën e rasteve, kjo është në rregull. Megjithatë, kur bëhet fjalë për të punuar me të dhëna të jashtme, këto kufizime mund të çojnë në probleme.

Për shembull, kur përdoret për të ruajtur një emër, ne nuk e dimë paraprakisht sa kohë do të zgjasë derisa përdoruesi ta vendosë atë. Ose kur duhet të shkruajmë numrin e rekordeve nga disku në një variabël, por nuk e dimë paraprakisht sa nga këto regjistrime ka. Ose mund të krijojmë një lojë me një numër të ndryshueshëm monstrash (gjatë lojës, disa përbindësha vdesin, të tjerët lindin), duke u përpjekur kështu të vrasim lojtarin.

Nëse na duhet të deklarojmë madhësinë e të gjitha variablave në kohën e përpilimit, atëherë më e mira që mund të bëjmë është të përpiqemi të hamendësojmë madhësinë e tyre maksimale, duke shpresuar se kjo do të jetë e mjaftueshme:

emri char; // Shpresojmë që përdoruesi të vendosë një emër me më pak se 30 karaktere! Regjistro rekord; // shpresojmë se nuk do të ketë më shumë se 400 regjistrime! Përbindësh përbindësh; // 30 monstra maksimumi i interpretimit të shumëkëndëshit; // kjo paraqitje 3d është më e mirë me më pak se 40,000 poligone!

Ky është një vendim i keq për të paktën tre arsye:

Së pari, memoria humbet nëse variablat nuk përdoren ose përdoren në të vërtetë, por jo plotësisht. Për shembull, nëse ndajmë 30 karaktere për secilin emër, por emrat do të marrin mesatarisht 15 karaktere, atëherë konsumi i kujtesës do të rezultojë të jetë dy herë më i madh se sa i nevojitet në të vërtetë. Ose merrni parasysh grupin e interpretimit: nëse përdor vetëm 20,000 shumëkëndësha, atëherë memoria me 20,000 shumëkëndësha është në të vërtetë e humbur (d.m.th. nuk përdoret)!

Së dyti, memoria për shumicën e variablave të zakonshëm (duke përfshirë grupet fikse) ndahet nga një rezervuar i veçantë memorie - rafte... Sasia e memories stack në një program është zakonisht e vogël - në Visual Studio është 1MB si parazgjedhje. Nëse e tejkaloni këtë numër, atëherë tejmbushje rafte dhe sistemi operativ do të përfundojë automatikisht programin tuaj.

Në Visual Studio, mund ta kontrolloni këtë duke ekzekutuar programin e mëposhtëm:

int main () (int vargu; // cakto 1 milion vlera të plota)

Kufiri i memories prej 1 MB mund të jetë problematik për shumë programe, veçanërisht kur përdoren grafika.

Së treti, dhe më e rëndësishmja, mund të çojë në kufizime artificiale dhe/ose tejmbushje të grupeve. Çfarë ndodh nëse përdoruesi përpiqet të lexojë 500 regjistrime nga disku, por ne kemi ndarë memorie për një maksimum prej 400? Ose i tregojmë përdoruesit një gabim që numri maksimal i regjistrimeve është 400, ose (në rastin më të keq) grupi do të tejmbushet dhe më pas diçka shumë e keqe.

Për fat të mirë, shpërndarja dinamike e memories mund t'i rregullojë lehtësisht këto probleme. Shpërndarja dinamike e memoriesËshtë një mënyrë për të kërkuar memorie nga sistemi operativ duke ekzekutuar programe kur nevojitet. Kjo memorie nuk ndahet nga memoria e kufizuar e grupit të programit, por nga ruajtja shumë më e madhe e menaxhuar nga sistemi operativ - grumbull (grumbuj) . Në kompjuterët modernë, grumbulli mund të jetë aq i madh sa gigabajt memorie.

Shpërndarja dinamike e variablave

Për të shpërndarë në mënyrë dinamike memorie për një variabël, përdorni operatorin i ri:

int e re; // alokoni në mënyrë dinamike një ndryshore numër të plotë dhe hidhni menjëherë rezultatin (pasi nuk e ruajmë askund)

Në shembullin e mësipërm, ne po kërkojmë ndarjen e memories për një ndryshore numër të plotë nga sistemi operativ. Operatori i ri kthehet duke përmbajtur adresën e memories së alokuar.

Një tregues krijohet për të hyrë në memorien e alokuar:

int * ptr = int e re; // alokoni në mënyrë dinamike një ndryshore numër të plotë dhe caktoni adresën e saj në ptr, në mënyrë që më vonë të mund t'i qasemi asaj

Më pas mund të çreferencojmë treguesin për të marrë vlerën:

* ptr = 8; // caktoje vlerën 8 në memorien e re të alokuar

Këtu është një nga rastet kur treguesit janë të dobishëm. Pa një tregues me një adresë në memorien e alokuar rishtazi, nuk do të kishim asnjë mënyrë për ta aksesuar atë.

Si funksionon shpërndarja dinamike e memories?

Kompjuteri juaj ka memorie (ndoshta shumica e saj) që është e disponueshme për përdorim nga aplikacionet. Kur nisni një aplikacion, sistemi juaj operativ e ngarkon atë aplikacion në një pjesë të kësaj memorie. Dhe kjo memorie e përdorur nga aplikacioni juaj është e ndarë në disa pjesë, secila prej të cilave kryen një detyrë specifike. Një pjesë përmban kodin tuaj, tjetra përdoret për të kryer operacione normale (duke mbajtur gjurmët e funksioneve që quhen, duke krijuar dhe shkatërruar variabla globale dhe lokale, etj.). Ne do të flasim për të më vonë. Sidoqoftë, pjesa më e madhe e memories së disponueshme është thjesht e ulur atje, duke pritur për kërkesat e alokimit nga programet.

Kur shpërndani memorie në mënyrë dinamike, po i kërkoni sistemit operativ që të rezervojë një pjesë të asaj memorie për ta përdorur programin tuaj. Nëse OS mund ta përmbushë këtë kërkesë, atëherë adresa e kësaj memorie do të kthehet në aplikacionin tuaj. Që tani e tutje, aplikacioni juaj mund ta përdorë këtë memorie sa më shpejt që të dojë. Kur të keni përfunduar tashmë gjithçka që ishte e nevojshme me këtë memorie, atëherë ajo duhet të kthehet përsëri në sistemin operativ për shpërndarje midis kërkesave të tjera.

Ndryshe nga shpërndarja e memories statike ose automatike, vetë programi është përgjegjës për kërkesën dhe kthimin e memories së alokuar dinamikisht.

Inicializimi i variablave të alokuara në mënyrë dinamike

Kur alokoni në mënyrë dinamike një ndryshore, mund ta inicializoni gjithashtu nëpërmjet ose inicializimit uniform (në C ++ 11):

int * ptr1 = int i ri (7); // përdorni inicializimin e drejtpërdrejtë int * ptr2 = new int (8); // përdorni inicializimin uniform

Fshirja e variablave

Kur gjithçka që ishte e nevojshme është bërë tashmë me një variabël të alokuar dinamikisht, ju duhet t'i tregoni qartë C ++ që të çlirojë këtë memorie. Për variabla individuale, kjo bëhet duke përdorur operatorin fshij:

// supozojmë se ptr është alokuar më parë duke përdorur operatorin e ri delete ptr; // ktheje memorien e treguar nga ptr në sistemin operativ ptr = 0; // bëj ptr një tregues null (përdor nullptr në vend të 0 në C ++ 11)

Çfarë do të thotë "fshij kujtesën"?

Operatori i fshirjes në fakt nuk fshin asgjë. Thjesht kthen memorien e alokuar më parë në sistemin operativ. Sistemi operativ më pas mund ta ricaktojë këtë memorie në një aplikacion tjetër (ose përsëri të njëjtën).

Edhe pse mund të duket se po fshijmë e ndryshueshme por nuk është! Një ndryshore treguese ka ende të njëjtin shtrirje si më parë dhe mund t'i caktohet një vlerë e re si çdo ndryshore tjetër.

Vini re se fshirja e një treguesi që nuk tregon memorien e alokuar në mënyrë dinamike mund të çojë në probleme.

Treguesit e varur

C ++ nuk jep garanci se çfarë do të ndodhë me përmbajtjen e memories së liruar ose vlerën e treguesit që fshihet. Në shumicën e rasteve, memoria e kthyer në sistemin operativ do të përmbajë të njëjtat vlera që kishte më parë çlirimi, dhe treguesi do të mbetet për të treguar memorien, vetëm tashmë i çliruar (i fshirë).

Treguesi që tregon memorien e liruar quhet tregues i varur... Mosreferencimi ose heqja e një treguesi të varur do të prodhojë rezultate të papritura. Merrni parasysh programin e mëposhtëm:

#përfshi int main () (int * ptr = int e re; * ptr = 8; // vendos vlerën në vendndodhjen e memories së caktuar, fshi ptr; // ktheje kujtesën përsëri në sistemin operativ.ptr tani është një tregues i varur std :: cout<< *ptr; // разыменование висячого указателя приведет к неожиданным результатам delete ptr; // попытка освободить память снова приведет к неожиданным результатам также return 0; }

#përfshi

int main ()

int * ptr = int e re; // caktoni në mënyrë dinamike një ndryshore numër të plotë

* ptr = 8; // vendos vlerën në vendndodhjen e caktuar të memories

fshij ptr; // shtyje kujtesën përsëri në sistemin operativ. ptr tani është një tregues i varur

std :: cout<< * ptr ; // mosreferencimi i një treguesi të varur do të çojë në rezultate të papritura

fshij ptr; // Përpjekja për të liruar kujtesën përsëri do të çojë në rezultate të papritura gjithashtu

kthimi 0;

Në programin e mësipërm, vlera 8, e cila i ishte caktuar më parë memorjes së caktuar, pas lëshimit mund të vazhdojë të jetë aty, ose mund të mos jetë. Është gjithashtu e mundur që memoria e çliruar mund të jetë alokuar tashmë në një aplikacion tjetër (ose për përdorim të vetë sistemit operativ), dhe përpjekja për të hyrë në të do të bëjë që sistemi operativ të përfundojë automatikisht programin tuaj.

Procesi i lirimit gjithashtu mund të krijojë disa tregues të varur. Merrni parasysh shembullin e mëposhtëm:

#përfshi int main () (int * ptr = int e re; // caktoni në mënyrë dinamike një variabël të plotë int * otherPtr = ptr; // otherPtr tani tregon të njëjtën memorie të alokuar si ptr delete ptr; // ktheni kujtesën përsëri në sistemin operativ . ptr dhe otherPtr janë tani tregues të varur ptr = 0; // ptr tani është nullptr // megjithatë Ptr tjetër është ende një tregues i varur! kthe 0;)

#përfshi

int main ()

int * ptr = int e re; // caktoni në mënyrë dinamike një ndryshore numër të plotë

int * tjeraPtr = ptr; // otherPtr tani tregon të njëjtën memorie të caktuar si ptr

fshij ptr; // shtyje kujtesën përsëri në sistemin operativ. ptr dhe otherPtr janë tani tregues të varur

ptr = 0; // ptr tani është nullptr

// megjithatë, otherPtr është ende një tregues i varur!

kthimi 0;

Së pari, përpiquni të shmangni situatat kur tregues të shumtë tregojnë në të njëjtën pjesë të memories së ndarë. Nëse kjo nuk është e mundur, atëherë sqaroni se cili nga të gjithë treguesit "zotëron" memorien (dhe është përgjegjës për fshirjen e tij), dhe cilët tregues thjesht i qasen asaj.

Së dyti, kur fshini një tregues, dhe nëse ai nuk del menjëherë pas fshirjes, atëherë ai duhet të bëhet null, d.m.th. vendosni vlerën në 0 (ose në C ++ 11). Me "dalje nga fusha e veprimit menjëherë pas fshirjes", nënkuptojmë që ju fshini treguesin në fund të bllokut në të cilin është deklaruar.

Rregulli: Vendosni treguesit në distancë në 0 (ose nullptr në C ++ 11) nëse ata nuk dalin jashtë fushëveprimit menjëherë pas fshirjes.

Operator i ri

Kur kërkoni memorie nga sistemi operativ, në raste të rralla, ajo mund të mos jetë e disponueshme (d.m.th., mund të mos jetë e disponueshme).

Si parazgjedhje, nëse e reja nuk funksionoi, memoria nuk u nda, atëherë bëhet një përjashtim keq_alloc... Nëse ky përjashtim nuk trajtohet në mënyrë korrekte (dhe do të jetë, pasi ende nuk kemi marrë parasysh përjashtimet dhe trajtimin e tyre), atëherë programi thjesht do të përfundojë (dështuar) me një gabim përjashtimi të patrajtuar.

Në shumë raste, procesi i hedhjes së një përjashtimi nga operatori i ri (si dhe një dështim i programit) është i padëshirueshëm, kështu që ekziston një formë alternative e re, e cila kthen një tregues null nëse memoria nuk mund të ndahet. Ju vetëm duhet të shtoni std :: konstante midis fjalës së re dhe llojit të përzgjedhjes së të dhënave:

int * vlera = i ri (std :: nothrow) int; // treguesi i vlerës do të bëhet i pavlefshëm nëse shpërndarja dinamike e një variabli të plotë dështon

Në shembullin e mësipërm, nëse new nuk kthen një tregues me memorie të alokuar dinamikisht, atëherë do të kthehet një tregues null.

Gjithashtu nuk rekomandohet ta çreferenconi atë, pasi do të çojë në rezultate të papritura (ka shumë të ngjarë, në rrëzimin e programit). Prandaj, është praktika më e mirë që të kontrollohen të gjitha kërkesat për alokimin e memories për t'u siguruar që këto kërkesa të kenë sukses dhe se memoria është alokuar.

int * vlera = i ri (std :: nothrow) int; (<< "Could not allocate memory"; }

Meqenëse mosndarja e memories me operatorin e ri ndodh shumë rrallë, programuesit zakonisht harrojnë ta kryejnë këtë kontroll!

Treguesit null dhe alokimi dinamik i memories

Treguesit null (treguesit me vlerë 0 ose nullptr) janë veçanërisht të dobishëm në procesin e alokimit të grumbullit. Prania e tyre, si të thuash, thotë: "Ky tregues nuk i është caktuar asnjë memorie". Dhe kjo, nga ana tjetër, mund të përdoret për të kryer ndarjen e memories së kushtëzuar:

// nëse ptr nuk ka ndarë ende memorie, alokoje atë nëse (! ptr) ptr = new int;

Heqja e treguesit null nuk ndikon asgjë. Kështu, sa vijon është e panevojshme:

nëse (ptr) fshij ptr;

nëse (ptr)

fshij ptr;

Në vend të kësaj, thjesht mund të shkruani:

fshij ptr;

Nëse ptr nuk është null, atëherë ndryshorja e alokuar dinamikisht do të hiqet. Nëse vlera e treguesit është zero, atëherë asgjë nuk do të ndodhë.

Rrjedhje memorie

Kujtesa e alokuar në mënyrë dinamike nuk është e shtrirë. Kjo do të thotë, ai mbetet i alokuar derisa të lirohet në mënyrë eksplicite ose derisa programi juaj të dalë (dhe sistemi operativ i pastron vetë të gjitha buferat e memories). Megjithatë, treguesit e përdorur për të ruajtur adresat e memories të alokuara në mënyrë dinamike ndjekin rregullat e shtrirjes së variablave normale. Kjo mospërputhje mund të shkaktojë sjellje interesante.

Merrni parasysh funksionin e mëposhtëm:

void doSomething () (int * ptr = int e re;)

Operatori i ri ju lejon të ndani memorie për vargje. Ajo kthehet

një tregues për elementin e parë të grupit në kllapa katrore. Kur shpërndahet memorie për vargje shumëdimensionale, të gjitha dimensionet përveç atij më të majtën duhet të jenë konstante. Dimensioni i parë mund të specifikohet nga një ndryshore, vlera e së cilës është e njohur për përdoruesin në kohën kur përdoret e reja, për shembull:

int * p = int e re [k]; // nuk mund të konvertohet nga gabimi "int (*)" në "int *".

int (* p) = int e re [k]; // djathtas

Kur shpërndahet memorie për një objekt, vlera e tij do të jetë e papërcaktuar. Megjithatë, objektit mund t'i caktohet një vlerë fillestare.

int * a = int i ri (10234);

Ky parametër nuk mund të përdoret për të inicializuar vargje. por

në vend të vlerës së inicializimit, mund të vendosni një listë të ndarë me presje

vlerat i kalohen konstruktorit gjatë ndarjes së memories për një grup (masa-

mbjellja e objekteve të reja të përcaktuara nga përdoruesi). Kujtesa për një sërë objektesh

mund të ndahet vetëm nëse klasa përkatëse

ka një konstruktor të paracaktuar.

matr () (); // konstruktori i paracaktuar

matr (int i, float j): a (i), b (j) ()

(matr mt (3, .5);

matr * p1 = matr e re; // true р1 - tregues për 2 objekte

matr * p2 = matr i ri (2,3.4); // e pasaktë, inicializimi është i pamundur

matr * p3 = matr i ri (2,3.4); // true p3 - objekt i inicializuar

(int i; // komponent-të dhënat e klasës A

A () () // konstruktor i klasës A

~ A () () // shkatërrues i klasës A

(A * a, * b; // përshkrimi i treguesve për një objekt të klasës A

noton * c, * d; // përshkrimi i treguesve të elementeve të tipit float

a = A e re; // alokimi i memories për një objekt të klasës A

b = A e re; // alokimi i memories për një grup objektesh të klasës A

c = noton i ri; // alokimi i memories për një element float

d = noton e re; // alokimi i memories për një grup elementësh float

fshij a; // memorie e lirë e zënë nga një objekt

fshij b; // lironi memorien e zënë nga grupi i objekteve

fshij c; // memorie e lirë e një elementi float

fshij d; ) // lirimi i memories së një grupi elementësh float

Organizimi i aksesit të jashtëm në komponentët lokalë të klasës (miku)

Ne kemi përmbushur tashmë rregullin bazë të OOP - të dhënat (të brendshme

variablat) e objektit mbrohen nga ndikimet e jashtme dhe aksesi në to mund të jetë

merrni vetëm duke përdorur funksionet (metodat) e objektit. Por ka raste të tilla

çajrat, kur duhet të organizojmë aksesin në të dhënat e një objekti, duke mos përdorur

njohja e ndërfaqes (funksioneve) e tij. Sigurisht, mund të shtoni një funksion të ri publik

në klasë për qasje të drejtpërdrejtë në variablat e brendshme. Megjithatë, në

në shumicën e rasteve, ndërfaqja e objektit zbaton disa operacione dhe

funksioni i ri mund të jetë i tepërt. Në të njëjtën kohë, ndonjëherë ka

domosdoshmëria e organizimit të aksesit të drejtpërdrejtë në të dhënat e brendshme (lokale).

dy objekte të ndryshme nga një funksion. Për më tepër, në C ++, një funksion nuk mundet

mund të jetë një komponent i dy klasave të ndryshme.

Për ta zbatuar këtë, kualifikuesi mik futet në C ++. Nëse disa

funksioni përkufizohet si një funksion mik për disa klasë, pastaj ai:

Nuk është një komponent funksioni i kësaj klase;

Ka akses në të gjithë komponentët e kësaj klase (private, publike dhe të mbrojtura).

Më poshtë është një shembull ku hyn një funksion i jashtëm

të dhënat e brendshme të klasës.

#përfshi

duke përdorur hapësirën e emrave std;

kls (int i, int J): i (I), j (J) () // konstruktor

int max () (kthim i> j? i: j;) // funksioni i komponentit të klasës kls

argëtim i dyfishtë i shokut (int, kls &); // shok-deklarim i funksionit të jashtëm argëtues

argëtim i dyfishtë (int i, kls & x) // funksion i jashtëm

(kthimi (dyfish) i / x.i;

cout<< obj.max() << endl;

Në C (C ++), ekzistojnë tre mënyra për të kaluar të dhënat në një funksion: me vlerë

është e mundur për ndonjë objekt ekzistues. Mund të dallohen kohët e mëposhtme

Licenca e lidhjeve dhe treguesve. Së pari, pamundësia e ekzistencës së zeros

lidhjet nënkupton që ato nuk kanë nevojë të kontrollohen për korrektësi. Dhe kur përdorni një tregues, duhet ta kontrolloni atë për një vlerë jo zero. Së dyti, treguesit mund të tregojnë objekte të ndryshme, dhe referenca është gjithmonë në një objekt të specifikuar gjatë inicializimit të tij. Nëse dëshironi të siguroni një funksion për të ndryshuar vlerat

parametrat i kaluan atij, pastaj në gjuhën C duhet të deklarohen ose

globalisht, ose puna me ta në një funksion kryhet nëpërmjet

treguesit e saj për këto variabla. Në C ++, argumentet në një funksion mund të kalohen

rum vihet me shenjën &.

void fun1 (int, int);

void fun2 (int &, int &);

(int i = 1, j = 2; // i dhe j janë parametra lokalë

cout<< "\n адрес переменных в main() i = "<<&i<<" j = "<<&j;

cout<< "\n i = "<Pse C ++ lundron kur Vasa u fundos... Nëse kjo është për ju vërtetë problem, unë mund të rekomandoj std :: unique_ptr , dhe në të ardhmen standardi mund të na japë një dynarray.

Objekte dinamike

Objektet dinamike zakonisht përdoren kur është e pamundur të lidhësh jetëgjatësinë e një objekti me një fushë specifike. Nëse mund ta bëni, duhet patjetër të përdorni memorien automatike (shihni Pse nuk duhet të mbipërdorni objekte dinamike). Por kjo është tema e një artikulli të veçantë.

Kur krijohet një objekt dinamik, dikush duhet ta fshijë atë dhe me kusht llojet e objekteve mund të ndahen në dy grupe: ata që nuk janë të vetëdijshëm për procesin e fshirjes së tyre dhe ata që dyshojnë për diçka. Le të themi se të parët kanë një model standard të menaxhimit të memories, ndërsa të dytët një jo standard.

Llojet me modelin standard të menaxhimit të memories përfshijnë të gjitha llojet standarde, duke përfshirë kontejnerët. Në të vërtetë, kontejneri menaxhon memorien që ka ndarë vetë. Ai nuk ka të bëjë me atë se kush e ka krijuar dhe si do të hiqet.

Llojet me një model jo standard të menaxhimit të memories përfshijnë, për shembull, objektet Qt. Këtu, çdo objekt ka një prind që është përgjegjës për fshirjen e tij. Dhe objekti e di për këtë, sepse ai trashëgon nga klasa QObject. Kjo përfshin gjithashtu lloje me një numër referencash, për shembull, të dizajnuara për të punuar me boost :: intrusive_ptr.

Me fjalë të tjera, një lloj me një model standard të menaxhimit të memories nuk ofron ndonjë mekanizëm shtesë për menaxhimin e jetëgjatësisë së tij. Kjo duhet të bëhet tërësisht nga ana e përdoruesit. Por një lloj me një model jo standard ofron mekanizma të tillë. Për shembull, QObject ka metoda setParent () dhe fëmijë () dhe përmban një listë të fëmijëve, ndërsa boost :: intrusive_ptr mbështetet në funksionet intrusive_ptr_add_ref dhe intrusive_ptr_release dhe përmban një numër referencash.

Nëse lloji i një objekti ka një model standard të menaxhimit të memories, atëherë për shkurtim do të themi se ai është një objekt me një menaxhim standard të memories. Në mënyrë të ngjashme, nëse lloji i një objekti ka një model jo standard të menaxhimit të memories, atëherë do të themi se ai është një objekt me një menaxhim jo standard të memories.

Më pas, ne do të shqyrtojmë objektet e të dy modeleve. Duke parë përpara, duhet thënë se për objektet me menaxhim standard të memories, definitivisht nuk ia vlen të përdorni të reja dhe të fshini në kodin e klientit, dhe për objektet me jo standarde varet nga modeli specifik.

* Disa përjashtime: idioma e puçrrave; një objekt shumë i madh (si p.sh. një bufer memorie).

** Një përjashtim është std :: locale :: facet (shih më poshtë).

Objekte dinamike me menaxhim standard të memories

Këto gjenden më shpesh në praktikë. Dhe ato duhet të provohen të përdoren në C ++ moderne, sepse qasjet standarde të përdorura veçanërisht në treguesit inteligjentë funksionojnë me to.

Në fakt tregues të zgjuar, po, kjo është përgjigjja. Atyre duhet t'u jepet kontrolli mbi jetëgjatësinë e objekteve dinamike. Janë dy prej tyre në C ++: std :: shared_ptr dhe std :: unique_ptr. Këtu nuk do të theksojmë std :: slow_ptr, pasi është thjesht një ndihmës për std :: shared_ptr në raste të caktuara përdorimi.

Sa i përket std :: auto_ptr, ai është hequr zyrtarisht nga C ++ që nga C ++ 17. U prefsh ne paqe!

Unë nuk do të ndalem këtu në pajisjen dhe përdorimin e treguesve të zgjuar, tk. kjo është përtej qëllimit të këtij neni. Më lejoni t'ju kujtoj menjëherë se ato vijnë me funksionet e mrekullueshme std :: make_shared dhe std :: make_unique, dhe ato duhet të përdoren për të krijuar tregues inteligjentë.

ato. në vend të këtij:
std :: unique_ptr biskota (Cookie e re (brum, sheqer, kanellë));
duhet te shkruani keshtu:
cookie automatike = std :: make_unique (brum, sheqer, kanellë);
Përparësitë e funksioneve të krijimit ndaj krijimit të qartë të treguesve inteligjentë janë të dokumentuara mirë nga Herb Sutter në GotW # 89 të tij dhe Scott Myers në Efektive Moderne C ++ të tij, Pika 21. Nuk do ta përsëris veten, vetëm një listë e shkurtër pikash këtu:

  • Për të dyja, bëni funksionet:
    • Siguria në kushtet e përjashtimeve.
    • Nuk ka emër të tipit dublikatë.
  • Për std :: make_shared:
    • Fitimi i performancës si blloku i kontrollit ndahet pranë vetë objektit, gjë që zvogëlon numrin e thirrjeve në menaxherin e memories dhe rrit lokalitetin e të dhënave. Optimizimi.
Funksionet e bëra kanë gjithashtu një numër kufizimesh, të cilat përshkruhen në detaje në të njëjtat burime:
  • Për të dyja, bëni funksionet:
    • Ju nuk mund ta transferoni fshirësin tuaj. Kjo është mjaft logjike, pasi Brenda, bëj që funksionet të përdorin standardin e ri sipas përkufizimit.
    • Ju nuk mund të përdorni inicializuesin e kllapave ose të gjitha të mirat e tjera që lidhen me përcjelljen e përsosur (shih Efektive Moderne C ++, Pika 30).
  • Për std :: make_shared:
    • Mbipërdorimi i mundshëm i memories për objekte të mëdha me referenca të dobëta jetëgjatë (std :: dobët_pointer).
    • Problemet me operatorët e rinj dhe të fshirë të anashkaluar në nivel klase.
    • Ndarje e mundshme false ndërmjet objektit dhe bllokut të kontrollit (shih pyetjen në StackOverflow).
Në praktikë, këto kufizime janë të rralla dhe nuk i heqin përfitimet. Rezulton se treguesit inteligjentë na fshehën thirrjen e fshirjes dhe funksionet e bërjes fshehën thirrjen e re nga ne. Si rezultat, morëm kod më të besueshëm, në të cilin nuk ka as të ri as fshirje.

Nga rruga, Stefan Lavey (a.k.a. STL) zbulon seriozisht strukturën e funksioneve të krijimit në leksionet e tij. Këtu është një rrëshqitje elokuente nga fjalimi i tij Mos Ndihmo Kompiluesin:

Objekte dinamike me menaxhim jo standard të memories

Përveç qasjes standarde për menaxhimin e kujtesës përmes treguesve inteligjentë, ekzistojnë modele të tjera. Për shembull, numërimi i referencës dhe marrëdhëniet e prindit tek fëmijët.

Objekte dinamike me numërim referencash


Një teknikë shumë e zakonshme që përdoret në shumë biblioteka. Le të marrim si shembull bibliotekën OpenSceneGraph. Është një motor 3D ndër-platformë me burim të hapur i shkruar në C ++ dhe OpenGL.

Shumica e klasave në të trashëgojnë nga klasa osg :: e referuar, e cila zbaton numërimin e referencës brenda. Metoda ref () rrit numëruesin, metoda unref () zvogëlon numëruesin dhe fshin objektin kur numëruesi bie në zero.

Kompleti vjen gjithashtu me një tregues inteligjent osg :: ref_ptr e cila thërret metodën T :: ref () në objektin e ruajtur në konstruktorin e tij dhe metodën T :: unref () në destruktor. E njëjta qasje përdoret në boost :: intrusive_ptr, vetëm atje në vend të metodave ref () dhe unref () ka funksione të jashtme.

Le të hedhim një vështrim në fragmentin e kodit nga udhëzuesi zyrtar OpenSceneGraph 3.0: Fillestar:
osg :: ref_ptr vertices = new osg :: Vec3Array; // ... osg :: ref_ptr normals = new osg :: Vec3Array; // ... osg :: ref_ptr geom = new osg :: Gjeometri; geom-> setVertexArray (vertices.get ()); gjeom->
Konstruksione shumë të njohura si osg :: ref_ptr p = T e re. Pikërisht në të njëjtën mënyrë funksionet std :: make_unique dhe std :: make_shared përdoren për të krijuar klasat std :: unique_ptr dhe std :: shared_ptr, ne mund të shkruajmë funksionin osg :: make_ref për të krijuar klasën osg :: ref_ptr. Kjo bëhet shumë thjesht, në analogji me funksionin std :: make_unique:
hapësira e emrit osg (shabllon osg :: ref_ptr make_ref (Args && ... args) (ktheje T-në e re (std :: përpara (args) ...); ))
Le ta rishkruajmë këtë copë kodi të armatosur me funksionin tonë të ri:
vertices auto = osg :: make_ref (); // ... auto normals = osg :: make_ref (); // ... auto geom = osg :: make_ref (); geom-> setVertexArray (vertices.get ()); geom-> setNormalArray (normals.get ()); //...
Ndryshimet janë të parëndësishme dhe mund të bëhen lehtësisht automatikisht. Në këtë mënyrë të thjeshtë, ne marrim siguri për sa i përket përjashtimeve, pa dyfishim të emrit të tipit dhe përputhshmëri të përsosur me stilin standard.

Thirrja e fshirjes ishte fshehur tashmë në metodën osg :: Referencuar :: unref () dhe tani ne kemi fshehur thirrjen e re në funksionin osg :: make_ref. Pra, nuk ka të reja dhe fshini.

* Teknikisht, në këtë fragment nuk ka situata që janë të pasigurta nga pikëpamja e përjashtimeve, por në konfigurime më komplekse mund të jenë.

Objekte dinamike për dialog pa mode në MFC


Le të shohim një shembull specifik për bibliotekën MFC. Është një mbështjellës i klasave C ++ mbi API-në e Windows. Përdoret për të thjeshtuar zhvillimin e GUI-ve për Windows.

Një teknikë interesante që Microsoft zyrtarisht rekomandon përdorimin për krijimin e dialogëve pa modele. Sepse dialogu është i pakuptimtë, nuk është plotësisht e qartë se kush është përgjegjës për fshirjen e tij. Sugjerohet që ai të fshijë veten në metodën CDialog të anashkaluar :: PostNcDestroy (). Kjo metodë thirret pasi të jetë përpunuar mesazhi WM_NCDESTROY, mesazhi i fundit i marrë nga dritarja në ciklin e saj jetësor.

Në shembullin më poshtë, dialogu krijohet duke klikuar butonin në metodën CMainFrame :: OnBnClickedCreate () dhe fshihet në metodën e anashkaluar CMyDialog :: PostNcDestroy ().
void CMainFrame :: OnBnClickedCreate () (auto * pDialog = CMyDialog i ri (kjo); pDialog-> ShowWindow (SW_SHOW);) klasa CMyDialog: publik CDialog (publik: CMyDialog (CWnd * pParent) (PaDALYa të mbrojtura) (Krijo DIDOGrent) void PostNcDestroy () anuloj (CDialog :: PostNcDestroy (); fshije këtë;));
Këtu nuk kemi të fshehur as thirrjen e re dhe as thirrjen e fshirjes. Ka shumë mënyra për të qëlluar veten në këmbë. Përveç problemeve të zakonshme me treguesit, mund të harroni të anashkaloni metodën PostNcDestroy () në dialogun tuaj dhe do të kemi një rrjedhje memorie. Kur shihni një telefonatë për të re, mund të dëshironi të telefononi fshijeni veten në një moment të caktuar, ne marrim një fshirje të dyfishtë. Mund të krijoni aksidentalisht një objekt dialogu në memorien automatike, përsëri marrim një fshirje të dyfishtë.

Le të përpiqemi të fshehim thirrjet për të reja dhe të fshijmë brenda klasës së ndërmjetme CModelessDialog dhe fabrikës CreateModelessDialog, e cila do të jetë përgjegjëse për dialogët pa model në aplikacionin tonë:
klasa CModelessDialog: CDialog publik (publik: CModelessDialog (UINT nIDTemplate, CWnd * pParent) (Krijo (nIDTemplate, pParent);) i mbrojtur: void PostNcDestroy () anuloj (CDialog :: PostNcDestroy (); fshije këtë;)); // Fabrika për krijimin e shabllonit të dialogëve modalë Rrjedh * CreateModelessDialog (Args && ... args) (// Në vend të static_assert në trupin e funksionit, mund të përdorni std :: enable_if në kokën e tij, i cili do të na lejojë të përdorim SFINAE. // Por meqenëse mbingarkesat e tjera të këtij funksioni nuk ka gjasa të priten, duket e arsyeshme të përdoret një zgjidhje më e thjeshtë dhe më përshkruese.static_assert (std :: is_base_of :: vlera, "CreateModelessDialog duhet të thirret për pasardhësit e CModelessDialog"); automatik * pDialog = i ri Rrjedh (std :: përpara (args) ...); pDialog-> ShowWindow (SW_SHOW); ktheje pDialog; )
Vetë klasa anulon metodën PostNcDestroy (), në të cilën kemi fshehur delete, dhe për të krijuar klasa të pasardhësve, përdoret një fabrikë në të cilën kemi fshehur të reja. Krijimi dhe përkufizimi i klasës trashëgimtare tani duket kështu:
void CMainFrame :: OnBnClickedCreate () (CreateModelessDialog (kjo); ) klasa CMyDialog: publik CModelessDialog (publik: CMyDialog (CWnd * pParent): CModelessDialog (IDD_MY_DIALOG, pParent) ());
Sigurisht që nuk i zgjidhëm të gjitha problemet në këtë mënyrë. Për shembull, një objekt ende mund të ndahet në pirg dhe të marrë një fshirje të dyfishtë. Është e mundur të ndalohet shpërndarja e një objekti në pirg vetëm duke modifikuar vetë klasën e objektit, për shembull, duke shtuar një konstruktor privat. Por ne nuk mund ta bëjmë këtë nga klasa bazë CModelessDialog në asnjë mënyrë. Sigurisht, ju mund të fshehni fare klasën CMyDialog dhe ta bëni fabrikën jo një shabllon, por më klasik, duke pranuar një identifikues të klasës. Por e gjithë kjo tashmë është përtej qëllimit të artikullit.

Gjithsesi, ne kemi thjeshtuar krijimin e një dialogu nga kodi i klientit dhe shkrimin e një klase të re dialogu. Dhe në të njëjtën kohë, ne hoqëm thirrjet e reja dhe fshijmë nga kodi i klientit.

Objekte dinamike me një marrëdhënie prind-fëmijë



Ato janë mjaft të zakonshme, veçanërisht në bibliotekat për zhvillimin e GUI. Si shembull, merrni parasysh Qt, një bibliotekë e njohur e aplikacioneve dhe zhvillimit të UI.

Shumica e klasave trashëgojnë nga QObject. Ai mban një listë të fëmijëve në vetvete dhe i fshin ata kur fshihet vetë. Ruan një tregues për prindin (mund të jetë i pavlefshëm) dhe mund të ndryshojë prindin gjatë jetës.

Një shembull i shkëlqyeshëm i një situate ku heqja e të rejave dhe fshirja nuk është aq e lehtë. Biblioteka është krijuar në atë mënyrë që këta operatorë të mund dhe duhet të përdoren në shumë raste. Unë sugjerova një mbështjellës për krijimin e objekteve me një prind jo null, por ideja nuk funksionoi (shih diskutimin në listën e postimeve Qt).

Kështu, nuk jam i vetëdijshëm për një mënyrë të mirë për të hequr qafe të rejat dhe për të fshirë në Qt.

Dinamik std :: locale :: objekte të aspektit


C ++ përdor std :: objektet lokale për të kontrolluar daljen e të dhënave në transmetime. Një lokal është një grup aspektesh që përcaktojnë se si shfaqen të dhëna të caktuara. Facet kanë numërimin e tyre të referencës dhe kur kopjoni lokale, aspektet nuk kopjohen, vetëm treguesi kopjohet dhe numri i referencës rritet.

Lokaliteti është vetë përgjegjës për heqjen e aspekteve kur numri i referencës bie në zero, por përdoruesi duhet të krijojë aspektet duke përdorur operatorin e ri (shih seksionin Shënime në përshkrimin e konstruktorit të vendndodhjes std ::):
std :: default default; std :: locale myLocale (e parazgjedhur, std e re :: codecvt_utf8 );
Ky mekanizëm u zbatua edhe para prezantimit të treguesve standardë inteligjentë dhe është përjashtuar nga rregullat e përgjithshme për përdorimin e klasave në bibliotekën standarde.

Mund të bëhet një mbështjellës i thjeshtë për gjenerimin e vendndodhjes për të hequr të rejat nga kodi i klientit. Sidoqoftë, ky është një përjashtim mjaft i njohur nga rregullat e përgjithshme, dhe mbase nuk ka kuptim të rrethoni një kopsht perimesh për të.

konkluzioni

Pra, së pari shikuam skenarë të tillë si krijimi i grupeve dinamike dhe objekteve dinamike me menaxhimin standard të memories. Në vend të të rejave dhe të fshirjeve, ne përdorëm kontejnerë standardë dhe bëmë funksione dhe përfunduam me kod më të thjeshtë dhe më të besueshëm.

Më pas shikuam disa shembuj të menaxhimit të memories jo standarde dhe pamë se si mund ta përmirësonim kodin tonë duke hequr të rejat dhe duke e fshirë në mbështjellës të përshtatshëm. Ne gjetëm gjithashtu një shembull ku kjo qasje nuk funksionon.

Megjithatë, në shumicën e rasteve ky rekomandim jep rezultate të shkëlqyera dhe mund të përdoret si një parim i paracaktuar. Tani mund të supozojmë se nëse kodi përdor ri ose fshin, ky është një rast i veçantë që kërkon vëmendje të veçantë. Nëse i shihni këto thirrje në kodin e klientit, merrni parasysh nëse ato janë vërtet të justifikuara.

  • Shmangni përdorimin e ri dhe fshini në kodin tuaj. Mendoni për to si operacione të menaxhimit manual të grumbullit të nivelit të ulët.
  • Përdorni kontejnerë standardë për strukturat dinamike të të dhënave.
  • Përdorni funksionet make për të krijuar objekte dinamike sa herë që është e mundur.
  • Krijoni mbështjellës për objektet me një model memorie jo standarde.

Nga autori

Personalisht, kam hasur në shumë raste të rrjedhjeve dhe prishjeve të memories për shkak të përdorimit të tepërt të të rejave dhe fshirjeve. Po, shumica e këtij kodi është shkruar shumë vite më parë, por më pas programuesit e rinj fillojnë të punojnë me të dhe mendojnë se kështu duhet të shkruhet.

Shpresoj që ky artikull të vijë si një udhëzues praktik në të cilin mund të dërgohet një zhvillues i ri në mënyrë që të mos humbasë.

Pak më shumë se një vit më parë, bëra një prezantim mbi këtë temë në konferencën C ++ Rusia. Pas fjalimit tim, audienca u nda në dy grupe: ata për të cilët gjithçka ishte e qartë dhe ata që bënë një zbulim të mrekullueshëm për veten e tyre. Unë besoj se konferencat shpesh marrin pjesë nga zhvillues tashmë me përvojë, kështu që edhe nëse mes tyre kishte shumë njerëz që ishin të rinj në këtë informacion, shpresoj se ky artikull do të jetë i dobishëm për komunitetin.

PS Gjatë diskutimit të artikullit, kolegët e mi dhe unë patëm një mosmarrëveshje të tërë për mënyrën e duhur: "Myers" ose "Meyers". Nga njëra anë, "Meyers" tingëllon më e njohur për veshin rus, dhe ne vetë duket se e kemi thënë gjithmonë këtë. Nga ana tjetër, është Myers që përdoret në wiki. Nëse shikoni librat e lokalizuar, atëherë në përgjithësi ka dikush në kaq shumë: këtyre dy opsioneve u shtohet edhe "Meyers". Në konferenca të ndryshme njerëzit prezenteështë në mënyra të ndryshme. Në fund të fundit ne arriti ta zbulonte se ai e quan veten "Myers", për të cilën ata vendosën.

Lidhjet

  1. Herb Sutter, Zgjidhja GotW # 89: Treguesit inteligjentë.
  2. Scott Meyers, C ++ moderne efektive, Pika 21, f. 139.
  3. Stephan T. Lavavej, Mos e ndihmoni përpiluesin.
  4. Bjarne Stroustrup, Gjuha e programimit C ++, 11.2.1, f. 281.
  5. Pesë mite popullore rreth C ++., Pjesa 2
  6. Mikhail Matrosov, C ++ pa të reja dhe fshini.

Etiketa:

Shto etiketa

Komentet 134

Artikujt kryesorë të lidhur