al'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. 2.4.4 Stroki 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' 2.4.5 Nul' 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 2.5.1. Perechisleniya 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. 2.6. |konomiya pamyati 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'. 2.6.1 Polya 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. 2.6.2. Ob容dineniya 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). 2.7 Uprazhneniya 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. 3.1 Kal'kulyator 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++. 3.1.1 Analizator 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. 3.1.2 Funkciya vvoda 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). 3.1.3 Tablica imen 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