Reguli de codificare pentru C ++ pentru experți reali. Continuăm să studiem complexitatea gestionării memoriei în C ++. Următoarele două pagini vor fi dedicate studiului aprofundat al operatorilor noi și eliminați. Dintre acestea, veți afla ce cere standardul C ++ asupra implementărilor utilizatorilor acestor operatori.
În ultimul articol, am reușit să ne dăm seama de ce este în general necesar să înlocuiți operatorii noi și să ștergeți cu versiunile lor și în ce cazuri putem face fără ea. Am atins, de asemenea, subiectul noii funcții și am discutat problema alinierii indicilor returnați.
Vom vorbi despre asta puțin mai târziu. În plus, noul auto-scris ar trebui să returneze valoarea corectă și să gestioneze corect solicitările de a aloca zero octeți. De asemenea, atunci când implementăm propriile noastre funcții de gestionare a memoriei, trebuie să avem grijă să nu ascundem formele lor "normale". Acum, despre toate astea în detaliu.
Contractele la scrierea operatorului nou Primul lucru pe care ar trebui să îl întoarcă noul utilizator este valoarea corectă. Dacă memoria este alocată cu succes, operatorul trebuie să returneze pointerul la ea. Dacă s-ar întâmpla ceva în neregulă, ar trebui ridicată o excepție de tipul bad_alloc.
Dar nu totul este la fel de simplu cum pare. Înainte de excepție, noul trebuie să genereze într-un ciclu un procesor de funcții care va încerca să rezolve o problemă. Ce ar trebui să facă această funcție, am analizat în detaliu în ultimul articol. Acum, vă reamintesc că poate elibera rezerva de memorie pregătită anterior în caz de lipsă, inițiază o excepție sau completează programul în totalitate. Este extrem de important ca funcția handler să funcționeze corect, deoarece ciclul de apeluri va fi executat până când situația conflictului va fi rezolvată. Următorul lucru important pe care trebuie să-l analizăm este prelucrarea cererilor de alocare a octeților zero ai memoriei. Destul de ciudat, suna asta, dar C ++ standard necesita in acest caz operarea corecta a operatorului. Acest comportament simplifică implementarea unor lucruri în alte locuri ale limbii. Luând în considerare toate acestea, puteți încerca să adăugați un pseudo-cod personalizat al unui utilizator:
Pseudo-codul definit de utilizator pentru operator nou
void * operator nou (std :: size_t size)
arunca (std :: bad_alloc)
folosind namespace std;
// procesează cererea pentru 0 octeți,
// presupunând că trebuie să alocați un octet
dacă (dimensiune == 0)
dimensiunea = 1;
în timp ce (adevărat)
// încercați să alocați octeți de mărime;
dacă (selectați-o posibilă)
întoarcere (pointer în memorie);
// alocarea memoriei a eșuat
// verificați dacă funcția de manipulare este setată
new_handler globalHandler = set_new_handler (0);
set_new_handler (globalHandler);
dacă (globalHandler)
(* globalHandler) ();
altfel
arunca std :: bad_alloc ();
>
>
În special sensibile, poate confunda alocarea unui octet de memorie atunci când suntem rugați să zero. Da, este nepoliticos, dar funcționează. Adevărul este că trebuie să returnați pointerul corect chiar și atunci când ni se cere 0 octeți, așa că trebuie să inventăm. Și cu cât invenția este mai simplă, cu atât va fi mai fiabilă.
Voi repeta încă o dată că noul operator cheamă funcția de handler în cazul unor probleme cu alocarea memoriei într-o buclă infinită. Este foarte important ca codul acestei funcții rezolvat corect problema, și-au pus la dispoziție mai multă memorie agitat un tip de excepție, care este derivat din bad_alloc, un alt set de tratare, a pus handler curent sau să nu se întoarcă deloc. În caz contrar, programul care ne-a denumit noua versiune va fi suspendat.
În mod separat, ar trebui să luăm în considerare cazul când noua este o funcție a unui membru al unei clase. De obicei, versiunile utilizatorilor de operatori de memorie sunt scrise pentru o alocare optimizată a memoriei. De exemplu, noul pentru clasa Base este închis pentru alocarea sizeof (Base) - nu mai mult, nu mai puțin.
Dar dacă am crea o clasă care moștenește de la Bază? Clasa copil va folosi, de asemenea, versiunea noului operator definit în baza. Dar mărimea unei clase derivate (numite derivate), este probabil să fie diferită de dimensiunea bazei: sizeof = (derivare) sizeof (Base) !. Din acest motiv, toate avantajele implementării proprii de noi pot să nu se producă. Despre care, apropo, mulți oameni uită și apoi suferă de suferințe inumane.
Problema moștenirii operatorului este nouă
clasă Publicul de bază:
static void * operator nou (std :: size_t size)
arunca (std :: bad_alloc);
.
>;
// noul operator nu este declarat în subclasă
clasă derivată: bază publică
;
// call Base :: operator nou
Derivat * p = derivat nou;
Rezolvarea problemei este destul de simplă, dar ar trebui făcută în avans. Este suficient pentru clasa de bază, în corpul operatorului nou, să verificați dimensiunea memoriei alocate. Dacă numărul de octeți cerut nu coincide cu numărul de octeți din obiectul Base, atunci cel mai bine este să alocați memorie la implementarea standard nouă. În plus, rezolvăm imediat problema procesării unei cereri de alocare a zero octeți de memorie - aceasta se va face deja prin versiunea obișnuită a operatorului.
Soluția problemei de moștenire pentru operator nou
void * operator nou (std :: size_t size)
arunca (std :: bad_alloc)
// dacă mărimea este greșită, sunați standard nou
dacă (dimensiunea! = sizeof (Base))
întoarce. operator nou (dimensiune);
// altfel, procesați cererea
.
>
La nivelul clasei, puteți defini, de asemenea, noi pentru matrice (operatorul nou []). Această declarație nu ar trebui să facă nimic decât să aloce un bloc de memorie neformatată. Nu putem efectua operațiuni cu obiecte care nu au fost încă create. Și, pe lângă aceasta, nu știm dimensiunea acestor obiecte, deoarece pot fi moștenitori ai clasei în care este definită noua []. Adică numărul de obiecte din matrice nu este neapărat egal cu (numărul cerut de octeți) / sizeof (Base). Mai mult, pentru rețelele dinamice, se poate aloca mai multă memorie decât obiectele pe care le vor lua ele însele pentru a furniza o rezervă.
Pseudo-codul definit de utilizator pentru operatorul de ștergere
void * operatorul șterge (void * rawMemory) throw ()
// dacă indicatorul nul nu face nimic
dacă (rawMemory == 0) returnează;
// eliberați memoria indicată de
rawMemory;
>
Dacă operatorul de ștergere este o funcție membră a clasei, atunci, ca și în cazul noilor, trebuie să aveți grijă să verificați mărimea memoriei de șters. Dacă noua implementare a utilizatorului pentru clasa Base alocă sizeof (Base) pentru bytes de memorie, atunci ștergerea de curățare trebuie să elibereze exact aceleași octeți. În caz contrar, în cazul în care dimensiunea memoriei amovibil nu se potrivește cu dimensiunea clasei, care definește un operator trebuie să transmită standardul de lucru șterge.
Pseudocodul funcției membrului ștergere
clasă Publicul de bază:
static void * operator nou (std :: size_t size)
arunca (std :: bad_alloc);
static void * operatorul șterge
(void * rawMemory, std :: size_t size) arunca ();
.
>;
void * Base :: operatorul șterge (void * rawMemory,
std :: size_t size) arunca ()
// dacă indicatorul nul nu face nimic
dacă (rawMemory == 0) returnează;
dacă (dimensiunea! = sizeof (Base)). operatorul șterge (rawMemory);
return;
>
// eliberați memoria indicată de
rawMemory;
>
Noi și eliminați operatorii cu alocare
Funcția operator nouă, care ia parametri suplimentari, se numește "noul operator cu alocare". De obicei, un parametru opțional este o variabilă de tip void *. Astfel, definiția de plasare a noilor modele arată astfel:
void * operator nou (std :: size_t, void * pMemory)
Într-un sens mai larg, noul cu plasare poate lua orice număr de parametri suplimentari de orice tip. Operatorul de ștergere este numit "găzduit" de același principiu - trebuie să accepte și parametri suplimentari pe lângă cei principali.
Acum, să luăm în considerare cazul în care creăm dinamic un obiect al unei clase. Codul pentru o astfel de operațiune ar trebui să fie bine cunoscut tuturor:
widget * pw = widget nou
Obiectul este creat în două etape. În primele cerințele de memorie alocate operatorului nou standard, iar într-un al doilea constructor de clasa este numit Widget, care inițializează obiectul. O situație poate apărea atunci când memoria este în prima etapă va fi selectat și constructorul aruncă o excepție, iar indicatorul * pw rămâne neinițializate. Achtung! În acest fel vom obține o scurgere de memorie potențială. Pentru a preveni acest lucru, sistemul de timp de rulare C ++ trebuie luat în considerație. Trebuie să apeleze operatorul de ștergere pentru memoria alocată în prima etapă a creării obiectului. Dar există o mică nuanță care poate strica totul. Apelurile C ++ ștergeți, semnătura cărora se potrivește cu semnătura nouă, utilizată pentru alocarea memoriei. Când folosim formularele standard de noi și șterge, nu există nici o problemă, dar dacă vom scrie o nouă destinație de plasare privată și să uitați nakodil formă corespunzătoare șterge, atunci suntem aproape o sută la sută probabilitatea de a avea o scurgere de memorie, atunci când o excepție este aruncată în constructorul clasei.
Acest cod poate duce la scurgeri de memorie
clasă Widget public:
.
static void * operator nou (std :: size_t size,
std :: ostream logStream) arunca (std :: bad_alloc);
static void * operatorul șterge (void * pMemory,
std :: size_t size) arunca ();
.
>;
Widget * pw = nou (std :: cerr) Widget;
Soluția la această problemă este să scrie operatorul de ștergere cu semnătura corespunzătoare semnăturii noi cu destinația de plasare. Dacă este necesar să anulați alocarea de memorie, această ștergere a operatorului va fi apelată de sistemul de timp de rulare C ++. În cod, aceasta ar putea arăta astfel:
Acum scurgeri nu ar trebui să fie
clasă Widget public:
.
static void * operator nou (std :: size_t size,
std :: ostream logStream)
arunca (std :: bad_alloc);
static void * operatorul șterge (void * pMemory,
std :: size_t size)
arunca ();
static void * operatorul șterge (void * pMemory,
std :: ostream logStream)
arunca ();
.
>;
Widget * pw = nou (std :: cerr) Widget;
Nu uitați că obiectul construit poate fi eliminat prin metoda standard de ștergere. Pentru a evita complet toate problemele posibile asociate cu alocarea memoriei, ar trebui să înlocuiți și această versiune a funcției fără memorie.
Un alt punct important este ascunderea numelor de funcții. Dacă definim orice formă nouă, atunci toate celelalte forme standard ale acestui operator vor fi indisponibile.
Ascunderea numelor
clasă Publicul de bază:
static void * operator nou (std :: size_t size,
std :: ostream logStream)
arunca (std :: bad_alloc);
...
>;
// Eroare! Forma obișnuită nouă este ascunsă
Bază * pb = Bază nouă;
// corect, alocat nou de la baza
Bază * pb = nouă (std :: cerr) Bază;
concluzie
Pe aceasta am terminat sa intelegem caracteristicile managementului memoriei in C ++ si am primit o parte din cunostintele utile care vor fi utile oricarui codificator de sine. Până la noi întâlniri în aer!
Distribuiți acest articol cu prietenii dvs.:
- Acum 2 ore