cii diskriminiruyushchego (t.e. nadezhnogo) ob容dineniya. Sleduyushchie tri glavy zavershayut opisanie vozmozhnostej S++ dlya postroeniya novyh tipov, i v nih soderzhitsya bol'she interesnyh primerov. 5.1 Vvedenie i kratkij obzor Ponyatie klassa, kotoromu posvyashchena eta i tri sleduyushchih glavy, sluzhit v S++ dlya togo, chtoby dat' programmistu instrument postroeniya novyh tipov. Imi pol'zovat'sya ne menee udobno, chem vstroennymi. V ideale ispol'zovanie opredelennogo pol'zovatelem tipa ne dolzhno otlichat'sya ot ispol'zovaniya vstroennyh tipov. Razlichiya vozmozhny tol'ko v sposobe postroeniya. Tip est' vpolne konkretnoe predstavlenie nekotorogo ponyatiya. Naprimer, v S++ tip float s operaciyami +, -, * i t.d. yavlyaetsya hotya i ogranichennym, no konkretnym predstavleniem matematicheskogo ponyatiya veshchestvennogo chisla. Novyj tip sozdaetsya dlya togo, chtoby stat' special'nym i konkretnym predstavleniem ponyatiya, kotoroe ne nahodit pryamogo i estestvennogo otrazheniya sredi vstroennyh tipov. Naprimer, v programme iz oblasti telefonnoj svyazi mozhno vvesti tip trunk_module (liniya-svyazi), v videoigre - tip explosion (vzryv), a v programme, obrabatyvayushchej tekst, - tip list_of_paragraphs (spisok-paragrafov). Obychno proshche ponimat' i izmenyat' programmu, v kotoroj tipy horosho predstavlyayut ispol'zuemye v zadache ponyatiya. Udachno podobrannoe mnozhestvo pol'zovatel'skih tipov delaet programmu bolee yasnoj. Ono pozvolyaet translyatoru obnaruzhivat' nedopustimoe ispol'zovanie ob容ktov, kotoroe v protivnom sluchae ostanetsya nevyyavlennym do otladki programmy. Glavnoe v opredelenii novogo tipa - eto otdelit' nesushchestvennye detali realizacii (naprimer, raspolozhenie dannyh v ob容kte novogo tipa) ot teh ego harakteristik, kotorye sushchestvenny dlya pravil'nogo ego ispol'zovaniya (naprimer, polnyj spisok funkcij, imeyushchih dostup k dannym). Takoe razdelenie obespechivaetsya tem, chto vsya rabota so strukturoj dannyh i vnutrenie, sluzhebnye operacii nad neyu dostupny tol'ko cherez special'nyj interfejs (cherez "odno gorlo"). Glava sostoit iz chetyreh chastej: $$5.2 Klassy i chleny. Zdes' vvoditsya osnovnoe ponyatie pol'zovatel'skogo tipa, nazyvaemogo klassom. Dostup k ob容ktam klassa mozhet ogranichivat'sya mnozhestvom funkcij, opisaniya kotoryh vhodyat v opisanie klassa. |ti funkcii nazyvayutsya funkciyami-chlenami i druz'yami. Dlya sozdaniya ob容ktov klassa ispol'zuyutsya special'nye funkcii-chleny, nazyvaemye konstruktorami. Mozhno opisat' special'nuyu funkciyu-chlen dlya udaleniya ob容ktov klassa pri ego unichtozhenii. Takaya funkciya nazyvaetsya destruktorom. $$5.3 Interfejsy i realizacii. Zdes' privodyatsya dva primera razrabotki, realizacii i ispol'zovaniya klassov. $$5.4 Dopolnitel'nye svojstva klassov. Zdes' privoditsya mnogo dopolnitel'nyh podrobnostej o klassah. Pokazano, kak funkcii, ne yavlyayushchejsya chlenom klassa, predostavit' dostup k ego chastnoj chasti. Takuyu funkciyu nazyvayut drugom klassa. Vvodyatsya ponyatiya staticheskih chlenov klassa i ukazatelej na chleny klassa. Zdes' zhe pokazano, kak opredelit' diskriminiruyushchee ob容dinenie. $$5.5 Konstruktory i destruktory. Ob容kt mozhet sozdavat'sya kak avtomaticheskij, staticheskij ili kak ob容kt v svobodnoj pamyati. Krome togo, ob容kt mozhet byt' chlenom nekotorogo agregata (massiva ili drugogo klassa), kotoryj tozhe mozhno razmeshchat' odnim iz etih treh sposobov. Podrobno ob座asnyaetsya ispol'zovanie konstruktorov i destruktorov, opisyvaetsya primenenie opredelyaemyh pol'zovatelem funkcij razmeshcheniya v svobodnoj pamyati i funkcij osvobozhdeniya pamyati. 5.2 Klassy i chleny Klass - eto pol'zovatel'skij tip. |tot razdel znakomit s osnovnymi sredstvami opredeleniya klassa, sozdaniya ego ob容ktov, raboty s takimi ob容ktami i, nakonec, udaleniya etih ob容ktov posle ispol'zovaniya. 5.2.1 Funkcii-chleny Posmotrim, kak mozhno predstavit' v yazyke ponyatie daty, ispol'zuya dlya etogo tip struktury i nabor funkcij, rabotayushchih s peremennymi etogo tipa: struct date { int month, day, year; }; date today; void set_date(date*, int, int, int); void next_date(date*); void print_date(const date*); // ... Nikakoj yavnoj svyazi mezhdu funkciyami i strukturoj date net. Ee mozhno ustanovit', esli opisat' funkcii kak chleny struktury: struct date { int month, day, year; void set(int, int, int); void get(int*, int* int*); void next(); void print(); }; Opisannye takim obrazom funkcii nazyvayutsya funkciyami-chlenami. Ih mozhno vyzyvat' tol'ko cherez peremennye sootvetstvuyushchego tipa, ispol'zuya standartnuyu zapis' obrashcheniya k chlenu struktury: date today; date my_birthday; void f() { my_birthday.set(30,12,1950); today.set(18,1,1991); my_birthday.print(); today.next(); } Poskol'ku raznye struktury mogut imet' funkcii-chleny s odinakovymi imenami, pri opredelenii funkcii-chlena nuzhno ukazyvat' imya struktury: void date::next() { if (++day > 28 ) { // zdes' slozhnyj variant } } V tele funkcii-chlena imena chlenov mozhno ispol'zovat' bez ukazaniya imeni ob容kta. V takom sluchae imya otnositsya k chlenu togo ob容kta, dlya kotorogo byla vyzvana funkciya. 5.2.2 Klassy My opredelili neskol'ko funkcij dlya raboty so strukturoj date, no iz ee opisaniya ne sleduet, chto eto edinstvennye funkcii, kotorye predostavlyayut dostup k ob容ktam tipa date. Mozhno ustanovit' takoe ogranichenie, opisav klass vmesto struktury: class date { int month, day, year; public: void set(int, int, int); void get(int*, int*, int*); void next(); void print() }; Sluzhebnoe slovo public (obshchij) razbivaet opisanie klassa na dve chasti. Imena, opisannye v pervoj chastnoj (private) chasti klassa, mogut ispol'zovat'sya tol'ko v funkciyah-chlenah. Vtoraya - obshchaya chast' - predstavlyaet soboj interfejs s ob容ktami klassa. Poetomu struktura - eto takoj klass, v kotorom po opredeleniyu vse chleny yavlyayutsya obshchimi. Funkcii-chleny klassa opredelyayutsya i ispol'zuyutsya tochno tak zhe, kak bylo pokazano v predydushchem razdele: void date::print() // pechat' daty v prinyatom v SSHA vide { cout << month << '/' << day << '/' << year ; } Odnako ot funkcij ne chlenov chastnye chleny klassa date uzhe ograzhdeny: void backdate() { today.day--; // oshibka } Est' ryad preimushchestv v tom, chto dostup k strukture dannyh ogranichen yavno ukazannym spiskom funkcij. Lyubaya oshibka v date (naprimer, December, 36, 1985) mogla byt' vnesena tol'ko funkciej-chlenom, poetomu pervaya stadiya otladki - lokalizaciya oshibki - proishodit dazhe do pervogo puska programmy. |to tol'ko chastnyj sluchaj obshchego pravila: lyuboe izmenenie v povedenii tipa date mozhet i dolzhno vyzyvat'sya izmeneniyami v ego chlenah. Drugoe preimushchestvo v tom, chto potencial'nomu pol'zovatelyu klassa dlya raboty s nim dostatochno znat' tol'ko opredeleniya funkcij-chlenov. Zashchita chastnyh dannyh osnovyvaetsya tol'ko na ogranichenii ispol'zovaniya imen chlenov klassa. Poetomu ee mozhno obojti s pomoshch'yu manipulyacij s adresami ili yavnyh preobrazovanij tipa, no eto uzhe mozhno schitat' moshennichestvom. 5.2.3 Ssylka na sebya V funkcii-chlene mozhno neposredstvenno ispol'zovat' imena chlenov togo ob容kta, dlya kotorogo ona byla vyzvana: class X { int m; public: int readm() { return m; } }; void f(X aa, X bb) { int a = aa.readm(); int b = bb.readm(); // ... } Pri pervom vyzove readm() m oboznachaet aa.m, a pri vtorom - bb.m. U funkcii-chlena est' dopolnitel'nyj skrytyj parametr, yavlyayushchijsya ukazatelem na ob容kt, dlya kotorogo vyzyvalas' funkciya. Mozhno yavno ispol'zovat' etot skrytyj parametr pod imenem this. Schitaetsya, chto v kazhdoj funkcii-chlene klassa X ukazatel' this opisan neyavno kak X *const this; i inicializiruetsya, chtoby ukazyvat' na ob容kt, dlya kotorogo funkciya-chlen vyzyvalas'. |tot ukazatel' nel'zya izmenyat', poskol'ku on postoyannyj (*const). YAvno opisat' ego tozhe nel'zya, t.k. this - eto sluzhebnoe slovo. Mozhno dat' ekvivalentnoe opisanie klassa X: class X { int m; public: int readm() { return this->m; } }; Dlya obrashcheniya k chlenam ispol'zovat' this izlishne. V osnovnom this ispol'zuetsya v funkciyah-chlenah, neposredstvenno rabotayushchih s ukazatelyami. Tipichnyj primer - funkciya, kotoraya vstavlyaet element v spisok s dvojnoj svyaz'yu: class dlink { dlink* pre; // ukazatel' na predydushchij element dlink* suc; // ukazatel' na sleduyushchij element public: void append(dlink*); // ... }; void dlink::append(dlink* p) { p->suc = suc; // t.e. p->suc = this->suc p->pre = this; // yavnoe ispol'zovanie "this" suc->pre = p; // t.e. this->suc->pre = p suc = p; // t.e. this->suc = p } dlink* list_head; void f(dlink* a, dlink* b) { // ... list_head->append(a); list_head->append(b); } Spiski s takoj obshchej strukturoj sluzhat fundamentom spisochnyh klassov, opisyvaemyh v glave 8. CHtoby prisoedinit' zveno k spisku, nuzhno izmenit' ob容kty, na kotorye nastroeny ukazateli this, pre i suc. Vse oni imeyut tip dlink, poetomu funkciya-chlen dlink::append() imeet k nim dostup. Zashchishchaemoj edinicej v S++ yavlyaetsya klass, a ne otdel'nyj ob容kt klassa. Mozhno opisat' funkciyu-chlen takim obrazom, chto ob容kt, dlya kotorogo ona vyzyvaetsya, budet dostupen ej tol'ko po chteniyu. Tot fakt, chto funkciya ne budet izmenyat' ob容kt, dlya kotorogo ona vyzyvaetsya (t.e. this*), oboznachaetsya sluzhebnym slovom const v konce spiska parametrov: class X { int m; public: readme() const { return m; } writeme(int i) { m = i; } }; Funkciyu-chlen so specifikaciej const mozhno vyzyvat' dlya postoyannyh ob容ktov, a funkciyu-chlen bez takoj specifikacii - nel'zya: void f(X& mutable, const X& constant) { mutable.readme(); // normal'no mutable.writeme(7); // normal'no constant.readme(); // normal'no constant.writeme(7); // oshibka } V etom primere razumnyj translyator smog by obnaruzhit', chto funkciya X::writeme() pytaetsya izmenit' postoyannyj ob容kt. Odnako, eto neprostaya zadacha dlya translyatora. Iz-za razdel'noj translyacii on v obshchem sluchae ne mozhet garantirovat' "postoyanstvo" ob容kta, esli net sootvetstvuyushchego opisaniya so specifikaciej const. Naprimer, opredeleniya readme() i writeme() mogli byt' v drugom fajle: class X { int m; public: readme() const; writeme(int i); }; V takom sluchae opisanie readme() so specifikaciej const sushchestvenno. Tip ukazatelya this v postoyannoj funkcii-chlene klassa X est' const X *const. |to znachit, chto bez yavnogo privedeniya s pomoshch'yu this nel'zya izmenit' znachenie ob容kta: class X { int m; public: // ... void implicit_cheat() const { m++; } // oshibka void explicit_cheat() const { ((X*)this)->m++; } // normal'no }; Otbrosit' specifikaciyu const mozhno potomu, chto ponyatie "postoyanstva" ob容kta imeet dva znacheniya. Pervoe, nazyvaemoe "fizicheskim postoyanstvom" sostoit v tom, chto ob容kt hranitsya v zashchishchennoj ot zapisi pamyati. Vtoroe, nazyvaemoe "logicheskim postoyanstvom" zaklyuchaetsya v tom, chto ob容kt vystupaet kak postoyannyj (neizmenyaemyj) po otnosheniyu k pol'zovatelyam. Operaciya nad logicheski postoyannym ob容ktom mozhet izmenit' chast' dannyh ob容kta, esli pri etom ne narushaetsya ego postoyanstvo s tochki zreniya pol'zovatelya. Operaciyami, nenarushayushchimi logicheskoe postoyanstvo ob容kta, mogut byt' buferizaciya znachenij, vedenie statistiki, izmenenie peremennyh-schetchikov v postoyannyh funkciyah-chlenah. Logicheskogo postoyanstva mozhno dostignut' privedeniem, udalyayushchim specifikaciyu const: class calculator1 { int cache_val; int cache_arg; // ... public: int compute(int i) const; // ... }; int calculator1::compute(int i) const { if (i == cache_arg) return cache_val; // neluchshij sposob ((calculator1*)this)->cache_arg = i; ((calculator1*)this)->cache_val = val; return val; } |togo zhe rezul'tata mozhno dostich', ispol'zuya ukazatel' na dannye bez const: struct cache { int val; int arg; }; class calculator2 { cache* p; // ... public: int compute(int i) const; // ... }; int calculator2::compute(int i) const { if (i == p->arg) return p->val; // neluchshij sposob p->arg = i; p->val = val; return val; } Otmetim, chto const nuzhno ukazyvat' kak v opisanii, tak i v opredelenii postoyannoj funkcii-chlena. Fizicheskoe postoyanstvo obespechivaetsya pomeshcheniem ob容kta v zashchishchennuyu po zapisi pamyat', tol'ko esli v klasse net konstruktora ($$7.1.6). 5.2.4 Inicializaciya Inicializaciya ob容ktov klassa s pomoshch'yu takih funkcij kak set_date() - neelegantnoe i chrevatoe oshibkami reshenie. Poskol'ku yavno ne bylo ukazano, chto ob容kt trebuet inicializacii, programmist mozhet libo zabyt' eto sdelat', libo sdelat' dvazhdy, chto mozhet privesti k stol' zhe katastroficheskim posledstviyam. Luchshe dat' programmistu vozmozhnost' opisat' funkciyu, yavno prednaznachennuyu dlya inicializacii ob容ktov. Poskol'ku takaya funkciya konstruiruet znachenie dannogo tipa, ona nazyvaetsya konstruktorom. |tu funkciyu legko raspoznat' - ona imeet to zhe imya, chto i ee klass: class date { // ... date(int, int, int); }; Esli v klasse est' konstruktor, vse ob容kty etogo klassa budut proinicializirovany. Esli konstruktoru trebuyutsya parametry, ih nado ukazyvat': date today = date(23,6,1983); date xmas(25,12,0); // kratkaya forma date my_birthday; // nepravil'no, nuzhen inicializator CHasto byvaet udobno ukazat' neskol'ko sposobov inicializacii ob容kta. Dlya etogo nuzhno opisat' neskol'ko konstruktorov: class date { int month, day, year; public: // ... date(int, int, int); // den', mesyac, god date(int, int); // den', mesyac i tekushchij god date(int); // den' i tekushchie god i mesyac date(); // standartnoe znachenie: tekushchaya data date(const char*); // data v strokovom predstavlenii }; Parametry konstruktorov podchinyayutsya tem zhe pravilam o tipah parametrov, chto i vse ostal'nye funkcii ($$4.6.6). Poka konstruktory dostatochno razlichayutsya po tipam svoih parametrov, translyator sposoben pravil'no vybrat' konstruktor: date today(4); date july4("July 4, 1983"); date guy("5 Nov"); date now; // inicializaciya standartnym znacheniem Razmnozhenie konstruktorov v primere c date tipichno. Pri razrabotke klassa vsegda est' soblazn dobavit' eshche odnu vozmozhnost', - a vdrug ona komu-nibud' prigoditsya. CHtoby opredelit' dejstvitel'no nuzhnye vozmozhnosti, nado porazmyshlyat', no zato v rezul'tate, kak pravilo, poluchaetsya bolee kompaktnaya i ponyatnaya programma. Sokratit' chislo shodnyh funkcij mozhno s pomoshch'yu standartnogo znacheniya parametra. V primere s date dlya kazhdogo parametra mozhno zadat' standartnoe znachenie, chto oznachaet: "vzyat' znachenie iz tekushchej daty". class date { int month, day, year; public: // ... date(int d =0, int m =0, y=0); // ... }; date::date(int d, int m, int y) { day = d ? d : today.day; month = m ? m : today.month; year = y ? y : today.year; // proverka pravil'nosti daty // ... } Kogda ispol'zuetsya standartnoe znachenie parametra, ono dolzhno otlichat'sya ot vseh dopustimyh znachenij parametra. V sluchae mesyaca i dnya ochevidno, chto pri znachenii nul' - eto tak, no neochevidno, chto nul' podhodit dlya znacheniya goda. K schast'yu, v evropejskom kalendare net nulevogo goda, t.k. srazu posle 1 g. do r.h. (year==-1) idet 1 g. r.h. (year==1). Odnako dlya obychnoj programmy eto, vozmozhno, slishkom tonkij moment. Ob容kt klassa bez konstruktora mozhet inicializirovat'sya prisvaivaniem emu drugogo ob容kta etogo zhe klassa. |to nezapreshcheno i v tom sluchae, kogda konstruktory opisany: date d = today; // inicializaciya prisvaivaniem Na samom dele, imeetsya standartnyj konstruktor kopirovaniya, opredelennyj kak poelementnoe kopirovanie ob容ktov odnogo klassa. Esli takoj konstruktor dlya klassa X ne nuzhen, mozhno pereopredelit' ego kak konstruktor kopirovaniya X::X(const X&). Podrobnee pogovorim ob etom v $$7.6. 5.2.5 Udalenie Pol'zovatel'skie tipy chashche imeyut, chem ne imeyut, konstruktory, kotorye provodyat nadlezhashchuyu inicializaciyu. Dlya mnogih tipov trebuetsya i obratnaya operaciya - destruktor, garantiruyushchaya pravil'noe udalenie ob容ktov etogo tipa. Destruktor klassa X oboznachaetsya ~X ("dopolnenie konstruktora"). V chastnosti, dlya mnogih klassov ispol'zuetsya svobodnaya pamyat' (sm. $$3.2.6), vydelyaemaya konstruktorom i osvobozhdaemaya destruktorom. Vot, naprimer, tradicionnoe opredelenie tipa stek, iz kotorogo dlya kratkosti polnost'yu vybroshena obrabotka oshibok: class char_stack { int size; char* top; char* s; public: char_stack(int sz) { top=s=new char[size=sz]; } ~char_stack() { delete[] s; } // destruktor void push(char c) { *top++ = c; } void pop() { return *--top; } }; Kogda ob容kt tipa char_stack vyhodit iz tekushchej oblasti vidimosti, vyzyvaetsya destruktor: void f() { char_stack s1(100); char_stack s2(200); s1.push('a'); s2.push(s1.pop()); char ch = s2.pop(); cout << ch << '\n'; } Kogda nachinaet vypolnyat'sya f(), vyzyvaetsya konstruktor char_stack, kotoryj razmeshchaet massiv iz 100 simvolov s1 i massiv iz 200 simvolov s2. Pri vozvrate iz f() pamyat', kotoraya byla zanyata oboimi massivami, budet osvobozhdena. 5.2.6 Podstanovka Programmirovanie s klassami predpolagaet, chto v programme poyavitsya mnozhestvo malen'kih funkcij. Po suti, vsyudu, gde v programme s tradicionnoj organizaciej stoyalo by obychnoe obrashchenie k strukture dannyh, ispol'zuetsya funkciya. To, chto bylo soglasheniem, stalo standartom, proveryaemym translyatorom. V rezul'tate programma mozhet stat' krajne neeffektivnoj. Hotya vyzov funkcii v C++ i ne stol' dorogostoyashchaya operaciya po sravneniyu s drugimi yazykami, vse-taki cena ee mnogo vyshe, chem u pary obrashchenij k pamyati, sostavlyayushchih telo trivial'noj funkcii. Preodolet' etu trudnost' pomogayut funkcii-podstanovki (inline). Esli v opisanii klassa funkciya-chlen opredelena, a ne tol'ko opisana, to ona schitaetsya podstanovkoj. |to znachit, naprimer, chto pri translyacii funkcij, ispol'zuyushchih char_stack iz predydushchego primera, ne budet ispol'zovat'sya nikakih operacij vyzova funkcij, krome realizacii operacij vyvoda! Drugimi slovami, pri razrabotke takogo klassa ne nuzhno prinimat' vo vnimanie zatraty na vyzov funkcij. Lyuboe, dazhe samoe malen'koe dejstvie, mozhno smelo opredelyat' kak funkciyu bez poteri effektivnosti. |to zamechanie snimaet naibolee chasto privodimyj dovod v pol'zu obshchih chlenov dannyh. Funkciyu-chlen mozhno opisat' so specifikaciej inline i vne opisaniya klassa: class char_stack { int size; char* top; char* s; public: char pop(); // ... }; inline char char_stack::pop() { return *--top; } Otmetim, chto nedopustimo opisyvat' raznye opredeleniya funkcii-chlena, yavlyayushchejsya podstanovkoj, v razlichnyh ishodnyh fajlah ($$R.7.1.2). |to narushilo by ponyatie o klasse kak o cel'nom tipe. 5.3 Interfejsy i realizacii CHto predstavlyaet soboj horoshij klass? |to nechto, obladayushchee horosho opredelennym mnozhestvom operacij. Nechto, rassmatrivaemoe kak "chernyj yashchik", upravlyat' kotorym mozhno tol'ko posredstvom etih operacij. Nechto, ch'e fakticheskoe predstavlenie mozhno izmenit' lyubym myslimym sposobom, no ne izmenyaya pri etom sposoba ispol'zovaniya operacij. Nechto, chto mozhet potrebovat'sya v neskol'kih ekzemplyarah. Ochevidnye primery horoshih klassov dayut kontejnery raznyh vidov: tablicy, mnozhestva, spiski, vektora, slovari i t.d. Takoj klass imeet operaciyu zaneseniya v kontejner. Obychno imeetsya i operaciya proverki: byl li dannyj chlen zanesen v kontejner? Mogut byt' operacii uporyadochivaniya vseh chlenov i prosmotra ih v opredelennom poryadke. Nakonec, mozhet byt' operaciya udaleniya chlena. Obychno kontejnernye klassy imeyut konstruktory i destruktory. 5.3.1 Al'ternativnye realizacii Poka opisanie obshchej chasti klassa i funkcij-chlenov ostaetsya neizmennym, mozhno, ne vliyaya na pol'zovatelej klassa, menyat' ego realizaciyu. V podtverzhdenie etogo rassmotrim tablicu imen iz programmy kal'kulyatora, privedennoj v glave 3. Struktura ee takova: struct name { char* string; name* next; double value; }; A vot variant klassa table (tablica imen): // fajl table.h class table { name* tbl; public: table() { tbl = 0; } name* look(char*, int = 0); name* insert(char* s) { return look(s,1); } }; |ta tablica otlichaetsya ot opredelennoj v glave 3 tem, chto eto nastoyashchij tip. Mozhno opisat' neskol'ko tablic, zavesti ukazatel' na tablicu i t.d. Naprimer: #include "table.h" table globals; table keywords; table* locals; main() { locals = new table; // ... } Privedem realizaciyu funkcii table::look(), v kotoroj ispol'zuetsya linejnyj poisk v spiske imen tablicy: #include <string.h> name* table::look(char* p, int ins) { for (name* n = tbl; n; n=n->next) if (strcmp(p,n->string) == 0) return n; if (ins == 0) error("imya ne najdeno"); name* nn = new name; nn->string = new char[strlen(p)+1]; strcpy(nn->string,p); nn->value = 1; nn->next = tbl; tbl = nn; return nn; } Teper' usovershenstvuem klass table tak, chtoby poisk imeni shel po klyuchu (hesh-funkcii ot imeni), kak eto i bylo sdelano v primere s kal'kulyatorom. Sdelat' eto trudnee, esli soblyudat' ogranichenie, trebuyushchee, chtoby ne vse programmy, ispol'zuyushchie privedennuyu versiyu klassa table, nado bylo izmenyat': class table { name** tbl; int size; public: table(int sz = 15); ~table(); name* look(char*, int = 0); name* insert(char* s) { return look(s,1); } }; Izmeneniya v strukture dannyh i konstruktore proizoshli potomu, chto dlya heshirovaniya tablica dolzhna imet' opredelennyj razmer. Zadanie konstruktora so standartnym znacheniem parametra garantiruet, chto starye programmy, v kotoryh ne ispol'zovalsya razmer tablicy, ostanutsya vernymi. Standartnye znacheniya parametrov polezny v takih sluchayah, kogda nuzhno izmenit' klass, ne vliyaya na programmy pol'zovatelej klassa. Teper' konstruktor i destruktor sozdayut i unichtozhayut heshirovannye tablicy: table::table(int sz) { if (sz < 0) error("razmer tablicy otricatelen"); tbl = new name*[size = sz]; for ( int i = 0; i<sz; i++) tbl[i] = 0; } table::~table() { for (int i = 0; i<size; i++) { name* nx; for (name* n = tbl[i]; n; n=nx) { nx = n->next; delete n->string; delete n; } } delete tbl; } Opisav destruktor dlya klassa name, mozhno poluchit' bolee yasnyj i prostoj variant table::~table(). Funkciya poiska prakticheski sovpadaet s privedennoj v primere kal'kulyatora ($$3.13): name* table::look(const char* p, int ins) { int ii = 0; char* pp = p; while (*pp) ii = ii<<1 ^ *pp++; if (ii < 0) ii = -ii; ii %= size; for (name* n=tbl[ii]; n; n=n->next) if (strcmp(p,n->string) == 0) return n; name* nn = new name; nn->string = new char[strlen(p)+1]; strcpy(nn->string,p); nn->value = 1; nn->next = tbl[ii]; tbl[ii] = nn; return nn; } Ochevidno, chto funkcii-chleny klassa dolzhny peretranslirovat'sya vsyakij raz, kogda v opisanie klassa vnositsya kakoe-libo izmenenie. V ideale takoe izmenenie nikak ne dolzhno otrazhat'sya na pol'zovatelyah klassa. K sozhaleniyu, obychno byvaet ne tak. Dlya razmeshcheniya peremennoj, imeyushchej tip klassa, translyator dolzhen znat' razmer ob容kta klassa. Esli razmer ob容kta izmenitsya, nuzhno peretranslirovat' fajly, v kotoryh ispol'zovalsya klass. Mozhno napisat' sistemnuyu programmu (i ona dazhe uzhe napisana), kotoraya budet opredelyat' minimal'noe mnozhestvo fajlov, podlezhashchih peretranslyacii posle izmeneniya klassa. No takaya programma eshche ne poluchila shirokogo rasprostraneniya. Vozmozhen vopros: pochemu S++ byl sproektirovan takim obrazom, chto posle izmeneniya chastnoj chasti klassa trebuetsya peretranslyaciya programm pol'zovatelya? Pochemu voobshche chastnaya chast' klassa prisutstvuet v opisanii klassa? Inymi slovami, pochemu opisaniya chastnyh chlenov prisutstvuyut v zagolovochnyh fajlah, dostupnyh pol'zovatelyu, esli vse ravno nedostupny dlya nego v programme? Otvet odin - effektivnost'. Vo mnogih sistemah programmirovaniya process translyacii i posledovatel'nost' komand, proizvodyashchaya vyzov funkcii, budet proshche, esli razmer avtomaticheskih (t.e. razmeshchaemyh v steke) ob容ktov izvesten na stadii translyacii. Mozhno ne znat' opredeleniya vsego klassa, esli predstavlyat' kazhdyj ob容kt kak ukazatel' na "nastoyashchij" ob容kt. |to pozvolyaet reshit' zadachu, poskol'ku vse ukazateli budut imet' odinakovyj razmer, a razmeshchenie nastoyashchih ob容ktov budet provodit'sya tol'ko v odnom fajle, v kotorom dostupny chastnye chasti klassov. Odnako, takoe reshenie privodit k dopolnitel'nomu rashodu pamyati na kazhdyj ob容kt i dopolnitel'nomu obrashcheniyu k pamyati pri kazhdom ispol'zovanii chlena. Eshche huzhe, chto kazhdyj vyzov funkcii s avtomaticheskim ob容ktom klassa trebuet vyzovov funkcij vydeleniya i osvobozhdeniya pamyati. K tomu zhe stanovitsya nevozmozhnoj realizaciya podstanovkoj funkcij-chlenov, rabotayushchih s chastnymi chlenami klassa. Nakonec, takoe izmenenie sdelaet nevozmozhnym svyazyvanie programm na S++ i na S, poskol'ku translyator S budet po drugomu obrabatyvat' struktury (struct). Poetomu takoe reshenie bylo sochteno nepriemlemym dlya S++. S drugoj storony, S++ predostavlyaet sredstvo dlya sozdaniya abstraktnyh tipov, v kotoryh svyaz' mezhdu interfejsom pol'zovatelya i realizaciej dovol'no slabaya. V glave 6 vvodyatsya proizvodnye klassy i opisyvayutsya abstraktnye bazovye klassy, a v $$13.3 poyasnyaetsya, kak s pomoshch'yu etih sredstv realizovat' abstraktnye tipy. Cel' etogo - dat' vozmozhnost' opredelyat' pol'zovatel'skie tipy stol' zhe effektivnye i konkretnye, kak i standartnye, i dat' osnovnye sredstva opredeleniya bolee gibkih variantov tipov, kotorye mogut okazat'sya i ne stol' effektivnymi. 5.3.2 Zakonchennyj primer klassa Programmirovanie bez upryatyvaniya dannyh (v raschete na struktury) trebuet men'shego predvaritel'nogo obdumyvaniya zadachi, chem programmirovanie s upryatyvaniem dannyh (v raschete na klassy). Strukturu mozhno opredelit' ne ochen' zadumyvayas' o tom, kak ee budut ispol'zovat'. Kogda opredelyaetsya klass, vnimanie koncentriruetsya na tom, chtoby obespechit' dlya novogo tipa polnyj nabor operacij. |to vazhnoe smeshchenie akcenta v proektirovanii programm. Obychno vremya, zatrachennoe na razrabotku novogo tipa, mnogokratno okupaetsya v processe otladki i razvitiya programmy. Vot primer zakonchennogo opredeleniya tipa intset, predstavlyayushchego ponyatie "mnozhestvo celyh": class intset { int cursize, maxsize; int *x; public: intset(int m, int n); // ne bolee m celyh iz 1..n ~intset(); int member(int t) const; // yavlyaetsya li t chlenom? void insert(int t); // dobavit' k mnozhestvu t void start(int& i) const { i = 0; } void ok(int& i) const { return i<cursize; } void next(int& i) const { return x[i++]; } }; Dlya proverki etogo klassa vnachale sozdadim, a zatem raspechataem mnozhestvo sluchajnyh celyh chisel. |to prostoe mnozhestvo celyh mozhno ispol'zovat' dlya proverki, est' li povtoreniya v ih posledovatel'nosti. No dlya bol'shinstva zadach nuzhen, konechno, bolee razvityj tip mnozhestva. Kak vsegda vozmozhny oshibki, poetomu nuzhna funkciya: #include <iostream.h> void error(const char *s) { cerr << "set: " << s << '\n'; exit(1); } Klass intset ispol'zuetsya v funkcii main(), dlya kotoroj dolzhno byt' zadano dva parametra: pervyj opredelyaet chislo sozdavaemyh sluchajnyh chisel, a vtoroj - diapazon ih znachenij: int main(int argc, char* argv[]) { if (argc != 3) error("nuzhno zadavat' dva parametra"); int count = 0; int m = atoi(argv[1]); // chislo elementov mnozhestva int n = atoi(argv[2]); // iz diapazona 1..n intset s(m,n); while (count<m) { int t = randint(n); if (s.member(t)==0) { s.insert(t); count++; } } print_in_order(&s); } Znachenie schetchika parametrov programmy argc ravno 3, hotya programma imeet tol'ko dva parametra. Delo v tom, chto v argv[0] vsegda peredaetsya dopolnitel'nyj parametr, soderzhashchij imya programmy. Funkciya extern "C" int atoi(const char*) yavlyaetsya standartnoj bibliotechnoj funkciej, preobrazuyushchej celoe iz strokovogo predstavleniya vo vnutrennyuyu dvoichnuyu formu. Kak obychno, esli vy ne hotite imet' takoe opisanie v svoej programme, to vam nado vklyuchit' v nee sootvetstvuyushchij zagolovochnyj fajl, soderzhashchij opisaniya standartnyh bibliotechnyh funkcij. Sluchajnye chisla generiruyutsya s pomoshch'yu standartnoj funkcii rand: extern "C" int rand(); // bud'te ostorozhny: // chisla ne sovsem sluchajnye int randint(int u) // diapazon 1..u { int r = rand(); if (r < 0) r = -r; return 1 + r%u; } Podrobnosti realizacii klassa malo interesny dlya pol'zovatelya, no v lyubom sluchae budut ispol'zovat'sya funkcii-chleny. Konstruktor razmeshchaet massiv celyh s razmerom, ravnym zadannomu maksimal'nomu razmeru mnozhestva, a destruktor udalyaet etot massiv: intset::intset(int m, int n) // ne bolee m celyh v 1..n { if (m<1 || n<m) error("nedopustimyj razmer intset"); cursize = 0; maxsize = m; x = new int[maxsize]; } intset::~intset() { delete x; } Celye dobavlyayutsya takim obrazom, chto oni hranyatsya vo mnozhestve v vozrastayushchem poryadke: void intset::insert(int t) { if (++cursize > maxsize) error("slishkom mnogo elementov"); int i = cursize-1; x[i] = t; while (i>0 && x[i-1]>x[i]) { int t = x[i]; // pomenyat' mestami x[i] i x[i-1] x[i] = x[i-1]; x[i-1] = t; i--; } } CHtoby najti element, ispol'zuetsya prostoj dvoichnyj poisk: int intset::member(int t) const // dvoichnyj poisk { int l = 0; int u = cursize-1; while (l <= u) { int m = (l+u)/2; if (t < x[m]) u = m-1; else if (t > x[m]) l = m+1; else return 1; // najden } return 0; // ne najden } Nakonec, nuzhno predostavit' pol'zovatelyu nabor operacij, s pomoshch'yu kotoryh on mog by organizovat' iteraciyu po mnozhestvu v nekotorom poryadke (ved' poryadok, ispol'zuemyj v predstavlenii intset, ot nego skryt). Mnozhestvo po svoej suti ne yavlyaetsya vnutrenne uporyadochennym, i nel'zya pozvolit' prosto vybirat' elementy massiva (a vdrug zavtra intset budet realizovano v vide svyazannogo spiska?). Pol'zovatel' poluchaet tri funkcii: start() - dlya inicializacii iteracii, ok() - dlya proverki, est' li sleduyushchij element, i next() - dlya polucheniya sleduyushchego elementa: class intset { // ... void start(int& i) const { i = 0; } int ok(int& i) const { return i<cursize; } int next(int& i) const { return x[i++]; } }; CHtoby obespechit' sovmestnuyu rabotu etih treh operacij, nado zapominat' tot element, na kotorom ostanovilas' iteraciya. Dlya etogo pol'zovatel' dolzhen zadavat' celyj parametr. Poskol'ku nashe predstavlenie mnozhestva uporyadochennoe, realizaciya etih operacij trivial'na. Teper' mozhno opredelit' funkciyu print_in_order: void print_in_order(intset* set) { int var; set->sart(var); while (set->ok(var)) cout << set->next(var) << '\n'; } Drugoj sposob postroeniya iteratora po mnozhestvu priveden v $$7.8. 5.4 Eshche o klassah V etom razdele opisany dopolnitel'nye svojstva klassa. Opisan sposob obespechit' dostup k chastnym chlenam v funkciyah, ne yavlyayushchihsya chlenami ($$5.4.1). Opisano, kak razreshit' kollizii imen chlenov ($$5.4.2) i kak sdelat' opisaniya klassov vlozhennymi ($$5.4.3), no pri etom izbezhat' nezhelatel'noj vlozhennosti ($$5.4.4). Vvoditsya ponyatie staticheskih chlenov (static), kotorye ispol'zuyutsya dlya predstavleniya operacij i dannyh, otnosyashchihsya k samomu klassu, a ne k otdel'nym ego ob容ktam ($$5.4.5). Razdel zavershaetsya primerom, pokazyvayushchim, kak mozhno postroit' diskriminiruyushchee (nadezhnoe) ob容dinenie ($$5.4.6). 5.4.1 Druz'ya Pust' opredeleny dva klassa: vector (vektor) i matrix (matrica). Kazhdyj iz nih skryvaet svoe predstavlenie, no daet polnyj nabor operacij dlya raboty s ob容ktami ego tipa. Dopustim, nado opredelit' funkciyu, umnozhayushchuyu matricu na vektor. Dlya prostoty predpolozhim, chto vektor imeet chetyre elementa s indeksami ot 0 do 3, a v matrice chetyre vektora tozhe s indeksami ot 0 do 3. Dostup k elementam vektora obespechivaetsya funkciej elem(), i analogichnaya funkciya est' dlya matricy. Mozhno opredelit' global'nuyu funkciyu multiply (umnozhit') sleduyushchim obrazom: vector multiply(const matrix& m, const vector& v); { vector r; for (int i = 0; i<3; i++) { // r[i] = m[i] * v; r.elem(i) = 0; for (int j = 0; j<3; j++) r.elem(i) +=m.elem(i,j) * v.elem(j); } return r; } |to vpolne estestvennoe reshenie, no ono mozhet okazat'sya ochen' neeffektivnym. Pri kazhdom vyzove multiply() funkciya elem() budet vyzyvat'sya 4*(1+4*3) raz. Esli v elem() provoditsya nastoyashchij kontrol' granic massiva, to na takoj kontrol' budet potracheno znachitel'no bol'she vremeni, chem na vypolnenie samoj funkcii, i v rezul'tate ona okazhetsya neprigodnoj dlya pol'zovatelej. S drugoj storony, esli elem() est' nekij special'nyj variant dostupa bez kontrolya, to tem samym my zasoryaem interfejs s vektorom i matricej osoboj funkciej dostupa, kotoraya nuzhna tol'ko dlya obhoda kontrolya. Esli mozhno bylo by sdelat' multiply chlenom oboih klassov vector i matrix, my mogli by obojtis' bez kontrolya indeksa pri obrashchenii k elementu matricy, no v to zhe vremya ne vvodit' special'noj funkcii elem(). Odnako, funkciya ne mozhet byt' chlenom dvuh klassov. Nado imet' v yazyke vozmozhnost' predostavlyat' funkcii, ne yavlyayushchejsya chlenom, pravo dostupa k chastnym chlenam klassa. Funkciya - ne chlen klassa, - imeyushchaya dostup k ego zakrytoj chasti, nazyvaetsya drugom etogo klassa. Funkciya mozhet stat' drugom klassa, esli v ego opisanii ona opisana kak friend (drug). Naprimer: class matrix; class vector { float v[4]; // ... friend vector multiply(const matrix&, const vector&); }; class matrix { vector v[4]; // ... friend vector multiply(const matrix&, const vector&); }; Funkciya-drug ne imeet nikakih osobennostej, za isklyucheniem prava dostupa k zakrytoj chasti klassa. V chastnosti, v takoj funkcii nel'zya ispol'zovat' ukazatel' this, esli tol'ko ona dejstvitel'no ne yavlyaetsya chlenom klassa. Opisanie friend yavlyaetsya nastoyashchim opisaniem. Ono vvodit imya funkcii v oblast' vidimosti klassa, v kotorom ona byla opisana, i pri etom proishodyat obychnye proverki na nalichie drugih opisanij takogo zhe imeni v etoj oblasti vidimosti. Opisanie friend mozhet nahoditsya kak v obshchej, tak i v chastnoj chastyah klassa, eto ne imeet znacheniya. Teper' mozhno napisat' funkciyu multiply, ispol'zuya elementy vektora i matricy neposredstvenno: vector multiply(const matrix& m, const vector& v) { vector r; for (int i = 0; i<3; i++) { // r[i] = m[i] * v; r.v[i] = 0; for ( int j = 0; j<3; j++) r.v[i] +=m.v[i][j] * v.v[j]; } return r; } Otmetim, chto podobno funkcii-chlenu druzhestvennaya funkciya yavno opisyvaetsya v opisanii klassa, s kotorym druzhit. Poetomu ona yavlyaetsya neot容mlemoj chast'yu interfejsa klassa naravne s funkciej-chlenom. Funkciya-chlen odnogo klassa mozhet byt' drugom drugogo klassa: class x { // ... void f(); }; class y { // ... friend void x::f(); }; Vpolne vozmozhno, chto vse funkcii odnogo klassa yavlyayutsya druz'yami drugogo klassa. Dlya etogo est' kratkaya forma zapisi: class x { friend class y; // ... }; V rezul'tate takogo opisaniya vse funkcii-chleny y stanovyatsya druz'yami klassa x. 5.4.2 Utochnenie imeni chlena Inogda polezno delat' yavnoe razlichie mezhdu imenami chlenov klassov i prochimi imenami. Dlya etogo ispol'zuetsya operaciya :: (razresheniya oblasti vidimosti): class X { int m; public: int readm() const { return m; } void setm(int m) { X::m = m; } }; V funkcii X::setm() parametr m skryvaet chlen m, poetomu k chlenu mozhno obrashchat'sya, tol'ko ispol'zuya utochnennoe imya X::m. Pravyj operand operacii :: dolzhen byt' imenem klassa. Nachinayushcheesya s :: imya dolzhno byt' global'nym imenem. |to osobenno polezno pri ispol'zovanii takih rasprostranennyh imen kak read, put, open, kotorymi mozhno oboznachat' funkcii-chleny, ne teryaya vozmozhnosti oboznachat' imi zhe funkcii, ne yavlyayushchiesya chlenami. Naprimer: class my_file {