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

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

15.8. Operatori novi i brisanje

Prema zadanim postavkama, dodjeljivanje objekta klase iz hrpe i oslobađanje memorije koju je zauzeo izvodi se pomoću globalnih operatora new() i delete() definiranih u standardna knjižnica C++. (Pogledali smo ove operatore u odjeljku 8.4.) Ali klasa također može implementirati vlastitu strategiju upravljanje memorijom pružanjem operatora članova istog imena. Ako su definirani u klasi, pozivaju se umjesto globalnih operatora za dodjelu i oslobađanje memorije za objekte ove klase.

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

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

void *operator new(size_t);

Kada se new() koristi za stvaranje objekta tipa klase, prevodilac provjerava je li takav operator definiran u toj klasi. Ako da, tada se poziva da se alocira memorija za objekt; u protivnom se poziva globalni operator new(). Na primjer, sljedeća uputa

Zaslon *ps = novi zaslon;

stvara objekt Screen u hrpi, a budući da ova klasa ima operator new(), poziva se. Operatorov parametar size_t automatski se inicijalizira na vrijednost jednake veličine Zaslon u bajtovima.

Dodavanje ili uklanjanje new() klasi nema utjecaja na korisnički kod. Poziv na new izgleda isto i za globalnog operatera i za operatora člana. Ako klasa Screen nema vlastiti new(), onda bi poziv ostao ispravan, samo bi se pozvao globalni operator umjesto operatora člana.

Korištenjem operatora rezolucije globalnog opsega, možete pozvati global new() čak i ako klasa Screen definira vlastitu verziju:

Zaslon *ps = ::novi zaslon;

void operator delete(void *);

Kada je operand delete pokazivač na objekt tipa klase, prevodilac provjerava je li operator delete() definiran u toj klasi. Ako da, tada se poziva da oslobodi memoriju, inače - globalna verzija operater. Sljedeće upute

Oslobađa memoriju zauzetu objektom Screen na koji pokazuje ps. Budući da Screen ima operator člana delete(), to je ono što se koristi. Operatorski parametar tipa void* automatski se inicijalizira na vrijednost ps. Dodavanje delete() u klasu ili njeno uklanjanje iz klase nema utjecaja na korisnički kod. Poziv za brisanje izgleda isto i za globalnog operatera i za operatora člana. Ako klasa Zaslon nije imala vlastiti operater delete(), tada bi poziv ostao ispravan, samo bi se pozvao globalni operator umjesto operatora člana.

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

U opći slučaj Upotrijebljeni operator delete() mora odgovarati operatoru new() s kojim je memorija dodijeljena. Na primjer, ako ps pokazuje na memorijsko područje koje je dodijelio globalni new(), tada se globalni delete() treba koristiti za njegovo oslobađanje.

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

// zamjenjuje

// void operator delete(void *);

Ako je drugi parametar prisutan, prevodilac ga automatski inicijalizira s vrijednošću koja je jednaka veličini u bajtovima objekta adresiranog prvim parametrom. (Ova opcija je važna u hijerarhiji klasa, gdje operator delete() može biti naslijeđen od strane izvedene klase. O nasljeđivanju se detaljnije govori u poglavlju 17.)

Pogledajmo detaljnije implementaciju operatora new() i delete() u klasi Screen. Naša strategija raspodjele memorije temeljit će se na povezani popis Zaslonski objekti na čiji početak pokazuje član freeStore. Svaki put kad se pozove operator član new(), vraća se sljedeći objekt na popisu. Kada se pozove delete(), objekt se vraća na popis. Ako je, prilikom stvaranja novog objekta, popis adresiran na freeStore prazan, tada se poziva globalni operator new() da se dobije blok memorije dovoljan za pohranu screenChunk objekata klase Screen.

I screenChunk i freeStore su od interesa samo za Screen, pa ćemo ih učiniti privatnim članovima. Osim toga, za sve stvorene objekte naše klase, vrijednosti ovih članova moraju biti iste, pa se stoga moraju proglasiti statičnima. Da bismo podržali strukturu povezanog popisa objekata zaslona, ​​potreban nam je treći sljedeći član:

void *operator new(size_t);

void operator delete(void *, size_t);

statični zaslon *freeStore;

static const int screenChunk;

Ovdje je jedan od moguće implementacije operator new() za klasu Screen:

#include "Screen.h"

#uključi cstddef

// statički članovi su inicijalizirani

// V izvorne datoteke programa, a ne u datotekama zaglavlja

Zaslon *Zaslon::freeStore = 0;

const int Screen::screenChunk = 24;

void *Screen::operator new(size_t size)

