Image
12.6.2016 0 Comments

C++ / Konverzie / 14. časť

Späť na úvod >> Späť na programovanie >> Späť na seriál

Tak už to mám za sebou. Môžete ma v mailoch oslovovať pán inžinier :–). Ale nie, teraz vážne. Úspešne som skončil štúdiom, odteraz som už len bežný pracujúci, nijaké prázdniny, nič, len tvrdá každodenná práca. A pre toto som sedemnásť a pol roka dral lavice a stoličky. No nič, prepáčte mi tú depresívnu náladu, verím, že je to len chvíľkový stav a že ma to časom prejde.

Vráťme sa radšej k seriálu. Minule sme dokončili rozsiahlu tému deklarácií a teraz, ako som avizoval, sa budeme zaoberať štandardnými konverziami v C++ a prácou preprocesora.

Štandardné konverzie

Témou tejto kapitoly sú konverzie (implicitné, teda vykonávané automaticky za rôznych okolností) medzi údajovými typmi v C++. K týmto konverziám najčastejšie dochádza pri vyhodnocovaní výrazov, v ktorých majú jednotlivé operandy rôzne typy. Takisto sa štandardné konverzie uplatňujú pri inicializácii premenných, pri odovzdávaní argumentov funkciám, skrátka všade tam, kde sa očakáva jeden typ a použije sa iný typ. Popri štandardných konverziách jazyk C++ pozná aj konverzie definované používateľom (teraz nemyslím pretypovanie operátorom (typ)!) – tieto konverzie sa však týkajú objektových typov (tried), preto o nich zatiaľ nebudeme hovoriť.

Celočíselné rozšírenia

Prvým typom automatickej konverzie typov je celočíselné rozšírenie. Ide o voľný (t.  j. môj) preklad pôvodného anglického termínu „integral promotion“. Tento typ konverzie sa vyskytuje tak často, že si to vlastne ani neuvedomíme. Totiž všade tam, kde sa očakáva typ int (v znamienkovom či bezznamienkovom variante), môžeme vždy použiť aj ľubovoľný z typov char, short int, enumeračný (enum) typ alebo bitové pole (budeme si o ňom hovoriť neskôr) – opäť v znamienkovej i bezznamienkovej modifikácii. Každý z týchto typov sa v takom prípade pred použitím konvertuje na typ int, prípadne unsigned int. Táto konverzia je priamočiara, pretože konvertujeme typ s menším rozsahom hodnôt na typ s väčším rozsahom hodnôt, takže v konečnom dôsledku sa nanajvýš ku konvertovanej hodnote zľava doplnia nuly.

Na tomto mieste je vhodné spomenúť, že pri odovzdávaní argumentov funkcii sa obyčajne typ char pomocou celočíselného rozšírenia konvertuje na typ int, čo vyplýva z toho, že argumenty sa odovzdávajú cez zásobník a inštrukcie na prácu so zásobníkom poväčšine nevedia ukladať len jeden bajt (čo je bežná veľkosť typu char). Čo sa týka typu short int, ten býva väčšinou 16-bitový a to ešte dnešné procesory zvládnu. V prípade 32-bitových programov sa však môže stať, že vzhľadom na efektivitu programu bude prekladač aj typ short int konvertovať na (v tomto prípade 32-bitový) int.

Ak odovzdávame funkcii také argumenty, ku ktorým neexistujú ich formálne ekvivalenty (t. j. funkcia bola deklarovaná s výpustkou [...]), dochádza k celočíselnému rozšíreniu menovaných typov vždy, nezávisle od prostredia a/alebo typu programu. Takisto sa v takom prípade konvertuje typ float na double.

Celočíselné konverzie

Celočíselné rozšírenia sa vzťahujú na uvedené typy. Často však potrebujeme (resp. nie my, ale prekladač) vykonať konverziu medzi dvoma všeobecnými celočíselnými typmi (napr. unsigned int a long). Pravidlá sú pomerne jednoduché. Ak prevádzame typ s väčším rozsahom na typ s menším rozsahom, musíme rátať s tým, že ak sa výsledok nezmestí do cieľového typu, dostaneme nedefinovanú hodnotu (implementačne závislú). Ak, naopak, prevádzame typ s menším rozsahom na typ s rovnakým či väčším rozsahom, konverzia sa podarí, ale výsledok bude závisieť od znamienkovosti či beznamienkovosti oboch typov.

