assa imeet dostup k chastnym chlenam svoego bazovogo klassa. Togda samo ponyatie chastnogo (zakrytogo) chlena teryaet vsyakij smysl, poskol'ku dlya dostupa k nemu dostatochno prosto opredelit' proizvodnyj klass. Teper' uzhe budet nedostatochno dlya vyyasneniya, kto ispol'zuet chastnye chleny klassa, prosmotret' vse funkcii-chleny i druzej etogo klassa. Pridetsya prosmotret' vse ishodnye fajly programmy, najti proizvodnye klassy, zatem issledovat' kazhduyu funkciyu etih klassov. Dalee nado snova iskat' proizvodnye klassy ot uzhe najdennyh i t.d. |to, po krajnej mere, utomitel'no, a skoree vsego nereal'no. Nuzhno vsyudu, gde eto vozmozhno, ispol'zovat' vmesto chastnyh chlenov zashchishchennye (sm. $$6.6.1). Kak pravilo, samoe nadezhnoe reshenie dlya proizvodnogo klassa - ispol'zovat' tol'ko obshchie chleny svoego bazovogo klassa: void manager::print() const { employee::print(); // pechat' dannyh o sluzhashchih // pechat' dannyh ob upravlyayushchih } Otmetim, chto operaciya :: neobhodima, poskol'ku funkciya print() pereopredelena v klasse manager. Takoe povtornoe ispol'zovanie imen tipichno dlya S++. Neostorozhnyj programmist napisal by: void manager::print() const { print(); // pechat' dannyh o sluzhashchih // pechat' dannyh ob upravlyayushchih } V rezul'tate on poluchil by rekursivnuyu posledovatel'nost' vyzovov manager::print(). 6.2.2 Konstruktory i destruktory Dlya nekotoryh proizvodnyh klassov nuzhny konstruktory. Esli konstruktor est' v bazovom klasse, to imenno on i dolzhen vyzyvat'sya s ukazaniem parametrov, esli takovye u nego est': class employee { // ... public: // ... employee(char* n, int d); }; class manager : public employee { // ... public: // ... manager(char* n, int i, int d); }; Parametry dlya konstruktora bazovogo klassa zadayutsya v opredelenii konstruktora proizvodnogo klassa. V etom smysle bazovyj klass vystupaet kak klass, yavlyayushchijsya chlenom proizvodnogo klassa: manager::manager(char* n, int l, int d) : employee(n,d), level(l), group(0) { } Konstruktor bazovogo klassa employee::employee() mozhet imet' takoe opredelenie: employee::employee(char* n, int d) : name(n), department(d) { next = list; list = this; } Zdes' list dolzhen byt' opisan kak staticheskij chlen employee. Ob容kty klassov sozdayutsya snizu vverh: vnachale bazovye, zatem chleny i, nakonec, sami proizvodnye klassy. Unichtozhayutsya oni v obratnom poryadke: snachala sami proizvodnye klassy, zatem chleny, a zatem bazovye. CHleny i bazovye sozdayutsya v poryadke opisaniya ih v klasse, a unichtozhayutsya oni v obratnom poryadke. 6.2.3 Ierarhiya klassov Proizvodnyj klass sam v svoyu ochered' mozhet byt' bazovym klassom: class employee { /* ... */ }; class manager : public employee { /* ... */ }; class director : public manager { /* ... */ }; Takoe mnozhestvo svyazannyh mezhdu soboj klassov obychno nazyvayut ierarhiej klassov. Obychno ona predstavlyaetsya derevom, no byvayut ierarhii s bolee obshchej strukturoj v vide grafa: class temporary { /* ... */ }; class secretary : public employee { /* ... */ }; class tsec : public temporary, public secretary { /* ... */ }; class consultant : public temporary, public manager { /* ... */ }; Vidim, chto klassy v S++ mogut obrazovyvat' napravlennyj aciklichnyj graf (podrobnee ob etom govoritsya v $$6.5.3). |tot graf dlya privedennyh klassov imeet vid: 6.2.4 Polya tipa CHtoby proizvodnye klassy byli ne prosto udobnoj formoj kratkogo opisaniya, v realizacii yazyka dolzhen byt' reshen vopros: k kakomu iz proizvodnyh klassov otnositsya ob容kt, na kotoryj smotrit ukazatel' base*? Sushchestvuet tri osnovnyh sposoba otveta: [1] Obespechit', chtoby ukazatel' mog ssylat'sya na ob容kty tol'ko odnogo tipa ($$6.4.2); [2] Pomestit' v bazovyj klass pole tipa, kotoroe smogut proveryat' funkcii; [3] ispol'zovat' virtual'nye funkcii ($$6.2.5). Ukazateli na bazovye klassy obyknovenno ispol'zuyutsya pri proektirovanii kontejnernyh klassov (mnozhestvo, vektor, spisok i t.d.). Togda v sluchae [1] my poluchim odnorodnye spiski, t.e. spiski ob容ktov odnogo tipa. Sposoby [2] i [3] pozvolyayut sozdavat' raznorodnye spiski, t.e. spiski ob容ktov neskol'kih razlichnyh tipov (na samom dele, spiski ukazatelej na eti ob容kty). Sposob [3] - eto special'nyj nadezhnyj v smysle tipa variant sposoba [2]. Osobenno interesnye i moshchnye varianty dayut kombinacii sposobov [1] i [3]; oni obsuzhdayutsya v glave 8. Vnachale obsudim prostoj sposob s polem tipa, t.e. sposob [2]. Primer s klassami manager/employee mozhno pereopredelit' tak: struct employee { enum empl_type { M, E }; empl_type type; employee* next; char* name; short department; // ... }; struct manager : employee { employee* group; short level; // ... }; Imeya eti opredeleniya, mozhno napisat' funkciyu, pechatayushchuyu dannye o proizvol'nom sluzhashchem: void print_employee(const employee* e) { switch (e->type) { case E: cout << e->name << '\t' << e->department << '\n'; // ... break; case M: cout << e->name << '\t' << e->department << '\n'; // ... manager* p = (manager*) e; cout << "level" << p->level << '\n'; // ... break; } } Napechatat' spisok sluzhashchih mozhno tak: void f(const employee* elist) { for (; elist; elist=elist->next) print_employee(elist); } |to vpolne horoshee reshenie, osobenno dlya nebol'shih programm, napisannyh odnim chelovekom, no ono imeet sushchestvennyj nedostatok: translyator ne mozhet proverit', naskol'ko pravil'no programmist obrashchaetsya s tipami. V bol'shih programmah eto privodit k oshibkam dvuh vidov. Pervyj - kogda programmist zabyvaet proverit' pole tipa. Vtoroj - kogda v pereklyuchatele ukazyvayutsya ne vse vozmozhnye znacheniya polya tipa. |tih oshibok dostatochno legko izbezhat' v processe napisaniya programmy, no sovsem nelegko izbezhat' ih pri vnesenii izmenenij v netrivial'nuyu programmu, a osobenno, esli eto bol'shaya programma, napisannaya kem-to drugim. Eshche trudnee izbezhat' takih oshibok potomu, chto funkcii tipa print() chasto pishutsya tak, chtoby mozhno bylo vospol'zovat'sya obshchnost'yu klassov: void print(const employee* e) { cout << e->name << '\t' << e->department << '\n'; // ... if (e->type == M) { manager* p = (manager*) e; cout << "level" << p->level << '\n'; // ... } } Operatory if, podobnye privedennym v primere, slozhno najti v bol'shoj funkcii, rabotayushchej so mnogimi proizvodnymi klassami. No dazhe kogda oni najdeny, nelegko ponyat', chto proishodit na samom dele. Krome togo, pri vsyakom dobavlenii novogo vida sluzhashchih trebuyutsya izmeneniya vo vseh vazhnyh funkciyah programmy, t.e. funkciyah, proveryayushchih pole tipa. V rezul'tate prihoditsya pravit' vazhnye chasti programmy, uvelichivaya tem samym vremya na otladku etih chastej. Inymi slovami, ispol'zovanie polya tipa chrevato oshibkami i trudnostyami pri soprovozhdenii programmy. Trudnosti rezko vozrastayut po mere rosta programmy, ved' ispol'zovanie polya tipa protivorechit principam modul'nosti i upryatyvaniya dannyh. Kazhdaya funkciya, rabotayushchaya s polem tipa, dolzhna znat' predstavlenie i specifiku realizacii vsyakogo klassa, yavlyayushchegosya proizvodnym dlya klassa, soderzhashchego pole tipa. 6.2.5 Virtual'nye funkcii S pomoshch'yu virtual'nyh funkcij mozhno preodolet' trudnosti, voznikayushchie pri ispol'zovanii polya tipa. V bazovom klasse opisyvayutsya funkcii, kotorye mogut pereopredelyat'sya v lyubom proizvodnom klasse. Translyator i zagruzchik obespechat pravil'noe sootvetstvie mezhdu ob容ktami i primenyaemymi k nim funkciyami: class employee { char* name; short department; // ... employee* next; static employee* list; public: employee(char* n, int d); // ... static void print_list(); virtual void print() const; }; Sluzhebnoe slovo virtual (virtual'naya) pokazyvaet, chto funkciya print() mozhet imet' raznye versii v raznyh proizvodnyh klassah, a vybor nuzhnoj versii pri vyzove print() - eto zadacha translyatora. Tip funkcii ukazyvaetsya v bazovom klasse i ne mozhet byt' pereopredelen v proizvodnom klasse. Opredelenie virtual'noj funkcii dolzhno davat'sya dlya togo klassa, v kotorom ona byla vpervye opisana (esli tol'ko ona ne yavlyaetsya chisto virtual'noj funkciej, sm. $$6.3). Naprimer: void employee::print() const { cout << name << '\t' << department << '\n'; // ... } My vidim, chto virtual'nuyu funkciyu mozhno ispol'zovat', dazhe esli net proizvodnyh klassov ot ee klassa. V proizvodnom zhe klasse ne obyazatel'no pereopredelyat' virtual'nuyu funkciyu, esli ona tam ne nuzhna. Pri postroenii proizvodnogo klassa nado opredelyat' tol'ko te funkcii, kotorye v nem dejstvitel'no nuzhny: class manager : public employee { employee* group; short level; // ... public: manager(char* n, int d); // ... void print() const; }; Mesto funkcii print_employee() zanyali funkcii-chleny print(), i ona stala ne nuzhna. Spisok sluzhashchih stroit konstruktor employee ($$6.2.2). Napechatat' ego mozhno tak: void employee::print_list() { for ( employee* p = list; p; p=p->next) p->print(); } Dannye o kazhdom sluzhashchem budut pechatat'sya v sootvetstvii s tipom zapisi o nem. Poetomu programma int main() { employee e("J.Brown",1234); manager m("J.Smith",2,1234); employee::print_list(); } napechataet J.Smith 1234 level 2 J.Brown 1234 Obratite vnimanie, chto funkciya pechati budet rabotat' dazhe v tom sluchae, esli funkciya employee_list() byla napisana i ottranslirovana eshche do togo, kak byl zaduman konkretnyj proizvodnyj klass manager! Ochevidno, chto dlya pravil'noj raboty virtual'noj funkcii nuzhno v kazhdom ob容kte klassa employee hranit' nekotoruyu sluzhebnuyu informaciyu o tipe. Kak pravilo, realizacii v kachestve takoj informacii ispol'zuyut prosto ukazatel'. |tot ukazatel' hranitsya tol'ko dlya ob容ktov klassa s virtual'nymi funkciyami, no ne dlya ob容ktov vseh klassov, i dazhe dlya ne dlya vseh ob容ktov proizvodnyh klassov. Dopolnitel'naya pamyat' otvoditsya tol'ko dlya klassov, v kotoryh opisany virtual'nye funkcii. Zametim, chto pri ispol'zovanii polya tipa, dlya nego vse ravno nuzhna dopolnitel'naya pamyat'. Esli v vyzove funkcii yavno ukazana operaciya razresheniya oblasti vidimosti ::, naprimer, v vyzove manager::print(), to mehanizm vyzova virtual'noj funkcii ne dejstvuet. Inache podobnyj vyzov privel by k beskonechnoj rekursii. Utochnenie imeni funkcii daet eshche odin polozhitel'nyj effekt: esli virtual'naya funkciya yavlyaetsya podstanovkoj (v etom net nichego neobychnogo), to v vyzove s operaciej :: proishodit podstanovka tela funkcii. |to effektivnyj sposob vyzova, kotoryj mozhno primenyat' v vazhnyh sluchayah, kogda odna virtual'naya funkciya obrashchaetsya k drugoj s odnim i tem zhe ob容ktom. Primer takogo sluchaya - vyzov funkcii manager::print(). Poskol'ku tip ob容kta yavno zadaetsya v samom vyzove manager::print(), net nuzhdy opredelyat' ego v dinamike dlya funkcii employee::print(), kotoraya i budet vyzyvat'sya. 6.3 Abstraktnye klassy Mnogie klassy shodny s klassom employee tem, chto v nih mozhno dat' razumnoe opredelenie virtual'nym funkciyam. Odnako, est' i drugie klassy. Nekotorye, naprimer, klass shape, predstavlyayut abstraktnoe ponyatie (figura), dlya kotorogo nel'zya sozdat' ob容kty. Klass shape priobretaet smysl tol'ko kak bazovyj klass v nekotorom proizvodnom klasse. Prichinoj yavlyaetsya to, chto nevozmozhno dat' osmyslennoe opredelenie virtual'nyh funkcij klassa shape: class shape { // ... public: virtual void rotate(int) { error("shape::rotate"); } virtual void draw() { error("shape::draw"): } // nel'zya ni vrashchat', ni risovat' abstraktnuyu figuru // ... }; Sozdanie ob容kta tipa shape (abstraktnoj figury) zakonnaya, hotya sovershenno bessmyslennaya operaciya: shape s; // bessmyslica: ``figura voobshche'' Ona bessmyslenna potomu, chto lyubaya operaciya s ob容ktom s privedet k oshibke. Luchshe virtual'nye funkcii klassa shape opisat' kak chisto virtual'nye. Sdelat' virtual'nuyu funkciyu chisto virtual'noj mozhno, dobaviv inicializator = 0: class shape { // ... public: virtual void rotate(int) = 0; // chisto virtual'naya funkciya virtual void draw() = 0; // chisto virtual'naya funkciya }; Klass, v kotorom est' virtual'nye funkcii, nazyvaetsya abstraktnym. Ob容kty takogo klassa sozdat' nel'zya: shape s; // oshibka: peremennaya abstraktnogo klassa shape Abstraktnyj klass mozhno ispol'zovat' tol'ko v kachestve bazovogo dlya drugogo klassa: class circle : public shape { int radius; public: void rotate(int) { } // normal'no: // pereopredelenie shape::rotate void draw(); // normal'no: // pereopredelenie shape::draw circle(point p, int r); }; Esli chisto virtual'naya funkciya ne opredelyaetsya v proizvodnom klasse, to ona i ostaetsya takovoj, a znachit proizvodnyj klass tozhe yavlyaetsya abstraktnym. Pri takom podhode mozhno realizovyvat' klassy poetapno: class X { public: virtual void f() = 0; virtual void g() = 0; }; X b; // oshibka: opisanie ob容kta abstraktnogo klassa X class Y : public X { void f(); // pereopredelenie X::f }; Y b; // oshibka: opisanie ob容kta abstraktnogo klassa Y class Z : public Y { void g(); // pereopredelenie X::g }; Z c; // normal'no Abstraktnye klassy nuzhny dlya zadaniya interfejsa bez utochneniya kakih-libo konkretnyh detalej realizacii. Naprimer, v operacionnoj sisteme detali realizacii drajvera ustrojstva mozhno skryt' takim abstraktnym klassom: class character_device { public: virtual int open() = 0; virtual int close(const char*) = 0; virtual int read(const char*, int) =0; virtual int write(const char*, int) = 0; virtual int ioctl(int ...) = 0; // ... }; Nastoyashchie drajvery budut opredelyat'sya kak proizvodnye ot klassa character_device. Posle vvedeniya abstraktnogo klassa u nas est' vse osnovnye sredstva dlya togo, chtoby napisat' zakonchennuyu programmu. 6.4 Primer zakonchennoj programmy Rassmotrim programmu risovaniya geometricheskih figur na ekrane. Ona estestvennym obrazom raspadaetsya na tri chasti: [1] monitor ekrana: nabor funkcij i struktur dannyh nizkogo urovnya dlya raboty s ekranom; operiruet tol'ko takimi ponyatiyami, kak tochki, linii; [2] biblioteka figur: mnozhestvo opredelenij figur obshchego vida (naprimer, pryamougol'nik, okruzhnost') i standartnye funkcii dlya raboty s nimi; [3] prikladnaya programma: konkretnye opredeleniya figur, otnosyashchihsya k zadache, i rabotayushchie s nimi funkcii. Kak pravilo, eti tri chasti programmiruyutsya raznymi lyud'mi v raznyh organizaciyah i v raznoe vremya, prichem oni obychno sozdayutsya v perechislennom poryadke. Pri etom estestvenno voznikayut zatrudneniya, poskol'ku, naprimer, u razrabotchika monitora net tochnogo predstavleniya o tom, dlya kakih zadach v konechnom schete on budet ispol'zovat'sya. Nash primer budet otrazhat' etot fakt. CHtoby primer imel dopustimyj razmer, biblioteka figur ves'ma ogranichena, a prikladnaya programma trivial'na. Ispol'zuetsya sovershenno primitivnoe predstavlenie ekrana, chtoby dazhe chitatel', na mashine kotorogo net graficheskih sredstv, sumel porabotat' s etoj programmoj. Mozhno legko zamenit' monitor ekrana na bolee razvituyu programmu, ne izmenyaya pri etom biblioteku figur ili prikladnuyu programmu. 6.4.1 Monitor ekrana Vnachale bylo zhelanie napisat' monitor ekrana na S, chtoby eshche bol'she podcherknut' razdelenie mezhdu urovnyami realizacii. No eto okazalos' utomitel'nym, i poetomu vybrano kompromissnoe reshenie: stil' programmirovaniya, prinyatyj v S (net funkcij-chlenov, virtual'nyh funkcij, pol'zovatel'skih operacij i t.d.), no ispol'zuyutsya konstruktory, parametry funkcij polnost'yu opisyvayutsya i proveryayutsya i t.d. |tot monitor ochen' napominaet programmu na S, kotoruyu modificirovali, chtoby vospol'zovat'sya vozmozhnostyami S++, no polnost'yu peredelyvat' ne stali. |kran predstavlen kak dvumernyj massiv simvolov i upravlyaetsya funkciyami put_point() i put_line(). V nih dlya svyazi s ekranom ispol'zuetsya struktura point: // fajl screen.h const int XMAX=40; const int YMAX=24; struct point { int x, y; point() { } point(int a,int b) { x=; y=b; } }; extern void put_point(int a, int b); inline void put_point(point p) { put_point(p.x,p.y); } extern void put_line(int, int, int, int); extern void put_line(point a, point b) { put_line(a.x,a.y,b.x,b.y); } extern void screen_init(); extern void screen_destroy(); extern void screen_refresh(); extern void screen_clear(); #include <iostream.h> Do vyzova funkcij, vydayushchih izobrazhenie na ekran (put_...), neobhodimo obratit'sya k funkcii inicializacii ekrana screen_init(). Izmeneniya v strukture dannyh, opisyvayushchej ekran, stanut vidimy na nem tol'ko posle vyzova funkcii obnovleniya ekrana screen_refresh(). CHitatel' mozhet ubedit'sya, chto obnovlenie ekrana proishodit prosto s pomoshch'yu kopirovaniya novyh znachenij v massiv, predstavlyayushchij ekran. Privedem funkcii i opredeleniya dannyh dlya upravleniya ekranom: #include "screen.h" #include <stream.h> enum color { black='*', white=' ' }; char screen[XMAX] [YMAX]; void screen_init() { for (int y=0; y<YMAX; y++) for (int x=0; x<XMAX; x++) screen[x] [y] = white; } Funkciya void screen_destroy() { } privedena prosto dlya polnoty kartiny. V real'nyh sistemah obychno nuzhny podobnye funkcii unichtozheniya ob容kta. Tochki zapisyvayutsya, tol'ko esli oni popadayut na ekran: inline int on_screen(int a, int b) // proverka popadaniya { return 0<=a && a <XMAX && 0<=b && b<YMAX; } void put_point(int a, int b) { if (on_screen(a,b)) screen[a] [b] = black; } Dlya risovaniya pryamyh linij ispol'zuetsya funkciya put_line(): void put_line(int x0, int y0, int x1, int y1) /* Narisovat' otrezok pryamoj (x0,y0) - (x1,y1). Uravnenie pryamoj: b(x-x0) + a(y-y0) = 0. Minimiziruetsya velichina abs(eps), gde eps = 2*(b(x-x0)) + a(y-y0). Sm. Newman, Sproull ``Principles of interactive Computer Graphics'' McGraw-Hill, New York, 1979. pp. 33-34. */ { register int dx = 1; int a = x1 - x0; if (a < 0) dx = -1, a = -a; register int dy = 1; int b = y1 - y0; if (b < 0) dy = -1, b = -b; int two_a = 2*a; int two_b = 2*b; int xcrit = -b + two_a; register int eps = 0; for (;;) { put_point(x0,y0); if (x0==x1 && y0==y1) break; if (eps <= xcrit) x0 +=dx, eps +=two_b; if (eps>=a || a<b) y0 +=dy, eps -=two_a; } } Imeyutsya funkcii dlya ochistki i obnovleniya ekrana: void screen_clear() { screen_init(); } void screen_refresh() { for (int y=YMAX-1; 0<=y; y--) { // s verhnej stroki do nizhnej for (int x=0; x<XMAX; x++) // ot levogo stolbca do pravogo cout << screen[x] [y]; cout << '\n'; } } No nuzhno ponimat', chto vse eti opredeleniya hranyatsya v nekotoroj biblioteke kak rezul'tat raboty translyatora, i izmenit' ih nel'zya. 6.4.2 Biblioteka figur Nachnem s opredeleniya obshchego ponyatiya figury. Opredelenie dolzhno byt' takim, chtoby im mozhno bylo vospol'zovat'sya (kak bazovym klassom shape) v raznyh klassah, predstavlyayushchih vse konkretnye figury (okruzhnosti, kvadraty i t.d.). Ono takzhe dolzhno pozvolyat' rabotat' so vsyakoj figuroj isklyuchitel'no s pomoshch'yu interfejsa, opredelyaemogo klassom shape: struct shape { static shape* list; shape* next; shape() { next = list; list = this; } virtual point north() const = 0; virtual point south() const = 0; virtual point east() const = 0; virtual point west() const = 0; virtual point neast() const = 0; virtual point seast() const = 0; virtual point nwest() const = 0; virtual point swest() const = 0; virtual void draw() = 0; virtual void move(int, int) = 0; }; Figury pomeshchayutsya na ekran funkciej draw(), a dvizhutsya po nemu s pomoshch'yu move(). Figury mozhno pomeshchat' otnositel'no drug druga, ispol'zuya ponyatie tochek kontakta. Dlya oboznacheniya tochek kontakta ispol'zuyutsya nazvaniya storon sveta v kompase: north - sever, ... , neast - severo-vostok, ... , swest - yugo-zapad. Klass kazhdoj konkretnoj figury sam opredelyaet smysl etih tochek i opredelyaet, kak risovat' figuru. Konstruktor shape::shape() dobavlyaet figuru k spisku figur shape::list. Dlya postroeniya etogo spiska ispol'zuetsya chlen next, vhodyashchij v kazhdyj ob容kt shape. Poskol'ku net smysla v ob容ktah tipa obshchej figury, klass shape opredelen kak abstraktnyj klass. Dlya zadaniya otrezka pryamoj nuzhno ukazat' dve tochki ili tochku i celoe. V poslednem sluchae otrezok budet gorizontal'nym, a celoe zadaet ego dlinu. Znak celogo pokazyvaet, gde dolzhna nahodit'sya zadannaya tochka otnositel'no konechnoj tochki, t.e. sleva ili sprava ot nee: class line : public shape { /* otrezok pryamoj ["w", "e" ] north() opredelyaet tochku - `` vyshe centra otrezka i tak daleko na sever, kak samaya ego severnaya tochka'' */ point w, e; public: point north() const { return point((w.x+e.x)/2,e.y<w.y?w.y:e:y); } point south() const { return point((w.x+e.x)/2,e.y<w.y?e.y:w.y); } point east() const; point west() const; point neast() const; point seast() const; point nwest() const; point swest() const; void move(int a, int b) { w.x +=a; w.y +=b; e.x +=a; e.y +=b; } void draw() { put_line(w,e); } line(point a, point b) { w = a; e = b; } line(point a, int l) { w = point(a.x+l-1,a.y); e = a; } }; Analogichno opredelyaetsya pryamougol'nik: class rectangle : public shape { /* nw ------ n ----- ne | | | | w c e | | | | sw ------ s ----- se */ point sw, ne; public: point north() const { return point((sw.x+ne.x)/2,ne.y); } point south() const { return point((sw.x+ne.x)/2,sw.y); } point east() const; point west() const; point neast() const { return ne; } point seast() const; point nwest() const; point swest() const { return sw; } void move(int a, int b) { sw.x+=a; sw.y+=b; ne.x+=a; ne.y+=b; } void draw(); rectangle(point,point); }; Pryamougol'nik stroitsya po dvum tochkam. Konstruktor uslozhnyaetsya, tak kak neobhodimo vyyasnyat' otnositel'noe polozhenie etih tochek: rectangle::rectangle(point a, point b) { if (a.x <= b.x) { if (a.y <= b.y) { sw = a; ne = b; } else { sw = point(a.x,b.y); ne = point(b.x,a.y); } } else { if (a.y <= b.y) { sw = point(b.x,a.y); ne = point(a.x,b.y); } else { sw = b; ne = a; } } } CHtoby narisovat' pryamougol'nik, nado narisovat' chetyre otrezka: void rectangle::draw() { point nw(sw.x,ne.y); point se(ne.x,sw.y); put_line(nw,ne); put_line(ne,se); put_line(se,sw); put_line(sw,nw); } V biblioteke figur est' opredeleniya figur i funkcii dlya raboty s nimi: void shape_refresh(); // narisovat' vse figury void stack(shape* p, const shape* q); // pomestit' p nad q Funkciya obnovleniya figur nuzhna, chtoby rabotat' s nashim primitivnym predstavleniem ekrana; ona prosto zanovo risuet vse figury. Otmetim, chto eta funkciya ne imeet ponyatiya, kakie figury ona risuet: void shape_refresh() { screen_clear(); for (shape* p = shape::list; p; p=p->next) p->draw(); screen_refresh(); } Nakonec, est' odna dejstvitel'no servisnaya funkciya, kotoraya risuet odnu figuru nad drugoj. Dlya etogo ona opredelyaet yug (south()) odnoj figury kak raz nad severom (north()) drugoj: void stack(shape* p, const shape* q) // pomestit' p nad q { point n = q->north(); point s = p->south(); p->move(n.x-s.x,n.y-s.y+1); } Predstavim teper', chto eta biblioteka yavlyaetsya sobstvennost'yu nekotoroj firmy, prodayushchej programmy, i, chto ona prodaet tol'ko zagolovochnyj fajl s opredeleniyami figur i ottranslirovannye opredeleniya funkcij. Vse ravno vy smozhete opredelit' novye figury, vospol'zovavshis' dlya etogo kuplennymi vami funkciyami. 6.4.3 Prikladnaya programma Prikladnaya programma predel'no prosta. Opredelyaetsya novaya figura myshape (esli ee narisovat', to ona napominaet lico), a zatem privoditsya funkciya main(), v kotoroj ona risuetsya so shlyapoj. Vnachale dadim opisanie figury myshape: #include "shape.h" class myshape : public rectangle { line* l_eye; // levyj glaz line* r_eye; // pravyj glaz line* mouth; // rot public: myshape(point, point); void draw(); void move(int, int); }; Glaza i rot yavlyayutsya otdel'nymi nezavisimymi ob容ktami kotorye sozdaet konstruktor klassa myshape: myshape::myshape(point a, point b) : rectangle(a,b) { int ll = neast().x-swest().x+1; int hh = neast().y-swest().y+1; l_eye = new line( point(swest().x+2,swest().y+hh*3/4),2); r_eye = new line( point(swest().x+ll-4,swest().y+hh*3/4),2); mouth = new line( point(swest().x+2,swest().y+hh/4),ll-4); } Ob容kty, predstavlyayushchie glaza i rot, vydayutsya funkciej shape_refresh() po otdel'nosti. V principe s nimi mozhno rabotat' nezavisimo ot ob容kta my_shape, k kotoromu oni prinadlezhat. |to odin iz sposobov zadaniya chert lica dlya stroyashchegosya ierarhicheski ob容kta myshape. Kak eto mozhno sdelat' inache, vidno iz zadaniya nosa. Nikakoj tip "nos" ne opredelyaetsya, on prosto dorisovyvaetsya v funkcii draw(): void myshape::draw() { rectangle::draw(); int a = (swest().x+neast().x)/2; int b = (swest().y+neast().y)/2; put_point(point(a,b)); } Dvizhenie figury myshape svoditsya k dvizheniyu ob容kta bazovogo klassa rectangle i k dvizheniyu vtorichnyh ob容ktov (l_eye, r_eye i mouth): void myshape::move(int a, int b) { rectangle::move(a,b); l_eye->move(a,b); r_eye->move(a,b); mouth->move(a,b); } Nakonec, opredelim neskol'ko figur i budem ih dvigat': int main() { screen_init(); shape* p1 = new rectangle(point(0,0),point(10,10)); shape* p2 = new line(point(0,15),17); shape* p3 = new myshape(point(15,10),point(27,18)); shape_refresh(); p3->move(-10,-10); stack(p2,p3); stack(p1,p2); shape_refresh(); screen_destroy(); return 0; } Vnov' obratim vnimanie na to, chto funkcii, podobnye shape_refresh() i stack(), rabotayut s ob容ktami, tipy kotoryh byli opredeleny zavedomo posle opredeleniya etih funkcij (i, veroyatno, posle ih translyacii). Vot poluchivsheesya lico so shlyapoj: *********** * * * * * * * * * * * * * * *********** ***************** *********** * * * ** ** * * * * * * * * * ******* * * * *********** Dlya uproshcheniya primera kopirovanie i udalenie figur ne obsuzhdalos'. 6.5 Mnozhestvennoe nasledovanie V $$1.5.3 i $$6.2.3 uzhe govorilos', chto u klassa mozhet byt' neskol'ko pryamyh bazovyh klassov. |to znachit, chto v opisanii klassa posle : mozhet byt' ukazano bolee odnogo klassa. Rassmotrim zadachu modelirovaniya, v kotoroj parallel'nye dejstviya predstavleny standartnoj bibliotekoj klassov task, a sbor i vydachu informacii obespechivaet bibliotechnyj klass displayed. Togda klass modeliruemyh ob容ktov (nazovem ego satellite) mozhno opredelit' tak: class satellite : public task, public displayed { // ... }; Takoe opredelenie obychno nazyvaetsya mnozhestvennym nasledovaniem. Obratno, sushchestvovanie tol'ko odnogo pryamogo bazovogo klassa nazyvaetsya edinstvennym nasledovaniem. Ko vsem opredelennym v klasse satellite operaciyam dobavlyaetsya ob容dinenie operacij klassov task i displayed: void f(satellite& s) { s.draw(); // displayed::draw() s.delay(10); // task::delay() s.xmit(); // satellite::xmit() } S drugoj storony, ob容kt tipa satellite mozhno peredavat' funkciyam s parametrom tipa task ili displayed: void highlight(displayed*); void suspend(task*); void g(satellite* p) { highlight(p); // highlight((displayed*)p) suspend(p); // suspend((task*)p); } Ochevidno, realizaciya etoj vozmozhnosti trebuet nekotorogo (prostogo) tryuka ot translyatora: nuzhno funkciyam s parametrami task i displayed peredat' raznye chasti ob容kta tipa satellite. Dlya virtual'nyh funkcij, estestvenno, vyzov i tak vypolnitsya pravil'no: class task { // ... virtual pending() = 0; }; class displayed { // ... virtual void draw() = 0; }; class satellite : public task, public displayed { // ... void pending(); void draw(); }; Zdes' funkcii satellite::draw() i satellite::pending() dlya ob容kta tipa satellite budut vyzyvat'sya tak zhe, kak esli by on byl ob容ktom tipa displayed ili task, sootvetstvenno. Otmetim, chto orientaciya tol'ko na edinstvennoe nasledovanie ogranichivaet vozmozhnosti realizacii klassov displayed, task i satellite. V takom sluchae klass satellite mog by byt' task ili displayed, no ne to i drugoe vmeste (esli, konechno, task ne yavlyaetsya proizvodnym ot displayed ili naoborot). V lyubom sluchae teryaetsya gibkost'. 6.5.1 Mnozhestvennoe vhozhdenie bazovogo klassa Vozmozhnost' imet' bolee odnogo bazovogo klassa vlechet za soboj vozmozhnost' neodnokratnogo vhozhdeniya klassa kak bazovogo. Dopustim, klassy task i displayed yavlyayutsya proizvodnymi klassa link, togda v satellite on budet vhodit' dvazhdy: class task : public link { // link ispol'zuetsya dlya svyazyvaniya vseh // zadach v spisok (spisok dispetchera) // ... }; class displayed : public link { // link ispol'zuetsya dlya svyazyvaniya vseh // izobrazhaemyh ob容ktov (spisok izobrazhenij) // ... }; No problem ne voznikaet. Dva razlichnyh ob容kta link ispol'zuyutsya dlya razlichnyh spiskov, i eti spiski ne konfliktuyut drug s drugom. Konechno, bez riska neodnoznachnosti nel'zya obrashchat'sya k chlenam klassa link, no kak eto sdelat' korrektno, pokazano v sleduyushchem razdele. Graficheski ob容kt satellite mozhno predstavit' tak: No mozhno privesti primery, kogda obshchij bazovyj klass ne dolzhen predstavlyat'sya dvumya razlichnymi ob容ktami (sm. $$6.5.3). 6.5.2 Razreshenie neodnoznachnosti Estestvenno, u dvuh bazovyh klassov mogut byt' funkcii-chleny s odinakovymi imenami: class task { // ... virtual debug_info* get_debug(); }; class displayed { // ... virtual debug_info* get_debug(); }; Pri ispol'zovanii klassa satellite podobnaya neodnoznachnost' funkcij dolzhna byt' razreshena: void f(satellite* sp) { debug_info* dip = sp->get_debug(); //oshibka: neodnoznachnost' dip = sp->task::get_debug(); // normal'no dip = sp->displayed::get_debug(); // normal'no } Odnako, yavnoe razreshenie neodnoznachnosti hlopotno, poetomu dlya ee ustraneniya luchshe vsego opredelit' novuyu funkciyu v proizvodnom klasse: class satellite : public task, public derived { // ... debug_info* get_debug() { debug_info* dip1 = task:get_debug(); debug_info* dip2 = displayed::get_debug(); return dip1->merge(dip2); } }; Tem samym lokalizuetsya informaciya iz bazovyh dlya satellite klassov. Poskol'ku satellite::get_debug() yavlyaetsya pereopredeleniem funkcij get_debug() iz oboih bazovyh klassov, garantiruetsya, chto imenno ona budet vyzyvat'sya pri vsyakom obrashchenii k get_debug() dlya ob容kta tipa satellite. Translyator vyyavlyaet kollizii imen, voznikayushchie pri opredelenii odnogo i togo zhe imeni v bolee, chem odnom bazovom klasse. Poetomu programmistu ne nado ukazyvat' kakoe imenno imya ispol'zuetsya, krome sluchaya, kogda ego ispol'zovanie dejstvitel'no neodnoznachno. Kak pravilo ispol'zovanie bazovyh klassov ne privodit k kollizii imen. V bol'shinstve sluchaev, dazhe esli imena sovpadayut, kolliziya ne voznikaet, poskol'ku imena ne ispol'zuyutsya neposredstvenno dlya ob容ktov proizvodnogo klassa. Analogichnaya problema, kogda v dvuh klassah est' funkcii s odnim imenem, no raznym naznacheniem, obsuzhdaetsya v $$13.8 na primere funkcii draw() dlya klassov Window i Cowboy. Esli neodnoznachnosti ne voznikaet, izlishne ukazyvat' imya bazovogo klassa pri yavnom obrashchenii k ego chlenu. V chastnosti, esli mnozhestvennoe nasledovanie ne ispol'zuetsya, vpolne dostatochno ispol'zovat' oboznachenie tipa "gde-to v bazovom klasse". |to pozvolyaet programmistu ne zapominat' imya pryamogo bazovogo klassa i spasaet ego ot oshibok (vprochem, redkih), voznikayushchih pri perestrojke ierarhii klassov. Naprimer, v funkcii iz $$6.2.5 void manager::print() { employee::print(); // ... } predpolagaetsya, chto employee - pryamoj bazovyj klass dlya manager. Rezul'tat etoj funkcii ne izmenitsya, esli employee okazhetsya kosvennym bazovym klassom dlya manager, a v pryamom bazovom klasse funkcii print() net. Odnako, kto-to mog by sleduyushchim obrazom perestroit' klassy: class employee { // ... virtual void print(); }; class foreman : public employee { // ... void print(); }; class manager : public foreman { // ... void print(); }; Teper' funkciya foreman::print() ne budet vyzyvat'sya, hotya pochti navernyaka predpolagalsya vyzov imenno etoj funkcii. S pomoshch'yu nebol'shoj hitrosti mozhno preodolet' etu trudnost': class foreman : public employee { typedef employee inherited; // ... void print(); }; class manager : public foreman { typedef foreman inherited; // ... void print(); }; void manager::print() { inherited::print(); // ... } Pravila oblastej vidimosti, v chastnosti te, kotorye otnosyatsya k vlozhennym tipam, garantiruyut, chto voznikshie neskol'ko tipov inherited ne budut konfliktovat' drug s drugom. V obshchem-to delo vkusa, schitat' reshenie s tipom inherited naglyadnym ili net. 6.5.3 Virtual'nye bazovye klassy V predydushchih razdelah mnozhestvennoe nasledovanie rassmatrivalos' kak sushchestvennyj faktor, pozvolyayushchij za schet sliyaniya klassov bezboleznenno integrirovat' nezavisimo sozdavavshiesya programmy. |to samoe osnovnoe primenenie mnozhestvennogo nasledovaniya, i, k schast'yu (no ne sluchajno), eto samyj prostoj i nadezhnyj sposob ego primeneniya. Inogda primenenie mnozhestvennogo nasledovaniya predpolagaet dostatochno tesnuyu svyaz' mezhdu klassami, kotorye rassmatrivayutsya kak "bratskie" bazovye klassy. Takie klassy-brat'ya obychno dolzhny proektirovat'sya sovmestno. V bol'shinstve sluchaev dlya etogo ne trebuetsya osobyj stil' programmirovaniya, sushchestvenno otlichayushchijsya ot togo, kotoryj my tol'ko chto rassmatrivali. Prosto na proizvodnyj klass vozlagaetsya nekotoraya dopolnitel'naya rabota. Obychno ona svoditsya k pereopredeleniyu odnoj ili neskol'kih virtual'nyh funkcij (sm. $$13.2 i $$8.7). V nekotoryh sluchayah klassy-brat'ya dolzhny imet' obshchuyu informaciyu. Poskol'ku S++ - yazyk so strogim kontrolem tipov, obshchnost' informacii vozmozhna tol'ko pri yavnom ukazanii togo, chto yavlyaetsya obshchim v etih klassah. Sposobom takogo ukazaniya mozhet sluzhit' virtual'nyj bazovyj klass. Virtual'nyj bazovyj klass mozhno ispol'zovat' dlya predstavleniya "golovnogo" klassa, kotoryj mozhet konkretizirovat'sya raznymi sposobami: class window { // golovnaya informaciya virtual void draw(); }; Dlya prostoty rassmotrim tol'ko odin vid obshchej informacii iz klassa window - funkciyu draw(). Mozhno opredelyat' raznye bolee razvitye klassy, predstavlyayushchie okna (window). V kazhdom opredelyaetsya svoya (bolee razvitaya) funkciya risovaniya (draw): class window_w_border : public virtual window { // klass "okno s ramkoj" // opredeleniya, svyazannye s ramkoj void draw(); }; class window_w_menu : public virtual window { // klass "okno s menyu" // opredeleniya, svyazannye s menyu void draw(); }; Teper' hotelos' by opredelit' okno s ramkoj i menyu: class window_w_border_and_menu : public virtual window, public window_w_border, public window_w_menu { // klass "okno s ramkoj i menyu" void draw(); }; Kazhdyj proizvodnyj klass dobavlyaet novye svojstva okna. CHtoby vospol'zovat'sya kombinaciej vseh etih svojstv, my dolzhny garantirovat', chto odin i tot zhe ob容kt klassa window ispol'zuetsya dlya predstavleniya vhozhdenij bazovogo klassa window v eti proizvodnye klassy. Imenno e