ovnogo tipa, ne mozhet byt' funkciej-chlenom. Tak, esli my pribavlyaem kompleksnuyu peremennuyu aa k celomu 2, to pri podhodyashchem opisanii funkcii-chlena aa+2 mozhno interpretirovat' kak aa.operator+(2), no 2+aa tak interpretirovat' nel'zya, poskol'ku ne sushchestvuet klassa int, dlya kotorogo + opredelyaetsya kak 2.operator+(aa). Dazhe esli by eto bylo vozmozhno, dlya interpretacii aa+2 i 2+aa prishlos' imet' delo s dvumya raznymi funkciyami-chlenami. |tot primer trivial'no zapisyvaetsya s pomoshch'yu funkcij, ne yavlyayushchihsya chlenami. Kazhdoe vyrazhenie proveryaetsya dlya vyyavleniya neodnoznachnostej. Esli pol'zovatel'skie operacii zadayut vozmozhnuyu interpretaciyu vyrazheniya, ono proveryaetsya v sootvetstvii s pravilami $$R.13.2. 7.3 Pol'zovatel'skie operacii preobrazovaniya tipa Opisannaya vo vvedenii realizaciya kompleksnogo chisla yavlyaetsya slishkom ogranichennoj, chtoby udovletvorit' kogo-nibud', i ee nado rasshirit'. Delaetsya prostym povtoreniem opisanij togo zhe vida, chto uzhe byli primeneny: class complex { double re, im; public: complex(double r, double i) { re=r; im=i; } friend complex operator+(complex, complex); friend complex operator+(complex, double); friend complex operator+(double, complex); friend complex operator-(complex, double); friend complex operator-(complex, double); friend complex operator-(double, complex); complex operator-(); // unarnyj - friend complex operator*(complex, complex); friend complex operator*(complex, double); friend complex operator*(double, complex); // ... }; Imeya takoe opredelenie kompleksnogo chisla, mozhno pisat': void f() { complex a(1,1), b(2,2), c(3,3), d(4,4), e(5,5); a = -b-c; b = c*2.0*c; c = (d+e)*a; } Vse-taki utomitel'no, kak my eto tol'ko chto delali dlya operator*() pisat' dlya kazhdoj kombinacii complex i double svoyu funkciyu. Bolee togo, razumnye sredstva dlya kompleksnoj arifmetiki dolzhny predostavlyat' desyatki takih funkcij (posmotrite, naprimer, kak opisan tip complex v <complex.h>). 7.3.1 Konstruktory Vmesto togo, chtoby opisyvat' neskol'ko funkcij, mozhno opisat' konstruktor, kotoryj iz parametra double sozdaet complex: class complex { // ... complex(double r) { re=r; im=0; } }; |tim opredelyaetsya kak poluchit' complex, esli zadan double. |to tradicionnyj sposob rasshireniya veshchestvennoj pryamoj do kompleksnoj ploskosti. Konstruktor s edinstvennym parametrom ne obyazatel'no vyzyvat' yavno: complex z1 = complex(23); complex z2 = 23; Obe peremennye z1 i z2 budut inicializirovat'sya vyzovom complex(23). Konstruktor yavlyaetsya algoritmom sozdaniya znacheniya zadannogo tipa. Esli trebuetsya znachenie nekotorogo tipa i sushchestvuet stroyashchij ego konstruktor, parametrom kotorogo yavlyaetsya eto znachenie, to togda etot konstruktor i budet ispol'zovat'sya. Tak, klass complex mozhno bylo opisat' sleduyushchim obrazom: class complex { double re, im; public: complex(double r, double i =0) { re=r; im=i; } friend complex operator+(complex, complex); friend complex operator*(complex, complex); complex operator+=(complex); complex operator*=(complex); // ... }; Vse operacii nad kompleksnymi peremennymi i celymi konstantami s uchetom etogo opisaniya stanovyatsya zakonnymi. Celaya konstanta budet interpretirovat'sya kak kompleksnoe chislo s mnimoj chast'yu, ravnoj nulyu. Tak, a=b*2 oznachaet a = operator*(b, complex( double(2), double(0) ) ) Novye versii operacij takih, kak + , imeet smysl opredelyat' tol'ko, esli praktika pokazhet, chto povyshenie effektivnosti za schet otkaza ot preobrazovanij tipa stoit togo. Naprimer, esli vyyasnitsya, chto operaciya umnozheniya kompleksnoj peremennoj na veshchestvennuyu konstantu yavlyaetsya kritichnoj, to k mnozhestvu operacij mozhno dobavit' operator*=(double): class complex { double re, im; public: complex(double r, double i =0) { re=r; im=i; } friend complex operator+(complex, complex); friend complex operator*(complex, complex); complex& operator+=(complex); complex& operator*=(complex); complex& operator*=(double); // ... }; Operacii prisvaivaniya tipa *= i += mogut byt' ochen' poleznymi dlya raboty s pol'zovatel'skimi tipami, poskol'ku obychno zapis' s nimi koroche, chem s ih obychnymi "dvojnikami" * i + , a krome togo oni mogut povysit' skorost' vypolneniya programmy za schet isklyucheniya vremennyh peremennyh: inline complex& complex::operator+=(complex a) { re += a.re; im += a.im; return *this; } Pri ispol'zovanii etoj funkcii ne trebuetsya vremennoj peremennoj dlya hraneniya rezul'tata, i ona dostatochno prosta, chtoby translyator mog "ideal'no" proizvesti podstanovku tela. Takie prostye operacii kak slozhenie kompleksnyh tozhe legko zadat' neposredstvenno: inline complex operator+(complex a, complex b) { return complex(a.re+b.re, a.im+b.im); } Zdes' v operatore return ispol'zuetsya konstruktor, chto daet translyatoru cennuyu podskazku na predmet optimizacii. No dlya bolee slozhnyh tipov i operacij, naprimer takih, kak umnozhenie matric, rezul'tat nel'zya zadat' kak odno vyrazhenie, togda operacii * i + proshche realizovat' s pomoshch'yu *= i += , i oni budut legche poddavat'sya optimizacii: matrix& matrix::operator*=(const matrix& a) { // ... return *this; } matrix operator*(const matrix& a, const matrix& b) { matrix prod = a; prod *= b; return prod; } Otmetim, chto v opredelennoj podobnym obrazom operacii ne nuzhnyh nikakih osobyh prav dostupa k klassu, k kotoromu ona primenyaetsya, t.e. eta operaciya ne dolzhna byt' drugom ili chlenom etogo klassa. Pol'zovatel'skoe preobrazovanie tipa primenyaetsya tol'ko v tom sluchae, esli ono edinstvennoe($$7.3.3). Postroennyj v rezul'tate yavnogo ili neyavnogo vyzova konstruktora, ob容kt yavlyaetsya avtomaticheskim, i unichtozhaetsya pri pervoj vozmozhnosti,- kak pravilo srazu posle vypolneniya operatora, v kotorom on byl sozdan. 7.3.2 Operacii preobrazovaniya Konstruktor udobno ispol'zovat' dlya preobrazovaniya tipa, no vozmozhny nezhelatel'nye posledstviya: [1] Neyavnye preobrazovaniya ot pol'zovatel'skogo tipa k osnovnomu nevozmozhny (poskol'ku osnovnye tipy ne yavlyayutsya klassami). [2] Nel'zya zadat' preobrazovanie iz novogo tipa v staryj, ne izmenyaya opisaniya starogo tipa. [3] Nel'zya opredelit' konstruktor s odnim parametrom, ne opredeliv tem samym i preobrazovanie tipa. Poslednee ne yavlyaetsya bol'shoj problemoj, a pervye dve mozhno preodolet', esli opredelit' operatornuyu funkciyu preobrazovaniya dlya ishodnogo tipa. Funkciya-chlen X::operator T(), gde T - imya tipa, opredelyaet preobrazovanie tipa X v T. Naprimer, mozhno opredelit' tip tiny (kroshechnyj), znacheniya kotorogo nahodyatsya v diapazone 0..63, i etot tip mozhet v arifmeticheskih operaciyah prakticheski svobodno smeshivat'sya s celymi: class tiny { char v; void assign(int i) { if (i>63) { error("vyhod iz diapazona"); v=i&~63; } v=i; } public: tiny(int i) { assign(i) } tiny(const tiny& t) { v = t.v; } tiny& operator=(const tiny& t) { v = t.v; return *this; } tiny& operator=(int i) { assign(i); return *this; } operator int() { return v; } }; Popadanie v diapazon proveryaetsya kak pri inicializacii ob容kta tiny, tak i v prisvaivanii emu int. Odin ob容kt tiny mozhno prisvoit' drugomu bez kontrolya diapazona. Dlya vypolneniya obychnyh operacij s celymi dlya peremennyh tipa tiny opredelyaetsya funkciya tiny::operator int(), proizvodyashchaya neyavnoe preobrazovanie tipa iz tiny v int. Tam, gde trebuetsya int, a zadana peremennaya tipa tiny, ispol'zuetsya preobrazovannoe k int znachenie: void main() { tiny c1 = 2; tiny c2 = 62; tiny c3 = c2 -c1; // c3 = 60 tiny c4 = c3; // kontrolya diapazona net (on ne nuzhen) int i = c1 + c2; // i = 64 c1 = c2 + 2 * c1; // vyhod iz diapazona: c1 = 0 (a ne 66) c2 = c1 - i; // vyhod iz diapazona: c2 = 0 c3 = c2; // kontrolya diapazona net (on ne nuzhen) } Bolee poleznym mozhet okazat'sya vektor iz ob容ktov tiny, poskol'ku on pozvolyaet ekonomit' pamyat'. CHtoby takoj tip bylo udobno ispol'zovat', mozhno vospol'zovat'sya operaciej indeksacii []. Pol'zovatel'skie operacii preobrazovaniya tipa mogut prigodit'sya dlya raboty s tipami, realizuyushchimi nestandartnye predstavleniya chisel (arifmetika s osnovaniem 100, arifmetika chisel s fiksirovannoj tochkoj, predstavlenie v dvoichno-desyatichnoj zapisi i t.d.). Pri etom obychno prihoditsya pereopredelyat' takie operacii, kak + i *. Osobenno poleznymi funkcii preobrazovaniya tipa okazyvayutsya dlya raboty s takimi strukturami dannyh, dlya kotoryh chtenie (realizovannoe kak operaciya preobrazovaniya) yavlyaetsya trivial'nym, a prisvaivanie i inicializaciya sushchestvenno bolee slozhnye operacii. Funkcii preobrazovaniya nuzhny dlya tipov istream i ostream, chtoby stali vozmozhnymi, naprimer, takie operatory: while (cin>>x) cout<<x; Operaciya vvoda cin>>x vozvrashchaet znachenie istream&. Ono neyavno preobrazuetsya v znachenie, pokazyvayushchee sostoyanie potoka cin, kotoroe zatem proveryaetsya v operatore while (sm. $$10.3.2). No vse-taki opredelyat' neyavnoe preobrazovanie tipa, pri kotorom mozhno poteryat' preobrazuemoe znachenie, kak pravilo, plohoe reshenie. Voobshche, luchshe ekonomno pol'zovat'sya operaciyami preobrazovaniya. Izbytok takih operacij mozhet vyzyvat' bol'shoe chislo neodnoznachnostej. Translyator obnaruzhivaet eti neodnoznachnosti, no razreshit' ih mozhet byt' sovsem neprosto. Vozmozhno vnachale luchshe dlya preobrazovanij ispol'zovat' poimenovannye funkcii, naprimer, X::intof(), i tol'ko posle togo, kak takuyu funkciyu kak sleduyut oprobuyut, i yavnoe preobrazovanie tipa budet sochteno neelegantnym resheniem, mozhno zamenit' operatornoj funkciej preobrazovaniya X::operator int(). 7.3.3 Neodnoznachnosti Prisvaivanie ili inicializaciya ob容kta klassa X yavlyaetsya zakonnym, esli prisvaivaemoe znachenie imeet tip X, ili esli sushchestvuet edinstvennoe preobrazovanie ego v znachenie tipa X. V nekotoryh sluchayah znachenie nuzhnogo tipa stroitsya s pomoshch'yu povtornyh primenenij konstruktorov ili operacij preobrazovaniya. |to dolzhno zadavat'sya yavnym obrazom, dopustimo neyavnoe pol'zovatel'skoe preobrazovanie tol'ko odnogo urovnya vlozhennosti. V nekotoryh sluchayah sushchestvuet neskol'ko sposobov postroeniya znacheniya nuzhnogo tipa, no eto yavlyaetsya nezakonnym. Privedem primer: class x { /* ... */ x(int); x(char*); }; class y { /* ... */ y(int); }; class z { /* ... */ z(x); }; x f(x); y f(y); z g(z); void k1() { f(1); // nedopustimo, neodnoznachnost': f(x(1)) ili f(y(1)) f(x(1)); f(y(1)); g("asdf"); // nedopustimo, g(z(x("asdf"))) ne ispol'zuetsya } Pol'zovatel'skie preobrazovaniya tipa rassmatrivayutsya tol'ko v tom sluchae, kogda bez nih nel'zya odnoznachno vybrat' vyzyvaemuyu funkciyu: class x { /* ... */ x(int); }; void h(double); void h(x); void k2() { h(1); } Vyzov h(1) mozhno interpretirovat' libo kak h(double(1)), libo kak h(x(1)), poetomu v silu trebovaniya odnoznachnosti ego mozhno schest' nezakonnym. No poskol'ku v pervoj interpretacii ispol'zuetsya tol'ko standartnoe preobrazovanie, to po pravilam, ukazannym v $$4.6.6 i $$R.13.2, vybiraetsya ono. Pravila na preobrazovaniya tipa ne slishkom prosto sformulirovat' i realizovat', ne obladayut oni i dostatochnoj obshchnost'yu. Rassmotrim trebovanie edinstvennosti zakonnogo preobrazovaniya. Proshche vsego razreshit' translyatoru primenyat' lyuboe preobrazovanie, kotoroe on sumeet najti. Togda dlya vyyasneniya korrektnosti vyrazheniya ne nuzhno rassmatrivat' vse sushchestvuyushchie preobrazovaniya. K sozhaleniyu, v takom sluchae povedenie programmy budet zaviset' ot togo, kakoe imenno preobrazovanie najdeno. V rezul'tate povedenie programmy budet zaviset' ot poryadka opisanij preobrazovanij. Poskol'ku chasto eti opisaniya razbrosany po raznym ishodnym fajlam (sozdannym, vozmozhno, raznymi programmistami), to rezul'tat programmy budet zaviset' v kakom poryadke eti fajly slivayutsya v programmu. S drugoj storony, mozhno voobshche zapretit' neyavnye preobrazovaniya, i eto samoe prostoe reshenie. No rezul'tatom budet nekachestvennyj interfejs, opredelyaemyj pol'zovatelem, ili vzryvnoj rost peregruzhennyh funkcij i operacij, chto my i videli na primere klassa complex iz predydushchego razdela. Pri samom obshchem podhode uchityvayutsya vse svedeniya o tipah i rassmatrivayutsya vse sushchestvuyushchie preobrazovaniya. Naprimer, s uchetom privedennyh opisanij v prisvaivanii aa=f(1) mozhno razobrat'sya s vyzovom f(1), poskol'ku tip aa zadaet edinstvennoe preobrazovanie. Esli aa imeet tip x, to edinstvennym preobrazovaniem budet f(x(1)), poskol'ku tol'ko ono daet nuzhnyj dlya levoj chasti tip x. Esli aa imeet tip y, budet ispol'zovat'sya f(y(1)). Pri samom obshchem podhode udaetsya razobrat'sya i s vyzovom g("asdf"), poskol'ku g(z(x("asdf))) yavlyaetsya ego edinstvennoj interpretaciej. Trudnost' etogo podhoda v tom, chto trebuetsya doskonal'nyj razbor vsego vyrazheniya, chtoby ustanovit' interpretaciyu kazhdoj operacii i vyzova funkcii. V rezul'tate translyaciya zamedlyaetsya, vychislenie vyrazheniya mozhet proizojti strannym obrazom i poyavlyayutsya zagadochnye soobshcheniya ob oshibkah, kogda translyator uchityvaet opredelennye v bibliotekah preobrazovaniya i t.d. V rezul'tate translyatoru prihoditsya uchityvat' bol'she informacii, chem izvestno samomu programmistu! Vybran podhod, pri kotorom proverka yavlyaetsya strogo voshodyashchim processom, kogda v kazhdyj moment rassmatrivaetsya tol'ko odna operaciya s operandami, tipy kotoryh uzhe proshli proverku. Trebovanie strogo voshodyashchego razbora vyrazheniya predpolagaet, chto tip vozvrashchaemogo znacheniya ne uchityvaetsya pri razreshenii peregruzki: class quad { // ... public: quad(double); // ... }; quad operator+(quad,quad); void f(double a1, double a2) { quad r1 = a1+a2; // slozhenie s dvojnoj tochnost'yu quad r2 = quad(a1)+a2; // vynuzhdaet ispol'zovat' // operacii s tipami quad } V proektirovanii yazyka delalsya raschet na strogo voshodyashchij razbor, poskol'ku on bolee ponyatnyj, a krome togo, ne delo translyatora reshat' takie voprosy, kakuyu tochnost' dlya slozheniya zhelaet programmist. Odnako, nado otmetit', chto esli opredelilis' tipy obeih chastej v prisvaivanii i inicializacii, to dlya ih razresheniya ispol'zuetsya oni oba: class real { // ... public: operator double(); operator int(); // ... }; void g(real a) { double d = a; // d = a.double(); int i = a; // i = a.int(); d = a; // d = a.double(); i = a; // i = a.int(); } V etom primere vyrazheniya vse ravno razbirayutsya strogo voshodyashchim metodom, kogda v kazhdyj moment rassmatrivayutsya tol'ko odna operaciya i tipy ee operandov. 7.4 Literaly Dlya klassov nel'zya opredelit' literal'nye znacheniya, podobnomu tomu kak 1.2 i 12e3 yavlyayutsya literalami tipa double. Odnako, dlya interpretacii znachenij klassov mogut ispol'zovat'sya vmesto funkcij-chlenov literaly osnovnyh tipov. Obshchim sredstvom dlya postroeniya takih znachenij sluzhat konstruktory s edinstvennym parametrom. Esli konstruktor dostatochno prostoj i realizuetsya podstanovkoj, vpolne razumno predstavlyat' ego vyzov kak literal. Naprimer, s uchetom opisaniya klassa complex v <complex.h> v vyrazhenii zz1*3+zz2*complex(1,2) proizojdet dva vyzova funkcij, a ne pyat'. Dve operacii * privedut k vyzovu funkcii, a operaciya + i vyzovy konstruktora dlya postroeniya complex(3) i complex(1,2) budut realizovany podstanovkoj. 7.5 Bol'shie ob容kty Pri vypolnenii lyuboj binarnoj operacii dlya tipa complex realizuyushchej etu operaciyu funkcii budut peredavat'sya kak parametry kopii oboih operandov. Dopolnitel'nye rashody, vyzvannye kopirovaniem dvuh znachenij tipa double, zametny, hotya po vsej vidimosti dopustimy. K sozhaleniyu predstavlenie ne vseh klassov yavlyaetsya stol' udobno kompaktnym. CHtoby izbezhat' izbytochnogo kopirovaniya, mozhno opredelyat' funkcii s parametrami tipa ssylki: class matrix { double m[4][4]; public: matrix(); friend matrix operator+(const matrix&, const matrix&); friend matrix operator*(const matrix&, const matrix&); }; Ssylki pozvolyayut bez izlishnego kopirovaniya ispol'zovat' vyrazheniya s obychnymi arifmeticheskimi operaciyami i dlya bol'shih ob容ktov. Ukazateli dlya etoj celi ispol'zovat' nel'zya, t.k. nevozmozhno pereopredelit' interpretaciyu operacii, esli ona primenyaetsya k ukazatelyu. Operaciyu plyus dlya matric mozhno opredelit' tak: matrix operator+(const matrix& arg1, const& arg2) { matrix sum; for (int i = 0; i<4; i++) for (int j=0; j<4; j++) sum.m[i] [j] = arg1.m[i][j] + arg2.m[i][j]; return sum; } Zdes' v funkcii operator+() operandy vybirayutsya po ssylke, a vozvrashchaetsya samo znachenie ob容kta. Bolee effektivnym resheniem byl by vozvrat tozhe ssylki: class matrix { // ... friend matrix& operator+(const matrix&, const matrix&); friend matrix& operator*(const matrix&, const matrix&); }; |to dopustimo, no voznikaet problema s vydeleniem pamyati. Poskol'ku ssylka na rezul'tat operacii budet peredavat'sya kak ssylka na vozvrashchaemoe funkciej znachenie, ono ne mozhet byt' avtomaticheskoj peremennoj etoj funkcii. Poskol'ku operaciya mozhet ispol'zovat'sya neodnokratno v odnom vyrazhenii, rezul'tat ne mozhet byt' i lokal'noj staticheskoj peremennoj. Kak pravilo, rezul'tat budet zapisyvat'sya v otvedennyj v svobodnoj pamyati ob容kt. Obychno byvaet deshevle (po zatratam na vremya vypolneniya i pamyat' dannyh i komand) kopirovat' rezul'tiruyushchee znachenie, chem razmeshchat' ego v svobodnoj pamyati i zatem v konechnom schete osvobozhdat' vydelennuyu pamyat'. K tomu zhe etot sposob proshche zaprogrammirovat'. 7.6 Prisvaivanie i inicializaciya Rassmotrim prostoj strokovyj klass string: struct string { char* p; int size; // razmer vektora, na kotoryj ukazyvaet p string(int size) { p = new char[size=sz]; } ~string() { delete p; } }; Stroka - eto struktura dannyh, soderzhashchaya ukazatel' na vektor simvolov i razmer etogo vektora. Vektor sozdaetsya konstruktorom i udalyaetsya destruktorom. No kak my videli v $$5.5.1 zdes' mogut vozniknut' problemy: void f() { string s1(10); string s2(20) s1 = s2; } Zdes' budut razmeshcheny dva simvol'nyh vektora, no v rezul'tate prisvaivaniya s1 = s2 ukazatel' na odin iz nih budet unichtozhen, i zamenitsya kopiej vtorogo. Po vyhode iz f() budet vyzvan dlya s1 i s2 destruktor, kotoryj dvazhdy udalit odin i tot zhe vektor, rezul'taty chego po vsej vidimosti budut plachevny. Dlya resheniya etoj problemy nuzhno opredelit' sootvetstvuyushchee prisvaivanie ob容ktov tipa string: struct string { char* p; int size; // razmer vektora, na kotoryj ukazyvaet p string(int size) { p = new char[size=sz]; } ~string() { delete p; } string& operator=(const string&); }; string& string::operator=(const string& a) { if (this !=&a) { // opasno, kogda s=s delete p; p = new char[size=a.size]; strcpy(p,a.p); } return *this; } Pri takom opredelenii string predydushchij primer projdet kak zadumano. No posle nebol'shogo izmeneniya v f() problema voznikaet snova, no v inom oblichii: void f() { string s1(10); string s2 = s1; // inicializaciya, a ne prisvaivanie } Teper' tol'ko odin ob容kt tipa string stroitsya konstruktorom string::string(int), a unichtozhat'sya budet dve stroki. Delo v tom, chto pol'zovatel'skaya operaciya prisvaivaniya ne primenyaetsya k neinicializirovannomu ob容ktu. Dostatochno vzglyanut' na funkciyu string::operator(), chtoby ponyat' prichinu etogo: ukazatel' p budet togda imet' neopredelennoe, po suti sluchajnoe znachenie. Kak pravilo, v operacii prisvaivaniya predpolagaetsya, chto ee parametry proinicializirovany. Dlya inicializacii tipa toj, chto privedena v etom primere eto ne tak po opredeleniyu. Sledovatel'no, chtoby spravit'sya s inicializaciej nuzhna pohozhaya, no svoya funkciya: struct string { char* p; int size; // razmer vektora, na kotoryj ukazyvaet p string(int size) { p = new char[size=sz]; } ~string() { delete p; } string& operator=(const string&); string(const string&); }; string::string(const string& a) { p=new char[size=sz]; strcpy(p,a.p); } Inicializaciya ob容kta tipa X proishodit s pomoshch'yu konstruktora X(const X&). My ne perestaem povtoryat', chto prisvaivanie i inicializaciya yavlyayutsya raznymi operaciyami. Osobenno eto vazhno v teh sluchayah, kogda opredelen destruktor. Esli v klasse X est' netrivial'nyj destruktor, naprimer, proizvodyashchij osvobozhdenie ob容kta v svobodnoj pamyati, veroyatnee vsego, v etom klasse potrebuetsya polnyj nabor funkcij, chtoby izbezhat' kopirovaniya ob容ktov po chlenam: class X { // ... X(something); // konstruktor, sozdayushchij ob容kt X(const X&); // konstruktor kopirovaniya operator=(const X&); // prisvaivanie: // udalenie i kopirovanie ~X(); // destruktor, udalyayushchij ob容kt }; Est' eshche dva sluchaya, kogda prihoditsya kopirovat' ob容kt: peredacha parametra funkcii i vozvrat eyu znacheniya. Pri peredache parametra neinicializirovannaya peremennaya, t.e. formal'nyj parametr inicializiruetsya. Semantika etoj operacii identichna drugim vidam inicializacii. Tozhe proishodit i pri vozvrate funkciej znacheniya, hotya etot sluchaj ne takoj ochevidnyj. V oboih sluchayah ispol'zuetsya konstruktor kopirovaniya: string g(string arg) { return arg; } main() { string s = "asdf"; s = g(s); } Ochevidno, posle vyzova g() znachenie s dolzhno byt' "asdf". Ne trudno zapisat' v parametr s kopiyu znacheniya s, dlya etogo nado vyzvat' konstruktor kopirovaniya dlya string. Dlya polucheniya eshche odnoj kopii znacheniya s po vyhode iz g() nuzhen eshche odin vyzov konstruktora string(const string&). Na etot raz inicializiruetsya vremennaya peremennaya, kotoraya zatem prisvaivaetsya s. Dlya optimizacii odnu, no ne obe, iz podobnyh operacij kopirovaniya mozhno ubrat'. Estestvenno, vremennye peremennye, ispol'zuemye dlya takih celej, unichtozhayutsya nadlezhashchim obrazom destruktorom string::~string() (sm. $$R.12.2). Esli v klasse X operaciya prisvaivaniya X::operator=(const X&) i konstruktor kopirovaniya X::X(const X&) yavno ne zadany programmistom, nedostayushchie operacii budut sozdany translyatorom. |ti sozdannye funkcii budut kopirovat' po chlenam dlya vseh chlenov klassa X. Esli chleny prinimayut prostye znacheniya, kak v sluchae kompleksnyh chisel, eto, to, chto nuzhno, i sozdannye funkcii prevratyatsya v prostoe i optimal'noe porazryadnoe kopirovanie. Esli dlya samih chlenov opredeleny pol'zovatel'skie operacii kopirovaniya, oni i budut vyzyvat'sya sootvetstvuyushchim obrazom: class Record { string name, address, profession; // ... }; void f(Record& r1) { Record r2 = r1; } Zdes' dlya kopirovaniya kazhdogo chlena tipa string iz ob容kta r1 budet vyzyvat'sya string::operator=(const string&). V nashem pervom i nepolnocennom variante strokovyj klass imeet chlen-ukazatel' i destruktor. Poetomu standartnoe kopirovanie po chlenam dlya nego pochti navernyaka neverno. Translyator mozhet preduprezhdat' o takih situaciyah. 7.7 Indeksaciya Operatornaya funkciya operator[] zadaet dlya ob容ktov klassov interpretaciyu indeksacii. Vtoroj parametr etoj funkcij (indeks) mozhet imet' proizvol'nyj tip. |to pozvolyaet, naprimer, opredelyat' associativnye massivy. V kachestve primera mozhno perepisat' opredelenie iz $$2.3.10, gde associativnyj massiv ispol'zovalsya v nebol'shoj programme, podschityvayushchej chislo vhozhdenij slov v fajle. Tam dlya etogo ispol'zovalas' funkciya. My opredelim nastoyashchij tip associativnogo massiva: class assoc { struct pair { char* name; int val; }; pair* vec; int max; int free; assoc(const assoc&); // predotvrashchaet kopirovanie assoc& operator=(const assoc&); // predotvrashchaet kopirovanie public: assoc(int); int& operator[](const char*); void print_all(); }; V ob容kte assoc hranitsya vektor iz struktur pair razmerom max. V peremennoj free hranitsya indeks pervogo svobodnogo elementa vektora. CHtoby predotvratit' kopirovanie ob容ktov assoc, konstruktor kopirovaniya i operaciya prisvaivaniya opisany kak chastnye. Konstruktor vyglyadit tak: assoc::assoc(int s) { max = (s<16) ? 16 : s; free = 0; vec = new pair[max]; } V realizacii ispol'zuetsya vse tot zhe neeffektivnyj algoritm poiska, chto i v $$2.3.10. No teper', esli vektor perepolnyaetsya, ob容kt assoc uvelichivaetsya: #include <string.h> int& assoc::operator[](const char* p) /* rabotaet s mnozhestvom par (struktur pair): provodit poisk p, vozvrashchaet ssylku na celoe znachenie iz najdennoj pary, sozdaet novuyu paru, esli p ne najdeno */ { register pair* pp; for (pp=&vec[free-1]; vec<=pp; pp-- ) if (strcmp(p,pp->name) == 0) return pp->val; if (free == max) { //perepolnenie: vektor uvelichivaetsya pair* nvec = new pair[max*2]; for (int i=0; i<max; i++) nvec[i] = vec[i]; delete vec; vec = nvec; max = 2*max; } pp = &vec[free++]; pp->name = new char[strlen(p)+1]; strcpy(pp->name,p); pp->val = 0; // nachal'noe znachenie = 0 return pp->val; } Poskol'ku predstavlenie ob容kta assoc skryto ot pol'zovatelya, nuzhno imet' vozmozhnost' napechatat' ego kakim-to obrazom. V sleduyushchem razdele budet pokazano kak opredelit' nastoyashchij iterator dlya takogo ob容kta. Zdes' zhe my ogranichimsya prostoj funkciej pechati: void assoc::print_all() { for (int i = 0; i<free; i++) cout << vec[i].name << ": " << vec[i].val << '\n'; } Nakonec, mozhno napisat' trivial'nuyu programmu: main() // podschet chisla vhozhdenij vo vhodnoj // potok kazhdogo slova { const MAX = 256; // bol'she dliny samogo dlinnogo slova char buf[MAX]; assoc vec(512); while (cin>>buf) vec[buf]++; vec.print_all(); } Opytnye programmisty mogut zametit', chto vtoroj kommentarij mozhno legko oprovergnut'. Reshit' voznikayushchuyu zdes' problemu predlagaetsya v uprazhnenii $$7.14 [20]. Dal'nejshee razvitie ponyatie associativnogo massiva poluchit v $$8.8. Funkciya operator[]() dolzhna byt' chlenom klassa. Otsyuda sleduet, chto ekvivalentnost' x[y] == y[x] mozhet ne vypolnyat'sya, esli x ob容kt klassa. Obychnye otnosheniya ekvivalentnosti, spravedlivye dlya operacij so vstroennymi tipami, mogut ne vypolnyat'sya dlya pol'zovatel'skih tipov ($$7.2.2, sm. takzhe $$7.9). 7.8 Vyzov funkcii Vyzov funkcii, t.e. konstrukciyu vyrazhenie(spisok-vyrazhenij), mozhno rassmatrivat' kak binarnuyu operaciyu, v kotoroj vyrazhenie yavlyaetsya levym operandom, a spisok-vyrazhenij - pravym. Operaciyu vyzova mozhno peregruzhat' kak i drugie operacii. V funkcii operator()() spisok fakticheskih parametrov vychislyaetsya i proveryaetsya po tipam soglasno obychnym pravilam peredachi parametrov. Peregruzka operacii vyzova imeet smysl prezhde vsego dlya tipov, s kotorymi vozmozhna tol'ko odna operaciya, a takzhe dlya teh tipov, odna iz operacij nad kotorymi imeet nastol'ko vazhnoe znachenie, chto vse ostal'nye v bol'shinstve sluchaev mozhno ne uchityvat'. My ne dali opredeleniya iteratora dlya associativnogo massiva tipa assoc. Dlya etoj celi mozhno opredelit' special'nyj klass assoc_iterator, zadacha kotorogo vydavat' elementy iz assoc v nekotorom poryadke. V iteratore neobhodimo imet' dostup k dannym, hranimym v assoc, poetomu on dolzhen byt' opisan kak friend: class assoc { friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int& operator[](const char*); }; Iterator mozhno opredelit' tak: class assoc_iterator { const assoc* cs; // massiv assoc int i; // tekushchij indeks public: assoc_iterator(const assoc& s) { cs = &s; i = 0; } pair* operator()() { return (i<cs->free)? &cs->vec[i++] : 0; } }; Massiv assoc ob容kta assoc_iterator nuzhno inicializirovat', i pri kazhdom obrashchenii k nemu s pomoshch'yu operatornoj funkcii () budet vozvrashchat'sya ukazatel' na novuyu paru (struktura pair) iz etogo massiva. Pri dostizhenii konca massiva vozvrashchaetsya 0: main() // podschet chisla vhozhdenij vo vhodnoj // potok kazhdogo slova { const MAX = 256; // bol'she dliny samogo dlinnogo slova char buf[MAX]; assoc vec(512); while (cin>>buf) vec[buf]++; assoc_iterator next(vec); pair* p; while ( p = next(vec) ) cout << p->name << ": " << p->val << '\n'; } Iterator podobnogo vida imeet preimushchestvo pered naborom funkcij, reshayushchim tu zhe zadachu: iterator mozhet imet' sobstvennye chastnye dannye, v kotoryh mozhno hranit' informaciyu o hode iteracii. Obychno vazhno i to, chto mozhno odnovremenno zapustit' srazu neskol'ko iteratorov odnogo tipa. Konechno, ispol'zovanie ob容ktov dlya predstavleniya iteratorov neposredstvenno nikak ne svyazano s peregruzkoj operacij. Odni predpochitayut ispol'zovat' tip iteratora s takimi operaciyami, kak first(), next() i last(), drugim bol'she nravitsya peregruzka operacii ++ , kotoraya pozvolyaet poluchit' iterator, ispol'zuemyj kak ukazatel' (sm. $$8.8). Krome togo, operatornaya funkciya operator() aktivno ispol'zuetsya dlya vydeleniya podstrok i indeksacii mnogomernyh massivov. Funkciya operator() dolzhna byt' funkciej-chlenom. 7.9 Kosvennoe obrashchenie Operaciyu kosvennogo obrashcheniya k chlenu -> mozhno opredelit' kak unarnuyu postfiksnuyu operaciyu. |to znachit, esli est' klass class Ptr { // ... X* operator->(); }; ob容kty klassa Ptr mogut ispol'zovat'sya dlya dostupa k chlenam klassa X takzhe, kak dlya etoj celi ispol'zuyutsya ukazateli: void f(Ptr p) { p->m = 7; // (p.operator->())->m = 7 } Prevrashchenie ob容kta p v ukazatel' p.operator->() nikak ne zavisit ot chlena m, na kotoryj on ukazyvaet. Imenno po etoj prichine operator->() yavlyaetsya unarnoj postfiksnoj operaciej. Odnako, my ne vvodim novyh sintaksicheskih oboznachenij, tak chto imya chlena po-prezhnemu dolzhno idti posle -> : void g(Ptr p) { X* q1 = p->; // sintaksicheskaya oshibka X* q2 = p.operator->(); // normal'no } Peregruzka operacii -> prezhde vsego ispol'zuetsya dlya sozdaniya "hitryh ukazatelej", t.e. ob容ktov, kotorye pomimo ispol'zovaniya kak ukazateli pozvolyayut provodit' nekotorye operacii pri kazhdom obrashchenii k ukazuemomu ob容ktu s ih pomoshch'yu. Naprimer, mozhno opredelit' klass RecPtr dlya organizacii dostupa k ob容ktam klassa Rec, hranimym na diske. Parametrom konstruktora RecPtr yavlyaetsya imya, kotoroe budet ispol'zovat'sya dlya poiska ob容kta na diske. Pri obrashchenii k ob容ktu s pomoshch'yu funkcii RecPtr::operator->() on perepisyvaetsya v osnovnuyu pamyat', a v konce raboty destruktor RecPtr zapisyvaet izmenennyj ob容kt obratno na disk. class RecPtr { Rec* in_core_address; const char* identifier; // ... public: RecPtr(const char* p) : identifier(p) { in_core_address = 0; } ~RecPtr() { write_to_disc(in_core_address,identifier); } Rec* operator->(); }; Rec* RecPtr::operator->() { if (in_core_address == 0) in_core_address = read_from_disc(identifier); return in_core_address; } Ispol'zovat' eto mozhno tak: main(int argc, const char* argv) { for (int i = argc; i; i--) { RecPtr p(argv[i]); p->update(); } } Na samom dele, tip RecPtr dolzhen opredelyat'sya kak shablon tipa (sm. $$8), a tip struktury Record budet ego parametrom. Krome togo, nastoyashchaya programma budet soderzhat' obrabotku oshibok i vzaimodejstvie s diskom budet organizovano ne stol' primitivno. Dlya obychnyh ukazatelej operaciya -> ekvivalentna operaciyam, ispol'zuyushchim * i []. Tak, esli opisano Y* p; to vypolnyaetsya sootnoshenie p->m == (*p).m == p[0].m Kak vsegda, dlya opredelennyh pol'zovatelem operacij takie sootnosheniya ne garantiruyutsya. Tam, gde vse-taki takaya ekvivalentnost' trebuetsya, ee mozhno obespechit': class X { Y* p; public: Y* operator->() { return p; } Y& operator*() { return *p; } Y& operator[](int i) { return p[i]; } }; Esli v vashem klasse opredeleno bolee odnoj podobnoj operacii, razumno budet obespechit' ekvivalentnost', tochno tak zhe, kak razumno predusmotret' dlya prostoj peremennoj x nekotorogo klassa, v kotorom est' operacii ++, += = i +, chtoby operacii ++x i x+=1 byli ekvivalentny x=x+1. Peregruzka -> kak i peregruzka [] mozhet igrat' vazhnuyu rol' dlya celogo klassa nastoyashchih programm, a ne yavlyaetsya prosto eksperimentom radi lyubopytstva. Delo v tom, chto v programmirovanii ponyatie kosvennosti yavlyaetsya klyuchevym, a peregruzka -> daet yasnyj, pryamoj i effektivnyj sposob predstavleniya etogo ponyatiya v programme. Est' drugaya tochka zreniya na operaciyu ->, kak na sredstvo zadat' v S++ ogranichennyj, no poleznyj variant ponyatiya delegirovaniya (sm. $$12.2.8 i 13.9). 7.10 Inkrement i dekrement Esli my dodumalis' do "hitryh ukazatelej", to logichno poprobovat' pereopredelit' operacii inkrementa ++ i dekrementa -- , chtoby poluchit' dlya klassov te vozmozhnosti, kotorye eti operacii dayut dlya vstroennyh tipov. Takaya zadacha osobenno estestvenna i neobhodima, esli stavitsya cel' zamenit' tip obychnyh ukazatelej na tip "hitryh ukazatelej", dlya kotorogo semantika ostaetsya prezhnej, no poyavlyayutsya nekotorye dejstviya dinamicheskogo kontrolya. Pust' est' programma s rasprostranennoj oshibkoj: void f1(T a) // tradicionnoe ispol'zovanie { T v[200]; T* p = &v[10]; p--; *p = a; // Priehali: `p' nastroen vne massiva, // i eto ne obnaruzheno ++p; *p = a; // normal'no } Estestvenno zhelanie zamenit' ukazatel' p na ob容kt klassa CheckedPtrToT, po kotoromu kosvennoe obrashchenie vozmozhno tol'ko pri uslovii, chto on dejstvitel'no ukazyvaet na ob容kt. Primenyat' inkrement i dekrement k takomu ukazatelyu budet mozhno tol'ko v tom sluchae, chto ukazatel' nastroen na ob容kt v granicah massiva i v rezul'tate etih operacij poluchitsya ob容kt v granicah togo zhe massiva: class CheckedPtrToT { // ... }; void f2(T a) // variant s kontrolem { T v[200]; CheckedPtrToT p(&v[0],v,200); p--; *p = a; // dinamicheskaya oshibka: // `p' vyshel za granicy massiva ++p; *p = a; // normal'no } Inkrement i dekrement yavlyayutsya edinstvennymi operaciyami v S++, kotorye mozhno ispol'zovat' kak postfiksnye i prefiksnye operacii. Sledovatel'no, v opredelenii klassa CheckedPtrToT my dolzhny predusmotret' otdel'nye funkcii dlya prefiksnyh i postfiksnyh operacij inkrementa i dekrementa: class CheckedPtrToT { T* p; T* array; int size; public: // nachal'noe znachenie `p' // svyazyvaem s massivom `a' razmera `s' CheckedPtrToT(T* p, T* a, int s); // nachal'noe znachenie `p' // svyazyvaem s odinochnym ob容ktom CheckedPtrToT(T* p); T* operator++(); // prefiksnaya T* operator++(int); // postfiksnaya T* operator--(); // prefiksnaya T* operator--(int); // postfiksnaya T& operator*(); // prefiksnaya }; Parametr tipa int sluzhit ukazaniem, chto funkciya budet vyzyvat'sya dlya postfiksnoj operacii. Na samom dele etot parametr yavlyaetsya iskusstvennym i nikogda ne ispol'zuetsya, a sluzhit tol'ko dlya razlichiya postfiksnoj i prefiksnoj operacii. CHtoby zapomnit', kakaya versiya funkcii operator++ ispol'zuetsya kak prefiksnaya operaciya, dostatochno pomnit', chto prefiksnoj yavlyaetsya versiya bez iskusstvennogo parametra, chto verno i dlya vseh drugih unarnyh arifmeticheskih i logicheskih operacij. Iskusstvennyj parametr ispol'zuetsya tol'ko dlya "osobyh" postfiksnyh operacij ++ i --. S pomoshch'yu klassa CheckedPtrToT primer mozhno zapisat' tak: void f3(T a) // variant s kontrolem { T v[200]; CheckedPtrToT p(&v[0],v,200); p.operator--(1); p.operator*() = a; // dinamicheskaya oshibka: // `p' vyshel za granicy massiva p.operator++(); p.operator*() = a; // normal'no } V uprazhnenii $$7.14 [19] predlagaetsya zavershit' opredelenie klassa CheckedPtrToT, a drugim uprazhneniem ($$9.10[2]) yavlyaetsya preobrazovanie ego v shablon tipa, v kotorom dlya soobshchenij o dinamicheskih oshibkah ispol'zuyutsya osobye situacii. Primery ispol'zovaniya operacij ++ i -- dlya iteracij mozhno najti v $$8.8. 7.11 Strokovyj klass Teper' mozhno privesti bolee osmyslennyj variant klassa string. V nem podschityvaetsya chislo ssylok na stroku, chtoby minimizirovat' kopirovanie, i ispol'zuyutsya kak konstanty standartnye stroki C++. #include <iostream.h> #include <string.h> class string { struct srep { char* s; // ukazatel' na stroku int n; // schetchik chisla ss