V prípade, že konvertujeme signed typ na unsigned typ, výsledkom bude najmenšie také neznamienkové číslo, ktoré je s prevádzaným znamienkovým kongruentné modulo 2n. (Na vysvetlenie: čísla a, b sú kongruentné modulo m, ak a = b + k . m, kde k je celé číslo). V praxi táto na pohľad komplikovaná požiadavka vedie k tomu, že bitová reprezentácia konvertovaného čísla sa obyčajne vôbec nezmení (nanajvýš dôjde k doplneniu nulami či jednotkami zľava), pretože záporné čísla sa kódujú pomocou doplnkového kódu. Pri jeho použití napríklad 16-bitové čísla 65 534 a –2 (ktoré sú kongruentné modulo 216), vyzerajú v pamäti rovnako, totiž 0xFFFE. Ak teda napríklad funkcii, ktorá očakáva argument typu unsigned int, odovzdáme hodnotu –2, funkcia dostane hodnotu 65 534 (predpokladáme 16-bitový typ int!). Podobne ak funkcia očakáva typ unsigned long, hodnota –2 sa skonvertuje na hodnotu 4 294 967 294, čo je 0xFFFFFFFE hexadecimálne (došlo k tzv. znamienkovému rozšíreniu, t. j. k doplneniu jednotkami zľava).

Pri konverzii unsigned typu na signed typ je to o niečo jednoduchšie – ak konvertovaná hodnota môže byť zobrazená vo výslednom type, k nijakej zmene nedochádza. V opačnom prípade je výsledok nedefinovaný, ale to je situácia, keď prevádzame typ s väčším rozsahom na typ s menším rozsahom (rozsahom sa myslí interval zobraziteľných čísel, nie bitová šírka typu).

Konverzie medzi float a double

Konverzie medzi typmi s pohyblivou desatinnou čiarkou sú v mnohom podobné celočíselným. Odpadajú nám síce starosti so znamienkami, ale stále sa môže stať, že výsledok nebude zobraziteľný v požadovanom cieľovom type. V takom prípade je, samozrejme, výsledná hodnota nedefinovaná. Ďalej môže nastať situácia, že výsledok síce spadá do rozsahu cieľového typu, ale tento typ má menšiu presnosť. Vtedy je výsledkom najbližšie vyššie či nižšie číslo, zobraziteľné v cieľovom type. Nakoniec, ak prevádzame typ s menšou presnosťou na typ s väčšou presnosťou, hodnota sa nijako nezmení.

Konverzie medzi celými a reálnymi číslami

V nadpise tohto odseku som použil výraz reálne čísla. Samozrejme, to je nezmysel, v počítači nedokážeme reprezentovať reálne čísla, iba racionálne (to sú tie s pohyblivou desatinnou čiarkou). Ale musím brať ohľad na šírku nadpisov, preto mi odpusťte tento malý kompromis. V ďalšom texte prevažne z priestorových dôvodov budem pod reálnymi číslami myslieť tie racionálne, ktoré sú reprezentované typmi float, double, long double.

Konverzia reálnej hodnoty na celočíselnú sa deje odseknutím desatinnej časti. Takáto konverzia je strojovo závislá (obyčajne ju vykonáva matematický koprocesor, resp. FPU časť procesora), nie je napríklad presne definované, ako konvertovať záporné čísla, resp. ktorým smerom ich „odseknúť“ – či smerom k nule alebo od nej. Okrem toho môže nastať situácia, keď sa (po odseknutí desatinnej časti) výsledok nezmestí do rozsahu celočíselného typu a v takom prípade je výsledok ako vždy nedefinovaný.

Opačná konverzia, t. j. prevod celého čísla na reálne, sa vykonáva tak presne, ako to výsledný reálny typ umožňuje. Vieme, že reálne čísla sa uchovávajú v tvare mantisa × 2exponent a vzhľadom na konečnú dĺžku mantisy sa so zvyšujúcim exponentom zväčšujú vzdialenosti medzi dvoma číslami, ktorých mantisa sa líši v najmenej významnom bite. Pre ilustráciu: predstavme si, že máme zariadenie, ktoré dokáže uchovávať reálne čísla na tri platné číslice v tvare m × 10e. Je zrejmé, že rozdiel medzi číslami 1,23 × 102 (čo je číslo 123) a 1,24 × 102 (čo je zase 124) je 1. Ak však zvýšime exponent na 3, rozdiel medzi 1,23 × 103 (čo je 1230) a 1,24 × 103 (čo je 1240) je desaťkrát väčší a teda všetky čísla medzi 1230 a 1239 vrátane budú (napríklad) reprezentované ako 1,23 × 103. Dochádza tu k očividnej strate presnosti, danej, ako som už spomínal, konečnou dĺžkou mantisy. V počítači je situácia veľmi podobná, až na to, že základ exponentu je iba 2 a mantisa je podstatne dlhšia. Stále sa však môže stať, že niektoré celé číslo nebude možné zobraziť úplne presne pomocou reálneho čísla.

