Kako podesiti pametne telefone i računare. Informativni portal
  • Dom
  • Windows 8
  • Korištenje novog delete za implementaciju nizova. §11 Nizovi i pokazivači

Korištenje novog delete za implementaciju nizova. §11 Nizovi i pokazivači

15.8. Operateri novi i brisanje

Podrazumevano, alokacija objekta klase iz hrpe i oslobađanje memorije koju zauzima vrši se pomoću globalnih operatora new () i delete (), definisanih u standardnoj biblioteci C ++. (Razgovarali smo o ovim operatorima u Odjeljku 8.4.) Ali klasa može implementirati svoju vlastitu strategiju upravljanja memorijom pružanjem operatora članova istog imena. Ako su definirani u klasi, pozivaju se umjesto globalnih operatora kako bi se dodijelila i oslobodila memorija za objekte ove klase.

Definirajmo operatore new () i delete () u našoj klasi Screen.

Operator člana new () mora vratiti vrijednost void * i uzeti kao svoj prvi parametar vrijednost size_t, gdje je size_t typedef definiran u datoteci zaglavlja sistema. Evo njegove najave:

void * operator novi (veličina_t);

Kada se new () koristi za kreiranje objekta tipa klase, kompajler provjerava da li je takav operator definiran u toj klasi. Ako je odgovor da, tada se za dodjelu memorije objektu poziva, u suprotnom - globalni operator new (). Na primjer, sljedeća izjava

Ekran * ps = novi ekran;

kreira objekat Screen u hrpi, a pošto ova klasa ima operator new (), poziva se. Operatorov parametar size_t se automatski inicijalizira vrijednošću jednakom veličini ekrana u bajtovima.

Dodavanje operatora new () u klasu ili njegovo uklanjanje odatle se ne odražava u korisničkom kodu. Poziv na new izgleda isto i za globalnog operatera i za operatora člana. Ako klasa Screen nije imala svoj new (), tada bi poziv ostao ispravan, samo bi se pozvao globalni operator umjesto operatora člana.

Operator rezolucije globalnog opsega može pozvati globalni new () čak i ako klasa Screen definira svoju vlastitu verziju:

Ekran * ps = :: novi ekran;

void operator delete (void *);

Kada je operand delete pokazivač na objekt tipa klase, kompajler provjerava da li je operator delete () definiran u toj klasi. Ako je tako, onda je on taj koji je pozvan da oslobodi memoriju, inače - globalna verzija operatora. Sljedeća instrukcija

oslobađa memoriju koju koristi objekt Screen na koji ukazuje ps. Pošto Screen ima operator člana delete (), to je ono što važi. Parametar operatora tipa void * se automatski inicijalizira na ps. Dodavanje delete () u klasu ili njegovo uklanjanje odatle ni na koji način ne utiče na korisnički kod. Poziv za brisanje izgleda isto i za globalnog operatera i za operatora člana. Ako klasa Screen nije imala svoj operator delete (), tada bi poziv ostao ispravan, samo bi se pozvao globalni operator umjesto operatora člana.

Koristeći operator rezolucije globalnog opsega, možete pozvati globalno delete () čak i ako Screen definira vlastitu verziju:

Općenito, korišteni operator delete () mora odgovarati operatoru new () kojem je memorija dodijeljena. Na primjer, ako ps ukazuje na područje memorije koje je dodijelio globalni new (), tada koristite globalno brisanje () da ga oslobodite.

Operator delete () definiran za tip klase može sadržavati dva parametra umjesto jednog. Prvi parametar bi i dalje trebao biti tipa void *, a drugi bi trebao biti unaprijed definiranog tipa size_t (ne zaboravite uključiti datoteku zaglavlja):

// zamjenjuje

// void operator delete (void *);

Ako postoji drugi parametar, kompajler ga automatski inicijalizira vrijednošću jednakom veličini objekta kojem se adresira prvi parametar u bajtovima. (Ovaj parametar je važan u hijerarhiji klasa u kojoj operator delete () može biti naslijeđen od strane izvedene klase. Više o nasljeđivanju govori se u poglavlju 17.)

Razmotrimo detaljnije implementaciju operatora new () i delete () u klasi Screen. Naša strategija alokacije memorije će se zasnivati ​​na povezanoj listi Screen objekata, na koje ukazuje član freeStore-a. Svaki put kada se pozove operator člana new (), vraća se sljedeći objekt sa liste. Kada pozovete delete (), objekat se vraća na listu. Ako je, prilikom kreiranja novog objekta, lista adresirana na freeStore prazna, tada se poziva globalni operator new () da dobije blok memorije dovoljan za pohranjivanje screenChunk objekata klase Screen.

I screenChunk i freeStore su od interesa samo za Screen, tako da ćemo ih učiniti privatnim članovima. Osim toga, za sve kreirane objekte naše klase, vrijednosti ovih članova moraju biti iste, te stoga moraju biti proglašene statičkim. Za održavanje strukture povezane liste objekata ekrana, potreban nam je treći sljedeći član:

void * operator novi (veličina_t);

void operator delete (void *, size_t);

statički ekran * freeStore;

static const int screenChunk;

Evo jedne moguće implementacije operatora new () za klasu Screen:

#include "Screen.h"

#include cstddef

// statički članovi su inicijalizirani

// u izvornim datotekama programa, a ne u datotekama zaglavlja

Ekran * Ekran :: freeStore = 0;

const int Screen :: screenChunk = 24;

void * Screen :: operator novi (size_t size)

