Sergej Derevyago. C++ 3rd: kommentarii --------------------------------------------------------------- © Copyright Sergej Derevyago, 2000 Versiya ispravlennaya i dopolnennaya, 12 Oct 2004 Origin: http://ders.stml.net/cpp/ ¡ http://ders.stml.net/cpp/ ---------------------------------------------------------------
Vvedenie | |
43 | 1.3.1. |ffektivnost' i struktura |
73 | 2.5.5. Virtual'nye funkcii |
79 | 2.7.2. Obobshchennye algoritmy |
128 | 5.1.1. Nol' |
192 | 7.4. Peregruzhennye imena funkcij |
199 | 7.6. Neukazannoe kolichestvo argumentov |
202 | 7.7. Ukazatel' na funkciyu |
296 | 10.4.6.2. CHleny-konstanty |
297 | 10.4.7. Massivy |
316 | 11.3.1. Operatory-chleny i ne-chleny |
328 | 11.5.1. Poisk druzej |
333 | 11.7.1. YAvnye konstruktory |
337 | 11.9. Vyzov funkcii |
344 | 11.12. Klass String |
351 | 12.2. Proizvodnye klassy |
361 | 12.2.6. Virtual'nye funkcii |
382 | 13.2.3. Parametry shablonov |
399 | 13.6.2. CHleny-shablony |
419 | 14.4.1. Ispol'zovanie konstruktorov i destruktorov |
421 | 14.4.2. auto_ptr |
422 | 14.4.4. Isklyucheniya i operator new |
431 | 14.6.1. Proverka specifikacij isklyuchenij |
431 | 14.6.3. Otobrazhenie isklyuchenij |
460 | 15.3.2. Dostup k bazovym klassam |
461 | 15.3.2.1. Mnozhestvennoe nasledovanie i upravlenie dostupom |
475 | 15.5. Ukazateli na chleny |
477 | 15.6. Svobodnaya pamyat' |
478 | 15.6. Svobodnaya pamyat' |
479 | 15.6.1. Vydelenie pamyati pod massiv |
480 | 15.6.2. "Virtual'nye konstruktory" |
498 | 16.2.3. STL-kontejnery |
505 | 16.3.4. Konstruktory |
508 | 16.3.5. Operacii so stekom |
526 | 17.1.4.1. Sravneniya |
541 | 17.4.1.2. Iteratory i pary |
543 | 17.4.1.3. Indeksaciya |
555 | 17.5.3.3. Drugie operacii |
556 | 17.6. Opredelenie novogo kontejnera |
583 | 18.4.4.1. Svyazyvateli |
584 | 18.4.4.2. Adaptery funkcij-chlenov |
592 | 18.6. Algoritmy, modificiruyushchie posledovatel'nost' |
592 | 18.6.1. Kopirovanie |
622 | 19.2.5. Obratnye iteratory |
634 | 19.4.1. Standartnyj raspredelitel' pamyati |
637 | 19.4.2. Raspredeliteli pamyati, opredelyaemye pol'zovatelem |
641 | 19.4.4. Neinicializirovannaya pamyat' |
647 | 20.2.1. Osobennosti simvolov |
652 | 20.3.4. Konstruktory |
655 | 20.3.6. Prisvaivanie |
676 | 21.2.2. Vyvod vstroennyh tipov |
687 | 21.3.4. Vvod simvolov |
701 | 21.4.6.3. Manipulyatory, opredelyaemye pol'zovatelem |
711 | 21.6.2. Potoki vvoda i bufera |
773 | 23.4.3.1. |tap 1: vyyavlenie klassov |
879 | A.5. Vyrazheniya |
931 | B.13.2. Druz'ya |
935 | B.13.6. template kak kvalifikator |
Optimizaciya | |
Makrosy | |
Ishodnyj kod |
V processe chteniya (i mnogokratnogo) perechityvaniya C++ 3rd u menya voznikalo mnozhestvo voprosov, bol'shaya chast' kotoryh otpadala posle izucheniya sobstvenno standarta i prodolzhitel'nyh razdumij, a za nekotorymi prihodilos' obrashchat'sya neposredstvenno k avtoru. Hochetsya vyrazit' bezuslovnuyu blagodarnost' d-ru Straustrupu za ego otvety na vse moi, zasluzhivayushchie vnimaniya, voprosy i razreshenie privesti dannye otvety zdes'.
Kak chitat' etu knigu. Prezhde vsego, nuzhno prochitat' "YAzyk programmirovaniya C++" i tol'ko na etape vtorogo ili tret'ego perechityvaniya obrashchat'sya k moemu materialu, t.k. zdes' krome ispravleniya oshibok russkogo perevoda izlagayutsya i ves'ma netrivial'nye veshchi, kotorye vryad li budut interesny srednemu programmistu na C++. Moej cel'yu bylo uluchshit' perevod C++ 3rd, naskol'ko eto vozmozhno i prolit' svet na mnozhestvo interesnyh osobennostej C++. Krome togo, original'noe (anglijskoe) izdanie perezhilo dovol'no mnogo tirazhej, i kazhdyj tirazh soderzhal nekotorye ispravleniya, ya postaralsya privesti vse sushchestvennye ispravleniya zdes'.
Esli vy chto-to ne ponyali v russkom perevode, to pervym delom stoit zaglyanut' v original: Bjarne Stroustrup "The C++ Programming language", 3rd edition i/ili v standart C++ (ISO/IEC 14882 Programming languages - C++, First edition, 1998-09-01). K slovu skazat', kak i lyuboj drugoj trud sravnimogo ob®ema i slozhnosti, standart C++ takzhe soderzhit oshibki. Dlya togo, chtoby byt' v kurse poslednih izmenenij standarta, budet poleznym prosmatrivat' C++ Standard Core Issues List i C++ Standard Library Issues List na ego official'noj stranice.
Takzhe ne pomeshaet oznakomit'sya s klassicheskoj STL, vedushchej nachalo neposredstvenno ot Aleksa Stepanova. I, glavnoe, ne zabud'te zaglyanut' k samomu B'ernu Straustrupu.
Kstati, esli vy eshche ne chitali "The C programming Language" by Brian W. Kernighan and Dennis M. Ritchie, 2e izdanie, to ya vam sovetuyu nepremenno eto sdelat' -- Klassika!
S uvazheniem, Sergej Derevyago.
new
, delete
, type_id
, dynamic_cast
, throw
i bloka try
, otdel'nye vyrazheniya i instrukcii C++ ne trebuyut podderzhki vo vremya vypolneniya.
Hotelos' by otmetit', chto est' eshche neskol'ko ochen' vazhnyh mest, gde my imeem neozhidannuyu i poroj ves'ma sushchestvennuyu "podderzhku vremeni vypolneniya". |to konstruktory/destruktory (slozhnyh) ob®ektov, kod sozdaniya/unichtozheniya massivov ob®ektov, prolog/epilog sozdayushchih ob®ekty funkcij i, otchasti, vyzovy virtual'nyh funkcij.
Dlya demonstracii dannoj pechal'noj osobennosti rassmotrim sleduyushchuyu programmu (zamechu, chto v ishodnom kode tekst programmy, kak pravilo, raznesen po neskol'kim fajlam dlya predotvrashcheniya agressivnogo vybrasyvaniya "mertvogo koda" kachestvennymi optimizatorami):
#include <stdio.h> #include <stdlib.h> #include <time.h> struct A { A(); ~A(); }; void ACon(); void ADes(); void f1() { A a; } void f2() { ACon(); ADes(); } long Var, Count; A::A() { Var++; } A::~A() { Var++; } void ACon() { Var++; } void ADes() { Var++; } int main(int argc,char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1,c2; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f1(); c2=clock(); printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f2(); c2=clock(); printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } }V nej funkcii
f1()
i f2()
delayut odno i to zhe, tol'ko pervaya neyavno, s pomoshch'yu konstruktora i destruktora klassa A
, a vtoraya s pomoshch'yu yavnogo vyzova ACon()
i ADes()
.
Dlya raboty programma trebuet odnogo parametra -- skol'ko millionov raz vyzyvat' testovye funkcii. Vyberite znachenie, pozvolyayushchee f1()
rabotat' neskol'ko sekund i posmotrite na rezul'tat dlya f2()
.
Pri ispol'zovanii kachestvennogo optimizatora nikakoj raznicy byt' ne dolzhno; tem ne menee, na nekotoryh platformah ona opredelenno est' i poroj dostigaet 10 raz!
A chto zhe inline
? Davajte vnesem ochevidnye izmeneniya:
struct A { A() { Var++; } ~A() { Var++; } }; void f1() { A a; } void f2() { Var++; Var++; }Teper' raznicy vo vremeni raboty
f1()
i f2()
ne byt' dolzhno. K neschast'yu, na bol'shinstve kompilyatorov ona vse zhe prisutstvuet.
CHto zhe proishodit? Nablyudaemyj nami effekt nazyvaetsya abstraction penalty, t.e. obratnaya storona abstrakcii ili nalagaemoe na nas nekachestvennymi kompilyatorami nakazanie za ispol'zovanie (ob®ektno-orientirovannyh) abstrakcij.
Davajte posmotrim kak abstraction penalty proyavlyaetsya v nashem sluchae.
CHto zhe iz sebya predstavlyaet
void f1() { A a; }ekvivalentnoe
void f1() // psevdokod { A::A(); A::~A(); }I chem ono otlichaetsya ot prostogo vyzova dvuh funkcij:
void f2() { ACon(); ADes(); }V dannom sluchae -- nichem! No, davajte rassmotrim pohozhij primer:
void f1() { A a; f(); } void f2() { ACon(); f(); ADes(); }Kak vy dumaete, ekvivalentny li dannye funkcii? Pravil'nyj otvet -- net, t.k.
f1()
predstavlyaet soboj
void f1() // psevdokod { A::A(); try { f(); } catch (...) { A::~A(); throw; } A::~A(); }T.e. esli konstruktor uspeshno zavershil svoyu rabotu, to yazykom garantiruetsya, chto obyazatel'no budet vyzvan destruktor. T.e. tam, gde sozdayutsya nekotorye ob®ekty, kompilyator special'no vstavlyaet bloki obrabotki isklyuchenij dlya garantii vyzova sootvetstvuyushchih destruktorov. A nakladnye rashody v original'noj
f1()
chashche vsego budut vyzvany prisutstviem nenuzhnyh v dannom sluchae blokov obrabotki isklyuchenij (fakticheski, prisutstviem "utyazhelennyh" prologov/epilogov):
void f1() // psevdokod { A::A(); try { // pusto } catch (...) { A::~A(); throw; } A::~A(); }Delo v tom, chto kompilyator obyazan korrektno obrabatyvat' vse vozmozhnye sluchai, poetomu dlya uproshcheniya kompilyatora ego razrabotchiki chasto ne prinimayut vo vnimanie "chastnye sluchai", v kotoryh mozhno ne generirovat' nenuzhnyj kod. Uvy, podobnogo roda uproshcheniya kompilyatora ochen' ploho skazyvayutsya na proizvoditel'nosti intensivno ispol'zuyushchego sredstva abstrakcii i
inline
funkcii koda. Horoshim primerom podobnogo roda koda yavlyaetsya STL, ch'e ispol'zovanie, pri nalichii plohogo optimizatora, vyzyvaet chrezmernye nakladnye rashody.
Poeksperimentirujte so svoim kompilyatorom dlya opredeleniya ego abstraction penalty -- garantirovanno prigoditsya pri optimizacii "uzkih mest".
vtbl
dlya kazhdogo takogo klassa.
Na samom dele pervoe utverzhdenie neverno, t.e. ob®ekt poluchennyj v rezul'tate mnozhestvennogo nasledovaniya ot polimorfnyh klassov budet soderzhat' neskol'ko "unasledovannyh" ukazatelej na vtbl
.
Rassmotrim sleduyushchij primer. Pust' u nas est' polimorfnyj (t.e. soderzhashchij virtual'nye funkcii) klass B1
:
struct B1 { // ya napisal struct chtoby ne vozit'sya s pravami dostupa int a1; int b1; virtual ~B1() { } };I pust' imeyushchayasya u nas realizaciya razmeshchaet
vptr
(ukazatel' na tablicu virtual'nyh funkcij klassa) pered ob®yavlennymi nami chlenami. Togda dannye ob®ekta klassa B1
budut raspolozheny v pamyati sleduyushchim obrazom:
vptr_1 // ukazatel' na vtbl klassa B1 a1 // ob®yavlennye nami chleny b1Esli teper' ob®yavit' analogichnyj klass
B2
i proizvodnyj klass D
struct D: B1, B2 { virtual ~D() { } };to ego dannye budut raspolozheny sleduyushchim obrazom:
vptr_d1 // ukazatel' na vtbl klassa D, dlya B1 zdes' byl vptr_1 a1 // unasledovannye ot B1 chleny b1 vptr_d2 // ukazatel' na vtbl klassa D, dlya B2 zdes' byl vptr_2 a2 // unasledovannye ot B2 chleny b2Pochemu zdes' dva
vptr
? Potomu, chto byla provedena optimizaciya, inache ih bylo by tri.
YA, konechno, ponyal, chto vy imeli vvidu: "Pochemu ne odin"? Ne odin, potomu chto my imeem vozmozhnost' preobrazovyvat' ukazatel' na proizvodnyj klass v ukazatel' na lyuboj iz bazovyh klassov. Pri etom, poluchennyj ukazatel' dolzhen ukazyvat' na korrektnyj ob®ekt bazovogo klassa. T.e. esli ya napishu:
D d; B2* ptr=&d;to v nashem primere
ptr
ukazhet v tochnosti na vptr_d2
. A sobstvennym vptr
klassa D
budet yavlyat'sya vptr_d1
. Znacheniya etih ukazatelej, voobshche govorya, razlichny. Pochemu? Potomu chto u B1
i B2
v vtbl
po odnomu i tomu zhe indeksu mogut byt' raspolozheny raznye virtual'nye funkcii, a D
dolzhen imet' vozmozhnost' ih pravil'no zamestit'. T.o. vtbl
klassa D
sostoit iz neskol'kih chastej: chast' dlya B1
, chast' dlya B2
i chast' dlya sobstvennyh nuzhd.
Podvodya itog, mozhno skazat', chto esli my ispol'zuem mnozhestvennoe nasledovanie ot bol'shogo chisla polimorfnyh klassov, to nakladnye rashody po pamyati mogut byt' dostatochno sushchestvennymi.
Sudya po vsemu, ot etih rashodov mozhno otkazat'sya, realizovav vyzov virtual'noj funkcii special'nym obrazom, a imenno: kazhdyj raz vychislyaya polozhenie vptr
otnositel'no this
i pereschityvaya indeks vyzyvaemoj virtual'noj funkcii v vtbl
. Odnako eto sprovociruet sushchestvennye rashody vremeni vypolneniya, chto nepriemlemo.
I raz uzh tak mnogo slov bylo skazano pro effektivnost', davajte real'no izmerim otnositel'nuyu stoimost' vyzova virtual'noj funkcii.
#include <stdio.h> #include <stdlib.h> #include <time.h> struct B { void f(); virtual void vf(); }; struct D : B { void vf(); // zameshchaem B::vf }; void f1(B* ptr) { ptr->f(); } void f2(B* ptr) { ptr->vf(); } long Var, Count; void B::f() { Var++; } void B::vf() { } void D::vf() { Var++; } int main(int argc,char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1,c2; D d; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f1(&d); c2=clock(); printf("f1(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000000; j++) f2(&d); c2=clock(); printf("f2(): %ld mlns calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK); } }V zavisimosti ot kompilyatora i platformy, nakladnye rashody na vyzov virtual'noj funkcii sostavili ot 10% do 2.5 raz. T.o. mozhno utverzhdat', chto "virtual'nost'" nebol'shih funkcij mozhet obojtis' sravnitel'no dorogo.
I slovo "nebol'shih" zdes' ne sluchajno, t.k. uzhe dazhe test s funkciej Akkermana (otlichno podhodyashchej dlya vyyavleniya otnositel'noj stoimosti vyzova)
#include <stdio.h> #include <stdlib.h> #include <time.h> struct B { int ackf(int x, int y); virtual int vackf(int x, int y); }; struct D : B { int vackf(int x, int y); // zameshchaem B::vackf }; void f1(B* ptr) { ptr->ackf(3, 5); // 42438 vyzovov! } void f2(B* ptr) { ptr->vackf(3, 5); // 42438 vyzovov! } int B::ackf(int x, int y) { if (x==0) return y+1; else if (y==0) return ackf(x-1, 1); else return ackf(x-1, ackf(x, y-1)); } int B::vackf(int x, int y) { return 0; } int D::vackf(int x, int y) { if (x==0) return y+1; else if (y==0) return vackf(x-1, 1); else return vackf(x-1, vackf(x, y-1)); } long Count; int main(int argc,char** argv) { if (argc>1) Count=atol(argv[1]); clock_t c1,c2; D d; { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f1(&d); c2=clock(); printf("f1(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } { c1=clock(); for (long i=0; i<Count; i++) for (long j=0; j<1000; j++) f2(&d); c2=clock(); printf("f2(): %ld ths calls per %.1f sec\n", Count, double(c2-c1)/CLK_TCK); } }pokazyvaet zametno drugie rezul'taty, sushchestvenno umen'shaya otnositel'nuyu raznost' vremeni vypolneniya.
char vc1[200]; char vc2[500]; void f() { copy(&vc1[0],&vc1[200],&vc2[0]); }
Nu, esli k delu podojti formal'no, to zapisat' my tak ne mozhem. Vot chto govorit ob etom d-r Straustrup:
The issue is whether taking the address of one-past-the-last element of an array is conforming C and C++. I could make the example clearly conforming by a simple rewrite:copy(vc1,vc1+200,vc2);However, I don't want to introduce addition to pointers at this point of the book. It is a surprise to most experienced C and C++ programmers that&vc1[200]
isn't completely equivalent tovc1+200
. In fact, it was a surprise to the C committee also and I expect it to be fixed in the upcoming revision of the standard. (also resolved for C9x - bs 10/13/98).Sut' voprosa v tom, razresheno li v C i C++ vzyatie adresa elementa, sleduyushchego za poslednim elementom massiva. YA mog sdelat' primer ochevidno korrektnym prostoj zamenoj:
copy(vc1,vc1+200,vc2);Odnako, ya ne hotel vvodit' slozhenie s ukazatelem v etoj chasti knigi. Dazhe dlya samyh opytnyh programmistov na C i C++ bol'shim syurprizom yavlyaetsya tot fakt, chto&vc1[200]
ne polnost'yu ekvivalentnovc1+200
. Fakticheski, eto okazalos' neozhidannost'yu i dlya C komiteta, i ya ozhidayu, chto eto nedorazumenie budet ustraneno v sleduyushchih redakciyah standarta.
Tak v chem zhe narushaetsya ekvivalentnost'? Po standartu C++ my imeem sleduyushchie ekvivalentnye preobrazovaniya:
&vc1[200] -> &(*((vc1)+(200))) -> &*(vc1+200)Dejstvitel'no li ravenstvo
&*(vc1+200) == vc1+200
neverno?
It is false in C89 and C++, but not in K&R C or C9x. The C89 standard simply said that&*(vc1+200)
means dereferencevc1+200
(which is an error) and then take the address of the result, and the C++ standard copiled the C89 wording. K&R C and C9x say that&*
cancels out so that&*(vc1+200) == vc2+200
.|to neverno v S89 i C++, no ne v K&R C ili S9h. Standart S89 govorit, chto
&*(vc1+200)
oznachaet razymenovanievc1+200
(chto yavlyaetsya oshibkoj) i zatem vzyatie adresa rezul'tata. I standart C++ prosto vzyal etu formulirovku iz S89. Odnako K&R C i S9h ustanavlivayut, chto&*
vzaimno unichtozhayutsya, t.e.&*(vc1+200) == vc1+200
.
Speshu vas uspokoit', chto na praktike v vyrazhenii &*(vc1+200)
nekorrektnoe razymenovanie *(vc1+200)
prakticheski nikogda ne proizojdet, t.k. rezul'tatom vsego vyrazheniya yavlyaetsya adres i ni odin ser'eznyj kompilyator ne stanet vybirat' znachenie po nekotoromu adresu (operaciya razymenovaniya) chtoby potom poluchit' tot zhe samyj adres s pomoshch'yu operacii &
.
NULL
, vospol'zujtes'
const int NULL=0;
Sut' dannogo soveta v tom, chto soglasno opredeleniyu yazyka ne sushchestvuet konteksta, v kotorom (opredelennoe v zagolovochnom fajle) znachenie NULL
bylo by korrektnym, v to vremya kak prosto 0
-- net.
Ishodya iz togo zhe opredeleniya, peredacha NULL
v funkcii s peremennym kolichestvom parametrov vmesto korrektnogo vyrazheniya vida static_cast<SomeType*>(0)
zapreshchena.
Bezuslovno, vse eto pravil'no, no na praktike NULL
v funkcii s peremennym kolichestvom parametrov vse zhe peredayut. Naprimer, tak:
#include <stdio.h> #include <stdarg.h> #include <stdlib.h> void error(int stat ...) { va_list ap; va_start(ap, stat); while (const char* sarg=va_arg(ap, const char *)) printf("%s", sarg); va_end(ap); exit(stat); } int main() { error(1, "Sluchilos' ", "strashnoe", NULL); // vnimanie, oshibka! // vmesto NULL nuzhno ispol'zovat' // static_cast<const char *>(0) }Imenno dlya podderzhki podobnogo roda praktiki (nekorrektnoj, no shiroko rasprostranennoj) realizaciyam razresheno opredelyat'
NULL
kak 0L
(a ne prosto 0
) na arhitekturah, gde sizeof(void*)==sizeof(long)>sizeof(int)
.
Privedennyj v knige punkt [2] nuzhno zamenit' na:
bool
v int
, char
v int
, short
v int;
§ B.6.1), float
v double
.
struct A { private: void f(int); public: void f(...); }; void g() { A a; a.f(1); // oshibka: vybiraetsya A::f(int), ispol'zovanie // kotoroj v g() zapreshcheno }Otsutstvie dannogo pravila porodilo by tonkie oshibki, kogda vybor podhodyashchej funkcii zavisel by ot mesta vyzova: v funkcii-chlene ili v obychnoj funkcii.
va_start()
, neobhodimo osushchestvit' vyzov va_end()
. Prichina sostoit v tom, chto va_start()
mozhet modificirovat' stek takim obrazom, chto stanet nevozmozhen normal'nyj vyhod iz funkcii.
Vvidu chego voznikayut sovershenno nezametnye podvodnye kamni.
Obshcheizvestno, chto obrabotka isklyucheniya predpolagaet raskrutku steka. Sledovatel'no, esli v moment vozbuzhdeniya isklyucheniya funkciya izmenila stek, to u vas garantirovanno budut nepriyatnosti.
Takim obrazom, do vyzova va_end()
sleduet vozderzhivat'sya ot potencial'no vyzyvayushchih isklyucheniya operacij. Special'no dobavlyu, chto vvod/vyvod C++ mozhet generirovat' isklyucheniya, t.e. "naivnaya" tehnika vyvoda v std::cout
do vyzova va_end()
chrevata nepriyatnostyami.
cmp3
v kachestve argumenta ssort()
narushilo by garantiyu togo, chto ssort()
vyzovetsya s argumentami mytype*
.
Zdes' imeet mesto dosadnaya opechatka, sovershenno iskazhayushchaya smysl predlozheniya. Sleduet chitat' tak: Prichina v tom, chto razreshenie ispol'zovaniya cmp3
v kachestve argumenta ssort()
narushilo by garantiyu togo, chto cmp3()
vyzovetsya s argumentami mytype*
.
Vrode by vse horosho, no pochemu tol'ko integral'nogo tipa? V chem prichina podobnoj diskriminacii? D-r Straustrup pishet po etomu povodu sleduyushchee:
The reason for "discriminating against" floating points in constant expressions is that the precision of floating point traditionally varied radically between processors. In principle, constant expressions should be evaluated on the target processor if you are cross compiling.Prichina podobnoj "diskriminacii" plavayushchej arifmetiki v konstantnyh vyrazheniyah v tom, chto obychno tochnost' podobnyh operacij na raznyh processorah sushchestvenno otlichaetsya. V principe, esli vy osushchestvlyaete kross-kompilyaciyu, to takie konstantnye vyrazheniya dolzhny vychislyat'sya na celevom processore.
T.e. v processe kross-kompilyacii na processore drugoj arhitektury budet krajne problematichno absolyutno tochno vychislit' konstantnoe vyrazhenie, kotoroe moglo by byt' ispol'zovano v kachestve literala (a ne adresa yachejki pamyati) v mashinnyh komandah celevogo processora.
Sudya po vsemu, za predelami zadach kross-kompilyacii (kotorye, k slovu skazat', vstrechayutsya ne tak uzh i chasto) nikakih problem s opredeleniem necelochislennyh konstant ne voznikaet, t.k. nekotorye kompilyatory vpolne dopuskayut kod vida
class Curious { static const float c5=7.0; };v kachestve (neperenosimogo) rasshireniya yazyka.
K schast'yu, eto ogranichenie mozhno sravnitel'no legko obojti. Naprimer, posredstvom vvedeniya lokal'nogo klassa:
#include <stdio.h> struct A { // ishodnyj klass int a; A(int a_) : a(a_) { printf("%d\n",a); } }; void f() { static int vals[]={2, 0, 0, 4}; static int curr=0; struct A_local : public A { // vspomogatel'nyj lokal'nyj A_local() : A(vals[curr++]) { } }; A_local arr[4]; // i dalee ispol'zuem kak A arr[4]; } int main() { f(); }T.k. lokal'nye klassy i ih ispol'zovanie ostalis' za ramkami knigi, dalee privoditsya sootvetstvuyushchij razdel standarta:
9.8 Ob®yavleniya lokal'nyh klassov [class.local]
int x; void f() { static int s; int x; extern int g(); struct local { int g() { return x; } // oshibka, auto x int h() { return s; } // OK int k() { return ::x; } // OK int l() { return g(); } // OK }; // ... } local* p = 0; // oshibka: net local v tekushchem kontekste
Y
mozhet byt' ob®yavlen vnutri lokal'nogo klassa X
i opredelen vnutri opredeleniya klassa X
ili zhe za ego predelami, no v tom zhe kontekste (scope), chto i klass X
. Vlozhennyj klass lokal'nogo klassa sam yavlyaetsya lokal'nym.
complex r1=x+y+z; // r1=operator+(x,operator+(y,z))
Na samom dele dannoe vyrazhenie budet prointerpretirovano tak:
complex r1=x+y+z; // r1=operator+(operator+(x,y),z)Potomu chto operaciya slozheniya levoassociativna:
(x+y)+z
.
// net f() v dannoj oblasti vidimosti class X { friend void f(); // bespolezno friend void h(const X&); // mozhet byt' najdena po argumentam }; void g(const X& x) { f(); // net f() v dannoj oblasti vidimosti h(x); // h() -- drug X }On vzyat iz spiska avtorskih ispravlenij k 8-mu tirazhu i pokazyvaet, chto esli
f
ne bylo v oblasti vidimosti, to ob®yavlenie funkcii-druga f()
vnutri klassa X
ne vnosit imya f
v oblast' vidimosti, tak chto popytka vyzova f()
iz g()
yavlyaetsya oshibkoj.
String s1='a'; // oshibka: net yavnogo preobrazovaniya char v String String s2(10); // pravil'no: stroka dlya hraneniya 10 simvolovmozhet pokazat'sya ochen' tonkoj...
No ona nesomnenno est'. I delo tut vot v chem.
Zapis'
X a=b;vsegda oznachaet sozdanie ob®ekta
a
klassa X
posredstvom kopirovaniya znacheniya nekotorogo drugogo ob®ekta klassa X
. Zdes' mozhet byt' dva varianta:
b
uzhe yavlyaetsya ob®ektom klassa X
. V etom sluchae my poluchim neposredstvennyj vyzov konstruktora kopirovaniya:
X a(b);
b
ob®ektom klassa X
ne yavlyaetsya. V etom sluchae dolzhen byt' sozdan vremennyj ob®ekt klassa X
, ch'e znachenie budet zatem skopirovano:
X a(X(b));Imenno etot vremennyj ob®ekt i ne mozhet byt' sozdan v sluchae explicit-konstruktora, chto privodit k oshibke kompilyacii.
12.8 Kopirovanie ob®ektov klassov [class.copy]
return
vyrazhenie yavlyaetsya imenem lokal'nogo ob®ekta, tip kotorogo (ignoriruya cv-kvalifikatory) sovpadaet s tipom vozvrata, realizacii razresheno ne sozdavat' vremennyj ob®ekt dlya hraneniya vozvrashchaemogo znacheniya, dazhe esli konstruktor kopirovaniya ili destruktor imeyut pobochnye effekty. V etih sluchayah ob®ekt budet unichtozhen pozdnee, chem byli by unichtozheny original'nyj ob®ekt i ego kopiya, esli by dannaya optimizaciya ne ispol'zovalas'.
#include <stdio.h> #include <string.h> struct A { static const int nsize=10; char n[nsize]; A(char cn) { n[0]=cn; n[1]=0; printf("%5s.A::A()\n", n); } A(const A& a) { if (strlen(a.n)<=nsize-2) { n[0]='?'; strcpy(n+1, a.n); } else strcpy(n, "beda"); printf("%5s.A::A(const A& %s)\n", n, a.n); } ~A() { printf("%5s.A::~A()\n", n); } A& operator=(const A& a) { if (strlen(a.n)<=nsize-2) { n[0]='='; strcpy(n+1, a.n); } else strcpy(n, "beda"); printf("%5s.A::operator=(const A& %s)\n", n, a.n); return *this; } }; A f1(A a) { printf("A f1(A %s)\n", a.n); return a; } A f2() { printf("A f2()\n"); A b('b'); return b; } A f3() { printf("A f3()\n"); return A('c'); } int main() { { A a('a'); A b='b'; A c(A('c')); A d=A('d'); } printf("----------\n"); { A a('a'); A b=f1(a); printf("b eto %s\n", b.n); } printf("----------\n"); { A a=f2(); printf("a eto %s\n", a.n); } printf("----------\n"); { A a=f3(); printf("a eto %s\n", a.n); } }Prezhde vsego, v
main()
raznymi sposobami sozdayutsya ob®ekty a
, b
, c
i d
. V normal'noj realizacii vy poluchite sleduyushchij vyvod:
a.A::A() b.A::A() c.A::A() d.A::A() d.A::~A() c.A::~A() b.A::~A() a.A::~A()Tam zhe, gde razrabotchiki kompilyatora shalturili, poyavyatsya nenuzhnye vremennye ob®ekty, naprimer:
... c.A::A() ?c.A::A(const A& c) c.A::~A() d.A::A() d.A::~A() ?c.A::~A() ...T.e.
A c(A('c'))
prevratilos' v A tmp('c'), c(tmp)
. Dalee, vyzov f1()
demonstriruet neyavnye vyzovy konstruktorov kopirovaniya vo vsej krase:
a.A::A() ?a.A::A(const A& a) A f1(A ?a) ??a.A::A(const A& ?a) ?a.A::~A() b eto ??a ??a.A::~A() a.A::~A()Na osnovanii
a
sozdaetsya vremennyj ob®ekt ?a
, i peredaetsya f1()
kachestve argumenta. Dalee, vnutri f1()
na osnovanii ?a
sozdaetsya drugoj vremennyj ob®ekt -- ??a
, on nuzhen dlya vozvrata znacheniya. I vot tut-to i proishodit isklyuchenie novogo vremennogo ob®ekta: b eto ??a
, t.e. lokal'naya peremennaya main()
b
-- eto ta samaya, sozdannaya v f1()
peremennaya ??a
, a ne ee kopiya (special'no dlya somnevayushchihsya: bud' eto ne tak, my by uvideli b eto ???a
).
Polnost'yu soglasen -- vse eto dejstvitel'no ochen' zaputano, no razobrat'sya vse zhe stoit. Dlya bolee yavnoj demonstracii isklyucheniya vremennoj peremennoj ya napisal f2()
i f3()
:
A f2() b.A::A() ?b.A::A(const A& b) b.A::~A() a eto ?b ?b.A::~A() ---------- A f3() c.A::A() a eto c c.A::~A()V
f3()
ono proishodit, a v f2()
-- net! Kak govoritsya, vse delo v volshebnyh puzyr'kah.
Drugogo ob®yasneniya net, t.k. vremennaya peremennaya mogla byla isklyuchena v oboih sluchayah (oh uzh mne eti pisateli kompilyatorov!).
A sejchas rassmotrim bolee interesnyj sluchaj -- peregruzku operatorov. Vnesem v nash klass sootvetstvuyushchie izmeneniya:
#include <stdio.h> #include <string.h> struct A { static const int nsize=10; static int tmpcount; int val; char n[nsize]; A(int val_) : val(val_) // dlya sozdaniya vremennyh ob®ektov { sprintf(n, "_%d", ++tmpcount); printf("%5s.A::A(int %d)\n", n, val); } A(char cn, int val_) : val(val_) { n[0]=cn; n[1]=0; printf("%5s.A::A(char, int %d)\n", n, val); } A(const A& a) : val(a.val) { if (strlen(a.n)<=nsize-2) { n[0]='?'; strcpy(n+1, a.n); } else strcpy(n, "beda"); printf("%5s.A::A(const A& %s)\n", n, a.n); } ~A() { printf("%5s.A::~A()\n", n); } A& operator=(const A& a) { val=a.val; if (strlen(a.n)<=nsize-2) { n[0]='='; strcpy(n+1, a.n)