Aritmetické konverzie

Tento typ konverzií sa uplatňuje pri vyhodnocovaní výrazov, keď dva operandy jedného operátora (obyčajne aritmetického) majú rôzny typ. Vtedy sa postupuje podľa nasledujúceho vzoru:

  • ak jeden z operandov je typu long double, aj druhý sa prevedie na long double
  • inak, ak jeden z operandov je typu double, aj druhý sa prevedie na double
  • inak, ak jeden z operandov je typu float, aj druhý sa prevedie na float
  • inak sa na oboch operandoch vykoná prípadné celočíselné rozšírenie
  • potom, ak jeden z operandov je typu unsigned long, aj druhý sa prevedie na unsigned long
  • inak, ak jeden z operandov je typu long a druhý typu unsigned, potom ak typ long je schopný zobraziť všetky hodnoty typu unsigned, prevedie sa operand typu unsigned na long, inak sa oba operandy prevedú na unsigned long
  • inak, ak jeden z operandov je typu long, aj druhý sa prevedie na long
  • inak, ak jeden z operandov je typu unsigned, aj druhý sa prevedie na unsigned
  • inak sú oba operandy typu int a k ďalšej konverzii nedochádza

Toto je presný a exaktný postup, pomocou ktorého sa oba operandy prevádzajú na najbližší spoločný typ (smerom nahor). Pri hlbšej analýze postupu zistíme, že takýmto spoločným typom nikdy nie je nižší typ ako int, resp. unsigned int.

Aritmetické konverzie sa týkajú väčšiny binárnych aritmetických operátorov a dokonca aj niektorých unárnych operátorov. Pri aplikácii unárneho operátora na celočíselný operand sa na ňom obyčajne najprv vykoná celočíselné rozšírenie. Ak máme teda napríklad premennú c typu char, potom výraz +c, ktorý na pohľad nemá nijaký efekt, je typu int! Rovnako sa mení typ short. Mimochodom, bolo by teraz rozumné vrátiť sa k časti venovanej operátorom (to bola 6. časť seriálu) a prejsť si vtedajší výklad ešte raz – uvedomíte si tak, kedy a pri ktorých operátoroch má zmysel uvažovať o aritmetických konverziách. Podotýkam, že aj pri operátoroch, ktorých operandy musia byť celočíselné (ako <<, >> a pod.), dochádza pred vyhodnotením ku konverzii, a to minimálne k celočíselným rozšíreniam.

Konverzie ukazovateľov

Aj pri práci s ukazovateľmi (inicializácia, priradenie, porovnanie) dochádza k niekoľkým konverziám. Konštantný výraz, ktorého hodnota je nulová, možno konvertovať na špeciálny ukazovateľ, nazývaný nulový ukazovateľ. Je zaručené, že hodnota takéhoto ukazovateľa bude odlišná od akejkoľvek platnej hodnoty. Obyčajne je bitová reprezentácia nulového ukazovateľa postupnosťou núl. Nulový ukazovateľ sa používa veľmi často, hlavne na vyjadrenie, že daný ukazovateľ neukazuje nikam (resp. že jeho hodnota je neplatná). Operátor delete aplikovaný na nulový ukazovateľ nemá nijaký nežiaduci efekt (čo je zaručené prekladačom!).

Ukazovateľ na ľubovoľný nekonštantný a ne-volatile objekt môžeme konvertovať na typ void*. Dokonca aj ukazovateľ na funkciu môžeme konvertovať na void*, ak typ void* je dostatočne široký (lebo void* je podstate ukazovateľom do dátového segmentu a ukazovateľ na funkciu je ukazovateľom do kódového segmentu; oba typy ukazovateľov nemusia byť rovnako široké).

Výraz typu „pole prvkov typu T“ môžeme konvertovať na ukazovateľ na prvý prvok poľa (čo sa aj s určitými výnimkami, o ktorých, dúfam, už veľmi dobre viete, deje automaticky). Výraz typu „funkcia vracajúca typ T“ sa pri použití automaticky konvertuje na typ „ukazovateľ na funkciu vracajúcu T“ s dvoma výnimkami. Jedna z nich je zrejmá – je to klasické funkčné volanie, operátor (). Druhou výnimkou je aplikácia operátora &, čo je vlastne explicitné vyjadrenie spomínanej konverzie.

Explicitné konverzie