if (! freeStore) (

// povezana lista je prazna: dobijete novi blok

// poziva se globalni operator new

size_t chunk = screenChunk * veličina;

reinterpret_cast Screen * (novi znak [komad]);

// uključiti rezultirajući blok u listu

p! = & freeStore [screenChunk - 1];

freeStore = freeStore-next;

A evo implementacije operatora delete ():

void Screen :: operator delete (void * p, size_t)

// vratimo "obrisani" objekat,

// na listu slobodnih

(static_cast Screen * (p)) - sljedeći = freeStore;

freeStore = static_cast ekran * (p);

Operator new () može se deklarirati u klasi bez odgovarajućeg delete (). U ovom slučaju, objekti se oslobađaju pomoću globalnog operatora istog imena. Također je dozvoljeno deklarirati operator delete () bez new (): objekti će biti kreirani pomoću globalnog operatora istog imena. Međutim, ovi operatori se obično implementiraju u isto vrijeme, kao u gornjem primjeru, pošto su programeru klase obično potrebna oba.

Oni su statički članovi klase, čak i ako ih programer eksplicitno ne deklarira kao takve i poštuju uobičajena ograničenja za takve funkcije člana: ne prosljeđuje im se pokazivač this, pa stoga mogu samo direktno pristupiti statičkim članovima. (Pogledajte raspravu o statičkim funkcijama člana u odjeljku 13.5.) Razlog zašto su ovi operatori učinjeni statičnim je zato što se pozivaju ili prije nego što je objekt klase izgrađen (new ()) ili nakon što je uništen (delete ()).

Dodjela memorije pomoću operatora new (), na primjer:

Ekran * ptr = novi ekran (10, 20);

// Pseudokod u C ++

ptr = Screen :: operator novi (sizeof (Screen));

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

Drugim riječima, prvo se poziva operator new () definiran u klasi da dodijeli memoriju za objekt, a zatim ovaj objekt inicijalizira konstruktor. Ako new () ne uspije, izbacuje se izuzetak tipa bad_alloc i konstruktor se ne poziva.

Oslobađanje memorije pomoću operatora delete (), na primjer:

je ekvivalentno uzastopnom izvršavanju instrukcija poput ove:

// Pseudokod u C ++

Ekran :: ~ Ekran (ptr);

Screen :: operator delete (ptr, sizeof (* ptr));

Dakle, kada je objekt uništen, prvo se poziva destruktor klase, a zatim operator delete () koji je definiran u klasi kako bi se oslobodila memorija. Ako je ptr 0, tada se ne pozivaju ni destruktor ni delete ().

15.8.1. Operateri novi i brisanje

Operator new () definiran u prethodnom pododjeljku poziva se samo kada je memorija dodijeljena za jedan objekt. Dakle, ova instrukcija poziva new () klase Screen:

Ekran * ps = novi ekran (24, 80);

dok se ispod poziva globalni operator new () da dodijeli memoriju iz hrpe za niz objekata tipa Screen:

// Screen :: operator new () je pozvan

Ekran * psa = novi ekran;

U klasi takođe možete deklarisati operatore new () i delete () za rad sa nizovima.

Operator člana new () mora vratiti vrijednost void * i uzeti vrijednost size_t kao prvi parametar. Evo njegove najave za Screen:

void * operator novi (veličina_t);

Kada koristi new za kreiranje niza objekata tipa klase, kompajler provjerava da li je operator new () definiran u klasi. Ako je tako, onda je on taj koji se poziva da bi se dodijelila memorija za niz, inače - globalni novi (). Sljedeća naredba hrpe kreira niz od deset Screen objekata:

Ekran * ps = novi ekran;

Ova klasa ima operator new (), zbog čega se poziva da dodijeli memoriju. Njegov parametar size_t se automatski inicijalizira na količinu memorije, u bajtovima, koja je potrebna za smještaj deset Screen objekata.

Čak i ako klasa ima operator člana new (), programer može pozvati global new () da kreira niz koristeći operator rezolucije globalnog opsega:

Ekran * ps = :: novi ekran;

Operator delete (), koji je član klase, mora biti tipa void i uzeti void * kao prvi parametar. Evo kako izgleda njegov oglas za Screen:

void operator delete (void *);

Da biste izbrisali niz objekata klase, delete se mora pozvati ovako:

Kada je operand delete pokazivač na objekat tipa klase, kompajler provjerava da li je operator delete () definiran u toj klasi. Ako je tako, onda je on taj koji je pozvan da oslobodi memoriju, inače - njenu globalnu verziju. Parametar tipa void * se automatski inicijalizira vrijednošću adrese početka memorijske oblasti u kojoj se nalazi niz.

Čak i ako klasa ima operator člana delete (), programer može pozvati globalno delete () koristeći operator rezolucije globalnog opsega:

Dodavanje novih () ili delete () operatora u klasu ili njihovo uklanjanje odatle se ne odražava u korisničkom kodu: pozivi globalnim operatorima i operatorima članicama izgledaju isto.

Prilikom kreiranja niza prvo se poziva new () kako bi se dodijelila potrebna memorija, a zatim se svaki element inicijalizira korištenjem zadanog konstruktora. Ako klasa ima barem jedan konstruktor, ali nema zadanog konstruktora, tada se pozivanje operatora new () smatra greškom. Ne postoji sintaksa za određivanje inicijalizatora elementa niza ili argumenata konstruktora klase prilikom kreiranja niza na ovaj način.

Kada se niz uništi, prvo se poziva destruktor klase da uništi elemente, a zatim se poziva operator delete () da oslobodi svu memoriju. Važno je koristiti ispravnu sintaksu kada to radite. Ako uputstva

ps ukazuje na niz objekata klase, tada će odsustvo uglastih zagrada uzrokovati pozivanje destruktora samo za prvi element, iako će memorija biti potpuno oslobođena.

Operator člana delete () može imati ne jedan, već dva parametra, dok drugi mora biti tipa size_t:

// zamjenjuje

// void operator delete (void *);

void operator delete (void *, size_t);

Ako je drugi parametar prisutan, kompajler ga automatski inicijalizira vrijednošću jednakom količini memorije koja je dodijeljena za niz u bajtovima.

Iz knjige C++ Referentni priručnik autor Stroustrap Bjarn

R.5.3.4 Operator delete Operator delete uništava objekat kreiran pomoću new.release-expression: :: opt delete cast-expression :: opt delete cast-expression Rezultat je tipa void. Operand za brisanje mora biti pokazivač koji vraća new. Učinak primjene operacije brisanja

Iz knjige Microsoft Visual C++ i MFC. Programiranje za Windows 95 i Windows NT autor Frolov Aleksandar Vjačeslavovič

Operatori new i delete Operator new kreira objekt specificiranog tipa. Kada to radi, on dodjeljuje memoriju potrebnu za pohranjivanje objekta i vraća pokazivač koji pokazuje na njega. Ako se iz nekog razloga memorija ne može dobiti, operator vraća nultu vrijednost. Operater

Iz knjige Efikasno korištenje C ++. 55 sigurnih načina za poboljšanje strukture i koda vaših programa od Meyersa Scotta

Pravilo 16: Koristite iste forme new i izbrišite Šta nije u redu sa sledećim fragmentom? Std :: string * stringArray = new std :: string; ... izbrišite stringArray; Na prvi pogled, sve je u redu - korišćenje new odgovara korišćenju obriši, ali ovdje nešto nije u redu. Ponašanje programa

Iz knjige Windows Script Host za Windows 2000 / XP autor Popov Andrej Vladimirovič

Poglavlje 8 Prilagođavanje novog i brisanja Ovih dana, kada računarska okruženja imaju ugrađenu podršku za sakupljanje smeća (kao što su Java i .NET), C++-ov ručni pristup upravljanju memorijom može izgledati pomalo zastarjelo. Međutim, mnogi programeri koji stvaraju zahtjevni

Iz knjige Standardi programiranja C++. 101 pravilo i preporuka autor Alexandrescu Andrey

Metoda brisanja Ako je force lažna ili nije navedena, metoda Delete neće moći izbrisati direktorij samo za čitanje. Postavljanje sile na true će odmah izbrisati takve direktorije. Korištenje metode Delete nije važno ako je navedeno

Iz knjige Flash Reference autor Tim autora

Metoda brisanja Ako je force lažna ili nije navedena, tada metoda Delete neće moći izbrisati datoteku samo za čitanje. Postavljanje sile na true će omogućiti da se takvi fajlovi odmah izbrišu. Napomena Umjesto metode Delete, možete koristiti metodu DeleteFile.

Iz knjige Firebird VODIČ ZA DIZAJNER BAZE PODATAKA od Borri Helen

Relacioni i Bulovi operatori Relacioni operatori se koriste za poređenje vrednosti dve varijable. Ovi operatori, opisani u tabeli. A2.11, može vratiti samo Booleove vrijednosti true ili false.Tabela A2.11. Operatori relacije Operatorski uvjet, za

Iz knjige Linux i UNIX: Shell programiranje. Vodič za programere. od Teinsley David

45. new i delete uvijek treba razvijati zajedno. Sažetak Svako preopterećenje operatora void * new (parms) u klasi mora biti praćeno odgovarajućim preopterećenjem operatora void * delete (void *, parms), gdje je parms lista dodatnih tipova parametara (prvi je uvijek std :: size_t). Također

Iz autorove knjige SQL Reference

delete - Brisanje objekta, elementa niza ili varijable delete (Operator) Ovaj operator se koristi za brisanje objekta, svojstva objekta, elementa niza ili varijabli iz skripte Sintaksa: identifikator brisanja; Argumenti: Opis: Operator delete uništava objekt ili varijabla, ime

Iz knjige Razumijevanje SQL-a od Grubera Martina

Naredba DELETE Naredba DELETE se koristi za brisanje cijelih redova iz tablice. SQL ne dopušta da jedan izraz DELETE izbriše redove iz više od jedne tablice. Naredba DELETE koja samo modificira trenutni red kursora naziva se pozicionirano brisanje.

Iz knjige autora

15.8. Operatori new i delete Podrazumevano, alokacija objekta klase iz gomile i oslobađanje memorije koju on zauzima se izvode pomoću globalnih operatora new () i delete (), koji su definisani u standardnoj biblioteci C ++. (Ove operatore smo pokrili u Odjeljku 8.4.) Ali klasa može implementirati

Iz knjige autora

15.8.1. Operatori new i delete Operator new (), definiran u prethodnom pododjeljku, poziva se samo kada je memorija dodijeljena za jedan objekt. Dakle, u ovoj izjavi, new () klase Screen se zove: // Screen :: operator new () Screen * ps = new Screen (24, 80); dok se ispod naziva

Kao što znate, u jeziku C, funkcije malloc () i free () se koriste za dinamičko dodjeljivanje i oslobađanje memorije. Međutim, C ++ sadrži dva operatora koji dodelu i oslobađanje memorije čine efikasnijim i jednostavnijim. Ovi operatori su novi i izbrisani. Njihov opšti oblik je:

pointer_variable = nova varijabla_type;

delete point_variable;

Ovdje varijabla_pokazivač je pokazivač tipa varijabla_tip. Operator new dodjeljuje memoriju za pohranjivanje vrijednosti tipa varijabla_type i vraća njenu adresu. Bilo koji tip podataka može se postaviti sa novim. Operator delete oslobađa memoriju na koju ukazuje pokazivač varijabla_pokazivač.

Ako se operacija dodjele memorije ne može izvesti, tada novi operator izbacuje izuzetak tipa xalloc. Ako program ne uhvati ovaj izuzetak, tada će biti prekinut. Iako je ovo po defaultu u redu za kratke programe, aplikacije iz stvarnog svijeta obično moraju uhvatiti izuzetak i postupati s njim na odgovarajući način. Da biste pratili ovaj izuzetak, morate uključiti datoteku zaglavlja osim.h.

Operator delete treba koristiti samo za pokazivače na memoriju dodijeljenu pomoću novog operatora. Upotreba operatora delete sa drugim vrstama adresa može uzrokovati ozbiljne probleme.

Postoji niz prednosti korištenja new u odnosu na malloc (). Prvo, novi operater automatski izračunava veličinu potrebne memorije. Nema potrebe za korištenjem operatora sizeof (). Što je još važnije, sprečava da se pogrešna količina memorije slučajno dodijeli. Drugo, operator new automatski vraća pokazivač traženog tipa, tako da nema potrebe za korištenjem operatora konverzije. Treće, kao što će uskoro biti opisano, moguće je inicijalizirati objekt koristeći new operator. Konačno, moguće je preopteretiti operator new i delete globalno ili u odnosu na klasu koja se kreira.

Ispod je jednostavan primjer korištenja operatora new i delete. Obratite pažnju na upotrebu bloka try/catch za praćenje grešaka u dodjeli memorije.

#include
#include
int main ()
{
int * p;
probaj (
p = novi int; // dodijeliti memoriju za int
) uhvatiti (xalloc xa) (
cout<< "Allocation failure.\n";
povratak 1;
}
* p = 20; // dodjeljivanje vrijednosti 20 ovoj memorijskoj lokaciji
cout<< *р; // демонстрация работы путем вывода значения
delete p; // slobodna memorija
return 0;
}

Ovaj program p dodjeljuje adresu bloka memorije koji je dovoljno velik da sadrži cijeli broj. Ovoj memoriji se tada dodjeljuje vrijednost i prikazuje se sadržaj memorije. Konačno, dinamički dodijeljena memorija se oslobađa.

Kao što je napomenuto, moguće je inicijalizirati memoriju pomoću novog operatora. Da biste to učinili, morate navesti vrijednost inicijalizacije u zagradama iza imena tipa. Na primjer, u sljedećem primjeru, memorija na koju ukazuje p je inicijalizirana na 99:

#include
#include
int main ()
{
int * p;
probaj (
p = novi int (99); // inicijalizacija 99
) uhvatiti (xalloc xa) (
cout<< "Allocation failure.\n";
povratak 1;
}
cout<< *p;
delete p;
return 0;
}

Nizovi se mogu dodijeliti novim. Opšti oblik za jednodimenzionalni niz je:

pointer_variable = nova varijabla_type [veličina];

Ovdje veličina određuje broj elemenata u nizu. Postoji važno ograničenje koje treba zapamtiti kada postavljate niz: ne može se inicijalizirati.

Da biste oslobodili dinamički dodijeljeni niz, koristite sljedeći oblik operatora delete:

delete point_variable;

Ovdje zagrade obavještavaju operator delete da oslobodi memoriju dodijeljenu za niz.

Sljedeći program dodjeljuje memoriju za niz od 10 floats. Elementima niza se dodeljuju vrednosti od 100 do 109, a zatim se sadržaj niza ispisuje na ekran:

#include
#include
int main ()
{
float * p;
int i;
probaj (
p = novi float; // dobijemo deseti element niza
) uhvatiti (xalloc xa) (
cout<< "Allocation failure.\n";
povratak 1;
}
// dodjeljivanje vrijednosti od 100 do 109
za (i = 0; i<10; i + +) p[i] = 100.00 + i;
// prikazujemo sadržaj niza
za (i = 0; i<10; i++) cout << p[i] << " ";
delete p; // brišemo cijeli niz
return 0;
}

C ++ podržava tri glavna tipa izlučevine (distribucija) memorija, sa dva od kojih smo već upoznati:

Statička alokacija memorije se izvršava za i varijable. Memorija se dodjeljuje jednom prilikom pokretanja programa i zadržava se tokom cijelog programa.

Automatska dodjela memorije se izvodi za i. Memorija se dodjeljuje kada uđete u blok koji sadrži ove varijable, a oslobađa se kada izađete iz njega.

je tema ovog članka.

I statička i automatska alokacija memorije imaju dvije zajedničke stvari:

Veličina varijable / niza mora biti poznata u vrijeme kompajliranja.

Alokacija i oslobađanje memorije se dešava automatski (kada se varijabla kreira ili uništi).

U većini slučajeva to je u redu. Međutim, kada je u pitanju rad s vanjskim ulazima, ova ograničenja mogu dovesti do problema.

Na primjer, kada se koristi za pohranjivanje imena, ne znamo unaprijed koliko će trajati dok ga korisnik ne unese. Ili kada treba da upišemo broj zapisa sa diska u promenljivu, a ne znamo unapred koliko tih zapisa ima. Ili možemo kreirati igru ​​s promjenjivim brojem čudovišta (tokom igre neka čudovišta umiru, druga se rađaju), pokušavajući tako ubiti igrača.

Ako trebamo deklarirati veličinu svih varijabli u vrijeme kompajliranja, najbolje što možemo učiniti je pokušati pogoditi njihovu maksimalnu veličinu, nadajući se da će ovo biti dovoljno:

char ime; // nadamo se da korisnik unese ime sa manje od 30 karaktera! Record record; // nadamo se da neće biti više od 400 zapisa! Monster monster; // Maksimalno 30 čudovišta Renderiranje poligona; // ovo 3D prikazivanje je bolje sa manje od 40.000 poligona!

Ovo je loša odluka iz najmanje tri razloga:

Prvo, memorija se gubi ako se varijable zapravo ne koriste ili se koriste, ali ne u potpunosti. Na primjer, ako za svako ime dodijelimo 30 znakova, ali će imena u prosjeku zauzimati 15 znakova, tada će se ispostaviti da će potrošnja memorije biti dvostruko veća nego što je stvarno potrebno. Ili razmislite o nizu za renderiranje: ako koristi samo 20.000 poligona, tada se memorija sa 20.000 poligona zapravo gubi (tj. ne koristi se)!

Drugo, memorija za većinu običnih varijabli (uključujući fiksne nizove) se dodjeljuje iz posebnog memorijskog rezervoara - stog... Količina memorije steka u programu je obično mala - u Visual Studio-u je podrazumevano 1MB. Ako prekoračite ovaj broj, onda stek overflow a operativni sistem će automatski prekinuti vaš program.

U Visual Studiju ovo možete provjeriti pokretanjem sljedećeg programa:

int main () (int array; // dodijeliti 1 milion cjelobrojnih vrijednosti)

Ograničenje memorije od 1MB može biti problematično za mnoge programe, posebno tamo gdje se koristi grafika.

Treće, i najvažnije, može dovesti do umjetnih ograničenja i/ili prelijevanja niza. Šta se događa ako korisnik pokuša pročitati 500 zapisa s diska, a mi smo dodijelili memoriju za maksimalno 400? Ili ćemo korisniku pokazati grešku da je maksimalni broj zapisa 400, ili (u najgorem slučaju) će se niz preliti i onda nešto jako loše.

Na sreću, dinamička alokacija memorije može lako riješiti ove probleme. Dinamička alokacija memorije To je način traženja memorije od operativnog sistema pokretanjem programa kada je to potrebno. Ova memorija se ne dodjeljuje iz ograničene memorije programskog stoga, već iz mnogo veće memorije kojom upravlja operativni sistem - hrpa (hrpe) . Na modernim računarima, hrpa može biti velika kao gigabajti memorije.

Dinamička alokacija varijabli

Da biste dinamički dodijelili memoriju za jednu varijablu, koristite operator novo:

new int; // dinamički dodijelimo cjelobrojnu varijablu i odmah odbacimo rezultat (pošto ga nigdje ne spremamo)

U gornjem primjeru, od operativnog sistema tražimo dodjelu memorije za cjelobrojnu varijablu. Novi operator se vraća sa adresom dodijeljene memorije.

Kreira se pokazivač za pristup dodijeljenoj memoriji:

int * ptr = novi int; // dinamički dodijeli cjelobrojnu varijablu i dodijeli njenu adresu ptr, tako da joj kasnije možemo pristupiti

Tada možemo dereferencirati pokazivač da dobijemo vrijednost:

* ptr = 8; // dodjeljuje vrijednost 8 novoalociranoj memoriji

Ovo je jedan od trenutaka kada su pokazivači korisni. Bez pokazivača sa adresom na novododijeljenu memoriju, ne bismo imali načina da joj pristupimo.

Kako funkcionira dinamička alokacija memorije?

Vaš računar ima memoriju (možda najveći dio) koja je dostupna aplikacijama. Kada pokrenete aplikaciju, vaš operativni sistem učitava tu aplikaciju u neki dio ove memorije. I ova memorija koju koristi vaša aplikacija podijeljena je na nekoliko dijelova, od kojih svaki obavlja određeni zadatak. Jedan dio sadrži vaš kod, drugi se koristi za obavljanje normalnih operacija (praćenje funkcija koje se zovu, kreiranje i uništavanje globalnih i lokalnih varijabli, itd.). O tome ćemo kasnije. Međutim, većina dostupne memorije samo leži tamo i čeka zahtjeve za dodjelu od programa.

Kada dinamički dodjeljujete memoriju, tražite od operativnog sistema da rezerviše dio te memorije za vaš program. Ako OS može ispuniti ovaj zahtjev, adresa ove memorije se vraća nazad vašoj aplikaciji. Od sada, vaša aplikacija može koristiti ovu memoriju čim želi. Kada ste već završili sve što je bilo potrebno sa ovom memorijom, onda se ona mora vratiti nazad u operativni sistem radi raspodjele između ostalih zahtjeva.

Za razliku od statičke ili automatske alokacije memorije, sam program je odgovoran za traženje i vraćanje dinamički dodijeljene memorije.

Inicijalizacija dinamički dodijeljenih varijabli

Kada dinamički dodijelite varijablu, možete je inicijalizirati putem ili uniformne inicijalizacije (u C ++ 11):

int * ptr1 = novi int (7); // koristi direktnu inicijalizaciju int * ptr2 = new int (8); // koristi uniformnu inicijalizaciju

Brisanje varijabli

Kada je sve što je bilo potrebno već urađeno sa dinamički dodijeljenom varijablom, morate eksplicitno reći C++-u da oslobodi ovu memoriju. Za pojedinačne varijable to se radi pomoću operatora izbrisati:

// pretpostavimo da je ptr prethodno dodijeljen pomoću novog operatora delete ptr; // vraćanje memorije na koju ukazuje ptr nazad u operativni sistem ptr = 0; // učini ptr null pokazivačem (koristite nullptr umjesto 0 u C ++ 11)

Šta znači "brisati memoriju"?

Operator delete zapravo ne briše ništa. Jednostavno vraća memoriju koja je prethodno dodijeljena natrag operativnom sistemu. Operativni sistem tada može ponovo dodijeliti ovu memoriju drugoj aplikaciji (ili opet istoj).

Iako se može činiti da brišemo varijabla ali nije! Varijabla pokazivača i dalje ima isti opseg kao i prije i može joj se dodijeliti nova vrijednost kao i svakoj drugoj varijabli.

Imajte na umu da brisanje pokazivača koji ne pokazuje na dinamički dodijeljenu memoriju može dovesti do problema.

Viseći pokazivači

C ++ ne garantuje šta će se desiti sa sadržajem oslobođene memorije ili vrednošću pokazivača koji se briše. U većini slučajeva, memorija vraćena operativnom sistemu će sadržavati iste vrijednosti koje je imala prije oslobođenje, a pokazivač će ostati da pokazuje na memoriju, samo što je već oslobođena (izbrisana).

Poziva se pokazivač koji pokazuje na oslobođenu memoriju viseći pokazivač... Dereferenciranje ili uklanjanje visećeg pokazivača će proizvesti neočekivane rezultate. Razmotrite sljedeći program:

#include int main () (int * ptr = new int; * ptr = 8; // postavi vrijednost u dodijeljenu memorijsku lokaciju delete ptr; // vrati memoriju nazad u operativni sistem.ptr je sada viseći pokazivač std :: cout<< *ptr; // разыменование висячого указателя приведет к неожиданным результатам delete ptr; // попытка освободить память снова приведет к неожиданным результатам также return 0; }

#include

int main ()

int * ptr = novi int; // dinamički dodijeli cjelobrojnu varijablu

* ptr = 8; // stavljamo vrijednost na dodijeljenu memorijsku lokaciju

delete ptr; // vraćanje memorije u operativni sistem. ptr je sada viseći pokazivač

std :: cout<< * ptr ; // dereferenciranje visećeg pokazivača će dovesti do neočekivanih rezultata

delete ptr; // pokušaj ponovnog oslobađanja memorije također će dovesti do neočekivanih rezultata

return 0;

U gornjem programu, vrijednost 8, koja je prethodno bila dodijeljena dodijeljenoj memoriji, nakon oslobađanja može i dalje biti tu, a možda i ne. Također je moguće da je oslobođena memorija već dodijeljena drugoj aplikaciji (ili za vlastitu upotrebu operativnog sistema), a pokušaj da joj se pristupi dovest će do toga da operativni sistem automatski prekine vaš program.

Proces oslobađanja također može stvoriti nekoliko viseći pokazivači. Razmotrite sljedeći primjer:

#include int main () (int * ptr = new int; // dinamički dodijeli cjelobrojnu varijablu int * otherPtr = ptr; // otherPtr sada ukazuje na istu dodijeljenu memoriju kao ptr delete ptr; // vraćanje memorije natrag u operativni sistem . ptr i otherPtr su sada viseći pokazivači ptr = 0; // ptr je sada nullptr // međutim otherPtr je još uvijek viseći pokazivač! vrati 0;)

#include

int main ()

int * ptr = novi int; // dinamički dodijeli cjelobrojnu varijablu

int * otherPtr = ptr; // otherPtr sada ukazuje na istu dodijeljenu memoriju kao ptr

delete ptr; // vraćanje memorije u operativni sistem. ptr i otherPtr su sada viseći pokazivači

ptr = 0; // ptr je sada nullptr

// međutim, otherPtr je još uvijek viseći pokazivač!

return 0;

Prvo, pokušajte izbjeći situacije u kojima više pokazivača ukazuje na isti dio dodijeljene memorije. Ako to nije moguće, onda razjasnite koji od svih pokazivača "posjeduje" memoriju (i odgovoran je za njeno brisanje), a koji joj pokazivači jednostavno pristupaju.

Drugo, kada obrišete pokazivač, i ako ne izađe odmah nakon brisanja, onda ga morate učiniti nultim, tj. postavite vrijednost na 0 (ili u C ++ 11). Pod "izlaskom iz opsega odmah nakon brisanja" mislimo da brišete pokazivač na samom kraju bloka u kojem je deklarisan.

Pravilo: Postavite udaljene pokazivače na 0 (ili nullptr u C ++ 11) ako ne izađu iz opsega odmah nakon brisanja.

Operater nov

Kada zahtijevate memoriju od operativnog sistema, u rijetkim slučajevima, ona možda neće biti dostupna (tj. možda neće biti dostupna).

Prema zadanim postavkama, ako novo ne radi, memorija nije dodijeljena, tada se stvara izuzetak bad_alloc... Ako se ovaj izuzetak ne obradi ispravno (a biće, budući da još nismo razmatrali izuzetke i njihovo rukovanje), tada će se program jednostavno prekinuti (srušiti) sa neobrađenom greškom izuzetka.

U mnogim slučajevima, proces izbacivanja izuzetka od strane novog operatora (kao i pad programa) je nepoželjan, tako da postoji alternativni oblik new, koji vraća null pokazivač ako se memorija ne može dodijeliti. Vi samo trebate dodati std :: nothrow konstantu između nove ključne riječi i tipa odabira podataka:

int * vrijednost = novo (std :: nothrow) int; // pokazivač vrijednosti će postati null ako dinamička alokacija cjelobrojne varijable ne uspije

U gornjem primjeru, ako new ne vrati pokazivač sa dinamički dodijeljenom memorijom, tada će biti vraćen null pokazivač.

Također se ne preporučuje dereferenciranje, jer će to dovesti do neočekivanih rezultata (najvjerovatnije do pada programa). Stoga je najbolja praksa provjeriti sve zahtjeve za dodjelu memorije kako biste bili sigurni da su ti zahtjevi uspješni i da je memorija dodijeljena.

int * vrijednost = novo (std :: nothrow) int; // zahtjev za dodjelu dinamičke memorije za cjelobrojnu vrijednost ako (! vrijednost) // rješava slučaj kada new vrati null (tj. memorija nije dodijeljena) (// obradi ovaj slučaj std :: cout<< "Could not allocate memory"; }

Budući da se nealociranje memorije sa novim operatorom dešava vrlo rijetko, programeri obično zaborave izvršiti ovu provjeru!

Null pokazivači i dinamička alokacija memorije

Null pokazivači (pokazivači sa vrijednošću 0 ili nullptr) su posebno korisni u procesu dodjele hrpe. Njihovo prisustvo, takoreći, govori: "ovaj pokazivač nije dodijeljen nikakva memorija." A ovo se zauzvrat može koristiti za obavljanje uvjetne dodjele memorije:

// ako ptr još nije dodijelio memoriju, dodijeli je ako (! ptr) ptr = new int;

Uklanjanje nulte pokazivača ne utiče ni na šta. Dakle, sljedeće je nepotrebno:

if (ptr) brisanje ptr;

ako (ptr)

delete ptr;

Umjesto toga, možete jednostavno napisati:

delete ptr;

Ako ptr nije null, tada će se dinamički dodijeljena varijabla ukloniti. Ako je vrijednost pokazivača nula, onda se ništa neće dogoditi.

Curenje memorije

Dinamički dodijeljena memorija nije obuhvaćena. To jest, ostaje dodijeljen dok se eksplicitno ne oslobodi ili dok vaš program ne izađe (a operativni sistem sam isprazni sve memorijske bafere). Međutim, pokazivači koji se koriste za pohranjivanje dinamički dodijeljenih memorijskih adresa slijede pravila opsega normalnih varijabli. Ovo neslaganje može uzrokovati zanimljivo ponašanje.

Razmotrite sljedeću funkciju:

void doSomething () (int * ptr = novi int;)

Novi operator vam omogućava da dodijelite memoriju za nizove. Vraća se

pokazivač na prvi element niza u uglastim zagradama. Prilikom dodjele memorije za višedimenzionalne nizove, sve dimenzije osim one krajnje lijeve moraju biti konstante. Prvu dimenziju može specificirati varijabla čija je vrijednost poznata korisniku do trenutka kada se koristi nova, na primjer:

int * p = novi int [k]; // ne može pretvoriti iz "int (*)" u "int *" grešku

int (* p) = novi int [k]; // desno

Prilikom dodjele memorije za objekat, njegova vrijednost će biti nedefinirana. Međutim, objektu se može dodijeliti početna vrijednost.

int * a = novi int (10234);

Ovaj parametar se ne može koristiti za inicijalizaciju nizova. ali

umjesto vrijednosti za inicijalizaciju, možete staviti listu razdvojenu zarezima

vrijednosti proslijeđene konstruktoru prilikom dodjele memorije za niz (masovno

sijanje novih objekata koje definira korisnik). Memorija za niz objekata

može se dodijeliti samo ako je odgovarajuća klasa

postoji zadani konstruktor.

matr () (); // zadani konstruktor

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

(matr mt (3, .5);

matr * p1 = novi matr; // istina r1 - pokazivač na 2 objekta

matr * p2 = novi matr (2,3.4); // netočno, inicijalizacija je nemoguća

matr * p3 = novi matr (2,3.4); // istina p3 - inicijalizirani objekt

(int i; // podaci komponente klase A

A () () // konstruktor klase A

~ A () () // destruktor klase A

(A * a, * b; // opis pokazivača na objekt klase A

float * c, * d; // opis pokazivača na elemente tipa float

a = novo A; // dodjela memorije za jedan objekt klase A

b = novo A; // dodjela memorije za niz objekata klase A

c = novi plovak; // dodjela memorije za jedan float element

d = novi plovak; // dodjela memorije za niz float elemenata

izbrisati a; // slobodna memorija koju zauzima jedan objekt

izbrisati b; // oslobađa memoriju koju zauzima niz objekata

izbrisati c; // slobodna memorija za jedan float element

delete d; ) // oslobađanje memorije niza float elemenata

Organizacija eksternog pristupa lokalnim komponentama klase (prijatelj)

Već smo upoznali osnovno pravilo OOP-a - podaci (interni

varijable) objekta su zaštićene od vanjskih utjecaja i pristup im se može ostvariti

dobiti samo koristeći funkcije (metode) objekta. Ali ima i takvih slučajeva

čajeva, kada trebamo organizirati pristup podacima objekta, a ne korištenje

poznavanje njegovog interfejsa (funkcija). Naravno, možete dodati novu javnu funkciju

klasi za direktan pristup internim varijablama. Međutim, u

u većini slučajeva, sučelje objekta implementira određene operacije, i

nova funkcija bi mogla biti pretjerana. Istovremeno, ponekad postoji

neophodnost organizovanja direktnog pristupa internim (lokalnim) podacima

dva različita objekta iz jedne funkcije. Štaviše, u C ++, jedna funkcija ne može

može biti komponenta dvije različite klase.

Da bi se ovo implementiralo, u C++ je uveden kvalifikator prijatelja. Ako neki

funkcija je definirana kao funkcija prijatelja za neku klasu, tada je:

Nije funkcionalna komponenta ove klase;

Ima pristup svim komponentama ove klase (privatnim, javnim i zaštićenim).

Ispod je primjer gdje eksterna funkcija pristupa

interni podaci klase.

#include

korištenje imenskog prostora std;

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

int max () (return i> j? i: j;) // komponentna funkcija klase kls

prijatelj dvostruka zabava (int, kls &); // prijateljska deklaracija vanjske funkcije fun

dvostruka zabava (int i, kls & x) // vanjska funkcija

(povratak (dvostruki) i / x.i;

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

U C (C ++) postoje tri načina prosljeđivanja podataka funkciji: po vrijednosti

moguće je do nekog postojećeg objekta. Mogu se razlikovati sljedeća vremena

Licencnost veza i pokazivača. Prvo, nemogućnost postojanja nule

veze implicira da ne treba provjeravati njihovu ispravnost. A kada koristite pokazivač, morate ga provjeriti za vrijednost koja nije nula. Drugo, pokazivači mogu ukazivati ​​na različite objekte, a referenca je uvijek na jedan objekt naveden tokom njegove inicijalizacije. Ako želite pružiti funkciju za promjenu vrijednosti

parametri koji su mu prosleđeni, onda u jeziku C i oni moraju biti deklarisani

globalno, ili se rad s njima u funkciji provodi kroz

njeni pokazivači na ove varijable. U C ++, argumenti funkciji se mogu proslijediti

rum se stavlja znakom &.

void zabava1 (int, int);

void fun2 (int &, int &);

(int i = 1, j = 2; // i i j su lokalni parametri

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

cout<< "\n i = "<Zašto C++ plovi kada je Vasa potonuo... Ako je ovo za tebe stvarno problem, mogu preporučiti std :: unique_ptr , a u budućnosti nam standard može dati dinareju.

Dinamički objekti

Dinamički objekti se obično koriste kada je nemoguće vezati životni vijek objekta za određeni opseg. Ako to možete, svakako biste trebali koristiti automatsku memoriju (pogledajte Zašto ne biste trebali pretjerano koristiti dinamičke objekte). Ali ovo je tema posebnog članka.

Kada se kreira dinamički objekat, neko ga mora obrisati, a uslovno se tipovi objekata mogu podeliti u dve grupe: oni koji nisu svjesni procesa njihovog brisanja i oni koji nešto sumnjaju. Recimo da prvi imaju standardni model upravljanja memorijom, dok drugi imaju nestandardni.

Tipovi sa standardnim modelom upravljanja memorijom uključuju sve standardne tipove, uključujući kontejnere. Zaista, kontejner upravlja memorijom koju je sam dodijelio. On nema nikakve veze s tim ko ga je stvorio i kako će biti uklonjen.

Tipovi sa nestandardnim modelom upravljanja memorijom uključuju, na primjer, Qt objekte. Ovdje svaki objekat ima roditelja koji je odgovoran za njegovo brisanje. I objekat zna za to, jer nasljeđuje od klase QObject. Ovo takođe uključuje tipove sa brojem referenci, na primer, dizajnirane da rade sa boost :: intrusive_ptr.

Drugim riječima, tip sa standardnim modelom upravljanja memorijom ne pruža nikakve dodatne mehanizme za upravljanje svojim životnim vijekom. Ovo bi u potpunosti trebala učiniti korisnik. Ali tip sa nestandardnim modelom pruža takve mehanizme. Na primjer, QObject ima metode setParent () i children () i sadrži listu djece, dok se boost :: intrusive_ptr oslanja na funkcije intrusive_ptr_add_ref i intrusive_ptr_release i sadrži broj referenci.

Ako tip objekta ima standardni model upravljanja memorijom, onda ćemo ukratko reći da je to objekt sa standardnim upravljanjem memorijom. Slično, ako tip objekta ima nestandardni model upravljanja memorijom, onda ćemo reći da je to objekt sa nestandardnim upravljanjem memorijom.

Zatim ćemo razmotriti objekte oba modela. Gledajući unaprijed, treba reći da se za objekte sa standardnim upravljanjem memorijom definitivno ne isplati koristiti new i delete u kodu klijenta, a za objekte sa nestandardnim to ovisi o konkretnom modelu.

* Neki izuzeci: pimpl idiom; veoma veliki objekat (kao što je memorijski bafer).

** Izuzetak je std :: locale :: facet (vidi dolje).

Dinamički objekti sa standardnim upravljanjem memorijom

One se najčešće sreću u praksi. I treba ih pokušati koristiti u modernom C++, jer standardni pristupi koji se koriste posebno u pametnim pokazivačima rade s njima.

Zapravo pametni pokazivači, da, to je odgovor. Treba im dati kontrolu nad životnim vijekom dinamičkih objekata. U C++ postoje dva: std :: shared_ptr i std :: unique_ptr. Nećemo naglašavati std :: slab_ptr ovdje, jer to je samo pomoćnik za std :: shared_ptr u određenim slučajevima upotrebe.

Što se tiče std :: auto_ptr, on je službeno zastario iz C ++ od C ++ 17. Počivaj u miru!

Neću se ovdje zadržavati na uređaju i upotrebi pametnih pokazivača, tk. ovo je izvan okvira ovog članka. Odmah da vas podsjetim da dolaze sa prekrasnim funkcijama std :: make_shared i std :: make_unique, i treba ih koristiti za kreiranje pametnih pokazivača.

One. umjesto ovog:
std :: unique_ptr kolačić (novi kolačić (tijesto, šećer, cimet));
trebao bi napisati ovako:
auto cookie = std :: make_unique (tijesto, šećer, cimet);
Prednosti make funkcija u odnosu na eksplicitno kreiranje pametnih pokazivača dobro su dokumentovane od strane Herba Suttera u svom GotW # 89 i Scott Myersa u njegovom Effective Modern C++, Item 21. Neću se ponavljati, samo kratku listu tačaka ovdje:

  • Za obje funkcije make:
    • Sigurnost u smislu izuzetaka.
    • Ne postoji duplikat naziva tipa.
  • Za std :: make_shared:
    • Dobitak performansi kao kontrolni blok se dodeljuje pored samog objekta, što smanjuje broj poziva menadžeru memorije i povećava lokalizaciju podataka. Optimizacija.
Make funkcije također imaju niz ograničenja, koja su detaljno opisana u istim izvorima:
  • Za obje funkcije make:
    • Ne možete prenijeti svoj delter. Ovo je sasvim logično, jer interno, neka funkcije koriste standard new po definiciji.
    • Ne možete koristiti inicijalizator u zagradama, niti sve druge sitnice povezane sa savršenim prosljeđivanjem (pogledajte Efektivni moderni C++, stavka 30).
  • Za std :: make_shared:
    • Potencijalna prekomjerna upotreba memorije za velike objekte s dugovječnim slabim referencama (std :: slab_pokazivač).
    • Problemi sa operatorima new i delete su poništeni na nivou klase.
    • Potencijalno lažno dijeljenje između objekta i kontrolnog bloka (pogledajte pitanje na StackOverflow).
U praksi, ova ograničenja su rijetka i ne umanjuju prednosti. Ispostavilo se da su pametni pokazivači sakrili poziv brisanja od nas, a funkcije make sakrile novi poziv od nas. Kao rezultat toga, dobili smo pouzdaniji kod, u kojem nema ni novog ni brisanja.

Inače, Stefan Lavey (a.k.a. STL) u svojim predavanjima ozbiljno otkriva strukturu make-funkcija. Evo elokventnog slajda iz njegovog govora Don't Help the Compiler:

Dinamički objekti sa nestandardnim upravljanjem memorijom

Pored standardnog pristupa upravljanju memorijom putem pametnih pokazivača, postoje i drugi modeli. Na primjer, brojanje referenci i odnosi između roditelja i djece.

Dinamički objekti s brojanjem referenci


Vrlo uobičajena tehnika koja se koristi u mnogim bibliotekama. Uzmimo biblioteku OpenSceneGraph kao primjer. To je open source cross-platform 3D motor napisan u C++ i OpenGL.

Većina klasa u njemu nasljeđuje od osg :: Referenced klase, koja interno implementira brojanje referenci. Metoda ref () povećava brojač, metoda unref () smanjuje brojač i briše objekt kada brojač padne na nulu.

Komplet također dolazi sa pametnim pokazivačem osg :: ref_ptr koji poziva metodu T :: ref () na pohranjenom objektu u svom konstruktoru i metodu T :: unref () u destruktoru. Isti pristup se koristi u boost :: intrusive_ptr, samo što tamo umjesto metoda ref() i unref() postoje vanjske funkcije.

Hajde da pogledamo isječak koda iz službenog OpenSceneGraph 3.0: vodič za početnike:
osg :: ref_ptr vertices = new osg :: Vec3Array; // ... osg :: ref_ptr normals = new osg :: Vec3Array; // ... osg :: ref_ptr geom = new osg :: Geometrija; geom-> setVertexArray (vertices.get ()); geom->
Vrlo poznate konstrukcije poput osg :: ref_ptr p = novi T. Na potpuno isti način se koriste funkcije std :: make_unique i std :: make_shared za kreiranje klasa std :: unique_ptr i std :: shared_ptr, možemo napisati osg :: make_ref funkciju za kreiranje osg :: ref_ptr klase. To se radi vrlo jednostavno, po analogiji sa funkcijom std :: make_unique:
imenski prostor osg (template osg :: ref_ptr make_ref (Args && ... args) (vrati novi T (std :: naprijed). (args) ...); ))
Prepišimo ovaj isječak koda naoružan našom novom funkcijom:
auto vertices = osg :: make_ref (); // ... auto normals = osg :: make_ref (); // ... auto geom = osg :: make_ref (); geom-> setVertexArray (vertices.get ()); geom-> setNormalArray (normals.get ()); // ...
Promjene su trivijalne i lako se mogu izvršiti automatski. Na ovaj jednostavan način dobijamo sigurnost u smislu izuzetaka, bez dupliranja naziva tipa i savršenu usklađenost sa standardnim stilom.

Poziv delete je već bio skriven u osg :: Referenced :: unref () metodi, a sada smo sakrili novi poziv u osg :: make_ref funkciji. Dakle, nema novog i brisanja.

* Tehnički, u ovom fragmentu nema situacija koje su nesigurne sa stanovišta izuzetaka, ali u složenijim konfiguracijama bi mogle biti.

Dinamički objekti za dijaloge bez načina u MFC-u


Pogledajmo primjer specifičan za MFC biblioteku. To je omotač C ++ klasa preko Windows API-ja. Koristi se za pojednostavljenje razvoja GUI-ja za Windows.

Zanimljiva tehnika koju Microsoft zvanično preporučuje da se koristi za kreiranje dijaloga bez modela. Jer dijalog je bez moderan, nije sasvim jasno ko je odgovoran za njegovo brisanje. Predlaže se da se izbriše u nadjačanoj metodi CDialog :: PostNcDestroy (). Ova metoda se poziva nakon što je obrađena poruka WM_NCDESTROY, posljednja poruka koju je prozor primio u svom životnom ciklusu.

U primjeru ispod, dijalog se kreira klikom na dugme u metodi CMainFrame :: OnBnClickedCreate () i briše se u nadjačanoj metodi CMyDialog :: PostNcDestroy ().
void CMainFrame :: OnBnClickedCreate () (auto * pDialog = novi CMyDialog (ovo); pDialog-> ShowWindow (SW_SHOW);) class CMyDialog: javni CDialog (javno: CMyDialog (CWnd * pParent) (Create: Parent protect (Create:Parent protect (PDI_SHOW);) void PostNcDestroy () override (CDialog :: PostNcDestroy (); izbriši ovo;));
Ovdje nemamo skrivene ni novi poziv ni poziv za brisanje. Postoji mnogo načina da pucate sebi u nogu. Pored uobičajenih problema sa pokazivačima, možete zaboraviti da zaobiđete metodu PostNcDestroy () u svom dijalogu i dobićemo curenje memorije. Kada vidite poziv na new, možda ćete poželjeti da sami pozovete delete u određenom trenutku, dobićemo dvostruko brisanje. Možete slučajno kreirati dijaloški objekat u automatskoj memoriji, opet dobijamo dvostruko brisanje.

Pokušajmo sakriti pozive na new i izbrisati unutar CModelessDialog međuklase i tvornice CreateModelessDialog, koja će biti odgovorna za dijaloge bez mode u našoj aplikaciji:
klasa CModelessDialog: javni CDialog (javno: CModelessDialog (UINT nIDTemplate, CWnd * pParent) (Kreiraj (nIDTemplate, pParent);) zaštićeno: void PostNcDestroy () nadjačavanje (CDialog :: PostNcDestroy (); izbriši ovo;)); // Tvornica za kreiranje predloška modalnih dijaloga Derived * CreateModelessDialog (Args && ... args) (// Umjesto static_assert u tijelu funkcije, možete koristiti std :: enable_if u njenom zaglavlju, što će nam omogućiti korištenje SFINAE. // Ali pošto druga preopterećenja ove funkcije malo je vjerovatno da će se očekivati, čini se razumnim koristiti jednostavnije i opisnije rješenje.static_assert (std :: is_base_of :: vrijednost, "CreateModelessDialog treba pozvati za potomke CModelessDialog"); auto * pDialog = novo Izvedeno (std :: naprijed (args) ...); pDialog-> ShowWindow (SW_SHOW); return pDialog; )
Sama klasa nadjačava metodu PostNcDestroy(), u kojoj smo sakrili delete, a za kreiranje klasa potomaka koristi se fabrika u kojoj smo sakrili novo. Kreiranje i definicija klase naslednika sada izgleda ovako:
void CMainFrame :: OnBnClickedCreate () (CreateModelessDialog (ovo); ) klasa CMyDialog: javni CModelessDialog (javni: CMyDialog (CWnd * pParent): CModelessDialog (IDD_MY_DIALOG, pParent) ());
Naravno, na ovaj način nismo riješili sve probleme. Na primjer, objekt se i dalje može dodijeliti na stog i dobiti dvostruko brisanje. Moguće je zabraniti dodjelu objekta na steku samo modifikacijom same klase objekta, na primjer, dodavanjem privatnog konstruktora. Ali to ni na koji način ne možemo učiniti iz osnovne klase CModelessDialog. Možete, naravno, potpuno sakriti klasu CMyDialog i učiniti fabriku ne šablonom, već klasičnijom, prihvatajući neki identifikator klase. Ali sve je to već izvan okvira članka.

U svakom slučaju, pojednostavili smo kreiranje dijaloga iz klijentskog koda i pisanje nove klase dijaloga. U isto vrijeme, uklonili smo nove i izbrisali pozive iz klijentskog koda.

Dinamički objekti s odnosom roditelj-dijete



Oni su prilično česti, posebno u bibliotekama za razvoj GUI. Kao primjer, razmotrite Qt, dobro poznatu biblioteku za razvoj aplikacija i korisničkog sučelja.

Većina klasa nasljeđuje QObject. Čuva listu djece u sebi i briše ih kada se izbriše. Pohranjuje pokazivač na roditelja (može biti null) i može promijeniti roditelja tijekom života.

Sjajan primjer situacije u kojoj se riješiti novog i izbrisati nije tako lako. Biblioteka je dizajnirana na takav način da se ovi operatori mogu i trebaju koristiti u mnogim slučajevima. Predložio sam omot za kreiranje objekata sa roditeljem koji nije null, ali ideja nije uspjela (pogledajte diskusiju na Qt mailing listi).

Prema tome, nisam svjestan dobrog načina da se riješim new i delete u Qt.

Dynamic std :: locale :: fasetni objekti


C ++ koristi std :: objekte lokalizacije za kontrolu izlaza podataka u tokove. Lokalizacija je skup aspekata koji određuju kako se određeni podaci prikazuju. Faseti imaju svoj broj referenci, a pri kopiranju lokaliteta, aspekti se ne kopiraju, samo se kopira pokazivač i broj referenci se povećava.

Lokalizacija je sama odgovorna za uklanjanje faseta kada broj referenci padne na nulu, ali korisnik mora kreirati aspekte koristeći new operator (pogledajte odjeljak Napomene u opisu konstruktora std :: locale):
std :: default; std :: locale myLocale (zadano, novi std :: codecvt_utf8 );
Ovaj mehanizam je implementiran čak i prije uvođenja standardnih pametnih pokazivača i izbačen je iz općih pravila korištenja klasa u standardnoj biblioteci.

Jednostavan omotač koji generira lokalizaciju može se napraviti za uklanjanje novog iz klijentskog koda. Međutim, ovo je prilično poznat izuzetak od općih pravila i možda nema smisla ograditi povrtnjak za njega.

Zaključak

Dakle, prvo smo pogledali scenarije kao što je kreiranje dinamičkih nizova i dinamičkih objekata sa standardnim upravljanjem memorijom. Umjesto new i delete, koristili smo standardne kontejnere i make funkcije i na kraju dobili jednostavniji i pouzdaniji kod.

Zatim smo pogledali neke primjere nestandardnog upravljanja memorijom i vidjeli kako možemo poboljšati naš kod uklanjanjem novog i brisanjem u odgovarajućim omotima. Pronašli smo i primjer gdje ovaj pristup ne funkcionira.

Međutim, u većini slučajeva ova preporuka daje odlične rezultate i može se koristiti kao osnovni princip. Sada možemo pretpostaviti da ako kod koristi new ili delete, ovo je poseban slučaj koji zahtijeva posebnu pažnju. Ako vidite ove pozive u kodu klijenta, razmislite da li su zaista opravdani.

  • Izbjegavajte korištenje novog i brisanja u svom kodu. Zamislite ih kao ručne operacije upravljanja hrpom niskog nivoa.
  • Koristite standardne kontejnere za dinamičke strukture podataka.
  • Koristite funkcije make za kreiranje dinamičkih objekata kad god je to moguće.
  • Kreirajte omote za objekte sa nestandardnim memorijskim modelom.

Od autora

Osobno sam se susreo sa mnogim slučajevima curenja memorije i rušenja zbog prekomjerne upotrebe new i delete. Da, većina ovog koda je napisana prije mnogo godina, ali tada mladi programeri počinju da rade s njim i misle da bi tako trebalo pisati.

Nadam se da će ovaj članak doći kao praktičan vodič na koji se može uputiti mladi programer kako ne bi zalutao.

Prije nešto više od godinu dana održao sam prezentaciju na ovu temu na konferenciji C++ Russia. Nakon mog govora, publika se podijelila u dvije grupe: one kojima je sve bilo očigledno i one koji su sami sebi napravili divno otkriće. Vjerujem da konferencije često posjećuju već iskusni programeri, pa čak i ako je među njima bilo puno ljudi koji su bili novi u ovim informacijama, nadam se da će ovaj članak biti koristan zajednici.

PS U toku rasprave o članku, moje kolege i ja smo imali čitav spor oko toga kako ispravno: "Myers" ili "Meyers". S jedne strane, "Meyers" zvuči poznatije ruskom uhu, a čini se da smo i sami to uvijek govorili. S druge strane, na wikiju se koristi Myers. Ako pogledate lokalizirane knjige, onda općenito ima bilo koga u tolikoj mjeri: na ove dvije opcije dodaje se i "Meyers". Na konferencijama drugačije ljudi prisutan na različite načine. Na kraju, mi uspeo da sazna da sebe naziva "Myers", za šta su se odlučili.

Linkovi

  1. Herb Sutter, Dobio sam # 89 Rješenje: Pametni pokazivači.
  2. Scott Meyers, Efektivni moderni C ++, tačka 21, str. 139.
  3. Stephan T. Lavavej, Nemojte pomoći kompajleru.
  4. Bjarne Stroustrup, Programski jezik C++, 11.2.1, str. 281.
  5. Pet popularnih mitova o C++., Dio 2
  6. Mihail Matrosov, C ++ bez novog i brisanja.

Tagovi:

Dodaj oznake

Komentari 134

Top srodni članci