B'ern Straustrup. YAzyk programmirovaniya S++
---------------------------------------------------------------
Vtoroe dopolnennoe izdanie
---------------------------------------------------------------
YAzyki programmirovaniya / S++
B'ern Straustrup
YAzyk programmirovaniya S++
Kniga B. Straustrupa "YAzyk programmirovaniya S++" daet opisanie yazyka,
ego klyuchevyh ponyatij i osnovnyh priemov programmirovaniya na nem. |to
zavershennoe rukovodstvo, napisannoe sozdatelem yazyka, kotoroe soderzhit
opisanie vseh sredstv S++, v tom chisle upravlenie isklyuchitel'- nymi
situaciyami, shablony tipa (parametrizovannye tipy dannyh) i mno- zhestvennoe
nasledovanie.
Kniga delitsya na tri chasti. Pervye desyat' glav yavlyayutsya uchebnikom,
sluzhashchim vvedeniem v yazyk, vklyuchaya podmnozhestvo sobstvenno S. V treh
posleduyushchih glavah obsuzhdayutsya voprosy proektirovaniya i sozdaniya
programmnogo obespecheniya s pomoshch'yu S++. Kniga zavershaetsya polnym
spravochnym rukovodstvom po yazyku.
V knige vy najdete:
* zakonchennyj uchebnik i rukovodstvo po yazyku.
* polnoe osveshchenie sredstv yazyka, nacelennyh na abstraktnye tipy dannyh
i ob容ktno-orientirovannoe programmirovanie.
* obsuzhdenie programmistskih i tehnicheskih voprosov, voznikayushchih v pro-
cesse proektirovaniya i sozdaniya bol'shih programmnyh sistem.
* opisanie sposobov postroeniya bibliotek vysokogo klassa.
* primery realizacii klyuchevyh tipov dannyh, opredelyaemyh pol'zovatelem,
takih kak graficheskie ob容kty, associativnye massivy i potoki vvoda-
vyvoda.
|ta kniga budet horoshim pomoshchnikom opytnomu programmistu, reshivshemu
ispol'zovat' S++ dlya netrivial'nyh zadach. Ee mozhno schitat' klyuchevoj v
lyubom sobranii knig po S++.
Ob avtore knigi:
B'ern Straustrup yavlyaetsya razrabotchikom yazyka S++ i sozdatelem pervogo
translyatora. On - sotrudnik nauchno-issledovatel'skogo vychislitel'nogo
centra AT&T Bell Laboratories v Myurrej Hill (N'yu-Dzhersi, SSHA). On poluchil
zvanie magistra matematiki i vychislitel'noj tehniki v universitete g.
Aarus (Daniya), a doktorskoe zvanie po vychislitel'noj tehnike v
kembridzhskom universitete (Angliya). On specializiruetsya v oblasti ras-
predelennyh sistem, operacionnyh sistem, modelirovaniya i programmiro-
vaniya. Vmeste s M. A. |llis on yavlyaetsya avtorom polnogo rukovodstva po
yazyku S++ - "Rukovodstvo po S++ s primechaniyami".
PREDISLOVIE
"A doroga idet vse dal'she i dal'she"
(Bil'bo Beginz)
Kak bylo obeshchano v pervom izdanii knigi, zaprosy pol'zovatelej
opredelili razvitie S++. Ego napravlyal opyt shirokogo kruga pol'zovatelej,
rabotayushchih v raznyh oblastyah programmirovaniya. Za shest' let, otdelyayushchih
nas ot pervogo izdaniya opisaniya S++, chislo pol'zovatelej vozroslo v sotni
raz. Za eti gody byli usvoeny mnogie uroki, byli predlozheny i podtverdili
praktikoj svoe pravo na sushchestvovanie razlichnye priemy programmirovaniya. O
nekotoryh iz nih i pojdet rech' nizhe.
Sdelannye za eti shest' let rasshireniya yazyka prezhde vsego byli
napravleny na povyshenie vyrazitel'nosti S++ kak yazyka abstrakcii dannyh i
ob容ktno-orientirovannogo programmirovaniya voobshche i kak sredstva dlya
sozdaniya vysokokachestvennyh bibliotek s pol'zovatel'skimi tipami dannyh v
chastnosti. Bibliotekoj vysokogo kachestva my schitaem biblioteku,
pozvolyayushchuyu pol'zovatelyu opredelyat' s pomoshch'yu klassov ponyatiya, rabota s
kotorymi sochetaet udobstvo, effektivnost' i nadezhnost'. Pod nadezhnost'yu
ponimaetsya to, chto klass predostavlyaet zashchishchennyj po tipam interfejs mezhdu
pol'zovatelyami biblioteki i ee razrabotchikami. |ffektivnost' predpolagaet,
chto ispol'zovanie klassov ne vlechet za soboj bol'shih nakladnyh rashodov po
pamyati ili vremeni po sravneniyu s "ruchnymi" programmami na S.
|ta kniga yavlyaetsya polnym opisaniem yazyka S++. Glavy s 1 po 10
predstavlyayut soboj uchebnik, znakomyashchij s yazykom. V glavah s 11 po 13
obsuzhdayutsya voprosy proektirovaniya i razvitiya programmnogo obespecheniya.
Zavershaetsya kniga spravochnym rukovodstvom po yazyku S++. Estestvenno, chto
vse rasshireniya yazyka i sposoby ih ispol'zovaniya, kotorye poyavilis' posle
vyhoda v svet pervogo izdaniya, yavlyayutsya chast'yu izlozheniya. K nim otnosyatsya
utochnennye pravila dlya razresheniya peregruzki imeni, sredstva upravleniya
pamyat'yu i sredstva kontrolya dostupa, nadezhnaya po tipam procedura
svyazyvaniya, staticheskie i postoyannye funkcii-chleny, abstraktnye klassy,
mnozhestvennoe nasledovanie, shablony tipov i obrabotka osobyh situacij.
S++ yavlyaetsya yazykom programmirovaniya obshchego naznacheniya.
Estestvennaya dlya nego oblast' primeneniya - sistemnoe programmirovanie,
ponimaemoe v shirokom smysle etogo slova. Krome togo, S++ uspeshno
ispol'zuetsya vo mnogih oblastyah prilozheniya, daleko vyhodyashchih za
ukazannye ramki. Realizacii S++ teper' est' na vseh mashinah, nachinaya
s samyh skromnyh mikrokomp'yuterov - do samyh bol'shih super-|VM, i
prakticheski dlya vseh operacionnyh sistem. Poetomu kniga daet lish' opisanie
sobstvenno yazyka, ne ob座asnyaya osobennosti konkretnyh realizacij, sredy
programmirovaniya ili bibliotek.
CHitatel' najdet v knige mnogo primerov s klassami, kotorye, nesmotrya
na nesomnennuyu pol'zu, mozhno schitat' igrushechnymi. Takoj stil' izlozheniya
pozvolyaet luchshe vydelit' osnovnye ponyatiya i poleznye priemy, togda kak v
nastoyashchih, zakonchennyh programmah oni byli by skryty massoj detalej. Dlya
bol'shinstva predlozhennyh zdes' klassov, kak to svyazannye spiski, massivy,
stroki simvolov, matricy, graficheskie klassy, associativnye massivy i
t.d., - privodyatsya versii "so 100% garantiej" nadezhnosti i pravil'nosti,
poluchennye na osnove klassov iz samyh raznyh kommercheskih i nekommercheskih
programm. Mnogie iz "promyshlennyh" klassov i bibliotek poluchilis' kak
pryamye ili kosvennye potomki igrushechnyh klassov, privodimyh zdes' kak
primery.
V etom izdanii knigi po sravneniyu s pervym bol'she vnimaniya udeleno
zadache obucheniya. Vmeste s tem, uroven' izlozheniya v ravnoj mere uchityvaet i
opytnyh programmistov, ni v chem ne umalyaya ih znanij i professionalizma.
Obsuzhdenie voprosov proektirovaniya soprovozhdaetsya bolee shirokoj podachej
materiala, vyhodyashchej za ramki opisanij konstrukcij yazyka i sposobam ih
ispol'zovaniya. V etom izdanii privoditsya bol'she tehnicheskih detalej i
povyshena strogost' izlozheniya. V osobennosti eto otnositsya k spravochnomu
rukovodstvu, kotoroe vobralo v sebya mnogoletnij opyt raboty v etom
napravlenii. Predpolagalos' sozdat' knigu s dostatochno vysokim urovnem
izlozheniya, kotoraya by sluzhila programmistam ne tol'ko knigoj dlya chteniya.
Itak, pered vami kniga s opisaniem yazyka S++, ego osnovnyh principov i
metodov programmirovaniya. Nadeemsya, chto ona dostavit vam radost'.
Vyrazhenie priznatel'nosti
Krome lic, perechislennyh v sootvetstvuyushchem razdele predisloviya k
pervomu izdaniyu knigi, mne hotelos' by vyrazit' svoyu blagodarnost' |lu
|ho, Stivu Baroffu, Dzhimu Koplinu, Tomu Hansenu, Peteru Dzhaglu, Brajanu
Kerniganu, |ndryu Kenigu, Billu Leggetu, Lorrejn Mingachchi, Uorrenu
Montgomeri, Majku Moubri, Robu Myurreyu, Dzhonatanu SHapiro, Majku Vilotu i
Peteru Vejnbergu za kommentarii chernovyh variantov vtorogo izdaniya knigi.
V razvitii yazyka S++ za period ot 1985 do 1991 gg. prinimali uchastie
mnogie specialisty. YA mogu upomyanut' lish' neskol'kih iz nih: |ndryu Keniga,
Brajana Kernigana, Daga Makilroya i Dzhonatana SHapiro. Krome togo, vyrazhayu
priznatel'nost' mnogim uchastnikam sozdaniya spravochnogo rukovodstva S++,
predlozhivshim svoi varianty, a takzhe tem, s kem dovelos' nesti tyazhkuyu noshu
v techenie pervogo goda raboty komiteta X3J16 po standartizacii yazyka S++.
Myurrej-Hill, sht.N'yu Dzhersi B'ern Straustrup
PREDISLOVIE K PERVOMU IZDANIYU
"YAzyk obrazuet sredu myshleniya i formiruet
predstavlenie o tom, o chem my dumaem".
(B.L.Uorf)
S++ - yazyk obshchego naznacheniya i zaduman dlya togo, chtoby nastoyashchie
programmisty poluchili udovol'stvie ot samogo processa programmirovaniya.
Za isklyucheniem vtorostepennyh detalej on soderzhit yazyk S kak podmnozhestvo.
YAzyk S rasshiryaetsya vvedeniem gibkih i effektivnyh sredstv, prednaznachennyh
dlya postroeniya novyh tipov. Programmist strukturiruet svoyu zadachu,
opredeliv novye tipy, kotorye tochno sootvetstvuyut ponyatiyam predmetnoj
oblasti zadachi. Takoj metod postroeniya programmy obychno nazyvayut
abstrakciej dannyh. Informaciya o tipah soderzhitsya v nekotoryh ob容ktah
tipov, opredelennyh pol'zovatelem. S takimi ob容ktami mozhno rabotat'
nadezhno i prosto dazhe v teh sluchayah, kogda ih tip nel'zya ustanovit' na
stadii translyacii. Programmirovanie s ispol'zovaniem takih ob容ktov obychno
nazyvayut ob容ktno-orientirovannym. Esli etot metod primenyaetsya pravil'no,
to programmy stanovyatsya koroche i ponyatnee, a soprovozhdenie ih uproshchaetsya.
Klyuchevym ponyatiem S++ yavlyaetsya klass. Klass - eto opredelyaemyj
pol'zovatelem tip. Klassy obespechivayut upryatyvanie dannyh, ih
inicializaciyu, neyavnoe preobrazovanie pol'zovatel'skih tipov, dinamicheskoe
zadanie tipov, kontroliruemoe pol'zovatelem upravlenie pamyat'yu i sredstva
dlya peregruzki operacij. V yazyke S++ koncepcii kontrolya tipov i modul'nogo
postroeniya programm realizovany bolee polno, chem v S. Krome togo, S++
soderzhit usovershenstvovaniya, pryamo s klassami ne svyazannye: simvolicheskie
konstanty, funkcii-podstanovki, standartnye znacheniya parametrov funkcij,
peregruzka imen funkcij, operacii upravleniya svobodnoj pamyat'yu i ssylochnyj
tip. V S++ sohraneny vse vozmozhnosti S effektivnoj raboty s osnovnymi
ob容ktami, otrazhayushchimi apparatnuyu "real'nost'" (razryady, bajty, slova,
adresa i t.d.). |to pozvolyaet dostatochno effektivno realizovyvat'
pol'zovatel'skie tipy.
Kak yazyk, tak i standartnye biblioteki S++ proektirovalis' v raschete
na perenosimost'. Imeyushchiesya realizacii yazyka budut rabotat' v bol'shinstve
sistem, podderzhivayushchih S. V programmah na S++ mozhno ispol'zovat'
biblioteki S. Bol'shinstvo sluzhebnyh programm, rasschitannyh na S, mozhno
ispol'zovat' i v S++.
Dannaya kniga v pervuyu ochered' rasschitana na professional'nyh
programmistov, zhelayushchih izuchit' novyj yazyk i ispol'zovat' ego dlya
netrivial'nyh zadach. V knige daetsya polnoe opisanie S++, soderzhitsya mnogo
zavershennyh primerov i eshche bol'she fragmentov programm.
Vyrazhenie priznatel'nosti
YAzyk S++ nikogda by ne stal real'nost'yu bez, esli by postoyanno ne
ispol'zovalis' predlozheniya i sovety i ne uchityvalas' konstruktivnaya
kritika so storony mnogih druzej i kolleg. Osobenno sleduet upomyanut' Toma
Kardzhila, Dzhima Kopli, St'yu Fel'dmana, Sendi Frezera, Stiva Dzhonsona,
Brajana Kernigana, Barta Lokanti, Daga Makilroya, Dennisa Ritchi, Lerri
Roslera, Dzherri SHvarca i Dzhona SHapiro, kotorye vnesli vazhnye dlya razvitiya
yazyka idei. Dejv Presotto realizoval tekushchuyu versiyu biblioteki potokovogo
vvoda/vyvoda.
Svoj vklad v razvitie S++ i sozdanie translyatora vnesli sotni lyudej,
kotorye prisylali mne predlozheniya po sovershenstvovaniyu yazyka, opisaniya
trudnostej, s kotorymi oni stalkivalis', i oshibki translyatora. Zdes' ya
mogu upomyanut' lish' nekotoryh iz nih: Gari Bishopa, |ndryu H'yuma, Toma
Karcesa, Viktora Milenkovicha, Roba Myurreya, Leoni Ross, Brajana SHmal'ta i
Garri Uokera.
Mnogie uchastvovali v podgotovke knigi k izdaniyu, osobenno Dzhon Bentli,
Laura Ivs, Brajan Kernigan, Ted Koval'ski, Stiv Mahani, Dzhon SHapiro i
uchastniki seminara po yazyku S++, kotoryj provodilsya firmoj Bell Labs v
Kolumbii, Ogajo, 26-27 iyunya 1985 g.
Myurrej-Hill, sht.N'yu-Dzhersi B'ern Straustrup
PREDVARITELXNYE ZAMECHANIYA
"O mnogom - molvil Morzh,-
prishla pora pogovorit' ".
L.Kerroll
Dannaya glava soderzhit kratkij obzor knigi, spisok literatury i
nekotorye dopolnitel'nye zamechaniya o yazyke S++. Zamechaniya kasayutsya istorii
sozdaniya S++, idej, kotorye okazali sushchestvennoe vliyanie na razrabotku
yazyka, i nekotoryh myslej po povodu programmirovaniya na S++. |ta glava ne
yavlyaetsya vvedeniem; privedennye zamechaniya ne yavlyayutsya neobhodimymi dlya
ponimaniya posleduyushchih glav. Nekotorye iz nih predpolagayut znakomstvo
chitatelya s S++.
Kniga sostoit iz treh chastej. Glavy s 1 po 10 yavlyayutsya uchebnikom po
yazyku. V glavah s 11 po 13 obsuzhdayutsya voprosy proektirovaniya i razvitiya
programmnogo obespecheniya s uchetom vozmozhnostej S++. V konce knigi
privedeno polnoe spravochnoe rukovodstvo po yazyku. Ischerpyvayushchee opisanie
konstrukcij S++ soderzhitsya tol'ko tam. Uchebnaya chast' knigi soderzhit
primery, sovety, predosterezheniya i uprazhneniya, dlya kotoryh ne nashlos'
mesta v rukovodstve.
Kniga v osnovnom posvyashchena voprosu, kak s pomoshch'yu yazyka C++
strukturirovat' programmu, a ne voprosu, kak zapisat' na nem algoritm.
Sledovatel'no, tam, gde mozhno bylo vybirat', predpochtenie otdavalos' ne
professional'nym, no slozhnym dlya ponimaniya, a trivial'nym algoritmam. Tak
v odnom iz primerov ispol'zuetsya puzyr'kovaya sortirovka, hotya algoritm
bystroj sortirovki bol'she podhodit dlya nastoyashchej programmy. CHasto
napisat' tu zhe programmu, no s bolee effektivnym algoritmom, predlagaetsya
v vide uprazhneniya.
Glava 1 soderzhit kratkij obzor osnovnyh koncepcij i konstrukcij S++.
Ona pozvolyaet poznakomit'sya s yazykom v obshchih chertah. Podrobnye ob座asneniya
konstrukcij yazyka i sposobov ih primeneniya soderzhatsya v posleduyushchih
glavah. Obsuzhdayutsya v pervuyu ochered' sredstva, obespechivayushchie abstrakciyu
dannyh i ob容ktno-orientirovannoe programmirovanie. Osnovnye sredstva
procedurnogo programmirovaniya upominayutsya kratko.
V glavah 2, 3 i 4 opisyvayutsya sredstva S++, kotorye ne ispol'zuyutsya
dlya opredeleniya novyh tipov: osnovnye tipy, vyrazheniya i struktury
upravleniya. Drugimi slovami, eti glavy soderzhat opisanie toj chasti yazyka,
kotoraya po suti predstavlyaet S. Izlozhenie v ukazannyh glavah idet v
uglublennom vide.
Glavy 5 - 8 posvyashcheny sredstvam postroeniya novyh tipov, kotorye ne
imeyut analogov v S. V glave 5 vvoditsya osnovnoe ponyatie - klass. V nej
pokazano, kak mozhno opredelyat' pol'zovatel'skie tipy (klassy),
inicializirovat' ih, obrashchat'sya k nim, i, nakonec, kak unichtozhat' ih.
Glava 6 posvyashchena ponyatiyu proizvodnyh klassov, kotoroe pozvolyaet stroit'
iz prostyh klassov bolee slozhnye. Ono daet takzhe vozmozhnost' effektivnoj i
bezopasnoj (v smysle tipa) raboty v teh situaciyah, kogda tipy ob容ktov na
stadii translyacii neizvestny. V glave 7 ob座asnyaetsya, kak mozhno opredelit'
unarnye i binarnye operacii dlya pol'zovatel'skih tipov, kak zadavat'
preobrazovaniya takih tipov, i kakim obrazom mozhno sozdavat', kopirovat' i
udalyat' ob容kty, predstavlyayushchie pol'zovatel'skie tipy. Glava 8 posvyashchena
shablonam tipa, t.e. takomu sredstvu S++, kotoroe pozvolyaet opredelit'
semejstvo tipov i funkcij.
V glave 9 obsuzhdaetsya obrabotka osobyh situacij, rassmatrivayutsya
vozmozhnye reakcii na oshibki i metody postroeniya ustojchivyh k oshibkam
sistem. V glave 10 opredelyayutsya klassy ostream i istream, predostavlyaemye
standartnoj bibliotekoj dlya potokovogo vvoda-vyvoda.
Glavy 11 - 13 posvyashcheny voprosam, svyazannym s primeneniem S++ dlya
proektirovaniya i realizacii bol'shih programmnyh sistem. V glave 11 v
osnovnom rassmatrivayutsya voprosy proektirovaniya i upravleniya programmnymi
proektami. V glave 12 obsuzhdaetsya vzaimosvyaz' mezhdu yazykom S++ i
problemami proektirovaniya. V glave 13 pokazany sposoby sozdaniya bibliotek.
Zavershaetsya kniga spravochnym rukovodstvom po S++.
Ssylki na razlichnye chasti knigi dayutsya v vide $$2.3.4, chto oznachaet
razdel 3.4 glavy 2. Dlya oboznacheniya spravochnogo rukovodstva primenyaetsya
bukva R, naprimer, $$R.8.5.5.
Zamechaniya po realizacii
Sushchestvuet neskol'ko rasprostranyaemyh nezavisimyh realizacij S++.
Poyavilos' bol'shoe chislo servisnyh programm, bibliotek i integrirovannyh
sistem programmirovaniya. Imeetsya massa knig, rukovodstv, zhurnalov, statej,
soobshchenij po elektronnoj pochte, tehnicheskih byulletenej, otchetov o
konferenciyah i kursov, iz kotoryh mozhno poluchit' vse neobhodimye svedeniya
o poslednih izmeneniyah v S++, ego ispol'zovanii, servisnyh programmah,
bibliotekah, novyh translyatorah i t.d. Esli vy ser'ezno rasschityvaete na
S++, stoit poluchit' dostup hotya by k dvum istochnikam informacii, poskol'ku
u kazhdogo istochnika mozhet byt' svoya poziciya.
Bol'shinstvo programmnyh fragmentov, privedennyh v knige, vzyaty
neposredstvenno iz tekstov programm, kotorye byli translirovany na mashine
DEC VAX 11/8550 pod upravleniem 10-j versii sistemy UNIX [25].
Ispol'zovalsya translyator, yavlyayushchijsya pryamym potomkom translyatora S++,
sozdannogo avtorom. Zdes' opisyvaetsya "chistyj S++", t.e. ne ispol'zuyutsya
nikakie zavisyashchie ot realizacii rasshireniya. Sledovatel'no, primery dolzhny
idti pri lyuboj realizacii yazyka. Odnako, shablony tipa i obrabotka osobyh
situacij otnosyatsya k samym poslednim rasshireniyam yazyka, i vozmozhno, chto
vash translyator ih ne soderzhit.
Uprazhneniya
Uprazhneniya dayutsya v konce kazhdoj glavy. CHashche vsego oni predlagayut
napisat' programmu. Resheniem mozhet schitat'sya programma, kotoraya
transliruetsya i pravil'no rabotaet hotya by na neskol'kih testah.
Uprazhneniya mogut znachitel'no razlichat'sya po slozhnosti, poetomu daetsya
priblizitel'naya ocenka stepeni ih slozhnosti. Rost slozhnosti
eksponencial'nyj, tak chto, esli na uprazhnenie (*1) u vas ujdet pyat' minut,
to (*2) mozhet zanyat' chas, a (*3) - celyj den'. Odnako vremya napisaniya i
otladki programmy bol'she zavisit ot opyta chitatelya, chem ot samogo
uprazhneniya. Na uprazhnenie (*1) mozhet potrebovat'sya celyj den', esli pered
zapuskom programmy chitatelyu pridetsya oznakomit'sya s novoj vychislitel'noj
sistemoj. S drugoj storony, tot, u kogo pod rukoj okazhetsya nuzhnyj nabor
programm, mozhet sdelat' uprazhnenie (*5) za odin chas.
Lyubuyu knigu po programmirovaniyu na yazyke S mozhno ispol'zovat' kak
istochnik dopolnitel'nyh uprazhnenij pri izuchenii glav 2 - 4. V knige Aho
([1]) privedeno mnogo obshchih struktur dannyh i algoritmov v terminah
abstraktnyh tipov dannyh. |tu knigu takzhe mozhno ispol'zovat' kak istochnik
uprazhnenij pri izuchenii glav 5 - 8. Odnako, ispol'zovannomu v nej yazyku ne
dostaet funkcij-chlenov i proizvodnyh klassov. Poetomu opredelyaemye
pol'zovatelem tipy na S++ mozhno napisat' bolee elegantno.
Zamechaniya po proektu yazyka
Pri razrabotke yazyka S++ odnim iz vazhnejshih kriteriev vybora byla
prostota. Kogda voznikal vopros, chto uprostit': rukovodstvo po yazyku i
druguyu dokumentaciyu ili translyator, - to vybor delali v pol'zu pervogo.
Ogromnoe znachenie pridavalos' sovmestimosti s yazykom S, chto pomeshalo
udalit' ego sintaksis.
V S++ net tipov dannyh i elementarnyh operacij vysokogo urovnya.
Naprimer, ne sushchestvuet tipa matrica s operaciej obrashcheniya ili tipa stroka
s operaciej konkatenacii. Esli pol'zovatelyu ponadobyatsya podobnye tipy, on
mozhet opredelit' ih v samom yazyke. Programmirovanie na S++ po suti
svoditsya k opredeleniyu universal'nyh ili zavisyashchih ot oblasti prilozheniya
tipov. Horosho produmannyj pol'zovatel'skij tip otlichaetsya ot vstroennogo
tipa tol'ko sposobom opredeleniya, no ne sposobom primeneniya.
Iz yazyka isklyuchalis' vozmozhnosti, kotorye mogut privesti k nakladnym
rashodam pamyati ili vremeni vypolneniya, dazhe esli oni neposredstvenno ne
ispol'zuyutsya v programme. Naprimer, bylo otvergnuto predlozhenie hranit' v
kazhdom ob容kte nekotoruyu sluzhebnuyu informaciyu. Esli pol'zovatel' opisal
strukturu, soderzhashchuyu dve velichiny, zanimayushchie po 16 razryadov, to
garantiruetsya, chto ona pomestitsya v 32-h razryadnyj registr.
YAzyk S++ proektirovalsya dlya ispol'zovaniya v dovol'no tradicionnoj
srede, a imenno: v sisteme programmirovaniya S operacionnoj sistemy UNIX.
No est' vpolne obosnovannye dovody v pol'zu ispol'zovaniya S++ v bolee
bogatoj programmnoj srede. Takie vozmozhnosti, kak dinamicheskaya zagruzka,
razvitye sistemy translyacii i bazy dannyh dlya hraneniya opredelenij tipov,
mozhno uspeshno ispol'zovat' bez ushcherba dlya yazyka.
Tipy S++ i mehanizmy upryatyvaniya dannyh rasschitany na opredelennyj
sintaksicheskij analiz, provodimyj translyatorom dlya obnaruzheniya sluchajnoj
porchi dannyh. Oni ne obespechivayut sekretnosti dannyh i zashchity ot
umyshlennogo narusheniya pravil dostupa k nim. Odnako, eti sredstva mozhno
svobodno ispol'zovat', ne boyas' nakladnyh rashodov pamyati i vremeni
vypolneniya programmy. Uchteno, chto konstrukciya yazyka aktivno ispol'zuetsya
togda, kogda ona ne tol'ko izyashchno zapisyvaetsya na nem, no i vpolne po
sredstvam obychnym programmam.
Istoricheskaya spravka
Bezuslovno S++ mnogim obyazan yazyku S [8], kotoryj sohranyaetsya kak ego
podmnozhestvo. Sohraneny i vse svojstvennye S sredstva nizkogo urovnya,
prednaznachennye dlya resheniya samyh nasushchnyh zadach sistemnogo
programmirovaniya. S, v svoyu ochered', mnogim obyazan svoemu predshestvenniku
yazyku BCPL [13]. Kommentarij yazyka BCPL byl vosstanovlen v S++. Esli
chitatel' znakom s yazykom BCPL, to mozhet zametit', chto v S++ po-prezhnemu
net bloka VALOF. Eshche odnim istochnikom vdohnoveniya byl yazyk SIMULA-67
[2,3]; imenno iz nego byla zaimstvovana koncepciya klassov (vmeste c
proizvodnymi klassami i virtual'nymi funkciyami). Operator inspect iz
SIMULA-67 namerenno ne byl vklyuchen v S++. Prichina - zhelanie
sposobstvovat' modul'nosti za schet ispol'zovaniya virtual'nyh funkcij.
Vozmozhnost' v S++ peregruzki operacij i svoboda razmeshcheniya opisanij vsyudu,
gde mozhet vstrechat'sya operator, napominayut yazyk Algol-68 [24].
S momenta vyhoda v svet pervogo izdaniya etoj knigi yazyk S++ podvergsya
sushchestvennym izmeneniyam i utochneniyam. V osnovnom eto kasaetsya razresheniya
neodnoznachnosti pri peregruzke, svyazyvanii i upravlenii pamyat'yu. Vmeste s
tem, byli vneseny neznachitel'nye izmeneniya s cel'yu uvelichit' sovmestimost'
s yazykom S. Byli takzhe vvedeny nekotorye obobshcheniya i sushchestvennye
rasshireniya, kak to: mnozhestvennoe nasledovanie, funkcii-chleny so
specifikaciyami static i const, zashchishchennye chleny (protected), shablony tipa
i obrabotka osobyh situacij. Vse eti rasshireniya i dorabotki byli naceleny
na to, chtoby S++ stal yazykom, na kotorom mozhno sozdavat' i ispol'zovat'
biblioteki. Vse izmeneniya opisyvayutsya v [10,18,20,21 i 23].
SHablony tipov poyavilis' chastichno iz-za zhelaniya formalizovat'
makrosredstva, a chastichno byli inspirirovany opisaniem genericheskih
ob容ktov v yazyke Ada (s uchetom ih dostoinstv i nedostatkov) i
parametrizirovannymi modulyami yazyka CLU. Mehanizm obrabotki osobyh
situacij poyavilsya otchasti pod vliyaniem yazykov Ada i CLU [11], a otchasti
pod vliyaniem ML [26]. Drugie rasshireniya, vvedennye za period mezhdu 1985 i
1991 g.g. (takie kak mnozhestvennoe nasledovanie, staticheskie funkcii-chleny
i chistye virtual'nye funkcii), skoree poyavilis' v rezul'tate obobshcheniya
opyta programmirovaniya na S++, chem byli pocherpnuty iz drugih yazykov.
Bolee rannie versii yazyka, poluchivshie nazvanie "S s klassami" [16],
ispol'zovalis', nachinaya s 1980 g. |tot yazyk voznik potomu, chto avtoru
potrebovalos' napisat' programmy modelirovaniya, upravlyaemye preryvaniyami.
YAzyk SIMULA-67 ideal'no podhodit dlya etogo, esli ne uchityvat'
effektivnost'. YAzyk "S s klassami" ispol'zovalsya dlya bol'shih zadach
modelirovaniya. Strogoj proverke podverglis' togda vozmozhnosti napisaniya na
nem programm, dlya kotoryh kritichny resursy vremeni i pamyati. V etom yazyke
nedostavalo peregruzki operacij, ssylok, virtual'nyh funkcij i mnogih
drugih vozmozhnostej. Vpervye S++ vyshel za predely issledovatel'skoj
gruppy, v kotoroj rabotal avtor, v iyule 1983 g., odnako togda mnogie
vozmozhnosti S++ eshche ne byli razrabotany.
Nazvanie S++ (si plyus plyus) , bylo pridumano Rikom Maskitti letom 1983
g. |to nazvanie otrazhaet evolyucionnyj harakter izmenenij yazyka S.
Oboznachenie ++ otnositsya k operacii narashchivaniya S. CHut' bolee korotkoe imya
S+ yavlyaetsya sintaksicheskoj oshibkoj. Krome togo, ono uzhe bylo ispol'zovano
kak nazvanie sovsem drugogo yazyka. Znatoki semantiki S nahodyat, chto S++
huzhe, chem ++S. YAzyk ne poluchil nazvaniya D, poskol'ku on yavlyaetsya
rasshireniem S, i v nem ne delaetsya popytok reshit' kakie-libo problemy za
schet otkaza ot vozmozhnostej S. Eshche odnu interesnuyu interpretaciyu nazvaniya
S++ mozhno najti v prilozhenii k [12].
Iznachal'no S++ byl zaduman dlya togo, chtoby avtoru i ego druz'yam ne
nado bylo programmirovat' na assemblere, S ili drugih sovremennyh yazykah
vysokogo urovnya. Osnovnoe ego prednaznachenie - uprostit' i sdelat' bolee
priyatnym process programmirovaniya dlya otdel'nogo programmista. Do
nedavnego vremeni ne bylo plana razrabotki S++ na bumage. Proektirovanie,
realizaciya i dokumentirovanie shli parallel'no. Nikogda ne sushchestvovalo
"proekta S++" ili "Komiteta po razrabotke S++". Poetomu yazyk razvivalsya i
prodolzhaet razvivat'sya tak, chtoby preodolet' vse problemy, s kotorymi
stolknulis' pol'zovateli. Tolchkami k razvitiyu sluzhat takzhe i obsuzhdeniya
avtorom vseh problem s ego druz'yami i kollegami.
V svyazi s lavinoobraznym processom uvelicheniya chisla pol'zovatelej S++,
prishlos' sdelat' sleduyushchie izmeneniya. Primerno v 1987 g. stalo ochevidno,
chto rabota po standartizacii S++ neizbezhna i chto sleduet nezamedlitel'no
pristupit' k sozdaniyu osnovy dlya nee [22]. V rezul'tate byli predprinyaty
celenapravlennye dejstviya, chtoby ustanovit' kontakt mezhdu razrabotchikami
S++ i bol'shinstvom pol'zovatelej. Primenyalas' obychnaya i elektronnaya
pochta, a takzhe bylo neposredstvennoe obshchenie na konferenciyah po S++ i
drugih vstrechah.
Firma AT&T Bell Laboratories vnesla osnovnoj vklad v etu rabotu,
predostaviv avtoru pravo izuchat' versii spravochnogo rukovodstva po yazyku
vmeste s upominavshimisya razrabotchikami i pol'zovatelyami. Ne sleduet
nedoocenivat' etot vklad, t.k. mnogie iz nih rabotayut v kompaniyah, kotorye
mozhno schitat' konkurentami firmy AT&T. Menee prosveshchennaya kompaniya mogla
by prosto nichego ne delat', i v rezul'tate poyavilos' by neskol'ko
nesoglasovannyh versij yazyka. Okolo sta predstavitelej iz poryadka 20
organizacij izuchali i kommentirovali to, chto stalo sovremennoj versiej
spravochnogo rukovodstva i ishodnymi materialami dlya ANSI po standartizacii
S++. Ih imena mozhno najti v "Annotirovannom spravochnom rukovodstve po
yazyku S++" [4]. Spravochnoe rukovodstvo polnost'yu voshlo v nastoyashchuyu knigu.
Nakonec, po iniciative firmy Hewlett-Packard v dekabre 1989 g. v sostave
ANSI byl obrazovan komitet X3J16. Ozhidaetsya, chto raboty po standartizacii
S++ v ANSI (amerikanskij standart) stanut sostavnoj chast'yu rabot po
standartizacii silami ISO (Mezhdunarodnoj organizacii po standartizacii).
S++ razvivalsya odnovremenno s razvitiem nekotoryh fundamental'nyh
klassov, predstavlennyh v dannoj knige. Naprimer, avtor razrabatyval
klassy complex, vector i stack, sozdavaya odnovremenno vozmozhnost'
peregruzki operacij. V rezul'tate etih zhe usilij i blagodarya sodejstviyu
D. SHapiro poyavilis' strokovye i spisochnye klassy. |ti klassy stali pervymi
bibliotechnymi klassami, kotorye nachali aktivno ispol'zovat'sya. Biblioteka
task, opisyvaemaya v [19] i v uprazhnenii 13 iz $$6.8 stala chast'yu samoj
pervoj programmy, napisannoj na yazyke "S s klassami". |ta programma i
ispol'zuemye v nej klassy byli sozdany dlya modelirovaniya v stile Simuly.
Biblioteka task byla sushchestvenno pererabotana D. SHapiro i prodolzhaet
aktivno ispol'zovat'sya do nastoyashchego vremeni. Potokovaya biblioteka, kak
ukazyvalos' v pervom izdanii knigi, byla razrabotana i primenena avtorom.
D. SHvarc preobrazoval ee v potokovuyu biblioteku vvoda-vyvoda ($$10),
ispol'zuya naryadu s drugimi priemami metod manipulyatorov |.Keniga
($$10.4.2). Klass map ($$8.8) byl predlozhen |.Kenigom. On zhe sozdal klass
Pool ($$13.10), chtoby ispol'zovat' dlya biblioteki predlozhennyj avtorom
sposob raspredeleniya pamyati dlya klassov ($$5.5.6). Na sozdanie ostal'nyh
shablonov povliyali shablony Vector, Map, Slist i sort, predstavlennye v
glave 8.
Sravnenie yazykov S++ i S
Vybor S v kachestve bazovogo yazyka dlya S++ ob座asnyaetsya sleduyushchimi ego
dostoinstvami:
(1) universal'nost', kratkost' i otnositel'no nizkij uroven';
(2) adekvatnost' bol'shinstvu zadach sistemnogo programmirovaniya;
(3) on idet v lyuboj sisteme i na lyuboj mashine;
(4) polnost'yu podhodit dlya programmnoj sredy UNIX.
V S sushchestvuyut svoi problemy, no v yazyke, razrabatyvaemom "s nulya" oni
poyavilis' by tozhe, a problemy S, po krajnej mere, horosho izvestny. Bolee
vazhno to, chto orientaciya na S pozvolila ispol'zovat' yazyk "S s klassami"
kak poleznyj (hotya i ne ochen' udobnyj) instrument v techenie pervyh mesyacev
razdumij o vvedenii v S klassov v stile Simuly.
S++ stal ispol'zovat'sya shire, no po mere rosta ego vozmozhnostej,
vyhodyashchih za predely S, vnov' i vnov' voznikala problema sovmestimosti.
YAsno, chto otkazavshis' ot chasti nasledstva S, mozhno izbezhat' nekotoryh
problem (sm., naprimer, [15]). |to ne bylo sdelano po sleduyushchim prichinam:
(1) sushchestvuyut milliony strok programm na S, kotorye mozhno uluchshit' s
pomoshch'yu S++, no pri uslovii, chto polnoj perepisi ih na yazyk S++ ne
potrebuetsya;
(2) sushchestvuyut milliony strok bibliotechnyh funkcij i sluzhebnyh
programm na S, kotorye mozhno bylo by ispol'zovat' v S++ pri usloviyah
sovmestimosti oboih yazykov na stadii svyazyvaniya i ih bol'shogo
sintaksicheskogo shodstva;
(3) sushchestvuyut sotni tysyach programmistov, znayushchih S; im dostatochno
ovladet' tol'ko novymi sredstvami S++ i ne nado izuchat' osnov yazyka;
(4) poskol'ku S i S++ budut ispol'zovat'sya odnimi i temi zhe lyud'mi na
odnih i teh zhe sistemah mnogie gody, razlichiya mezhdu yazykami dolzhny byt'
libo minimal'nymi, libo maksimal'nymi, chtoby svesti k minimumu kolichestvo
oshibok i nedorazumenij. Opisanie S++ bylo pererabotano tak, chtoby
garantirovat', chto lyubaya dopustimaya v oboih yazykah konstrukciya oznachala v
nih odno i to zhe.
YAzyk S sam razvivalsya v poslednie neskol'ko let, chto otchasti bylo
svyazano s razrabotkoj S++ [14]. Standart ANSI dlya S [27] soderzhit,
naprimer, sintaksis opisaniya funkcij, pozaimstvovannyj iz yazyka "S s
klassami". Proishodit vzaimnoe zaimstvovanie, naprimer, tip ukazatelya
void* byl priduman dlya ANSI S, a vpervye realizovan v S++. Kak bylo
obeshchano v pervom izdanii etoj knigi, opisanie S++ bylo dorabotano, chtoby
isklyuchit' neopravdannye rashozhdeniya. Teper' S++ bolee sovmestim s yazykom
S, chem eto bylo vnachale ($$R.18). V ideale S++ dolzhen maksimal'no
priblizhat'sya k ANSI C, no ne bolee [9]. Stoprocentnoj sovmestimosti
nikogda ne bylo i ne budet, poskol'ku eto narushit nadezhnost' tipov i
soglasovannost' ispol'zovaniya vstroennyh i pol'zovatel'skih tipov, a eti
svojstva vsegda byli odnimi iz glavnyh dlya S++.
Dlya izucheniya S++ ne obyazatel'no znat' S. Programmirovanie na S
sposobstvuet usvoeniyu priemov i dazhe tryukov, kotorye pri programmirovanii
na S++ stanovyatsya prosto nenuzhnymi. Naprimer, yavnoe preobrazovanie tipa
(privedenie) , v S++ nuzhno gorazdo rezhe, chem v S (sm. "Zamechaniya dlya
programmistov na S" nizhe). Tem ne menee, horoshie programmy na yazyke S po
suti yavlyayutsya programmami na S++. Naprimer, vse programmy iz klassicheskogo
opisaniya S [8] yavlyayutsya programmami na S++. V processe izucheniya S++ budet
polezen opyt raboty s lyubym yazykom so staticheskimi tipami.
|ffektivnost' i struktura
Razvitie yazyka S++ proishodilo na baze yazyka S, i, za nebol'shim
isklyucheniem, S byl sohranen v kachestve podmnozhestva C++. Bazovyj yazyk S
byl sproektirovan takim obrazom, chto imeetsya ochen' tesnaya svyaz' mezhdu
tipami, operaciyami, operatorami i ob容ktami, s kotorymi neposredstvenno
rabotaet mashina, t.e. chislami, simvolami i adresami. Za isklyucheniem
operacij new, delete i throw, a takzhe proveryaemogo bloka, dlya vypolneniya
operatorov i vyrazhenij S++ ne trebuetsya skrytoj dinamicheskoj apparatnoj
ili programmnoj podderzhki.
V S++ ispol'zuetsya ta zhe (ili dazhe bolee effektivnaya)
posledovatel'nost' komand dlya vyzova funkcij i vozvrata iz nih, chto i v S.
Esli dazhe eti dovol'no effektivnye operacii stanovyatsya slishkom dorogimi,
to vyzov funkcii mozhet byt' zamenen podstanovkoj ee tela, prichem
sohranyaetsya udobnaya funkcional'naya zapis' bezo vsyakih rashodov na vyzov
funkcii.
Pervonachal'no yazyk S zadumyvalsya kak konkurent assemblera, sposobnyj
vytesnit' ego iz osnovnyh i naibolee trebovatel'nyh k resursam zadach
sistemnogo programmirovaniya. V proekte S++ byli prinyaty mery, chtoby uspehi
S v etoj oblasti ne okazalis' pod ugrozoj. Razlichie mezhdu dvumya yazykami
prezhde vse sostoit v stepeni vnimaniya, udelyaemogo tipam i strukturam. YAzyk
S vyrazitelen i v to zhe vremya snishoditelen po otnosheniyu k tipam. YAzyk S++
eshche bolee vyrazitelen, no takoj vyrazitel'nosti mozhno dostich' lish' togda,
kogda tipam udelyayut bol'shoe vnimanie. Kogda tipy ob容ktov izvestny,
translyator pravil'no raspoznaet takie vyrazheniya, v kotoryh inache
programmistu prishlos' by zapisyvat' operacii s utomitel'nymi
podrobnostyami. Krome togo, znanie tipov pozvolyaet translyatoru
obnaruzhivat' takie oshibki, kotorye v protivnom sluchae byli by vyyavleny
tol'ko pri testirovanii. Otmetim, chto samo po sebe ispol'zovanie strogoj
tipizacii yazyka dlya kontrolya parametrov funkcii, zashchity dannyh ot
nezakonnogo dostupa, opredeleniya novyh tipov i operacij ne vlechet
dopolnitel'nyh rashodov pamyati i uvelicheniya vremeni vypolneniya programmy.
V proekte S++ osoboe vnimanie udelyaetsya strukturirovaniyu programmy.
|to vyzvano uvelicheniem razmerov programm so vremeni poyavleniya S.
Nebol'shuyu programmu (skazhem, ne bolee 1000 strok) mozhno zastavit' iz
upryamstva rabotat', narushaya vse pravila horoshego stilya programmirovaniya.
Odnako, dejstvuya tak, chelovek uzhe ne smozhet spravit'sya s bol'shoj
programmoj. Esli u vashej programmy v 10 000 strok plohaya struktura, to vy
obnaruzhite, chto novye oshibki poyavlyayutsya v nej tak zhe bystro, kak udalyayutsya
starye. S++ sozdavalsya s cel'yu, chtoby bol'shuyu programmu mozhno bylo
strukturirovat' takim obrazom, chtoby odnomu cheloveku ne prishlos' rabotat'
s tekstom v 25000 strok. V nastoyashchee vremya mozhno schitat', chto eta cel'
polnost'yu dostignuta.
Sushchestvuyut, konechno, programmy eshche bol'shego razmera. Odnako te iz nih,
kotorye dejstvitel'no ispol'zuyutsya, obychno mozhno razbit' na neskol'ko
prakticheski nezavisimyh chastej, kazhdaya iz kotoryh imeet znachitel'no
men'shij upomyanutogo razmer. Estestvenno, trudnost' napisaniya i
soprovozhdeniya programmy opredelyaetsya ne tol'ko chislom strok teksta, no i
slozhnost'yu predmetnoj oblasti. Tak chto privedennye zdes' chisla, kotorymi
obosnovyvalis' nashi soobrazheniya, ne nado vosprinimat' slishkom ser'ezno.
K sozhaleniyu, ne vsyakuyu chast' programmy mozhno horosho strukturirovat',
sdelat' nezavisimoj ot apparatury, dostatochno ponyatnoj i t.d. V S++ est'
sredstva, neposredstvenno i effektivno predstavlyayushchie apparatnye
vozmozhnosti. Ih ispol'zovanie pozvolyaet izbavit'sya ot bespokojstva o
nadezhnosti i prostote ponimaniya programmy. Takie chasti programmy mozhno
skryvat', predostavlyaya nadezhnyj i prostoj interfejs s nimi.
Estestvenno, esli S++ ispol'zuetsya dlya bol'shoj programmy, to eto
oznachaet, chto yazyk ispol'zuyut gruppy programmistov. Poleznuyu rol' zdes'
sygrayut svojstvennye yazyku modul'nost', gibkost' i strogo tipizirovannye
interfejsy. V S++ est' takoj zhe horoshij nabor sredstv dlya sozdaniya bol'shih
programm, kak vo mnogih yazykah. No kogda programma stanovitsya eshche bol'she,
problemy po ee sozdaniyu i soprovozhdeniyu peremeshchayutsya iz oblasti yazyka v
bolee global'nuyu oblast' programmnyh sredstv i upravleniya proektom. |tim
voprosam posvyashcheny glavy 11 i 12.
V etoj knige osnovnoe vnimanie udelyaetsya metodam sozdaniya
universal'nyh sredstv, poleznyh tipov, bibliotek i t.d. |ti metody mozhno
uspeshno primenyat' kak dlya malen'kih, tak i dlya bol'shih programm. Bolee
togo, poskol'ku vse netrivial'nye programmy sostoyat iz neskol'kih v
znachitel'noj stepeni nezavisimyh drug ot druga chastej, metody
programmirovaniya otdel'nyh chastej prigodyatsya kak sistemnym, tak i
prikladnym programmistam.
Mozhet vozniknut' podozrenie, chto zapis' programmy s ispol'zovaniem
podrobnoj sistemy tipov, uvelichit razmer teksta. Dlya programmy na S++ eto
ne tak: programma na S++, v kotoroj opisany tipy formal'nyh parametrov
funkcij, opredeleny klassy i t.p., obychno byvaet dazhe koroche svoego
ekvivalenta na S, gde eti sredstva ne ispol'zuyutsya. Kogda v programme na
S++ ispol'zuyutsya biblioteki, ona takzhe okazyvaetsya koroche svoego
ekvivalenta na S, esli, konechno, on sushchestvuet.
Filosofskie zamechaniya
YAzyk programmirovaniya reshaet dve vzaimosvyazannye zadachi: pozvolyaet
programmistu zapisat' podlezhashchie vypolneniyu dejstviya i formiruet ponyatiya,
kotorymi programmist operiruet, razmyshlyaya o svoej zadache. Pervoj celi
ideal'no otvechaet yazyk, kotoryj ochen' "blizok mashine". Togda so vsemi ee
osnovnymi "sushchnostyami" mozhno prosto i effektivno rabotat' na etom yazyke,
prichem delaya eto ochevidnym dlya programmista sposobom. Imenno eto imeli v
vidu sozdateli S. Vtoroj celi ideal'no otvechaet yazyk, kotoryj nastol'ko
"blizok k postavlennoj zadache", chto na nem neposredstvenno i tochno
vyrazhayutsya ponyatiya, ispol'zuemye v reshenii zadachi. Imenno eto imelos' v
vidu, kogda pervonachal'no opredelyalis' sredstva, dobavlyaemye k S.
Svyaz' mezhdu yazykom, na kotorom my dumaem i programmiruem, a takzhe
mezhdu zadachami i ih resheniyami, kotorye mozhno predstavit' v svoem
voobrazhenii, dovol'no blizka. Po etoj prichine ogranichivat' vozmozhnosti
yazyka tol'ko poiskom oshibok programmista - v luchshem sluchae opasno. Kak i
v sluchae estestvennyh yazykov, ochen' polezno obladat', po krajnej mere,
dvuyazychiem. YAzyk predostavlyaet programmistu nekotorye ponyatiya v vide
yazykovyh instrumentov; esli oni ne podhodyat dlya zadachi, ih prosto
ignoriruyut. Naprimer, esli sushchestvenno ogranichit' ponyatie ukazatelya, to
programmist budet vynuzhden dlya sozdaniya struktur, ukazatelej i t.p.
ispol'zovat' vektora i operacii s celymi. Horoshij proekt programmy i
otsutstvie v nej oshibok nel'zya garantirovat' tol'ko nalichiem ili
otsutstviem opredelennyh vozmozhnostej v yazyke.
Tipizaciya yazyka dolzhna byt' osobenno polezna dlya netrivial'nyh zadach.
Dejstvitel'no, ponyatie klassa v S++ proyavilo sebya kak moshchnoe
konceptual'noe sredstvo.
Zamechaniya o programmirovanii na yazyke S++
Predpolagaetsya, chto v ideal'nom sluchae razrabotka programmy delitsya na
tri etapa: vnachale neobhodimo dobit'sya yasnogo ponimaniya zadachi, zatem
opredelit' klyuchevye ponyatiya, ispol'zuemye dlya ee resheniya, i, nakonec,
poluchennoe reshenie vyrazit' v vide programmy. Odnako, detali resheniya i
tochnye ponyatiya, kotorye budut ispol'zovat'sya v nem, chasto proyasnyayutsya
tol'ko posle togo, kak ih popytayutsya vyrazit' v programme. Imenno v etom
sluchae bol'shoe znachenie priobretaet vybor yazyka programmirovaniya.
Vo mnogih zadachah ispol'zuyutsya ponyatiya, kotorye trudno predstavit' v
programme v vide odnogo iz osnovnyh tipov ili v vide funkcii bez svyazannyh
s nej staticheskih dannyh. Takoe ponyatie mozhet predstavlyat' v programme
klass. Klass - eto tip; on opredelyaet povedenie svyazannyh s nim ob容ktov:
ih sozdanie, obrabotku i unichtozhenie. Krome etogo, klass opredelyaet
realizaciyu ob容ktov v yazyke, no na nachal'nyh stadiyah razrabotki programmy
eto ne yavlyaetsya i ne dolzhno yavlyat'sya glavnoj zabotoj. Dlya napisaniya
horoshej programmy nado sostavit' takoj nabor klassov, v kotorom kazhdyj
klass chetko predstavlyaet odno ponyatie. Obychno eto oznachaet, chto
programmist dolzhen sosredotochit'sya na voprosah: Kak sozdayutsya ob容kty
dannogo klassa? Mogut li oni kopirovat'sya i (ili) unichtozhat'sya? Kakie
operacii mozhno opredelit' nad etimi ob容ktami? Esli na eti voprosy
udovletvoritel'nyh otvetov ne nahoditsya, to, skoree vsego, eto oznachaet,
chto ponyatie ne bylo dostatochno yasno sformulirovano. Togda, vozmozhno,
stoit eshche porazmyshlyat' nad zadachej i predlagaemym resheniem, a ne
nemedlenno pristupat' k programmirovaniyu, nadeyas' v processe nego najti
otvety.
Proshche vsego rabotat' s ponyatiyami, kotorye imeyut tradicionnuyu
matematicheskuyu formu predstavleniya: vsevozmozhnye chisla, mnozhestva,
geometricheskie figury i t.d. Dlya takih ponyatij polezno bylo by imet'
standartnye biblioteki klassov, no k momentu napisaniya knigi ih eshche ne
bylo. V programmnom mire nakopleno udivitel'noe bogatstvo iz takih
bibliotek, no net ni formal'nogo, ni fakticheskogo standarta na nih. YAzyk
S++ eshche dostatochno molod, i ego biblioteki ne razvilis' v takoj stepeni,
kak sam yazyk.
Ponyatie ne sushchestvuet v vakuume, vokrug nego vsegda gruppiruyutsya
svyazannye s nim ponyatiya. Opredelit' v programme vzaimootnosheniya klassov,
inymi slovami, ustanovit' tochnye svyazi mezhdu ispol'zuemymi v zadache
ponyatiyami, byvaet trudnee, chem opredelit' kazhdyj iz klassov sam po sebe. V
rezul'tate ne dolzhno poluchit'sya "kashi" - kogda kazhdyj klass (ponyatie)
zavisit ot vseh ostal'nyh. Pust' est' dva klassa A i B. Togda svyazi mezhdu
nimi tipa "A vyzyvaet funkciyu iz B", "A sozdaet ob容kty B", "A imeet chlen
tipa B" obychno ne vyzyvayut kakih-libo trudnostej. Svyazi zhe tipa "A
ispol'zuet dannye iz B", kak pravilo, mozhno voobshche isklyuchit'.
Odno iz samyh moshchnyh intellektual'nyh sredstv, pozvolyayushchih spravit'sya
so slozhnost'yu, - eto ierarhicheskoe uporyadochenie, t.e. uporyadochenie
svyazannyh mezhdu soboj ponyatij v drevovidnuyu strukturu, v kotoroj samoe
obshchee ponyatie nahoditsya v korne dereva. CHasto udaetsya organizovat' klassy
programmy kak mnozhestvo derev'ev ili kak napravlennyj aciklichnyj graf. |to
oznachaet, chto programmist opredelyaet nabor bazovyh klassov, kazhdyj iz
kotoryh imeet svoe mnozhestvo proizvodnyh klassov. Nabor operacij samogo
obshchego vida dlya bazovyh klassov (ponyatij) obychno opredelyaetsya s pomoshch'yu
virtual'nyh funkcij ($$6.5). Interpretaciya etih operacij, po mere
nadobnosti, mozhet utochnyat'sya dlya kazhdogo konkretnogo sluchaya, t.e. dlya
kazhdogo proizvodnogo klassa.
Estestvenno, est' ogranicheniya i pri takoj organizacii programmy.
Inogda ispol'zuemye v programme ponyatiya ne udaetsya uporyadochit' dazhe s
pomoshch'yu napravlennogo aciklichnogo grafa. Nekotorye ponyatiya okazyvayutsya po
svoej prirode vzaimosvyazannymi. Ciklicheskie zavisimosti ne vyzovut
problem, esli mnozhestvo vzaimosvyazannyh klassov nastol'ko malo, chto v nem
legko razobrat'sya. Dlya predstavleniya na S++ mnozhestva vzaimozavisimyh
klassov mozhno ispol'zovat' druzhestvennye klassy ($$5.4.1).
Esli ponyatiya programmy nel'zya uporyadochit' v vide dereva ili
napravlennogo aciklichnogo grafa, a mnozhestvo vzaimozavisimyh ponyatij ne
poddaetsya lokalizacii, to, po vsej vidimosti, vy popali v takoe
zatrudnitel'noe polozhenie, vyjti iz kotorogo ne smozhet pomoch' ni odin iz
yazykov programmirovaniya. Esli vam ne udalos' dostatochno prosto
sformulirovat' svyazi mezhdu osnovnymi ponyatiyami zadachi, to, skoree vsego,
vam ne udastsya ee zaprogrammirovat'.
Eshche odin sposob vyrazheniya obshchnosti ponyatij v yazyke predostavlyayut
shablony tipa. SHablonnyj klass zadaet celoe semejstvo klassov. Naprimer,
shablonnyj klass spisok zadaet klassy vida "spisok ob容ktov T", gde T mozhet
byt' proizvol'nym tipom. Takim obrazom, shablonnyj tip ukazyvaet, kak
poluchaetsya novyj tip iz zadannogo v kachestve parametra. Samye tipichnye
shablonnye klassy - eto kontejnery, v chastnosti, spiski, massivy i
associativnye massivy.
Napomnim, chto mozhno legko i prosto zaprogrammirovat' mnogie zadachi,
ispol'zuya tol'ko prostye tipy, struktury dannyh, obychnye funkcii i
neskol'ko klassov iz standartnyh bibliotek. Ves' apparat postroeniya novyh
tipov sleduet privlekat' tol'ko togda, kogda on dejstvitel'no neobhodim.
Vopros "Kak napisat' horoshuyu programmu na S++?" ochen' pohozh na vopros
"Kak pishetsya horoshaya anglijskaya proza?". Na nego est' dva otveta: "Nuzhno
znat', chto vy, sobstvenno, hotite napisat'" i "Praktika i podrazhanie
horoshemu stilyu". Oba soveta prigodny dlya S++ v toj zhe mere, chto i dlya
anglijskogo yazyka, i oboim dostatochno trudno sledovat'.
Neskol'ko poleznyh sovetov
Nizhe predstavlen "svod pravil", kotoryj stoit uchityvat' pri izuchenii
S++. Kogda vy stanete bolee opytnymi, to na baze etih pravil smozhete
sformulirovat' svoi sobstvennye, kotorye budut bolee podhodit' dlya vashih
zadach i bolee sootvetstvovat' vashemu stilyu programmirovaniya. Soznatel'no
vybrany ochen' prostye pravila, i v nih opushcheny podrobnosti. Ne sleduet
vosprinimat' ih slishkom bukval'no. Horoshaya programma trebuet i uma, i
vkusa, i terpeniya. S pervogo raza obychno ona ne poluchaetsya, poetomu
eksperimentirujte! Itak, svod pravil.
[1] Kogda vy pishite programmu, to sozdaete konkretnye predstavleniya
teh ponyatij, kotorye ispol'zovalis' v reshenii postavlennoj
zadachi. Struktura programmy dolzhna otrazhat' eti ponyatiya nastol'ko
yavno, naskol'ko eto vozmozhno.
[a] Esli vy schitaete "nechto" otdel'nym ponyatiem, to sdelajte ego
klassom.
[b] Esli vy schitaete "nechto" sushchestvuyushchim nezavisimo, to sdelajte
ego ob容ktom nekotorogo klassa.
[c] Esli dva klassa imeyut nechto sushchestvennoe, i ono yavlyaetsya dlya nih
obshchim, to vyrazite etu obshchnost' s pomoshch'yu bazovogo klassa.
[d] Esli klass yavlyaetsya kontejnerom nekotoryh ob容ktov, sdelajte
ego shablonnym klassom.
[2] Esli opredelyaetsya klass, kotoryj ne realizuet matematicheskih ob容ktov
vrode matric ili kompleksnyh chisel i ne yavlyaetsya tipom nizkogo
urovnya napodobie svyazannogo spiska, to:
[a] Ne ispol'zujte global'nyh dannyh.
[b] Ne ispol'zujte global'nyh funkcij (ne chlenov).
[c] Ne ispol'zujte obshchih dannyh-chlenov.
[d] Ne ispol'zujte funkcii friend (no tol'ko dlya togo, chtoby
izbezhat' [a], [b] ili [c]).
[e] Ne obrashchajtes' k dannym-chlenam drugogo ob容kta neposredstvenno.
[f] Ne zavodite v klasse "pole tipa"; ispol'zujte virtual'nye
funkcii.
[g] Ispol'zujte funkcii-podstanovki tol'ko kak sredstvo
znachitel'noj optimizacii.
Zamechanie dlya programmistov na S
CHem luchshe programmist znaet S, tem trudnee budet dlya nego pri
programmirovanii na S++ otojti ot stilya programmirovaniya na S. Tak on
teryaet potencial'nye preimushchestva S++. Poetomu sovetuem prosmotret' razdel
"Otlichiya ot S" v spravochnom rukovodstve ($$R.18). Zdes' my tol'ko ukazhem
na te mesta, v kotoryh ispol'zovanie dopolnitel'nyh vozmozhnostej S++
privodit k luchshemu resheniyu, chem programmirovanie na chistom S. Makrokomandy
prakticheski ne nuzhny v S++: ispol'zujte const ($$2.5) ili enum ($$2.5.1),
chtoby opredelit' poimenovannye konstanty; ispol'zujte inline ($$4.6.2),
chtoby izbezhat' rashodov resursov, svyazannyh s vyzovom funkcij; ispol'zujte
shablony tipa ($$8), chtoby zadat' semejstvo funkcij i tipov. Ne opisyvajte
peremennuyu, poka ona dejstvitel'no vam ne ponadobitsya, a togda ee mozhno
srazu inicializirovat', ved' v S++ opisanie mozhet poyavlyat'sya v lyubom
meste, gde dopustim operator. Ne ispol'zujte malloc(), etu operaciyu luchshe
realizuet new ($$3.2.6). Ob容dineniya nuzhny ne stol' chasto, kak v S,
poskol'ku al'ternativnost' v strukturah realizuetsya s pomoshch'yu proizvodnyh
klassov. Starajtes' obojtis' bez ob容dinenij, no esli oni vse-taki nuzhny,
ne vklyuchajte ih v osnovnye interfejsy; ispol'zujte bezymyannye ob容dineniya
($$2.6.2). Starajtes' ne ispol'zovat' ukazatelej tipa void*,
arifmeticheskih operacij s ukazatelyami, massivov v stile S i operacij
privedeniya. Esli vse-taki vy ispol'zuete eti konstrukcii, upryatyvajte ih
dostatochno nadezhno v kakuyu-nibud' funkciyu ili klass. Ukazhem, chto
svyazyvanie v stile S vozmozhno dlya funkcii na S++, esli ona opisana so
specifikaciej extern "C" ($$4.4).
No gorazdo vazhnee starat'sya dumat' o programme kak o mnozhestve
vzaimosvyazannyh ponyatij, predstavlyaemyh klassami i ob容ktami, chem
predstavlyat' ee kak summu struktur dannyh i funkcij, chto-to delayushchih s
etimi dannymi.
Spisok literatury
V knige nemnogo neposredstvennyh ssylok na literaturu. Zdes' priveden
spisok knig i statej, na kotorye est' pryamye ssylki, a takzhe teh, kotorye
tol'ko upominayutsya.
[1] A.V.Aho, J.E.Hopcroft, and J.D.Ulman: Data Structures and
Algoritms. Addison-Wesley, Reading, Massachusetts. 1983.
[2] O-J.Dahl, B.Myrhaug, and K.Nugaard: SIMULA Common Base Language.
Norwegian Computing Ctnter S-22. Oslo, Norway. 1970
[3] O-J.Dahl and C.A.R.Hoare: Hierarhical Program Construction in
Structured Programming. Academic Press, New York. 1972. pp. 174-220.
[4] Margaret A.Ellis and Bjarne Stroustrup: The Annotated C++
Reference Manual. Addison-Wesley, Reading, Massachusetts. 1990.
[5] A.Goldberg and D.Rodson: SMALLTALK-80 - The Language and Its
Implementation. Addison-Wesley, Reading, Massachusetts. 1983.
[6] R.E.Griswold et.al.: The Snobol14 Programming Language.
Prentice-Hall, Englewood Cliffs, New Jersy, 1970.
[7] R.E.Griswold and M.T.Griswold: The ICON Programming Language.
Prentice-Hall, Englewood Cliffs, New Jersy. 1983.
[8] Brian W.Kernighan and Dennis M.Ritchie: The C Programming
Language. Prentice-Hall, Englewood Cliffs, New Jersy. 1978. Second
edition 1988.
[9] Andrew Koenig and Bjarne Stroustrup: C++: As Close to C as
possible - but no closer. The C++ Report. Vol.1 No.7. July 1989.
[10] Andrew Koenig and Bjarne Stroustrup: Exception Handling for C++
(revised). Proc USENIX C++ Conference, April 1990. Also, Journal of Object
Oriented Programming, Vol.3 No.2, July/August 1990. pp.16-33.
[11] Barbara Liskov et.al.: CLU Reference Manual. MIT/LCS/TR-225.
[12] George Orwell: 1984. Secker and Warburg, London. 1949.
[13] Martin Richards and Colin Whitby-Strevens: BCPL - The Language
and Its Compiler. Cambridge University Press. 1980.
[14] L.Rosler: The Evolution of C - Past and Future. AT&T Bell
Laboratories Technical Journal. Vol.63 No.8 Part 2. October 1984.
pp.1685-1700.
[15] Ravi Sethi: Uniform Syntax for Type Expressions and Declarations.
Software Practice & Experience, Vol.11. 1981. pp.623-628.
[16] Bjarne Stroustrup: Adding Classes to C: An Exercise in Language
Evolution. Software Practice & Experience, Vol.13. 1983. pp.139-61.
[17] Bjarne Stroustrup: The C++ Programming Language. Addison-Wesley.
1986.
[18] Bjarne Stroustrup: Multiple Inheritance for C++. Proc. EUUG
Spring Conference, May 1987. Also USENIX Computer Systems, Vol.2 No 4,
Fall 1989.
[19] Bjarne Stroustrup and Jonathan Shopiro: A Set of C classes for
Co-Routine Style Programming. Proc. USENIX C++ conference, Santa Fe.
November 1987. pp.417-439.
[20] Bjarne Stroustrup: Type-safe Linkage for C++. USENIX Computer
Systems, Vol.1 No.4 Fall 1988.
[21] Bjurne Stroustrup: Parameterized Type for C++. Proc. USENIX C++
Conference, Denver, October 1988. pp.1-18. Also, USENIX Computer Systems,
Vol.2 No.1 Winter 1989.
[22] Bjarne Stroustrup: Standardizing C++. The C++ Report. Vol.1
No.1. January 1989.
[23] Bjarne Stroustrup: The Evolution of C++: 1985-1989. USENIX
Computer Systems, Vol.2 No.3. Summer 1989.
[24] P.M.Woodward and S.G.Bond: Algol 68-R Users Guide. Her Majesty's
Stationery Office, London. 1974.
[25] UNIX Time-Sharing System: Programmer's Manual. Research Version,
Tenth Edition. AT&T Bell Laboratories, Murray Hill, New Jersy, February
1985.
[26] Aake Wilkstroem: Functional Programming Using ML. Prentice-Hall,
Englewood Cliffs, New Jersy. 1987.
[27] X3 Secretariat: Standard - The C Language. X3J11/90-013.
Computer and Business Equipment Manufactures Association, 311 First
Street, NW, Suite 500, Washington, DC 20001, USA.
Ssylki na istochniki po proektirovaniyu i razvitiyu bol'shih sistem
programmnogo obespecheniya mozhno najti v konce glavy 11.
"Nachnem s togo, chto vzdernem
vseh etih zakonnikov, yazykovedov".
("Korol' Genrih VI", dejstvie II)
V etoj glave soderzhitsya kratkij obzor osnovnyh koncepcij i konstrukcij
yazyka S++. On sluzhit dlya beglogo znakomstva s yazykom. Podrobnoe opisanie
vozmozhnostej yazyka i metodov programmirovaniya na nem daetsya v sleduyushchih
glavah. Razgovor vedetsya v osnovnom vokrug abstrakcii dannyh i
ob容ktno-orientirovannogo programmirovaniya, no perechislyayutsya i osnovnye
vozmozhnosti procedurnogo programmirovaniya.
YAzyk programmirovaniya S++ zadumyvalsya kak yazyk, kotoryj budet:
- luchshe yazyka S;
- podderzhivat' abstrakciyu dannyh;
- podderzhivat' ob容ktno-orientirovannoe programmirovanie.
V etoj glave ob座asnyaetsya smysl etih fraz bez podrobnogo opisaniya
konstrukcij yazyka.
$$1.2 soderzhit neformal'noe opisanie razlichij "procedurnogo",
"modul'nogo" i "ob容ktno-orientirovannogo" programmirovaniya. Privedeny
konstrukcii yazyka, kotorye sushchestvenny dlya kazhdogo iz perechislennyh stilej
programmirovaniya. Svojstvennyj S stil' programmirovaniya obsuzhdaetsya v
razdelah "procedurnoe programmirovanie i "modul'noe programmirovanie".
YAzyk S++ - "luchshij variant S". On luchshe podderzhivaet takoj stil'
programmirovaniya, chem sam S, prichem eto delaetsya bez poteri kakoj-libo
obshchnosti ili effektivnosti po sravneniyu s S. V to zhe vremya yazyk C
yavlyaetsya podmnozhestvom S++. Abstrakciya dannyh i ob容ktno-orientirovannoe
programmirovanie rassmatrivayutsya kak "podderzhka abstrakcii dannyh" i
"podderzhka ob容ktno- orientirovannogo programmirovaniya". Pervaya baziruetsya
na vozmozhnosti opredelyat' novye tipy i rabotat' s nimi, a vtoraya - na
vozmozhnosti zadavat' ierarhiyu tipov.
$$1.3 soderzhit opisanie osnovnyh konstrukcij dlya procedurnogo i
modul'nogo programmirovaniya. V chastnosti, opredelyayutsya funkcii, ukazateli,
cikly, vvod-vyvod i ponyatie programmy kak sovokupnosti razdel'no
transliruemyh modulej. Podrobno eti vozmozhnosti opisany v glavah 2, 3 i 4.
$$1.4 soderzhit opisanie sredstv, prednaznachennyh dlya effektivnoj
realizacii abstrakcii dannyh. V chastnosti, opredelyayutsya klassy, prostejshij
mehanizm kontrolya dostupa, konstruktory i destruktory, peregruzka
operacij, preobrazovaniya pol'zovatel'skih tipov, obrabotka osobyh situacij
i shablony tipov. Podrobno eti vozmozhnosti opisany v glavah 5, 7, 8 i 9.
$$1.5 soderzhit opisanie sredstv podderzhki ob容ktno-orientirovannogo
programmirovaniya. V chastnosti, opredelyayutsya proizvodnye klassy i
virtual'nye funkcii, obsuzhdayutsya nekotorye voprosy realizacii. Vse eto
podrobno izlozheno v glave 6.
$$1.6 soderzhit opisanie opredelennyh ogranichenij na puti
sovershenstvovaniya kak yazykov programmirovaniya obshchego naznacheniya voobshche,
tak i S++ v chastnosti. |ti ogranicheniya svyazany s effektivnost'yu, s
protivorechashchimi drug drugu trebovaniyami raznyh oblastej prilozheniya,
problemami obucheniya i neobhodimost'yu translyacii i vypolneniya programm v
staryh sistemah.
Esli kakoj-to razdel okazhetsya dlya vas neponyatnym, nastoyatel'no
sovetuem prochitat' sootvetstvuyushchie glavy, a zatem, oznakomivshis' s
podrobnym opisaniem osnovnyh konstrukcij yazyka, vernut'sya k etoj glave.
Ona nuzhna dlya togo, chtoby mozhno bylo sostavit' obshchee predstavlenie o
yazyke. V nej nedostatochno svedenij, chtoby nemedlenno nachat'
programmirovat'.
1.2 Paradigmy programmirovaniya
Ob容ktno-orientirovannoe programmirovanie - eto metod
programmirovaniya, sposob napisaniya "horoshih" programm dlya mnozhestva zadach.
Esli etot termin imeet kakoj-to smysl, to on dolzhen podrazumevat': takoj
yazyk programmirovaniya, kotoryj predostavlyaet horoshie vozmozhnosti dlya
ob容ktno-orientirovannogo stilya programmirovaniya.
Zdes' sleduet ukazat' na vazhnye razlichiya. Govoryat, chto yazyk
podderzhivaet nekotoryj stil' programmirovaniya, esli v nem est' takie
vozmozhnosti, kotorye delayut programmirovanie v etom stile udobnym
(dostatochno prostym, nadezhnym i effektivnym). YAzyk ne podderzhivaet
nekotoryj stil' programmirovaniya, esli trebuyutsya bol'shie usiliya ili dazhe
iskusstvo, chtoby napisat' programmu v etom stile. Odnako eto ne oznachaet,
chto yazyk zapreshchaet pisat' programmy v etom stile. Dejstvitel'no, mozhno
pisat' strukturnye programmy na Fortrane i ob容ktno-orientirovannye
programmy na S, no eto budet pustoj tratoj sil, poskol'ku dannye yazyki ne
podderzhivayut ukazannyh stilej programmirovaniya.
Podderzhka yazykom opredelennoj paradigmy (stilya) programmirovaniya yavno
proyavlyaetsya v konkretnyh yazykovyh konstrukciyah, rasschitannyh na nee. No
ona mozhet proyavlyat'sya v bolee tonkoj, skrytoj forme, kogda otklonenie ot
paradigmy diagnostiruetsya na stadii translyacii ili vypolneniya programmy.
Samyj ochevidnyj primer - eto kontrol' tipov. Krome togo, yazykovaya
podderzhka paradigmy mozhet dopolnyat'sya proverkoj na odnoznachnost' i
dinamicheskim kontrolem. Podderzhka mozhet predostavlyat'sya i pomimo samogo
yazyka, naprimer, standartnymi bibliotekami ili sredoj programmirovaniya.
Nel'zya skazat', chto odin yazyk luchshe drugogo tol'ko potomu, chto v nem
est' vozmozhnosti, kotorye v drugom otsutstvuyut. CHasto byvaet kak raz
naoborot. Zdes' bolee vazhno ne to, kakimi vozmozhnostyami obladaet yazyk, a
to, naskol'ko imeyushchiesya v nem vozmozhnosti podderzhivayut izbrannyj stil'
programmirovaniya dlya opredelennogo kruga zadach. Poetomu mozhno
sformulirovat' sleduyushchie trebovaniya k yazyku:
[1] Vse konstrukcii yazyka dolzhny estestvenno i elegantno opredelyat'sya
v nem.
[2] Dlya resheniya opredelennoj zadachi dolzhna byt' vozmozhnost'
ispol'zovat' sochetaniya konstrukcij, chtoby izbezhat' neobhodimosti vvodit'
dlya etoj celi novuyu konstrukciyu.
[3] Dolzhno byt' minimal'noe chislo neochevidnyh konstrukcij special'nogo
naznacheniya.
[4] Konstrukciya dolzhna dopuskat' takuyu realizaciyu, chtoby v
neispol'zuyushchej ee programme ne vozniklo dopolnitel'nyh rashodov.
[5] Pol'zovatelyu dostatochno znat' tol'ko to mnozhestvo konstrukcij,
kotoroe neposredstvenno ispol'zuetsya v ego programme.
Pervoe trebovanie apelliruet k logike i esteticheskomu vkusu. Dva
sleduyushchih vyrazhayut princip minimal'nosti. Dva poslednih mozhno inache
sformulirovat' tak: "to, chego vy ne znaete, ne smozhet nanesti vam vreda".
S uchetom ogranichenij, ukazannyh v etih pravilah, yazyk S++
proektirovalsya dlya podderzhki abstrakcii dannyh i ob容ktno-orientirovannogo
programmirovaniya v dobavlenie k tradicionnomu stilyu S. Vprochem, eto ne
znachit, chto yazyk trebuet kakogo-to odnogo stilya programmirovaniya ot vseh
pol'zovatelej.
Teper' perejdem k konkretnym stilyam programmirovaniya i posmotrim
kakovy osnovnye konstrukcii yazyka, ih podderzhivayushchie. My ne sobiraemsya
davat' polnoe opisanie etih konstrukcij.
1.2.1 Procedurnoe programmirovanie
Pervonachal'noj (i, vozmozhno, naibolee ispol'zuemoj) paradigmoj
programmirovaniya bylo:
Opredelite, kakie procedury vam nuzhny; ispol'zujte luchshie iz izvestnyh
vam algoritmov!
Udarenie delalos' na obrabotku dannyh s pomoshch'yu algoritma,
proizvodyashchego nuzhnye vychisleniya. Dlya podderzhki etoj paradigmy yazyki
predostavlyali mehanizm peredachi parametrov i polucheniya rezul'tatov
funkcij. Literatura, otrazhayushchaya takoj podhod, zapolnena rassuzhdeniyami o
sposobah peredachi parametrov, o tom, kak razlichat' parametry raznyh tipov,
o razlichnyh vidah funkcij (procedury, podprogrammy, makrokomandy, ...) i
t.d. Pervym procedurnym yazykom byl Fortran, a Algol60, Algol68, Paskal' i
S prodolzhili eto napravlenie.
Tipichnym primerom horoshego stilya v takom ponimanii mozhet sluzhit'
funkciya izvlecheniya kvadratnogo kornya. Dlya zadannogo parametra ona vydaet
rezul'tat, kotoryj poluchaetsya s pomoshch'yu ponyatnyh matematicheskih operacij:
double sqrt ( double arg )
{
// programma dlya vychisleniya kvadratnogo kornya
}
voide some_function ()
{
double root = sqrt ( 2 );
// ..
}
Dvojnaya naklonnaya cherta // nachinaet kommentarij, kotoryj prodolzhaetsya
do konca stroki.
Pri takoj organizacii programmy funkcii vnosyat opredelennyj poryadok v
haos razlichnyh algoritmov.
1.2.2 Modul'noe programmirovanie
So vremenem pri v proektirovanii programm akcent smestilsya s
organizacii procedur na organizaciyu struktur dannyh. Pomimo vsego prochego
eto vyzvano i rostom razmerov programm. Modulem obychno nazyvayut
sovokupnost' svyazannyh procedur i teh dannyh, kotorymi oni upravlyayut.
Paradigma programmirovaniya priobrela vid:
Opredelite, kakie moduli nuzhny; podelite programmu tak, chtoby dannye
byli skryty v etih modulyah
|ta paradigma izvestna takzhe kak "princip sokrytiya dannyh". Esli v
yazyke net vozmozhnosti sgruppirovat' svyazannye procedury vmeste s dannymi,
to on ploho podderzhivaet modul'nyj stil' programmirovaniya. Teper' metod
napisaniya "horoshih" procedur primenyaetsya dlya otdel'nyh procedur modulya.
Tipichnyj primer modulya - opredelenie steka. Zdes' neobhodimo reshit' takie
zadachi:
[1] Predostavit' pol'zovatelyu interfejs dlya steka (naprimer, funkcii
push () i pop ()).
[2] Garantirovat', chto predstavlenie steka (naprimer, v vide massiva
elementov) budet dostupno lish' cherez interfejs pol'zovatelya.
[3] Obespechivat' inicializaciyu steka pered pervym ego ispol'zovaniem.
YAzyk Modula-2 pryamo podderzhivaet etu paradigmu, togda kak S tol'ko
dopuskaet takoj stil'. Nizhe predstavlen na S vozmozhnyj vneshnij interfejs
modulya, realizuyushchego stek:
// opisanie interfejsa dlya modulya,
// realizuyushchego stek simvolov:
void push ( char );
char pop ();
const int stack_size = 100;
Dopustim, chto opisanie interfejsa nahoditsya v fajle stack.h, togda
realizaciyu steka mozhno opredelit' sleduyushchim obrazom:
#include "stack.h" // ispol'zuem interfejs steka
static char v [ stack_size ]; // ``static'' oznachaet lokal'nyj
// v dannom fajle/module
static char * p = v; // stek vnachale pust
void push ( char c )
{
//proverit' na perepolnenie i pomestit' v stek
}
char pop ()
{
//proverit', ne pust li stek, i schitat' iz nego
}
Vpolne vozmozhno, chto realizaciya steka mozhet izmenit'sya, naprimer, esli
ispol'zovat' dlya hraneniya svyazannyj spisok. Pol'zovatel' v lyubom sluchae ne
imeet neposredstvennogo dostupa k realizacii: v i p - staticheskie
peremennye, t.e. peremennye lokal'nye v tom module (fajle), v kotorom oni
opisany. Ispol'zovat' stek mozhno tak:
#include "stack.h" // ispol'zuem interfejs steka
void some_function ()
{
push ( 'c' );
char c = pop ();
if ( c != 'c' ) error ( "nevozmozhno" );
}
Poskol'ku dannye est' edinstvennaya veshch', kotoruyu hotyat skryvat',
ponyatie upryatyvaniya dannyh trivial'no rasshiryaetsya do ponyatiya upryatyvaniya
informacii, t.e. imen peremennyh, konstant, funkcij i tipov, kotorye tozhe
mogut byt' lokal'nymi v module. Hotya S++ i ne prednaznachalsya special'no
dlya podderzhki modul'nogo programmirovaniya, klassy podderzhivayut koncepciyu
modul'nosti ($$5.4.3 i $$5.4.4). Pomimo etogo S++, estestvenno, imeet uzhe
prodemonstrirovannye vozmozhnosti modul'nosti, kotorye est' v S, t.e.
predstavlenie modulya kak otdel'noj edinicy translyacii.
Modul'noe programmirovanie predpolagaet gruppirovku vseh dannyh odnogo
tipa vokrug odnogo modulya, upravlyayushchego etim tipom. Esli potrebuyutsya steki
dvuh raznyh vidov, mozhno opredelit' upravlyayushchij imi modul' s takim
interfejsom:
class stack_id { /* ... */ }; // stack_id tol'ko tip
// nikakoj informacii o stekah
// zdes' ne soderzhitsya
stack_id create_stack ( int size ); // sozdat' stek i vozvratit'
// ego identifikator
void push ( stack_id, char );
char pop ( stack_id );
destroy_stack ( stack_id ); // unichtozhenie steka
Konechno takoe reshenie namnogo luchshe, chem haos, svojstvennyj
tradicionnym, nestrukturirovannym resheniyam, no modeliruemye takim sposobom
tipy sovershenno ochevidno otlichayutsya ot "nastoyashchih", vstroennyh. Kazhdyj
upravlyayushchij tipom modul' dolzhen opredelyat' svoj sobstvennyj algoritm
sozdaniya "peremennyh" etogo tipa. Ne sushchestvuet universal'nyh pravil
prisvaivaniya identifikatorov, oboznachayushchih ob容kty takogo tipa. U
"peremennyh" takih tipov ne sushchestvuet imen, kotorye byli by izvestny
translyatoru ili drugim sistemnym programmam, i eti "peremennye" ne
podchinyayutsya obychnym pravilam oblastej vidimosti i peredachi parametrov.
Tip, realizuemyj upravlyayushchim im modulem, po mnogim vazhnym aspektam
sushchestvenno otlichaetsya ot vstroennyh tipov. Takie tipy ne poluchayut toj
podderzhki so storony translyatora (raznogo vida kontrol'), kotoraya
obespechivaetsya dlya vstroennyh tipov. Problema zdes' v tom, chto programma
formuliruetsya v terminah nebol'shih (odno-dva slova) deskriptorov ob容ktov,
a ne v terminah samih ob容ktov ( stack_id mozhet sluzhit' primerom takogo
deskriptora). |to oznachaet, chto translyator ne smozhet otlovit' glupye,
ochevidnye oshibki, vrode teh, chto dopushcheny v privedennoj nizhe funkcii:
void f ()
{
stack_id s1;
stack_id s2;
s1 = create_stack ( 200 );
// oshibka: zabyli sozdat' s2
push ( s1,'a' );
char c1 = pop ( s1 );
destroy_stack ( s2 ); // nepriyatnaya oshibka
// oshibka: zabyli unichtozhit' s1
s1 = s2; // eto prisvaivanie yavlyaetsya po suti
// prisvaivaniem ukazatelej,
// no zdes' s2 ispol'zuetsya posle unichtozheniya
}
Inymi slovami, koncepciya modul'nosti, podderzhivayushchaya paradigmu
upryatyvaniya dannyh, ne zapreshchaet takoj stil' programmirovaniya, no i ne
sposobstvuet emu.
V yazykah Ada, Clu, S++ i podobnyh im eta trudnost' preodolevaetsya
blagodarya tomu, chto pol'zovatelyu razreshaetsya opredelyat' svoi tipy, kotorye
traktuyutsya v yazyke prakticheski tak zhe, kak vstroennye. Takie tipy obychno
nazyvayut abstraktnymi tipami dannyh, hotya luchshe, pozhaluj, ih nazyvat'
prosto pol'zovatel'skimi. Bolee strogim opredeleniem abstraktnyh tipov
dannyh bylo by ih matematicheskoe opredelenie. Esli by udalos' ego dat',
to, chto my nazyvaem v programmirovanii tipami, bylo by konkretnym
predstavleniem dejstvitel'no abstraktnyh sushchnostej. Kak opredelit' "bolee
abstraktnye" tipy, pokazano v $$4.6. Paradigmu zhe programmirovaniya mozhno
vyrazit' teper' tak:
Opredelite, kakie tipy vam nuzhny; predostav'te polnyj nabor operacij
dlya kazhdogo tipa.
Esli net neobhodimosti v raznyh ob容ktah odnogo tipa, to stil'
programmirovaniya, sut' kotorogo svoditsya k upryatyvaniyu dannyh, i
sledovanie kotoromu obespechivaetsya s pomoshch'yu koncepcii modul'nosti, vpolne
adekvaten etoj paradigme.
Arifmeticheskie tipy, podobnye tipam racional'nyh i kompleksnyh chisel,
yavlyayutsya tipichnymi primerami pol'zovatel'skih tipov:
class complex
{
double re, im;
public:
complex(double r, double i) { re=r; im=i; }
complex(double r) // preobrazovanie float->complex
{ re=r; im=0; }
friend complex operator+(complex, complex);
friend complex operator-(complex, complex); // vychitanie
friend complex operator-(complex) // unarnyj minus
friend complex operator*(complex, complex);
friend complex operator/(complex, complex);
// ...
};
Opisanie klassa (t.e. opredelyaemogo pol'zovatelem tipa) complex zadaet
predstavlenie kompleksnogo chisla i nabor operacij s kompleksnymi chislami.
Predstavlenie yavlyaetsya chastnym (private): re i im dostupny tol'ko dlya
funkcij, ukazannyh v opisanii klassa complex. Podobnye funkcii mogut byt'
opredeleny tak:
complex operator + ( complex a1, complex a2 )
{
return complex ( a1.re + a2.re, a1.im + a2.im );
}
i ispol'zovat'sya sleduyushchim obrazom:
void f ()
{
complex a = 2.3;
complex b = 1 / a;
complex c = a + b * complex ( 1, 2.3 );
// ...
c = - ( a / b ) + 2;
}
Bol'shinstvo modulej (hotya i ne vse) luchshe opredelyat' kak
pol'zovatel'skie tipy.
1.2.4 Predely abstrakcii dannyh
Abstraktnyj tip dannyh opredelyaetsya kak nekij "chernyj yashchik". Posle
svoego opredeleniya on po suti nikak ne vzaimodejstvuet s programmoj. Ego
nikak nel'zya prisposobit' dlya novyh celej, ne menyaya opredeleniya. V etom
smysle eto negibkoe reshenie. Pust', naprimer, nuzhno opredelit' dlya
graficheskoj sistemy tip shape (figura). Poka schitaem, chto v sisteme mogut
byt' takie figury: okruzhnost' (circle), treugol'nik (triangle) i kvadrat
(square). Pust' uzhe est' opredeleniya tochki i cveta:
class point { /* ... */ };
class color { /* ... */ };
Tip shape mozhno opredelit' sleduyushchim obrazom:
enum kind { circle, triangle, square };
class shape
{
point center;
color col;
kind k;
// predstavlenie figury
public:
point where () { return center; }
void move ( point to ) { center = to; draw (); }
void draw ();
void rotate ( int );
// eshche nekotorye operacii
};
"Pole tipa" k neobhodimo dlya togo, chtoby takie operacii, kak draw () i
rotate (), mogli opredelyat', s kakoj figuroj oni imeyut delo (v yazykah
vrode Paskalya mozhno ispol'zovat' dlya etogo zapis' s variantami, v kotoroj
k yavlyaetsya polem-deskriminantom). Funkciyu draw () mozhno opredelit' tak:
void shape :: draw ()
{
switch ( k )
{
case circle:
// risovanie okruzhnosti
break;
case triangle:
// risovanie treugol'nika
break;
case square:
// risovanie kvadrata
break;
}
}
|to ne funkciya, a koshmar. V nej nuzhno uchest' vse vozmozhnye figury,
kakie tol'ko est'. Poetomu ona dopolnyaetsya novymi operatorami, kak tol'ko
v sisteme poyavlyaetsya novaya figura. Ploho to, chto posle opredeleniya novoj
figury nuzhno proverit' i, vozmozhno, izmenit' vse starye operacii klassa.
Poetomu, esli vam nedostupen ishodnyj tekst kazhdoj operacii klassa, vvesti
novuyu figuru v sistemu prosto nevozmozhno. Poyavlenie lyuboj novoj figury
privodit k manipulyaciyam s tekstom kazhdoj sushchestvennoj operacii klassa.
Trebuetsya dostatochno vysokaya kvalifikaciya, chtoby spravit'sya s etoj
zadachej, no vse ravno mogut poyavit'sya oshibki v uzhe otlazhennyh chastyah
programmy, rabotayushchih so starymi figurami. Vozmozhnost' vybora
predstavleniya dlya konkretnoj figury sil'no suzhaetsya, esli trebovat', chtoby
vse ee predstavleniya ukladyvalis' v uzhe zadannyj format, specificirovannyj
obshchim opredeleniem figury (t.e. opredeleniem tipa shape).
1.2.5 Ob容ktno-orientirovannoe programmirovanie
Problema sostoit v tom, chto my ne razlichaem obshchie svojstva figur
(naprimer, figura imeet cvet, ee mozhno narisovat' i t.d.) i svojstva
konkretnoj figury (naprimer, okruzhnost' - eto takaya figura, kotoraya imeet
radius, ona izobrazhaetsya s pomoshch'yu funkcii, risuyushchej dugi i t.d.). Sut'
ob容ktno-orientirovannogo programmirovaniya v tom, chto ono pozvolyaet
vyrazhat' eti razlichiya i ispol'zuet ih. YAzyk, kotoryj imeet konstrukcii dlya
vyrazheniya i ispol'zovaniya podobnyh razlichij, podderzhivaet
ob容ktno-orientirovannoe programmirovanie. Vse drugie yazyki ne
podderzhivayut ego. Zdes' osnovnuyu rol' igraet mehanizm nasledovaniya,
zaimstvovannyj iz yazyka Simula. Vnachale opredelim klass, zadayushchij obshchie
svojstva vseh figur:
class shape
{
point center;
color col;
// ...
public:
point where () { return center; }
void move ( point to ) { center = to; draw(); }
virtual void draw ();
virtual void rotate ( int );
// ...
};
Te funkcii, dlya kotoryh mozhno opredelit' zayavlennyj interfejs, no
realizaciya kotoryh (t.e. telo s operatornoj chast'yu) vozmozhna tol'ko dlya
konkretnyh figur, otmecheny sluzhebnym slovom virtual (virtual'nye). V
Simule i S++ virtual'nost' funkcii oznachaet: "funkciya mozhet byt'
opredelena pozdnee v klasse, proizvodnom ot dannogo". S uchetom takogo
opredeleniya klassa mozhno napisat' obshchie funkcii, rabotayushchie s figurami:
void rotate_all ( shape v [], int size, int angle )
// povernut' vse elementy massiva "v" razmera "size"
// na ugol ravnyj "angle"
{
int i = 0;
while ( i<size )
{
v [ i ] . rotate ( angle );
i = i + 1;
}
}
Dlya opredeleniya konkretnoj figury sleduet ukazat', prezhde vsego, chto
eto - imenno figura i zadat' ee osobye svojstva (vklyuchaya i virtual'nye
funkcii):
class circle : public shape
{
int radius;
public:
void draw () { /* ... */ };
void rotate ( int ) {} // da, poka pustaya funkciya
};
V yazyke S++ klass circle nazyvaetsya proizvodnym po otnosheniyu k klassu
shape, a klass shape nazyvaetsya bazovym dlya klassa circle. Vozmozhna
drugaya terminologiya, ispol'zuyushchaya nazvaniya "podklass" i "superklass" dlya
klassov circle i shape sootvetstvenno. Teper' paradigma programmirovaniya
formuliruetsya tak:
Opredelite, kakoj klass vam neobhodim; predostav'te polnyj nabor
operacij dlya kazhdogo klassa; obshchnost' klassov vyrazite yavno s pomoshch'yu
nasledovaniya.
Esli obshchnost' mezhdu klassami otsutstvuet, vpolne dostatochno abstrakcii
dannyh. Naskol'ko primenimo ob容ktno-orientirovannoe programmirovanie dlya
dannoj oblasti prilozheniya opredelyaetsya stepen'yu obshchnosti mezhdu raznymi
tipami, kotoraya pozvolyaet ispol'zovat' nasledovanie i virtual'nye funkcii.
V nekotoryh oblastyah, takih, naprimer, kak interaktivnaya grafika, est'
shirokij prostor dlya ob容ktno-orientirovannogo programmirovaniya. V drugih
oblastyah, v kotoryh ispol'zuyutsya tradicionnye arifmeticheskie tipy i
vychisleniya nad nimi, trudno najti primenenie dlya bolee razvityh stilej
programmirovaniya, chem abstrakciya dannyh. Zdes' sredstva, podderzhivayushchie
ob容ktno-orientirovannoe programmirovanie, ochevidno, izbytochny.
Nahozhdenie obshchnosti sredi otdel'nyh tipov sistemy predstavlyaet soboj
netrivial'nyj process. Stepen' takoj obshchnosti zavisit ot sposoba
proektirovaniya sistemy. V processe proektirovaniya vyyavlenie obshchnosti
klassov dolzhno byt' postoyannoj cel'yu. Ona dostigaetsya dvumya sposobami:
libo proektirovaniem special'nyh klassov, ispol'zuemyh kak "kirpichi" pri
postroenii drugih, libo poiskom pohozhih klassov dlya vydeleniya ih obshchej
chasti v odin bazovyj klass.
S popytkami ob座asnit', chto takoe ob容ktno-orientirovannoe
programmirovanie, ne ispol'zuya konkretnyh konstrukcij yazykov
programmirovaniya, mozhno poznakomit'sya v rabotah [2] i [6], privedennyh v
spiske literatury v glave 11.
Itak, my ukazali, kakuyu minimal'nuyu podderzhku dolzhen obespechivat' yazyk
programmirovaniya dlya procedurnogo programmirovaniya, dlya upryatyvaniya
dannyh, abstrakcii dannyh i ob容ktno-orientirovannogo programmirovaniya.
Teper' neskol'ko podrobnee opishem sredstva yazyka, hotya i ne samye
sushchestvennye, no pozvolyayushchie bolee effektivno realizovat' abstrakciyu
dannyh i ob容ktno-orientirovannoe programmirovanie.
Minimal'naya podderzhka procedurnogo programmirovaniya vklyuchaet funkcii,
arifmeticheskie operacii, vybirayushchie operatory i cikly. Pomimo etogo dolzhny
byt' predostavleny operacii vvoda- vyvoda. Bazovye yazykovye sredstva S++
unasledoval ot S (vklyuchaya ukazateli), a operacii vvoda-vyvoda
predostavlyayutsya bibliotekoj. Samaya zachatochnaya koncepciya modul'nosti
realizuetsya s pomoshch'yu mehanizma razdel'noj translyacii.
1.3.1 Programma i standartnyj vyvod
Samaya malen'kaya programma na S++ vyglyadit tak:
main () { }
V etoj programme opredelyaetsya funkciya, nazyvaemaya main, kotoraya ne
imeet parametrov i nichego ne delaet. Figurnye skobki { i } ispol'zuyutsya v
S++ dlya gruppirovaniya operatorov. V dannom sluchae oni oboznachayut nachalo i
konec tela (pustogo) funkcii main. V kazhdoj programme na S++ dolzhna byt'
svoya funkciya main(), i programma nachinaetsya s vypolneniya etoj funkcii.
Obychno programma vydaet kakie-to rezul'taty. Vot programma, kotoraya
vydaet privetstvie Hello, World! (Vsem privet!):
#include <iostream.h>
int main ()
{
cout << "Hello, World!\n";
}
Stroka #include <iostream.h> soobshchaet translyatoru, chto nado vklyuchit' v
programmu opisaniya, neobhodimye dlya raboty standartnyh potokov vvoda-
vyvoda, kotorye nahodyatsya v iostream.h. Bez etih opisanij vyrazhenie
cout << "Hello, World!\n"
ne imelo by smysla. Operaciya << ("vydat'") zapisyvaet svoj vtoroj
parametr v pervyj parametr. V dannom sluchae stroka "Hello, World!\n"
zapisyvaetsya v standartnyj vyhodnoj potok cout. Stroka - eto
posledovatel'nost' simvolov, zaklyuchennaya v dvojnye kavychki. Dva simvola:
obratnoj drobnoj cherty \ i neposredstvenno sleduyushchij za nim - oboznachayut
nekotoryj special'nyj simvol. V dannom sluchae \n yavlyaetsya simvolom konca
stroki (ili perevoda stroki), poetomu on vydaetsya posle simvolov Hello,
world!
Celoe znachenie, vozvrashchaemoe funkciej main(), esli tol'ko ono est',
schitaetsya vozvrashchaemym sisteme znacheniem programmy. Esli nichego ne
vozvrashchaetsya, sistema poluchit kakoe-to "musornoe" znachenie.
Sredstva vvoda/vyvoda potokovoj biblioteki podrobno opisyvayutsya v
glave 10.
1.3.2 Peremennye i arifmeticheskie operacii
Kazhdoe imya i kazhdoe vyrazhenie obyazany imet' tip. Imenno tip opredelyaet
operacii, kotorye mogut vypolnyat'sya nad nimi. Naprimer, v opisanii
int inch;
govoritsya, chto inch imeet tip int, t.e. inch yavlyaetsya celoj
peremennoj.
Opisanie - eto operator, kotoryj vvodit imya v programmu. V opisanii
ukazyvaetsya tip imeni. Tip, v svoyu ochered', opredelyaet kak pravil'no
ispol'zovat' imya ili vyrazhenie.
Osnovnye tipy, naibolee priblizhennye k "apparatnoj real'nosti" mashiny,
takovy:
char
short
int
long
Oni predstavlyayut celye chisla. Sleduyushchie tipy:
float
double
long double
predstavlyayut chisla s plavayushchej tochkoj. Peremennaya tipa char imeet
razmer, nuzhnyj dlya hraneniya odnogo simvola na dannoj mashine (obychno eto
odin bajt). Peremennaya int imeet razmer, neobhodimyj dlya celoj arifmetiki
na dannoj mashine (obychno eto odno slovo).
Sleduyushchie arifmeticheskie operacii mozhno ispol'zovat' nad lyubym
sochetaniem perechislennyh tipov:
+ (plyus, unarnyj i binarnyj)
- (minus, unarnyj i binarnyj)
* (umnozhenie)
/ (delenie)
% (ostatok ot deleniya)
To zhe verno dlya operacij otnosheniya:
== (ravno)
!= (ne ravno)
< (men'she chem)
<= (men'she ili ravno)
>= (bol'she ili ravno)
Dlya operacij prisvaivaniya i arifmeticheskih operacij v S++ vypolnyayutsya
vse osmyslennye preobrazovaniya osnovnyh tipov, chtoby ih mozhno bylo
neogranichenno ispol'zovat' lyubye ih sochetaniya:
double d;
int i;
short s;
// ...
d = d + i;
i = s * i;
Simvol = oboznachaet obychnoe prisvaivanie.
1.3.3 Ukazateli i massivy
Massiv mozhno opisat' tak:
char v [ 10 ]; // massiv iz 10 simvolov
Opisanie ukazatelya imeet takoj vid:
char * p; // ukazatel' na simvol
Zdes' [] oznachaet "massiv iz", a simvol * oznachaet "ukazatel' na".
Znachenie nizhnej granicy indeksa dlya vseh massivov ravno nulyu, poetomu v
imeet 10 elementov: v [ 0 ] ... v [ 9 ]. Peremennaya tipa ukazatel' mozhet
soderzhat' adres ob容kta sootvetstvuyushchego tipa:
p = & v [ 3 ]; // p ukazyvaet na 4-j element massiva v
Unarnaya operaciya & oznachaet vzyatie adresa.
1.3.4 Uslovnye operatory i cikly
V S++ est' tradicionnyj nabor vybirayushchih operatorov i ciklov. Nizhe
privodyatsya primery operatorov if, switch i while.
V sleduyushchem primere pokazano preobrazovanie dyujma v santimetr i
obratno. Predpolagaetsya, chto vo vhodnom potoke znachenie v santimetrah
zavershaetsya simvolom i, a znachenie v dyujmah - simvolom c:
#include <iostream.h>
int main ()
{
const float fac = 2.54;
float x, in, cm;
char ch = 0;
cout << "enter length: ";
cin >> x; // vvod chisla s plavayushchej tochkoj
cin >> ch // vvod zavershayushchego simvola
if ( ch == 'i' )
{ // dyujm
in = x;
cm = x * fac;
}
else if ( ch == 'c' )
{ // santimetry
in = x / fac;
cm = x;
}
else
in = cm = 0;
cout << in << " in = " << cm << " cm\n";
}
Operaciya >> ("vvesti iz") ispol'zuetsya kak operator vvoda; cin
yavlyaetsya standartnym vhodnym potokom. Tip operanda, raspolozhennogo sprava
ot operacii >>, opredelyaet, kakoe znachenie vvoditsya; ono zapisyvaetsya v
etot operand.
Operator switch (pereklyuchatel') sravnivaet znachenie s naborom
konstant. Proverku v predydushchem primere mozhno zapisat' tak:
switch ( ch )
{
case 'i':
in = x;
cm = x * fac;
break;
case 'c':
in = x / fac;
cm = x;
break;
default:
in = cm = 0;
break;
}
Operatory break ispol'zuyutsya dlya vyhoda iz pereklyuchatelya. Vse
konstanty variantov dolzhny byt' razlichny. Esli sravnivaemoe znachenie ne
sovpadaet ni s odnoj iz nih, vypolnyaetsya operator s metkoj default.
Variant default mozhet i otsutstvovat'.
Privedem zapis', zadayushchuyu kopirovanie 10 elementov odnogo massiva v
drugoj:
int v1 [ 10 ];
int v2 [ 10 ];
// ...
for ( int i=0; i<10; i++ ) v1 [ i ] = v2 [ i ];
Slovami eto mozhno vyrazit' tak: "Nachat' s i ravnogo nulyu, i poka i
men'she 10, kopirovat' i-tyj element i uvelichivat' i." Inkrement (++)
peremennoj celogo tipa prosto svoditsya k uvelicheniyu na 1.
Funkciya - eto poimenovannaya chast' programmy, kotoraya mozhet vyzyvat'sya
iz drugih chastej programmy stol'ko raz, skol'ko neobhodimo. Privedem
programmu, vydayushchuyu stepeni chisla dva:
extern float pow ( float, int );
// pow () opredelena v drugom meste
int main ()
{
for ( int i=0; i<10; i++ ) cout << pow ( 2, i ) << '\n';
}
Pervaya stroka yavlyaetsya opisaniem funkcii. Ona zadaet pow kak funkciyu s
parametrami tipa float i int, vozvrashchayushchuyu znachenie tipa float. Opisanie
funkcii neobhodimo dlya ee vyzova, ee opredelenie nahoditsya v drugom meste.
Pri vyzove funkcii tip kazhdogo fakticheskogo parametra sveryaetsya s
tipom, ukazannym v opisanii funkcii, tochno tak zhe, kak esli by
inicializirovalas' peremennaya opisannogo tipa. |to garantiruet nadlezhashchuyu
proverku i preobrazovaniya tipov. Naprimer, vyzov funkcii pow(12.3,"abcd")
translyator sochtet oshibochnym, poskol'ku "abcd" yavlyaetsya strokoj, a ne
parametrom tipa int. V vyzove pow(2,i) translyator preobrazuet celuyu
konstantu (celoe 2) v chislo s plavayushchej tochkoj (float), kak togo trebuet
funkciya. Funkciya pow mozhet byt' opredelena sleduyushchim obrazom:
float pow ( float x, int n )
{
if ( n < 0 )
error ( "oshibka: dlya pow () zadan otricatel'nyj pokazatel'");
switch ( n )
{
case 0: return 1;
case 1: return x;
default: return x * pow ( x, n-1 );
}
}
Pervaya chast' opredeleniya funkcii zadaet ee imya, tip vozvrashchaemogo
znacheniya (esli ono est'), a takzhe tipy i imena formal'nyh parametrov (esli
oni sushchestvuyut). Znachenie vozvrashchaetsya iz funkcii s pomoshch'yu operatora
return.
Raznye funkcii obychno imeyut raznye imena, no funkciyam, vypolnyayushchim
shodnye operacii nad ob容ktami raznyh tipov, luchshe dat' odno imya. Esli
tipy parametrov takih funkcij razlichny, to translyator vsegda mozhet
razobrat'sya, kakuyu funkciyu nuzhno vyzyvat'. Naprimer, mozhno imet' dve
funkcii vozvedeniya v stepen': odnu - dlya celyh chisel, a druguyu - dlya chisel
s plavayushchej tochkoj:
int pow ( int, int );
double pow ( double, double );
//...
x = pow ( 2,10 ); // vyzov pow ( int, int )
y = pow ( 2.0, 10.0 );// vyzov pow ( double, double )
Takoe mnogokratnoe ispol'zovanie imeni nazyvaetsya peregruzkoj imeni
funkcii ili prosto peregruzkoj; peregruzka rassmatrivaetsya osobo v glave
7.
Parametry funkcii mogut peredavat'sya libo "po znacheniyu", libo "po
ssylke". Rassmotrim opredelenie funkcii, kotoraya osushchestvlyaet vzaimoobmen
znachenij dvuh celyh peremennyh. Esli ispol'zuetsya standartnyj sposob
peredachi parametrov po znacheniyu, to pridetsya peredavat' ukazateli:
void swap ( int * p, int * q )
{
int t = * p;
* p = * q;
* q = t;
}
Unarnaya operaciya * nazyvaetsya kosvennost'yu (ili operaciej
razymenovaniya), ona vybiraet znachenie ob容kta, na kotoryj nastroen
ukazatel'. Funkciyu mozhno vyzyvat' sleduyushchim obrazom:
void f ( int i, int j )
{
swap ( & i, & j );
}
Esli ispol'zovat' peredachu parametra po ssylke, mozhno obojtis' bez
yavnyh operacij s ukazatelem:
void swap (int & r1, int & r2 )
{
int t = r1;
r1 = r2;
r2 = t;
}
void g ( int i, int j )
{
swap ( i, j );
}
Dlya lyubogo tipa T zapis' T& oznachaet "ssylka na T". Ssylka sluzhit
sinonimom toj peremennoj, kotoroj ona inicializirovalas'. Otmetim, chto
peregruzka dopuskaet sosushchestvovanie dvuh funkcij swap v odnoj programme.
Programma S++ pochti vsegda sostoit iz neskol'kih razdel'no
transliruemyh "modulej". Kazhdyj "modul'" obychno nazyvaetsya ishodnym
fajlom, no inogda - edinicej translyacii. On sostoit iz posledovatel'nosti
opisanij tipov, funkcij, peremennyh i konstant. Opisanie extern pozvolyaet
iz odnogo ishodnogo fajla ssylat'sya na funkciyu ili ob容kt, opredelennye v
drugom ishodnom fajle. Naprimer:
extern "C" double sqrt ( double );
extern ostream cout;
Samyj rasprostranennyj sposob obespechit' soglasovannost' opisanij
vneshnih vo vseh ishodnyh fajlah - pomestit' takie opisaniya v special'nye
fajly, nazyvaemye zagolovochnymi. Zagolovochnye fajly mozhno vklyuchat' vo vse
ishodnye fajly, v kotoryh trebuyutsya opisaniya vneshnih. Naprimer, opisanie
funkcii sqrt hranitsya v zagolovochnom fajle standartnyh matematicheskih
funkcij s imenem math.h, poetomu, esli nuzhno izvlech' kvadratnyj koren' iz
4, mozhno napisat':
#include <math.h>
//...
x = sqrt ( 4 );
Poskol'ku standartnye zagolovochnye fajly mogut vklyuchat'sya vo mnogie
ishodnye fajly, v nih net opisanij, dublirovanie kotoryh moglo by vyzvat'
oshibki. Tak, telo funkcii prisutstvuet v takih fajlah, esli tol'ko eto
funkciya-podstanovka, a inicializatory ukazany tol'ko dlya konstant ($$4.3).
Ne schitaya takih sluchaev, zagolovochnyj fajl obychno sluzhit hranilishchem dlya
tipov, on predostavlyaet interfejs mezhdu razdel'no transliruemymi chastyami
programmy.
V komande vklyucheniya zaklyuchennoe v uglovye skobki imya fajla (v nashem
primere - <math.h>) ssylaetsya na fajl, nahodyashchijsya v standartnom kataloge
vklyuchaemyh fajlov. CHasto eto - katalog /usr/include/CC. Fajly, nahodyashchiesya
v drugih katalogah, oboznachayutsya svoimi putevymi imenami, vzyatymi v
kavychki. Poetomu v sleduyushchih komandah:
#include "math1.h"
#include "/usr/bs/math2.h"
vklyuchayutsya fajl math1.h iz tekushchego kataloga pol'zovatelya i fajl
math2.h iz kataloga /usr/bs.
Privedem nebol'shoj zakonchennyj primer, v kotorom stroka opredelyaetsya v
odnom fajle, a pechataetsya v drugom. V fajle header.h opredelyayutsya nuzhnye
tipy:
// header.h
extern char * prog_name;
extern void f ();
Fajl main.c yavlyaetsya osnovnoj programmoj:
// main.c
#include "header.h"
char * prog_name = "primitivnyj, no zakonchennyj primer";
int main ()
{
f ();
}
a stroka pechataetsya funkciej iz fajla f.c:
// f.c
#include <stream.h>
#include "header.h"
void f ()
{
cout << prog_name << '\n';
}
Pri zapuske translyatora S++ i peredache emu neobhodimyh
fajlov-parametrov v razlichnyh realizaciyah mogut ispol'zovat'sya raznye
rasshireniya imen dlya programm na S++. Na mashine avtora translyaciya i zapusk
programmy vyglyadit tak:
$ CC main.c f.c -o silly
$ silly
primitivnyj, no zakonchennyj primer
$
Krome razdel'noj translyacii koncepciyu modul'nosti v S++ podderzhivayut
klassy ($$5.4).
1.4 Podderzhka abstrakcii dannyh
Podderzhka programmirovaniya s abstrakciej dannyh v osnovnom svoditsya k
vozmozhnosti opredelit' nabor operacij (funkcii i operacii) nad tipom. Vse
obrashcheniya k ob容ktam etogo tipa ogranichivayutsya operaciyami iz zadannogo
nabora. Odnako, imeya takie vozmozhnosti, programmist skoro obnaruzhivaet,
chto dlya udobstva opredeleniya i ispol'zovaniya novyh tipov nuzhny eshche
nekotorye rasshireniya yazyka. Horoshim primerom takogo rasshireniya yavlyaetsya
peregruzka operacij.
1.4.1 Inicializaciya i udalenie
Kogda predstavlenie tipa skryto, neobhodimo dat' pol'zovatelyu sredstva
dlya inicializacii peremennyh etogo tipa. Prostejshee reshenie - do
ispol'zovaniya peremennoj vyzyvat' nekotoruyu funkciyu dlya ee inicializacii.
Naprimer:
class vector
{
// ...
public:
void init ( init size ); // vyzov init () pered pervym
// ispol'zovaniem ob容kta vector
// ...
};
void f ()
{
vector v;
// poka v nel'zya ispol'zovat'
v.init ( 10 );
// teper' mozhno
}
No eto nekrasivoe i chrevatoe oshibkami reshenie. Budet luchshe, esli
sozdatel' tipa opredelit dlya inicializacii peremennyh nekotoruyu
special'nuyu funkciyu. Esli takaya funkciya est', to dve nezavisimye operacii
razmeshcheniya i inicializacii peremennoj sovmeshchayutsya v odnoj (inogda ee
nazyvayut installyaciej ili prosto postroeniem). Funkciya inicializacii
nazyvaetsya konstruktorom. Konstruktor vydelyaetsya sredi vseh prochih funkcij
dannogo klassa tem, chto imeet takoe zhe imya, kak i sam klass. Esli ob容kty
nekotorogo tipa stroyatsya netrivial'no, to nuzhna eshche odna dopolnitel'naya
operaciya dlya udaleniya ih posle poslednego ispol'zovaniya. Funkciya udaleniya
v S++ nazyvaetsya destruktorom. Destruktor imeet to zhe imya, chto i ego
klass, no pered nim stoit simvol ~ (v S++ etot simvol ispol'zuetsya dlya
operacii dopolneniya). Privedem primer:
class vector
{
int sz; // chislo elementov
int * v; // ukazatel' na celye
public:
vector ( int ); // konstruktor
~vector (); // destruktor
int& operator [] ( int index ); // operaciya indeksacii
};
Konstruktor klassa vector mozhno ispol'zovat' dlya kontrolya nad oshibkami
i vydeleniya pamyati:
vector::vector ( int s )
{
if ( s <= 0 )
error ( "nedopustimyj razmer vektora" );
sz = s;
v = new int [ s ]; // razmestit' massiv iz s celyh
}
Destruktor klassa vector osvobozhdaet ispol'zovavshuyusya pamyat':
vector::~vector ()
{
delete [] v; // osvobodit' massiv, na kotoryj
// nastroen ukazatel' v
}
Ot realizacii S++ ne trebuetsya osvobozhdeniya vydelennoj s pomoshch'yu new
pamyati, esli na nee bol'she ne ssylaetsya ni odin ukazatel' (inymi slovami,
ne trebuetsya avtomaticheskaya "sborka musora"). V zamen etogo mozhno bez
vmeshatel'stva pol'zovatelya opredelit' v klasse sobstvennye funkcii
upravleniya pamyat'yu. |to tipichnyj sposob primeneniya konstruktorov i
destruktorov, hotya est' mnogo ne svyazannyh s upravleniem pamyat'yu
primenenij etih funkcij (sm., naprimer, $$9.4).
1.4.2 Prisvaivanie i inicializaciya
Dlya mnogih tipov zadacha upravleniya imi svoditsya k postroeniyu i
unichtozheniyu svyazannyh s nimi ob容ktov, no est' tipy, dlya kotoryh etogo
malo. Inogda neobhodimo upravlyat' vsemi operaciyami kopirovaniya. Vernemsya
k klassu vector:
void f ()
{
vector v1 ( 100 );
vector v2 = v1; // postroenie novogo vektora v2,
// inicializiruemogo v1
v1 = v2; // v2 prisvaivaetsya v1
// ...
}
Dolzhna byt' vozmozhnost' opredelit' interpretaciyu operacij
inicializacii v2 i prisvaivaniya v1. Naprimer, v opisanii:
class vector
{
int * v;
int sz;
public:
// ...
void operator = ( const vector & ); // prisvaivanie
vector ( const vector & ); // inicializaciya
};
ukazyvaetsya, chto prisvaivanie i inicializaciya ob容ktov tipa vector
dolzhny vypolnyat'sya s pomoshch'yu opredelennyh pol'zovatelem operacij.
Prisvaivanie mozhno opredelit' tak:
void vector::operator = ( const vector & a )
// kontrol' razmera i kopirovanie elementov
{
if ( sz != a.sz )
error ( "nedopustimyj razmer vektora dlya =" );
for ( int i = 0; i < sz; i++ ) v [ i ] = a.v [ i ];
}
Poskol'ku eta operaciya ispol'zuet dlya prisvaivaniya "staroe znachenie"
vektora, operaciya inicializacii dolzhna zadavat'sya drugoj funkciej,
naprimer, takoj:
vector::vector ( const vector & a )
// inicializaciya vektora znacheniem drugogo vektora
{
sz = a.sz; // razmer tot zhe
v = new int [ sz ]; // vydelit' pamyat' dlya massiva
for ( int i = 0; i < sz; i++ ) //kopirovanie elementov
v [ i ] = a.v [ i ];
}
V yazyke S++ konstruktor vida T(const T&) nazyvaetsya konstruktorom
kopirovaniya dlya tipa T. Lyubuyu inicializaciyu ob容ktov tipa T on vypolnyaet s
pomoshch'yu znacheniya nekotorogo drugogo ob容kta tipa T. Pomimo yavnoj
inicializacii konstruktory vida T(const T&) ispol'zuyutsya dlya peredachi
parametrov po znacheniyu i polucheniya vozvrashchaemogo funkciej znacheniya.
Zachem programmistu mozhet ponadobit'sya opredelit' takoj tip, kak vektor
celyh chisel? Kak pravilo, emu nuzhen vektor iz elementov, tip kotoryh
neizvesten sozdatelyu klassa Vector. Sledovatel'no, nado sumet' opredelit'
tip vektora tak, chtoby tip elementov v etom opredelenii uchastvoval kak
parametr, oboznachayushchij "real'nye" tipy elementov:
template < class T > class Vector
{ // vektor elementov tipa T
T * v;
int sz;
public:
Vector ( int s )
{
if ( s <= 0 )
error ( "nedopustimyj dlya Vector razmer" );
v = new T [ sz = s ];
// vydelit' pamyat' dlya massiva s tipa T
}
T & operator [] ( int i );
int size () { return sz; }
// ...
};
Takovo opredelenie shablona tipa. On zadaet sposob polucheniya semejstva
shodnyh klassov. V nashem primere shablon tipa Vector pokazyvaet, kak mozhno
poluchit' klass vektor dlya zadannogo tipa ego elementov. |to opisanie
otlichaetsya ot obychnogo opisaniya klassa nalichiem nachal'noj konstrukcii
template<class T>, kotoraya i pokazyvaet, chto opisyvaetsya ne klass, a
shablon tipa s zadannym parametrom-tipom (zdes' on ispol'zuetsya kak tip
elementov). Teper' mozhno opredelyat' i ispol'zovat' vektora raznyh tipov:
void f ()
{
Vector < int > v1 ( 100 ); // vektor iz 100 celyh
Vector < complex > v2 ( 200 ); // vektor iz 200
// kompleksnyh chisel
v2 [ i ] = complex ( v1 [ x ], v1 [ y ] );
// ...
}
Vozmozhnosti, kotorye realizuet shablon tipa, inogda nazyvayutsya
parametricheskimi tipami ili genericheskimi ob容ktami. Ono shodno s
vozmozhnostyami, imeyushchimisya v yazykah Clu i Ada. Ispol'zovanie shablona tipa
ne vlechet za soboj kakih-libo dopolnitel'nyh rashodov vremeni po sravneniyu
s ispol'zovaniem klassa, v kotorom vse tipy ukazany neposredstvenno.
1.4.4 Obrabotka osobyh situacij
Po mere rosta programm, a osobenno pri aktivnom ispol'zovanii
bibliotek poyavlyaetsya neobhodimost' standartnoj obrabotki oshibok (ili, v
bolee shirokom smysle, "osobyh situacij"). YAzyki Ada, Algol-68 i Clu
podderzhivayut standartnyj sposob obrabotki osobyh situacij.
Snova vernemsya k klassu vector. CHto nuzhno delat', kogda operacii
indeksacii peredano znachenie indeksa, vyhodyashchee za granicy massiva?
Sozdatel' klassa vector ne znaet, na chto rasschityvaet pol'zovatel' v takom
sluchae, a pol'zovatel' ne mozhet obnaruzhit' podobnuyu oshibku (esli by mog,
to eta oshibka voobshche ne voznikla by). Vyhod takoj: sozdatel' klassa
obnaruzhivaet oshibku vyhoda za granicu massiva, no tol'ko soobshchaet o nej
neizvestnomu pol'zovatelyu. Pol'zovatel' sam prinimaet neobhodimye mery.
Naprimer:
class vector {
// opredelenie tipa vozmozhnyh osobyh situacij
class range { };
// ...
};
Vmesto vyzova funkcii oshibki v funkcii vector::operator[]() mozhno
perejti na tu chast' programmy, v kotoroj obrabatyvayutsya osobye situacii.
|to nazyvaetsya "zapustit' osobuyu situaciyu" ("throw the exception"):
int & vector::operator [] ( int i )
{
if ( i < 0 || sz <= i ) throw range ();
return v [ i ];
}
V rezul'tate iz steka budet vybirat'sya informaciya, pomeshchaemaya tuda pri
vyzovah funkcij, do teh por, poka ne budet obnaruzhen obrabotchik osoboj
situacii s tipom range dlya klassa vektor (vector::range); on i budet
vypolnyat'sya.
Obrabotchik osobyh situacij mozhno opredelit' tol'ko dlya special'nogo
bloka:
void f ( int i )
{
try
{
// v etom bloke obrabatyvayutsya osobye situacii
// s pomoshch'yu opredelennogo nizhe obrabotchika
vector v ( i );
// ...
v [ i + 1 ] = 7; // privodit k osoboj situacii range
// ...
g (); // mozhet privesti k osoboj situacii range
// na nekotoryh vektorah
}
catch ( vector::range )
{
error ( "f (): vector range error" );
return;
}
}
Ispol'zovanie osobyh situacij delaet obrabotku oshibok bolee
uporyadochennoj i ponyatnoj. Obsuzhdenie i podrobnosti otlozhim do glavy 9.
1.4.5 Preobrazovaniya tipov
Opredelyaemye pol'zovatelem preobrazovaniya tipa, naprimer, takie, kak
preobrazovanie chisla s plavayushchej tochkoj v kompleksnoe, kotoroe neobhodimo
dlya konstruktora complex(double), okazalis' ochen' poleznymi v S++.
Programmist mozhet zadavat' eti preobrazovaniya yavno, a mozhet polagat'sya na
translyator, kotoryj vypolnyaet ih neyavno v tom sluchae, kogda oni neobhodimy
i odnoznachny:
complex a = complex ( 1 );
complex b = 1; // neyavno: 1 -> complex ( 1 )
a = b + complex ( 2 );
a = b + 2; // neyavno: 2 -> complex ( 2)
Preobrazovaniya tipov nuzhny v S++ potomu, chto arifmeticheskie operacii
so smeshannymi tipami yavlyayutsya normoj dlya yazykov, ispol'zuemyh v chislovyh
zadachah. Krome togo, bol'shaya chast' pol'zovatel'skih tipov, ispol'zuemyh
dlya "vychislenij" (naprimer, matricy, stroki, mashinnye adresa) dopuskaet
estestvennoe preobrazovanie v drugie tipy (ili iz drugih tipov).
Preobrazovaniya tipov sposobstvuyut bolee estestvennoj zapisi programmy:
complex a = 2;
complex b = a + 2; // eto oznachaet: operator + ( a, complex ( 2 ))
b = 2 + a; // eto oznachaet: operator + ( complex ( 2 ), a )
V oboih sluchayah dlya vypolneniya operacii "+" nuzhna tol'ko odna funkciya,
a ee parametry edinoobrazno traktuyutsya sistemoj tipov yazyka. Bolee togo,
klass complex opisyvaetsya tak, chto dlya estestvennogo i besprepyatstvennogo
obobshcheniya ponyatiya chisla net neobhodimosti chto-to izmenyat' dlya celyh chisel.
1.4.6 Mnozhestvennye realizacii
Osnovnye sredstva, podderzhivayushchie ob容ktno-orientirovannoe
programmirovanie, a imenno: proizvodnye klassy i virtual'nye funkcii,-
mozhno ispol'zovat' i dlya podderzhki abstrakcii dannyh, esli dopustit'
neskol'ko realizacij odnogo tipa. Vernemsya k primeru so stekom:
template < class T >
class stack
{
public:
virtual void push ( T ) = 0; // chistaya virtual'naya funkciya
virtual T pop () = 0; // chistaya virtual'naya funkciya
};
Oboznachenie =0 pokazyvaet, chto dlya virtual'noj funkcii ne trebuetsya
nikakogo opredeleniya, a klass stack yavlyaetsya abstraktnym, t.e. on mozhet
ispol'zovat'sya tol'ko kak bazovyj klass. Poetomu steki mozhno ispol'zovat',
no ne sozdavat':
class cat { /* ... */ };
stack < cat > s; // oshibka: stek - abstraktnyj klass
void some_function ( stack <cat> & s, cat kitty ) // normal'no
{
s.push ( kitty );
cat c2 = s.pop ();
// ...
}
Poskol'ku interfejs steka nichego ne soobshchaet o ego predstavlenii, ot
pol'zovatelej steka polnost'yu skryty detali ego realizacii.
Mozhno predlozhit' neskol'ko razlichnyh realizacij steka. Naprimer, stek
mozhet byt' massivom:
template < class T >
class astack : public stack < T >
{
// istinnoe predstavlenie ob容kta tipa stek
// v dannom sluchae - eto massiv
// ...
public:
astack ( int size );
~astack ();
void push ( T );
T pop ();
};
Mozhno realizovat' stek kak svyazannyj spisok:
template < class T >
class lstack : public stack < T >
{
// ...
};
Teper' mozhno sozdavat' i ispol'zovat' steki:
void g ()
{
lstack < cat > s1 ( 100 );
astack < cat > s2 ( 100 );
cat Ginger;
cat Snowball;
some_function ( s1, Ginger );
some_function ( s2, Snowball );
}
O tom, kak predstavlyat' steki raznyh vidov, dolzhen bespokoit'sya tol'ko
tot, kto ih sozdaet (t.e. funkciya g()), a pol'zovatel' steka (t.e. avtor
funkcii some_function()) polnost'yu ograzhden ot detalej ih realizacii.
Platoj za podobnuyu gibkost' yavlyaetsya to, chto vse operacii nad stekami
dolzhny byt' virtual'nymi funkciyami.
1.5 Podderzhka ob容ktno-orientirovannogo programmirovaniya
Podderzhku ob容ktno-orientirovannogo programmirovaniya obespechivayut
klassy vmeste s mehanizmom nasledovaniya, a takzhe mehanizm vyzova
funkcij-chlenov v zavisimosti ot istinnogo tipa ob容kta (delo v tom, chto
vozmozhny sluchai, kogda etot tip neizvesten na stadii translyacii). Osobenno
vazhnuyu rol' igraet mehanizm vyzova funkcij-chlenov. Ne menee vazhny
sredstva, podderzhivayushchie abstrakciyu dannyh (o nih my govorili ranee). Vse
dovody v pol'zu abstrakcii dannyh i baziruyushchihsya na nej metodov, kotorye
pozvolyayut estestvenno i krasivo rabotat' s tipami, dejstvuyut i dlya yazyka,
podderzhivayushchego ob容ktno-orientirovannoe programmirovanie. Uspeh oboih
metodov zavisit ot sposoba postroeniya tipov, ot togo, naskol'ko oni
prosty, gibki i effektivny. Metod ob容ktno-orientirovannogo
programmirovaniya pozvolyaet opredelyat' bolee obshchie i gibkie
pol'zovatel'skie tipy po sravneniyu s temi, kotorye poluchayutsya, esli
ispol'zovat' tol'ko abstrakciyu dannyh.
Osnovnoe sredstvo podderzhki ob容ktno-orientirovannogo programmirovaniya
- eto mehanizm vyzova funkcii-chlena dlya dannogo ob容kta, kogda istinnyj
tip ego na stadii translyacii neizvesten. Pust', naprimer, est' ukazatel'
p. Kak proishodit vyzov p->rotate(45)? Poskol'ku S++ baziruetsya na
staticheskom kontrole tipov, zadayushchee vyzov vyrazhenie imeet smysl tol'ko
pri uslovii, chto funkciya rotate() uzhe byla opisana. Dalee, iz oboznacheniya
p->rotate() my vidim, chto p yavlyaetsya ukazatelem na ob容kt nekotorogo
klassa, a rotate dolzhna byt' chlenom etogo klassa. Kak i pri vsyakom
staticheskom kontrole tipov proverka korrektnosti vyzova nuzhna dlya togo,
chtoby ubedit'sya (naskol'ko eto vozmozhno na stadii translyacii), chto tipy v
programme ispol'zuyutsya neprotivorechivym obrazom. Tem samym garantiruetsya,
chto programma svobodna ot mnogih vidov oshibok.
Itak, translyatoru dolzhno byt' izvestno opisanie klassa, analogichnoe
tem, chto privodilis' v $$1.2.5:
class shape
{
// ...
public:
// ...
virtual void rotate ( int );
// ...
};
a ukazatel' p dolzhen byt' opisan, naprimer, tak:
T * p;
gde T - klass shape ili proizvodnyj ot nego klass. Togda translyator
vidit, chto klass ob容kta, na kotoryj nastroen ukazatel' p, dejstvitel'no
imeet funkciyu rotate(), a funkciya imeet parametr tipa int. Znachit,
p->rotate(45) korrektnoe vyrazhenie.
Poskol'ku shape::rotate() byla opisana kak virtual'naya funkciya, nuzhno
ispol'zovat' mehanizm vyzova virtual'noj funkcii. CHtoby uznat', kakuyu
imenno iz funkcij rotate sleduet vyzvat', nuzhno do vyzova poluchit' iz
ob容kta nekotoruyu sluzhebnuyu informaciyu, kotoraya byla pomeshchena tuda pri ego
sozdanii. Kak tol'ko ustanovleno, kakuyu funkciyu nado vyzvat', dopustim
circle::rotate, proishodit ee vyzov s uzhe upominavshimsya kontrolem tipa.
Obychno v kachestve sluzhebnoj informacii ispol'zuetsya tablica adresov
funkcij, a translyator preobrazuet imya rotate v indeks etoj tablicy. S
uchetom etoj tablicy ob容kt tipa shape mozhno predstavit' tak:
center
vtbl:
color &X::draw
&Y::rotate
...
...
Funkcii iz tablicy virtual'nyh funkcij vtbl pozvolyayut pravil'no
rabotat' s ob容ktom dazhe v teh sluchayah, kogda v vyzyvayushchej funkcii
neizvestny ni tablica vtbl, ni raspolozhenie dannyh v chasti ob容kta,
oboznachennoj ... . Zdes' kak X i Y oboznacheny imena klassov, v kotorye
vhodyat vyzyvaemye funkcii. Dlya ob容kta circle oba imeni X i Y est' circle.
Vyzov virtual'noj funkcii mozhet byt' po suti stol' zhe effektiven, kak
vyzov obychnoj funkcii.
Neobhodimost' kontrolya tipa pri obrashcheniyah k virtual'nym funkciyam
mozhet okazat'sya opredelennym ogranicheniem dlya razrabotchikov bibliotek.
Naprimer, horosho by predostavit' pol'zovatelyu klass "stek chego-ugodno".
Neposredstvenno v S++ eto sdelat' nel'zya. Odnako, ispol'zuya shablony tipa i
nasledovanie, mozhno priblizit'sya k toj effektivnosti i prostote
proektirovaniya i ispol'zovaniya bibliotek, kotorye svojstvenny yazykam s
dinamicheskim kontrolem tipov. K takim yazykam otnositsya, naprimer, yazyk
Smalltalk, na kotorom mozhno opisat' "stek chego-ugodno". Rassmotrim
opredelenie steka s pomoshch'yu shablona tipa:
template < class T > class stack
{
T * p;
int sz;
public:
stack ( int );
~stack ();
void push ( T );
T & pop ();
};
Ne oslablyaya staticheskogo kontrolya tipov, mozhno ispol'zovat' takoj stek
dlya hraneniya ukazatelej na ob容kty tipa plane (samolet):
stack < plane * > cs ( 200 );
void f ()
{
cs.push ( new Saab900 ); // Oshibka pri translyacii :
// trebuetsya plane*, a peredan car*
cs.push ( new Saab37B );
// prekrasno: Saab 37B - na samom
// dele samolet, t.e. tipa plane
cs.pop () -> takeoff ();
cs.pop () -> takeoff ();
}
Esli staticheskogo kontrolya tipov net, privedennaya vyshe oshibka
obnaruzhitsya tol'ko pri vypolnenii programmy:
// primer dinamicheskoe kontrolya tipa
// vmesto staticheskogo; eto ne S++
Stack s; // stek mozhet hranit' ukazateli na ob容kty
// proizvol'nogo tipa
void f ()
{
s.push ( new Saab900 );
s.push ( new Saab37B );
s.pop () -> takeoff (); // prekrasno: Saab 37B - samolet
cs.pop () -> takeoff (); // dinamicheskaya oshibka:
// mashina ne mozhet vzletet'
}
Dlya sposoba opredeleniya, dopustima li operaciya nad ob容ktom, obychno
trebuetsya bol'she dopolnitel'nyh rashodov, chem dlya mehanizma vyzova
virtual'nyh funkcij v S++.
Rasschityvaya na staticheskij kontrol' tipov i vyzov virtual'nyh funkcij,
my prihodim k inomu stilyu programmirovaniya, chem nadeyas' tol'ko na
dinamicheskij kontrol' tipov. Klass v S++ zadaet strogo opredelennyj
interfejs dlya mnozhestva ob容ktov etogo i lyubogo proizvodnogo klassa, togda
kak v Smalltalk klass zadaet tol'ko minimal'no neobhodimoe chislo operacij,
i pol'zovatel' vprave primenyat' nezadannye v klasse operacii. Inymi
slovami, klass v S++ soderzhit tochnoe opisanie operacij, i pol'zovatelyu
garantiruetsya, chto tol'ko eti operacii translyator sochtet dopustimymi.
1.5.3 Mnozhestvennoe nasledovanie
Esli klass A yavlyaetsya bazovym klassom dlya B, to B nasleduet atributy
A. t.e. B soderzhit A plyus eshche chto-to. S uchetom etogo stanovitsya ochevidno,
chto horosho, kogda klass B mozhet nasledovat' iz dvuh bazovyh klassov A1 i
A2. |to nazyvaetsya mnozhestvennym nasledovaniem.
Privedem nekij tipichnyj primer mnozhestvennogo nasledovaniya. Pust' est'
dva bibliotechnyh klassa displayed i task. Pervyj predstavlyaet zadachi,
informaciya o kotoryh mozhet vydavat'sya na ekran s pomoshch'yu nekotorogo
monitora, a vtoroj - zadachi, vypolnyaemye pod upravleniem nekotorogo
dispetchera. Programmist mozhet sozdavat' sobstvennye klassy, naprimer,
takie:
class my_displayed_task: public displayed, public task
{
// tekst pol'zovatelya
};
class my_task: public task {
// eta zadacha ne izobrazhaetsya
// na ekrane, t.k. ne soderzhit klass displayed
// tekst pol'zovatelya
};
class my_displayed: public displayed
{
// a eto ne zadacha
// t.k. ne soderzhit klass task
// tekst pol'zovatelya
};
Esli nasledovat'sya mozhet tol'ko odin klass, to pol'zovatelyu dostupny
tol'ko dva iz treh privedennyh klassov. V rezul'tate libo poluchaetsya
dublirovanie chastej programmy, libo teryaetsya gibkost', a, kak pravilo,
proishodit i to, i drugoe. Privedennyj primer prohodit v S++ bezo vsyakih
dopolnitel'nyh rashodov vremeni i pamyati po sravneniyu s programmami, v
kotoryh nasleduetsya ne bolee odnogo klassa. Staticheskij kontrol' tipov ot
etogo tozhe ne stradaet.
Vse neodnoznachnosti vyyavlyayutsya na stadii translyacii:
class task
{
public:
void trace ();
// ...
};
class displayed
{
public:
void trace ();
// ...
};
class my_displayed_task:public displayed, public task
{
// v etom klasse trace () ne opredelyaetsya
};
void g ( my_displayed_task * p )
{
p -> trace (); // oshibka: neodnoznachnost'
}
V etom primere vidny otlichiya S++ ot ob容ktno-orientirovannyh dialektov
yazyka Lisp, v kotoryh est' mnozhestvennoe nasledovanie. V etih dialektah
neodnoznachnost' razreshaetsya tak: ili schitaetsya sushchestvennym poryadok
opisaniya, ili schitayutsya identichnymi ob容kty s odnim i tem zhe imenem v
raznyh bazovyh klassah, ili ispol'zuyutsya kombinirovannye sposoby, kogda
sovpadenie ob容ktov dolya bazovyh klassov sochetaetsya s bolee slozhnym
sposobom dlya proizvodnyh klassov. V S++ neodnoznachnost', kak pravilo,
razreshaetsya vvedeniem eshche odnoj funkcii:
class my_displayed_task:public displayed, public task
{
// ...
public:
void trace ()
{
// tekst pol'zovatelya
displayed::trace (); // vyzov trace () iz displayed
task::trace (); // vyzov trace () iz task
}
// ...
};
void g ( my_displayed_task * p )
{
p -> trace (); // teper' normal'no
}
Pust' chlenu klassa (nevazhno funkcii-chlenu ili chlenu, predstavlyayushchemu
dannye) trebuetsya zashchita ot "nesankcionirovannogo dostupa". Kak razumno
ogranichit' mnozhestvo funkcij, kotorym takoj chlen budet dostupen? Ochevidnyj
otvet dlya yazykov, podderzhivayushchih ob容ktno-orientirovannoe
programmirovanie, takov: dostup imeyut vse operacii, kotorye opredeleny dlya
etogo ob容kta, inymi slovami, vse funkcii-chleny. Naprimer:
class window
{
// ...
protected:
Rectangle inside;
// ...
};
class dumb_terminal : public window
{
// ...
public:
void prompt ();
// ...
};
Zdes' v bazovom klasse window chlen inside tipa Rectangle opisyvaetsya
kak zashchishchennyj (protected), no funkcii-chleny proizvodnyh klassov,
naprimer, dumb_terminal::prompt(), mogut obratit'sya k nemu i vyyasnit', s
kakogo vida oknom oni rabotayut. Dlya vseh drugih funkcij chlen
window::inside nedostupen.
V takom podhode sochetaetsya vysokaya stepen' zashchishchennosti
(dejstvitel'no, vryad li vy "sluchajno" opredelite proizvodnyj klass) s
gibkost'yu, neobhodimoj dlya programm, kotorye sozdayut klassy i ispol'zuyut
ih ierarhiyu (dejstvitel'no, "dlya sebya" vsegda mozhno v proizvodnyh klassah
predusmotret' dostup k zashchishchennym chlenam).
Neochevidnoe sledstvie iz etogo: nel'zya sostavit' polnyj i
okonchatel'nyj spisok vseh funkcij, kotorym budet dostupen zashchishchennyj chlen,
poskol'ku vsegda mozhno dobavit' eshche odnu, opredeliv ee kak funkciyu-chlen v
novom proizvodnom klasse. Dlya metoda abstrakcii dannyh takoj podhod chasto
byvaet malo priemlemym. Esli yazyk orientiruetsya na metod abstrakcii
dannyh, to ochevidnoe dlya nego reshenie - eto trebovanie ukazyvat' v
opisanii klassa spisok vseh funkcij, kotorym nuzhen dostup k chlenu. V S++
dlya etoj celi ispol'zuetsya opisanie chastnyh (private) chlenov. Ono
ispol'zovalos' i v privodivshihsya opisaniyah klassov complex i shape.
Vazhnost' inkapsulyacii, t.e. zaklyucheniya chlenov v zashchitnuyu obolochku,
rezko vozrastaet s rostom razmerov programmy i uvelichivayushchimsya razbrosom
oblastej prilozheniya. V $$6.6 bolee podrobno obsuzhdayutsya vozmozhnosti yazyka
po inkapsulyacii.
1.6 Predely sovershenstva
YAzyk S++ proektirovalsya kak "luchshij S", podderzhivayushchij abstrakciyu
dannyh i ob容ktno-orientirovannoe programmirovanie. Pri etom on dolzhen
byt' prigodnym dlya bol'shinstva osnovnyh zadach sistemnogo programmirovaniya.
Osnovnaya trudnost' dlya yazyka, kotoryj sozdavalsya v raschete na metody
upryatyvaniya dannyh, abstrakcii dannyh i ob容ktno-orientirovannogo
programmirovaniya, v tom, chto dlya togo, chtoby byt' yazykom obshchego
naznacheniya, on dolzhen:
- idti na tradicionnyh mashinah;
- sosushchestvovat' s tradicionnymi operacionnymi sistemami i yazykami;
- sopernichat' s tradicionnymi yazykami programmirovaniya v effektivnosti
vypolneniya programmy;
- byt' prigodnym vo vseh osnovnyh oblastyah prilozheniya.
|to znachit, chto dolzhny byt' vozmozhnosti dlya effektivnyh chislovyh
operacij (arifmetika s plavayushchej tochkoj bez osobyh nakladnyh rashodov,
inache pol'zovatel' predpochtet Fortran) i sredstva takogo dostupa k pamyati,
kotoryj pozvolit pisat' na etom yazyke drajvery ustrojstv. Krome togo, nado
umet' pisat' vyzovy funkcij v dostatochno neprivychnoj zapisi, prinyatoj dlya
obrashchenij v tradicionnyh operacionnyh sistemah. Nakonec, dolzhna byt'
vozmozhnost' iz yazyka, podderzhivayushchego ob容ktno-orientirovannoe
programmirovanie, vyzyvat' funkcii, napisannye na drugih yazykah, a iz
drugih yazykov vyzyvat' funkciyu na etom yazyke, podderzhivayushchem
ob容ktno-orientirovannoe programmirovanie.
Dalee, nel'zya rasschityvat' na shirokoe ispol'zovanie iskomogo yazyka
programmirovaniya kak yazyka obshchego naznacheniya, esli realizaciya ego celikom
polagaetsya na vozmozhnosti, kotorye otsutstvuyut v mashinah s tradicionnoj
arhitekturoj.
Esli ne vvodit' v yazyk vozmozhnosti nizkogo urovnya, to pridetsya dlya
osnovnyh zadach bol'shinstva oblastej prilozheniya ispol'zovat' nekotorye
yazyki nizkogo urovnya, naprimer S ili assembler. No S++ proektirovalsya s
raschetom, chto v nem mozhno sdelat' vse, chto dopustimo na S, prichem bez
uvelicheniya vremeni vypolneniya. Voobshche, S++ proektirovalsya, ishodya iz
principa, chto ne dolzhno voznikat' nikakih dopolnitel'nyh zatrat vremeni i
pamyati, esli tol'ko etogo yavno ne pozhelaet sam programmist.
YAzyk proektirovalsya v raschete na sovremennye metody translyacii,
kotorye obespechivayut proverku soglasovannosti programmy, ee effektivnost'
i kompaktnost' predstavleniya. Osnovnym sredstvom bor'by so slozhnost'yu
programm viditsya, prezhde vsego, strogij kontrol' tipov i inkapsulyaciya.
Osobenno eto kasaetsya bol'shih programm, sozdavaemyh mnogimi lyud'mi.
Pol'zovatel' mozhet ne yavlyat'sya odnim iz sozdatelej takih programm, i mozhet
voobshche ne byt' programmistom. Poskol'ku nikakuyu nastoyashchuyu programmu
nel'zya napisat' bez podderzhki bibliotek, sozdavaemyh drugimi
programmistami, poslednee zamechanie mozhno otnesti prakticheski ko vsem
programmam.
S++ proektirovalsya dlya podderzhki togo principa, chto vsyakaya programma
est' model' nekotoryh sushchestvuyushchih v real'nosti ponyatij, a klass yavlyaetsya
konkretnym predstavleniem ponyatiya, vzyatogo iz oblasti prilozheniya ($$12.2).
Poetomu klassy pronizyvayut vsyu programmu na S++, i nalagayutsya zhestkie
trebovaniya na gibkost' ponyatiya klassa, kompaktnost' ob容ktov klassa i
effektivnost' ih ispol'zovaniya. Esli rabotat' s klassami budet neudobno
ili slishkom nakladno, to oni prosto ne budut ispol'zovat'sya, i programmy
vyrodyatsya v programmy na "luchshem S". Znachit pol'zovatel' ne sumeet
nasladit'sya temi vozmozhnostyami, radi kotoryh, sobstvenno, i sozdavalsya
yazyk.
* GLAVA 2. OPISANIYA I KONSTANTY
"Sovershenstvo dostizhimo tol'ko v moment
kraha".
(S.N. Parkinson)
V dannoj glave opisany osnovnye tipy (char, int, float i t.d.) i
sposoby postroeniya na ih osnove novyh tipov (funkcij, vektorov, ukazatelej
i t.d.). Opisanie vvodit v programmu imya, ukazav ego tip i, vozmozhno,
nachal'noe znachenie. V etoj glave vvodyatsya takie ponyatiya, kak opisanie i
opredelenie, tipy, oblast' vidimosti imen, vremya zhizni ob容ktov.
Dayutsya oboznacheniya literal'nyh konstant S++ i sposoby zadaniya
simvolicheskih konstant. Privodyatsya primery, kotorye prosto
demonstriruyut vozmozhnosti yazyka. Bolee osmyslennye primery, illyustriruyushchie
vozmozhnosti vyrazhenij i operatorov yazyka S++, budut privedeny v sleduyushchej
glave. V etoj glave lish' upominayutsya sredstva dlya opredeleniya
pol'zovatel'skih tipov i operacij nad nimi. Oni obsuzhdayutsya v glavah 5 i 7.
Imya (identifikator) sleduet opisat' prezhde, chem ono budet ispol'zovat'sya
v programme na S++. |to oznachaet, chto nuzhno ukazat' ego tip, chtoby
translyator znal, k kakogo vida ob容ktam otnositsya imya. Nizhe privedeny
neskol'ko primerov, illyustriruyushchih vse raznoobrazie opisanij:
char ch;
int count = 1;
char* name = "Njal";
struct complex { float re, im; };
complex cvar;
extern complex sqrt(complex);
extern int error_number;
typedef complex point;
float real(complex* p) { return p->re; };
const double pi = 3.1415926535897932385;
struct user;
template<class T> abs(T a) { return a<0 ? -a : a; }
enum beer { Carlsberg, Tuborg, Thor };
Iz etih primerov vidno, chto rol' opisanij ne svoditsya lish' k privyazke
tipa k imeni. Bol'shinstvo ukazannyh opisanij odnovremenno yavlyayutsya
opredeleniyami, t.e. oni sozdayut ob容kt, na kotoryj ssylaetsya imya.
Dlya ch, count, name i cvar takim ob容ktom yavlyaetsya element pamyati
sootvetstvuyushchego razmera. |tot element budet ispol'zovat'sya kak
peremennaya, i govoryat, chto dlya nego otvedena pamyat'. Dlya real podobnym
ob容ktom budet zadannaya funkciya.
Dlya konstanty pi ob容ktom budet chislo 3.1415926535897932385.
Dlya complex ob容ktom budet novyj tip. Dlya point ob容ktom yavlyaetsya
tip complex, poetomu point stanovitsya sinonimom complex. Sleduyushchie
opisaniya uzhe ne yavlyayutsya opredeleniyami:
extern complex sqrt(complex);
extern int error_number;
struct user;
|to oznachaet, chto ob容kty, vvedennye imi, dolzhny byt' opredeleny
gde-to v drugom meste programmy. Telo funkcii sqrt dolzhno byt' ukazano
v kakom-to drugom opisanii. Pamyat' dlya peremennoj error_number tipa
int dolzhna vydelyat'sya v rezul'tate drugogo opisaniya error_number.
Dolzhno byt' i kakoe-to drugoe opisanie tipa user, iz kotorogo mozhno
ponyat', chto eto za tip. V programme na yazyke S++ dolzhno byt' tol'ko
odno opredelenie kazhdogo imeni, no opisanij mozhet byt' mnogo. Odnako vse
opisaniya dolzhny byt' soglasovany po tipu vvodimogo v nih ob容kta.
Poetomu v privedennom nizhe fragmente soderzhatsya dve oshibki:
int count;
int count; // oshibka: pereopredelenie
extern int error_number;
extern short error_number; // oshibka: nesootvetstvie tipov
Zato v sleduyushchem fragmente net ni odnoj oshibki (ob ispol'zovanii
extern sm. #4.2):
extern int error_number;
extern int error_number;
V nekotoryh opisaniyah ukazyvayutsya "znacheniya" ob容ktov, kotorye oni
opredelyayut:
struct complex { float re, im; };
typedef complex point;
float real(complex* p) { return p->re };
const double pi = 3.1415926535897932385;
Dlya tipov, funkcij i konstant "znachenie" ostaetsya neizmennym;
dlya dannyh, ne yavlyayushchihsya konstantami, nachal'noe znachenie mozhet
vposledstvii izmenyat'sya:
int count = 1;
char* name = "Bjarne";
//...
count = 2;
name = "Marian";
Iz vseh opredelenij tol'ko sleduyushchee ne zadaet znacheniya:
char ch;
Vsyakoe opisanie, kotoroe zadaet znachenie, yavlyaetsya opredeleniem.
Opisaniem opredelyaetsya oblast' vidimosti imeni. |to znachit, chto
imya mozhet ispol'zovat'sya tol'ko v opredelennoj chasti teksta programmy.
Esli imya opisano v funkcii (obychno ego nazyvayut "lokal'nym imenem"), to
oblast' vidimosti imeni prostiraetsya ot tochki opisaniya
do konca bloka, v kotorom poyavilos' eto opisanie. Esli imya ne nahoditsya
v opisanii funkcii ili klassa (ego obychno nazyvayut "global'nym imenem"),
to oblast' vidimosti prostiraetsya ot tochki opisaniya do konca fajla,
v kotorom poyavilos' eto opisanie.
Opisanie imeni v bloke mozhet skryvat' opisanie v ob容mlyushchem bloke ili
global'noe imya; t.e. imya mozhet byt' pereopredeleno tak, chto ono budet
oboznachat' drugoj ob容kt vnutri bloka. Posle vyhoda iz bloka prezhnee
znachenie imeni (esli ono bylo) vosstanavlivaetsya. Privedem primer:
int x; // global'noe x
void f()
{
int x; // lokal'noe x skryvaet global'noe x
x = 1; // prisvoit' lokal'nomu x
{
int x; // skryvaet pervoe lokal'noe x
x = 2; // prisvoit' vtoromu lokal'nomu x
}
x = 3; // prisvoit' pervomu lokal'nomu x
}
int* p = &x; // vzyat' adres global'nogo x
V bol'shih programmah ne izbezhat' pereopredeleniya imen. K sozhaleniyu,
chelovek legko mozhet proglyadet' takoe pereopredelenie. Voznikayushchie
iz-za etogo oshibki najti neprosto, vozmozhno potomu, chto oni
dostatochno redki. Sledovatel'no, pereopredelenie imen sleduet
svesti k minimumu. Esli vy oboznachaete global'nye peremennye ili
lokal'nye peremennye v bol'shoj funkcii takimi imenami, kak i ili x,
to sami naprashivaetes' na nepriyatnosti.
Est' vozmozhnost' s pomoshch'yu operacii razresheniya oblasti vidimosti
:: obratit'sya k skrytomu global'nomu imeni, naprimer:
int x;
void f2()
{
int x = 1; // skryvaet global'noe x
::x = 2; // prisvaivanie global'nomu x
}
Vozmozhnost' ispol'zovat' skrytoe lokal'noe imya otsutstvuet.
Oblast' vidimosti imeni nachinaetsya v tochke ego opisaniya (po
okonchanii opisatelya, no eshche do nachala inicializatora - sm. $$R.3.2). |to
oznachaet, chto imya mozhno ispol'zovat' dazhe do togo, kak zadano ego
nachal'noe znachenie. Naprimer:
int x;
void f3()
{
int x = x; // oshibochnoe prisvaivanie
}
Takoe prisvaivanie nedopustimo i lisheno smysla. Esli vy popytaetes'
translirovat' etu programmu, to poluchite preduprezhdenie: "ispol'zovanie
do zadaniya znacheniya". Vmeste s tem, ne primenyaya operatora ::, mozhno
ispol'zovat' odno i to zhe imya dlya oboznacheniya dvuh razlichnyh ob容ktov
bloka. Naprimer:
int x = 11;
void f4() // izvrashchennyj primer
{
int y = x; // global'noe x
int x = 22;
y = x; // lokal'noe x
}
Peremennaya y inicializiruetsya znacheniem global'nogo x, t.e. 11,
a zatem ej prisvaivaetsya znachenie lokal'noj peremennoj x, t.e. 22.
Imena formal'nyh parametrov funkcii schitayutsya opisannymi v samom
bol'shom bloke funkcii, poetomu v opisanii nizhe est' oshibka:
void f5(int x)
{
int x; // oshibka
}
Zdes' x opredeleno dvazhdy v odnoj i toj zhe oblasti vidimosti.
|to hotya i ne slishkom redkaya, no dovol'no tonkaya oshibka.
Mozhno vydelyat' pamyat' dlya "peremennyh", ne imeyushchih imen, i
ispol'zovat' eti peremennye.
Vozmozhno dazhe prisvaivanie takim stranno vyglyadyashchim "peremennym",
naprimer, *p[a+10]=7. Sledovatel'no, est' potrebnost' imenovat'
"nechto hranyashcheesya v pamyati". Mozhno privesti podhodyashchuyu citatu iz
spravochnogo rukovodstva: "Lyuboj ob容kt - eto nekotoraya oblast'
pamyati, a adresom nazyvaetsya vyrazhenie, ssylayushcheesya na ob容kt ili
funkciyu" ($$R.3.7). Slovu adres (lvalue - left value, t.e. velichina
sleva) pervonachal'no pripisyvalsya smysl "nechto, chto mozhet v
prisvaivanii stoyat' sleva". Adres mozhet ssylat'sya i na konstantu
(sm. $$2.5). Adres, kotoryj ne byl opisan so specifikaciej const,
nazyvaetsya izmenyaemym adresom.
2.1.3 Vremya zhizni ob容ktov
Esli tol'ko programmist ne vmeshaetsya yavno, ob容kt budet sozdan pri
poyavlenii ego opredeleniya i unichtozhen, kogda ischeznet iz
oblasti vidimosti. Ob容kty s global'nymi imenami sozdayutsya,
inicializiruyutsya (prichem tol'ko odin raz) i sushchestvuyut do konca
programmy. Esli lokal'nye ob容kty opisany so sluzhebnym slovom
static, to oni takzhe sushchestvuyut do konca programmy. Inicializaciya ih
proishodit, kogda v pervyj raz upravlenie "prohodit cherez"
opisanie etih ob容ktov, naprimer:
int a = 1;
void f()
{
int b = 1; // inicializiruetsya pri kazhdom vyzove f()
static int c = a; // inicializiruetsya tol'ko odin raz
cout << " a = " << a++
<< " b = " << b++
<< " c = " << c++ << '\n';
}
int main()
{
while (a < 4) f();
}
Zdes' programma vydast takoj rezul'tat:
a = 1 b = 1 c = 1
a = 2 b = 1 c = 2
a = 3 b = 1 c = 3
''Iz primerov etoj glavy dlya kratkosti izlozheniya isklyuchena
makrokomanda #include <iostream>. Ona nuzhna lish' v teh iz nih, kotorye
vydayut rezul'tat.
Operaciya "++" yavlyaetsya inkrementom, t. e. a++ oznachaet: dobavit' 1
k peremennoj a.
Global'naya peremennaya ili lokal'naya peremennaya static, kotoraya ne byla
yavno inicializirovana, inicializiruetsya neyavno nulevym znacheniem (#2.4.5).
Ispol'zuya operacii new i delete, programmist mozhet sozdavat'
ob容kty, vremenem zhizni kotoryh on upravlyaet sam (sm. $$3.2.6).
Imya (identifikator) yavlyaetsya posledovatel'nost'yu bukv ili cifr.
Pervyj simvol dolzhen byt' bukvoj. Bukvoj schitaetsya i simvol
podcherkivaniya _. YAzyk S++ ne ogranichivaet chislo simvolov v imeni.
No v realizaciyu vhodyat programmnye komponenty, kotorymi sozdatel'
translyatora upravlyat' ne mozhet (naprimer, zagruzchik), a oni,
k sozhaleniyu, mogut ustanavlivat' ogranicheniya. Krome togo, nekotorye
sistemnye programmy, neobhodimye dlya vypolneniya programmy na S++, mogut
rasshiryat' ili suzhat' mnozhestvo simvolov, dopustimyh v identifikatore.
Rasshireniya (naprimer, ispol'zovanie $ v imeni) mogut narushit'
perenosimost' programmy. Nel'zya ispol'zovat' v kachestve imen
sluzhebnye slova S++ (sm. $$R.2.4), naprimer:
hello this_is_a_most_unusially_long_name
DEFINED foO bAr u_name HorseSense
var0 var1 CLASS _class ___
Teper' privedem primery posledovatel'nostej simvolov, kotorye ne mogut
ispol'zovat'sya kak identifikatory:
012 a fool $sys class 3var
pay.due foo~bar .name if
Zaglavnye i strochnye bukvy schitayutsya razlichnymi, poetomu Count i
count - raznye imena. No vybirat' imena, pochti ne otlichayushchiesya
drug ot druga, nerazumno. Vse imena, nachinayushchiesya s simvola
podcherkivaniya, rezerviruyutsya dlya ispol'zovaniya v samoj realizacii
ili v teh programmah, kotorye vypolnyayutsya sovmestno s rabochej,
poetomu krajne legkomyslenno vstavlyat' takie imena v
svoyu programmu.
Pri razbore programmy translyator vsegda stremitsya vybrat' samuyu
dlinnuyu posledovatel'nost' simvolov, obrazuyushchih imya, poetomu var10
- eto imya, a ne idushchie podryad imya var i chislo 10. Po toj zhe prichine
elseif - odno imya (sluzhebnoe), a ne dva sluzhebnyh imeni else i if.
S kazhdym imenem (identifikatorom) v programme svyazan tip. On
zadaet te operacii, kotorye mogut primenyat'sya k imeni (t.e. k ob容ktu,
kotoryj oboznachaet imya), a takzhe interpretaciyu etih operacij.
Privedem primery:
int error_number;
float real(complex* p);
Poskol'ku peremennaya error_number opisana kak int (celoe), ej mozhno
prisvaivat', a takzhe mozhno ispol'zovat' ee znacheniya v arifmeticheskih
vyrazheniyah. Funkciyu real mozhno vyzyvat' s parametrom, soderzhashchim
adres complex. Mozhno poluchat' adresa i peremennoj, i funkcii.
Nekotorye imena, kak v nashem primere int i complex, yavlyayutsya imenami
tipov. Obychno imya tipa nuzhno, chtoby zadat' v opisanii tipa nekotoroe
drugoe imya. Krome togo, imya tipa mozhet ispol'zovat'sya
v kachestve operanda v operaciyah sizeof (s ee pomoshch'yu opredelyayut
razmer pamyati, neobhodimyj dlya ob容ktov etogo tipa) i new (s ee
pomoshch'yu mozhno razmestit' v svobodnoj pamyati ob容kt etogo tipa).
Naprimer:
int main()
{
int* p = new int;
cout << "sizeof(int) = " << sizeof(int) '\n';
}
Eshche imya tipa mozhet ispol'zovat'sya v operacii yavnogo preobrazovaniya
odnogo tipa k drugomu ($$3.2.5), naprimer:
float f;
char* p;
//...
long ll = long(p); // preobrazuet p v long
int i = int(f); // preobrazuet f v int
Osnovnye tipy S++ predstavlyayut samye rasprostranennye edinicy pamyati
mashin i vse osnovnye sposoby raboty s nimi. |to:
char
short int
int
long int
Perechislennye tipy ispol'zuyutsya dlya predstavleniya razlichnogo
razmera celyh. CHisla s plavayushchej tochkoj predstavleny tipami:
float
double
long double
Sleduyushchie tipy mogut ispol'zovat'sya dlya predstavleniya bezznakovyh celyh,
logicheskih znachenij, razryadnyh massivov i t.d.:
unsigned char
unsigned short int
unsigned int
unsigned long int
Nizhe privedeny tipy, kotorye ispol'zuyutsya dlya yavnogo zadaniya znakovyh
tipov:
signed char
signed short int
signed int
signed long int
Poskol'ku po umolchaniyu znacheniya tipa int schitayutsya znakovymi, to
sootvetstvuyushchie tipy s signed yavlyayutsya sinonimami tipov bez
etogo sluzhebnogo slova.
No tip signed char predstavlyaet osobyj interes: vse 3 tipa - unsigned char,
signed char i prosto char schitayutsya razlichnymi (sm. takzhe $$R.3.6.1).
Dlya kratkosti (i eto ne vlechet nikakih posledstvij) slovo int mozhno
ne ukazyvat' v mnogoslovnyh tipah, t.e. long oznachaet long int, unsigned -
unsigned int. Voobshche, esli v opisanii ne ukazan tip, to predpolagaetsya,
chto eto int. Naprimer, nizhe dany dva opredeleniya ob容kta tipa int:
const a = 1; // nebrezhno, tip ne ukazan
static x; // tot zhe sluchaj
Vse zhe obychno propusk tipa v opisanii v nadezhde, chto po umolchaniyu
eto budet tip int, schitaetsya durnym stilem. On mozhet vyzvat' tonkij i
nezhelatel'nyj effekt (sm. $$R.7.1).
Dlya hraneniya simvolov i raboty s nimi naibolee podhodit tip char.
Obychno on predstavlyaet bajt iz 8 razryadov. Razmery vseh ob容ktov v S++
kratny razmeru char, i po opredeleniyu znachenie sizeof(char) tozhdestvenno 1.
V zavisimosti ot mashiny znachenie tipa char mozhet byt' znakovym
ili bezznakovym celym. Konechno, znachenie tipa unsigned char vsegda
bezznakovoe, i, zadavaya yavno etot tip, my uluchshaem perenosimost'
programmy. Odnako, ispol'zovanie unsigned char vmesto char mozhet
snizit' skorost' vypolneniya programmy. Estestvenno, znachenie
tipa signed char vsegda znakovoe.
V yazyk vvedeno neskol'ko celyh, neskol'ko bezznakovyh tipov
i neskol'ko tipov s plavayushchej tochkoj, chtoby programmist mog polnee
ispol'zovat' vozmozhnosti sistemy komand. U mnogih mashin
znachitel'no razlichayutsya razmery vydelyaemoj pamyati, vremya dostupa
i skorost' vychislenij dlya znachenij razlichnyh osnovnyh tipov.
Kak pravilo, znaya osobennosti konkretnoj mashiny, legko vybrat'
optimal'nyj osnovnoj tip (naprimer, odin iz tipov int) dlya dannoj
peremennoj. Odnako, napisat' dejstvitel'no perenosimuyu programmu,
ispol'zuyushchuyu takie vozmozhnosti nizkogo urovnya, neprosto. Dlya razmerov
osnovnyh tipov vypolnyayutsya sleduyushchie sootnosheniya:
1==sizeof(char)<=sizeof(short)<=sizeof(int)<=sizeof(long)
sizeof(float)<=sizeof(double)<=sizeof(long double)
sizeof(I)==sizeof(signed I)==sizeof(unsigned I)
Zdes' I mozhet byt' tipa char, short, int ili long. Pomimo etogo
garantiruetsya, chto char predstavlen ne menee, chem 8 razryadami, short
- ne menee, chem 16 razryadami i long - ne menee, chem 32 razryadami. Tip char
dostatochen dlya predstavleniya lyubogo simvola iz nabora simvolov
dannoj mashiny. No eto oznachaet tol'ko to, chto tip char mozhet
predstavlyat' celye v diapazone 0..127. Predpolozhit' bol'shee -
riskovanno.
Tipy bezznakovyh celyh bol'she vsego podhodyat dlya takih programm, v
kotoryh pamyat' rassmatrivaetsya kak massiv razryadov. No, kak
pravilo, ispol'zovanie unsigned vmesto int, ne daet nichego horoshego,
hotya takim obrazom rasschityvali vyigrat' eshche odin razryad dlya
predstavleniya polozhitel'nyh celyh. Opisyvaya peremennuyu kak unsigned,
nel'zya garantirovat', chto ona budet tol'ko polozhitel'noj, poskol'ku
dopustimy neyavnye preobrazovaniya tipa, naprimer:
unsigned surprise = -1;
|to opredelenie dopustimo (hotya kompilyator mozhet vydat' preduprezhdenie
o nem).
2.3.2 Neyavnoe preobrazovanie tipa
V prisvaivanii i vyrazhenii osnovnye tipy mogut sovershenno svobodno
ispol'zovat'sya sovmestno. Znacheniya preobrazovyvayutsya vsyudu, gde
eto vozmozhno, takim obrazom, chtoby informaciya ne teryalas'. Tochnye
pravila preobrazovanij dany v $$R.4 i $$R.5.4.
Vse-taki est' situacii, kogda informaciya mozhet byt' poteryana ili
dazhe iskazhena. Potencial'nym istochnikom takih situacij stanovyatsya
prisvaivaniya, v kotoryh znachenie odnogo tipa prisvaivaetsya znacheniyu
drugogo tipa, prichem v predstavlenii poslednego ispol'zuetsya
men'she razryadov. Dopustim, chto sleduyushchie prisvaivaniya vypolnyayutsya
na mashine, v kotoroj celye predstavlyayutsya v dopolnitel'nom kode, i simvol
zanimaet 8 razryadov:
int i1 = 256+255;
char ch = i1 // ch == 255
int i2 = ch; // i2 == ?
V prisvaivanii ch=i1 teryaetsya odin razryad (i samyj vazhnyj!), a kogda
my prisvaivaem znachenie peremennoj i2, u peremennoj ch znachenie "vse
edinicy", t.e. 8 edinichnyh razryadov. No kakoe znachenie primet i2? Na
mashine DEC VAX, v kotoroj char predstavlyaet znakovye znacheniya, eto budet
-1, a na mashine Motorola 68K, v kotoroj char - bezznakovyj,
eto budet 255. V S++ net dinamicheskih sredstv kontrolya
podobnyh situacij, a kontrol' na etape translyacii voobshche slishkom
slozhen, poetomu nado byt' ostorozhnymi.
Ishodya iz osnovnyh (i opredelennyh pol'zovatelem) tipov, mozhno s
pomoshch'yu sleduyushchih operacij opisaniya:
* ukazatel'
& ssylka
[] massiv
() funkciya
a takzhe s pomoshch'yu opredeleniya struktur, zadat' drugie, proizvodnye tipy.
Naprimer:
int* a;
float v[10];
char* p[20]; // massiv iz 20 simvol'nyh ukazatelej
void f(int);
struct str { short length; char* p; };
Pravila postroeniya tipov s pomoshch'yu etih operacij podrobno ob座asneny
v $$R.8. Klyuchevaya ideya sostoit v tom, chto opisanie ob容kta proizvodnogo
tipa dolzhno otrazhat' ego ispol'zovanie, naprimer:
int v[10]; // opisanie vektora
i = v[3]; // ispol'zovanie elementa vektora
int* p; // opisanie ukazatelya
i = *p; // ispol'zovanie ukazuemogo ob容kta
Oboznacheniya, ispol'zuemye dlya proizvodnyh tipov, dostatochno trudny
dlya ponimaniya lish' potomu, chto operacii * i & yavlyayutsya prefiksnymi, a
[] i () - postfiksnymi. Poetomu v zadanii tipov, esli prioritety
operacij ne otvechayut celi, nado stavit' skobki. Naprimer, prioritet
operacii [] vyshe, chem u *, i my imeem:
int* v[10]; // massiv ukazatelej
int (*p)[10]; // ukazatel' massiva
Bol'shinstvo lyudej prosto zapominaet, kak vyglyadyat naibolee chasto
upotreblyaemye tipy.
Mozhno opisat' srazu neskol'ko imen v odnom opisanii. Togda ono soderzhit
vmesto odnogo imeni spisok otdelyaemyh drug ot druga zapyatymi
imen. Naprimer, mozhno tak opisat' dve peremennye celogo tipa:
int x, y; // int x; int y;
Kogda my opisyvaem proizvodnye tipy, ne nado zabyvat', chto operacii
opisanij primenyayutsya tol'ko k dannomu imeni (a vovse ne ko vsem
ostal'nym imenam togo zhe opisaniya). Naprimer:
int* p, y; // int* p; int y; NO NE int* y;
int x, *p; // int x; int* p;
int v[10], *p; // int v[10]; int* p;
No takie opisaniya zaputyvayut programmu, i, vozmozhno, ih sleduet
izbegat'.
Tip void sintaksicheski ekvivalenten osnovnym tipam, no ispol'zovat'
ego mozhno tol'ko v proizvodnom tipe. Ob容ktov tipa void ne sushchestvuet.
S ego pomoshch'yu zadayutsya ukazateli na ob容kty neizvestnogo tipa ili
funkcii, nevozvrashchayushchie znachenie.
void f(); // f ne vozvrashchaet znacheniya
void* pv; // ukazatel' na ob容kt neizvestnogo tipa
Ukazatel' proizvol'nogo tipa mozhno prisvaivat' peremennoj tipa void*.
Na pervyj vzglyad etomu trudno najti primenenie, poskol'ku dlya void*
nedopustimo kosvennoe obrashchenie (razymenovanie). Odnako, imenno
na etom ogranichenii osnovyvaetsya ispol'zovanie tipa void*. On
pripisyvaetsya parametram funkcij, kotorye ne dolzhny znat' istinnogo
tipa etih parametrov. Tip void* imeyut takzhe bestipovye ob容kty,
vozvrashchaemye funkciyami.
Dlya ispol'zovaniya takih ob容ktov nuzhno vypolnit' yavnuyu operaciyu
preobrazovaniya tipa. Takie funkcii obychno nahodyatsya na samyh nizhnih
urovnyah sistemy, kotorye upravlyayut apparatnymi
resursami. Privedem primer:
void* malloc(unsigned size);
void free(void*);
void f() // raspredelenie pamyati v stile Si
{
int* pi = (int*)malloc(10*sizeof(int));
char* pc = (char*)malloc(10);
//...
free(pi);
free(pc);
}
Oboznachenie: (tip) vyrazhenie - ispol'zuetsya dlya zadaniya operacii
preobrazovaniya vyrazheniya k tipu, poetomu pered prisvaivaniem
pi tip void*, vozvrashchaemyj v pervom vyzove malloc(), preobrazuetsya
v tip int. Primer zapisan v arhaichnom stile; luchshij stil'
upravleniya razmeshcheniem v svobodnoj pamyati pokazan v $$3.2.6.
Dlya bol'shinstva tipov T ukazatel' na T imeet tip T*. |to znachit, chto
peremennaya tipa T* mozhet hranit' adres ob容kta tipa T. Ukazateli na
massivy i funkcii, k sozhaleniyu, trebuyut bolee slozhnoj zapisi:
int* pi;
char** cpp; // ukazatel' na ukazatel' na char
int (*vp)[10]; // ukazatel' na massiv iz 10 celyh
int (*fp)(char, char*); // ukazatel' na funkciyu s parametrami
// char i char*, vozvrashchayushchuyu int
Glavnaya operaciya nad ukazatelyami - eto kosvennoe obrashchenie
(razymenovanie), t.e. obrashchenie k ob容ktu, na kotoryj nastroen
ukazatel'. |tu operaciyu obychno nazyvayut prosto kosvennost'yu.
Operaciya kosvennosti * yavlyaetsya prefiksnoj unarnoj operaciej.
Naprimer:
char c1 = 'a';
char* p = &c1; // p soderzhit adres c1
char c2 = *p; // c2 = 'a'
Peremennaya, na kotoruyu ukazyvaet p,- eto c1, a znachenie, kotoroe
hranitsya v c1, ravno 'a'. Poetomu prisvaivaemoe c2 znachenie *p
est' 'a'.
Nad ukazatelyami mozhno vypolnyat' i nekotorye arifmeticheskie operacii.
Nizhe v kachestve primera predstavlena funkciya, podschityvayushchaya chislo
simvolov v stroke, zakanchivayushchejsya nulevym simvolom (kotoryj
ne uchityvaetsya):
int strlen(char* p)
{
int i = 0;
while (*p++) i++;
return i;
}
Mozhno opredelit' dlinu stroki po-drugomu: snachala najti ee konec, a zatem
vychest' adres nachala stroki iz adresa ee konca.
int strlen(char* p)
{
char* q = p;
while (*q++) ;
return q-p-1;
}
SHiroko ispol'zuyutsya ukazateli na funkcii; oni osobo obsuzhdayutsya
v $$4.6.9
Dlya tipa T T[size] yavlyaetsya tipom "massiva iz size elementov tipa T".
|lementy indeksiruyutsya ot 0 do size-1. Naprimer:
float v[3]; // massiv iz treh chisel s plavayushchej tochkoj:
// v[0], v[1], v[2]
int a[2][5]; // dva massiva, iz pyati celyh kazhdyj
char* vpc; // massiv iz 32 simvol'nyh ukazatelej
Mozhno sleduyushchim obrazom zapisat' cikl, v kotorom pechatayutsya celye
znacheniya propisnyh bukv:
extern "C" int strlen(const char*); // iz <string.h>
char alpha[] = "abcdefghijklmnopqrstuvwxyz";
main()
{
int sz = strlen(alpha);
for (int i=0; i<sz; i++) {
char ch = alpha[i];
cout << '\''<< ch << '\''
<< " = " <<int(ch)
<< " = 0" << oct(ch)
<< " = 0x" << hex(ch) << '\n';
}
}
Zdes' funkcii oct() i hex() vydayut svoj parametr celogo tipa
v vos'merichnom i shestnadcaterichnom vide sootvetstvenno. Obe funkcii
opisany v <iostream.h>. Dlya podscheta chisla simvolov v alpha
ispol'zuetsya funkciya strlen() iz <string.h>, no vmesto nee mozhno
bylo ispol'zovat' razmer massiva alpha ($$2.4.4). Dlya mnozhestva
simvolov ASCII rezul'tat budet takim:
'a' = 97 = 0141 = 0x61
'b' = 98 = 0142 = 0x62
'c' = 99 = 0143 = 0x63
...
Otmetim, chto ne nuzhno ukazyvat' razmer massiva alpha: translyator
ustanovit ego, podschitav chislo simvolov v stroke, zadannoj v kachestve
inicializatora. Zadanie massiva simvolov v vide stroki inicializatora
- eto udobnyj, no k sozhaleniyu, edinstvennyj sposob podobnogo primeneniya
strok. Prisvaivanie stroki massivu nedopustimo, poskol'ku
v yazyke prisvaivanie massivam ne opredeleno, naprimer:
char v[9];
v = "a string"; // oshibka
Klassy pozvolyayut realizovat' predstavlenie strok s bol'shim naborom
operacij (sm. $$7.10).
Ochevidno, chto stroki prigodny tol'ko dlya inicializacii simvol'nyh
massivov; dlya drugih tipov prihoditsya ispol'zovat' bolee slozhnuyu
zapis'. Vprochem, ona mozhet ispol'zovat'sya i dlya simvol'nyh massivov.
Naprimer:
int v1[] = { 1, 2, 3, 4 };
int v2[] = { 'a', 'b', 'c', 'd' };
char v3[] = { 1, 2, 3, 4 };
char v4[] = { 'a', 'b', 'c', 'd' };
Zdes' v3 i v4 - massivy iz chetyreh (a ne pyati) simvolov; v4 ne okanchivaetsya
nulevym simvolom, kak togo trebuyut soglashenie o strokah i bol'shinstvo
bibliotechnyh funkcij. Ispol'zuya takoj massiv char my sami
gotovim pochvu dlya budushchih oshibok.
Mnogomernye massivy predstavleny kak massivy massivov. Odnako nel'zya
pri zadanii granichnyh znachenij indeksov ispol'zovat', kak eto delaetsya
v nekotoryh yazykah, zapyatuyu. Zapyataya - eto osobaya operaciya dlya
perechisleniya vyrazhenij (sm. $$3.2.2). Mozhno poprobovat' zadat' takoe
opisanie:
int bad[5,2]; // oshibka
ili takoe
int v[5][2];
int bad = v[4,1]; // oshibka
int good = v[4][1]; // pravil'no
Nizhe opisyvaetsya
massiv iz dvuh elementov, kazhdyj iz kotoryh yavlyaetsya, v svoyu ochered',
massivom iz 5 elementov tipa char:
char v[2][5];
V sleduyushchem primere pervyj massiv inicializiruetsya pyat'yu pervymi bukvami
alfavita, a vtoroj - pyat'yu mladshimi ciframi.
char v[2][5] = {
{ 'a', 'b', 'c', 'd', 'e' },
{ '0', '1', '2', '3', '4' }
};
main() {
for (int i = 0; i<2; i++) {
for (int j = 0; j<5; j++)
cout << "v[" << i << "][" << j
<< "]=" << v[i][j] << " ";
cout << '\n';
}
}
V rezul'tate poluchim:
v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e
v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4
2.3.7 Ukazateli i massivy
Ukazateli i massivy v yazyke Si++ tesno svyazany. Imya massiva mozhno
ispol'zovat' kak ukazatel' na ego pervyj element, poetomu primer s
massivom alpha mozhno zapisat' tak:
int main()
{
char alpha[] = "abcdefghijklmnopqrstuvwxyz";
char* p = alpha;
char ch;
while (ch = *p++)
cout << ch << " = " << int (ch)
<< " = 0" << oct(ch) << '\n';
}
Mozhno takzhe zadat' opisanie p sleduyushchim obrazom:
char* p = &alpha[0];
|ta ekvivalentnost' shiroko ispol'zuetsya pri vyzovah funkcij s
parametrom-massivom, kotoryj vsegda peredaetsya kak ukazatel' na ego
pervyj element. Takim obrazom, v sleduyushchem primere v oboih vyzovah
strlen peredaetsya odno i to zhe znachenie:
void f()
{
extern "C" int strlen(const char*); // iz <string.h>
char v[] = "Annemarie";
char* p = v;
strlen(p);
strlen(v);
}
No v tom i zagvoedka, chto obojti eto nel'zya: ne sushchestvuet sposoba tak
opisat' funkciyu, chtoby pri ee vyzove massiv v kopirovalsya ($$4.6.3).
Rezul'tat primeneniya k ukazatelyam arifmeticheskih operacij +,
-, ++ ili -- zavisit ot tipa ukazuemyh ob容ktov. Esli takaya operaciya
primenyaetsya k ukazatelyu p tipa T*, to schitaetsya, chto p ukazyvaet na
massiv ob容ktov tipa T. Togda p+1 oboznachaet sleduyushchij element
etogo massiva, a p-1 - predydushchij element. Otsyuda sleduet, chto
znachenie (adres) p+1 budet na sizeof(T) bajtov bol'she, chem znachenie
p. Poetomu v sleduyushchej programme
main()
{
char cv[10];
int iv[10];
char* pc = cv;
int* pi = iv;
cout << "char* " << long(pc+1)-long(pc) << '\n';
cout << "int* " << long(pi+1)-long(pi) << '\n';
}
s uchetom togo, chto na mashine avtora (Maccintosh) simvol zanimaet odin bajt,
a celoe - chetyre bajta, poluchim:
char* 1
int* 4
Pered vychitaniem ukazateli byli yavnoj operaciej preobrazovany
k tipu long ($$3.2.5). On ispol'zovalsya dlya preobrazovaniya vmesto
"ochevidnogo" tipa int, poskol'ku v nekotoryh realizaciyah yazyka S++
ukazatel' mozhet ne pomestit'sya v tip int (t.e. sizeof(int)<sizeof(char*)).
Vychitanie ukazatelej opredeleno tol'ko v tom sluchae, kogda
oni oba ukazyvayut na odin i tot zhe massiv (hotya v yazyke net
vozmozhnostej garantirovat' etot fakt). Rezul'tat vychitaniya odnogo
ukazatelya iz drugogo raven chislu (celoe) elementov massiva, nahodyashchihsya
mezhdu etimi ukazatelyami. Mozhno skladyvat' s ukazatelem ili vychitat' iz nego
znachenie celogo tipa; v oboih sluchayah rezul'tatom budet ukazatel'.
Esli poluchitsya znachenie, ne yavlyayushcheesya ukazatelem na element togo zhe
massiva, na kotoryj byl nastroen ishodnyj ukazatel' (ili ukazatelem na
sleduyushchij za massivom element), to rezul'tat ispol'zovaniya takogo
znacheniya neopredelen. Privedem primer:
void f()
{
int v1[10];
int v2[10];
int i = &v1[5]-&v1[3]; // 2
i = &v1[5]-&v2[3]; // neopredelennyj rezul'tat
int* p = v2+2; // p == &v2[2]
p = v2-2; // *p neopredeleno
}
Kak pravilo, slozhnyh arifmeticheskih operacij s ukazatelyami ne trebuetsya
i luchshe vsego ih izbegat'.
Sleduet skazat', chto v
bol'shinstve realizacij yazyka S++ net kontrolya nad granicami massivov.
Opisanie massiva ne yavlyaetsya samodostatochnym, poskol'ku neobyazatel'no
v nem budet hranit'sya chislo elementov massiva.
Ponyatie massiva v S yavlyaetsya, po suti, ponyatiem yazyka nizkogo
urovnya. Klassy pomogayut razvit' ego (sm. $$1.4.3).
Massiv predstavlyaet soboj sovokupnost' elementov odnogo tipa, a
struktura yavlyaetsya sovokupnost'yu elementov proizvol'nyh
(prakticheski) tipov. Naprimer:
struct address {
char* name; // imya "Jim Dandy"
long number; // nomer doma 61
char* street; // ulica "South Street"
char* town; // gorod "New Providence"
char* state[2]; // shtat 'N' 'J'
int zip; // indeks 7974
};
Zdes' opredelyaetsya novyj tip, nazyvaemyj address, kotoryj zadaet
pochtovyj adres. Opredelenie ne yavlyaetsya dostatochno obshchim, chtoby
uchest' vse sluchai adresov, no ono vpolne prigodno dlya primera. Obratite
vnimanie na tochku s zapyatoj v konce opredeleniya: eto odin iz
nemnogih v S++ sluchaev, kogda posle figurnoj skobki trebuetsya
tochka s zapyatoj, poetomu pro nee chasto zabyvayut.
Peremennye tipa address mozhno opisyvat' tochno tak zhe, kak i lyubye
drugie peremennye, a s pomoshch'yu operacii . (tochka) mozhno obrashchat'sya
k otdel'nym chlenam struktury. Naprimer:
address jd;
jd.name = "Jim Dandy";
jd.number = 61;
Inicializirovat' peremennye tipa struct mozhno tak zhe, kak massivy.
Naprimer:
address jd = {
"Jim Dandy",
61, "South Street",
"New Providence", {'N','J'}, 7974
};
No luchshe dlya etih celej ispol'zovat' konstruktor ($$5.2.4). Otmetim,
chto jd.state nel'zya inicializirovat' strokoj "NJ". Ved' stroki
okanchivayutsya nulevym simvolom '\0', znachit v stroke "NJ" tri simvola,
a eto na odin bol'she, chem pomeshchaetsya v jd.state.
K strukturnym ob容ktam chasto obrashchayutsya c pomoshch'yu ukazatelej,
ispol'zuya operaciyu ->. Naprimer:
void print_addr(address* p)
{
cout << p->name << '\n'
<< p->number << ' ' << p->street << '\n'
<< p->town << '\n'
<< p->state[0] << p->state[1]
<< ' ' << p->zip << '\n';
}
Ob容kty strukturnogo tipa mogut byt' prisvoeny, peredany kak fakticheskie
parametry funkcij i vozvrashcheny funkciyami v kachestve rezul'tata. Naprimer:
address current;
address set_current(address next)
{
address prev = current;
current = next;
return prev;
}
Drugie dopustimye operacii, naprimer, takie, kak sravnenie (== i !=),
neopredeleny. Odnako pol'zovatel' mozhet sam opredelit' eti operacii
(sm. glavu 7).
Razmer ob容kta strukturnogo tipa ne obyazatel'no raven summe
razmerov vseh ego chlenov. |to proishodit po toj prichine, chto
na mnogih mashinah trebuetsya razmeshchat' ob容kty opredelennyh tipov,
tol'ko vyravnivaya ih po nekotoroj zavisyashchej ot sistemy adresacii
granice (ili prosto potomu, chto rabota pri takom vyravnivanii budet
bolee effektivnoj ). Tipichnyj primer - eto vyravnivanie celogo po
slovnoj granice. V rezul'tate vyravnivaniya mogut poyavit'sya "dyrki" v
strukture. Tak, na uzhe upominavshejsya mashine avtora sizeof(address)
ravno 24, a ne 22, kak mozhno bylo ozhidat'.
Sleduet takzhe upomyanut', chto tip mozhno ispol'zovat' srazu posle ego
poyavleniya v opisanii, eshche do togo, kak budet zaversheno vse opisanie.
Naprimer:
struct link{
link* previous;
link* successor;
};
Odnako novye ob容kty tipa struktury nel'zya opisat' do teh por, poka ne
poyavitsya ee polnoe opisanie. Poetomu opisanie
struct no_good {
no_good member;
};
yavlyaetsya oshibochnym (translyator ne v sostoyanii ustanovit' razmer no_good).
CHtoby pozvolit' dvum (ili bolee) strukturnym tipam ssylat'sya drug na
druga, mozhno prosto opisat' imya odnogo iz nih kak imya nekotorogo
strukturnogo tipa. Naprimer:
struct list; // budet opredeleno pozdnee
struct link {
link* pre;
link* suc;
list* member_of;
};
struct list {
link* head;
};
Esli by ne bylo pervogo opisaniya list, opisanie chlena link privelo by k
sintaksicheskoj oshibke.
Mozhno takzhe ispol'zovat' imya strukturnogo tipa eshche do togo, kak tip budet
opredelen, esli tol'ko eto ispol'zovanie ne predpolagaet znaniya razmera
struktury. Naprimer:
class S; // 'S' - imya nekotorogo tipa
extern S a;
S f();
void g(S);
No privedennye opisaniya mozhno ispol'zovat' lish' posle togo, kak tip S
budet opredelen:
void h()
{
S a; // oshibka: S - neopisano
f(); // oshibka: S - neopisano
g(a); // oshibka: S - neopisano
}
2.3.9 |kvivalentnost' tipov
Dva strukturnyh tipa schitayutsya razlichnymi dazhe togda, kogda oni imeyut
odni i te zhe chleny. Naprimer, nizhe opredeleny razlichnye tipy:
struct s1 { int a; };
struct s2 { int a; };
V rezul'tate imeem:
s1 x;
s2 y = x; // oshibka: nesootvetstvie tipov
Krome togo, strukturnye tipy otlichayutsya ot osnovnyh tipov, poetomu
poluchim:
s1 x;
int i = x; // oshibka: nesootvetstvie tipov
Est', odnako, vozmozhnost', ne opredelyaya novyj tip, zadat' novoe imya
dlya tipa. V opisanii, nachinayushchemsya sluzhebnym slovom typedef, opisyvaetsya
ne peremennaya ukazannogo tipa, a vvoditsya novoe imya dlya tipa.
Privedem primer:
typedef char* Pchar;
Pchar p1, p2;
char* p3 = p1;
|to prosto udobnoe sredstvo sokrashcheniya zapisi.
Ssylku mozhno rassmatrivat' kak eshche odno imya ob容kta.
V osnovnom ssylki ispol'zuyutsya dlya zadaniya parametrov i vozvrashchaemyh
funkciyami znachenij , a takzhe dlya peregruzki operacij (sm.$$7).
Zapis' X& oboznachaet ssylku na X. Naprimer:
int i = 1;
int& r = i; // r i i ssylayutsya na odno i to zhe celoe
int x = r; // x = 1
r = 2; // i = 2;
Ssylka dolzhna byt' inicializirovana, t.e.
dolzhno byt' nechto, chto ona mozhet oboznachat'. Sleduet pomnit', chto
inicializaciya ssylki sovershenno otlichaetsya ot operacii prisvaivaniya.
Hotya mozhno ukazyvat' operacii nad ssylkoj, ni odna iz nih na samu ssylku
ne dejstvuet, naprimer,
int ii = 0;
int& rr = ii;
rr++; // ii uvelichivaetsya na 1
Zdes' operaciya ++ dopustima, no rr++ ne uvelichivaet samu
ssylku rr; vmesto etogo ++ primenyaetsya k celomu, t.e. k peremennoj ii.
Sledovatel'no, posle inicializacii znachenie ssylki ne mozhet byt'
izmeneno: ona vsegda ukazyvaet na tot ob容kt, k kotoromu byla privyazana
pri ee inicializacii. CHtoby poluchit' ukazatel' na ob容kt,
oboznachaemyj ssylkoj rr, mozhno napisat' &rr.
Ochevidnoj realizaciej ssylki mozhet sluzhit' postoyannyj ukazatel',
kotoryj ispol'zuetsya tol'ko dlya kosvennogo obrashcheniya. Togda inicializaciya
ssylki budet trivial'noj, esli v kachestve inicializatora ukazan adres
(t.e. ob容kt, adres kotorogo mozhno poluchit'; sm. $$R.3.7).
Inicializator dlya tipa T dolzhen byt' adresom. Odnako, inicializator
dlya &T mozhet byt' i ne adresom, i dazhe ne tipom T. V takih sluchayah
delaetsya sleduyushchee:
[1] vo-pervyh, esli neobhodimo, primenyaetsya preobrazovanie tipa
(sm.$$R.8.4.3);
[2] zatem poluchivsheesya znachenie pomeshchaetsya vo vremennuyu peremennuyu;
[3] nakonec, adres etoj peremennoj ispol'zuetsya v kachestve inicializatora
ssylki.
Pust' imeyutsya opisaniya:
double& dr = 1; // oshibka: nuzhen adres
const double& cdr = 1; // normal'no
|to interpretiruetsya tak:
double* cdrp; // ssylka, predstavlennaya kak ukazatel'
double temp;
temp = double(1);
cdrp = &temp;
Ssylki na peremennye i ssylki na konstanty razlichayutsya po sleduyushchej
prichine: v pervom sluchae sozdanie vremennoj peremennoj chrevato
oshibkami, poskol'ku prisvaivanie etoj peremennoj oznachaet prisvaivanie
vremennoj peremennoj, kotoraya mogla k etomu momentu ischeznut'.
Estestvenno, chto vo vtorom sluchae podobnyh problem ne sushchestvuet.
i ssylki na konstanty chasto ispol'zuyutsya kak parametry funkcij
(sm.$$R.6.3).
Ssylka mozhet ispol'zovat'sya dlya funkcii, kotoraya izmenyaet znachenie svoego
parametra. Naprimer:
void incr(int& aa) { aa++; }
void f()
{
int x = 1;
incr(x); // x = 2
}
Po opredeleniyu peredacha parametrov imeet tu zhe semantiku, chto i
inicializaciya, poetomu pri vyzove funkcii incr ee parametr aa
stanovitsya drugim imenem dlya x. Luchshe, odnako, izbegat' izmenyayushchih
svoi parametry funkcij, chtoby ne zaputyvat' programmu. V bol'shinstve
sluchaev predpochtitel'nee, chtoby funkciya vozvrashchala rezul'tat yavnym
obrazom, ili chtoby ispol'zovalsya parametr tipa ukazatelya:
int next(int p) { return p+1; }
void inc(int* p) { (*p)++; }
void g()
{
int x = 1;
x = next(x); // x = 2
inc(&x); // x = 3
}
Krome perechislennogo, s pomoshch'yu ssylok mozhno opredelit' funkcii,
ispol'zuemye kak v pravoj, tak i v levoj chastyah prisvaivaniya.
Naibolee interesnoe primenenie eto obychno nahodit pri opredelenii
netrivial'nyh pol'zovatel'skih tipov. V kachestve primera opredelim
prostoj associativnyj massiv. Nachnem s opredeleniya struktury
pair:
struct pair {
char* name; // stroka
int val; // celoe
};
Ideya zaklyuchaetsya v tom, chto so strokoj svyazyvaetsya nekotoroe celoe znachenie.
Netrudno napisat' funkciyu poiska find(), kotoraya rabotaet so strukturoj
dannyh, predstavlyayushchej associativnyj massiv. V nem dlya kazhdoj otlichnoj ot
drugih stroki soderzhitsya struktura pair (para: stroka i znachenie ). V
dannom primere - eto prosto massiv. CHtoby sokratit' primer, ispol'zuetsya
predel'no prostoj, hotya i neeffektivnyj algoritm:
const int large = 1024;
static pair vec[large+1];
pair* find(const char* p)
/*
// rabotaet so mnozhestvom par "pair":
// ishchet p, esli nahodit, vozvrashchaet ego "pair",
// v protivnom sluchae vozvrashchaet neispol'zovannuyu "pair"
*/
{
for (int i=0; vec[i].name; i++)
if (strcmp(p,vec[i].name)==0) return &vec[i];
if (i == large) return &vec[large-1];
return &vec[i];
}
|tu funkciyu ispol'zuet funkciya value(), kotoraya realizuet massiv celyh,
indeksiruemyj strokami (hotya privychnee stroki indeksirovat' celymi):
int& value(const char* p)
{
pair* res = find(p);
if (res->name == 0) { // do sih por stroka ne vstrechalas',
// znachit nado inicializirovat'
res->name = new char[strlen(p)+1];
strcpy(res->name,p);
res->val = 0; // nachal'noe znachenie ravno 0
}
return res->val;
}
Dlya zadannogo parametra (stroki) value() nahodit ob容kt,
predstavlyayushchij celoe (a ne prosto znachenie sootvetstvuyushchego celogo) i
vozvrashchaet ssylku na nego. |ti funkcii mozhno ispol'zovat', naprimer, tak:
const int MAX = 256; // bol'she dliny samogo dlinnogo slova
main()
// podschityvaet chastotu slov vo vhodnom potoke
{
char buf[MAX];
while (cin>>buf) value(buf)++;
for (int i=0; vec[i].name; i++)
cout << vec[i].name << ": " << vec [i].val<< '\n';
}
V cikle while iz standartnogo vhodnogo potoka cin chitaetsya po odnomu
slovu i zapisyvaetsya v bufer buf (sm. glava 10), pri etom kazhdyj
raz znachenie schetchika, svyazannogo so schityvaemoj strokoj, uvelichivaetsya.
Schetchik otyskivaetsya v associativnom massive vec s pomoshch'yu funkcii
find(). V cikle for pechataetsya poluchivshayasya tablica razlichnyh slov iz cin
vmeste s ih chastotoj. Imeya vhodnoj potok
aa bb bb aa aa bb aa aa
programma vydaet:
aa: 5
bb: 3
S pomoshch'yu shablonnogo klassa i peregruzhennoj operacii [] ($$8.8)
dostatochno prosto dovesti massiv iz etogo primera do nastoyashchego
associativnogo massiva.
V S++ mozhno zadavat' znacheniya vseh osnovnyh tipov:
simvol'nye konstanty, celye konstanty i konstanty s plavayushchej tochkoj.
Krome togo, nul' (0) mozhno ispol'zovat' kak znachenie ukazatelya
proizvol'nogo tipa, a simvol'nye stroki yavlyayutsya konstantami tipa
char[]. Est' vozmozhnost' opredelit' simvolicheskie konstanty.
Simvolicheskaya konstanta - eto imya, znachenie kotorogo v ego oblasti
vidimosti izmenyat' nel'zya. V S++ simvolicheskie konstanty mozhno zadat'
tremya sposobami: (1) dobaviv sluzhebnoe slovo const v opredelenii,
mozhno svyazat' s imenem lyuboe znachenie proizvol'nogo tipa;
(2) mnozhestvo celyh konstant mozhno opredelit' kak perechislenie;
(3) konstantoj yavlyaetsya imya massiva ili funkcii.
Celye konstanty mogut poyavlyat'sya v chetyreh oblich'yah: desyatichnye,
vos'merichnye, shestnadcaterichnye i simvol'nye konstanty. Desyatichnye
konstanty ispol'zuyutsya chashche vsego i vyglyadyat estestvenno:
0 1234 976 12345678901234567890
Desyatichnaya konstanta imeet tip int, esli ona umeshchaetsya v pamyat',
otvodimuyu dlya int, v protivnom sluchae ee tip long. Translyator dolzhen
preduprezhdat' o konstantah, velichina kotoryh prevyshaet vybrannyj format
predstavleniya chisel.
Konstanta, nachinayushchayasya s nulya, za kotorym sleduet x (0x), yavlyaetsya
shestnadcaterichnym chislom (s osnovaniem 16), a konstanta, kotoraya
nachinayushchayasya s nulya, za kotorym sleduet cifra, yavlyaetsya vos'merichnym
chislom (s osnovaniem 8). Privedem primery vos'merichnyh konstant:
0 02 077 0123
Ih desyatichnye ekvivalenty ravny sootvetstvenno: 0, 2, 63, 83.
V shestnadcaterichnoj zapisi eti konstanty vyglyadyat tak:
0x0 0x2 0x3f 0x53
Bukvy a, b, c, d, e i f ili ekvivalentnye im zaglavnye bukvy
ispol'zuyutsya dlya predstavleniya chisel 10, 11, 12, 13, 14 i 15,
sootvetstvenno. Vos'merichnaya i shestnadcaterichnaya formy zapisi naibolee
podhodyat dlya zadaniya nabora razryadov, a
ispol'zovanie ih dlya obychnyh chisel mozhet dat' neozhidannyj effekt.
Naprimer, na mashine, v kotoroj int predstavlyaetsya kak 16-razryadnoe
chislo v dopolnitel'nom kode, 0xffff est' otricatel'noe desyatichnoe
chislo -1. Esli by dlya predstavleniya celogo ispol'zovalos' bol'shee chislo
razryadov, to eto bylo by chislom 65535.
Okonchanie U mozhet ispol'zovat'sya dlya yavnogo zadaniya konstant tipa
unsigned. Analogichno, okonchanie L yavno zadaet konstantu tipa long.
Naprimer:
void f(int);
void f(unsigned int);
void f(long int);
void g()
{
f(3); // vyzov f(int)
f(3U); // vyzov f(unsigned int)
f(3L); // vyzov f(long int)
}
2.4.2 Konstanty s plavayushchej tochkoj
Konstanty s plavayushchej tochkoj imeyut tip double. Translyator dolzhen
preduprezhdat' o takih konstantah, znachenie kotoryh ne ukladyvaetsya v
format, vybrannyj dlya predstavleniya chisel s plavayushchej tochkoj. Privedem
primery konstant s plavayushchej tochkoj:
1.23 .23 0.23 1. 1.0 1.2e10 1.23e-15
Otmetim, chto vnutri konstanty s plavayushchej tochkoj ne dolzhno byt' probelov.
Naprimer, 65.43 e-21 ne yavlyaetsya konstantoj s plavayushchej tochkoj, translyator
raspoznaet eto kak chetyre otdel'nye leksemy:
65.43 e - 21
chto vyzovet sintaksicheskuyu oshibku.
Esli nuzhna konstanta s plavayushchej tochkoj tipa float, to ee mozhno poluchit',
ispol'zuya okonchanie f:
3.14159265f 2.0f 2.997925f
2.4.3 Simvol'nye konstanty
Simvol'noj konstantoj yavlyaetsya simvol, zaklyuchennyj v odinochnye kavychki,
naprimer, 'a' ili '0'. Simvol'nye konstanty mozhno schitat' konstantami,
kotorye dayut imena celym znacheniyam simvolov iz nabora, prinyatogo na
mashine, na kotoroj vypolnyaetsya programma.
|to neobyazatel'no tot zhe nabor simvolov, kotoryj est' na mashine,
gde programma translirovalas'. Takim obrazom, esli vy zapuskaete
programmu na mashine, ispol'zuyushchej nabor simvolov
ASCII, to znachenie '0' ravno 48, a esli mashina ispol'zuet kod EBCDIC,
to ono budet ravno 240. Ispol'zovanie simvol'nyh konstant vmesto ih
desyatichnogo celogo ekvivalenta povyshaet perenosimost' programm.
Nekotorye special'nye kombinacii simvolov, nachinayushchiesya s obratnoj
drobnoj cherty, imeyut standartnye nazvaniya:
Konec stroki NL(LF) \n
Gorizontal'naya tabulyaciya HT \t
Vertikal'naya tabulyaciya VT \v
Vozvrat BS \b
Vozvrat karetki CR \r
Perevod formata FF \f
Signal BEL \a
Obratnaya drobnaya cherta \ \\
Znak voprosa ? \?
Odinochnaya kavychka ' \'
Dvojnaya kavychka " \"
Nulevoj simvol NUL \0
Vos'merichnoe chislo ooo \ooo
SHestnadcaterichnoe chislo hhh \xhhh
Nesmotrya na ih vid, vse eti kombinacii zadayut odin simvol. Tip
simvol'noj konstanty - char. Mozhno takzhe zadavat' simvol s pomoshch'yu
vos'merichnogo chisla, predstavlennogo odnoj, dvumya ili tremya
vos'merichnymi ciframi (pered ciframi idet \) ili s pomoshch'yu
shestnadcaterichnogo chisla
(pered shestnadcaterichnymi ciframi idet \x). CHislo shestnadcaterichnyh
cifr v takoj posledovatel'nosti neogranicheno. Posledovatel'nost'
vos'merichnyh ili shestnadcaterichnyh cifr zavershaetsya pervym simvolom,
ne yavlyayushchimsya takoj cifroj. Privedem primery:
'\6' '\x6' 6 ASCII ack
'\60' '\x30' 48 ASCII '0'
'\137' '\x05f' 95 ASCII '_'
|tim sposobom mozhno predstavit' lyuboj simvol iz nabora simvolov
mashiny. V chastnosti, zadavaemye takim obrazom simvoly mozhno
vklyuchat' v simvol'nye stroki (sm. sleduyushchij razdel). Zametim, chto
esli dlya simvolov
ispol'zuetsya chislovaya forma zadaniya, to narushaetsya perenosimost'
programmy mezhdu mashinami s razlichnymi naborami simvolov.
Stroka - eto posledovatel'nost' simvolov, zaklyuchennaya v dvojnye kavychki:
"eto stroka"
Kazhdaya stroka soderzhit na odin simvol bol'she, chem yavno zadano:
vse stroki okanchivayutsya nulevym simvolom ('\0'), imeyushchim
znachenie 0. Poetomu
sizeof("asdf")==5;
Tipom stroki schitaetsya "massiv iz sootvetstvuyushchego chisla simvolov",
poetomu tip "asdf" est' char[5]. Pustaya stroka zapisyvaetsya kak
"" i imeet tip char[1]. Otmetim, chto dlya lyuboj stroki s vypolnyaetsya
strlen(s)==sizeof(s)-1, poskol'ku funkciya strlen() ne uchityvaet
zavershayushchij simvol '\0'.
Vnutri stroki mozhno ispol'zovat' dlya predstavleniya nevidimyh
simvolov special'nye kombinacii s \. V chastnosti, v stroke mozhno
zadat' sam simvol dvojnoj kavychki " ili simvol \. CHashche vsego iz
takih simvolov okazyvaetsya nuzhnym simvol konca stroki '\n', naprimer:
cout << "zvukovoj signal v konce soobshcheniya\007\n"
Zdes' 7 - eto znachenie v ASCII simvola BEL (signal), kotoryj v
perenosimom vide oboznachaetsya kak \a.
Net vozmozhnosti zadat' v stroke "nastoyashchij" simvol konca stroki:
"eto ne stroka,
a sintaksicheskaya oshibka"
Dlya bol'shej naglyadnosti programmy dlinnye stroki mozhno razbivat'
probelami, naprimer:
char alpha[] = "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Podobnye, podryad idushchie, stroki budut ob容dinyat'sya v odnu, poetomu
massiv alpha mozhno ekvivalentnym obrazom inicializirovat' s pomoshch'yu
odnoj stroki:
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
V stroke mozhno zadavat' simvol '\0', no bol'shinstvo programm
ne ozhidaet posle nego vstrechi s kakimi-libo eshche simvolami. Naprimer,
stroku "asdf\000hjkl" standartnye funkcii strcpy() i strlen()
budut rassmatrivat' kak stroku "asdf".
Esli vy zadaete v stroke posledovatel'nost'yu vos'merichnyh cifr
chislovuyu konstantu, to razumno ukazat' vse tri cifry. Zapis'
etoj stroki i tak ne slishkom prosta, chtoby eshche i razdumyvat',
otnositsya li cifra k chislu ili yavlyaetsya otdel'nym simvolom.
Dlya shestnadcaterichnyh konstant ispol'zujte dva razryada. Rassmotrim
sleduyushchie primery:
char v1[] = "a\x0fah\0129"; // 'a' '\xfa' 'h' '\12' '9'
char v2[] = "a\xfah\129"; // 'a' '\xfa' 'h' '\12' '9'
char v3[] = "a\xfad\127"; // 'a' '\xfad' '\127'
Nul' (0) imeet tip int. Blagodarya standartnym preobrazovaniyam ($$R.4)
0 mozhno ispol'zovat' kak konstantu celogo tipa, ili tipa s plavayushchej
tochkoj, ili tipa ukazatelya. Nel'zya razmestit' nikakoj ob容kt, esli
vmesto adresa ukazan 0. Kakoj iz tipov nulya ispol'zovat', opredelyaetsya
kontekstom. Obychno (no neobyazatel'no) nul' predstavlyaetsya
posledovatel'nost'yu razryadov "vse nuli" podhodyashchej dliny.
2.5 Poimenovannye konstanty
Dobaviv k opisaniyu ob容kta sluzhebnoe slovo const, mozhno prevratit'
etot ob容kt iz peremennoj v konstantu, naprimer:
const int model = 90;
const int v[] = { 1, 2, 3, 4 };
Poskol'ku konstante nel'zya nichego prisvoit', ona dolzhna byt'
inicializirovana. Opisyvaya kakoj-libo ob容kt kak const, my garantiruem,
chto ego znachenie ne izmenyaetsya v oblasti vidimosti:
model = 200; // oshibka
model++; // oshibka
Otmetim, chto specifikaciya const skoree ogranichivaet vozmozhnosti
ispol'zovaniya ob容kta, chem ukazyvaet, gde sleduet razmeshchat' ob容kt.
Mozhet byt' vpolne razumnym i dazhe poleznym opisanie funkcii s tipom
vozvrashchaemogo znacheniya const:
const char* peek(int i) // vernut' ukazatel' na stroku-konstantu
{
return hidden[i];
}
Privedennuyu funkciyu mozhno bylo by ispol'zovat' dlya peredachi stroki,
zashchishchennoj ot zapisi, v druguyu programmu, gde ona budet chitat'sya.
Voobshche govorya, translyator mozhet vospol'zovat'sya tem faktom, chto ob容kt
yavlyaetsya const, dlya razlichnyh celej (konechno, eto zavisit ot
"razumnosti" translyatora). Samoe ochevidnoe - eto to, chto dlya
konstanty ne nuzhno otvodit' pamyat', poskol'ku ee znachenie izvestno
translyatoru. Dalee, inicializator dlya konstanty, kak pravilo (no ne
vsegda) yavlyaetsya postoyannym vyrazheniem, kotoroe mozhno vychislit' na
etape translyacii. Odnako, dlya massiva konstant obychno prihoditsya
otvodit' pamyat', poskol'ku v obshchem sluchae translyator ne znaet,
kakoj element massiva ispol'zuetsya v vyrazhenii. No i v etom sluchae
na mnogih mashinah vozmozhna optimizaciya, esli pomestit' takoj massiv
v zashchishchennuyu ot zapisi pamyat'.
Zadavaya ukazatel', my imeem delo s dvumya ob容ktami: s samim ukazatelem
i s ukazuemym ob容ktom. Esli v opisanii ukazatelya est' "prefiks"
const, to konstantoj ob座avlyaetsya sam ob容kt, no ne ukazatel' na nego,
naprimer:
const char* pc = "asdf"; // ukazatel' na konstantu
pc[3] = 'a'; // oshibka
pc = "ghjk"; // normal'no
CHtoby opisat' kak konstantu sam ukazatel', a ne ukazuemyj ob容kt,
nuzhno ispol'zovat' operaciyu * pered const. Naprimer:
char *const cp = "asdf"; // ukazatel'-konstanta
cp[3] = 'a'; // normal'no
cp = "ghjk"; // oshibka
CHtoby sdelat' konstantami i ukazatel', i ob容kt, nado oba ob座avit'
const, naprimer:
const char *const cpc = "asdf"; // ukazatel'-konstanta na const
cpc[3] = 'a'; // oshibka
cpc = "ghjk"; // oshibka
Ob容kt mozhet byt' ob座avlen konstantoj pri obrashchenii k nemu s pomoshch'yu
ukazatelya, i v to zhe vremya byt' izmenyaemym, esli obrashchat'sya k
nemu drugim sposobom. Osobenno eto udobno ispol'zovat' dlya parametrov
funkcii. Opisav parametr-ukazatel' funkcii kak const, my zapreshchaem
izmenyat' v nej ukazuemyj ob容kt, naprimer:
char* strcpy(char* p, const char* q); // ne mozhet izmenyat' *q
Ukazatelyu na konstantu mozhno prisvoit' adres peremennoj, t.k. eto
ne prineset vreda. Odnako, adres konstanty nel'zya prisvaivat' ukazatelyu
bez specifikacii const, inache stanet vozmozhnym menyat' ee znachenie,
naprimer:
int a = 1;
const int c = 2;
const int* p1 = &c; // normal'no
const int* p2 = &a; // normal'no
int* p3 = &c; // oshibka
*p3 = 7; // menyaet znachenie c
Est' sposob svyazyvaniya imen s celymi konstantami, kotoryj chasto bolee
udoben, chem opisanie s const. Naprimer:
enum { ASM, AUTO, BREAK };
Zdes' opredeleny tri celyh konstanty, kotorye nazyvayutsya elementami
perechisleniya, i im prisvoeny znacheniya. Poskol'ku po umolchaniyu znacheniya
elementov perechisleniya nachinayutsya s 0 i idut v vozrastayushchem poryadke,
to privedennoe perechislenie ekvivalentno opredeleniyam:
const ASM = 0;
const AUTO = 1;
const BREAK = 2;
Perechislenie mozhet imet' imya, naprimer:
enum keyword { ASM, AUTO, BREAK };
Imya perechisleniya stanovitsya novym tipom. S pomoshch'yu standartnyh
preobrazovanij tip perechisleniya mozhet neyavno privodit'sya k tipu int.
Obratnoe preobrazovanie (iz tipa int v perechislenie) dolzhno byt' zadano
yavno. Naprimer:
void f()
{
keyword k = ASM;
int i = ASM;
k = i // oshibka
k = keyword(i);
i = k;
k = 4; // oshibka
}
Poslednee preobrazovanie poyasnyaet, pochemu net neyavnogo preobrazovaniya
iz int v perechislenie: bol'shinstvo znachenij tipa int ne imeet
predstavleniya v dannom perechislenii.
Opisav peremennuyu s tipom keyword vmesto ochevidnogo int, my dali
kak pol'zovatelyu, tak i translyatoru opredelennuyu informaciyu o tom,
kak budet ispol'zovat'sya eta peremennaya. Naprimer, dlya sleduyushchego
operatora
keyword key;
switch (key) {
case ASM:
// vypolnit' chto-libo
break;
case BREAK:
// vypolnit' chto-libo
break;
}
translyator mozhet vydat' preduprezhdenie, poskol'ku iz treh vozmozhnyh
znachenij tipa keyword ispol'zuyutsya tol'ko dva.
Znacheniya elementov perechisleniya mozhno zadavat' i yavno. Naprimer:
enum int16 {
sign=0100000,
most_significant=040000,
least_significant=1
};
Zadavaemye znacheniya neobyazatel'no dolzhny byt' razlichnymi, polozhitel'nymi
i idti v vozrastayushchem poryadke.
V processe sozdaniya netrivial'noj programmy rano ili pozdno nastupaet
moment, kogda trebuetsya bol'she pamyati, chem mozhno vydelit' ili
zaprosit'. Est' dva sposoba vyzhat' eshche nekotoroe kolichestvo pamyati:
[1] pakovat' v bajty peremennye s malymi znacheniyami;
[2] ispol'zovat' odnu i tu zhe pamyat' dlya hraneniya raznyh ob容ktov
v raznoe vremya.
Pervyj sposob realizuetsya s pomoshch'yu polej, a vtoroj - s pomoshch'yu
ob容dinenij. I te, i drugie opisyvayutsya nizhe. Poskol'ku naznachenie
etih konstrukcij svyazano v osnovnom s optimizaciej programmy, i
poskol'ku, kak pravilo, oni neperenosimy, programmistu sleduet
horoshen'ko podumat', prezhde chem ispol'zovat' ih. CHasto luchshe izmenit'
algoritm raboty s dannymi, naprimer, bol'she ispol'zovat' dinamicheski
vydelyaemuyu pamyat', chem zaranee otvedennuyu staticheskuyu pamyat'.
Kazhetsya rastochitel'nym ispol'zovat' dlya priznaka, prinimayushchego
tol'ko dva znacheniya ( naprimer: da, net) tip char, no ob容kt tipa
char yavlyaetsya v S++ naimen'shim ob容ktom, kotoryj mozhet nezavisimo
razmeshchat'sya v pamyati. Odnako, est' vozmozhnost' sobrat' peremennye
s malym diapazonom znachenij voedino, opredeliv ih kak polya struktury.
CHlen struktury yavlyaetsya polem, esli v ego opredelenii posle imeni
ukazano chislo razryadov, kotoroe on dolzhen zanimat'. Dopustimy
bezymyannye polya. Oni ne vliyayut na rabotu s poimenovannymi polyami,
no mogut uluchshit' razmeshchenie polej v pamyati dlya konkretnoj mashiny:
struct sreg {
unsigned enable : 1;
unsigned page : 3;
unsigned : 1; // ne ispol'zuetsya
unsigned mode : 2;
unsigned : 4; // ne ispol'zuetsya
unsigned access : 1;
unsigned length : 1;
unsigned non_resident : 1;
};
Privedennaya struktura opisyvaet razryady nulevogo
registra sostoyaniya DEC PDP11/45 (predpolagaetsya, chto polya v slove
razmeshchayutsya sleva napravo). |tot primer pokazyvaet takzhe drugoe
vozmozhnoe primenenie polej: davat' imena tem chastyam
ob容kta, razmeshchenie kotoryh opredeleno izvne. Pole dolzhno imet'
celyj tip ($$R.3.6.1 i $$R.9.6), i ono ispol'zuetsya analogichno drugim
ob容ktam celogo tipa. No est' isklyuchenie: nel'zya brat' adres polya.
V yadre operacionnoj sistemy ili v otladchike tip sreg mog by
ispol'zovat'sya sleduyushchim obrazom:
sreg* sr0 = (sreg*)0777572;
//...
if (sr0->access) { // narushenie prav dostupa
// razobrat'sya v situacii
sr0->access = 0;
}
Tem ne menee,
primenyaya polya dlya upakovki neskol'kih peremennyh v odin bajt, my
neobyazatel'no sekonomim pamyat'. |konomitsya pamyat' dlya dannyh, no
na bol'shinstve mashin odnovremenno vozrastaet ob容m komand,
nuzhnyh dlya raboty s upakovannymi dannymi.
Izvestny dazhe takie programmy, kotorye znachitel'no sokrashchalis' v ob容me,
esli dvoichnye peremennye, zadavaemye polyami, preobrazovyvalis' v
peremennye tipa char! Krome togo, dostup k char ili int obychno
proishodit namnogo bystree, chem dostup k polyu. Polya - eto prosto
udobnaya kratkaya forma zadaniya logicheskih operacij dlya izvlecheniya
ili zaneseniya informacii v chasti slova.
Rassmotrim tablicu imen, v kotoroj kazhdyj element soderzhit imya i
ego znachenie. Znachenie mozhet zadavat'sya libo strokoj, libo celym chislom:
struct entry {
char* name;
char type;
char* string_value; // ispol'zuetsya esli type == 's'
int int_value; // ispol'zuetsya esli type == 'i'
};
void print_entry(entry* p)
{
switch(p->type) {
case 's':
cout << p->string_value;
break;
case 'i':
cout << p->int_value;
break;
default:
cerr << "type corrupted\n";
break;
}
}
Poskol'ku peremennye
string_value i int_value nikogda ne mogut ispol'zovat'sya odnovremenno,
ochevidno, chto chast' pamyati propadaet vpustuyu. |to mozhno legko ispravit',
opisav obe peremennye kak chleny ob容dineniya, naprimer, tak:
struct entry {
char* name;
char type;
union {
char* string_value; // ispol'zuetsya esli type == 's'
int int_value; // ispol'zuetsya esli type == 'i'
};
};
Teper' garantiruetsya, chto pri vydelenii pamyati dlya entry chleny
string_value i int_value budut razmeshchat'sya s odnogo adresa, i
pri etom ne nuzhno menyat' vse chasti programmy, rabotayushchie s entry.
Iz etogo sleduet, chto vse chleny ob容dineniya vmeste zanimayut takoj zhe
ob容m pamyati, kakoj zanimaet naibol'shij chlen ob容dineniya.
Nadezhnyj sposob raboty s ob容dineniem zaklyuchaetsya v tom, chtoby
vybirat' znachenie s pomoshch'yu togo zhe samogo chlena, kotoryj ego zapisyval.
Odnako, v bol'shih programmah trudno garantirovat', chto ob容dinenie
ispol'zuetsya tol'ko takim sposobom, a v rezul'tate ispol'zovaniya
ne togo chlena ob枸dineniya mogut voznikat' trudno obnaruzhivaemye oshibki.
No mozhno vstroit' ob容dinenie v takuyu strukturu, kotoraya obespechit
pravil'nuyu svyaz' mezhdu znacheniem polya tipa i tekushchim tipom chlena
ob容dineniya ($$5.4.6).
Inogda ob容dineniya ispol'zuyut dlya "psevdopreobrazovanij" tipa
(v osnovnom na eto idut programmisty, privykshie k yazykam, v kotoryh
net sredstv preobrazovaniya tipov, i v rezul'tate prihoditsya obmanyvat'
translyator). Privedem primer takogo "preobrazovaniya" int v int*
na mashine VAX, kotoroe dostigaetsya prostym sovpadeniem razryadov:
struct fudge {
union {
int i;
int* p;
};
};
fudge a;
a.i = 4095;
int* p = a.p; // nekorrektnoe ispol'zovanie
V dejstvitel'nosti eto vovse ne preobrazovanie tipa, t.k. na odnih
mashinah int i int* zanimayut raznyj ob容m pamyati, a na drugih celoe
ne mozhet razmeshchat'sya po adresu, zadavaemomu nechetnym chislom. Takoe
ispol'zovanie ob容dinenij ne yavlyaetsya perenosimym, togda kak
sushchestvuet perenosimyj sposob zadaniya yavnogo preobrazovaniya
tipa ($$3.2.5).
Inogda ob容dineniya ispol'zuyut special'no, chtoby izbezhat'
preobrazovaniya tipov. Naprimer, mozhno ispol'zovat' fudge, chtoby
uznat', kak predstavlyaetsya ukazatel' 0:
fudge.p = 0;
int i = fudge.i; // i neobyazatel'no dolzhno byt' 0
Ob容dineniyu mozhno dat' imya, to est' mozhno sdelat' ego
polnopravnym tipom. Naprimer, fudge mozhno opisat' tak:
union fudge {
int i;
int* p;
};
i ispol'zovat' (nekorrektno) tochno tak zhe, kak i ran'she. Vmeste s tem,
poimenovannye ob容dineniya mozhno ispol'zovat' i vpolne korrektnym
i opravdannym sposobom (sm. $$5.4.6).
1. (*1) Zapustit' programmu "Hello, world" (sm. $$1.3.1).
2. (*1) Dlya kazhdogo opisaniya iz $$2.1 sdelat' sleduyushchee: esli opisanie
ne yavlyaetsya opredeleniem, to napisat' sootvetstvuyushchee opredelenie;
esli zhe opisanie yavlyaetsya opredeleniem, napisat' dlya nego opisanie,
kotoroe ne yavlyalos' by odnovremenno i opredeleniem.
3. (*1) Napishite opisaniya sleduyushchih ob容ktov: ukazatelya na simvol;
massiva iz 10 celyh; ssylki na massiv iz 10 celyh; ukazatelya
na massiv simvol'nyh strok; ukazatelya na ukazatel' na simvol;
celogo-konstanty; ukazatelya na celoe-konstantu; konstantnogo
ukazatelya na celoe. Opisaniya snabdit' inicializaciej.
4. (*1.5) Napishite programmu, kotoraya pechataet razmery osnovnyh tipov
i tipa ukazatelya. Ispol'zujte operaciyu sizeof.
5. (*1.5) Napishite programmu, kotoraya pechataet bukvy ot 'a' do 'z' i cifry
ot '0' do '9' i ih celye znacheniya. Prodelajte to zhe samoe dlya drugih
vidimyh simvolov. Prodelajte eto, ispol'zuya shestnadcaterichnuyu
zapis'.
6. (*1) Napechatajte posledovatel'nost' razryadov predstavleniya ukazatelya
0 na vashej mashine. Podskazka: sm.$$2.6.2.
7. (*1.5) Napishite funkciyu, pechatayushchuyu poryadok i mantissu parametra tipa
double.
8. (*2) Kakovy na ispol'zuemoj vami mashine naibol'shie i naimen'shie
znacheniya sleduyushchih tipov: char, short,int,long, float, double,
long double, unsigned, char*, int* i void*? Est' li kakie-to
osobye ogranicheniya na eti znacheniya? Naprimer, mozhet li int* byt'
nechetnym celym? Kak vyravnivayutsya v pamyati ob容kty etih tipov?
Naprimer, mozhet li celoe imet' nechetnyj adres?
9. (*1) Kakova maksimal'naya dlina lokal'nogo imeni, kotoroe
mozhno ispol'zovat' v vashej realizacii S++ ? Kakova maksimal'naya
dlina vneshnego imeni? Est' li kakie-nibud' ogranicheniya na simvoly,
kotorye mozhno ispol'zovat' v imeni?
10. (*1) Napishite funkciyu, kotoraya menyaet mestami znacheniya dvuh celyh.
V kachestve tipa parametrov ispol'zujte int*. Napishite druguyu funkciyu
s tem zhe naznacheniem, ispol'zuya v kachestve tipa parametrov int&.
11. (*1) Kakov razmer massiva str v sleduyushchem primere:
char str[] = "a short string";
Kakova dlina stroki "a short string"?
12. (*1.5) Sostav'te tablicu iz nazvanij mesyacev goda i chisla dnej
v kazhdom iz nih. Napishite programmu, pechatayushchuyu ee. Prodelajte
eto dvazhdy: odin raz - ispol'zuya massivy dlya nazvanij mesyacev
i kolichestva dnej, a drugoj raz - ispol'zuya massiv struktur,
kazhdaya iz kotoryh soderzhit nazvanie mesyaca i kolichestvo dnej v nem.
13. (*1) S pomoshch'yu typedef opredelite tipy: unsigned char, konstantnyj
unsigned char, ukazatel' na celoe, ukazatel' na ukazatel' na
simvol, ukazatel' na massiv simvolov, massiv iz 7 ukazatelej
na celoe, ukazatel' na massiv iz 7 ukazatelej na celoe i massiv iz
8 massivov iz 7 ukazatelej na celoe.
14. (*1) Opredelit' funkcii f(char), g(char&) i h(const char&) i
vyzvat' ih, ispol'zuya v kachestve parametrov 'a', 49, 3300, c, uc, i
sc, gde c - char, uc - unsigned char i sc - signed char. Kakoj
vyzov yavlyaetsya zakonnym? Pri kakom vyzove translyatoru pridetsya
zavesti vremennuyu peremennuyu?
* GLAVA 3. VYRAZHENIYA I OPERATORY
"No s drugoj storony ne sleduet
zabyvat' pro effektivnost'"
(Dzhon Bentli)
S++ imeet sravnitel'no nebol'shoj nabor operatorov, kotoryj pozvolyaet
sozdavat' gibkie struktury upravleniya, i bogatyj nabor operacij dlya
raboty s dannymi. Osnovnye ih vozmozhnosti pokazany v etoj glave na odnom
zavershennom primere. Zatem privoditsya svodka vyrazhenij, i podrobno
obsuzhdayutsya operacii preobrazovaniya tipa i razmeshchenie v svobodnoj pamyati.
Dalee dana svodka operatorov, a v konce glavy obsuzhdaetsya vydelenie
teksta probelami i ispol'zovanie kommentariev.
My poznakomimsya s vyrazheniyami i operatorami na primere programmy
kal'kulyatora. Kal'kulyator realizuet chetyre osnovnyh arifmeticheskih
dejstviya v vide infiksnyh operacij nad chislami s plavayushchej tochkoj.
V kachestve uprazhneniya predlagaetsya dobavit' k kal'kulyatoru
peremennye. Dopustim, vhodnoj potok imeet vid:
r=2.5
area=pi*r*r
(zdes' pi imeet predopredelennoe znachenie). Togda programma kal'kulyatora
vydast:
2.5
19.635
Rezul'tat vychislenij dlya pervoj vhodnoj stroki raven 2.5, a rezul'tat
dlya vtoroj stroki - eto 19.635.
Programma kal'kulyatora sostoit iz chetyreh osnovnyh chastej:
analizatora, funkcii vvoda, tablicy imen i drajvera. Po suti - eto
translyator v miniatyure, v kotorom analizator provodit sintaksicheskij
analiz, funkciya vvoda obrabatyvaet vhodnye dannye i provodit
leksicheskij analiz, tablica imen hranit postoyannuyu informaciyu, nuzhnuyu
dlya raboty, a drajver vypolnyaet inicializaciyu,
vyvod rezul'tatov i obrabotku oshibok. K takomu kal'kulyatoru mozhno
dobavit' mnogo drugih poleznyh vozmozhnostej, no programma ego i tak
dostatochno velika (200 strok), a vvedenie novyh vozmozhnostej
tol'ko uvelichit ee ob容m, ne davaya dopolnitel'noj
informacii dlya izucheniya S++.
Grammatika yazyka kal'kulyatora opredelyaetsya sleduyushchimi pravilami:
programma:
END // END - eto konec vvoda
spisok-vyrazhenij END
spisok-vyrazhenij:
vyrazhenie PRINT // PRINT - eto '\n' ili ';'
vyrazhenie PRINT spisok-vyrazhenij
vyrazhenie:
vyrazhenie + term
vyrazhenie - term
term
term:
term / pervichnoe
term * pervichnoe
pervichnoe
pervichnoe:
NUMBER // chislo s plavayushchej zapyatoj v S++
NAME // imya v yazyke S++ za isklyucheniem '_'
NAME = vyrazhenie
- pervichnoe
( vyrazhenie )
Inymi slovami, programma est' posledovatel'nost' strok, a kazhdaya
stroka soderzhit odno ili neskol'ko vyrazhenij, razdelennyh tochkoj
s zapyatoj. Osnovnye elementy vyrazheniya - eto chisla, imena i
operacii *, /, +, - (unarnyj i binarnyj minus) i =. Imena
neobyazatel'no opisyvat' do ispol'zovaniya.
Dlya sintaksicheskogo analiza ispol'zuetsya metod, obychno nazyvaemyj
rekursivnym spuskom. |to rasprostranennyj i dostatochno ochevidnyj
metod. V takih yazykah kak S++, to est' v kotoryh operaciya vyzova
ne sopryazhena s bol'shimi nakladnymi rashodami, eto metod effektiven.
Dlya kazhdogo pravila grammatiki imeetsya svoya funkciya, kotoraya vyzyvaet
drugie funkcii. Terminal'nye simvoly (naprimer, END, NUMBER, + i -)
raspoznayutsya leksicheskim analizatorom get_token(). Neterminal'nye
simvoly raspoznayutsya funkciyami sintaksicheskogo analizatora expr(),
term() i prim(). Kak tol'ko oba operanda vyrazheniya ili podvyrazheniya
stali izvestny, ono vychislyaetsya. V nastoyashchem translyatore v etot
moment sozdayutsya komandy, vychislyayushchie vyrazhenie.
Analizator ispol'zuet dlya vvoda funkciyu get_token().
Znachenie poslednego vyzova get_token() hranitsya v global'noj peremennoj
curr_tok. Peremennaya curr_tok prinimaet znacheniya elementov perechisleniya
token_value:
enum token_value {
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*', DIV='/',
PRINT=';', ASSIGN='=', LP='(', RP=')'
};
token_value curr_tok;
Dlya vseh funkcij analizatora predpolagaetsya, chto get_token() uzhe
byla vyzvana, i poetomu v curr_tok hranitsya sleduyushchaya leksema,
podlezhashchaya analizu. |to pozvolyaet analizatoru zaglyadyvat' na odnu
leksemu vpered. Kazhdaya funkciya analizatora vsegda chitaet
na odnu leksemu bol'she, chem nuzhno dlya raspoznavaniya togo pravila,
dlya kotorogo ona vyzyvalas'. Kazhdaya funkciya analizatora vychislyaet
"svoe" vyrazhenie i vozvrashchaet ego rezul'tat. Funkciya expr() obrabatyvaet
slozhenie i vychitanie. Ona sostoit iz odnogo cikla, v kotorom
raspoznannye termy skladyvayutsya ili vychitayutsya:
double expr() // skladyvaet i vychitaet
{
double left = term();
for(;;) // ``vechno''
switch(curr_tok) {
case PLUS:
get_token(); // sluchaj '+'
left += term();
break;
case MINUS:
get_token(); // sluchaj '-'
left -= term();
break;
default:
return left;
}
}
Sama po sebe eta funkciya delaet nemnogo. Kak prinyato v
vysokourovnevyh funkciyah bol'shih programm, ona vypolnyaet zadanie,
vyzyvaya drugie funkcii. Otmetim, chto vyrazheniya vida 2-3+4
vychislyayutsya kak (2-3)+4, chto predopredelyaetsya pravilami grammatiki.
Neprivychnaya zapis' for(;;) - eto standartnyj sposob zadaniya beskonechnogo
cikla, i ego mozhno oboznachit' slovom "vechno". |to vyrozhdennaya forma
operatora for, i al'ternativoj ej mozhet sluzhit' operator while(1).
Operator switch vypolnyaetsya povtorno do teh por, poka ne
perestanut poyavlyat'sya operacii + ili - , a togda po umolchaniyu vypolnyaetsya
operator return (default).
Operacii += i -= ispol'zuyutsya dlya vypolneniya operacij slozheniya i
vychitaniya. Mozhno napisat' ekvivalentnye prisvaivaniya: left=left+term() i
left=left-term(). Odnako variant left+=term() i left-=term() ne
tol'ko koroche, no i bolee chetko opredelyaet trebuemoe dejstvie. Dlya binarnoj
operacii @ vyrazhenie x@=y oznachaet x=x@y, za isklyucheniem togo, chto x
vychislyaetsya tol'ko odin raz. |to primenimo k binarnym operaciyam:
+ - * / % & | ^ << >>
poetomu vozmozhny sleduyushchie operacii prisvaivaniya:
+= -= *= /= %= &= |= ^= <<= >>=
Kazhdaya operaciya yavlyaetsya otdel'noj leksemoj, poetomu a + =1
soderzhit sintaksicheskuyu oshibku (iz-za probela mezhdu + i =). Rasshifrovka
operacij sleduyushchaya: % - vzyatie ostatka, &, | i ^ - razryadnye logicheskie
operacii I, ILI i Isklyuchayushchee ILI; << i >> sdvig vlevo i sdvig vpravo.
Funkcii term() i get_token() dolzhny byt' opisany do opredeleniya expr().
V glave 4 rassmatrivaetsya postroenie programmy v vide sovokupnosti
fajlov. Za odnim isklyucheniem, vse programmy kal'kulyatora mozhno sostavit'
tak, chtoby v nih vse ob容kty opisyvalis' tol'ko odin raz i do ih
ispol'zovaniya. Isklyucheniem yavlyaetsya funkciya expr(), kotoraya vyzyvaet
funkciyu term(), a ona, v svoyu ochered', vyzyvaet prim(), i uzhe ta, nakonec,
vyzyvaet expr(). |tot cikl neobhodimo kak-to razorvat', dlya chego vpolne
podhodit zadannoe do opredeleniya prim() opisanie:
double expr(); // eto opisanie neobhodimo
Funkciya term() spravlyaetsya s umnozheniem i deleniem analogichno
tomu, kak funkciya expr() so slozheniem i vychitaniem:
double term() // umnozhaet i skladyvaet
{
double left = prim();
for(;;)
switch(curr_tok) {
case MUL:
get_token(); // sluchaj '*'
left *= prim();
break;
case DIV:
get_token(); // sluchaj '/'
double d = prim();
if (d == 0) return error("delenie na 0");
left /= d;
break;
default:
return left;
}
}
Proverka otsutstviya deleniya na nul' neobhodima, poskol'ku
rezul'tat deleniya na nul' neopredelen i, kak pravilo, privodit k
katastrofe.
Funkciya error() budet rassmotrena pozzhe. Peremennaya d poyavlyaetsya v
programme tam, gde ona dejstvitel'no nuzhna, i srazu zhe inicializiruetsya.
Vo mnogih yazykah opisanie mozhet nahodit'sya tol'ko v nachale bloka.
No takoe ogranichenie mozhet iskazhat' estestvennuyu strukturu programmy i
sposobstvovat' poyavleniyu oshibok.
CHashche vsego ne inicializirovannye lokal'nye peremennye
svidetel'stvuyut o plohom stile programmirovaniya. Isklyuchenie sostavlyayut
te peremennye, kotorye inicializiruyutsya operatorami vvoda, i peremennye
tipa massiva ili struktury, dlya kotoryh net tradicionnoj
inicializacii s pomoshch'yu odinochnyh prisvaivanij. Sleduet napomnit', chto =
yavlyaetsya operaciej prisvaivaniya, togda kak == est' operaciya sravneniya.
Funkciya prim, obrabatyvayushchaya pervichnoe, vo mnogom pohozha na
funkcii expr i term(). No raz my doshli do niza v ierarhii vyzovov,
to v nej koe-chto pridetsya sdelat'. Cikl dlya nee ne nuzhen:
double number_value;
char name_string[256];
double prim() // obrabatyvaet pervichnoe
{
switch (curr_tok) {
case NUMBER: // konstanta s plavayushchej tochkoj
get_token();
return number_value;
case NAME:
if (get_token() == ASSIGN) {
name* n = insert(name_string);
get_token();
n->value = expr();
return n->value;
}
return look(name_string)->value;
case MINUS: // unarnyj minus
get_token();
return -prim();
case LP:
get_token();
double e = expr();
if (curr_tok != RP) return error("trebuetsya )");
get_token();
return e;
case END:
return 1;
default:
return error("trebuetsya pervichnoe");
}
}
Kogda poyavlyaetsya NUMBER (to est' konstanta s plavayushchej tochkoj),
vozvrashchaetsya ee znachenie. Funkciya vvoda get_token() pomeshchaet znachenie
konstanty v global'nuyu peremennuyu number_value. Esli v programme
ispol'zuyutsya global'nye peremennye, to chasto eto ukazyvaet na to, chto
struktura ne do konca prorabotana, i poetomu trebuetsya nekotoraya
optimizaciya. Imenno tak obstoit delo v dannom sluchae. V ideale leksema
dolzhna sostoyat' iz dvuh chastej: znacheniya, opredelyayushchego vid leksemy
(v dannoj programme eto token_value), i (esli neobhodimo) sobstvenno
znacheniya leksemy. Zdes' zhe imeetsya tol'ko odna prostaya peremennaya
curr_tok, poetomu dlya hraneniya poslednego prochitannogo znacheniya NUMBER
trebuetsya global'naya peremennaya number_value. Takoe reshenie prohodit
potomu, chto kal'kulyator vo vseh vychisleniyah vnachale vybiraet odno chislo,
a zatem schityvaet drugoe iz vhodnogo potoka. V kachestve uprazhneniya
predlagaetsya izbavit'sya ot etoj izlishnej global'noj peremennoj
($$3.5 [15]).
Esli poslednee znachenie NUMBER hranitsya v global'noj peremennoj
number_value, to strokovoe predstavlenie poslednego znacheniya NAME
hranitsya v name_string. Pered tem, kak chto-libo delat' s imenem,
kal'kulyator dolzhen zaglyanut' vpered, chtoby vyyasnit', budet li emu
prisvaivat'sya znachenie, ili zhe budet tol'ko ispol'zovat'sya sushchestvuyushchee
ego znachenie. V oboih sluchayah nado obratit'sya k tablice imen. |ta tablica
rassmatrivaetsya v $$3.1.3; a zdes' dostatochno tol'ko znat', chto ona
sostoit iz zapisej, imeyushchih vid:
struct name {
char* string;
name* next;
double value;
};
CHlen next ispol'zuetsya tol'ko sluzhebnymi funkciyami, rabotayushchimi
s tablicej:
name* look(const char*);
name* insert(const char*);
Obe funkcii vozvrashchayut ukazatel' na tu zapis' name, kotoraya sootvetstvuet
ih parametru-stroke. Funkciya look() "rugaetsya", esli imya ne bylo
zaneseno v tablicu. |to oznachaet, chto v kal'kulyatore mozhno ispol'zovat'
imya bez predvaritel'nogo opisaniya, no v pervyj raz ono mozhet
poyavit'sya tol'ko v levoj chasti prisvaivaniya.
Poluchenie vhodnyh dannyh - chasto samaya zaputannaya chast' programmy.
Prichina kroetsya v tom, chto programma dolzhna vzaimodejstvovat'
s pol'zovatelem, to est' "mirit'sya" s ego prihotyami, uchityvat' prinyatye
soglasheniya i predusmatrivat' kazhushchiesya redkimi oshibki.
Popytki zastavit' cheloveka vesti sebya bolee udobnym dlya mashiny obrazom,
kak pravilo, rassmatrivayutsya kak nepriemlemye, chto spravedlivo.
Zadacha vvoda dlya funkcii nizkogo urovnya sostoit v posledovatel'nom
schityvanii simvolov i sostavlenii iz nih leksemy, s kotoroj rabotayut
uzhe funkcii bolee vysokogo urovnya. V etom primere nizkourovnevyj vvod
delaet funkciya get_token(). K schast'yu, napisanie nizkourovnevoj
funkcii vvoda dostatochno redkaya zadacha. V horoshih sistemah est'
standartnye funkcii dlya takih operacij.
Pravila vvoda dlya kal'kulyatora byli special'no vybrany neskol'ko
gromozdkimi dlya potokovyh funkcij vvoda. Neznachitel'nye izmeneniya
v opredeleniyah leksem prevratili by get_token() v obmanchivo prostuyu
funkciyu.
Pervaya slozhnost' sostoit v tom, chto simvol konca stroki '\n'
vazhen dlya kal'kulyatora, no potokovye funkcii vvoda vosprinimayut ego
kak simvol obobshchennogo probela. Inache govorya, dlya etih funkcij '\n'
imeet znachenie tol'ko kak simvol, zavershayushchij leksemu.
Poetomu prihoditsya analizirovat' vse obobshchennye probely (probel,
tabulyaciya i t.p.). |to delaetsya v operatore do, kotoryj ekvivalenten
operatoru while, za isklyucheniem togo, chto telo operatora do
vsegda vypolnyaetsya hotya by odin raz:
char ch;
do { // propuskaet probely za isklyucheniem '\n'
if(!cin.get(ch)) return curr_tok = END;
} while (ch!='\n' && isspace(ch));
Funkciya cin.get(ch) chitaet odin simvol iz standartnogo vhodnogo potoka
v ch. Znachenie usloviya if(!cin.get(ch)) - lozh', esli iz potoka cin
nel'zya poluchit' ni odnogo simvola. Togda vozvrashchaetsya leksema END, chtoby
zakonchit' rabotu kal'kulyatora. Operaciya ! (NOT) nuzhna potomu, chto
v sluchae uspeshnogo schityvaniya get() vozvrashchaet nenulevoe znachenie.
Funkciya-podstanovka isspace() iz <ctype.h> proveryaet, ne yavlyaetsya
li ee parametr obobshchennym probelom ($$10.3.1). Ona vozvrashchaet nenulevoe
znachenie, esli yavlyaetsya, i nul' v protivnom sluchae. Proverka realizuetsya
kak obrashchenie k tablice, poetomu dlya skorosti luchshe vyzyvat' isspace(),
chem proveryat' samomu. To zhe mozhno skazat' o funkciyah isalpha(), isdigit()
i isalnum(), kotorye ispol'zuyutsya v get_token().
Posle propuska obobshchennyh probelov sleduyushchij schitannyj simvol
opredelyaet, kakoj budet nachinayushchayasya s nego leksema. Prezhde, chem
privesti vsyu funkciyu, rassmotrim nekotorye sluchai otdel'no. Leksemy
'\n' i ';', zavershayushchie vyrazhenie, obrabatyvayutsya sleduyushchim obrazom:
switch (ch) {
case ';':
case '\n':
cin >> ws; // propusk obobshchennogo probela
return curr_tok=PRINT;
Neobyazatel'no snova propuskat' probel, no, sdelav eto, my
izbezhim povtornyh vyzovov funkcii get_token(). Peremennaya ws, opisannaya
v fajle <stream.h>, ispol'zuetsya tol'ko kak priemnik nenuzhnyh probelov.
Oshibka vo vhodnyh dannyh, a takzhe konec vvoda ne budut obnaruzheny do
sleduyushchego vyzova funkcii get_token(). Obratite vnimanie, kak neskol'ko
metok vybora pomechayut odnu posledovatel'nost' operatorov, zadannuyu
dlya etih variantov. Dlya oboih simvolov ('\n' i ';') vozvrashchaetsya leksema
PRINT, i ona zhe pomeshchaetsya v curr_tok.
CHisla obrabatyvayutsya sleduyushchim obrazom:
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '.':
cin.putback(ch);
cin >> number_value;
return curr_tok=NUMBER;
Razmeshchat' metki variantov gorizontal'no, a ne vertikal'no,- ne samyj
luchshij sposob, poskol'ku takoj tekst trudnee chitat'; no pisat' stroku
dlya kazhdoj cifry utomitel'no. Poskol'ku operator >> mozhet chitat'
konstantu s plavayushchej tochkoj tipa double, programma trivial'na:
prezhde vsego nachal'nyj simvol (cifra ili tochka) vozvrashchaetsya nazad
v cin, a zatem konstantu mozhno schitat' v number_value.
Imya, t.e. leksema NAME, opredelyaetsya kak bukva, za kotoroj mozhet
idti neskol'ko bukv ili cifr:
if (isalpha(ch)) {
char* p = name_string;
*p++ = ch;
while (cin.get(ch) && isalnum(ch)) *p++ = ch;
cin.putback(ch);
*p = 0;
return curr_tok=NAME;
}
|tot fragment programmy zanosit v name_string stroku, okanchivayushchuyusya
nulevym simvolom. Funkcii isalpha() i isalnum() opredeleny v <ctype.h>.
Rezul'tat isalnum(c) nenulevoj, esli c - bukva ili cifra, i nulevoj
v protivnom sluchae.
Privedem, nakonec, funkciyu vvoda polnost'yu:
token_value get_token()
{
char ch;
do { // propuskaet obobshchennye probely za isklyucheniem '\n'
if(!cin.get(ch)) return curr_tok = END;
} while (ch!='\n' && isspace(ch));
switch (ch) {
case ';':
case '\n':
cin >> ws; // propusk obobshchennogo probela
return curr_tok=PRINT;
case '*':
case '/':
case '+':
case '-':
case '(':
case ')':
case '=':
return curr_tok=token_value(ch);
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '.':
cin.putback(ch);
cin >> number_value;
return curr_tok=NUMBER;
default: // NAME, NAME= ili oshibka
if (isalpha(ch)) {
char* p = name_string;
*p++ = ch;
while (cin.get(ch) && isalnum(ch)) *p++ = ch;
cin.putback(ch);
*p = 0;
return curr_tok=NAME;
}
error("nedopustimaya leksema");
return curr_tok=PRINT;
}
}
Preobrazovanie operacii v znachenie leksemy dlya nee trivial'no,
poskol'ku v perechislenii token_value leksema operacii byla opredelena
kak celoe (kod simvola operacii).
Est' funkciya poiska v tablice imen:
name* look(char* p, int ins =0);
Vtoroj ee parametr pokazyvaet, byla li simvol'naya stroka, oboznachayushchaya
imya, ranee zanesena v tablicu. Inicializator =0 zadaet standartnoe
znachenie parametra, kotoroe ispol'zuetsya, esli funkciya look()
vyzyvaetsya tol'ko s odnim parametrom. |to udobno, tak kak
mozhno pisat' look("sqrt2"), chto oznachaet look("sqrt2",0),
t.e. poisk, a ne zanesenie v tablicu. CHtoby bylo tak zhe udobno zadavat'
operaciyu zaneseniya v tablicu, opredelyaetsya vtoraya funkciya:
inline name* insert(const char* s) { return look(s,1); }
Kak ranee upominalos', zapisi v etoj tablice imeyut takoj tip:
struct name {
char* string;
name* next;
double value;
};
CHlen next ispol'zuetsya dlya svyazi zapisej v tablice.
Sobstvenno tablica - eto prosto massiv ukazatelej na ob容kty tipa name:
const TBLSZ = 23;
name* table[TBLSZ];
Poskol'ku po umolchaniyu vse staticheskie ob容kty inicializiruyutsya nulem,
takoe trivial'noe opisanie tablicy table obespechivaet takzhe i nuzhnuyu
inicializaciyu.
Dlya poiska imeni v tablice funkciya look() ispol'zuet prostoj
hesh-kod (zapisi, v kotoryh imena imeyut odinakovyj hesh-kod,
svyazyvayutsya):
vmeste):
int ii = 0; // hesh-kod
const char* pp = p;
while (*pp) ii = ii<<1 ^ *pp++;
if (ii < 0) ii = -ii;
ii %= TBLSZ;
Inymi slovami, s pomoshch'yu operacii ^ ("isklyuchayushchee ILI") vse simvoly
vhodnoj stroki p poocheredno dobavlyayutsya k ii. Razryad v rezul'tate x^y
raven 1 togda i tol'ko togda, kogda eti razryady v operandah x i y razlichny.
Do vypolneniya operacii ^ znachenie ii sdvigaetsya na odin razryad vlevo,
chtoby ispol'zovalsya ne tol'ko odin bajt ii. |ti dejstviya mozhno
zapisat' takim obrazom:
ii <<= 1;
ii ^= *pp++;
Dlya horoshego hesh-koda luchshe ispol'zovat' operaciyu ^, chem +. Operaciya
sdviga vazhna dlya polucheniya priemlemogo hesh-koda v oboih sluchayah.
Operatory
if (ii < 0) ii = -ii;
ii %= TBLSZ;
garantiruyut, chto znachenie ii budet iz diapazona 0...TBLSZ-1. Napomnim,
chto % - eto operaciya vzyatiya ostatka. Nizhe polnost'yu privedena
funkciya look:
#include <string.h>
name* look(const char* p, int ins =0)
{
int ii = 0; // hesh-kod
const char* pp = p;
while (*pp) ii = ii<<1 ^ *pp++;
if (ii < 0) ii = -ii;
ii %= TBLSZ;
for (name* n=table[ii]; n; n=n->next) // poisk
if (strcmp(p,n->string) == 0) return n;
if (ins == 0) error("imya ne najdeno");
name* nn = new name; // zanesenie
nn->string = new char[strlen(p)+1];
strcpy(nn->string,p);
nn->value = 1;
nn->next = table[ii];
table[ii] = nn;
return nn;
}
Posle vychisleniya hesh-koda ii idet prostoj poisk imeni po chlenam
next. Imena sravnivayutsya s pomoshch'yu standartnoj funkcii
sravneniya strok strcmp(). Esli imya najdeno, to vozvrashchaetsya ukazatel'
na soderzhashchuyu ego zapis', a v protivnom sluchae zavoditsya novaya zapis'
s etim imenem.
Dobavlenie novogo imeni oznachaet sozdanie novogo ob容kta name
v svobodnoj pamyati s pomoshch'yu operacii new (sm. $$3.2.6), ego
inicializaciyu i vklyuchenie v spisok imen. Poslednee vypolnyaetsya kak
zanesenie novogo imeni v nachalo spiska, poskol'ku eto mozhno sdelat' dazhe
bez proverki togo, est' li spisok voobshche. Simvol'naya stroka imeni
takzhe razmeshchaetsya v svobodnoj pamyati. Funkciya strlen() ukazyvaet,
skol'ko pamyati nuzhno dlya stroki, operaciya new otvodit nuzhnuyu pamyat',
a funkciya strcpy() kopiruet v nee stroku. Vse strokovye funkcii
opisany v <string.h>:
extern int strlen(const char*);
extern int strcmp(const char*, const char*);
extern char* strcpy(char*, const char*);
Poskol'ku programma dostatochno prosta, ne nado osobo bespokoit'sya
ob obrabotke oshibok. Funkciya error prosto podschityvaet chislo oshibok,
vydaet soobshchenie o nih i vozvrashchaet upravlenie obratno:
int no_of_errors;
double error(const char* s)
{
cerr << "error: " << s << "\n";
no_of_errors++;
return 1;
}
Nebuferizovannyj vyhodnoj potok cerr obychno ispol'zuetsya imenno dlya
vydachi soobshchenij ob oshibkah.
Upravlenie vozvrashchaetsya iz error() potomu, chto oshibki, kak pravilo,
vstrechayutsya posredi vychisleniya vyrazheniya. Znachit nado libo polnost'yu
prekrashchat' vychisleniya, libo vozvrashchat' znachenie, kotoroe ne dolzhno
vyzvat' posleduyushchih oshibok. Dlya prostogo kal'kulyatora bol'she podhodit
poslednee. Esli by funkciya get_token() otslezhivala nomera strok, to
funkciya error() mogla by ukazyvat' pol'zovatelyu priblizitel'noe mesto
oshibki. |to bylo by polezno pri neinteraktivnoj rabote s kal'kulyatorom.
CHasto posle poyavleniya oshibki programma dolzhna zavershit'sya, poskol'ku
ne udalos' predlozhit' razumnyj variant ee dal'nejshego vypolneniya.
Zavershit' ee mozhno s pomoshch'yu vyzova funkcii exit(), kotoraya zakanchivaet
rabotu s vyhodnymi potokami ($$10.5.1) i zavershaet programmu,
vozvrashchaya svoj parametr v kachestve ee rezul'tata.
Bolee radikal'nyj sposob zaversheniya programmy - eto vyzov funkcii abort(),
kotoraya preryvaet vypolnenie programmy nemedlenno ili srazu zhe posle
sohraneniya informacii dlya otladchika (sbros operativnoj pamyati).
Podrobnosti vy mozhete najti v svoem spravochnom rukovodstve.
Bolee tonkie priemy obrabotki oshibok mozhno predlozhit', esli
orientirovat'sya na osobye situacii (sm.$$9), no predlozhennoe reshenie
vpolne priemlemo dlya igrushechnogo kal'kulyatora v 200 strok.
Kogda vse chasti programmy opredeleny, nuzhen tol'ko drajver, chtoby
inicializirovat' i zapustit' process. V nashem primere s etim
spravitsya funkciya main():
int main()
{
// vstavit' predopredelennye imena:
insert("pi")->value = 3.1415926535897932385;
insert("e")->value = 2.7182818284590452354;
while (cin) {
get_token();
if (curr_tok == END) break;
if (curr_tok == PRINT) continue;
cout << expr() << '\n';
}
return no_of_errors;
}
Prinyato, chto funkciya main() vozvrashchaet nul', esli programma zavershaetsya
normal'no, i nenulevoe znachenie, esli proishodit inache. Nenulevoe
znachenie vozvrashchaetsya kak chislo oshibok. Okazyvaetsya, vsya inicializaciya
svoditsya k zaneseniyu predopredelennyh imen v tablicu.
V cikle main chitayutsya vyrazheniya i vydayutsya rezul'taty. |to delaet
odna stroka:
cout << expr() << '\n';
Proverka cin pri kazhdom prohode cikla garantiruet zavershenie programmy,
dazhe esli chto-to sluchitsya s vhodnym potokom, a proverka na leksemu
END nuzhna dlya normal'nogo zaversheniya cikla, kogda funkciya get_token()
obnaruzhit konec fajla. Operator break sluzhit dlya vyhoda iz
blizhajshego ob容mlyushchego operatora switch ili cikla (t.e. operatora for,
while ili do). Proverka na leksemu PRINT (t.e. na '\n' i ';') snimaet
s funkcii expr() obyazannost' obrabatyvat' pustye vyrazheniya. Operator
continue ekvivalenten perehodu na konec cikla, poetomu v nashem
sluchae fragment:
while (cin) {
// ...
if (curr_tok == PRINT) continue;
cout << expr() << "\n";
}
ekvivalenten fragmentu:
while (cin) {
// ...
if (curr_tok == PRINT) goto end_of_loop;
cout << expr() << "\n";
end_of_loop: ;
}
Bolee podrobno cikly opisyvayutsya v $$R.6
3.1.6 Parametry komandnoj stroki
Kogda programma kal'kulyatora uzhe byla napisana i otlazhena, vyyasnilos',
chto neudobno vnachale zapuskat' ee, vvodit' vyrazhenie, a zatem vyhodit'
iz kal'kulyatora. Tem bolee, chto obychno nuzhno prosto vychislit' odno
vyrazhenie. Esli eto vyrazhenie zadat' kak parametr komandnoj stroki
zapuska kal'kulyatora, to mozhno sekonomit' neskol'ko nazhatij klavishi.
Kak uzhe bylo skazano, vypolnenie programmy nachinaetsya vyzovom main().
Pri etom vyzove main() poluchaet dva parametra: chislo parametrov (obychno
nazyvaemyj argc) i massiv strok parametrov (obychno nazyvaemyj argv).
Parametry - eto simvol'nye stroki, poetomu argv imeet tip char*[argc+1].
Imya programmy (v tom vide, kak ono bylo zadano v komandnoj stroke)
peredaetsya v argv[0], poetomu argc vsegda ne men'she edinicy. Naprimer,
dlya komandnoj stroki
dc 150/1.1934
parametry imeyut znacheniya:
argc 2
argv[0] "dc"
argv[1] "150/1.1934"
argv[2] 0
Dobrat'sya do parametrov komandnoj stroki prosto; problema v tom, kak
ispol'zovat' ih tak, chtoby ne menyat' samu programmu. V dannom sluchae eto
okazyvaetsya sovsem prosto, poskol'ku vhodnoj potok mozhet byt' nastroen
na simvol'nuyu stroku vmesto fajla ($$10.5.2). Naprimer, mozhno opredelit'
cin tak, chtoby simvoly chitalis' iz stroki, a ne iz standartnogo
vhodnogo potoka:
int main(int argc, char* argv[])
{
switch(argc) {
case 1: // schityvat' iz standartnogo vhodnogo potoka
break;
case 2: // schityvat' iz stroki parametrov
cin = *new istream(argv[1],strlen(argv[1]));
break;
default:
error("slishkom mnogo parametrov");
return 1;
}
// dal'she prezhnij variant main
}
Pri etom istrstream - eto funkciya istream, kotoraya schityvaet
simvoly iz stroki, yavlyayushchejsya ee pervym parametrom. CHtoby ispol'zovat'
istrstream nuzhno vklyuchit' v programmu fajl <strstream.h>, a ne
obychnyj <iostream.h>. V ostal'nom zhe programma ostalas' bez izmenenij,
krome dobavleniya parametrov v funkciyu main() i ispol'zovaniya ih
v operatore switch. Mozhno legko izmenit' funkciyu main() tak, chtoby ona
mogla prinimat' neskol'ko parametrov iz komandnoj stroki. Odnako
eto ne slishkom nuzhno, tem bolee, chto mozhno neskol'kih vyrazhenij
peredat' kak odin parametr:
dc "rate=1.1934;150/rate;19.75/rate;217/rate"
Kavychki neobhodimy potomu, chto simvol ';' sluzhit v sisteme UNIX
razdelitelem komand. V drugih sistemah mogut byt' svoi soglasheniya o
parametrah komandnoj stroki.
Polnoe i podrobnoe opisanie operacij yazyka S++ dano v $$R.7. Sovetuem
prochitat' etot razdel. Zdes' zhe privoditsya kratkaya svodka operacij i
neskol'ko primerov. Kazhdaya operaciya soprovozhdaetsya odnim ili
neskol'kimi harakternymi dlya nee imenami i primerom ee ispol'zovaniya.
V etih primerah class_name oboznachaet imya klassa, member - imya chlena,
object - vyrazhenie, zadayushchee ob容kt klassa, pointer - vyrazhenie, zadayushchee
ukazatel', expr - prosto vyrazhenie, a lvalue (adres) - vyrazhenie,
oboznachayushchee ne yavlyayushchijsya konstantoj ob容kt. Oboznachenie (type) zadaet
imya tipa v obshchem vide (s vozmozhnym dobavleniem *, () i t.d.).
Esli ono ukazano bez skobok, sushchestvuyut ogranicheniya.
Poryadok primeneniya unarnyh operacij i operacij prisvaivaniya
"sprava nalevo", a vseh ostal'nyh operacij - "sleva napravo".
To est', a=b=c oznachaet a=(b=c), a+b+c oznachaet (a+b)+c, i *p++ oznachaet
*(p++), a ne (*p)++.
____________________________________________________________
Operacii S++
============================================================
:: Razreshenie oblasti vidimosti class_name :: member
:: Global'noe :: name
____________________________________________________________
. Vybor chlena object . member
-> Vybor chlena pointer -> member
[] Indeksirovanie pointer [ expr ]
() Vyzov funkcii expr ( expr_list )
() Strukturnoe znachenie type ( expr_list )
sizeof Razmer ob容kta sizeof expr
sizeof Razmer tipa sizeof ( type )
____________________________________________________________
++ Postfiksnyj inkrement lvalue ++
++ Prefiksnyj inkrement ++ lvalue
-- Postfiksnyj dekrement lvalue --
-- Prefiksnyj dekrement -- lvalue
~ Dopolnenie ~ expr
! Logicheskoe NE ! expr
- Unarnyj minus - expr
+ Unarnyj plyus + expr
& Vzyatie adresa & lvalue
* Kosvennost' * expr
new Sozdanie (razmeshchenie) new type
delete Unichtozhenie (osvobozhdenie) delete pointer
delete[] Unichtozhenie massiva delete[] pointer
() Privedenie(preobrazovanie)tipa ( type ) expr
____________________________________________________________
. * Vybor chlena kosvennyj object . pointer-to-member
->* Vybor chlena kosvennyj pointer -> pointer-to-member
____________________________________________________________
* Umnozhenie expr * expr
/ Delenie expr / expr
% Ostatok ot deleniya expr % expr
____________________________________________________________
+ Slozhenie (plyus) expr + expr
- Vychitanie (minus) expr - expr
____________________________________________________________
Vse operacii tablicy, nahodyashchiesya mezhdu dvumya blizhajshimi drug
k drugu gorizontal'nymi chertami,
imeyut odinakovyj prioritet. Prioritet operacij umen'shaetsya pri
dvizhenii "sverhu vniz". Naprimer, a+b*c oznachaet a+(b*c), tak kak *
imeet prioritet vyshe, chem +; a vyrazhenie a+b-c oznachaet (a+b)-c,
poskol'ku + i - imeyut odinakovyj prioritet, i operacii + i -
primenyayutsya "sleva napravo".
|
____________________________________________________________
Operacii S++ (prodolzhenie)
============================================================
<< Sdvig vlevo expr << expr
>> Sdvig vpravo expr >> expr
____________________________________________________________
< Men'she expr < expr
<= Men'she ili ravno expr <= expr
> Bol'she expr > expr
>= Bol'she ili ravno expr >= expr
____________________________________________________________
== Ravno expr == expr
!= Ne ravno expr != expr
____________________________________________________________
& Porazryadnoe I expr & expr
____________________________________________________________
^ Porazryadnoe isklyuchayushchee ILI expr ^ expr
____________________________________________________________
| Porazryadnoe vklyuchayushchee ILI expr | expr
____________________________________________________________
&& Logicheskoe I expr && expr
____________________________________________________________
|| Logicheskoe ILI expr || expr
____________________________________________________________
? : Operaciya usloviya expr? expr : expr
____________________________________________________________
= Prostoe prisvaivanie lvalue = expr
*= Prisvaivanie s umnozheniem lvalue *= expr
/= Prisvaivanie s deleniem lvalue /= expr
%= Prisvaivanie s vzyatiem lvalue %= expr
ostatka ot deleniya
+= Prisvaivanie so slozheniem lvalue += expr
-= Prisvaivanie s vychitaniem lvalue -= expr
<<= Prisvaivanie so sdvigom vlevo lvalue <<= expr
>>= Prisvaivanie so sdvigom vpravo lvalue >>= expr
&= Prisvaivanie s porazryadnym I lvalue &= expr
|= Prisvaivanie s porazryadnym lvalue |= expr
vklyuchayushchim ILI
^= Prisvaivanie s porazryadnym lvalue ^= expr
isklyuchayushchim ILI
____________________________________________________________
Zapyataya (posledovatel'nost') expr , expr
____________________________________________________________
Sintaksis yazyka S++ peregruzhen skobkami, i raznoobrazie ih primenenij
sposobno sbit' s tolku. Oni vydelyayut fakticheskie parametry pri
vyzove funkcij, imena tipov, zadayushchih funkcii, a takzhe sluzhat dlya
razresheniya konfliktov mezhdu operaciyami s odinakovym prioritetom.
K schast'yu, poslednee vstrechaetsya ne slishkom chasto, poskol'ku prioritety
i poryadok primeneniya operacij opredeleny tak, chtoby vyrazheniya vychislyalis'
"estestvennym obrazom" (t.e. naibolee rasprostranennym obrazom).
Naprimer, vyrazhenie
if (i<=0 || max<i) // ...
oznachaet sleduyushchee: "Esli i men'she ili ravno nulyu, ili esli max men'she i".
To est', ono ekvivalentno
if ( (i<=0) || (max<i) ) // ...
no ne ekvivalentno dopustimomu, hotya i bessmyslennomu vyrazheniyu
if (i <= (0||max) < i) // ...
Tem ne menee, esli programmist ne uveren v ukazannyh pravilah,
sleduet ispol'zovat' skobki, prichem nekotorye predpochitayut dlya
nadezhnosti pisat' bolee dlinnye i menee elegantnye vyrazheniya, kak:
if ( (i<=0) || (max<i) ) // ...
Pri uslozhnenii podvyrazhenij skobki ispol'zuyutsya chashche. Ne nado, odnako,
zabyvat', chto slozhnye vyrazheniya yavlyayutsya istochnikom oshibok. Poetomu,
esli u vas poyavitsya oshchushchenie, chto v etom vyrazhenii nuzhny skobki,
luchshe razbejte ego na chasti i vvedite dopolnitel'nuyu peremennuyu.
Byvayut sluchai, kogda prioritety operacij ne privodyat k "estestvennomu"
poryadku vychislenij. Naprimer, v vyrazhenii
if (i&mask == 0) // lovushka! & primenyaetsya posle ==
ne proishodit maskirovanie i (i&mask), a zatem proverka rezul'tata
na 0. Poskol'ku u == prioritet vyshe, chem u &, eto vyrazhenie ekvivalentno
i&(mask==0). V etom sluchae skobki igrayut vazhnuyu rol':
if ((i&mask) == 0) // ...
Imeet smysl privesti eshche odno vyrazhenie, kotoroe vychislyaetsya
sovsem ne tak, kak mog by ozhidat' neiskushennyj pol'zovatel':
if (0 <= a <= 99) // ...
Ono dopustimo, no interpretiruetsya kak (0<=a)<=99, i rezul'tat pervogo
sravneniya raven ili 0, ili 1, no ne znacheniyu a (esli, konechno,
a ne est' 1). Proverit', popadaet li a v diapazon 0...99, mozhno tak:
if (0<=a && a<=99) // ...
Sredi novichkov rasprostranena oshibka, kogda v uslovii vmesto ==
(ravno) ispol'zuyut = (prisvoit'):
if (a = 7) // oshibka: prisvaivanie konstanty v uslovii
// ...
Ona vpolne ob座asnima, poskol'ku v bol'shinstve yazykov "=" oznachaet "ravno".
Dlya translyatora ne sostavit truda soobshchat' ob oshibkah podobnogo roda.
3.2.2 Poryadok vychislenij
Poryadok vychisleniya podvyrazhenij, vhodyashchih v vyrazhenie, ne vsegda
opredelen. Naprimer:
int i = 1;
v[i] = i++;
Zdes' vyrazhenie mozhet vychislyat'sya ili kak v[1]=1, ili kak v[2]=1.
Esli net ogranichenij na poryadok vychisleniya podvyrazhenij, to translyator
poluchaet vozmozhnost' sozdavat' bolee optimal'nyj kod. Translyatoru
sledovalo by preduprezhdat' o dvusmyslennyh vyrazheniyah, no k sozhaleniyu
bol'shinstvo iz nih ne delaet etogo.
Dlya operacij
&& || ,
garantiruetsya, chto ih levyj operand vychislyaetsya ran'she pravogo operanda.
Naprimer, v vyrazhenii b=(a=2,a+1) b prisvoitsya znachenie 3. Primer
operacii || byl dan v $$3.2.1, a primer operacii && est' v $$3.3.1.
Otmetim, chto operaciya zapyataya otlichaetsya po smyslu ot toj zapyatoj, kotoraya
ispol'zuetsya dlya razdeleniya parametrov pri vyzove funkcij. Pust' est'
vyrazheniya:
f1(v[i],i++); // dva parametra
f2( (v[i],i++) ) // odin parametr
Vyzov funkcii f1 proishodit s dvumya parametrami: v[i] i i++, no
poryadok vychisleniya vyrazhenij parametrov neopredelen. Zavisimost'
vychisleniya znachenij fakticheskih parametrov ot poryadka vychislenij
- daleko ne luchshij stil' programmirovaniya. K tomu zhe programma
stanovitsya neperenosimoj.
Vyzov f2 proishodit s odnim parametrom, yavlyayushchimsya vyrazheniem,
soderzhashchim operaciyu zapyataya: (v[i], i++). Ono ekvivalentno i++.
Skobki mogut prinuditel'no zadat' poryadok vychisleniya. Naprimer,
a*(b/c) mozhet vychislyat'sya kak (a*b)/c (esli tol'ko pol'zovatel'
vidit v etom kakoe-to razlichie). Zametim, chto dlya znachenij s plavayushchej
tochkoj rezul'taty vychisleniya vyrazhenij a*(b/c) i (a*b)/ mogut
razlichat'sya ves'ma znachitel'no.
3.2.3 Inkrement i dekrement
Operaciya ++ yavno zadaet inkrement v otlichie ot neyavnogo ego zadaniya
s pomoshch'yu slozheniya i prisvaivaniya. Po opredeleniyu ++lvalue oznachaet
lvalue+=1, chto, v svoyu ochered' oznachaet lvalue=lvalue+1 pri uslovii,
chto soderzhimoe lvalue ne vyzyvaet pobochnyh effektov. Vyrazhenie,
oboznachayushchee operand inkrementa, vychislyaetsya tol'ko odin raz. Analogichno
oboznachaetsya operaciya dekrementa (--). Operacii ++ i -- mogut
ispol'zovat'sya kak prefiksnye i postfiksnye operacii. Znacheniem ++x
yavlyaetsya novoe (t. e. uvelichennoe na 1) znachenie x. Naprimer, y=++x
ekvivalentno y=(x+=1). Naprotiv, znachenie x++ ravno prezhnemu znacheniyu x.
Naprimer, y=x++ ekvivalentno y=(t=x,x+=1,t), gde t - peremennaya togo
zhe tipa, chto i x.
Napomnim, chto operacii inkrementa i dekrementa ukazatelya
ekvivalentny slozheniyu 1 s ukazatelem ili vychitaniyu 1 iz ukazatelya, prichem
vychislenie proishodit v elementah massiva, na kotoryj nastroen
ukazatel'. Tak, rezul'tatom p++ budet ukazatel' na sleduyushchij element.
Dlya ukazatelya p tipa T* sleduyushchee sootnoshenie verno po opredeleniyu:
long(p+1) == long(p) + sizeof(T);
CHashche vsego operacii inkrementa i dekrementa ispol'zuyutsya dlya
izmeneniya peremennyh v cikle. Naprimer, kopirovanie stroki,
okanchivayushchejsya nulevym simvolom, zadaetsya sleduyushchim obrazom:
inline void cpy(char* p, const char* q)
{
while (*p++ = *q++) ;
}
YAzyk S++ (podobno S) imeet kak storonnikov, tak i protivnikov imenno
iz-za takogo szhatogo, ispol'zuyushchego slozhnye vyrazheniya stilya
programmirovaniya. Operator
while (*p++ = *q++) ;
veroyatnee vsego, pokazhetsya nevrazumitel'nym dlya neznakomyh s S.
Imeet smysl povnimatel'nee posmotret' na takie konstrukcii, poskol'ku
dlya C i C++ oni ne yavlyaetsya redkost'yu.
Snachala rassmotrim bolee tradicionnyj sposob kopirovaniya massiva
simvolov:
int length = strlen(q)
for (int i = 0; i<=length; i++) p[i] = q[i];
|to neeffektivnoe reshenie: stroka okanchivaetsya nulem; edinstvennyj
sposob najti ee dlinu - eto prochitat' ee vsyu do nulevogo simvola;
v rezul'tate stroka chitaetsya i dlya ustanovleniya ee dliny, i dlya
kopirovaniya, to est' dvazhdy. Poetomu poprobuem takoj variant:
for (int i = 0; q[i] !=0 ; i++) p[i] = q[i];
p[i] = 0; // zapis' nulevogo simvola
Poskol'ku p i q - ukazateli, mozhno obojtis' bez peremennoj i,
ispol'zuemoj dlya indeksacii:
while (*q !=0) {
*p = *q;
p++; // ukazatel' na sleduyushchij simvol
q++; // ukazatel' na sleduyushchij simvol
}
*p = 0; // zapis' nulevogo simvola
Poskol'ku operaciya postfiksnogo inkrementa pozvolyaet snachala ispol'zovat'
znachenie, a zatem uzhe uvelichit' ego, mozhno perepisat' cikl tak:
while (*q != 0) {
*p++ = *q++;
}
*p = 0; // zapis' nulevogo simvola
Otmetim, chto rezul'tat vyrazheniya *p++ = *q++ raven *q. Sledovatel'no,
mozhno perepisat' nash primer i tak:
while ((*p++ = *q++) != 0) { }
V etom variante uchityvaetsya, chto *q ravno nulyu tol'ko togda, kogda
*q uzhe skopirovano v *p, poetomu mozhno isklyuchit' zavershayushchee
prisvaivanie nulevogo simvola. Nakonec, mozhno eshche bolee sokratit'
zapis' etogo primera, esli uchest', chto pustoj blok ne nuzhen, a
operaciya "!= 0" izbytochna, t.k. rezul'tat uslovnogo vyrazheniya i tak
vsegda sravnivaetsya s nulem. V rezul'tate my prihodim k
pervonachal'nomu variantu, kotoryj vyzyval nedoumenie:
while (*p++ = *q++) ;
Neuzheli etot variant trudnee ponyat', chem privedennye vyshe? Tol'ko
neopytnym programmistam na S++ ili S! Budet li poslednij variant
naibolee effektivnym po zatratam vremeni i pamyati? Esli ne schitat'
pervogo varianta s funkciej strlen(), to eto neochevidno. Kakoj iz
variantov okazhetsya effektivnee, opredelyaetsya kak specifikoj sistemy
komand, tak i vozmozhnostyami translyatora. Naibolee effektivnyj algoritm
kopirovaniya dlya vashej mashiny mozhno najti v standartnoj funkcii kopirovaniya
strok iz fajla <string.h>:
int strcpy(char*, const char*);
3.2.4 Porazryadnye logicheskie operacii
Porazryadnye logicheskie operacii
& | ^ ~ >> <<
primenyayutsya k celym, to est' k ob容ktam tipa char, short, int, long i
k ih bezznakovym analogam. Rezul'tat operacii takzhe budet celym.
CHashche vsego porazryadnye logicheskie operacii ispol'zuyutsya dlya
raboty s nebol'shim po velichine mnozhestvom dannyh (massivom razryadov).
V etom sluchae kazhdyj razryad bezznakovogo celogo predstavlyaet odin
element mnozhestva, i chislo elementov opredelyaetsya kolichestvom razryadov.
Binarnaya operaciya & interpretiruetsya kak peresechenie mnozhestv,
operaciya | kak ob容dinenie, a operaciya ^ kak raznost' mnozhestv.
S pomoshch'yu perechisleniya mozhno zadat' imena elementam mnozhestva.
Nizhe priveden primer, zaimstvovannyj iz <iostream.h>:
class ios {
public:
enum io_state {
goodbit=0, eofbit=1, failbit=2, badbit=4
};
// ...
};
Sostoyanie potoka mozhno ustanovit' sleduyushchim prisvaivaniem:
cout.state = ios::goodbit;
Utochnenie imenem ios neobhodimo, potomu chto opredelenie io_state nahoditsya
v klasse ios, a takzhe chtoby ne vozniklo kollizij, esli pol'zovatel' zavedet svoi
imena napodobie goodbit.
Proverku na korrektnost' potoka i uspeshnoe okonchanie operacii mozhno
zadat' tak:
if (cout.state&(ios::badbit|ios::failbit)) // oshibka v potoke
Eshche odni skobki neobhodimy potomu, chto operaciya & imeet bolee vysokij
prioritet, chem operaciya "|".
Funkciya, obnaruzhivshaya konec vhodnogo potoka, mozhet soobshchat' ob etom tak:
cin.state |= ios::eofbit;
Operaciya |= ispol'zuetsya potomu, chto v potoke uzhe mogla byt' oshibka
(t.e. state==ios::badbit), i prisvaivanie
cin.state =ios::eofbit;
moglo by zateret' ee priznak. Ustanovit' otlichiya v sostoyanii dvuh
potokov mozhno sleduyushchim sposobom:
ios::io_state diff = cin.state^cout.state;
Dlya takih tipov, kak io_state, nahozhdenie razlichij ne slishkom poleznaya
operaciya, no dlya drugih shodnyh tipov ona mozhet okazat'sya ves'ma
poleznoj. Naprimer, polezno sravnenie dvuh razryadnyh massiva, odin iz
kotoryh predstavlyaet nabor vseh vozmozhnyh obrabatyvaemyh preryvanij,
a drugoj - nabor preryvanij, ozhidayushchih obrabotki.
Otmetim, chto ispol'zovanie polej ($$R.9.6) mozhet sluzhit' udobnym
i bolee lakonichnym sposobom raboty s chastyami slova, chem sdvigi i
maskirovanie. S chastyami slova mozhno rabotat' i s pomoshch'yu porazryadnyh
logicheskih operacij. Naprimer, mozhno vydelit' srednie 16 razryadov
iz srediny 32-razryadnogo celogo:
unsigned short middle(int a) { return (a>>8)&0xffff; }
Tol'ko ne putajte porazryadnye logicheskie operacii s prosto logicheskimi
operaciyami:
&& || !
Rezul'tatom poslednih mozhet byt' 0 ili 1, i oni v osnovnom
ispol'zuyutsya v uslovnyh vyrazheniyah operatorov if, while ili for
($$3.3.1). Naprimer, !0 (ne nul') imeet znachenie 1, togda kak ~0
(dopolnenie nulya) predstavlyaet soboj nabor razryadov "vse edinicy",
kotoryj obychno yavlyaetsya znacheniem -1 v dopolnitel'nom kode.
3.2.5 Preobrazovanie tipa
Inogda byvaet neobhodimo yavno preobrazovat' znachenie odnogo tipa v
znachenie drugogo. Rezul'tatom yavnogo preobrazovaniya budet
znachenie ukazannogo tipa, poluchennoe iz znacheniya drugogo tipa.
Naprimer:
float r = float(1);
Zdes' pered prisvaivaniem celoe znachenie 1 preobrazuetsya v znachenie
s plavayushchej tochkoj 1.0f. Rezul'tat preobrazovaniya tipa ne yavlyaetsya
adresom, poetomu emu prisvaivat' nel'zya (esli tol'ko tip ne yavlyaetsya
ssylkoj).
Sushchestvuyut dva vida zapisi yavnogo preobrazovaniya tipa:
tradicionnaya zapis', kak operaciya privedeniya v S, naprimer, (double)a
i funkcional'naya zapis', naprimer, double(a). Funkcional'nuyu zapis'
nel'zya ispol'zovat' dlya tipov, kotorye ne imeyut prostogo imeni.
Naprimer, chtoby preobrazovat' znachenie v tip ukazatelya, nado ili
ispol'zovat' privedenie
char* p = (char*)0777;
ili opredelit' novoe imya tipa:
typedef char* Pchar;
char* p = Pchar(0777);
Po mneniyu avtora, funkcional'naya zapis' v netrivial'nyh sluchayah
predpochtitel'nee. Rassmotrim dva ekvivalentnyh primera:
Pname n2 = Pbase(n1->tp)->b_name; // funkcional'naya zapis'
Pname n3 = ((Pbase)n2->tp)->b_name; // zapis' s privedeniem
Poskol'ku operaciya -> imeet bol'shij prioritet, chem operaciya privedeniya,
poslednee vyrazhenie vypolnyaetsya tak:
((Pbase)(n2->tp))->b_name
Ispol'zuya yavnoe preobrazovanie v tip ukazatelya mozhno vydat' dannyj ob容kt
za ob容kt proizvol'nogo tipa. Naprimer, prisvaivanie
any_type* p = (any_type*)&some_object;
pozvolit obrashchat'sya k nekotoromu ob容ktu (some_object) cherez ukazatel'
p kak k ob容ktu proizvol'nogo tipa (any_type). Tem ne menee, esli
some_object v dejstvitel'nosti imeet tip ne any_type, mogut poluchit'sya
strannye i nezhelatel'nye rezul'taty.
Esli preobrazovanie tipa ne yavlyaetsya neobhodimym, ego voobshche sleduet
izbegat'. Programmy, v kotoryh est' takie preobrazovaniya, obychno
trudnee ponimat', chem programmy, ih ne imeyushchie. V to zhe vremya
programmy s yavno zadannymi preobrazovaniyami tipa ponyatnee,
chem programmy, kotorye obhodyatsya bez takih preobrazovanij, potomu chto
ne vvodyat tipov dlya predstavleniya ponyatij bolee vysokogo urovnya.
Tak, naprimer, postupayut programmy, upravlyayushchie registrom ustrojstva s
pomoshch'yu sdviga i maskirovaniya celyh, vmesto togo, chtoby opredelit'
podhodyashchuyu strukturu (struct) i rabotat' neposredstvenno s nej
(sm. $$2.6.1). Korrektnost' yavnogo preobrazovaniya tipa chasto
sushchestvenno zavisit ot togo, naskol'ko programmist ponimaet, kak yazyk
rabotaet s ob容ktami razlichnyh tipov, i kakova specifika dannoj realizacii
yazyka. Privedem primer:
int i = 1;
char* pc = "asdf";
int* pi = &i;
i = (int)pc;
pc = (char*)i; // ostorozhno: znachenie pc mozhet izmenit'sya.
// Na nekotoryh mashinah sizeof(int)
// men'she, chem sizeof(char*)
pi = (int*)pc;
pc = (char*)pi; // ostorozhno: pc mozhet izmenit'sya
// Na nekotoryh mashinah char* imeet ne takoe
// predstavlenie, kak int*
Dlya mnogih mashin eti prisvaivaniya nichem ne grozyat, no dlya nekotoryh
rezul'tat mozhet byt' plachevnym. V luchshem sluchae podobnaya programma
budet perenosimoj. Obychno bez osobogo riska mozhno predpolozhit',
chto ukazateli na razlichnye struktury imeyut odinakovoe predstavlenie.
Dalee, proizvol'nyj ukazatel' mozhno prisvoit' (bez yavnogo preobrazovaniya
tipa) ukazatelyu tipa void*, a void* mozhet byt' yavno preobrazovan
obratno v ukazatel' proizvol'nogo tipa.
V yazyke S++ yavnye preobrazovaniya tipa okazyvaetsya izlishnimi vo mnogih
sluchayah, kogda v S (i drugih yazykah) oni trebuyutsya. Vo mnogih
programmah mozhno voobshche obojtis' bez yavnyh preobrazovanij tipa, a vo
mnogih drugih oni mogut byt' lokalizovany v neskol'kih podprogrammah.
Imenovannyj ob容kt yavlyaetsya libo staticheskim, libo avtomaticheskim
(sm.$$2.1.3). Staticheskij ob容kt razmeshchaetsya v pamyati v moment zapuska
programmy i sushchestvuet tam do ee zaversheniya. Avtomaticheskij ob容kt
razmeshchaetsya v pamyati vsyakij raz, kogda upravlenie popadaet v blok,
soderzhashchij opredelenie ob容kta, i sushchestvuet tol'ko do teh por, poka
upravlenie ostaetsya v etom bloke. Tem ne menee, chasto byvaet udobno
sozdat' novyj ob容kt, kotoryj sushchestvuet do teh por, poka on
ne stanet nenuzhnym. V chastnosti, byvaet udobno sozdat' ob容kt, kotoryj
mozhno ispol'zovat' posle vozvrata iz funkcii, gde on byl sozdan.
Podobnye ob容kty sozdaet operaciya new, a operaciya delete ispol'zuetsya
dlya ih unichtozheniya v dal'nejshem. Pro ob容kty, sozdannye operaciej new,
govoryat, chto oni razmeshchayutsya v svobodnoj pamyati. Primerami takih
ob容ktov yavlyayutsya uzly derev'ev ili elementy spiska, kotorye vhodyat
v struktury dannyh, razmer kotoryh na etape translyacii neizvesten.
Davajte rassmotrim v kachestve primera nabrosok translyatora, kotoryj
stroitsya analogichno programme kal'kulyatora. Funkcii sintaksicheskogo
analiza sozdayut iz predstavlenij vyrazhenij derevo, kotoroe budet
v dal'nejshem ispol'zovat'sya dlya generacii koda. Naprimer:
struct enode {
token_value oper;
enode* left;
enode* right;
};
enode* expr()
{
enode* left = term();
for(;;)
switch(curr_tok) {
case PLUS:
case MINUS:
get_token();
enode* n = new enode;
n->oper = curr_tok;
n->left = left;
n->right = term();
left = n;
break;
default:
return left;
}
}
Generator koda mozhet ispol'zovat' derevo vyrazhenij, naprimer tak:
void generate(enode* n)
{
switch (n->oper) {
case PLUS:
// sootvetstvuyushchaya generaciya
delete n;
}
}
Ob容kt, sozdannyj s pomoshch'yu operacii new, sushchestvuet, do teh por,
poka on ne budet yavno unichtozhen operaciej delete. Posle etogo
pamyat', kotoruyu on zanimal, vnov' mozhet ispol'zovat'sya new. Obychno net
nikakogo "sborshchika musora", ishchushchego ob容kty, na kotorye nikto
ne ssylaetsya, i predostavlyayushchego zanimaemuyu imi pamyat' operacii new dlya
povtornogo ispol'zovaniya. Operandom delete mozhet byt'
tol'ko ukazatel', kotoryj vozvrashchaet operaciya new, ili nul'.
Primenenie delete k nulyu ne privodit ni k kakim dejstviyam.
Operaciya new mozhet takzhe sozdavat' massivy ob容ktov, naprimer:
char* save_string(const char* p)
{
char* s = new char[strlen(p)+1];
strcpy(s,p);
return s;
}
Otmetim, chto dlya pereraspredeleniya pamyati, otvedennoj operaciej new,
operaciya delete dolzhna umet' opredelyat' razmer razmeshchennogo ob容kta.
Naprimer:
int main(int argc, char* argv[])
{
if (argc < 2) exit(1);
char* p = save_string(arg[1]);
delete[] p;
}
CHtoby dobit'sya etogo, prihoditsya pod ob容kt, razmeshchaemyj standartnoj
operaciej new, otvodit' nemnogo bol'she pamyati, chem pod staticheskij
(obychno, bol'she na odno slovo). Prostoj operator delete unichtozhaet
otdel'nye ob容kty, a operaciya delete[] ispol'zuetsya dlya unichtozheniya
massivov.
Operacii so svobodnoj pamyat'yu realizuyutsya funkciyami ($$R.5.3.3-4):
void* operator new(size_t);
void operator delete(void*);
Zdes' size_t - bezznakovyj celochislennyj tip, opredelennyj v <stddef.h>.
Standartnaya realizaciya funkcii operator new() ne inicializiruet
predostavlyaemuyu pamyat'.
CHto sluchitsya, kogda operaciya new ne smozhet bol'she najti svobodnoj
pamyati dlya razmeshcheniya? Poskol'ku dazhe virtual'naya pamyat' nebeskonechna,
takoe vremya ot vremeni proishodit. Tak, zapros vida:
char* p = new char [100000000];
obychno ne prohodit normal'no. Kogda operaciya new ne mozhet vypolnit'
zapros, ona vyzyvaet funkciyu, kotoraya byla zadana kak parametr
pri obrashchenii k funkcii set_new_handler() iz <new.h>. Naprimer,
v sleduyushchej programme:
#include <iostream.h>
#include <new.h>
#include <stdlib.h>
void out_of_store()
{
cerr << "operator new failed: out of store\n";
exit(1);
}
int main()
{
set_new_handler(&out_of_store);
char* p = new char[100000000];
cout << "done, p = " << long(p) << '\n';
}
skoree vsego, budet napechatano ne "done", a soobshchenie:
operator new failed: out of store
// operaciya new ne proshla: net pamyati
S pomoshch'yu funkcii new_handler mozhno sdelat' nechto bolee slozhnoe,
chem prosto zavershit' programmu. Esli izvesten algoritm operacij new i
delete (naprimer, potomu, chto pol'zovatel' opredelil svoi funkcii
operator new i operator delete), to obrabotchik new_handler mozhet
popytat'sya najti svobodnuyu pamyat' dlya new. Drugimi slovami,
pol'zovatel' mozhet napisat' svoj "sborshchik musora", tem samym sdelav
vyzov operacii delete neobyazatel'nym. Odnako takaya zadacha,
bezuslovno, ne pod silu novichku.
Po tradicii operaciya new prosto vozvrashchaet ukazatel' 0, esli ne
udalos' najti dostatochno svobodnoj pamyati. Reakciya zhe na eto
new_handler ne byla ustanovlena. Naprimer, sleduyushchaya programma:
#include <stream.h>
main()
{
char* p = new char[100000000];
cout << "done, p = " << long(p) << '\n';
}
vydast
done, p = 0
Pamyat' ne vydelena, i vam sdelano preduprezhdenie! Otmetim, chto, zadav
reakciyu na takuyu situaciyu v funkcii new_handler, pol'zovatel' beret
na sebya proverku: ischerpana li svobodnaya pamyat'. Ona dolzhna vypolnyat'sya
pri kazhdom obrashchenii v programme k new (esli tol'ko pol'zovatel'
ne opredelil sobstvennye funkcii dlya razmeshcheniya ob容ktov
pol'zovatel'skih tipov; sm.$$R.5.5.6).
Polnoe i posledovatel'noe opisanie operatorov S++ soderzhitsya v
$$R.6. Sovetuem oznakomit'sya s etim razdelom. Zdes' zhe daetsya
svodka operatorov i neskol'ko primerov.
------------------------------------------------------------------
Sintaksis operatorov
------------------------------------------------------------------
operator:
opisanie
{ spisok-operatorov opt }
vyrazhenie opt ;
if ( vyrazhenie ) operator
if ( vyrazhenie ) operator else operator
switch ( vyrazhenie ) operator
while ( vyrazhenie ) operator
do operator while ( vyrazhenie )
for (nachal'nyj-operator-for vyrazhenie opt; vyrazhenie opt) operator
case vyrazhenie-konstanta : operator
default : operator
break ;
continue ;
return vyrazhenie opt ;
goto identifikator ;
identifikator : operator
spisok-operatorov:
operator
spisok-operatorov operator
nachal'nyj-operator-for:
opisanie
vyrazhenie opt ;
----------------------------------------------------------------------
Obratite vnimanie, chto opisanie yavlyaetsya operatorom, no net operatorov
prisvaivaniya ili vyzova funkcii (oni otnosyatsya k vyrazheniyam).
3.3.1 Vybirayushchie operatory
Znachenie mozhno proverit' s pomoshch'yu operatorov if ili switch:
if ( vyrazhenie ) operator
if ( vyrazhenie ) operator else operator
switch ( vyrazhenie ) operator
V yazyke S++ sredi osnovnyh tipov net otdel'nogo bulevskogo (tip
so znacheniyami istina, lozh'). Vse operacii otnoshenij:
== != < > <= >=
dayut v rezul'tate celoe 1, esli otnoshenie vypolnyaetsya, i 0 v protivnom
sluchae. Obychno opredelyayut konstanty TRUE kak 1 i FALSE kak 0.
V operatore if, esli vyrazhenie imeet nenulevoe znachenie,
vypolnyaetsya pervyj operator, a inache vypolnyaetsya vtoroj (esli
on ukazan). Takim obrazom, v kachestve usloviya dopuskaetsya lyuboe vyrazhenie
tipa celoe ili ukazatel'. Pust' a celoe, togda
if (a) // ...
ekvivalentno
if (a != 0) ...
Logicheskie operacii
&& || !
obychno ispol'zuyutsya v usloviyah. V operaciyah && i || vtoroj operand
ne vychislyaetsya, esli rezul'tat opredelyaetsya znacheniem pervogo
operanda. Naprimer, v vyrazhenii
if (p && l<p->count) // ...
snachala proveryaetsya znachenie p, i tol'ko esli ono ne ravno nulyu, to
proveryaetsya otnoshenie l<p->count.
Nekotorye prostye operatory if udobno zamenyat' vyrazheniyami
usloviya. Naprimer, vmesto operatora
if (a <= b)
max = b;
else
max = a;
luchshe ispol'zovat' vyrazhenie
max = (a<=b) ? b : a;
Uslovie v vyrazhenii usloviya ne obyazatel'no okruzhat' skobkami, no
esli ih ispol'zovat', to vyrazhenie stanovitsya ponyatnee.
Prostoj pereklyuchatel' (switch) mozhno zapisat' s pomoshch'yu
serii operatorov if. Naprimer,
switch (val) {
case 1:
f();
break;
case 2:
g();
break;
default:
h();
break;
}
mozhno ekvivalentno zadat' tak:
if (val == 1)
f();
else if (val == 2)
g();
else
h();
Smysl obeih konstrukcij sovpadaet, no vse zhe pervaya predpochtitel'nee,
poskol'ku v nej naglyadnee pokazana sut' operacii: proverka na
sovpadenie znacheniya val so znacheniem iz mnozhestva konstant. Poetomu v
netrivial'nyh sluchayah zapis', ispol'zuyushchaya pereklyuchatel', ponyatnee.
Nuzhno pozabotit'sya o kakom-to zavershenii operatora, ukazannogo
v variante pereklyuchatelya, esli tol'ko vy ne hotite, chtoby stali
vypolnyat'sya operatory iz sleduyushchego varianta. Naprimer,
pereklyuchatel'
switch (val) { // vozmozhna oshibka
case 1:
cout << "case 1\n";
case 2:
cout << "case 2\n";
default:
cout << "default: case not found\n";
}
pri val==1 napechataet k bol'shomu udivleniyu neposvyashchennyh:
case 1
case 2
default: case not found
Imeet smysl otmetit' v kommentariyah te redkie sluchai, kogda standartnyj
perehod na sleduyushchij variant ostavlen namerenno. Togda etot perehod
vo vseh ostal'nyh sluchayah mozhno smelo schitat' oshibkoj. Dlya
zaversheniya operatora v variante chashche vsego ispol'zuetsya break, no
inogda ispol'zuyutsya return i dazhe goto. Privedem primer:
switch (val) { // vozmozhna oshibka
case 0:
cout << "case 0\n";
case1:
case 1:
cout << "case 1\n";
return;
case 2:
cout << "case 2\n";
goto case1;
default:
cout << "default: case not found\n";
return;
}
Zdes' pri znachenii val ravnom 2 my poluchim:
case 2
case 1
Otmetim, chto metku varianta nel'zya ispol'zovat' v operatore goto:
goto case 2; // sintaksicheskaya oshibka
Preziraemyj operator goto vse-taki est' v S++:
goto identifikator;
identifikator: operator
Voobshche govorya, on malo ispol'zuetsya v yazykah vysokogo urovnya, no
mozhet byt' ochen' polezen, esli tekst na S++ sozdaetsya ne chelovekom,
a avtomaticheski, t.e. s pomoshch'yu programmy. Naprimer,
operatory goto ispol'zuyutsya pri sozdanii analizatora po zadannoj
grammatike yazyka s pomoshch'yu programmnyh sredstv.
Krome togo, operatory goto mogut prigodit'sya v teh sluchayah,
kogda na pervyj plan vyhodit skorost' raboty programmy. Odin iz
nih - kogda v real'nom vremeni proishodyat kakie-to vychisleniya vo
vnutrennem cikle programmy.
Est' nemnogie situacii i v obychnyh programmah, kogda primenenie
goto opravdano. Odna iz nih - vyhod iz vlozhennogo cikla ili
pereklyuchatelya. Delo v tom, chto operator break vo vlozhennyh ciklah
ili pereklyuchatelyah pozvolyaet perejti tol'ko na odin uroven' vyshe.
Privedem primer:
void f()
{
int i;
int j;
for ( i = 0; i < n; i++)
for (j = 0; j<m; j++)
if (nm[i][j] == a) goto found;
// zdes' a ne najdeno
// ...
found:
// nm[i][j] == a
}
Est' eshche operator continue, kotoryj pozvolyaet perejti na konec
cikla. CHto eto znachit, ob座asneno v $$3.1.5.
3.4 Kommentarii i raspolozhenie teksta
Programmu gorazdo legche chitat', i ona stanovitsya namnogo ponyatnee, esli
razumno ispol'zovat' kommentarii i sistematicheski vydelyat' tekst
programmy probelami. Est' neskol'ko sposobov raspolozheniya teksta
programmy, no net prichin schitat', chto odin iz nih - nailuchshij. Hotya
u kazhdogo svoj vkus. To zhe mozhno skazat' i o kommentariyah.
Odnako mozhno zapolnit' programmu takimi kommentariyami, chto chitat'
i ponimat' ee budet tol'ko trudnee. Translyator ne v silah ponyat'
kommentarij, poetomu on ne mozhet ubedit'sya v tom, chto kommentarij:
[1] osmyslennyj,
[2] dejstvitel'no opisyvaet programmu,
[3] ne ustarel.
Vo mnogih programmah popadayutsya nepostizhimye, dvusmyslennye i prosto
nevernye kommentarii. Luchshe voobshche obhodit'sya bez nih, chem davat'
takie kommentarii.
Esli nekij fakt mozhno pryamo vyrazit' v yazyke, to tak i sleduet
delat', i ne nado schitat', chto dostatochno upomyanut' ego v kommentarii.
Poslednee zamechanie otnositsya k kommentariyam, podobnym privedennym
nizhe:
// peremennuyu "v" neobhodimo inicializirovat'.
// peremennaya "v" mozhet ispol'zovat'sya tol'ko v funkcii "f()".
// do vyzova lyuboj funkcii iz etogo fajla
// neobhodimo vyzvat' funkciyu "init()".
// v konce svoej programmy vyzovite funkciyu "cleanup()".
// ne ispol'zujte funkciyu "weird()".
// funkciya "f()" imeet dva parametra.
Pri pravil'nom programmirovanii na S++ takie kommentarii obychno
okazyvayutsya izlishnimi. CHtoby imenno eti kommentarii stali nenuzhnymi,
mozhno vospol'zovat'sya pravilami svyazyvaniya ($$4.2) i oblastej
vidimosti, a takzhe pravilami inicializacii i unichtozheniya ob容ktov
klassa ($$5.5).
Esli nekotoroe utverzhdenie vyrazhaetsya samoj programmoj, ne nuzhno
povtoryat' ego v kommentarii. Naprimer:
a = b + c; // a prinimaet znachenie b+c
count++; // uvelichim schetchik count
Takie kommentarii huzhe, chem izbytochnye. Oni razduvayut ob容m teksta,
zatumanivayut programmu i mogut byt' dazhe lozhnymi. V to zhe vremya
kommentarii imenno takogo roda ispol'zuyut dlya primerov v uchebnikah
po yazykam programmirovaniya, podobnyh etoj knige. |to odna iz
mnogih prichin, po kotoroj uchebnaya programma otlichaetsya ot nastoyashchej.
Mozhno rekomendovat' takoj stil' vvedeniya kommentariev v
programmu:
[1] nachinat' s kommentariya kazhdyj fajl programmy: ukazat' v
obshchih chertah, chto v nej opredelyaetsya, dat' ssylki na
spravochnye rukovodstva, obshchie idei po soprovozhdeniyu
programmy i t.d.;
[2] snabzhat' kommentariem kazhdoe opredelenie klassa ili shablona
tipa;
[3] kommentirovat' kazhduyu netrivial'nuyu funkciyu, ukazav: ee
naznachenie, ispol'zuemyj algoritm (esli tol'ko on neocheviden)
i, vozmozhno, predpolozheniya ob okruzhenii, v kotorom rabotaet
funkciya;
[4] kommentirovat' opredelenie kazhdoj global'noj peremennoj;
[5] davat' nekotoroe chislo kommentariev v teh mestah, gde
algoritm neocheviden ili neperenosim;
[6] bol'she prakticheski nichego.
Privedem primer:
// tbl.c: Realizaciya tablicy imen.
/*
Ispol'zovan metod Gaussa
sm. Ral'ston "Nachal'nyj kurs po ..." str. 411.
*/
// v swap() predpolagaetsya, chto stek AT&T nachinaetsya s 3B20.
/************************************
Avtorskie prava (c) 1991 AT&T, Inc
Vse prava sohraneny
**************************************/
Pravil'no podobrannye i horosho sostavlennye kommentarii igrayut v
programme vazhnuyu rol'. Napisat' horoshie kommentarii ne menee
trudno, chem samu programmu, i eto - iskusstvo, v kotorom stoit
sovershenstvovat'sya.
Zametim, chto esli v funkcii ispol'zuyutsya tol'ko kommentarii
vida //, to lyubuyu ee chast' mozhno sdelat' kommentariem s pomoshch'yu
/* */, i naoborot.
1. (*1) Sleduyushchij cikl for perepishite s pomoshch'yu operatora while:
for (i=0; i<max_length; i++)
if (input_line[i] == '?') quest_count++;
Zapishite cikl, ispol'zuya v kachestve ego upravlyayushchej peremennoj
ukazatel' tak, chtoby uslovie imelo vid *p=='?'.
2. (*1) Ukazhite poryadok vychisleniya sleduyushchih vyrazhenij, zadav polnuyu
skobochnuyu strukturu:
a = b + c * d << 2 & 8
a & 077 != 3
a == b || a == c && c < 5
c = x != 0
0 <= i < 7
f(1,2) + 3
a = - 1 + + b -- - 5
a = b == c ++
a = b = c = 0
a[4][2] *= * b ? c : * d * 2
a-b, c=d
3. (*2) Ukazhite 5 razlichnyh konstrukcij na S++, znachenie kotoryh
neopredeleno.
4. (*2) Privedite 10 raznyh primerov neperenosimyh konstrukcij
na S++.
5. (*1) CHto proizojdet pri delenii na nul' v vashej programme na S++?
CHto budet v sluchae perepolneniya ili poteri znachimosti?
6. (*1) Ukazhite poryadok vychisleniya sleduyushchih vyrazhenij, zadav ih
polnuyu skobochnuyu strukturu:
*p++
*--p
++a--
(int*)p->m
*p.m
*a[i]
7. (*2) Napishite takie funkcii: strlen() - podschet dliny stroki,
strcpy() - kopirovanie strok i strcmp() - sravnenie strok. Kakimi
dolzhny byt' tipy parametrov i rezul'tatov funkcij? Sravnite ih
so standartnymi versiyami, imeyushchimisya v <string.h> i v vashem
rukovodstve.
8. (*1) Vyyasnite, kak vash translyator otreagiruet na takie oshibki:
void f(int a, int b)
{
if (a = 3) // ...
if (a&077 == 0) // ...
a := b+1;
}
Posmotrite, kakova budet reakciya na bolee prostye oshibki.
9. (*2) Napishite funkciyu cat(), kotoraya poluchaet dva parametra-stroki
i vozvrashchaet stroku, yavlyayushchuyusya ih konkatenaciej. Dlya
rezul'tiruyushchej stroki ispol'zujte pamyat', otvedennuyu s pomoshch'yu
new. Napishite funkciyu rev() dlya perevertyvaniya stroki, peredannoj
ej v kachestve parametra. |to oznachaet, chto posle vyzova rev(p)
poslednij simvol p stanet pervym i t.d.
10. (*2) CHto delaet sleduyushchaya funkciya?
void send(register* to, register* from, register count)
// Psevdoustrojstvo. Vse kommentarii soznatel'no udaleny
{
register n=(count+7)/8;
switch (count%8) {
case 0: do { *to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n>0);
}
}
Kakov mozhet byt' smysl etoj funkcii?
11. (*2) Napishite funkciyu atoi(), kotoraya imeet parametr - stroku cifr
i vozvrashchaet sootvetstvuyushchee ej celoe. Naprimer, atoi("123")
ravno 123. Izmenite funkciyu atoi() tak, chtoby ona mogla
perevodit' v chislo posledovatel'nost' cifr ne tol'ko v desyatichnoj,
no i v vos'merichnoj i shestnadcaterichnoj zapisi, prinyatoj v S++.
Dobav'te vozmozhnost' perevoda simvol'nyh konstant S++. Napishite
funkciyu itoa() dlya perevoda celogo znacheniya v strokovoe
predstavlenie.
12. (*2) Perepishite funkciyu get_token() ($$3.12) tak, chtoby ona chitala
celuyu stroku v bufer, a zatem vydavala leksemy, chitaya po
simvolu iz bufera.
13. (*2) Vvedite v programmu kal'kulyatora iz $$3.1 takie funkcii, kak
sqrt(), log() i sin(). Podskazka: zadajte predopredelennye
imena i vyzyvajte funkcii s pomoshch'yu massiva ukazatelej na nih.
Ne zabyvajte proveryat' parametry, peredavaemye etim
funkciyam.
14. (*3) Vvedite v kal'kulyator vozmozhnost' opredelyat' pol'zovatel'skie
funkcii. Podskazka: opredelite funkciyu kak posledovatel'nost'
operatorov, budto by zadannuyu samim pol'zovatelem. |tu
posledovatel'nost' mozhno hranit' ili kak stroku simvolov, ili
kak spisok leksem. Kogda vyzyvaetsya funkciya, nado vybirat' i
vypolnyat' operacii. Esli pol'zovatel'skie funkcii mogut
imet' parametry, to pridetsya pridumat' formu zapisi i dlya nih.
15. (*1.5) Peredelajte programmu kal'kulyatora, ispol'zuya strukturu
symbol vmesto staticheskih peremennyh name_string i number_value:
struct symbol {
token_value tok;
union {
double number_value;
char* name_string;
};
};
16.(*2.5) Napishite programmu, kotoraya udalyaet vse kommentarii iz
programmy na S++. |to znachit, nado chitat' simvoly iz cin i
udalyat' kommentarii dvuh vidov: // i /* */. Poluchivshijsya tekst
zapishite v cout. Ne zabot'tes' o krasivom vide poluchivshegosya
teksta (eto uzhe drugaya, bolee slozhnaya zadacha). Korrektnost'
programm nevazhna. Nuzhno uchityvat' vozmozhnost' poyavleniya simvolov
//, /* i */ v kommentariyah, strokah i simvol'nyh konstantah.
17. (*2) Issledujte razlichnye programmy i vyyasnite, kakie sposoby
vydeleniya teksta probelami i kakie kommentarii ispol'zuyutsya.
Iteraciya prisushcha cheloveku,
a rekursiya - bogu.
- L. Dojch
Vse netrivial'nye programmy sostoyat iz neskol'kih razdel'no
transliruemyh edinic, po tradicii nazyvaemyh fajlami. V etoj glave
opisano, kak razdel'no transliruemye funkcii mogut vyzyvat' drug druga,
kakim obrazom oni mogut imet' obshchie dannye, i kak dobit'sya
neprotivorechivosti tipov, ispol'zuemyh v raznyh fajlah programmy.
Podrobno obsuzhdayutsya funkcii, v tom chisle:
peredacha parametrov, peregruzka imeni funkcii,
standartnye znacheniya parametrov, ukazateli na funkcii i, estestvenno,
opisaniya i opredeleniya funkcij. V konce glavy obsuzhdayutsya
makrovozmozhnosti yazyka.
Rol' fajla v yazyke S++ svoditsya k tomu, chto on opredelyaet fajlovuyu
oblast' vidimosti ($$R.3.2). |to oblast' vidimosti global'nyh
funkcij (kak staticheskih, tak i podstanovok), a takzhe global'nyh
peremennyh (kak staticheskih, tak i so specifikaciej const). Krome
togo, fajl yavlyaetsya tradicionnoj edinicej hraneniya v sisteme, a
takzhe edinicej translyacii. Obychno sistemy hranyat, transliruyut i
predstavlyayut pol'zovatelyu programmu na S++ kak mnozhestvo fajlov,
hotya sushchestvuyut sistemy, ustroennye inache. V etoj glave budet
obsuzhdat'sya v osnovnom tradicionnoe ispol'zovanie fajlov.
Vsyu programmu pomestit' v odin fajl, kak pravilo, nevozmozhno,
poskol'ku programmy standartnyh funkcij i programmy operacionnoj
sistemy nel'zya vklyuchit' v tekstovom vide v programmu pol'zovatelya.
Voobshche, pomeshchat' vsyu programmu pol'zovatelya v odin fajl obychno
neudobno i nepraktichno. Razbieniya programmy na fajly mozhet
oblegchit' ponimanie obshchej struktury programmy i daet translyatoru
vozmozhnost' podderzhivat' etu strukturu. Esli edinicej translyacii
yavlyaetsya fajl, to dazhe pri nebol'shom izmenenii v nem sleduet
ego peretranslirovat'. Dazhe dlya programm ne slishkom bol'shogo
razmera vremya na peretranslyaciyu mozhno znachitel'no sokratit', esli
ee razbit' na fajly podhodyashchego razmera.
Vernemsya k primeru s kal'kulyatorom. Reshenie bylo dano v vide
odnogo fajla. Kogda vy popytaetes' ego translirovat', neizbezhno
vozniknut nekotorye problemy s poryadkom opisanij. Po krajnej mere
odno "nenastoyashchee" opisanie pridetsya dobavit' k tekstu, chtoby
translyator mog razobrat'sya v ispol'zuyushchih drug druga funkciyah
expr(), term() i prim(). Po tekstu programmy vidno, chto ona
sostoit iz chetyreh chastej: leksicheskij analizator (skaner),
sobstvenno analizator, tablica imen i drajver. Odnako, etot fakt
nikak ne otrazhen v samoj programme. Na samom dele kal'kulyator
ne byl zaprogrammirovan imenno tak. Tak ne sleduet pisat'
programmu. Dazhe esli ne uchityvat' vse rekomendacii po
programmirovaniyu, soprovozhdeniyu i optimizacii dlya takoj "zryashnoj"
programmy, vse ravno ee sleduet sozdavat' iz neskol'kih fajlov
hotya by dlya udobstva.
CHtoby razdel'naya translyaciya stala vozmozhnoj, programmist
dolzhen predusmotret' opisaniya, iz kotoryh translyator poluchit
dostatochno svedenij o tipah dlya translyacii fajla, sostavlyayushchego
tol'ko chast' programmy. Trebovanie neprotivorechivosti ispol'zovaniya
vseh imen i tipov dlya programmy, sostoyashchej iz neskol'kih razdel'no
transliruemyh chastej, tak zhe spravedlivo, kak i dlya programmy,
sostoyashchej iz odnogo fajla. |to vozmozhno tol'ko v tom sluchae, kogda
opisaniya, nahodyashchiesya v raznyh edinicah translyacii, budut
soglasovany. V vashej sisteme programmirovaniya imeyutsya sredstva,
kotorye sposobny ustanovit', vypolnyaetsya li eto. V chastnosti, mnogie
protivorechiya obnaruzhivaet redaktor svyazej. Redaktor svyazej - eto programma,
kotoraya svyazyvaet po imenam razdel'no transliruemye chasti programmy.
Inogda ego po oshibke nazyvayut zagruzchikom.
Esli yavno ne opredeleno inache, to imya, ne yavlyayushcheesya lokal'nym dlya
nekotoroj funkcii ili klassa, dolzhno oboznachat' odin i tot zhe tip,
znachenie, funkciyu ili ob容kt vo vseh edinicah translyacii dannoj
programmy. Inymi slovami, v programme mozhet byt' tol'ko odin
nelokal'nyj tip, znachenie, funkciya ili ob容kt s dannym imenem.
Rassmotrim dlya primera dva fajla:
// file1.c
int a = 1;
int f() { /* kakie-to operatory */ }
// file2.c
extern int a;
int f();
void g() { a = f(); }
V funkcii g() ispol'zuyutsya te samye a i f(), kotorye opredeleny v
fajle file1.c. Sluzhebnoe slovo extern pokazyvaet, chto opisanie
a v fajle file2.c yavlyaetsya tol'ko opisaniem, no ne opredeleniem.
Esli by prisutstvovala inicializaciya a, to extern prosto
proignorirovalos' by, poskol'ku opisanie s inicializaciej vsegda
schitaetsya opredeleniem. Lyuboj ob容kt v programme mozhet opredelyat'sya
tol'ko odin raz. Opisyvat'sya zhe on mozhet neodnokratno, no vse
opisaniya dolzhny byt' soglasovany po tipu. Naprimer:
// file1.c:
int a = 1;
int b = 1;
extern int c;
// file2.c:
int a;
extern double b;
extern int c;
Zdes' soderzhitsya tri oshibki: peremennaya a opredelena dvazhdy ("int a;"
- eto opredelenie, oznachayushchee "int a=0;"); b opisano dvazhdy, prichem
s raznymi tipami; c opisano dvazhdy, no neopredeleno. Takie oshibki
(oshibki svyazyvaniya) translyator, kotoryj obrabatyvaet fajly
po otdel'nosti, obnaruzhit' ne mozhet, no bol'shaya ih chast'
obnaruzhivaetsya redaktorom svyazej.
Sleduyushchaya programma dopustima v S, no ne v S++:
// file1.c:
int a;
int f() { return a; }
// file2.c:
int a;
int g() { return f(); }
Vo-pervyh, oshibkoj yavlyaetsya vyzov f() v file2.c, poskol'ku v etom
fajle f() ne opisana. Vo-vtoryh, fajly programmy ne mogut byt'
pravil'no svyazany, poskol'ku a opredeleno dvazhdy.
Esli imya opisano kak static, ono stanovitsya lokal'nom v etom
fajle. Naprimer:
// file1.c:
static int a = 6;
static int f() { /* ... */ }
// file2.c:
static int a = 7;
static int f() { /* ... */ }
Privedennaya programma pravil'na, poskol'ku a i f opredeleny kak
staticheskie. V kazhdom fajle svoya peremennaya a i funkciya f().
Esli peremennye i funkcii v dannoj chasti programmy opisany kak
static, to v etoj chasti programmy proshche razobrat'sya, poskol'ku ne nuzhno
zaglyadyvat' v drugie chasti. Opisyvat' funkcii kak staticheskie
polezno eshche i po toj prichine, chto translyatoru predostavlyaetsya
vozmozhnost' sozdat' bolee prostoj variant operacii vyzova funkcii.
Esli imya ob容kta ili funkcii lokal'no v dannom fajle, to govoryat,
chto ob容kt podlezhit vnutrennemu svyazyvaniyu. Obratno, esli imya
ob容kta ili funkcii nelokal'no v dannom fajle, to on podlezhit
vneshnemu svyazyvaniyu.
Obychno govoryat, chto imena tipov, t.e. klassov i perechislenij,
ne podlezhat svyazyvaniyu. Imena global'nyh klassov i perechislenij
dolzhny byt' unikal'nymi vo vsej programme i imet' edinstvennoe
opredelenie. Poetomu, esli est' dva dazhe identichnyh opredeleniya
odnogo klassa, eto - vse ravno oshibka:
// file1.c:
struct S { int a; char b; };
extern void f(S*);
// file2.c:
struct S { int a; char b; };
void f(S* p) { /* ... */ }
No bud'te ostorozhny: opoznat' identichnost' dvuh opisanij klassa
ne v sostoyanii bol'shinstvo sistem programmirovaniya S++. Takoe
dublirovanie mozhet vyzvat' dovol'no tonkie oshibki (ved' klassy
v raznyh fajlah budut schitat'sya razlichnymi).
Global'nye funkcii-podstanovki podlezhat vnutrennemu svyazyvaniyu,
i to zhe po umolchaniyu spravedlivo dlya konstant. Sinonimy tipov,
t.e. imena typedef, lokal'ny v svoem fajle, poetomu opisaniya
v dvuh dannyh nizhe fajlah ne protivorechat drug drugu:
// file1.c:
typedef int T;
const int a = 7;
inline T f(int i) { return i+a; }
// file2.c:
typedef void T;
const int a = 8;
inline T f(double d) { cout<<d; }
Konstanta mozhet poluchit' vneshnee svyazyvanie tol'ko s pomoshch'yu yavnogo
opisaniya:
// file3.c:
extern const int a;
const int a = 77;
// file4.c:
extern const int a;
void g() { cout<<a; }
V etom primere g() napechataet 77.
Tipy odnogo ob容kta ili funkcii dolzhny byt' soglasovany vo vseh ih
opisaniyah. Dolzhen byt' soglasovan po tipam i vhodnoj tekst,
obrabatyvaemyj translyatorom, i svyazyvaemye chasti programmy. Est'
prostoj, hotya i nesovershennyj, sposob dobit'sya soglasovannosti
opisanij v razlichnyh fajlah. |to: vklyuchit' vo vhodnye fajly,
soderzhashchie operatory i opredeleniya dannyh, zagolovochnye fajly,
kotorye soderzhat interfejsnuyu informaciyu.
Sredstvom vklyucheniya tekstov sluzhit makrokomanda #include,
kotoraya pozvolyaet sobrat' v odin fajl (edinicu translyacii)
neskol'ko ishodnyh fajlov programmy. Komanda
#include "vklyuchaemyj-fajl"
zamenyaet stroku, v kotoroj ona byla zadana, na soderzhimoe fajla
vklyuchaemyj-fajl. Estestvenno, eto soderzhimoe dolzhno byt' tekstom
na S++, poskol'ku ego budet chitat' translyator. Kak pravilo, operaciya
vklyucheniya realizuetsya otdel'noj programmoj, nazyvaemoj preprocessorom
S++. Ona vyzyvaetsya sistemoj programmirovaniya pered sobstvenno
translyaciej dlya obrabotki takih komand vo vhodnom tekste. Vozmozhno
i drugoe reshenie: chast' translyatora, neposredstvenno rabotayushchaya
s vhodnym tekstom, obrabatyvaet komandy vklyucheniya fajlov po mere ih
poyavleniya v tekste. V toj sisteme programmirovaniya, v kotoroj
rabotaet avtor, chtoby uvidet' rezul'tat komand vklyucheniya fajlov,
nuzhno zadat' komandu:
CC -E file.c
|ta komanda dlya obrabotki fajla file.c zapuskaet preprocessor
(i tol'ko!), podobno tomu, kak komanda CC bez flaga -E zapuskaet sam
translyator.
Dlya vklyucheniya fajlov iz standartnyh katalogov (obychno katalogi
s imenem INCLUDE) nado vmesto kavychek ispol'zovat' uglovye skobki
< i >. Naprimer:
#include <stream.h> // vklyuchenie iz standartnogo kataloga
#include "myheader.h" // vklyuchenie iz tekushchego kataloga
Vklyuchenie iz standartnyh katalogov imeet to preimushchestvo, chto imena
etih katalogov nikak ne svyazany s konkretnoj programmoj (obychno
vnachale vklyuchaemye fajly ishchutsya v kataloge /usr/include/CC, a
zatem v /usr/include). K sozhaleniyu, v etoj komande probely sushchestvenny:
#include < stream.h> // <stream.h> ne budet najden
Bylo by nelepo, esli by kazhdyj raz pered vklyucheniem fajla
trebovalas' ego peretranslyaciya. Obychno vklyuchaemye fajly soderzhat
tol'ko opisaniya, a ne operatory i opredeleniya, trebuyushchie sushchestvennoj
translyatornoj obrabotki. Krome togo, sistema programmirovaniya
mozhet predvaritel'no ottranslirovat' zagolovochnye
fajly, esli, konechno, ona nastol'ko razvita, chto sposobna sdelat'
eto, ne izmenyaya semantiki programmy.
Ukazhem, chto mozhet soderzhat' zagolovochnyj fajl:
Opredeleniya tipov struct point { int x, y; };
SHablony tipov template<class T>
class V { /* ... */ }
Opisaniya funkcij extern int strlen(const char*);
Opredeleniya inline char get() { return *p++; }
funkcij-podstanovok
Opisaniya dannyh extern int a;
Opredeleniya konstant const float pi = 3.141593;
Perechisleniya enum bool { false, true };
Opisaniya imen class Matrix;
Komandy vklyucheniya fajlov #include <signal.h>
Makroopredeleniya #define Case break;case
Kommentarii /* proverka na konec fajla */
Perechislenie togo, chto stoit pomeshchat' v zagolovochnyj fajl, ne yavlyaetsya
trebovaniem yazyka, eto prosto sovet po razumnomu ispol'zovaniyu vklyucheniya
fajlov. S drugoj storony, v zagolovochnom fajle nikogda ne dolzhno byt':
Opredelenij obychnyh funkcij char get() { return *p++; }
Opredelenij dannyh int a;
Opredelenij sostavnyh const tb[i] = { /* ... */ };
konstant
Po tradicii zagolovochnye fajly imeyut rasshirenie .h, a fajly,
soderzhashchie opredeleniya funkcij ili dannyh, rasshirenie .c. Inogda
ih nazyvayut "h-fajly" ili "s-fajly" sootvetstvenno. Ispol'zuyut
i drugie rasshireniya dlya etih fajlov: .C, cxx, .cpp i
.cc. Prinyatoe rasshirenie vy najdete v svoem spravochnom rukovodstve.
Makrosredstva opisyvayutsya v $$4.7. Otmetim tol'ko, chto v S++ oni
ispol'zuyutsya ne stol' shiroko, kak v S, poskol'ku S++ imeet opredelennye
vozmozhnosti v samom yazyke: opredeleniya konstant (const),
funkcij-podstanovok (inline), dayushchie vozmozhnost' bolee prostoj
operacii vyzova, i shablonov tipa, pozvolyayushchie porozhdat' semejstvo
tipov i funkcij ($$8).
Sovet pomeshchat' v zagolovochnyj fajl opredeleniya tol'ko prostyh,
no ne sostavnyh, konstant ob座asnyaetsya vpolne pragmaticheskoj prichinoj.
Prosto bol'shinstvo translyatorov ne nastol'ko razumno, chtoby
predotvratit' sozdanie nenuzhnyh kopij sostavnoj konstanty. Voobshche
govorya, bolee prostoj variant vsegda yavlyaetsya bolee obshchim, a znachit
translyator dolzhen uchityvat' ego v pervuyu ochered', chtoby sozdat'
horoshuyu programmu.
4.3.1 Edinstvennyj zagolovochnyj fajl
Proshche vsego razbit' programmu na neskol'ko fajlov sleduyushchim
obrazom: pomestit' opredeleniya vseh funkcij i dannyh v nekotoroe
chislo vhodnyh fajlov, a vse tipy, neobhodimye dlya svyazi mezhdu
nimi, opisat' v edinstvennom zagolovochnom fajle. Vse vhodnye
fajly budut vklyuchat' zagolovochnyj fajl. Programmu
kal'kulyatora mozhno razbit' na chetyre vhodnyh fajla .c:
lex.c, syn.c, table.c i main.c. Zagolovochnyj fajl dc.h budet
soderzhat' opisaniya kazhdogo imeni, kotoroe ispol'zuetsya bolee
chem v odnom .c fajle:
// dc.h: obshchee opisanie dlya kal'kulyatora
#include <iostream.h>
enum token_value {
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*', DIV='/',
PRINT=';', ASSIGN='=', LP='(', RP=')'
};
extern int no_of_errors;
extern double error(const char* s);
extern token_value get_token();
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern double expr();
extern double term();
extern double prim();
struct name {
char* string;
name* next;
double value;
};
extern name* look(const char* p, int ins = 0);
inline name* insert(const char* s) { return look(s,1); }
Esli ne privodit' sami operatory, lex.c dolzhen imet' takoj vid:
// lex.c: vvod i leksicheskij analiz
#include "dc.h"
#include <ctype.h>
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }
Ispol'zuya sostavlennyj zagolovochnyj fajl, my dob'emsya,
chto opisanie kazhdogo ob容kta, vvedennogo pol'zovatelem, obyazatel'no
okazhetsya v tom fajle, gde etot ob容kt opredelyaetsya. Dejstvitel'no,
pri obrabotke fajla lex.c translyator stolknetsya s opisaniyami
extern token_value get_token();
// ...
token_value get_token() { /* ... */ }
|to pozvolit translyatoru obnaruzhit' lyuboe rashozhdenie v tipah,
ukazannyh pri opisanii dannogo imeni. Naprimer, esli by funkciya
get_token() byla opisana s tipom token_value, no opredelena s
tipom int, translyaciya fajla lex.c vyyavila by oshibku: nesootvetstvie
tipa.
Fajl syn.c mozhet imet' takoj vid:
// syn.c: sintaksicheskij analiz i vychisleniya
#include "dc.h"
double prim() { /* ... */ }
double term() { /* ... */ }
double expr() { /* ... */ }
Fajl table.c mozhet imet' takoj vid:
// table.c: tablica imen i funkciya poiska
#include "dc.h"
extern char* strcmp(const char*, const char*);
extern char* strcpy(char*, const char*);
extern int strlen(const char*);
const int TBLSZ = 23;
name* table[TBLSZ];
name* look(char* p, int ins) { /* ... */ }
Otmetim, chto raz strokovye funkcii opisany v samom fajle table.c,
translyator ne mozhet proverit' soglasovannost' etih opisanij po tipam.
Vsegda luchshe vklyuchit' sootvetstvuyushchij zagolovochnyj fajl,
chem opisyvat' v fajle .c nekotoroe imya kak extern. |to mozhet
privesti k vklyucheniyu "slishkom mnogogo", no takoe vklyuchenie nestrashno,
poskol'ku ne vliyaet na skorost' vypolneniya programmy i ee razmer, a
programmistu pozvolyaet sekonomit' vremya. Dopustim, funkciya strlen() snova
opisyvaetsya v privedennom nizhe fajle main.c. |to tol'ko lishnij
vvod simvolov i potencial'nyj istochnik oshibok, t.k. translyator
ne smozhet obnaruzhit' rashozhdeniya v dvuh opisaniyah strlen() (vprochem,
eto mozhet sdelat' redaktor svyazej). Takoj problemy ne vozniklo by,
esli by v fajle dc.h soderzhalis' vse opisaniya extern, kak pervonachal'no
i predpolagalos'. Podobnaya nebrezhnost' prisutstvuet v nashem primere,
poskol'ku ona tipichna dlya programm na S. Ona ochen' estestvenna
dlya programmista, no chasto privodit k oshibkam i takim programmam,
kotorye trudno soprovozhdat'. Itak, preduprezhdenie sdelano!
Nakonec, privedem fajl main.c:
// main.c: inicializaciya, osnovnoj cikl, obrabotka oshibok
#include "dc.h"
double error(char* s) { /* ... */ }
extern int strlen(const char*);
int main(int argc, char* argv[]) { /* ... */ }
V odnom vazhnom sluchae zagolovochnye fajly vyzyvayut bol'shoe neudobstvo.
S pomoshch'yu serii zagolovochnyh fajlov i standartnoj
biblioteki rasshiryayut vozmozhnosti yazyka, vvodya mnozhestvo tipov (kak
obshchih, tak i rasschitannyh na konkretnye prilozheniya; sm. glavy 5-9).
V takom sluchae tekst kazhdoj edinicy translyacii mozhet nachinat'sya
tysyachami strok zagolovochnyh fajlov. Soderzhimoe zagolovochnyh
fajlov biblioteki, kak pravilo, stabil'no i menyaetsya redko. Zdes'
ochen' prigodilsya by pretranslyator, kotoryj obrabatyvaet ego. Po suti,
nuzhen yazyk special'nogo naznacheniya so svoim translyatorom. No ustoyavshihsya
metodov postroeniya takogo pretranslyatora poka net.
4.3.2 Mnozhestvennye zagolovochnye fajly
Razbienie programmy v raschete na odin zagolovochnyj fajl bol'she
podhodit dlya nebol'shih programm, otdel'nye chasti kotoryh ne
imeyut samostoyatel'nogo naznacheniya. Dlya takih programm dopustimo,
chto po zagolovochnomu fajlu nel'zya opredelit', ch'i opisaniya tam
nahodyatsya i po kakoj prichine. Zdes' mogut pomoch' tol'ko kommentarii.
Vozmozhno al'ternativnoe reshenie: pust' kazhdaya chast' programmy
imeet svoj zagolovochnyj fajl, v kotorom opredelyayutsya sredstva,
predostavlyaemye drugim chastyam. Teper' dlya kazhdogo fajla .c budet
svoj fajl .h, opredelyayushchij, chto mozhet predostavit' pervyj. Kazhdyj fajl
.c budet vklyuchat' kak svoj fajl .h, tak i nekotorye drugie fajly .h,
ishodya iz svoih potrebnostej.
Poprobuem ispol'zovat' takuyu organizaciyu programmy dlya
kal'kulyatora. Zametim, chto funkciya error() nuzhna prakticheski vo vseh
funkciyah programmy, a sama ispol'zuet tol'ko <iostream.h>. Takaya
situaciya tipichna dlya funkcij, obrabatyvayushchih oshibki.
Sleduet otdelit' ee ot fajla main.c:
// error.h: obrabotka oshibok
extern int no_of_errors;
extern double error(const char* s);
// error.c
#include <iostream.h>
#include "error.h"
int no_of_errors;
double error(const char* s) { /* ... */ }
Pri takom podhode k razbieniyu programmy kazhduyu paru fajlov .c
i .h mozhno rassmatrivat' kak modul', v kotorom fajl .h zadaet
ego interfejs, a fajl .c opredelyaet ego realizaciyu.
Tablica imen ne zavisit ni ot kakih chastej kal'kulyatora, krome
chasti obrabotki oshibok. Teper' etot fakt mozhno vyrazit'
yavno:
// table.h: opisanie tablicy imen
struct name {
char* string;
name* next;
double value;
};
extern name* look(const char* p, int ins = 0);
inline name* insert(const char* s) { return look(s,1); }
// table.h: opredelenie tablicy imen
#include "error.h"
#include <string.h>
#include "table.h"
const int TBLSZ = 23;
name* table[TBLSZ];
name* look(const char* p, int ins) { /* ... */ }
Zamet'te, chto teper' opisaniya strokovyh funkcij berutsya iz vklyuchaemogo
fajla <string.h>. Tem samym udalen eshche odin istochnik oshibok.
// lex.h: opisaniya dlya vvoda i leksicheskogo analiza
enum token_value {
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*',
PRINT=';', ASSIGN='=', LP='(', RP= ')'
};
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern token_value get_token();
Interfejs s leksicheskim analizatorom dostatochno zaputannyj. Poskol'ku
nedostatochno sootvetstvuyushchih tipov dlya leksem, pol'zovatelyu
funkcii get_token() predostavlyayutsya te zhe bufery number_value
i name_string, s kotorymi rabotaet sam leksicheskij analizator.
// lex.c: opredeleniya dlya vvoda i leksicheskogo analiza
#include <iostream.h>
#include <ctype.h>
#include "error.h"
#include "lex.h"
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }
Interfejs s sintaksicheskim analizatorom opredelen chetko:
// syn.h: opisaniya dlya sintaksicheskogo analiza i vychislenij
extern double expr();
extern double term();
extern double prim();
// syn.c: opredeleniya dlya sintaksicheskogo analiza i vychislenij
#include "error.h"
#include "lex.h"
#include "syn.h"
double prim() { /* ... */ }
double term() { /* ... */ }
double expr() { /* ... */ }
Kak obychno, opredelenie osnovnoj programmy trivial'no:
// main.c: osnovnaya programma
#include <iostream.h>
#include "error.h"
#include "lex.h"
#include "syn.h"
#include "table.h"
int main(int argc, char* argv[]) { /* ... */ }
Kakoe chislo zagolovochnyh fajlov sleduet ispol'zovat' dlya dannoj
programmy zavisit ot mnogih faktorov. Bol'shinstvo ih opredelyaetsya
sposobom obrabotki fajlov imenno v vashej sisteme, a ne
sobstvenno v S++. Naprimer, esli vash redaktor ne mozhet rabotat'
odnovremenno s neskol'kimi fajlami, dialogovaya obrabotka neskol'kih
zagolovochnyh fajlov zatrudnyaetsya. Drugoj primer: mozhet okazat'sya,
chto otkrytie i chtenie 10 fajlov po 50 strok kazhdyj zanimaet
sushchestvenno bol'she vremeni, chem otkrytie i chtenie odnogo fajla iz 500
strok. V rezul'tate pridetsya horoshen'ko podumat', prezhde chem
razbivat' nebol'shuyu programmu, ispol'zuya mnozhestvennye zagolovochnye
fajly. Predosterezhenie: obychno mozhno upravit'sya s mnozhestvom, sostoyashchim
primerno iz 10 zagolovochnyh fajlov (plyus standartnye zagolovochnye
fajly). Esli zhe vy budete razbivat' programmu na minimal'nye logicheskie
edinicy s zagolovochnymi fajlami (naprimer, sozdavaya dlya kazhdoj struktury
svoj zagolovochnyj fajl), to mozhete ochen' legko poluchit' neupravlyaemoe
mnozhestvo iz soten zagolovochnyh fajlov.
4.4 Svyazyvanie s programmami na drugih yazykah
Programmy na S++ chasto soderzhat chasti, napisannye na drugih yazykah, i
naoborot, chasto fragment na S++ ispol'zuetsya v programmah,
napisannyh na drugih yazykah. Sobrat' v odnu programmu
fragmenty, napisannye na raznyh yazykah, ili, napisannye na odnom
yazyke, no v sistemah programmirovaniya s raznymi soglasheniyami o
svyazyvanii, dostatochno trudno. Naprimer, raznye yazyki ili raznye
realizacii odnogo yazyka mogut razlichat'sya ispol'zovaniem registrov
pri peredache parametrov, poryadkom razmeshcheniya parametrov v steke,
upakovkoj takih vstroennyh tipov, kak celye ili stroki, formatom
imen funkcij, kotorye translyator peredaet redaktoru svyazej, ob容mom
kontrolya tipov, kotoryj trebuetsya ot redaktora svyazej. CHtoby
uprostit' zadachu, mozhno v opisanii vneshnih ukazat' uslovie
svyazyvaniya. Naprimer, sleduyushchee opisanie ob座avlyaet strcpy vneshnej
funkciej i ukazyvaet, chto ona dolzhna svyazyvat'sya soglasno poryadku
svyazyvaniya v S:
extern "C" char* strcpy(char*, const char*);
Rezul'tat etogo opisaniya otlichaetsya ot rezul'tata obychnogo opisaniya
extern char* strcpy(char*, const char*);
tol'ko poryadkom svyazyvaniya dlya vyzyvayushchih strcpy() funkcij. Sama
semantika vyzova i, v chastnosti, kontrol' fakticheskih parametrov
budut odinakovy v oboih sluchayah. Opisanie extern "C" imeet smysl
ispol'zovat' eshche i potomu, chto yazyki S i S++, kak i ih
realizacii, blizki drug drugu. Otmetim, chto v opisanii extern "C"
upominanie S otnositsya k poryadku svyazyvaniya, a ne k yazyku, i chasto
takoe opisanie ispol'zuyut dlya svyazi s Fortranom ili assemblerom.
|ti yazyki v opredelennoj stepeni podchinyayutsya poryadku svyazyvaniya
dlya S.
Utomitel'no dobavlyat' "C" ko mnogim opisaniyam vneshnih, i
est' vozmozhnost' ukazat' takuyu specifikaciyu srazu dlya gruppy
opisanij. Naprimer:
extern "C" {
char* strcpy(char*, const char);
int strcmp(const char*, const char*)
int strlen(const char*)
// ...
}
V takuyu konstrukciyu mozhno vklyuchit' ves' zagolovochnyj fajl S, chtoby
ukazat', chto on podchinyaetsya svyazyvaniyu dlya S++, naprimer:
extern "C" {
#include <string.h>
}
Obychno s pomoshch'yu takogo priema iz standartnogo zagolovochnogo fajla
dlya S poluchayut takoj fajl dlya S++. Vozmozhno inoe reshenie s
pomoshch'yu uslovnoj translyacii:
#ifdef __cplusplus
extern "C" {
#endif
char* strcpy(char*, const char*);
int strcmp(const char*, const char*);
int strlen(const char*);
// ...
#ifdef __cplusplus
}
#endif
Predopredelennoe makroopredelenie __cplusplus nuzhno, chtoby obojti
konstrukciyu extern "C" { ...}, esli zagolovochnyj fajl ispol'zuetsya
dlya S.
Poskol'ku konstrukciya extern "C" { ... } vliyaet tol'ko na
poryadok svyazyvaniya, v nej mozhet soderzhat'sya lyuboe opisanie,
naprimer:
extern "C" {
// proizvol'nye opisaniya
// naprimer:
static int st;
int glob;
}
Nikak ne menyaetsya klass pamyati i oblast' vidimosti
opisyvaemyh ob容ktov, poetomu po-prezhnemu st podchinyaetsya vnutrennemu
svyazyvaniyu, a glob ostaetsya global'noj peremennoj.
Ukazhem eshche raz, chto opisanie extern "C" vliyaet tol'ko na
poryadok svyazyvaniya i ne vliyaet na poryadok vyzova funkcii. V chastnosti,
funkciya, opisannaya kak extern "C", vse ravno podchinyaetsya pravilam
kontrolya tipov i preobrazovaniya fakticheskih parametrov, kotorye v C++
strozhe, chem v S. Naprimer:
extern "C" int f();
int g()
{
return f(1); // oshibka: parametrov byt' ne dolzhno
}
4.5 Kak sozdat' biblioteku
Rasprostraneny takie oboroty (i v etoj knige tozhe): "pomestit'
v biblioteku", "poiskat' v takoj-to biblioteke". CHto oni
oznachayut dlya programm na S++? K sozhaleniyu, otvet zavisit ot
ispol'zuemoj sistemy. V etom razdele govoritsya o tom, kak
sozdat' i ispol'zovat' biblioteku dlya desyatoj versii sistemy YUNIKS.
Drugie sistemy dolzhny predostavlyat' pohozhie vozmozhnosti. Biblioteka
sostoit iz fajlov .o, kotorye poluchayutsya v rezul'tate translyacii
fajlov .c. Obychno sushchestvuet odin ili neskol'ko fajlov .h, v kotoryh
soderzhatsya neobhodimye dlya vyzova fajlov .o opisaniya.
Rassmotrim v kachestve primera, kak dlya chetko ne ogovorennogo mnozhestva
pol'zovatelej mozhno dostatochno udobno opredelit' nekotoroe
mnozhestvo standartnyh matematicheskih funkcij. Zagolovochnyj fajl
mozhet imet' takoj vid:
extern "C" { // standartnye matematicheskie funkcii
// kak pravilo napisany na S
double sqrt(double); // podmnozhestvo <math.h>
double sin(double);
double cos(double);
double exp(double);
double log(double);
// ...
}
Opredeleniya etih funkcij budut nahodit'sya v fajlah sqrt.c, sin.c,
cos.c, exp.c i log.c, sootvetstvenno.
Biblioteku s imenem math.a mozhno sozdat' s pomoshch'yu takih
komand:
$ CC -c sqrt.c sin.c cos.c exp.c log.c
$ ar cr math.a sqrt.o sin.o cos.o exp.o log.o
$ ranlib math.a
Zdes' simvol $ yavlyaetsya priglasheniem sistemy.
Vnachale transliruyutsya ishodnye teksty, i poluchayutsya moduli
s temi zhe imenami. Komanda ar (arhivator) sozdaet arhiv pod imenem
math.a. Nakonec, dlya bystrogo dostupa k funkciyam arhiv indeksiruetsya.
Esli v vashej sisteme net komandy ranlib (vozmozhno ona i ne nuzhna),
to, po krajnej mere, mozhno najti v spravochnom rukovodstve
ssylku na imya ar. CHtoby ispol'zovat' biblioteku v svoej
programme, nado zadat' rezhim translyacii sleduyushchim obrazom:
$ CC myprog.c math.a
Vstaet vopros: chto daet nam biblioteka math.a? Ved' mozhno bylo by
neposredstvenno ispol'zovat' fajly .o, naprimer tak:
$ CC myprog.c sqrt.o sin.o cos.o exp.o log.o
Delo v tom, chto vo mnogih sluchayah trudno pravil'no ukazat', kakie
fajly .o dejstvitel'no nuzhny. V privedennoj vyshe komande
ispol'zovalis' vse iz nih. Esli zhe v myprog vyzyvayutsya tol'ko
sqrt() i cos(), togda, vidimo, dostatochno zadat' takuyu komandu:
$ CC myprog.c sqrt.o cos.o
No eto budet neverno, t.k. funkciya cos() vyzyvaet sin().
Redaktor svyazej, kotoryj vyzyvaetsya komandoj CC dlya obrabotki
fajlov .a (v nashem sluchae dlya fajla math.a), umeet iz mnozhestva
fajlov, obrazuyushchih biblioteku, izvlekat' tol'ko nuzhnye fajly
.o. Inymi slovami, svyazyvanie s bibliotekoj pozvolyaet vklyuchat'
v programmy mnogo opredelenij odnogo imeni (v tom chisle opredeleniya
funkcij i peremennyh, ispol'zuemyh tol'ko vnutrennimi funkciyami,
o kotoryh pol'zovatel' nikogda ne uznaet). V to zhe vremya v
rezul'tiruyushchuyu programmu vojdet tol'ko minimal'no neobhodimoe
chislo opredelenij.
Samyj rasprostranennyj sposob zadaniya v S++ kakih-to dejstvij - eto
vyzov funkcii, kotoraya vypolnyaet takie dejstviya. Opredelenie funkcii
est' opisanie togo, kak ih vypolnit'. Neopisannye funkcii
vyzyvat' nel'zya.
Opisanie funkcii soderzhit ee imya, tip vozvrashchaemogo znacheniya
(esli ono est') i chislo i tipy parametrov, kotorye dolzhny
zadavat'sya pri vyzove funkcii. Naprimer:
extern double sqrt(double);
extern elem* next_elem();
extern char* strcpy(char* to, const char* from);
extern void exit(int);
Semantika peredachi parametrov tozhdestvenna semantike
inicializacii: proveryayutsya tipy fakticheskih parametrov i, esli
nuzhno, proishodyat neyavnye preobrazovaniya tipov. Tak, esli
uchest' privedennye opisaniya, to v sleduyushchem opredelenii:
double sr2 = sqrt(2);
soderzhitsya pravil'nyj vyzov funkcii sqrt() so znacheniem s plavayushchej
tochkoj 2.0. Kontrol' i preobrazovanie tipa fakticheskogo parametra
imeet v S++ ogromnoe znachenie.
V opisanii funkcii mozhno ukazyvat' imena parametrov. |to
oblegchaet chtenie programmy, no translyator eti imena prosto
ignoriruet.
4.6.2 Opredeleniya funkcij
Kazhdaya vyzyvaemaya v programme funkciya dolzhna byt' gde-to v nej
opredelena, prichem tol'ko odin raz. Opredelenie funkcii - eto ee
opisanie, v kotorom soderzhitsya telo funkcii. Naprimer:
extern void swap(int*, int*); // opisanie
void swap(int* p, int* q) // opredelenie
{
int t = *p;
*p = *q;
*q = *t;
}
Ne tak redki sluchai, kogda v opredelenii funkcii ne ispol'zuyutsya
nekotorye parametry:
void search(table* t, const char* key, const char*)
{
// tretij parametr ne ispol'zuetsya
// ...
}
Kak vidno iz etogo primera, parametr ne ispol'zuetsya, esli
ne zadano ego imya. Podobnye funkcii poyavlyayutsya pri uproshchenii
programmy ili esli rasschityvayut na ee dal'nejshee rasshirenie. V
oboih sluchayah rezervirovanie mesta v opredelenii funkcii dlya
neispol'zuemogo parametra garantiruet, chto drugie funkcii,
soderzhashchie vyzov dannoj, ne pridetsya menyat'.
Uzhe govorilos', chto funkciyu mozhno opredelit' kak podstanovku
(inline). Naprimer:
inline fac(int i) { return i<2 ? 1 : n*fac(n-1); }
Specifikaciya inline sluzhit podskazkoj translyatoru, chto vyzov
funkcii fac mozhno realizovat' podstanovkoj ee tela, a ne s pomoshch'yu
obychnogo mehanizma vyzova funkcij ($$R.7.1.2). Horoshij optimiziruyushchij
translyator vmesto generacii vyzova fac(6) mozhet prosto ispol'zovat'
konstantu 720. Iz-za nalichiya vzaimorekursivnyh vyzovov funkcij-podstanovok,
a takzhe funkcij-podstanovok, rekursivnost' kotoryh zavisit ot vhodnyh
dannyh, nel'zya utverzhdat', chto kazhdyj vyzov funkcii-podstanovki
dejstvitel'no realizuetsya podstanovkoj ee tela. Stepen' optimizacii,
provodimoj translyatorom, nel'zya formalizovat', poetomu odni
translyatory sozdadut komandy 6*5*4*3*2*1, drugie - 6*fac(5), a
nekotorye ogranichatsya neoptimizirovannym vyzovom fac(6).
CHtoby realizaciya vyzova podstanovkoj stala vozmozhna dazhe
dlya ne slishkom razvityh sistem programmirovaniya, nuzhno, chtoby ne
tol'ko opredelenie, no i opisanie funkcii-podstanovki nahodilos'
v tekushchej oblasti vidimosti. V ostal'nom specifikaciya inline
ne vliyaet na semantiku vyzova.
4.6.3 Peredacha parametrov
Pri vyzove funkcii vydelyaetsya pamyat' dlya ee formal'nyh parametrov,
i kazhdyj formal'nyj parametr inicializiruetsya znacheniem
sootvetstvuyushchego fakticheskogo parametra. Semantika peredachi
parametrov tozhdestvenna semantike inicializacii. V chastnosti, sveryayutsya
tipy formal'nogo i sootvetstvuyushchego emu fakticheskogo parametra, i
vypolnyayutsya vse standartnye i pol'zovatel'skie preobrazovaniya tipa.
Sushchestvuyut special'nye pravila peredachi massivov ($$4.6.5).
Est' vozmozhnost' peredat' parametr, minuya kontrol' tipa ($$4.6.8),
i vozmozhnost' zadat' standartnoe znachenie parametra ($$4.6.7).
Rassmotrim funkciyu:
void f(int val, int& ref)
{
val++;
ref++;
}
Pri vyzove f() v vyrazhenii val++ uvelichivaetsya lokal'naya kopiya
pervogo fakticheskogo parametra, togda kak v ref++ - sam vtoroj
fakticheskij parametr uvelichivaetsya sam. Poetomu v funkcii
void g()
{
int i = 1;
int j = 1;
f(i,j);
}
uvelichitsya znachenie j, no ne i. Pervyj parametr i peredaetsya po
znacheniyu, a vtoroj parametr j peredaetsya po ssylke. V $$2.3.10
my govorili, chto funkcii, kotorye izmenyayut svoj peredavaemyj
po ssylke parametr, trudnee ponyat', i chto poetomu luchshe ih izbegat'
(sm. takzhe $$10.2.2). No bol'shie ob容kty, ochevidno, gorazdo
effektivnee peredavat' po ssylke, chem po znacheniyu. Pravda mozhno
opisat' parametr so specifikaciej const, chtoby garantirovat', chto
peredacha po ssylke ispol'zuetsya tol'ko dlya effektivnosti, i
vyzyvaemaya funkciya ne mozhet izmenit' znachenie ob容kta:
void f(const large& arg)
{
// znachenie "arg" nel'zya izmenit' bez yavnyh
// operacij preobrazovaniya tipa
}
Esli v opisanii parametra ssylki const ne ukazano, to eto
rassmatrivaetsya kak namerenie izmenyat' peredavaemyj ob容kt:
void g(large& arg); // schitaetsya, chto v g() arg budet menyat'sya
Otsyuda moral': ispol'zujte const vsyudu, gde vozmozhno.
Tochno tak zhe, opisanie parametra, yavlyayushchegosya ukazatelem, so
specifikaciej const govorit o tom, chto ukazuemyj ob容kt ne budet
izmenyat'sya v vyzyvaemoj funkcii. Naprimer:
extern int strlen(const char*); // iz <string.h>
extern char* strcpy(char* to, const char* from);
extern int strcmp(const char*, const char*);
Znachenie takogo priema rastet vmeste s rostom programmy.
Otmetim, chto semantika peredachi parametrov otlichaetsya ot semantiki
prisvaivaniya. |to razlichie sushchestvenno dlya parametrov, yavlyayushchihsya
const ili ssylkoj, a takzhe dlya parametrov s tipom, opredelennym
pol'zovatelem ($1.4.2).
Literal, konstantu i parametr, trebuyushchij preobrazovaniya,
mozhno peredavat' kak parametr tipa const&, no bez specifikacii
const peredavat' nel'zya. Dopuskaya preobrazovaniya dlya parametra tipa
const T&, my garantiruem, chto on mozhet prinimat' znacheniya iz togo zhe
mnozhestva, chto i parametr tipa T, znachenie kotorogo peredaetsya
pri neobhodimosti s pomoshch'yu vremennoj peremennoj.
float fsqrt(const float&); // funkciya sqrt v stile Fortrana
void g(double d)
{
float r;
r = fsqrt(2.0f); // peredacha ssylki na vremennuyu
// peremennuyu, soderzhashchuyu 2.0f
r = fsqrt(r); // peredacha ssylki na r
r = fsqrt(d); // peredacha ssylki na vremennuyu
// peremennuyu, soderzhashchuyu float(d)
}
Zapret na preobrazovaniya tipa dlya parametrov-ssylok bez specifikacii
const vveden dlya togo, chtoby izbezhat' nelepyh oshibok, svyazannyh
s ispol'zovaniem pri peredache parametrov vremennyh peremennyh:
void update(float& i);
void g(double d)
{
float r;
update(2.0f); // oshibka: parametr-konstanta
update(r); // normal'no: peredaetsya ssylka na r
update(d); // oshibka: zdes' nuzhno preobrazovyvat' tip
}
4.6.4 Vozvrashchaemoe znachenie
Esli funkciya ne opisana kak void, ona dolzhna vozvrashchat' znachenie.
Naprimer:
int f() { } // oshibka
void g() { } // normal'no
Vozvrashchaemoe znachenie ukazyvaetsya v operatore return v tele funkcii.
Naprimer:
int fac(int n) { return (n>1) ? n*fac(n-1) : 1; }
V tele funkcii mozhet byt' neskol'ko operatorov return:
int fac(int n)
{
if (n > 1)
return n*fac(n-1);
else
return 1;
}
Podobno peredache parametrov, operaciya vozvrashcheniya znacheniya funkcii
ekvivalentna inicializacii. Schitaetsya, chto operator return
inicializiruet peremennuyu, imeyushchuyu tip vozvrashchaemogo znacheniya.
Tip vyrazheniya v operatore return sveryaetsya s tipom funkcii, i
proizvodyatsya vse standartnye i pol'zovatel'skie preobrazovaniya
tipa. Naprimer:
double f()
{
// ...
return 1; // neyavno preobrazuetsya v double(1)
}
Pri kazhdom vyzove funkcii sozdaetsya novaya kopiya ee formal'nyh
parametrov i avtomaticheskih peremennyh. Zanyataya imi pamyat' posle
vyhoda iz funkcii budet snova ispol'zovat'sya, poetomu nerazumno
vozvrashchat' ukazatel' na lokal'nuyu peremennuyu. Soderzhimoe pamyati,
na kotoruyu nastroen takoj ukazatel', mozhet izmenit'sya nepredskazuemym
obrazom:
int* f()
{
int local = 1;
// ...
return &local; // oshibka
}
|ta oshibka ne stol' tipichna, kak shodnaya oshibka, kogda tip funkcii -
ssylka:
int& f()
{
int local = 1;
// ...
return local; // oshibka
}
K schast'yu, translyator preduprezhdaet o tom, chto vozvrashchaetsya ssylka
na lokal'nuyu peremennuyu. Vot drugoj primer:
int& f() { return 1; } // oshibka
Esli v kachestve parametra funkcii ukazan massiv, to peredaetsya
ukazatel' na ego pervyj element. Naprimer:
int strlen(const char*);
void f()
{
char v[] = "massiv";
strlen(v);
strlen("Nikolaj");
}
|to oznachaet, chto fakticheskij parametr tipa T[] preobrazuetsya k tipu T*,
i zatem peredaetsya. Poetomu prisvaivanie elementu formal'nogo
parametra-massiva izmenyaet etot element. Inymi slovami,
massivy otlichayutsya ot drugih tipov tem, chto oni ne peredayutsya
i ne mogut peredavat'sya po znacheniyu.
V vyzyvaemoj funkcii razmer peredavaemogo massiva neizvesten.
|to nepriyatno, no est' neskol'ko sposobov obojti dannuyu trudnost'.
Prezhde vsego, vse stroki okanchivayutsya nulevym simvolom, i znachit ih
razmer legko vychislit'. Mozhno peredavat' eshche odin parametr,
zadayushchij razmer massiva. Drugoj sposob: opredelit'
strukturu, soderzhashchuyu ukazatel' na massiv i razmer massiva, i
peredavat' ee kak parametr (sm. takzhe $$1.2.5). Naprimer:
void compute1(int* vec_ptr, int vec_size); // 1-yj sposob
struct vec { // 2-oj sposob
int* ptr;
int size;
};
void compute2(vec v);
Slozhnee s mnogomernymi massivami, no chasto vmesto nih mozhno
ispol'zovat' massiv ukazatelej, svedya eti sluchai k odnomernym
massivam. Naprimer:
char* day[] = {
"mon", "tue", "wed", "thu", "fri", "sat", "sun"
};
Teper' rassmotrim funkciyu, rabotayushchuyu s dvumernym massivom - matricej.
Esli razmery oboih indeksov izvestny na etape translyacii, to
problem net:
void print_m34(int m[3][4])
{
for (int i = 0; i<3; i++) {
for (int j = 0; j<4; J++)
cout << ' ' << m[i][j];
cout << '\n';
}
}
Konechno, matrica po-prezhnemu peredaetsya kak ukazatel', a razmernosti
privedeny prosto dlya polnoty opisaniya.
Pervaya razmernost' dlya vychisleniya adresa elementa nevazhna
($$R.8.2.4), poetomu ee mozhno peredavat' kak parametr:
void print_mi4(int m[][4], int dim1)
{
for ( int i = 0; i<dim1; i++) {
for ( int j = 0; j<4; j++)
cout << ' ' << m[i][j];
cout << '\n';
}
}
Samyj slozhnyj sluchaj - kogda nado peredavat' obe razmernosti.
Zdes' "ochevidnoe" reshenie prosto neprigodno:
void print_mij(int m[][], int dim1, int dim2) // oshibka
{
for ( int i = 0; i<dim1; i++) {
for ( int j = 0; j<dim2; j++)
cout << ' ' << m[i][j];
cout << '\n';
}
}
Vo-pervyh, opisanie parametra m[][] nedopustimo, poskol'ku dlya
vychisleniya adresa elementa mnogomernogo massiva nuzhno znat'
vtoruyu razmernost'. Vo-vtoryh, vyrazhenie m[i][j]
vychislyaetsya kak *(*(m+i)+j), a eto, po vsej vidimosti, ne to, chto
imel v vidu programmist. Privedem pravil'noe reshenie:
void print_mij(int** m, int dim1, int dim2)
{
for (int i = 0; i< dim1; i++) {
for (int j = 0; j<dim2; j++)
cout << ' ' << ((int*)m)[i*dim2+j]; // zaputano
cout << '\n';
}
}
Vyrazhenie, ispol'zuemoe dlya vybora elementa matricy, ekvivalentno
tomu, kotoroe sozdaet dlya etoj zhe celi translyator, kogda izvestna
poslednyaya razmernost'. Mozhno vvesti dopolnitel'nuyu peremennuyu,
chtoby eto vyrazhenie stalo ponyatnee:
int* v = (int*)m;
// ...
v[i*dim2+j]
Luchshe takie dostatochno zaputannye mesta v programme upryatyvat'.
Mozhno opredelit' tip mnogomernogo massiva s sootvetstvuyushchej
operaciej indeksirovaniya. Togda pol'zovatel' mozhet i ne znat', kak
razmeshchayutsya dannye v massive (sm. uprazhnenie 18 v $$7.13).
4.6.6 Peregruzka imeni funkcii
Obychno imeet smysl davat' raznym funkciyam raznye imena. Esli zhe
neskol'ko funkcij vypolnyaet odno i to zhe dejstvie nad ob容ktami
raznyh tipov, to udobnee dat' odinakovye imena vsem etim funkciyam.
Peregruzkoj imeni nazyvaetsya ego ispol'zovanie dlya oboznacheniya
raznyh operacij nad raznymi tipami. Sobstvenno uzhe dlya osnovnyh
operacij S++ primenyaetsya peregruzka. Dejstvitel'no: dlya operacij
slozheniya est' tol'ko odno imya +, no ono ispol'zuetsya dlya slozheniya
i celyh chisel, i chisel s plavayushchej tochkoj, i ukazatelej. Takoj
podhod legko mozhno rasprostranit' na operacii, opredelennye
pol'zovatelem, t.e. na funkcii. Naprimer:
void print(int); // pechat' celogo
void print(const char*) // pechat' stroki simvolov
Dlya translyatora v takih peregruzhennyh funkciyah obshchee tol'ko
odno - imya. Ochevidno, po smyslu takie funkcii shodny, no yazyk
ne sposobstvuet i ne prepyatstvuet vydeleniyu peregruzhennyh funkcij.
Takim obrazom, opredelenie peregruzhennyh funkcij sluzhit, prezhde
vsego, dlya udobstva zapisi. No dlya funkcij s takimi tradicionnymi
imenami, kak sqrt, print ili open, nel'zya etim udobstvom prenebregat'.
Esli samo imya igraet vazhnuyu semanticheskuyu rol', naprimer,
v takih operaciyah, kak + , * i << ($$7.2), ili dlya konstruktora
klassa ($$5.2.4 i $$7.3.1), to takoe udobstvo stanovitsya sushchestvennym
faktorom. Pri vyzove funkcii s imenem f translyator dolzhen
razobrat'sya, kakuyu imenno funkciyu f sleduet vyzyvat'. Dlya etogo
sravnivayutsya tipy fakticheskih parametrov, ukazannye v vyzove, s tipami
formal'nyh parametrov vseh opisanij funkcij s imenem f. V rezul'tate
vyzyvaetsya ta funkciya, u kotoroj formal'nye parametry nailuchshim
obrazom sopostavilis' s parametrami vyzova, ili vydaetsya oshibka
esli takoj funkcii ne nashlos'. Naprimer:
void print(double);
void print(long);
void f()
{
print(1L); // print(long)
print(1.0); // print(double)
print(1); // oshibka, neodnoznachnost': chto vyzyvat'
// print(long(1)) ili print(double(1)) ?
}
Podrobno pravila sopostavleniya parametrov opisany v $$R.13.2. Zdes'
dostatochno privesti ih sut'. Pravila primenyayutsya v sleduyushchem
poryadke po ubyvaniyu ih prioriteta:
[1] Tochnoe sopostavlenie: sopostavlenie proizoshlo bez vsyakih
preobrazovanij tipa ili tol'ko s neizbezhnymi preobrazovaniyami
(naprimer, imeni massiva v ukazatel', imeni funkcii v ukazatel'
na funkciyu i tipa T v const T).
[2] Sopostavlenie s ispol'zovaniem standartnyh celochislennyh
preobrazovanij, opredelennyh v $$R.4.1 (t.e. char v int,
short v int i ih bezznakovyh dvojnikov v int), a takzhe
preobrazovanij float v double.
[3] Sopostavlenie s ispol'zovaniem standartnyh preobrazovanij,
opredelennyh v $$R.4 (naprimer, int v double, derived* v
base*, unsigned v int).
[4] Sopostavlenie s ispol'zovaniem pol'zovatel'skih preobrazovanij
($$R.12.3).
[5] Sopostavlenie s ispol'zovaniem ellipsisa ... v opisanii
funkcii.
Esli najdeny dva sopostavleniya po samomu prioritetnomu pravilu,
to vyzov schitaetsya neodnoznachnym, a znachit oshibochnym. |ti pravila
sopostavleniya parametrov rabotayut s uchetom pravil preobrazovanij
chislovyh tipov dlya S i S++. Pust' imeyutsya takie opisaniya funkcii
print:
void print(int);
void print(const char*);
void print(double);
void print(long);
void print(char);
Togda rezul'taty sleduyushchih vyzovov print() budut takimi:
void h(char c, int i, short s, float f)
{
print(c); // tochnoe sopostavlenie: vyzyvaetsya print(char)
print(i); // tochnoe sopostavlenie: vyzyvaetsya print(int)
print(s); // standartnoe celochislennoe preobrazovanie:
// vyzyvaetsya print(int)
print(f); // standartnoe preobrazovanie:
// vyzyvaetsya print(double)
print('a'); // tochnoe sopostavlenie: vyzyvaetsya print(char)
print(49); // tochnoe sopostavlenie: vyzyvaetsya print(int)
print(0); // tochnoe sopostavlenie: vyzyvaetsya print(int)
print("a"); // tochnoe sopostavlenie:
// vyzyvaetsya print(const char*)
}
Obrashchenie print(0) privodit k vyzovu print(int), ved' 0 imeet tip int.
Obrashchenie print('a') privodit k vyzovu print(char), t.k. 'a' - tipa
char ($$R.2.5.2).
Otmetim, chto na razreshenie neopredelennosti pri peregruzke ne
vliyaet poryadok opisanij rassmatrivaemyh funkcij, a tipy vozvrashchaemyh
funkciyami znachenij voobshche ne uchityvayutsya.
Ishodya iz etih pravil mozhno garantirovat', chto esli effektivnost'
ili tochnost' vychislenij znachitel'no razlichayutsya dlya
rassmatrivaemyh tipov, to vyzyvaetsya funkciya, realizuyushchaya samyj
prostoj algoritm. Naprimer:
int pow(int, int);
double pow(double, double); // iz <math.h>
complex pow(double, complex); // iz <complex.h>
complex pow(complex, int);
complex pow(complex, double);
complex pow(complex, complex);
void k(complex z)
{
int i = pow(2,2); // vyzyvaetsya pow(int,int)
double d = pow(2.0,2); // vyzyvaetsya pow(double,double)
complex z2 = pow(2,z); // vyzyvaetsya pow(double,complex)
complex z3 = pow(z,2); // vyzyvaetsya pow(complex,int)
complex z4 = pow(z,z); // vyzyvaetsya pow(complex,complex)
}
4.6.7 Standartnye znacheniya parametrov
V obshchem sluchae u funkcii mozhet byt' bol'she parametrov, chem v samyh
prostyh i naibolee chasto ispol'zuemyh sluchayah. V chastnosti, eto
svojstvenno funkciyam, stroyashchim ob容kty (naprimer, konstruktoram,
sm. $$5.2.4). Dlya bolee gibkogo ispol'zovaniya etih funkcij inogda
primenyayutsya neobyazatel'nye parametry. Rassmotrim v kachestve primera
funkciyu pechati celogo chisla. Vpolne razumno primenit' v kachestve
neobyazatel'nogo parametra osnovanie schisleniya pechataemogo chisla,
hotya v bol'shinstve sluchaev chisla budut pechatat'sya kak desyatichnye
celye znacheniya. Sleduyushchaya funkciya
void print (int value, int base =10);
void F()
{
print(31);
print(31,10);
print(31,16);
print(31,2);
}
napechataet takie chisla:
31 31 1f 11111
Vmesto standartnogo znacheniya parametra mozhno bylo by ispol'zovat'
peregruzku funkcii print:
void print(int value, int base);
inline void print(int value) { print(value,10); }
Odnako v poslednem variante tekst programmy ne stol' yavno demonstriruet
zhelanie imet' odnu funkciyu print, no pri etom obespechit' udobnuyu i
kratkuyu formu zapisi.
Tip standartnogo parametra sveryaetsya s tipom ukazannogo znacheniya
pri translyacii opisaniya funkcii, a znachenie etogo parametra vychislyaetsya
v moment vyzova funkcii. Zadavat' standartnoe znachenie mozhno tol'ko
dlya zavershayushchih podryad idushchih parametrov:
int f(int, int =0, char* =0); // normal'no
int g(int =0, int =0, char*); // oshibka
int h(int =0, int, char* =0); // oshibka
Otmetim, chto v dannom kontekste nalichie probela mezhdu simvolami * i =
ves'ma sushchestvenno, poskol'ku *= yavlyaetsya operaciej prisvaivaniya:
int nasty(char*=0); // sintaksicheskaya oshibka
4.6.8 Neopredelennoe chislo parametrov
Sushchestvuyut funkcii, v opisanii kotoryh nevozmozhno ukazat' chislo
i tipy vseh dopustimyh parametrov. Togda spisok formal'nyh
parametrov zavershaetsya ellipsisom (...), chto oznachaet:
"i, vozmozhno, eshche neskol'ko argumentov". Naprimer:
int printf(const char* ...);
Pri vyzove printf obyazatel'no dolzhen byt' ukazan parametr
tipa char*, odnako mogut byt' (a mogut i ne byt') eshche drugie
parametry. Naprimer:
printf("Hello, world\n");
printf("My name is %s %s\n", first_name, second_name);
printf("%d + %d = %d\n", 2,3,5);
Takie funkcii pol'zuyutsya dlya raspoznavaniya svoih fakticheskih
parametrov nedostupnoj translyatoru informaciej. V sluchae funkcii
printf pervyj parametr yavlyaetsya strokoj, specificiruyushchej format vyvoda.
Ona mozhet soderzhat' special'nye simvoly, kotorye pozvolyayut pravil'no
vosprinyat' posleduyushchie parametry. Naprimer, %s oznachaet -"budet
fakticheskij parametr tipa char*", %d oznachaet -"budet fakticheskij
parametr tipa int" (sm. $$10.6). No translyator etogo ne znaet, i
poetomu on ne mozhet ubedit'sya, chto ob座avlennye parametry dejstvitel'no
prisutstvuyut v vyzove i imeyut sootvetstvuyushchie tipy. Naprimer,
sleduyushchij vyzov
printf("My name is %s %s\n",2);
normal'no transliruetsya, no privedet (v luchshem sluchae) k neozhidannoj
vydache. Mozhete proverit' sami.
Ochevidno, chto raz parametr neopisan, to translyator ne imeet svedenij
dlya kontrolya i standartnyh preobrazovanij tipa etogo parametra.
Poetomu char ili short peredayutsya kak int, a float kak double, hotya
pol'zovatel', vozmozhno, imel v vidu drugoe.
V horosho produmannoj programme mozhet potrebovat'sya, v vide
isklyucheniya, lish' neskol'ko funkcij, v kotoryh ukazany ne vse tipy
parametrov. CHtoby obojti kontrol' tipov parametrov, luchshe ispol'zovat'
peregruzku funkcij ili standartnye znacheniya parametrov, chem
parametry, tipy kotoryh ne byli opisany. |llipsis stanovitsya
neobhodimym tol'ko togda, kogda mogut menyat'sya ne tol'ko tipy, no
i chislo parametrov. CHashche vsego ellipsis ispol'zuetsya
dlya opredeleniya interfejsa s bibliotekoj standartnyh funkcij na S,
esli etim funkciyam net zameny:
extern "C" int fprintf(FILE*, const char* ...);
extern "C" int execl(const char* ...);
Est' standartnyj nabor makroopredelenij, nahodyashchijsya v <stdarg.h>,
dlya vybora nezadannyh parametrov etih funkcij. Rassmotrim funkciyu
reakcii na oshibku, pervyj parametr kotoroj pokazyvaet stepen' tyazhesti
oshibki. Za nim mozhet sledovat' proizvol'noe chislo strok. Nuzhno
sostavit' soobshchenie ob oshibke s uchetom, chto kazhdoe slovo iz nego
peredaetsya kak otdel'naya stroka:
extern void error(int ...)
extern char* itoa(int);
main(int argc, char* argv[])
{
switch (argc) {
case 1:
error(0,argv[0],(char*)0);
break;
case 2:
error(0,argv[0],argv[1],(char*)0);
break;
default:
error(1,argv[0],
"With",itoa(argc-1),"arguments",(char*)0);
}
// ...
}
Funkciya itoa vozvrashchaet stroku simvolov, predstavlyayushchuyu ee celyj
parametr. Funkciyu reakcii na oshibku mozhno opredelit' tak:
#include <stdarg.h>
void error(int severity ...)
/*
za "severity" (stepen' tyazhesti oshibki) sleduet
spisok strok, zavershayushchijsya nulem
*/
{
va_list ap;
va_start(ap,severity); // nachalo parametrov
for (;;) {
char* p = va_arg(ap,char*);
if (p == 0) break;
cerr << p << ' ';
}
va_end(ap); // ochistka parametrov
cerr << '\n';
if (severity) exit(severity);
}
Vnachale pri vyzove va_start() opredelyaetsya i inicializiruetsya
va_list. Parametrami makroopredeleniya va_start yavlyayutsya imya tipa
va_list i poslednij formal'nyj parametr. Dlya vyborki po poryadku
neopisannyh parametrov ispol'zuetsya makroopredelenie va_arg().
V kazhdom obrashchenii k va_arg nuzhno zadavat' tip ozhidaemogo fakticheskogo
parametra. V va_arg() predpolagaetsya, chto parametr takogo tipa
prisutstvuet v vyzove, no obychno net vozmozhnosti proverit' eto.
Pered vyhodom iz funkcii, v kotoroj bylo obrashchenie k va_start,
neobhodimo vyzvat' va_end. Prichina v tom, chto v va_start()
mogut byt' takie operacii so stekom, iz-za kotoryh korrektnyj vozvrat
iz funkcii stanovitsya nevozmozhnym. V va_end() ustranyayutsya vse
nezhelatel'nye izmeneniya steka.
Privedenie 0 k (char*)0 neobhodimo potomu, chto sizeof(int)
ne obyazano sovpadat' s sizeof(char*). |tot primer demonstriruet
vse te slozhnosti, s kotorymi prihoditsya stalkivat'sya
programmistu, esli on reshil obojti kontrol' tipov, ispol'zuya
ellipsis.
4.6.9 Ukazatel' na funkciyu
Vozmozhny tol'ko dve operacii s funkciyami: vyzov i vzyatie adresa.
Ukazatel', poluchennyj s pomoshch'yu poslednej operacii, mozhno
vposledstvii ispol'zovat' dlya vyzova funkcii. Naprimer:
void error(char* p) { /* ... */ }
void (*efct)(char*); // ukazatel' na funkciyu
void f()
{
efct = &error; // efct nastroen na funkciyu error
(*efct)("error"); // vyzov error cherez ukazatel' efct
}
Dlya vyzova funkcii s pomoshch'yu ukazatelya (efct v nashem primere)
nado vnachale primenit' operaciyu kosvennosti k ukazatelyu - *efct.
Poskol'ku prioritet operacii vyzova () vyshe, chem prioritet
kosvennosti *, nel'zya pisat' prosto *efct("error"). |to budet
oznachat' *(efct("error")), chto yavlyaetsya oshibkoj. Po toj zhe
prichine skobki nuzhny i pri opisanii ukazatelya na funkciyu. Odnako,
pisat' prosto efct("error") mozhno, t.k. translyator ponimaet, chto
efct yavlyaetsya ukazatelem na funkciyu, i sozdaet komandy, delayushchie
vyzov nuzhnoj funkcii.
Otmetim, chto formal'nye parametry v ukazatelyah na funkciyu opisyvayutsya
tak zhe, kak i v obychnyh funkciyah. Pri prisvaivanii ukazatelyu na funkciyu
trebuetsya tochnoe sootvetstvie tipa funkcii i tipa prisvaivaemogo
znacheniya. Naprimer:
void (*pf)(char*); // ukazatel' na void(char*)
void f1(char*); // void(char*);
int f2(char*); // int(char*);
void f3(int*); // void(int*);
void f()
{
pf = &f1; // normal'no
pf = &f2; // oshibka: ne tot tip vozvrashchaemogo
// znacheniya
pf = &f3; // oshibka: ne tot tip parametra
(*pf)("asdf"); // normal'no
(*pf)(1); // oshibka: ne tot tip parametra
int i = (*pf)("qwer"); // oshibka: void prisvaivaetsya int
}
Pravila peredachi parametrov odinakovy i dlya obychnogo vyzova,
i dlya vyzova s pomoshch'yu ukazatelya.
CHasto byvaet udobnee oboznachit' tip ukazatelya na funkciyu imenem,
chem vse vremya ispol'zovat' dostatochno slozhnuyu zapis'. Naprimer:
typedef int (*SIG_TYP)(int); // iz <signal.h>
typedef void (SIG_ARG_TYP)(int);
SIG_TYP signal(int, SIG_ARG_TYP);
Takzhe chasto byvaet polezen massiv ukazatelej na funkcii. Naprimer,
mozhno realizovat' sistemu menyu dlya redaktora s vvodom, upravlyaemym
mysh'yu, ispol'zuya massiv ukazatelej na funkcii, realizuyushchie komandy.
Zdes' net vozmozhnosti podrobno opisat' takoj redaktor, no dadim samyj
obshchij ego nabrosok:
typedef void (*PF)();
PF edit_ops[] = { // komandy redaktora
&cut, &paste, &snarf, &search
};
PF file_ops[] = { // upravlenie fajlom
&open, &reshape, &close, &write
};
Dalee nado opredelit' i inicializirovat' ukazateli, s pomoshch'yu kotoryh
budut zapuskat'sya funkcii, realizuyushchie vybrannye iz menyu komandy.
Vybor proishodit nazhatiem klavishi myshi:
PF* button2 = edit_ops;
PF* button3 = file_ops;
Dlya nastoyashchej programmy redaktora nado opredelit' bol'shee chislo
ob容ktov, chtoby opisat' kazhduyu poziciyu v menyu. Naprimer, neobhodimo
gde-to hranit' stroku, zadayushchuyu tekst, kotoryj budet vydavat'sya dlya
kazhdoj pozicii. Pri rabote s sistemoj menyu naznachenie klavish myshi
budet postoyanno menyat'sya. CHastichno eti izmeneniya mozhno predstavit'
kak izmeneniya znachenij ukazatelya, svyazannogo s dannoj klavishej. Esli
pol'zovatel' vybral poziciyu menyu, kotoraya opredelyaetsya, naprimer,
kak poziciya 3 dlya klavishi 2, to sootvetstvuyushchaya komanda realizuetsya
vyzovom:
(*button2[3])();
CHtoby polnost'yu ocenit' moshchnost' konstrukcii ukazatel' na funkciyu,
stoit popytat'sya napisat' programmu bez nee. Menyu mozhno izmenyat'
v dinamike, esli dobavlyat' novye funkcii v tablicu komand.
Dovol'no prosto sozdavat' v dinamike i novye menyu.
Ukazateli na funkcii pomogayut realizovat' polimorficheskie
podprogrammy, t.e. takie podprogrammy, kotorye mozhno primenyat'
k ob容ktam razlichnyh tipov:
typedef int (*CFT)(void*,void*);
void sort(void* base, unsigned n, unsigned int sz, CFT cmp)
/*
Sortirovka vektora "base" iz n elementov
v vozrastayushchem poryadke;
ispol'zuetsya funkciya sravneniya, na kotoruyu ukazyvaet cmp.
Razmer elementov raven "sz".
Algoritm ochen' neeffektivnyj: sortirovka puzyr'kovym metodom
*/
{
for (int i=0; i<n-1; i++)
for (int j=n-1; i<j; j--) {
char* pj = (char*)base+j*sz; // b[j]
char* pj1 = pj - sz; // b[j-1]
if ((*cmp)(pj,pj1) < 0) {
// pomenyat' mestami b[j] i b[j-1]
for (int k = 0; k<sz; k++) {
char temp = pj[k];
pj[k] = pj1[k];
pj1[k] = temp;
}
}
}
}
V podprogramme sort neizvesten tip sortiruemyh ob容ktov; izvestno
tol'ko ih chislo (razmer massiva), razmer kazhdogo elementa i funkciya,
kotoraya mozhet sravnivat' ob容kty. My vybrali dlya funkcii sort()
takoj zhe zagolovok, kak u qsort() - standartnoj funkcii sortirovki
iz biblioteki S. |tu funkciyu ispol'zuyut nastoyashchie programmy.
Pokazhem, kak s pomoshch'yu sort() mozhno otsortirovat' tablicu s takoj
strukturoj:
struct user {
char* name; // imya
char* id; // parol'
int dept; // otdel
};
typedef user* Puser;
user heads[] = {
"Ritchie D.M.", "dmr", 11271,
"Sethi R.", "ravi", 11272,
"SZYmanski T.G.", "tgs", 11273,
"Schryer N.L.", "nls", 11274,
"Schryer N.L.", "nls", 11275
"Kernighan B.W.", "bwk", 11276
};
void print_id(Puser v, int n)
{
for (int i=0; i<n; i++)
cout << v[i].name << '\t'
<< v[i].id << '\t'
<< v[i].dept << '\n';
}
CHtoby imet' vozmozhnost' sortirovat', nuzhno vnachale opredelit'
podhodyashchie funkcii sravneniya. Funkciya sravneniya dolzhna vozvrashchat'
otricatel'noe chislo, esli ee pervyj parametr men'she vtorogo,
nul', esli oni ravny, i polozhitel'noe chislo v protivnom sluchae:
int cmp1(const void* p, const void* q)
// sravnenie strok, soderzhashchih imena
{
return strcmp(Puser(p)->name, Puser(q)->name);
}
int cmp2(const void* p, const void* q)
// sravnenie nomerov razdelov
{
return Puser(p)->dept - Puser(q)->dept;
}
Sleduyushchaya programma sortiruet i pechataet rezul'tat:
int main()
{
sort(heads,6,sizeof(user), cmp1);
print_id(heads,6); // v alfavitnom poryadke
cout << "\n";
sort(heads,6,sizeof(user),cmp2);
print_id(heads,6); // po nomeram otdelov
}
Dopustima operaciya vzyatiya adresa i dlya funkcii-podstanovki, i dlya
peregruzhennoj funkcii ($$R.13.3).
Otmetim, chto neyavnoe preobrazovanie ukazatelya na chto-to v
ukazatel' tipa void* ne vypolnyaetsya dlya parametra funkcii, vyzyvaemoj
cherez ukazatel' na nee. Poetomu funkciyu
int cmp3(const mytype*, const mytype*);
nel'zya ispol'zovat' v kachestve parametra dlya sort().
Postupiv inache, my narushaem zadannoe v opisanii uslovie, chto
cmp3() dolzhna vyzyvat'sya s parametrami tipa mytype*. Esli vy
special'no hotite narushit' eto uslovie, to dolzhny ispol'zovat'
yavnoe preobrazovanie tipa.
Makrosredstva yazyka opredelyayutsya v $$R.16. V S++ oni igrayut gorazdo
men'shuyu rol', chem v S. Mozhno dazhe dat' takoj sovet: ispol'zujte
makroopredeleniya tol'ko togda, kogda ne mozhete bez nih obojtis'.
Voobshche govorya, schitaetsya, chto prakticheski kazhdoe poyavlenie
makroimeni yavlyaetsya svidetel'stvom nekotoryh nedostatkov
yazyka, programmy ili programmista. Makrosredstva sozdayut opredelennye
trudnosti dlya raboty sluzhebnyh sistemnyh programm, poskol'ku
oni pererabatyvayut programmnyj tekst eshche do translyacii. Poetomu, esli
vasha programma ispol'zuet makrosredstva,
to servis, predostavlyaemyj takimi programmami, kak otladchik,
profilirovshchik, programma perekrestnyh ssylok, budet dlya nee
nepolnym. Esli vse-taki vy reshite ispol'zovat'
makrokomandy, to vnachale tshchatel'no izuchite opisanie preprocessora
S++ v vashem spravochnom rukovodstve i ne starajtes' byt' slishkom umnym.
Prostoe makroopredelenie imeet vid:
#define imya ostatok-stroki
V tekste programmy leksema imya zamenyaetsya na ostatok-stroki. Naprimer,
ob容kt = imya
budet zameneno na
ob容kt = ostatok-stroki
Makroopredelenie mozhet imet' parametry. Naprimer:
#define mac(a,b) argument1: a argument2: b
V makrovyzove mac dolzhny byt' zadany dve stroki, predstavlyayushchie
parametry. Pri podstanovke oni zamenyat a i b v makroopredelenii
mac(). Poetomu stroka
expanded = mac(foo bar, yuk yuk)
pri podstanovke preobrazuetsya v
expanded = argument1: foo bar argument2: yuk yuk
Makroimena nel'zya peregruzhat'. Rekursivnye makrovyzovy stavyat
pered preprocessorom slishkom slozhnuyu zadachu:
// oshibka:
#define print(a,b) cout<<(a)<<(b)
#define print(a,b,c) cout<<(a)<<(b)<<(c)
// slishkom slozhno:
#define fac(n) (n>1) ?n*fac(n-1) :1
Preprocessor rabotaet so strokami i prakticheski nichego ne znaet o
sintaksise C++, tipah yazyka i oblastyah vidimosti. Translyator
imeet delo tol'ko s uzhe raskrytym makroopredeleniem, poetomu
oshibka v nem mozhet diagnostirovat'sya uzhe posle podstanovki, a ne pri
opredelenii makroimeni. V rezul'tate poyavlyayutsya dovol'no putannye
soobshcheniya ob oshibkah.
Dopustimy takie makroopredeleniya:
#define Case break;case
#define forever for(;;)
A vot sovershenno izlishnie makroopredeleniya:
#define PI 3.141593
#define BEGIN {
#define END }
Sleduyushchie makroopredeleniya mogut privesti k oshibkam:
#define SQUARE(a) a*a
#define INCR_xx (xx)++
#define DISP = 4
CHtoby ubedit'sya v etom, dostatochno poprobovat' sdelat' podstanovku
v takom primere:
int xx = 0; // global'nyj schetchik
void f() {
int xx = 0; // lokal'naya peremennaya
xx = SQUARE(xx+2); // xx = xx +2*xx+2;
INCR_xx; // uvelichivaetsya lokal'naya peremennaya xx
if (a-DISP==b) { // a-=4==b
// ...
}
}
Pri ssylke na global'nye imena v makroopredelenii ispol'zujte operaciyu
razresheniya oblasti vidimosti ($$2.1.1), i vsyudu, gde eto vozmozhno,
zaklyuchajte imya parametra makroopredeleniya v skobki. Naprimer:
#define MIN(a,b) (((a)<(b))?(a):(b))
Esli makroopredelenie dostatochno slozhnoe, i trebuetsya kommentarij
k nemu, to razumnee napisat' kommentarij vida /* */, poskol'ku
v realizacii S++ mozhet ispol'zovat'sya preprocessor S, kotoryj ne
raspoznaet kommentarii vida //. Naprimer:
#define m2(a) something(a) /* glubokomyslennyj kommentarij */
S pomoshch'yu makrosredstv mozhno sozdat' svoj sobstvennyj yazyk,
pravda, skoree vsego, on budet neponyaten drugim. Krome togo, preprocessor
S predostavlyaet dovol'no slabye makrosredstva. Esli vasha zadacha
netrivial'na, vy, skoree vsego, obnaruzhite, chto reshit' ee s pomoshch'yu etih
sredstv libo nevozmozhno, libo chrezvychajno trudno. V kachestve
al'ternativy tradicionnomu ispol'zovaniyu makrosredstv v yazyk vvedeny
konstrukcii const, inline i shablony tipov. Naprimer:
const int answer = 42;
template<class T>
inline T min(T a, T b) { return (a<b)?a:b; }
1. (*1) Sostav'te sleduyushchie opisaniya: funkciya s parametrami tipa
ukazatel' na simvol i ssylka na celoe, nevozvrashchayushchaya znacheniya;
ukazatel' na takuyu funkciyu; funkciya s parametrom, imeyushchim tip
takogo ukazatelya; funkciya, vozvrashchayushchaya takoj ukazatel'. Napishite
opredelenie funkcii, u kotoroj parametr i vozvrashchaemoe znachenie
imeyut tip takogo ukazatelya. Podskazka: ispol'zujte typedef.
2. (*1) Kak ponimat' sleduyushchee opisanie? Gde ono mozhet prigodit'sya?
typedef int (rifii&) (int, int);
3. (*1.5) Napishite programmu, podobnuyu toj, chto vydaet "Hello, world".
Ona poluchaet imya (name) kak parametr komandnoj stroki i vydaet
"Hello, name". Izmenite programmu tak, chtoby ona poluchala
proizvol'noe chislo imen i vsem im vydavala svoe privetstvie:
"Hello, ...".
4. (1.5) Napishite programmu, kotoraya, berya iz komandnoj stroki
proizvol'noe chislo imen fajlov, vse eti fajly perepisyvaet
odin za drugim v cout. Poskol'ku v programme proishodit
konkatenaciya fajlov, vy mozhete nazvat' ee cat ot slova
concatenation - konkatenaciya).
5. (*2) Perevedite nebol'shuyu programmu s yazyka S na S++. Izmenite
zagolovochnye fajly tak, chtoby oni soderzhali opisanie vseh
vyzyvaemyh funkcij i opisanie tipov vseh parametrov. Po vozmozhnosti
vse komandy #define zamenite konstrukciyami enum, const ili
inline. Udalite iz fajlov .c vse opisaniya vneshnih, a opredeleniya
funkcij privedite k vidu, sootvetstvuyushchemu S++. Vyzovy malloc() i
free() zamenite operaciyami new i delete. Udalite nenuzhnye operacii
privedeniya.
6. (*2) Napishite funkciyu sort() ($$4.6.9), ispol'zuyushchuyu bolee
effektivnyj algoritm sortirovki.
7. (*2) Posmotrite na opredelenie struktury tnode v $$R.9.3. Napishite
funkciyu, zanosyashchuyu novye slova v derevo uzlov tnode. Napishite
funkciyu dlya vyvoda uzlov dereva tnode. Napishite funkciyu,
kotoraya proizvodit takoj vyvod v alfavitnom poryadke.
Izmenite strukturu tnode tak, chtoby v nej soderzhalsya
tol'ko ukazatel' na slovo proizvol'noj dliny, kotoroe razmeshchaetsya
s pomoshch'yu new v svobodnoj pamyati. Izmenite funkciyu tak, chtoby
ona rabotala s novoj strukturoj tnode.
8. (*1) Napishite funkciyu itoa(), kotoraya ispol'zovalas' v primere
iz $$4.6.8.
9. (*2) Uznajte, kakie standartnye zagolovochnye fajly est' v vashej
sisteme. Porojtes' v katalogah /usr/include ili /usr/include/CC
(ili v teh katalogah, gde hranyatsya standartnye zagolovochnye
fajly vashej sistemy). Prochitajte lyuboj pokazavshijsya interesnym
fajl.
10. (*2) Napishite funkciyu, kotoraya budet perevorachivat' dvumernyj
massiv. (Pervyj element massiva stanet poslednim).
11. (*2) Napishite shifruyushchuyu programmu, kotoraya chitaet simvoly iz
cin i pishet ih v cout v zashifrovannom vide. Mozhno ispol'zovat'
sleduyushchij prostoj metod shifracii: dlya simvola s zashifrovannoe
predstavlenie poluchaetsya v rezul'tate operacii s^key[i], gde
key - massiv simvolov, peredavaemyj v komandnoj stroke. Simvoly
iz massiva key ispol'zuyutsya v ciklicheskom poryadke, poka ne budet
prochitan ves' vhodnoj potok. Pervonachal'nyj tekst poluchaetsya
povtornym primeneniem toj zhe operacii s temi zhe elementami key.
Esli massiv key ne zadan (ili zadana pustaya stroka), shifraciya ne
proishodit.
12. (*3) Napishite programmu, kotoraya pomogaet deshifrirovat' tekst,
zashifrovannyj opisannym vyshe sposobom, kogda klyuch (t.e. massiv
key) neizvesten. Podskazka: sm. D Kahn "The Codebreakers",
Macmillan, 1967, New York, str. 207-213.
13. (*3) Napishite funkciyu obrabotki oshibok, pervyj parametr kotoryj
podoben formatiruyushchej stroke-parametru printf() i soderzhit formaty
%s, %c i %d. Za nim mozhet sledovat' proizvol'noe kolichestvo
chislovyh parametrov. Funkciyu printf() ne ispol'zujte. Esli smysl
formata %s i drugih formatov vam neizvesten, obratites' k $$10.6.
Ispol'zujte <stdarg.h>.
14. (*1) Kakoe imya vy vybrali by dlya tipov ukazatelej na funkcii,
kotorye opredelyayutsya s pomoshch'yu typedef?
15. (*2) Issledujte raznye programmy, chtoby poluchit' predstavlenie
o raznyh ispol'zuemyh na praktike stilyah imenovaniya. Kak
ispol'zuyutsya zaglavnye bukvy? Kak ispol'zuetsya podcherk? V kakih
sluchayah ispol'zuyutsya takie imena, kak i ili x?
16. (*1) Kakie oshibki soderzhatsya v sleduyushchih makroopredeleniyah?
#define PI = 3.141593;
#define MAX(a,b) a>b?a:b
#define fac(a) (a)*fac((a)-1)
17. (*3) Napishite makroprocessor s prostymi vozmozhnostyami, kak u
preprocessora S. Tekst chitajte iz cin, a rezul'tat zapisyvajte
v cout. Vnachale realizujte makroopredeleniya bez parametrov.
Podskazka: v programme kal'kulyatora est' tablica imen i
sintaksicheskij analizator, kotorymi mozhno vospol'zovat'sya.
18. (*2) Napishite programmu, izvlekayushchuyu kvadratnyj koren' iz dvuh (2)
s pomoshch'yu standartnoj funkcii sqrt(), no ne vklyuchajte v programmu
<math.h>. Sdelajte eto uprazhnenie s pomoshch'yu funkcii sqrt()
na Fortrane.
19. (*2) Realizujte funkciyu print() iz $$4.6.7.
"|ti tipy ne abstraktnye, oni stol' zhe real'ny,
kak int i float"
- Dag Makilroj
V etoj glave opisyvayutsya vozmozhnosti opredeleniya novyh tipov,
dlya kotoryh dostup k dannym ogranichen zadannym mnozhestvom
funkcij, osushchestvlyayushchih ego. Ob座asnyaetsya, kak mozhno ispol'zovat'
chleny struktury dannyh, kak ee zashchishchat', inicializirovat' i,
nakonec, unichtozhat'. V primerah privedeny prostye klassy dlya
upravleniya tablicej imen, raboty so stekom, mnozhestvom i
realizacii 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.
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.
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.
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.
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).
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.
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.
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.
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).
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 {
// ...
public:
int open(const char*, const char*);
};
int my_file::jpen(const char* name, const char* spec)
{
// ...
if (::open(name,flag)) { // ispol'zuetsya open() iz UNIX(2)
// ...
}
// ...
}
Opisanie klassa mozhet byt' vlozhennym. Naprimer:
class set {
struct setmem {
int mem;
setmem* next;
setmem(int m, setmem* n) { mem=m; next=n; }
};
setmem* first;
public:
set() { first=0; }
insert(int m) { first = new setmem(m,first); }
// ...
};
Dostupnost' vlozhennogo klassa ogranichivaetsya oblast'yu vidimosti
leksicheski ob容mlyushchego klassa:
setmem m1(1,0); // oshibka: setmem ne nahoditsya
// v global'noj oblasti vidimosti
Esli tol'ko opisanie vlozhennogo klassa ne yavlyaetsya sovsem prostym,
to luchshe opisyvat' etot klass otdel'no, poskol'ku vlozhennye opisaniya
mogut stat' ochen' zaputannymi:
class setmem {
friend class set; // dostupno tol'ko dlya chlenov set
int mem;
setmem* next;
setmem(int m, setmem* n) { mem=m; next=n; }
// mnogo drugih poleznyh chlenov
};
class set {
setmem* first;
public:
set() { first=0; }
insert(int m) { first = new setmem(m,first); }
// ...
};
Poleznoe svojstvo vlozhennosti - eto sokrashchenie chisla global'nyh imen,
a nedostatok ego v tom, chto ono narushaet svobodu ispol'zovaniya
vlozhennyh tipov (sm. $$12.3).
Imya klassa-chlena (vlozhennogo klassa) mozhno ispol'zovat' vne
opisaniya ob容mlyushchego ego klassa tak zhe, kak imya lyubogo drugogo
chlena:
class X {
struct M1 { int m; };
public:
struct M2 { int m; };
M1 f(M2);
};
void f()
{ M1 a; // oshibka: imya `M1' vne oblasti vidimosti
M2 b; // oshibka: imya `M1' vne oblasti vidimosti
X::M1 c; // oshibka: X::M1 chastnyj chlen
X::M2 d; // normal'no
}
Otmetim, chto kontrol' dostupa proishodit i dlya imen vlozhennyh
klassov.
V funkcii-chlene oblast' vidimosti klassa nachinaetsya posle
utochneniya X:: i prostiraetsya do konca opisaniya funkcii. Naprimer:
M1 X::f(M2 a) // oshibka: imya `M1' vne oblasti vidimosti
{ /* ... */ }
X::M1 X::f(M2 a) // normal'no
{ /* ... */ }
X::M1 X::f(X::M2 a) // normal'no, no tret'e utochnenie X:: izlishne
{ /* ... */ }
5.4.4 Staticheskie chleny
Klass - eto tip, a ne nekotoroe dannoe, i dlya kazhdogo ob容kta
klassa sozdaetsya svoya kopiya chlenov, predstavlyayushchih dannye. Odnako,
naibolee udachnaya realizaciya nekotoryh tipov trebuet, chtoby vse
ob容kty etogo tipa imeli nekotorye obshchie dannye. Luchshe, esli eti
dannye mozhno opisat' kak chast' klassa. Naprimer, v operacionnyh
sistemah ili pri modelirovanii upravleniya zadachami chasto nuzhen
spisok zadach:
class task {
// ...
static task* chain;
// ...
};
Opisav chlen chain kak staticheskij, my poluchaem garantiyu, chto
on budet sozdan v edinstvennom chisle, t.e. ne budet sozdavat'sya
dlya kazhdogo ob容kta task. No on nahoditsya v oblasti vidimosti
klassa task, i mozhet byt' dostupen vne etoj oblasti, esli tol'ko
opisan v obshchej chasti. V etom sluchae imya chlena dolzhno utochnyat'sya
imenem klassa:
if (task::chain == 0) // kakie-to operatory
V funkcii-chlene ego mozhno oboznachat' prosto chain. Ispol'zovanie
staticheskih chlenov klassa mozhet zametno sokratit' potrebnost' v
global'nyh peremennyh.
Opisyvaya chlen kak staticheskij, my ogranichivaem ego oblast'
vidimosti i delaem ego nezavisimym ot otdel'nyh ob容ktov ego
klassa. |to svojstvo polezno kak dlya funkcij-chlenov, tak i dlya
chlenov, predstavlyayushchih dannye:
class task {
// ...
static task* task_chain;
static void shedule(int);
// ...
};
No opisanie staticheskogo chlena - eto tol'ko opisanie, i gde-to
v programme dolzhno byt' edinstvennoe opredelenie dlya opisyvaemogo
ob容kta ili funkcii, naprimer, takoe:
task* task::task_chain = 0;
void task::shedule(int p) { /* ... */ }
Estestvenno, chto i chastnye chleny mogut opredelyat'sya podobnym obrazom.
Otmetim, chto sluzhebnoe slovo static ne nuzhno i dazhe nel'zya
ispol'zovat' v opredelenii staticheskogo chlena klassa. Esli by ono
prisutstvovalo, voznikla by neodnoznachnost': ukazyvaet li ono na to,
chto chlen klassa yavlyaetsya staticheskim, ili ispol'zuetsya dlya
opisaniya global'nogo ob容kta ili funkcii?
Slovo static odno iz samyh peregruzhennyh sluzhebnyh slov v S
i S++. K staticheskomu chlenu, predstavlyayushchemu dannye, otnosyatsya
oba osnovnyh ego znacheniya: "staticheski razmeshchaemyj" , t.e.
protivopolozhnyj ob容ktam, razmeshchaemym v steke ili svobodnoj pamyati,
i "staticheskij" v smysle s ogranichennoj oblast'yu vidimosti, t.e.
protivopolozhnyj ob容ktam, podlezhashchim vneshnemu svyazyvaniyu. K
funkciyam-chlenam otnositsya tol'ko vtoroe znachenie static.
5.4.5 Ukazateli na chleny
Mozhno brat' adres chlena klassa. Operaciya vzyatiya adresa funkcii-chlena
chasto okazyvaetsya poleznoj, poskol'ku celi i sposoby primeneniya
ukazatelej na funkcii, o kotoryh my govorili v $$4.6.9, v ravnoj
stepeni otnosyatsya i k takim funkciyam. Ukazatel' na chlen mozhno poluchit',
primeniv operaciyu vzyatiya adresa & k polnost'yu utochnennomu imeni
chlena klassa, naprimer, &class_name::member_name. CHtoby opisat'
peremennuyu tipa "ukazatel' na chlen klassa X", nado ispol'zovat'
opisatel' vida X::*. Naprimer:
#include <iostream.h>
struct cl
{
char* val;
void print(int x) { cout << val << x << '\n'; }
cl(char* v) { val = v; }
};
Ukazatel' na chlen mozhno opisat' i ispol'zovat' tak:
typedef void (cl::*PMFI)(int);
int main()
{
cl z1("z1 ");
cl z2("z2 ");
cl* p = &z2;
PMFI pf = &cl::print;
z1.print(1);
(z1.*pf)(2);
z2.print(3);
(p->*pf)(4);
}
Ispol'zovanie typedef dlya zameny trudno vosprinimaemogo opisatelya
v S dostatochno tipichnyj sluchaj. Operacii .* i ->* nastraivayut
ukazatel' na konkretnyj ob容kt, vydavaya v rezul'tate funkciyu,
kotoruyu mozhno vyzyvat'. Prioritet operacii () vyshe, chem u operacij
.* i ->*, poetomu nuzhny skobki.
Vo mnogih sluchayah virtual'nye funkcii ($$6.2.5) uspeshno
zamenyayut ukazateli na funkcii.
5.4.6 Struktury i ob容dineniya
Po opredeleniyu struktura - eto klass, vse chleny kotorogo obshchie,
t.e. opisanie
struct s { ...
eto prosto kratkaya forma opisaniya
class s { public: ...
Poimenovannoe ob容dinenie opredelyaetsya kak struktura, vse chleny
kotoroj imeyut odin i tot zhe adres ($$R.9.5). Esli izvestno, chto
v kazhdyj moment vremeni ispol'zuetsya znachenie tol'ko odnogo chlena
struktury, to ob座aviv ee ob容dineniem, mozhno sekonomit' pamyat'.
Naprimer, mozhno ispol'zovat' ob容dinenie dlya hraneniya leksem
translyatora S:
union tok_val {
char* p; // stroka
char v[8]; // identifikator (ne bolee 8 simvolov)
long i; // znacheniya celyh
double d; // znacheniya chisel s plavayushchej tochkoj
};
Problema s ob容dineniyami v tom, chto translyator v obshchem sluchae
ne znaet, kakoj chlen ispol'zuetsya v dannyj moment, i poetomu
kontrol' tipa nevozmozhen. Naprimer:
void strange(int i)
{
tok_val x;
if (i)
x.p = "2";
else
x.d = 2;
sqrt(x.d); // oshibka, esli i != 0
}
Krome togo, opredelennoe takim obrazom ob容dinenie nel'zya
inicializirovat' takim kazhushchimsya vpolne estestvennym sposobom:
tok_val val1 = 12; // oshibka: int prisvaivaetsya tok_val
tok_val val2 = "12"; // oshibka: char* prisvaivaetsya tok_val
Dlya pravil'noj inicializacii nado ispol'zovat' konstruktory:
union tok_val {
char* p; // stroka
char v[8]; // identifikator (ne bolee 8 simvolov)
long i; // znacheniya celyh
double d; // znacheniya chisel s plavayushchej tochkoj
tok_val(const char*); // nuzhno vybirat' mezhdu p i v
tok_val(int ii) { i = ii; }
tok_val(double dd) { d = dd; }
};
|ti opisaniya pozvolyayut razreshit' s pomoshch'yu tipa chlenov neodnoznachnost'
pri peregruzke imeni funkcii (sm. $$4.6.6 i $$7.3). Naprimer:
void f()
{
tok_val a = 10; // a.i = 10
tok_val b = 10.0; // b.d = 10.0
}
Esli eto nevozmozhno (naprimer, dlya tipov char* i char[8] ili int
i char i t.d.), to opredelit', kakoj chlen inicializiruetsya, mozhno,
izuchiv inicializator pri vypolnenii programmy, ili vvedya
dopolnitel'nyj parametr. Naprimer:
tok_val::tok_val(const char* pp)
{
if (strlen(pp) <= 8)
strncpy(v,pp,8); // korotkaya stroka
else
p = pp; // dlinnaya stroka
}
No luchshe podobnoj neodnoznachnosti izbegat'.
Standartnaya funkciya strncpy() podobno strcpy() kopiruet
stroki, no u nee est' dopolnitel'nyj parametr, zadayushchij
maksimal'noe chislo kopiruemyh simvolov.
To, chto dlya inicializacii ob容dineniya ispol'zuyutsya konstruktory,
eshche ne garantiruet ot sluchajnyh oshibok pri rabote s ob容dineniem, kogda
prisvaivaetsya znachenie odnogo tipa, a vybiraetsya znachenie drugogo
tipa. Takuyu garantiyu mozhno poluchit', esli zaklyuchit' ob容dinenie
v klass, v kotorom budet otslezhivat'sya tip zanosimogo znacheniya :
class tok_val {
public:
enum Tag { I, D, S, N };
private:
union {
const char* p;
char v[8];
long i;
double d;
};
Tag tag;
void check(Tag t) { if (tag != t) error(); }
public:
Tag get_tag() { return tag; }
tok_val(const char* pp);
tok_val(long ii) { i = ii; tag = I; }
tok_val(double dd) { d = dd; tag = D; }
long& ival() { check(I); return i; }
double& fval() { check(D); return d; }
const char*& sval() { check(S); return p; }
char* id() { check(N); return v; }
};
tok_val::tok_val(const char* pp)
{
if (strlen(pp) <= 8) { // korotkaya stroka
tag = N;
strncpy(v,pp,8);
}
else { // dlinnaya stroka
tag = S;
p = pp; // zapisyvaetsya tol'ko ukazatel'
}
}
Ispol'zovat' klass tok_val mozhno tak:
void f()
{
tok_val t1("korotkaya"); // prisvaivaetsya v
tok_val t2("dlinnaya stroka"); // prisvaivaetsya p
char s[8];
strncpy(s,t1.id(),8); // normal'no
strncpy(s,t2.id(),8); // check() vydast oshibku
}
Opisav tip Tag i funkciyu get_tag() v obshchej chasti, my garantiruem,
chto tip tok_val mozhno ispol'zovat' kak tip parametra. Takim obrazom,
poyavlyaetsya nadezhnaya v smysle tipov al'ternativa opisaniyu parametrov
s ellipsisom. Vot, naprimer, opisanie funkcii obrabotki oshibok,
kotoraya mozhet imet' odin, dva, ili tri parametra s tipami char*,
int ili double:
extern tok_val no_arg;
void error(
const char* format,
tok_val a1 = no_arg,
tok_val a2 = no_arg,
tok_val a3 = no_arg);
5.5 Konstruktory i destruktory
Esli u klassa est' konstruktor, on vyzyvaetsya vsyakij raz pri
sozdanii ob容kta etogo klassa. Esli u klassa est' destruktor,
on vyzyvaetsya vsyakij raz, kogda unichtozhaetsya ob容kt etogo klassa.
Ob容kt mozhet sozdavat'sya kak:
[1] avtomaticheskij, kotoryj sozdaetsya kazhdyj raz, kogda ego
opisanie vstrechaetsya pri vypolnenii programmy, i unichtozhaetsya
po vyhode iz bloka, v kotorom on opisan;
[2] staticheskij, kotoryj sozdaetsya odin raz pri zapuske programmy
i unichtozhaetsya pri ee zavershenii;
[3] ob容kt v svobodnoj pamyati, kotoryj sozdaetsya operaciej new
i unichtozhaetsya operaciej delete;
[4] ob容kt-chlen, kotoryj sozdaetsya v processe sozdaniya drugogo
klassa ili pri sozdanii massiva, elementom kotorogo on
yavlyaetsya.
Krome etogo ob容kt mozhet sozdavat'sya, esli v vyrazhenii yavno
ispol'zuetsya ego konstruktor ($$7.3) ili kak vremennyj ob容kt
($$R.12.2). V oboih sluchayah takoj ob容kt ne imeet imeni. V sleduyushchih
podrazdelah predpolagaetsya, chto ob容kty otnosyatsya k klassu s
konstruktorom i destruktorom. V kachestve primera ispol'zuetsya
klass table iz $$5.3.1.
5.5.1 Lokal'nye peremennye
Konstruktor lokal'noj peremennoj vyzyvaetsya kazhdyj raz, kogda pri
vypolnenii programmy vstrechaetsya ee opisanie. Destruktor lokal'noj
peremennoj vyzyvaetsya vsyakij raz po vyhode iz bloka, gde ona
byla opisana. Destruktory dlya lokal'nyh peremennyh vyzyvayutsya v
poryadke, obratnom vyzovu konstruktorov pri ih sozdanii:
void f(int i)
{
table aa;
table bb;
if (i>0) {
table cc;
// ...
}
// ...
}
Zdes' aa i bb sozdayutsya (imenno v takom poryadke) pri kazhdom vyzove
f(), a unichtozhayutsya oni pri vozvrate iz f() v obratnom poryadke -
bb, zatem aa. Esli v tekushchem vyzove f() i bol'she nulya, to cc
sozdaetsya posle bb i unichtozhaetsya prezhde nego.
Poskol'ku aa i bb - ob容kty klassa table, prisvaivanie aa=bb
oznachaet kopirovanie po chlenam bb v aa (sm. $$2.3.8). Takaya
interpretaciya prisvaivaniya mozhet privesti k neozhidannomu (i obychno
nezhelatel'nomu) rezul'tatu, esli prisvaivayutsya ob容kty klassa,
v kotorom opredelen konstruktor:
void h()
{
table t1(100);
table t2 = t1; // nepriyatnost'
table t3(200);
t3 = t2; // nepriyatnost'
}
V etom primere konstruktor table vyzyvaetsya dvazhdy: dlya t1 i t3.
On ne vyzyvaetsya dlya t2, poskol'ku etot ob容kt inicializiruetsya
prisvaivaniem. Tem ne menee, destruktor dlya table vyzyvaetsya tri
raza: dlya t1, t2 i t3! Dalee, standartnaya interpretaciya
prisvaivaniya - eto kopirovanie po chlenam, poetomu pered vyhodom
iz h() t1, t2 i t3 budut soderzhat' ukazatel' na massiv imen, pamyat'
dlya kotorogo byla vydelena v svobodnoj pamyati pri sozdanii t1.
Ukazatel' na pamyat', vydelennuyu dlya massiva imen pri sozdanii
t3, budet poteryan. |tih nepriyatnostej mozhno izbezhat' (sm. $$1.4.2
i $$7.6).
5.5.2 Staticheskaya pamyat'
Rassmotrim takoj primer:
table tbl(100);
void f(int i)
{
static table tbl2(i);
}
int main()
{
f(200);
// ...
}
Zdes' konstruktor, opredelennyj v $$5.3.1, budet vyzyvat'sya dvazhdy:
odin raz dlya tbl i odin raz dlya tbl2. Destruktor table::~table()
takzhe budet vyzvan dvazhdy: dlya unichtozheniya tbl i tbl2 po vyhode
iz main(). Konstruktory global'nyh staticheskih ob容ktov v fajle
vyzyvayutsya v tom zhe poryadke, v kakom vstrechayutsya v fajle
opisaniya ob容ktov, a destruktory dlya nih vyzyvayutsya v obratnom
poryadke. Konstruktor lokal'nogo staticheskogo ob容kta vyzyvaetsya,
kogda pri vypolnenii programmy pervyj raz vstrechaetsya opredelenie
ob容kta.
Tradicionno vypolnenie main() rassmatrivalos' kak vypolnenie
vsej programmy. Na samom dele, eto ne tak dazhe dlya S. Uzhe
razmeshchenie staticheskogo ob容kta klassa s konstruktorom i (ili)
destruktorom pozvolyaet programmistu zadat' dejstviya, kotorye
budut vypolnyat'sya do vyzova main() i (ili) po vyhode iz main().
Vyzov konstruktorov i destruktorov dlya staticheskih ob容ktov
igraet v S++ chrezvychajno vazhnuyu rol'. S ih pomoshch'yu mozhno obespechit'
sootvetstvuyushchuyu inicializaciyu i udalenie struktur dannyh,
ispol'zuemyh v bibliotekah. Rassmotrim <iostream.h>. Otkuda
berutsya cin, cout i cerr? Kogda oni inicializiruyutsya? Bolee
sushchestvennyj vopros: poskol'ku dlya vyhodnyh potokov ispol'zuyutsya
vnutrennie bufera simvolov, to proishodit vytalkivanie etih
buferov, no kogda? Est' prostoj i ochevidnyj otvet: vse dejstviya
vypolnyayutsya sootvetstvuyushchimi konstruktorami i destruktorami do
zapuska main() i po vyhode iz nee (sm. $$10.5.1). Sushchestvuyut al'ternativy
ispol'zovaniyu konstruktorov i destruktorov dlya inicializacii i
unichtozheniya bibliotechnyh struktur dannyh, no vse oni ili ochen'
specializirovany, ili neuklyuzhi, ili i to i drugoe vmeste.
Esli programma zavershaetsya obrashchenie k funkcii exit(), to
vyzyvayutsya destruktory dlya vseh postroennyh staticheskih ob容ktov.
Odnako, esli programma zavershaetsya obrashcheniem k abort(), etogo
ne proishodit. Zametim, chto exit() ne zavershaet
programmu nemedlenno. Vyzov exit() v destruktore mozhet privesti
k beskonechnoj rekursii. Esli nuzhna garantiya, chto budut unichtozheny
kak staticheskie, tak i avtomaticheskie ob容kty, mozhno vospol'zovat'sya
osobymi situaciyami ($$9).
Inogda pri razrabotke biblioteki byvaet neobhodimo ili prosto
udobno sozdat' tip s konstruktorom i destruktorom tol'ko dlya
odnoj celi: inicializacii i unichtozheniya ob容ktov. Takoj tip
ispol'zuetsya tol'ko odin raz dlya razmeshcheniya staticheskogo ob容kta,
chtoby vyzvat' konstruktory i destruktory.
Rassmotrim primer:
main()
{
table* p = new table(100);
table* q = new table(200);
delete p;
delete p; // veroyatno, vyzovet oshibku pri vypolnenii
}
Konstruktor table::table() budet vyzyvat'sya dvazhdy, kak i destruktor
table::~table(). No eto nichego ne znachit, t.k. v S++ ne
garantiruetsya, chto destruktor budet vyzyvat'sya tol'ko dlya ob容kta,
sozdannogo operaciej new. V etom primere q ne unichtozhaetsya voobshche,
zato p unichtozhaetsya dvazhdy! V zavisimosti ot tipa p i q programmist
mozhet schitat' ili ne schitat' eto oshibkoj. To, chto ob容kt ne
udalyaetsya, obychno byvaet ne oshibkoj, a prosto poterej pamyati. V to zhe
vremya povtornoe udalenie p - ser'eznaya oshibka. Povtornoe primenenie
delete k tomu zhe samomu ukazatelyu mozhet privesti k beskonechnomu
ciklu v podprogramme, upravlyayushchej svobodnoj pamyat'yu. No v yazyke
rezul'tat povtornogo udaleniya ne opredelen, i on zavisit ot
realizacii.
Pol'zovatel' mozhet opredelit' svoyu realizaciyu operacij new i
delete (sm. $$3.2.6 i $$6.7). Krome togo, mozhno ustanovit'
vzaimodejstvie konstruktora ili destruktora s operaciyami new i
delete (sm. $$5.5.6 i $$6.7.2). Razmeshchenie massivov v svobodnoj
pamyati obsuzhdaetsya v $$5.5.5.
5.5.4 Ob容kty klassa kak chleny
Rassmotrim primer:
class classdef {
table members;
int no_of_members;
// ...
classdef(int size);
~classdef();
};
Cel' etogo opredeleniya, ochevidno, v tom, chtoby classdef soderzhal
chlen, yavlyayushchijsya tablicej razmerom size, no est' slozhnost': nado
obespechit' vyzov konstruktora table::table() s parametrom size. |to
mozhno sdelat', naprimer, tak:
classdef::classdef(int size)
:members(size)
{
no_of_members = size;
// ...
}
Parametr dlya konstruktora chlena (t.e. dlya table::table()) ukazyvaetsya
v opredelenii (no ne v opisanii) konstruktora klassa, soderzhashchego
chlen (t.e. v opredelenii classdef::classdef()). Konstruktor dlya
chlena budet vyzyvat'sya do vypolneniya tela togo konstruktora, kotoryj
zadaet dlya nego spisok parametrov.
Analogichno mozhno zadat' parametry dlya konstruktorov drugih chlenov
(esli est' eshche drugie chleny):
class classdef {
table members;
table friends;
int no_of_members;
// ...
classdef(int size);
~classdef();
};
Spiski parametrov dlya chlenov otdelyayutsya drug ot druga zapyatymi (a ne
dvoetochiyami), a spisok inicializatorov dlya chlenov mozhno zadavat' v
proizvol'nom poryadke:
classdef::classdef(int size)
: friends(size), members(size), no_of_members(size)
{
// ...
}
Konstruktory vyzyvayutsya v tom poryadke, v kotorom oni zadany v
opisanii klassa.
Podobnye opisaniya konstruktorov sushchestvenny dlya tipov,
inicializaciya i prisvaivanie kotoryh otlichny drug ot druga, inymi
slovami, dlya ob容ktov, yavlyayushchihsya chlenami klassa s konstruktorom,
dlya postoyannyh chlenov ili dlya chlenov tipa ssylki. Odnako, kak
pokazyvaet chlen no_of_members iz privedennogo primera, takie
opisaniya konstruktorov mozhno ispol'zovat' dlya chlenov lyubogo
tipa.
Esli konstruktoru chlena ne trebuetsya parametrov, to i ne nuzhno
zadavat' nikakih spiskov parametrov. Tak, poskol'ku konstruktor
table::table() byl opredelen so standartnym znacheniem parametra,
ravnym 15, dostatochno takogo opredeleniya:
classdef::classdef(int size)
: members(size), no_of_members(size)
{
// ...
}
Togda razmer tablicy friends budet raven 15.
Esli unichtozhaetsya ob容kt klassa, kotoryj sam soderzhit ob容kty
klassa (naprimer, classdef), to vnachale vypolnyaetsya telo
destruktora ob容mlyushchego klassa, a zatem destruktory chlenov v poryadke,
obratnom ih opisaniyu.
Rassmotrim vmesto vhozhdeniya ob容ktov klassa v kachestve chlenov
tradicionnoe al'ternativnoe emu reshenie: imet' v klasse ukazateli
na chleny i inicializirovat' chleny v konstruktore:
class classdef {
table* members;
table* friends;
int no_of_members;
// ...
};
classdef::classdef(int size)
{
members = new table(size);
friends = new table; // ispol'zuetsya standartnyj
// razmer table
no_of_members = size;
// ...
}
Poskol'ku tablicy sozdavalis' s pomoshch'yu operacii new, oni dolzhny
unichtozhat'sya operaciej delete:
classdef::~classdef()
{
// ...
delete members;
delete friends;
}
Takie otdel'no sozdavaemye ob容kty mogut okazat'sya poleznymi, no
uchtite, chto members i friends ukazyvayut na nezavisimye ot nih
ob容kty, kazhdyj iz kotoryh nado yavno razmeshchat' i udalyat'. Krome
togo, ukazatel' i ob容kt v svobodnoj pamyati summarno zanimayut
bol'she mesta, chem ob容kt-chlen.
5.5.5 Massivy ob容ktov klassa
CHtoby mozhno bylo opisat' massiv ob容ktov klassa s konstruktorom,
etot klass dolzhen imet' standartnyj konstruktor, t.e. konstruktor,
vyzyvaemyj bez parametrov. Naprimer, v sootvetstvii s opredeleniem
table tbl[10];
budet sozdan massiv iz 10 tablic, kazhdaya iz kotoryh inicializiruetsya
vyzovom table::table(15), poskol'ku vyzov table::table() budet
proishodit' s fakticheskim parametrom 15.
V opisanii massiva ob容ktov ne predusmotreno vozmozhnosti ukazat'
parametry dlya konstruktora. Esli chleny massiva obyazatel'no nado
inicializirovat' raznymi znacheniyami, to nachinayutsya tryuki s
global'nymi ili staticheskimi chlenami.
Kogda unichtozhaetsya massiv, destruktor dolzhen vyzyvat'sya dlya
kazhdogo elementa massiva. Dlya massivov, kotorye razmeshchayutsya ne
s pomoshch'yu new, eto delaetsya neyavno. Odnako dlya razmeshchennyh v svobodnoj
pamyati massivov neyavno vyzyvat' destruktor nel'zya, poskol'ku translyator
ne otlichit ukazatel' na otdel'nyj ob容kt massiva ot ukazatelya na nachalo
massiva, naprimer:
void f()
{
table* t1 = new table;
table* t2 = new table[10];
delete t1; // udalyaetsya odna tablica
delete t2; // nepriyatnost':
// na samom dele udalyaetsya 10 tablic
}
V dannom sluchae programmist dolzhen ukazat', chto t2 - ukazatel'
na massiv:
void g(int sz)
{
table* t1 = new table;
table* t2 = new table[sz];
delete t1;
delete[] t2;
}
Funkciya razmeshcheniya hranit chislo elementov dlya kazhdogo razmeshchaemogo
massiva. Trebovanie ispol'zovat' dlya udaleniya massivov tol'ko operaciyu
delete[] osvobozhdaet funkciyu razmeshcheniya ot obyazannosti hranit' schetchiki
chisla elementov dlya kazhdogo massiva. Ispolnenie takoj obyazannosti v
realizaciyah S++ vyzyvalo by sushchestvennye poteri vremeni i pamyati
i narushilo sovmestimost' s S.
Esli v vashej programme mnogo nebol'shih ob容ktov, razmeshchaemyh v
svobodnoj pamyati, to mozhet okazat'sya, chto mnogo vremeni tratitsya
na razmeshchenie i udalenie takih ob容ktov. Dlya vyhoda iz etoj
situacii mozhno opredelit' bolee optimal'nyj raspredelitel' pamyati
obshchego naznacheniya, a mozhno peredat' obyazannost' raspredeleniya
svobodnoj pamyati sozdatelyu klassa, kotoryj dolzhen budet
opredelit' sootvetstvuyushchie funkcii razmeshcheniya i udaleniya.
Vernemsya k klassu name, kotoryj ispol'zovalsya v primerah s
table. On mog by opredelyat'sya tak:
struct name {
char* string;
name* next;
double value;
name(char*, double, name*);
~name();
void* operator new(size_t);
void operator delete(void*, size_t);
private:
enum { NALL = 128 };
static name* nfree;
};
Funkcii name::operator new() i name::operator delete() budut
ispol'zovat'sya (neyavno) vmesto global'nyh funkcij operator new()
i operator delete(). Programmist mozhet dlya konkretnogo tipa napisat'
bolee effektivnye po vremeni i pamyati funkcii razmeshcheniya i
udaleniya, chem universal'nye funkcii operator new() i
operator delete(). Mozhno, naprimer, razmestit' zaranee "kuski"
pamyati, dostatochnoj dlya ob容ktov tipa name, i svyazat' ih v spisok;
togda operacii razmeshcheniya i udaleniya svodyatsya k prostym operaciyam
so spiskom. Peremennaya nfree ispol'zuetsya kak nachalo spiska
neispol'zovannyh kuskov pamyati:
void* name::operator new(size_t)
{
register name* p = nfree; // snachala vydelit'
if (p)
nfree = p->next;
else { // vydelit' i svyazat' v spisok
name* q = (name*) new char[NALL*sizeof(name) ];
for (p=nfree=&q[NALL-1]; q<p; p--) p->next = p-1;
(p+1)->next = 0;
}
return p;
}
Raspredelitel' pamyati, vyzyvaemyj new, hranit vmeste s ob容ktom ego
razmer, chtoby operaciya delete vypolnyalas' pravil'no. |togo
dopolnitel'nogo rashoda pamyati mozhno legko izbezhat', esli
ispol'zovat' raspredelitel', rasschitannyj na konkretnyj tip. Tak,
na mashine avtora funkciya name::operator new() dlya hraneniya ob容kta
name ispol'zuet 16 bajtov, togda kak standartnaya global'naya
funkciya operator new() ispol'zuet 20 bajtov.
Otmetim, chto v samoj funkcii name::operator new() pamyat' nel'zya
vydelyat' takim prostym sposobom:
name* q= new name[NALL];
|to vyzovet beskonechnuyu rekursiyu, t.k. new budet vyzyvat'
name::name().
Osvobozhdenie pamyati obychno trivial'no:
void name::operator delete(void* p, size_t)
{
((name*)p)->next = nfree;
nfree = (name*) p;
}
Privedenie parametra tipa void* k tipu name* neobhodimo, poskol'ku
funkciya osvobozhdeniya vyzyvaetsya posle unichtozheniya ob容kta, tak chto
bol'she net real'nogo ob容kta tipa name, a est' tol'ko kusok
pamyati razmerom sizeof(name). Parametry tipa size_t v privedennyh
funkciyah name::operator new() i name::operator delete() ne
ispol'zovalis'. Kak mozhno ih ispol'zovat', budet pokazano v $$6.7.
Otmetim, chto nashi funkcii razmeshcheniya i udaleniya ispol'zuyutsya
tol'ko dlya ob容ktov tipa name, no ne dlya massivov names.
1. (*1) Izmenite programmu kal'kulyatora iz glavy 3 tak, chtoby
mozhno bylo vospol'zovat'sya klassom table.
2. (*1) Opredelite tnode ($$R.9) kak klass s konstruktorami i
destruktorami i t.p., opredelite derevo iz ob容ktov tipa
tnode kak klass s konstruktorami i destruktorami i t.p.
3. (*1) Opredelite klass intset ($$5.3.2) kak mnozhestvo strok.
4. (*1) Opredelite klass intset kak mnozhestvo uzlov tipa tnode.
Strukturu tnode pridumajte sami.
5. (*3) Opredelite klass dlya razbora, hraneniya, vychisleniya i pechati
prostyh arifmeticheskih vyrazhenij, sostoyashchih iz celyh konstant i
operacij +, -, * i /. Obshchij interfejs klassa dolzhen vyglyadet'
primerno tak:
class expr {
// ...
public:
expr(char*);
int eval();
void print();
};
Konstruktor expr::expr() imeet parametr-stroku, zadayushchuyu vyrazhenie.
Funkciya expr::eval() vozvrashchaet znachenie vyrazheniya, a expr::print()
vydaet predstavlenie vyrazheniya v cout. Ispol'zovat' eti funkcii
mozhno tak:
expr("123/4+123*4-3");
cout << "x = " << x.eval() << "\n";
x.print();
Dajte dva opredeleniya klassa expr: pust' v pervom dlya predstavleniya
ispol'zuetsya svyazannyj spisok uzlov, a vo vtorom - stroka
simvolov. Poeksperimentirujte s raznymi formatami pechati
vyrazheniya, a imenno: s polnost'yu rasstavlennymi skobkami,
v postfiksnoj zapisi, v assemblernom kode i t.d.
6. (*1) Opredelite klass char_queue (ochered' simvolov) tak, chtoby
ego obshchij interfejs ne zavisel ot predstavleniya. Realizujte
klass kak: (1) svyazannyj spisok i (2) vektor. O parallel'nosti
ne dumajte.
7. (*2) Opredelite klass histogram (gistogramma), v kotorom vedetsya
podschet chisel v opredelennyh intervalah, zadavaemyh v vide
parametrov konstruktoru etogo klassa. Opredelite funkciyu
vydachi gistogrammy. Sdelajte obrabotku znachenij, vyhodyashchih za
interval. Podskazka: obratites' k <task.h>.
8. (*2) Opredelite neskol'ko klassov, porozhdayushchih sluchajnye chisla
s opredelennymi raspredeleniyami. Kazhdyj klass dolzhen imet'
konstruktor, zadayushchij parametry raspredeleniya i funkciyu draw,
vozvrashchayushchuyu "sleduyushchee" znachenie. Podskazka: obratites' k
<task.h> i klassu intset.
9. (*2) Perepishite primery date ($$5.2.2 i $$5.2.4), char_stack
($$5.2.5) i intset ($$5.3.2), ne ispol'zuya nikakih funkcij-chlenov
(dazhe konstruktorov i destruktorov). Ispol'zujte tol'ko class
i friend. Prover'te kazhduyu iz novyh versij i sravnite ih
s versiyami, v kotoryh ispol'zuyutsya funkcii-chleny.
10.(*3) Dlya nekotorogo yazyka sostav'te opredeleniya klassa dlya tablicy
imen i klassa, predstavlyayushchego zapis' v etoj tablice. Issledujte
translyator dlya etogo yazyka, chtoby uznat', kakoj dolzhna byt' nastoyashchaya
tablica imen.
11.(*2) Izmenite klass expr iz uprazhneniya 5 tak, chtoby v vyrazhenii
mozhno bylo ispol'zovat' peremennye i operaciyu prisvaivaniya =.
Ispol'zujte klass dlya tablicy imen iz uprazhneniya 10.
12.(*1) Pust' est' programma:
#include <iostream.h>
main()
{
cout << "Vsem privet\n";
}
Izmenite ee tak, chtoby ona vydavala:
Inicializaciya
Vsem privet
Udalenie
Samu funkciyu main() menyat' nel'zya.
Ne plodi ob容kty bez nuzhdy.
- V. Okkam
|ta glava posvyashchena ponyatiyu proizvodnogo klassa. Proizvodnye
klassy - eto prostoe, gibkoe i effektivnoe sredstvo opredeleniya
klassa. Novye vozmozhnosti dobavlyayutsya k uzhe sushchestvuyushchemu
klassu, ne trebuya ego pereprogrammirovaniya ili peretranslyacii.
S pomoshch'yu proizvodnyh klassov mozhno organizovat' obshchij
interfejs s neskol'kimi razlichnymi klassami tak, chto v drugih
chastyah programmy mozhno budet edinoobrazno rabotat' s ob容ktami
etih klassov. Vvoditsya ponyatie virtual'noj funkcii, kotoroe
pozvolyaet ispol'zovat' ob容kty nadlezhashchim obrazom dazhe
v teh sluchayah, kogda ih tip na stadii translyacii neizvesten.
Osnovnoe naznachenie proizvodnyh klassov - uprostit'
programmistu zadachu vyrazheniya obshchnosti klassov.
6.1 Vvedenie i kratkij obzor
Lyuboe ponyatie ne sushchestvuet izolirovanno, ono sushchestvuet vo
vzaimosvyazi s drugimi ponyatiyami, i moshchnost' dannogo ponyatiya vo
mnogom opredelyaetsya nalichiem takih svyazej. Raz klass sluzhit dlya
predstavleniya ponyatij, vstaet vopros, kak predstavit' vzaimosvyaz'
ponyatij. Ponyatie proizvodnogo klassa i podderzhivayushchie ego
yazykovye sredstva sluzhat dlya predstavleniya ierarhicheskih svyazej,
inymi slovami, dlya vyrazheniya obshchnosti mezhdu klassami. Naprimer,
ponyatiya okruzhnosti i treugol'nika svyazany mezhdu soboj, tak kak
oba oni predstavlyayut eshche ponyatie figury, t.e. soderzhat bolee obshchee
ponyatie. CHtoby predstavlyat' v programme okruzhnosti i treugol'niki
i pri etom ne upuskat' iz vida, chto oni yavlyayutsya figurami, nado
yavno opredelyat' klassy okruzhnost' i treugol'nik tak, chtoby bylo vidno,
chto u nih est' obshchij klass - figura. V glave issleduetsya, chto
vytekaet iz etoj prostoj idei, kotoraya po suti yavlyaetsya osnovoj togo,
chto obychno nazyvaetsya ob容ktno-orientirovannym programmirovaniem.
Glava sostoit iz shesti razdelov:
$$6.2 s pomoshch'yu serii nebol'shih primerov vvoditsya ponyatie proizvodnogo
klassa, ierarhii klassov i virtual'nyh funkcij.
$$6.3 vvoditsya ponyatie chisto virtual'nyh funkcij i abstraktnyh
klassov, dany nebol'shie primery ih ispol'zovaniya.
$$6.4 proizvodnye klassy pokazany na zakonchennom primere
$$6.5 vvoditsya ponyatie mnozhestvennogo nasledovaniya kak vozmozhnost'
imet' dlya klassa bolee odnogo pryamogo bazovogo klassa,
opisyvayutsya sposoby razresheniya kollizij imen, voznikayushchih
pri mnozhestvennom nasledovanii.
$$6.6 obsuzhdaetsya mehanizm kontrolya dostupa.
$$6.7 privodyatsya nekotorye priemy upravleniya svobodnoj pamyat'yu dlya
proizvodnyh klassov.
V posleduyushchih glavah takzhe budut privodit'sya primery, ispol'zuyushchie
eti vozmozhnosti yazyka.
Obsudim, kak napisat' programmu ucheta sluzhashchih nekotoroj
firmy. V nej mozhet ispol'zovat'sya, naprimer, takaya struktura dannyh:
struct employee { // sluzhashchie
char* name; // imya
short age; // vozrast
short department; // otdel
int salary; // oklad
employee* next;
// ...
};
Pole next nuzhno dlya svyazyvaniya v spisok zapisej o sluzhashchih
odnogo otdela (employee). Teper' poprobuem opredelit' strukturu dannyh
dlya upravlyayushchego (manager):
struct manager {
employee emp; // zapis' employee dlya upravlyayushchego
employee* group; // podchinennyj kollektiv
short level;
// ...
};
Upravlyayushchij takzhe yavlyaetsya sluzhashchim, poetomu zapis' employee
hranitsya v chlene emp ob容kta manager. Dlya cheloveka eta obshchnost'
ochevidna, no dlya translyatora chlen emp nichem ne otlichaetsya ot
drugih chlenov klassa. Ukazatel' na strukturu manager (manager*)
ne yavlyaetsya ukazatelem na employee (employee*), poetomu
nel'zya svobodno ispol'zovat' odin vmesto drugogo. V chastnosti,
bez special'nyh dejstvij nel'zya ob容kt manager vklyuchit' v spisok
ob容ktov tipa employee. Pridetsya libo ispol'zovat' yavnoe privedenie
tipa manager*, libo v spisok zapisej employee vklyuchit' adres
chlena emp. Oba resheniya nekrasivy i mogut byt' dostatochno zaputannymi.
Pravil'noe reshenie sostoit v tom, chtoby tip manager byl tipom
employee s nekotoroj dopolnitel'noj informaciej:
struct manager : employee {
employee* group;
short level;
// ...
};
Klass manager yavlyaetsya proizvodnym ot employee, i, naoborot, employee
yavlyaetsya bazovym klassom dlya manager. Pomimo chlena group v klasse
manager est' chleny klassa employee (name, age i t.d.).
Graficheski otnoshenie nasledovaniya obychno izobrazhaetsya v vide
strelki ot proizvodnyh klassov k bazovomu:
employee
^
|
manager
Obychno govoryat, chto proizvodnyj klass nasleduet bazovyj klass, poetomu
i otnoshenie mezhdu nimi nazyvaetsya nasledovaniem. Inogda bazovyj klass
nazyvayut superklassom, a proizvodnyj - podchinennym klassom. No
eti terminy mogut vyzyvat' nedoumenie, poskol'ku ob容kt proizvodnogo
klassa soderzhit ob容kt svoego bazovogo klassa. Voobshche proizvodnyj
klass bol'she svoego bazovogo v tom smysle, chto v nem soderzhitsya
bol'she dannyh i opredeleno bol'she funkcij.
Imeya opredeleniya employee i manager, mozhno sozdat' spisok
sluzhashchih, chast' iz kotoryh yavlyaetsya i upravlyayushchimi:
void f()
{
manager m1, m2;
employee e1, e2;
employee* elist;
elist = &m1; // pomestit' m1 v elist
m1.next = &e1; // pomestit' e1 v elist
e1.next = &m2; // pomestit' m2 v elist
m2.next = &e2; // pomestit' m2 v elist
e2.next = 0; // konec spiska
}
Poskol'ku upravlyayushchij yavlyaetsya i sluzhashchim, ukazatel' manager*
mozhno ispol'zovat' kak employee*. V to zhe vremya sluzhashchij ne
obyazatel'no yavlyaetsya upravlyayushchim, i poetomu employee* nel'zya
ispol'zovat' kak manager*.
V obshchem sluchae, esli klass derived imeet obshchij bazovyj klass
base, to ukazatel' na derived mozhno bez yavnyh preobrazovanij tipa
prisvaivat' peremennoj, imeyushchej tip ukazatelya na base. Obratnoe
preobrazovanie ot ukazatelya na base k ukazatelyu na derived mozhet byt'
tol'ko yavnym:
void g()
{
manager mm;
employee* pe = &mm; // normal'no
employee ee;
manager* pm = ⅇ // oshibka:
// ne vsyakij sluzhashchij yavlyaetsya upravlyayushchim
pm->level = 2; // katastrofa: pri razmeshchenii ee
// pamyat' dlya chlena `level' ne vydelyalas'
pm = (manager*) pe; // normal'no: na samom dele pe
// ne nastroeno na ob容kt mm tipa manager
pm->level = 2; // otlichno: pm ukazyvaet na ob容kt mm
// tipa manager, a v nem pri razmeshchenii
// vydelena pamyat' dlya chlena `level'
}
Inymi slovami, esli rabota s ob容ktom proizvodnogo klassa idet cherez
ukazatel', to ego mozhno rassmatrivat' kak ob容kt bazovogo klassa.
Obratnoe neverno. Otmetim, chto v obychnoj realizacii S++ ne
predpolagaetsya dinamicheskogo kontrolya nad tem, chtoby posle preobrazovaniya
tipa, podobnogo tomu, kotoroe ispol'zovalos' v prisvaivanii pe v pm,
poluchivshijsya v rezul'tate ukazatel' dejstvitel'no byl nastroen na ob容kt
trebuemogo tipa (sm. $$13.5).
Prostye struktury dannyh vrode employee i manager sami po sebe
ne slishkom interesny, a chasto i ne osobenno polezny. Poetomu dobavim
k nim funkcii:
class employee {
char* name;
// ...
public:
employee* next; // nahoditsya v obshchej chasti, chtoby
// mozhno bylo rabotat' so spiskom
void print() const;
// ...
};
class manager : public employee {
// ...
public:
void print() const;
// ...
};
Nado otvetit' na nekotorye voprosy. Kakim obrazom funkciya-chlen
proizvodnogo klassa manager mozhet ispol'zovat' chleny bazovogo klassa
employee? Kakie chleny bazovogo klassa employee mogut ispol'zovat'
funkcii-chleny proizvodnogo klassa manager? Kakie chleny bazovogo
klassa employee mozhet ispol'zovat' funkciya, ne yavlyayushchayasya chlenom ob容kta
tipa manager? Kakie otvety na eti voprosy dolzhna davat' realizaciya
yazyka, chtoby oni maksimal'no sootvetstvovali zadache programmista?
Rassmotrim primer:
void manager::print() const
{
cout << " imya " << name << '\n';
}
CHlen proizvodnogo klassa mozhet ispol'zovat' imya iz obshchej chasti svoego
bazovogo klassa naravne so vsemi drugimi chlenami, t.e. bez ukazaniya
imeni ob容kta. Predpolagaetsya, chto est' ob容kt, na kotoryj nastroen
this, poetomu korrektnym obrashcheniem k name budet this->name. Odnako,
pri translyacii funkcii manager::print() budet zafiksirovana oshibka:
chlenu proizvodnogo klassa ne predostavleno pravo dostupa k chastnym
chlenam ego bazovogo klassa, znachit name nedostupno v etoj funkcii.
Vozmozhno mnogim eto pokazhetsya strannym, no davajte rassmotrim
al'ternativnoe reshenie: funkciya-chlen proizvodnogo klassa 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.
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:
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.
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.
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.
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 eto obespechivaet opisanie window vo
vseh proizvodnyh klassah kak virtual'nogo bazovogo klassa.
Mozhno sleduyushchim obrazom izobrazit' sostav ob容kta klassa
window_w_border_and_menu:
CHtoby uvidet' raznicu mezhdu obychnym i virtual'nym nasledovaniem,
sravnite etot risunok s risunkom iz $$6.5, pokazyvayushchim sostav ob容kta
klassa satellite. V grafe nasledovaniya kazhdyj bazovyj klass s dannym
imenem, kotoryj byl ukazan kak virtual'nyj, budet predstavlen
edinstvennym ob容ktom etogo klassa. Naprotiv, kazhdyj bazovyj
klass, kotoryj pri opisanii nasledovaniya ne byl ukazan kak
virtual'nyj, budet predstavlen svoim sobstvennym ob容ktom.
Teper' nado napisat' vse eti funkcii draw(). |to ne slishkom
trudno, no dlya neostorozhnogo programmista zdes' est' lovushka.
Snachala pojdem samym prostym putem, kotoryj kak raz k nej i vedet:
void window_w_border::draw()
{
window::draw();
// risuem ramku
}
void window_w_menu::draw()
{
window::draw();
// risuem menyu
}
Poka vse horosho. Vse eto ochevidno, i my sleduem obrazcu opredeleniya
takih funkcij pri uslovii edinstvennogo nasledovaniya ($$6.2.1), kotoryj
rabotal prekrasno. Odnako, v proizvodnom klasse sleduyushchego urovnya
poyavlyaetsya lovushka:
void window_w_border_and_menu::draw() // lovushka!
{
window_w_border::draw();
window_w_menu::draw();
// teper' operacii, otnosyashchiesya tol'ko
// k oknu s ramkoj i menyu
}
Na pervyj vzglyad vse vpolne normal'no. Kak obychno, snachala vypolnyayutsya
vse operacii, neobhodimye dlya bazovyh klassov, a zatem te, kotorye
otnosyatsya sobstvenno k proizvodnym klassam. No v rezul'tate
funkciya window::draw() budet vyzyvat'sya dvazhdy! Dlya bol'shinstva
graficheskih programm eto ne prosto izlishnij vyzov, a porcha
kartinki na ekrane. Obychno vtoraya vydacha na ekran zatiraet pervuyu.
CHtoby izbezhat' lovushki, nado dejstvovat' ne tak pospeshno. My
otdelim dejstviya, vypolnyaemye bazovym klassom, ot dejstvij,
vypolnyaemyh iz bazovogo klassa. Dlya etogo v kazhdom klasse vvedem
funkciyu _draw(), kotoraya vypolnyaet nuzhnye tol'ko dlya nego
dejstviya, a funkciya draw() budet vypolnyat' te zhe dejstviya plyus
dejstviya, nuzhnye dlya kazhdogo bazovogo klassa. Dlya klassa window
izmeneniya svodyatsya k vvedeniyu izlishnej funkcii:
class window {
// golovnaya informaciya
void _draw();
void draw();
};
Dlya proizvodnyh klassov effekt tot zhe:
class window_w_border : public virtual window {
// klass "okno s ramkoj"
// opredeleniya, svyazannye s ramkoj
void _draw();
void draw();
};
void window_w_border::draw()
{
window::_draw();
_draw(); // risuet ramku
};
Tol'ko dlya proizvodnogo klassa sleduyushchego urovnya proyavlyaetsya
otlichie funkcii, kotoroe i pozvolyaet obojti lovushku s povtornym
vyzovom window::draw(), poskol'ku teper' vyzyvaetsya window::_draw()
i tol'ko odin raz:
class window_w_border_and_menu
: public virtual window,
public window_w_border,
public window_w_menu {
void _draw();
void draw();
};
void window_w_border_and_menu::draw()
{
window::_draw();
window_w_border::_draw();
window_w_menu::_draw();
_draw(); // teper' operacii, otnosyashchiesya tol'ko
// k oknu s ramkoj i menyu
}
Ne obyazatel'no imet' obe funkcii window::draw() i window::_draw(),
no nalichie ih pozvolyaet izbezhat' razlichnyh prostyh opisok.
V etom primere klass window sluzhit hranilishchem obshchej dlya
window_w_border i window_w_menu informacii i opredelyaet interfejs
dlya obshcheniya etih dvuh klassov. Esli ispol'zuetsya edinstvennoe
nasledovanie, to obshchnost' informacii v dereve klassov dostigaetsya
tem, chto eta informaciya peredvigaetsya k kornyu dereva do teh
por, poka ona ne stanet dostupna vsem zainteresovannym v nej
uzlovym klassam. V rezul'tate legko voznikaet nepriyatnyj effekt:
koren' dereva ili blizkie k nemu klassy ispol'zuyutsya kak prostranstvo
global'nyh imen dlya vseh klassov dereva, a ierarhiya klassov vyrozhdaetsya
v mnozhestvo nesvyazannyh ob容ktov.
Sushchestvenno, chtoby v kazhdom iz klassov-brat'ev pereopredelyalis'
funkcii, opredelennye v obshchem virtual'nom bazovom klasse. Takim
obrazom kazhdyj iz brat'ev mozhet poluchit' svoj variant operacij,
otlichnyj ot drugih. Pust' v klasse window est' obshchaya funkciya
vvoda get_input():
class window {
// golovnaya informaciya
virtual void draw();
virtual void get_input();
};
V odnom iz proizvodnyh klassov mozhno ispol'zovat' etu funkciyu,
ne zadumyvayas' o tom, gde ona opredelena:
class window_w_banner : public virtual window {
// klass "okno s zagolovkom"
void draw();
void update_banner_text();
};
void window_w_banner::update_banner_text()
{
// ...
get_input();
// izmenit' tekst zagolovka
}
V drugom proizvodnom klasse funkciyu get_input() mozhno opredelyat',
ne zadumyvayas' o tom, kto ee budet ispol'zovat':
class window_w_menu : public virtual window {
// klass "okno s menyu"
// opredeleniya, svyazannye s menyu
void draw();
void get_input(); // pereopredelyaet window::get_input()
};
Vse eti opredeleniya sobirayutsya vmeste v proizvodnom klasse sleduyushchego
urovnya:
class window_w_banner_and_menu
: public virtual window,
public window_w_banner,
public window_w_menu
{
void draw();
};
Kontrol' neodnoznachnosti pozvolyaet ubedit'sya, chto v klassah-brat'yah
opredeleny raznye funkcii:
class window_w_input : public virtual window {
// ...
void draw();
void get_input(); // pereopredelyaet window::get_input
};
class window_w_input_and_menu
: public virtual window,
public window_w_input,
public window_w_menu
{ // oshibka: oba klassa window_w_input i
// window_w_menu pereopredelyayut funkciyu
// window::get_input
void draw();
};
Translyator obnaruzhivaet podobnuyu oshibku, a ustranit' neodnoznachnost'
mozhno obychnym sposobom: vvesti v klassy window_w_input i
window_w_menu funkciyu, pereopredelyayushchuyu "funkciyu-narushitelya", i
kakim-to obrazom ustranit' neodnoznachnost':
class window_w_input_and_menu
: public virtual window,
public window_w_input,
public window_w_menu
{
void draw();
void get_input();
};
V etom klasse window_w_input_and_menu::get_input() budet
pereopredelyat' vse funkcii get_input(). Podrobno mehanizm razresheniya
neodnoznachnosti opisan v $$R.10.1.1.
CHlen klassa mozhet byt' chastnym (private), zashchishchennym (protected)
ili obshchim (public):
CHastnyj chlen klassa X mogut ispol'zovat' tol'ko funkcii-chleny i
druz'ya klassa X.
Zashchishchennyj chlen klassa X mogut ispol'zovat' tol'ko funkcii-chleny
i druz'ya klassa X, a tak zhe funkcii-chleny i druz'ya vseh
proizvodnyh ot X klassov (sm. $$5.4.1).
Obshchij chlen mozhno ispol'zovat' v lyuboj funkcii.
|ti pravila sootvetstvuyut deleniyu obrashchayushchihsya k klassu funkcij na tri
vida: funkcii, realizuyushchie klass (ego druz'ya i chleny), funkcii,
realizuyushchie proizvodnyj klass (druz'ya i chleny proizvodnogo klassa) i
vse ostal'nye funkcii.
Kontrol' dostupa primenyaetsya edinoobrazno ko vsem imenam. Na
kontrol' dostupa ne vliyaet, kakuyu imenno sushchnost' oboznachaet imya.
|to oznachaet, chto chastnymi mogut byt' funkcii-chleny, konstanty i t.d.
naravne s chastnymi chlenami, predstavlyayushchimi dannye:
class X {
private:
enum { A, B };
void f(int);
int a;
};
void X::f(int i)
{
if (i<A) f(i+B);
a++;
}
void g(X& x)
{
int i = X::A; // oshibka: X::A chastnyj chlen
x.f(2); // oshibka: X::f chastnyj chlen
x.a++; // oshibka: X::a chastnyj chlen
}
6.6.1 Zashchishchennye chleny
Dadim primer zashchishchennyh chlenov, vernuvshis' k klassu window iz
predydushchego razdela. Zdes' funkcii _draw() prednaznachalis' tol'ko dlya
ispol'zovaniya v proizvodnyh klassah, poskol'ku predostavlyali nepolnyj
nabor vozmozhnostej, a poetomu ne byli dostatochny udobny i
nadezhny dlya obshchego primeneniya. Oni byli kak by stroitel'nym
materialom dlya bolee razvityh funkcij. S drugoj storony, funkcii draw()
prednaznachalis' dlya obshchego primeneniya. |to razlichie mozhno vyrazit',
razbiv interfejsy klassov window na dve chasti - zashchishchennyj interfejs
i obshchij interfejs:
class window {
public:
virtual void draw();
// ...
protected:
void _draw();
// drugie funkcii, sluzhashchie stroitel'nym materialom
private:
// predstavlenie klassa
};
Takoe razbienie mozhno provodit' i v proizvodnyh klassah, takih, kak
window_w_border ili window_w_menu.
Prefiks _ ispol'zuetsya v imenah zashchishchennyh funkcij, yavlyayushchihsya
chast'yu realizacii klassa, po obshchemu pravilu: imena, nachinayushchiesya s _ ,
ne dolzhny prisutstvovat' v chastyah programmy, otkrytyh dlya obshchego
ispol'zovaniya. Imen, nachinayushchihsya s dvojnogo simvola podcherkivaniya,
luchshe voobshche izbegat' (dazhe dlya chlenov).
Vot menee praktichnyj, no bolee podrobnyj primer:
class X {
// po umolchaniyu chastnaya chast' klassa
int priv;
protected:
int prot;
public:
int publ;
void m();
};
Dlya chlena X::m dostup k chlenam klassa neogranichen:
void X::m()
{
priv = 1; // normal'no
prot = 2; // normal'no
publ = 3; // normal'no
}
CHlen proizvodnogo klassa imeet dostup tol'ko k obshchim i zashchishchennym
chlenam:
class Y : public X {
void mderived();
};
Y::mderived()
{
priv = 1; // oshibka: priv chastnyj chlen
prot = 2; // normal'no: prot zashchishchennyj chlen, a
// mderived() chlen proizvodnogo klassa Y
publ = 3; // normal'no: publ obshchij chlen
}
V global'noj funkcii dostupny tol'ko obshchie chleny:
void f(Y* p)
{
p->priv = 1; // oshibka: priv chastnyj chlen
p->prot = 2; // oshibka: prot zashchishchennyj chlen, a f()
// ne drug ili chlen klassov X i Y
p->publ = 3; // normal'no: publ obshchij chlen
}
6.6.2 Dostup k bazovym klassam
Podobno chlenu bazovyj klass mozhno opisat' kak chastnyj, zashchishchennyj
ili obshchij:
class X {
public:
int a;
// ...
};
class Y1 : public X { };
class Y2 : protected X { };
class Y3 : private X { };
Poskol'ku X - obshchij bazovyj klass dlya Y1, v lyuboj funkcii, esli est'
neobhodimost', mozhno (neyavno) preobrazovat' Y1* v X*, i pritom
v nej budut dostupny obshchie chleny klassa X:
void f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // normal'no: X - obshchij bazovyj klass Y1
py1->a = 7; // normal'no
px = py2; // oshibka: X - zashchishchennyj bazovyj klass Y2
py2->a = 7; // oshibka
px = py3; // oshibka: X - chastnyj bazovyj klass Y3
py3->a = 7; // oshibka
}
Teper' pust' opisany
class Y2 : protected X { };
class Z2 : public Y2 { void f(); };
Poskol'ku X - zashchishchennyj bazovyj klass Y2, tol'ko druz'ya i chleny Y2,
a takzhe druz'ya i chleny lyubyh proizvodnyh ot Y2 klassov (v chastnosti
Z2) mogut pri neobhodimosti preobrazovyvat' (neyavno) Y2* v X*.
Krome togo oni mogut obrashchat'sya k obshchim i zashchishchennym chlenam klassa X:
void Z2::f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // normal'no: X - obshchij bazovyj klass Y1
py1->a = 7; // normal'no
px = py2; // normal'no: X - zashchishchennyj bazovyj klass Y2,
// a Z2 - proizvodnyj klass Y2
py2->a = 7; // normal'no
px = py3; // oshibka: X - chastnyj bazovyj klass Y3
py3->a = 7; // oshibka
}
Nakonec, rassmotrim:
class Y3 : private X { void f(); };
Poskol'ku X - chastnyj bazovyj klass Y3, tol'ko druz'ya i chleny Y3
mogut pri neobhodimosti preobrazovyvat' (neyavno) Y3* v X*.
Krome togo oni mogut obrashchat'sya k obshchim i zashchishchennym chlenam
klassa X:
void Y3::f(Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // normal'no: X - obshchij bazovyj klass Y1
py1->a = 7; // normal'no
px = py2; // oshibka: X - zashchishchennyj bazovyj klass Y2
py2->a = 7; // oshibka
px = py3; // normal'no: X - chastnyj bazovyj klass Y3,
// a Y3::f chlen Y3
py3->a = 7; // normal'no
}
Esli opredelit' funkcii operator new() i operator delete(),
upravlenie pamyat'yu dlya klassa mozhno vzyat' v svoi ruki. |to takzhe mozhno,
(a chasto i bolee polezno), sdelat' dlya klassa, sluzhashchego bazovym
dlya mnogih proizvodnyh klassov. Dopustim, nam potrebovalis' svoi
funkcii razmeshcheniya i osvobozhdeniya pamyati dlya klassa employee ($$6.2.5)
i vseh ego proizvodnyh klassov:
class employee {
// ...
public:
void* operator new(size_t);
void operator delete(void*, size_t);
};
void* employee::operator new(size_t s)
{
// otvesti pamyat' v `s' bajtov
// i vozvratit' ukazatel' na nee
}
void employee::operator delete(void* p, size_t s)
{
// `p' dolzhno ukazyvat' na pamyat' v `s' bajtov,
// otvedennuyu funkciej employee::operator new();
// osvobodit' etu pamyat' dlya povtornogo ispol'zovaniya
}
Naznachenie do sej pory zagadochnogo parametra tipa size_t stanovitsya
ochevidnym. |to - razmer osvobozhdaemogo ob容kta. Pri udalenii prostogo
sluzhashchego etot parametr poluchaet znachenie sizeof(employee), a pri
udalenii upravlyayushchego - sizeof(manager). Poetomu sobstvennye
funkcii klassy dlya razmeshcheniya mogut ne hranit' razmer kazhdogo
razmeshchaemogo ob容kta. Konechno, oni mogut hranit' eti razmery (podobno
funkciyam razmeshcheniya obshchego naznacheniya) i ignorirovat' parametr
size_t v vyzove operator delete(), no togda vryad li oni budut luchshe,
chem funkcii razmeshcheniya i osvobozhdeniya obshchego naznacheniya.
Kak translyator opredelyaet nuzhnyj razmer, kotoryj nado peredat'
funkcii operator delete()? Poka tip, ukazannyj v operator delete(),
sootvetstvuet istinnomu tipu ob容kta, vse prosto; no rassmotrim
takoj primer:
class manager : public employee {
int level;
// ...
};
void f()
{
employee* p = new manager; // problema
delete p;
}
V etom sluchae translyator ne smozhet pravil'no opredelit' razmer. Kak
i v sluchae udaleniya massiva, nuzhna pomoshch' programmista. On dolzhen
opredelit' virtual'nyj destruktor v bazovom klasse employee:
class employee {
// ...
public:
// ...
void* operator new(size_t);
void operator delete(void*, size_t);
virtual ~employee();
};
Dazhe pustoj destruktor reshit nashu problemu:
employee::~employee() { }
Teper' osvobozhdenie pamyati budet proishodit' v destruktore (a v nem
razmer izvesten), a lyuboj proizvodnyj ot employee klass takzhe budet
vynuzhden opredelyat' svoj destruktor (tem samym budet ustanovlen
nuzhnyj razmer), esli tol'ko pol'zovatel' sam ne opredelit ego.
Teper' sleduyushchij primer projdet pravil'no:
void f()
{
employee* p = new manager; // teper' bez problem
delete p;
}
Razmeshchenie proishodit s pomoshch'yu (sozdannogo translyatorom) vyzova
employee::operator new(sizeof(manager))
a osvobozhdenie s pomoshch'yu vyzova
employee::operator delete(p,sizeof(manager))
Inymi slovami, esli nuzhno imet' korrektnye funkcii razmeshcheniya i
osvobozhdeniya dlya proizvodnyh klassov, nado libo opredelit'
virtual'nyj destruktor v bazovom klasse, libo ne ispol'zovat'
v funkcii osvobozhdeniya parametr size_t. Konechno, mozhno bylo
pri proektirovanii yazyka predusmotret' sredstva, osvobozhdayushchie
pol'zovatelya ot etoj problemy. No togda pol'zovatel' "osvobodilsya" by
i ot opredelennyh preimushchestv bolee optimal'noj, hotya i menee nadezhnoj
sistemy.
V obshchem sluchae, vsegda est' smysl opredelyat' virtual'nyj
destruktor dlya vseh klassov, kotorye dejstvitel'no ispol'zuyutsya kak
bazovye, t.e. s ob容ktami proizvodnyh klassov rabotayut i, vozmozhno,
udalyayut ih, cherez ukazatel' na bazovyj klass:
class X {
// ...
public:
// ...
virtual void f(); // v X est' virtual'naya funkciya, poetomu
// opredelyaem virtual'nyj destruktor
virtual ~X();
};
6.7.1 Virtual'nye konstruktory
Uznav o virtual'nyh destruktorah, estestvenno sprosit': "Mogut li
konstruktory to zhe byt' virtual'nymi?" Esli otvetit' korotko - net.
Mozhno dat' bolee dlinnyj otvet: "Net, no mozhno legko poluchit'
trebuemyj effekt".
Konstruktor ne mozhet byt' virtual'nym, poskol'ku dlya pravil'nogo
postroeniya ob容kta on dolzhen znat' ego istinnyj tip. Bolee togo,
konstruktor - ne sovsem obychnaya funkciya. On mozhet vzaimodejstvovat'
s funkciyami upravleniya pamyat'yu, chto nevozmozhno dlya obychnyh
funkcij. Ot obychnyh funkcij-chlenov on otlichaetsya eshche tem, chto
ne vyzyvaetsya dlya sushchestvuyushchih ob容ktov. Sledovatel'no nel'zya poluchit'
ukazatel' na konstruktor.
No eti ogranicheniya mozhno obojti, esli opredelit' funkciyu,
soderzhashchuyu vyzov konstruktora i vozvrashchayushchuyu postroennyj ob容kt.
|to udachno, poskol'ku neredko byvaet nuzhno sozdat' novyj ob容kt,
ne znaya ego istinnogo tipa. Naprimer, pri translyacii inogda
voznikaet neobhodimost' sdelat' kopiyu dereva, predstavlyayushchego
razbiraemoe vyrazhenie. V dereve mogut byt' uzly vyrazhenij raznyh
vidov. Dopustim, chto uzly, kotorye soderzhat povtoryayushchiesya v vyrazhenii
operacii, nuzhno kopirovat' tol'ko odin raz. Togda nam potrebuetsya
virtual'naya funkciya razmnozheniya dlya uzla vyrazheniya.
Kak pravilo "virtual'nye konstruktory" yavlyayutsya standartnymi
konstruktorami bez parametrov ili konstruktorami kopirovaniya,
parametrom kotoryh sluzhit tip rezul'tata:
class expr {
// ...
public:
expr(); // standartnyj konstruktor
virtual expr* new_expr() { return new expr(); }
};
Virtual'naya funkciya new_expr() prosto vozvrashchaet standartno
inicializirovannyj ob容kt tipa expr, razmeshchennyj v svobodnoj pamyati.
V proizvodnom klasse mozhno pereopredelit' funkciyu new_expr() tak,
chtoby ona vozvrashchala ob容kt etogo klassa:
class conditional : public expr {
// ...
public:
conditional(); // standartnyj konstruktor
expr* new_expr() { return new conditional(); }
};
|to oznachaet, chto, imeya ob容kt klassa expr, pol'zovatel' mozhet
sozdat' ob容kt v "tochnosti takogo zhe tipa":
void user(expr* p1, expr* p2)
{
expr* p3 = p1->new_expr();
expr* p4 = p2->new_expr();
// ...
}
Peremennym p3 i p4 prisvaivayutsya ukazateli neizvestnogo, no podhodyashchego
tipa.
Tem zhe sposobom mozhno opredelit' virtual'nyj konstruktor
kopirovaniya, nazyvaemyj operaciej razmnozheniya, no nado podojti
bolee tshchatel'no k specifike operacii kopirovaniya:
class expr {
// ...
expr* left;
expr* right;
public:
// ...
// kopirovat' `s' v `this'
inline void copy(expr* s);
// sozdat' kopiyu ob容kta, na kotoryj smotrit this
virtual expr* clone(int deep = 0);
};
Parametr deep pokazyvaet razlichie mezhdu kopirovaniem sobstvenno
ob容kta (poverhnostnoe kopirovanie) i kopirovaniem vsego poddereva,
kornem kotorogo sluzhit ob容kt (glubokoe kopirovanie). Standartnoe
znachenie 0 oznachaet poverhnostnoe kopirovanie.
Funkciyu clone() mozhno ispol'zovat', naprimer, tak:
void fct(expr* root)
{
expr* c1 = root->clone(1); // glubokoe kopirovanie
expr* c2 = root->clone(); // poverhnostnoe kopirovanie
// ...
}
YAvlyayas' virtual'noj, funkciya clone() sposobna razmnozhat' ob容kty
lyubogo proizvodnogo ot expr klassa.
Nastoyashchee kopirovanie mozhno opredelit' tak:
void expr::copy(expression* s, int deep)
{
if (deep == 0) { // kopiruem tol'ko chleny
*this = *s;
}
else { // projdemsya po ukazatelyam:
left = s->clone(1);
right = s->clone(1);
// ...
}
}
Funkciya expr::clone() budet vyzyvat'sya tol'ko dlya ob容ktov tipa
expr (no ne dlya proizvodnyh ot expr klassov), poetomu mozhno prosto
razmestit' v nej i vozvratit' iz nee ob容kt tipa expr, yavlyayushchijsya
sobstvennoj kopiej:
expr* expr::clone(int deep)
{
expr* r = new expr(); // stroim standartnoe vyrazhenie
r->copy(this,deep); // kopiruem `*this' v `r'
return r;
}
Takuyu funkciyu clone() mozhno ispol'zovat' dlya proizvodnyh ot expr
klassov, esli v nih ne poyavlyayutsya chleny-dannye (a eto kak raz
tipichnyj sluchaj):
class arithmetic : public expr {
// ...
// novyh chlenov-dannyh net =>
// mozhno ispol'zovat' uzhe opredelennuyu funkciyu clone
};
S drugoj storony, esli dobavleny chleny-dannye, to nuzhno opredelyat'
sobstvennuyu funkciyu clone():
class conditional : public expression {
expr* cond;
public:
inline void copy(cond* s, int deep = 0);
expr* clone(int deep = 0);
// ...
};
Funkcii copy() i clone() opredelyayutsya podobno svoim dvojnikam iz
expression:
expr* conditional::clone(int deep)
{
conditional* r = new conditional();
r->copy(this,deep);
return r;
}
void conditional::copy(expr* s, int deep)
{
if (deep == 0) {
*this = *s;
}
else {
expr::copy(s,1); // kopiruem chast' expr
cond = s->cond->clone(1);
}
}
Opredelenie poslednej funkcii pokazyvaet otlichie nastoyashchego
kopirovaniya v expr::copy() ot polnogo razmnozheniya v expr::clone()
(t.e. sozdaniya novogo ob容kta i kopirovaniya v nego). Prostoe
kopirovanie okazyvaetsya poleznym dlya opredeleniya bolee slozhnyh
operacij kopirovaniya i razmnozheniya. Razlichie mezhdu copy() i clone()
ekvivalentno razlichiyu mezhdu operaciej prisvaivaniya i konstruktorom
kopirovaniya ($$1.4.2) i ekvivalentno razlichiyu mezhdu funkciyami
_draw() i draw() ($$6.5.3). Otmetim, chto funkciya copy() ne yavlyaetsya
virtual'noj. Ej i ne nado byt' takovoj, poskol'ku virtual'na
vyzyvayushchaya ee funkciya clone(). Ochevidno, chto prostye operacii
kopirovaniya mozhno takzhe opredelyat' kak funkcii-podstanovki.
6.7.2 Ukazanie razmeshcheniya
Po umolchaniyu operaciya new sozdaet ukazannyj ej ob容kt v svobodnoj
pamyati. Kak byt', esli nado razmestit' ob容kt v opredelennom meste?
|togo mozhno dobit'sya pereopredeleniem operacii razmeshcheniya. Rassmotrim
prostoj klass:
class X {
// ...
public:
X(int);
// ...
};
Ob容kt mozhno razmestit' v lyubom meste, esli vvesti v funkciyu
razmeshcheniya dopolnitel'nye parametry:
// operaciya razmeshcheniya v ukazannom meste:
void* operator new(size_t, void* p) { return p; }
i zadav eti parametry dlya operacii new sleduyushchim obrazom:
char buffer[sizeof(X)];
void f(int i)
{
X* p = new(buffer) X(i); // razmestit' X v buffer
// ...
}
Funkciya operator new(), ispol'zuemaya operaciej new, vybiraetsya
soglasno pravilam sopostavleniya parametrov ($$R.13.2). Vse
funkcii operator new() dolzhny imet' pervym parametrom size_t.
Zadavaemyj etim parametrom razmer neyavno peredaetsya operaciej
new.
Opredelennaya nami funkciya operator new() s zadavaemym razmeshcheniem
yavlyaetsya samoj prostoj iz funkcij podobnogo roda. Mozhno privesti
drugoj primer funkcii razmeshcheniya, vydelyayushchej pamyat' iz nekotoroj
zadannoj oblasti:
class Arena {
// ...
virtual void* alloc(size_t) = 0;
virtual void free(void*) = 0;
};
void operator new(size_t sz, Arena* a)
{
return a.alloc(sz);
}
Teper' mozhno otvodit' pamyat' dlya ob容ktov proizvol'nyh tipov iz
razlichnyh oblastej (Arena):
extern Arena* Persistent; // postoyannaya pamyat'
extern Arena* Shared; // razdelyaemaya pamyat'
void g(int i)
{
X* p = new(Persistent) X(i); // X v postoyannoj pamyati
X* q = new(Shared) X(i); // X v razdelyaemoj pamyati
// ...
}
Esli my pomeshchaem ob容kt v oblast' pamyati, kotoraya neposredstvenno
ne upravlyaetsya standartnymi funkciyami raspredeleniya svobodnoj
pamyati, to nado pozabotit'sya o pravil'nom unichtozhenii ob容kta.
Osnovnym sredstvom zdes' yavlyaetsya yavnyj vyzov destruktora:
void h(X* p)
{
p->~X(); // vyzov destruktora
Persistent->free(p); // osvobozhdenie pamyati
}
Zametim, chto yavnyh vyzovov destruktorov, kak i global'nyh funkcij
razmeshcheniya special'nogo naznacheniya, sleduet, po vozmozhnosti,
izbegat'. Byvayut sluchai, kogda obojtis' bez nih trudno, no
novichok dolzhen trizhdy podumat', prezhde chem ispol'zovat' yavnyj
vyzov destruktora, i dolzhen snachala posovetovat'sya s bolee opytnym
kollegoj.
1. (*1) Pust' est' klass
class base {
public:
virtual void iam() { cout << "base\n"; }
};
Opredelite dva proizvodnyh ot base klassa i v kazhdom opredelite
funkciyu iam(), vydayushchuyu imya svoego klassa. Sozdajte ob容kty
etih klassov i vyzovite iam() dlya nih. Prisvojte adresa ob容ktov
proizvodnyh klassov ukazatelyu tipa base* i vyzovite iam() s
pomoshch'yu etih ukazatelej.
2. (*2) Realizujte primitivy upravleniya ekranom ($$6.4.1) razumnym
dlya vashej sistemy obrazom.
3. (*2) Opredelite klassy triangle (treugol'nik) i circle
(okruzhnost').
4. (*2) Opredelite funkciyu, risuyushchuyu otrezok pryamoj, soedinyayushchij
dve figury. Vnachale nado najti samye blizhajshie tochki figur, a
zatem soedinit' ih.
5. (*2) Izmenite primer s klassom shape tak, chtoby line bylo
proizvodnym klassom ot rectangle, ili naoborot.
6. (*2) Pust' est' klass
class char_vec {
int sz;
char element [1];
public:
static new_char_vec(int s);
char& operator[] (int i) { return element[i]; }
// ...
};
Opredelite funkciyu new_char_vec() dlya otvedeniya nepreryvnogo
uchastka pamyati dlya ob容ktov char_vec tak, chtoby elementy mozhno
bylo indeksirovat' kak massiv element[]. V kakom sluchae eta
funkciya vyzovet ser'eznye trudnosti?
7. (*1) Opishite struktury dannyh, kotorye nuzhny dlya
primera s klassom shape iz $$6.4, i ob座asnite, kak mozhet
vypolnyat'sya virtual'nyj vyzov.
8. (*1.5) Opishite struktury dannyh, kotorye nuzhny dlya primera
s klassom satellite iz $$6.5, i ob座asnite, kak mozhet vypolnyat'sya
virtual'nyj vyzov.
9. (*2) Opishite struktury dannyh, kotorye nuzhny dlya primera s
klassom window iz $$6.5.3, i ob座asnite, kak mozhet vypolnyat'sya
virtual'nyj vyzov.
10. (*2) Opishite klass graficheskih ob容ktov s naborom vozmozhnyh
operacij, kotoryj budet obshchim bazovym v biblioteke graficheskih
ob容ktov. Issledujte kakie-nibud' graficheskie biblioteki,
chtoby ponyat', kakie operacii nuzhny. Opredelite klass ob容ktov
bazy dannyh s naborom vozmozhnyh operacij, kotoryj budet
obshchim bazovym klassom ob容ktov, hranyashchihsya kak posledovatel'nost'
polej bazy dannyh. Issledujte kakie-nibud' bazy dannyh, chtoby
ponyat', kakie operacii nuzhny. Opredelite ob容kt graficheskoj
bazy dannyh, ispol'zuya ili ne ispol'zuya mnozhestvennoe
nasledovanie. Obsudite otnositel'nye plyusy i minusy oboih
reshenij.
11. (*2) Napishite variant funkcii clone() iz $$6.7.1, v kotorom
razmnozhaemyj ob容kt mozhet pomeshchat'sya v oblast' Arena
($$6.7.2), peredavaemuyu kak parametr. Realizujte prostoj
klass Arena kak proizvodnyj ot Arena.
12. (*2) Pust' est' klassy Circle (okruzhnost'), Square (kvadrat) i
Triangle (treugol'nik), proizvodnye ot klassa shape. Opredelite
funkciyu intersect() s dvumya parametrami tipa Shape*, kotoraya
vyzyvaet podhodyashchuyu funkciyu, chtoby vyyasnit', peresekayutsya li
zadannye dve figury. Dlya etogo v ukazannyh klassah nuzhno
opredelit' sootvetstvuyushchie virtual'nye funkcii. Ne trat'te
sily na funkciyu, kotoraya dejstvitel'no ustanavlivaet, chto
figury peresekayutsya, dobejtes' tol'ko pravil'noj
posledovatel'nosti vyzovov funkcij.
13. (*5) Razrabotajte i realizujte biblioteku dlya modelirovaniya,
upravlyaemogo sobytiyami. Podskazka: ispol'zujte <task.h>.
Tam uzhe ustarevshie funkcii i mozhno napisat' luchshe. Dolzhen
byt' klass task (zadacha). Ob容kt task dolzhen umet' sohranyat'
svoe sostoyanie i vosstanavlivat' ego (dlya etogo mozhno
opredelit' funkcii task::save() i task::restore()) i togda
on mozhet dejstvovat' kak soprogramma. Special'nye zadachi
mozhno opredelyat' kak ob容kty klassov, proizvodnyh ot task.
Programmu, kotoruyu vypolnyaet zadacha, opredelite kak
virtual'nuyu funkciyu. Dolzhna byt' vozmozhnost' peredavat'
parametry novoj zadache kak parametry ee konstruktoru ili
konstruktoram. Dolzhen byt' dispetcher, kotoryj realizuet
ponyatie virtual'nogo vremeni. Opredelite funkciyu
task::delay(long), kotoraya budet "s容dat'" virtual'noe
vremya. Vazhnyj vopros razrabotki: yavlyaetsya li
dispetcher chast'yu klassa task, ili on dolzhen byt' nezavisimym?
Zadachi dolzhny imet' vozmozhnost' obshcheniya drug s drugom.
Dlya etoj celi razrabotajte klass queue (ochered'). Pridumajte
sposob, chtoby zadacha mogla ozhidat' vhodnoj potok iz neskol'kih
ocheredej. Vse dinamicheskie oshibki dolzhny obrabatyvat'sya
edinoobrazno. Kak organizovat' otladku programm, napisannyh
s pomoshch'yu takoj biblioteki?
Esli ya vybirayu slovo, ono znachit tol'ko to,
chto ya reshu, ni bol'she i ni men'she.
- SHaltaj Boltaj
Glava soderzhit opisanie mehanizma peregruzki operacij v S++.
Programmist mozhet zadat' interpretaciyu operacij, kogda oni
primenyayutsya k ob容ktam opredelennogo klassa. Pomimo arifmeticheskih,
logicheskih i operacij otnosheniya mozhno pereopredelit' vyzov
funkcij (), indeksaciyu [], kosvennoe obrashchenie ->, a takzhe
prisvaivanie i inicializaciyu. Mozhno opredelit' yavnye i skrytye
preobrazovaniya mezhdu pol'zovatel'skimi i osnovnymi tipami. Pokazano,
kak opredelit' klass, ob容kt kotorogo mozhno kopirovat' i
unichtozhat' tol'ko s pomoshch'yu special'nyh, opredelennyh pol'zovatelem
funkcij.
Obychno v programmah ispol'zuyutsya ob容kty, yavlyayushchiesya konkretnym
predstavleniem abstraktnyh ponyatij. Naprimer, v S++ tip dannyh int
vmeste s operaciyami +, -, *, / i t.d. realizuet (hotya i ogranichenno)
matematicheskoe ponyatie celogo. Obychno s ponyatiem svyazyvaetsya nabor
dejstvij, kotorye realizuyutsya v yazyke v vide osnovnyh operacij nad
ob容ktami, zadavaemyh v szhatom, udobnom i privychnom vide.
K sozhaleniyu, v yazykah programmirovaniya neposredstvenno predstavlyaetsya
tol'ko maloe chislo ponyatij. Tak, ponyatiya kompleksnyh chisel, algebry
matric, logicheskih signalov i strok v S++ ne imeyut neposredstvennogo
vyrazheniya. Vozmozhnost' zadat' predstavlenie slozhnyh ob容ktov vmeste
s naborom operacij, vypolnyaemyh nad takimi ob容ktami,
realizuyut v S++ klassy. Pozvolyaya programmistu opredelyat' operacii
nad ob容ktami klassov, my poluchaem bolee udobnuyu i tradicionnuyu
sistemu oboznachenij dlya raboty s etimi ob容ktami po sravneniyu s toj,
v kotoroj vse operacii zadayutsya kak obychnye funkcii. Privedem primer:
class complex {
double re, im;
public:
complex(double r, double i) { re=r; im=i; }
friend complex operator+(complex, complex);
friend complex operator*(complex, complex);
};
Zdes' privedena prostaya realizaciya ponyatiya kompleksnogo chisla, kogda
ono predstavleno paroj chisel s plavayushchej tochkoj dvojnoj tochnosti,
s kotorymi mozhno operirovat' tol'ko s pomoshch'yu operacij + i *.
Interpretaciyu etih operacij zadaet programmist v opredeleniyah funkcij
s imenami operator+ i operator*. Tak, esli b i c imeyut tip complex,
to b+c oznachaet (po opredeleniyu) operator+(b,c). Teper' mozhno
priblizit'sya k privychnoj zapisi kompleksnyh vyrazhenij:
void f()
{
complex a = complex(1,3.1);
complex b = complex(1.2,2);
complex c = b;
a = b+c;
b = b+c*a;
c = a*b+complex(1,2);
}
Sohranyayutsya obychnye prioritety operacij, poetomu vtoroe vyrazhenie
vypolnyaetsya kak b=b+(c*a), a ne kak b=(b+c)*a.
Mozhno opisat' funkcii, opredelyayushchie interpretaciyu sleduyushchih operacij:
+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
|= << >> >>= <<= == != <= >= &&
|| ++ -- ->* , -> [] () new delete
Poslednie pyat' operacij oznachayut: kosvennoe obrashchenie ($$7.9),
indeksaciyu ($$7.7), vyzov funkcii ($$7.8), razmeshchenie v svobodnoj
pamyati i osvobozhdenie ($$3.2.6). Nel'zya izmenit' prioritety etih
operacij, ravno kak i sintaksicheskie pravila dlya vyrazhenij. Tak,
nel'zya opredelit' unarnuyu operaciyu % , takzhe kak i binarnuyu operaciyu
!. Nel'zya vvesti novye leksemy dlya oboznacheniya operacij, no esli
nabor operacij vas ne ustraivaet, mozhno vospol'zovat'sya privychnym
oboznacheniem vyzova funkcii. Poetomu ispol'zujte pow(), a ne ** .
|ti ogranicheniya mozhno schest' drakonovskimi, no bolee svobodnye
pravila legko privodyat k neodnoznachnosti. Dopustim, my opredelim
operaciyu ** kak vozvedenie v stepen', chto na pervyj vzglyad kazhetsya
ochevidnoj i prostoj zadachej. No esli kak sleduet podumat', to
voznikayut voprosy: dolzhny li operacii ** vypolnyat'sya sleva napravo
(kak v Fortrane) ili sprava nalevo (kak v Algole)? Kak
interpretirovat' vyrazhenie a**p kak a*(*p) ili kak (a)**(p)?
Imenem operatornoj funkcii yavlyaetsya sluzhebnoe slovo operator, za
kotorym idet sama operaciya, naprimer, operator<<. Operatornaya
funkciya opisyvaetsya i vyzyvaetsya kak obychnaya funkciya. Ispol'zovanie
simvola operacii yavlyaetsya prosto kratkoj formoj zapisi vyzova
operatornoj funkcii:
void f(complex a, complex b)
{
complex c = a + b; // kratkaya forma
complex d = operator+(a,b); // yavnyj vyzov
}
S uchetom privedennogo opisaniya tipa complex inicializatory v etom
primere yavlyayutsya ekvivalentnymi.
7.2.1 Binarnye i unarnye operacii
Binarnuyu operaciyu mozhno opredelit' kak funkciyu-chlen s odnim
parametrom, ili kak global'nuyu funkciyu s dvumya parametrami. Znachit,
dlya lyuboj binarnoj operacii @ vyrazhenie aa @ bb interpretiruetsya
libo kak aa.operator(bb), libo kak operator@(aa,bb). Esli opredeleny obe
funkcii, to vybor interpretacii proishodit po pravilam sopostavleniya
parametrov ($$R.13.2). Prefiksnaya ili postfiksnaya unarnaya operaciya
mozhet opredelyat'sya kak funkciya-chlen bez parametrov, ili kak global'naya
funkciya s odnimi parametrom. Dlya lyuboj prefiksnoj unarnoj operacii
@ vyrazhenie @aa interpretiruetsya libo kak aa.operator@(), libo kak
operator@(aa). Esli opredeleny obe funkcii, to vybor interpretacii
proishodit po pravilam sopostavleniya parametrov ($$R.13.2). Dlya
lyuboj postfiksnoj unarnoj operacii @ vyrazhenie @aa interpretiruetsya
libo kak aa.operator@(int), libo kak operator@(aa,int). Podrobno
eto ob座asnyaetsya v $$7.10. Esli opredeleny obe funkcii, to vybor
interpretacii proishodit po pravilam sopostavleniya parametrov
($$13.2). Operaciyu mozhno opredelit' tol'ko v sootvetstvii s
sintaksicheskimi pravilami, imeyushchimisya dlya nee v grammatike S++.
V chastnosti, nel'zya opredelit' % kak unarnuyu operaciyu, a + kak
ternarnuyu. Proillyustriruem skazannoe primerami:
class X {
// chleny (neyavno ispol'zuetsya ukazatel' `this'):
X* operator&(); // prefiksnaya unarnaya operaciya &
// (vzyatie adresa)
X operator&(X); // binarnaya operaciya & (I porazryadnoe)
X operator++(int); // postfiksnyj inkrement
X operator&(X,X); // oshibka: & ne mozhet byt' ternarnoj
X operator/(); // oshibka: / ne mozhet byt' unarnoj
};
// global'nye funkcii (obychno druz'ya)
X operator-(X); // prefiksnyj unarnyj minus
X operator-(X,X); // binarnyj minus
X operator--(X&,int); // postfiksnyj inkrement
X operator-(); // oshibka: net operanda
X operator-(X,X,X); // oshibka: ternarnaya operaciya
X operator%(X); // oshibka: unarnaya operaciya %
Operaciya [] opisyvaetsya v $$7.7, operaciya () v $$7.8, operaciya ->
v $$7.9, a operacii ++ i -- v $$7.10.
7.2.2 Predopredelennye svojstva operacij
Ispol'zuetsya tol'ko neskol'ko predpolozhenij o svojstvah pol'zovatel'skih
operacij. V chastnosti, operator=, operator[], operator() i
operator-> dolzhny byt' nestaticheskimi funkciyami-chlenami. |tim
obespechivaetsya to, chto pervyj operand etih operacij yavlyaetsya adresom.
Dlya nekotoryh vstroennyh operacij ih interpretaciya opredelyaetsya
kak kombinaciya drugih operacij, vypolnyaemyh nad temi zhe operandami.
Tak, esli a tipa int, to ++a oznachaet a+=1, chto v svoyu ochered'
oznachaet a=a+1. Takie sootnosheniya ne sohranyayutsya dlya pol'zovatel'skih
operacij, esli tol'ko pol'zovatel' special'no ne opredelil ih s takoj
cel'yu. Tak, opredelenie operator+=() dlya tipa complex nel'zya vyvesti
iz opredelenij complex::operator+() i complex operator=().
Po istoricheskoj sluchajnosti okazalos', chto operacii = (prisvaivanie),
&(vzyatie adresa) i , (operaciya zapyataya) obladayut predopredelennymi
svojstvami dlya ob容ktov klassov. No mozhno zakryt' ot proizvol'nogo
pol'zovatelya eti svojstva, esli opisat' eti operacii kak chastnye:
class X {
// ...
private:
void operator=(const X&);
void operator&();
void operator,(const X&);
// ...
};
void f(X a, X b)
{
a= b; // oshibka: operaciya = chastnaya
&a; // oshibka: operaciya & chastnaya
a,b // oshibka: operaciya , chastnaya
}
S drugoj storony, mozhno naoborot pridat' s pomoshch'yu sootvetstvuyushchih
opredelenij etim operaciyam inoe znachenie.
7.2.3 Operatornye funkcii i pol'zovatel'skie tipy
Operatornaya funkciya dolzhna byt' libo chlenom, libo imet' po krajnej
mere odin parametr, yavlyayushchijsya ob容ktom klassa (dlya funkcij,
pereopredelyayushchih operacii new i delete, eto ne obyazatel'no). |to
pravilo garantiruet, chto pol'zovatel' ne sumeet izmenit'
interpretaciyu vyrazhenij, ne soderzhashchih ob容ktov pol'zovatel'skogo
tipa. V chastnosti, nel'zya opredelit' operatornuyu funkciyu, rabotayushchuyu
tol'ko s ukazatelyami. |tim garantiruetsya, chto v S++ vozmozhny
rasshireniya, no ne mutacii (ne schitaya operacij =, &, i , dlya ob容ktov
klassa).
Operatornaya funkciya, imeyushchaya pervym parametr osnovnogo 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>).
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().
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.
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.
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.
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).
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.
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 ssylok
srep() { n = 1; }
};
srep *p;
public:
string(const char *); // string x = "abc"
string(); // string x;
string(const string &); // string x = string ...
string& operator=(const char *);
string& operator=(const string &);
~string();
char& operator[](int i);
friend ostream& operator<<(ostream&, const string&);
friend istream& operator>>(istream&, string&);
friend int operator==(const string &x, const char *s)
{ return strcmp(x.p->s,s) == 0; }
friend int operator==(const string &x, const string &y)
{ return strcmp(x.p->s,y.p->s) == 0; }
friend int operator!=(const string &x, const char *s)
{ return strcmp(x.p->s,s) != 0; }
friend int operator!=(const string &x, const string &y)
{ return strcmp(x.p->s,y.p->s) != 0; }
};
Konstruktory i destruktory trivial'ny:
string::string()
{
p = new srep;
p->s = 0;
}
string::string(const string& x)
{
x.p->n++;
p = x.p;
}
string::string(const char* s)
{
p = new srep;
p->s = new char[ strlen(s)+1 ];
strcpy(p->s, s);
}
string::~string()
{
if (--p->n == 0) {
delete[] p->s;
delete p;
}
}
Kak i vsegda operacii prisvaivaniya pohozhi na konstruktory. V nih
nuzhno pozabotit'sya ob udalenii pervogo operanda, zadayushchego levuyu
chast' prisvaivaniya:
string& string::operator=(const char* s)
{
if (p->n > 1) { // otsoedinyaemsya ot staroj stroki
p->n--;
p = new srep;
}
else // osvobozhdaem stroku so starym znacheniem
delete[] p->s;
p->s = new char[ strlen(s)+1 ];
strcpy(p->s, s);
return *this;
}
string& string::operator=(const string& x)
{
x.p->n++; // zashchita ot sluchaya ``st = st''
if (--p->n == 0) {
delete[] p->s;
delete p
}
p = x.p;
return *this;
}
Operaciya vyvoda pokazyvaet kak ispol'zuetsya schetchik chisla ssylok.
Ona soprovozhdaet kak eho kazhduyu vvedennuyu stroku (vvod proishodit
s pomoshch'yu operacii << , privedennoj nizhe):
ostream& operator<<(ostream& s, const string& x)
{
return s << x.p->s << " [" << x.p->n << "]\n";
}
Operaciya vvoda proishodit s pomoshch'yu standartnoj funkcii vvoda
simvol'noj stroki ($$10.3.1):
istream& operator>>(istream& s, string& x)
{
char buf[256];
s >> buf; // nenadezhno: vozmozhno perepolnenie buf
// pravil'noe reshenie sm. v $$10.3.1
x = buf;
cout << "echo: " << x << '\n';
return s;
}
Operaciya indeksacii nuzhna dlya dostupa k otdel'nym simvolam.
Indeks kontroliruetsya:
void error(const char* p)
{
cerr << p << '\n';
exit(1);
}
char& string::operator[](int i)
{
if (i<0 || strlen(p->s)<i) error("nedopustimoe znachenie indeksa");
return p->s[i];
}
V osnovnoj programme prosto dany neskol'ko primerov primeneniya
strokovyh operacij. Slova iz vhodnogo potoka chitayutsya v stroki,
a zatem stroki pechatayutsya. |to prodolzhaetsya do teh por, poka ne
budet obnaruzhena stroka done, ili zakonchatsya stroki dlya zapisi
slov, ili zakonchitsya vhodnoj potok. Zatem pechatayutsya vse stroki
v obratnom poryadke i programma zavershaetsya.
int main()
{
string x[100];
int n;
cout << " zdes' nachalo \n";
for ( n = 0; cin>>x[n]; n++) {
if (n==100) {
error("slishkom mnogo slov");
return 99;
}
string y;
cout << (y = x[n]);
if (y == "done") break;
}
cout << "teper' my idem po slovam v obratnom poryadke \n";
for (int i=n-1; 0<=i; i--) cout << x[i];
return 0;
}
V zaklyuchenii mozhno obsudit', kogda pri obrashchenii v zakrytuyu chast'
pol'zovatel'skogo tipa stoit ispol'zovat' funkcii-chleny, a kogda
funkcii-druz'ya. Nekotorye funkcii, naprimer konstruktory, destruktory
i virtual'nye funkcii ($$R.12), obyazany byt' chlenami, no dlya drugih
est' vozmozhnost' vybora. Poskol'ku, opisyvaya funkciyu kak chlen, my
ne vvodim novogo global'nogo imeni, pri otsutstvii drugih dovodov
sleduet ispol'zovat' funkcii-chleny.
Rassmotrim prostoj klass X:
class X {
// ...
X(int);
int m1();
int m2() const;
friend int f1(X&);
friend int f2(const X&);
friend int f3(X);
};
Vnachale ukazhem, chto chleny X::m1() i X::m2() mozhno vyzyvat' tol'ko
dlya ob容ktov klassa X. Preobrazovanie X(int) ne budet primenyat'sya
k ob容ktu, dlya kotorogo vyzvany X::m1() ili X::m2():
void g()
{
1.m1(); // oshibka: X(1).m1() ne ispol'zuetsya
1.m2(); // oshibka: X(1).m2() ne ispol'zuetsya
}
Global'naya funkciya f1() imeet to zhe svojstvo ($$4.6.3), poskol'ku
ee parametr - ssylka bez specifikacii const. S funkciyami f2() i
f3() situaciya inaya:
void h()
{
f1(1); // oshibka: f1(X(1)) ne ispol'zuetsya
f2(1); // normal'no: f2(X(1));
f3(1); // normal'no: f3(X(1));
}
Sledovatel'no operaciya, izmenyayushchaya sostoyanie ob容kta klassa,
dolzhna byt' chlenom ili global'noj funkciej s parametrom-ssylkoj
bez specifikacii const. Operacii nad osnovnymi tipami, kotorye
trebuyut v kachestve operandov adresa (=, *, ++ i t.d.),
dlya pol'zovatel'skih tipov estestvenno opredelyat' kak chleny.
Obratno, esli trebuetsya neyavnoe preobrazovanie tipa dlya vseh
operandov nekotoroj operacii, to realizuyushchaya ee funkciya dolzhna
byt' ne chlenom, a global'noj funkciej i imet' parametr tipa ssylki
so specifikaciej const ili nessylochnyj parametr. Tak obychno obstoit
delo s funkciyami, realizuyushchimi operacii, kotorye dlya osnovnyh
tipov ne trebuyut adresov v kachestve operandov (+, -, || i t.d.).
Esli operacii preobrazovaniya tipa ne opredeleny, to net
neoproverzhimyh dovodov v pol'zu funkcii-chlena pered funkciej-drugom
s parametrom-ssylkoj i naoborot. Byvaet, chto programmistu prosto
odna forma zapisi vyzova nravitsya bol'she, chem drugaya.
Naprimer, mnogim dlya oboznacheniya funkcii obrashcheniya matricy m bol'she
nravitsya zapis' inv(m), chem m.inv(). Konechno, esli funkciya
inv() obrashchaet samu matricu m, a ne vozvrashchaet novuyu, obratnuyu m,
matricu, to inv() dolzhna byt' chlenom.
Pri vseh prochih ravnyh usloviyah luchshe vse-taki ostanovit'sya
na funkcii-chlene. Mozhno privesti takie dovody. Nel'zya garantirovat',
chto kogda-nibud' ne budet opredelena operaciya obrashcheniya. Nel'zya vo
vseh sluchayah garantirovat', chto budushchie izmeneniya ne povlekut za
soboj izmeneniya v sostoyanii ob容kta. Zapis' vyzova funkcii-chlena
yasno pokazyvaet programmistu, chto ob容kt mozhet byt' izmenen, togda
kak zapis' s parametrom-ssylkoj daleko ne stol' ochevidna. Dalee,
vyrazheniya dopustimye v funkcii-chlene mogut byt' sushchestvenno
koroche ekvivalentnyh vyrazhenij v global'noj funkcii. Global'naya
funkciya dolzhna ispol'zovat' yavno zadannye parametry, a v
funkcii-chlene mozhno neyavno ispol'zovat' ukazatel' this. Nakonec,
poskol'ku imena chlenov ne yavlyayutsya global'nymi imenami, oni obychno
okazyvayutsya koroche, chem imen global'nyh funkcij.
Kak i vsyakoe drugoe yazykovoe sredstvo, peregruzka operacij mozhet
ispol'zovat'sya razumno i nerazumno. V chastnosti, vozmozhnost'yu
pridavat' novyj smysl obychnym operaciyam mozhno vospol'zovat'sya
tak, chto programma budet sovershenno nepostizhimoj. Predstav'te,
kakovo budet chitatelyu, esli v svoej programme vy pereopredelili
operaciyu + tak, chtoby ona oboznachala vychitanie. Opisannyj zdes'
mehanizm peregruzki budet zashchishchat' programmista i pol'zovatelya ot
takih bezrassudstv. Poetomu programmist ne mozhet izmenit' ni
smysl operacij nad osnovnymi tipami dannyh, takimi, kak int, ni
sintaksis vyrazhenij i prioritety operacij dlya nih.
Po vsej vidimosti peregruzku operacij imeet smysl ispol'zovat'
dlya podrazhaniya tradicionnomu ispol'zovaniyu operacij. Zapis' s obychnym
vyzovom funkcii mozhno ispol'zovat' v teh sluchayah, kogda tradicionnoj
zapisi s bazovoj operaciej ne sushchestvuet, ili, kogda nabor operacij,
kotorye dopuskayut peregruzku, ne dostatochen, chtoby zapisat' s ego
pomoshch'yu nuzhnye dejstviya.
1. (*2) Opredelite iterator dlya klassa string. Opredelite operaciyu
konkatenacii + i operaciyu += , znachashchuyu "dobavit' v konec stroki".
Kakie eshche operacii vy hoteli by i smogli opredelit' dlya etogo
klassa?
2. (*1.5) Opredelite dlya strokovogo klassa operaciyu vydeleniya podstroki
s pomoshch'yu peregruzki ().
3. (*3) Opredelite klass string takim obrazom, chtoby operaciyu
vydeleniya podstroki mozhno bylo primenyat' k levoj chasti
prisvaivaniya. Vnachale napishite variant, v kotorom stroku mozhno
prisvaivat' podstroke toj zhe dliny, a zatem variant s razlichnymi
dlinami strok.
4. (*2) Razrabotajte klass string takim obrazom, chtoby ob容kty
ego traktovalis' pri peredache parametrov i prisvaivanii kak
znacheniya, t.e. chtoby v klasse string kopirovalis' sami predstavleniya
strok, a ne tol'ko upravlyayushchie struktury.
5. (*3) Izmenite klass string iz predydushchego uprazhneniya tak, chtoby
stroki kopirovalis' tol'ko pri neobhodimosti. |to znachit, chto
nuzhno hranit' odno obshchee predstavleniya dvuh odinakovyh strok do
teh por, poka odna iz nih ne izmenitsya. Ne pytajtes' zadat' operaciyu
vydeleniya podstroki, kotoruyu odnovremenno mozhno primenyat' i k
levoj chasti prisvaivaniya.
6. (*4) Opredelite klass string, obladayushchij perechislennymi v
predydushchih uprazhneniyah svojstvami: ob容kty ego traktuyutsya kak
znacheniya, kopirovanie yavlyaetsya otlozhennym (t.e. proishodit tol'ko
pri neobhodimosti) i operaciyu vydeleniya podstroki mozhno primenyat'
k levoj chasti prisvaivaniya.
7. (*2) Kakie preobrazovaniya tipa ispol'zuyutsya v vyrazheniyah sleduyushchej
programmy?
struct X {
int i;
X(int);
operator+(int);
};
struct Y {
int i;
Y(X);
operator+(X);
operator int();
};
extern X operator*(X,Y);
extern int f(X);
X x = 1;
Y y = x;
int i = 2;
int main()
{
i + 10; y + 10; y + 10 * y;
x + y + i; x * X +i; f(7);
f(y); y + y; 106 + y;
}
Opredelite X i Y kak celye tipy. Izmenite programmu tak, chtoby
ee mozhno bylo vypolnit' i ona napechatala znacheniya vseh
pravil'nyh vyrazhenij.
8. (*2) Opredelite klass INT, kotoryj budet ekvivalenten tipu int.
Podskazka: opredelite funkciyu INT::operator int().
9. (*1) Opredelite klass RINT, kotoryj budet ekvivalenten tipu int,
za isklyucheniem togo, chto dopustimymi budut tol'ko operacii:
+ (unarnyj i binarnyj), - (unarnyj i binarnyj), *, / i %.
Podskazka: ne nado opredelyat' RINT::operator int().
10. (*3) Opredelite klass LINT, ekvivalentnyj klassu RINT, no v
nem dlya predstavleniya celogo dolzhno ispol'zovat'sya ne menee 64
razryadov.
11. (*4) Opredelite klass, realizuyushchij arifmetiku s proizvol'noj
tochnost'yu. Podskazka: Pridetsya ispol'zovat' pamyat' tak, kak
eto delaetsya v klasse string.
12. (*2) Napishite programmu, v kotoroj blagodarya makrokomandam i
peregruzke budet nevozmozhno razobrat'sya. Sovet: opredelite dlya
tipa INT + kak -, i naoborot; s pomoshch'yu makroopredeleniya zadajte
int kak INT. Krome togo, bol'shuyu putanicu mozhno sozdat',
pereopredelyaya shiroko izvestnye funkcii, i ispol'zuya parametry
tipa ssylki i zadavaya vvodyashchie v zabluzhdenie kommentarii.
13. (*3) Obmenyajtes' resheniyami uprazhneniya [12] s vashim drugom.
Poprobujte ponyat', chto delaet ego programma, ne zapuskaya ee. Esli
vy sdelaete eto uprazhnenie, vam stanet yasno, chego nado izbegat'.
14. (*2) Perepishite primery s klassami complex ($$7.3), tiny
($$7.3.2) i string ($$7.11), ne ispol'zuya druzhestvennye funkcii.
Ispol'zujte tol'ko funkcii-chleny. Prover'te novye versii etih
klassov. Sravnite ih s versiyami, v kotoryh ispol'zuyutsya
druzhestvennye funkcii. Obratites' k uprazhneniyu 5.3.
15. (*2) Opredelite tip vec4 kak vektor iz chetyreh chisel s plavayushchej
tochkoj. Opredelite dlya nego funkciyu operator[]. Dlya kombinacij
vektorov i chisel s plavayushchej tochkoj opredelite operacii:
+, -, *, /, =, +=, -=, *= i /=.
16. (*3) Opredelite klass mat4 kak vektor iz chetyreh elementov tipa
vec4. Opredelite dlya nego funkciyu operator[], vozvrashchayushchuyu vec4.
Opredelite dlya etogo tipa obychnye operacii s matricami. Opredelite
v mat4 funkciyu, proizvodyashchuyu preobrazovanie Gaussa s matricej.
17. (*2) Opredelite klass vector, analogichnyj klassu vec4, no zdes'
razmer vektora dolzhen zadavat'sya kak parametr konstruktora
vector::vector(int).
18. (*3) Opredelite klass matrix, analogichnyj klassu mat4, no zdes'
razmernosti matricy dolzhny zadavat'sya kak parametry konstruktora
matrix::matrix(int,int).
19. (*3) Zavershite opredelenie klassa CheckedPtrToT iz $$7.10 i
prover'te ego. CHtoby opredelenie etogo klassa bylo polnym,
neobhodimo opredelit', po krajnej mere, takie operacii: *, ->,
=, ++ i --. Ne vydavajte dinamicheskuyu oshibku, poka dejstvitel'no
ne proizojdet obrashchenie po ukazatelyu s neopredelennym znacheniem.
20. (*1.5) Perepishite primer s programmoj podscheta slov iz $$7.7
tak, chtoby v nej ne bylo zaranee zadannoj maksimal'noj dliny
slova.
Vot vasha citata
- B'ern Straustrup
V etoj glave vvoditsya ponyatie shablona tipa. S ego pomoshch'yu mozhno
dostatochno prosto opredelit' i realizovat' bez poter' v
effektivnosti vypolneniya programmy i, ne otkazyvayas' ot staticheskogo
kontrolya tipov, takie kontejnernye klassy, kak spiski i associativnye
massivy. Krome togo, shablony tipa pozvolyayut opredelit' srazu dlya
celogo semejstva tipov obobshchennye (genericheskie) funkcii, naprimer,
takie, kak sort (sortirovka). V kachestve primera shablona tipov i
ego svyazi s drugimi konstrukciyami yazyka privoditsya semejstvo
spisochnyh klassov. CHtoby pokazat' sposoby polucheniya programmy iz
v znachitel'noj stepeni nezavisimyh chastej, privoditsya neskol'ko
variantov shablonnoj funkcii sort(). V konce opredelyaetsya prostoj
shablon tipa dlya associativnogo massiva i pokazyvaetsya na dvuh
nebol'shih demonstracionnyh programmah, kak im pol'zovat'sya.
Odnim iz samyh poleznyh vidov klassov yavlyaetsya kontejnernyj klass,
t.e. takoj klass, kotoryj hranit ob容kty kakih-to drugih tipov.
Spiski, massivy, associativnye massivy i mnozhestva - vse eto
kontejnernye klassy. S pomoshch'yu opisannyh v glavah 5 i 7 sredstv
mozhno opredelit' klass, kak kontejner ob容ktov edinstvennogo,
izvestnogo tipa. Naprimer, v $$5.3.2 opredelyaetsya mnozhestvo celyh.
No kontejnernye klassy obladayut tem interesnym svojstvom, chto tip
soderzhashchihsya v nih ob容ktov ne imeet osobogo znacheniya dlya
sozdatelya kontejnera, no dlya pol'zovatelya konkretnogo kontejnera
etot tip yavlyaetsya sushchestvennym. Sledovatel'no, tip soderzhashchihsya
ob容ktov dolzhen parametrom kontejnernogo klassa, i sozdatel' takogo
klassa budet opredelyat' ego s pomoshch'yu tipa-parametra. Dlya kazhdogo
konkretnogo kontejnera (t.e. ob容kta kontejnernogo klassa) pol'zovatel'
budet ukazyvat' kakim dolzhen byt' tip soderzhashchihsya v nem ob容ktov.
Primerom takogo kontejnernogo klassa byl shablon tipa Vector iz
$$1.4.3.
V etoj glave issleduetsya prostoj shablon tipa stack (stek) i
v rezul'tate vvoditsya ponyatie shablonnogo klassa. Zatem rassmatrivayutsya
bolee polnye i pravdopodobnye primery neskol'kih rodstvennyh shablonov
tipa dlya spiska. Vvodyatsya shablonnye funkcii i formuliruyutsya pravila,
chto mozhet byt' parametrom takih funkcij. V konce privoditsya shablon
tipa dlya associativnogo massiva.
SHablon tipa dlya klassa zadaet sposob postroeniya otdel'nyh klassov,
podobno tomu, kak opisanie klassa zadaet sposob postroeniya ego
otdel'nyh ob容ktov. Mozhno opredelit' stek, soderzhashchij elementy
proizvol'nogo tipa:
template<class T>
class stack {
T* v;
T* p;
int sz;
public:
stack(int s) { v = p = new T[sz=s]; }
~stack() { delete[] v; }
void push(T a) { *p++ = a; }
T pop() { return *--p; }
int size() const { return p-v; }
};
Dlya prostoty ne uchityvalsya kontrol' dinamicheskih oshibok. Ne schitaya
etogo, primer polnyj i vpolne pravdopodobnyj.
Prefiks template<class T> ukazyvaet, chto opisyvaetsya shablon
tipa s parametrom T, oboznachayushchim tip, i chto eto oboznachenie
budet ispol'zovat'sya v posleduyushchem opisanii. Posle togo, kak
identifikator T ukazan v prefikse, ego mozhno ispol'zovat' kak lyuboe
drugoe imya tipa. Oblast' vidimosti T prodolzhaetsya do konca opisaniya,
nachavshegosya prefiksom template<class T>. Otmetim, chto v prefikse T
ob座avlyaetsya tipom, i ono ne obyazano byt' imenem klassa. Tak, nizhe
v opisanii ob容kta sc tip T okazyvaetsya prosto char.
Imya shablonnogo klassa, za kotorym sleduet tip, zaklyuchennyj v
uglovye skobki <>, yavlyaetsya imenem klassa (opredelyaemym shablonom
tipa), i ego mozhno ispol'zovat' kak vse imena klassa. Naprimer, nizhe
opredelyaetsya ob容kt sc klassa stack<char>:
stack<char> sc(100); // stek simvolov
Esli ne schitat' osobuyu formu zapisi imeni, klass stack<char>
polnost'yu ekvivalenten klassu opredelennomu tak:
class stack_char {
char* v;
char* p;
int sz;
public:
stack_char(int s) { v = p = new char[sz=s]; }
~stack_char() { delete[] v; }
void push(char a) { *p++ = a; }
char pop() { return *--p; }
int size() const { return p-v; }
};
Mozhno podumat', chto shablon tipa - eto hitroe makroopredelenie,
podchinyayushcheesya pravilam imenovaniya, tipov i oblastej vidimosti,
prinyatym v S++. |to, konechno, uproshchenie, no eto takoe uproshchenie,
kotoroe pomogaet izbezhat' bol'shih nedorazumenij. V chastnosti,
primenenie shablona tipa ne predpolagaet kakih-libo sredstv
dinamicheskoj podderzhki pomimo teh, kotorye ispol'zuyutsya dlya obychnyh
"ruchnyh" klassov. Ne sleduet tak zhe dumat', chto ono privodit k
sokrashcheniyu programmy.
Obychno imeet smysl vnachale otladit' konkretnyj klass, takoj,
naprimer, kak stack_char, prezhde, chem stroit' na ego osnove shablon tipa
stack<T>. S drugoj storony, dlya ponimaniya shablona tipa polezno
predstavit' sebe ego dejstvie na konkretnom tipe, naprimer int ili
shape*, prezhde, chem pytat'sya predstavit' ego vo vsej obshchnosti.
Imeya opredelenie shablonnogo klassa stack, mozhno sleduyushchim
obrazom opredelyat' i ispol'zovat' razlichnye steki:
stack<shape*> ssp(200); // stek ukazatelej na figury
stack<Point> sp(400); // stek struktur Point
void f(stack<complex>& sc) // parametr tipa `ssylka na
// complex'
{
sc.push(complex(1,2));
complex z = 2.5*sc.pop();
stack<int>*p = 0; // ukazatel' na stek celyh
p = new stack<int>(800); // stek celyh razmeshchaetsya
// v svobodnoj pamyati
for ( int i = 0; i<400; i++) {
p->push(i);
sp.push(Point(i,i+400));
}
// ...
}
Poskol'ku vse funkcii-chleny klassa stack yavlyayutsya podstanovkami,
i v etom primere translyator sozdaet vyzovy funkcij tol'ko dlya
razmeshcheniya v svobodnoj pamyati i osvobozhdeniya.
Funkcii v shablone tipa mogut i ne byt' podstanovkami, shablonnyj
klass stack s polnym pravom mozhno opredelit' i tak:
template<class T> class stack {
T* v;
T* p;
int sz;
public:
stack(int);
~stack();
void push(T);
T pop();
int size() const;
};
V etom sluchae opredelenie funkcii-chlena stack dolzhno byt' dano
gde-to v drugom meste, kak eto i bylo dlya funkcij- chlenov
obychnyh, neshablonnyh klassov. Podobnye funkcii tak zhe
parametriziruyutsya tipom, sluzhashchim parametrom dlya ih shablonnogo
klassa, poetomu opredelyayutsya oni s pomoshch'yu shablona tipa dlya
funkcii. Esli eto proishodit vne shablonnogo klassa, eto nado delat'
yavno:
template<class T> void stack<T>::push(T a)
{
*p++ = a;
}
template<class T> stack<T>::stack(int s)
{
v = p = new T[sz=s];
}
Otmetim, chto v predelah oblasti vidimosti imeni stack<T> utochnenie
<T> yavlyaetsya izbytochnym, i stack<T>::stack - imya konstruktora.
Zadacha sistemy programmirovaniya, a vovse ne programmista,
predostavlyat' versii shablonnyh funkcij dlya kazhdogo fakticheskogo
parametra shablona tipa. Poetomu dlya privodivshegosya vyshe primera
sistema programmirovaniya dolzhna sozdat' opredeleniya konstruktorov dlya
klassov stack<shape*>, stack<Point> i stack<int>, destruktorov dlya
stack<shape*> i stack<Point>, versii funkcij push() dlya stack<complex>,
stack<int> i stack<Point> i versiyu funkcii pop() dlya stack<complex>.
Takie sozdavaemye funkcii budut sovershenno obychnymi funkciyami-chlenami,
naprimer:
void stack<complex>::push(complex a) { *p++ = a; }
Zdes' otlichie ot obychnoj funkcii-chlena tol'ko v forme imeni klassa.
Tochno tak zhe, kak v programme mozhet byt' tol'ko odno opredelenie
funkcii-chlena klassa, vozmozhno tol'ko odno opredelenie shablona
tipa dlya funkcii-chlena shablonnogo klassa. Esli trebuetsya opredelenie
funkcii-chlena shablonnogo klassa dlya konkretnogo tipa, to zadacha sistemy
programmirovaniya najti shablon tipa dlya etoj funkcii-chlena i sozdat'
nuzhnuyu versiyu funkcii. V obshchem sluchae sistema programmirovaniya
mozhet rasschityvat' na ukazaniya ot programmista, kotorye pomogut
najti nuzhnyj shablon tipa.
Vazhno sostavlyat' opredelenie shablona tipa takim obrazom, chtoby
ego zavisimost' ot global'nyh dannyh byla minimal'noj. Delo v tom,
shablon tipa budet ispol'zovat'sya dlya porozhdeniya funkcij i klassov
na osnove zaranee neizvestnogo tipa i v neizvestnyh kontekstah.
Prakticheski lyubaya, dazhe slabaya zavisimost' ot konteksta mozhet
proyavit'sya kak problema pri otladke programmy pol'zovatelem, kotoryj,
veroyatnee vsego, ne byl sozdatelem shablona tipa. K sovetu izbegat',
naskol'ko eto vozmozhno, ispol'zovanij global'nyh imen, sleduet
otnosit'sya osobenno ser'ezno pri razrabotke shablona tipa.
8.3 SHablony tipa dlya spiska
Na praktike pri razrabotke klassa, sluzhashchego kollekciej ob容ktov,
chasto prihoditsya uchityvat' vzaimootnosheniya ispol'zuyushchihsya v realizacii
klassov, upravlenie pamyat'yu i neobhodimost' opredelit' iterator po
soderzhimomu kollekcii. CHasto byvaet tak, chto neskol'ko rodstvennyh
klassov razrabatyvayutsya sovmestno ($$12.2). V kachestve primera my
predlozhim semejstvo klassov, predstavlyayushchih odnosvyaznye spiski i
shablony tipa dlya nih.
8.3.1 Spisok s prinuditel'noj svyaz'yu
Vnachale opredelim prostoj spisok, v kotorom predpolagaetsya, chto
v kazhdom zanosimom v spisok ob容kte est' pole svyazi. Potom etot
spisok budet ispol'zovat'sya kak stroitel'nyj material dlya sozdaniya
bolee obshchih spiskov, v kotoryh ob容kt ne obyazan imet' pole svyazi.
Sperva v opisaniyah klassov budet privedena tol'ko obshchaya chast',
a realizaciya budet dana v sleduyushchem razdele. |to delaetsya za tem,
chtoby voprosy proektirovaniya klassov ne zatemnyalis' detalyami ih
realizacii.
Nachnem s tipa slink, opredelyayushchego pole svyazi v odnosvyaznom spiske:
struct slink {
slink* next;
slink() { next = 0; }
slink(slink* p) { next = p; }
};
Teper' mozhno opredelit' klass, kotoryj mozhet soderzhat' ob容kty
lyubogo, proizvodnogo ot slink, klassa:
class slist_base {
// ...
public:
int insert(slink*); // dobavit' v nachalo spiska
int append(slink*); // dobavit' k koncu spiska
slink* get(); // udalit' i vozvratit' nachalo spiska
// ...
};
Takoj klass mozhno nazvat' spiskom s prinuditel'noj svyaz'yu, poskol'ku
ego mozhno ispol'zovat' tol'ko v tom sluchae, kogda vse elementy imeyut
pole slink, kotoroe ispol'zuetsya kak ukazatel' na slist_base.
Samo imya slist_base (bazovyj odnosvyaznyj spisok) govorit, chto etot
klass budet ispol'zovat'sya kak bazovyj dlya odnosvyaznyh spisochnyh
klassov. Kak obychno, pri razrabotke semejstva rodstvennyh klassov
voznikaet vopros, kak vybirat' imena dlya razlichnyh chlenov semejstva.
Poskol'ku imena klassov ne mogut peregruzhat'sya, kak eto delaetsya
dlya imen funkcij, dlya obuzdaniya razmnozheniya imen peregruzka nam ne
pomozhet.
Klass slist_base mozhno ispol'zovat' tak:
void f()
{
slist_base slb;
slb.insert(new slink);
// ...
slink* p = slb.get();
// ...
delete p;
}
No poskol'ku struktura slink ne mozhet soderzhat' nikakoj informacii
pomimo svyazi, etot primer ne slishkom interesen. CHtoby vospol'zovat'sya
slist_base, nado opredelit' poleznyj, proizvodnyj ot slink, klass.
Naprimer, v translyatore ispol'zuyutsya uzly dereva programmy name
(imya), kotorye prihoditsya svyazyvat' v spisok:
class name : public slink {
// ...
};
void f(const char* s)
{
slist_base slb;
slb.insert(new name(s));
// ...
name* p = (name*)slb.get();
// ...
delete p;
}
Zdes' vse normal'no, no poskol'ku opredelenie klassa slist_base
dano cherez strukturu slink, prihoditsya ispol'zovat' yavnoe privedenie
tipa dlya preobrazovaniya znacheniya tipa slink*, vozvrashchaemogo
funkciej slist_base::get(), v name*. |to nekrasivo. Dlya bol'shoj
programmy, v kotoroj mnogo spiskov i proizvodnyh ot slink klassov,
eto k tomu zhe chrevato oshibkami. Nam prigodilas' by nadezhnaya po
tipu versiya klassa slist_base:
template<class T>
class Islist : private slist_base {
public:
void insert(T* a) { slist_base::insert(a); }
T* get() { return (T*) slist_base::get(); }
// ...
};
Privedenie v funkcii Islist::get() sovershenno opravdano i nadezhno,
poskol'ku v klasse Islist garantiruetsya, chto kazhdyj ob容kt v spiske
dejstvitel'no imeet tip T ili tip proizvodnogo ot T klassa. Otmetim,
chto slist_base yavlyaetsya chastnym bazovym klassom Islist. My net hotim,
chtoby pol'zovatel' sluchajno natolknulsya na nenadezhnye detali
realizacii.
Imya Islist (intrusive singly linked list) oboznachaet
odnosvyaznyj spisok s prinuditel'noj svyaz'yu. |tot shablon tipa
mozhno ispol'zovat' tak:
void f(const char* s)
{
Islist<name> ilst;
ilst.insert(new name(s));
// ...
name* p = ilst.get();
// ...
delete p
}
Popytki nekorrektnogo ispol'zovaniya budet vyyavleny na stadii
translyacii:
class expr : public slink {
// ...
};
void g(expr* e)
{
Islist<name> ilst;
ilst.insert(e); // oshibka: Islist<name>::insert(),
// a nuzhno name*
// ...
}
Nuzhno otmetit' neskol'ko vazhnyh momentov otnositel'no nashego primera.
Vo-pervyh, reshenie nadezhno v smysle tipov (pregrada trivial'nym
oshibkam stavitsya v ochen' ogranichennoj chasti programmy, a imenno,
v funkciyah dostupa iz Islist). Vo-vtoryh, nadezhnost' tipov
dostigaetsya bez uvelicheniya zatrat vremeni i pamyati, poskol'ku
funkcii dostupa iz Islist trivial'ny i realizuyutsya podstanovkoj.
V-tret'ih, poskol'ku vsya nastoyashchaya rabota so spiskom delaetsya v
realizacii klassa slist_base (poka eshche ne predstavlennoj), nikakogo
dublirovaniya funkcij ne proishodit, a ishodnyj tekst realizacii,
t.e. funkcii slist_base, voobshche ne dolzhen byt' dostupen pol'zovatelyu.
|to mozhet byt' sushchestvenno v kommercheskom ispol'zovanii sluzhebnyh
programm dlya spiskov. Krome togo, dostigaetsya razdelenie mezhdu
interfejsom i ego realizaciej, i stanovitsya vozmozhnoj smena realizacii
bez peretranslyacii programm pol'zovatelya. Nakonec, prostoj spisok
s prinuditel'noj svyaz'yu blizok po ispol'zovaniyu pamyati i vremeni
k optimal'nomu resheniyu. Inymi slovami, takoj podhod blizok k
optimal'nomu po vremeni, pamyati, upryatyvaniyu dannyh i kontrolyu
tipov i v tozhe vremya on obespechivaet bol'shuyu gibkost' i kompaktnost'
vyrazhenij.
K sozhaleniyu, ob容kt mozhet popast' v Islist tol'ko, esli on
yavlyaetsya proizvodnym ot slink. Znachit nel'zya imet' spisok Islist
iz znachenij tipa int, nel'zya sostavit' spisok iz znachenij kakogo-to
ranee opredelennogo tipa, ne yavlyayushchegosya proizvodnym ot slink.
Krome togo, pridetsya postarat'sya, chtoby vklyuchit' ob容kt v dva spiska
Islist ($$6.5.1).
8.3.2 Spisok bez prinuditel'noj svyazi
Posle "ekskursa" v voprosy postroeniya i ispol'zovaniya spiska s
prinuditel'noj svyaz'yu perejdem k postroeniyu spiskov bez prinuditel'noj
svyazi. |to znachit, chto elementy spiska ne obyazany soderzhat'
dopolnitel'nuyu informaciyu, pomogayushchuyu v realizacii spisochnogo klassa.
Poskol'ku my bol'she ne mozhem rasschityvat', chto ob容kt v spiske
imeet pole svyazi, takuyu svyaz' nado predusmotret' v realizacii:
template<class T>
struct Tlink : public slink {
T info;
Tlink(const T& a) : info(a) { }
};
Klass Tlink<T> hranit kopiyu ob容ktov tipa T pomimo polya svyazi, kotoroe
idet ot ego bazovogo klassa slink. Otmetim, chto ispol'zuetsya
inicializator v vide info(a), a ne prisvaivanie info=a. |to
sushchestvenno dlya effektivnosti operacii v sluchae tipov, imeyushchih
netrivial'nye konstruktory kopirovaniya i operacii prisvaivaniya
($$7.11). Dlya takih tipov (naprimer, dlya String) opredeliv konstruktor
kak
Tlink(const T& a) { info = a; }
my poluchim, chto budet stroit'sya standartnyj ob容kt String, a uzhe
zatem emu budet prisvaivat'sya znachenie.
Imeya klass, opredelyayushchij svyaz', i klass Islist, poluchit'
opredelenie spiska bez prinuditel'noj svyazi sovsem prosto:
template<class T>
class Slist : private slist_base {
public:
void insert(const T& a)
{ slist_base::insert(new Tlink<T>(a)); }
void append(const T& a)
{ slist_base::append(new Tlink<T>(a)); }
T get();
// ...
};
template<class T>
T Slist<T>::get()
{
Tlink<T>* lnk = (Tlink<T>*) slist_base::get();
T i = lnk->info;
delete lnk;
return i;
}
Rabotat' so spiskom Slist tak zhe prosto, kak i so spiskom Ilist.
Razlichie v tom, chto mozhno vklyuchat' v Slist ob容kt, klass kotorogo ne
yavlyaetsya proizvodnym ot slink, a takzhe mozhno vklyuchat' odin ob容kt
v dva spiska:
void f(int i)
{
Slist<int> lst1;
Slist<int> lst2;
lst1.insert(i);
lst2.insert(i);
// ...
int i1 = lst1.get();
int i2 = lst2.get();
// ...
}
Odnako, spisok s prinuditel'noj svyaz'yu, naprimer Islist, pozvolyal
sozdavat' sushchestvenno bolee effektivnuyu programmu i daval bolee
kompaktnoe predstavlenie. Dejstvitel'no, pri kazhdom vklyuchenii
ob容kta v spisok Slist nuzhno razmestit' ob容kt Tlink, a pri kazhdom
udalenii ob容kta iz Slist nuzhno udalit' ob容kt Tlink, prichem
kazhdyj raz kopiruetsya ob容kt tipa T. Kogda voznikaet takaya problema
dopolnitel'nyh rashodov, mogut pomoch' dva priema. Vo-pervyh,
Tlink yavlyaetsya pryamym kandidatom dlya razmeshcheniya s pomoshch'yu prakticheski
optimal'noj funkcii razmeshcheniya special'nogo naznacheniya (sm. $$5.5.6).
Togda dopolnitel'nye rashody pri vypolnenii programmy sokratyatsya
do obychno priemlemogo urovnya. Vo-vtoryh, poleznym okazyvaetsya takoj
priem, kogda ob容kty hranyatsya v "pervichnom" spiske, imeyushchim
prinuditel'nuyu svyaz', a spiski bez prinuditel'noj svyazi ispol'zuyutsya
tol'ko, kogda trebuetsya vklyuchenie ob容kta v neskol'ko spiskov:
void f(name* p)
{
Islist<name> lst1;
Slist<name*> lst2;
lst1.insert(p); // svyaz' cherez ob容kt `*p'
lst2.insert(p); // dlya hraneniya `p' ispol'zuetsya
// otdel'nyj ob容kt tipa spisok
// ...
}
Konechno, podobnye tryuki mozhno delat' tol'ko v otdel'nom komponente
programmy, chtoby ne dopustit' putanicy spisochnyh tipov v
interfejsah razlichnyh komponent. No eto imenno tot sluchaj, kogda
radi effektivnosti i kompaktnosti programmy na nih stoit idti.
Poskol'ku konstruktor Slist kopiruet parametr dlya insert(),
spisok Slist prigoden tol'ko dlya takih nebol'shih ob容ktov, kak
celye, kompleksnye chisla ili ukazateli. Esli dlya ob容ktov kopirovanie
slishkom nakladno ili nepriemlemo po smyslovym prichinam, obychno
vyhod byvaet v tom, chtoby vmesto ob容ktov pomeshchat' v spisok
ukazateli na nih. |to sdelano v privedennoj vyshe funkcii f() dlya
lst2.
Otmetim, chto raz parametr dlya Slist::insert() kopiruetsya, peredacha
ob容kta proizvodnogo klassa funkcii insert(), ozhidayushchej ob容kt
bazovogo klassa, ne projdet gladko, kak mozhno bylo (po naivnosti)
podumat':
class smiley : public circle { /* ... */ };
void g1(Slist<circle>& olist, const smiley& grin)
{
olist.insert(grin); // lovushka!
}
V spisok budet vklyuchena tol'ko chast' circle ob容kta tipa smiley.
Otmetim, chto eta nepriyatnost' budet obnaruzhena translyatorom v tom
sluchae, kotoryj mozhno schitat' naibolee veroyatnym. Tak, esli by
rassmatrivaemyj bazovyj klass byl abstraktnym, translyator zapretil
by "urezanie" ob容kta proizvodnogo klassa:
void g2(Slist<shape>& olist, const circle& c)
{
olist.insert(c); // oshibka: popytka sozdat' ob容kt
// abstraktnogo klassa
}
CHtoby izbezhat' "urezaniya" ob容kta nuzhno ispol'zovat' ukazateli:
void g3(Slist<shape*>& plist, const smiley& grin)
{
olist.insert(&grin); // prekrasno
}
Ne nuzhno ispol'zovat' parametr-ssylku dlya shablonnogo klassa:
void g4(Slist<shape&>& rlist, const smiley& grin)
{
rlist.insert(grin); // oshibka: budet sozdany komandy,
// soderzhashchie ssylku na ssylku (shape&&)
}
Pri generacii po shablonu tipa ssylki, ispol'zuemye podobnym obrazom,
privedut oshibkam v tipah. Generaciya po shablonu tipa dlya funkcii
Slist::insert(T&);
privedet k poyavleniyu nedopustimoj funkcii
Slist::insert(shape&&);
Ssylka ne yavlyaetsya ob容ktom, poetomu nel'zya imet' ssylku na ssylku.
Poskol'ku spisok ukazatelej yavlyaetsya poleznoj konstrukciej,
imeet smysl dat' emu special'noe imya:
template<class T>
class Splist : private Slist<void*> {
public:
void insert(T* p) { Slist<void*>::insert(p); }
void append(T* p) { Slist<void*>::append(p); }
T* get() { return (T*) Slist<void*>::get(); }
};
class Isplist : private slist_base {
public:
void insert(T* p) { slist_base::insert(p); }
void append(T* p) { slist_base::append(p); }
T* get() { return (T*) slist_base::get(); }
};
|ti opredeleniya k tomu zhe uluchshayut kontrol' tipov i eshche bol'she
sokrashchayut neobhodimost' dublirovat' funkcii.
CHasto byvaet polezno, chtoby tip elementa, ukazyvaemyj v shablone
tipa, sam byl shablonnym klassom. Naprimer, razrezhennuyu matricu,
soderzhashchuyu daty, mozhno opredelit' tak:
typedef Slist< Slist<date> > dates;
Obratite vnimanie na nalichie probelov v etom opredelenii. Esli mezhdu
pervoj i vtoroj uglovoj skobkoj > net probelov, vozniknet
sintaksicheskaya oshibka, poskol'ku >> v opredelenii
typedef Slist<Slist<date>> dates;
budet traktovat'sya kak operaciya sdviga vpravo. Kak obychno, vvodimoe
v typedef imya sluzhit sinonimom oboznachaemogo im tipa, a ne yavlyaetsya
novym tipom. Konstrukciya typedef polezna dlya imenovaniya dlya
dlinnyh imen shablonnyh klassov takzhe, kak ona polezna dlya lyubyh
drugih dlinnyh imen tipov.
Otmetim, chto parametr shablona tipa, kotoryj mozhet po raznomu
ispol'zovat'sya v ego opredelenii, dolzhen vse ravno ukazyvat'sya sredi
spiska parametrov shablona odin raz. Poetomu shablon tipa, v kotorom
ispol'zuetsya ob容kt T i spisok elementov T, nado opredelyat' tak:
template<class T> class mytemplate {
T ob;
Slist<T> slst;
// ...
};
a vovse ne tak:
template<class T, class Slist<t> > class mytemplate {
T obj;
Slist<T> slst;
// ...
};
V $$8.6 i $$R.14.2 dany pravila, chto mozhet byt' parametrom shablona
tipa.
Realizaciya funkcij slist_base ochevidna. Edinstvennaya trudnost'
svyazana s obrabotkoj oshibok. Naprimer, chto delat' esli pol'zovatel'
s pomoshch'yu funkcii get() pytaetsya vzyat' element iz pustogo spiska.
Podobnye situacii razbirayutsya v funkcii obrabotki oshibok
slist_handler(). Bolee razvityj metod, rasschitannyj na osobye
situacii, budet obsuzhdat'sya v glave 9.
Privedem polnoe opisanie klassa slist_base:
class slist_base {
slink* last; // last->next yavlyaetsya nachalom spiska
public:
void insert(slink* a); // dobavit' v nachalo spiska
void append(slink* a); // dobavit' v konec spiska
slink* get(); // udalit' i vozvratit'
// nachalo spiska
void clear() { last = 0; }
slist_base() { last = 0; }
slist_base(slink* a) { last = a->next = a; }
friend class slist_base_iter;
};
CHtoby uprostit' realizaciyu obeih funkcij insert i append, hranitsya
ukazatel' na poslednij element zamknutogo spiska:
void slist_base_insert(slink* a) // dobavit' v nachalo spiska
{
if (last)
a->next = last->next;
else
last = a;
last->next = a;
}
Zamet'te, chto last->next - pervyj element spiska.
void slist_base::append(slink* a) // dobavit' v konec spiska
{
if (last) {
a->next = last->next;
last = last->next = a;
}
else
last = a->next = a;
}
slist* slist_base::get() // udalit' i vozvratit' nachalo spiska
{
if (last == 0)
slist_handler("nel'zya vzyat' iz pustogo spiska");
slink* f = last->next;
if (f== last)
last = 0;
else
last->next = f->next;
return f;
}
Vozmozhno bolee gibkoe reshenie, kogda slist_handler - ukazatel' na
funkciyu, a ne sama funkciya. Togda vyzov
slist_handler("nel'zya vzyat' iz pustogo spiska");
budet zadavat'sya tak
(*slist_handler)(" nel'zya vzyat' iz pustogo spiska");
Kak my uzhe delali dlya funkcii new_handler ($$3.2.6), polezno
zavesti funkciyu, kotoraya pomozhet pol'zovatelyu sozdavat' svoi
obrabotchiki oshibok:
typedef void (*PFV)(const char*);
PFV set_slist_handler(PFV a)
{
PFV old = slist_handler;
slist_handler = a;
return old;
}
PFV slist_handler = &default_slist_handler;
Osobye situacii, kotorye obsuzhdayutsya v glave 9, ne tol'ko dayut
al'ternativnyj sposob obrabotki oshibok, no i sposob realizacii
slist_handler.
V klasse slist_base net funkcij dlya prosmotra spiska, mozhno tol'ko
vstavlyat' i udalyat' elementy. Odnako, v nem opisyvaetsya kak drug
klass slist_base_iter, poetomu mozhno opredelit' podhodyashchij dlya
spiska iterator. Vot odin iz vozmozhnyh, zadannyj v tom stile, kakoj
byl pokazan v $$7.8:
class slist_base_iter {
slink* ce; // tekushchij element
slist_base* cs; // tekushchij spisok
public:
inline slist_base_iter(slist_base& s);
inline slink* operator()()
};
slist_base_iter::slist_base_iter(slist_base& s)
{
cs = &s;
ce = cs->last;
}
slink* slist_base_iter::operator()()
// vozvrashchaet 0, kogda iteraciya konchaetsya
{
slink* ret = ce ? (ce=ce->next) : 0;
if (ce == cs->last) ce = 0;
return ret;
}
Ishodya iz etih opredelenij, legko poluchit' iteratory dlya Slist i
Islist. Snachala nado opredelit' druzhestvennye klassy dlya iteratorov
po sootvetstvuyushchim kontejnernym klassam:
template<class T> class Islist_iter;
template<class T> class Islist {
friend class Islist_iter<T>;
// ...
};
template<class T> class Slist_iter;
template<class T> class Slist {
friend class Slist_iter<T>;
// ...
};
Obratite vnimanie, chto imena iteratorov poyavlyayutsya bez opredeleniya
ih shablonnogo klassa. |to sposob opredeleniya v usloviyah vzaimnoj
zavisimosti shablonov tipa.
Teper' mozhno opredelit' sami iteratory:
template<class T>
class Islist_iter : private slist_base_iter {
public:
Islist_iter(Islist<T>& s) : slist_base_iter(s) { }
T* operator()()
{ return (T*) slist_base_iter::operator()(); }
};
template<class T>
class Slist_iter : private slist_base_iter {
public:
Slist_iter(Slist<T>& s) : slist_base_iter(s) { }
inline T* operator()();
};
T* Slist_iter::operator()()
{
return ((Tlink<T>*) slist_base_iter::operator()())->info;
}
Zamet'te, chto my opyat' ispol'zovali priem, kogda iz odnogo bazovogo
klassa stroitsya semejstvo proizvodnyh klassov (a imenno, shablonnyj
klass). My ispol'zuem nasledovanie, chtoby vyrazit' obshchnost' klassov
i izbezhat' nenuzhnogo dublirovaniya funkcij. Trudno pereocenit'
stremlenie izbezhat' dublirovaniya funkcij pri realizacii takih prostyh
i chasto ispol'zuemyh klassov kak spiski i iteratory. Pol'zovat'sya
etimi iteratorami mozhno tak:
void f(name* p)
{
Islist<name> lst1;
Slist<name> lst2;
lst1.insert(p);
lst2.insert(p);
// ...
Islist_iter<name> iter1(lst1);
const name* p;
while (p=iter1()) {
list_iter<name> iter2(lst1);
const name* q;
while (q=iter2()) {
if (p == q) cout << "najden" << *p << '\n';
}
}
}
Est' neskol'ko sposobov zadat' iterator dlya kontejnernogo klassa.
Razrabotchik programmy ili biblioteki dolzhen vybrat' odin iz nih
i priderzhivat'sya ego. Privedennyj sposob mozhet pokazat'sya slishkom
hitrym. V bolee prostom variante mozhno bylo prosto pereimenovat'
operator()() kak next(). V oboih variantah predpolagaetsya vzaimosvyaz'
mezhdu kontejnernym klassom i iteratorom dlya nego, tak chto mozhno
pri vypolnenii iteratora obrabotat' sluchai, kogda elementy dobavlyayutsya
ili udalyayutsya iz kontejnera. |tot i nekotorye drugie sposoby zadaniya
iteratorov byli by nevozmozhny, esli by iterator zavisel ot funkcii
pol'zovatelya, v kotoroj est' ukazateli na elementy iz kontejnera.
Kak pravilo, kontejner ili ego iteratory realizuyut ponyatie "ustanovit'
iteraciyu na nachalo" i ponyatie "tekushchego elementa".
Esli ponyatie tekushchego elementa predostavlyaet ne iterator, a sam
kontejner, iteraciya proishodit v prinuditel'nom poryadke po otnosheniyu
k kontejneru analogichno tomu, kak polya svyazi prinuditel'no hranyatsya
v ob容ktah iz kontejnera. Znachit trudno odnovremenno vesti dve
iteracii dlya odnogo kontejnera, no rashody na pamyat' i vremya pri takoj
organizacii iteracii blizki k optimal'nym. Privedem primer:
class slist_base {
// ...
slink* last; // last->next golova spiska
slink* current; // tekushchij element
public:
// ...
slink* head() { return last?last->next:0; }
slink* current() { return current; }
void set_current(slink* p) { current = p; }
slink* first() { set_current(head()); return current; }
slink* next();
slink* prev();
};
Podobno tomu, kak v celyah effektivnosti i kompaktnosti programmy
mozhno ispol'zovat' dlya odnogo ob容kta kak spisok s prinuditel'noj
svyaz'yu, tak i spisok bez nee, dlya odnogo kontejnera mozhno
ispol'zovat' prinuditel'nuyu i neprinuditel'nuyu iteraciyu:
void f(Islist<name>& ilst)
// medlennyj poisk imen-dublikatov
{
list_iter<name> slow(ilst); // ispol'zuetsya iterator
name* p;
while (p = slow()) {
ilst.set_current(p); // rasschityvaem na tekushchij element
name* q;
while (q = ilst.next())
if (strcmp(p->string,q->string) == 0)
cout << "dublikat" << p << '\n';
}
}
Eshche odin vid iteratorov pokazan v $$8.8.
8.4 SHablony tipa dlya funkcij
Ispol'zovanie shablonnyh klassov oznachaet nalichie shablonnyh
funkcij-chlenov. Pomimo etogo, mozhno opredelit' global'nye shablonnye
funkcii, t.e. shablony tipa dlya funkcij, ne yavlyayushchihsya chlenami klassa.
SHablon tipa dlya funkcij porozhdaet semejstvo funkcij tochno takzhe,
kak shablon tipa dlya klassa porozhdaet semejstvo klassov. |tu vozmozhnost'
my obsudim na posledovatel'nosti primerov, v kotoryh privodyatsya
varianty funkcii sortirovki sort(). Kazhdyj iz variantov v posleduyushchih
razdelah budet illyustrirovat' obshchij metod.
Kak obychno my sosredotochimsya na organizacii programmy, a ne na
razrabotke ee algoritma, poetomu ispol'zovat'sya budet trivial'nyj
algoritm. Vse varianty shablona tipa dlya sort() nuzhny dlya togo,
chtoby pokazat' vozmozhnosti yazyka m poleznye priemy programmirovaniya.
Varianty ne uporyadocheny v sootvetstvii s tem, naskol'ko oni horoshi.
Krome togo, mozhno obsudit' i tradicionnye varianty bez shablonov tipa,
v chastnosti, peredachu ukazatelya na funkciyu, proizvodyashchuyu sravnenie.
8.4.1 Prostoj shablon tipa dlya global'noj funkcii
Nachnem s prostejshego shablona dlya sort():
template<class T> void sort(Vector<T>&);
void f(Vector<int>& vi,
Vector<String>& vc,
Vector<int>& vi2,
Vector<char*>& vs)
{
sort(vi); // sort(Vector<int>& v);
sort(vc); // sort(Vector<String>& v);
sort(vi2); // sort(Vector<int>& v);
sort(vs); // sort(Vector<char*>& v);
}
Kakaya imenno funkciya sort() budet vyzyvat'sya opredelyaetsya fakticheskim
parametrom. Programmist daet opredelenie shablona tipa dlya funkcii,
a zadacha sistemy programmirovaniya obespechit' sozdanie pravil'nyh
variantov funkcii po shablonu i vyzov sootvetstvuyushchego varianta.
Naprimer, prostoj shablon s algoritmom puzyr'kovoj sortirovki mozhno
opredelit' tak:
template<class T> void sort(Vector<T>& v)
/*
Sortirovka elementov v poryadke vozrastaniya
Ispol'zuetsya sortirovka po metodu puzyr'ka
*/
{
unsigned n = v.size();
for (int i=0; i<n-1; i++)
for (int j=n-1; i<j; j--)
if (v[j] < v[j-1]) { // menyaem mestami v[j] i v[j-1]
T temp = v[j];
v[j] = v[j-1];
v[j-1] = temp;
}
}
Sovetuem sravnit' eto opredelenie s funkciej sortirovki s tem zhe
algoritmom iz $$4.6.9. Sushchestvennoe otlichie etogo varianta v tom,
chto vsya neobhodimaya informaciya peredaetsya v edinstvennom parametre
v. Poskol'ku tip sortiruemyh elementov izvesten (iz tipa fakticheskogo
parametra, mozhno neposredstvenno sravnivat' elementy, a ne peredavat'
ukazatel' na proizvodyashchuyu sravnenie funkciyu. Krome togo, net nuzhdy
vozit'sya s operaciej sizeof. Takoe reshenie kazhetsya bolee krasivym
i k tomu zhe ono bolee effektivno, chem obychnoe. Vse zhe ono stalkivaetsya
s trudnost'yu. Dlya nekotoryh tipov operaciya < ne opredelena, a dlya
drugih, naprimer char*, ee opredelenie protivorechit tomu, chto
trebuetsya v privedennom opredelenii shablonnoj funkcii. (Dejstvitel'no,
nam nuzhno sravnivat' ne ukazateli na stroki, a sami stroki).
V pervom sluchae popytka sozdat' variant sort() dlya takih tipov
zakonchitsya neudachej (na chto i sleduet nadeyat'sya) , a vo vtorom
poyavit'sya funkciya, proizvodyashchaya neozhidannyj rezul'tat.
CHtoby pravil'no sortirovat' vektor iz elementov char* my mozhem
prosto zadat' samostoyatel'no podhodyashchee opredelenie funkcii
sort(Vector<char*>&):
void sort(Vector<char*>& v)
{
unsigned n = v.size();
for (int i=0; i<n-1; i++)
for ( int j=n-1; i<j; j--)
if (strcmp(v[j],v[j-1])<0) {
// menyaem mestami v[j] i v[j-1]
char* temp = v[j];
v[j] = v[j-1];
v[j-1] = temp;
}
}
Poskol'ku dlya vektorov iz ukazatelej na stroki pol'zovatel' dal
svoe osoboe opredelenie funkcii sort(), ono i budet ispol'zovat'sya,
a sozdavat' dlya nee opredelenie po shablonu s parametrom tipa
Vector<char*>& ne nuzhno. Vozmozhnost' dat' dlya osobo vazhnyh ili
"neobychnyh" tipov svoe opredelenie shablonnoj funkcii daet cennoe
kachestvo gibkosti v programmirovanii i mozhet byt' vazhnym sredstvom
dovedeniya programmy do optimal'nyh harakteristik.
8.4.2 Proizvodnye klassy pozvolyayut vvesti novye operacii
V predydushchem razdele funkciya sravneniya byla "vstroennoj" v tele
sort() (prosto ispol'zovalas' operaciya <). Vozmozhno drugoe reshenie,
kogda ee predostavlyaet sam shablonnyj klass Vector. Odnako, takoe
reshenie imeet smysl tol'ko pri uslovii, chto dlya tipov elementov
vozmozhno osmyslennoe ponyatie sravneniya. Obychno v takoj situacii
funkciyu sort() opredelyayut tol'ko dlya vektorov, na kotoryh opredelena
operaciya < :
template<class T> void sort(SortableVector<T>& v)
{
unsigned n = v.size();
for (int i=0; i<n-1; i++)
for (int j=n-1; i<j; j--)
if (v.lessthan(v[j],v[j-1])) {
// menyaem mestami v[j] i v[j-1]
T temp = v[j];
v[j] = v[j-1];
v[j-1] = temp;
}
}
Klass SortableVector (sortiruemyj vektor) mozhno opredelit' tak:
template<class T> class SortableVector
: public Vector<T>, public Comparator<T> {
public:
SortableVector(int s) : Vector<T>(s) { }
};
CHtoby eto opredelenie imelo smysl eshche nado opredelit' shablonnyj
klass Comparator (sravnivatel'):
template<class T> class Comparator {
public:
inline static lessthan(T& a, T& b) // funkciya "men'she"
{ return strcmp(a,b)<0; }
// ...
};
CHtoby ustranit' tot effekt, chto v nashem sluchae operaciya < daet
ne tot rezul'tat dlya tipa char*, my opredelim special'nyj variant
klassa sravnivatelya:
class Comparator<char*> {
public:
inline static lessthan(const char* a, const char* b)
// funkciya "men'she"
{ return strcmp(a,b)<0; }
// ...
};
Opisanie special'nogo varianta shablonnogo klassa dlya char* polnost'yu
podobno tomu, kak v predydushchem razdele my opredelili special'nyj
variant shablonnoj funkcii dlya etoj zhe celi. CHtoby opisanie special'nogo
varianta shablonnogo klassa srabotalo, translyator dolzhen obnaruzhit'
ego do ispol'zovaniya. Inache budet ispol'zovat'sya sozdavaemyj po
shablonu klass. Poskol'ku klass dolzhen imet' v tochnosti odno
opredelenie v programme, ispol'zovat' i special'nyj variant klassa,
i variant, sozdavaemyj po shablonu, budet oshibkoj.
Poskol'ku u nas uzhe special'nyj variant klassa Comparator dlya
char*, special'nyj variant klassa SortableVector dlya char* ne
nuzhen, i mozhem, nakonec, poprobovat' sortirovku:
void f(SortableVector<int>& vi,
SortableVector<String>& vc,
SortableVector<int>& vi2,
SortableVector<char*>& vs)
{
sort(vi);
sort(vc);
sort(vi2);
sort(vs);
}
Vozmozhno imet' dva vida vektorov i ne ochen' horosho, no, po krajnej
mere, SortableVector yavlyaetsya proizvodnym ot Vector. Znachit esli
v funkcii ne nuzhna sortirovka, to v nej i ne nado znat' o klasse
SortableVector, a tam, gde nuzhno, srabotaet neyavnoe preobrazovanie
ssylki na proizvodnyj klass v ssylku na obshchij bazovyj klass. My
vveli proizvodnyj ot Vector i Comparator klass SortableVector
(vmesto togo, chtoby dobavit' funkcii k klassu, proizvodnomu ot odnogo
Vector) prosto potomu, chto klass Comparator uzhe naprashivalsya v
predydushchim primere. Takoj podhod tipichen pri sozdanii bol'shih
bibliotek. Klass Comparator estestvennyj kandidat dlya biblioteki,
poskol'ku v nem mozhno ukazat' razlichnye trebovaniya k operaciyam
sravneniya dlya raznyh tipov.
8.4.3 Peredacha operacij kak parametrov funkcij
Mozhno ne zadavat' funkciyu sravneniya kak chast' tipa
Vector, a peredavat' ee kak vtoroj parametr funkcii sort().
|tot parametr yavlyaetsya ob容ktom klassa, v kotorom opredelena
realizaciya operacii sravneniya:
template<class T> void sort(Vector<T>& v, Comparator<T>& cmp)
{
unsigned n = v.size();
for (int i = 0; i<n-1; i++)
for ( int j = n-1; i<j; j--)
if (cmp.lessthan(v[j],v[j-1])) {
// menyaem mestami v[j] i v[j-1]
T temp = v[j];
v[j] = v[j-1];
v[j-1] = temp;
}
}
|tot variant mozhno rassmatrivat' kak obobshchenie tradicionnogo priema,
kogda operaciya sravneniya peredaetsya kak ukazatel' na funkciyu.
Vospol'zovat'sya etim mozhno tak:
void f(Vector<int>& vi,
Vector<String>& vc,
Vector<int>& vi2,
Vector<char*>& vs)
{
Comparator<int> ci;
Comparator<char*> cs;
Comparator<String> cc;
sort(vi,ci); // sort(Vector<int>&);
sort(vc,cc); // sort(Vector<String>&);
sort(vi2,ci); // sort(Vector<int>&);
sort(vs,cs); // sort(Vector<char*>&);
}
Otmetim, chto vklyuchenie v shablon klassa Comparator kak parametra
garantiruet, chto funkciya lessthan budet realizovyvat'sya podstanovkoj.
V chastnosti, eto polezno, esli v shablonnoj funkcii ispol'zuetsya
neskol'ko funkcij, a ne odna operaciya sravneniya, i osobenno eto
polezno, kogda eti funkcii zavisyat ot hranyashchihsya v tom zhe ob容kte
dannyh.
8.4.4 Neyavnaya peredacha operacij
V primere iz predydushchego razdela ob容kty Comparator na samom dele
nikak ne ispol'zovalis' v vychisleniyah. |to prosto "iskusstvennye"
parametry, nuzhnye dlya pravil'nogo kontrolya tipov. Vvedenie takih
parametrov dostatochno obshchij i poleznyj priem, hotya i ne slishkom
krasivyj. Odnako, esli ob容kt ispol'zuetsya tol'ko dlya peredachi
operacii (kak i bylo v nashem sluchae), t.e. v vyzyvaemoj funkcii
ne ispol'zuetsya ni znachenie, ni adres ob容kta, to mozhno vmesto etogo
peredavat' operaciyu neyavno:
template<class T> void sort(Vector<T>& v)
{
unsigned n = v.size();
for (int i=0; i<n-1; i++)
for (int j=n-1; i<j; j--)
if (Comparator<T>::lessthan(v[j],v[j-1])) {
// menyaem mestami v[j] i v[j-1]
T temp = v[j];
v[j] = v[j-1];
v[j-1] = temp;
}
}
V rezul'tate my prihodim k pervonachal'nomu variantu ispol'zovaniya
sort():
void f(Vector<int>& vi,
Vector<String>& vc,
Vector<int>& vi2,
Vector<char*>& vs)
{
sort(vi); // sort(Vector<int>&);
sort(vc); // sort(Vector<String>&);
sort(vi2); // sort(Vector<int>&);
sort(vs); // sort(Vector<char*>&);
}
Osnovnoe preimushchestvo etogo varianta, kak i dvuh predydushchih, po
sravneniyu s ishodnym variantom v tom, chto chast' programmy, zanyataya
sobstvenno sortirovkoj, otdelena ot chastej, v kotoryh nahodyatsya
takie operacii, rabotayushchie s elementami, kak, naprimer lessthan.
Neobhodimost' podobnogo razdeleniya rastet s rostom programmy, i
osobennyj interes eto razdelenie predstavlyaet pri proektirovanii
bibliotek. Zdes' sozdatel' biblioteki ne mozhet znat' tipy parametrov
shablona, a pol'zovateli ne znayut (ili ne hotyat znat') specifiku
ispol'zuemyh v shablone algoritmov. V chastnosti, esli by v funkcii
sort() ispol'zovalsya bolee slozhnyj, optimizirovannyj i rasschitannyj
na kommercheskoe primenenie algoritm, pol'zovatel' ne ochen' by
stremilsya napisat' svoyu osobuyu versiyu dlya tipa char*, kak eto bylo
sdelano v $$8.4.1. Hotya realizaciya klassa Comparator dlya special'nogo
sluchaya char* trivial'na i mozhet ispol'zovat'sya i v drugih situaciyah.
8.4.5 Vvedenie operacij s pomoshch'yu parametrov shablonnogo klassa
Vozmozhny situacii, kogda neyavnost' svyazi mezhdu shablonnoj funkciej
sort() i shablonnym klassom Comparator sozdaet trudnosti. Neyavnuyu
svyaz' legko upustit' iz vidu i v to zhe vremya razobrat'sya v nej
mozhet byt' neprosto. Krome togo, poskol'ku eta svyaz' "vstroena"
v funkciyu sort(), nevozmozhno ispol'zovat' etu funkciyu dlya
sortirovki vektorov odnogo tipa, esli operaciya sravneniya rasschitana
na drugoj tip (sm. uprazhnenie 3 v $$8.9). Pomestiv funkciyu sort()
v klass, my mozhem yavno zadavat' svyaz' s klassom Comparator:
template<class T, class Comp> class Sort {
public:
static void sort(Vector<T>&);
};
Ne hochetsya povtoryat' tip elementa, i eto mozhno ne delat', esli
ispol'zovat' typedef v shablone Comparator:
template<class T> class Comparator {
public:
typedef T T; // opredelenie Comparator<T>::T
static int lessthan(T& a, T& b) {
return a < b;
}
// ...
};
V special'nom variante dlya ukazatelej na stroki eto opredelenie
vyglyadit tak:
class Comparator<char*> {
public:
typedef char* T;
static int lessthan(T a, T b) {
return strcmp(a,b) < 0;
}
// ...
};
Posle etih izmenenij mozhno ubrat' parametr, zadayushchij tip elementa,
iz klassa Sort:
template<class T, class Comp> class Sort {
public:
static void sort(Vector<T>&);
};
Teper' mozhno ispol'zovat' sortirovku tak:
void f(Vector<int>& vi,
Vector<String>& vc,
Vector<int>& vi2,
Vector<char*>& vs)
{
Sort< int,Comparator<int> >::sort(vi);
Sort< String,Comparator<String> >:sort(vc);
Sort< int,Comparator<int> >::sort(vi2);
Sort< char*,Comparator<char*> >::sort(vs);
}
i opredelit' funkciyu sort() sleduyushchim obrazom:
template<class T, class Comp>
void Sort<T,Comp>::sort(Vector<T>& v)
{
for (int i=0; i<n-1; i++)
for (int j=n-1; i<j; j--)
if (Comp::lessthan(v[j],v[j-1])) {
T temp = v[j];
v[j] = v[j-1];
v[j-1] = temp;
}
}
Poslednij variant yarko demonstriruet kak mozhno soedinyat' v odnu
programmu otdel'nye ee chasti. |tot primer mozhno eshche bol'she
uprostit', esli ispol'zovat' klass sravnitelya (Comp) v kachestve
edinstvennogo parametra shablona. V etom sluchae v opredeleniyah klassa
Sort i funkcii Sort::sort() tip elementa budet oboznachat'sya kak Comp::T.
8.5 Razreshenie peregruzki dlya shablonnoj funkcii
K parametram shablonnoj funkcii nel'zya primenyat' nikakih preobrazovanij
tipa. Vmesto etogo pri neobhodimosti sozdayutsya novye varianty
funkcii:
template<class T> T sqrt(t);
void f(int i, double d, complex z)
{
complex z1 = sqrt(i); // sqrt(int)
complex z2 = sqrt(d); // sqrt(double)
complex z3 = sqrt(z); // sqrt(complex)
// ...
}
Zdes' dlya vseh treh tipov parametrov budet sozdavat'sya po shablonu
svoya funkciya sqrt. Esli pol'zovatel' zahochet chego-nibud' inogo,
naprimer vyzvat' sqrt(double), zadavaya parametr int, nuzhno
ispol'zovat' yavnoe preobrazovanie tipa:
template<class T> T sqrt(T);
void f(int i, double d, complex z)
{
complex z1 = sqrt(double(i)); // sqrt(double)
complex z2 = sqrt(d); // sqrt(double)
complex z3 = sqrt(z); // sqrt(complex)
// ...
}
V etom primere po shablonu budut sozdavat'sya opredeleniya tol'ko dlya
sqrt(double) i sqrt(complex).
SHablonnaya funkciya mozhet peregruzhat'sya kak prostoj, tak i shablonnoj
funkciej togo zhe imeni. Razreshenie peregruzki kak shablonnyh, tak i
obychnyh funkcij s odinakovymi imenami proishodit za tri shagaX:
X |ti pravila slishkom strogie, i, po vsej vidimosti budut oslableny,
chtoby razreshit' preobrazovaniya ssylok i ukazatelej, a, vozmozhno,
i drugie standartnye preobrazovaniya. Kak obychno, pri takih
preobrazovaniyah budet dejstvovat' kontrol' odnoznachnosti.
[1] Najti funkciyu s tochnym sopostavleniem parametrov ($$R.13.2);
esli takaya est', vyzvat' ee.
[2] Najti shablon tipa, po kotoromu mozhno sozdat' vyzyvaemuyu
funkciyu s tochnym sopostavleniem parametrov; esli takaya est',
vyzvat' ee.
[3] Poprobovat' pravila razresheniya dlya obychnyh funkcij ($$r13.2);
esli funkciya najdena po etim pravilam, vyzvat' ee, inache
vyzov yavlyaetsya oshibkoj.
V lyubom sluchae, esli na pervom shage najdeno bolee odnoj funkcii,
vyzov schitaetsya neodnoznachnym i yavlyaetsya oshibkoj. Naprimer:
template<class T>
T max(T a, T b) { return a>b?a:b; };
void f(int a, int b, char c, char d)
{
int m1 = max(a,b); // max(int,int)
char m2 = max(c,d); // max(char,char)
int m3 = max(a,c); // oshibka: nevozmozhno
// sozdat' max(int,char)
}
Poskol'ku do generacii funkcii po shablonu ne primenyaetsya nikakih
preobrazovanij tipa (pravilo [2]), poslednij vyzov v etom
primere nel'zya razreshit' kak max(a,int(c)). |to mozhet sdelat' sam
pol'zovatel', yavno opisav funkciyu max(int,int). Togda vstupaet
v silu pravilo [3]:
template<class T>
T max(T a, T b) { return a>b?a:b; }
int max(int,int);
void f(int a, int b, char c, char d)
{
int m1 = max(a,b); // max(int,int)
char m2 = max(c,d); // max(char,char)
int m3 = max(a,c); // max(int,int)
}
Programmistu ne nuzhno davat' opredelenie funkcii max(int,int),
ono po umolchaniyu budet sozdano po shablonu.
Mozhno opredelit' shablon max tak, chtoby srabotal pervonachal'nyj
variant nashego primera:
template<class T1, class T2>
T1 max(T1 a, T2 b) { return a>b?a:b; };
void f(int a, int b, char c, char d)
{
int m1 = max(a,b); // int max(int,int)
char m2 = max(c,d); // char max(char,char)
int m3 = max(a,c); // max(int,char)
}
Odnako, v S i S++ pravila dlya vstroennyh tipov i operacij nad nimi
takovy, chto ispol'zovat' podobnyj shablon s dvumya parametrami
mozhet byt' sovsem neprosto. Tak, mozhet okazat'sya neverno zadavat'
tip rezul'tata funkcii kak pervyj parametr (T1), ili, po krajnej
mere, eto mozhet privesti k neozhidannomu rezul'tatu, naprimer dlya
vyzova
max(c,i); // char max(char,int)
Esli v shablone dlya funkcii, kotoraya
mozhet imet' mnozhestvo parametrov s razlichnymi arifmeticheskimi
tipami, ispol'zuyutsya dva parametra, to v rezul'tate po shablonu budet
porozhdat'sya slishkom bol'shoe chislo opredelenij raznyh funkcij.
Bolee razumno dobivat'sya preobrazovaniya tipa, yavno opisav funkciyu
s nuzhnymi tipami.
8.6 Parametry shablona tipa
Parametr shablona tipa ne obyazatel'no dolzhen byt' imenem tipa
(sm. $$R.14.2). Pomimo imen tipov mozhno zadavat' stroki, imena
funkcij i vyrazheniya-konstanty. Inogda byvaet nuzhno zadat'
kak parametr celoe:
template<class T, int sz> class buffer {
T v[sz]; // bufer ob容ktov proizvol'nogo tipa
// ...
};
void f()
{
buffer<char,128> buf1;
buffer<complex,20> buf2;
// ...
}
My sdelali sz parametrom shablona buffer, a ne ego ob容ktov, i eto
oznachaet, chto razmer bufera dolzhen byt' izvesten na stadii
translyacii, chtoby ego ob容kty bylo mozhno razmeshchat', ne ispol'zuya
svobodnuyu pamyat'. Blagodarya etomu svojstvu takie shablony kak buffer
polezny dlya realizacii kontejnernyh klassov, poskol'ku dlya poslednih
pervostepennym faktorom, opredelyayushchim ih effektivnost', yavlyaetsya
vozmozhnost' razmeshchat' ih vne svobodnoj pamyati. Naprimer, esli v
realizacii klassa string korotkie stroki razmeshchayutsya v steke, eto daet
sushchestvennyj vyigrysh dlya programmy, poskol'ku v bol'shinstve zadach
prakticheski vse stroki ochen' korotkie. Dlya realizacii takih tipov kak
raz i mozhet prigodit'sya shablon buffer.
Kazhdyj parametr shablona tipa dlya funkcii dolzhen vliyat' na tip
funkcii, i eto vliyanie vyrazhaetsya v tom, chto on uchastvuet po
krajnej mere v odnom iz tipov formal'nyh parametrov funkcij,
sozdavaemyh po shablonu. |to nuzhno dlya togo, chtoby funkcii mozhno bylo
vybirat' i sozdavat', osnovyvayas' tol'ko na ih parametrah:
template<class T> void f1(T); // normal'no
template<class T> void f2(T*); // normal'no
template<class T> T f3(int); // oshibka
template<int i> void f4(int[][i]); // oshibka
template<int i> void f5(int = i); // oshibka
template<class T, class C> void f6(T); // oshibka
template<class T> void f7(const T&, complex); // normal'no
template<class T> void f8(Vector< List<T> >); // normal'no
Zdes' vse oshibki vyzvany tem, chto parametr-tip shablona nikak ne
vliyaet na formal'nye parametry funkcij.
Podobnogo ogranicheniya net v shablonah tipa dlya klassov. Delo v tom,
chto parametr dlya takogo shablona nuzhno ukazyvat' vsyakij raz, kogda
opisyvaetsya ob容kt shablonnogo klassa. S drugoj storony, dlya shablonnyh
klassov voznikaet vopros: kogda dva sozdannyh po shablonu tipa mozhno
schitat' odinakovymi? Dva imeni shablonnogo klassa oboznachayut odin i
tot zhe klass, esli sovpadayut imena ih shablonov, a ispol'zuemye v etih
imenah parametry imeyut odinakovye znacheniya (s uchetom vozmozhnyh
opredelenij typedef, vychisleniya vyrazhenij-konstant i t.d.). Vernemsya
k shablonu buffer:
template<class T, int sz>
class buffer {
T v[sz];
// ...
};
void f()
{
buffer<char,20> buf1;
buffer<complex,20> buf2;
buffer<char,20> buf3;
buffer<char,100> buf4;
buf1 = buf2; // oshibka: nesootvetstvie tipov
buf1 = buf3; // normal'no
buf1 = buf4; // oshibka: nesootvetstvie tipov
// ...
}
Esli v shablone tipa dlya klassa ispol'zuyutsya parametry, zadayushchie
ne tipy, vozmozhno poyavlenie konstrukcij, vyglyadyashchih dvusmyslenno:
template<int i>
class X { /* ... */ };
void f(int a, int b)
{
X < a > b>; // Kak eto ponimat': X<a> b i potom
// nedopustimaya leksema, ili X< (a>b) >; ?
}
|tot primer sintaksicheski oshibochen, poskol'ku pervaya uglovaya skobka
> zavershaet parametr shablona. V maloveroyatnom sluchae, kogda vam
ponadobitsya parametr shablona, yavlyayushchijsya vyrazheniem "bol'she chem",
ispol'zujte skobki: X< (a>b)>.
8.7 SHablony tipa i proizvodnye klassy
My uzhe videli, chto sochetanie proizvodnyh klassov (nasledovanie) i
shablonov tipa mozhet byt' moshchnym sredstvom. SHablon tipa vyrazhaet
obshchnost' mezhdu vsemi tipami, kotorye ispol'zuyutsya kak ego parametry,
a bazovyj klass vyrazhaet obshchnost' mezhdu vsemi predstavleniyami
(ob容ktami) i nazyvaetsya interfejsom. Zdes' vozmozhny nekotorye
prostye nedorazumeniya, kotoryh nado izbegat'.
Dva sozdannyh po odnomu shablonu tipa budut razlichny i mezhdu nimi
nevozmozhno otnoshenie nasledovaniya krome edinstvennogo sluchaya, kogda
u etih tipov identichny parametry shablona. Naprimer:
template<class T>
class Vector { /* ... */ }
Vector<int> v1;
Vector<short> v2;
Vector<int> v3;
Zdes' v1 i v3 odnogo tipa, a v2 imeet sovershenno drugoj tip. Iz togo
fakta, chto short neyavno preobrazuetsya v int, ne sleduet, chto est'
neyavnoe preobrazovanie Vector<short> v Vector<int>:
v2 = v3; // nesootvetstvie tipov
No etogo i sledovalo ozhidat', poskol'ku net vstroennogo preobrazovaniya
int[] v short[].
Analogichnyj primer:
class circle: public shape { /* ... */ };
Vector<circle*> v4;
Vector<shape*> v5;
Vector<circle*> v6;
Zdes' v4 i v6 odnogo tipa, a v5 imeet sovershenno drugoj tip. Iz togo
fakta, chto sushchestvuet neyavnoe preobrazovanie circle v shape i
circle* v shape*, ne sleduet, chto est' neyavnye preobrazovaniya
Vector<circle*> v Vector<shape*> ili Vector<circle*>* v
Vector<shape*>* :
v5 = v6; // nesootvetstvie tipov
Delo v tom, chto v obshchem sluchae struktura (predstavlenie) klassa,
sozdannogo po shablonu tipa, takova, chto dlya nee ne predpolagayutsya
otnosheniya nasledovaniya. Tak, sozdannyj po shablonu klass mozhet
soderzhat' ob容kt tipa, zadannogo v shablone kak parametr, a ne prosto
ukazatel' na nego. Krome togo, dopushchenie podobnyh preobrazovanij
privodit k narusheniyu kontrolya tipov:
void f(Vector<circle>* pc)
{
Vector<shape>* ps = pc; // oshibka: nesootvetstvie tipov
(*ps)[2] = new square; // krugluyu nozhku suem v kvadratnoe
// otverstie (pamyat' vydelena dlya
// square, a ispol'zuetsya dlya circle
}
Na primerah shablonov Islist, Tlink, Slist, Splist, Islist_iter,
Slist_iter i SortableVector my videli, chto shablony tipa dayut
udobnoe sredstvo dlya sozdaniya celyh semejstv klassov. Bez shablonov
sozdanie takih semejstv tol'ko s pomoshch'yu proizvodnyh klassov
mozhet byt' utomitel'nym zanyatiem, a znachit, vedushchim k oshibkam.
S drugoj storony, esli otkazat'sya ot proizvodnyh klassov i ispol'zovat'
tol'ko shablony, to poyavlyaetsya mnozhestvo kopij funkcij-chlenov shablonnyh
klassov, mnozhestvo kopij opisatel'noj chasti shablonnyh klassov i vo
mnozhestve povtoryayutsya funkcii, ispol'zuyushchie shablony tipa.
8.7.1 Zadanie realizacii s pomoshch'yu parametrov shablona
V kontejnernyh klassah chasto prihoditsya vydelyat' pamyat'. Inogda
byvaet neobhodimo (ili prosto udobno) dat' pol'zovatelyu vozmozhnost'
vybirat' iz neskol'kih variantov vydeleniya pamyati, a takzhe pozvolit'
emu zadavat' svoj variant. |to mozhno sdelat' neskol'kimi sposobami.
Odin iz sposobov sostoit v tom, chto opredelyaetsya shablon tipa dlya
sozdaniya novogo klassa, v interfejs kotorogo vhodit opisanie
sootvetstvuyushchego kontejnera i klassa, proizvodyashchego vydelenie pamyati
po sposobu, opisannomu v $$6.7.2:
template<class T, class A> class Controlled_container
: public Container<T>, private A {
// ...
void some_function()
{
// ...
T* p = new(A::operator new(sizeof(T))) T;
// ...
}
// ...
};
SHablon tipa zdes' neobhodim, poskol'ku my sozdaem kontejnernyj klass.
Nasledovanie ot Container<T> nuzhno, chtoby klass Controlled_container
mozhno bylo ispol'zovat' kak kontejnernyj klass. SHablon tipa s
parametrom A pozvolit nam ispol'zovat' razlichnye funkcii razmeshcheniya:
class Shared : public Arena { /* ... */ };
class Fast_allocator { /* ... */ };
Controlled_container<Process_descriptor,Shared> ptbl;
Controlled_container<Node,Fast_allocator> tree;
Controlled_container<Personell_record,Persistent> payroll;
|to universal'nyj sposob predostavlyat' proizvodnym klassam
soderzhatel'nuyu informaciyu o realizacii. Ego polozhitel'nymi kachestvami
yavlyayutsya sistematichnost' i vozmozhnost' ispol'zovat' funkcii-podstanovki.
Dlya etogo sposoba harakterny neobychno dlinnye imena. Vprochem, kak
obychno, typedef pozvolyaet zadat' sinonimy dlya slishkom dlinnyh imen
tipov:
typedef
Controlled_container<Personell_record,Persistent> pp_record;
pp_record payroll;
Obychno shablon tipa dlya sozdaniya takogo klassa kak pp_record ispol'zuyut
tol'ko v tom sluchae, kogda dobavlyaemaya informaciya po realizacii
dostatochno sushchestvenna, chtoby ne vnosit' ee v proizvodnyj klass ruchnym
programmirovaniem. Primerom takogo shablona mozhet byt' obshchij
(vozmozhno, dlya nekotoryh bibliotek standartnyj) shablonnyj klass
Comparator ($$8.4.2), a takzhe netrivial'nye (vozmozhno, standartnye
dlya nekotoryh bibliotek) klassy Allocator (klassy dlya vydeleniya pamyati).
Otmetim, chto postroenie proizvodnyh klassov v takih primerah
idet po "osnovnomu prospektu", kotoryj opredelyaet interfejs s
pol'zovatelem (v nashem primere eto Container). No est' i "bokovye
ulicy", zadayushchie detali realizacii.
Iz vseh universal'nyh nevstroennyh tipov samym poleznym, po vsej
vidimosti, yavlyaetsya associativnyj massiv. Ego chasto nazyvayut
tablicej (map), a inogda slovarem, i on hranit pary znachenij.
Imeya odno iz znachenij, nazyvaemoe klyuchom, mozhno poluchit' dostup
k drugomu, nazyvaemomu prosto znacheniem. Associativnyj massiv
mozhno predstavlyat' kak massiv, v kotorom indeks ne obyazan byt'
celym:
template<class K, class V> class Map {
// ...
public:
V& operator[](const K&); // najti V, sootvetstvuyushchee K
// i vernut' ssylku na nego
// ...
};
Zdes' klyuch tipa K oboznachaet znachenie tipa V. Predpolagaetsya, chto
klyuchi mozhno sravnivat' s pomoshch'yu operacij == i <, tak chto massiv
mozhno hranit' v uporyadochennom vide. Otmetim, chto klass Map
otlichaetsya ot tipa assoc iz $$7.8 tem, chto dlya nego nuzhna operaciya
"men'she chem", a ne funkciya heshirovaniya.
Privedem prostuyu programmu podscheta slov, v kotoroj ispol'zuyutsya
shablon Map i tip String:
#include <String.h>
#include <iostream.h>
#include "Map.h"
int main()
{
Map<String,int> count;
String word;
while (cin >> word) count[word]++;
for (Mapiter<String,int> p = count.first(); p; p++)
cout << p.value() << '\t' << p.key() << '\n';
return 0;
}
My ispol'zuem tip String dlya togo, chtoby ne bespokoit'sya o vydelenii
pamyati i perepolnenii ee, o chem prihoditsya pomnit', ispol'zuya tip
char*. Iterator Mapiter nuzhen dlya vybora po poryadku vseh znachenij
massiva. Iteraciya v Mapiter zadaetsya kak imitaciya raboty
s ukazatelyami. Esli vhodnoj potok imeet vid
It was new. It was singular. It was simple. It must succeed.
programma vydast
4 It
1 must
1 new.
1 simple.
1 singular.
1 succeed.
3 was.
Konechno, opredelit' associativnyj massiv mozhno mnogimi sposobami, a,
imeya opredelenie Map i svyazannogo s nim klassa iteratora, my mozhem
predlozhit' mnogo sposobov dlya ih realizacii. Zdes' vybran
trivial'nyj sposob realizacii. Ispol'zuetsya linejnyj poisk, kotoryj
ne podhodit dlya bol'shih massivov. Estestvenno, rasschitannaya na
kommercheskoe primenenie realizaciya budet sozdavat'sya, ishodya iz
trebovanij bystrogo poiska i kompaktnosti predstavleniya
(sm. uprazhnenie 4 iz $$8.9).
My ispol'zuem spisok s dvojnoj svyaz'yu Link:
template<class K, class V> class Map;
template<class K, class V> class Mapiter;
template<class K, class V> class Link {
friend class Map<K,V>;
friend class Mapiter<K,V>;
private:
const K key;
V value;
Link* pre;
Link* suc;
Link(const K& k, const V& v) : key(k), value(v) { }
~Link() { delete suc; } // rekursivnoe udalenie vseh
// ob容ktov v spiske
};
Kazhdyj ob容kt Link soderzhit paru (klyuch, znachenie). Klassy opisany
v Link kak druz'ya, i eto garantiruet, chto ob容kty Link mozhno
sozdavat', rabotat' s nimi i unichtozhat' tol'ko s pomoshch'yu
sootvetstvuyushchih klassov iteratora i Map. Obratite vnimanie na
predvaritel'nye opisaniya shablonnyh klassov Map i Mapiter.
SHablon Map mozhno opredelit' tak:
template<class K, class V> class Map {
friend class Mapiter<K,V>;
Link<K,V>* head;
Link<K,V>* current;
V def_val;
K def_key;
int sz;
void find(const K&);
void init() { sz = 0; head = 0; current = 0; }
public:
Map() { init(); }
Map(const K& k, const V& d)
: def_key(k), def_val(d) { init(); }
~Map() { delete head; } // rekursivnoe udalenie
// vseh ob容ktov v spiske
Map(const Map&);
Map& operator= (const Map&);
V& operator[] (const K&);
int size() const { return sz; }
void clear() { delete head; init(); }
void remove(const K& k);
// funkcii dlya iteracii
Mapiter<K,V> element(const K& k)
{
(void) operator[](k); // sdelat' k tekushchim elementom
return Mapiter<K,V>(this,current);
}
Mapiter<K,V> first();
Mapiter<K,V> last();
};
|lementy hranyatsya v uporyadochennom spiske s dojnoj svyaz'yu. Dlya
prostoty nichego ne delaetsya dlya uskoreniya poiska
(sm. uprazhnenie 4 iz $$8.9). Klyuchevoj zdes' yavlyaetsya funkciya
operator[]():
template<class K, class V>
V& Map<K,V>::operator[] (const K& k)
{
if (head == 0) {
current = head = new Link<K,V>(k,def_val);
current->pre = current->suc = 0;
return current->value;
}
Link<K,V>* p = head;
for (;;) {
if (p->key == k) { // najdeno
current = p;
return current->value;
}
if (k < p->key) { // vstavit' pered p (v nachalo)
current = new Link<K,V>(k,def_val);
current->pre = p->pre;
current->suc = p;
if (p == head) // tekushchij element stanovitsya nachal'nym
head = current;
else
p->pre->suc = current;
p->pre = current;
return current->value;
}
Link<K,V>* s = p->suc;
if (s == 0) { // vstavit' posle p (v konec)
current = new Link<K,V>(k,def_val);
current->pre = p;
current->suc = 0;
p->suc = current;
return current->value;
}
p = s;
}
}
Operaciya indeksacii vozvrashchaet ssylku na znachenie, kotoroe
sootvetstvuet zadannomu kak parametr klyuchu. Esli takoe znachenie
ne najdeno, vozvrashchaetsya novyj element so standartnym znacheniem.
|to pozvolyaet ispol'zovat' operaciyu indeksacii v levoj chasti
prisvaivaniya. Standartnye znacheniya dlya klyuchej i znachenij
ustanavlivayutsya konstruktorami Map. V operacii indeksacii opredelyaetsya
znachenie current, ispol'zuemoe iteratorami.
Realizaciya ostal'nyh funkcij-chlenov ostavlena v kachestve
uprazhneniya:
template<class K, class V>
void Map<K,V>::remove(const K& k)
{
// sm. uprazhnenie 2 iz $$8.10
}
template<class K, class V>
Map<K,V>::Map(const Map<K,V>& m)
{
// kopirovanie tablicy Map i vseh ee elementov
}
template<class K, class V>
Map& Map<K,V>::operator=(const Map<K,V>& m)
{
// kopirovanie tablicy Map i vseh ee elementov
}
Teper' nam ostalos' tol'ko opredelit' iteraciyu. V klasse Map
est' funkcii-chleny first(), last() i element(const K&), kotorye
vozvrashchayut iterator, ustanovlennyj sootvetstvenno na pervyj, poslednij
ili zadavaemyj klyuchom-parametrom element. Sdelat' eto mozhno, poskol'ku
elementy hranyatsya v uporyadochennom po klyucham vide.
Iterator Mapiter dlya Map opredelyaetsya tak:
template<class K, class V> class Mapiter {
friend class Map<K,V>;
Map<K,V>* m;
Link<K,V>* p;
Mapiter(Map<K,V>* mm, Link<K,V>* pp)
{ m = mm; p = pp; }
public:
Mapiter() { m = 0; p = 0; }
Mapiter(Map<K,V>& mm);
operator void*() { return p; }
const K& key();
V& value();
Mapiter& operator--(); // prefiksnaya
void operator--(int); // postfiksnaya
Mapiter& operator++(); // prefiksnaya
void operator++(int); // postfiksnaya
};
Posle pozicionirovaniya iteratora funkcii key() i value() iz Mapiter
vydayut klyuch i znachenie togo elementa, na kotoryj ustanovlen
iterator.
template<class K, class V> const K& Mapiter<K,V>::key()
{
if (p) return p->key; else return m->def_key;
}
template<class K, class V> V& Mapiter<K,V>::value()
{
if (p) return p->value; else return m->def_val;
}
Po analogii s ukazatelyami opredeleny operacii ++ i -- dlya prodvizheniya
po elementam Map vpered i nazad:
Mapiter<K,V>& Mapiter<K,V>::operator--() //prefiksnyj dekrement
{
if (p) p = p->pre;
return *this;
}
void Mapiter<K,V>::operator--(int) // postfiksnyj dekrement
{
if (p) p = p->pre;
}
Mapiter<K,V>& Mapiter<K,V>::operator++() // prefiksnyj inkrement
{
if (p) p = p->suc;
return *this;
}
void Mapiter<K,V>::operator++(int) // postfiksnyj inkrement
{
if (p) p = p->suc;
}
Postfiksnye operacii opredeleny tak, chto oni ne vozvrashchayut nikakogo
znacheniya. Delo v tom, chto zatraty na sozdanie i peredachu novogo
ob容kta Mapiter na kazhdom shage iteracii znachitel'ny, a pol'za ot
nego budet ne velika.
Ob容kt Mapiter mozhno inicializirovat' tak, chtoby on byl
ustanovlen na nachalo Map:
template<class K, class V> Mapiter<K,V>::Mapiter(Map<K,V>& mm)
{
m == &mm; p = m->head;
}
Operaciya preobrazovaniya operator void*() vozvrashchaet nul', esli
iterator ne ustanovlen na element Map, i nenulevoe znachenie inache.
Znachit mozhno proveryat' iterator iter, naprimer, tak:
void f(Mapiter<const char*, Shape*>& iter)
{
// ...
if (iter) {
// ustanovlen na element tablicy
}
else {
// ne ustanovlen na element tablicy
}
// ...
}
Analogichnyj priem ispol'zuetsya dlya kontrolya potokovyh operacij
vvoda-vyvoda v $$10.3.2.
Esli iterator ne ustanovlen na element tablicy, ego funkcii
key() i value() vozvrashchayut ssylki na standartnye ob容kty.
Esli posle vseh etih opredelenij vy zabyli ih naznachenie, mozhno
privesti eshche odnu nebol'shuyu programmu, ispol'zuyushchuyu tablicu Map.
Pust' vhodnoj potok yavlyaetsya spiskom par znachenij sleduyushchego vida:
hammer 2
nail 100
saw 3
saw 4
hammer 7
nail 1000
nail 250
Nuzhno otsortirovat' spisok tak, chtoby znacheniya, sootvetstvuyushchie odnomu
predmetu, skladyvalis', i napechatat' poluchivshijsya spisok vmeste s
itogovym znacheniem:
hammer 9
nail 1350
saw 7
-------------------
total 1366
Vnachale napishem funkciyu, kotoraya chitaet vhodnye stroki i zanosit
predmety s ih kolichestvom v tablicu. Klyuchom v etoj tablice yavlyaetsya
pervoe slovo stroki:
template<class K, class V>
void readlines(Map<K,V>&key)
{
K word;
while (cin >> word) {
V val = 0;
if (cin >> val)
key[word] +=val;
else
return;
}
}
Teper' mozhno napisat' prostuyu programmu, vyzyvayushchuyu funkciyu
readlines() i pechatayushchuyu poluchivshuyusya tablicu:
main()
{
Map<String,int> tbl("nil",0);
readlines(tbl);
int total = 0;
for (Mapiter<String,int> p(tbl); p; ++p) {
int val = p.value();
total +=val;
cout << p.key() << '\t' << val << '\n';
}
cout << "--------------------\n";
cout << "total\t" << total << '\n';
}
1. (*2) Opredelite semejstvo spiskov s dvojnoj svyaz'yu, kotorye
budut dvojnikami spiskov s odnoj svyaz'yu, opredelennyh v $$8.3.
2. (*3) Opredelite shablon tipa String, parametrom kotorogo yavlyaetsya
tip simvola. Pokazhite kak ego mozhno ispol'zovat' ne tol'ko dlya
obychnyh simvolov, no i dlya gipoteticheskogo klassa lchar, kotoryj
predstavlyaet simvoly ne iz anglijskogo alfavita ili rasshirennyj
nabor simvolov. Nuzhno postarat'sya tak opredelit' String, chtoby
pol'zovatel' ne zametil uhudsheniya harakteristik programmy po
pamyati i vremeni ili v udobstve po sravneniyu s obychnym strokovym
klassom.
3. (*1.5) Opredelite klass Record (zapis') s dvumya chlenami-dannymi:
count (kolichestvo) i price (cena). Uporyadochite vektor iz takih
zapisej po kazhdomu iz chlenov. Pri etom nel'zya izmenyat' funkciyu
sortirovki i shablon Vector.
4. (*2) Zavershite opredeleniya shablonnogo klassa Map, napisav
nedostayushchie funkcii-chleny.
5. (*2) Zadajte druguyu realizaciyu Map iz $$8.8, ispol'zuya spisochnyj
klass s dvojnoj svyaz'yu.
6. (*2.5) Zadajte druguyu realizaciyu Map iz $$8.8, ispol'zuya
sbalansirovannoe derevo. Takie derev'ya opisany v $$6.2.3 knigi
D. Knut "Iskusstvo programmirovaniya dlya |VM" t.1, "Mir", 1978 [K].
7. (*2) Sravnite kachestvo dvuh realizacij Map. V pervoj ispol'zuetsya
klass Link so svoej sobstvennoj funkciej razmeshcheniya, a vo vtoroj
- bez nee.
8. (*3) Sravnite proizvoditel'nost' programmy podscheta slov iz
$$8.8 i takoj zhe programmy, ne ispol'zuyushchej klassa Map. Operacii
vvoda-vyvoda dolzhny odinakovo ispol'zovat'sya v obeih programmah.
Sravnite neskol'ko takih programm, ispol'zuyushchih raznye varianty
klassa Map, v tom chisle i klass iz vashej biblioteki, esli on tam
est'.
9. (*2.5) S pomoshch'yu klassa Map realizujte topologicheskuyu sortirovku.
Ona opisana v [K] t.1, str. 323-332. (sm. uprazhnenie 6).
10. (*2) Modificirujte programmu iz $$8.8 tak, chtoby ona rabotala
pravil'no dlya dlinnyh imen i dlya imen, soderzhashchih probely
(naprimer, "thumb back").
11. (*2) Opredelite shablon tipa dlya chteniya razlichnyh vidov strok,
naprimer, takih (predmet, kolichestvo, cena).
12. (*2) Opredelite klass Sort iz $$8.4.5, ispol'zuyushchij sortirovku
po metodu SHella. Pokazhite kak mozhno zadat' metod sortirovki
s pomoshch'yu parametra shablona. Algoritm sortirovki opisan v [K]
t.3, $$5.2.1 (sm. uprazhnenie 6).
13. (*1) Izmenite opredeleniya Map i Mapiter tak, chtoby postfiksnye
operacii ++ i -- vozvrashchali ob容kt Mapiter.
14. (*1.5) Ispol'zujte shablony tipa v stile modul'nogo
programmirovaniya, kak eto bylo pokazano v $$8.4.5 i napishite
funkciyu sortirovki, rasschitannuyu srazu na Vector<T> i T[].
YA prerval vas, poetomu ne preryvajte menya.
- Uinston CHerchill
V etoj glave opisan mehanizm obrabotki osobyh situacij i nekotorye,
osnovyvayushchiesya na nem, sposoby obrabotki oshibok. Mehanizm sostoit
v zapuske osoboj situacii, kotoruyu dolzhen perehvatit' special'nyj
obrabotchik. Opisyvayutsya pravila perehvata osobyh situacij i
pravila reakcii na neperehvachennye i neozhidannye osobye situacii.
Celye gruppy osobyh situacij mozhno opredelit' kak proizvodnye
klassy. Opisyvaetsya sposob, ispol'zuyushchij destruktory i obrabotku
osobyh situacij, kotoryj obespechivaet nadezhnoe i skrytoe ot
pol'zovatelya upravlenie resursami.
Sozdatel' biblioteki sposoben obnaruzhit' dinamicheskie oshibki, no ne
predstavlyaet kakoj v obshchem sluchae dolzhna byt' reakciya na nih.
Pol'zovatel' biblioteki sposoben napisat' reakciyu na takie oshibki,
no ne v silah ih obnaruzhit'. Esli by on mog, to sam razobralsya by
s oshibkami v svoej programme, i ih ne prishlos' by vyyavlyat'
v bibliotechnyh funkciyah. Dlya resheniya etoj problemy v yazyk vvedeno
ponyatie osoboj situacii X.
X Tol'ko nedavno komitetom po standartizacii S++ osobye situacii byli
vklyucheny v standart yazyka, no na vremya napisaniya etoj knigi oni eshche
ne voshli v bol'shinstvo realizacij.
Sut' etogo ponyatiya v tom, chto funkciya, kotoraya obnaruzhila oshibku i ne
mozhet spravit'sya s neyu, zapuskaet osobuyu situaciyu, rasschityvaya, chto
ustranit' problemu mozhno v toj funkcii, kotoraya pryamo ili oposredovanno
vyzyvala pervuyu. Esli funkciya rasschitana na obrabotku oshibok nekotorogo
vida, ona mozhet ukazat' eto yavno, kak gotovnost' perehvatit' dannuyu
osobuyu situaciyu.
Rassmotrim v kachestve primera kak dlya klassa Vector mozhno
predstavlyat' i obrabatyvat' osobye situacii, vyzvannye vyhodom za
granicu massiva:
class Vector {
int* p;
int sz;
public:
class Range { }; // klass dlya osoboj situacii
int& operator[](int i);
// ...
};
Predpolagaetsya, chto ob容kty klassa Range budut ispol'zovat'sya kak
osobye situacii, i zapuskat' ih mozhno tak:
int& Vector::operator[](int i)
{
if (0<=i && i<sz) return p[i];
throw Range();
}
Esli v funkcii predusmotrena reakciya na oshibku nedopustimogo znacheniya
indeksa, to tu chast' funkcii, v kotoroj eti oshibki budut perehvatyvat'sya,
nado pomestit' v operator try. V nem dolzhen byt' i obrabotchik osoboj
situacii:
void f(Vector& v)
{
// ...
try {
do_something(v); // soderzhatel'naya chast', rabotayushchaya s v
}
catch (Vector::Range) {
// obrabotchik osoboj situacii Vector::Range
// esli do_something() zavershitsya neudachno,
// nuzhno kak-to sreagirovat' na eto
// syuda my popadem tol'ko v tom sluchae, kogda
// vyzov do_something() privedet k vyzovu Vector::operator[]()
// iz-za nedopustimogo znacheniya indeksa
}
// ...
}
Obrabotchikom osoboj situacii nazyvaetsya konstrukciya
catch ( /* ... */ ) {
// ...
}
Ee mozhno ispol'zovat' tol'ko srazu posle bloka, nachinayushchegosya sluzhebnym
slovom try, ili srazu posle drugogo obrabotchika osoboj situacii. Sluzhebnym
yavlyaetsya i slovo catch. Posle nego idet v skobkah opisanie, kotoroe
ispol'zuetsya analogichno opisaniyu formal'nyh parametrov funkcii, a imenno,
v nem zadaetsya tip ob容ktov, na kotorye rasschitan obrabotchik, i,
vozmozhno, imena parametrov (sm. $$9.3). Esli v do_something() ili v
lyuboj vyzvannoj iz nee funkcii proizojdet oshibka indeksa (na lyubom
ob容kte Vector), to obrabotchik perehvatit osobuyu situaciyu i budet
vypolnyat'sya chast', obrabatyvayushchaya oshibku. Naprimer, opredeleniya sleduyushchih
funkcij privedut k zapusku obrabotchika v f():
void do_something()
{
// ...
crash(v);
// ...
}
void crash(Vector& v)
{
v[v.size()+10]; // iskusstvenno vyzyvaem oshibku indeksa
}
Process zapuska i perehvata osoboj situacii predpolagaet prosmotr
cepochki vyzovov ot tochki zapuska osoboj situacii do funkcii, v kotoroj
ona perehvatyvaetsya. Pri etom vosstanavlivaetsya sostoyanie steka,
sootvetstvuyushchee funkcii, perehvativshej oshibku, i pri prohode po vsej
cepochke vyzovov dlya lokal'nyh ob容ktov funkcij iz etoj cepochki vyzyvayutsya
destruktory. Podrobno eto opisano v $$9.4.
Esli pri prosmotre vsej cepochki vyzovov, nachinaya s zapustivshej
osobuyu situaciyu funkcii, ne obnaruzhitsya podhodyashchij obrabotchik, to
programma zavershaetsya. Podrobno eto opisano v $$9.7.
Esli obrabotchik perehvatil osobuyu situaciyu, to ona budet obrabatyvat'sya
i drugie, rasschitannye na etu situaciyu, obrabotchiki ne budut
rassmatrivat'sya. Inymi slovami, aktivirovan budet tol'ko tot obrabotchik,
kotoryj nahoditsya v samoj poslednej vyzyvavshejsya funkcii, soderzhashchej
sootvetstvuyushchie obrabotchiki. V nashem primere funkciya f() perehvatit
Vector::Range, poetomu etu osobuyu situaciyu nel'zya perehvatit' ni v
kakoj vyzyvayushchej f() funkcii:
int ff(Vector& v)
{
try {
f(v); // v f() budet perehvachena Vector::Range
}
catch (Vector::Range) { // znachit syuda my nikogda ne popadem
// ...
}
}
9.1.1 Osobye situacii i tradicionnaya obrabotka oshibok
Nash sposob obrabotki oshibok po mnogim parametram vygodno otlichaetsya ot
bolee tradicionnyh sposobov. Perechislim, chto mozhet sdelat' operaciya
indeksacii Vector::operator[]() pri obnaruzhenii nedopustimogo znacheniya
indeksa:
[1] zavershit' programmu;
[2] vozvratit' znachenie, traktuemoe kak "oshibka";
[3] vozvratit' normal'noe znachenie i ostavit' programmu v
neopredelennom sostoyanii;
[4] vyzvat' funkciyu, zadannuyu dlya reakcii na takuyu oshibku.
Variant [1] ("zavershit' programmu") realizuetsya po umolchaniyu v tom
sluchae, kogda osobaya situaciya ne byla perehvachena. Dlya bol'shinstva
oshibok mozhno i nuzhno obespechit' luchshuyu reakciyu.
Variant [2] ("vozvratit' znachenie "oshibka"") mozhno realizovat'
ne vsegda, poskol'ku ne vsegda udaetsya opredelit' znachenie "oshibka".
Tak, v nashem primere lyuboe celoe yavlyaetsya dopustimym znacheniem dlya
rezul'tata operacii indeksacii. Esli mozhno vydelit' takoe osoboe
znachenie, to chasto etot variant vse ravno okazyvaetsya neudobnym,
poskol'ku proveryat' na eto znachenie prihoditsya pri kazhdom vyzove. Tak
mozhno legko udvoit' razmer programmy. Poetomu dlya obnaruzheniya vseh
oshibok etot variant redko ispol'zuetsya posledovatel'no.
Variant [3] ("ostavit' programmu v neopredelennom sostoyanii")
imeet tot nedostatok, chto vyzyvavshaya funkciya mozhet ne zametit'
nenormal'nogo sostoyaniya programmy. Naprimer, vo mnogih funkciyah
standartnoj biblioteki S dlya signalizacii ob oshibke ustanavlivaetsya
sootvetstvuyushchee znachenie global'noj peremennoj errno. Odnako,
v programmah pol'zovatelya obychno net dostatochno posledovatel'nogo
kontrolya errno, i v rezul'tate voznikayut navedennye oshibki,
vyzvannye tem, chto standartnye funkcii vozvrashchayut ne to znachenie.
Krome togo, esli v programme est' parallel'nye vychisleniya,
ispol'zovanie odnoj global'noj peremennoj dlya signalizacii o raznyh
oshibkah neizbezhno privedet k katastrofe.
Obrabotka osobyh situacij ne prednaznachalas' dlya teh sluchaev,
na kotorye rasschitan variant [4] ( "vyzvat' funkciyu reakcii na
oshibku"). Otmetim, odnako, chto esli osobye situacii ne predusmotreny,
to vmesto funkcii reakcii na oshibku mozhno kak raz ispol'zovat'
tol'ko odin iz treh perechislennyh variantov. Obsuzhdenie funkcij
reakcij i osobyh situaciej budet prodolzheno v $$9.4.3.
Mehanizm osobyh situacij uspeshno zamenyaet tradicionnye
sposoby obrabotki oshibok v teh sluchayah, kogda poslednie yavlyayutsya
nepolnym, nekrasivym ili chrevatym oshibkami resheniem. |tot mehanizm
pozvolyaet yavno otdelit' chast' programmy, v kotoroj obrabatyvayutsya
oshibki, ot ostal'noj ee chasti, tem samym programma stanovitsya bolee
ponyatnoj i s nej proshche rabotat' razlichnym servisnym programmam.
Svojstvennyj etomu mehanizmu regulyarnyj sposob obrabotki oshibok
uproshchaet vzaimodejstvie mezhdu razdel'no napisannymi chastyami
programmy.
V etom sposobe obrabotki oshibok est' dlya programmiruyushchih na S
novyj moment: standartnaya reakciya na oshibku (osobenno na oshibku
v bibliotechnoj funkcii) sostoit v zavershenii programmy. Tradicionnoj
byla reakciya prodolzhat' programmu v nadezhde, chto ona kak-to
zavershitsya sama. Poetomu sposob, baziruyushchijsya na osobyh situaciyah,
delaet programmu bolee "hrupkoj" v tom smysle, chto trebuetsya
bol'she usilij i vnimaniya dlya ee normal'nogo vypolneniya. No eto vse-taki
luchshe, chem poluchat' nevernye rezul'taty na bolee pozdnej stadii
razvitiya programmy (ili poluchat' ih eshche pozzhe, kogda programmu
sochtut zavershennoj i peredadut nichego ne podozrevayushchemu pol'zovatelyu).
Esli zavershenie programmy yavlyaetsya nepriemlemoj reakciej, mozhno
smodelirovat' tradicionnuyu reakciyu s pomoshch'yu perehvata vseh osobyh
situacij ili vseh osobyh situacij, prinadlezhashchih special'nomu
klassu ($$9.3.2).
Mehanizm osobyh situacij mozhno rassmatrivat' kak dinamicheskij
analog mehanizma kontrolya tipov i proverki neodnoznachnosti
na stadii translyacii. Pri takom podhode bolee vazhnoj stanovitsya
stadiya proektirovaniya programmy, i trebuetsya bol'shaya podderzhka
processa vypolneniya programmy, chem dlya programm na S. Odnako,
v rezul'tate poluchitsya bolee predskazuemaya programma, ee budet proshche
vstroit' v programmnuyu sistemu, ona budet ponyatnee drugim programmistam i
s nej proshche budet rabotat' razlichnym servisnym programmam. Mozhno
skazat', chto mehanizm osobyh situacij podderzhivaet,
podobno drugim sredstvam S++, "horoshij" stil' programmirovaniya,
kotoryj v takih yazykah, kak S, mozhno primenyat' tol'ko ne v polnom
ob容me i na neformal'nom urovne.
Vse zhe nado soznavat', chto obrabotka oshibok ostaetsya trudnoj
zadachej, i, hotya mehanizm osobyh situacij bolee strogij,
chem tradicionnye sposoby, on vse ravno nedostatochno strukturirovan
po sravneniyu s konstrukciyami, dopuskayushchimi tol'ko lokal'nuyu peredachu
upravleniya.
9.1.2 Drugie tochki zreniya na osobye situacii
"Osobaya situaciya" - odno iz teh ponyatij, kotorye imeyut raznyj smysl
dlya raznyh lyudej. V S++ mehanizm osobyh situacij prednaznachen dlya
obrabotki oshibok. V chastnosti, on prednaznachen dlya obrabotki oshibok
v programmah, sostoyashchih iz nezavisimo sozdavaemyh komponentov.
|tot mehanizm rasschitan na osobye situacii, voznikayushchie tol'ko pri
posledovatel'nom vypolnenii programmy (naprimer, kontrol' granic
massiva). Asinhronnye osobye situacii takie, naprimer, kak preryvaniya
ot klaviatury, nel'zya neposredstvenno obrabatyvat' s pomoshch'yu etogo
mehanizma. V razlichnyh sistemah sushchestvuyut drugie mehanizmy,
naprimer, signaly, no oni zdes' ne rassmatrivayutsya, poskol'ku zavisyat
ot konkretnoj sistemy.
Mehanizm osobyh situacij yavlyaetsya konstrukciej s nelokal'noj
peredachej upravleniya i ego mozhno rassmatrivat' kak variant operatora
return. Poetomu osobye situacii mozhno ispol'zovat' dlya celej, nikak
ne svyazannyh s obrabotkoj oshibok ($$9.5). Vse-taki osnovnym
naznacheniem mehanizma osobyh situacij i temoj etoj glavy budet
obrabotka oshibok i sozdanie ustojchivyh k oshibkam programm.
9.2 Razlichenie osobyh situacij
Estestvenno, v programme vozmozhny neskol'ko razlichnyh dinamicheskih
oshibok. |ti oshibki mozhno sopostavit' s osobymi situaciyami, imeyushchimi
razlichnye imena. Tak, v klasse Vector obychno prihoditsya vyyavlyat'
i soobshchat' ob oshibkah dvuh vidov: oshibki diapazona i oshibki,
vyzvannye nepodhodyashchim dlya konstruktora parametrom:
class Vector {
int* p;
int sz;
public:
enum { max = 32000 };
class Range { }; // osobaya situaciya indeksa
class Size { }; // osobaya situaciya "nevernyj razmer"
Vector(int sz);
int& operator[](int i);
// ...
};
Kak bylo skazano, operaciya indeksacii zapuskaet osobuyu situaciyu
Range, esli ej zadan vyhodyashchij iz diapazona znachenij indeks.
Konstruktor zapuskaet osobuyu situaciyu Size, esli emu zadan
nedopustimyj razmer vektora:
Vector::Vector(int sz)
{
if (sz<0 || max<sz) throw Size();
// ...
}
Pol'zovatel' klassa Vector mozhet razlichit' eti dve osobye situacii,
esli v proveryaemom bloke (t.e. v bloke operatora try) ukazhet
obrabotchiki dlya obeih situacij:
void f()
{
try {
use_vectors();
}
catch (Vector::Range) {
// ...
}
catch (Vector::Size) {
// ...
}
}
V zavisimosti ot osoboj situacii budet vypolnyat'sya sootvetstvuyushchij
obrabotchik. Esli upravlenie dojdet do konca operatorov obrabotchika,
sleduyushchim budet vypolnyat'sya operator, kotoryj idet posle spiska
obrabotchikov:
void f()
{
try {
use_vectors();
}
catch (Vector::Range) {
// ispravit' indeks i
// poprobovat' opyat':
f();
}
catch (Vector::Size) {
cerr << "Oshibka v konstruktore Vector::Size";
exit(99);
}
// syuda my popadem, esli voobshche ne bylo osobyh situacij
// ili posle obrabotki osoboj situacii Range
}
Spisok obrabotchikov napominaet pereklyuchatel', no zdes' v tele
obrabotchika operatory break ne nuzhny. Sintaksis spiska obrabotchikov
otlichen ot sintaksisa variantov case pereklyuchatelya chastichno po
etoj prichine, chastichno potomu, chtoby pokazat', chto kazhdyj
obrabotchik opredelyaet svoyu oblast' vidimosti (sm. $$9.8).
Ne obyazatel'no vse osobye situacii perehvatyvat' v odnoj funkcii:
void f1()
{
try {
f2(v);
}
catch (Vector::Size) {
// ...
}
}
void f2(Vector& v)
{
try {
use_vectors();
}
catch (Vector::Range) {
// ...
}
}
Zdes' f2() perehvatit osobuyu situaciyu Range, voznikayushchuyu v
use_vectors(), a osobaya situaciya Size budet ostavlena dlya f1().
S tochki zreniya yazyka osobaya situaciya schitaetsya obrabotannoj srazu
pri vhode v telo ee obrabotchika. Poetomu vse osobye situacii,
zapuskaemye pri vypolnenii etogo obrabotchika, dolzhny obrabatyvat'sya
v funkciyah, vyzvavshih tu funkciyu, kotoraya soderzhit proveryaemyj blok.
Znachit v sleduyushchem primere ne vozniknet beskonechnogo cikla:
try {
// ...
}
catch (input_overflow) {
// ...
throw input_overflow();
}
Zdes' input_overflow (perepolnenie pri vvode) - imya global'nogo klassa.
Obrabotchiki osobyh situacij mogut byt' vlozhennymi:
try {
// ...
}
catch (xxii) {
try {
// slozhnaya reakciya
}
catch (xxii) {
// oshibka v processe slozhnoj reakcii
}
}
Odnako, takaya vlozhennost' redko byvaet nuzhna v obychnyh programmah,
i chashche vsego ona yavlyaetsya svidetel'stvom plohogo stilya.
9.3 Imena osobyh situacij
Osobaya situaciya perehvatyvaetsya blagodarya svoemu tipu. Odnako,
zapuskaetsya ved' ne tip, a ob容kt. Esli nam nuzhno peredat' nekotoruyu
informaciyu iz tochki zapuska v obrabotchik, to dlya etogo ee sleduet
pomestit' v zapuskaemyj ob容kt. Naprimer, dopustim nuzhno znat'
znachenie indeksa, vyhodyashchee za granicy diapazona:
class Vector {
// ...
public:
class Range {
public:
int index;
Range(int i) : index(i) { }
};
// ...
int& operator[](int i)
// ...
};
int Vector::operator[](int i)
{
if (o<=i && i <sz) return p[i];
throw Range(i);
}
CHtoby issledovat' nedopustimoe znachenie indeksa, v obrabotchike
nuzhno dat' imya ob容ktu, predstavlyayushchemu osobuyu situaciyu:
void f(Vector& v)
{
// ...
try {
do_something(v);
}
catch (Vector::Range r ) {
cerr << "nedopustimyj indeks" << r.index << '\n';
// ...
}
// ...
}
Konstrukciya v skobkah posle sluzhebnogo slova catch yavlyaetsya po suti
opisaniem i ona analogichna opisaniyu formal'nogo parametra funkcii.
V nej ukazyvaetsya kakim mozhet byt' tip parametra (t.e. osoboj
situacii) i mozhet zadavat'sya imya dlya fakticheskoj, t.e. zapushchennoj,
osoboj situacii. Vspomnim, chto v shablonah tipov u nas byl vybor
dlya imenovaniya osobyh situacij. V kazhdom sozdannom po shablonu klasse
byl svoj klass osoboj situacii:
template<class T> class Allocator {
// ...
class Exhausted { }
// ...
T* get();
};
void f(Allocator<int>& ai, Allocator<double>& ad)
{
try {
// ...
}
catch (Allocator<int>::Exhausted) {
// ...
}
catch (Allocator<double>::Exhausted) {
// ...
}
}
S drugoj storony, osobaya situaciya mozhet byt' obshchej dlya vseh
sozdannyh po shablonu klassov:
class Allocator_Exhausted { };
template<class T> class Allocator {
// ...
T* get();
};
void f(Allocator<int>& ai, Allocator<double>& ad)
{
try {
// ...
}
catch (Allocator_Exhausted) {
// ...
}
}
Kakoj sposob zadaniya osoboj situacii predpochtitel'nej, skazat' trudno.
Vybor zavisit ot naznacheniya rassmatrivaemogo shablona.
9.3.1 Gruppirovanie osobyh situacij
Osobye situacii estestvennym obrazom razbivayutsya na semejstva.
Dejstvitel'no, logichno predstavlyat' semejstvo Matherr, v kotoroe
vhodyat Overflow (perepolnenie), Underflow (poterya znachimosti) i
nekotorye drugie osobye situacii. Semejstvo Matherr obrazuyut
osobye situacii, kotorye mogut zapuskat' matematicheskie funkcii
standartnoj biblioteki.
Odin iz sposobov zadaniya takogo semejstva svoditsya k opredeleniyu
Matherr kak tipa, vozmozhnye znacheniya kotorogo vklyuchayut Overflow i
vse ostal'nye:
enum { Overflow, Underflow, Zerodivide, /* ... */ };
try {
// ...
}
catch (Matherr m) {
switch (m) {
case Overflow:
// ...
case Underflow:
// ...
// ...
}
// ...
}
Drugoj sposob predpolagaet ispol'zovanie nasledovaniya i virtual'nyh
funkcij, chtoby ne vvodit' pereklyuchatelya po znacheniyu polya tipa.
Nasledovanie pomogaet opisat' semejstva osobyh situacij:
class Matherr { };
class Overflow: public Matherr { };
class Underflow: public Matherr { };
class Zerodivide: public Matherr { };
// ...
CHasto byvaet tak, chto nuzhno obrabotat' osobuyu situaciyu Matherr
ne zavisimo ot togo, kakaya imenno situaciya iz etogo semejstva
proizoshla. Nasledovanie pozvolyaet sdelat' eto prosto:
try {
// ...
}
catch (Overflow) {
// obrabotka Overflow ili lyuboj proizvodnoj situacii
}
catch (Matherr) {
// obrabotka lyuboj otlichnoj ot Overflow situacii
}
V etom primere Overflow razbiraetsya otdel'no, a vse drugie osobye
situacii iz Matherr razbirayutsya kak odin obshchij sluchaj. Konechno,
funkciya, soderzhashchaya catch (Matherr), ne budet znat' kakuyu imenno
osobuyu situaciyu ona perehvatyvaet. No kakoj by ona ni byla, pri
vhode v obrabotchik peredavaemaya ee kopiya budet Matherr. Obychno eto
kak raz to, chto nuzhno. Esli eto ne tak, osobuyu situaciyu mozhno
perehvatit' po ssylke (sm. $$9.3.2).
Ierarhicheskoe uporyadochivanie osobyh situacij mozhet igrat' vazhnuyu
rol' dlya sozdaniya yasnoj struktury programmy. Dejstvitel'no, pust'
takoe uporyadochivanie otsutstvuet, i nuzhno obrabotat' vse osobye
situacii standartnoj biblioteki matematicheskih funkcij. Dlya etogo
pridetsya do beskonechnosti perechislyat' vse vozmozhnye osobye situacii:
try {
// ...
}
catch (Overflow) { /* ... */ }
catch (Underflow) { /* ... */ }
catch (Zerodivide) { /* ... */ }
// ...
|to ne tol'ko utomitel'no, no i opasno, poskol'ku mozhno zabyt'
kakuyu-nibud' osobuyu situaciyu. Krome togo, neobhodimost' perechislit'
v proveryaemom bloke vse osobye situacii prakticheski garantiruet,
chto, kogda semejstvo osobyh situacij biblioteki rasshiritsya, v
programme pol'zovatelya vozniknet oshibka. |to znachit, chto pri
vvedenii novoj osoboj situacii v biblioteki matematicheskih funkcij
pridetsya peretranslirovat' vse chasti programmy, kotorye soderzhat
obrabotchiki vseh osobyh situacij iz Matherr. V obshchem sluchae takaya
peretranslyaciya nepriemlema. CHasto dazhe net vozmozhnosti najti
vse trebuyushchie peretranslyacii chasti programmy. Esli takaya
vozmozhnost' est', nel'zya trebovat', chtoby vsegda byl
dostupen ishodnoj tekst lyuboj chasti bol'shoj programmy, ili chtoby u nas
byli prava izmenyat' lyubuyu chast' bol'shoj programmy, ishodnyj tekst
kotoroj my imeem. Na samom dele, pol'zovatel' ne dolzhen dumat'
o vnutrennem ustrojstve bibliotek. Vse eti problemy peretranslyacii
i soprovozhdeniya mogut privesti k tomu, chto posle sozdaniya
pervoj versii biblioteki budet nel'zya vvodit' v nej novye
osobye situacii. No takoe reshenie ne podhodit prakticheski dlya vseh
bibliotek.
Vse eti dovody govoryat za to, chto osobye situacii nuzhno
opredelyat' kak ierarhiyu klassov (sm. takzhe $$9.6.1). |to, v svoyu
ochered', oznachaet, chto osobye situacii mogut byt' chlenami neskol'kih
grupp:
class network_file_err // oshibki fajlovoj sistemy v seti
: public network_err, // oshibki seti
public file_system_err { // oshibki fajlovoj sistemy
// ...
};
Osobuyu situaciyu network_file_err mozhno perehvatit' v funkciyah,
obrabatyvayushchih osobye situacii seti:
void f()
{
try {
// kakie-to operatory
}
catch (network_err) {
// ...
}
}
Ee takzhe mozhno perehvatit' v funkciyah, obrabatyvayushchih osobye situacii
fajlovoj sistemy:
void g()
{
try {
// kakie-to drugie operatory
}
catch (file_system_err) {
// ...
}
}
|to vazhnyj moment, poskol'ku takoj sistemnyj servis kak rabota v
seti dolzhen byt' prozrachen, a eto oznachaet, chto sozdatel' funkcii
g() mozhet dazhe i ne znat', chto eta funkciya budet vypolnyat'sya
v setevom rezhime.
Otmetim, chto v nastoyashchee vremya net standartnogo mnozhestva
osobyh situacij dlya standartnoj matematicheskoj biblioteki i
biblioteki vvoda-vyvoda. Zadacha komitetov ANSI i ISO po standartizacii
S++ reshit' nuzhno li takoe mnozhestvo i kakie v nem sleduet ispol'zovat'
imena i klassy.
Poskol'ku mozhno srazu perehvatit' vse osobye situacii
(sm. $$9.3.2), net nastoyatel'noj neobhodimosti sozdavat' dlya etoj
celi obshchij, bazovyj dlya vseh osobyh situacij, klass. Odnako, esli
vse osobye situacii yavlyayutsya proizvodnymi ot pustogo klassa
Exception (osobaya situaciya), to v interfejsah ih ispol'zovanie
stanovitsya bolee regulyarnym (sm. $$9.6). Esli vy ispol'zuete obshchij
bazovyj klass Exception, ubedites', chto v nem nichego net krome
virtual'nogo destruktora. V protivnom sluchae takoj klass mozhet
vstupit' v protivorechie s predpolagaemym standartom.
9.3.2 Proizvodnye osobye situacii
Esli dlya obrabotki osobyh situacij my ispol'zuem ierarhiyu klassov,
to, estestvenno, kazhdyj obrabotchik dolzhen razbirat'sya tol'ko s
chast'yu informacii, peredavaemoj pri osobyh situaciyah. Mozhno skazat',
chto, kak pravilo, osobaya situaciya perehvatyvaetsya obrabotchikom
ee bazovogo klassa, a ne obrabotchikom klassa, sootvetstvuyushchego
imenno etoj osoboj situacii. Imenovanie i perehvat obrabotchikom osoboj
situacii semanticheski ekvivalentno imenovaniyu i polucheniyu parametra
v funkcii. Proshche govorya, formal'nyj parametr inicializiruetsya
znacheniem fakticheskogo parametra. |to oznachaet, chto zapushchennaya
osobaya situaciya "nizvoditsya" do osoboj situacii, ozhidaemoj
obrabotchikom. Naprimer:
class Matherr {
// ...
virtual void debug_print();
};
class Int_overflow : public Matherr {
public:
char* op;
int opr1, opr2;;
int_overflow(const char* p, int a, int b)
{ cerr << op << '(' << opr1 << ',' << opr2 << ')'; }
};
void f()
{
try {
g();
}
catch (Matherr m) {
// ...
}
}
Pri vhode v obrabotchik Matherr osobaya situaciya m yavlyaetsya ob容ktom
Matherr, dazhe esli pri obrashchenii k g() byla zapushchena Int_overflow.
|to oznachaet, chto dopolnitel'naya informaciya, peredavaemaya v
Int_overflow, nedostupna.
Kak obychno, chtoby imet' dostup k dopolnitel'noj informacii mozhno
ispol'zovat' ukazateli ili ssylki. Poetomu mozhno bylo napisat' tak:
int add(int x, int y) // slozhit' x i y s kontrolem
{
if (x > 0 && y > 0 && x > MAXINT - y
|| x < 0 && y < 0 && x < MININT + y)
throw Int_overflow("+", x, y);
// Syuda my popadaem, libo kogda proverka
// na perepolnenie dala otricatel'nyj rezul'tat,
// libo kogda x i y imeyut raznye znaki
return x + y;
}
void f()
{
try {
add(1,2);
add(MAXINT,-2);
add(MAXINT,2); // a dal'she - perepolnenie
}
catch (Matherr& m) {
// ...
m.debug_print();
}
}
Zdes' poslednee obrashchenie k add privedet k zapusku osoboj situacii,
kotoryj, v svoyu ochered', privedet k vyzovu Int_overflow::debug_print().
Esli by osobaya situaciya peredavalas' po znacheniyu, a ne po ssylke, to
byla by vyzvana funkciya Matherr::debug_print().
Neredko byvaet tak, chto perehvativ osobuyu situaciyu, obrabotchik
reshaet, chto s etoj oshibkoj on nichego ne smozhet podelat'. V takom
sluchae samoe estestvennoe zapustit' osobuyu situaciyu snova v nadezhde,
chto s nej sumeet razobrat'sya drugoj obrabotchik:
void h()
{
try {
// kakie-to operatory
}
catch (Matherr) {
if (can_handle_it) { // esli obrabotka vozmozhna,
// sdelat' ee
}
else {
throw; // povtornyj zapusk perehvachennoj
// osoboj situacii
}
}
}
Povtornyj zapusk zapisyvaetsya kak operator throw bez parametrov.
Pri etom snova zapuskaetsya ishodnaya osobaya situaciya, kotoraya byla
perehvachena, a ne ta ee chast', na kotoruyu rasschitan obrabotchik
Matherr. Inymi slovami, esli byla zapushchena Int_overflow, vyzyvayushchaya
h() funkciya mogla by perehvatit' ee kak Int_overflow, nesmotrya na
to, chto ona byla perehvachena v h() kak Matherr i zapushchena snova:
void k()
{
try {
h();
// ...
}
catch (Int_overflow) {
// ...
}
}
Polezen vyrozhdennyj sluchaj perezapuska. Kak i dlya funkcij,
ellipsis ... dlya obrabotchika oznachaet "lyuboj parametr", poetomu
operator catch (...) oznachaet perehvat lyuboj osoboj situacii:
void m()
{
try {
// kakie-to operatory
}
catch (...) {
// privesti vse v poryadok
throw;
}
}
|tot primer nado ponimat' tak: esli pri vypolnenii osnovnoj chasti
m() voznikaet osobaya situaciya, vypolnyaetsya obrabotchik, kotorye
vypolnyaet obshchie dejstviya po ustraneniyu posledstvij osoboj situacii,
posle etih dejstvij osobaya situaciya, vyzvavshaya ih, zapuskaetsya
povtorno.
Poskol'ku obrabotchik mozhet perehvatit' proizvodnye osobye situacii
neskol'kih tipov, poryadok, v kotorom idut obrabotchiki v proveryaemom
bloke, sushchestvenen. Obrabotchiki pytayutsya perehvatit' osobuyu
situaciyu v poryadke ih opisaniya. Privedem primer:
try {
// ...
}
catch (ibuf) {
// obrabotka perepolneniya bufera vvoda
}
catch (io) {
// obrabotka lyuboj oshibki vvoda-vyvoda
}
catch (stdlib) {
// obrabotka lyuboj osoboj situacii v biblioteke
}
catch (...) {
// obrabotka vseh ostal'nyh osobyh situacij
}
Tip osoboj situacii v obrabotchike sootvetstvuet tipu zapushchennoj
osoboj situacii v sleduyushchih sluchayah: esli eti tipy sovpadayut, ili
vtoroj tip yavlyaetsya tipom dostupnogo bazovogo klassa zapushchennoj situacii,
ili on yavlyaetsya ukazatelem na takoj klass, a tip ozhidaemoj situacii
tozhe ukazatel' ($$R.4.6).
Poskol'ku translyatoru izvestna ierarhiya klassov, on sposoben
obnaruzhit' takie nelepye oshibki, kogda obrabotchik catch (...) ukazan
ne poslednim, ili kogda obrabotchik situacii bazovogo klassa
predshestvuet obrabotchiku proizvodnoj ot etogo klassa situacii
($$R15.4). V oboih sluchaya, posleduyushchij obrabotchik (ili obrabotchiki)
ne mogut byt' zapushcheny, poskol'ku oni "maskiruyutsya" pervym
obrabotchikom.
Esli v nekotoroj funkcii potrebuyutsya opredelennye resursy, naprimer,
nuzhno otkryt' fajl, otvesti blok pamyati v oblasti svobodnoj
pamyati, ustanovit' monopol'nye prava dostupa i t.d., dlya dal'nejshej
raboty sistemy obychno byvaet krajne vazhno, chtoby resursy byli
osvobozhdeny nadlezhashchim obrazom. Obychno takoj "nadlezhashchij sposob"
realizuet funkciya, v kotoroj proishodit zapros resursov i osvobozhdenie
ih pered vyhodom. Naprimer:
void use_file(const char* fn)
{
FILE* f = fopen(fn,"w");
// rabotaem s f
fclose(f);
}
Vse eto vyglyadit vpolne normal'no do teh por, poka vy ne pojmete,
chto pri lyuboj oshibke, proisshedshej posle vyzova fopen() i do vyzova
fclose(), vozniknet osobaya situaciya, v rezul'tate kotoroj my
vyjdem iz use_file(), ne obrashchayas' k fclose().X
X Stoit skazat', chto ta zhe problema voznikaet i v yazykah, ne
podderzhivayushchih osobye situacii. Tak, obrashchenie k funkcii longjump()
iz standartnoj biblioteki S mozhet imet' takie zhe nepriyatnye
posledstviya.
Esli vy sozdaete ustojchivuyu k oshibkam sistemam, etu problemu
pridetsya reshat'. Mozhno dat' primitivnoe reshenie:
void use_file(const char* fn)
{
FILE* f = fopen(fn,"w");
try {
// rabotaem s f
}
catch (...) {
fclose(f);
throw;
}
fclose(f);
}
Vsya chast' funkcii, rabotayushchaya s fajlom f, pomeshchena v proveryaemyj
blok, v kotorom perehvatyvayutsya vse osobye situacii, zakryvaetsya
fajl i osobaya situaciya zapuskaetsya povtorno.
Nedostatok etogo resheniya v ego mnogoslovnosti, gromozdkosti
i potencial'noj rastochitel'nosti. K tomu zhe vsyakoe mnogoslovnoe
i gromozdkoe reshenie chrevato oshibkami, hotya by v silu ustalosti
programmista. K schast'yu, est' bolee priemlemoe reshenie. V obshchem
vide problemu mozhno sformulirovat' tak:
void acquire()
{
// zapros resursa 1
// ...
// zapros resursa n
// ispol'zovanie resursov
// osvobozhdenie resursa n
// ...
// osvobozhdenie resursa 1
}
Kak pravilo byvaet vazhno, chtoby resursy osvobozhdalis' v obratnom
po sravneniyu s zaprosami poryadke. |to ochen' sil'no napominaet poryadok
raboty s lokal'nymi ob容ktami, sozdavaemymi konstruktorami i
unichtozhaemymi destruktorami. Poetomu my mozhem reshit' problemu zaprosa i
osvobozhdeniya resursov, esli budem ispol'zovat' podhodyashchie ob容kty
klassov s konstruktorami i destruktorami. Naprimer, mozhno opredelit'
klass FilePtr, kotoryj vystupaet kak tip FILE* :
class FilePtr {
FILE* p;
public:
FilePtr(const char* n, const char* a)
{ p = fopen(n,a); }
FilePtr(FILE* pp) { p = pp; }
~FilePtr() { fclose(p); }
operator FILE*() { return p; }
};
Postroit' ob容kt FilePtr mozhno libo, imeya ob容kt tipa FILE*, libo,
poluchiv nuzhnye dlya fopen() parametry. V lyubom sluchae etot
ob容kt budet unichtozhen pri vyhode iz ego oblasti vidimosti, i
ego destruktor zakroet fajl. Teper' nash primer szhimaetsya do takoj
funkcii:
void use_file(const char* fn)
{
FilePtr f(fn,"w");
// rabotaem s f
}
Destruktor budet vyzyvat'sya nezavisimo ot togo, zakonchilas' li funkciya
normal'no, ili proizoshel zapusk osoboj situacii.
9.4.1 Konstruktory i destruktory
Opisannyj sposob upravleniya resursami obychno nazyvayut "zapros resursov
putem inicializacii". |to universal'nyj priem, rasschitannyj na
svojstva konstruktorov i destruktorov i ih vzaimodejstvie s
mehanizmom osobyh situacij.
Ob容kt ne schitaetsya postroennym, poka ne zavershil vypolnenie ego
konstruktor. Tol'ko posle etogo vozmozhna raskrutka steka,
soprovozhdayushchaya vyzov destruktora ob容kta. Ob容kt, sostoyashchij iz
vlozhennyh ob容ktov, postroen v toj stepeni, v kakoj postroeny
vlozhennye ob容kty.
Horosho napisannyj konstruktor dolzhen garantirovat', chto ob容kt
postroen polnost'yu i pravil'no. Esli emu ne udaetsya sdelat' eto,
on dolzhen, naskol'ko eto vozmozhno, vosstanovit' sostoyanie sistemy,
kotoroe bylo do nachala postroeniya. Dlya prostyh konstruktorov bylo by
ideal'no vsegda udovletvoryat' hotya by odnomu usloviyu - pravil'nosti
ili zakonchennosti ob容ktov, i nikogda ne ostavlyat' ob容kt
v "napolovinu postroennom" sostoyanii. |togo mozhno dobit'sya, esli
primenyat' pri postroenii chlenov sposob "zaprosa resursov putem
inicializacii".
Rassmotrim klass X, konstruktoru kotorogo trebuetsya dva resursa:
fajl x i zamok y (t.e. monopol'nye prava dostupa k chemu-libo).
|ti zaprosy mogut byt' otkloneny i privesti k zapusku osoboj
situacii. CHtoby ne uslozhnyat' rabotu programmista, mozhno potrebovat',
chtoby konstruktor klassa X nikogda ne zavershalsya tem, chto zapros na
fajl udovletvoren, a na zamok net. Dlya predstavleniya dvuh vidov
resursov my budem ispol'zovat' ob容kty dvuh klassov FilePtr i
LockPtr (estestvenno, bylo by dostatochno odnogo klassa, esli x i y
resursy odnogo vida). Zapros resursa vyglyadit kak inicializaciya
predstavlyayushchego resurs ob容kta:
class X {
FilePtr aa;
LockPtr bb;
// ...
X(const char* x, const char* y)
: aa(x), // zapros `x'
bb(y) // zapros `y'
{ }
// ...
};
Teper', kak eto bylo dlya sluchaya lokal'nyh ob容ktov, vsyu sluzhebnuyu
rabotu, svyazannuyu s resursami, mozhno vozlozhit' na realizaciyu.
Pol'zovatel' ne obyazan sledit' za hodom takoj rabotoj. Naprimer,
esli posle postroeniya aa i do postroeniya bb vozniknet osobaya
situaciya, to budet vyzvan tol'ko destruktor aa, no ne bb.
|to oznachaet, chto esli strogo priderzhivat'sya etoj prostoj
shemy zaprosa resursov, to vse budet v poryadke. Eshche bolee vazhno
to, chto sozdatelyu konstruktora ne nuzhno samomu pisat' obrabotchiki
osobyh situacij.
Dlya trebovanij vydelit' blok v svobodnoj pamyati harakteren samyj
proizvol'nyj poryadok zaprosa resursov. Primery takih zaprosov uzhe
neodnokratno vstrechalis' v etoj knige:
class X {
int* p;
// ...
public:
X(int s) { p = new int[s]; init(); }
~X() { delete[] p; }
// ...
};
|to tipichnyj primer ispol'zovaniya svobodnoj pamyati, no v sovokupnosti s
osobymi situaciyami on mozhet privesti k ee ischerpaniyu pamyati.
Dejstvitel'no, esli v init() zapushchena osobaya situaciya, to otvedennaya
pamyat' ne budet osvobozhdena. Destruktor ne budet vyzyvat'sya, poskol'ku
postroenie ob容kta ne bylo zaversheno. Est' bolee nadezhnyj variant
etogo primera:
template<class T> class MemPtr {
public:
T* p;
MemPtr(size_t s) { p = new T[s]; }
~MemPtr() { delete[] p; }
operator T*() { return p; }
}
class X {
MemPtr<int> cp;
// ...
public:
X(int s):cp(s) { init(); }
// ...
};
Teper' unichtozhenie massiva, na kotoryj ukazyvaet p, proishodit neyavno
v MemPtr. Esli init() zapustit osobuyu situaciyu, otvedennaya pamyat'
budet osvobozhdena pri neyavnom vyzove destruktora dlya polnost'yu
postroennogo vlozhennogo ob容kta cp.
Otmetim takzhe, chto standartnaya strategiya vydeleniya pamyati v S++
garantiruet, chto esli funkcii operator new() ne udalos' vydelit' pamyat'
dlya ob容kta, to konstruktor dlya nego nikogda ne budet vyzyvat'sya. |to
oznachaet, chto pol'zovatelyu ne nado opasat'sya, chto konstruktor ili
destruktor mozhet byt' vyzvan dlya nesushchestvuyushchego ob容kta.
Teoreticheski dopolnitel'nye rashody, trebuyushchiesya dlya obrabotki
osobyh situacij, kogda na samom dele ni odna iz nih ne voznikla, mogut
byt' svedeny k nulyu. Odnako, vryad li eto verno dlya rannih
realizaciyah yazyka. Poetomu budet razumno v kritichnyh vnutrennih ciklah
programmy poka ne ispol'zovat' lokal'nye peremennye klassov
s destruktorami.
Ne vse programmy dolzhny byt' ustojchivy ko vsem vidam oshibok. Ne vse
resursy yavlyayutsya nastol'ko kritichnymi, chtoby opravdat' popytki
zashchitit' ih s pomoshch'yu opisannogo sposoba "zaprosa resursov putem
inicializacii". Est' mnozhestvo programm, kotorye prosto chitayut
vhodnye dannye i vypolnyayutsya do konca. Dlya nih samoj podhodyashchej
reakciej na dinamicheskuyu oshibku budet prosto prekrashchenie scheta
(posle vydachi sootvetstvuyushchego soobshcheniya). Osvobozhdenie vseh
zatrebovannyh resursov vozlagaetsya na sistemu, a pol'zovatel'
dolzhen proizvesti povtornyj zapusk programmy s bolee podhodyashchimi
vhodnymi dannymi. Nasha shema prednaznachena dlya zadach, v kotoryh
takaya primitivnaya reakciya na dinamicheskuyu oshibku nepriemlema.
Naprimer, razrabotchik biblioteki obychno ne v prave delat' dopushcheniya
o tom, naskol'ko ustojchiva k oshibkam, dolzhna byt' programma,
rabotayushchaya s bibliotekoj. Poetomu on dolzhen uchityvat' vse dinamicheskie
oshibki i osvobozhdat' vse resursy do vozvrata iz bibliotechnoj funkcii
v pol'zovatel'skuyu programmu. Metod "zaprosa resursov putem
inicializacii" v sovokupnosti s osobymi situaciyami, signaliziruyushchimi
ob oshibke, mozhet prigodit'sya pri sozdanii mnogih bibliotek.
9.4.3 Ischerpanie resursa
Est' odna iz vechnyh problem programmirovaniya: chto delat', esli ne
udalos' udovletvorit' zapros na resurs? Naprimer, v predydushchem
primere my spokojno otkryvali s pomoshch'yu fopen() fajly i zaprashivali s
pomoshch'yu operacii new blok svobodnoj pamyati, ne zadumyvayas' pri etom,
chto takogo fajla mozhet ne byt', a svobodnaya pamyat' mozhet ischerpat'sya.
Dlya resheniya takogo roda problem u programmistov est' dva sposoba:
Povtornyj zapros: pol'zovatel' dolzhen izmenit' svoj zapros i
povtorit' ego.
Zavershenie: zaprosit' dopolnitel'nye resursy ot sistemy, esli
ih net, zapustit' osobuyu situaciyu.
Pervyj sposob predpolagaet dlya zadaniya priemlemogo zaprosa
sodejstvie pol'zovatelya, vo vtorom pol'zovatel' dolzhen byt' gotov
pravil'no otreagirovat' na otkaz v vydelenii resursov. V bol'shinstve
sluchaev poslednij sposob namnogo proshche i pozvolyaet podderzhivat' v
sisteme razdelenie razlichnyh urovnej abstrakcii.
V S++ pervyj sposob podderzhan mehanizmom vyzova funkcij, a
vtoroj - mehanizmom osobyh situacij. Oba sposoba mozhno
prodemonstrirovat' na primere realizacii i ispol'zovaniya operacii
new:
#include <stdlib.h>
extern void* _last_allocation;
extern void* operator new(size_t size)
{
void* p;
while ( (p=malloc(size))==0 ) {
if (_new_handler)
(*_new_handler)(); // obratimsya za pomoshch'yu
else
return 0;
}
return _last_allocation=p;
}
Esli operaciya new() ne mozhet najti svobodnoj pamyati, ona obrashchaetsya
k upravlyayushchej funkcii _new_handler(). Esli v _new_handler() mozhno
vydelit' dostatochnyj ob容m pamyati, vse normal'no. Esli net, iz
upravlyayushchej funkcii nel'zya vozvratit'sya v operaciyu new, t.k.
vozniknet beskonechnyj cikl. Poetomu upravlyayushchaya funkciya mozhet
zapustit' osobuyu situaciyu i predostavit' ispravlyat' polozhenie
programme, obrativshejsya k new:
void my_new_handler()
{
try_find_some_memory(); // popytaemsya najti
// svobodnuyu pamyat'
if (found_some()) return; // esli ona najdena, vse v poryadke
throw Memory_exhausted(); // inache zapuskaem osobuyu
// situaciyu "Ischerpanie_pamyati"
}
Gde-to v programme dolzhen byt' proveryaemyj blok s sootvetstvuyushchim
obrabotchikom:
try {
// ...
}
catch (Memory_exhausted) {
// ...
}
V funkcii operator new() ispol'zovalsya ukazatel' na upravlyayushchuyu
funkciyu _new_handler, kotoryj nastraivaetsya standartnoj funkciej
set_new_handler(). Esli nuzhno nastroit'sya na sobstvennuyu upravlyayushchuyu
funkciyu, nado obratit'sya tak
set_new_handler(&my_new_handler);
Perehvatit' situaciyu Memory_exhausted mozhno sleduyushchim obrazom:
void (*oldnh)() = set_new_handler(&my_new_handler);
try {
// ...
}
catch (Memory_exhausted) {
// ...
}
catch (...) {
set_new_handler(oldnh); // vosstanovit' ukazatel' na
// upravlyayushchuyu funkciyu
throw(); // povtornyj zapusk osoboj situacii
}
set_new_handler(oldnh); // vosstanovit' ukazatel' na
// upravlyayushchuyu funkciyu
Mozhno postupit' eshche luchshe, esli k upravlyayushchej funkcii primenit'
opisannyj v $$9.4 metod "zaprosa resursov putem inicializacii" i
ubrat' obrabotchik catch (...).
V reshenii, ispol'zuyushchim my_new_handler(), ot tochki obnaruzheniya
oshibki do funkcii, v kotoroj ona obrabatyvaetsya, ne peredaetsya
nikakoj informacii. Esli nuzhno peredat' kakie-to dannye, to
pol'zovatel' mozhet vklyuchit' svoyu upravlyayushchuyu funkciyu v klass.
Togda v funkcii, obnaruzhivshej oshibku, nuzhnye dannye mozhno pomestit' v
ob容kt etogo klassa. Podobnyj sposob, ispol'zuyushchij ob容kty-funkcii,
primenyalsya v $$10.4.2 dlya realizacii manipulyatorov. Sposob, v
kotorom ispol'zuetsya ukazatel' na funkciyu ili ob容kt-funkciya dlya
togo, chtoby iz upravlyayushchej funkcii, obsluzhivayushchej nekotoryj resurs,
proizvesti "obratnyj vyzov" funkcii zaprosivshej etot resurs,
obychno nazyvaetsya prosto obratnym vyzovom (callback).
Pri etom nuzhno ponimat', chto chem bol'she informacii peredaetsya
iz obnaruzhivshej oshibku funkcii v funkciyu, pytayushchuyusya ee ispravit',
tem bol'she zavisimost' mezhdu etimi dvumya funkciyami. V obshchem sluchae
luchshe svodit' k minimumu takie zavisimosti, poskol'ku vsyakoe
izmenenie v odnoj iz funkcij pridetsya delat' s uchetom drugoj funkciej,
a, vozmozhno, ee tozhe pridetsya izmenyat'. Voobshche, luchshe ne smeshivat'
otdel'nye komponenty programmy. Mehanizm osobyh situacij pozvolyaet
sohranyat' razdel'nost' komponentov luchshe, chem obychnyj mehanizm vyzova
upravlyayushchih funkcij, kotorye zadaet funkciya, zatrebovavshaya resurs.
V obshchem sluchae razumnyj podhod sostoit v tom, chtoby vydelenie
resursov bylo mnogourovnevym (v sootvetstvii s urovnyami abstrakcii).
Pri etom nuzhno izbegat' togo, chtoby funkcii odnogo urovnya zaviseli
ot upravlyayushchej funkcii, vyzyvaemoj na drugom urovne. Opyt sozdaniya
bol'shih programmnyh sistem pokazyvaet, chto so vremenem udachnye
sistemy razvivayutsya imenno v etom napravlenii.
9.4.4 Osobye situacii i konstruktory
Osobye situacii dayut sredstvo signalizirovat' o proishodyashchih v
konstruktore oshibkah. Poskol'ku konstruktor ne vozvrashchaet takoe
znachenie, kotoroe mogla by proverit' vyzyvayushchaya funkciya, est'
sleduyushchie obychnye (t.e. ne ispol'zuyushchie osobye situacii) sposoby
signalizacii:
[1] Vozvratit' ob容kt v nenormal'nom sostoyanii v raschete,
chto pol'zovatel' proverit ego sostoyanie.
[2] Ustanovit' znachenie nelokal'noj peremennoj, kotoroe
signaliziruet, chto sozdat' ob容kt ne udalos'.
Osobye situacii pozvolyayut tot fakt, chto sozdat' ob容kt ne udalos',
peredat' iz konstruktora vovne:
Vector::Vector(int size)
{
if (sz<0 || max<sz) throw Size();
// ...
}
V funkcii, sozdayushchej vektora, mozhno perehvatit' oshibki, vyzvannye
nedopustimym razmerom (Size()) i popytat'sya na nih otreagirovat':
Vector* f(int i)
{
Vector* p;
try {
p = new Vector v(i);
}
catch (Vector::Size) {
// reakciya na nedopustimyj razmer vektora
}
// ...
return p;
}
Upravlyayushchaya sozdaniem vektora funkciya sposobna pravil'no
otreagirovat' na oshibku. V samom obrabotchike osoboj situacii mozhno
primenit' kakoj-nibud' iz standartnyh sposobov diagnostiki i
vosstanovleniya posle oshibki. Pri kazhdom perehvate osoboj situacii
v upravlyayushchej funkcii mozhet byt' svoj vzglyad na prichinu oshibki. Esli
s kazhdoj osoboj situaciej peredayutsya opisyvayushchie ee dannye, to ob容m
dannyh, kotorye nuzhno analizirovat' dlya kazhdoj oshibki, rastet. Osnovnaya
zadacha obrabotki oshibok v tom, chtoby obespechit' nadezhnyj i udobnyj
sposob peredachi dannyh ot ishodnoj tochki obnaruzheniya oshibki do togo
mesta, gde posle nee vozmozhno osmyslennoe vosstanovlenie.
Sposob "zaprosa resursov putem inicializacii" - samyj nadezhnoe
i krasivoe reshenie v tom sluchae, kogda imeyutsya konstruktory, trebuyushchie
bolee odnogo resursa. Po suti on pozvolyaet svesti zadachu vydeleniya
neskol'kih resursov k povtorno primenyaemomu, bolee prostomu, sposobu,
rasschitannomu na odin resurs.
9.5 Osobye situacii mogut ne byt' oshibkami
Esli osobaya situaciya ozhidalas', byla perehvachena i ne okazala
plohogo vozdejstviya na hod programmy, to stoit li ee nazyvat'
oshibkoj? Tak govoryat tol'ko potomu, chto programmist dumaet o nej
kak ob oshibke, a mehanizm osobyh situacij yavlyaetsya sredstvom
obrabotki oshibok. S drugoj storony, osobye situacii mozhno
rassmatrivat' prosto kak eshche odnu strukturu upravleniya. Podtverdim
eto primerom:
class message { /* ... */ }; // soobshchenie
class queue { // ochered'
// ...
message* get(); // vernut' 0, esli ochered' pusta
// ...
};
void f1(queue& q)
{
message* m = q.get();
if (m == 0) { // ochered' pusta
// ...
}
// ispol'zuem m
}
|tot primer mozhno zapisat' tak:
class Empty { } // tip osoboj situacii "Pustaya_ochered'"
class queue {
// ...
message* get(); // zapustit' Empty, esli ochered' pusta
// ...
};
void f2(queue& q)
{
try {
message* m = q.get();
// ispol'zuem m
}
catch (Empty) { // ochered' pusta
// ...
}
}
V variante s osoboj situaciej est' dazhe kakaya-to prelest'. |to
horoshij primer togo, kogda trudno skazat', mozhno li schitat'
takuyu situaciyu oshibkoj. Esli ochered' ne dolzhna byt' pustoj
(t.e. ona byvaet pustoj ochen' redko, skazhem odin raz iz tysyachi),
i dejstviya v sluchae pustoj ocheredi mozhno rassmatrivat' kak
vosstanovlenie, to v funkcii f2() vzglyad na osobuyu situaciyu budet
takoj, kotorogo my do sih por i priderzhivalis' (t.e. obrabotka osobyh
situacij est' obrabotka oshibok). Esli ochered' chasto byvaet pustoj,
a prinimaemye v etom sluchae dejstviya obrazuyut odnu iz vetvej
normal'nogo hoda programmy, to pridetsya otkazat'sya ot takogo vzglyada
na osobuyu situaciyu, a funkciyu f2() nado perepisat':
class queue {
// ...
message* get(); // zapustit' Empty, esli ochered' pusta
int empty();
// ...
};
void f3(queue& q)
{
if (q.empty()) { // ochered' pusta
// ...
}
else {
message* m = q.get();
// ispol'zuem m
}
}
Otmetim, chto vynesti iz funkcii get() proverku ocheredi na pustotu
mozhno tol'ko pri uslovii, chto k ocheredi net parallel'nyh obrashchenij.
Ne tak to prosto otkazat'sya ot vzglyada, chto obrabotka osoboj
situacii est' obrabotka oshibki. Poka my priderzhivaemsya takoj tochki
zreniya, programma chetko podrazdelyaetsya na dve chasti: obychnaya chast'
i chast' obrabotki oshibok. Takaya programma bolee ponyatna. K sozhaleniyu,
v real'nyh zadachah provesti chetkoe razdelenie nevozmozhno, poetomu
struktura programmy dolzhna (i budet) otrazhat' etot fakt. Dopustim,
ochered' byvaet pustoj tol'ko odin raz (tak mozhet byt', esli funkciya
get() ispol'zuetsya v cikle, i pustota ocheredi govorit o konce cikla).
Togda pustota ocheredi ne yavlyaetsya chem-to strannym ili oshibochnym.
Poetomu, ispol'zuya dlya oboznacheniya konca ocheredi osobuyu situaciyu,
my rasshiryaem predstavlenie ob osobyh situaciyah kak oshibkah. S drugoj
storony, dejstviya, prinimaemye v sluchae pustoj ocheredi, yavno otlichayutsya
ot dejstvij, prinimaemyh v hode cikla (t.e. v obychnom sluchae).
Mehanizm osobyh situacij yavlyaetsya menee strukturirovannym,
chem takie lokal'nye struktury upravleniya kak operatory if ili for.
Obychno on k tomu zhe yavlyaetsya ne stol' effektivnym, esli osobaya
situaciya dejstvitel'no voznikla. Poetomu osobye situacii sleduet
ispol'zovat' tol'ko v tom sluchae, kogda net horoshego resheniya s bolee
tradicionnymi upravlyayushchimi strukturami, ili ono, voobshche, nevozmozhno.
Naprimer, v sluchae pustoj ocheredi mozhno prekrasno ispol'zovat' dlya
signalizacii ob etom znachenie, a imenno nulevoe znachenie ukazatelya na
stroku message, znachit osobaya situaciya zdes' ne nuzhna. Odnako, esli by
iz klassa queue my poluchali vmesto ukazatelya znachenie tipa int, to
to moglo ne najtis' takogo znacheniya, oboznachayushchego pustuyu ochered'.
V takom sluchae funkciya get() stanovitsya ekvivalentnoj operacii
indeksacii iz $$9.1, i bolee privlekatel'no predstavlyat' pustuyu ochered'
s pomoshch'yu osoboj situacii. Poslednee soobrazhenie podskazyvaet, chto
v samom obshchem shablone tipa dlya ocheredi pridetsya dlya oboznacheniya pustoj
ocheredi ispol'zovat' osobuyu situaciyu, a rabotayushchaya s ochered'yu funkciya
budet takoj:
void f(Queue<X>& q)
{
try {
for (;;) { // ``beskonechnyj cikl''
// preryvaemyj osoboj situaciej
X m = q.get();
// ...
}
}
catch (Queue<X>::Empty) {
return;
}
}
Esli privedennyj cikl vypolnyaetsya tysyachi raz, to on, po vsej
vidimosti, budet bolee effektivnym, chem obychnyj cikl s proverkoj
usloviya pustoty ocheredi. Esli zhe on vypolnyaetsya tol'ko neskol'ko
raz, to obychnyj cikl pochti navernyaka effektivnej.
V ocheredi obshchego vida osobaya situaciya ispol'zuetsya kak sposob
vozvrata iz funkcii get(). Ispol'zovanie osobyh situacij kak sposoba
vozvrata mozhet byt' elegantnym sposobom zaversheniya funkcij poiska.
Osobenno eto podhodit dlya rekursivnyh funkcij poiska v dereve. Odnako,
primenyaya osobye situacii dlya takih celej, legko perejti gran' razumnogo
i poluchit' malovrazumitel'nuyu programmu. Vse-taki vsyudu, gde eto
dejstvitel'no opravdano, nado priderzhivat'sya toj tochki zreniya, chto
obrabotka osoboj situacii est' obrabotka oshibki. Obrabotka oshibok po
samoj svoej prirode zanyatie slozhnoe, poetomu cennost' imeyut lyubye
metody, kotorye dayut yasnoe predstavlenie oshibok v yazyke i sposob
ih obrabotki.
Zapusk ili perehvat osoboj situacii otrazhaetsya na vzaimootnosheniyah
funkcij. Poetomu imeet smysl zadavat' v opisanii funkcii mnozhestvo
osobyh situacij, kotorye ona mozhet zapustit':
void f(int a) throw (x2, x3, x4);
V etom opisanii ukazano, chto f() mozhet zapustit' osobye situacii
x2, x3 i x4, a takzhe situacii vseh proizvodnyh ot nih tipov, no
bol'she nikakie situacii ona ne zapuskaet. Esli funkciya perechislyaet svoi
osobye situacii, to ona daet opredelennuyu garantiyu vsyakoj vyzyvayushchej ee
funkcii, a imenno, esli popytaetsya zapustit' inuyu osobuyu situaciyu, to
eto privedet k vyzovu funkcii unexpected().
Standartnoe prednaznachenie unexpected() sostoit v vyzove funkcii
terminate(), kotoraya, v svoyu ochered', obychno vyzyvaet abort().
Podrobnosti dany v $$9.7.
Po suti opredelenie
void f() throw (x2, x3, x4)
{
// kakie-to operatory
}
ekvivalentno takomu opredeleniyu
void f()
{
try {
// kakie-to operatory
}
catch (x2) { // povtornyj zapusk
throw;
}
catch (x3) { // povtornyj zapusk
throw;
}
catch (x4) { // povtornyj zapusk
throw;
}
catch (...) {
unexpected();
}
}
Preimushchestvo yavnogo zadaniya osobyh situacij funkcii v ee opisanii
pered ekvivalentnym sposobom, kogda proishodit proverka na
osobye situacii v tele funkcii, ne tol'ko v bolee kratkoj zapisi.
Glavnoe zdes' v tom, chto opisanie funkcii vhodit v ee interfejs,
kotoryj vidim dlya vseh vyzyvayushchih funkcij. S drugoj storony,
opredelenie funkcii mozhet i ne byt' universal'no dostupnym.
Dazhe esli u vas est' ishodnye teksty vseh bibliotechnyh funkcij,
obychno zhelanie izuchat' ih voznikaet ne chasto.
Esli v opisanii funkcii ne ukazany ee osobye situacii, schitaetsya,
chto ona mozhet zapustit' lyubuyu osobuyu situaciyu.
int f(); // mozhet zapustit' lyubuyu osobuyu situaciyu
Esli funkciya ne budet zapuskat' nikakih osobyh situacij, ee mozhno
opisat', yavno ukazav pustoj spisok:
int g() throw (); // ne zapuskaet nikakih osobyh situacij
Kazalos' bylo by logichno, chtoby po umolchaniyu funkciya ne zapuskala
nikakih osobyh situacij. No togda prishlos' by opisyvat' svoi osobye
situacii prakticheski dlya kazhdoj funkcii |to, kak pravilo,
trebovalo by ee peretranslyacii, a krome togo prepyatstvovalo by obshcheniyu
s funkciyami, napisannymi na drugih yazykah. V rezul'tate programmist
stal by stremit'sya otklyuchit' mehanizm osobyh situacij i pisal by
izlishnie operatory, chtoby obojti ih. Pol'zovatel' schital by takie
programmy nadezhnymi, poskol'ku mog ne zametit' podmeny, no eto bylo
by sovershenno neopravdano.
9.6.1 Neozhidannye osobye situacii
Esli k opisaniyu osobyh situacij otnosit'sya ne dostatochno ser'ezno, to
rezul'tatom mozhet byt' vyzov unexpected(), chto nezhelatel'no vo vseh
sluchaya, krome otladki. Izbezhat' vyzova unexpected() mozhno, esli horosho
organizovat' strukturu osobyh situacii i opisanie interfejsa. S
drugoj storony, vyzov unexpected() mozhno perehvatit' i sdelat' ego
bezvrednym.
Esli komponent Y horosho razrabotan, vse ego osobye situacii
mogut byt' tol'ko proizvodnymi odnogo klassa, skazhem Yerr. Poetomu,
esli est' opisanie
class someYerr : public Yerr { /* ... */ };
to funkciya, opisannaya kak
void f() throw (Xerr, Yerr, IOerr);
budet peredavat' lyubuyu osobuyu situaciyu tipa Yerr vyzyvayushchej funkcii.
V chastnosti, obrabotka osoboj situacii tipa someYerr v f() svedetsya k
peredache ee vyzyvayushchej f() funkcii.
Byvaet sluchai, kogda okonchanie programmy pri poyavlenii
neozhidannoj osoboj situacii yavlyaetsya slishkom strogim resheniem.
Dopustim funkciya g() napisana dlya nesetevogo rezhima v raspredelennoj
sisteme. Estestvenno, v g() nichego neizvestno ob osobyh situaciyah,
svyazannyh s set'yu, poetomu pri poyavlenii lyuboj iz nih vyzyvaetsya
unexpected(). Znachit dlya ispol'zovaniya g() v raspredelennoj sisteme
nuzhno predostavit' obrabotchik setevyh osobyh situacij ili perepisat'
g(). Esli dopustit', chto perepisat' g() nevozmozhno ili nezhelatel'no,
problemu mozhno reshit', pereopredeliv dejstvie funkcii unexpected().
Dlya etogo sluzhit funkciya set_unexpected(). Vnachale my opredelim
klass, kotoryj pozvolit nam primenit' dlya funkcij unexpected()
metod "zaprosa resursov putem inicializacii" :
typedef void(*PFV)();
PFV set_unexpected(PFV);
class STC { // klass dlya sohraneniya i vosstanovleniya
PFV old; // funkcij unexpected()
public:
STC(PFV f) { old = set_unexpected(f); }
~STC() { set_unexpected(old); }
};
Teper' my opredelim funkciyu, kotoraya dolzhna v nashem primere zamenit'
unexpected():
void rethrow() { throw; } // perezapusk vseh setevyh
// osobyh situacij
Nakonec, mozhno dat' variant funkcii g(), prednaznachennyj dlya raboty
v setevom rezhime:
void networked_g()
{
STC xx(&rethrow); // teper' unexpected() vyzyvaet rethrow()
g();
}
V predydushchem razdele bylo pokazano, chto unexpected() potencial'no
vyzyvaetsya iz obrabotchika catch (...). Znachit v nashem sluchae
obyazatel'no proizojdet povtornyj zapusk osoboj situacii. Povtornyj
zapusk, kogda osobaya situaciya ne zapuskalas', privodit k vyzovu
terminate(). Poskol'ku obrabotchik catch (...) nahoditsya vne toj
oblasti vidimosti, v kotoroj byla zapushchena setevaya osobaya situaciya,
beskonechnyj cikl vozniknut' ne mozhet.
Est' eshche odno, dovol'no opasnoe, reshenie, kogda na neozhidannuyu
osobuyu situaciyu prosto "zakryvayut glaza":
void muddle_on() { cerr << "ne zamechaem osoboj situacii\n"; }
// ...
STC xx(&muddle_on); // teper' dejstvie unexpected() svoditsya
// prosto k pechati soobshcheniya
Takoe pereopredelenie dejstviya unexpected() pozvolyaet normal'no
vernut'sya iz funkcii, obnaruzhivshej neozhidannuyu osobuyu situaciyu.
Nesmotrya na svoyu ochevidnuyu opasnost', eto reshenie ispol'zuetsya.
Naprimer, mozhno "zakryt' glaza" na osobye situacii v odnoj chasti
sistemy i otlazhivat' drugie ee chasti. Takoj podhod mozhet byt'
polezen v processe otladki i razvitiya sistemy, perenesennoj s yazyka
programmirovaniya bez osobyh situacij. Vse-taki, kak pravilo luchshe,
esli oshibki proyavlyayutsya kak mozhno ran'she.
Vozmozhno drugoe reshenie, kogda vyzov unexpected() preobrazuetsya
v zapusk osoboj situacii Fail (neudacha):
void fail() { throw Fail; }
// ...
STC yy(&fail);
Pri takom reshenii vyzyvayushchaya funkciya ne dolzhna podrobno razbirat'sya v
vozmozhnom rezul'tate vyzyvaemoj funkcii: eta funkcii zavershitsya
libo uspeshno (t.e. vozvratitsya normal'no), libo neudachno (t.e.
zapustit Fail). Ochevidnyj nedostatok etogo resheniya v tom, chto
ne uchityvaetsya dopolnitel'naya informaciya, kotoraya mozhet soprovozhdat'
osobuyu situaciyu. Vprochem, pri neobhodimosti ee mozhno uchest', esli
peredavat' informaciyu vmeste s Fail.
9.7 Neperehvachennye osobye situacii
Esli osobaya situaciya zapushchena i ne perehvachena, to vyzyvaetsya funkciya
terminate(). Ona zhe vyzyvaetsya, kogda sistema podderzhki osobyh
situacij obnaruzhivaet, chto struktura steka narushena, ili kogda
v processe obrabotki osoboj situacii pri raskruchivanii steka vyzyvaetsya
destruktor, i on pytaetsya zavershit' svoyu rabotu, zapustiv osobuyu
situaciyu.
Dejstvie terminate() svoditsya k vypolneniyu samoj poslednej
funkcii, zadannoj kak parametr dlya set_terminate():
typedef void (*PFV)();
PFV set_terminate(PFV);
Funkciya set_terminate() vozvrashchaet ukazatel' na tu funkciyu, kotoraya
byla zadana kak parametr v predydushchem obrashchenii k nej.
Neobhodimost' takoj funkcii kak terminate() ob座asnyaetsya tem,
chto inogda vmesto mehanizma osobyh situacij trebuyutsya bolee
grubye priemy. Naprimer, terminate() mozhno ispol'zovat' dlya
prekrashcheniya processa, a, vozmozhno, i dlya povtornogo zapuska sistemy.
|ta funkciya sluzhit ekstrennym sredstvom, kotoroe primenyaetsya, kogda
otkazala strategiya obrabotki oshibok, rasschitannaya na osobye
situacii, i samoe vremya primenit' strategiyu bolee nizkogo urovnya.
Funkciya unexpected() ispol'zuetsya v shodnyh, no ne stol'
ser'eznyh sluchayah, a imenno, kogda funkciya zapustila osobuyu situaciyu,
ne ukazannuyu v ee opisanii. Dejstvie funkcii unexpected() svoditsya
k vypolneniyu samoj poslednej funkcii, zadannoj kak parametr dlya
funkcii set_unexpected().
Po umolchaniyu unexpected() vyzyvaet terminate(), a ta, v svoyu
ochered', vyzyvaet funkciyu abort(). Predpolagaetsya, chto takoe soglashenie
ustroit bol'shinstvo pol'zovatelej.
Predpolagaetsya, chto funkciya terminate() ne vozvrashchaetsya v
obrativsheyusya nej funkciyu.
Napomnim, chto vyzov abort() svidetel'stvuet o nenormal'nom
zavershenii programmy. Dlya normal'nogo vyhoda iz programmy
ispol'zuetsya funkciya exit(). Ona vozvrashchaet znachenie, kotoroe
pokazyvaet okruzhayushchej sisteme naskol'ko korrektno zakonchilas' programma.
9.8 Drugie sposoby obrabotki oshibok
Mehanizm osobyh situacij nuzhen dlya togo, chtoby iz odnoj chasti
programmy mozhno bylo soobshchit' v druguyu o vozniknovenii v pervoj
"osoboj situacii". Pri etom predpolagaetsya, chto chasti programmy
napisany nezavisimo drug ot druga, i v toj chasti, kotoraya
obrabatyvaet osobuyu situaciyu, vozmozhna osmyslennaya reakciya na
oshibku.
Kak zhe dolzhen byt' ustroen obrabotchik osoboj situacii? Privedem
neskol'ko variantov:
int f(int arg)
{
try {
g(arg);
}
catch (x1) {
// ispravit' oshibku i povtorit'
g(arg);
}
catch (x2) {
// proizvesti vychisleniya i vernut' rezul'tat
return 2;
}
catch (x3) {
// peredat' oshibku
throw;
}
catch (x4) {
// vmesto x4 zapustit' druguyu osobuyu situaciyu
throw xxii;
}
catch (x5) {
// ispravit' oshibku i prodolzhit' so sleduyushchego operatora
}
catch (...) {
// otkaz ot obrabotki oshibki
terminate();
}
// ...
}
Ukazhem, chto v obrabotchike dostupny peremennye iz oblasti vidimosti,
soderzhashchej proveryaemyj blok etogo obrabotchika. Peremennye,
opisannye v drugih obrabotchikah ili drugih proveryaemyh blokah,
konechno, nedostupny:
void f()
{
int i1;
// ...
try {
int i2;
// ...
}
catch (x1) {
int i3;
// ...
}
catch (x4) {
i1 = 1; // normal'no
i2 = 2; // oshibka: i2 zdes' nevidimo
i3 = 3; // oshibka: i3 zdes' nevidimo
}
}
Nuzhna obshchaya strategiya dlya effektivnogo ispol'zovaniya obrabotchikov v
programme. Vse komponenty programmy dolzhny soglasovanno ispol'zovat'
osobye situacii i imet' obshchuyu chast' dlya obrabotki oshibok. Mehanizm
obrabotki osobyh situacij yavlyaetsya nelokal'nym po svoej suti,
poetomu tak vazhno priderzhivat'sya obshchej strategii. |to predpolagaet,
chto strategiya obrabotki oshibok dolzhna razrabatyvat'sya na samyh
rannih stadiyah proektah. Krome togo, eta strategiya dolzhna
byt' prostoj (po sravneniyu so slozhnost'yu vsej programmy) i yasnoj.
Posledovatel'no provodit' slozhnuyu strategiyu v takoj slozhnoj
po svoej prirode oblasti programmirovaniya, kak vosstanovlenie posle
oshibok, budet prosto nevozmozhno.
Prezhde vsego stoit srazu otkazat'sya ot togo, chto odno sredstvo
ili odin priem mozhno primenyat' dlya obrabotki vseh oshibok. |to
tol'ko uslozhnit sistemu. Udachnaya sistema, obladayushchaya ustojchivost'yu
k oshibkam, dolzhna stroit'sya kak mnogourovnevaya. Na kazhdom urovne
nado obrabatyvat' nastol'ko mnogo oshibok, naskol'ko eto vozmozhno
bez narusheniya struktury sistemy, ostavlyaya obrabotku drugih oshibok bolee
vysokim urovnyam. Naznachenie terminate() podderzhat' takoj podhod,
predostavlyaya vozmozhnost' ekstrennogo vyhoda iz takogo polozheniya,
kogda narushen sam mehanizm obrabotki osobyh situacij, ili kogda
on ispol'zuetsya polnost'yu, no osobaya situaciya okazalas'
neperehvachennoj. Funkciya unexpected() prednaznachena dlya vyhoda iz
takogo polozheniya, kogda ne srabotalo osnovannoe na opisanii vseh osobyh
situacij sredstvo zashchity. |to sredstvo mozhno predstavlyat' kak brandmauer,
t.e. stenu, okruzhayushchuyu kazhduyu funkciyu, i prepyatstvuyushchuyu
rasprostraneniyu oshibki. Popytka provodit' v kazhdoj funkcii polnyj
kontrol', chtoby imet' garantiyu, chto funkciya libo uspeshno zavershitsya,
libo zakonchitsya neudachno, no odnim iz opredelennyh i korrektnyh
sposobov, ne mozhet prinesti uspeh. Prichiny etogo mogut byt' razlichnymi
dlya raznyh programm, no dlya bol'shih programm mozhno nazvat' sleduyushchie:
[1] rabota, kotoruyu nuzhno provesti, chtoby garantirovat' nadezhnost'
kazhdoj funkcii, slishkom velika, i poetomu ee ne udastsya
provesti dostatochno posledovatel'no;
[2] poyavyatsya slishkom bol'shie dopolnitel'nye rashody pamyati i vremeni,
kotorye budut nedopustimy dlya normal'noj raboty sistemy
(budet tendenciya neodnokratno proveryat' na odnu i tu zhe oshibku,
a znachit postoyanno budut proveryat'sya peremennye s pravil'nymi
znacheniyami);
[3] takim ogranicheniyam ne budut podchinyat'sya funkcii, napisannye na
drugih yazykah;
[4] takoe ponyatie nadezhnosti yavlyaetsya chisto lokal'nym i ono
nastol'ko uslozhnyaet sistemu, chto stanovitsya dopolnitel'noj
nagruzkoj dlya ee obshchej nadezhnosti.
Odnako, razbit' programmu na otdel'nye podsistemy, kotorye libo
uspeshno zavershayutsya, libo zakanchivayutsya neudachno, no odnim iz
opredelennyh i korrektnyh sposobov, vpolne vozmozhno, vazhno i dazhe
vygodno. Takim svojstvom dolzhny obladat' osnovnye biblioteki,
podsistemy ili klyuchevye funkcii. Opisanie osobyh situacij dolzhno
vhodit' v interfejsy takih bibliotek ili podsistem.
Inogda prihoditsya ot odnogo stilya reakcii na oshibku perehodit'
na drugoj. Naprimer, mozhno posle vyzova standartnoj funkcii S
proveryat' znachenie errno i, vozmozhno, zapuskat' osobuyu situaciyu,
a mozhno, naoborot, perehvatyvat' osobuyu situaciyu i ustanavlivat'
znachenie errno pered vyhodom iz standartnoj funkcii v S-programmu:
void callC()
{
errno = 0;
cfunction();
if (errno) throw some_exception(errno);
}
void fromC()
{
try {
c_pl_pl_function();
}
catch (...) {
errno = E_CPLPLFCTBLEWIT;
}
}
Pri takoj smene stilej vazhno byt' posledovatel'nym, chtoby izmenenie
reakcii na oshibku bylo polnym.
Obrabotka oshibok dolzhna byt', naskol'ko eto vozmozhno, strogo
ierarhicheskoj sistemoj. Esli v funkcii obnaruzhena dinamicheskaya
oshibka, to ne nuzhno obrashchat'sya za pomoshch'yu dlya vosstanovleniya ili
vydeleniya resursov k vyzyvayushchej funkcii. Pri takih obrashcheniyah v
strukture sistemy voznikayut ciklicheskie zavisimosti, v rezul'tate
chego ee trudnee ponyat', i vozmozhno vozniknovenie beskonechnyh
ciklov v processe obrabotki i vosstanovleniya posle oshibki.
CHtoby chast' programmy, prednaznachennaya dlya obrabotki oshibok
byla bolee uporyadochennoj, stoit primenyat' takie uproshchayushchie delo
priemy, kak "zapros resursov putem inicializacii", i ishodit' iz
takih uproshchayushchih delo dopushchenij, chto "osobye situacii yavlyayutsya
oshibkami".
1. (*2) Obobshchite klass STC do shablona tipa, kotoryj pozvolyaet
hranit' i ustanavlivat' funkcii raznyh tipov.
2. (*3) Dopolnite klass CheckedPtrToT iz $$7.10 do shablona tipa, v
kotorom osobye situacii signaliziruyut o dinamicheskih oshibkah.
3. (*3) Napishite funkciyu find dlya poiska v binarnom dereve uzlov
po znacheniyu polya tipa char*. Esli najden uzel s polem, imeyushchim
znachenie "hello", ona dolzhna vozvrashchat' ukazatel' na nego. Dlya
oboznacheniya neudachnogo poiska ispol'zujte osobuyu situaciyu.
4. (*1) Opredelite klass Int, sovpadayushchij vo vsem so vstroennym
tipom int za isklyucheniem togo, chto vmesto perepolneniya ili
poteri znachimosti v etom klasse zapuskayutsya osobye situacii.
Podskazka: sm. $$9.3.2.
5. (*2) Perenesite iz standartnogo interfejsa S v vashu operacionnuyu
sistemu osnovnye operacii s fajlami: otkrytie, zakrytie, chtenie i
zapis'. Realizujte ih kak funkcii na S++ s tem zhe naznacheniem,
chto i funkcij na S, no v sluchae oshibok zapuskajte osobye
situacii.
6. (*1) Napishite polnoe opredelenie shablona tipa Vector s osobymi
situaciyami Range i Size. Podskazka: sm. $$9.3.
7. (*1) Napishite cikl dlya vychisleniya summy elementov vektora,
opredelennogo v uprazhnenii 6, prichem ne proveryajte razmer vektora.
Pochemu eto plohoe reshenie?
8. (*2.5) Dopustim klass Exception ispol'zuetsya kak bazovyj dlya vseh
klassov, zadayushchih osobye situacii. Kakov dolzhen byt' ego vid?
Kakaya ot nego mogla byt' pol'za? Kakie neudobstva mozhet vyzvat'
trebovanie obyazatel'nogo ispol'zovaniya etogo klassa?
9. (*2) Napishite klass ili shablon tipa, kotoryj pomozhet realizovat'
obratnyj vyzov.
10. (*2) Napishite klass Lock (zamok) dlya kakoj-nibud' sistemy,
dopuskayushchej parallel'noe vypolnenie.
11. (*1) Pust' opredelena funkciya
int main() { /* ... */ }
Izmenite ee tak, chtoby v nej perehvatyvalis' vse osobye situacii,
preobrazovyvalis' v soobshcheniya ob oshibke i vyzov abort().
Podskazka: v funkcii fromC() iz $$9.8 uchteny ne vse sluchai.
"Dostupno tol'ko to, chto vidimo"
B. Kernigan
V yazyke S++ net sredstv dlya vvoda-vyvoda. Ih i ne nuzhno, poskol'ku
takie sredstva mozhno prosto i elegantno sozdat' na samom yazyke.
Opisannaya zdes' biblioteka potokovogo vvoda-vyvoda realizuet strogij
tipovoj i vmeste s tem gibkij i effektivnyj sposob simvol'nogo vvoda i
vyvoda celyh, veshchestvennyh chisel i simvol'nyh strok, a takzhe yavlyaetsya
bazoj dlya rasshireniya, rasschitannogo na rabotu s pol'zovatel'skimi tipami
dannyh. Pol'zovatel'skij interfejs biblioteki nahoditsya v fajle
<iostream.h>. |ta glava posvyashchena samoj potokovoj biblioteke, nekotorym
sposobam raboty s nej i opredelennym priemam realizacii biblioteki.
SHiroko izvestna trudnost' zadachi proektirovaniya i realizacii
standartnyh sredstv vvoda-vyvoda dlya yazykov programmirovaniya.
Tradicionno sredstva vvoda-vyvoda byli rasschitany isklyuchitel'no na
nebol'shoe chislo vstroennyh tipov dannyh. Odnako, v netrivial'nyh
programmah na S++ est' mnogo pol'zovatel'skih tipov dannyh, poetomu
neobhodimo predostavit'
vozmozhnost' vvoda-vyvoda znachenij takih tipov. Ochevidno, chto sredstva
vvoda-vyvoda dolzhny byt' prostymi, udobnymi, nadezhnymi v ispol'zovanii
i, chto vazhnee vsego, adekvatnymi. Poka nikto ne nashel resheniya, kotoroe
udovletvorilo by vseh; poetomu neobhodimo dat' vozmozhnost' pol'zovatelyu
sozdavat' inye sredstva vvoda-vyvoda, a takzhe rasshiryat' standartnye
sredstva vvoda-vyvoda v raschete na opredelennoe primenenie.
Cel' sozdaniya S++ byla v tom, chtoby pol'zovatel' mog opredelit' novye
tipy dannyh, rabota s kotorymi byla by stol' zhe udobna i effektivna kak
i so vstroennymi tipami. Takim obrazom, kazhetsya razumnym potrebovat',
chtoby sredstva vvoda-vyvoda dlya S++ programmirovalis' s ispol'zovaniem
vozmozhnostej S++, dostupnyh kazhdomu. Predstavlennye zdes' potokovye
sredstva vvoda-vyvoda poyavilis' v rezul'tate popytki udovletvorit'
etim trebovaniyam.
Osnovnaya zadacha potokovyh sredstv vvoda-vyvoda - eto process
preobrazovaniya ob容ktov opredelennogo tipa v posledovatel'nost' simvolov
i naoborot. Sushchestvuyut i drugie shemy vvoda-vyvoda, no ukazannaya yavlyaetsya
osnovnoj, i esli schitat' simvol prosto naborom bitov, ignoriruya ego
estestvennuyu svyaz' s alfavitom, to mnogie shemy dvoichnogo vvoda-vyvoda
mozhno svesti k nej. Poetomu programmistskaya sut' zadachi svoditsya k
opisaniyu svyazi mezhdu ob容ktom opredelennogo tipa i bestipovoj (chto
sushchestvenno) strokoj.
Posleduyushchie razdely opisyvayut osnovnye chasti potokovoj biblioteki S++:
10.2 Vyvod: To, chto dlya prikladnoj programmy predstavlyaetsya vyvodom,
na samom dele yavlyaetsya preobrazovaniem takih ob容ktov kak int,
char *, complex ili Employee_record v posledovatel'nost' simvolov.
Opisyvayutsya sredstva dlya zapisi ob容ktov vstroennyh i
pol'zovatel'skih tipov dannyh.
10.3 Vvod: Opisany funkcii dlya vvoda simvolov, strok i znachenij
vstroennyh i pol'zovatel'skih tipov dannyh.
10.4 Formatirovanie: CHasto sushchestvuyut opredelennye trebovaniya k vidu
vyvoda, naprimer, int dolzhno pechatat'sya desyatichnymi ciframi,
ukazateli v shestnadcaterichnoj zapisi, a veshchestvennye chisla dolzhny
byt' s yavno zadannoj tochnost'yu fiksirovannogo razmera.
Obsuzhdayutsya funkcii formatirovaniya i opredelennye programmistskie
priemy ih sozdaniya, v chastnosti, manipulyatory.
10.5 Fajly i potoki: Kazhdaya programma na S++ mozhet ispol'zovat' po
umolchaniyu tri potoka - standartnyj vyvod (cout), standartnyj vvod
(cin) i standartnyj potok oshibok (cerr). CHtoby rabotat' s kakimi-
libo ustrojstvami ili fajlami nado sozdat' potoki i privyazat' ih
k etim ustrojstvam ili fajlam. Opisyvaetsya mehanizm otkrytiya i
zakrytiya fajlov i svyazyvaniya fajlov s potokami.
10.6 Vvod-vyvod dlya S: obsuzhdaetsya funkciya printf iz fajla <stdio.h>
dlya S a takzhe svyaz' mezhdu bibliotekoj dlya S i <iostream.h> dlya
S++.
Ukazhem, chto sushchestvuet mnogo nezavisimyh realizacij
potokovoj biblioteki vvoda-vyvoda i nabor sredstv, opisannyh zdes', budet
tol'ko podmnozhestvom sredstv, imeyushchihsya v vashej biblioteke. Govoryat,
chto vnutri lyuboj bol'shoj programmy est' malen'kaya programma, kotoraya
stremitsya vyrvat'sya naruzhu. V etoj glave predprinyata popytka opisat'
kak raz malen'kuyu potokovuyu biblioteku vvoda-vyvoda, kotoraya pozvolit
ocenit' osnovnye koncepcii potokovogo vvoda-vyvoda i poznakomit'
s naibolee poleznymi sredstvami. Ispol'zuya tol'ko sredstva,
opisannye zdes', mozhno napisat' mnogo programm; esli vozniknet
neobhodimost' v bolee slozhnyh sredstvah, obratites' za detalyami k vashemu
rukovodstvu po S++. Zagolovochnyj fajl <iostream.h> opredelyaet interfejs
potokovoj biblioteki. V rannih versiyah potokovoj biblioteki ispol'zovalsya
fajl <stream.h>. Esli sushchestvuyut oba fajla, <iostream.h> opredelyaet polnyj
nabor sredstv, a <stream.h> opredelyaet podmnozhestvo, kotoroe
sovmestimo s rannimi, menee bogatymi potokovymi bibliotekami.
Estestvenno, dlya pol'zovaniya potokovoj bibliotekoj vovse ne nuzhno
znanie tehniki ee realizacii, tem bolee, chto tehnika mozhet byt'
razlichnoj dlya razlichnyh realizacij. Odnako, realizaciya vvoda-vyvoda
yavlyaetsya zadachej, diktuyushchej opredelennye usloviya, znachit priemy, najdennye
v processe ee resheniya, mozhno primenit' i dlya drugih zadach, a samo eto
reshenie dostojno izucheniya.
Stroguyu tipovuyu i edinoobraznuyu rabotu kak so vstroennymi, tak i s
pol'zovatel'skimi tipami mozhno obespechit', esli ispol'zovat'
edinstvennoe peregruzhennoe imya funkcii dlya razlichnyh operacij vyvoda.
Naprimer:
put(cerr,"x = "); // cerr - vyhodnoj potok oshibok
put(cerr,x);
put(cerr,'\n');
Tip argumenta opredelyaet kakuyu funkciyu nado vyzyvat' v kazhdom sluchae.
Takoj podhod primenyaetsya v neskol'kih yazykah, odnako, eto slishkom
dlinnaya zapis'. Za schet peregruzki operacii << , chtoby ona oznachala
"vyvesti" ("put to"), mozhno poluchit' bolee prostuyu zapis' i razreshit'
programmistu vyvodit' v odnom operatore posledovatel'nost' ob容ktov,
naprimer tak:
cerr << "x = " << x << '\n';
Zdes' cerr oboznachaet standartnyj potok oshibok. Tak, esli h tipa int
so znacheniem 123, to privedennyj operator vydast
x = 123
i eshche simvol konca stroki v standartnyj potok oshibok. Analogichno, esli h
imeet pol'zovatel'skij tip complex so znacheniem (1,2.4), to ukazannyj
operator vydast
x = (1,2.4)
v potok cerr. Takoj podhod legko ispol'zovat' poka x takogo tipa, dlya
kotorogo opredelena operaciya <<, a pol'zovatel' mozhet prosto
doopredelit' << dlya novyh tipov.
My ispol'zovali operaciyu vyvoda, chtoby izbezhat' mnogoslovnosti,
neizbezhnoj, esli primenyat' funkciyu vyvoda. No pochemu imenno simvol << ?
Nevozmozhno izobresti novuyu leksemu (sm. 7.2). Kandidatom dlya vvoda i
vyvoda byla operaciya prisvaivaniya, no bol'shinstvo lyudej predpochitaet,
chtoby operacii vvoda i vyvoda byli razlichny. Bolee togo, poryadok
vypolneniya operacii = nepodhodyashchij, tak cout=a=b oznachaet cout=(a=b).
Probovali ispol'zovat' operacii < i >, no k nim tak krepko privyazano
ponyatie "men'she chem" i "bol'she chem", chto operacii vvoda-vyvoda s nimi
vo vseh prakticheski sluchayah ne poddavalis' prochteniyu.
Operacii << i >> pohozhe ne sozdayut takih problem. Oni asimetrichny,
chto pozvolyaet pripisyvat' im smysl "v" i "iz". Oni ne otnosyatsya k chislu
naibolee chasto ispol'zuemyh operacij nad vstroennymi tipami, a
prioritet << dostatochno nizkij, chtoby pisat' arifmeticheskie vyrazheniya v
kachestve operanda bez skobok:
cout << "a*b+c=" << a*b+c << '\n';
Skobki nuzhny, esli vyrazhenie soderzhit operacii s bolee nizkim
prioritetom:
cout << "a^b|c=" << (a^b|c) << '\n';
Operaciyu sdviga vlevo mozhno ispol'zovat' v operacii vyvoda, no, konechno,
ona dolzhna byt' v skobkah:
cout << "a<<b=" << (a<<b) << '\n';
10.2.1 Vyvod vstroennyh tipov
Dlya upravleniya vyvodom vstroennyh tipov opredelyaetsya klass ostream
s operaciej << (vyvesti):
class ostream : public virtual ios {
// ...
public:
ostream& operator<<(const char*); //stroki
ostream& operator<<(char);
ostream& operator<<(short i)
{ return *this << int(i); }
ostream& operator<<(int);
ostream& operator<<(long);
ostream& operator<<(double);
ostream& operator<<(const void*); // ukazateli
// ...
};
Estestvenno, v klasse ostream dolzhen byt' nabor funkcij operator<<()
dlya raboty s bezznakovymi tipami.
Funkciya operator<< vozvrashchaet ssylku na klass ostream, iz
kotorogo ona vyzyvalas', chtoby k nej mozhno bylo primenit' eshche raz
operator<<. Tak, esli h tipa int, to
cerr << "x = " << x;
ponimaetsya kak
(cerr.operator<<("x = ")).operator<<(x);
V chastnosti, eto oznachaet, chto esli neskol'ko ob容ktov vyvodyatsya s
pomoshch'yu odnogo operatora vyvoda, to oni budut vydavat'sya v
estestvennom poryadke: sleva - napravo.
Funkciya ostream::operator<<(int) vyvodit celye znacheniya, a
funkciya ostream::operator<<(char) - simvol'nye. Poetomu funkciya
void val(char c)
{
cout << "int('"<< c <<"') = " << int(c) << '\n';
}
pechataet celye znacheniya simvolov i s pomoshch'yu programmy
main()
{
val('A');
val('Z');
}
budet napechatano
int('A') = 65
int('Z') = 90
Zdes' predpolagaetsya kodirovka simvolov ASCII, na vashej mashine mozhet byt'
inoj rezul'tat. Obratite vnimanie, chto simvol'naya konstanta imeet
tip char, poetomu cout<<'Z' napechataet bukvu Z, a vovse ne celoe 90.
Funkciya ostream::operator<<(const void*) napechataet znachenie
ukazatelya v takoj zapisi, kotoraya bolee podhodit dlya ispol'zuemoj
sistemy adresacii.
Programma
main()
{
int i = 0;
int* p = new int(1);
cout << "local " << &i
<< ", free store " << p << '\n';
}
vydast na mashine, ispol'zuemoj avtorom,
local 0x7fffead0, free store 0x500c
Dlya drugih sistem adresacii mogut byt' inye soglasheniya ob izobrazhenii
znachenij ukazatelej.
Obsuzhdenie bazovogo klassa ios otlozhim do 10.4.1.
10.2.2 Vyvod pol'zovatel'skih tipov
Rassmotrim pol'zovatel'skij tip dannyh:
class complex {
double re, im;
public:
complex(double r = 0, double i = 0) { re=r; im=i; }
friend double real(complex& a) { return a.re; }
friend double imag(complex& a) { return a.im; }
friend complex operator+(complex, complex);
friend complex operator-(complex, complex);
friend complex operator*(complex, complex);
friend complex operator/(complex, complex);
//...
};
Dlya novogo tipa complex operaciyu << mozhno opredelit' tak:
ostream& operator<<(ostream&s, complex z)
{
return s << '(' real(z) << ',' << imag(z) << ')';
};
i ispol'zovat' kak operator<< dlya vstroennyh tipov. Naprimer,
main()
{
complex x(1,2);
cout << "x = " << x << '\n';
}
vydast
x = (1,2)
Dlya opredeleniya operacii vyvoda nad pol'zovatel'skimi tipami dannyh
ne nuzhno modificirovat' opisanie klassa ostream, ne trebuetsya i dostup
k strukturam dannyh, skrytym v opisanii klassa. Poslednee ochen' kstati,
poskol'ku opisanie klassa ostream nahoditsya sredi standartnyh
zagolovochnyh fajlov, dostup po zapisi k kotorym zakryt dlya bol'shinstva
pol'zovatelej, i izmenyat' kotorye oni vryad li zahotyat, dazhe esli by
mogli. |to vazhno i po toj prichine, chto daet zashchitu ot sluchajnoj porchi
etih struktur dannyh. Krome togo imeetsya vozmozhnost' izmenit'
realizaciyu ostream, ne zatragivaya pol'zovatel'skih programm.
Vvod vo mnogom shoden s vyvodom. Est' klass istream, kotoryj realizuet
operaciyu vvoda >> ("vvesti iz" - "input from") dlya nebol'shogo nabora
standartnyh tipov. Dlya pol'zovatel'skih tipov mozhno opredelit' funkciyu
operator>>.
10.3.1 Vvod vstroennyh tipov
Klass istream opredelyaetsya sleduyushchim obrazom:
class istream : public virtual ios {
//...
public:
istream& operator>>(char*); // stroka
istream& operator>>(char&); // simvol
istream& operator>>(short&);
istream& operator>>(int&);
istream& operator>>(long&);
istream& operator>>(float&);
istream& operator>>(double&);
//...
};
Funkcii vvoda operator>> opredelyayutsya tak:
istream& istream::operator>>(T& tvar)
{
// propuskaem obobshchennye probely
// kakim-to obrazom chitaem T v`tvar'
return *this;
}
Teper' mozhno vvesti v VECTOR posledovatel'nost' celyh, razdelyaemyh
probelami, s pomoshch'yu funkcii:
int readints(Vector<int>& v)
// vozvrashchaem chislo prochitannyh celyh
{
for (int i = 0; i<v.size(); i++)
{
if (cin>>v[i]) continue;
return i;
}
// slishkom mnogo celyh dlya razmera Vector
// nuzhna sootvetstvuyushchaya obrabotka oshibki
}
Poyavlenie znacheniya s tipom, otlichnym ot int, privodit k prekrashcheniyu
operacii vvoda, i cikl vvoda zavershaetsya. Tak, esli my vvodim
1 2 3 4 5. 6 7 8.
to funkciya readints() prochitaet pyat' celyh chisel
1 2 3 4 5
Simvol tochka ostanetsya pervym simvolom, podlezhashchim vvodu. Pod probelom,
kak opredeleno v standarte S, ponimaetsya obobshchennyj probel, t.e.
probel, tabulyaciya, konec stroki, perevod stroki ili vozvrat karetki.
Proverka na obobshchennyj probel vozmozhna s pomoshch'yu funkcii isspace()
iz fajla <ctype.h>.
V kachestve al'ternativy mozhno ispol'zovat' funkcii get():
class istream : public virtual ios {
//...
istream& get(char& c); // simvol
istream& get(char* p, int n, char ='n'); // stroka
};
V nih obobshchennyj probel rassmatrivaetsya kak lyuboj drugoj simvol i
oni prednaznacheny dlya takih operacij vvoda, kogda ne delaetsya nikakih
predpolozhenij o vvodimyh simvolah.
Funkciya istream::get(char&) vvodit odin simvol v svoj parametr.
Poetomu programmu posimvol'nogo kopirovaniya mozhno napisat' tak:
main()
{
char c;
while (cin.get(c)) cout << c;
}
Takaya zapis' vyglyadit nesimmetrichno, i u operacii >> dlya vyvoda simvolov
est' dvojnik pod imenem put(), tak chto mozhno pisat' i tak:
main()
{
char c;
while (cin.get(c)) cout.put(c);
}
Funkciya s tremya parametrami istream::get() vvodit v simvol'nyj vektor
ne menee n simvolov, nachinaya s adresa p. Pri vsyakom obrashchenii k get()
vse simvoly, pomeshchennye v bufer (esli oni byli), zavershayutsya 0, poetomu
esli vtoroj parametr raven n, to vvedeno ne bolee n-1 simvolov. Tretij
parametr opredelyaet simvol, zavershayushchij vvod. Tipichnoe ispol'zovanie
funkcii get() s tremya parametrami svoditsya k chteniyu stroki v bufer
zadannogo razmera dlya ee dal'nejshego razbora, naprimer tak:
void f()
{
char buf[100];
cin >> buf; // podozritel'no
cin.get(buf,100,'\n'); // nadezhno
//...
}
Operaciya cin>>buf podozritel'na, poskol'ku stroka iz bolee chem 99
simvolov perepolnit bufer. Esli obnaruzhen zavershayushchij simvol, to on
ostaetsya v potoke pervym simvolom podlezhashchim vvodu. |to pozvolyaet
proveryat' bufer na perepolnenie:
void f()
{
char buf[100];
cin.get(buf,100,'\n'); // nadezhno
char c;
if (cin.get(c) && c!='\n') {
// vhodnaya stroka bol'she, chem ozhidalos'
}
//...
}
Estestvenno, sushchestvuet versiya get() dlya tipa unsigned char.
V standartnom zagolovochnom fajle <ctype.h> opredeleny neskol'ko
funkcij, poleznyh dlya obrabotki pri vvode:
int isalpha(char) // 'a'..'z' 'A'..'Z'
int isupper(char) // 'A'..'Z'
int islower(char) // 'a'..'z'
int isdigit(char) // '0'..'9'
int isxdigit(char) // '0'..'9' 'a'..'f' 'A'..'F'
int isspace(char) // ' ' '\t' vozvrashchaet konec stroki
// i perevod formata
int iscntrl(char) // upravlyayushchij simvol v diapazone
// (ASCII 0..31 i 127)
int ispunct(char) // znak punktuacii, otlichen ot privedennyh vyshe
int isalnum(char) // isalpha() | isdigit()
int isprint(char) // vidimyj: ascii ' '..'~'
int isgraph(char) // isalpha() | isdigit() | ispunct()
int isascii(char c) { return 0<=c && c<=127; }
Vse oni, krome isascii(), rabotayut s pomoshch'yu prostogo prosmotra,
ispol'zuya simvol kak indeks v tablice atributov simvolov. Poetomu
vmesto vyrazheniya tipa
(('a'<=c && c<='z') || ('A'<=c && c<='Z')) // bukva
kotoroe ne tol'ko utomitel'no pisat', no ono mozhet byt' i oshibochnym
(na mashine s kodirovkoj EBCDIC ono zadaet ne tol'ko bukvy), luchshe
ispol'zovat' vyzov standartnoj funkcii isalpha(), kotoryj k tomu
zhe bolee effektiven.
V kachestve primera privedem funkciyu eatwhite(), kotoraya chitaet iz
potoka obobshchennye probely:
istream& eatwhite(istream& is)
{
char c;
while (is.get(c)) {
if (isspace(c)==0) {
is.putback(c);
break;
}
}
return is;
}
V nej ispol'zuetsya funkciya putback(), kotoraya vozvrashchaet simvol v
potok, i on stanovitsya pervym podlezhashchim chteniyu.
10.3.2 Sostoyaniya potoka
S kazhdym potokom (istream ili ostream) svyazano opredelennoe sostoyanie.
Nestandartnye situacii i oshibki obrabatyvayutsya s pomoshch'yu proverki i
ustanovki sostoyaniya podhodyashchim obrazom.
Uznat' sostoyanie potoka mozhno s pomoshch'yu operacij nad klassom ios:
class ios { //ios yavlyaetsya bazovym dlya ostream i istream
//...
public:
int eof() const; // doshli do konca fajla
int fail() const; // sleduyushchaya operaciya budet neudachna
int bad() const; // potok isporchen
int good() const; // sleduyushchaya operaciya budet uspeshnoj
//...
};
Poslednyaya operaciya vvoda schitaetsya uspeshnoj, esli sostoyanie zadaetsya
good() ili eof(). Esli sostoyanie zadaetsya good(), to posleduyushchaya
operaciya vvoda mozhet byt' uspeshnoj, v protivnom sluchae ona budet
neudachnoj. Primenenie operacii vvoda k potoku v sostoyanii, zadavaemom
ne good(), schitaetsya pustoj operaciej. Esli proizoshla neudacha pri
popytke chteniya v peremennuyu v, to znachenie v ne izmenilos' (ono ne
izmenitsya, esli v imeet tip, upravlyaemyj funkciyami chlena iz istream
ili ostream). Razlichie mezhdu sostoyaniyami, zadavaemymi kak fail() ili
kak bad() ulovit' trudno, i ono imeet smysl tol'ko dlya razrabotchikov
operacij vvoda. Esli sostoyanie est' fail(), to schitaetsya, chto potok
ne povrezhden, i nikakie simvoly ne propali; o sostoyanii bad() nichego
skazat' nel'zya.
Znacheniya, oboznachayushchie eti sostoyaniya, opredeleny v klasse ios:
class ios {
//...
public:
enum io_state {
goodbit=0,
eofbit=1,
filebit=2,
badbit=4,
};
//...
};
Istinnye znacheniya sostoyanij zavisyat ot realizacii, i ukazannye znacheniya
privedeny tol'ko, chtoby izbezhat' sintaksicheski nepravil'nyh konstrukcij.
Proveryat' sostoyanie potoka mozhno sleduyushchim obrazom:
switch (cin.rdstate()) {
case ios::goodbit:
// poslednyaya operaciya s cin byla uspeshnoj
break;
case ios::eofbit:
// v konce fajla
break;
case ios::filebit:
// nekotoryj analiz oshibki
// vozmozhno neplohoj
break;
case ios::badbit:
// cin vozmozhno isporchen
break;
}
V bolee rannih realizaciyah dlya znachenij sostoyanij ispol'zovalis'
global'nye imena. |to privodilo k nezhelatel'nomu zasoreniyu
prostranstva imenovaniya, poetomu novye imena dostupny tol'ko v predelah
klassa ios. Esli vam neobhodimo ispol'zovat' starye imena v sochetanii s
novoj bibliotekoj, mozhno vospol'zovat'sya sleduyushchimi opredeleniyami:
const int _good = ios::goodbit;
const int _bad = ios::badbit;
const int _file = ios::filebit;
const int _eof = ios::eofbit;
typedef ios::io_state state_value ;
Razrabotchiki bibliotek dolzhny zabotitsya o tom, chtoby ne dobavlyat'
novyh imen k global'nomu prostranstvu imenovaniya. Esli elementy
perechisleniya vhodyat v obshchij interfejs biblioteki, oni vsegda
dolzhny ispol'zovat'sya v klasse s prefiksami, naprimer, kak ios::goodbit
i ios::io_state.
Dlya peremennoj lyubogo tipa, dlya kotorogo opredeleny operacii
<< i >>, cikl kopirovaniya zapisyvaetsya sleduyushchim obrazom:
while (cin>>z) cout << z << '\n';
Esli potok poyavlyaetsya v uslovii, to proveryaetsya sostoyanie potoka, i
uslovie vypolnyaetsya (t.e. rezul'tat ego ne 0) tol'ko dlya sostoyaniya
good(). Kak raz v privedennom vyshe cikle proveryaetsya sostoyanie potoka
istream, chto yavlyaetsya rezul'tatom operacii cin>>z. CHtoby uznat',
pochemu proizoshla neudacha v cikle ili uslovii, nado proverit' sostoyanie.
Takaya proverka dlya potoka realizuetsya s pomoshch'yu operacii
privedeniya (7.3.2).
Tak, esli z yavlyaetsya simvol'nym vektorom, to v privedennom cikle
chitaetsya standartnyj vvod i vydaetsya dlya kazhdoj stroki standartnogo
vyvoda po odnomu slovu (t.e. posledovatel'nosti simvolov, ne yavlyayushchihsya
obobshchennymi probelami). Esli z imeet tip complex, to v etom cikle
s pomoshch'yu operacij, opredelennyh v 10.2.2 i 10.2.3, budut kopirovat'sya
kompleksnye chisla. SHablonnuyu funkciyu kopirovaniya dlya potokov so
znacheniyami proizvol'nogo tipa mozhno napisat' sleduyushchim obrazom:
complex z;
iocopy(z,cin,cout); // kopirovanie complex
double d;
iocopy(d,cin,cout); // kopirovanie double
char c;
iocopy(c,cin,cout); // kopirovanie char
Poskol'ku nadoedaet proveryat' na korrektnost' kazhduyu operaciyu vvoda-
vyvoda, to rasprostranennym istochnikom oshibok yavlyayutsya imenno te mesta v
programme, gde takoj kontrol' sushchestvenen. Obychno operacii vyvoda ne
proveryayut, no inogda oni mogut zavershit'sya neudachno. Potokovyj vvod-
vyvod razrabatyvalsya iz togo principa, chtoby sdelat' isklyuchitel'nye
situacii legkodostupnymi, i tem samym uprostit' obrabotku oshibok
v processe vvoda-vyvoda.
10.3.3 Vvod pol'zovatel'skih tipov
Operaciyu vvoda dlya pol'zovatel'skogo tipa mozhno opredelit' v tochnosti
tak zhe, kak i operaciyu vyvoda, no dlya operacii vvoda sushchestvenno, chtoby
vtoroj parametr imel tip ssylki, naprimer:
istream& operator>>(istream& s, complex& a)
/*
format input rasschitan na complex; "f" oboznachaet float:
f
( f )
( f , f )
*/
{
double re = 0, im = 0;
char c = 0;
s >> c;
if (c == '(') {
s >> re >> c;
if (c == ',') s >> im >> c;
if (c != ')') s.clear(ios::badbit); // ustanovim sostoyanie
}
else {
s.putback(c);
s >> re;
}
if (s) a = complex(re,im);
return s;
}
Nesmotrya na szhatost' koda, obrabatyvayushchego oshibki, na samom dele
uchityvaetsya bol'shaya chast' oshibok. Inicializaciya lokal'noj peremennoj
s nuzhna dlya togo, chtoby v nee ne popalo sluchajnoe znachenie, naprimer
'(', v sluchae neudachnoj operacii. Poslednyaya proverka sostoyaniya potoka
garantiruet, chto parametr a poluchit znachenie tol'ko pri uspeshnom vvode.
Operaciya, ustanavlivayushchaya sostoyanie potoka, nazvana clear()
(zdes' clear - yasnyj, pravil'nyj),
poskol'ku chashche vsego ona ispol'zuetsya dlya vosstanovleniya sostoyaniya potoka
kak good(); znacheniem po umolchaniyu dlya parametra ios::clear() yavlyaetsya
ios::goodbit.
Vse primery iz 10.2 soderzhali neformatirovannyj vyvod, kotoryj yavlyalsya
preobrazovaniem ob容kta v posledovatel'nost' simvolov, zadavaemuyu
standartnymi pravilami, dlina kotoroj takzhe opredelyaetsya etimi
pravilami. CHasto programmistam trebuyutsya bolee razvitye vozmozhnosti.
Tak, voznikaet potrebnost' kontrolirovat' razmer pamyati, neobhodimoj
dlya operacii vyvoda, i format, ispol'zuemyj dlya vydachi chisel.
Tochno tak zhe dopustimo upravlenie nekotorymi aspektami vvoda.
Bol'shinstvo sredstv upravleniya vvodom-vyvodom sosredotocheny v klasse
ios, kotoryj yavlyaetsya bazovym dlya ostream i istream. Po suti zdes'
nahoditsya upravlenie svyaz'yu mezhdu istream ili ostream i buferom,
ispol'zuemym dlya operacij vvoda-vyvoda. Imenno klass ios kontroliruet:
kak simvoly popadayut v bufer i kak oni vybirayutsya ottuda. Tak, v klasse
ios est' chlen, soderzhashchij informaciyu ob ispol'zuemoj pri chtenii ili
zapisi celyh chisel sistemy schisleniya (desyatichnaya, vos'merichnaya ili
shestnadcaterichnaya), o tochnosti veshchestvennyh chisel i t.p., a takzhe
funkcii dlya proverki i ustanovki znachenij peremennyh, upravlyayushchih
potokom.
class ios {
//...
public:
ostream* tie(ostream* s); // svyazat' input i output
ostream* tie(); // vozvratit' "tie"
int width(int w); // ustanovit' pole width
int width() const;
char fill(char); // ustanovit' simvol zapolneniya
char fill() const; // vernut' simvol zapolneniya
long flags(long f);
long flags() const;
long setf(long setbits, long field);
long setf(long);
long unsetf(long);
int precision(int); // ustanovit' tochnost' dlya float
int precision() const;
int rdstate(); const; // sostoyaniya potokov, sm. $$10.3.2
int eof() const;
int fail() const;
int bad() const;
int good() const;
void clear(int i=0);
//...
};
V 10.3.2 opisany funkcii, rabotayushchie s sostoyaniem potoka, ostal'nye
privedeny nizhe.
10.4.1.1 Svyazyvanie potokov
Funkciya tie() mozhet ustanovit' i razorvat' svyaz' mezhdu ostream i
istream. Rassmotrim primer:
main()
{
String s;
cout << "Password: ";
cin >> s;
// ...
}
Kak mozhno garantirovat', chto priglashenie Password: poyavitsya na
ekrane prezhde, chem vypolnit'sya operaciya chteniya? Vyvod v cout i vvod
iz cin buferizuyutsya, prichem nezavisimo, poetomu Password: poyavitsya
tol'ko po zavershenii programmy, kogda zakroetsya bufer vyvoda.
Reshenie sostoit v tom, chtoby svyazat' cout i cin s pomoshch'yu
operacii cin.tie(cout).
Esli ostream svyazan s potokom istream, to bufer vyvoda vydaetsya pri
kazhdoj operacii vvoda nad istream. Togda operacii
cout << "Password: ";
cin >> s;
ekvivalentny
cout << "Password: ";
cout.flush();
cin >> s;
Obrashchenie is.tie(0) razryvaet svyaz' mezhdu potokom is i potokom, s
kotorym on byl svyazan, esli takoj byl. Podobno drugim potokovym
funkciyam, ustanavlivayushchim opredelennoe znachenie, tie(s) vozvrashchaet
predydushchee znachenie, t.e. znachenie svyazannogo potoka pered obrashcheniem
ili 0. Vyzov bez parametra tie() vozvrashchaet tekushchee znachenie.
Funkciya width() ustanavlivaet minimal'noe chislo simvolov, ispol'zuyushcheesya
v posleduyushchej operacii vyvoda chisla ili stroki. Tak v rezul'tate
sleduyushchih operacij
cout.width(4);
cout << '(' << 12 << ')';
poluchim chislo 12 v pole razmerom 4 simvola, t.e.
( 12)
Zapolnenie polya zadannymi simvolami ili vyravnivanie mozhno ustanovit' s
pomoshch'yu funkcii fill(), naprimer:
cout.width(4);
cout.fill('#');
cout << '(' << "ab" << ')';
napechataet
(##ab)
Po umolchaniyu pole zapolnyaetsya probelami, a razmer polya po umolchaniyu
est' 0, chto oznachaet "stol'ko simvolov, skol'ko nuzhno". Vernut' razmeru
polya standartnoe znachenie mozhno s pomoshch'yu vyzova
cout.width(0); // ``stol'ko simvolov, skol'ko nado''
Funkciya width() zadaet minimal'noe chislo simvolov. Esli poyavitsya bol'she
simvolov, oni budut napechatany vse, poetomu
cout.width(4);
cout << '(' << "121212" << ")\n";
napechataet
(121212)
Prichina, po kotoroj razresheno perepolnenie polya, a ne usechenie vyvoda,
v tom, chtoby izbezhat' zavisaniya pri vyvode. Luchshe poluchit' pravil'nuyu
vydachu, vyglyadyashchuyu nekrasivo, chem krasivuyu vydachu, yavlyayushchuyusya
nepravil'noj.
Vyzov width() vliyaet tol'ko na odnu sleduyushchuyu za nim operaciyu
vyvoda, poetomu
cout.width(4);
cout.fill('#');
cout << '(' << 12 << "),(" << '(' <<12 << ")\n";
napechataet
(##12),(12)
a ne
(##12),(##12)
kak mozhno bylo by ozhidat'. Odnako, zamet'te, chto esli by vliyanie
rasprostranyalos' na vse operacii vyvoda chisel i strok, poluchilsya by
eshche bolee neozhidannyj rezul'tat:
(##12#),(##12#
)
S pomoshch'yu standartnogo manipulyatora, pokazannogo v 10.4.2.1, mozhno bolee
elegantno zadavat' razmera polya vyvoda.
10.4.1.3 Sostoyanie formata
V klasse ios soderzhitsya sostoyanie formata, kotoroe upravlyaetsya
funkciyami flags() i setf(). Po suti eti funkcii nuzhny, chtoby
ustanovit' ili otmenit' sleduyushchie flagi:
class ios {
public:
// upravlyayushchie formatom flagi:
enum {
skipws=01, // propusk obobshchennyh probelov dlya input
// pole vyravnivaniya:
left=02, // dobavlenie pered znacheniem
right=04, // dobavlenie posle znacheniya
internal=010, // dobavlenie mezhdu znakom i znacheniem
// osnovanie celogo:
dec=020, // vos'merichnoe
oct=040, // desyatichnoe
hex=0100, // shestnadcaterichnoe
showbase=0200, // pokazat' osnovanie celogo
showpoint=0400, // vydat' nuli v konce
uppercase=01000, // 'E', 'X' , a ne 'e', 'x'
showpos=02000, // '+' dlya polozhitel'nyh chisel
// zapis' chisla tipa float:
scientific=04000, // .dddddd Edd
fixed=010000, // dddd.dd
// sbros v vyhodnoj potok:
unitbuf=020000, // posle kazhdoj operacii
stdio=040000 // posle kazhdogo simvola
};
//...
};
Smysl flagov budet raz座asnen v posleduyushchih razdelah. Konkretnye
znacheniya flagov zavisyat ot realizacii i dany zdes' tol'ko dlya togo,
chtoby izbezhat' sintaksicheski nevernyh konstrukcij.
Opredelenie interfejsa kak nabora flagov i operacij dlya ih
ustanovki ili otmeny - eto ocenennyj vremenem, hotya i neskol'ko
ustarevshij priem. Osnovnoe ego dostoinstvo v tom, chto pol'zovatel'
mozhet sobrat' voedino nabor flagov, naprimer, tak:
const int my_io_options =
ios::left|ios::oct|ios::showpoint|ios::fixed;
Takoe mnozhestvo flagov mozhno zadavat' kak parametr odnoj operacii
cout.flags(my_io_options);
a takzhe prosto peredavat' mezhdu funkciyami odnoj programmy:
void your_function(int ios_options);
void my_function()
{
// ...
your_function(my_io_options);
// ...
}
Mnozhestvo flagov mozhno ustanovit' s pomoshch'yu funkcii flags(), naprimer:
void your_function(int ios_options)
{
int old_options = cout.flags(ios_options);
// ...
cout.flags(old_options); // reset options
}
Funkciya flags() vozvrashchaet staroe znachenie mnozhestva flagov. |to
pozvolyaet pereustanovit' znacheniya vseh flagov, kak pokazano vyshe,
a takzhe zadat' znachenie otdel'nomu flagu. Naprimer vyzov
myostream.flags(myostream.flags()|ios::showpos);
zastavlyaet klass myostream vydavat' polozhitel'nye chisla so znakom
+ i, v to zhe vremya, ne menyaet znacheniya drugih flagov. Poluchaetsya
staroe znachenie mnozhestva flagov, k kotoromu dobavlyaetsya s pomoshch'yu
operacii | flag showpos. Funkciya setf() delaet to zhe samoe,
poetomu ekvivalentnaya zapis' imeet vid
myostream.setf(ios::showpos);
Posle ustanovki flag sohranyaet znachenie do yavnoj otmeny.
Vse-taki upravlenie vvodom-vyvodom s pomoshch'yu ustanovki i otmeny
flagov - gruboe i vedushchee k oshibkam reshenie. Esli tol'ko vy tshchatel'no
ne izuchite svoe spravochnoe rukovodstvo i ne budete primenyat' flagi
tol'ko v prostyh sluchayah, kak eto delaetsya v posleduyushchih razdelah, to
luchshe ispol'zovat' manipulyatory (opisannye v 10.4.2.1). Priemy raboty
s sostoyaniem potoka luchshe izuchit' na primere realizacii klassa, chem
izuchaya interfejs klassa.
Priem zadaniya novogo znacheniya mnozhestva flagov s pomoshch'yu operacii | i
funkcij flags() i setf() rabotaet tol'ko togda, kogda odin bit opredelyaet
znachenie flaga. Ne takaya situaciya pri zadanii sistemy schisleniya celyh
ili vida vydachi veshchestvennyh. Zdes' znachenie, opredelyayushchee vid vydachi,
nel'zya zadat' odnim bitom ili kombinaciej otdel'nyh bitov.
Reshenie, prinyatoe v <iostream.h>, svoditsya k ispol'zovaniyu
versii funkcii setf(), rabotayushchej so vtorym "psevdoparametrom", kotoryj
pokazyvaet kakoj imenno flag my hotim dobavit' k novomu znacheniyu.
Poetomu obrashcheniya
cout.setf(ios::oct,ios::basefield); // vos'merichnoe
cout.setf(ios::dec,ios::basefield); // desyatichnoe
cout.setf(ios::hex,ios::basefield); // shestnadcaterichnoe
ustanovyat sistemu schisleniya, ne zatragivaya drugih komponentov sostoyaniya
potoka. Esli sistema schisleniya ustanovlena, ona ispol'zuetsya do yavnoj
pereustanovki, poetomu
cout << 1234 << ' '; // desyatichnoe po umolchaniyu
cout << 1234 << ' ';
cout.setf(ios::oct,ios::basefield); // vos'merichnoe
cout << 1234 << ' ';
cout << 1234 << ' ';
cout.setf(ios::hex,ios::basefield); // shestnadcaterichnoe
cout << 1234 << ' ';
cout << 1234 << ' ';
napechataet
1234 1234 2322 2322 4d2 4d2
Esli poyavitsya neobhodimost' ukazyvat' sistemu schisleniya dlya kazhdogo
vydavaemogo chisla, sleduet ustanovit' flag showbase. Poetomu, dobaviv
pered privedennymi vyshe obrashcheniyami
cout.setf(ios::showbase);
my poluchim
1234 1234 02322 02322 0x4d2 0x4d2
Standartnye manipulyatory, privedennye v $$10.4.2.1, predlagayut bolee
elegantnyj sposob opredeleniya sistemy schisleniya pri vyvode celyh.
10.4.1.5 Vyravnivanie polej
S pomoshch'yu obrashchenij k setf() mozhno upravlyat' raspolozheniem simvolov
v predelah polya:
cout.setf(ios::left,ios::adjustfield); // vlevo
cout.setf(ios::right,ios::adjustfield); // vpravo
cout.setf(ios::internal,ios::adjustfield); // vnutrennee
Budet ustanovleno vyravnivanie v pole vyvoda, opredelyaemom funkciej
ios::width(), prichem ne zatragivaya drugih komponentov sostoyaniya potoka.
Vyravnivanie mozhno zadat' sleduyushchim obrazom:
cout.width(4);
cout << '(' << -12 << ")\n";
cout.width(4);
cout.setf(ios::left,ios::adjustfield);
cout << '(' << -12 << ")\n";
cout.width(4);
cout.setf(ios::internal,ios::adjustfield);
cout << '(' << -12 << "\n";
chto vydast
( -12)
(-12 )
(- 12)
Esli ustanovlen flag vyravnivaniya internal (vnutrennij), to simvoly
dobavlyayutsya mezhdu znakom i velichinoj. Kak vidno, standartnym yavlyaetsya
vyravnivanie vpravo.
10.4.1.6 Vyvod plavayushchih chisel.
Vyvod veshchestvennyh velichin takzhe upravlyaetsya s pomoshch'yu funkcij,
rabotayushchih s sostoyaniem potoka. V chastnosti, obrashcheniya:
cout.setf(ios::scientific,ios::floatfield);
cout.setf(ios::fixed,ios::floatfield);
cout.setf(0,ios::floatfield); // vernut'sya k standartnomu
ustanovyat vid pechati veshchestvennyh chisel bez izmeneniya drugih
komponentov sostoyaniya potoka.
Naprimer:
cout << 1234.56789 << '\n';
cout.setf(ios::scientific,ios::floatfield);
cout << 1234.56789 << '\n';
cout.setf(ios::fixed,ios::floatfield);
cout << 1234.56789 << '\n';
napechataet
1234.57
1.234568e+03
1234.567890
Posle tochki pechataetsya n cifr, kak zadaetsya v obrashchenii
cout.precision(n)
Po umolchaniyu n ravno 6. Vyzov funkcii precision vliyaet na vse operacii
vvoda-vyvoda s veshchestvennymi do sleduyushchego obrashcheniya k precision,
poetomu
cout.precision(8);
cout << 1234.56789 << '\n';
cout << 1234.56789 << '\n';
cout.precision(4);
cout << 1234.56789 << '\n';
cout << 1234.56789 << '\n';
vydast
1234.5679
1234.5679
1235
1235
Zamet'te, chto proishodit okruglenie, a ne otbrasyvanie drobnoj chasti.
Standartnye manipulyatory, vvedennye v $$10.4.2.1, predlagayut
bolee elegantnyj sposob zadaniya formata vyvoda veshchestvennyh.
K nim otnosyatsya raznoobraznye operacii, kotorye prihoditsya
primenyat' srazu pered ili srazu posle operacii vvoda-vyvoda. Naprimer:
cout << x;
cout.flush();
cout << y;
cin.eatwhite();
cin >> x;
Esli pisat' otdel'nye operatory kak vyshe, to logicheskaya svyaz' mezhdu
operatorami neochevidna, a esli uteryana logicheskaya svyaz', programmu
trudnee ponyat'.
Ideya manipulyatorov pozvolyaet takie operacii kak flush() ili
eatwhite() pryamo vstavlyat' v spisok operacij vvoda-vyvoda. Rassmotrim
operaciyu flush(). Mozhno opredelit' klass s operaciej operator<<(), v
kotorom vyzyvaetsya flush():
class Flushtype { };
ostream& operator<<(ostream& os, Flushtype)
{
return flush(os);
}
opredelit' ob容kt takogo tipa
Flushtype FLUSH;
i dobit'sya vydachi bufera, vklyuchiv FLUSH v spisok ob容ktov, podlezhashchih
vyvodu:
cout << x << FLUSH << y << FLUSH ;
Teper' ustanovlena yavnaya svyaz' mezhdu operaciyami vyvoda i sbrasyvaniya
bufera. Odnako, dovol'no bystro nadoest opredelyat' klass i ob容kt dlya
kazhdoj operacii, kotoruyu my hotim primenit' k potochnoj operacii vyvoda.
K schast'yu, mozhno postupit' luchshe. Rassmotrim takuyu funkciyu:
typedef ostream& (*Omanip) (ostream&);
ostream& operator<<(ostream& os, Omanip f)
{
return f(os);
}
Zdes' operaciya vyvoda ispol'zuet parametry tipa "ukazatel' na funkciyu,
imeyushchuyu argument ostream& i vozvrashchayushchuyu ostream&". Otmetiv, chto flush()
est' funkciya tipa "funkciya s argumentom ostream& i vozvrashchayushchaya
ostream&", my mozhem pisat'
cout << x << flush << y << flush;
poluchiv vyzov funkcii flush(). Na samom dele v fajle <iostream.h>
funkciya flush() opisana kak
ostream& flush(ostream&);
a v klasse est' operaciya operator<<, kotoraya ispol'zuet ukazatel' na
funkciyu, kak ukazano vyshe:
class ostream : public virtual ios {
// ...
public:
ostream& operator<<(ostream& ostream& (*)(ostream&));
// ...
};
V privedennoj nizhe stroke bufer vytalkivaetsya v potok cout dvazhdy v
podhodyashchee vremya:
cout << x << flush << y << flush;
Pohozhie opredeleniya sushchestvuyut i dlya klassa istream:
istream& ws(istream& is ) { return is.eatwhite(); }
class istream : public virtual ios {
// ...
public:
istream& operator>>(istream&, istream& (*) (istream&));
// ...
};
poetomu v stroke
cin >> ws >> x;
dejstvitel'no obobshchennye probely budut ubrany do popytki chteniya v x.
Odnako, poskol'ku po umolchaniyu dlya operacii >> probely "s容dayutsya" i
tak, dannoe primenenie ws() izbytochno.
Nahodyat primenenie i manipulyatory s parametrami. Naprimer,
mozhet poyavit'sya zhelanie s pomoshch'yu
cout << setprecision(4) << angle;
napechatat' znachenie veshchestvennoj peremennoj angle s tochnost'yu do
chetyreh znakov posle tochki.
Dlya etogo nuzhno umet' vyzyvat' funkciyu, kotoraya ustanovit
znachenie peremennoj, upravlyayushchej v potoke tochnost'yu veshchestvennyh.
|to dostigaetsya, esli opredelit' setprecision(4) kak ob容kt, kotoryj
mozhno "vyvodit'" s pomoshch'yu operator<<():
class Omanip_int {
int i;
ostream& (*f) (ostream&,int);
public:
Omanip_int(ostream& (*ff) (ostream&,int), int ii)
: f(ff), i(ii) { }
friend ostream& operator<<(ostream& os, Omanip& m)
{ return m.f(os,m.i); }
};
Konstruktor Omanip_int hranit svoi argumenty v i i f, a s pomoshch'yu
operator<< vyzyvaetsya f() s parametrom i. CHasto ob容kty takih klassov
nazyvayut ob容kt-funkciya. CHtoby rezul'tat stroki
cout << setprecision(4) << angle
byl takim, kak my hoteli, neobhodimo chtoby obrashchenie setprecision(4)
sozdavalo bezymyannyj ob容kt klassa Omanip_int, soderzhashchij znachenie 4
i ukazatel' na funkciyu, kotoraya ustanavlivaet v potoke ostream znachenie
peremennoj, zadayushchej tochnost' veshchestvennyh:
ostream& _set_precision(ostream&,int);
Omanip_int setprecision(int i)
{
return Omanip_int(&_set_precision,i);
}
Uchityvaya sdelannye opredeleniya, operator<<() privedet k vyzovu
precision(i).
Utomitel'no opredelyat' klassy napodobie Omanip_int dlya vseh
tipov argumentov, poetomu opredelim shablon tipa:
template<class T> class OMANIP {
T i;
ostream& (*f) (ostream&,T);
public:
OMANIP(ostream (*ff) (ostream&,T), T ii)
: f(ff), i(ii) { }
friend ostream& operator<<(ostream& os, OMANIP& m)
{ return m.f(os,m.i) }
};
S pomoshch'yu OMANIP primer s ustanovkoj tochnosti mozhno sokratit' tak:
ostream& precision(ostream& os,int)
{
os.precision(i);
return os;
}
OMANIP<int> setprecision(int i)
{
return OMANIP<int>(&precision,i);
}
V fajle <iomanip.h> mozhno najti shablon tipa OMANIP, ego dvojnik dlya
istream - shablon tipa SMANIP, a SMANIP - dvojnik dlya ioss.
Nekotorye iz standartnyh manipulyatorov, predlagaemyh potochnoj
bibliotekoj, opisany nizhe. Otmetim,chto programmist mozhet opredelit' novye
neobhodimye emu manipulyatory, ne zatragivaya opredelenij istream,
ostream, OMANIP ili SMANIP.
Ideyu manipulyatorov predlozhil A. Kenig. Ego vdohnovili procedury
razmetki (layout ) sistemy vvoda-vyvoda Algola68. Takaya tehnika imeet
mnogo interesnyh prilozhenij pomimo vvoda-vyvoda. Sut' ee v tom, chto
sozdaetsya ob容kt, kotoryj mozhno peredavat' kuda ugodno i kotoryj
ispol'zuetsya kak funkciya. Peredacha ob容kta yavlyaetsya bolee gibkim
resheniem, poskol'ku detali vypolneniya chastichno opredelyayutsya sozdatelem
ob容kta, a chastichno tem, kto k nemu obrashchaetsya.
10.4.2.1 Standartnye manipulyatory vvoda-vyvoda
|to sleduyushchie manipulyatory:
// Simple manipulators:
ios& oct(ios&); // v vos'merichnoj zapisi
ios& dec(ios&); // v desyatichnoj zapisi
ios& hex(ios&); // v shestnadcaterichnoj zapisi
ostream& endl(ostream&); // dobavit' '\n' i vyvesti
ostream& ends(ostream&); // dobavit' '\0' i vyvesti
ostream& flush(ostream&); // vydat' potok
istream& ws(istream&); // udalit' obobshchennye probely
// Manipulyatory imeyut parametry:
SMANIP<int> setbase(int b);
SMANIP<int> setfill(int f);
SMANIP<int> setprecision(int p);
SMANIP<int> setw(int w);
SMANIP<long> resetiosflags(long b);
SMANIP<long> setiosflags(long b);
Naprimer,
cout << 1234 << ' '
<< hex << 1234 << ' '
<< oct << 1234 << endl;
napechataet
1234 4d2 2322
i
cout << setw(4) << setfill('#') << '(' << 12 << ")\n";
cout << '(' << 12 << ")\n";
napechataet
(##12)
(12)
Ne zabud'te vklyuchit' fajl <iomanip.h>, esli ispol'zuete manipulyatory s
parametrami.
V klasse ostream est' lish' neskol'ko funkcij dlya upravleniya vyvodom,
bol'shaya chast' takih funkcij nahoditsya v klasse ios.
class ostream : public virtual ios {
//...
public:
ostream& flush();
ostream& seekp(streampos);
ostream& seekp(streamoff, seek_dir);
streampos tellp();
//...
};
Kak my uzhe govorili, funkciya flush() opustoshaet bufer v vyhodnoj potok.
Ostal'nye funkcii ispol'zuyutsya dlya pozicionirovaniya v ostream pri
zapisi. Okonchanie na bukvu p ukazyvaet, chto imenno poziciya ispol'zuetsya
pri vydache simvolov v zadannyj potok. Konechno eti funkcii imeyut smysl,
tol'ko esli potok prisoedinen k chemu-libo, chto dopuskaet
pozicionirovanie, naprimer fajl. Tip streampos predstavlyaet poziciyu simvola
v fajle, a tip streamoff predstavlyaet smeshchenie otnositel'no pozicii,
zadannoj seek_dir. Vse oni opredeleny v klasse ios:
class ios {
//...
enum seek_dir {
beg=0, // ot nachala fajla
cur=1, // ot tekushchej pozicii v fajle
end=2 // ot konca fajla
};
//...
};
Pozicii v potoke otschityvayutsya ot 0, kak esli by fajl byl massivom iz
n simvolov:
char file[n-1];
i esli fout prisoedineno k file, to
fout.seek(10);
fout<<'#';
pomestit # v file[10].
Kak i dlya ostream, bol'shinstvo funkcij formatirovaniya i upravleniya
vvodom nahoditsya ne v klasse iostream, a v bazovom klasse ios.
class istream : public virtual ios {
//...
public:
int peek()
istream& putback(char c);
istream& seekg(streampos);
istream& seekg(streamoff, seek_dir);
streampos tellg();
//...
};
Funkcii pozicionirovaniya rabotayut kak i ih dvojniki iz ostream.
Okonchanie na bukvu g pokazyvaet, chto imenno poziciya ispol'zuetsya pri
vvode simvolov iz zadannogo potoka. Bukvy p i g nuzhny, poskol'ku
my mozhem sozdat' proizvodnyj klass iostreams iz klassov ostream i
istream, i v nem neobhodimo sledit' za poziciyami vvoda i vyvoda.
S pomoshch'yu funkcii peek() programma mozhet uznat' sleduyushchij simvol,
podlezhashchij vvodu, ne zatragivaya rezul'tata posleduyushchego chteniya. S
pomoshch'yu funkcii putback(), kak pokazano v $$10.3.3, mozhno vernut'
nenuzhnyj simvol nazad v potok, chtoby on byl prochitan v drugoe vremya.
Nizhe privedena programma kopirovaniya odnogo fajla v drugoj. Imena
fajlov berutsya iz komandnoj stroki programmy:
#include <fstream.h>
#include <libc.h>
void error(char* s, char* s2 ="")
{
cerr << s << ' ' << s2 << '\n';
exit(1);
}
int main(int argc, char* argv[])
{
if (argc != 3) error("wrong number of arguments");
ifstream from(argv[1]);
if (!from) error("cannot open input file",argv[1]);
ostream to(argv[2]);
if (!to) error("cannot open output file",argv[2]);
char ch;
while (from.get(ch)) to.put(ch);
if (!from.eof() || to.bad())
error("something strange happened");
return 0;
}
Dlya otkrytiya vyhodnogo fajla sozdaetsya ob容kt klassa ofstream -
vyhodnoj potok fajla, ispol'zuyushchij v kachestve argumenta imya fajla.
Analogichno, dlya otkrytiya vhodnogo fajla sozdaetsya ob容kt klassa
ifstream - vhodnoj fajlovyj potok, takzhe ispol'zuyushchij v kachestve
argumenta imya fajla. V oboih sluchayah sleduet proverit' sostoyanie
sozdannogo ob容kta, chtoby ubedit'sya v uspeshnom otkrytii fajla, a
esli eto ne tak, operacii zavershatsya ne uspeshno, no korrektno.
Po umolchaniyu ifstream vsegda otkryvaetsya na chtenie, a ofstream
otkryvaetsya na zapis'. V ostream i v istream mozhno ispol'zovat'
neobyazatel'nyj vtoroj argument, ukazyvayushchij inye rezhimy otkrytiya:
class ios {
public:
//...
enum open_mode {
in=1, // otkryt' na chtenie
out=2, // otkryt' kak vyhodnoj
ate=4, // otkryt' i peremestit'sya v konec fajla
app=010, // dobavit'
trunc=020, // sokratit' fajl do nulevoj dliny
nocreate=040, // neudacha, esli fajl ne sushchestvuet
noreplace=0100 // neudacha, esli fajl sushchestvuet
};
//...
};
Nastoyashchie znacheniya dlya open_mode i ih smysl veroyatno budut zaviset'
ot realizacii. Bud'te dobry, za detalyami obratites' k rukovodstvu po
vashej biblioteke ili eksperimentirujte. Privedennye kommentarii
mogut proyasnit' ih naznachenie. Naprimer, mozhno otkryt' fajl s usloviem,
chto operaciya otkrytiya ne vypolnitsya, esli fajl uzhe ne sushchestvuet:
void f()
{
ofstream mystream(name,ios::out|ios::nocreate);
if (ofstream.bad()) {
//...
}
//...
}
Takzhe mozhno otkryt' fajl srazu na chtenie i zapis':
fstream dictionary("concordance", ios::in|ios::out);
Vse operacii, dopustimye dlya ostream i ostream, mozhno primenyat' k
fstream. Na samom dele, klass fstream yavlyaetsya proizvodnym ot iostream,
kotoryj yavlyaetsya, v svoyu ochered', proizvodnym ot istream i ostream.
Prichina, po kotoroj informaciya po buferizacii i formatirovaniyu dlya
ostream i istream nahoditsya v virtual'nom bazovom klasse ios, v tom,
chtoby zastavit' dejstvovat' vsyu etu posledovatel'nost' proizvodnyh
klassov. Po etoj zhe prichine operacii pozicionirovaniya v istream i
ostream imeyut raznye imena - seekp() i seekg(). V iostream est'
otdel'nye pozicii dlya chteniya i zapisi.
Fajl mozhet byt' zakryt yavno, esli vyzvat' close() dlya ego potoka:
mystream.close();
No eto neyavno delaet destruktor potoka, tak chto yavnyj vyzov close()
mozhet ponadobit'sya, esli tol'ko fajl nuzhno zakryt' do dostizheniya
konca oblasti opredelennosti potoka.
Zdes' voznikaet vopros, kak realizaciya mozhet obespechit'
sozdanie predopredelennyh potokov cout, cin i cerr do ih pervogo
ispol'zovaniya i zakrytie ih tol'ko posle poslednego ispol'zovaniya.
Konechno, raznye realizacii biblioteki potokov iz <iostream.h> mogut
po-raznomu reshat' etu zadachu. V konce koncov, reshenie - eto
prerogativa realizacii, i ono dolzhno byt' skryto ot pol'zovatelya. Zdes'
privoditsya tol'ko odin sposob, primenennyj tol'ko v odnoj realizacii,
no on dostatochno obshchij, chtoby garantirovat' pravil'nyj poryadok
sozdaniya i unichtozheniya global'nyh ob容ktov razlichnyh tipov.
Osnovnaya ideya v tom, chtoby opredelit' vspomogatel'nyj klass,
kotoryj po suti sluzhit schetchikom, sledyashchim za tem, skol'ko raz
<iostream.h> byl vklyuchen v razdel'no kompilirovavshiesya programmnye
fajly:
class Io_init {
static int count;
//...
public:
Io_init();
^Io_init();
};
static Io_init io_init ;
Dlya kazhdogo programmnogo fajla opredelen svoj ob容kt s imenem io_init.
Konstruktor dlya ob容ktov io_init ispol'zuet Io_init::count kak pervyj
priznak togo, chto dejstvitel'naya inicializaciya global'nyh ob容ktov
potokovoj biblioteki vvoda-vyvoda sdelana v tochnosti odin raz:
Io_init::Io_init()
{
if (count++ == 0) {
// inicializirovat' cout
// inicializirovat' cerr
// inicializirovat' cin
// i t.d.
}
}
Obratno, destruktor dlya ob容ktov io_init ispol'zuet Io_count, kak
poslednee ukazanie na to, chto vse potoki zakryty:
Io_init::^Io_init()
{
if (--count == 0) {
// ochistit' cout (sbros, i t.d.)
// ochistit' cerr (sbros, i t.d.)
// ochistit' cin
// i t.d.
}
}
|to obshchij priem raboty s bibliotekami, trebuyushchimi inicializacii i
udaleniya global'nyh ob容ktov. Vpervye v S++ ego primenil D. SHvarc.
V sistemah, gde pri vypolnenii vse programmy razmeshchayutsya v osnovnoj
pamyati, dlya etogo priema net pomeh. Esli eto ne tak, to nakladnye
rashody, svyazannye s vyzovom v pamyat' kazhdogo programmnogo fajla
dlya vypolneniya funkcij inicializacii, budut zametny. Kak vsegda,
luchshe, po vozmozhnosti, izbegat' global'nyh ob容ktov. Dlya klassov,
v kotoryh kazhdaya operaciya znachitel'na po ob容mu vypolnyaemoj raboty,
chtoby garantirovat' inicializaciyu, bylo by razumno proveryat' takie
pervye priznaki (napodobie Io_init::count) pri kazhdoj operacii.
Odnako, dlya potokov takoj podhod byl by izlishne rastochitel'nym.
Kak bylo pokazano, potok mozhet byt' privyazan k fajlu, t.e. massivu
simvolov, hranyashchemusya ne v osnovnoj pamyati, a, naprimer, na diske. Tochno
tak zhe potok mozhno privyazat' k massivu simvolov v osnovnoj pamyati.
Naprimer, mozhno vospol'zovat'sya vyhodnym strokovym potokom ostrstream
dlya formatirovaniya soobshchenij, ne podlezhashchih nemedlennoj pechati:
char* p = new char[message_size];
ostrstream ost(p,message_size);
do_something(arguments,ost);
display(p);
S pomoshch'yu standartnyh operacij vyvoda funkciya do_something mozhet pisat'
v potok ost, peredavat' ost podchinyayushchimsya ej funkciyam i t.p. Kontrol'
perepolneniya ne nuzhen, poskol'ku ost znaet svoj razmer i pri zapolnenii
perejdet v sostoyanie, opredelyaemoe fail(). Zatem funkciya display mozhet
poslat' soobshchenie v "nastoyashchij" vyhodnoj potok. Takoj priem naibolee
podhodit v teh sluchayah, kogda okonchatel'naya operaciya vyvoda
prednaznachena dlya zapisi na bolee slozhnoe ustrojstvo, chem tradicionnoe,
orientirovannoe na posledovatel'nost' strok, vyvodnoe ustrojstvo.
Naprimer, tekst iz ost mozhet byt' pomeshchen v fiksirovannuyu oblast' na ekrane.
Analogichno, istrstream yavlyaetsya vvodnym strokovym potokom,
chitayushchim iz posledovatel'nosti simvolov, zakanchivayushchejsya nulem:
void word_per_line(char v[], int sz)
/*
pechatat' "v" razmerom "sz" po odnomu slovu v stroke
*/
{
istrstream ist(v,sz); // sozdat' istream dlya v
char b2[MAX]; // dlinnee samogo dlinnogo slova
while (ist>>b2) cout <<b2 << "\n";
}
Zavershayushchij nul' schitaetsya koncom fajla.
Strokovye potoki opisany v fajle <strstream.h>.
Vse operacii vvoda-vyvoda byli opredeleny bez vsyakoj svyazi s tipom
fajla, no nel'zya odinakovo rabotat' so vsemi ustrojstvami bez ucheta
algoritma buferizacii. Ochevidno, chto potoku ostream, privyazannomu k
stroke simvolov, nuzhen ne takoj bufer, kak ostream, privyazannomu k
fajlu. Takie voprosy reshayutsya sozdaniem vo vremya inicializacii raznyh
buferov dlya potokov raznyh tipov. No sushchestvuet tol'ko odin nabor
operacij nad etimi tipami buferov, poetomu v ostream net funkcij, kod
kotoryh uchityvaet razlichie buferov. Odnako, funkcii, sledyashchie za
perepolneniem i obrashcheniem k pustomu buferu, yavlyayutsya virtual'nymi.
|to horoshij primer primeneniya virtual'nyh funkcij dlya edinoobraznoj
raboty s ekvivalentnymi logicheski, no razlichno realizovannymi
strukturami, i oni vpolne spravlyayutsya s trebuemymi algoritmami buferizacii.
Opisanie bufera potoka v fajle <iostream.h> mozhet vyglyadet' sleduyushchim
obrazom:
class streambuf { // upravlenie buferom potoka
protected:
char* base; // nachalo bufera
char* pptr; // sleduyushchij svobodnyj bajt
char* gptr; // sleduyushchij zapolnennyj bajt
char* eptr; // odin iz ukazatelej na konec bufera
char alloc; // bufer, razmeshchennyj s pomoshch'yu "new"
//...
// Opustoshit' bufer:
// Vernut' EOF pri oshibke, 0 - udacha
virtual int overflow(int c = EOF);
// Zapolnit' bufer:
// Vernut' EOF v sluchae oshibki ili konca vhodnogo potoka,
// inache vernut' ocherednoj simvol
virtual int underflow();
//...
public:
streambuf();
streambuf(char* p, int l);
virtual ~streambuf();
int snextc() // poluchit' ocherednoj simvol
{
return (++gptr==pptr) ? underflow() : *gptr&0377;
}
int allocate(); // otvesti pamyat' pod bufer
//...
};
Podrobnosti realizacii klassa streambuf privedeny zdes' tol'ko dlya
polnoty predstavleniya. Ne predpolagaetsya, chto est' obshchedostupnye
realizacii, ispol'zuyushchie imenno eti imena. Obratite vnimanie na
opredelennye zdes' ukazateli, upravlyayushchie buferom; s ih pomoshch'yu
prostye posimvol'nye operacii s potokom mozhno opredelit' maksimal'no
effektivno (i prichem odnokratno) kak funkcii-podstanovki. Tol'ko
funkcii overflow() i underflow() trebuet svoej realizacii dlya kazhdogo
algoritma buferizacii, naprimer:
class filebuf : public streambuf {
protected:
int fd; // deskriptor fajla
char opened; // priznak otkrytiya fajla
public:
filebuf() { opened = 0; }
filebuf(int nfd, char* p, int l)
: streambuf(p,l) { /* ... */ }
~filebuf() { close(); }
int overflow(int c=EOF);
int underflow();
filebuf* open(char *name, ios::open_mode om);
int close() { /* ... */ }
//...
};
int filebuf::underflow() // zapolnit' bufer iz "fd"
{
if (!opened || allocate()==EOF) return EOF;
int count = read(fd, base, eptr-base);
if (count < 1) return EOF;
gptr = base;
pptr = base + count;
return *gptr & 0377; // &0377 predotvrashchaet razmnozhenie znaka
}
Za dal'nejshimi podrobnostyami obratites' k rukovodstvu po realizacii
klassa streambuf.
Poskol'ku tekst programm na S i na S++ chasto putayut, to putayut inogda
i potokovyj vvod-vyvod S++ i funkcii vvoda-vyvoda semejstva printf dlya
yazyka S. Dalee, t.k. S-funkcii mozhno vyzyvat' iz programmy na S++, to
mnogie predpochitayut ispol'zovat' bolee znakomye funkcii vvoda-vyvoda S.
Po etoj prichine zdes' budet dana osnova funkcij vvoda-vyvoda S.
Obychno operacii vvoda-vyvoda na S i na S++ mogut idti po ocheredi na
urovne strok. Peremeshivanie ih na urovne posimvol'nogo vvoda-vyvoda
vozmozhno dlya nekotoryh realizacij, no takaya programma mozhet byt'
neperenosimoj. Nekotorye realizacii potokovoj biblioteki S++ pri dopushchenii
vvoda-vyvoda na S trebuyut vyzova staticheskoj funkcii-chlena
ios::sync_with_stdio().
V obshchem, potokovye funkcii vyvoda imeyut pered standartnoj
funkciej S printf() to preimushchestvo, chto potokovye funkcii obladayut
opredelennoj tipovoj nadezhnost'yu i edinoobrazno opredelyayut vyvod
ob容ktov predopredelennogo i pol'zovatel'skogo tipov.
Osnovnaya funkciya vyvoda S est'
int printf(const char* format, ...)
i ona vyvodit proizvol'nuyu posledovatel'nost' parametrov v formate,
zadavaemom strokoj formatirovaniya format. Stroka formatirovaniya sostoit
iz ob容ktov dvuh tipov: prostye simvoly, kotorye prosto kopiruyutsya v
vyhodnoj potok, i specifikacii preobrazovanij, kazhdaya iz kotoryh
preobrazuet i pechataet ocherednoj parametr. Kazhdaya specifikaciya
preobrazovaniya nachinaetsya s simvola %, naprimer
printf("there were %d members present.",no_of_members);
Zdes' %d ukazyvaet, chto no_of_members sleduet schitat' celym i pechatat'
kak sootvetstvuyushchuyu posledovatel'nost' desyatichnyh cifr. Esli
no_of_members==127, to budet napechatano
there were 127 members present.
Nabor specifikacij preobrazovanij dostatochno bol'shoj i obespechivaet
bol'shuyu gibkost' pechati. Za simvolom % mozhet sledovat':
- neobyazatel'nyj znak minus, zadayushchij vyravnivanie vlevo v ukazannom
pole dlya preobrazovannogo znacheniya;
d neobyazatel'naya stroka cifr, zadayushchaya shirinu polya; esli v
preobrazovannom znachenii men'she simvolov, chem shirina stroki, to ono
dopolnitsya do shiriny polya probelami sleva (ili sprava, esli dana
specifikaciya vyravnivaniya vlevo); esli stroka shiriny polya nachinaetsya
s nulya, to dopolnenie budet provoditsya nulyami, a ne probelami;
. neobyazatel'nyj simvol tochka sluzhit dlya otdeleniya shiriny polya ot
posleduyushchej stroki cifr;
d neobyazatel'naya stroka cifr, zadayushchaya tochnost', kotoraya opredelyaet
chislo cifr posle desyatichnoj tochki dlya znachenij v specifikaciyah
e ili f, ili zhe zadaet maksimal'noe chislo pechataemyh simvolov
stroki;
* dlya zadaniya shiriny polya ili tochnosti mozhet ispol'zovat'sya * vmesto
stroki cifr. V etom sluchae dolzhen byt' parametr celogo tipa, kotoryj
soderzhit znachenie shiriny polya ili tochnosti;
h neobyazatel'nyj simvol h ukazyvaet, chto posleduyushchaya specifikaciya d,
o, x ili u otnositsya k parametru tipa korotkoe celoe;
l neobyazatel'nyj simvol l ukazyvaet, chto posleduyushchaya specifikaciya d,
o, x ili u otnositsya k parametru tipa dlinnoe celoe;
% oboznachaet, chto nuzhno napechatat' sam simvol %; parametr ne nuzhen;
c simvol, ukazyvayushchij tip trebuemogo preobrazovaniya. Simvoly
preobrazovaniya i ih smysl sleduyushchie:
d Celyj parametr vydaetsya v desyatichnoj zapisi;
o Celyj parametr vydaetsya v vos'merichnoj zapisi;
x Celyj parametr vydaetsya v shestnadcaterichnoj zapisi;
f Veshchestvennyj ili s dvojnoj tochnost'yu parametr vydaetsya v
desyatichnoj zapisi vida [-]ddd.ddd, gde chislo cifr posle
tochki ravno specifikacii tochnosti dlya parametra. Esli tochnost'
ne zadana, pechataetsya shest' cifr; esli yavno zadana tochnost' 0,
tochka i cifry posle nee ne pechatayutsya;
e Veshchestvennyj ili s dvojnoj tochnost'yu parametr vydaetsya v
desyatichnoj zapisi vida [-]d.ddde+dd; zdes' odna cifra pered
tochkoj, a chislo cifr posle tochki ravno specifikacii tochnosti
dlya parametra; esli ona ne zadana pechataetsya shest' cifr;
g Veshchestvennyj ili s dvojnoj tochnost'yu parametr pechataetsya po toj
specifikacii d, f ili e, kotoraya daet bol'shuyu tochnost' pri
men'shej shirine polya;
c Simvol'nyj parametr pechataetsya. Nulevye simvoly ignoriruyutsya;
s Parametr schitaetsya strokoj (simvol'nyj ukazatel'), i pechatayutsya
simvoly iz stroki do nulevogo simvola ili do dostizheniya chisla
simvolov, ravnogo specifikacii tochnosti; no, esli tochnost'
ravna 0 ili ne ukazana, pechatayutsya vse simvoly do nulevogo;
p Parametr schitaetsya ukazatelem i ego vid na pechati zavisit ot
realizacii;
u Bezznakovyj celyj parametr pechataetsya v desyatichnoj zapisi.
Nesushchestvuyushchee pole ili pole s shirinoj, men'shej real'noj, privedet
k usecheniyu polya. Dopolnenie probelami proishodit, esli tol'ko
specifikaciya shiriny polya bol'she real'noj shiriny.
Nizhe priveden bolee slozhnyj primer:
char* src_file_name;
int line;
char* line_format = "\n#line %d \"%s\"\n";
main()
{
line = 13;
src_file_name = "C++/main.c";
printf("int a;\n");
printf(line_format,line,src_file_name);
printf("int b;\n");
}
v kotorom pechataetsya
int a;
#line 13 "C++/main.c"
int b;
Ispol'zovanie printf() nenadezhno v tom smysle, chto net nikakogo
kontrolya tipov. Tak, nizhe priveden izvestnyj sposob polucheniya
neozhidannogo rezul'tata - pechati musornogo znacheniya ili chego pohuzhe:
char x;
// ...
printf("bad input char: %s",x);
Odnako, eti funkcii obespechivayut bol'shuyu gibkost' i znakomy
programmiruyushchim na S.
Kak obychno, getchar() pozvolyaet znakomym sposobom chitat' simvoly iz
vhodnogo potoka:
int i;:
while ((i=getchar())!=EOF) { // simvol'nyj vvod C
// ispol'zuem i
}
Obratite vnimanie: chtoby bylo zakonnym sravnenie s velichinoj EOF tipa
int pri proverke na konec fajla, rezul'tat getchar() nado pomeshchat' v
peremennuyu tipa int, a ne char.
Za podrobnostyami o vvode-vyvode na S otsylaem k vashemu rukovodstvu
po S ili knige Kernigana i Ritchi "YAzyk programmirovaniya S".
1. (*1.5) CHitaya fajl veshchestvennyh chisel, sostavlyat' iz par prochitannyh
chisel kompleksnye chisla, zapisat' kompleksnye chisla.
2. (*1.5) Opredelit' tip name_and_address (tip_i_adres). Opredelit' dlya
nego << i >>. Napisat' programmu kopirovaniya ob容ktov potoka
name_and_address.
3. (*2) Razrabotat' neskol'ko funkcij dlya zaprosa i chteniya dannyh
raznyh tipov. Predlozheniya: celoe, veshchestvennoe chislo, imya fajla,
pochtovyj adres, data, lichnaya informaciya, i t.p. Popytajtes' sdelat'
ih ustojchivymi k oshibkam.
4. (*1.5) Napishite programmu, kotoraya pechataet: (1) strochnye bukvy,
(2) vse bukvy, (3) vse bukvy i cifry, (4) vse simvoly, vhodyashchie v
identifikator v vashej versii S++, (5) vse znaki punktuacii,
(6) celye znacheniya vseh upravlyayushchih simvolov, (7) vse obobshchennye
probely, (8) celye znacheniya vseh obobshchennyh probelov, i, nakonec,
(9) vse izobrazhaemye simvoly.
5. (*4) Realizujte standartnuyu biblioteku vvoda-vyvoda S (<stdio.h>)
s pomoshch'yu standartnoj biblioteki vvoda-vyvoda S++ (<iostream.h>).
6. (*4) Realizujte standartnuyu biblioteku vvoda-vyvoda S++
(<iostream.h>) s pomoshch'yu standartnoj biblioteki vvoda-vyvoda S
(<stdio.h>).
7. (*4) Realizujte biblioteki S i S++ tak, chtoby ih mozhno bylo
ispol'zovat' odnovremenno.
8. (*2) Realizujte klass, dlya kotorogo operaciya [] peregruzhena tak,
chtoby obespechit' proizvol'noe chtenie simvolov iz fajla.
9. (*3) Povtorite uprazhnenie 8, no dobejtes', chtoby operaciya [] byla
primenima dlya chteniya i dlya zapisi. Podskazka: pust' [] vozvrashchaet
ob容kt "deskriptor tipa", dlya kotorogo prisvaivanie oznachaet:
prisvoit' cherez deskriptor fajlu, a neyavnoe privedenie k tipu char
oznachaet chtenie fajla po deskriptoru.
10.(*2) Povtorite uprazhnenie 9, pozvolyaya operacii [] indeksirovat'
ob容kty proizvol'nyh tipov, a ne tol'ko simvoly.
11.(*3.5) Produmajte i realizujte operaciyu formatnogo vvoda.
Ispol'zujte dlya zadaniya formata stroku specifikacij kak v printf().
Dolzhna byt' vozmozhnost' popytok primeneniya neskol'kih specifikacij dlya
odnogo vvoda, chtoby najti trebuemyj format. Klass formatnogo vvoda
dolzhen byt' proizvodnym klassa istream.
12.(*4) Pridumajte (i realizujte) luchshie formaty vvoda.
13.(**2) Opredelite dlya vyvoda manipulyator based s dvumya parametrami:
sistema schisleniya i celoe znachenie, i pechatajte celoe v
predstavlenii, opredelyaemom sistemoj schisleniya. Naprimer, based(2,9)
napechataet 1001.
14.(**2) Napishite "miniatyurnuyu" sistemu vvoda-vyvoda, kotoraya realizuet
klassy istream, ostream, ifstream, ofstream i predostavlyaet funkcii,
takie kak operator<<() i operator>>() dlya celyh, i operacii, takie
kak open() i close() dlya fajlov. Ispol'zujte isklyuchitel'nye
situacii, a ne peremennye sostoyaniya, dlya soobshcheniya ob oshibkah.
15.(**2) Napishite manipulyator, kotoryj vklyuchaet i otklyuchaet eho
simvola.
* PROEKTIROVANIE I RAZVITIE
"Serebryanoj puli ne sushchestvuet."
- F. Bruks
V etoj glave obsuzhdayutsya podhody k razrabotke programmnogo obespecheniya.
Obsuzhdenie zatragivaet kak tehnicheskie, tak i sociologicheskie aspekty
processa razvitiya programmnogo obespecheniya. Programma rassmatrivaetsya
kak model' real'nosti, v kotoroj kazhdyj klass predstavlyaet opredelennoe
ponyatie. Klyuchevaya zadacha proektirovaniya sostoit v opredelenii dostupnoj
i zashchishchennoj chastej interfejsa klassa, ishodya iz kotoryh opredelyayutsya
razlichnye chasti programmy. Opredelenie etih interfejsov est'
iterativnyj process, obychno trebuyushchij eksperimentirovaniya. Upor
delaetsya na vazhnoj roli proektirovaniya i organizacionnyh faktorov
v processe razvitiya programmnogo obespecheniya.
Sozdanie lyuboj netrivial'noj programmnoj sistemy - slozhnaya i chasto
vymatyvayushchaya zadacha. Dazhe dlya otdel'nogo programmista sobstvenno zapis'
operatorov programmy est' tol'ko chast' vsej raboty. Obychno analiz vsej
zadachi, proektirovanie programmy v celom, dokumentaciya, testirovanie,
soprovozhdenie i upravlenie vsem etim zatmevaet zadachu napisaniya i
otladki otdel'nyh chastej programmy. Konechno, mozhno vse eti vidy
deyatel'nosti oboznachit' kak "programmirovanie" i zatem vpolne
obosnovanno utverzhdat': "YA ne proektiruyu, ya tol'ko programmiruyu".
No kak by ne nazyvalis' otdel'nye vidy deyatel'nosti, byvaet inogda vazhno
sosredotochit'sya na nih po otdel'nosti, tak zhe kak inogda byvaet vazhno
rassmotret' ves' process v celom. Stremyas' poskoree dovesti sistemu
do postavki, nel'zya upuskat' iz vida ni detali, ni kartinu v celom,
hotya dovol'no chasto proishodit imenno eto.
|ta glava sosredotochena na teh chastyah processa razvitiya programmy,
kotorye ne svyazany s napisaniem i otladkoj otdel'nyh programmnyh
fragmentov. Obsuzhdenie zdes' menee tochnoe i detal'noe, chem vo vseh
ostal'nyh chastyah knigi, gde rassmatrivayutsya konkretnye cherty yazyka
ili opredelennye priemy programmirovaniya. |to neizbezhno, poskol'ku net
gotovyh receptov sozdaniya horoshih programm. Detal'nye recepty "kak"
mogut sushchestvovat' tol'ko dlya opredelennyh, horosho razrabotannyh oblastej
primeneniya, no ne dlya dostatochno shirokih oblastej prilozheniya. V
programmirovanii ne sushchestvuet zamenitelej razuma, opyta i vkusa.
Sledovatel'no, v etoj glave vy najdete tol'ko obshchie rekomendacii,
al'ternativnye podhody i ostorozhnye vyvody.
Slozhnost' dannoj tematiki svyazana s abstraktnoj prirodoj programm
i tem faktom, chto priemy, primenimye dlya nebol'shih proektov (skazhem,
programma v 10000 strok, sozdannaya odnim ili dvumya lyud'mi), ne
rasprostranyayutsya na srednie ili bol'shie proekty. Po etoj prichine inogda
my privodim primery iz menee abstraktnyh inzhenernyh disciplin, a ne
tol'ko iz programmirovaniya. Ne preminem napomnit', chto "dokazatel'stvo po
analogii" yavlyaetsya moshennichestvom, i analogii sluzhat zdes' tol'ko v
kachestve primera. Ponyatiya proektirovaniya, formuliruemye
s pomoshch'yu opredelennyh konstrukcij S++, i poyasnyaemye primerami, my
budem obsuzhdat' v glavah 12 i 13. Predlozhennye v etoj glave rekomendacii,
otrazhayutsya kak v samom yazyke S++, tak i v reshenii konkretnyh programmnyh
zadach po vsej knige.
Snova napomnim, chto v silu chrezvychajnogo raznoobraziya oblastej
primeneniya, programmistov i sredy, v kotoroj razvivaetsya programmnaya
sistema, nel'zya ozhidat', chto kazhdyj vyvod, sdelannyj zdes', budet
pryamo primenim dlya vashej zadachi. |ti vyvody primenimy
vo mnogih samyh raznyh sluchayah, no ih nel'zya schitat' universal'nymi
zakonami. Smotrite na nih so zdorovoj dolej skepticizma.
YAzyk S++ mozhno prosto ispol'zovat' kak luchshij variant S. Odnako,
postupaya tak, my ne ispol'zuem naibolee moshchnye vozmozhnosti S++ i
opredelennye priemy programmirovaniya na nem, tak chto realizuem lish'
maluyu dolyu potencial'nyh dostoinstv S++. V etoj glave izlagaetsya takoj
podhod k proektirovaniyu, kotoryj pozvolyaet polnost'yu ispol'zovat'
vozmozhnosti abstraktnyh dannyh i sredstva ob容ktnogo programmirovaniya S++.
Takoj podhod obychno nazyvayut ob容ktno-orientirovannym proektirovaniem.
V glave 12 obsuzhdayutsya osnovnye priemy programmirovaniya na S++, tam zhe
soderzhitsya predosterezhenie ot somnitel'nyh idej, chto est' tol'ko odin
"pravil'nyj" sposob ispol'zovaniya S++, i chto dlya polucheniya
maksimal'nogo vyigrysha sleduet vsyakoe sredstvo S++
primenyat' v lyuboj programme ($$12.1).
Ukazhem nekotorye osnovnye principy, rassmatrivaemye v etoj glave:
- iz vseh voprosov, svyazannyh s processom razvitiya programmnogo
obespecheniya, samyj vazhnyj - chetko soznavat', chto sobstvenno vy
pytaetes' sozdat'.
- Uspeshnyj process razvitiya programmnogo obespecheniya - eto dli-
tel'nyj process.
- Sistemy, kotorye my sozdaem, stremyatsya k predelu slozhnosti
po otnosheniyu kak k samim sozdatelyam, tak i ispol'zuemym sredstvam.
- |ksperiment yavlyaetsya neobhodimoj chast'yu proekta dlya razrabotki vseh
netrivial'nyh programmnyh sistem.
- Proektirovanie i programmirovanie - eto iterativnye processy.
- Razlichnye stadii proekta programmnogo obespecheniya, takie kak:
proektirovanie, programmirovanie i testirovanie - nevozmozhno strogo
razdelit'.
- Proektirovanie i programmirovanie nel'zya rassmatrivat' v otryve ot
voprosov upravleniya etimi vidami deyatel'nosti.
Nedoocenit' lyuboj iz etih principov ochen' legko, no obychno nakladno.
V to zhe vremya trudno voplotit' eti abstraktnye idei na praktike.
Zdes' neobhodim opredelennyj opyt. Podobno postroeniyu lodki, ezde na
velosipede ili programmirovaniyu proektirovanie - eto iskusstvo, kotorym
nel'zya ovladet' tol'ko s pomoshch'yu teoreticheskih zanyatij.
Mozhet byt' vse eti emkie principy mozhno szhat' v odin:
proektirovanie i programmirovanie - vidy chelovecheskoj deyatel'nosti;
zabud' pro eto - i vse propalo.
Slishkom chasto my zabyvaem pro eto i rassmatrivaem process razvitiya
programmnogo obespecheniya prosto kak "posledovatel'nost' horosho
opredelennyh shagov, na kazhdom iz kotoryh po zadannym pravilam
proizvodyatsya nekotorye dejstviya nad vhodnymi dannymi, chtoby poluchit'
trebuemyj rezul'tat". Sam stil' predydushchego predlozheniya vydaet
prisutstvie chelovecheskoj prirody!
|ta glava otnositsya k proektam, kotorye mozhno schitat' chestolyubivymi,
esli uchityvat' resursy i opyt lyudej, sozdayushchih sistemu. Pohozhe, eto v
prirode kak individuuma, tak i organizacii - brat'sya za proekty na
predele svoih vozmozhnostej. Esli zadacha ne soderzhit opredelennyj vyzov,
net smysla udelyat' osoboe vnimanie ee proektirovaniyu. Takie zadachi
reshayutsya v ramkah uzhe ustoyavshejsya struktury, kotoruyu ne sleduet razrushat'.
Tol'ko esli zamahivayutsya na chto-to ambicioznoe, poyavlyaetsya potrebnost'
v novyh, bolee moshchnyh sredstvah i priemah. Krome togo, sushchestvuet
tendenciya u teh, kto "znaet kak delat'", pereporuchat' proekt novichkam,
kotorye ne imeyut takih znanij.
Ne sushchestvuet "edinstvennogo pravil'nogo sposoba" dlya
proektirovaniya i sozdaniya vsej sistemy. YA by schital veru v "edinstvennyj
pravil'nyj sposob" detskoj bolezn'yu, esli by etoj bolezn'yu slishkom
chasto ne zabolevali i opytnye programmisty. Napomnim eshche raz: tol'ko
po toj prichine, chto priem uspeshno ispol'zovalsya v techenie goda dlya
odnogo proekta, ne sleduet, chto on bez vsyakih izmenenij okazhetsya
stol' zhe polezen dlya drugogo cheloveka ili drugoj zadachi. Vsegda vazhno
ne imet' predubezhdenij.
Ubezhdenie v tom, chto net edinstvenno vernogo resheniya, pronizyvaet
ves' proekt yazyka S++, i, v osnovnom, po etoj prichine v pervom izdanii
knigi ne bylo razdela, posvyashchennogo proektirovaniyu: ya ne hotel, chtoby
ego rassmatrivali kak "manifest" moih lichnyh simpatij. Po etoj zhe
prichine zdes', kak i v glavah 12 i 13, net chetko opredelennogo vzglyada na
process razvitiya programmnogo obespecheniya, skoree zdes' prosto daetsya
obsuzhdenie opredelennogo kruga, chasto voznikayushchih, voprosov i
predlagayutsya nekotorye resheniya, okazavshiesya poleznymi v opredelennyh
usloviyah.
Za etim vvedeniem sleduet kratkoe obsuzhdenie celej i sredstv
razvitiya programmnogo obespecheniya v $$11.2, a dal'she glava raspadaetsya
na dve osnovnyh chasti:
- $$11.3 soderzhit opisanie processa razvitiya programmnogo obespecheniya.
- $$11.4 soderzhit nekotorye prakticheskie rekomendacii po organizacii
etogo processa.
Vzaimosvyaz' mezhdu proektirovaniem i yazykom programmirovaniya obsuzhdaetsya
v glave 12, a glava 13 posvyashchena voprosam proektirovaniya bibliotek dlya
S++.
Ochevidno, bol'shaya chast' rassuzhdenij otnositsya k programmnym proektam
bol'shogo ob容ma. CHitateli, kotorye ne uchastvuyut v takih razrabotkah,
mogut sidet' spokojno i radovat'sya, chto vse eti uzhasy ih minovali,
ili zhe oni mogut vybrat' voprosy, kasayushchiesya tol'ko ih interesov. Net
nizhnej granicy razmera programmy, nachinaya s kotoroj imeet smysl zanyat'sya
proektirovaniem prezhde, chem nachat' pisat' programmu. Odnako vse-taki est' nizhnyaya
granica, nachinaya s kotoroj mozhno ispol'zovat' kakie-libo metody
proektirovaniya. Voprosy, svyazannye s razmerom, obsuzhdayutsya v $$11.4.2.
Trudnee vsego v programmnyh proektah borot'sya s ih slozhnost'yu.
Est' tol'ko odin obshchij sposob bor'by so slozhnost'yu: razdelyaj i
vlastvuj. Esli zadachu udalos' razdelit' na dve podzadachi, kotorye mozhno
reshat' v otdel'nosti, to mozhno schitat' ee reshennoj za schet razdeleniya
bolee, chem napolovinu. |tot prostoj princip primenim dlya udivitel'no
bol'shogo chisla situacij. V chastnosti, ispol'zovanie modulej ili klassov
pri razrabotke programmnyh sistem pozvolyaet razbit' programmu na dve
chasti: chast' realizacii i chast', otkrytuyu pol'zovatelyu - kotorye
svyazany mezhdu soboj (v ideale) vpolne opredelennym interfejsom. |to
osnovnoj, vnutrenne prisushchij programmirovaniyu, princip bor'by so
slozhnost'yu.
Podobno etomu i process proektirovaniya programmy mozhno razbit' na
otdel'nye vidy deyatel'nosti s chetko opredelennym (v ideale)
vzaimodejstviem mezhdu lyud'mi, uchastvuyushchimi v nih. |to osnovnoj,
vnutrenne prisushchij proektirovaniyu, princip bor'by so slozhnost'yu i
podhod k upravleniyu lyud'mi,zanyatymi v proekte.
V oboih sluchayah vydelenie chastej i opredelenie interfejsa mezhdu
chastyami - eto to mesto, gde trebuetsya maksimum opyta i chut'ya. Takoe
vydelenie ne yavlyaetsya chisto mehanicheskim processom, obychno ono trebuet
pronicatel'nosti, kotoraya mozhet poyavit'sya tol'ko v rezul'tate dosko-
nal'nogo ponimaniya sistemy na razlichnyh urovnyah abstrakcii (sm.
$$11.3.3, $$12.2.1 i $$13.3). Blizorukij vzglyad na programmu ili na
process razrabotki programmnogo obespecheniya chasto privodit k defektnoj
sisteme. Otmetim, chto kak programmy, tak i programmistov razdelit'
prosto. Trudnee dostignut' effektivnogo vzaimodejstviya mezhdu uchastnikami
po obe storony granicy, ne narushaya ee i ne delaya vzaimodejstvie slishkom
zhestkim.
Zdes' predlozhen opredelennyj podhod k proektirovaniyu, a ne polnoe
formal'noe opisanie metoda proektirovaniya. Takoe opisanie vyhodit za
predmetnuyu oblast' knigi. Podhod, predlozhennyj zdes', mozhno primenyat'
s razlichnoj stepen'yu formalizacii, i on mozhet sluzhit' bazoj dlya razlichnyh
formal'nyh specifikacij. V tozhe vremya nel'zya schitat' etu glavu
referatom, i zdes' ne delaetsya popytka rassmotret' kazhduyu temu,
otnosyashchuyusya k processu razrabotki programm ili izlozhit' kazhduyu tochku
zreniya. |to tozhe vyhodit za predmetnuyu oblast' knigi. Referat po etoj
tematike mozhno najti v [2]. V etoj knige ispol'zuetsya dostatochno
obshchaya i tradicionnaya terminologiya. Samye "interesnye" terminy, kak:
proektirovanie, prototip, programmist - imeyut v literature neskol'ko
opredelenij, chasto protivorechashchih drug drugu, poetomu predosteregaem vas
ot togo, chtoby, ishodya iz prinyatyh v vashem okruzhenii opredelenij terminov,
vy ne vynesli iz knigi to, na chto avtor sovershenno ne rasschityval.
Cel' programmirovaniya - sozdat' produkt, udovletvoryayushchij pol'zovatelya.
Vazhnejshim sredstvom dlya dostizhenii etoj celi yavlyaetsya sozdanie
programmy s yasnoj vnutrennej strukturoj i vospitanie kollektiva
programmistov i razrabotchikov, imeyushchih dostatochnyj opyt i motivaciyu,
chtoby bystro i effektivno reagirovat' na vse izmeneniya.
Pochemu eto tak? Ved' vnutrennya struktura programmy i process, s
pomoshch'yu kotorogo ona poluchena, v ideale nikak ne kasayutsya konechnogo
pol'zovatelya. Bolee togo, esli konechnyj pol'zovatel' pochemu-to
interesuetsya tem, kak napisana programma, to chto-to s etoj programmoj
ne tak. Pochemu, nesmotrya na eto, tak vazhny struktura programmy i lyudi,
ee sozdavshie? V konce koncov konechnyj pol'zovatel' nichego ob etom
ne dolzhen znat'.
YAsnaya vnutrennyaya struktura programmy oblegchaet:
- testirovanie,
- perenosimost',
- soprovozhdenie,
- rasshirenie,
- reorganizaciyu i
- ponimanie.
Glavnoe zdes' v tom, chto lyubaya udachnaya bol'shaya programma imeet
dolguyu zhizn', v techenie kotoroj nad nej rabotayut
pokoleniya programmistov i razrabotchikov, ona perenositsya na novuyu
mashinu, prisposablivaetsya k nepredusmotrennym trebovaniyam i neskol'ko
raz perestraivaetsya. Vo vse vremya zhizni neobhodimo v priemlemoe vremya i
s dopustimym chislom oshibok vydavat' versii programmy. Ne planirovat' vse
eto - vse ravno, chto zaplanirovat' neudachu.
Otmetim, chto, hotya v ideal'nom sluchae sluchae pol'zovateli ne
dolzhny znat' vnutrennyuyu strukturu sistemy, na praktike oni obychno
hotyat ee znat'. Naprimer, pol'zovatel' mozhet zhelat' poznakomit'sya v
detalyah s razrabotkoj sistemy s cel'yu nauchit'sya kontrolirovat'
vozmozhnosti i nadezhnost' sistemy na sluchaj peredelok i rasshirenij.
Esli rassmatrivaemyj programmnyj produkt est' ne polnaya sistema, a nabor
bibliotek dlya polucheniya programmnyh sistem, to pol'zovatel' zahochet
uznat' pobol'she "detalej", chtoby oni sluzhili istochnikom idej i
pomogali luchshe ispol'zovat' biblioteku.
Nuzhno umet' ochen' tochno opredelit' ob容m proektirovaniya programmy.
Nedostatochnyj ob容m privodit k beskonechnomu srezaniyu ostryh uglov
("pobystree peredadim sistemu, a oshibku ustranim v sleduyushchej versii").
Izbytochnyj ob容m privodit k uslozhnennomu opisaniyu sistemy, v kotorom
sushchestvennoe teryaetsya v formal'nostyah, v rezul'tate chego pri
reorganizacii programmy poluchenie rabotayushchej versii zatyagivaetsya ("novaya
struktura namnogo luchshe staroj, pol'zovatel' soglasen zhdat' radi nee").
K tomu zhe voznikayut takie potrebnosti v resursah, kotorye nepozvolitel'ny
dlya bol'shinstva potencial'nyh pol'zovatelej. Vybor ob容ma
proektirovaniya - samyj trudnyj moment v razrabotke, imenno zdes'
proyavlyaetsya talant i opyt. Vybor trudno sdelat' i dlya odnogo programmista
ili razrabotchika, no on eshche trudnee dlya bol'shih zadach, gde zanyato
mnogo lyudej raznogo urovnya kvalifikacii.
Organizaciya dolzhna sozdavat' programmnyj produkt i soprovozhdat'
ego, nesmotrya na izmeneniya v shtate, v napravlenii raboty ili v
upravlyayushchej strukture. Rasprostranennyj sposob resheniya etih problem
zaklyuchalsya v popytke svedeniya processa sozdaniya sistemy k neskol'kim
otnositel'no prostym zadacham, ukladyvayushchimsya v zhestkuyu strukturu.
Naprimer, sozdat' gruppu legko obuchaemyh (deshevyh) i vzaimozamenyaemyh
programmistov nizkogo urovnya ("kodirovshchikov") i gruppu ne takih
deshevyh, no vzaimozamenyaemyh (a znachit takzhe ne unikal'nyh)
razrabotchikov. Schitaetsya, chto kodirovshchiki ne prinimayut reshenij po
proektirovaniyu, a razrabotchiki ne utruzhdayut sebya "gryaznymi"
podrobnostyami kodirovaniya. Obychno takoj podhod privodit k neudache, a
gde on srabatyvaet, poluchaetsya slishkom gromozdkaya sistema s plohimi
harakteristikami.
Nedostatki takogo podhoda sostoyat v sleduyushchem:
- slaboe vzaimodejstvie mezhdu programmistami i razrabotchikami
privodit k neeffektivnosti, promedleniyu, upushchennym vozmozhnostyam i
povtoreniyu oshibok iz-za plohogo ucheta i otsutstviya obmena opytom;
- suzhenie oblasti tvorchestva razrabotchikov privodit
k slabomu professional'nomu rostu, bezyniciativnosti, nebrezhnosti i
bol'shoj tekuchesti kadrov.
Po suti, podobnye sistemy - eto bespoleznaya trata redkih chelovecheskih
talantov. Sozdanie struktury, v ramkah kotoroj lyudi mogut najti
primenenie raznym talantam, ovladet' novym rodom deyatel'nosti i
uchastvovat' v tvorcheskoj rabote - eto ne tol'ko blagorodnoe delo, no
i praktichnoe, kommercheski vygodnoe predpriyatie.
S drugoj storony, nel'zya sozdat' sistemu, predstavit' dokumentaciyu
po nej i beskonechno ee soprovozhdat' bez nekotoroj zhestkoj organizacionnoj
struktury. Dlya chisto novatorskogo proekta horosho nachat' s togo, chto
prosto najti luchshih specialistov i pozvolit' im reshat' zadachu v
sootvetstvii s ih ideyami. No po mere razvitiya proekta trebuetsya vse
bol'she planirovaniya, specializacii i strogo opredelennogo vzaimodejstviya
mezhdu zanyatymi v nem lyud'mi. Pod strogo opredelennym ponimaetsya ne
matematicheskaya ili avtomaticheski verificiruemaya zapis' (hotya eto
bezuslovno horosho tam, gde vozmozhno i primenimo), a skoree nabor
ukazanij po zapisi, imenovaniyu, dokumentacii, testirovaniyu i t.p.
No i zdes' neobhodimo chuvstvo mery. Slishkom zhestkaya struktura mozhet
meshat' rostu i zatrudnyat' sovershenstvovanie. Zdes' podvergaetsya
proverke talant i opyt menedzhera. Dlya otdel'nogo rabotnika analogichnaya
problema svoditsya k opredeleniyu, gde nuzhno proyavit' smekalku, a gde
dejstvovat' po receptam.
Mozhno rekomendovat' planirovat' ne na period do vydachi sleduyushchej
versii sistemy, a na bolee dolgij srok. Stroit' plany tol'ko do
vypuska ocherednoj versii - znachit planirovat' neudachu. Nuzhno imet'
organizaciyu i strategiyu razvitiya programmnogo obespecheniya, kotorye
naceleny na sozdanie i podderzhanie mnogih versij raznyh sistem, t.e.
nuzhno mnogokratnoe planirovanie uspeha.
Cel' proektirovaniya v vyrabotke yasnoj i otnositel'no prostoj
vnutrennej struktury programmy, nazyvaemoj inogda arhitekturoj, inymi
slovami karkasa, v kotoryj ukladyvayutsya otdel'nye programmnye fragmenty,
i kotoryj pomogaet napisaniyu etih fragmentov.
Proekt - konechnyj rezul'tat processa proektirovaniya (esli tol'ko
byvaet konechnyj produkt u iterativnogo processa). On yavlyaetsya
sredotochiem vzaimodejstvij mezhdu razrabotchikom i programmistom i
mezhdu programmistami. Zdes' neobhodimo soblyusti chuvstvo mery. Esli ya,
kak otdel'nyj programmist, proektiruyu nebol'shuyu programmu, kotoruyu
sobirayus' napisat' zavtra, to tochnost' i polnota opisaniya proekta
mozhet svestis' k neskol'kim karakulyam na obratnoj storone konverta.
Na drugom polyuse nahoditsya sistema, nad kotoroj rabotayut sotni
programmistov i razrabotchikov, i zdes' mogut potrebovat'sya toma
tshchatel'no sostavlennyh specifikacij proekta na formal'nom ili
poluformal'nom yazyke. Opredelenie nuzhnoj stepeni tochnosti, detalizacii
i formal'nosti proektirovaniya yavlyaetsya uzhe samo po sebe netrivial'noj
tehnicheskoj i administrativnoj zadachej.
Dalee budet predpolagat'sya, chto proekt sistemy zapisyvaetsya
kak ryad opredelenij klassov (v kotoryh chastnye opisaniya opushcheny
kak lishnie detali) i vzaimootnoshenij mezhdu nimi. |to uproshchenie, t.k.
konkretnyj proekt mozhet uchityvat': voprosy parallel'nosti, ispol'zovanie
global'nogo prostranstva imen, ispol'zovanie global'nyh funkcij i
dannyh, postroenie programmy dlya minimizacii peretranslyacii,
ustojchivost', mnogomashinnyj rezhim i t.p. No pri obsuzhdenii na dannom
urovne detalizacii bez uproshcheniya ne obojtis', a klassy v kontekste S++
yavlyayutsya klyuchevym ponyatiem proektirovaniya. Nekotorye iz ukazannyh
voprosov budut obsuzhdat'sya nizhe, a te, kotorye pryamo zatragivayut
proektirovanie bibliotek S++, budut rassmotreny v glave 13. Bolee
podrobnoe obsuzhdenie i primery opredelennyh metodov ob容ktno-
orientirovannogo proektirovaniya soderzhatsya v [2].
My soznatel'no ne provodili chetkogo razdeleniya analiza i
proektirovaniya, poskol'ku obsuzhdenie ih razlichij vyhodit za ramki etoj
knigi, i ono zavisit ot primenyaemyh metodov proektirovaniya. Glavnoe v tom,
chtoby vybrat' metod analiza, podhodyashchij dlya metoda proektirovaniya, i
vybrat' metod proektirovaniya, podhodyashchij dlya stilya programmirovaniya
i ispol'zuemogo yazyka.
Process razvitiya programmnogo obespecheniya - eto iterativnyj i
rasshiryayushchijsya process. Po mere razvitiya kazhdaya stadiya povtoryaetsya
mnogokratno, i pri vsyakom vozvrate na nekotoruyu stadiyu processa utochnyaetsya
konechnyj produkt, poluchaemyj na etoj stadii. V obshchem sluchae process
ne imeet ni nachala, ni konca, poskol'ku, proektiruya i realizuya sistemu,
vy nachinaete, ispol'zuya kak bazu drugie proekty, biblioteki i prikladnye
sistemy, v konce raboty posle vas ostaetsya opisanie proekta i programma,
kotorye drugie mogut utochnyat', modificirovat', rasshiryat' i perenosit'.
Estestvenno konkretnyj proekt imeet opredelennoe nachalo i konec, i
vazhno (hotya chasto udivitel'no trudno) chetko i strogo ogranichit' vremya
i oblast' dejstviya proekta. No zayavlenie, chto vy nachinaete s "chistogo
lista", mozhet privesti k ser'eznym problemam dlya vas, takzhe kak i poziciya,
chto posle peredachi okonchatel'noj versii - hot' potop, vyzovet
ser'eznye problemy dlya vashih posledovatelej (ili dlya vas v novoj
roli).
Iz etogo vytekaet, chto sleduyushchie razdely mozhno chitat' v lyubom
poryadke, poskol'ku voprosy proektirovaniya i realizacii mogut v
real'nom proekte perepletat'sya pochti proizvol'no. Imenno, "proekt"
pochti vsegda podvergaetsya pereproektirovaniyu na osnove predydushchego
proekta, opredelennogo opyta realizacii, ogranichenij, nakladyvaemyh
srokami, masterstvom rabotnikov, voprosami sovmestimosti i t.p.
Zdes' osnovnaya trudnost' dlya menedzhera ili razrabotchika ili
programmista v tom, chtoby sozdat' takoj poryadok v etom processe,
kotoryj ne prepyatstvuet usovershenstvovaniyam i ne zapreshchaet povtornye
prohody, neobhodimye dlya uspeshnogo razvitiya.
U processa razvitiya tri stadii:
- Analiz: opredelenie oblasti zadachi.
- Proektirovanie: sozdanie obshchej struktury sistemy.
- Realizaciya: programmirovanie i testirovanie.
Ne zabud'te ob iterativnoj prirode etih processov (nesprosta stadii
ne byli pronumerovany), i zamet'te, chto nikakie vazhnye aspekty processa
razvitiya programmy ne vydelyayutsya v otdel'nye stadii, poskol'ku oni
dolzhny dopuskat':
- |ksperimentirovanie.
- Testirovanie.
- Analiz proektirovaniya i realizacii.
- Dokumentirovanie.
- Soprovozhdenie.
Soprovozhdenie programmnogo obespecheniya rassmatrivaetsya prosto kak
eshche neskol'ko prohodov po stadiyam processa razvitiya (sm. takzhe
$$11.3.6).
Ochen' vazhno, chtoby analiz, proektirovanie i realizaciya ne byli
slishkom otorvany drug ot druga, i chtoby lyudi, prinimayushchie v nih
uchastie, byli odnogo urovnya kvalifikacii dlya nalazhivaniya effektivnyh
kontaktov.
V bol'shih proektah slishkom chasto byvaet inache. V ideale, v processe
razvitiya proekta rabotniki dolzhny sami perehodit' s odnoj stadii na
druguyu: luchshij sposob peredachi tonkoj informacii - eto ispol'zovat'
golovu rabotnika. K sozhaleniyu, v organizaciyah chasto ustanavlivayut
bar'ery dlya takih perehodov, naprimer, u razrabotchika mozhet byt'
bolee vysokij status i (ili) bolee vysokij oklad, chem u "prostogo"
programmista. Ne prinyato, chtoby sotrudniki hodili po otdelam s cel'yu
nabrat'sya opyta i znanij, no pust', po krajnej mere, budut
regulyarnymi sobesedovaniya sotrudnikov, zanyatyh na raznyh stadiyah proekta.
Dlya srednih i malyh proektov obychno ne delayut razlichiya mezhdu
analizom i proektirovaniem - eti stadii slivayutsya v odnu. Dlya malyh
proektov takzhe ne razdelyayut proektirovanie i programmirovanie.
Konechno, tem samym reshaetsya problema vzaimodejstviya. Dlya dannogo
proekta vazhno najti podhodyashchuyu stepen' formalizacii i vyderzhat'
nuzhnuyu stepen' razdeleniya mezhdu stadiyami ($$11.4.2). Net edinstvenno
vernogo sposoba dlya etogo.
Privedennaya zdes' model' processa razvitiya programmnogo obespecheniya
radikal'no otlichaetsya ot tradicionnoj modeli "kaskad" (waterfall).
V poslednej process razvitiya protekaet linejno ot stadii analiza do
stadii testirovaniya. Osnovnoj nedostatok modeli kaskad tot, chto v nej
informaciya dvizhetsya tol'ko v odnom napravlenii. Esli vyyavlena
problema "nizhe po techeniyu", to voznikaet sil'noe metodologicheskoe
i organizacionnoe davlenie, chtoby reshit' problemu na dannom urovne,
ne zatragivaya predydushchih stadij processa. Otsutstvie povtornyh
prohodov privodit k defektnomu proektu, a v rezul'tate lokal'nogo
ustraneniya problem poluchaetsya iskazhennaya realizaciya. V teh
neizbezhnyh sluchayah, kogda informaciya dolzhna byt' peredana nazad k
istochniku ee polucheniya i vyzvat' izmeneniya v proekte, my poluchim
lish' slaboe "kolyhanie" na vseh urovnyah sistemy, stremyashchejsya podavit'
vnesennoe izmenenie, a znachit sistema ploho prisposoblena k
izmeneniyam. Argument v pol'zu "nikakih izmenenij" ili "tol'ko lokal'nye
izmeneniya" chasto svoditsya k tomu, chto odin otdel ne hochet
perekladyvat' bol'shuyu rabotu na drugoj otdel "radi ih zhe blaga".
CHasto byvaet tak, chto ko vremeni, kogda oshibka uzhe najdena, ispisano
stol'ko bumagi otnositel'no oshibochnogo resheniya, chto usiliya,
nuzhnye na ispravlenie dokumentacii, zatmevayut usiliya dlya ispravleniya
samoj programmy. Takim obrazom, bumazhnaya rabota mozhet stat' glavnoj
problemoj processa sozdaniya sistemy. Konechno, takie problemy mogut byt'
i voznikayut v processe razvitiya bol'shih sistem. V konce koncov,
opredelennaya rabota s bumagami neobhodima. No vybor linejnoj modeli
razvitiya (kaskad) mnogokratno uvelichivaet veroyatnost', chto eta
problema vyjdet iz-pod kontrolya.
Nedostatok modeli kaskad v otsutstvii povtornyh prohodov i
nesposobnosti reagirovat' na izmeneniya. Opasnost' predlagaemoj zdes'
iterativnoj modeli sostoit v iskushenii zamenit' razmyshlenie i
real'noe razvitie na posledovatel'nost' beskonechnyh izmenenij.
Tot i drugoj nedostatki legche ukazat', chem ustranit', i dlya togo,
kto organizuet rabotu, legko prinyat' prostuyu aktivnost' za real'nyj
progress.
Vy mozhete udelyat' pristal'noe vnimanie detalyam, ispol'zovat'
razumnye priemy upravleniya, razvituyu tehnologiyu, no nichto ne spaset
vas, esli net yasnogo ponimaniya togo, chto vy pytaetes' sozdat'. Bol'she
vsego proektov provalivalos' imenno iz-za otsutstviya horosho
sformulirovannyh realistichnyh celej, a ne po kakoj-libo inoj prichine.
CHto by vy ne delali i chem by ne zanimalis', nado yasno predstavlyat'
imeyushchiesya u vas sredstva, stavit' dostizhimye celi i orientiry i ne
iskat' tehnicheskih reshenij sociologicheskih problem. S drugoj storony,
nado primenyat' tol'ko adekvatnuyu tehnologiyu, dazhe esli ona potrebuet
zatrat,- lyudi rabotayut luchshe, imeya adekvatnye sredstva i priemlemuyu
sredu. Ne zabluzhdajtes', dumaya, chto legko vypolnit' eti rekomendacii.
Process razvitiya sistemy - eto iterativnaya deyatel'nost'. Osnovnoj
cikl svoditsya k povtoryaemym v sleduyushchej posledovatel'nosti shagam:
[1] Sozdat' obshchee opisanie proekta.
[2] Vydelit' standartnye komponenty.
[a] Podognat' komponenty pod dannyj proekt.
[3] Sozdat' novye standartnye komponenty.
[a] Podognat' komponenty pod dannyj proekt.
[4] Sostavit' utochnennoe opisanie proekta.
V kachestve primera rassmotrim avtomobil'nyj zavod. Proekt dolzhen
nachinat'sya s samogo obshchego opisaniya novoj mashiny. |tot pervyj shag
baziruetsya na nekotorom analize i opisanii mashiny v samyh obshchih
terminah, kotorye skoree otnosyatsya k predpolagaemomu ispol'zovaniyu,
chem k harakteristikam zhelaemyh vozmozhnostej mashiny. CHasto samoj
trudnoj chast'yu proekta byvaet vybor zhelaemyh vozmozhnostej, ili,
tochnee, opredelenie otnositel'no prostogo kriteriya vybora zhelaemyh
vozmozhnostej. Udacha zdes', kak pravilo, yavlyaetsya
rezul'tatom raboty otdel'nogo pronicatel'nogo cheloveka i chasto
nazyvaetsya predvideniem. Slishkom tipichno kak raz otsutstvie
yasnyh celej, chto privodit k neuverenno razvivayushchimsya ili prosto
provalivayushchimsya proektam.
Itak, dopustim neobhodimo sozdat' mashinu srednego razmera s
chetyr'mya dvercami i dostatochno moshchnym motorom. Ochevidno, chto
na pervom etape proekta ne sleduet nachinat' proektirovanie mashiny
(i vseh ee komponentov) s nulya. Hotya programmist ili razrabotchik
programmnogo obespecheniya v podobnyh obstoyatel'stvah postupit imenno
tak.
Na pervom etape nado vyyasnit', kakie komponenty dostupny na
vashem sobstvennom sklade i kakie mozhno poluchit' ot nadezhnyh
postavshchikov. Najdennye takim obrazom komponenty ne obyazatel'no
v tochnosti podojdut dlya novoj mashiny. Vsegda trebuetsya podgonka
komponentov. Mozhet byt' dazhe potrebuetsya izmenit' harakteristiki
"sleduyushchej versii" vybrannyh komponentov, chtoby sdelat' ih
prigodnymi dlya proekta. Naprimer, mozhet sushchestvovat' vpolne prigodnyj
motor, vyrabatyvayushchij nemnogo men'shuyu moshchnost'.Togda
ili vy, ili postavshchik motora dolzhny predlozhit', ne izmenyaya obshchego
opisaniya proekta, v kachestve kompensacii dopolnitel'nyj
zaryadnyj generator. Zametim, chto sdelat' eto,"ne izmenyaya obshchego opisaniya
proekta", maloveroyatno, esli tol'ko samo opisanie ne prisposobleno
k opredelennoj podgonke. Obychno podobnaya
podgonka trebuet kooperacii mezhdu vami i postavshchikom motorov.
Shodnye voprosy voznikayut i u programmista ili razrabotchika
programmnogo obespecheniya. Zdes' podgonku obychno oblegchaet effektivnoe
ispol'zovanie proizvodnyh klassov. No ne rasschityvajte provesti
proizvol'nye rasshireniya v proekte bez opredelennogo predvideniya
ili kooperacii s sozdatelem takih klassov.
Kogda ischerpaetsya nabor podhodyashchih standartnyh komponentov,
proektirovshchik mashiny ne speshit zanyat'sya proektirovaniem novyh
optimal'nyh komponentov dlya svoej mashiny. |to bylo by slishkom
rastochitel'no. Dopustim, chto ne nashlos' podhodyashchego bloka
kondicionirovaniya vozduha, zato est' svobodnoe prostranstvo, imeyushchee
formu bukvy L, v motornom otseke. Vozmozhno reshenie razrabotat'
blok kondicionirovaniya ukazannoj formy. No veroyatnost' togo, chto
blok podobnoj strannoj formy budet ispol'zovat'sya v mashinah drugogo
tipa (dazhe posle znachitel'noj podgonki), krajne nizka. |to oznachaet,
chto nash proektirovshchik mashiny ne smozhet razdelit' zatraty na
proizvodstvo takogo bloka s sozdatelyami mashin drugogo tipa, a znachit
vremya zhizni etogo bloka korotko. Poetomu stoit sproektirovat' blok,
kotoryj najdet bolee shirokoe primenenie, t.e. razrabotat'
razumnyj proekt bloka, bolee prisposoblennyj dlya podgonki, chem nashe
L-obraznoe chudishche. Vozmozhno, eto potrebuet bol'shih usilij, i dazhe
pridetsya dlya prisposobleniya bolee universal'nogo bloka izmenit'
obshchee opisanie proekta mashiny. Poskol'ku novyj blok razrabatyvalsya
dlya bolee obshchego primeneniya, chem nashe L-obraznoe chudishche,
predpolozhitel'no, dlya nego potrebuetsya nekotoraya podgonka, chtoby
polnost'yu udovletvorit' nashi peresmotrennye zaprosy.
Podobnaya zhe al'ternativa voznikaet i u programmista ili razrabotchika
programmnogo obespecheniya: vmesto togo, chtoby sozdat' programmu,
privyazannuyu k konkretnomu proektu, razrabotchik mozhet sproektirovat'
novuyu dostatochno universal'nuyu programmu, kotoraya budet imet'
horoshie shansy stat' standartnoj v opredelennoj oblasti.
Nakonec, kogda my proshlis' po vsem standartnym komponentam,
sostavlyaetsya "okonchatel'noe" obshchee opisanie proekta. Neskol'ko
special'no razrabotannyh sredstv ukazyvayutsya kak vozmozhnye. Veroyatno,
v sleduyushchem godu pridetsya dlya novoj modeli povtorit' nashi shagi,
i kak raz eti special'nye sredstva pridetsya peredelat' ili vybrosit'.
Kak ni pechal'no, no opyt tradicionno proektirovavshihsya programm
pokazyvaet, chto lish' neskol'ko chastej sistemy mozhno vydelit' v
otdel'nye komponenty i lish' neskol'ko iz nih prigodny vne
dannogo proekta.
My ne pytaemsya utverzhdat', chto vse razrabotchiki mashin
dejstvuyut stol' razumno, kak v privedennom primere, a razrabotchiki
programm sovershayut vse ukazannye oshibki. Utverzhdaetsya, chto ukazannaya
metodika razrabotki mashin primenima i dlya programmnogo obespecheniya.
Tak, v etoj i sleduyushchej glavah dany priemy ispol'zovaniya ee dlya S++.
Tem ne menee mozhno skazat', chto sama priroda programmirovaniya
sposobstvuet soversheniyu ukazannyh oshibok ($$12.2.1 i $$12.2.5).
V razdele 11.4.3 oprovergaetsya professional'noe predubezhdenie protiv
ispol'zovaniya opisannoj zdes' modeli proektirovaniya.
Zametim, chto model' razvitiya programmnogo obespecheniya horosho
primenima tol'ko v raschete na bol'shie sroki. Esli vash gorizont
suzhaetsya do vremeni vydachi ocherednoj versii, net smysla sozdavat'
i podderzhivat' funkcionirovanie standartnyh komponentov. |to
prosto privedet k izlishnim nakladnym rashodam. Nasha model'
rasschitana na organizacii so vremenem zhizni, za kotoroe prohodit
neskol'ko proektov, i s razmerami, kotorye pozvolyayut nesti
dopolnitel'nye rashody i na sredstva proektirovaniya, programmirovaniya,
i na soprovozhdenie proektov, i na povyshenie kvalifikacii razrabotchikov,
programmistov i menedzherov. Fakticheski eto eskiz nekotoroj fabriki po
proizvodstvu programm. Kak ni udivitel'no, ona tol'ko masshtabom
otlichaetsya ot dejstvij luchshih programmistov, kotorye dlya povysheniya svoej
proizvoditel'nosti v techenii let nakaplivali zapas priemov i metodov
proektirovaniya, sozdavali instrumenty i biblioteki. Pohozhe, chto
bol'shinstvo organizacij prosto ne umeet vospol'zovat'sya dostizheniyami
luchshih sotrudnikov, kak iz-za otsutstviya predvideniya, tak i po
nesposobnosti primenit' eti dostizheniya v dostatochno shirokom
ob容me.
Vse-taki nerazumno trebovat', chtoby "standartnye komponenty"
byli standartnymi universal'no. Sushchestvuet lish' maloe chislo
mezhdunarodnyh standartnyh bibliotek, a v svoem bol'shinstve komponenty
okazhutsya standartnymi tol'ko v predelah strany, otrasli, kompanii,
proizvodstvennoj cepochki, otdela ili oblasti prilozheniya i t.d.
Prosto mir slishkom velik, chtoby universal'nyj standart
vseh komponentov i sredstv byl real'noj ili zhelannoj cel'yu proekta.
11.3.2 Celi proektirovaniya
Kakovy samye obshchie celi proektirovaniya? Konechno, prostota, no v chem
kriterij prostoty? Poskol'ku my schitaem, chto proekt dolzhen razvivat'sya
vo vremeni, t.e. sistema budet rasshiryat'sya, perenosit'sya,
nastraivat'sya i, voobshche, izmenyat'sya massoj sposobov, kotorye nevozmozhno
predusmotret', neobhodimo stremit'sya k takoj sisteme proektirovaniya
i realizacii, kotoraya byla by prostoj s uchetom, chto ona budet
menyat'sya mnogimi sposobami. Na samom dele, praktichno dopustit',
chto sami trebovaniya k sisteme budut menyat'sya neodnokratno za period
ot nachal'nogo proekta do vydachi pervoj versii sistemy.
Vyvod takov: sistema dolzhna proektirovat'sya maksimal'no prostoj
pri uslovii, chto ona budet podvergat'sya serii izmenenij. My dolzhny
proektirovat' v raschete na izmeneniya, t.e. stremit'sya k
- gibkosti,
- rasshiryaemosti i
- perenosimosti
Luchshee reshenie - vydelit' chasti sistemy, kotorye veroyatnee vsego budut
menyat'sya, v samostoyatel'nye edinicy, i predostavit' programmistu ili
razrabotchiku gibkie vozmozhnosti dlya modifikacij takih edinic. |to
mozhno sdelat', esli vydelit' klyuchevye dlya dannoj zadachi ponyatiya
i predostavit' klass, otvechayushchij za vsyu informaciyu, svyazannuyu s
otdel'nym ponyatiem (i tol'ko s nim). Togda izmenenie budet zatragivat'
tol'ko opredelennyj klass. Estestvenno, takoj ideal'nyj sposob
gorazdo legche opisat', chem voplotit'.
Rassmotrim primer: v zadache modelirovaniya meteorologicheskih
ob容ktov nuzhno predstavit' dozhdevoe oblako. Kak eto sdelat'?
U nas net obshchego metoda izobrazheniya oblaka, poskol'ku ego vid zavisit
ot vnutrennego sostoyaniya oblaka, a ono mozhet byt' zadano tol'ko
samim oblakom.
Pervoe reshenie: pust' oblako izobrazhaet sebya samo. Ono podhodit
dlya mnogih ogranichennyh prilozhenij. No ono ne yavlyaetsya dostatochno
obshchim, poskol'ku sushchestvuet mnogo sposobov predstavleniya oblaka:
detal'naya kartina, nabrosok ochertanij, piktogramma, karta i t.p.
Drugimi slovami, vid oblaka opredelyaetsya kak im samim, tak i ego
okruzheniem.
Vtoroe reshenie zaklyuchaetsya v tom, chtoby predostavit' samomu oblaku
dlya ego izobrazheniya svedeniya o ego okruzhenii. Ono goditsya dlya
bol'shego chisla sluchaev. Odnako i eto ne obshchee reshenie. Esli my
predostavlyaem oblaku svedeniya ob ego okruzhenii, to narushaem osnovnoj
postulat, kotoryj trebuet, chtoby klass otvechal tol'ko za odno
ponyatie, i kazhdoe ponyatie voploshchalos' opredelennym klassom.
Mozhet okazat'sya nevozmozhnym predlozhit' soglasovannoe opredelenie
"okruzheniya oblaka", poskol'ku, voobshche govorya, kak vyglyadit oblako
zavisit ot samogo oblaka i nablyudatelya. CHem predstavlyaetsya oblako
mne, sil'no zavisit ot togo, kak ya smotryu na nego: nevooruzhennym
glazom, s pomoshch'yu polyarizacionnogo fil'tra, s pomoshch'yu meteoradara i t.d.
Pomimo nablyudatelya i oblaka sleduet uchityvat' i "obshchij fon", naprimer,
otnositel'noe polozhenie solnca. K dal'nejshemu uslozhneniyu kartiny
privodit dobavlenie novyh ob容ktov tipa drugih oblakov, samoletov.
CHtoby sdelat' zadachu razrabotchika prakticheski nerazreshimoj, mozhno
dobavit' vozmozhnost' odnovremennogo sushchestvovaniya neskol'kih
nablyudatelej.
Tret'e reshenie sostoit v tom, chtoby oblako, a takzhe i drugie
ob容kty, naprimer, samolety ili solnce, sami opisyvali sebya po
otnosheniyu k nablyudatelyu. Takoj podhod obladaet dostatochnoj
obshchnost'yu, chtoby udovletvorit' bol'shinstvo zaprosovX. Odnako,
on mozhet privesti k znachitel'nomu uslozhneniyu i bol'shim nakladnym
rashodam pri vypolnenii. Kak, naprimer, dobit'sya togo, chtoby
nablyudatel' ponimal opisaniya, proizvedennye oblakom ili drugimi
ob容ktami?
X Dazhe eta model' budet, po vsej vidimosti, ne dostatochnoj dlya takih
predel'nyh sluchaev, kak grafika s vysokoj stepen'yu razreshimosti.
YA dumayu, chto dlya polucheniya ochen' detal'noj kartiny nuzhen drugoj
uroven' abstrakcii.
Dozhdevye oblaka - eto ne tot ob容kt, kotoryj chasto vstretish'
v programmah, no ob容kty, uchastvuyushchie v razlichnyh operaciyah vvoda
i vyvoda, vstrechayutsya chasto. Poetomu mozhno schitat' primer s oblakom
prigodnym dlya programmirovaniya voobshche i dlya razrabotki bibliotek
v chastnosti. Logicheski shozhij primer v S++ predstavlyayut manipulyatory,
kotorye ispol'zuyutsya dlya formatirovaniya vyvoda v potokovom
vvode-vyvode ($$10.4.2). Zametim, chto tret'e reshenie ne est' "vernoe
reshenie", eto prosto bolee obshchee reshenie. Razrabotchik dolzhen
sbalansirovat' razlichnye trebovaniya sistemy, chtoby najti uroven'
obshchnosti i abstrakcii, prigodnyj dlya dannoj zadachi v dannoj oblasti.
Zolotoe pravilo: dlya programmy s dolgim srokom zhizni pravil'nym
budet samyj obshchij uroven' abstrakcii, kotoryj vam eshche ponyaten i
kotoryj vy mozhete sebe pozvolit', no ne obyazatel'no absolyutno
obshchij. Obobshchenie, vyhodyashchee za predely dannogo proekta i
ponyatiya lyudej, v nem uchastvuyushchih, mozhet prinesti vred, t.e.
privesti k zaderzhkam, nepriemlemym harakteristikam, neupravlyaemym
proektam i prosto k provalu.
CHtoby ispol'zovanie ukazannyh metodov bylo ekonomichno i
poddavalos' upravleniyu, proektirovanie i upravlenie dolzhno
uchityvat' povtornoe ispol'zovanie, o chem govoritsya v $$11.4.1 i
ne sleduet sovsem zabyvat' ob effektivnosti (sm. $$11.3.7).
11.3.3 SHagi proektirovaniya
Rassmotrim proektirovanie otdel'nogo klassa. Obychno eto ne luchshij
metod. Ponyatiya ne sushchestvuyut izolirovanno, naoborot, ponyatie
opredelyaetsya v svyazi s drugimi ponyatiyami. Analogichno i klass ne
sushchestvuet izolirovanno, a opredelyaetsya sovmestno s mnozhestvom
svyazannyh mezhdu soboj klassov. |to mnozhestvo chasto nazyvayut
bibliotekoj klassov ili komponentom. Inogda vse klassy komponenta
obrazuyut edinuyu ierarhiyu, inogda eto ne tak (sm. $$12.3).
Mnozhestvo klassov komponenta byvayut ob容dineny nekotorym logicheskim
usloviem, inogda eto - obshchij stil' programmirovaniya ili opisaniya,
inogda - predostavlyaemyj servis. Komponent yavlyaetsya edinicej
proektirovaniya, dokumentacii, prava sobstvennosti i,
chasto, povtornogo ispol'zovaniya.
|to ne oznachaet, chto esli vy ispol'zuete odin klass komponenta, to
dolzhny razbirat'sya vo vseh i umet' primenyat' vse klassy komponenta ili
dolzhny podgruzhat' k vashej programme moduli vseh klassov komponenta. V
tochnosti naoborot, obychno stremyatsya obespechit', chtoby ispol'zovanie
klassa velo k minimumu nakladnyh rashodov: kak mashinnyh resursov,
tak i chelovecheskih usilij. No dlya ispol'zovaniya lyubogo klassa
komponenta nuzhno ponimat' logicheskoe uslovie, kotoroe ego
opredelyaet (mozhno nadeyat'sya, chto ono predel'no yasno izlozheno v
dokumentacii), ponimat' soglasheniya i stil', primenennyj v processe
proektirovaniya i opisaniya komponenta, i dostupnyj servis (esli on
est').
Itak, perejdem k sposobam proektirovaniya komponenta. Poskol'ku
chasto eto neprostaya zadacha, imeet smysl razbit' ee na shagi i,
skoncentrirovavshis' na podzadachah, dat' polnoe i posledovatel'noe
opisanie. Obychno net edinstvenno pravil'nogo sposoba razbieniya.
Tem ne menee, nizhe privoditsya opisanie posledovatel'nosti shagov,
kotoraya prigodilas' v neskol'kih sluchayah:
[1] Opredelit' ponyatie / klass i ustanovit' osnovnye svyazi
mezhdu nimi.
[2] Utochnit' opredeleniya klassov, ukazav nabor operacij dlya
kazhdogo.
[a] Provesti klassifikaciyu operacij. V chastnosti utochnit'
neobhodimost' postroeniya, kopirovaniya i unichtozheniya.
[b] Ubedit'sya v minimal'nosti, polnote i udobstve.
[3] Utochnit' opredeleniya klassov, ukazav ih zavisimost' ot
drugih klassov.
[a] Nasledovanie.
[b] Ispol'zovanie zavisimostej.
[4] Opredelit' interfejsy klassov.
[a] Podelit' funkcii na obshchie i zashchishchennye.
[b] Opredelit' tochnyj tip operacij klassa.
Otmetim, chto eto shagi iterativnogo processa. Obychno dlya polucheniya
proekta, kotoryj mozhno uverenno ispol'zovat' dlya pervichnoj realizacii
ili povtornoj realizacii, nuzhno neskol'ko raz prodelat'
posledovatel'nost' shagov. Odnim iz preimushchestv glubokogo analiza i
predlozhennoj zdes' abstrakcii dannyh okazyvaetsya otnositel'naya
legkost', s kotoroj mozhno perestroit' vzaimootnosheniya klassov
dazhe posle programmirovaniya kazhdogo klassa. Hotya eto nikogda ne
byvaet prosto.
Dalee sleduet pristupit' k realizacii klassov, a zatem
vernut'sya, chtoby ocenit' proekt, ishodya iz opyta realizacii.
Rassmotrim eti shagi v otdel'nosti.
11.3.3.1 SHag 1: opredelenie klassov
Opredelite ponyatiya/klassy i ustanovite osnovnye svyazi mezhdu nimi.
Glavnoe v horoshem proekte - pryamo otrazit' kakoe-libo ponyatie
"real'nosti", t.e. ulovit' ponyatie iz oblasti prilozheniya klassov,
predstavit' vzaimosvyaz' mezhdu klassami strogo opredelennym sposobom,
naprimer, s pomoshch'yu nasledovaniya, i povtorit' eti dejstviya na
raznyh urovnyah abstrakcii. No kak my mozhem ulovit' eti ponyatiya?
Kak na praktike reshit', kakie nam nuzhny klassy?
Luchshe poiskat' otvet v samoj oblasti prilozheniya, chem ryt'sya
v programmistskom hranilishche abstrakcij i ponyatij. Obratites' k tomu,
kto stal ekspertom po rabote v nekogda sdelannoj sisteme, a takzhe
k tomu, kto stal kritikom sistemy, prishedshej ej na smenu. Zapomnite
vyrazheniya togo i drugogo.
CHasto govoryat, chto sushchestvitel'nye igrayut rol' klassov i ob容ktov,
ispol'zuemyh v programme, eto dejstvitel'no tak. No eto tol'ko nachalo.
Dalee, glagoly mogut predstavlyat' operacii nad ob容ktami ili
obychnye (global'nye) funkcii, vyrabatyvayushchie novye znacheniya, ishodya
iz svoih parametrov, ili dazhe klassy. V kachestve primera
mozhno rassmatrivat' funkcional'nye ob容kty, opisannye v $$10.4.2.
Takie glagoly, kak "povtorit'" ili "sovershit'" (commit) mogut byt'
predstavleny iterativnym ob容ktom ili ob容ktom, predstavlyayushchim
operaciyu vypolneniya programmy v bazah dannyh.
Dazhe prilagatel'nye mozhno uspeshno
predstavlyat' s pomoshch'yu klassov, naprimer, takie, kak "hranimyj",
"parallel'nyj", "registrovyj", "ogranichennyj". |to mogut byt' klassy,
kotorye pomogut razrabotchiku ili programmistu, zadav virtual'nye
bazovye klassy, specificirovat' i vybrat' nuzhnye svojstva dlya
klassov, proektiruemyh pozdnee.
Luchshee sredstvo dlya poiska etih ponyatij / klassov - grifel'naya
doska, a luchshij metod pervogo utochneniya - eto beseda so specialistami
v oblasti prilozheniya ili prosto s druz'yami. Obsuzhdenie neobhodimo,
chtoby sozdat' nachal'nyj zhiznesposobnyj slovar' terminov i ponyatijnuyu
strukturu. Malo kto mozhet sdelat' eto v odinochku. Obratites' k [1],
chtoby uznat' o metodah podobnyh utochnenij.
Ne vse klassy sootvetstvuyut ponyatiyam iz oblasti prilozheniya.
Nekotorye mogut predstavlyat' resursy sistemy ili abstrakcii
perioda realizacii (sm. $$12.2.1).
Vzaimootnosheniya, o kotoryh my govorim, estestvenno ustanavlivayutsya
v oblasti prilozheniya ili (v sluchae povtornyh prohodov po shagam
proektirovaniya) voznikayut iz posleduyushchej raboty nad strukturoj klassov.
Oni otrazhayut nashe ponimanie osnov oblasti prilozheniya. CHasto oni
yavlyayutsya klassifikaciej osnovnyh ponyatij. Primer takogo otnosheniya:
mashina s vydvizhnoj lestnicej est' gruzovik, est' pozharnaya mashina,
est' dvizhushcheesya sredstvo.
V $$11.3.3.2 i $$11.3.3.5 predlagaetsya nekotoraya tochka zreniya na
klassy i ierarhiyu klassov, esli neobhodimo uluchshit' ih strukturu.
11.3.3.2 SHag 2: opredelenie nabora operacij
Utochnite opredeleniya klassov, ukazav nabor operacij dlya kazhdogo.
V dejstvitel'nosti nel'zya razdelit' processy opredeleniya klassov i
vyyasneniya togo, kakie operacii dlya nih nuzhny. Odnako, na praktike
oni razlichayutsya, poskol'ku pri opredelenii klassov vnimanie
koncentriruetsya na osnovnyh ponyatiyah, ne ostanavlivayas'
na programmistskih voprosah ih realizacii, togda kak pri opredelenii
operacij prezhde vsego sosredotachivaetsya na tom, chtoby zadat' polnyj i
udobnyj nabor operacij. CHasto byvaet slishkom trudno sovmestit' oba
podhoda, v osobennosti, uchityvaya, chto svyazannye klassy nado
proektirovat' odnovremenno.
Vozmozhno neskol'ko podhodov k processu opredeleniya nabora operacij.
Predlagaem sleduyushchuyu strategiyu:
[1] Rassmotrite, kakim obrazom ob容kt klassa budet sozdavat'sya,
kopirovat'sya (esli nuzhno) i unichtozhat'sya.
[2] Opredelite minimal'nyj nabor operacij, kotoryj neobhodim
dlya ponyatiya, predstavlennogo klassom.
[3] Rassmotrite operacii, kotorye mogut byt' dobavleny dlya udobstva
zapisi, i vklyuchite tol'ko neskol'ko dejstvitel'no vazhnyh.
[4] Rassmotrite, kakie operacii mozhno schitat' trivial'nymi, t.e.
takimi, dlya kotoryh klass vystupaet v roli interfejsa dlya
realizacii proizvodnogo klassa.
[5] Rassmotrite, kakoj obshchnosti imenovaniya i funkcional'nosti
mozhno dostignut' dlya vseh klassov komponenta.
Ochevidno, chto eto - strategiya minimalizma. Gorazdo proshche dobavlyat'
lyubuyu funkciyu, prinosyashchuyu oshchutimuyu pol'zu, i sdelat' vse operacii
virtual'nymi. No, chem bol'she funkcij, tem bol'she veroyatnost', chto
oni ne budut ispol'zovat'sya, nalozhat opredelennye ogranicheniya na
realizaciyu i zatrudnyat evolyuciyu sistemy. Tak, funkcii, kotorye
mogut neposredstvenno chitat' i pisat' v peremennuyu sostoyaniya ob容kta
iz klassa, vynuzhdayut ispol'zovat' edinstvennyj sposob realizacii i
znachitel'no sokrashchayut vozmozhnosti pereproektirovaniya. Takie funkcii
snizhayut uroven' abstrakcii ot ponyatiya do ego konkretnoj realizacii.
K tomu zhe dobavlenie funkcij dobavlyaet raboty programmistu i
dazhe razrabotchiku, kogda on vernetsya k proektirovaniyu. Gorazdo
legche vklyuchit' v interfejs eshche odnu funkciyu, kak tol'ko
ustanovlena potrebnost' v nej, chem udalit' ee ottuda, kogda uzhe
ona stala privychnoj.
Prichina, po kotoroj my trebuem yavnogo prinyatiya resheniya o
virtual'nosti dannoj funkcii, ne ostavlyaya ego na stadiyu realizacii,
v tom, chto, ob座aviv funkciyu virtual'noj, my sushchestvenno povliyaem
na ispol'zovanie ee klassa i na vzaimootnosheniya etogo klassa s
drugimi. Ob容kty iz klassa, imeyushchego hotya by odnu virtual'nuyu
funkciyu, trebuyut netrivial'nogo raspredeleniya pamyati, esli sravnit'
ih s ob容ktami iz takih yazykov kak S ili Fortran. Klass s hotya by
odnoj virtual'noj funkciej po suti vystupaet v roli interfejsa
po otnosheniyu k klassam, kotorye "eshche mogut byt' opredeleny", a
virtual'naya funkciya predpolagaet zavisimost' ot klassov, kotorye
"eshche mogu byt' opredeleny" (sm. $$12.2.3)
Otmetim, chto strategiya minimalizma trebuet, pozhaluj, bol'shih
usilij so storony razrabotchika.
Pri opredelenii nabora operacij bol'she vnimaniya sleduet udelyat'
tomu, chto nado sdelat', a ne tomu, kak eto delat'.
Inogda polezno klassificirovat' operacii klassa po tomu,
kak oni rabotayut s vnutrennim sostoyaniem ob容ktov:
- Bazovye operacii: konstruktory, destruktory, operacii kopirovaniya.
- Selektory: operacii, ne izmenyayushchie sostoyaniya ob容kta.
- Modifikatory: operacii, izmenyayushchie sostoyanie ob容kta.
- Operacii preobrazovanij, t.e. operacii porozhdayushchie ob容kt
drugogo tipa, ishodya iz znacheniya (sostoyaniya) ob容kta, k kotoromu
oni primenyayutsya.
- Povtoriteli: operacii, kotorye otkryvayut dostup k ob容ktam klassa
ili ispol'zuyut posledovatel'nost' ob容ktov.
|to ne est' razbienie na ortogonal'nye gruppy operacij. Naprimer,
povtoritel' mozhet byt' sproektirovan kak selektor ili modifikator.
Vydelenie etih grupp prosto prednaznacheno pomoch' v processe
proektirovaniya interfejsa klassa. Konechno, dopustima i drugaya
klassifikaciya. Provedenie takoj klassifikacii osobenno polezno dlya
podderzhaniya neprotivorechivosti mezhdu klassami v ramkah odnogo
komponenta.
V yazyke S++ est' konstrukciya, pomogayushchaya zadaniyu selektorov i
modifikatorov v vide funkcii-chlena so specifikaciej const i bez nee.
Krome togo, est' sredstva, pozvolyayushchie yavno zadat' konstruktory,
destruktory i funkcii preobrazovaniya. Operaciya kopirovaniya realizuetsya
s pomoshch'yu operacij prisvaivaniya i konstruktorov kopirovaniya.
11.3.3.3 SHag 3: ukazanie zavisimostej
Utochnite opredelenie klassov, ukazav ih zavisimosti ot drugih klassov.
Razlichnye vidy zavisimostej obsuzhdayutsya v $$12.2. Osnovnymi po
otnosheniyu k proektirovaniyu sleduet schitat' otnosheniya nasledovaniya
i ispol'zovaniya. Oba predpolagayut ponimanie togo, chto znachit dlya
klassa otvechat' za opredelennoe svojstvo sistemy. Otvechat' za chto-libo
ne oznachaet, chto klass dolzhen soderzhat' v sebe vsyu informaciyu, ili,
chto ego funkcii-chleny dolzhny sami provodit' vse neobhodimye operacii.
Kak raz naoborot, kazhdyj klass, imeyushchij opredelennyj uroven'
otvetstvennosti, organizuet rabotu, pereporuchaya ee v vide
podzadach drugim klassam, kotorye imeyut men'shij uroven' otvetstvennosti.
No nado predosterech', chto zloupotreblenie etim priemom privodit
k neeffektivnym i ploho ponimaemym proektam, poskol'ku
proishodit razmnozhenie klassov i ob容ktov do takoj stepeni, chto
vmesto real'noj raboty proizvoditsya tol'ko seriya zaprosov na
ee vypolnenie. To, chto mozhno sdelat' v dannom meste, sleduet
sdelat'.
Neobhodimost' uchest' otnosheniya nasledovaniya i ispol'zovaniya
na etape proektirovaniya (a ne tol'ko v processe realizacii) pryamo
vytekaet iz togo, chto klassy predstavlyayut opredelennye ponyatiya.
Otsyuda takzhe sleduet, chto imenno komponent (t.e. mnozhestvo
svyazannyh klassov), a ne otdel'nyj klass, yavlyayutsya edinicej
proektirovaniya.
11.3.3.4 SHag 4: opredelenie interfejsov
Opredelite interfejsy klassov. Na etoj stadii proektirovaniya ne nuzhno
rassmatrivat' privatnye funkcii. Voprosy realizacii, voznikayushchie na
stadii proektirovaniya, luchshe vsego obsuzhdat' na shage 3 pri
rassmotrenii razlichnyh zavisimostej. Bolee togo, sushchestvuet
zolotoe pravilo: esli klass ne dopuskaet po krajnej mere dvuh
sushchestvenno otlichayushchihsya realizacij, to chto-to yavno ne v poryadke s etim
klassom, eto prosto zamaskirovannaya realizaciya, a ne predstavlenie
abstraktnogo ponyatiya. Vo mnogih sluchayah dlya otveta na vopros:
"Dostatochno li interfejs klassa nezavisim ot realizacii?"- nado
ukazat', vozmozhna li dlya klassa shema lenivyh vychislenij.
Otmetim, chto obshchie bazovye klassy i druz'ya (friend) yavlyayutsya
chast'yu obshchego interfejsa klassa (sm. $$5.4.1 i $$12.4). Poleznym
uprazhneniem mozhet byt' opredelenie razdel'nogo interfejsa dlya
klassov-naslednikov i vseh ostal'nyh klassov s pomoshch'yu razbieniya
interfejsa na obshchuyu i zakrytye chasti.
Imenno na etom shage sleduet produmat' i opisat' tochnye opredeleniya
tipov argumentov. V ideale zhelatel'no imet' maksimal'noe chislo
interfejsov so staticheskimi tipami, otnosyashchimisya k oblasti prilozheniya
(sm. $$12.1.3 i $$12.4).
Pri opredelenii interfejsov sleduet obratit' vnimanie na te
klassy, gde nabor operacij predstavlen bolee, chem na odnom urovne
abstrakcii. Naprimer, v klasse file u nekotoryh funkcij-chlenov
argumenty imeyut tip file_descriptor (deskriptor_fajla), a u drugih
argumenty - stroka simvolov, kotoraya oboznachaet imya fajla.
Operacii s file_descriptor rabotayut na drugom urovne (men'shem)
abstrakcii, chem operacii s imenem fajla, tak chto dazhe stranno,
chto oni otnosyatsya k odnomu klassu. Vozmozhno, bylo by luchshe imet'
dva klassa: odin predstavlyaet ponyatie deskriptora fajla, a
drugoj - ponyatie imeni fajla. Obychno vse operacii klassa dolzhny
predstavlyat' ponyatiya odnogo urovnya abstrakcii. Esli eto ne tak,
to stoit podumat' o reorganizacii i ego, i svyazannyh s nim klassov.
11.3.3.5 Perestrojka ierarhii klassov
SHagi 1 i 3 trebuyut issledovaniya klassov i ih ierarhii, chtoby
ubedit'sya, chto oni adekvatno otvechayut nashim trebovaniyam. Obychno
eto ne tak, i prihoditsya provodit' perestrojku dlya uluchsheniya
struktury, proekta ili realizacii.
Samaya tipichnaya perestrojka ierarhii klassov sostoit v vydelenii
obshchej chasti dvuh klassov v novyj klass ili v razbienii klassa na dva
novyh. V oboih sluchayah v rezul'tate poluchitsya tri klassa:
bazovyj klass i dva
proizvodnyh. Kogda sleduet provodit' takuyu perestrojku? Kakovy
obshchie pokazaniya, chto takaya perestrojka budet poleznoj?
K sozhaleniyu net prostogo i universal'nogo otveta na eti
voprosy. |to i ne udivitel'no, poskol'ku to, chto predlagaetsya,
ne yavlyaetsya meloch'yu pri realizacii, a izmenyaet osnovnye
ponyatiya sistemy. Vazhnoj i netrivial'noj zadachej yavlyaetsya poisk
obshchnosti sredi klassov i vydelenie obshchej chasti. Net tochnogo
opredeleniya obshchnosti, no sleduet obrashchat' vnimanie na obshchnost'
dlya ponyatij sistemy, a ne prosto dlya udobstva realizacii. Ukazaniyami,
chto dva klassa imeyut nechto obshchee, chto vozmozhno vydelit' v obshchij bazovyj
klass, sluzhat shozhie sposoby ispol'zovaniya, shodstvo naborov operacij,
shodstvo realizacij i prosto tot fakt, chto chasto v processe obsuzhdeniya
proekta oba klassa poyavlyayutsya odnovremenno. S drugoj
storony, esli est'
neskol'ko naborov operacij klassa s razlichnymi sposobami ispol'zovaniya,
esli eti nabory obespechivayut dostup k razdel'nym podmnozhestvam ob容ktov
realizacii, i, esli klass voznikaet v processe obsuzhdeniya nesvyazannyh
tem, to etot klass yavlyaetsya yavnym kandidatom dlya razbieniya na chasti.
V silu tesnoj svyazi mezhdu ponyatiyami i klassami problemy
perestrojki ierarhii klassov vysvechivayutsya na poverhnosti problem
imenovaniya klassov i ispol'zovaniya imen klassov v processe obsuzhdeniya
proekta. Esli imena klassov i ih uporyadochennost', zadavaemaya ierarhiej
klassov, kazhutsya neudobnymi pri obsuzhdenii proekta, znachit, po vsej
vidimosti, est' vozmozhnost' uluchsheniya ierarhii. Zametim, chto
podrazumevaetsya, chto analiz ierarhii klassov luchshe provodit' ne v
odinochku. Esli vy okazalis' v takom polozhenii, kogda ne s kem
obsudit' proekt, horoshim vyhodom budet popytat'sya sostavit' uchebnoe
opisanie sistemy, ispol'zuya imena klassov.
11.3.3.6 Ispol'zovanie modelej
Kogda pishesh' stat'yu, pytaesh'sya najti podhodyashchuyu dlya temy model'. Nuzhno
ne brosat'sya srazu pechatat' tekst, a poiskat' stat'i na shodnye temy,
vdrug najdetsya takaya, kotoraya mozhet posluzhit' otpravnoj tochkoj.
Esli eyu okazhetsya moya sobstvennaya stat'ya, to mozhno budet ispol'zovat'
dazhe kuski iz nee, izmenyaya po mere nadobnosti drugie chasti, i vvodit'
novuyu informaciyu tol'ko tam, gde trebuet logika predmeta. Takim
obrazom, ishodya iz pervogo izdaniya, napisana eta kniga. Predel'nyj
sluchaj takogo podhoda - eto napisanie otkrytki-formulyara, gde prosto
nuzhno ukazat' imya i, vozmozhno, dobavit' paru strok dlya pridaniya
"lichnogo" otnosheniya. Po suti takie otkrytki pishutsya s ukazaniem otlichiya
ot standarta.
Vo vseh vidah tvorcheskoj deyatel'nosti ispol'zovanie sushchestvuyushchih
sistem v kachestve modelej dlya novyh proektov yavlyaetsya skoree pravilom,
a ne isklyucheniem. Vsegda, kogda eto vozmozhno, proektirovanie i
programmirovanie dolzhny osnovyvat'sya na predydushchih rabotah. |to
sokrashchaet stepeni svobody dlya razrabotchika i pozvolyaet sosredotochit'
vnimanie na men'shem chisle voprosov v zadannoe vremya. Nachat' bol'shoj
proekt "prakticheski s nulya" - eto mozhet vozbuzhdat', no pravil'nee budet
upotrebit' termin "op'yanenie", kotoroe privedet k "p'yanomu
bluzhdaniyu" v mnozhestve variantov. Postroenie modeli ne nakladyvaet
kakih-libo ogranichenij i ne oznachaet pokornogo sledovaniya ej, eto
prosto osvobozhdaet razrabotchika ot nekotoryh voprosov.
Zametim, chto na samom dele ispol'zovanie modelej neizbezhno,
poskol'ku kazhdyj proekt sinteziruetsya iz opyta ego razrabotchikov.
Luchshe, kogda ispol'zovanie modeli yavlyaetsya yavno sformulirovannym
resheniem, togda vse dopushcheniya delayutsya yavno, opredelyaetsya obshchij
slovar' terminov, poyavlyaetsya nachal'nyj karkas proekta i uvelichivaetsya
veroyatnost' togo, chto u razrabotchikov est' obshchij podhod.
Estestvenno, chto vybor nachal'noj modeli yavlyaetsya vazhnym resheniem,
i obychno ono prinimaetsya tol'ko posle poiska potencial'nyh modelej
i tshchatel'noj ocenki variantov. Bolee togo, vo mnogih sluchayah model'
podhodit tol'ko pri uslovii ponimaniya togo, chto potrebuyutsya
znachitel'nye izmeneniya dlya voploshcheniya ee idej v inoj oblasti
prilozheniya. No proektirovanie programmnogo obespecheniya - tyazhelyj
trud, i nado ispol'zovat' lyubuyu pomoshch'. Ne sleduet otkazyvat'sya
ot ispol'zovaniya modelej iz-za neopravdannogo prenebrezheniya k
imitacii. Imitaciya - ne chto inoe, kak forma iskrennego voshishcheniya,
a, s uchetom prava sobstvennosti i avtorskogo prava, ispol'zovanie
modelej i predshestvuyushchih rabot v kachestve istochnika vdohnoveniya -
dopustimyj sposob dlya vseh novatorskih rabot vo vseh vidah
deyatel'nosti. To, chto bylo pozvoleno SHekspiru, podhodit i dlya nas.
Nekotorye oboznachayut ispol'zovanie modelej v processe proektirovaniya
kak "proektirovanie povtornogo ispol'zovaniya".
11.3.4 |ksperiment i analiz
V nachale chestolyubivogo proekta nam neizvesten luchshij sposob postroeniya
sistemy. CHasto byvaet tak, chto my dazhe ne znaem tochno, chto dolzhna
delat' sistema, poskol'ku konkretnye fakty proyasnyatsya tol'ko v processe
postroeniya, testirovaniya i ekspluatacii sistemy. Kak zadolgo do
sozdaniya zakonchennoj sistemy poluchit' svedeniya, neobhodimye dlya
ponimaniya togo, kakie resheniya pri proektirovanii okazhutsya
sushchestvennymi, i k kakim posledstviyam oni privedut?
Nuzhno provodit' eksperimenty. Konechno, nuzhen analiz proekta i ego
realizacii, kak tol'ko poyavlyaetsya pishcha dlya nego. Preimushchestvenno
obsuzhdenie vertitsya vokrug al'ternativ pri proektirovanii i
realizacii. Za isklyucheniem redkih sluchaev proektirovanie est'
social'naya aktivnost', kotoraya vedet po puti prezentacii i
obsuzhdenij. CHasto samym vazhnym sredstvom proektirovaniya okazyvaetsya
prostaya grifel'naya doska; bez nee idei proekta, nahodyashchiesya v
zarodyshe, ne mogut razvit'sya i stat' obshchim dostoyaniem v srede
razrabotchikov i programmistov.
Pohozhe, chto samyj populyarnyj sposob provedeniya eksperimenta svoditsya
k postroeniyu prototipa, t.e. umen'shennoj versii sistemy. Prototip ne
obyazan udovletvoryat' harakteristikam real'nyh sistem, obychno v
izobilii est' mashinnye resursy i programmnaya podderzhka, i v takih
usloviyah programmisty i razrabotchiki stanovyatsya neprivychno opytnymi,
horosho obrazovannymi i aktivnymi. Poyavlyaetsya cel' - sdelat'
rabotayushchij prototip kak mozhno skoree, chtoby nachat' issledovanie
variantov proekta i sposobov realizacii.
Takoj podhod, esli primenyat' ego razumno, mozhet privesti k
uspehu. No on takzhe mozhet sluzhit' opravdaniem neudachno sdelannyh
sistem. Delo v tom, chto udelyaya osoboe vnimanie prototipu, mozhno
prijti k smeshcheniyu usilij ot "issledovanie variantov
proekta" k "poluchenie kak mozhno skoree rabochej versii sistemy".
Togda bystro ugasnet interes k vnutrennej strukture prototipa
("ved' eto tol'ko prototip"), a rabota po proektirovaniyu budet
vytesnyat'sya manipulirovaniem s realizaciej prototipa. Proschet
zaklyuchaetsya v tom, chto takaya realizaciya mozhet legko privesti k sisteme,
kotoraya imeet vid "pochti zakonchennoj", a po suti yavlyaetsya pozhiratelem
resursov i koshmarom dlya teh, kto ee soprovozhdaet. V etom
sluchae na prototip tratyatsya vremya i energiya, kotorye luchshe priberech'
dlya real'noj sistemy. Dlya razrabotchikov i menedzherov est' iskushenie
peredelat' prototip v konechnyj programmnyj produkt, a "iskusstvo
nastrojki sistemy" otlozhit' do vypuska sleduyushchej versii. Esli idti
takim putem, to prototipy otricayut vse osnovy proektirovaniya.
Shodnaya problema voznikaet, esli issledovateli privyazyvayutsya
k tem sredstvam, kotorye oni sozdali pri postroenii prototipa,
i zabyvayut, chto oni mogut okazat'sya neprigodnymi dlya
rabochej sistemy, i chto svoboda ot ogranichenij i formal'nostej, k
kotoroj oni privykli, rabotaya v nebol'shoj gruppe, mozhet okazat'sya
nevozmozhnoj v bol'shom kollektive, b'yushchimsya nad ustraneniem dlinnoj
cepi prepyatstvij.
I v to zhe vremya sozdanie prototipov mozhet sygrat' vazhnuyu rol'.
Rassmotrim, naprimer, proektirovanie pol'zovatel'skogo interfejsa. Dlya
etoj zadachi vnutrennyaya struktura toj chasti sistemy, kotoraya pryamo ne
obshchaetsya s pol'zovatelem, obychno ne vazhna, i ispol'zovanie prototipov -
eto edinstvennyj sposob uznat',
kakova budet reakciya pol'zovatelya pri rabote s sistemoj.
Drugim primerom sluzhat prototipy, pryamo prednaznachennye dlya izucheniya
vnutrennej struktury sistemy. Zdes' uzhe interfejs s pol'zovatelem
mozhet byt' primitivnym, vozmozhna rabota s model'yu pol'zovatelej.
Ispol'zovanie prototipov - eto sposob eksperimentirovaniya.
Ozhidaemyj rezul'tat - eto bolee glubokoe ponimanie celej, a ne
sam prototip. Vozmozhno, sushchnost' prototipa zaklyuchaetsya v tom,
chto on yavlyaetsya nastol'ko nepolnym, chto mozhet sluzhit' lish' sredstvom
dlya eksperimenta, i ego nel'zya prevratit' v konechnyj produkt bez
bol'shih zatrat na pereproektirovanie i na druguyu realizaciyu. Ostavlyaya
prototip "nepolnym", my tem samym pereklyuchaem vnimanie na
eksperiment i umen'shaem opasnost' prevrashcheniya prototipa v zakonchennyj
produkt. |to takzhe pochti izbavlyaet ot iskusheniya vzyat' za osnovu
proekta sistemy proekt prototipa, pri etom zabyvaya ili ignoriruya te
ogranicheniya, kotorye vnutrenne prisushchi prototipu. Posle eksperimenta
prototip nado prosto vybrosit'.
Ne sleduet zabyvat' o drugih sposobah provedeniya eksperimenta,
kotorye mogut sluzhit' vo mnogih sluchayah al'ternativoj sozdaniyu prototipa,
i tam, gde oni primenimy, ih ispol'zovanie predpochtitel'nee, poskol'ku
oni obladayut bol'shej tochnost'yu i trebuyut men'shih zatrat
vremeni razrabotchika i resursov sistemy. Primerami mogut sluzhit'
matematicheskie modeli i razlichnye formy modelirovaniya. Po suti,
sushchestvuet beskonechnaya vozrastayushchaya posledovatel'nost',
nachinaya ot matematicheskih modelej,
ko vse bolee i bolee detal'nym sposobam modelirovaniya, zatem k
prototipam, k chastichnym realizaciyam sistemy, vplot' do polnoj sistemy.
|to podvodit k idee postroeniya sistemy, ishodya iz nachal'nogo
proekta i realizacii, i dvigayas' putem povtornogo prohozhdeniya
etapov proektirovaniya i realizacii. |to ideal'naya strategiya,
no ona pred座avlyaet vysokie trebovaniya k sredstvam proektirovaniya
i realizacii, i v nej soderzhitsya opredelennyj risk togo, chto
programmnyj ob容m, realizuyushchij resheniya, prinyatye
pri nachal'nom proektirovanii, v processe razvitiya vyrastet do takoj
velichiny, chto sushchestvennoe uluchshenie proekta budet prosto nevozmozhno.
Pohozhe, chto po krajnej mere teper' takuyu strategiyu primenyayut
ili v proektah ot malogo do srednego razmerov, t.e. tam, gde
maloveroyatny peredelki obshchego proekta, ili zhe dlya pereproektirovaniya
i inoj realizacii posle vydachi pervonachal'noj versii sistemy, gde
ukazannaya strategiya stanovitsya neizbezhnoj.
Pomimo eksperimentov, prednaznachennyh dlya ocenki reshenij,
prinimaemyh na etape proektirovaniya, istochnikom polucheniya poleznoj
informacii mozhet byt' analiz sobstvenno proektirovaniya i (ili)
realizacii. Naprimer, mozhet okazat'sya poleznym izuchenie razlichnyh
zavisimostej mezhdu klassami (sm.$$ 12.2), ne sleduet zabyvat' i o
takih tradicionnyh vspomogatel'nyh sredstvah realizacii, kak
graf vyzovov funkcij, ocenka proizvoditel'nosti i t.p.
Zametim, chto specifikaciya (rezul'tat analiza sistemy) i proekt
mogut soderzhat' oshibki, kak i realizaciya, i vozmozhno, oni dazhe
bol'she podverzheny oshibkam, t.k. yavlyayutsya menee tochnymi, ne mogut byt'
provereny na praktike i obychno ne okruzheny takimi razvitymi sredstvami,
kak te, chto sluzhat dlya analiza i proverki realizacii. Vvedenie
bol'shej formalizacii v yazyk ili zapis', s pomoshch'yu kotoroj izlozhen proekt,
v kakoj-to stepeni oblegchaet ispol'zovaniya etih sredstv dlya
proektirovaniya. No, kak skazano v $$12.1.1, eto nel'zya delat'
za schet uhudsheniya yazyka, ispol'zuemogo dlya realizacii. K tomu
zhe formal'naya zapis' mozhet sama stat' istochnikom trudnostej i
problem. |to proishodit, kogda vybrannaya stepen' formalizacii ploho
podhodit dlya konkretnyh zadach, kogda strogost' formalizacii prevoshodit
matematicheskuyu osnovu sistemy i kvalifikaciyu razrabotchikov i
programmistov, i kogda formal'noe opisanie sistemy nachinaet
rashodit'sya s real'noj sistemoj, dlya kotoroj ono prednaznachalos'.
Zaklyuchenie o neobhodimosti opyta i o tom, chto proektirovanie
neizbezhno soprovozhdaetsya oshibkami i ploho podderzhano programmnymi
sredstvami, sluzhit osnovnym dovodom v pol'zu iterativnoj modeli
proektirovaniya i realizacii. Al'ternativa - eto linejnaya model'
processa razvitiya, nachinaya s analiza i konchaya testirovaniem, no
ona sushchestvenno defektna, poskol'ku ne dopuskaet povtornyh
prohodov, ishodya iz opyta, poluchennogo na razlichnyh etapah razvitiya
sistemy.
Programma, kotoraya ne proshla testirovanie, ne rabotaet. Ideal, chtoby
posle proektirovaniya i (ili) verifikacii programma zarabotala s
pervogo raza, nedostizhim dlya vseh, za isklyucheniem samyh trivial'nyh
programm. Sleduet stremit'sya k idealu, no ne zabluzhdat'sya, chto
testirovanie prostoe delo.
"Kak provodit' testirovanie?" - na etot vopros nel'zya otvetit'
v obshchem sluchae. Odnako, vopros "Kogda nachinat' testirovanie?" imeet
takoj otvet - na samom rannem etape, gde eto vozmozhno. Strategiya
testirovaniya dolzhna byt' razrabotana kak chast' proekta i vklyuchena
v realizaciyu, ili, po krajnej mere, razrabatyvat'sya parallel'no
s nimi. Kak tol'ko poyavlyaetsya rabotayushchaya sistema, nado nachinat'
testirovanie. Otkladyvanie testirovaniya do "provedeniya polnoj
realizacii" - vernyj sposob vyjti iz grafika ili peredat' versiyu
s oshibkami.
Vsyudu, gde eto vozmozhno, proektirovanie dolzhno vestis' tak,
chtoby testirovat' sistemu bylo dostatochno prosto. V chastnosti,
imeet smysl sredstva testirovaniya pryamo vstraivat' v sistemu.
Inogda eto ne delaetsya iz-za boyazni slishkom ob容mnyh proverok na
stadii vypolneniya, ili iz-za opasenij, chto izbytochnost', neobhodimaya
dlya polnogo testirovaniya, izlishne uslozhnit struktury dannyh.
Obychno takie opaseniya neopravdany, poskol'ku sobstvenno programmy
proverki i dopolnitel'nye konstrukcii, neobhodimye dlya nih,
mozhno pri neobhodimosti udalit' iz sistemy pered ee postavkoj
pol'zovatelyu. Inogda mogut prigoditsya utverzhdeniya o svojstvah
programmy (sm. $$12.2.7).
Bolee vazhnoj, chem nabor testov, yavlyaetsya podhod, kogda
struktura sistemy takova, chto est' real'nye shansy ubedit' sebya
i pol'zovatelej, chto oshibki mozhno isklyuchit' s pomoshch'yu opredelennogo
nabora staticheskih proverok, staticheskogo analiza i testirovaniya.
Esli razrabotana strategiya postroeniya sistemy, ustojchivoj k oshibkam
(sm.$$9.8), strategiya testirovaniya obychno razrabatyvaetsya kak
vspomogatel'naya.
Esli voprosy testirovaniya polnost'yu ignoriruyutsya na etape
proektirovaniya, vozniknut problemy s testirovaniem, vremenem
postavki i soprovozhdeniem sistemy. Luchshe vsego nachat' rabotat'
nad strategiej testirovaniya s interfejsov klassov i ih
vzaimozavisimostej (kak predlagaetsya v $$12.2 i $$12.4).
Trudno opredelit' neobhodimyj ob容m testirovaniya. Odnako,
ochevidno, chto problemu predstavlyaet nedostatok testirovaniya,
a ne ego izbytok. Skol'ko imenno resursov v sravnenii s proektirovaniem
i realizaciej sleduet otvesti dlya testirovaniya zavisit ot
prirody sistemy i metodov ee postroeniya. Odnako, mozhno predlozhit'
sleduyushchee pravilo: otvodit' bol'she resursov vremeni i chelovecheskih
usilij na testirovanie sistemy, chem na polucheniya ee pervoj realizacii.
"Soprovozhdenie programmnogo obespecheniya" - neudachnyj termin. Slovo
"soprovozhdenie" predlagaet nevernuyu analogiyu s apparaturoj. Programmy
ne trebuyut smazki, ne imeyut dvizhushchihsya chastej, kotorye iznashivayutsya
tak, chto trebuyut zameny, u nih net treshchin, v kotorye popadaet
voda, vyzyvaya rzhavchinu. Programmy mozhno vosproizvodit' v tochnosti
i peredavat' v techenii minuty na dlinnye rasstoyaniya. Koroche,
programmy eto sovsem ne to, chto apparatura. (V originale:
"Software is not hardware").
Deyatel'nost', kotoraya oboznachaetsya, kak soprovozhdenie programm,
na samom dele, sostoit iz pereproektirovaniya i povtornoj realizacii,
a znachit vhodit v obychnyj cikl razvitiya programmnogo obespecheniya.
Esli v proekte uchteny voprosy rasshiryaemosti, gibkosti i perenosimosti,
to obychnye zadachi soprovozhdeniya reshayutsya estestvennym obrazom.
Podobno testirovaniyu zadachi soprovozhdeniya ne dolzhny reshat'sya
vne osnovnogo napravleniya razvitiya proekta i ih ne sleduet otkladyvat'
na potom.
D. Knutu prinadlezhit utverzhdenie "Neprodumannaya optimizaciya - koren'
vseh bed". Nekotorye slishkom horosho ubedilis' v spravedlivosti etogo
i schitayut vrednymi vse zaboty ob optimizacii. Na samom dele voprosy
effektivnosti nado vse vremya imet' v vidu vo vremya proektirovaniya i
realizacii. |to ne oznachaet, chto razrabotchik dolzhen zanimat'sya
zadachami lokal'noj optimizacii, tol'ko zadacha optimizacii na samom
global'nom urovne dolzhna ego volnovat'.
Luchshij sposob dobit'sya effektivnosti - eto sozdat' yasnyj i
prostoj proekt. Tol'ko takoj proekt mozhet ostat'sya otnositel'no
ustojchivym na ves' period razvitiya i posluzhit' osnovoj dlya
nastrojki sistemy s cel'yu povysheniya proizvoditel'nosti. Zdes'
vazhno izbezhat' "gargantyualizma", kotoryj yavlyaetsya proklyatiem
bol'shih proektov. Slishkom chasto lyudi dobavlyayut opredelennye
vozmozhnosti sistemy "na vsyakij sluchaj" (sm. $$11.3.3.2 i $$11.4.3),
udvaivaya, uchetveryaya razmer vypolnyaemoj programmy radi zavitushek.
Eshche huzhe to, chto takie uslozhnennye sistemy trudno poddayutsya
analizu, a po etomu trudno otlichit' izbytochnye nakladnye rashody
ot neobhodimyh i provesti analiz i optimizacii na obshchem urovne.
Optimizaciya dolzhna byt' rezul'tatom analiza i ocenki proizvoditel'nosti
sistemy, a ne proizvol'nym manipulirovaniem s programmnym kodom,
prichem eto osobenno spravedlivo dlya bol'shih sistem, gde intuiciya
razrabotchika ili programmista ne mozhet sluzhit' nadezhnym ukazatelem
v voprosah effektivnosti.
Vazhno izbegat' po suti neeffektivnyh konstrukcij, a tak zhe
takih konstrukcij, kotorye mozhno dovesti do priemlemogo urovnya
vypolneniya, tol'ko zatrativ massu vremeni i usilij. Po etoj zhe
prichine vazhno svesti k minimumu ispol'zovanie neperenosimyh po
svoej suti konstrukcij i sredstv, poskol'ku ih nalichie prepyatstvuet
rabote sistemy na drugih mashinah (menee moshchnyh, menee dorogih).
Esli tol'ko eto imeet kakoj-to smysl, bol'shinstvo lyudej delaet to,
chto ih pooshchryayut delat'. Tak, v kontekste programmnogo proekta, esli
menedzher pooshchryaet opredelennye sposoby dejstvij i nakazyvaet za
drugie, redkie programmisty ili razrabotchiki risknut svoim
polozheniem, vstrechaya soprotivleniya ili bezrazlichiya administracii,
chtoby delat' tak, kak oni polagayut nuzhnymX.
X Organizaciya, v kotoroj schitayut svoih programmistov nedoumkami,
ochen' skoro poluchit programmistov, kotorye budut rady i sposobny
dejstvovat' tol'ko kak nedoumki.
Otsyuda sleduet, chto menedzher dolzhen pooshchryat' takie struktury,
kotorye sootvetstvuyut sformulirovannym celyam proekta i realizacii.
Odnako na praktike slishkom chasto byvaet inache. Sushchestvennoe
izmenenie stilya programmirovaniya dostizhimo tol'ko pri sootvetstvuyushchem
izmenenii v stile proektirovaniya, krome togo, obychno i to i drugoe
trebuet izmeneniya v stile upravleniya. Myslitel'naya i organizacionnaya
inerciya slishkom prosto svodyat vse k lokal'nym izmeneniyam, hotya
tol'ko global'nye izmeneniya mogut prinesti uspeh. Prekrasnoj
illyustraciej sluzhit perehod na yazyk s ob容ktno-orientirovannym
programmirovaniem, naprimer na S++, kogda on ne vlechet za soboj
sootvetstvuyushchih izmenenij v metodah proektirovaniya, chtoby
vospol'zovat'sya novymi vozmozhnostyami yazyka (sm. $$12.1), i, naoborot,
kogda perehod na "ob容ktno-orientirovannoe proektirovanie" ne
soprovozhdaetsya perehod na yazyk realizacii, kotoryj podderzhivaet
etot stil'.
11.4.1 Povtornoe ispol'zovanie
CHasto osnovnoj prichinoj perehoda na novyj yazyk ili novyj metod
proektirovaniya nazyvayut to, chto eto oblegchaet povtornoe ispol'zovanie
programm ili proekta. Odnako, vo mnogih organizaciyah pooshchryayut
sotrudnika ili gruppu, kogda oni predpochitayut izobretat' koleso.
Naprimer, esli proizvoditel'nost' programmista izmeryaetsya chislom
strok programmy, to budet li on pisat' malen'kie programmy,
rabotayushchie so standartnymi bibliotekami, za schet svoego dohoda
i, mozhet byt', polozheniya? A menedzher, esli on oplachivaetsya
proporcional'no chislu lyudej v ego gruppe, budet li on ispol'zovat'
programmy, sdelannye drugimi kollektivami, esli on mozhet prosto
nanyat' eshche paru programmistov v svoyu gruppu? Kompaniya mozhet
poluchit' pravitel'stvennyj kontrakt, v kotorom ee dohod sostavlyaet
fiksirovannyj procent ot rashodov na proekt, budet li ona
sokrashchat' svoj dohod za schet ispol'zovaniya naibolee effektivnyh
sredstv? Trudno obespechit' voznagrazhdenie za povtornoe ispol'zovanie,
no esli administraciya ne najdet sposobov pooshchreniya i voznagrazhdeniya,
to ego prosto ne budet.
Povtornoe ispol'zovanie yavlyaetsya prezhde vsego social'nym
faktorom. Povtornoe ispol'zovanie programmy vozmozhno pri uslovii,
chto
[1] ona rabotaet; nel'zya ispol'zovat' povtorno, esli eto nevozmozhno
i v pervyj raz;
[2] ona ponyatna; zdes' imeet znachenie struktura programmy, nalichie
kommentariev, dokumentacii, rukovodstva;
[3] ona mozhet rabotat' vmeste s programmami, kotorye ne sozdavalis'
special'no s takim usloviem;
[4] mozhno rasschityvat' na ee soprovozhdenie (ili pridetsya delat'
eto samomu, chto obychno ne hochetsya);
[5] eto vygodno (hotya mozhno i razdelit' rashody po razrabotke
i soprovozhdeniyu s drugimi pol'zovatelyami) i, nakonec;
[6] ee mozhno najti.
K etomu mozhno eshche dobavit', chto komponent ne yavlyaetsya povtorno
ispol'zuemym, poka kto-to dejstvitel'no ne sdelal eto. Obychno zadacha
prisposobleniya komponenta k sushchestvuyushchemu okruzheniyu privodit k
utochneniyu nabora operacij, obobshcheniyu ego povedeniya, i povysheniyu ego
sposobnosti adaptacii k drugim programmam. Poka vse eto ne prodelano
hotya by odin raz, neozhidannye ostrye ugly nahodyatsya dazhe u
komponentov, kotorye tshchatel'no proektirovalis' i realizovyvalis'.
Lichnyj opyt podskazyvaet, chto usloviya dlya povtornogo
ispol'zovaniya voznikayut tol'ko v tom sluchae, kogda nahoditsya
konkretnyj chelovek, zanyatyj etim voprosom. V malen'kih gruppah
eto obychno byvaet tot, kto sluchajno ili zaplanirovanno okazyvaetsya
hranitelem obshchih bibliotek ili dokumentacii. V bol'shih organizaciyah
eto byvaet gruppa ili otdel, kotorye poluchayut privilegiyu sobirat',
dokumentirovat', populyarizirovat' i soprovozhdat' programmnoe
obespechenie, ispol'zuemoe razlichnymi gruppami.
Nel'zya nedoocenivat' takie gruppy "standartnyh komponentov".
Ukazhem, chto v pervom priblizhenii, sistema otrazhaet organizaciyu,
kotoraya ee sozdala. Esli v organizacii net sredstv pooshchreniya i
voznagrazhdeniya kooperacii i razdeleniya truda, to i na praktike
oni budut isklyucheniem. Gruppa standartnyh komponentov dolzhna
aktivno predlagat' svoi komponenty. Obychnaya tradicionnaya
dokumentaciya vazhna, no ee nedostatochno. Pomimo etogo ukazannaya
gruppa dolzhna predostavlyat' rukovodstva i druguyu informaciyu,
kotoraya pozvolit potencial'nomu pol'zovatelyu otyskat' komponent i
ponyat' kak on mozhet emu pomoch'. Znachit eta gruppa dolzhna predprinimat'
dejstviya, kotorye obychno svyazyvayutsya s sistemoj obrazovaniya i
marketinga. CHleny gruppy komponentov dolzhny vsegda, kogda eto
vozmozhno, rabotat' v tesnom sotrudnichestve s razrabotchikami iz
oblastej prilozheniya. Tol'ko togda oni budut v kurse zaprosov
pol'zovatelej i sumeyut pochuyat' vozmozhnosti ispol'zovaniya
standartnogo komponenta v razlichnyh oblastyah. |to yavlyaetsya
argumentom za ispol'zovanie takoj gruppy v roli konsul'tanta i v
pol'zu vnutrennih postavok programm, chtoby informaciya iz gruppy
komponentov mogla svobodno rasprostranyat'sya.
Zametim, chto ne vse programmy dolzhny byt' rasschitany na
povtornoe ispol'zovanie, inymi slovami, povtornoe ispol'zovanie ne
yavlyaetsya universal'nym svojstvom. Skazat', chto nekotoryj komponent
mozhet byt' povtorno ispol'zovan, oznachaet, chto v ramkah opredelennoj
struktury ego povtornoe ispol'zovanie ne potrebuet znachitel'nyh
usilij. No v bol'shinstve sluchaev perenos v druguyu strukturu mozhet
potrebovat' bol'shoj raboty. V etom smysle povtornoe ispol'zovanie
sil'no napominaet perenosimost'. Vazhno ponimat', chto povtornoe
ispol'zovanie yavlyaetsya rezul'tatom proektirovaniya, stavivshego
takuyu cel', modifikacii komponentov na osnove opyta i special'nyh
usilij, predprinyatyh dlya poiska sredi sushchestvuyushchih komponentov
kandidatov na povtornoe ispol'zovanie. Neosoznannoe ispol'zovanie
sredstv yazyka ili priemov programmirovaniya ne mozhet chudesnym
obrazom garantirovat' povtornoe ispol'zovanie. Takie sredstva yazyka
S++, kak klassy, virtual'nye funkcii i shablony tipa, sposobstvuyut
proektirovaniyu, oblegchayushchemu povtornoe ispol'zovanie (znachit delayut
ego bolee veroyatnym), no sami po sebe eti sredstva ne garantiruyut
povtornoe ispol'zovanie.
CHelovek i organizaciya sklonny izlishne radovat'sya tomu, chto oni
"dejstvuyut po pravil'noj metode". V institutskoj srede eto chasto
zvuchit kak "razvitie soglasno strogim predpisaniyam". V oboih sluchayah
zdravyj smysl stanovitsya pervoj zhertvoj strastnogo i chasto iskrennego
zhelaniya vnesti uluchsheniya. K neschast'yu, esli zdravogo smysla ne hvataet,
to ushcherb, nanesennyj nerazumnymi dejstviyami, mozhet byt' neogranichennym.
Vernemsya k etapam processa razvitiya, perechislennym v $$11.3, i
k shagam proektirovaniya, ukazannym v $$11.3.3. Otnositel'no prosto
pererabotat' eti etapy v tochnyj metod proektirovaniya, kogda shag tochno
opredelen, imeet horosho opredelennye vhodnye i vyhodnye dannye i
poluformal'nuyu zapis' dlya zadaniya vhodnyh i vyhodnyh dannyh. Mozhno
sostavit' protokol, kotoromu dolzhno podchinyat'sya proektirovanie,
sozdat' sredstva, predostavlyayushchie opredelennye udobstva dlya zapisi
i organizacii processa. Dalee, issleduya klassifikaciyu zavisimostej,
privedennuyu v $$12.2, mozhno postanovit', chto opredelennye zavisimosti
yavlyayutsya horoshimi, a drugie sleduet schitat' plohimi, i predostavit'
sredstva analiza, kotorye obespechat provedenie takih ocenok vo vseh
stadiyah proekta. CHtoby zavershit' takuyu "standartizaciyu" processa
sozdaniya programm, mozhno bylo by vvesti standarty na dokumentaciyu
(v tom chisle pravila na pravopisanie i grammatiku i soglasheniya o
formate dokumentacii), a tak zhe standarty na obshchij vid programm
(v tom chisle ukazaniya kakie sredstva yazyka sleduet ispol'zovat',
a kakie net, perechislenie dopustimyh bibliotek i teh, kotorye ne nuzhno
ispol'zovat', soglasheniya ob imenovanii funkcij, tipov, peremennyh,
pravila raspolozheniya teksta programmy i t.d.).
Vse eto mozhet sposobstvovat' uspehu proekta. Po krajnej mere,
bylo by yavnoj glupost'yu, brat'sya za proekt sistemy, kotoraya
predpolozhitel'no budet imet' poryadka desyati millionov strok teksta,
nad kotoroj budut rabotat' sotni chelovek, i kotoruyu budut
soprovozhdat' tysyachi chelovek v techenii desyatiletij, ne imeya dostatochno
horosho opredelennogo i strogogo plana po vsem perechislennym vyshe
poziciyam.
K schast'yu, bol'shinstvo sistem ne otnositsya k etoj kategorii.
Tem ne menee, esli resheno, chto dannyj metod proektirovaniya ili
sledovanie ukazannym obrazcam v programmirovanii i dokumentacii
yavlyayutsya "pravil'nymi", to nachinaet okazyvat'sya davlenie, chtoby
primenyat' ih povsemestno. V nebol'shih proektah eto privodit k
nelepym ogranicheniyam i bol'shim nakladnym rashodam. V chastnosti,
eto mozhet privesti k tomu, chto meroj razvitiya i uspeha stanovitsya
ne produktivnaya rabota, a peresylka bumazhek i zapolnenie razlichnyh
blankov. Esli eto sluchitsya, to v takom proekte nastoyashchih
programmistov i razrabotchikov vytesnyat byurokraty.
Kogda proishodit takoe nelepoe zloupotreblenie metodami
proektirovaniya (po vsej vidimosti sovershenno razumnymi), to neudacha
proekta stanovitsya opravdaniem otkaza ot prakticheski vsyakoj
formalizacii processa razrabotki programmnogo obespecheniya. |to,
v svoyu ochered', vedet k takoj putanice i takim provalam, kotorye
kak raz i dolzhen byl predotvratit' nadlezhashchij metod proektirovaniya.
Osnovnaya problema sostoit v opredelenii stepeni formalizacii,
prigodnoj dlya processa razvitiya konkretnogo proekta. Ne rasschityvajte
legko najti ee reshenie. Po suti dlya malogo proekta kazhdyj metod
mozhet srabotat'. Eshche huzhe to, chto pohozhe prakticheski kazhdyj metod,
dazhe esli on ploho produman i zhestok po otnosheniyu k ispolnitelyam,
mozhet srabotat' dlya bol'shogo proekta, esli vy gotovy zatratit'
ujmu vremeni i deneg.
V processe razvitiya programmnogo obespecheniya glavnaya zadacha -
sohranit' celostnost' proekta. Trudnost' etoj zadachi zavisit
nelinejno ot razmera proekta. Sformulirovat' i sohranit' osnovnye
ustanovki v bol'shom proekte mozhet tol'ko odin chelovek ili malen'kaya
gruppa. Bol'shinstvo lyudej tratit stol'ko vremeni na reshenie
podzadach, tehnicheskie detali, povsednevnuyu administrativnuyu rabotu,
chto obshchie celi proekta legko zabyvaet ili zamenyaet ih na bolee
lokal'nye i blizkie celi. Vernyj put' k neudache, kogda net cheloveka
ili gruppy s pryamym zadaniem sledit' za celostnost'yu proekta.
Vernyj put' k neudache, kogda u takogo cheloveka ili gruppy net
sredstv vozdejstvovat' na proekt v celom.
Otsutstvie soglasovannyh dal'nih celej namnogo bolee opasno
dlya proekta i organizacii, chem otsutstvie kakogo-libo odnogo
konkretnogo svojstva. Nebol'shaya gruppa lyudej dolzhna sformulirovat'
takie obshchie celi, postoyanno derzhat' ih v ume, sostavit' dokumenty,
soderzhashchie samoe obshchee opisanie proekta, sostavit' poyasneniya k
osnovnym ponyatiyam, i voobshche, pomogat' vsem ostal'nym pomnit' o
naznachenii proekta.
11.4.3 CHelovecheskij faktor
Opisannyj zdes' metod proektirovaniya rasschitan na iskusnyh
razrabotchikov i programmistov, poetomu ot ih podbora zavisit
uspeh organizacii.
Menedzhery chasto zabyvayut, chto organizaciya sostoit iz
individuumov. Rasprostraneno mnenie, chto programmisty ravny i
vzaimozamenyaemy. |to zabluzhdenie mozhet pogubit' organizaciyu za schet
vytesneniya mnogih samyh aktivnyh sotrudnikov i prinuzhdeniya
ostal'nyh rabotat' nad zadachami znachitel'no nizhe ih urovnya.
Individuumy vzaimozamenyaemy tol'ko, esli im ne dayut primenit'
svoj talant, kotoryj podnimaet ih nad obshchim minimal'nym urovnem,
neobhodimym dlya resheniya dannoj zadachi. Poetomu mif o vzaimozamenyaemosti
beschelovechen i po suti svoej rastochitelen.
Mnogie sistemy ocenok proizvoditel'nosti programmista pooshchryayut
rastochitel'nost' i ne mogut uchest' sushchestvennyj lichnyj vklad
cheloveka. Samym ochevidnym primerom sluzhit shiroko rasprostranennaya
praktika ocenivat' uspeh v kolichestve zaprogrammirovannyh strok,
vydannyh stranic dokumentacii, propushchennyh testov i t.p.
Takie cifry effektno vyglyadyat na diagrammah, no imeyut samoe
otdalennoe otnoshenie k dejstvitel'nosti. Naprimer, esli
proizvoditel'nost' izmeryat' chislom zaprogrammirovannyh strok, to
udachnoe povtornoe ispol'zovanie uhudshit ocenku truda programmista.
Obychno tot zhe effekt budet imet' udachnoe primenenie luchshih priemov
v processe pereproektirovaniya bol'shoj chasti sistemy.
Kachestvo rezul'tata izmerit' znachitel'no trudnee, chem kolichestvo,
i voznagrazhdat' ispolnitelya ili gruppu sleduet za kachestvo ih truda,
a ne na osnove grubyh kolichestvennyh ocenok. K sozhaleniyu, naskol'ko
izvestno, prakticheskaya razrabotka sposobov ocenki kachestva eshche ne
nachalas'. K tomu zhe ocenki, kotorye nepolno opisyvayut sostoyanie
proekta, mogut iskazit' process ego razvitiya. Lyudi prisposablivayutsya,
chtoby ulozhit'sya v otvedennyj srok i perestraivayut svoyu rabotu v
sootvetstvii s ocenkami proizvoditel'nosti, v rezul'tate stradaet
obshchaya celostnost' sistemy i ee proizvoditel'nost'. Naprimer, esli
otveden srok dlya vyyavleniya opredelennogo chisla oshibok, to dlya togo,
chtoby ulozhit'sya v nego, aktivno ispol'zuyut proverki na stadii
vypolneniya, chto uhudshaet proizvoditel'nost' sistemy. Obratno, esli
uchityvayutsya tol'ko harakteristiki sistemy na stadii vypolneniya, to
chislo nevyyavlennyh oshibok budet rasti pri uslovii nedostatka
vremeni u ispolnitelej. Otsutstvie horoshih i razumnyh ocenok
kachestva povyshaet trebovaniya k tehnicheskoj kvalifikacii menedzherov,
inache budet postoyannaya tendenciya pooshchryat' proizvol'nuyu aktivnost',
a ne real'nyj progress. Ne nado zabyvat', chto menedzhery tozhe lyudi,
i oni dolzhny po krajnej mere nastol'ko razbirat'sya v novyh
tehnologiyah, kak i te, kem oni upravlyayut.
Zdes', kak i v drugih aspektah processa razvitiya programmnogo
obespecheniya, sleduet rassmatrivat' bol'shie vremennye sroki. Po suti
nevozmozhno ukazat' proizvoditel'nost' cheloveka na osnove ego
raboty za god. Odnako, mnogie sotrudniki imeyut kartochku svoih
dostizhenij za bol'shoj period, i ona mozhet posluzhit' nadezhnym ukazaniem
dlya predskazaniya ih proizvoditel'nosti. Esli ne prinimat' vo vnimanie
takie kartochki, chto i delaetsya, kogda sotrudnikov schitayut
vzaimozamenyaemymi spicami v kolese organizacii, to u menedzhera
ostayutsya tol'ko vvodyashchie v zabluzhdeniya kolichestvennye ocenki.
Esli my rassmatrivaem tol'ko dostatochno bol'shie vremennye
sroki i otkazyvaemsya ot metodov upravleniya, rasschitannyh na
"vzaimozamenyaemyh nedoumkov", to nado priznat', chto individuumu
(kak razrabotchiku ili programmistu, tak i menedzheru) nuzhen bol'shoj
srok, chtoby dorasti do bolee interesnoj i vazhnoj raboty. Takoj
podhod ne odobryaet kak "skakanie" s mesta na mesto, tak i peredachu
raboty drugomu iz-za kar'ernyh soobrazhenij. Cel'yu dolzhen byt'
nizkij oborot klyuchevyh specialistov i klyuchevyh menedzherov. Nikakoj
menedzher ne dob'etsya uspeha bez podhodyashchih tehnicheskih znanij i
vzaimoponimaniya s osnovnymi razrabotchikami i programmistami.
V tozhe vremya, v konechnom schete nikakaya gruppa razrabotchikov ili
programmistov ne dob'etsya uspeha bez podderzhki kompetentnyh
menedzherov i bez ponimaniya hotya by osnovnyh netehnicheskih voprosov,
kasayushchihsya okruzheniya, v kotorom oni rabotayut.
Kogda trebuetsya predlozhit' nechto novoe, na perednij plan vyhodyat
osnovnye specialisty - analitiki, razrabotchiki, programmisty. Imenno
oni dolzhny reshit' trudnuyu i kriticheskuyu zadachu vnedreniya novoj
tehnologii. |to te lyudi, kotorye dolzhny ovladet' novymi metodami i
vo mnogih sluchayah zabyt' starye privychki. |to ne tak legko. Ved'
eti lyudi sdelali bol'shoj lichnyj vklad v sozdanie staryh metodov i
svoyu reputaciyu kak specialista obosnovyvayut uspehami, poluchennymi s
pomoshch'yu staryh metodov. Tak zhe obstoit delo i s mnogimi menedzherami.
Estestvenno u takih lyudej est' strah pered izmeneniyami. On mozhet
privesti k preuvelicheniyu problem, voznikayushchih pri izmeneniyah, i k
nezhelaniyu priznat' problemy, vyzvannye starymi metodami. Estestvenno,
s drugoj storony lyudi, vystupayushchie za izmeneniya, mogut pereocenivat'
vygody, kotorye prinesut izmeneniya, i nedoocenivat' voznikayushchie
zdes' problemy. |ti dve gruppy lyudej dolzhny obshchat'sya, oni dolzhny
nauchit'sya govorit' na odnom yazyke i dolzhny pomoch' drug drugu
razrabotat' podhodyashchuyu shemu perehoda. Al'ternativoj budet
organizacionnyj paralich i uhod samyh sposobnyh lyudej iz oboih grupp.
Tem i drugim sleduet znat', chto samye udachlivye iz "staryh vorchunov"
mogli byt' "molodymi l'vami" v proshlom godu, i esli cheloveku dali
vozmozhnost' nauchit'sya bez vsyakih izdevatel'stv, to on mozhet stat'
samym stojkim i razumnym storonnikom peremen. On budet obladat'
neocenimymi svojstvami zdorovogo skepticizma, znaniya pol'zovatelej
i ponimaniya organizacionnyh prepyatstvij. Storonniki nemedlennyh i
radikal'nyh izmenenij dolzhny osoznat', chto gorazdo chashche nuzhen
perehod, predpolagayushchij postepennoe vnedrenie novyh metodov.
S drugoj storony, te, kto ne zhelaet peremen, dolzhny poiskat' dlya
sebya takie oblasti, gde eto vozmozhno, chem vesti ozhestochennye,
ar'ergardnye boi v toj oblasti, gde novye trebovaniya uzhe zadali
sovershenno inye usloviya dlya uspeshnogo proekta.
V etoj glave my zatronuli mnogo tem, no kak pravilo ne davali
nastoyatel'nyh i konkretnyh rekomendacij po proektirovaniyu. |to
sootvetstvuet moemu ubezhdeniyu, chto net "edinstvenno vernogo resheniya".
Principy i priemy sleduet primenyat' tem sposobom, kotoryj luchshe
podhodit dlya konkretnyh zadach. Dlya etogo nuzhen vkus, opyt i razum.
Vse-taki mozhno ukazat' nekotoryj svod pravil, kotoryj razrabotchik
mozhet ispol'zovat' v kachestve orientirov, poka ne naberetsya dostatochno
opyta, chtoby vyrabotat' luchshie. Nizhe priveden svod takih pravil.
|ti pravila mozhno ispol'zovat' v kachestve otpravnoj tochki v
processe vyrabotki osnovnyh napravlenij dlya proekta ili organizacii
ili v kachestve proverochnogo spiska. Podcherknu eshche raz, chto oni ne
yavlyayutsya universal'nymi pravilami i ne mogut zamenit' razmyshleniya.
- Uznajte, chto vam predstoit sozdat'.
- Stav'te opredelennye i osyazaemye celi.
- Ne pytajtes' s pomoshch'yu tehnicheskih priemov reshit' social'nye
problemy.
- Rasschityvajte na bol'shoj srok
- v proektirovanii, i
- upravlenii lyud'mi.
- Ispol'zujte sushchestvuyushchie sistemy v kachestve modelej, istochnika
vdohnoveniya i otpravnoj tochki.
- Proektirujte v raschete na izmeneniya:
- gibkost',
- rasshiryaemost',
- perenosimost', i
- povtornoe ispol'zovanie.
- Dokumentirujte, predlagajte i podderzhivajte povtorno ispol'zuemye
komponenty.
- Pooshchryajte i voznagrazhdajte povtornoe ispol'zovanie
- proektov,
- bibliotek, i
- klassov.
- Sosredotoch'tes' na proektirovanii komponenty.
- Ispol'zujte klassy dlya predstavleniya ponyatij.
- Opredelyajte interfejsy tak, chtoby sdelat' otkrytym minimal'nyj
ob容m informacii, trebuemoj dlya interfejsa.
- Provodite stroguyu tipizaciyu interfejsov vsegda, kogda eto
vozmozhno.
- Ispol'zujte v interfejsah tipy iz oblasti prilozheniya vsegda,
kogda eto vozmozhno.
- Mnogokratno issledujte i utochnyajte kak proekt, tak i realizaciyu.
- Ispol'zujte luchshie dostupnye sredstva dlya proverki i analiza
- proekta, i
- realizacii.
- |ksperimentirujte, analizirujte i provodite testirovanie na
samom rannem vozmozhnom etape.
- Stremites' k prostote, maksimal'noj prostote, no ne sverh togo.
- Ne razrastajtes', ne dobavlyajte vozmozhnosti "na vsyakij sluchaj".
- Ne zabyvajte ob effektivnosti.
- Sohranyajte uroven' formalizacii, sootvetstvuyushchim razmeru proekta.
- Ne zabyvajte, chto razrabotchiki, programmisty i dazhe menedzhery
ostayutsya lyud'mi.
Eshche nekotorye pravila mozhno najti v $$12.5
11.6 Spisok literatury s kommentariyami
V etoj glave my tol'ko poverhnostno zatronuli voprosy proektirovaniya
i upravleniya programmnymi proektami. Po etoj prichine nizhe predlagaetsya
spisok literatury s kommentariyami. Znachitel'no bolee obshirnyj spisok
literatury s kommentariyami mozhno najti v [2].
[1] Bruce Anderson and Sanjiv Gossain: An Iterative Design Model for
Reusable Object-Oriented Software. Proc. OOPSLA'90. Ottawa,
Canada. pp. 12-27.
Opisanie modeli iterativnogo proektirovaniya i povtornogo
proektirovaniya s nekotorymi primerami i obsuzhdeniem rezul'tatov.
[2] Grady Booch: Object Oriented Design. Benjamin Cummings. 1991.
V etoj knige est' detal'noe opisanie proektirovaniya, opredelennyj
metod proektirovaniya s graficheskoj formoj zapisi i neskol'ko
bol'shih primerov proekta, zapisannyh na razlichnyh yazykah. |to
prevoshodnaya kniga, kotoraya vo mnogom povliyala na etu glavu. V nej
bolee gluboko rassmatrivayutsya mnogie iz zatronutyh zdes' voprosov.
[3] Fred Brooks: The Mythical Man Month. Addison Wesley. 1982.
Kazhdyj dolzhen perechityvat' etu knigu raz v paru let.
Predosterezhenie ot vysokomeriya. Ona neskol'ko ustarela v
tehnicheskih voprosah, no sovershenno ne ustarela vo vsem, chto
kasaetsya otdel'nogo rabotnika, organizacii i voprosov razmera.
[4] Fred Brooks: No Silver Bullet. IEEE Computer, Vol.20 No.4.
April 1987.
Svodka razlichnyh podhodov k processu razvitiya bol'shih programmnyh
sistem s ochen' poleznym predosterezheniem ot very v magicheskie
recepty ("zolotaya pulya").
[5] De Marco and Lister: Peopleware. Dorset House Publishing Co. 1987.
Odna iz nemnogih knig, posvyashchennyh roli chelovecheskogo faktora
v proizvodstve programmnogo obespecheniya. Neobhodima dlya kazhdogo
menedzhera. Dostatochno uspokaivayushchaya dlya chteniya pered snom.
Lekarstvo ot mnogih glupostej.
[6] Ron Kerr: A Materialistic View of the Software "Engineering"
Analogy. in SIGPLAN Notices, March 1987. pp 123-125.
Ispol'zovanie analogii v etoj i sleduyushchej glavah vo mnogom
obyazano nablyudeniyam iz ukazannoj stat'i, a tak zhe besedam s
R. Kerrom, kotorye etomu predshestvovali.
[7] Barbara Liskov: Data Abstraction and Hierarchy. Proc. OOPSLA'87
(Addendum). Orlando, Florida. pp 17-34.
Issleduetsya kak ispol'zovanie nasledovaniya mozhet povredit'
koncepcii abstraktnyh dannyh. Ukazhem, chto v S++ est' special'nye
yazykovye sredstva, pomogayushchie izbezhat' bol'shinstvo ukazannyh
problem ($$12.2.5).
[8] C. N. Parkinson: Parkinson's Law and other Studies in
Administration. Houghton-Mifflin. Boston. 1957.
Odno iz zabavnyh i samyh yazvitel'nyh opisanij bed, k kotorym
privodit process administrirovaniya.
[9] Bertrand Meyer: Object Oriented Software Construction.
Prentice Hall. 1988.
Stranicy 1-64 i 323-334 soderzhat horoshee opisanie odnogo vzglyada
na ob容ktno-orientirovannoe programmirovanie i proektirovanie,
a takzhe mnogo zdravyh, prakticheskih sovetov. V ostal'noj chasti
knigi opisyvaetsya yazyk |jffel' (Eiffel).
[10] Alan Snyder: Encapsulation and Inheritance in Object-Oriented
Programming Languages. Proc. OOPSLA'86. Portland, Oregon. pp.38-45.
Vozmozhno pervoe horoshee opisanie vzaimodejstviya obolochki i
nasledovaniya. V stat'e tak zhe na horoshem urovne rassmatrivayutsya
nekotorye ponyatiya, svyazannye s mnozhestvennym nasledovaniem.
[11] Rebecca Wirfs-Brock, Brian Wilkerson, and Lauren Wiener:
Designing Object-Oriented Software. Prentice Hall. 1990.
Opisyvaetsya antropomorfnyj metod proektirovaniya osnovannyj na
special'nyh kartochkah CRC (Classes, Responsibilities,
Collaboration) (t.e. Klassy, Otvetstvennost', Sotrudnichestvo).
Tekst, a mozhet byt' i sam metod tyagoteet k yazyku Smalltalk.
Stremis' k prostote, maksimal'noj prostote, no ne sverh togo.
- A. |jnshtejn
|ta glava posvyashchena svyazi mezhdu proektirovaniem i yazykom
programmirovaniya S++. V nej issleduetsya primenenie klassov pri
proektirovanii i ukazyvayutsya opredelennye vidy zavisimostej, kotorye
sleduet vydelyat' kak vnutri klassa, tak i mezhdu klassami. Izuchaetsya
rol' staticheskogo kontrolya tipov. Issleduetsya primenenie nasledovaniya
i svyaz' nasledovaniya i prinadlezhnosti. Obsuzhdaetsya ponyatie komponenta
i dayutsya nekotorye obrazcy dlya interfejsov.
12.1 Proektirovanie i yazyk programmirovaniya.
Esli by mne nado bylo postroit' most, to ya ser'ezno podumal by, iz
kakogo materiala ego stroit', i proekt mosta sil'no zavisel by ot
vybrannogo materiala, a, sledovatel'no, razumnye proekty kamennogo
mosta otlichayutsya ot razumnyh proektov metallicheskogo mosta ili
ot razumnyh proektov derevyannogo mosta i t.d. Ne stoit
rasschityvat' na vybor podhodyashchego dlya mosta materiala bez opredelennyh
znanij o materialah i ih ispol'zovanii. Konechno, vam ne nado byt'
specialistom plotnikom dlya proektirovaniya derevyannogo mosta, no vy
dolzhny znat' osnovy konstruirovaniya iz dereva, chtoby predpochest' ego
metallu v kachestve materiala dlya mosta. Bolee togo, hotya dlya
proektirovaniya derevyannogo mosta vy i ne dolzhny byt' specialistom
plotnikom, vam neobhodimo dostatochno detal'no znat' svojstva
dereva i eshche bol'she znat' o plotnikah.
Analogichno, pri vybore yazyka programmirovaniya dlya
opredelennogo programmnogo obespecheniya nado znat' neskol'ko yazykov,
a dlya uspeshnogo proektirovaniya programmy nado dostatochno detal'no
znat' vybrannyj yazyk realizacii, dazhe esli vam lichno ne predstoit
napisat' ni odnoj strochki programmy. Horoshij proektirovshchik mosta
cenit svojstva ispol'zuemyh im materialov i primenyaet ih dlya uluchsheniya
proekta. Analogichno, horoshij razrabotchik programm ispol'zuet sil'nye
storony yazyka realizacii i, naskol'ko vozmozhno, stremitsya izbezhat' takogo
ego ispol'zovaniya, kotoroe vyzovet trudnosti na stadii realizacii.
Mozhno podumat', chto tak poluchaetsya estestvennym obrazom, esli
v proektirovanii uchastvuet tol'ko odin razrabotchik ili programmist, odnako
dazhe v etom sluchae programmist v silu nedostatka opyta ili iz-za
neopravdannoj priverzhennosti k stilyu programmirovaniya, rasschitannomu na
sovershenno drugie yazyki, mozhet sbit'sya na nevernoe ispol'zovanie yazyka.
Esli razrabotchik sushchestvenno otlichaetsya ot programmista, osobenno
esli u nih raznaya programmistskaya kul'tura, vozmozhnost' poyavleniya
v okonchatel'noj versii sistemy oshibok, neeffektivnyh i neelegantnyh reshenij
pochti navernyaka prevratitsya v neizbezhnost'.
Itak, chem mozhet pomoch' razrabotchiku yazyk programmirovaniya? On
mozhet predostavit' takie yazykovye sredstva, kotorye pozvolyat
vyrazit' pryamo na yazyke programmirovaniya osnovnye ponyatiya proekta.
Togda oblegchaetsya realizaciya, proshche podderzhivat' ee sootvetstvie
proektu, proshche organizovat' obshchenie mezhdu
razrabotchikami i programmistami, i poyavlyaetsya vozmozhnost' sozdat'
bolee sovershennye sredstva kak dlya razrabotchikov, tak i dlya
programmistov.
Naprimer, mnogie metody proektirovaniya udelyayut znachitel'noe vnimanie
zavisimostyam mezhdu razlichnymi chastyami programmy (obychno s cel'yu
ih umen'sheniya i garantii togo, chto eti chasti budut ponyatny i horosho
opredeleny). YAzyk, dopuskayushchij yavnoe zadanie interfejsov mezhdu
chastyami programmy, mozhet pomoch' v etom voprose
razrabotchikam. On mozhet garantirovat', chto dejstvitel'no budut
sushchestvovat' tol'ko predpolagaemye zavisimosti. Poskol'ku
bol'shinstvo zavisimostej yavno vyrazheno v programme na takom yazyke,
mozhno razrabotat' sredstva, chitayushchie programmu i vydayushchie grafy
zavisimostej. V etom sluchae razrabotchiku i drugim ispolnitelyam legche
uyasnit' strukturu programmy. Takie yazyki programmirovaniya kak S++
pomogayut sokratit' razryv mezhdu proektom i programmoj, a znachit
umen'shayut vozmozhnost' putanicy i nedoponimanij.
Bazovoe ponyatie S++ - eto klass. Klass imeet opredelennyj
tip. Krome togo, klass yavlyaetsya pervichnym sredstvom upryatyvaniya
informacii. Mozhno opisyvat' programmy v terminah pol'zovatel'skih
tipov i ierarhij etih tipov. Kak vstroennye, tak i pol'zovatel'skie
tipy podchinyayutsya pravilam staticheskogo kontrolya tipov. Virtual'nye
funkcii predostavlyayut, ne narushaya pravil staticheskih tipov,
mehanizm svyazyvaniya na etape vypolneniya. SHablony tipa pozvolyayut
sozdavat' parametrizovannye tipy. Osobye situacii pozvolyayut sdelat'
regulyarnoj reakciyu na oshibki. Vse eti sredstva S++ mozhno
ispol'zovat' bez dopolnitel'nyh nakladnyh
rashodov v sravnenii s programmoj na S. Takovy glavnejshie
sredstva S++, kotorye dolzhen predstavlyat' i uchityvat' razrabotchik.
Krome togo, sushchestvenno povliyat' na prinyatie reshenij na stadii
proektirovaniya mozhet nalichie dostupnyh bol'shih bibliotek
sleduyushchego naznacheniya: dlya raboty s matricami, dlya svyazi s
bazami dannyh, dlya podderzhki parallel'nogo
programmirovaniya, graficheskie biblioteki i t.d.
Strah pered noviznoj, neprigodnyj zdes' opyt raboty na drugih
yazykah, v drugih sistemah ili oblastyah prilozheniya, bednye sredstva
proektirovaniya - vse eto privodit k neoptimal'nomu ispol'zovaniyu S++.
Sleduet otmetit' tri momenta, kogda razrabotchiku ne udaetsya
izvlech' vygodu iz vozmozhnostej S++ i uchest' ogranicheniya yazyka:
[1] Ignorirovanie klassov i sostavlenie proekta takim obrazom, chto
programmistam prihoditsya ogranichivat'sya tol'ko S.
[2] Ignorirovanie proizvodnyh klassov i virtual'nyh funkcij,
ispol'zovanie tol'ko podmnozhestva abstraktnyh dannyh.
[3] Ignorirovanie staticheskogo kontrolya tipov i sostavlenie proekta
takim obrazom, chto programmisty vynuzhdeny primenyat' dinamicheskie
proverki tipov.
Obychno ukazannye momenty voznikayut u razrabotchikov, svyazannyh s:
[1] C, ili tradicionnoj sistemoj CASE ili metodami strukturnogo
proektirovaniya;
[2] Adoj ili metodami proektirovaniya s pomoshch'yu abstrakcii dannyh;
[3] yazykami, blizkimi Smalltalk ili Lisp.
V kazhdom sluchae sleduet reshit': nepravil'no vybran yazyk
realizacii (schitaya, chto metod proektirovaniya vybran verno), ili
razrabotchiku ne udalos' prisposobit'sya i ocenit' yazyk (schitaya, chto
yazyk realizacii vybran verno).
Sleduet skazat', chto net nichego neobychnogo ili pozornogo v
takom rashozhdenii. Prosto eto rashozhdenie, kotoroe privedet k
neoptimal'nomu proektu, vozlozhit dopolnitel'nuyu rabotu na
programmistov, a v sluchae, kogda struktura ponyatij proekta
znachitel'no bednee struktury yazyka S++, to i na samih razrabotchikov.
Otmetim, chto neobyazatel'no vse programmy dolzhny
strukturirovat'sya opirayas' na ponyatiya klassov i (ili) ierarhij klassov,
i neobyazatel'no vsyakaya programma dolzhna ispol'zovat' vse sredstva,
predostavlyaemye S++. Kak raz naoborot, dlya uspeha proekta neobhodimo,
chtoby lyudyam ne navyazyvali ispol'zovanie yazykovyh sredstv, s kotorymi
oni tol'ko poznakomilis'. Cel' posleduyushchego izlozheniya ne v tom,
chtoby navyazat' dogmatichnoe ispol'zovanie klassov, ierarhij i
strogo tipizirovannyh interfejsov, a v tom, chtoby pokazat'
vozmozhnosti ih ispol'zovaniya vsyudu, gde pozvolyaet oblast'
prilozheniya, ogranicheniya S++ i opyt ispolnitelej. V $$12.1.4 budut
rassmotreny podhody k razlichnomu ispol'zovaniyu S++ v proekte
pod zagolovkom "Proekt-gibrid".
12.1.1 Ignorirovanie klassov
Rassmotrim pervyj iz ukazannyh momentov - ignorirovanie klassov.
V takom sluchae poluchivshayasya programma na S++ budet priblizitel'no
ekvivalentna S-programme, razrabotannoj po tomu zhe proektu, i,
mozhno skazat', chto oni budut priblizitel'no ekvivalentny programmam
na Ade ili Kobole, razrabotannym po nemu zhe.
Po suti proekt sostavlen kak nezavisyashchij ot yazyka realizacii, chto
prinuzhdaet programmista ogranichivat'sya obshchim podmnozhestvom yazykov
S, Ada ili Kobol. Zdes' est' svoi preimushchestva. Naprimer, poluchivsheesya
v rezul'tate strogoe razdelenie dannyh i programmnogo koda pozvolyaet
legko ispol'zovat' tradicionnye bazy dannyh, kotorye razrabotany
dlya takih programm. Poskol'ku ispol'zuetsya ogranichennyj yazyk
programmirovaniya, ot programmistov trebuetsya men'she opytnosti
(ili, po krajnej mere drugoj ee uroven'). Dlya mnogih prilozhenij,
naprimer, dlya tradicionnyh baz dannyh, rabotayushchih s
fajlom posledovatel'no, takoj podhod vpolne razumen, a tradicionnye
priemy, otrabotannye za desyatiletiya, vpolne adekvatny zadache.
Odnako tam, gde oblast' prilozheniya sushchestvenno otlichaetsya ot
tradicionnoj posledovatel'noj obrabotki zapisej (ili simvolov),
ili slozhnost' zadachi vyshe, kak, naprimer, v dialogovoj sisteme
CASE, nedostatok yazykovoj podderzhki abstraktnyh dannyh
iz-za otkaza ot klassov (esli ih ne uchityvat') povredit
proektu. Slozhnost' zadachi ne umen'shitsya, no, poskol'ku sistema
realizovana na obednennom yazyke, struktura programmy ploho budet
otvechat' proektu. U nee slishkom bol'shoj ob容m, ne hvataet proverki tipov,
i, voobshche, ona ploho prisposoblena dlya ispol'zovaniya razlichnyh
vspomogatel'nyh sredstv. |to put', privodyashchij k koshmaram pri ee
soprovozhdenii.
Obychno dlya preodoleniya ukazannyh trudnostej sozdayut special'nye
sredstva, podderzhivayushchie ponyatiya, ispol'zuemye v proekte. Blagodarya
im sozdayutsya konstrukcii bolee vysokogo
urovnya i organizuyutsya proverki s cel'yu kompensirovat' defekty
(ili soznatel'noe obednenie) yazyka realizacii. Tak metod
proektirovaniya stanovitsya samocel'yu, i dlya nego sozdaetsya special'nyj
yazyk programmirovaniya. Takie yazyki programmirovaniya v bol'shinstve
sluchaev yavlyayutsya plohoj zamenoj shiroko rasprostranennyh yazykov
programmirovaniya obshchego naznacheniya, kotorye soprovozhdayutsya
podhodyashchimi sredstvami proektirovaniya. Ispol'zovat' S++ s takim
ogranicheniem, kotoroe dolzhno kompensirovat'sya pri proektirovanii
special'nymi sredstvami, bessmyslenno. Hotya nesootvetstvie mezhdu
yazykom programmirovaniya i sredstvami proektirovaniya mozhet byt' prosto
stadiej processa perehoda, a znachit vremennym yavleniem.
Samoj tipichnoj prichinoj ignorirovaniya klassov pri proektirovanii
yavlyaetsya prostaya inerciya. Tradicionnye yazyki programmirovaniya ne
predostavlyayut ponyatiya klassa, i v tradicionnyh metodah proektirovaniya
otrazhayutsya etot nedostatok. Obychno v processe proektirovaniya
naibol'shee vnimanie udelyaetsya razbieniyu zadachi na procedury,
proizvodyashchie trebuemye dejstviya. V glave 1 eto ponyatie nazyvalos'
procedurnym programmirovaniem, a v oblasti proektirovaniya ono
imenuetsya kak funkcional'naya dekompoziciya. Voznikaet tipichnyj
vopros "Mozhno li ispol'zovat' S++ sovmestno s metodom proektirovaniya,
baziruyushchimsya na funkcional'noj dekompozicii?" Da, mozhno, no,
veroyatnee vsego, v rezul'tate vy pridete k ispol'zovaniyu S++ kak
prosto uluchshennogo S so vsemi ukazannymi vyshe problemami. |to
mozhet byt' priemlemo na period perehoda na novyj yazyk, ili dlya
uzhe zavershennogo proektirovaniya, ili dlya podzadach, v kotoryh
ispol'zovanie klassov ne daet sushchestvennyh vygod (esli uchityvat'
opyt programmirovaniya na S++ k dannomu momentu), no v obshchem
sluchae na bol'shom otrezke vremeni otkaz ot svobodnogo
ispol'zovaniya klassov, svyazannyj s metodom funkcional'noj
dekompozicii, nikak ne sovmestim s effektivnym ispol'zovaniem S++.
Procedurno-orientirovannyj i ob容ktno-orientirovannyj
podhody k programmirovaniyu razlichayutsya po svoej suti i obychno
vedut k sovershenno raznym resheniyam odnoj zadachi. |tot vyvod
veren kak dlya stadii realizacii, tak i dlya stadii proektirovaniya:
vy koncentriruete vnimanie ili na predprinimaemyh dejstviyah, ili na
predstavlyaemyh sushchnostyah, no ne na tom i drugom odnovremenno.
Togda pochemu metod ob容ktno-orientirovannogo proektirovaniya
predpochtitel'nee metoda funkcional'noj dekompozicii?
Glavnaya prichina v tom, chto funkcional'naya dekompoziciya ne daet
dostatochnoj abstrakcii dannyh. A otsyuda uzhe sleduet, chto proekt
budet
- menee podatlivym k izmeneniyam,
- menee prisposoblennym dlya ispol'zovaniya razlichnyh vspomogatel'nyh
sredstv,
- menee prigodnym dlya parallel'nogo razvitiya i
- menee prigodnym dlya parallel'nogo vypolneniya.
Delo v tom, chto funkcional'naya dekompoziciya vynuzhdaet ob座avlyat'
"vazhnye" dannye global'nymi, poskol'ku, esli sistema strukturirovana
kak derevo funkcij, vsyakoe dannoe, dostupnoe dvum funkciyam, dolzhno
byt' global'nym po otnosheniyu k nim. |to privodit k tomu, chto
"vazhnye" dannye "vsplyvayut" k vershine dereva, po
mere togo kak vse bol'shee chislo funkcij trebuet dostupa k nimX.
X V tochnosti tak zhe proishodit v sluchae ierarhii klassov s odnim
kornem, kogda "vazhnye" dannye vsplyvayut po napravleniyu k bazovomu
klassu.
Kogda my koncentriruem vnimanie na opisaniyah klassov, zaklyuchayushchih
opredelennye dannye v obolochku, to zavisimosti mezhdu razlichnymi
chastyami programmy vyrazheny yavno i mozhno ih prosledit'. Eshche bolee
vazhno to, chto pri takom podhode umen'shaetsya chislo zavisimostej
v sisteme za schet luchshej rasstanovki ssylok na dannye.
Odnako, nekotorye zadachi luchshe reshayutsya s pomoshch'yu nabora
procedur. Smysl "ob容ktno-orientirovannogo" proektirovaniya ne v
tom, chtoby udalit' vse global'nye procedury iz programmy ili
ne imet' v sisteme procedurno-orientirovannyh chastej. Osnovnaya
ideya skoree v tom, chto klassy, a ne global'nye procedury stanovyatsya
glavnym ob容ktom vnimaniya na stadii proektirovaniya. Ispol'zovanie
procedurnogo stilya dolzhno byt' osoznannym resheniem, a ne resheniem,
prinimaemym po umolchaniyu. Kak klassy, tak i procedury sleduet
primenyat' soobrazno oblasti prilozheniya, a ne prosto kak
neizmennye metody proektirovaniya.
12.1.2 Ignorirovanie nasledovaniya
Rassmotrim variant 2 - proekt, kotoryj ignoriruet nasledovanie. V etom
sluchae v okonchatel'noj programme prosto ne ispol'zuyutsya vozmozhnosti
osnovnogo sredstva S++, hotya i poluchayutsya opredelennye vygody pri
ispol'zovanii S++ po sravneniyu s ispol'zovaniem yazykov S, Paskal',
Fortran, Kobol i t.p. Obychnye dovody v pol'zu etogo, pomimo inercii,
utverzhdeniya, chto "nasledovanie - eto detal' realizacii", ili "nasledovanie
prepyatstvuet upryatyvaniyu informacii", ili "nasledovanie zatrudnyaet
vzaimodejstvie s drugimi sistemami programmirovaniya".
Schitat' nasledovanie vsego lish' detal'yu realizacii - znachit
ignorirovat' ierarhiyu klassov, kotoraya mozhet neposredstvenno
modelirovat' otnosheniya mezhdu ponyatiyami v oblasti prilozheniya. Takie
otnosheniya dolzhny byt' yavno vyrazheny v proekte, chtoby dat'
vozmozhnost' razrabotchiku produmat' ih.
Sil'nye dovody mozhno privesti v pol'zu isklyucheniya nasledovaniya
iz teh chastej programmy na S++, kotorye neposredstvenno vzaimodejstvuyut
s programmami, napisannymi na drugih yazykah. No eto ne yavlyaetsya
dostatochnoj prichinoj, chtoby otkazat'sya ot nasledovaniya v sisteme
v celom, eto prosto dovod v pol'zu togo, chtoby akkuratno opredelit'
i inkapsulirovat' programmnyj interfejs s "vneshnim mirom".
Analogichno, chtoby izbavit'sya ot bespokojstva, vyzvannogo putanicej s
upryatyvaniem informacii pri nalichii nasledovaniya, nado ostorozhno
ispol'zovat' virtual'nye funkcii i zakrytye chleny, no ne
otkazyvat'sya ot nasledovaniya.
Sushchestvuet dostatochno mnogo situacij, kogda ispol'zovanie
nasledovaniya ne daet yavnyh vygod, no politika
"nikakogo nasledovaniya" privedet k menee ponyatnoj i menee gibkoj
sisteme, v kotoroj nasledovanie "poddelyvaetsya" s pomoshch'yu
bolee tradicionnyh konstrukcij yazyka i proektirovaniya.
Dlya bol'shih proektov eto sushchestvenno. Bolee togo,
vpolne vozmozhno, chto nesmotrya na takuyu politiku, nasledovanie
vse ravno budet ispol'zovat'sya, poskol'ku programmisty, rabotayushchie
na S++, najdut ubeditel'nye dovody v pol'zu proektirovaniya s uchetom
nasledovaniya v razlichnyh chastyah sistemy. Takim obrazom, politika
"nikakogo nasledovaniya" privedet lish' k tomu, chto v sisteme budet
otsutstvovat' celostnaya obshchaya struktura, a ispol'zovanie ierarhii
klassov budet ogranicheno opredelennymi podsistemami.
Inymi slovami, bud'te nepredubezhdennymi. Ierarhiya klassov
ne yavlyaetsya obyazatel'noj chast'yu vsyakoj horoshej programmy, no est'
massa situacij, kogda ona mozhet pomoch' kak v ponimanii oblasti
prilozheniya, tak i v formulirovanii reshenij. Utverzhdenie, chto
nasledovanie mozhet nepravil'no ili chrezmerno ispol'zovat'sya,
sluzhit tol'ko dovodom v pol'zu ostorozhnosti, a vovse ne v pol'zu
otkaza ot nego.
12.1.3 Ignorirovanie staticheskogo kontrolya tipov
Rassmotrim variant 3, otnosyashchijsya k proektu, v kotorom ignoriruetsya
staticheskij kontrol' tipov. Rasprostranennye dovody v pol'zu otkaza
na stadii proektirovaniya ot staticheskogo kontrolya tipov svodyatsya
k tomu, chto "tipy - eto produkt yazykov programmirovaniya", ili chto
"bolee estestvenno rassuzhdat' ob ob容ktah, ne zabotyas' o tipah",
ili "staticheskij kontrol' tipov vynuzhdaet nas dumat' o realizacii
na slishkom rannem etape". Takoj podhod vpolne dopustim do teh por,
poka on rabotaet i ne prinosit vreda. Vpolne razumno na stadii
proektirovaniya ne zabotit'sya o detalyah proverki tipov, i chasto
vpolne dopustimo na stadii analiza i nachal'nyh stadiyah proektirovaniya
polnost'yu zabyt' o voprosah, svyazannyh s tipami. V to zhe vremya,
klassy i ierarhii klassov ochen' polezny na stadii proektirovaniya,
v chastnosti, oni dayut nam bol'shuyu opredelennost' ponyatij, pozvolyayut
tochno zadat' vzaimootnosheniya mezhdu ponyatiyami i pomogayut rassuzhdat'
o ponyatiyah. Po mere razvitiya proekta eta opredelennost' i tochnost'
preobrazuetsya vo vse bolee konkretnye utverzhdeniya o klassah i ih
interfejsah.
Vazhno ponimat', chto tochno opredelennye i strogo tipizirovannye
interfejsy yavlyayutsya fundamental'nym sredstvom proektirovaniya. YAzyk
S++ byl sozdan kak raz s uchetom etogo. Strogo tipizirovannyj
interfejs garantiruet, chto tol'ko sovmestimye chasti
programmy mogut byt' skompilirovany i skomponovany voedino, i tem
samym pozvolyaet delat' otnositel'no strogie dopushcheniya ob etih chastyah.
|ti dopushcheniya obespechivayutsya sistemoj tipov yazyka.
V rezul'tate svodyatsya k minimumu proverki na etape
vypolneniya, chto povyshaet effektivnost' i privodit k znachitel'nomu
sokrashcheniyu fazy integracii chastej proekta, realizovannyh raznymi
programmistami. Real'nyj polozhitel'nyj opyt
integracii sistemy so strogo tipizirovannymi interfejsami privel
k tomu, chto voprosy integracii voobshche ne figuriruyut sredi osnovnyh
tem etoj glavy.
Rassmotrim sleduyushchuyu analogiyu: v fizicheskom mire my postoyanno
soedinyaem razlichnye ustrojstva, i sushchestvuet kazhushcheesya beskonechnym
chislo standartov na soedineniya. Glavnaya osobennost' etih soedinenij:
oni special'no sproektirovany takim obrazom, chtoby sdelat' nevozmozhnym
soedinenie dvuh ustrojstv, nerasschitannyh na nego,
to est' soedinenie dolzhno byt' sdelano edinstvennym
pravil'nym sposobom. Vy ne mozhete podsoedinit' elektrobritvu k
rozetke s vysokim napryazheniem. Esli by vy smogli sdelat' eto, to
sozhgli by britvu ili sgoreli sami. Massa izobretatel'nosti byla
proyavlena, chtoby dobit'sya nevozmozhnosti soedineniya dvuh
nesovmestimyh ustrojstv. Al'ternativoj odnovremennogo ispol'zovaniya
neskol'kih nesovmestimyh ustrojstv mozhet posluzhit' takoe ustrojstvo,
kotoroe samo sebya zashchishchaet ot nesovmestimyh s nim ustrojstv,
podklyuchayushchihsya k ego vhodu. Horoshim primerom mozhet sluzhit' stabilizator
napryazheniya. Poskol'ku ideal'nuyu sovmestimost' ustrojstv nel'zya
garantirovat' tol'ko na "urovne soedineniya", inogda trebuetsya bolee
dorogaya zashchita v elektricheskoj cepi, kotoraya pozvolyaet v dinamike
prisposobit'sya ili (i) zashchitit'sya ot skachkov napryazheniya.
Zdes' prakticheski pryamaya analogiya: staticheskij kontrol'
tipov ekvivalenten sovmestimosti na urovne soedineniya, a dinamicheskie
proverki sootvetstvuyut zashchite ili adaptacii v cepi. Rezul'tatom
neudachnogo kontrolya kak v fizicheskom, tak i v programmnom mire budet
ser'eznyj ushcherb. V bol'shih sistemah ispol'zuyutsya oba vida kontrolya.
Na rannem etape proektirovaniya vpolne dostatochno prostogo utverzhdeniya:
"|ti dva ustrojstva neobhodimo soedinit'"; no skoro stanovitsya
sushchestvennym, kak imenno sleduet ih soedinit': "Kakie garantii
daet soedinenie otnositel'no povedeniya ustrojstv?", ili
"Vozniknovenie kakih oshibochnyh situacij vozmozhno?", ili
"Kakova priblizitel'naya cena takogo soedineniya?"
Primenenie "staticheskoj tipizacii" ne ogranichivaetsya programmnym
mirom. V fizike i inzhenernyh naukah povsemestno rasprostraneny
edinicy izmereniya (metry, kilogrammy, sekundy), chtoby izbezhat'
smeshivaniya nesovmestimyh sushchnostej.
V nashem opisanii shagov proektirovaniya v $$11.3.3 tipy
poyavlyayutsya na scene uzhe na shage 2 (ochevidno, posle neskol'ko
iskusstvennogo ih rassmotreniya na shage 1) i stanovyatsya glavnoj
temoj shaga 4.
Staticheski kontroliruemye interfejsy - eto
osnovnoe sredstvo vzaimodejstviya programmnyh chastej sistemy
na S++, sozdannyh raznymi gruppami, a opisanie interfejsov etih
chastej (s uchetom tochnyh opredelenij tipov) stanovitsya osnovnym
sposobom sotrudnichestva mezhdu otdel'nymi gruppami programmistov.
|ti interfejsy yavlyayutsya osnovnym rezul'tatom processa proektirovaniya
i sluzhat glavnym sredstvom obshcheniya mezhdu razrabotchikami i
programmistami.
Otkaz ot etogo privodit k proektam, v kotoryh neyasna
struktura programmy, kontrol' oshibok otlozhen na stadiyu
vypolneniya, kotorye trudno horosho realizovat' na S++.
Rassmotrim interfejs, opisannyj s pomoshch'yu "ob容ktov",
opredelyayushchih sebya samostoyatel'no. Vozmozhno, naprimer, takoe opisanie:
"Funkciya f() imeet argument, kotoryj dolzhen byt' samoletom"
(chto proveryaetsya samoj funkciej vo vremya ee vypolneniya), v otlichie
ot opisaniya "Funkciya f() imeet argument, tip kotorogo est' samolet"
(chto proveryaetsya translyatorom). Pervoe opisanie yavlyaetsya sushchestvenno
nedostatochnym opisaniem interfejsa, t.k. privodit k dinamicheskoj proverke
vmesto staticheskogo kontrolya. Analogichnyj vyvod iz primera s
samoletom sdelan v $$1.5.2. Zdes' ispol'zovany bolee tochnye
specifikacii, i ispol'zovan shablon tipa i virtual'nye funkcii vzamen
neogranichennyh dinamicheskih proverok dlya togo, chtoby perenesti
vyyavlenie oshibok s etapa vypolneniya na etap translyacii. Razlichie
vremen raboty programm s dinamicheskim i staticheskim kontrolem
mozhet byt' ves'ma znachitel'nym, obychno ono nahoditsya v diapazone
ot 3 do 10 raz.
No ne sleduet vpadat' v druguyu krajnost'. Nel'zya obnaruzhit'
vse oshibki s pomoshch'yu staticheskogo kontrolya. Naprimer, dazhe
programmy s samym obshirnym staticheskim kontrolem uyazvimy k sboyam
apparatury. No vse zhe, v ideale nuzhno imet' bol'shoe raznoobrazie
interfejsov so staticheskoj tipizaciej s pomoshch'yu tipov iz oblasti
prilozheniya, sm. $$12.4.
Mozhet poluchit'sya, chto proekt, sovershenno
razumnyj na abstraktnom urovne, stolknetsya s ser'eznymi
problemami, esli ne uchityvaet ogranicheniya bazovyh sredstv, v
dannom sluchae S++. Naprimer, ispol'zovanie imen, a ne tipov dlya
strukturirovaniya sistemy privedet k nenuzhnym problemam dlya
sistemy tipov S++ i, tem samym, mozhet stat' prichinoj oshibok i
nakladnyh rashodov pri vypolnenii. Rassmotrim tri klassa:
class X { // pseudo code, not C++
f()
g()
}
class Y {
g()
h()
}
class Z {
h()
f()
}
ispol'zuemye nekotorymi funkciyami bestipovogo proekta:
k(a, b, c) // pseudo code, not C++
{
a.f()
b.g()
c.h()
}
Zdes' obrashcheniya
X x
Y y
Z z
k(x,y,z) // ok
k(z,x,y) // ok
budut uspeshnymi, poskol'ku k() prosto trebuet, chtoby ee pervyj
parametr imel operaciyu f(), vtoroj parametr - operaciyu g(), a
tretij parametr - operaciyu h(). S drugoj storony obrashcheniya
k(y,x,z); // fail
k(x,z,y); // fail
zavershatsya neudachno. |tot primer dopuskaet sovershenno razumnye
realizacii na yazykah s polnym dinamicheskim kontrolem (naprimer,
Smalltalk ili CLOS), no v S++ on ne imeet pryamogo
predstavleniya, poskol'ku yazyk trebuet, chtoby obshchnost' tipov byla
realizovana kak otnoshenie k bazovomu klassu. Obychno primery,
podobnye etomu, mozhno predstavit' na S++, esli zapisyvat' utverzhdeniya
ob obshchnosti s pomoshch'yu yavnyh opredelenij klassov, no eto potrebuet
bol'shogo hitroumiya i vspomogatel'nyh sredstv. Mozhno sdelat',
naprimer, tak:
class F {
virtual void f();
};
class G {
virtual void g();
};
class H {
virtual void h();
};
class X : public virtual F, public virtual G {
void f();
void g();
};
class Y : public virtual G, public virtual H {
void g();
void h();
};
class Z : public virtual H, public virtual F {
void h();
void f();
};
k(const F& a, const G& b, const H& c)
{
a.f();
b.g();
c.h();
}
main()
{
X x;
Y y;
Z z;
k(x,y,z); // ok
k(z,x,y); // ok
k(y,x,z); // error F required for first argument
k(x,z,y); // error G required for second argument
}
Obratite vnimanie, chto sdelav predpolozheniya k() o svoih argumentah
yavnymi, my peremestili kontrol' oshibok s etapa vypolneniya na etap
translyacii. Slozhnye primery, podobnye privedennomu, voznikayut,
kogda pytayutsya realizovat' na S++ proekty, sdelannye na osnove
opyta raboty s drugimi sistemami tipov. Obychno eto vozmozhno,
no v rezul'tate poluchaetsya neestestvennaya i neeffektivnaya programma.
Takoe nesovpadenie mezhdu priemami proektirovaniya i yazykom
programmirovaniya mozhno sravnit' s nesovpadeniem pri poslovnom
perevode s odnogo estestvennogo yazyka na drugoj. Ved' anglijskij
s nemeckoj grammatikoj vyglyadit stol' zhe neuklyuzhe, kak i nemeckij
s anglijskoj grammatikoj, no oba yazyka mogut byt' dostupny
ponimaniyu togo, kto beglo govorit na odnom iz nih.
|tot primer podtverzhdaet tot vyvod, chto klassy v programme yavlyayutsya
konkretnym voploshcheniem ponyatij, ispol'zuemyh pri proektirovanii,
poetomu nechetkie otnosheniya mezhdu klassami privodyat k nechetkosti
osnovnyh ponyatij proektirovaniya.
Perehod na novye metody raboty mozhet byt' muchitelen dlya lyuboj
organizacii. Raskol vnutri nee i rashozhdeniya mezhdu sotrudnikami mogut
byt' znachitel'nymi. No rezkij reshitel'nyj perehod, sposobnyj v odnochas'e
prevratit' effektivnyh i kvalificirovannyh storonnikov "staroj shkoly"
v neeffektivnyh novichkov "novoj shkoly" obychno nepriemlem. V to zhe
vremya, nel'zya dostich' bol'shih vysot bez izmenenij, a
znachitel'nye izmeneniya obychno svyazany s riskom.
YAzyk S++ sozdavalsya s cel'yu sokratit' takoj risk za schet
postepennogo vvedeniya novyh metodov. Hotya ochevidno, chto naibol'shie
preimushchestva pri ispol'zovanii S++ dostigayutsya za schet abstrakcii
dannyh, ob容ktno-orientirovannogo programmirovaniya i
ob容ktno-orientirovannogo proektirovaniya, sovershenno neochevidno,
chto bystree vsego dostich' etogo mozhno reshitel'nym
razryvom s proshlym. Vryad li takoj yavnyj razryv budet vozmozhen,
obychno stremlenie k usovershenstvovaniyam sderzhivaetsya ili dolzhno
sderzhivat'sya, chtoby perehod k nim byl upravlyaemym. Nuzhno uchityvat'
sleduyushchee:
- Razrabotchikam i programmistam trebuetsya vremya dlya ovladeniya
novymi metodami.
- Novye programmy dolzhny vzaimodejstvovat' so starymi programmami.
- Starye programmy nuzhno soprovozhdat' (chasto beskonechno).
- Rabota po tekushchim proektam i programmam dolzhna byt'
vypolnena v srok.
- Sredstva, rasschitannye na novye metody, nuzhno adaptirovat' k
lokal'nomu okruzheniyu.
Zdes' rassmatrivayutsya kak raz situacii, svyazannye s perechislennymi
trebovaniyami. Legko nedoocenit' dva pervyh trebovaniya.
Poskol'ku v S++ vozmozhny neskol'ko shem programmirovaniya,
yazyk dopuskaet postepennyj perehod na nego, ispol'zuya
sleduyushchie preimushchestva takogo perehoda:
- Izuchaya S++, programmisty mogut prodolzhat' rabotat'.
- V okruzhenii, bednom na programmnye sredstva, ispol'zovanie S++
mozhet prinesti znachitel'nye vygody.
- Programmy, napisannye na S++, mogut horosho vzaimodejstvovat'
s programmami, napisannymi na S ili drugih tradicionnyh yazykah.
- YAzyk imeet bol'shoe podmnozhestvo, sovmestimoe s S.
Ideya zaklyuchaetsya v postepennom perehode programmista s
tradicionnogo yazyka na S++: vnachale on programmiruet na S++
v tradicionnom procedurnom stile, zatem s pomoshch'yu metodov abstrakcii
dannyh, i nakonec, kogda ovladeet yazykom i svyazannymi s nim sredstvami,
polnost'yu perehodit na ob容ktno-orientirovannoe programmirovanie.
Zametim, chto horosho sproektirovannuyu biblioteku ispol'zovat' namnogo
proshche, chem proektirovat' i realizovyvat', poetomu dazhe s pervyh svoih
shagov novichok mozhet poluchit' preimushchestva, ispol'zuya bolee
razvitye sredstva S++.
Ideya postepennogo, poshagovogo ovladeniya S++, a takzhe vozmozhnost'
smeshivat' programmy na S++ s programmami, napisannymi na yazykah,
ne imeyushchih sredstv abstrakcii dannyh i ob容ktno-orientirovannogo
programmirovaniya, estestvenno privodit k proektu, imeyushchemu
gibridnyj stil'. Bol'shinstvo interfejsov mozhno poka ostavit'
na procedurnom urovne, poskol'ku chto-libo bolee slozhnoe ne
prineset nemedlennogo vyigrysha. Naprimer, obrashchenie k standartnoj
biblioteke math iz S opredelyaetsya na S++ tak:
extern "C" {
#include <math.h>
}
i standartnye matematicheskie funkcii iz biblioteki mozhno ispol'zovat'
tak zhe, kak i v S. Dlya vseh osnovnyh bibliotek takoe vklyuchenie
dolzhno byt' sdelano temi, kto postavlyaet biblioteki, tak chto
programmist na S++ dazhe ne budet znat', na kakom yazyke realizovana
bibliotechnaya funkciya. Ispol'zovanie bibliotek, napisannyh na takih
yazykah kak S, yavlyaetsya pervym i vnachale samym vazhnym sposobom
povtornogo ispol'zovaniya na S++.
Na sleduyushchem shage, kogda stanut neobhodimy bolee slozhnye
priemy, sredstva, realizovannye na takih yazykah kak S ili Fortran,
predstavlyayutsya v vide klassov za schet inkapsulyacii struktur dannyh
i funkcij v interfejs klassov S++. Prostym primerom
vvedeniya bolee vysokogo semanticheskogo urovnya za schet perehoda
ot urovnya procedur plyus struktur dannyh k urovnyu abstrakcii dannyh
mozhet sluzhit' klass strok iz $$7.6. Zdes' za schet inkapsulyacii
simvol'nyh strok i standartnyh strokovyh funkcij S
poluchaetsya novyj strokovyj tip, kotoryj gorazdo proshche ispol'zovat'.
Podobnym obrazom mozhno vklyuchit' v ierarhiyu klassov lyuboj
vstroennyj ili otdel'no opredelennyj tip. Naprimer, tip int
mozhno vklyuchit' v ierarhiyu klassov tak:
class Int : public My_object {
int i;
public:
// definition of operations
// see exercises [8]-[11] in section 7.14 for ideas
// opredeleniya operacij poluchayutsya v uprazhneniyah [8]-[11]
// za ideyami obratites' k razdelu 7.14
};
Tak sleduet delat', esli dejstvitel'no est' potrebnost'
vklyuchit' takie tipy v ierarhiyu.
Obratno, klassy S++ mozhno predstavit' v programme na S ili
Fortrane kak funkcii i struktury dannyh. Naprimer:
class myclass {
// representation
public:
void f();
T1 g(T2);
// ...
};
extern "C" { // map myclass into C callable functions:
void myclass_f(myclass* p) { p->f(); }
T1 myclass_g(myclass* p, T2 a) { return p->g(a); }
// ...
};
V S-programme sleduet opredelit' eti funkcii v zagolovochnom fajle
sleduyushchim obrazom:
// in C header file
extern void myclass_f(struct myclass*);
extern T1 myclass_g(struct myclass*, T2);
Takoj podhod pozvolyaet razrabotchiku na S++, esli u nego uzhe est'
zapas programm, napisannyh na yazykah, v kotoryh otsutstvuyut ponyatiya
abstrakcii dannyh i ierarhii klassov, postepenno priobshchat'sya k etim
ponyatiyam, dazhe pri tom trebovanii, chto okonchatel'nuyu versii programmy
mozhno budet vyzyvat' iz tradicionnyh procedurnyh yazykov.
Osnovnoe polozhenie ob容ktno-orientirovannogo proektirovaniya i
programmirovaniya zaklyuchaetsya v tom, chto programma sluzhit model'yu
nekotoryh ponyatij real'nosti. Klassy v programme predstavlyayut
osnovnye ponyatiya oblasti prilozheniya i, v chastnosti, osnovnye
ponyatiya samogo processa modelirovaniya real'nosti. Ob容kty klassov
predstavlyayut predmety real'nogo mira i produkty processa
realizacii.
My rassmotrim strukturu programmy s tochki zreniya sleduyushchih
vzaimootnoshenij mezhdu klassami:
- otnosheniya nasledovaniya,
- otnosheniya prinadlezhnosti,
- otnosheniya ispol'zovaniya i
- zaprogrammirovannye otnosheniya.
Pri rassmotrenii etih otnoshenij neyavno predpolagaetsya, chto ih analiz
yavlyaetsya uzlovym momentom v proekte sistemy. V $$12.4 issleduyutsya
svojstva, kotorye delayut klass i ego interfejs poleznymi dlya
predstavleniya ponyatij. Voobshche govorya, v ideale, zavisimost' klassa
ot ostal'nogo mira dolzhna byt' minimal'na i chetko opredelena, a
sam klass dolzhen cherez interfejs otkryvat' lish' minimal'nyj ob容m
informacii dlya ostal'nogo mira.
Podcherknem, chto klass v S++ yavlyaetsya tipom, poetomu sami klassy
i vzaimootnosheniya mezhdu nimi obespecheny znachitel'noj podderzhkoj
so storony translyatora i v obshchem sluchae poddayutsya staticheskomu analizu.
12.2.1 CHto predstavlyayut klassy?
Po suti v sisteme byvayut klassy dvuh vidov:
[1] klassy, kotorye pryamo otrazhayut ponyatiya oblasti prilozheniya,
t.e. ponyatiya, kotorye ispol'zuet konechnyj pol'zovatel' dlya
opisaniya svoih zadach i vozmozhnyh reshenij;
i
[2] klassy, kotorye yavlyayutsya produktom samoj realizacii, t.e.
otrazhayut ponyatiya, ispol'zuemye razrabotchikami i programmistami
dlya opisaniya sposobov realizacii.
Nekotorye iz klassov, yavlyayushchihsya produktami realizacii, mogut
predstavlyat' i ponyatiya real'nogo mira. Naprimer, programmnye i
apparatnye resursy sistemy yavlyayutsya horoshimi kandidatami
na rol' klassov, predstavlyayushchih oblast' prilozheniya. |to otrazhaet
tot fakt, chto sistemu mozhno rassmatrivat' s neskol'kih tochek
zreniya, i to, chto s odnoj yavlyaetsya detal'yu realizacii, s
drugoj mozhet byt' ponyatiem oblasti prilozheniya. Horosho
sproektirovannaya sistema dolzhna soderzhat' klassy, kotorye
dayut vozmozhnost' rassmatrivat' sistemu s logicheski
raznyh tochek zreniya. Privedem primer:
[1] klassy, predstavlyayushchie pol'zovatel'skie ponyatiya (naprimer,
legkovye mashiny i gruzoviki),
[2] klassy, predstavlyayushchie obobshcheniya pol'zovatel'skih ponyatij
(dvizhushchiesya sredstva),
[3] klassy, predstavlyayushchie apparatnye resursy (naprimer, klass
upravleniya pamyat'yu),
[4] klassy, predstavlyayushchie sistemnye resursy (naprimer,
vyhodnye potoki),
[5] klassy, ispol'zuemye dlya realizacii drugih klassov (naprimer,
spiski, ocheredi, blokirovshchiki) i
[6] vstroennye tipy dannyh i struktury upravleniya.
V bol'shih sistemah ochen' trudno sohranyat' logicheskoe razdelenie
tipov razlichnyh klassov i podderzhivat' takoe razdelenie mezhdu
razlichnymi urovnyami abstrakcii. V privedennom vyshe perechislenii
predstavleny tri urovnya abstrakcii:
[1+2] predstavlyaet pol'zovatel'skoe otrazhenie sistemy,
[3+4] predstavlyaet mashinu, na kotoroj budet rabotat' sistema,
[5+6] predstavlyaet nizkourovnevoe (so storony yazyka programmirovaniya)
otrazhenie realizacii.
CHem bol'she sistema, tem bol'shee chislo urovnej abstrakcii neobhodimo
dlya ee opisaniya, i tem trudnee opredelyat' i podderzhivat' eti urovni
abstrakcii. Otmetim, chto takim urovnyam abstrakcii est' pryamoe
sootvetstvie v prirode i v razlichnyh postroeniyah chelovecheskogo
intellekta. Naprimer, mozhno rassmatrivat' dom kak ob容kt,
sostoyashchij iz
[1] atomov,
[2] molekul,
[3] dosok i kirpichej,
[4] polov, potolkov i sten;
[5] komnat.
Poka udaetsya hranit' razdel'no predstavleniya etih urovnej abstrakcii,
mozhno podderzhivat' celostnoe predstavlenie o dome. Odnako, esli
smeshat' ih, vozniknet bessmyslica. Naprimer, predlozhenie
"Moj dom sostoit iz neskol'kih tysyach funtov ugleroda, nekotoryh
slozhnyh polimerov, iz 5000 kirpichej, dvuh vannyh komnat i 13
potolkov" - yavno absurdno. Iz-za abstraktnoj prirody
programm podobnoe utverzhdenie o kakoj-libo slozhnoj programmnoj
sisteme daleko ne vsegda vosprinimayut kak bessmyslicu.
V processe proektirovaniya vydelenie ponyatij iz oblasti prilozheniya
v klass vovse ne yavlyaetsya prostoj mehanicheskoj operaciej. Obychno
eta zadacha trebuet bol'shoj pronicatel'nosti. Zametim, chto sami
ponyatiya oblasti prilozheniya yavlyayutsya abstrakciyami. Naprimer, v
prirode ne sushchestvuyut "nalogoplatel'shchiki", "monahi" ili "sotrudniki".
|ti ponyatiya ne chto inoe, kak metki, kotorymi oboznachayut bednuyu
lichnost', chtoby klassificirovat' ee po otnosheniyu k nekotoroj
sisteme. CHasto real'nyj ili voobrazhaemyj mir (naprimer, literatura,
osobenno fantastika) sluzhat istochnikom ponyatij, kotorye kardinal'no
preobrazuyutsya pri perevode ih v klassy. Tak, ekran moego komp'yutera
(Makkintosh) sovsem ne pohodit na poverhnost' moego stola, hotya
komp'yuter sozdavalsya s cel'yu realizovat' ponyatie "nastol'nyj" X,
a okna na moem displee imeyut samoe otdalennoe otnoshenie k
prisposobleniyam dlya prezentacii chertezhej v moej komnate.
X YA by ne vynes takogo besporyadka u sebya na ekrane.
Sut' modelirovaniya real'nosti ne v pokornom sledovanii tomu,
chto my vidim, a v ispol'zovanii real'nosti kak nachala dlya proektirovaniya,
istochnika vdohnoveniya i kak yakorya, kotoryj uderzhivaet, kogda
stihiya programmirovaniya grozit lishit' nas sposobnosti
ponimaniya svoej sobstvennoj programmy.
Zdes' polezno predosterech': novichkam obychno trudno "nahodit'"
klassy, no vskore eto preodolevaetsya bez kakih-libo
nepriyatnostej. Dalee obychno prihodit etap, kogda klassy i otnosheniya
nasledovaniya mezhdu nimi beskontrol'no mnozhatsya. Zdes' uzhe
voznikayut problemy, svyazannye so slozhnost'yu, effektivnost'yu i
yasnost'yu poluchennoj programmy. Daleko ne kazhduyu otdel'nuyu detal'
sleduet predstavlyat' otdel'nym klassom, i daleko ne kazhdoe
otnoshenie mezhdu klassami sleduet predstavlyat' kak otnoshenie
nasledovaniya. Starajtes' ne zabyvat', chto cel' proekta - smodelirovat'
sistemu s podhodyashchim urovnem detalizacii i podhodyashchim urovnem
abstrakcii. Dlya bol'shih sistem najti kompromiss mezhdu prostotoj i
obshchnost'yu daleko ne prostaya zadacha.
Rassmotrim modelirovanie transportnogo potoka v gorode, cel' kotorogo
dostatochno tochno opredelit' vremya, trebuyushcheesya, chtoby avarijnye dvizhushchiesya
sredstva dostigli punkta naznacheniya. Ochevidno, nam nado imet'
predstavleniya legkovyh i gruzovyh mashin, mashin skoroj pomoshchi,
vsevozmozhnyh pozharnyh i policejskih mashin, avtobusov i t.p.
Poskol'ku vsyakoe ponyatie real'nogo mira ne sushchestvuet izolirovanno,
a soedineno mnogochislennymi svyazyami s drugimi ponyatiyami,
voznikaet takoe otnoshenie kak nasledovanie. Ne razobravshis' v ponyatiyah
i ih vzaimnyh svyazyah, my ne v sostoyanii postich' nikakoe otdel'noe
ponyatie. Takzhe i model', esli ne otrazhaet otnosheniya mezhdu
ponyatiyami, ne mozhet adekvatno predstavlyat' sami ponyatiya. Itak, v
nashej programme nuzhny klassy dlya predstavleniya ponyatij, no etogo
nedostatochno. Nam nuzhny sposoby predstavleniya otnoshenij mezhdu klassami.
Nasledovanie yavlyaetsya moshchnym sposobom pryamogo predstavleniya
ierarhicheskih otnoshenij. V nashem primere, my, po vsej vidimosti,
sochli by avarijnye sredstva special'nymi dvizhushchimisya sredstvami
i, pomimo etogo, vydelili by sredstva, predstavlennye legkovymi i
gruzovymi mashinami. Togda ierarhiya klassov priobrela by takoj vid:
dvizhushcheesya sredstvo
legkovaya mashina avarijnoe sredstvo gruzovaya mashina
policejskaya mashina mashina skoroj pomoshchi pozharnaya mashina
mashina s vydvizhnoj lestnicej
Zdes' klass Emergency predstavlyaet vsyu informaciyu, neobhodimuyu dlya
modelirovaniya avarijnyh dvizhushchihsya sredstv, naprimer: avarijnaya
mashina mozhet narushat' nekotorye pravila dvizheniya, ona imeet
prioritet na perekrestkah, nahoditsya pod kontrolem dispetchera
i t.d.
Na S++ eto mozhno zadat' tak:
class Vehicle { /*...*/ };
class Emergency { /* */ };
class Car : public Vehicle { /*...*/ };
class Truck : public Vehicle { /*...*/ };
class Police_car : public Car , public Emergency {
//...
};
class Ambulance : public Car , public Emergency {
//...
};
class Fire_engine : public Truck , Emergency {
//...
};
class Hook_and_ladder : public Fire_engine {
//...
};
Nasledovanie - eto otnoshenie samogo vysokogo poryadka, kotoroe pryamo
predstavlyaetsya v S++ i ispol'zuetsya preimushchestvenno na rannih
etapah proektirovaniya. CHasto voznikaet problema vybora: ispol'zovat'
nasledovanie dlya predstavleniya otnosheniya ili predpochest' emu
prinadlezhnost'. Rassmotrim drugoe opredelenie ponyatiya avarijnogo
sredstva: dvizhushcheesya sredstvo schitaetsya avarijnym, esli ono
neset sootvetstvuyushchij svetovoj signal. |to pozvolit uprostit'
ierarhiyu klassov, zameniv klass Emergency na chlen klassa
Vehicle:
dvizhushcheesya sredstvo (Vehicle {eptr})
legkovaya mashina (Car) gruzovaya mashina (Truck)
policejskaya mashina (Police_car) mashina skoroj pomoshchi (Ambulance)
pozharnaya mashina (Fire_engine)
mashina s vydvizhnoj lestnicej (Hook_and_ladder)
Teper' klass Emergency ispol'zuetsya prosto kak chlen v teh klassah,
kotorye predstavlyayut avarijnye dvizhushchiesya sredstva:
class Emergency { /*...*/ };
class Vehicle { public: Emergency* eptr; /*...*/ };
class Car : public Vehicle { /*...*/ };
class Truck : public Vehicle { /*...*/ };
class Police_car : public Car { /*...*/ };
class Ambulance : public Car { /*...*/ };
class Fire_engine : public Truck { /*...*/ };
class Hook_and_ladder : public Fire_engine { /*...*/ };
Zdes' dvizhushcheesya sredstvo schitaetsya avarijnym, esli Vehicle::eptr
ne ravno nulyu. "Prostye" legkovye i gruzovye mashiny inicializiruyutsya
Vehicle::eptr ravnym nulyu, a dlya drugih Vehicle::eptr dolzhno byt'
ustanovleno v nenulevoe znachenie, naprimer:
Car::Car() // konstruktor Car
{
eptr = 0;
}
Police_car::Police_car() // konstruktor Police_car
{
eptr = new Emergency;
}
Takie opredeleniya uproshchayut preobrazovanie avarijnogo sredstva v
obychnoe i naoborot:
void f(Vehicle* p)
{
delete p->eptr;
p->eptr = 0; // bol'she net avarijnogo dvizhushchegosya sredstva
//...
p->eptr = new Emergency; // ono poyavilos' snova
}
Tak kakoj zhe variant ierarhii klassov luchshe? V obshchem sluchae otvet takoj:
"Luchshej yavlyaetsya programma, kotoraya naibolee neposredstvenno otrazhaet
real'nyj mir". Inymi slovami, pri vybore modeli my dolzhny stremit'sya
k bol'shej ee"real'nosti", no s uchetom neizbezhnyh ogranichenij,
nakladyvaemyh trebovaniyami prostoty i effektivnosti. Poetomu,
nesmotrya na prostotu preobrazovaniya obychnogo dvizhushchegosya sredstva v
avarijnoe, vtoroe reshenie predstavlyaetsya nepraktichnym.
Pozharnye mashiny i mashiny skoroj pomoshchi - eto
dvizhushchiesya sredstva special'nogo naznacheniya so special'no
podgotovlennym personalom, oni dejstvuyut pod upravleniem komand
dispetchera, trebuyushchih special'nogo oborudovaniya dlya svyazi. Takoe
polozhenie oznachaet, chto prinadlezhnost' k avarijnym dvizhushchimsya sredstvam -
eto bazovoe ponyatie, kotoroe dlya uluchsheniya kontrolya tipov i
primeneniya razlichnyh programmnyh sredstv dolzhno byt' pryamo
predstavleno v programme. Esli by my modelirovali situaciyu, v kotoroj
naznachenie dvizhushchihsya sredstv ne stol' opredelenno,
skazhem, situaciyu, v kotoroj chastnyj transport periodicheski ispol'zuetsya
dlya dostavki special'nogo personala k mestu proisshestviya, a svyaz'
obespechivaetsya s pomoshch'yu portativnyh priemnikov, togda mog by
okazat'sya podhodyashchim i drugoj sposob modelirovaniya sistemy.
Dlya teh, kto schitaet primer modelirovaniya dvizheniya transporta
ekzotichnym, imeet smysl skazat', chto v processe proektirovaniya
pochti postoyanno voznikaet podobnyj vybor mezhdu nasledovaniem
i prinadlezhnost'yu. Analogichnyj primer est' v $$12.2.5, gde
opisyvaetsya svitok (scrollbar) - prokruchivanie informacii v okne.
12.2.3 Zavisimosti v ramkah ierarhii klassov.
Estestvenno, proizvodnyj klass zavisit ot svoih bazovyh klassov.
Gorazdo rezhe uchityvayut, chto obratnoe takzhe mozhet byt'
spravedlivoX.
X |tu mysl' mozhno vyrazit' takim sposobom: "Sumasshestvie nasleduetsya,
vy mozhete poluchit' ego ot svoih detej."
Esli klass soderzhit virtual'nuyu funkciyu, proizvodnye klassy mogut
po svoemu usmotreniyu reshat', realizovyvat' li chast' operacij etoj
funkcii kazhdyj raz, kogda ona pereopredelyaetsya v proizvodnom
klasse. Esli chlen bazovogo klassa sam vyzyvaet odnu iz virtual'nyh
funkcij proizvodnogo klassa, togda realizaciya bazovogo klassa
zavisit ot realizacij ego proizvodnyh klassov. Tochno tak zhe, esli
klass ispol'zuet zashchishchennyj chlen, ego realizaciya budet zaviset' ot
proizvodnyh klassov. Rassmotrim opredeleniya:
class B {
//...
protected:
int a;
public:
virtual int f();
int g() { int x = f(); return x-a; }
};
Kakov rezul'tat raboty g()? Otvet sushchestvenno zavisit ot opredeleniya
f() v nekotorom proizvodnom klasse. Nizhe privoditsya variant, pri
kotorom g() budet vozvrashchat' 1:
class D1 : public B {
int f() { return a+1; }
};
a pri nizhesleduyushchem opredelenii g() napechataet "Hello, World" i vernet 0:
class D1 : public {
int f() { cout<<"Hello, World\n"; return a; }
};
|tot primer demonstriruet odin iz vazhnejshih momentov, svyazannyh
s virtual'nymi funkciyami. Hotya vy mozhete skazat', chto eto
glupost', i programmist nikogda ne napishet nichego podobnogo.
Delo zdes' v tom, chto virtual'naya funkciya yavlyaetsya chast'yu
interfejsa s bazovym klassom, i chto etot klass budet, po vsej
vidimosti, ispol'zovat'sya bez informacii o ego proizvodnyh klassah.
Sledovatel'no, mozhno tak opisat' povedenie ob容kta bazovogo klassa,
chtoby v dal'nejshem pisat' programmy, nichego ne znaya o ego proizvodnyh
klassah.
Vsyakij klass, kotoryj pereopredelyaet proizvodnuyu funkciyu, dolzhen
realizovat' variant etoj funkcii. Naprimer, virtual'naya funkciya
rotate() iz klassa Shape vrashchaet geometricheskuyu figuru, a funkcii
rotate() dlya proizvodnyh klassov, takih, kak Circle i Triangle,
dolzhny vrashchat' ob容kty sootvetstvuyushchih tipov, inache budet narusheno
osnovnoe polozhenie o klasse Shape. No o povedenii klassa B ili ego
proizvodnyh klassov D1 i D2 ne sformulirovano nikakih polozhenij,
poetomu privedennyj primer i kazhetsya nerazumnym. Pri postroenii
klassa glavnoe vnimanie sleduet udelyat' opisaniyu ozhidaemyh
dejstvij virtual'nyh funkcij.
Sleduet li schitat' normal'noj zavisimost' ot neizvestnyh
(vozmozhno eshche neopredelennyh) proizvodnyh klassov? Otvet, estestvenno,
zavisit ot celej programmista. Esli cel' sostoit v tom, chtoby
izolirovat' klass ot vsyakih vneshnih vliyanij i, tem samym, dokazat',
chto on vedet sebya opredelennym obrazom, to luchshe izbegat'
virtual'nyh funkcij i zashchishchennyh chlenov. Esli cel' sostoit v tom,
chtoby razrabotat' strukturu, v kotoruyu posleduyushchie programmisty
(ili vy sami cherez nedelyu) smogut vstraivat' svoi programmy, to imenno
virtual'nye funkcii i predlagayut elegantnyj sposob resheniya,
a zashchishchennye chleny mogut byt' polezny pri ego realizacii.
V kachestve primera rassmotrim prostoj shablon tipa, opredelyayushchij
bufer:
template<class T> class buffer {
// ...
void put(T);
T get();
};
Esli reakciya na perepolnenie i obrashchenie k pustomu buferu, "zapayana"
v sam klass, ego primenenie budet ogranicheno. No esli funkcii put()
i get() obrashchayutsya k virtual'nym funkciyam overflow() i underflow()
sootvetstvenno, to pol'zovatel' mozhet, udovletvoryaya svoim
nuzhdam, sozdat' bufera razlichnyh tipov:
template<class T> class buffer {
//...
virtual int overflow(T);
virtual int underflow();
void put(T); // vyzvat' overflow(T), kogda bufer polon
T get(); // vyzvat' underflow(T), kogda bufer pust
};
template<class T> class circular_buffer : public buffer<T> {
//...
int overflow(T); // perejti na nachalo bufera, esli on polon
int underflow();
};
template<class T> class expanding_buffer : public buffer<T> {
//...
int overflow(T); // uvelichit' razmer bufera, esli on polon
int underflow();
};
|tot metod ispol'zovalsya v bibliotekah potokovogo vvoda-vyvoda
($$10.5.3).
12.2.4 Otnosheniya prinadlezhnosti
Esli ispol'zuetsya otnoshenie prinadlezhnosti, to sushchestvuet dva osnovnyh
sposoba predstavleniya ob容kta klassa X:
[1] Opisat' chlen tipa X.
[2] Opisat' chlen tipa X* ili X&.
Esli znachenie ukazatelya ne budet menyat'sya i voprosy
effektivnosti ne volnuyut, eti sposoby ekvivalentny:
class X {
//...
public:
X(int);
//...
};
class C {
X a;
X* p;
public:
C(int i, int j) : a(i), p(new X(j)) { }
~C() { delete p; }
};
V takih situaciyah predpochtitel'nee neposredstvennoe chlenstvo ob容kta,
kak X::a v primere vyshe, potomu chto ono daet ekonomiyu
vremeni, pamyati i kolichestva vvodimyh simvolov. Obratites' takzhe
k $$12.4 i $$13.9.
Sposob, ispol'zuyushchij ukazatel', sleduet primenyat' v teh
sluchayah, kogda prihoditsya perestraivat' ukazatel' na
"ob容kt-element" v techenii zhizni "ob容kta-vladel'ca". Naprimer:
class C2 {
X* p;
public:
C(int i) : p(new X(i)) { }
~C() { delete p; }
X* change(X* q)
{
X* t = p;
p = q;
return t;
}
};
CHlen tipa ukazatel' mozhet takzhe ispol'zovat'sya, chtoby dat' vozmozhnost'
peredavat' "ob容kt-element" v kachestve parametra:
class C3 {
X* p;
public:
C(X* q) : p(q) { }
// ...
}
Razreshaya ob容ktam soderzhat' ukazateli na drugie ob容kty, my sozdaem
to, chto obychno nazyvaetsya "ierarhiya ob容ktov". |to al'ternativnyj
i vspomogatel'nyj sposob strukturirovaniya po otnosheniyu k ierarhii
klassov. Kak bylo pokazano na primere avarijnogo dvizhushchegosya
sredstva v $$12.2.2, chasto eto dovol'no tonkij vopros proektirovaniya:
predstavlyat' li svojstvo klassa kak eshche odin bazovyj klass
ili kak chlen klassa. Potrebnost' v pereopredelenii sleduet schitat'
ukazaniem, chto pervyj variant luchshe. No esli nado imet'
vozmozhnost' predstavlyat' nekotoroe svojstvo s pomoshch'yu razlichnyh
tipov, to luchshe ostanovit'sya na vtorom variante. Naprimer:
class XX : public X { /*...*/ };
class XXX : public X { /*...*/ };
void f()
{
C3* p1 = new C3(new X); // C3 "soderzhit" X
C3* p2 = new C3(new XX); // C3 "soderzhit" XX
C3* p3 = new C3(new XXX); // C3 "soderzhit" XXX
//...
}
Privedennye opredeleniya nel'zya smodelirovat' ni s pomoshch'yu proizvodnogo
klassa C3 ot X, ni s pomoshch'yu C3, imeyushchego chlen tipa X, poskol'ku
neobhodimo ukazyvat' tochnyj tip chlena. |to vazhno dlya klassov s
virtual'nymi funkciyami, takih, naprimer,kak klass Shape ($$1.1.2.5), i
dlya klassa abstraktnogo mnozhestva ($$13.3).
Zametim, chto ssylki mozhno primenyat' dlya uproshcheniya klassov,
ispol'zuyushchih chleny-ukazateli, esli v techenie zhizni ob容kta-vladel'ca
ssylka nastroena tol'ko na odin ob容kt, naprimer:
class C4 {
X& r;
public:
C(X& q) : r(q) { }
// ...
};
12.2.5 Prinadlezhnost' i nasledovanie
Uchityvaya slozhnost' vazhnost' otnoshenij nasledovaniya, net nichego
udivitel'nogo v tom, chto chasto ih nepravil'no ponimayut i ispol'zuyut
sverh mery. Esli klass D opisan kak obshchij proizvodnyj ot
klassa B, to chasto govoryat, chto D est' B:
class B { /* ... */ ;
class D : public B /* ... */ }; // D sorta B
Inache eto mozhno sformulirovat' tak: nasledovanie - eto
otnoshenie "est'", ili, bolee tochno dlya klassov D i B, nasledovanie
- eto otnoshenie D sorta B. V otlichie ot etogo, esli klass D
soderzhit v kachestve chlena drugoj klass B, to govoryat, chto
D "imeet" B:
class D { // D imeet B
// ...
public:
B b;
// ...
};
Inymi slovami, prinadlezhnost' - eto otnoshenie "imet'" ili
dlya klassov D i B prosto: D soderzhit B.
Imeya dva klassa B i D, kak vybirat' mezhdu nasledovaniem i
prinadlezhnost'yu? Rassmotrim klassy samolet i motor.Novichki obychno
sprashivayut: budet li horoshim resheniem sdelat' klass samolet
proizvodnym ot klassa motor. |to plohoe reshenie, poskol'ku
samolet ne "est'" motor, samolet "imeet" motor. Sleduet podojti
k etomu voprosu, rassmotrev, mozhet li samolet "imet'" dva ili
bol'she motorov. Poskol'ku eto predstavlyaetsya vpolne vozmozhnym
(dazhe esli my imeem delo s programmoj, v kotoroj vse samolety
budut s odnim motorom), sleduet ispol'zovat' prinadlezhnost', a
ne nasledovanie. Vopros "Mozhet li on imet' dva..?" okazyvaetsya
udivitel'no poleznym vo mnogih somnitel'nyh sluchayah. Kak vsegda,
nashe izlozhenie zatragivaet neulovimuyu sushchnost' programmirovaniya.
Esli by vse klassy bylo tak zhe legko predstavit', kak samolet i
motor, to bylo by prosto izbezhat' i trivial'nyh oshibok tipa toj,
kogda samolet opredelyaetsya kak proizvodnoe ot klassa motor. Odnako,
takie oshibki dostatochno chasty, osobenno u teh, kto
schitaet nasledovanie eshche odnim mehanizmom dlya sochetaniya
konstrukcij yazyka programmirovaniya. Nesmotrya na udobstvo i
lakonichnost' zapisi, kotoruyu predostavlyaet nasledovanie, ego
nado ispol'zovat' tol'ko dlya vyrazheniya teh otnoshenij,
kotorye chetko opredeleny v proekte. Rassmotrim opredeleniya:
class B {
public:
virtual void f();
void g();
};
class D1 { // D1 soderzhit B
public:
B b;
void f(); // ne pereopredelyaet b.f()
};
void h1(D1* pd)
{
B* pb = pd; // oshibka: nevozmozhno preobrazovanie D1* v B*
pb = &pd->b;
pb->q(); // vyzov B::q
pd->q(); // oshibka: D1 ne imeet chlen q()
pd->b.q();
pb->f(); // vyzov B::f (zdes' D1::f ne pereopredelyaet)
pd->f(); // vyzov D1::f
}
Obratite vnimanie, chto v etom primere net neyavnogo preobrazovaniya
klassa k odnomu iz ego elementov, i chto klass, soderzhashchij v
kachestve chlena drugoj klass, ne pereopredelyaet virtual'nye
funkcii etogo chlena. Zdes' yavnoe otlichie ot primera, privedennogo
nizhe:
class D2 : public B { // D2 est' B
public:
void f(); // pereopredelenie B::f()
};
void h2(D2* pd)
{
B* pb = pd; // normal'no: D2* neyavno preobrazuetsya v B*
pb->q(); // vyzov B::q
pd->q(); // vyzov B::q
pb->f(); // vyzov virtual'noj funkcii: obrashchenie k D2::f
pd->f(); // vyzov D2::f
}
Udobstvo zapisi, prodemonstrirovannoe v primere s klassom D2, po
sravneniyu s zapis'yu v primere s klassom D1, yavlyaetsya prichinoj, po
kotoroj takim nasledovaniem zloupotreblyayut. No sleduet pomnit',
chto sushchestvuet opredelennaya plata za udobstvo zapisi v vide
vozrosshej zavisimosti mezhdu B i D2 (sm. $$12.2.3). V chastnosti,
legko zabyt' o neyavnom preobrazovanii D2 v B. Esli tol'ko takie
preobrazovaniya ne otnosyatsya k semantike vashih klassov, sleduet
izbegat' opisaniya proizvodnogo klassa v obshchej chasti. Esli klass
predstavlyaet opredelennoe ponyatie, a nasledovanie ispol'zuetsya
kak otnoshenie "est'", to takie preobrazovaniya obychno kak raz to,
chto nuzhno.
Odnako, byvayut takie situacii, kogda zhelatel'no
imet' nasledovanie, no nel'zya dopuskat' preobrazovaniya. Rassmotrim
zadanie klassa cfield (controled field - upravlyaemoe pole), kotoryj,
pomimo vsego prochego, daet vozmozhnost' kontrolirovat' na stadii
vypolneniya dostup k drugomu klassu field. Na pervyj vzglyad kazhetsya
sovershenno pravil'nym opredelit' klass cfield kak proizvodnyj ot
klassa field:
class cfield : public field {
// ...
};
|to vyrazhaet tot fakt, chto cfield, dejstvitel'no, est' sorta field,
uproshchaet zapis' funkcii, kotoraya ispol'zuet chlen chasti field klassa
cfield, i, chto samoe glavnoe, pozvolyaet v klasse cfield
pereopredelyat' virtual'nye funkcii iz field. Zagvozdka zdes' v tom,
chto preobrazovanie cfield* k field*, vstrechayushcheesya v opredelenii
klassa cfield, pozvolyaet obojti lyuboj kontrol' dostupa k field:
void q(cfield* p)
{
*p = "asdf"; // obrashchenie k field kontroliruetsya
// funkciej prisvaivaniya cfield:
// p->cfield::operator=("asdf")
field* q = p; // neyavnoe preobrazovanie cfield* v field*
*q = "asdf"; // priehali! kontrol' obojden
}
Mozhno bylo by opredelit' klass cfield tak, chtoby field byl ego chlenom,
no togda cfield ne mozhet pereopredelyat' virtual'nye funkcii field.
Luchshim resheniem zdes' budet ispol'zovanie nasledovaniya so specifikaciej
private (chastnoe nasledovanie):
class cfield : private field { /* ... */ }
S pozicii proektirovaniya, esli ne uchityvat' (inogda vazhnye) voprosy
pereopredeleniya, chastnoe nasledovanie ekvivalentno prinadlezhnosti.
V etom sluchae primenyaetsya metod, pri kotorom klass opredelyaetsya
v obshchej chasti kak proizvodnyj ot abstraktnogo bazovogo klassa zadaniem
ego interfejsa, a takzhe opredelyaetsya s pomoshch'yu chastnogo nasledovaniya ot
konkretnogo klassa, zadayushchego realizaciyu ($$13.3). Poskol'ku
nasledovanie, ispol'zuemoe kak chastnoe, yavlyaetsya
specifikoj realizacii, i ono ne otrazhaetsya v tipe proizvodnogo klassa,
to ego inogda nazyvayut "nasledovaniem po realizacii", i ono
yavlyaetsya kontrastom dlya nasledovaniya v obshchej chasti, kogda nasleduetsya
interfejs bazovogo klassa i dopustimy neyavnye preobrazovaniya k
bazovomu tipu. Poslednee nasledovanie inogda nazyvayut opredeleniem
podtipa ili "interfejsnym nasledovaniem".
Dlya dal'nejshego obsuzhdeniya vozmozhnosti vybora nasledovaniya
ili prinadlezhnosti rassmotrim, kak predstavit' v dialogovoj
graficheskoj sisteme svitok (oblast' dlya prokruchivaniya v nej
informacii), i kak privyazat' svitok k oknu na ekrane. Potrebuyutsya
svitki dvuh vidov: gorizontal'nye i vertikal'nye. |to mozhno
predstavit' s pomoshch'yu dvuh tipov horizontal_scrollbar i
vertical_scrollbar ili s pomoshch'yu odnogo tipa scrollbar, kotoryj
imeet argument, opredelyayushchij, yavlyaetsya raspolozhenie vertikal'nym
ili gorizontal'nym. Pervoe reshenie predpolagaet, chto est' eshche
tretij tip, zadayushchij prosto svitok - scrollbar, i etot tip
yavlyaetsya bazovym klassom dlya dvuh opredelennyh svitkov. Vtoroe
reshenie predpolagaet dopolnitel'nyj argument u tipa scrollbar i
nalichie znachenij, zadayushchih vid svitka. Naprimer, tak:
enum orientation { horizontal, vertical };
Kak tol'ko my ostanovimsya na odnom iz reshenij, opredelitsya
ob容m izmenenij, kotorye pridetsya vnesti v sistemu. Dopustim, v
etom primere nam potrebuetsya vvesti svitki tret'ego vida. Vnachale
predpolagalos', chto mogut byt' svitki tol'ko dvuh vidov (ved'
vsyakoe okno imeet tol'ko dva izmereniya), no v etom primere,
kak i vo mnogih drugih, vozmozhny rasshireniya, kotorye voznikayut
kak voprosy pereproektirovaniya. Naprimer, mozhet poyavit'sya
zhelanie ispol'zovat' "upravlyayushchuyu knopku" (tipa myshi) vmesto svitkov
dvuh vidov. Takaya knopka zadavala by prokrutku v razlichnyh
napravleniyah v zavisimosti ot togo, v kakoj chasti okna nazhal
ee pol'zovatel'. Nazhatie v seredine verhnej strochki dolzhno
vyzyvat' "prokruchivanie vverh", nazhatie v seredine levogo stolbca -
"prokruchivanie vlevo", nazhatie v levom verhnem uglu -
"prokruchivanie vverh i vlevo". Takaya knopka ne yavlyaetsya
chem-to neobychnym, i ee mozhno rassmatrivat' kak utochnenie ponyatiya
svitka, kotoroe osobenno podhodit dlya teh oblastej prilozheniya,
kotorye svyazany ne s obychnymi tekstami, a s bolee slozhnoj
informaciej.
Dlya dobavleniya upravlyayushchej knopki k programme, ispol'zuyushchej
ierarhiyu iz treh svitkov, trebuetsya dobavit' eshche odin klass, no
ne nuzhno menyat' programmu, rabotayushchuyu so starymi svitkami:
svitok
gorizontal'nyj_svitok vertikal'nyj_svitok upravlyayushchaya_knopka
|to polozhitel'naya storona "ierarhicheskogo resheniya".
Zadanie orientacii svitka v kachestve parametra privodit k
zadaniyu polej tipa v ob容ktah svitka i ispol'zovaniyu pereklyuchatelej
v tele funkcij-chlenov svitka. Inymi slovami, pered nami obychnaya
dilemma: vyrazit' dannyj aspekt struktury sistemy s pomoshch'yu
opredelenij ili realizovat' ego v operatornoj chasti programmy.
Pervoe reshenie uvelichivaet ob容m staticheskih proverok i ob容m
informacii, nad kotoroj mogut rabotat' raznye vspomogatel'nye
sredstva. Vtoroe reshenie otkladyvaet proverki na stadiyu vypolneniya
i razreshaet menyat' tela otdel'nyh funkcij, ne izmenyaya obshchuyu
strukturu sistemy, kakoj ona predstavlyaetsya s tochki zreniya
staticheskogo kontrolya ili vspomogatel'nyh sredstv. V bol'shinstve
sluchaev, predpochtitel'nee pervoe reshenie.
Polozhitel'noj storonoj resheniya s edinym tipom svitka yavlyaetsya to,
chto legko peredavat' informaciyu o vide nuzhnogo nam svitka drugoj
funkcii:
void helper(orientation oo)
{
//...
p = new scrollbar(oo);
//...
}
void me()
{
helper(horizontal);
}
Takoj podhod pozvolyaet na stadii vypolneniya legko perenastroit' svitok
na druguyu orientaciyu. Vryad li eto ochen' vazhno v primere so svitkami,
no eto mozhet okazat'sya sushchestvennym v pohozhih primerah. Sut' v tom,
chto vsegda nado delat' opredelennyj vybor, a eto chasto neprosto.
Teper' rassmotrim kak privyazat' svitok k oknu. Esli schitat'
window_with_scrollbar (okno_so_svitkom) kak nechto, chto yavlyaetsya
window i scrollbar, my poluchim podobnoe:
class window_with_scrollbar
: public window, public scrollbar {
// ...
};
|to pozvolyaet lyubomu ob容ktu tipa window_with_scrollbar vystupat'
i kak window, i kak scrollbar, no ot nas trebuetsya reshenie
ispol'zovat' tol'ko edinstvennyj tip scrollbar.
Esli, s drugoj storony, schitat' window_with_scrollbar ob容ktom
tipa window, kotoryj imeet scrollbar, my poluchim takoe opredelenie:
class window_with_scrollbar : public window {
// ...
scrollbar* sb;
public:
window_with_scrollbar(scrollbar* p, /* ... */)
: window(/* ... */), sb(p)
{
// ...
}
// ...
};
Zdes' my mozhem ispol'zovat' reshenie so svitkami treh tipov. Peredacha
samogo svitka v kachestve parametra pozvolyaet oknu (window) ne
zapominat' tip ego svitka. Esli potrebuetsya, chtoby ob容kt tipa
window_with_scrollbar dejstvoval kak scrollbar, mozhno dobavit'
operaciyu preobrazovaniya:
window_with_scrollbar :: operator scrollbar&()
{
return *sb;
}
12.2.6 Otnosheniya ispol'zovaniya
Dlya sostavleniya i ponimaniya proekta chasto neobhodimo znat',
kakie klassy i kakim sposobom ispol'zuet dannyj klass.
Takie otnosheniya klassov
na S++ vyrazhayutsya neyavno. Klass mozhet ispol'zovat' tol'ko te
imena, kotorye gde-to opredeleny, no net takoj chasti v programme
na S++, kotoraya soderzhala by spisok vseh ispol'zuemyh imen.
Dlya polucheniya takogo spiska neobhodimy
vspomogatel'nye sredstva (ili, pri ih otsutstvii, vnimatel'noe
chtenie). Mozhno sleduyushchim obrazom klassificirovat' te sposoby,
s pomoshch'yu kotoryh klass X mozhet ispol'zovat' klass Y:
- X ispol'zuet imya Y
- X ispol'zuet Y
- X vyzyvaet funkciyu-chlen Y
- X chitaet chlen Y
- X pishet v chlen Y
- X sozdaet Y
- X razmeshchaet auto ili static peremennuyu iz Y
- X sozdaet Y s pomoshch'yu new
- X ispol'zuet razmer Y
My otnesli ispol'zovanie razmera ob容kta k ego sozdaniyu, poskol'ku
dlya etogo trebuetsya znanie polnogo opredeleniya klassa. S drugoj
storony, my vydelili v otdel'noe otnoshenie ispol'zovanie imeni Y,
poskol'ku, ukazyvaya ego v opisanii Y* ili v opisanii
vneshnej funkcii, my vovse ne nuzhdaemsya v dostupe k opredeleniyu Y:
class Y; // Y - imya klassa
Y* p;
extern Y f(const Y&);
My otdelili sozdanie Y s pomoshch'yu new ot sluchaya opisaniya
peremennoj, poskol'ku vozmozhna takaya realizaciya S++, pri kotoroj
dlya sozdaniya Y s pomoshch'yu new neobyazatel'no znat'
razmer Y. |to mozhet byt' sushchestvenno dlya ogranicheniya vseh zavisimostej
v proekte i svedeniya k minimumu peretranslyacii posle vneseniya izmenenij.
YAzyk S++ ne trebuet, chtoby sozdatel' klassov tochno opredelyal,
kakie klassy i kak on budet ispol'zovat'. Odna iz prichin etogo
zaklyuchena v tom, chto samye vazhnye klassy zavisyat ot stol' bol'shogo
kolichestva drugih klassov, chto dlya pridaniya luchshego vida programme
nuzhna sokrashchennaya forma zapisi spiska ispol'zuemyh klassov, naprimer,
s pomoshch'yu komandy #include. Drugaya prichina v tom, chto klassifikaciya
etih zavisimostej i, v chastnosti, ob枸dinenie nekotoryh zavisimostej
ne yavlyaetsya obyazannost'yu yazyka programmirovaniya. Naoborot, celi
razrabotchika, programmista ili vspomogatel'nogo sredstva opredelyayut to,
kak imenno sleduet rassmatrivat' otnosheniya ispol'zovaniya. Nakonec, to,
kakie zavisimosti predstavlyayut bol'shij interes, mozhet zaviset' ot
specifiki realizacii yazyka.
12.2.7 Otnosheniya vnutri klassa
Do sih por my obsuzhdali tol'ko klassy, i hotya operacii upominalis',
esli ne schitat' obsuzhdeniya shagov processa razvitiya programmnogo
obespecheniya ($$11.3.3.2), to oni byli na vtorom plane, ob容kty zhe
prakticheski voobshche ne upominalis'. Ponyat' eto prosto: v S++
klass, a ne funkciya ili ob容kt, yavlyaetsya osnovnym ponyatiem
organizacii sistemy.
Klass mozhet skryvat' v sebe vsyakuyu specifiku realizacii,
naravne s "gryaznymi" priemami programmirovaniya, a inogda on
vynuzhden eto delat'. V to zhe vremya ob容kty bol'shinstva klassov
sami obrazuyut regulyarnuyu strukturu i ispol'zuyutsya takimi sposobami,
chto ih dostatochno prosto opisat'. Ob容kt klassa mozhet byt'
sovokupnost'yu drugih vlozhennyh ob容ktov (ih chasto nazyvayut chlenami),
mnogie iz kotoryh, v svoyu ochered', yavlyayutsya ukazatelyami ili ssylkami
na drugie ob容kty. Poetomu otdel'nyj ob容kt mozhno rassmatrivat' kak
koren' dereva ob容ktov, a vse vhodyashchie v nego ob容kty kak "ierarhiyu
ob容ktov", kotoraya dopolnyaet ierarhiyu klassov, rassmotrennuyu v $$12.2.4.
Rassmotrim v kachestve primera klass strok iz $$7.6:
class String {
int sz;
char* p;
public:
String(const char* q);
~String();
//...
};
Ob容kt tipa String mozhno izobrazit' tak:
Znachenie chlenov ili ob容ktov, dostupnyh s pomoshch'yu chlenov klassa,
nazyvaetsya sostoyaniem ob容kta (ili prosto znacheniem ob容kta).
Glavnoe pri postroenii klassa - eto: privesti ob容kt v polnost'yu
opredelennoe sostoyanie (inicializaciya), sohranyat' polnost'yu opredelennoe
sostoyanie ob枸kta v processe vypolneniya nad nim razlichnyh operacij,
i v konce raboty unichtozhit' ob容kt bez vsyakih posledstvij. Svojstvo,
kotoroe delaet sostoyanie ob容kta polnost'yu opredelennym, nazyvaetsya
invariantom.
Poetomu naznachenie inicializacii - zadat' konkretnye znacheniya,
pri kotoryh vypolnyaetsya invariant ob容kta. Dlya kazhdoj operacii klassa
predpolagaetsya, chto invariant dolzhen imet' mesto pered vypolneniem
operacii i dolzhen sohranit'sya posle operacii. V konce raboty
destruktor narushaet invariant, unichtozhaya ob容kt. Naprimer,
konstruktor String::String(const char*) garantiruet,
chto p ukazyvaet na massiv iz, po krajnej mere, sz elementov, prichem
sz imeet osmyslennoe znachenie i v[sz-1]==0. Lyubaya strokovaya operaciya
ne dolzhna narushat' eto utverzhdenie.
Pri proektirovanii klassa trebuetsya bol'shoe iskusstvo, chtoby
sdelat' realizaciyu klassa dostatochno prostoj i dopuskayushchej
nalichie poleznyh invariantov, kotorye neslozhno zadat'. Legko
trebovat', chtoby klass imel invariant, trudnee predlozhit' poleznyj
invariant, kotoryj ponyaten i ne nakladyvaet zhestkih ogranichenij
na dejstviya razrabotchika klassa ili na effektivnost' realizacii.
Zdes' "invariant" ponimaetsya kak programmnyj fragment,
vypolniv kotoryj, mozhno proverit' sostoyanie ob容kta. Vpolne vozmozhno
dat' bolee strogoe i dazhe matematicheskoe opredelenie invarianta, i v
nekotoryh situaciyah ono mozhet okazat'sya bolee podhodyashchim. Zdes' zhe
pod invariantom ponimaetsya prakticheskaya, a znachit, obychno ekonomnaya,
no nepolnaya proverka sostoyaniya ob容kta.
Ponyatie invarianta poyavilos' v rabotah Flojda, Naura i Hora,
posvyashchennyh pred- i post-usloviyam, ono vstrechaetsya vo vseh vazhnyh
stat'yah po abstraktnym tipam dannyh i verifikacii programm za
poslednie 20 let. Ono zhe yavlyaetsya osnovnym predmetom otladki v C++.
Obychno, v techenie raboty funkcii-chlena invariant ne sohranyaetsya.
Poetomu funkcii, kotorye mogut vyzyvat'sya v te momenty, kogda
invariant ne dejstvuet, ne dolzhny vhodit' v obshchij interfejs klassa.
Takie funkcii dolzhny byt' chastnymi ili zashchishchennymi.
Kak mozhno vyrazit' invariant v programme na S++? Prostoe reshenie -
opredelit' funkciyu, proveryayushchuyu invariant, i vstavit' vyzovy etoj
funkcii v obshchie operacii. Naprimer:
class String {
int sz;
int* p;
public:
class Range {};
class Invariant {};
void check();
String(const char* q);
~String();
char& operator[](int i);
int size() { return sz; }
//...
};
void String::check()
{
if (p==0 || sz<0 || TOO_LARGE<=sz || p[sz-1])
throw Invariant;
}
char& String::operator[](int i)
{
check(); // proverka na vhode
if (i<0 || i<sz) throw Range; // dejstvuet
check(); // proverka na vyhode
return v[i];
}
|tot variant prekrasno rabotaet i ne oslozhnyaet zhizn' programmista.
No dlya takogo prostogo klassa kak String proverka invarianta budet
zanimat' bol'shuyu chast' vremeni scheta. Poetomu programmisty obychno
vypolnyayut proverku invarianta tol'ko pri otladke:
inline void String::check()
{
if (!NDEBUG)
if (p==0 || sz<0 || TOO_LARGE<=sz || p[sz])
throw Invariant;
}
My vybrali imya NDEBUG, poskol'ku eto makroopredelenie, kotoroe
ispol'zuetsya dlya analogichnyh celej v standartnom makroopredelenii
S assert(). Tradicionno NDEBUG ustanavlivaetsya s cel'yu ukazat',
chto otladki net. Ukazav, chto check() yavlyaetsya podstanovkoj, my
garantirovali, chto nikakaya programma ne budet sozdana, poka konstanta
NDEBUG ne budet ustanovlena v znachenie, oboznachayushchee otladku.
S pomoshch'yu shablona tipa Assert() mozhno zadat' menee regulyarnye
utverzhdeniya, naprimer:
template<class T, class X> inline void Assert(T expr,X x)
{
if (!NDEBUG)
if (!expr) throw x;
}
vyzovet osobuyu situaciyu x, esli expr lozhno, i my ne otklyuchili
proverku s pomoshch'yu NDEBUG. Ispol'zovat' Assert() mozhno tak:
class Bad_f_arg { };
void f(String& s, int i)
{
Assert(0<=i && i<s.size(),Bad_f_arg());
//...
}
SHablon tipa Assert() podrazhaet makrokomande assert() yazyka S.
Esli i ne nahoditsya v trebuemom diapazone, voznikaet osobaya
situaciya Bad_f_arg.
S pomoshch'yu otdel'noj konstanty ili konstanty iz klassa proverit'
podobnye utverzhdeniya ili invarianty - pustyakovoe delo. Esli zhe
neobhodimo proverit' invarianty s pomoshch'yu ob容kta, mozhno opredelit'
proizvodnyj klass, v kotorom proveryayutsya operaciyami iz klassa, gde net
proverki, sm. upr.8 v $$13.11.
Dlya klassov s bolee slozhnymi operaciyami rashody na proverki mogut
byt' znachitel'ny, poetomu proverki mozhno ostavit' tol'ko dlya "poimki"
trudno obnaruzhivaemyh oshibok. Obychno polezno ostavlyat' po krajnej
mere neskol'ko proverok dazhe v ochen' horosho otlazhennoj programme.
Pri vseh usloviyah sam fakt opredeleniya invariantov i ispol'zovaniya
ih pri otladke daet neocenimuyu pomoshch' dlya polucheniya pravil'noj
programmy i, chto bolee vazhno, delaet ponyatiya, predstavlennye
klassami, bolee regulyarnymi i strogo opredelennymi. Delo v tom, chto
kogda vy sozdaete invarianty, to rassmatrivaete klass s drugoj
tochki zreniya i vnosite opredelennuyu izbytochnost' v programmu.
To i drugoe uvelichivaet veroyatnost' obnaruzheniya oshibok, protivorechij
i nedosmotrov.
My ukazali v $$11.3.3.5, chto dve samye obshchie formy preobrazovaniya
ierarhii klassov sostoyat v razbienii klassa na dva i v vydelenii
obshchej chasti dvuh klassov v bazovyj klass. V oboih sluchayah horosho
produmannyj invariant mozhet podskazat' vozmozhnost' takogo
preobrazovaniya. Esli, sravnivaya invariant s programmami operacij,
mozhno obnaruzhit', chto bol'shinstvo proverok invarianta izlishni,
to znachit klass sozrel dlya razbieniya. V etom sluchae podmnozhestvo operacij
imeet dostup tol'ko k podmnozhestvu sostoyanij ob容kta. Obratno,
klassy sozreli dlya sliyaniya, esli u nih shodnye invarianty, dazhe
pri nekotorom razlichii v ih realizacii.
Otmetim, chto v S++ klass, a ne otdel'nyj ob容kt, yavlyaetsya toj
edinicej, kotoraya dolzhna byt' inkapsulirovana (zaklyuchena v obolochku).
Naprimer:
class list {
list* next;
public:
int on(list*);
};
int list::on(list* p)
{
list* q = this;
for(;;) {
if (p == q) return 1;
if (q == 0) return 0;
q = q->next;
}
}
Zdes' obrashchenie k chastnomu ukazatelyu list::next dopustimo, poskol'ku
list::on() imeet dostup ko vsyakomu ob容ktu klassa list, na kotoryj
u nego est' ssylka. Esli eto neudobno, situaciyu mozhno uprostit',
otkazavshis' ot vozmozhnosti dostupa cherez funkciyu-chlen k
predstavleniyam drugih ob容ktov, naprimer:
int list::on(list* p)
{
if (p == this) return 1;
if (p == 0) return 0;
return next->on(p);
}
No teper' iteraciya prevrashchaetsya v rekursiyu, chto mozhet sil'no
zamedlit' vypolnenie programmy, esli tol'ko translyator
ne sumeet obratno preobrazovat' rekursiyu v iteraciyu.
12.2.8 Programmiruemye otnosheniya
Konkretnyj yazyk programmirovaniya ne mozhet pryamo podderzhivat'
lyuboe ponyatie lyubogo metoda proektirovaniya. Esli yazyk programmirovaniya
ne sposoben pryamo predstavit' ponyatie proektirovaniya, sleduet
ustanovit' udobnoe otobrazhenie konstrukcij, ispol'zuemyh v proekte,
na yazykovye konstrukcii. Naprimer, metod proektirovaniya mozhet
ispol'zovat' ponyatie delegirovaniya, oznachayushchee, chto vsyakaya
operaciya, kotoraya ne opredelena dlya klassa A, dolzhna vypolnyat'sya
v nem s pomoshch'yu ukazatelya p na sootvetstvuyushchij chlen klassa B,
v kotorom ona opredelena. Na S++ nel'zya vyrazit' eto pryamo. Odnako,
realizaciya etogo ponyatiya nastol'ko v duhe S++, chto legko predstavit'
programmu realizacii:
class A {
B* p;
//...
void f();
void ff();
};
class B {
//...
void f();
void g();
void h();
};
Tot fakt, chto V delegiruet A s pomoshch'yu ukazatelya A::p,
vyrazhaetsya v sleduyushchej zapisi:
class A {
B* p; // delegirovanie s pomoshch'yu p
//...
void f();
void ff();
void g() { p->g(); } // delegirovanie q()
void h() { p->h(); } // delegirovanie h()
};
Dlya programmista sovershenno ochevidno, chto zdes' proishodit, odnako zdes'
yavno narushaetsya princip vzaimnoodnoznachnogo sootvetstviya. Takie
"programmiruemye" otnosheniya trudno vyrazit' na yazykah programmirovaniya,
i poetomu k nim trudno primenyat' razlichnye vspomogatel'nye sredstva.
Naprimer, takoe sredstvo mozhet ne otlichit' "delegirovanie" ot B
k A s pomoshch'yu A::p ot lyubogo drugogo ispol'zovaniya B*.
Vse-taki sleduet vsyudu, gde eto vozmozhno, dobivat'sya
vzaimnoodnoznachnogo sootvetstviya mezhdu ponyatiyami proekta i ponyatiyami
yazyka programmirovaniya. Ono daet opredelennuyu prostotu i garantiruet,
chto proekt adekvatno otobrazhaetsya v programme, chto uproshchaet
rabotu programmista i vspomogatel'nyh sredstv.
Operacii preobrazovanij tipa yavlyayutsya mehanizmom, s pomoshch'yu kotorogo
mozhno predstavit' v yazyke klass programmiruemyh otnoshenij, a imenno:
operaciya preobrazovaniya X::operator Y() garantiruet, chto vsyudu,
gde dopustimo ispol'zovanie Y, mozhno primenyat' i X. Takoe zhe
otnoshenie zadaet konstruktor Y::Y(X). Otmetim, chto operaciya
preobrazovaniya tipa (kak i konstruktor) skoree sozdaet novyj ob容kt,
chem izmenyaet tip sushchestvuyushchego ob容kta. Zadat' operaciyu preobrazovaniya
k funkcii Y - oznachaet prosto potrebovat' neyavnogo primeneniya
funkcii, vozvrashchayushchej Y. Poskol'ku neyavnye primeneniya operacij
preobrazovaniya tipa i operacij, opredelyaemyh konstruktorami, mogut
privesti k nepriyatnostyam, polezno proanalizirovat' ih v otdel'nosti
eshche v proekte.
Vazhno ubedit'sya, chto graf primenenij operacij preobrazovaniya tipa
ne soderzhit ciklov. Esli oni est', voznikaet dvusmyslennaya situaciya,
pri kotoroj tipy, uchastvuyushchie v ciklah, stanovyatsya nesovmestimymi v
kombinacii. Naprimer:
class Big_int {
//...
friend Big_int operator+(Big_int,Big_int);
//...
operator Rational();
//...
};
class Rational {
//...
friend Rational operator+(Rational,Rational);
//...
operator Big_int();
};
Tipy Rational i Big_int ne tak gladko vzaimodejstvuyut, kak mozhno
bylo by podumat':
void f(Rational r, Big_int i)
{
//...
g(r+i); // oshibka, neodnoznachnost':
// operator+(r,Rational(i)) ili
// operator+(Big_int(r),i)
g(r,Rational(i)); // yavnoe razreshenie neopredelennosti
g(Big_int(r),i); // eshche odno
}
Mozhno bylo by izbezhat' takih "vzaimnyh" preobrazovanij, sdelav
nekotorye iz nih yavnymi. Naprimer, preobrazovanie Big_int k tipu
Rational mozhno bylo by zadat' yavno s pomoshch'yu funkcii make_Rational()
vmesto operacii preobrazovaniya, togda slozhenie v privedennom
primere razreshalos' by kak g(BIg_int(r),i). Esli nel'zya izbezhat'
"vzaimnyh" operacij preobrazovaniya tipov, to nuzhno preodolevat'
voznikayushchie stolknoveniya ili s pomoshch'yu yavnyh preobrazovanij (kak bylo
pokazano), ili s pomoshch'yu opredeleniya neskol'kih razlichnyh versij
binarnoj operacii (v nashem sluchae +).
V yazyke S++ net konstrukcij, kotorye mogut vyrazit' pryamo v programme
ponyatie komponenta, t.e. mnozhestva svyazannyh klassov. Osnovnaya
prichina etogo v tom, chto mnozhestvo klassov (vozmozhno s sootvetstvuyushchimi
global'nymi funkciyami i t.p.) mozhet soedinyat'sya v komponent po
samym raznym priznakam. Otsutstvie yavnogo predstavleniya ponyatiya v
yazyke zatrudnyaet provedenie granicy mezhdu informaciej (imena),
ispol'zuemoj vnutri komponenta, i informaciej (imena), peredavaemoj
iz komponenta pol'zovatelyam.
V ideale, komponent opredelyaetsya mnozhestvom interfejsov, ispol'zuemyh
dlya ego realizacii, plyus mnozhestvom interfejsov, predstavlyaemyh
pol'zovatelem, a vse prochee schitaetsya "specifikoj realizacii" i
dolzhno byt' skryto ot ostal'nyh chastej sistemy. Takovo mozhet byt'
v dejstvitel'nosti predstavlenie o komponente u razrabotchika.
Programmist dolzhen smirit'sya s tem faktom, chto S++ ne daet
obshchego ponyatiya prostranstva imen komponenta, tak chto ego
prihoditsya "modelirovat'" s pomoshch'yu ponyatij klassov i edinic
translyacii, t.e. teh sredstv, kotorye est' v S++ dlya ogranicheniya
oblasti dejstviya nelokal'nyh imen.
Rassmotrim dva klassa, kotorye dolzhny sovmestno ispol'zovat'
funkciyu f() i peremennuyu v. Proshche vsego opisat' f i v kak
global'nye imena. Odnako, vsyakij opytnyj programmist znaet, chto
takoe "zasorenie" prostranstva imen mozhet privesti v konce koncov
k nepriyatnostyam: kto-to mozhet nenarochno ispol'zovat' imena f ili v
ne po naznacheniyu ili narochno obratit'sya k f ili v,
pryamo ispol'zuya "specifiku realizacii" i obojdya tem samym yavnyj
interfejs komponenta. Zdes' vozmozhny tri resheniya:
[1] Dat' "neobychnye" imena ob容ktam i funkciyam, kotorye ne
rasschitany na pol'zovatelya.
[2] Ob容kty ili funkcii, ne prednaznachennye dlya pol'zovatelya,
opisat' v odnom iz fajlov programmy kak staticheskie (static).
[3] Pomestit' ob容kty i funkcii, ne prednaznachennye dlya pol'zovatelya,
v klass, opredelenie kotorogo zakryto dlya pol'zovatelej.
Pervoe reshenie primitivno i dostatochno neudobno dlya sozdatelya
programmy, no ono dejstvuet:
// ne ispol'zujte specifiku realizacii compX,
// esli tol'ko vy ne razrabotchik compX:
extern void compX_f(T2*, const char*);
extern T3 compX_v;
// ...
Takie imena kak compX_f i compX_v vryad li mogut privesti k kollizii, a na
tot dovod, chto pol'zovatel' mozhet byt' zloumyshlennikom i ispol'zovat'
eti imena pryamo, mozhno otvetit', chto pol'zovatel' v lyubom sluchae mozhet
okazat'sya zloumyshlennikom, i chto yazykovye mehanizmy zashchity predohranyayut
ot neschastnogo sluchaya, a ne ot zlogo umysla. Preimushchestvo etogo
resheniya v tom, chto ono primenimo vsegda i horosho izvestno. V to zhe
vremya ono nekrasivo, nenadezhno i uslozhnyaet vvod teksta.
Vtoroe reshenie bolee nadezhno, no menee universal'no:
// specifika realizacii compX:
static void compX_f(T2* a1, const char *a2) { /* ... */ }
static T3 compX_v;
// ...
Trudno garantirovat', chto informaciya, ispol'zuemaya v klassah odnogo
komponenta, budet dostupna tol'ko v odnoj edinice translyacii,
poskol'ku operacii, rabotayushchie s etoj informaciej, dolzhny
byt' dostupny vezde. |to reshenie mozhet k tomu zhe privesti k
gromadnym edinicam translyacii, a v nekotoryh otladchikah dlya S++
ne organizovan dostup k imenam staticheskih funkcij i peremennyh.
V to zhe vremya eto reshenie nadezhno i chasto optimal'no dlya nebol'shih
komponentov.
Tret'e reshenie mozhno rassmatrivat' kak formalizaciyu i obobshchenie
pervyh dvuh:
class compX_details { // specifika realizacii compX
public:
static void f(T2*, const char*);
static T3 v;
// ...
};
Opisanie compX_details budet ispol'zovat' tol'ko sozdatel' klassa,
ostal'nye ne dolzhny vklyuchat' ego v svoi programmy.
V komponente konechno mozhet byt' mnogo klassov, ne prednaznachennyh
dlya obshchego pol'zovaniya. Esli ih imena tozhe rasschitany tol'ko na
lokal'noe ispol'zovanie, to ih takzhe mozhno "spryatat'" vnutri
klassov, soderzhashchih specifiku realizacii:
class compX_details { // specifika realizacii compX.
public:
// ...
class widget {
// ...
};
// ...
};
Ukazhem, chto vlozhennost' sozdaet bar'er dlya ispol'zovaniya widget
v drugih chastyah programmy. Obychno klassy, predstavlyayushchie
yasnye ponyatiya, schitayutsya pervymi kandidatami na povtornoe
ispol'zovanie, i, znachit sostavlyayut chast' interfejsa komponenta,
a ne detal' realizacii. Drugimi slovami, hotya dlya sohraneniya
nadlezhashchego urovnya abstrakcii vlozhennye ob容kty, ispol'zuemye dlya
predstavleniya nekotorogo ob容kta klassa, luchshe schitat' skrytymi
detalyami realizacii, klassy, opredelyayushchie takie vlozhennye ob容kty,
luchshe ne delat' skrytymi, esli oni imeyut dostatochnuyu obshchnost'.
Tak, v sleduyushchem primere upryatyvanie, pozhaluj, izlishne:
class Car {
class Wheel {
// ...
};
Wheel flw, frw, rlw, rrw;
// ...
};
Vo mnogih situaciyah dlya podderzhaniya urovnya abstrakcii ponyatiya
mashiny (Car) sleduet upryatyvat' real'nye kolesa (klass Wheel),
ved' kogda vy rabotaete s mashinoj, vy ne mozhete nezavisimo ot nee
ispol'zovat' kolesa. S drugoj storony, sam klass Wheel yavlyaetsya
vpolne podhodyashchim dlya shirokogo ispol'zovaniya, poetomu luchshe
vynesti ego opredelenie iz klassa Car:
class Wheel {
// ...
};
class Car {
Wheel flw, frw, rlw, rrw;
// ...
};
Ispol'zovat' li vlozhennost'? Otvet na etot vopros zavisit
ot celej proekta i obshchnosti ispol'zuemyh ponyatij. Kak vlozhennost',
tak i ee otsutstvie mogut byt' vpolne dopustimymi resheniyami dlya dannogo
proekta. No poskol'ku vlozhennost' predohranyaet ot zasoreniya
obshchego prostranstva imen, v svode pravil nizhe rekomenduetsya
ispol'zovat' vlozhennost', esli tol'ko net prichin ne delat' etogo.
Otmetim, chto zagolovochnye fajly dayut moshchnoe sredstvo dlya
razlichnyh predstavlenij komponent raznym pol'zovatelyam, i oni zhe
pozvolyayut udalyat' iz predstavleniya komponenta dlya pol'zovatelya te
klassy, kotorye svyazany so specifikoj realizacii.
Drugim sredstvom postroeniya komponenta i predstavleniya ego
pol'zovatelyu sluzhit ierarhiya. Togda bazovyj klass ispol'zuetsya kak
hranilishche obshchih dannyh i funkcij. Takim sposobom ustranyaetsya
problema, svyazannaya s global'nymi dannymi i funkciyami, prednaznachennymi
dlya realizacii obshchih zaprosov klassov dannogo komponenta.
S drugoj storony, pri takom reshenii klassy komponenta stanovyatsya
slishkom svyazannymi drug s drugom, a pol'zovatel' popadaet v zavisimost'
ot vseh bazovyh klassov teh komponentov, kotorye emu dejstvitel'no
nuzhny. Zdes' takzhe proyavlyaetsya tendenciya k tomu, chto chleny,
predstavlyayushchie "poleznye" funkcii i dannye "vsplyvayut" k bazovomu
klassu, tak chto pri slishkom bol'shoj ierarhii klassov problemy s
global'nymi dannymi i funkciyami proyavyatsya uzhe v ramkah etoj ierarhii.
Veroyatnee vsego, eto proizojdet dlya ierarhii s odnim kornem, a dlya
bor'by s etim yavleniem mozhno primenyat' virtual'nye bazovye klassy
($$6.5.4). Inogda luchshe vybrat' ierarhiyu dlya predstavleniya komponenta,
a inogda net. Kak vsegda sdelat' vybor predstoit razrabotchiku.
12.4 Interfejsy i realizacii
Ideal'nyj interfejs dolzhen
- predstavlyat' polnoe i soglasovannoe mnozhestvo ponyatij dlya
pol'zovatelya,
- byt' soglasovannym dlya vseh chastej komponenta,
- skryvat' specifiku realizacii ot pol'zovatelya,
- dopuskat' neskol'ko realizacij,
- imet' staticheskuyu sistemu tipov,
- opredelyat'sya s pomoshch'yu tipov iz oblasti prilozheniya,
- zaviset' ot drugih interfejsov lish' chastichno i vpolne opredelennym
obrazom.
Otmetiv neobhodimost' soglasovannosti dlya vseh klassov, kotorye
obrazuyut interfejs komponenta s ostal'nym mirom, my mozhem uprostit'
vopros interfejsa, rassmotrev tol'ko odin klass, naprimer:
class X { // primer plohogo opredeleniya interfejsa
Y a;
Z b;
public:
void f(const char* ...);
void g(int[],int);
void set_a(Y&);
Y& get_a();
};
V etom interfejse soderzhitsya ryad potencial'nyh problem:
-Tipy Y i Z ispol'zuyutsya tak, chto opredeleniya Y i Z dolzhny byt'
izvestny vo vremya translyacii.
- U funkcii X::f mozhet byt' proizvol'noe chislo parametrov
neizvestnogo tipa (vozmozhno, oni kakim-to obrazom kontroliruyutsya
"strokoj formata", kotoraya peredaetsya v kachestve pervogo
parametra).
- Funkciya X::g imeet parametr tipa int[]. Vozmozhno eto normal'no,
no obychno eto svidetel'stvuet o tom, chto opredelenie slishkom
nizkogo urovnya abstrakcii. Massiv celyh ne yavlyaetsya dostatochnym
opredeleniem, tak kak neizvestno iz skol'kih on mozhet
sostoyat' elementov.
- Funkcii set_a() i get_a(), po vsej vidimosti, raskryvayut
predstavlenie ob容ktov klassa X, razreshaya pryamoj dostup
k X::a.
Zdes' funkcii-chleny obrazuyut interfejs na slishkom nizkom urovne
abstrakcii. Kak pravilo klassy s interfejsom takogo urovnya otnosyatsya
k specifike realizacii bol'shogo komponenta, esli oni voobshche mogut
k chemu-nibud' otnosit'sya. V ideale parametr funkcii iz interfejsa
dolzhen soprovozhdat'sya takoj informaciej, kotoroj dostatochno
dlya ego ponimaniya. Mozhno sformulirovat' takoe pravilo: nado umet'
peredavat' zaprosy na obsluzhivanie udalennomu serveru po uzkomu
kanalu.
YAzyk S++ raskryvaet predstavlenie klassa kak chast' interfejsa.
|to predstavlenie mozhet byt' skrytym (s pomoshch'yu private ili
protected), no obyazatel'no dostupnym translyatoru, chtoby on mog razmestit'
avtomaticheskie (lokal'nye) peremennye, sdelat' podstanovku tela
funkcii i t.d. Otricatel'nym sledstviem etogo yavlyaetsya to, chto
ispol'zovanie tipov klassov v predstavlenii klassa mozhet privesti k
vozniknoveniyu nezhelatel'nyh zavisimostej. Privedet li ispol'zovanie
chlenov tipa Y i Z k problemam, zavisit ot togo, kakovy v dejstvitel'nosti
tipy Y i Z. Esli eto dostatochno prostye tipy, napodobie complex ili
String, to ih ispol'zovanie budet vpolne dopustimym v bol'shinstve sluchaev.
Takie tipy mozhno schitat' ustojchivymi, i neobhodimost' vklyuchat'
opredeleniya ih klassov budet vpolne dopustimoj nagruzkoj dlya translyatora.
Esli zhe Y i Z sami yavlyayutsya klassami interfejsa bol'shogo
komponenta (naprimer, tipa graficheskoj sistemy ili sistemy obespecheniya
bankovskih schetov), to pryamuyu zavisimost' ot nih mozhno schitat'
nerazumnoj. V takih sluchayah predpochtitel'nee ispol'zovat' chlen,
yavlyayushchijsya ukazatelem ili ssylkoj:
class X {
Y* a;
Z& b;
// ...
};
Pri etom sposobe opredelenie X otdelyaetsya ot opredelenij Y i Z, t.e.
teper' opredelenie X zavisit tol'ko ot imen Y i Z. Realizaciya X,
konechno, budet po-prezhnemu zaviset' ot opredelenij Y i Z, no eto
uzhe ne budet okazyvat' neblagopriyatnogo vliyaniya na pol'zovatelej X.
Vysheskazannoe illyustriruet vazhnoe utverzhdenie: U interfejsa,
skryvayushchego znachitel'nyj ob容m informacii (chto i dolzhen delat' poleznyj
interfejs), dolzhno byt' sushchestvenno men'she zavisimostej, chem
u realizacii, kotoraya ih skryvaet. Naprimer, opredelenie klassa X
mozhno translirovat' bez dostupa k opredeleniyam Y i Z. Odnako,
v opredeleniyah funkcij-chlenov klassa X, kotorye rabotayut so
ssylkami na ob容kty Y i Z, dostup k opredeleniyam Y i Z neobhodim.
Pri analize zavisimostej sleduet rassmatrivat' razdel'no
zavisimosti v interfejse i v realizacii. V ideale dlya oboih vidov
zavisimostej graf zavisimostej sistemy dolzhen byt' napravlennym
neciklichnym grafom, chto oblegchaet ponimanie i testirovanie
sistemy. Odnako, eta cel' bolee vazhna i chashche dostizhima dlya
realizacij, chem dlya interfejsov.
Otmetim, chto klass opredelyaet tri interfejsa:
class X {
private:
// dostupno tol'ko dlya chlenov i druzej
protected:
// dostupno tol'ko dlya chlenov i druzej, a takzhe
// dlya chlenov i druzej proizvodnyh klassov
public:
// obshchedostupno
};
CHleny dolzhny obrazovyvat' samyj ogranichennyj iz vozmozhnyh interfejsov.
Inymi slovami, chlen dolzhen byt' opisan kak private, esli net
prichin dlya bolee shirokogo dostupa k nemu; esli zhe takovye est', to
chlen dolzhen byt' opisan kak protected, esli net dopolnitel'nyh prichin
zadat' ego kak public. V bol'shinstve sluchaev ploho zadavat' vse dannye,
predstavlyaemye chlenami, kak public. Funkcii i klassy, obrazuyushchie obshchij
interfejs, dolzhny byt' sproektirovany takim obrazom, chtoby predstavlenie
klassa sovpadalo s ego rol'yu v proekte kak sredstva predstavleniya
ponyatij. Napomnim, chto druz'ya yavlyayutsya chast'yu obshchego interfejsa.
Otmetim, chto abstraktnye klassy mozhno ispol'zovat' dlya
predstavleniya ponyatiya upryatyvaniya bolee vysokogo urovnya ($$1.4.6,
$$6.3, $$13.3).
V etoj glave my kosnulis' mnogih tem, no, kak pravilo, izbegali
davat' nastoyatel'nye i konkretnye rekomendacii po rassmatrivaemym
voprosam. |to otvechaet moemu ubezhdeniyu, chto net "edinstvenno vernogo
resheniya". Principy i priemy sleduet primenyat' sposobom, naibolee
podhodyashchim dlya konkretnoj zadachi. Zdes' trebuyutsya vkus, opyt i
razum. Tem ne menee, mozhno predlozhit' svod pravil, kotorye
razrabotchik mozhet ispol'zovat' v kachestve orientirov, poka ne
priobretet dostatochno opyta, chtoby vyrabotat' luchshie.
|tot svod pravil privoditsya nizhe.
On mozhet sluzhit' otpravnoj tochkoj v processe vyrabotki
osnovnyh napravlenij proekta konkretnoj zadachi, ili zhe on mozhet
ispol'zovat'sya organizaciej v kachestve proverochnogo spiska. Podcherknu
eshche raz, chto eti pravila ne yavlyayutsya universal'nymi i ne mogut
zamenit' soboj razmyshleniya.
- Nacelivajte pol'zovatelya na primenenie abstrakcii dannyh i
ob容ktno-orientirovannogo programmirovaniya.
- Postepenno perehodite na novye metody, ne speshite.
- Ispol'zujte vozmozhnosti S++ i metody ob枸ktno-orientirovannogo
programmirovaniya tol'ko po mere nadobnosti.
_ Dobejtes' sootvetstviya stilya proekta i programmy.
- Koncentrirujte vnimanie na proektirovanii komponenta.
_ Ispol'zujte klassy dlya predstavleniya ponyatij.
- Ispol'zujte obshchee nasledovanie dlya predstavleniya otnoshenij "est'".
- Ispol'zujte prinadlezhnost' dlya predstavleniya otnoshenij "imeet".
- Ubedites', chto otnosheniya ispol'zovaniya ponyatny, ne obrazuyut
ciklov, i chto chislo ih minimal'no.
- Aktivno ishchite obshchnost' sredi ponyatij oblasti prilozheniya i
realizacii, i voznikayushchie v rezul'tate bolee obshchie ponyatiya
predstavlyajte kak bazovye klassy.
- Opredelyajte interfejs tak, chtoby otkryvat' minimal'noe kolichestvo
trebuemoj informacii:
- Ispol'zujte, vsyudu gde eto mozhno, chastnye dannye i funkcii-chleny.
- Ispol'zujte opisaniya public ili protected, chtoby otlichit'
zaprosy razrabotchika proizvodnyh klassov ot zaprosov obychnyh
pol'zovatelej.
- Svedite k minimumu zavisimosti odnogo interfejsa ot drugih.
- Podderzhivajte stroguyu tipizaciyu interfejsov.
- Zadavajte interfejsy v terminah tipov iz oblasti prilozheniya.
Dopolnitel'nye pravila mozhno najti $$11.5.
* PROEKTIROVANIE BIBLIOTEK
Proekt biblioteki - eto proekt yazyka,
(fol'klor firmy Bell Laboratories)
... i naoborot.
- A. Kenig
|ta glava soderzhit opisanie razlichnyh priemov, okazavshihsya poleznymi
pri sozdanii bibliotek dlya yazyka S++. V chastnosti, v nej
rassmatrivayutsya konkretnye tipy, abstraktnye tipy, uzlovye klassy,
upravlyayushchie klassy i interfejsnye klassy. Pomimo etogo obsuzhdayutsya
ponyatiya obshirnogo interfejsa i struktury oblasti prilozheniya,
ispol'zovanie dinamicheskoj informacii o tipah i metody upravleniya
pamyat'yu. Vnimanie akcentiruetsya na tom, kakimi svojstvami dolzhny
obladat' bibliotechnye klassy, a ne na specifike yazykovyh sredstv,
kotorye ispol'zuyutsya dlya realizacii takih klassov, i ne na
opredelennyh poleznyh funkciyah, kotorye dolzhna predostavlyat' biblioteka.
Razrabotka biblioteki obshchego naznacheniya - eto gorazdo bolee trudnaya
zadacha, chem sozdanie obychnoj programmy. Programma - eto reshenie
konkretnoj zadachi dlya konkretnoj oblasti prilozheniya, togda kak
biblioteka dolzhna predostavlyat' vozmozhnost' reshenie dlya mnozhestva zadach,
svyazannyh s mnogimi oblastyami prilozheniya. V obychnoj programme
pozvolitel'ny sil'nye dopushcheniya ob ee okruzhenii, togda kak horoshuyu
biblioteku mozhno uspeshno ispol'zovat' v raznoobraznyh okruzheniyah,
sozdavaemyh mnozhestvom razlichnyh programm. CHem bolee obshchej i poleznoj
okazhetsya biblioteka, tem v bol'shem chisle okruzhenij ona budet
proveryat'sya, i tem zhestche budut trebovaniya k ee korrektnosti, gibkosti,
effektivnosti, rasshiryaemosti, perenosimosti, neprotivorechivosti,
prostote, polnote, legkosti ispol'zovaniya i t.d. Vse zhe biblioteka
ne mozhet dat' vam vse, poetomu nuzhen opredelennyj kompromiss.
Biblioteku mozhno rassmatrivat' kak special'nyj, interesnyj variant
togo, chto v predydushchej glave my nazyvali komponentom. Kazhdyj
sovet po proektirovaniyu i soprovozhdeniyu komponentov stanovitsya
predel'no vazhnym dlya bibliotek, i, naoborot, mnogie metody
postroeniya bibliotek nahodyat primenenie pri proektirovanii razlichnyh
komponentov.
Bylo by slishkom samonadeyanno ukazyvat' kak sleduet
konstruirovat' biblioteki. V proshlom okazalis' uspeshnymi neskol'ko
razlichnyh metodov, a sam predmet ostaetsya polem aktivnyh diskussij
i eksperimentov. Zdes' tol'ko obsuzhdayutsya nekotorye vazhnye aspekty
etoj zadachi i predlagayutsya nekotorye priemy, okazavshiesya poleznymi
pri sozdanii bibliotek. Ne sleduet zabyvat', chto biblioteki prednaznacheny
dlya sovershenno raznyh oblastej programmirovaniya, poetomu ne prihoditsya
rasschityvat', chto kakoj-to odin metod okazhetsya naibolee priemlemym dlya
vseh bibliotek. Dejstvitel'no, net nikakih prichin polagat', chto metody,
okazavshiesya poleznymi pri realizacii sredstv parallel'nogo
programmirovaniya dlya yadra mnogoprocessornoj operacionnoj sistemy,
okazhutsya naibolee priemlemymi pri sozdanii biblioteki, prednaznachennoj
dlya resheniya nauchnyh zadach, ili biblioteki, predstavlyayushchej graficheskij
interfejs.
Ponyatie klassa S++ mozhet ispol'zovat'sya samymi raznymi
sposobami, poetomu raznoobrazie stilej programmirovaniya mozhet
privesti k besporyadku. Horoshaya biblioteka dlya svedeniya takogo
besporyadka k minimumu obespechivaet soglasovannyj stil' programmirovaniya,
ili, po krajnej mere, neskol'ko takih stilej. |tot podhod delaet
biblioteku bolee "predskazuemoj", a znachit pozvolyaet legche i bystree
izuchit' ee i pravil'no ispol'zovat'. Dalee opisyvayutsya pyat'
"arhitipichnyh" klassov, i obsuzhdayutsya prisushchie im sil'nye i slabye
storony: konkretnye tipy ($$13.2), abstraktnye tipy ($$13.3),
uzlovye klassy ($$13.4), interfejsnye klassy ($$13.8), upravlyayushchie
klassy ($$13.9). Vse eti vidy klassov otnosyatsya k oblasti ponyatij,
a ne yavlyayutsya konstrukciyami yazyka. Kazhdoe ponyatie voploshchaetsya
s pomoshch'yu osnovnoj konstrukcii - klassa. V ideale nado imet'
minimal'nyj nabor prostyh i ortogonal'nyh vidov klassov, ishodya iz
kotorogo mozhno postroit' lyuboj poleznyj i razumno-opredelennyj klass.
Ideal nami ne dostignut i, vozmozhno, nedostizhim voobshche. Vazhno ponyat',
chto lyuboj iz perechislennyh vidov klassov igraet svoyu rol' pri
proektirovanii biblioteki i, esli rasschityvat' na obshchee primenenie,
nikakoj iz nih ne yavlyaetsya po svoej suti luchshe drugih.
V etoj glave vvoditsya ponyatie obshirnogo interfejsa ($$13.6),
chtoby vydelit' nekotoryj obshchij sluchaj vseh etih vidov klassov.
S pomoshch'yu nego opredelyaetsya ponyatie karkasa oblasti prilozheniya ($$13.7).
Zdes' rassmatrivayutsya prezhde vsego klassy, otnosyashchiesya strogo k
odnomu iz perechislennyh vidov, hotya, konechno, ispol'zuyutsya
klassy i gibridnogo vida. No ispol'zovanie klassa gibridnogo vida
dolzhno byt' rezul'tatom osoznannogo resheniya, voznikshego pri ocenke
plyusov i minusov razlichnyh vidov, a ne rezul'tatom pagubnogo stremleniya
uklonit'sya ot vybora vida klassa (slishkom chasto "otlozhim poka vybor"
oznachaet prosto nezhelanie dumat'). Neiskushennym razrabotchikam
biblioteki luchshe vsego derzhat'sya podal'she ot klassov gibridnogo
vida. Im mozhno posovetovat' sledovat' stilyu programmirovaniya toj iz
sushchestvuyushchih bibliotek, kotoraya obladaet vozmozhnostyami, neobhodimymi dlya
proektiruemoj biblioteki. Otvazhit'sya na sozdanie biblioteki obshchego
naznacheniya mozhet tol'ko iskushennyj programmist, i kazhdyj sozdatel'
biblioteki vposledstvii budet "osuzhden" na dolgie gody ispol'zovaniya,
dokumentirovaniya i soprovozhdeniya svoego sobstvennogo sozdaniya.
V yazyke S++ ispol'zuyutsya staticheskie tipy. Odnako, inogda
voznikaet neobhodimost' v dopolnenie k vozmozhnostyam, neposredstvenno
predostavlyaemym virtual'nymi funkciyami, poluchat' dinamicheskuyu informaciyu
o tipah. Kak eto sdelat', opisano v $$13.5. Nakonec, pered vsyakoj
netrivial'noj bibliotekoj vstaet zadacha upravleniya pamyat'yu. Priemy ee
resheniya rassmatrivayutsya v $$13.10. Estestvenno, v etoj glave nevozmozhno
rassmotret' vse metody, okazavshiesya poleznymi pri sozdanii biblioteki.
Poetomu mozhno otoslat' k drugim mestam knigi, gde rassmotreny
sleduyushchie voprosy: rabota s oshibkami i ustojchivost' k oshibkam ($$9.8),
ispol'zovanie funkcional'nyh ob容ktov i obratnyh vyzovov ($$10.4.2
i $$9.4.3) , ispol'zovanie shablonov tipa dlya postroeniya klassov
($$8.4).
Mnogie temy etoj glavy svyazany s klassami, yavlyayushchimisya kontejnerami,
(naprimer, massivy i spiski). Konechno, takie kontejnernye klassy
yavlyayutsya shablonami tipa (kak bylo skazano v $$1.i 4.3 $$8). No
zdes' dlya uproshcheniya izlozheniya v primerah ispol'zuyutsya klassy,
soderzhashchie ukazateli na ob容kty tipa klass. CHtoby poluchit' nastoyashchuyu
programmu, nado ispol'zovat' shablony tipa, kak pokazano v glave 8.
Takie klassy kak vector ($$1.4), Slist ($$8.3), date ($$5.2.2) i
complex ($$7.3) yavlyayutsya konkretnymi v tom smysle, chto kazhdyj iz
nih predstavlyaet dovol'no prostoe ponyatie i obladaet neobhodimym
naborom operacij. Imeetsya vzaimnoodnoznachnoe sootvetstvie mezhdu
interfejsom klassa i ego realizaciej. Ni odin iz nih (iznachal'no)
ne prednaznachalsya v kachestve bazovogo dlya polucheniya proizvodnyh klassov.
Obychno v ierarhii klassov konkretnye tipy stoyat osobnyakom. Kazhdyj
konkretnyj tip mozhno ponyat' izolirovanno, vne svyazi s drugimi klassami.
Esli realizaciya konkretnogo tipa udachna, to rabotayushchie s nim programmy
sravnimy po razmeru i skorosti so sdelannymi vruchnuyu programmami,
v kotoryh ispol'zuetsya nekotoraya special'naya versiya obshchego ponyatiya.
Dalee, esli proizoshlo znachitel'noe izmenenie realizacii, obychno
modificiruetsya i interfejs, chtoby otrazit' eti izmeneniya. Interfejs,
po svoej suti, obyazan pokazat' kakie izmeneniya okazalis' sushchestvennymi
v dannom kontekste. Interfejs bolee vysokogo urovnya ostavlyaet
bol'she svobody dlya izmeneniya realizacii, no mozhet uhudshit'
harakteristiki programmy. Bolee togo, horoshaya realizaciya zavisit
tol'ko ot minimal'nogo chisla dejstvitel'no sushchestvennyh klassov.
Lyuboj iz etih klassov mozhno ispol'zovat' bez nakladnyh rashodov,
voznikayushchih na etape translyacii ili vypolneniya, i vyzvannyh
prisposobleniem k drugim, "shodnym" klassam programmy.
Podvodya itog, mozhno ukazat' takie usloviya, kotorym dolzhen
udovletvoryat' konkretnyj tip:
[1] polnost'yu otrazhat' dannoe ponyatie i metod ego realizacii;
[2] s pomoshch'yu podstanovok i operacij, polnost'yu ispol'zuyushchih
poleznye svojstva ponyatiya i ego realizacii, obespechivat'
effektivnost' po skorosti i pamyati, sravnimuyu
s "ruchnymi programmami";
[3] imet' minimal'nuyu zavisimost' ot drugih klassov;
[4] byt' ponyatnym i poleznym dazhe izolirovanno.
Vse eto dolzhno privesti k tesnoj svyazi mezhdu pol'zovatelem i
programmoj, realizuyushchej konkretnyj tip. Esli v realizacii proizoshli
izmeneniya, programmu pol'zovatelya pridetsya peretranslirovat',
poskol'ku v nej navernyaka soderzhatsya vyzovy funkcij, realizuemye
podstanovkoj, a takzhe lokal'nye peremennye konkretnogo tipa.
Dlya nekotoryh oblastej prilozheniya konkretnye tipy obespechivayut
osnovnye tipy, pryamo ne predstavlennye v S++, naprimer:
kompleksnye chisla, vektora, spiski, matricy, daty, associativnye
massivy, stroki simvolov i simvoly, iz drugogo (ne anglijskogo)
alfavita. V mire, sostoyashchem iz konkretnyh ponyatij, na samom dele
net takoj veshchi kak spisok. Vmesto etogo est' mnozhestvo spisochnyh
klassov, kazhdyj iz kotoryh specializiruetsya na predstavlenii
kakoj-to versii ponyatiya spisok. Sushchestvuet dyuzhina spisochnyh
klassov, v tom chisle: spisok s odnostoronnej svyaz'yu; spisok s
dvustoronnej svyaz'yu; spisok s odnostoronnej svyaz'yu, v kotorom
pole svyazi ne prinadlezhit ob容ktu; spisok s dvustoronnej svyaz'yu,
v kotorom polya svyazi ne prinadlezhat ob容ktu; spisok s odnostoronnej
svyaz'yu, dlya kotorogo mozhno prosto i effektivno opredelit' vhodit
li v nego dannyj ob容kt; spisok s dvustoronnej svyaz'yu, dlya
kotorogo mozhno prosto i effektivno opredelit' vhodit li v nego dannyj
ob容kt i t.d.
Nazvanie "konkretnyj tip" (CDT - concrete data type, t.e.
konkretnyj tip dannyh) , bylo vybrano po kontrastu s terminom
"abstraktnyj tip" (ADT - abstract data type, t.e. abstraktnyj tip
dannyh). Otnosheniya mezhdu CDT i ADT obsuzhdayutsya v $$13.3.
Sushchestvenno, chto konkretnye tipy ne prednaznacheny dlya yavnogo
vyrazheniya nekotoroj obshchnosti. Tak, tipy slist i vector mozhno
ispol'zovat' v kachestve al'ternativnoj realizacii ponyatiya
mnozhestva, no v yazyke eto yavno ne otrazhaetsya. Poetomu, esli
programmist hochet rabotat' s mnozhestvom, ispol'zuet konkretnye
tipy i ne imeet opredeleniya klassa mnozhestvo, to on dolzhen vybirat'
mezhdu tipami slist i vector. Togda programma zapisyvaetsya v
terminah vybrannogo klassa, skazhem, slist, i esli potom predpochtut
ispol'zovat' drugoj klass, programmu pridetsya perepisyvat'.
|to potencial'noe neudobstvo kompensiruetsya nalichiem vseh
"estestvennyh" dlya dannogo klassa operacij, naprimer takih, kak
indeksaciya dlya massiva i udalenie elementa dlya spiska. |ti
operacii predstavleny v optimal'nom variante, bez "neestestvennyh"
operacij tipa indeksacii spiska ili udaleniya massiva, chto moglo
by vyzvat' putanicu. Privedem primer:
void my(slist& sl)
{
for (T* p = sl.first(); p; p = sl.next())
{
// moj kod
}
// ...
}
void your(vector& v)
{
for (int i = 0; i<v.size(); i++)
{
// vash kod
}
// ...
}
Sushchestvovanie takih "estestvennyh" dlya vybrannogo metoda realizacii
operacij obespechivaet effektivnost' programmy i znachitel'no oblegchaet
ee napisanie. K tomu zhe, hotya realizaciya vyzova podstanovkoj obychno
vozmozhna tol'ko dlya prostyh operacij tipa indeksacii massiva ili
polucheniya sleduyushchego elementa spiska, ona okazyvaet znachitel'nyj
effekt na skorost' vypolneniya programmy. Zagvozdka zdes' sostoit v tom,
chto fragmenty programmy, ispol'zuyushchie po svoej suti ekvivalentnye operacii,
kak, naprimer, dva privedennyh vyshe cikla, mogut vyglyadet' nepohozhimi
drug na druga, a fragmenty programmy, v kotoryh dlya ekvivalentnyh
operacij ispol'zuyutsya raznye konkretnye tipy, ne mogu zamenyat' drug
druga. Obychno, voobshche, nevozmozhno svesti shodnye fragmenty programmy
v odin.
Pol'zovatel', obrashchayushchijsya k nekotoroj funkcii, dolzhen tochno
ukazat' tip ob容kta, s kotorym rabotaet funkciya, naprimer:
void user()
{
slist sl;
vector v(100);
my(sl);
your(v);
my(v); // oshibka: nesootvetstvie tipa
your(sl); // oshibka: nesootvetstvie tipa
}
CHtoby kompensirovat' zhestkost' etogo trebovaniya, razrabotchik nekotoroj
poleznoj funkcii dolzhen predostavit' neskol'ko ee versij, chtoby u
pol'zovatelya byl vybor:
void my(slist&);
void my(vector&);
void your(slist&);
void your(vector&);
void user()
{
slist sl;
vector v(100);
my(sl);
your(v);
my(v); // teper' normal'no: vyzov my(vector&)
your(sl); // teper' normal'no: vyzov your(slist&)
}
Poskol'ku telo funkcii sushchestvenno zavisit ot tipa ee parametra,
nado napisat' kazhduyu versiyu funkcij my() i your() nezavisimo drug
ot druga, chto mozhet byt' hlopotno.
S uchetom vsego izlozhennogo konkretnyj tip, mozhno skazat', pohodit
na vstroennye tipy. Polozhitel'noj storonoj etogo yavlyaetsya tesnaya
svyaz' mezhdu pol'zovatelem tipa i ego sozdatelem, a takzhe mezhdu
pol'zovatelyami, kotorye sozdayut ob容kty dannogo tipa, i pol'zovatelyami,
kotorye pishut funkcii, rabotayushchie s etimi ob容ktami. CHtoby
pravil'no ispol'zovat' konkretnyj tip, pol'zovatel' dolzhen
razbirat'sya v nem detal'no. Obychno ne sushchestvuet kakih-to
universal'nyh svojstv, kotorymi obladali by vse konkretnye tipy
biblioteki, i chto pozvolilo by pol'zovatelyu, rasschityvaya na eti
svojstva, ne tratit' sily na izuchenie otdel'nyh klassov. Takova
plata za kompaktnost' programmy i effektivnost' ee vypolneniya.
Inogda eto vpolne razumnaya plata, inogda net. Krome togo, vozmozhen
takoj sluchaj, kogda otdel'nyj konkretnyj klass proshche ponyat' i
ispol'zovat', chem bolee obshchij (abstraktnyj) klass. Imenno tak
byvaet s klassami, predstavlyayushchimi horosho izvestnye tipy dannyh,
takie kak massivy ili spiski.
Tem ne menee, ukazhem, chto v ideale nado skryvat', naskol'ko
vozmozhno, detali realizacii, poka eto ne uhudshaet harakteristiki
programmy. Bol'shuyu pomoshch' zdes' okazyvayut funkcii-podstanovki.
Esli sdelat' otkrytymi peremennye, yavlyayushchiesya chlenami, s pomoshch'yu opisaniya
public, ili neposredstvenno rabotat' s nimi s pomoshch'yu funkcij, kotorye
ustanavlivayut i poluchayut znacheniya etih peremennyh, to pochti vsegda
eto privodit k plohomu rezul'tatu. Konkretnye tipy dolzhny byt' vse-taki
nastoyashchimi tipami, a ne prosto programmnoj kuchej s neskol'kim funkciyami,
dobavlennymi radi udobstva.
Samyj prostoj sposob oslabit' svyaz' mezhdu pol'zovatelem klassa
i ego sozdatelem, a takzhe mezhdu programmami, v kotoryh ob容kty
sozdayutsya, i programmami, v kotoryh oni ispol'zuyutsya, sostoit v vvedenii
ponyatiya abstraktnyh bazovyh klassov. |ti klassy predstavlyayut
interfejs so mnozhestvom realizacij odnogo ponyatiya. Rassmotrim
klass set, soderzhashchij mnozhestvo ob容ktov tipa T:
class set {
public:
virtual void insert(T*) = 0;
virtual void remove(T*) = 0;
virtual int is_member(T*) = 0;
virtual T* first() = 0;
virtual T* next() = 0;
virtual ~set() { }
};
|tot klass opredelyaet interfejs s proizvol'nym mnozhestvom (set),
opirayas' na vstroennoe ponyatie iteracii po elementam mnozhestva.
Zdes' tipichno otsutstvie konstruktora i nalichie virtual'nogo
destruktora, sm. takzhe $$6.7. Rassmotrim primer:
class slist_set : public set, private slist {
slink* current_elem;
public:
void insert(T*);
void remove(T*);
int is_member(T*);
virtual T* first();
virtual T* next();
slist_set() : slist(), current_elem(0) { }
};
class vector_set : public set, private vector {
int current_index;
public:
void insert(T*);
void remove(T*);
int is_member(T*);
T* first() { current_index = 0; return next(); }
T* next();
vector_set(int initial_size)
: array(initial_size), current_index(0) { }
};
Realizaciya konkretnogo tipa ispol'zuetsya kak chastnyj bazovyj
klass, a ne chlen klassa. |to sdelano i dlya udobstva zapisi, i potomu,
chto nekotorye konkretnye tipy mogut imet' zashchishchennyj interfejs
s cel'yu predostavit' bolee pryamoj dostup k svoim chlenam iz proizvodnyh
klassov. Krome togo, podobnym obrazom v realizacii mogut ispol'zovat'sya
nekotorye klassy, kotorye imeyut virtual'nye funkcii i ne yavlyayutsya
konkretnymi tipami. Tol'ko s pomoshch'yu obrazovaniya proizvodnyh klassov
mozhno v novom klasse izyashchno pereopredelit' (podavit') virtual'nuyu
funkciyu klassa realizacii. Interfejs opredelyaetsya abstraktnym klassom.
Teper' pol'zovatel' mozhet zapisat' svoi funkcii iz $$13.2
takim obrazom:
void my(set& s)
{
for (T* p = s.first(); p; p = s.next())
{
// moj kod
}
// ...
}
void your(set& s)
{
for (T* p = s.first(); p; p = s.next())
{
// vash kod
}
// ...
}
Stalo ochevidnym shodstvo mezhdu dvumya funkciyami, i teper' dostatochno
imet' tol'ko odnu versiyu dlya kazhdoj iz funkcij my() ili your(),
poskol'ku dlya obshcheniya s slist_set i vector_set obe versii ispol'zuyut
interfejs, opredelyaemyj klassom set:
void user()
{
slist_set sl;
vector_set v(100);
my(sl);
your(v);
my(v);
your(sl);
}
Bolee togo, sozdateli funkcij my() i your() ne obyazany znat' opisanij
klassov slist_set i vector_set, i funkcii my() i your() nikoim
obrazom ne zavisyat ot etih opisanij. Ih ne nado peretranslirovat'
ili kak-to izmenyat', ni esli izmenilis' klassy slist_set ili
vector_set ni dazhe, esli predlozhena novaya realizaciya etih klassov.
Izmeneniya otrazhayutsya lish' na funkciyah, kotorye neposredstvenno
ispol'zuyut eti klassy, dopustim vector_set. V chastnosti, mozhno
vospol'zovat'sya tradicionnym primeneniem zagolovochnyh fajlov i
vklyuchit' v programmy s funkciyami my() ili your() fajl opredelenij
set.h, a ne fajly slist_set.h ili vector_set.h.
V obychnoj situacii operacii abstraktnogo klassa zadayutsya kak
chistye virtual'nye funkcii, i takoj klass ne imeet chlenov,
predstavlyayushchih dannye (ne schitaya skrytogo ukazatelya na tablicu
virtual'nyh funkcij). |to ob座asnyaetsya tem, chto dobavlenie nevirtual'noj
funkcii ili chlena, predstavlyayushchego dannye, potrebuet opredelennyh
dopushchenij o klasse, kotorye budut ogranichivat' vozmozhnye realizacii.
Izlozhennyj zdes' podhod k abstraktnym klassam blizok po duhu tradicionnym
metodam, osnovannym na strogom razdelenii interfejsa i ego realizacij.
Abstraktnyj tip sluzhit v kachestve interfejsa, a konkretnye tipy
predstavlyayut ego realizacii.
Takoe razdelenie interfejsa i ego realizacij predpolagaet
nedostupnost' operacij, yavlyayushchihsya "estestvennymi" dlya kakoj-to
odnoj realizacii, no ne dostatochno obshchimi, chtoby vojti v
interfejs. Naprimer, poskol'ku v proizvol'nom mnozhestve net
uporyadochennosti, v interfejs set nel'zya vklyuchat' operaciyu
indeksirovaniya, dazhe esli dlya realizacii konkretnogo mnozhestva
ispol'zuetsya massiv. |to privodit k uhudsheniyu harakteristik programmy
iz-za otsutstviya ruchnoj optimizacii. Dalee, stanovitsya kak pravilo
nevozmozhnoj realizaciya funkcij podstanovkoj (esli ne schitat' kakih-to
konkretnyh situacij, kogda nastoyashchij tip izvesten translyatoru), poetomu
vse poleznye operacii interfejsa, zadayutsya kak vyzovy
virtual'nyh funkcij. Kak i dlya konkretnyh tipov zdes' plata za
abstraktnye tipy inogda priemlema, inogda slishkom vysoka.
Podvodya itog, perechislim kakim celyam dolzhen sluzhit' abstraktnyj tip:
[1] opredelyat' nekotoroe ponyatie takim obrazom, chto v programme
mogut sosushchestvovat' dlya nego neskol'ko realizacij;
[2] primenyaya virtual'nye funkcii, obespechivat' dostatochno vysokuyu
stepen' kompaktnosti i effektivnosti vypolneniya programmy;
[3] svodit' k minimumu zavisimost' lyuboj realizacii ot drugih
klassov;
[4] predstavlyat' samo po sebe osmyslennoe ponyatie.
Nel'zya skazat', chto abstraktnye tipy luchshe konkretnyh tipov, eto
prosto drugie tipy. Kakie iz nih predpochest' - eto, kak pravilo,
trudnyj i vazhnyj vopros dlya pol'zovatelya. Sozdatel' biblioteki mozhet
uklonit'sya ot otveta na nego i predostavit' varianty s obeimi tipami,
tem samym vybor perekladyvaetsya na pol'zovatelya. No zdes' vazhno yasno
ponimat', s klassom kakogo vida imeesh' delo. Obychno neudachej
zakanchivaetsya popytka ogranichit' obshchnost' abstraktnogo tipa, chtoby
skorost' programm, rabotayushchih s nim, priblizilas' k skorosti programm,
rasschitannyh na konkretnyj tip. V etom sluchae nel'zya
ispol'zovat' vzaimozamenyaemye realizacii bez bol'shoj peretranslyacii
programmy posle vneseniya izmenenij. Stol' zhe neudachna byvaet
popytka dat' "obshchnost'" v konkretnyh tipah, chtoby oni mogli po
moshchnosti ponyatij priblizit'sya k abstraktnym tipam. |to snizhaet
effektivnost' i primenimost' prostyh klassov. Klassy etih dvuh vidov
mogut sosushchestvovat', i oni dolzhny mirno sosushchestvovat' v programme.
Konkretnyj klass voploshchaet realizaciyu abstraktnogo tipa, i smeshivat'
ego s abstraktnym klassom ne sleduet.
Otmetim, chto ni konkretnye, ni abstraktnye tipy ne sozdayutsya
iznachal'no kak bazovye klassy dlya postroeniya v dal'nejshem proizvodnyh
klassov. Postroenie proizvodnyh k abstraktnym tipam klassov
skoree nuzhno dlya zadaniya realizacij, chem dlya razvitiya samogo ponyatiya
interfejsa. Vsyakij konkretnyj ili abstraktnyj tip prednaznachen dlya chetkogo
i effektivnogo predstavleniya v programme otdel'nogo ponyatiya. Klassy,
kotorym eto udaetsya, redko byvayut horoshimi kandidatami dlya sozdaniya
na ih baze novyh, no svyazannyh s nimi, klassov. Dejstvitel'no, popytki
postroit' proizvodnye, "bolee razvitye" klassy na baze konkretnyh ili
abstraktnyh tipov, takih kak, stroki, kompleksnye chisla, spiski ili
associativnye massivy privodyat obychno k gromozdkim konstrukciyam.
Kak pravilo eti klassy sleduet ispol'zovat' kak chleny ili chastnye bazovye
klassy, togda ih mozhno effektivno primenyat', ne vyzyvaya putanicy i
protivorechij v interfejsah i realizaciyah etih i novyh klassov.
Kogda sozdaetsya konkretnyj ili abstraktnyj tip, akcent sleduet
sdelat' na tom, chtoby predlozhit' prostoj, realizuyushchij horosho
produmannoe ponyatie, interfejs. Popytki rasshirit' oblast' prilozheniya
klassa, nagruzhaya ego opisanie vsevozmozhnymi "poleznymi" svojstvami,
privodyat tol'ko k besporyadku i neeffektivnosti. |tim zhe konchayutsya
naprasnye usiliya garantirovat' povtornoe ispol'zovanie klassa, kogda
kazhduyu funkciyu-chlen ob座avlyayut virtual'noj, ne podumav zachem i kak
eti funkcii budut pereopredelyat'sya.
Pochemu my ne stali opredelyat' klassy slist i vector kak pryamye
proizvodnye ot klassa set, obojdyas' tem samym bez klassov slist_set
i vector_set? Drugimi slovami zachem nuzhny konkretnye tipy, kogda uzhe
opredeleny abstraktnye tipy? Mozhno predlozhit' tri otveta:
[1] |ffektivnost': takie tipy, kak vector ili slist nado sozdavat'
bez nakladnyh rashodov, vyzvannyh otdaleniem realizacij
ot interfejsov (razdeleniya interfejsa i realizacii trebuet
koncepciya abstraktnogo tipa).
[2] Mnozhestvennyj interfejs: chasto raznye ponyatiya luchshe vsego
realizovat' kak proizvodnye ot odnogo klassa.
[3] Povtornoe ispol'zovanie: nuzhen mehanizm, kotoryj pozvolit
prisposobit' dlya nashej biblioteki tipy, razrabotannye
"gde-to v drugom meste".
Konechno, vse eti otvety svyazany. V kachestve primera [2] rassmotrim
ponyatie generatora iteracij. Trebuetsya opredelit' generator
iteracij (v dal'nejshem iterator) dlya lyubogo tipa tak, chtoby s ego
pomoshch'yu mozhno bylo porozhdat' posledovatel'nost' ob容ktov etogo tipa.
Estestvenno dlya etogo nuzhno ispol'zovat' uzhe upominavshijsya klass slist.
Odnako, nel'zya prosto opredelit' obshchij iterator nad slist, ili dazhe
nad set, poskol'ku obshchij iterator dolzhen dopuskat' iteracii i bolee
slozhnyh ob容ktov, ne yavlyayushchihsya mnozhestvami, naprimer, vhodnye potoki
ili funkcii, kotorye pri ocherednom vyzove dayut sleduyushchee znachenie iteracii.
Znachit nam nuzhny i mnozhestvo i iterator, i v tozhe vremya
nezhelatel'no dublirovat' konkretnye tipy, kotorye yavlyayutsya ochevidnymi
realizaciyami razlichnyh vidov mnozhestv i iteratorov. Mozhno graficheski
predstavit' zhelatel'nuyu strukturu klassov tak:
Zdes' klassy set i iter predostavlyayut interfejsy, a slist i stream
yavlyayutsya chastnymi klassami i predstavlyayut realizacii. Ochevidno,
nel'zya perevernut' etu ierarhiyu klassov i, predostavlyaya obshchie
interfejsy, stroit' proizvodnye konkretnye tipy ot abstraktnyh klassov.
V takoj ierarhii kazhdaya poleznaya operaciya nad kazhdym poleznym abstraktnym
ponyatiem dolzhna predstavlyat'sya v obshchem abstraktnom bazovom klasse.
Dal'nejshee obsuzhdenie etoj temy soderzhitsya v $$13.6.
Privedem primer prostogo abstraktnogo tipa, yavlyayushchegosya
iteratorom ob容ktov tipa T:
class iter {
virtual T* first() = 0;
virtual T* next() = 0;
virtual ~iter() { }
};
class slist_iter : public iter, private slist {
slink* current_elem;
public:
T* first();
T* next();
slist_iter() : current_elem(0) { }
};
class input_iter : public iter {
isstream& is;
public:
T* first();
T* next();
input_iter(istream& r) : is(r) { }
};
Mozhno takim obrazom ispol'zovat' opredelennye nami tipy:
void user(const iter& it)
{
for (T* p = it.first(); p; p = it.next()) {
// ...
}
}
void caller()
{
slist_iter sli;
input_iter ii(cin);
// zapolnenie sli
user(sli);
user(ii);
}
My primenili konkretnyj tip dlya realizacii abstraktnogo tipa, no
mozhno ispol'zovat' ego i nezavisimo ot abstraktnyh tipov ili prosto
vvodit' takie tipy dlya povysheniya effektivnosti programmy,
sm. takzhe $$13.5. Krome togo, mozhno ispol'zovat' odin konkretnyj tip
dlya realizacii neskol'kih abstraktnyh tipov.
V razdele $$13.9 opisyvaetsya bolee gibkij iterator. Dlya nego
zavisimost' ot realizacii, kotoraya postavlyaet podlezhashchie iteracii
ob容kty, opredelyaetsya v moment inicializacii i mozhet izmenyat'sya v hode
vypolneniya programmy.
V dejstvitel'nosti ierarhiya klassov stroitsya, ishodya iz sovsem drugoj
koncepcii proizvodnyh klassov, chem koncepciya interfejs-realizaciya,
kotoraya ispol'zovalas' dlya abstraktnyh tipov. Klass rassmatrivaetsya
kak fundament stroeniya. No dazhe, esli v osnovanii nahoditsya abstraktnyj
klass, on dopuskaet nekotoroe predstavlenie v programme i sam predostavlyaet
dlya proizvodnyh klassov kakie-to poleznye funkcii. Primerami uzlovyh
klassov mogut sluzhit' klassy rectangle ($$6.4.2) i satellite ($$6.5.1).
Obychno v ierarhii klass predstavlyaet nekotoroe obshchee ponyatie, a
proizvodnye klassy predstavlyayut konkretnye varianty etogo ponyatiya.
Uzlovoj klass yavlyaetsya neot容mlemoj chast'yu ierarhii klassov. On pol'zuetsya
servisom, predstavlyaemym bazovymi klassami, sam obespechivaet opredelennyj
servis i predostavlyaet virtual'nye funkcii i (ili) zashchishchennyj
interfejs, chtoby pozvolit' dal'nejshuyu detalizaciyu svoih operacij v
proizvodnyh klassah.
Tipichnyj uzlovoj klass ne tol'ko predostavlyaet realizaciyu
interfejsa, zadavaemogo ego bazovym klassom (kak eto delaet klass
realizacii po otnosheniyu k abstraktnomu tipu), no i sam rasshiryaet
interfejs, dobavlyaya novye funkcii. Rassmotrim v kachestve primera
klass dialog_box, kotoryj predstavlyaet okno nekotorogo vida na ekrane.
V etom okne poyavlyayutsya voprosy pol'zovatelyu i v nem on zadaet svoj
otvet s pomoshch'yu nazhatiya klavishi ili "myshi":
class dialog_box : public window {
// ...
public:
dialog_box(const char* ...); // zakanchivayushchijsya nulem spisok
// oboznachenij klavish
// ...
virtual int ask();
};
Zdes' vazhnuyu rol' igraet funkciya ask() i konstruktor, s pomoshch'yu kotorogo
programmist ukazyvaet ispol'zuemye klavishi i zadaet ih chislovye znacheniya.
Funkciya ask() izobrazhaet na ekrane okno i vozvrashchaet nomer nazhatoj v otvet
klavishi. Mozhno predstavit' takoj variant ispol'zovaniya:
void user()
{
for (;;) {
// kakie-to komandy
dialog_box cont("continue",
"try again",
"abort",
(char*) 0);
switch (cont.ask()) {
case 0: return;
case 1: break;
case 2: abort();
}
}
}
Obratim vnimanie na ispol'zovanie konstruktora. Konstruktor, kak
pravilo, nuzhen dlya uzlovogo klassa i chasto eto netrivial'nyj
konstruktor. |tim uzlovye klassy otlichayutsya ot abstraktnyh klassov,
dlya kotoryh redko nuzhny konstruktory.
Pol'zovatel' klassa dialog_box ( a ne tol'ko sozdatel' etogo
klassa) rasschityvaet na servis, predstavlyaemyj ego bazovymi klassami.
V rassmatrivaemom primere predpolagaetsya, chto sushchestvuet
nekotoroe standartnoe razmeshchenie novogo okna na ekrane. Esli
pol'zovatel' zahochet upravlyat' razmeshcheniem okna, bazovyj dlya
dialog_box klass window (okno) dolzhen predostavlyat' takuyu vozmozhnost',
naprimer:
dialog_box cont("continue","try again","abort",(char*)0);
cont.move(some_point);
Zdes' funkciya dvizheniya okna move() rasschityvaet na opredelennye
funkcii bazovyh klassov.
Sam klass dialog_box yavlyaetsya horoshim kandidatom dlya postroeniya
proizvodnyh klassov. Naprimer, vpolne razumno imet' takoe okno,
v kotorom, krome nazhatiya klavishi ili vvoda s mysh'yu, mozhno zadavat'
stroku simvolov (skazhem, imya fajla). Takoe okno dbox_w_str stroitsya
kak proizvodnyj klass ot prostogo okna dialog_box:
class dbox_w_str : public dialog_box {
// ...
public:
dbox_w_str (
const char* sl, // stroka zaprosa pol'zovatelyu
const char* ... // spisok oboznachenij klavish
);
int ask();
virtual char* get_string();
//...
};
Funkciya get_string() yavlyaetsya toj operaciej, s pomoshch'yu
kotoroj programmist poluchaet zadannuyu pol'zovatelem stroku. Funkciya
ask() iz klassa dbox_w_str garantiruet, chto stroka vvedena pravil'no,
a esli pol'zovatel' ne stal vvodit' stroku, to togda v programmu
vozvrashchaetsya sootvetstvuyushchee znachenie (0).
void user2()
{
// ...
dbox_w_str file_name("please enter file name",
"done",
(char*)0);
file_name.ask();
char* p = file_name.get_string();
if (p) {
// ispol'zuem imya fajla
}
else {
// imya fajla ne zadano
}
//
}
Podvedem itog - uzlovoj klass dolzhen:
[1] rasschityvat' na svoi bazovye klassy kak dlya ih realizacii,
tak i dlya predstavleniya servisa pol'zovatelyam etih klassov;
[2] predstavlyat' bolee polnyj interfejs (t.e. interfejs s bol'shim
chislom funkcij-chlenov) pol'zovatelyam, chem bazovye klassy;
[3] osnovyvat' v pervuyu ochered' (no ne isklyuchitel'no) svoj
obshchij interfejs na virtual'nyh funkciyah;
[4] zaviset' ot vseh svoih (pryamyh i kosvennyh) bazovyh klassov;
[5] imet' smysl tol'ko v kontekste svoih bazovyh klassov;
[6] sluzhit' bazovym klassom dlya postroeniya proizvodnyh klassov;
[7] voploshchat'sya v ob容kte.
Ne vse, no mnogie, uzlovye klassy budut udovletvoryat' usloviyam
1, 2, 6 i 7. Klass, kotoryj ne udovletvoryaet usloviyu 6, pohodit
na konkretnyj tip i mozhet byt' nazvan konkretnym uzlovym klassom.
Klass, kotoryj ne udovletvoryaet usloviyu 7, pohodit na abstraktnyj
tip i mozhet byt' nazvan abstraktnym uzlovym klassom. U mnogih
uzlovyh klassov est' zashchishchennye chleny, chtoby predostavit' dlya
proizvodnyh klassov menee ogranichennyj interfejs.
Ukazhem na sledstvie usloviya 4: dlya translyacii svoej programmy
pol'zovatel' uzlovogo klassa dolzhen vklyuchit' opisaniya vseh ego
pryamyh i kosvennyh bazovyh klassov, a takzhe opisaniya
vseh teh klassov, ot kotoryh, v svoyu ochered', zavisyat bazovye klassy.
V etom uzlovoj klass opyat' predstavlyaet kontrast s abstraktnym tipom.
Pol'zovatel' abstraktnogo tipa ne zavisit ot vseh klassov,
ispol'zuyushchihsya dlya realizacii tipa i dlya translyacii svoej programmy
ne dolzhen vklyuchat' ih opisaniya.
13.5 Dinamicheskaya informaciya o tipe
Inogda byvaet polezno znat' istinnyj tip ob容kta do ego ispol'zovaniya
v kakih-libo operaciyah. Rassmotrim funkciyu my(set&) iz $$13.3.
void my_set(set& s)
{
for ( T* p = s.first(); p; p = s.next()) {
// moj kod
}
// ...
}
Ona horosha v obshchem sluchae, no predstavim,- stalo izvestno,
chto mnogie parametry mnozhestva predstavlyayut soboj ob容kty tipa
slist. Vozmozhno takzhe stal izvesten algoritm perebora elementov, kotoryj
znachitel'no effektivnee dlya spiskov, chem dlya proizvol'nyh
mnozhestv. V rezul'tate eksperimenta udalos' vyyasnit', chto imenno
etot perebor yavlyaetsya uzkim mestom v sisteme. Togda, konechno, imeet
smysl uchest' v programme otdel'no variant s slist. Dopustiv vozmozhnost'
opredeleniya istinnogo tipa parametra, zadayushchego mnozhestvo, funkciyu
my(set&) mozhno zapisat' tak:
void my(set& s)
{
if (ref_type_info(s) == static_type_info(slist_set)) {
// sravnenie dvuh predstavlenij tipa
// s tipa slist
slist& sl = (slist&)s;
for (T* p = sl.first(); p; p = sl.next()) {
// effektivnyj variant v raschete na list
}
}
else {
for ( T* p = s.first(); p; p = s.next()) {
// obychnyj variant dlya proizvol'nogo mnozhestva
}
}
// ...
}
Kak tol'ko stal izvesten konkretnyj tip slist, stali
dostupny opredelennye operacii so spiskami, i dazhe stala vozmozhna
realizaciya osnovnyh operacij podstanovkoj.
Privedennyj variant funkcii dejstvuet otlichno, poskol'ku
slist - eto konkretnyj klass, i dejstvitel'no imeet smysl otdel'no
razbirat' variant, kogda parametr yavlyaetsya slist_set. Rassmotrim
teper' takuyu situaciyu, kogda zhelatel'no otdel'no razbirat' variant kak
dlya klassa, tak i dlya vseh ego proizvodnyh klassov. Dopustim, my
imeem klass dialog_box iz $$13.4 i hotim uznat', yavlyaetsya li on
klassom dbox_w_str. Poskol'ku mozhet sushchestvovat' mnogo proizvodnyh
klassov ot dbox_w_str, prostuyu proverku na sovpadenie s nim
nel'zya schitat' horoshim resheniem. Dejstvitel'no, proizvodnye klassy
mogut predstavlyat' samye raznye varianty zaprosa stroki. Naprimer,
odin proizvodnyj ot dbox_w_str klass mozhet predlagat' pol'zovatelyu
varianty strok na vybor, drugoj mozhet obespechit' poisk v kataloge
i t.d. Znachit, nuzhno proveryat' i na sovpadenie so vsemi proizvodnymi
ot dbox_w_str klassami. |to tak zhe tipichno dlya uzlovyh klassov, kak
proverka na vpolne opredelennyj tip tipichna dlya abstraktnyh klassov,
realizuemyh konkretnymi tipami.
void f(dialog_box& db)
{
dbox_w_str* dbws = ptr_cast(dbox_w_str, &db);
if (dbws) { // dbox_w_str
// zdes' mozhno ispol'zovat' dbox_w_str::get_string()
}
else {
// ``obychnyj'' dialog_box
}
// ...
}
Zdes' "operaciya" privedeniya ptr_cast() svoj vtoroj parametr
(ukazatel') privodit k svoemu pervomu parametru (tipu) pri uslovii, chto
ukazatel' nastroen na ob容kt tip, kotorogo sovpadaet s zadannym
(ili yavlyaetsya proizvodnym klassom ot zadannogo tipa). Dlya proverki
tipa dialog_box ispol'zuetsya ukazatel', chtoby posle privedeniya ego
mozhno bylo sravnit' s nulem.
Vozmozhno al'ternativnoe reshenie s pomoshch'yu ssylki na dialog_box:
void g(dialog_box& db)
{
try {
dbox_w_str& dbws = ref_cast(dialog_box,db);
// zdes' mozhno ispol'zovat' dbox_w_str::get_string()
}
catch (Bad_cast) {
// ``obychnyj'' dialog_box
}
// ...
}
Poskol'ku net priemlemogo predstavleniya nulevoj ssylki, s kotoroj
mozhno sravnivat', ispol'zuetsya osobaya situaciya, oboznachayushchaya oshibku
privedeniya (t.e. sluchaj, kogda tip ne est' dbox_w_str). Inogda
luchshe izbegat' sravneniya s rezul'tatom privedeniya.
Razlichie funkcij ref_cast() i ptr_cast() sluzhit horoshej
illyustraciej razlichij mezhdu ssylkami i ukazatelyami: ssylka obyazatel'no
ssylaetsya na ob容kt, togda kak ukazatel' mozhet i ne ssylat'sya,
poetomu dlya ukazatelya chasto nuzhna proverka.
13.5.1 Informaciya o tipe
V S++ net inogo standartnogo sredstva polucheniya dinamicheskoj informacii
o tipe, krome vyzovov virtual'nyh funkcijX.
X Hotya bylo sdelano neskol'ko predlozhenij po rasshireniyu S++ v etom
napravlenii.
Smodelirovat' takoe sredstvo dovol'no prosto i v bol'shinstve
bol'shih bibliotek est' vozmozhnosti dinamicheskih zaprosov o tipe.
Zdes' predlagaetsya reshenie, obladayushchee tem poleznym svojstvom,
chto ob容m informacii o tipe mozhno proizvol'no rasshiryat'. Ego mozhno
realizovat' s pomoshch'yu vyzovov virtual'nyh funkcij, i ono mozhet
vhodit' v rasshirennye realizacii S++.
Dostatochno udobnyj interfejs s lyubym sredstvom, postavlyayushchim
informaciyu o tipe, mozhno zadat' s pomoshch'yu sleduyushchih operacij:
typeid static_type_info(type) // poluchit' typeid dlya imeni tipa
typeid ptr_type_info(pointer) // poluchit' typeid dlya ukazatelya
typeid ref_type_info(reference) // poluchit' typeid dlya ssylki
pointer ptr_cast(type,pointer) // preobrazovanie ukazatelya
reference ref_cast(type,reference) // preobrazovanie ssylki
Pol'zovatel' klassa mozhet obojtis' etimi operaciyami, a sozdatel'
klassa dolzhen predusmotret' v opisaniyah klassov opredelennye
"prisposobleniya", chtoby soglasovat' operacii s realizaciej
biblioteki.
Bol'shinstvo pol'zovatelej, kotorym voobshche nuzhna dinamicheskaya
identifikaciya tipa, mozhet ogranichit'sya operaciyami privedeniya
ptr_cast() i ref_cast(). Takim obrazom pol'zovatel' otstranyaetsya ot
dal'nejshih slozhnostej, svyazannyh s dinamicheskoj identifikaciej
tipa. Krome togo, ogranichennoe ispol'zovanie dinamicheskoj informacii
o tipe men'she vsego chrevato oshibkami.
Esli nedostatochno znat', chto operaciya privedeniya proshla uspeshno,
a nuzhen istinnyj tip (naprimer, ob容ktno-orientirovannyj
vvod-vyvod), to mozhno ispol'zovat' operacii dinamicheskih zaprosov o tipe:
static_type_info(), ptr_type_info() i ref_type_info(). |ti operacii
vozvrashchayut ob容kt klassa typeid. Kak bylo pokazano v primere s
set i slist_set, ob容kty klassa typeid mozhno sravnivat'. Dlya
bol'shinstva zadach etih svedenij o klasse typeid dostatochno. No dlya
zadach, kotorym nuzhna bolee polnaya informaciya o tipe, v klasse
typeid est' funkciya get_type_info():
class typeid {
friend class Type_info;
private:
const Type_info* id;
public:
typeid(const Type_info* p) : id(p) { }
const Type_info* get_type_info() const { return id; }
int operator==(typeid i) const ;
};
Funkciya get_type_info() vozvrashchaet ukazatel' na nemenyayushchijsya (const)
ob容kt klassa Type_info iz typeid. Sushchestvenno, chto ob容kt
ne menyaetsya: eto dolzhno garantirovat', chto dinamicheskaya informaciya
o tipe otrazhaet staticheskie tipy ishodnoj programmy. Ploho, esli
pri vypolnenii programmy nekotoryj tip mozhet izmenyat'sya.
S pomoshch'yu ukazatelya na ob容kt klassa Type_info pol'zovatel'
poluchaet dostup k informacii o tipe iz typeid i, teper' ego
programma nachinaet zaviset' ot konkretnoj sistemy dinamicheskih
zaprosov o tipe i ot struktury dinamicheskoj informacii o nem.
No eti sredstva ne vhodyat v standart yazyka, a zadat' ih s pomoshch'yu
horosho produmannyh makroopredelenij neprosto.
V klasse Type_info est' minimal'nyj ob容m informacii dlya realizacii
operacii ptr_cast(); ego mozhno opredelit' sleduyushchim obrazom:
class Type_info {
const char* n; // imya
const Type_info** b; // spisok bazovyh klassov
public:
Type_info(const char* name, const Type_info* base[]);
const char* name() const;
Base_iterator bases(int direct=0) const;
int same(const Type_info* p) const;
int has_base(const Type_info*, int direct=0) const;
int can_cast(const Type_info* p) const;
static const Type_info info_obj;
virtual typeid get_info() const;
static typeid info();
};
Dve poslednie funkcii dolzhny byt' opredeleny v kazhdom proizvodnom
ot Type_info klasse.
Pol'zovatel' ne dolzhen zabotit'sya o strukture ob容kta Type_info, i
ona privedena zdes' tol'ko dlya polnoty izlozheniya. Stroka, soderzhashchaya
imya tipa, vvedena dlya togo, chtoby dat' vozmozhnost' poiska informacii
v tablicah imen, naprimer, v tablice otladchika. S pomoshch'yu nee a takzhe
informacii iz ob容kta Type_info mozhno vydavat' bolee osmyslennye
diagnosticheskie soobshcheniya. Krome togo, esli vozniknet potrebnost'
imet' neskol'ko ob容ktov tipa Type_info, to imya mozhet sluzhit' unikal'nym
klyuchom etih ob容ktov.
const char* Type_info::name() const
{
return n;
}
int Type_info::same(const Type_info* p) const
{
return this==p || strcmp(n,p->n)==0;
}
int Type_info::can_cast(const Type_info* p) const
{
return same(p) || p->has_base(this);
}
Dostup k informacii o bazovyh klassah obespechivaetsya funkciyami
bases() i has_base(). Funkciya bases() vozvrashchaet iterator, kotoryj
porozhdaet ukazateli na bazovye klassy ob容ktov Type_info, a s
pomoshch'yu funkcii has_base() mozhno opredelit' yavlyaetsya li zadannyj klass
bazovym dlya drugogo klassa. |ti funkcii imeyut neobyazatel'nyj parametr
direct, kotoryj pokazyvaet, sleduet li rassmatrivat' vse bazovye klassy
(direct=0), ili tol'ko pryamye bazovye klassy (direct=1). Nakonec,
kak opisano nizhe, s pomoshch'yu funkcij get_info() i info() mozhno
poluchit' dinamicheskuyu informaciyu o tipe dlya samogo klassa Type_info.
Zdes' sredstvo dinamicheskih zaprosov o tipe soznatel'no
realizuetsya s pomoshch'yu sovsem prostyh klassov. Tak mozhno izbezhat'
privyazki k opredelennoj biblioteke. Realizaciya v raschete na
konkretnuyu biblioteku mozhet byt' inoj. Mozhno, kak vsegda, posovetovat'
pol'zovatelyam izbegat' izlishnej zavisimosti ot detalej realizacii.
Funkciya has_base() ishchet bazovye klassy s pomoshch'yu imeyushchegosya v
Type_info spiska bazovyh klassov. Hranit' informaciyu o tom, yavlyaetsya
li bazovyj klass chastnym ili virtual'nym, ne nuzhno, poskol'ku
vse oshibki, svyazannye s ogranicheniyami dostupa ili neodnoznachnost'yu,
budut vyyavleny pri translyacii.
class base_iterator {
short i;
short alloc;
const Type_info* b;
public:
const Type_info* operator() ();
void reset() { i = 0; }
base_iterator(const Type_info* bb, int direct=0);
~base_iterator() { if (alloc) delete[] (Type_info*)b; }
};
V sleduyushchem primere ispol'zuetsya neobyazatel'nyj parametr dlya ukazaniya,
sleduet li rassmatrivat' vse bazovye klassy (direct==0) ili tol'ko pryamye
bazovye klassy (direct==1).
base_iterator::base_iterator(const Type_info* bb, int direct)
{
i = 0;
if (direct) { // ispol'zovanie spiska pryamyh bazovyh klassov
b = bb;
alloc = 0;
return;
}
// sozdanie spiska pryamyh bazovyh klassov:
// int n = chislo bazovyh
b = new const Type_info*[n+1];
// zanesti bazovye klassy v b
alloc = 1;
return;
}
const Type_info* base_iterator::operator() ()
{
const Type_info* p = &b[i];
if (p) i++;
return p;
}
Teper' mozhno zadat' operacii zaprosov o tipe s pomoshch'yu makroopredelenij:
#define static_type_info(T) T::info()
#define ptr_type_info(p) ((p)->get_info())
#define ref_type_info(r) ((r).get_info())
#define ptr_cast(T,p) \
(T::info()->can_cast((p)->get_info()) ? (T*)(p) : 0)
#define ref_cast(T,r) \
(T::info()->can_cast((r).get_info()) \
? 0 : throw Bad_cast(T::info()->name()), (T&)(r))
Predpolagaetsya, chto tip osoboj situacii Bad_cast (Oshibka_privedeniya)
opisan tak:
class Bad_cast {
const char* tn;
// ...
public:
Bad_cast(const char* p) : tn(p) { }
const char* cast_to() { return tn; }
// ...
};
V razdele $$4.7 bylo skazano, chto poyavlenie makroopredelenij
sluzhit signalom voznikshih problem. Zdes' problema v tom, chto tol'ko
translyator imeet neposredstvennyj dostup k literal'nym tipam,
a makroopredeleniya skryvayut specifiku realizacii. Po suti dlya hraneniya
informacii dlya dinamicheskih zaprosov o tipah prednaznachena tablica
virtual'nyh funkcij. Esli realizaciya neposredstvenno podderzhivaet
dinamicheskuyu identifikaciyu tipa, to rassmatrivaemye operacii mozhno
realizovat' bolee estestvenno, effektivno i elegantno. V chastnosti,
ochen' prosto realizovat' funkciyu ptr_cast(), kotoraya preobrazuet
ukazatel' na virtual'nyj bazovyj klass v ukazatel' na ego proizvodnye
klassy.
13.5.3 Kak sozdat' sistemu dinamicheskih zaprosov o tipe
Zdes' pokazano, kak mozhno pryamo realizovat' dinamicheskie zaprosy
o tipe, kogda v translyatore takih vozmozhnostej net. |to dostatochno
utomitel'naya zadacha i mozhno propustit' etot razdel, tak kak v nem
est' tol'ko detali konkretnogo resheniya.
Klassy set i slist_set iz $$13.3 sleduet izmenit' tak, chtoby
s nimi mogli rabotat' operacii zaprosov o tipe. Prezhde vsego, v
bazovyj klass set nuzhno vvesti funkcii-chleny, kotorye ispol'zuyut
operacii zaprosov o tipe:
class set {
public:
static const Type_info info_obj;
virtual typeid get_info() const;
static typeid info();
// ...
};
Pri vypolnenii programmy edinstvennym predstavitelem ob容kta tipa
set yavlyaetsya set::info_obj, kotoryj opredelyaetsya tak:
const Type_info set::info_obj("set",0);
S uchetom etogo opredeleniya funkcii trivial'ny:
typeid set::get_info() const { return &info_obj; }
typeid set::info() { return &info_obj; }
typeid slist_set::get_info() const { return &info_obj; }
typeid slist_set::info() { return &info_obj; }
Virtual'naya funkciya get_info() budet predostavlyat' operacii
ref_type_info() i ptr_type_info(), a staticheskaya funkciya info()
- operaciyu static_type_info().
Pri takom postroenii sistemy zaprosov o tipe osnovnaya trudnost'
na praktike sostoit v tom, chtoby dlya kazhdogo klassa ob容kt tipa
Type_info i dve funkcii, vozvrashchayushchie ukazatel' na etot ob容kt,
opredelyalis' tol'ko odin raz.
Nuzhno neskol'ko izmenit' klass slist_set:
class slist_set : public set, private slist {
// ...
public:
static const Type_info info_obj;
virtual typeid get_info() const;
static typeid info();
// ...
};
static const Type_info* slist_set_b[]
= { &set::info_obj, &slist::info_obj, 0 };
const Type_info slist_set::info_obj("slist_set",slist_set_b);
typeid slist_set::get_info() const { return &info_obj; }
typeid slist_set::info() { return &info_obj; }
13.5.4 Rasshirennaya dinamicheskaya informaciya o tipe
V klasse Type_info soderzhitsya tol'ko minimum informacii, neobhodimoj
dlya identifikacii tipa i bezopasnyh operacij privedeniya. No poskol'ku
v samom klasse Type_info est' funkcii-chleny info() i get_info(),
mozhno postroit' proizvodnye ot nego klassy, chtoby v dinamike
opredelyat', kakie ob容kty Type_info vozvrashchayut eti funkcii. Takim
obrazom, ne menyaya klassa Type_info, pol'zovatel' mozhet poluchat'
bol'she informacii o tipe s pomoshch'yu ob容ktov, vozvrashchaemyh funkciyami
dynamic_type() i static_type(). Vo mnogih sluchayah dopolnitel'naya
informaciya dolzhna soderzhat' tablicu chlenov ob容kta:
struct Member_info {
char* name;
Type_info* tp;
int offset;
};
class Map_info : public Type_info {
Member_info** mi;
public:
static const Type_info info_obj;
virtual typeid get_info() const;
static typeid info();
// funkcii dostupa
};
Klass Type_info vpolne podhodit dlya standartnoj biblioteki. |to
bazovyj klass s minimumom neobhodimoj informacii, iz kotorogo
mozhno poluchat' proizvodnye klassy, predostavlyayushchie bol'she informacii.
|ti proizvodnye klassy mogut opredelyat' ili sami pol'zovateli, ili
kakie-to sluzhebnye programmy, rabotayushchie s tekstom na S++, ili sami
translyatory yazyka.
13.5.5 Pravil'noe i nepravil'noe ispol'zovanie dinamicheskoj
informacii o tipe
Dinamicheskaya informaciya o tipe mozhet ispol'zovat'sya vo mnogih
situaciyah, v tom chisle dlya: ob容ktnogo vvoda-vyvoda,
ob容ktno-orientirovannyh baz dannyh, otladki. V tozhe vremya
velika veroyatnost' oshibochnogo ispol'zovaniya takoj informacii.
Izvestno,chto v yazyke Simula ispol'zovanie takih sredstv,
kak pravilo, privodit k oshibkam. Poetomu eti sredstva ne byli
vklyucheny v S++. Slishkom velik soblazn vospol'zovat'sya dinamicheskoj
informaciej o tipe, togda kak pravil'nee vyzvat' virtual'nuyu
funkciyu. Rassmotrim v kachestve primera klass Shape iz $$1.2.5.
Funkciyu rotate mozhno bylo zadat' tak:
void rotate(const Shape& s)
// nepravil'noe ispol'zovanie dinamicheskoj
// informacii o tipe
{
if (ref_type_info(s)==static_type_info(Circle)) {
// dlya etoj figury nichego ne nado
}
else if (ref_type_info(s)==static_type_info(Triangle)) {
// vrashchenie treugol'nika
}
else if (ref_type_info(s)==static_type_info(Square)) {
// vrashchenie kvadrata
}
// ...
}
Esli dlya pereklyuchatelya po tipu polya my ispol'zuem dinamicheskuyu
informaciyu o tipe, to tem samym narushaem v programme princip
modul'nosti i otricaem sami celi ob容ktno-orientirovannogo programmirovaniya.
K tomu zhe eto reshenie chrevato oshibkami: esli v kachestve
parametra funkcii budet peredan ob容kt proizvodnogo ot Circle klassa,
to ona srabotaet neverno (dejstvitel'no, vrashchat' krug (Circle)
net smysla, no dlya ob容kta, predstavlyayushchego proizvodnyj klass, eto
mozhet potrebovat'sya). Opyt pokazyvaet, chto programmistam, vospitannym
na takih yazykah kak S ili Paskal', trudno izbezhat' etoj lovushki.
Stil' programmirovaniya etih yazykov trebuet men'she predusmotritel'nosti,
a pri sozdanii biblioteki takoj stil' mozhno prosto schitat'
nebrezhnost'yu.
Mozhet vozniknut' vopros, pochemu v interfejs s sistemoj dinamicheskoj
informacii o tipe vklyuchena uslovnaya operaciya privedeniya ptr_cast(), a ne
operaciya is_base(), kotoraya neposredstvenno opredelyaetsya s pomoshch'yu
operacii has_base() iz klassa Type_info. Rassmotrim takoj primer:
void f(dialog_box& db)
{
if (is_base(&db,dbox_w_str)) { // yavlyaetsya li db bazovym
// dlya dbox_w-str?
dbox_w_str* dbws = (dbox_w_str*) &db;
// ...
}
// ...
}
Reshenie s pomoshch'yu ptr_cast ($$13.5) bolee korotkoe, k tomu zhe zdes'
yavnaya i bezuslovnaya operaciya privedeniya otdelena ot proverki v operatore
if, znachit poyavlyaetsya vozmozhnost' oshibki, neeffektivnosti i dazhe
nevernogo rezul'tata. Nevernyj rezul'tat mozhet vozniknut' v teh
redkih sluchayah, kogda sistema dinamicheskoj identifikacii tipa
raspoznaet, chto odin tip yavlyaetsya proizvodnym ot drugogo, no
translyatoru etot fakt neizvesten, naprimer:
class D;
class B;
void g(B* pb)
{
if (is_base(pb,D)) {
D* pb = (D*)pb;
// ...
}
// ...
}
Esli translyatoru poka neizvestno sleduyushchee opisanie klassa D:
class D : public A, public B {
// ...
};
to voznikaet oshibka, t.k. pravil'noe privedenie ukazatelya pb k D*
trebuet izmeneniya znacheniya ukazatelya. Reshenie s operaciej ptr_cast()
ne stalkivaetsya s etoj trudnost'yu, poskol'ku eta operaciya primenima
tol'ko pri uslovii, chto v oblasti vidimosti nahodyatsya opisaniya
obeih ee parametrov. Privedennyj primer pokazyvaet, chto operaciya
privedeniya dlya neopisannyh klassov po suti svoej nenadezhna, no
zapreshchenie ee sushchestvenno uhudshaet sovmestimost' s yazykom S.
Kogda obsuzhdalis' abstraktnye tipy ($$13.3) i uzlovye klassy ($$13.4),
bylo podcherknuto, chto vse funkcii bazovogo klassa realizuyutsya
v samom bazovom ili v proizvodnom klasse. No sushchestvuet i drugoj
sposob postroeniya klassov. Rassmotrim, naprimer, spiski, massivy,
associativnye massivy, derev'ya i t.d. Estestvenno zhelanie dlya vseh
etih tipov,