Okrem štandardných konverzií, ktoré sa vykonávajú viac-menej automaticky, máme možnosť explicitne predpísať prekladaču, že chceme konvertovať hodnotu jedného typu na iný typ. Takáto konverzia, ako vieme, sa zapisuje v tvare (typ)výraz alebo typ(výraz). Platia pre ňu určité pravidlá, o ktorých si teraz povieme.

Ak môže byť nejaký typ skonvertovaný na iný pomocou štandardných konverzií, môže byť skonvertovaný aj pomocou explicitných konverzií; význam bude rovnaký.

Ľubovoľný ukazovateľ možno explicitne konvertovať na celočíselný typ s dostatočnou veľkosťou. Výsledok bude mať obyčajne rovnakú bitovú reprezentáciu, takže dostaneme príslušnú adresu, ktorá je obsahom premennej typu ukazovateľ. Naopak môžeme explicitne konvertovať celočíselnú hodnotu na ukazovateľ. Je zrejmé, že výsledok nemusí vôbec ukazovať na platné dáta, ale to už je starosť programátora. Tento trik sa často používal v DOS-e pri prístupe k videopamäti –  jednoducho sa zapísala takáto deklarácia:

char far * vga = (char far *) 0xA0000000;

Pre tých, ktorí vedia, že začiatok framebuffera klasickej VGA karty leží na adrese A000:0000, je všetko jasné. Kľúčové slovo far je špecialitou dosovských prekladačov a vyjadruje, že ide o tzv. vzdialený ukazovateľ, ktorý sa skladá z dvoch častí – segmentovej a offsetovej, ktoré sú v pamäti uložené tak, že pri konverzii far ukazovateľa na celé číslo dostaneme hodnotu segment * 216 + offset. V prípade uvedenej adresy teda dostaneme číslo, uvedené ako inicializátor v našej deklarácii.

Ukazovateľ na jeden typ môžeme explicitne konvertovať na ukazovateľ na iný typ; výsledok, ktorý dostaneme, však môže pri použití spôsobiť výnimku vzhľadom na prípadné požiadavky na zarovnanie objektov v pamäti (niektoré procesory vyžadujú napríklad umiestnenie objektov typu int na adresách deliteľných štyrmi).

Ľubovoľný objekt môžeme explicitne konvertovať na referenčný typ X&, ak ukazovateľ na tento objekt môžeme explicitne konvertovať na typ X*. Výsledok pretypovania na referenciu je ako jediný spomedzi všetkých pretypovaní l-hodnotou.

Je povolené pretypovať medzi sebou ukazovateľ na funkciu a ukazovateľ na dáta, pokiaľ majú oba typy ukazovateľov dostatočne veľkú bitovú šírku. Samozrejme, opäť sa môže stať, že pri použití výsledného ukazovateľa dôjde k chybe ochrany pamäte. Ďalej je povolené pretypovať medzi sebou ukazovatele na rôzne funkcie (t. j. funkcie s rôznym počtom a typmi argumentov a návratovej hodnoty). Ako dopadne volanie funkcie pomocou takto pretypovaného ukazovateľa, ťažko predvídať (hlavne keď funkcia očakáva nejaké argumenty, ktoré nedostane).

Ukazovateľ na konštantný (const) objekt možno pretypovať na ukazovateľ na nekonštantný objekt. Takisto možno pretypovať konštantný objekt alebo referenciu na takýto objekt na nekonštantný objekt, resp. referenciu naň. Pri pokuse o modifikáciu konštantného objektu pomocou pretypovaného ukazovateľa či referencie môžu nastať dva prípady: buď dôjde k chybe ochrany pamäte (ak je napríklad konštantný objekt uložený v read-only segmente), alebo sa nestane nič, t. j. výsledok bude rovnaký ako pri modifikácii nekonštantného objektu. Rovnaké pravidlá platia pre volatile objekty a ukazovatele/referencie na ne.

Čo robí preprocesor

V druhej polovici tejto časti si povieme pár slov o tom, čo sa deje vo fáze spracovania C++ programu preprocesorom a ako toto spracovanie môžeme ovplyvňovať. Vieme, že preprocesor je prvý, kto vidí zdrojový text programu počas jeho prekladu. Výstup preprocesora sa následne odovzdá kompilátoru. Činnosť preprocesora môžeme zhrnúť do troch základných bodov: rozvoj makier, podmienená kompilácia a vkladanie súborov.

Príkazy, ktorými ovplyvňujeme fázu preprocesingu, sa nazývajú obyčajne direktívy preprocesora a začínajú sa znakom #. Je dôležité vedieť, že tento znak musí byť prvým znakom na riadku iným ako biela medzera (t. j. medzera alebo tabulátor), inak preprocesor príslušnú direktívu nerozpozná. Direktívy preprocesora majú svoju vlastnú gramatiku, nezávislú od gramatiky C++ – de facto sa kompilátor C++ nikdy nedozvie, že to, čo dostal na vstupe, prešlo nejakým preprocesorom. Efekt direktívy preprocesora sa končí na konci spracúvanej prekladovej jednotky (t. j. súboru), pokiaľ ho explicitne nezmení iná direktíva.

