V kv∞tnovΘm Φφsle Chipu jsme se p°i povφdßnφ o operßtorech new a delete seznßmili p°edevÜφm s pozadφm jejich fungovßnφ a s n∞kter²mi novinkami, kterΘ v tomto ohledu p°inesl standard ISO/ANSI jazyka C++. Dnes se podφvßme p°edevÜφm na problΘmy, na kterΘ m∙₧e programßtor p°i jejich pou₧itφ narazit.
Jako v₧dy i p°i pou₧φvßnφ operßtor∙ new a delete m∙₧eme ud∞lat chyby a "zad∞lat" si tak na sluÜnou porci problΘm∙. Podφvejme se te∩ na n∞kterΘ obzvlßÜt∞ p∞knΘ. Nßsledujφcφ p°φklady pochßzejφ nejen z program∙ zaΦφnajφcφch cΘΦka°∙, ale bohu₧el i z knih û naÜich i zahraniΦnφch. N∞kterΘ z nich dokonce nesly oznaΦenφ "uΦebnice"...
Kontrola v²sledku
Operßtor new nemusφ usp∞t. Pam∞¥ poΦφtaΦe m∙₧e b²t sice velkß, ale je v₧dy koneΦnß. Proto je t°eba v²sledek operßtoru new kontrolovat. To znamenß podle okolnostφ bu∩ testovat, zda je v²sledek (vrßcenß adresa) r∙zn² od 0, nebo uzav°φt alokaΦnφ v²raz do bloku try.
Nedßvno jsem v jednΘ zahraniΦnφ knize naÜel tvrzenφ, ₧e testovat v²sledek operßtoru new vlastn∞ nenφ nutnΘ û dφky mechanismu virtußlnφ pam∞ti pr² dnes majφ programy k dispozici tolik pam∞¥ovΘho prostoru, ₧e ho prakticky nelze vyΦerpat. Nemohu se ubrßnit dojmu, ₧e se tφm °ada programßtor∙ opravdu °φdφ. UvedenΘ tvrzenφ vypadß v∞rohodn∞, nebo¥ 4 GB jsou opravdu hodn∞, nebo alespo≥ nßm to tak p°ipadß. Nikde vÜak nenφ psßno, ₧e nßÜ program pob∞₧φ v₧dy v prost°edφ s dostateΦn∞ velk²m diskov²m prostorem nebo ₧e zßrove≥ s nφm nepob∞₧φ dalÜφ programy konzumujφcφ obrovskΘ mno₧stvφ pam∞ti. Tak₧e zmφn∞nΘ tvrzenφ p°ece jen p°φliÜ rozumnΘ nenφ.
Ostatn∞ v²roky tohoto typu zastarßvajφ velice rychle. Vzpome≥me jen, jak Bill Gates roku 1981 prohlaÜoval, ₧e 640 KB operaΦnφ pam∞ti by m∞lo b²t dost pro ka₧dΘho...
P°edefinovßnφ globßlnφch operßtor∙
Na samotnΘm p°edefinovßnφ globßlnφch operßtor∙ new a delete ve skuteΦnosti nenφ nic ÜpatnΘho. Musφme ale mφt stßle na pam∞ti, ₧e nßhrada standardnφch funkcφ operator new(size_t) a dalÜφch platφ po celou dobu b∞hu programu, ₧e zaΦφnß jeÜt∞ p°ed spuÜt∞nφm funkce main() a trvß i po jejφm ukonΦenφ. To znamenß, ₧e se uplatnφ i p°i vytvß°enφ globßlnφch instancφ knihovnφch t°φd (nap°. proud∙ cin, cout atd.) a p°i jejich uvol≥ovßnφ.
Je tedy t°eba takovou nßhradu peΦliv∞ uvß₧it, nebo¥ m∙₧e mφt nep°φjemnΘ nßsledky. Nap°φklad pokusy s alokacφ pam∞ti do "arΘny", vyhrazenΘho pole, mohou zp∙sobit zhroucenφ programu, nebo¥ se nemusφ poda°it alokovat dostateΦnΘ mno₧stvφ pam∞ti pro objektovΘ datovΘ proudy.
Existujφ ovÜem i subtiln∞jÜφ chyby, kterΘ m∙₧e p°edefinovßnφ globßlnφch operßtor∙ new a delete zp∙sobit. Podφvejme se na p°φklad. Chceme û nap°φklad kv∙li lad∞nφ û zajistit, aby operßtor new inicializoval p°id∞lenou pam∞¥ urΦitou hodnotou,. aby nap°φklad ulo₧il do vÜech bit∙ hodnotu 1. NapφÜeme tedy nßsledujφcφ funkci:
Bude to v po°ßdku? TΘm∞°. Tato funkce se chovß podobn∞ jako standardnφ operßtor new, a₧ na to, ₧e nespolupracuje s funkcφ set_new_handler(). Pokud by na to n∞kterß Φßst programu spolΘhala, vzniknou chyby, kterΘ se t∞₧ko hledajφ.
Ke svΘrßzn²m problΘm∙m m∙₧e vΘst pou₧itφ n∞kter²ch standardnφch objekt∙ v p°edefinovan²ch funkcφch operator new() nebo operator delete(). Kdybychom nap°φklad vytvo°ili funkci operator delete(), kterß bude krom∞ uvol≥ovßnφ pam∞ti informovat o tom, ₧e je volßna, dejme tomu takto:
void operator delete(void*p)
{
std::cout << "volß se operßtor delete" << std::endl;
free(p);
}
doΦkali bychom se nejspφÜ nep°φjemnΘho p°ekvapenφ. V n∞kter²ch p°ekladaΦφch by program po ukonΦenφ ohlßsil nedefinovanou chybu, v n∞kter²ch by vznikl p°i pou₧itφ operßtoru delete nekoneΦn² cyklus. ProΦ?
Standardnφ proudy si mohou p°i pou₧itφ alokovat pomocnou pam∞¥ a k tomu vyu₧φvajφ operßtory new a delete. To ale znamenß, ₧e po vstupu do funkce operator delete() se pou₧ije operßtor new, v zßp∞tφ pak operßtor delete, kter² zavolß funkci operator delete(), ta pou₧ije op∞t new a delete atd. Program pak skonΦφ vyΦerpßnφm zßsobnφku.
PodobnΘ problΘmy se mohou objevit takΘ p°i pou₧itφ objektov²ch datov²ch proud∙ ve funkci operator new(), kterß nahrazuje standardnφ verzi.
To znamenß, ₧e p°edefinovßnφ standardnφch operßtor∙ se û pokud to jde û vyhneme. Nic nßm toti₧ nebrßnφ funkci operator new()p°et∞₧ovat, tj. definovat vlastnφ verze s dodateΦn²mi parametry. Tyto p°etφ₧enΘ verze pou₧ijeme jen tam, kde je opravdu pot°ebujeme, a pro standardnφ objekty ponechßme standardnφ new.
Dvojφ volßnφ konstruktoru
Nßsledujφcφ chyba m∙₧e vypadat neuv∞°iteln∞, naÜel jsem ji vÜak v jednΘ n∞meckΘ knize, kterß se tvß°ila jako referenΦnφ p°φruΦka jazyka C++. Autor p°edvßd∞l operßtor new definovan² jako metodu takto:
class {
public:
X();
void* operator new(size_t s);
};
void* X::operator new(size_t s)
{
X* x = ::new X;
// N∞jakß ·prava vytvo°enΘ instance
return x;
}
Zde autor v operßtoru new nejprve vytvo°φ pomocφ globßlnφho operßtoru novou instanci, n∞jak ji upravφ a ukazatel na ni vrßtφ. Vypadß to docela dob°e, ale je tu nejmΘn∞ jeden problΘm: Pro tuto instanci se bude dvakrßt volat konstruktor, a to m∙₧e mφt podobn∞ zhoubnΘ nßsledky, jako kdy₧ se konstruktor v∙bec nezavolß. Jestli₧e toti₧ n∞kde v programu napφÜeme nap°.
X* ux = new X;
prob∞hnou obvyklΘ operace û nejprve se zavolß metoda X::operator new(), kterß by m∞la vyhradit pam∞¥. Ta ji opravdu vyhradφ, ovÜem pou₧ije k tomu globßlnφ operßtor new, a ten pro tuto pam∞¥ ihned zavolß konstruktor t°φdy X. Pak X::operator new()ukazatel na vytvo°enou instanci vrßtφ. Po nßvratu pro ni zavolß operßtor new znovu konstruktor. Kdyby konstruktor t°φdy X nap°φklad alokoval dynamickou pam∞¥, otevφral soubory apod., mohou nastat problΘmy.
Pokud by programßtor cht∞l podobn²m zp∙sobem postupovat, m∞l by v metod∞ X::operator new() pou₧φt zßpis operßtorovΘ funkce:
void* X::operator new(size_t s)
{
X* x = ::operator new(s);
// N∞jakß ·prava alokovanΘ pam∞ti
return x;
}
Takto definovan² operßtor new vÜak vlastn∞ nahrazuje konstruktor, a to je zbyteΦnΘ. Pokud nßm tedy nejde o n∞jakou "preventivnφ" inicializaci, kterß mß t°eba usnadnit hledßnφ chyb, je lepÜφ ponechat inicializaci konstruktoru û to je p°ece jeho vlastnφ ·loha.
Zd∞d∞nΘ delete
Deklarujeme-li funkce operator new() a operator delete() jako metody, budou statickΘ, i kdy₧ klφΦovΘ slovo static neuvedeme. To znamenß, ₧e nemohou b²t virtußlnφ û a to m∙₧e obΦas vΘst k problΘm∙m. Podφvejme se na p°φklad:
int a[1000];
class X
{
public:
void *operator new(size_t s){
cout << "new X" << endl;
return a;
}
void operator delete(void* p) {
cout << "delete X" << endl;
}
};
class Y: public X
{
public:
void *operator new(size_t s){
cout << "new Y" << endl;
return a;
}
void operator delete(void* p){
cout << "delete Y" << endl;
}
};
Zde jsme deklarovali t°φdu Y jako potomka t°φdy X. Jak p°edek, tak potomek obsahujφ vlastnφ verze operßtor∙ new a delete. (Jejich implementace zde mß p°edevÜφm za ·kol vypsat upozorn∞nφ û na n∞m bude toti₧ nejsnßze vid∞t, oΦ jde.)
P°i konstrukci novΘ instance v∞tÜinou problΘmy nenastanou. NapφÜeme-li v programu
X* ux = new Y;
zavolß se metoda Y::operator new(), jak oΦekßvßme, a vypφÜe °et∞zec new Y. Jestli₧e ale napφÜeme
delete ux;
zavolß se metoda p°edka, X::operator delete(), kterß vypφÜe delete X û a to je Üpatn∞ (jinak bychom nemuseli definovat v potomkovi novou verzi tΘto funkce).
╪eÜenφ je ovÜem jednoduchΘ: StaΦφ v p°edkovi, ve t°φd∞ X, definovat virtußlnφ destruktor. P°idßme-li tedy do deklarace t°φdy X °ßdek
virtual ~X(){}
bude vÜe v po°ßdku; p°φkazem
delete ux;
zavolßme toti₧ opravdu operßtor delete pro t°φdu Y.
Alokace vφcerozm∞rnΘho pole
O tΘto chyb∞ jsem v Chipu u₧ kdysi psal. V zaΦßteΦnick²ch programech se vÜak objevuje s ·pornou pravidelnostφ, a proto proklßdßm za ·ΦelnΘ se k nφ vrßtit.
Podφvejme se na nßsledujφcφ p°φklad:
int** m = (int**)new int[2][3]; // !!!
ProblΘm je, ₧e pokud n∞co takovΘho napφÜete, v n∞kter²ch prost°edφch û nap°. ve stßle jeÜt∞ ₧ijφcφm operaΦnφm systΘmu DOS û m∙₧e vßÜ program dlouhou dobu b∞₧et, ani₧ by se cokoli ÜpatnΘho d∞lo. Pak se ovÜem zhroutφ, nebo¥ si p°epφÜe Φßst pam∞ti û data, k≤d programu, Φßst operaΦnφho systΘmu, podle toho, co m∙₧e napßchat v∞tÜφ Ükody.
Dokonce i v prost°edφch s ochranou pam∞ti û nap°φklad pod Win32 û m∙₧e tato konstrukce za jist²ch okolnostφ chvφli fungovat, pak ovÜem skonΦφ v²jimkou, poruÜenφm ochrany pam∞ti.
Jak to tedy mß vypadat? Pokud chceme alokovat pole, musφme pou₧φt ukazatel na prvnφ prvek. Dvourozm∞rnΘ pole se sklßdß z jednorozm∞rn²ch polφ, tak₧e pot°ebujeme ukazatel na pole, nikoli ukazatel na ukazatel. P°esn∞ji, pole vytvo°enΘ v²razem new int[2][3] je pole o dvou prvcφch slo₧enΘ z polφ o t°ech prvcφch typu int. Pot°ebujeme ukazatel na jeho prvnφ prvek, tedy ukazatel na pole o t°ech prvcφch typu int:
int (*mat)[3] = new int[2][3]; // OK
S takto alokovan²m polem lze zachßzet jako s "normßlnφm" polem, m∙₧eme nap°. napsat
for(int i = 0; i < 2; i++)
for(int j = 0; j < 3; j++)
mat[i][j] = 10*i+j;
Zmφn∞nß chyba nesporn∞ pochßzφ z oblφbenΘho tvrzenφ mnoha autor∙ uΦebnic jazyk∙ C a C++, ₧e pole a ukazatele jsou v t∞chto jazycφch jedno a totΘ₧. (Nevφm, jak m∙₧e n∞kdo n∞co takovΘho v∙bec napsat, nicmΘn∞ nejde o nijak vzßcnΘ tvrzenφ.) Odtud je ji₧ jen krok k p°edstav∞, ₧e tedy dvourozm∞rnΘ pole je totΘ₧ co ukazatel na ukazatel. Navφc p°ekladaΦ tuto chybu nezachytφ, nebo¥ ukazatel na ukazatel opravdu lze dvakrßt indexovat û v²znam je ovÜem pon∞kud jin² ne₧ dvakrßt indexovan² identifikßtor pole nebo ukazatel na pole.
Je-li M ukazatel na ukazatel na int, oΦekßvß p°ekladaΦ, ₧e jde o ukazatel na prvnφ prvek pole typu int a dovolφ nßm ho indexovat. Podobn∞ je-li m ukazatel na ukazatel na int, oΦekßvß p°ekladaΦ, ₧e jde o prvnφ prvek pole slo₧enΘho z ukazatel∙ na int. Pak m[i] bude znamenat i-t² prvek tohoto pole, tedy ukazatel na int, a tedy ukazatel na prvnφ prvek pole typu int. Nakonec m[i][j] je prvek v poli, na kterΘ tento ukazatel ukazuje. Nßzorn∞ji je to vid∞t na obrßzku 1.
Na druhΘ stran∞ je-li mat ukazatel na jednorozm∞rnΘ pole, oΦekßvß p°ekladaΦ, ₧e jde o prvnφ prvek pole slo₧enΘho z polφ, mat[i] je i-t² prvek tohoto pole a mat[i][j] je j-t² prvek i-tΘho prvku (obr. 2).
Podrobn∞jÜφ rozbor najdete v Φlßnku Kdy₧ se cΘΦka°i s plusy neda°φ (4) v Chipu 11/95 nebo v mΘ knize Pasti a propasti jazyka C++ (Grada 1997, ISBN 80-7169-607-2).
Ve skuteΦnosti zde narß₧φme jeÜt∞ na jeden problΘm: ProΦ je v zßpisu oznaΦenΘm t°emi vyk°iΦnφky p°etypovßnφ? Proto₧e p°ekladaΦ odmφtl tento p°φkaz p°elo₧it s od∙vodn∞nφm, ₧e nedokß₧e konvertovat ukazatel na pole na ukazatel na ukazatel. U₧ to m∞lo programßtora varovat, ₧e je n∞co v nepo°ßdku û operßtor new vracφ v₧dy ukazatel na typ, jak² si autor poruΦil. Zde ovÜem programßtor ignoroval upozorn∞nφ a prosadil svou, ani₧ o v∞ci p°em²Ülel.
Pole objekt∙
Podφvejme se na nßsledujφcφ deklaraci t°φdy Z:
class Z
{
public:
void* operator new(size_t s);
Z();
// ... a dalÜφ slo₧ky
};
Tato t°φda obsahuje operßtor new pro alokaci jednoduch²ch prom∞nn²ch, nikoli pro alokaci pole. To znamenß, ₧e napφÜeme-li
Z* uz = new Z;
Z* upz = new Z[10];
pou₧ije se v prvnφm p°φpad∞ pro alokaci pam∞ti metoda Z::operator new(), avÜak ve druhΘm p°φpad∞ se pou₧ije globßlnφ funkce operator new[](). Pokud chceme °φdit takΘ alokaci polφ t°φdy Z, musφme doplnit odpovφdajφcφ metodu. Obvykle staΦφ, kdy₧ se "polnφ" alokaΦnφ funkce odvolß na "obyΦejnou":
void* Z::operator new[](unsigned s)
{
return operator new(s);
}
Poznamenejme, ₧e takto je zpravidla implementovßna i standardnφ globßlnφ funkce operator new[]().
P°i implementaci "obyΦejnΘ" alokaΦnφ funkce, tj. metody operator new(size_t s), musφme poΦφtat s tφm, ₧e bude volßna i s hodnotou parametru s, kterß nenφ rovna velikosti instance t°φdy Z. V p°φpad∞ alokace pole o N prvcφch m∙₧e mφt parametr s obecn∞ hodnotu N*sizeof(Z)+k, kde k p°edstavuje jakousi re₧ii (t°eba mφsto, do kterΘho si program ulo₧φ poΦet prvk∙ pole pro pozd∞jÜφ orientaci, nap°φklad p°i volßnφ destruktor∙).
New mß mφt svΘ delete
Podφvejme se znovu na t°φdu Z z p°edchozφho odstavce. Jestli₧e alokujeme instanci p°φkazem
Z* uz = new Z;
a pak ji uvolnφme p°φkazem
delete uz;
pou₧ije se k alokaci metoda Z::operator new(), avÜak k uvoln∞nφ globßlnφ funkce operator delete(). To je nejspφÜ chyba: Pokud operßtor new pou₧φvß p°i alokaci n∞jak² zvlßÜtnφ postup, nap°φklad p°id∞luje pam∞¥ ve zvlßÜtnφ hald∞, je nezbytnΘ pam∞¥ stejn²m zp∙sobem i uvol≥ovat, tedy definovat takΘ metodu operator delete(). (TotΘ₧ platφ i pro "polnφ" verze t∞chto operßtor∙.)
JeÜt∞ jednou pole objekt∙
ObΦas takΘ zapomeneme, ₧e p°i uvol≥ovßnφ pole je t°eba pou₧φt operßtor delete[], nikoli jen delete. Pokud pracujeme s neobjektov²mi poli, v∞tÜinou to projde. V p°φpad∞ polφ objektov²ch typ∙ je situace horÜφ, liÜφ se vÜak p°ekladaΦ od p°ekladaΦe. Je-li X t°φda a napφÜeme-li
X* ux = new X[N];
delete ux; // Mß b²t delete[] ux;
obvykle se nezavolß sprßvn² destruktor pro vÜechny instance. M∙₧e vÜak dojφt i k poruÜenφ ochrany pam∞ti.
Zßpis typu
Operßtor new mß ni₧Üφ prioritu ne₧ nap°φklad operßtor volßnφ funkce. Proto m∙₧e p°ekladaΦ odmφtnout n∞kterß komplikovan∞jÜφ oznaΦenφ typu za klφΦov²m slovem new. Jestli₧e chceme alokovat dynamickou prom∞nnou typu "ukazatel na funkci typu void bez parametr∙" a napφÜeme
void (** v)() = new void (*)();
ohlßsφ p°ekladaΦ nejspφÜ °adu podivn²ch chyb.
Tato situace mß n∞kolik °eÜenφ. StaΦφ t°eba oznaΦenφ typu uzßvorkovat:
void f(void);
void (** v)(void) = new (void (*)())(f);
(**v)(); // Volßnφ funkce f()
Zde jsme nov∞ vytvo°enΘ prom∞nnΘ p°i°adili jako poΦßteΦnφ hodnotu adresu funkce f() a vzßp∞tφ jsme tuto funkci zavolali.
Asi nejp°ehledn∞jÜφ je pojmenovat po₧adovan² typ pomocφ deklarace typedef, nap°φklad
typedef void (*funkce)(void);
a pak nov∞ zavedenΘ pou₧φt v alokaΦnφm v²razu:
funkce* u = new funkce(f);
T°φda je obor viditelnosti
Nßsledujφcφ p°φklad skonΦφ chybou p°i p°ekladu, mΘn∞ zkuÜenφ programßto°i pak ovÜem obvi≥ujφ p°ekladaΦ, ₧e obsahuje chybu (to jsem si kdysi myslel i jß).
Class X
{
public:
void* operator new(size_t s, int a);
// ... a dalÜφ slo₧ky
};
X* ux = new X; // Chyba
T°φda X obsahuje operßtor new deklarovan² jako metodu s jednφm dodateΦn²m parametrem, nicmΘn∞ v nßsledujφcφm p°φkazu pou₧φvßme operßtor new bez dodateΦn²ch parametr∙. I kdy₧ se zdß, ₧e by p°ekladaΦ m∞l podle poΦtu a typu parametr∙ zjistit, ₧e chceme pou₧φt globßlnφ operßtor new, nepoznß to a ohlßsφ, ₧e ve t°φd∞ X operßtor new s po₧adovan²mi parametry neexistuje. D∙vod je z°ejm²: t°φda je toti₧ takΘ "oblast viditelnosti" a v nφ je globßlnφ operßtor new zastφn∞n lokßlnφ definicφ. Pokud chceme pou₧φt globßlnφ operßtor new, musφme si o n∞j explicitn∞ °φci pomocφ rozliÜovacφho operßtoru ::, pak bude vÜe v po°ßdku:
X* ux = ::new X; // OK
Konstruktory, destruktory a skalßrnφ typy
V obou dφlech povφdßnφ o operßtorech new a delete jsme stßle hovo°ili o konstruktorech a destruktorech, jako kdybychom nealokovali nic jinΘho ne₧ instance objektov²ch typ∙. Ve skuteΦnosti lze vÜe, co jsme °ekli, p°enΘst i na skalßrnφ datovΘ typy. Standardnφ C++ toti₧ dovoluje i pro tyto typy pou₧φvat zßpisy jako int() nebo a.~int(), kde a je prom∞nnß typu int ("konstruktor" nebo "destruktor" typu int). Tento "konstruktor" inicializuje zpravidla hodnotou 0, "destruktor" skalßrnφho typu ned∞lß nic. Proto m∙₧eme takΘ s klidem hovo°it o inicializaci dynamicky alokovanΘ skalßrnφ prom∞nnΘ pomocφ konstruktoru.
I kdy₧ to vypadß podivn∞, mß uvedenΘ pravidlo dobr² d∙vod: Umo₧≥uje pou₧φvat naprosto stejn²m zp∙sobem objektovΘ typy a skalßrnφ typy v Üablonßch a v n∞kter²ch dalÜφch situacφch.
JeÜt∞ nenφ konec...
Operßtory new a delete nejsou jedinΘ nßstroje pro alokaci pam∞ti v C++. Vedle funkcφ malloc(), calloc()a free(), zd∞d∞n²ch po jazyku C, p°inesl standard jazyka i tzv. alokßtory. To jsou t°φdy, kterΘ zapouzd°ujφ nßstroje pro alokaci pam∞ti a kterΘ se hojn∞ vyu₧φvajφ p°edevÜφm ve standardnφ ÜablonovΘ knihovn∞ C++. O nich si povφme n∞kdy jindy v samostatnΘm Φlßnku.