ako (!freeStore) (

// povezana lista je prazna: get novi blok

// globalni operator new se poziva

size_t chunk = screenChunk * veličina;

reinterpret_cast Screen* (novi char[ chunk ]);

// uključi primljeni blok u popis

p != &freeStore[ screenChunk - 1 ];

freeStore = freeStore-sljedeća;

A ovdje je implementacija operatora delete():

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

// umetnite "udaljeni" objekt natrag,

// na besplatni popis

(static_cast Screen* (p))-next = freeStore;

freeStore = static_cast Screen* (p);

Operator new() može se deklarirati u klasi bez odgovarajućeg delete(). U ovom slučaju, objekti se oslobađaju korištenjem globalnog operatora istog imena. Također je dopušteno deklarirati delete() operator bez new(): objekti će biti kreirani korištenjem globalnog operatora istog imena. Međutim, obično se ovi operatori implementiraju istovremeno, kao u gornjem primjeru, budući da programer klase obično treba oba.

Oni su statički članovi klase, čak i ako ih programer eksplicitno ne deklarira kao takve, i podložni su uobičajenim ograničenjima za takve funkcije članova: ne prosljeđuje im se pokazivač this, i stoga mogu izravno pristupiti samo statičkim članovima. (Pogledajte raspravu o statičkim funkcijama članicama u odjeljku 13.5.) Razlog zašto su ovi operatori statični jest to što se pozivaju ili prije konstrukcije objekta klase (new()) ili nakon što je uništen (delete()).

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

Zaslon *ptr = novi zaslon(10, 20);

// Pseudokod u C++

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

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

Drugim riječima, prvi se poziva operator klase new() da alocira memoriju za objekt, a zatim objekt inicijalizira konstruktor. Ako new() ne uspije, pokreće se iznimka tipa bad_alloc i konstruktor se ne poziva.

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

ekvivalent sekvencijalno izvođenje takve upute:

// Pseudokod u C++

Zaslon::~Zaslon(ptr);

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

Dakle, kada se objekt uništi, prvo se poziva destruktor klase, a zatim se poziva operator delete() definiran u klasi da oslobodi memoriju. Ako je ptr 0, tada se ne poziva ni destruktor ni delete().

15.8.1. Operatori novi i brisanje

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

Zaslon *ps = novi zaslon(24, 80);

dok se ispod globalni operator new() poziva za dodjelu memorije iz gomile za niz objekata tipa Screen:

// pozvani Screen::operator new()

Zaslon *psa = novi Zaslon;

Klasa također može deklarirati new() i delete() operatore za rad s nizovima.

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

void *operator new(size_t);

Kada se koristi new za stvaranje niza objekata tipa klase, prevodilac provjerava ima li klasa definiran operator new(). Ako da, tada se poziva za dodjelu memorije za niz; u suprotnom se poziva globalni new(). U slijedeći upute u hrpi se stvara niz od deset objekata zaslona:

Zaslon *ps = novi zaslon;

Ova klasa ima operator new(), zbog čega se poziva za dodjelu memorije. Njegov parametar size_t automatski se inicijalizira na vrijednost jednaku količini memorije, u bajtovima, potrebnoj za držanje deset Screen objekata.

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

Zaslon *ps = ::novi zaslon;

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

void operator delete(void *);

Za brisanje niza objekata klase, delete se mora pozvati ovako:

Kada je operand delete pokazivač na objekt tipa klase, prevodilac provjerava je li operator delete() definiran u toj klasi. Ako da, tada se poziva da oslobodi memoriju; u protivnom se poziva njegova globalna verzija. Parametar tipa void* automatski se inicijalizira na vrijednost adrese početka memorijskog područja u kojem se niz nalazi.

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

Dodavanje ili uklanjanje new() ili delete() operatora u klasu ne utječe na korisnički kod: pozivi i globalnim i članskim operatorima izgledaju isto.

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

Kada se polje 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. Ako upute

ps pokazuje na niz objekata klase, zatim na odsutnost uglate zagrade uzrokovat će pozivanje destruktora samo za prvi element, iako će memorija biti potpuno oslobođena.

Operator član delete() može imati dva parametra umjesto jednog, a 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 s vrijednošću koja je jednaka količini memorije dodijeljenoj nizu u bajtovima.

Iz knjige Vodič za pomoć u C++ Autor Stroustrap Bjarne

R.5.3.4 Operacija brisanja Operacija brisanja uništava objekt kreiran pomoću new.deallocation-expression: ::opt delete cast-expression::opt delete cast-expression Rezultat je tipa void. Operand brisanja mora biti pokazivač koji vraća novo. Učinak korištenja 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 stvara objekt dati tip. Pritom dodjeljuje memoriju potrebnu za pohranjivanje objekta i vraća pokazivač koji pokazuje na njega. Ako se iz nekog razloga memorija ne može dobiti, operater se vraća nulta vrijednost. Operater

Iz knjige Učinkovito korištenje C++. 55 prave načine poboljšati strukturu i kod svojih programa autora Meyersa Scotta

Pravilo 16: Koristite iste oblike new i delete Što nije u redu sa sljedećim isječkom?std::string *stringArray = new std::string;...delete stringArray;Na prvi pogled, sve je u u savršenom redu– upotreba new odgovara upotrebi delete, ali tu nešto nije u redu. Ponašanje programa

Iz Windows knjige Domaćin skripte za Windows 2000/XP Autor Popov Andrej Vladimirovič

Poglavlje 8 Konfiguriranje novog i brisanja Danas, kada računalna 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 se činiti pomalo zastarjelim. Međutim, mnogi programeri koji stvaraju zahtjevne

Iz knjige Standardi programiranja u C++. 101 pravila i preporuke Autor Alexandrescu Andrej

Metoda brisanja Ako je parametar sile netočan ili nije naveden, korištenjem metode brisanja neće biti moguće izbrisati direktorij s atributom samo za čitanje. Postavljanje sile na true omogućit će da se takvi direktoriji odmah izbrišu. Kada koristite metodu Delete, nije bitno hoće li navedeni

Iz knjige Flash Reference Autor Tim autora

Metoda brisanja Ako je parametar sile netočan ili nije naveden, korištenjem metode brisanja neće biti moguće izbrisati datoteku s atributom samo za čitanje. Postavljanje sile na true omogućit će trenutno brisanje takvih datoteka. Napomena Možete koristiti metodu DeleteFile umjesto metode Delete.

Iz knjige Firebird DATABASE DEVELOPER'S GUIDE autora Borri Helen

Relacijski i logički operatori Relacijski operatori koriste se za usporedbu vrijednosti dviju varijabli. Ovi operatori, opisani u tablici. P2.11, može vratiti samo logičke vrijednosti true ili false. Tablica P2.11. Relacijski operatori Operator Uvjet, kada

Iz knjige Linux i UNIX: programiranje ljuski. Vodič za razvojne programere. autora Tainsleya Davida

45. new i delete uvijek trebaju biti dizajnirani zajedno Sažetak Svako preopterećenje void* operatora new(parms) u klasi mora biti popraćeno odgovarajućim preopterećenjem void operatora delete(void*, parms), gdje je parms popis tipova dodatni parametri(od kojih je prvi uvijek std::size_t). Isti

Iz knjige SQL Help autora

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 brisanja uništava objekt ili varijabla, ime

Iz knjige Understanding SQL autora Grubera Martina

Izjava DELETE DELETE zahtjev koristi se za brisanje cijelih redaka tablice. SQL ne dopušta jednu naredbu DELETE za brisanje redaka iz više od jedne tablice. Zahtjev DELETE koji mijenja samo jedan trenutna linija kursor se naziva pozicionirano brisanje.

Iz autorove knjige

15.8. Operatori new i delete Prema zadanim postavkama, dodjeljivanje objekta klase iz gomile i oslobađanje memorije koju on zauzima izvodi se pomoću globalnih operatora new() i delete() definiranih u standardnoj biblioteci C++. (Pogledali smo ove operatore u odjeljku 8.4.) Ali klasa može implementirati

Iz autorove knjige

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 se instrukciji new() klase Screen naziva: Screen::operator new()Screen *ps = new Screen(24, 80); dok se ispod naziva

Kao što znate, u jeziku C funkcije malloc() i free() koriste se za dinamičku dodjelu i oslobađanje memorije. Međutim, C++ sadrži dva operatora koji učinkovitije i jednostavnije izvode dodjelu i poništavanje memorije. Ovi operatori su novi i brisanje. Njihovo opći oblik ima oblik:

varijabla pokazivača = nova vrsta_varijable;

brisanje varijable pokazivača;

Ovdje je pokazivač-varijabla pokazivač tipa varijabla-tip. Novi operator dodjeljuje memoriju za pohranu vrijednosti tipa variable_type i vraća njezinu adresu. Bilo koja vrsta podataka može se postaviti s novim. Operator brisanja oslobađa memoriju na koju ukazuje pointer_variable.

Ako se operacija dodjele memorije ne može izvesti, tada novi operator baca iznimku tipa xalloc. Ako program ne uhvati ovu iznimku, tada će biti prekinuto izvođenje. Iako je ovo zadano ponašanje zadovoljavajuće za kratke programe, za stvarno aplikacijski programi Obično želite uhvatiti iznimku i postupiti s njom na odgovarajući način. Da biste uhvatili ovu iznimku, morate uključiti datoteku zaglavlja osim.h.

Operator za brisanje trebao bi se koristiti samo za pokazivače na memoriju dodijeljenu pomoću operatora new. Korištenje operatora za brisanje s drugim vrstama adresa može uzrokovati ozbiljne probleme.

Postoje brojne 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, sprječava vas da slučajno dodijelite pogrešnu količinu memorije. Drugo, novi operator automatski vraća pokazivač na traženi tip, tako da nema potrebe za korištenjem operatora pretvorbe tipa. Treće, kao što će biti uskoro opisano, moguće je inicijalizirati objekt korištenjem novog operatora. Konačno, moguće je preopteretiti new operator i delete operator globalno ili u odnosu na klasu koja se stvara.

Dolje je jednostavan primjer korištenja operatora new i delete. Obratite pozornost na upotrebu bloka try/catch za praćenje pogrešaka dodjele memorije.

#uključi
#uključi
int main()
{
int *p;
pokušaj (
p = novi int; // dodijeliti memoriju za int
) uhvati (xalloc xa) (
cout<< "Allocation failure.\n";
povratak 1;
}
*p = 20; // dodjeljivanje ovoj memorijskoj lokaciji vrijednosti 20
cout<< *р; // демонстрация работы путем вывода значения
izbrisati p; // oslobađanje memorije
povratak 0;
}

Ovaj program varijabli p dodjeljuje adresu bloka memorije dovoljno velikog da sadrži cijeli broj. Zatim se ovoj memoriji dodjeljuje vrijednost i sadržaj memorije se prikazuje na ekranu. Na kraju se oslobađa dinamički dodijeljena memorija.

Kao što je navedeno, memoriju možete inicijalizirati pomoću operatora new. Da biste to učinili, trebate navesti inicijalizirajuću vrijednost u zagradama nakon naziva tipa. Na primjer, u sljedećem primjeru, memorija na koju ukazuje p je inicijalizirana na 99:

#uključi
#uključi
int main()
{
int *p;
pokušaj (
p = novi int(99); // inicijalizacija 99
) uhvati (xalloc xa) (
cout<< "Allocation failure.\n";
povratak 1;
}
cout<< *p;
izbrisati p;
povratak 0;
}

Možete koristiti new za dodjelu nizova. Opći oblik za jednodimenzionalni niz je:

pokazivač_varijable = nova vrsta_varijable [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 dodijeljeno polje, morate koristiti sljedeći oblik operatora brisanja:

brisanje varijable pokazivača;

Ovdje zagrade obavještavaju operatora brisanja da oslobodi memoriju dodijeljenu nizu.

Sljedeći program dodjeljuje memoriju za niz od 10 float elemenata. Elementima niza se dodjeljuju vrijednosti od 100 do 109, a zatim se sadržaj niza ispisuje na ekran:

#uključi
#uključi
int main()
{
lebdjeti *p;
int i;
pokušaj (
p = novi plovak; // dobivanje desetog elementa niza
) catch(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;
// izlaz sadržaja niza
za (i=0; i<10; i++) cout << p[i] << " ";
izbrisati p; // brisanje cijelog niza
povratak 0;
}

C++ podržava tri glavne vrste pražnjenje (distribucija) memorija, od kojih su nam dva već poznata:

Statička dodjela memorije vrijedi za i varijable. Memorija se dodjeljuje jednom, kada se program pokrene, i zadržava se tijekom cijelog programa.

Automatska dodjela memorije vrijedi za i . Memorija se dodjeljuje pri ulasku u blok u kojem se te varijable nalaze, a oslobađa pri izlasku iz njega.

je tema ovog članka.

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

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

Memorija se automatski dodjeljuje i oslobađa (kada se varijabla stvori ili uništi).

U većini slučajeva to je u redu. Međutim, kada je riječ o radu s vanjskim unosom, ta ograničenja mogu dovesti do problema.

Na primjer, kada se koristi za pohranjivanje imena, ne znamo unaprijed koliko će vremena proći dok ga korisnik ne unese. Ili kada trebamo napisati broj zapisa s diska u varijablu, ali ne znamo unaprijed koliko tih zapisa ima. Ili možemo stvoriti igru ​​s nedosljednim brojem čudovišta (tijekom igre neka čudovišta umiru, druga se rađaju), čime pokušavamo ubiti igrača.

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

char ime; // nadajmo se da će korisnik unijeti ime kraće od 30 znakova! Rekordni rekord; // nadajmo se da broj zapisa neće biti veći od 400! Čudovište čudovište; // Maksimalno 30 čudovišta Renderiranje poligona; // bolje da ovaj 3d prikaz bude manji od 40 000 poligona!

Ovo je loše rješenje 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 imena u prosjeku zauzimaju 15 znakova, tada će potrošnja memorije biti dvostruko veća od one koja je stvarno potrebna. Ili razmislite o nizu za renderiranje: ako koristi samo 20 000 poligona, tada je memorija s 20 000 poligona učinkovito izgubljena (tj. neiskorištena)!

Drugo, memorija za većinu uobičajenih varijabli (uključujući fiksne nizove) dodjeljuje se iz posebnog spremnika memorije - stog. Količina memorije steka u programu obično je mala - u Visual Studiju je prema zadanim postavkama 1 MB. Ako prekoračite ovaj broj, onda stack overflow, a operativni sustav će automatski prekinuti vaš program.

U Visual Studio to možete provjeriti pokretanjem sljedećeg programa:

int main() ( niz int; // dodijeli 1 milijun cjelobrojnih vrijednosti ​​)

Ograničenje memorije od 1 MB može biti problematično za mnoge programe, osobito one koji koriste grafiku.

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

Srećom, ti se problemi lako rješavaju pomoću dinamičke dodjele memorije. Dinamička dodjela memorije je način na koji pokrenuti programi traže memoriju od operativnog sustava kada je to potrebno. Ova memorija nije dodijeljena iz programske ograničene memorije steka, već iz puno veće memorije kojom upravlja operativni sustav - hrpa (gomile) . Na modernim računalima veličina hrpe može biti gigabajti memorije.

Dinamička dodjela varijabli

Za dinamičku dodjelu memorije za jednu varijablu, koristite operator novi:

novi int; // dinamički dodijeliti cjelobrojnu varijablu i odmah odbaciti rezultat (budući da ga nigdje ne spremamo)

U gornjem primjeru od operacijskog sustava tražimo dodjelu memorije za cjelobrojnu varijablu. Novi operator vraća sadrži adresu dodijeljene memorije.

Kreira se pokazivač za pristup dodijeljenoj memoriji:

int *ptr = novi int; // dinamički dodjeljujemo cjelobrojnu varijablu i dodjeljujemo njenu adresu ptr-u kako bismo joj kasnije mogli pristupiti

Zatim možemo dereferencirati pokazivač da dobijemo vrijednost:

*ptr = 8; // dodijeliti vrijednost 8 novododijeljenoj memoriji

Ovo je jedan slučaj u kojem su pokazivači korisni. Bez pokazivača na adresu novododijeljene memorije, ne bismo joj mogli pristupiti.

Kako funkcionira dinamička dodjela memorije?

Vaše računalo ima memoriju (možda većinu) koja je dostupna za korištenje aplikacijama. Kada pokrenete aplikaciju, vaš operativni sustav učitava tu aplikaciju u neki dio ove memorije. A ova memorija koju koristi vaša aplikacija podijeljena je u nekoliko dijelova od kojih svaki obavlja određeni zadatak. Jedan dio sadrži vaš kod, drugi se koristi za izvođenje uobičajenih operacija (praćenje poziva koje su funkcije, stvaranje i uništavanje globalnih i lokalnih varijabli, itd.). O tome ćemo kasnije. Međutim, većina dostupne memorije jednostavno stoji tamo, čekajući zahtjeve programa za dodjelu.

Kada dinamički dodjeljujete memoriju, tražite od operativnog sustava da rezervira dio te memorije za korištenje od strane vašeg programa. Ako OS može ispuniti ovaj zahtjev, tada se adresa ove memorije vraća vašoj aplikaciji. Od ove točke nadalje, vaša aplikacija može koristiti ovu memoriju kad god poželi. Kada ste već napravili sve što je bilo potrebno s tom memorijom, potrebno ju je vratiti natrag u operativni sustav kako bi se rasporedila među ostalim zahtjevima.

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

Inicijaliziranje dinamički dodijeljenih varijabli

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

int *ptr1 = novo int (7); // koristi izravnu inicijalizaciju int *ptr2 = new int ( 8 ); // koristi uniformnu inicijalizaciju

Uklanjanje varijabli

Kada je sve što je bilo potrebno već učinjeno s dinamički dodijeljenom varijablom, trebate izričito reći C++ da oslobodi ovu memoriju. Za pojedinačne varijable to se radi pomoću operatora izbrisati:

// pretpostavimo da je ptr prethodno dodijeljen pomoću operatora new delete ptr; // vrati memoriju na koju pokazuje ptr natrag u operativni sustav ptr = 0; // napravi ptr null pointerom (koristi nullptr umjesto 0 u C++11)

Što znači "brisanje memorije"?

Operator za brisanje zapravo ništa ne briše. Jednostavno vraća memoriju koja je prethodno bila dodijeljena natrag operativnom sustavu. Operativni sustav zatim tu memoriju može dodijeliti drugoj aplikaciji (ili ponovno istoj aplikaciji).

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

Imajte na umu da brisanje pokazivača koji ne pokazuje na dinamički dodijeljenu memoriju može uzrokovati probleme.

Viseći znakovi

C++ ne jamči što će se dogoditi sa sadržajem oslobođene memorije ili s vrijednošću pokazivača koji se briše. U većini slučajeva, memorija vraćena operativnom sustavu sadržavat će iste vrijednosti koje je imala prije. oslobođenje, a pokazivač će i dalje pokazivati ​​na memoriju, samo već oslobođenu (izbrisanu).

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

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

#uključi

int main()

int * ptr = novo int; // dinamički dodijeliti cjelobrojnu varijablu

* ptr = 8; // smjestiti vrijednost u ćeliju dodijeljene memorije

brisati ptr; // vraćanje memorije operativnom sustavu. ptr je sada viseći pokazivač

std::cout<< * ptr ; // dereferenciranje visećeg pokazivača proizvest će neočekivane rezultate

brisati ptr; //ponovni pokušaj oslobađanja memorije također će proizvesti neočekivane rezultate

vratiti 0;

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

Proces oslobađanja memorije također može rezultirati stvaranjem nekoliko viseći znakovi. Razmotrite sljedeći primjer:

#uključi int main() ( int *ptr = new int; // dinamički dodjeljuje cjelobrojnu varijablu int *otherPtr = ptr; // otherPtr sada pokazuje na istu dodijeljenu memoriju kao ptr delete ptr; // vraća memoriju natrag u operativni sustav. ptr i otherPtr sada su viseći pokazivači ptr = 0; // ptr je sada nullptr // međutim otherPtr je još uvijek viseći pokazivač! return 0; )

#uključi

int main()

int * ptr = novo int; // dinamički dodijeliti cjelobrojnu varijablu

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

brisati ptr; // vraćanje memorije operativnom sustavu. ptr i otherPtr sada su viseći pokazivači

ptr = 0; // ptr je sada nullptr

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

vratiti 0;

Prvo pokušajte izbjeći situacije u kojima višestruki pokazivači pokazuju 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 izbrišete pokazivač, i ako ne izađe odmah nakon brisanja, tada ga treba 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 deklariran.

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

Operater novi

Prilikom traženja memorije od operativnog sustava, u rijetkim slučajevima ona možda neće biti dostupna (odnosno, možda neće biti dostupna).

Prema zadanim postavkama, ako new ne radi, memorija se ne dodjeljuje, tada se generira iznimka loša_dodjela. Ako se ovom iznimkom ne postupa ispravno (što će biti, budući da još nismo pokrili iznimke i njihovo rukovanje), tada će se program jednostavno prekinuti (srušiti) s greškom neobrađene iznimke.

U mnogim slučajevima, postupak izbacivanja iznimke s new operatorom (kao i rušenje programa) je nepoželjan, tako da postoji alternativni oblik new koji vraća nulti pokazivač ako se memorija ne može dodijeliti. Samo trebate dodati konstantu std::nothrow između nove ključne riječi i tipa dodjele podataka:

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

U gornjem primjeru, ako new ne vrati pokazivač s dinamički dodijeljenom memorijom, tada će se vratiti nulti pokazivač.

Dereferenciranje se također ne preporučuje jer će to dovesti do neočekivanih rezultata (najvjerojatnije pada programa). Stoga je najbolja praksa provjeriti sve zahtjeve za dodjelom memorije kako bi se osiguralo da su zahtjevi uspjeli i da je memorija dodijeljena.

int *vrijednost = novo (std::nothrow) int; // zahtjev za dodjelu dinamičke memorije za cjelobrojnu vrijednost if (!value) // obrada slučaja kada new vraća null (tj. memorija nije dodijeljena) ( // obrada ovog slučaja std::cout<< "Could not allocate memory"; }

Budući da je nedodjeljivanje memorije od strane novog operatora iznimno rijetko, programeri obično zaborave izvršiti ovu provjeru!

Null pokazivači i dinamička dodjela memorije

Null pokazivači (pokazivači s vrijednošću 0 ili nullptr) posebno su korisni tijekom procesa dinamičke dodjele memorije. Čini se da njihova prisutnost govori: "nijema memorije dodijeljene ovom pokazivaču." A ovo se pak može koristiti za izvođenje uvjetne dodjele memorije:

// ako ptr-u još nije dodijeljena memorija, dodijelite je if (!ptr) ptr = new int;

Uklanjanje nultog pokazivača nema učinka. Dakle, sljedeće nije potrebno:

if (ptr) izbrisati ptr;

ako (ptr)

brisati ptr;

Umjesto toga možete jednostavno napisati:

brisati ptr;

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

Curenje memorije

Dinamički dodijeljena memorija nema opseg. To jest, ostaje dodijeljen sve dok se izričito ne oslobodi ili dok vaš program ne izađe (i operativni sustav sam ne očisti sve memorijske međuspremnike). Međutim, pokazivači koji se koriste za pohranu dinamički dodijeljenih memorijskih adresa slijede pravila opsega normalnih varijabli. Ova razlika može uzrokovati zanimljivo ponašanje.

Razmotrite sljedeću funkciju:

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

Novi operator omogućuje vam dodjeljivanje memorije za nizove. Vraća se

pokazivač na prvi element niza u uglatim zagradama. Prilikom dodjele memorije za višedimenzionalne nizove, sve dimenzije osim one krajnje lijeve moraju biti konstantne. Prva dimenzija može biti određena varijablom čija je vrijednost poznata korisniku u trenutku kada se koristi new, na primjer:

int *p=novo int[k]; // pogreška se ne može pretvoriti iz "int (*)" u "int *"

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

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

int *a = novo int (10234);

Ova se opcija ne može koristiti za inicijalizaciju nizova. Međutim

umjesto vrijednosti inicijalizacije možete staviti popis odvojen zarezom

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

siv novih objekata koje je odredio korisnik). Memorija za niz objekata

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

postoji zadani konstruktor.

matr())(; // zadani konstruktor

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

( matr mt(3,.5);

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

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

matr *p3=novi matr (2,3.4); // true p3 – inicijalizirani objekt

( int i; // komponenta podataka klase A

A()() // konstruktor klase A

~A()() // destruktor klase A

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

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

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

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

c=novi plovak; // dodijeliti memoriju za jedan float element

d=novi plovak; // dodijeliti memoriju za niz float elemenata

izbrisati a; // oslobađanje memorije koju zauzima jedan objekt

izbrisati b; // oslobađanje memorije zauzete nizom objekata

izbrisati c; // oslobađanje memorije jednog float elementa

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

Organiziranje vanjskog pristupa lokalnim komponentama klase (prijatelj)

Već smo se upoznali s osnovnim pravilom OOP-a - podaci (interni

varijable) objekta zaštićeni su od vanjskih utjecaja i pristup njima se može

dobiti samo korištenjem funkcija (metoda) objekta. Ali ima i takvih slučajeva

čajevi, kada trebamo organizirati pristup objektnim podacima bez korištenja

učenje njegovog sučelja (funkcija). Naravno, možete dodati novu javnu funkciju

u klasu kako bi dobili izravan pristup internim varijablama. Međutim, u

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

nova značajka može biti suvišna. U isto vrijeme, ponekad postoji

potreba organiziranja izravnog pristupa internim (lokalnim) podacima

dva različita objekta iz jedne funkcije. U isto vrijeme, u C++ jedna funkcija ne može

može biti komponenta dviju različitih klasa.

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

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

Nije funkcijska komponenta ove klase;

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

Ispod je primjer pristupa vanjske funkcije

interni podaci klase.

#uključi

korištenje imenskog prostora std;

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

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

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

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

(vrati (dvostruko)i/x.i;

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

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

moguće na nekom postojećem objektu. Mogu se razlikovati sljedeća vremena:

prisutnost poveznica i pokazivača. Prvo, nemogućnost postojanja nule

poveznice znači da ne treba provjeravati njihovu ispravnost. A kada koristite pokazivač, trebate ga provjeriti za vrijednost koja nije null. Drugo, pokazivači mogu upućivati ​​na različite objekte, ali referenca uvijek pokazuje na jedan objekt, naveden kada je inicijaliziran. Ako želite dopustiti funkciji da mijenja vrijednosti

parametri koji su mu proslijeđeni, tada se moraju deklarirati ili u jeziku C

globalno, ili se rad s njima u funkcijama provodi putem prijenosa na

Sadrži pokazivače na te varijable. U C++-u se argumenti mogu proslijediti funkciji

rum je označen &.

void fun1(int,int);

void zabava2(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 kad je Vasa potonula. Ako je ovo za vas stvarno problem, mogu preporučiti std::unique_ptr , au budućnosti bi nam standard mogao dati dynarray.

Dinamički objekti

Dinamički objekti obično se koriste kada je nemoguće vezati životni vijek objekta na određeni opseg. Ako se to može učiniti, vjerojatno biste trebali koristiti automatsku memoriju (pogledajte zašto ne biste trebali zlorabiti dinamičke objekte). Ali to je tema zasebnog članka.

Kada se stvori dinamički objekt, netko ga mora obrisati, a tipove objekata možemo podijeliti u dvije skupine: one koji ni na koji način nisu svjesni procesa njihovog brisanja i one koji nešto sumnjaju. Reći ćemo da prvi imaju standardni model upravljanja memorijom, a drugi nestandardni.

Vrste sa standardnim modelom upravljanja memorijom uključuju sve standardne vrste, uključujući spremnike. Zapravo, spremnik upravlja memorijom koju je sam sebi dodijelio. Nije ga briga tko ga je stvorio ili kako će biti uklonjen.

Tipovi s nestandardnim modelom upravljanja memorijom uključuju, na primjer, Qt objekte. Ovdje svaki objekt ima roditelja koji je odgovoran za njegovo brisanje. I objekt zna za ovo, jer nasljeđuje od klase QObject. Ovo također uključuje tipove s brojem referenci, na primjer, one dizajnirane za rad s boost::intrusive_ptr.

Drugim riječima, tip sa standardnim modelom upravljanja memorijom ne nudi nikakve dodatne mehanizme za upravljanje životnim vijekom. Ovo bi u potpunosti trebao riješiti korisnik. Ali tip s nestandardnim modelom pruža takve mehanizme. Na primjer, QObject ima metode setParent() i children() i sadrži popis djece, a tip boost::intrusive_ptr oslanja se na funkcije intrusive_ptr_add_ref i intrusive_ptr_release i sadrži referentni brojač.

Ako vrsta objekta ima standardni model upravljanja memorijom, tada ćemo radi kratkoće reći da je to objekt sa standardnim upravljanjem memorijom. Slično, ako vrsta objekta ima nestandardni model upravljanja memorijom, tada ćemo reći da je to objekt s nestandardnim upravljanjem memorijom.

Zatim, pogledajmo objekte oba modela. Gledajući unaprijed, vrijedi reći da za objekte sa standardnim upravljanjem memorijom definitivno ne biste trebali koristiti new i delete u kodu klijenta, a za objekte s nestandardnim upravljanjem memorijom to ovisi o specifičnom modelu.

* Neke iznimke: idiom pimpl; vrlo velik objekt (na primjer, memorijski međuspremnik).

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

Dinamički objekti sa standardnim upravljanjem memorijom

Takvi se najčešće susreću u praksi. I to su oni koji bi trebali pokušati koristiti u modernom C++, jer standardni pristupi, koji se posebno koriste u pametnim pokazivačima, rade s njima.

Zapravo, pametni pokazivači, da, su odgovor. Treba im dati kontrolu nad vijekom trajanja dinamičkih objekata. Postoje dva od njih u C++: std::shared_ptr i std::unique_ptr. Ovdje nećemo istaknuti std::weak_ptr 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 uklonjen iz C++ počevši od C++17. Počivaj u miru!

Ovdje se neću zadržavati na dizajnu i korištenju pametnih pokazivača jer... ovo je izvan okvira članka. Dopustite mi da vas odmah podsjetim da dolaze u paketu s prekrasnim funkcijama std::make_shared i std::make_unique, a treba ih koristiti za stvaranje pametnih pokazivača.

Oni. umjesto ovoga:
std::unique_ptr kolačić (novi kolačić (tijesto, šećer, cimet));
treba napisati ovako:
automatski kolačić = std::make_unique (tijesto, šećer, cimet);
Prednosti make funkcija u odnosu na eksplicitno stvaranje pametnih pokazivača prekrasno su opisali Herb Sutter u svom GotW #89 i Scott Myers u svom Effective Modern C++, Item 21. Neću se ponavljati, ali ću samo kratko dati popis bodova ovdje:

  • Za obje make funkcije:
    • Sigurnost u smislu izuzetaka.
    • Ne postoji dupli naziv tipa.
  • Za std::make_shared:
    • Dobitak u produktivnosti, jer kontrolni blok se dodjeljuje uz sam objekt, što smanjuje broj poziva upravitelju memorije i povećava lokalitet podataka. Optimizacija.
Make funkcije također imaju brojna ograničenja, detaljno opisana u istim izvorima:
  • Za obje make funkcije:
    • Ne možete proći vlastiti program za brisanje. To je sasvim logično, jer interno, čine funkcije, po definiciji, koriste standard new .
    • Ne možete koristiti inicijalizator u zagradama, niti sve ostale sitnice povezane sa savršenim prosljeđivanjem (pogledajte Učinkoviti moderni C++, točka 30).
  • Za std::make_shared:
    • Potencijalna potrošnja memorije za velike objekte s dugotrajnim slabim referencama (std::weak_pointer).
    • Problemi s operatorima new i delete nadjačanim na razini klase.
    • Potencijalno lažno dijeljenje između objekta i kontrolnog bloka (pogledajte pitanje na StackOverflowu).
U praksi su ta ograničenja rijetka i ne umanjuju prednosti. Ispostavilo se da su pametni pokazivači od nas sakrili poziv za brisanje, a make funkcije sakrile od nas poziv za new. Kao rezultat, dobili smo pouzdaniji kod, koji ne sadrži ni new ni delete .

Usput, o strukturi make funkcija ozbiljno raspravlja u svojim izvješćima Stefan Lavavey (a.k.a. STL). Evo rječitog slajda iz njegovog izvješća Don't Help the Compiler:

Dinamički objekti s nestandardnim upravljanjem memorijom

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

Dinamički objekti s brojanjem referenci


Vrlo česta tehnika koja se koristi u mnogim knjižnicama. Uzmimo biblioteku OpenSceneGraph kao primjer. To je otvoreni 3D mehanizam za više platformi napisan u C++ i OpenGL.

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

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

Pogledajmo dio koda koji je dan u službenom OpenSceneGraph 3.0: Vodiču za početnike:
osg::ref_ptr vrhovi = novi osg::Vec3Array; // ... osg::ref_ptr normale = novi osg::Vec3Array; // ... osg::ref_ptr geom = novi osg::Geometrija; geom->setVertexArray(vertices.get()); geom->
Vrlo poznate konstrukcije poput osg::ref_ptr p = novi T . Na točno isti način na koji se funkcije std::make_unique i std::make_shared koriste za stvaranje klasa std::unique_ptr i std::shared_ptr, možemo napisati funkciju osg::make_ref za stvaranje klase osg::ref_ptr . To se radi vrlo jednostavno, analogno funkciji std::make_unique:
imenski prostor osg (predložak osg::ref_ptr make_ref(Args&&... args) ( vrati novi T(std::forward (args)...); ) )
Napišimo ponovno ovaj dio koda opremljen našom novom funkcijom:
auto vertices = osg::make_ref (); // ... automatske normale = osg::make_ref (); // ... auto geom = osg::make_ref (); geom->setVertexArray(vertices.get()); geom->setNormalArray(normals.get()); // ...
Promjene su trivijalne i lako se mogu napraviti automatski. Na ovaj jednostavan način dobivamo iznimnu sigurnost, nema duplih naziva tipa i izvrsnu usklađenost sa standardnim stilom.

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

* Tehnički, u ovom fragmentu nema situacija koje su nesigurne u smislu iznimaka, ali u složenijim konfiguracijama moglo bi ih biti.

Dinamički objekti za bezmodelne dijaloge u MFC-u


Pogledajmo primjer specifičan za MFC knjižnicu. Ovo je omotač C++ klasa preko Windows API-ja. Koristi se za pojednostavljenje razvoja GUI-ja u sustavu Windows.

Zanimljiva tehnika koju Microsoft službeno preporuča koristiti za stvaranje nemodalnih dijaloga. Jer Dijalog je moderan, nije sasvim jasno tko je odgovoran za njegovo brisanje. Predlaže se da se izbriše u nadjačanoj metodi CDialog::PostNcDestroy(). Ova metoda se poziva nakon obrade WM_NCDESTROY poruke, zadnje poruke koju je prozor primio u svom životnom ciklusu.

U donjem primjeru, dijaloški okvir se stvara klikom na gumb u metodi CMainFrame::OnBnClickedCreate() i briše u nadjačanoj metodi CMyDialog::PostNcDestroy().
void CMainFrame::OnBnClickedCreate() ( auto* pDialog = new CMyDialog(this); pDialog->ShowWindow(SW_SHOW); ) class CMyDialog: public CDialog ( public: CMyDialog(CWnd* pParent) ( Create(IDD_MY_DIALOG, pParent); ) protected: void PostNcDestroy() override ( CDialog::PostNcDestroy(); delete this; ) );
Ovdje nemamo skriven ni novi ni poziv za brisanje. Postoji mnogo načina da si pucate u nogu. Uz uobičajene probleme s pokazivačima, možete zaboraviti nadjačati metodu PostNcDestroy() u svom dijaloškom okviru, što rezultira curenjem memorije. Kada vidite poziv za new , možda ćete poželjeti sami pozvati delete u određenom trenutku, što će rezultirati dvostrukim brisanjem. Možete slučajno stvoriti dijaloški objekt u automatskoj memoriji, opet dobivamo dvostruko brisanje.

Pokušajmo sakriti pozive na new i delete unutar međuklase CModelessDialog i tvornice CreateModelessDialog, koja će biti odgovorna za nemodelne dijaloge u našoj aplikaciji:
class CModelessDialog: public CDialog ( public: CModelessDialog(UINT nIDTemplate, CWnd* pParent) ( Create(nIDTemplate, pParent); ) protected: void PostNcDestroy() override ( CDialog::PostNcDestroy(); izbriši ovo; ) ); // Tvornica za kreiranje predloška modalnih dijaloga Izvedeno* CreateModelessDialog(Args&&... args) ( // Umjesto static_assert u tijelu funkcije, možemo koristiti std::enable_if u njenom zaglavlju, što će nam omogućiti upotrebu SFINAE. // Ali budući da su druga preopterećenja ove funkcije nije vjerojatno za očekivati, čini se razumnim koristiti jednostavnije i vizualnije rješenje: static_assert(std::is_base_of ::value, "CreateModelessDialog treba pozvati za potomke CModelessDialog"); auto* pDialog = novo izvedeno(std::forward (args)...); pDialog->ShowWindow(SW_SHOW); return pDialog; )
Sama klasa nadjačava metodu PostNcDestroy() u kojoj smo sakrili delete , a za stvaranje klasa potomaka koristi se tvornica u kojoj smo sakrili new. Stvaranje i definiranje klase potomaka sada izgleda ovako:
void CMainFrame::OnBnClickedCreate() ( CreateModelessDialog (ovaj); ) class CMyDialog: public CModelessDialog ( public: CMyDialog(CWnd* pParent) : CModelessDialog(IDD_MY_DIALOG, pParent) () );
Naravno, nismo sve probleme riješili na ovaj način. Na primjer, objekt još uvijek može biti dodijeljen na stogu i biti dvostruko izbrisan. Možete spriječiti dodjelu objekta na stogu samo modificiranjem same klase objekta, na primjer dodavanjem privatnog konstruktora. Ali ne postoji način na koji to možemo učiniti iz osnovne klase CModelessDialog. Možete, naravno, potpuno sakriti klasu CMyDialog i učiniti tvornicu ne predloškom, već klasičnijom, prihvaćajući određeni identifikator klase. Ali sve je to izvan okvira članka.

U svakom slučaju, olakšali smo stvaranje dijaloga iz koda klijenta i pisanje nove klase dijaloga. Istovremeno smo uklonili pozive new i delete iz koda klijenta.

Dinamički objekti s odnosom roditelj-dijete



Javljaju se prilično često, osobito u knjižnicama za razvoj GUI-ja. Kao primjer, razmotrite Qt, dobro poznatu biblioteku za razvoj aplikacija i korisničkog sučelja.

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

Izvrstan primjer situacije u kojoj uklanjanje new i delete neće ići tako lako. Knjižnica je dizajnirana na takav način da se ovi operatori mogu i trebaju koristiti u mnogim slučajevima. Predložio sam omotač za stvaranje objekata s roditeljem koji nije null, ali ideja nije uspjela (pogledajte raspravu na Qt listi za slanje e-pošte).

Tako da ne znam dobar način da se riješim novih i izbrišem u Qt-u.

Dinamički objekti std::locale::facet


Za kontrolu izlaza podataka u tokove u C++-u koriste se objekti std::locale. Lokalitet je skup aspekata koji određuju kako se određeni podaci prikazuju. Fasete imaju vlastiti brojač referenci i prilikom kopiranja lokaliteta, fasete se ne kopiraju, kopira se samo pokazivač i brojač referenci se povećava.

Sama lokalizacija odgovorna je za brisanje faseta kada broj referenci dosegne nulu, ali korisnik mora kreirati fasete pomoću novog operatora (pogledajte odjeljak Napomene u opisu konstruktora std::locale):
std::locale default; std::locale myLocale(zadano, novo std::codecvt_utf8 );
Ovaj mehanizam implementiran je i prije uvođenja standardnih pametnih pokazivača i odskače od općih pravila za korištenje klasa u standardnoj biblioteci.

Možete napraviti jednostavan omot koji stvara lokalizaciju za uklanjanje novog iz koda klijenta. Međutim, ovo je prilično dobro poznata iznimka od općih pravila i možda nema smisla praviti vrt za to.

Zaključak

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

Zatim smo pogledali niz primjera nestandardnog upravljanja memorijom i vidjeli kako možemo poboljšati kod uklanjanjem novih 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 izvrsne rezultate i može se koristiti kao zadano načelo. Sada možemo uzeti u obzir da ako kod koristi new ili delete, to je poseban slučaj koji zahtijeva posebnu pozornost. Ako vidite ove pozive u kodu klijenta, razmislite jesu li doista opravdani.

  • Izbjegavajte korištenje new i delete u svom kodu. Zamislite ih kao operacije ručnog upravljanja hrpom niske razine.
  • Koristite standardne spremnike za dinamičke strukture podataka.
  • Koristite make funkcije za stvaranje dinamičkih objekata kad god je to moguće.
  • Stvorite omote za objekte s nestandardnim memorijskim modelom.

Od autora

Osobno sam se susreo s mnogim slučajevima curenja memorije i rušenja zbog pretjerane upotrebe novih i brisanja. Da, većina ovog koda je napisana prije mnogo godina, ali onda mladi programeri počnu raditi s njim i misle da to tako treba biti napisano.

Nadam se da će ovaj članak poslužiti kao praktičan vodič na koji se mladog programera može uputiti kako ne bi zalutao.

Prije nešto više od godinu dana održao sam prezentaciju o ovoj temi na konferenciji C++ Russia. Nakon mog govora publika se podijelila u dvije skupine: one kojima je sve bilo očito i one koji su za sebe došli do predivnog otkrića. Vjerujem da konferencije obično posjećuju iskusniji programeri, pa čak i ako je bilo puno ljudi kojima su te informacije bile nove, nadam se da će ovaj članak biti koristan zajednici.

P.S Dok smo raspravljali o članku, moji kolege i ja vodili smo cijelu raspravu o tome što je točno: “Myers” ili “Meyers”. S jedne strane, “Meyers” ruskim ušima zvuči poznatije, a i mi sami kao da smo oduvijek tako govorili. S druge strane, "Myers" se koristi na wikiju. Ako pogledate lokalizirane knjige, općenito ima puno stvari: ovim dvjema opcijama dodaje se i "Meyers". Na konferencijama drugačiji narod predstaviti to na različite načine. U konačnici mi uspio doznati, da sebe naziva “Myers”, na što su se i odlučili.

Linkovi

  1. Herb Sutter GotW #89 Rješenje: Pametni pokazivači.
  2. Scott Meyers Učinkoviti moderni C++, točka 21, str. 139.
  3. Stephan T. Lavavej, Ne pomažite kompajleru.
  4. Bjarne Stroustrup, Programski jezik C++, 11.2.1, str. 281.
  5. Pet popularnih mitova o C++.,2. dio
  6. Mihail Matrosov, C++ bez novog i brisanja.

Oznake:

Dodaj oznake

Komentari 134

Najbolji članci na temu