V prípade, že potrebujeme zapísať dlhšiu direktívu (a pri používaní makier to nie je nič nezvyčajné), môžeme pokračovať na ďalšom riadku, musíme však predchádzajúci riadok ukončiť znakom \ (back-slash). Preprocesor dva (či viaceré) takto rozdelené riadky pred spracovaním jednoducho spojí. Ako príklad (trochu netypický) môže slúžiť nasledujúca direktíva (ktorá je vám už veľmi dobre známa):

#include \
    <stdio.h>

Znak \ však nesmie byť posledným znakom súboru.

Preprocesing môžeme rozložiť do niekoľkých fáz. Skutočná implementácia môže byť, samozrejme, ľubovoľná, ale výsledný efekt musí byť zhodný. Tu sú jednotlivé fázy:

  • vykonajú sa prípadné systémovo-závislé preklady znakov (napr. CR/LF namiesto LF a pod.), trigrafy (pozri ďalej) sa nahradia svojimi jednoznakovými ekvivalentmi
  • spoja sa riadky rozdelené pomocou znaku \ (jednoducho sa zo zdrojového textu vymaže každá dvojica znakov \ a prechod na nový riadok)
  • zdrojový text sa rozloží na postupnosť lexikálnych jednotiek (nielen jazyka C++, ale aj jazyka preprocesora!), každý komentár sa nahradí jednou medzerou
  • vykonajú sa jednotlivé direktívy a definované makrá sa nahradia svojimi rozvojmi
  • „escape“ sekvencie v znakových a reťazcových konštantách (ako \n a pod.) sa nahradia svojimi ekvivalentmi
  • susediace reťazcové konštanty sa spoja (ako napr. "ab" "cd" sa spojí do "abcd")

Výsledkom preprocesingu je upravený zdrojový text programu, ktorý sa predloží na ďalšie spracovanie kompilátoru.

Spomínané trigrafy (trigrafové sekvencie) sú dnes už prakticky zbytočnou a dávno zastaranou súčasťou C++. Ich cieľom malo byť umožnenie zápisu niektorých „interpunkčných“ znakov, nevyhnutných na zápis programu, aj v takých znakových sadách, ktoré prekypovali diakritikou na úkor práve týchto znakov. V tabuľke č. 1 je uvedený vždy znak a jeho trigrafový ekvivalent. Nie všetky prekladače podporujú trigrafy, to už však dnes zrejme nikoho trápiť nebude.

znak

trigraf

znak

trigraf

znak

trigraf

#

?==

[

??(

{

??<

\

??/

]

??(

}

??>

^

??´

I

??!

~

??–

Tab. 1 – „Trigrafové sekvencie“

Definícia a rozvoj makier

Jednou z najdôležitejších úloh preprocesora je rozvoj vložených makier. Pod makrom budeme v ďalšom texte rozumieť textové makro, čo nie je nič iné ako vopred definovaná postupnosť znakov, ktorá sa počas preprocesingu nahradí inou, takisto vopred definovanou postupnosťou znakov. Poviete si, na čo je to dobré, to nemôžem do programu priamo napísať tú výslednú postupnosť? Čo však v prípade, že máte nejaký úsek programu (hocijaký, od jedinej číslice, vyjadrujúcej počet kanálov vašej zvukovej karty, cez krátku textovú správu, pomocou ktorej žiadate od používateľa, aby stlačil nejaký kláves, až po jednu celú funkciu) a tento úsek sa vyskytuje na viacerých miestach. Ak by nebolo makier, každá zmena takéhoto opakovaného úseku by znamenala komplikované prechádzanie celým zdrojovým textom, hľadanie všetkých výskytov a ich úpornú modifikáciu, nehovoriac o zúfalých pokusoch udržať zhodné všetky kópie úseku. Makrá celú námahu zjednodušia obmedzením hľadania miesta modifikácie na jediný bod – miesto definície makra. V celom programe sa bude používať len názov tohto makra, ktorý sa počas preprocesingu nahradí jeho skutočným obsahom. Je pravda, že so zavedením konštantných objektov v C++ (teda objektov deklarovaných ako const) význam makier mierne ustúpil do pozadia, no vzhľadom na možnosť definície parametrizovaných makier stále ešte existujú situácie, keď je vhodné použiť namiesto konštanty makro preprocesora (i keď, aby som bol objektívny, aj parametrizované makrá majú svoju náhradu – funkcie inline). Základnou nevýhodou použitia makier je skutočnosť, že ich reprezentáciu pozná iba preprocesor, a preto nie sú dostupné počas ladenia v debuggeri.

Prvý typ makier (bez parametrov) sa definuje pomocou direktívy #define:

#define identifikátor reťazec

Počnúc prvým riadkom za touto direktívou nahradí preprocesor všetky výskyty identifikátora uvedeným reťazcom. Reťazec môže obsahovať aj medzery, prípadné biele znaky medzi ním a identifikátorom a za ním (v definícii, samozrejme) sa ignorujú. Raz definovaný identifikátor môžeme pomocou #define predefinovať iba na rovnaký substitučný reťazec. Príklad:

#define N 256
double matrix[N][N];

Po prechode preprocesorom sa uvedená deklarácia zmení na:

double matrix[256][256];

Druhým typom makier sú makrá s parametrami. Definujú sa podobne:

#define identifikátor( param , … , param ) reťazec

Medzi identifikátorom a ľavou okrúhlou zátvorkou nesmie byť medzera! Parametre makra sú takisto bežné identifikátory. Každý výskyt identifikátora, nasledovaného zátvorkou (, zoznamom lexikálnych jednotiek a pravou zátvorkou ) sa nahradí uvedeným reťazcom, v ktorom budú výskyty jednotlivých (formálnych) parametrov nahradené skutočnými parametrami (podobne ako pri volaní funkcií). Skutočné argumenty (lexikálne jednotky) musia byť oddelené čiarkami. Počet formálnych a skutočných argumentov musí byť zhodný. Ukážeme si príklad:

#define INDEX_MASK 0xFF00
#define EXTRACT(word,mask) word & mask

S takto definovanými makrami sa výraz

index = EXTRACT(data, INDEX_MASK);

rozvinie na

index = data & 0xFF00;

Je veľmi dôležité uvedomiť si, že pri rozvoji makier dochádza k čisto textovej substitúcii, nezávislej od konštrukcií a gramatiky C++. Z toho dôvodu sa riadok s direktívou #define nekončí bodkočiarkou (pokiaľ to tak explicitne nechceme). A ďalej si treba dobre premyslieť, čo všetko môže byť skutočným argumentom nášho makra. Klasický príklad – majme makro, ktoré vypočíta súčin svojich argumentov (no, nevypočíta, ale vytvorí taký výraz):

#define KRAT(a,b) a * b

Na pohľad je všetko v poriadku. Ak však zavoláme toto makro s nasledujúcimi parametrami:

x = KRAT(5 + 6, 7 + 8);

dostaneme po substitúcii výsledok:

x = 5 + 6 * 7 + 8;

čo zrejme nie je to, čo sme chceli. Univerzálnym riešením je preto používať pri definícii takýchto parametrických makier zátvorky všade tam, kde by mohlo dôjsť k nesprávnej interpretácii. Správne definované makro KRAT potom vyzerá takto:

#define KRAT(a,b) ((a) * (b))

Ak vám nie je jasné, prečo je celý súčin navyše obalený ďalším párom zátvoriek, zamyslite sa nad volaním KRAT(1+2, 3+4) + KRAT(5+6, 7+8).

Pri definícii makier máme k dispozícii dva špeciálne operátory, # a ##. Každý formálny parameter v definícii makra, ktorému tesne predchádza operátor #, bude pri rozvoji makra nahradený nie priamo skutočným parametrom, ale reťazcovým literálom C++, obsahujúcim tento skutočný parameter. Myslím, že príklad to ozrejmí:

#define LABEL(text) "label_" #text

Ak toto makro použijeme napríklad v tvare:

LABEL(x001)

dostaneme po nahradení takýto výsledok:

"label_" "x001"

z čoho po spojení reťazcov vznikne literál:

"label_x001"

Argumentom makra LABEL môže byť prakticky ľubovoľný text, ktorý sa pri substitúcii de facto „obalí“ úvodzovkami.

Druhý z operátorov, ##, slúži na spájanie lexikálnych jednotiek. Ak sa vyskytne v definícii makra, po nahradení formálnych parametrov skutočnými sa jednoducho odstráni spolu so všetkými bielymi medzerami, ktoré ho obklopujú. Príklad:

#define PTRDEF(T) typedef T* ptr_ ## T

Argumentom tohto makra by malo byť meno existujúceho typu. Makro slúži na deklaráciu nového typu, ekvivalentného s ukazovateľom na pôvodný typ. Jeho použitie napríklad v tvare:

PTRDEF(double);

vedie k substitúcii:

typedef double* ptr_double;

ktorej dôsledkom je deklarácia nového typu ptr_double. Vidíme, že postupnosť znakov ptr_ ## T z definície makra sa po nahradení T reťazcom double a následnom spojení zmenila na ptr_double.

Po nahradení všetkých parametrov makra skutočnými argumentmi je výsledok opätovne podrobený analýze a prípadným ďalším substitúciám. Ak sa však v rozvoji makra vyskytne jeho názov, rekurzívna substitúcia sa nevykoná (pretože by sa nikdy neskončila). Rozvoj makra sa skončí, keď sa v ňom nenájde nijaký ďalší reťazec podliehajúci substitúcii. Ak je prípadne výsledkom rozvoja niečo, čo na pohľad vyzerá ako direktíva preprocesora, už sa to ďalej nespracúva.

V prípade, že potrebujeme predefinovať existujúce makro, musíme ho najprv zrušiť direktívou #undef. Jej syntax je jednoduchá:

#undef identifikátor

Ak náhodou identifikátor nie je platným názvom makra, direktíva sa ignoruje.

Vkladanie súborov

Na vkladanie iných (obyčajne hlavičkových) súborov do zdrojových súborov programu slúži direktíva #include. Opisoval som ju už v jednej z predchádzajúcich častí, takže sa nebudem opakovať. Stručne len spomeniem, že sú povolené dva tvary:

#include <meno_súboru>

a

#include "meno_súboru"

(rozdiely pozri v predchádzajúcom výklade). Ak reťazec za direktívou #include nemá ani jeden z dvoch uvedených tvarov, podlieha normálnemu spracovaniu preprocesorom, ktorého výsledok musí mať jeden z týchto tvarov.

Podmienená kompilácia

Preprocesor jazyka C++ umožňuje riadiť, ktoré časti zdrojového kódu sa budú prekladať a ktoré nie. Všeobecná schéma na vyjadrenie podmienenej kompilácie je nasledujúca:

#if konšt-výraz
...
#elif konšt-výraz
...
#else
...
#endif

Prvá z direktív, #if, začína úsek podliehajúci podmienenej kompilácii. Ak je konštantný výraz, ktorý je argumentom tejto direktívy, nenulový, všetok nasledujúci text až po prvú z direktív #elif, #else a #endif sa bude prekladať. V opačnom prípade (t. j. výraz je nulový) sa text ignoruje a bude sa hľadať nasledujúca direktíva. Ak je ňou #elif, celý proces s vyhodnocovaním výrazu sa opakuje, ak je ňou #else, prekladať sa bude text nasledujúci za #else. Celý blok podmienenej kompilácie ukončuje direktíva #endif. V rámci jedného bloku môže byť aj viacero direktív #elif. Je zrejmé, že prekladu bude podliehať najviac jeden úsek kódu, a to ten, ktorý nasleduje za direktívou s nenulovou hodnotou výrazu, resp. za direktívou #else. Tento úsek kódu podlieha, samozrejme, aj ďalšiemu spracovaniu preprocesorom (ako prípadná ďalšia podmienená kompilácia, rozvoj makier atď.).

Konštantný výraz musí spĺňať požiadavky na správnosť z hľadiska gramatiky C++, musí byť celočíselný, nesmie obsahovať pretypovanie, operátor sizeof a enumeračné konštanty. Typy int a unsigned int sa berú ako long a unsigned long. Navyše je dovolené používať špeciálny unárny operátor defined, ktorý sa vyhodnocuje na jednotku, ak je jeho argument definovaným (a dosiaľ nezrušeným) makrom, a na nulu, ak nie je. Možno ho použiť v dvoch tvaroch:

defined identifikátor

alebo

defined (identifikátor)

Direktívu v tvare:

#if defined identifikátor

môžeme skrátene zapísať aj takto:

#ifdef identifikátor

a direktívu v tvare:

#if !defined identifikátor

zase takto:

#ifndef identifikátor

Ešte si ukážeme krátky príklad. V nasledujúcom úseku kódu definujeme typ NUMBER podľa zvolenej presnosti ako double alebo float. Voľba sa realizuje na základe existencie či neexistencie makra HIGH_PRECISION:

#define HIGH_PRECISION
#ifdef HIGH_PRECISION
    typedef double NUMBER;
#else
    typedef float NUMBER;
#endif
 
NUMBER vector[1000];

Samozrejme, pokiaľ chceme, aby program reagoval na túto našu voľbu, musíme ďalej dôsledne používať typ NUMBER.

Ďalšie direktívy

V C++ máme k dispozícii okrem spomínaných direktív ešte niekoľko ďalších. Prvá z nich, direktíva #line, slúži na zmenu aktuálneho čísla riadka či mena súboru na účely symbolického ladenia. Samozrejme, nemení tieto údaje z hľadiska kompilátora, ale len na účely prípadného výpisu, zmenou špeciálnych automaticky definovaných makier __LINE__ a __FILE__ (pozri ďalej). Jej syntax je nasledujúca:

#line konštanta "meno_súboru"

Meno súboru nie je povinné. Riadok s touto direktívou pred spracovaním podlieha textovej substitúcii.

Direktíva #error slúži na zastavenie prekladu:

#error správa

Správu uvedenú v direktíve vypíše kompilátor po zastavení prekladu. Táto direktíva sa používa v prípade, že nechceme pokračovať v preklade, lebo nie je napríklad splnená určitá podmienka. Jednoduchý príklad – ak sa omylom budeme pokúšať C++ program kompilátorom jazyka C, môže takáto direktíva zastaviť preklad bez toho, aby sme od kompilátora dostali dlhočizný zoznam čudesných chýb:

#ifndef _cplusplus
#error "C++ compiler must be used!"
#endif

Makro _cplusplus je automaticky definované, pokiaľ používame kompilátor jazyka C++.

Direktíva #pragma umožňuje výrobcom kompilátorov doplniť existujúcu funkčnosť preprocesora a/alebo kompilátora vlastnými vylepšeniami. Jej rôzne formy sú vždy implementačne závislé, typicky sa používa na potlačenie rôznych druhov varovaní, na optimalizáciu prekladu, definíciu zarovnávania dátových štruktúr a pod. Všeobecná syntax je:

#pragma reťazec

Pre bližšie informácie treba nahliadnuť do dokumentácie k prekladaču.

Poslednou direktívou je tzv. prázdna direktíva, ktorá nemá nijaký efekt. Zapisuje sa veľmi jednoducho:

#

Počas kompilácie prekladač automaticky definuje niekoľko makier. Dve z nich, __LINE__ a __FILE__, sme už spomínali. Prvé z nich obsahuje vždy číslo práve prekladaného riadka, druhé zase meno práve prekladaného súboru. Makro __DATE__ obsahuje dátum prekladu (vo forme reťazcového literálu, t. j. v úvodzovkách) a makro __TIME__ logicky čas prekladu (opäť vo forme reťazca). Tieto makrá nemožno predefinovať ani zrušiť. Navyše by mal každý prekladač C++ definovať makro _cplusplus.

Popri spomínaných makrách každý z prekladačov môže definovať rôzne iné makrá, vyjadrujúce napr. typ prostredia, typ aplikácie, pamäťový model a iné.

Čo nás čaká nabudúce?

Nabudúce sa budeme venovať o štandardnej knižnici jazyka C. Nebude to podrobný opis ani prerozprávanie manuálu, skôr len naznačím okruhy funkcií, ktoré máme k dispozícii. To bude posledná časť zaoberajúca sa neobjektovou polovicou C++. Úmyselne som vynechal problematiku štruktúrovaných typov (struct, union), ktoré súvisia s objektovými typmi a bude vhodnejšie hovoriť o nich v tejto súvislosti.

C++

Nechajte si posielať prehľad najdôležitejších správ emailom

Mohlo by Vás zaujímať

Ako na to

Tipy a triky: Ako na snímku obrazovky na akomkoľvek počítači s Windows?

02.12.2016 00:13

Ak snímky obrazovky robíte často apotrebujete napríklad funkcie na posun stránok alebo snímanie zobrazenia pri vyššom rozlíšení displeja, zrejme používate nejakú špecializovanú aplikáciu. Väčšina použ ...

Ako na to 1

Tipy a triky: Ako aplikácii prednastaviť spúšťanie s administrátorskými právami?

30.11.2016 00:10

Väčšina aspoň trochu skúsenejších používateľov vie, že aj keď máte na operačnom systéme Windows vytvorený administrátorský účet, aplikácie pre bezpečnosť nefungujú vždy splnými administrátorskými práv ...

Ako na to 2

Tipy a triky: Ako vypnúť uzamykaciu obrazovku vo Windows 10?

29.11.2016 00:10

Rozčuľuje vás, že pred každým prihlásením doúčtu vášho počítača musíte prejsť uzamykacou obrazovkou? Windows 10 na tejto obrazovke ukazuje čas,dátum anejakú zaujímavú fotografiu zrôznych kútov sveta. ...

Žiadne komentáre

Vyhľadávanie

Kyocera - prve-zariadenia-formatu-a4-s-vykonom-a3

Najnovšie videá