Image
12.6.2016 0 Comments

C++ / Deklarácie I. / 11. časť

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

Zima je predo dvermi, pomaly sa nám schyľuje k Vianociam, aj Nový rok tu bude, čo nevidieť, poďme si zaspievať… jaj, pardon, to sem nepatrí. Takže poďme sa venovať téme, ktorú pre jej rozsah budeme musieť rozdeliť do viacerých častí, téme, ktorú sme vždy len tak letmo spomenuli s odkazom, že „podrobnejšie v niektorom z budúcich pokračovaní “ – téme deklaráciídefinícií. Vzhľadom na trochu väčšiu náročnosť   vás vopred varujem, aby ste radšej čítali pozorne a nepreskakovali odseky, inak sa môže stať, že budete mať z celého výkladu iba guláš. Ako vždy budem podopierať teóriu príkladmi (a ako vždy v takýchto situáciách nie príliš zmysluplnými, skôr účelovými), takže to hádam spolu dokážeme… no dobre, dobre, nechám už politiku na pokoji.

Základné pojmy

Výklad začneme objasnením  určitých základných pojmov, ktoré sa problematiky deklarácií úzko týkajú. Pod menom budeme rozumieť objekt, funkciu, enumerátor (ešte nevieme, čo to je, ale dostaneme sa k tomu), typ, hodnotu alebo návestie. Nové meno zavádzame do programu práve deklaráciou. Každé meno má pridruženú oblasť programu, v ktorej môže byť použité. Túto oblasť budeme nazývať rozsah platnosti, čo je voľný preklad anglického termínu scope. Každé meno má svoj typ, na základe ktorého môžeme určiť, čo sa s menom dá robiť a kde ho môžeme použiť. Jedno meno použité v rôznych miestach programu (rôznych blokoch, funkciách či súboroch) môže, ale nemusí reprezentovať tú istú entitu.

Objekt je oblasť, kde sú uložené údaje (ang. region of storage). Pojem  objekt sa prakticky  zhoduje s pojmom premenná, tak ako sme si ju dosiaľ prezentovali. Nie každý objekt však musí byť prístupný svojím menom (spomeňme si na dynamicky alokované premenné, ktoré sú prakticky prístupné len pomocou ukazovateľov na ne). Pomenované objekty majú definovanú tzv. ukladaciu triedu (storage class), ktorá určuje, kedy a ako sa objekt vytvorí, ako dlho bude existovať a kedy a ako sa zničí. Význam údajov uložených v objekte závisí od typu výrazu, ktorým k objektu pristupujeme.

Deklarácie a definície

Je nevyhnutné, aby ste hneď od začiatku chápali rozdiel medzi deklaráciou a definíciou. Deklarácia, ako sme si už povedali, zavádza do programu jedno alebo viacero mien. Definíciou je taká deklarácia, ktorá súčasne spôsobí, že prekladač pri preklade daného úseku programu vyhradí pre deklarované meno pamäť. Deklarácie, ktoré nie sú definíciami, sa dajú zhrnúť do nasledujúcich kategórií: deklarácie funkcií, ktoré neobsahujú telo funkcie (pozri predposledný diel – ukážkový program a v ňom spomínané prototypy funkcií), deklarácie bez inicializácie obsahujúce špecifikátor extern (určuje, že meno má tzv. externé linkovanie – preberieme neskôr) a typedef deklarácie (vytvárajúce nové, programátorom definované typy); vynechali sme ešte niekoľko prípadov týkajúcich sa deklarácie tried a ich členov, túto problematiku však odložíme do druhej polovice seriálu. Všetky ostatné deklarácie sú súčasne definíciami, a teda spôsobia alokáciu pamäte prekladačom.

Uveďme si niekoľko príkladov definícií:

int a;
extern double pi = 3.14;
int f(int x) { return x*x; }

a niekoľko príkladov deklarácií, ktoré nie sú definíciami:

extern int a;
extern double pi;
int f (int x);
typedef int BOOL;

Pre každý objekt, funkciu a enumerátor musí byť v programe práve jedna definícia (ktorá má na starosti rezerváciu pamäte). Funkcia, ktorú nikde v programe nevoláme ani nezisťujeme jej adresu, nemusí byť definovaná, hoci sme uviedli jej prototyp.

Rozsah platnosti

V C++ existujú štyri rôzne rozsahy platnosti: lokálny, funkčný, súborový a triedny. Ten posledný nám o niečo starším možno evokuje socialistickými ideológmi definovaný  triedny boj, týka sa však rozsahu platnosti členov objektových tried a zatiaľ si ho nebudeme opisovať.

Lokálny rozsah platnosti (local scope) majú mená deklarované v bloku. Blokom sa myslí oblasť uzavretá v krútených zátvorkách, teda zložený príkaz, telo funkcie a pod. Meno s lokálnym rozsahom platnosti môžeme použiť len v bloku, v ktorom je definované, a prípadne v blokoch, ktoré sú v tomto bloku vnorené. V prípade lokálneho rozsahu platnosti obyčajne hovoríme o lokálnych menách.

Funkčný rozsah platnosti (function scope) je špecifickým rozšírením lokálneho rozsahu a mená takto deklarované možno použiť hocikde vo funkcii, v ktorej sú deklarované (to znamená, že aj skôr, ako vôbec boli deklarované, čo nie je možné pri predchádzajúcom type rozsahu, keď, ako už vieme, môžeme použiť meno až za bodom jeho deklarácie). Jedinými entitami, ktoré v C++ môžu mať funkčný rozsah platnosti, sú návestia. Isto si spomínate, ako deklarujeme návestie, a  na to že v príkaze goto môžeme použiť aj návestie, ktoré sa nachádza o niekoľko riadkov programu dopredu. Rozsahom platnosti návestia je funkcia, v ktorej bolo deklarované; raz definované návestie nemožno deklarovať v tej istej funkcii znova. Návestia však nezdieľajú priestor mien s ostatnými identifikátormi, a preto môžeme mať v jednej funkcii napríklad návestie end aj premennú end.

Súborový rozsah platnosti (file scope) majú všetky mená deklarované mimo akéhokoľvek bloku alebo triedy. Použiť takéto meno môžeme hocikde v danom súbore (súbor berieme ako prekladovú jednotku [translation unit] – preklad programu sa deje po jednotlivých súboroch, spomínate si ešte na druhé pokračovanie seriálu?), ale takisto až za miestom deklarácie. Mená deklarované so súborovým rozsahom platnosti sa často nazývajú globálnymi a minimálne v danej prekladovej jednotke musia byť jedinečné. Funkcie môžeme deklarovať iba s týmto rozsahom platnosti – neexistujú lokálne funkcie ako v Pascale!

V podstate nám na začiatok stačí rozlišovať lokálne a globálne mená a vedieť, kedy ktoré môžeme používať. Na nasledujúcom príklade (tentoraz ide o kompletný program!) si ukážeme oba typy mien:

#include <stdio.h>
 
int a;
 
void foo()
{
    int b;
    printf("Entering foo()...\n");
    a = 13;
    b = 25;
    printf("a = %i, b = %i\n", a, b);
    printf("Exiting foo()...\n");
}
 
void main()
{
    int b;
    printf("Entering main()...\n");
    a = 5;
    b = 8;
    printf("a = %i, b = %i\n", a, b);
    foo();
    printf("a = %i, b = %i\n", a, b);
    printf("Exiting main()...\n");
}

V príklade máme definovanú jednu globálnu premennú a. Okrem hlavnej funkcie main() sme definovali ešte jednu pomocnú funkciu foo(). V  oboch funkciách sa nachádza deklarácia (pravda, aj definícia, ale nebudem to už ďalej zdôrazňovať) lokálnej premennej b. Volaná funkcia foo() napohľad modifikuje tie isté premenné ako funkcia main(), ale iba premenná a v oboch prípadoch predstavuje ten istý objekt, a to z toho dôvodu, že je deklarovaná ako globálna. Znamená to takisto, že všade v našom programe meno a reprezentuje tú istú premennú (pokiaľ ho neprekryjeme - pozri ďalší odsek) a nemôžeme nikde definovať inú globálnu premennú s rovnakým názvom. Meno b v každej z funkcií predstavuje rôzny objekt – miestnu, lokálnu premennú, o čom sa môžeme presvedčiť aj z kontrolných výpisov. (Poznámka: Výrok týkajúci sa jedinečnosti globálneho objektu s názvom a platí len pre jednosúborový program, ako napríklad ten náš. V časti o linkovaní sa dozviete, ako je to pri viacsúborových programoch.)

V prípade, že deklarujeme lokálnu premennú s rovnakým názvom, ako už existujúca (teda deklarovaná) globálna premenná (čo je, samozrejme, dovolené), bude pôvodná premenná pod svojím menom ďalej nedostupná. Ak chceme pracovať s oboma,  v prvom rade nebudeme vymýšľať a lokálnu premennú nazveme ináč. Niekedy však nemáme iné východisko a vtedy nám na sprístupnenie globálnej premennej poslúži špeciálny operátor, o ktorom sme  ešte nehovorili, a to operátor „rozlíšenia rozsahu platnosti“ (ako vždy je lepší pôvodný anglický termín scope resolution operator), ktorého symbol je :: (dve dvojbodky za sebou, inak aj „štvorbodka“). Tento operátor je unárny a prefixový, jeho asociativita nemá význam. Jeho operandom musí byť meno so súborovým rozsahom platnosti. Operátor nemá nijaký vplyv na obsah premennej ani na jeho interpretáciu (toho obsahu), používa sa v tomto prefixovom tvare len na sprístupnenie inak zakrytých globálnych mien. Príklad:

#include <stdio.h>
 
int i = 11;
 
void main()
{
    int i = 22;
    printf("i = %i, ::i = %i\n", i, ::i);
    {
        int i = 33;
        printf("i = %i, ::i = %i\n", i, ::i);
    }
}

V programe sa nachádzajú tri premenné i. Jedna globálna s hodnotou 11 a dve lokálne, jedna s hodnotou 22 na úrovni tela funkcie, druhá s hodnotou 33 vo vnorenom bloku. Druhá deklarácia zakrýva prvú a tretia druhú, globálna premenná je však stále prístupná prostredníctvom operátora ::, ako vidieť z výpisov. Neexistuje však spôsob, akým by sme sprístupnili zakrytú lokálnu premennú (v našom príklade premennú i s hodnotou 22).

Na tomto mieste spomeniem ešte malú zaujímavosť – bod deklarácie nejakého mena sa nachádza hneď za jeho deklarátorom a pred prípadným inicializátorom. V praxi z toho vyplýva možnosť inicializácie premennej samej sebou (ale neviem, na čo by to bolo dobré) a, naopak, nemožnosť inicializovať lokálnu premennú globálnou premennou rovnakého mena:

double t = 1.0;
{
    double t = t;
}

Lokálna premenná t sa neinicializuje obsahom globálnej premennej t, ale svojím vlastným obsahom (ktorý navyše nie je definovaný).

Linkovanie

Všetky mená so súborovým rozsahom platnosti (teda globálne mená) v programe majú pridruženú vlastnosť zvanú linkovanie (linkage). Upozorňujem, že vlastnosť „linkovanie“ nemá nič spoločné s činnosťou „linkovanie“, ktorá sa deje počas prekladu programu. Takýto preklad som zvolil z toho dôvodu, že termín „spájanie“ viac inklinuje k opisu činnosti ako k názvu vlastnosti.

Linkovanie v C++ môže byť dvojakého charakteru: interné a externé. Mená s interným linkovaním sú lokálne pre daný súbor (prekladovú jednotku) a z častí programu nachádzajúcich sa v iných súboroch nie sú viditeľné ani inak prístupné. Ak máme v jednom súbore deklarované nejaké meno s interným linkovaním, môžeme to isté meno použiť v inom súbore bez toho, aby nám prekladač vynadal. Samozrejme, v inom súbore toto meno bude predstavovať úplne inú entitu (premennú, funkciu, typ a pod.).

Druhým typom linkovania je externé linkovanie. Ako už isto tušíte, meno, ktoré má externé linkovanie, bude predstavovať v celom programe (t. j. v rámci všetkých jeho súborov!) jedinečnú entitu. Takéto meno musí byť v rámci všetkých súborov definované práve raz, deklarované môže byť aj viackrát, ale všetky jeho deklarácie sa musia zhodnúť na použitom type/typoch (pri premenných je to jeden typ – typ premennej, pri funkciách je to viacero typov – typy argumentov, návratovej hodnoty atď.). Ak vám nie je jasné, na čo je dobré deklarovať jedno meno viackrát, odpoveď je jednoduchá – meno s externým linkovaním je prístupné zo všetkých súborov práve vďaka deklarácii v každom zo súborov, v ktorých ho chceme používať. Pritom práve v jednom z nich bude aj naozaj definované, t. j. bude mu pri preklade pridelená pamäť.

O tom, ako rozlišujeme interné a externé linkovanie, si povieme o niečo neskôr, keď budeme opisovať štruktúru deklarácie mena.

Ukladacia trieda

Posledným z opisovaných atribútov deklarovaných mien je ukladacia trieda. Táto vlastnosť súvisí s uložením objektov v pamäti a na jej základe rozlišujeme dva typy objektov: automatické a statické.

Automatické objekty sa môžu nachádzať len v rámci nejakého bloku, vznikajú (fyzicky v pamäti) pri každom prechode programu miestom svojej deklarácie a zanikajú pri opustení bloku. Je zrejmé, že automatické objekty môžu mať iba lokálny rozsah platnosti. Prekladač prakticky vždy umiestňuje automatické objekty na zásobník (t. j. do zásobníkového segmentu programu), čo v praxi vyzerá asi tak, že obyčajne pri vstupe do funkcie sa na zásobníku vyhradí miesto (posunom ukazovateľa zásobníka, pre 386+ je to známy register ESP, resp. SP) pre všetky lokálne premenné funkcie a na konci funkcie sa miesto zase zruší. Pokiaľ automatické objekty sami explicitne neinicializujeme, ich implicitným obsahom budú tie smeti, ktoré na danom mieste zásobníka práve boli. Prípadná inicializácia sa vykonáva pri každom prechode deklaráciou.

Všetky lokálne premenné funkcií a všetky premenné deklarované v rámci zložených príkazov sú automatické. Pozor však, automatickými sú aj premenné deklarované v rámci výkonného príkazu príkazov for, if a pod. Teda v nasledujúcom príklade:

int x = –1;
while (++x < 100)
    for (int i = 0; i < 5; i++)
    { ... }

je premenná i automatická a vzniká a zaniká pri každej iterácii vonkajšieho cyklu while (čiže stokrát). Opäť tento fakt nadobudne význam až pri premenných objektového typu. Súčasne je premenná i nedostupná mimo príkazu while, pretože jeho výkonný príkaz sa považuje z hľadiska rozsahu platnosti za blok. Pri použití niektorého zo skokových príkazov môžeme skočiť aj dovnútra bloku, nesmieme však preskočiť deklaráciu automatickej premennej, spojenú s inicializáciou. Ak takýto skok potrebujeme, musíme uzavrieť automatickú premennú do samostatného bloku. Príklad nesprávneho skoku:

void foo()
{
    goto here;  // chyba!
    ...
    int x = 123;
    ...
here:
    ...
}
 
a správneho:
 
void bar()
{
    goto here;  // ok
    ...
    {
        int x = 123;
        ...
    }
    ...
here:
    ...
}

Naproti tomu statické objekty existujú a zachovávajú si svoju hodnotu (samozrejme, pokiaľ ju nezmeníme) počas celého behu programu. Vznikajú pri štarte programu obyčajne ešte pred volaním funkcie main() (termín „vznikajú“ nadobudne význam až v súvislosti s objektovými typmi), implicitne sa inicializujú na nulu pretypovanú na svoj typ (polia sa vynulujú po prvkoch, objektové premenné sa inicializujú špeciálne) a zanikajú po skončení programu (návratom z main() alebo volaním exit()). Všetky globálne objekty sú statické, lokálnym takúto ukladaciu triedu musíme v prípade potreby explicitne predpísať (kľúčovým slovom static, ale k tomu sa o chvíľu dostaneme).

Tajomstvo deklarácií

Tak sme si opísali, aké rôzne vlastnosti majú deklarované mená. Tieto vlastnosti menám priraďujeme práve prostredníctvom deklarácií. Ale určite už netrpezlivo čakáte, kedy sa konečne k opisu tých deklarácií dostaneme. Takže pozor… teraz! Hm, ale raz neviem, z ktorej strany mám začať. Celá táto problematika je jeden veľký prepletenec faktov a je prakticky nemožné  podať ho lineárne so zachovaním nejakej rozumnej príčinnej súvislosti, čím som chcel povedať, že sa nedá povedať najprv A, potom B a nakoniec C, lebo opis A si vyžaduje znalosť C… no myslím, že  vyvolávam zbytočný zmätok, takže radšej začnime.

Celé tajomstvo je na prvý pohľad veľmi jednoduché. Deklarácia jedného či viacerých mien vyzerá takto:

spec-list  decl-list;

kde „spec-list“ je zoznam špecifikátorov a „decl-list“ zoznam deklarátorov. Deklarátory opisujú, čo vlastne ideme deklarovať a aký názov to bude mať, špecifikátory zase vlastnosti novo deklarovaného mena/mien. Za určitých okolností možno jedno alebo druhé vynechať, povieme si o tom pri konkrétnych prípadoch. Najjednoduchší príklad deklarácie:

double val;

V tejto deklarácii je deklarátorom identifikátor novej premennej val, špecifikátorom kľúčové slovo double, ktoré určuje typ tejto premennej.

Okrem uvedeného vzoru deklarácie existujú ešte špeciálne prípady, ako napríklad asm deklarácia, ktorá je však implementačne závislá a nebudeme si ju tu zatiaľ opisovať, potom deklarácia šablón alebo deklarácia umožňujúca pripojiť k programu C++  skompilované funkcie napísané v jazyku C. K posledným dvom sa vrátime v budúcnosti.

Teraz si výklad rozdelíme na dve časti. Najprv si povieme o špecifikátoroch, pod pojmom deklarátor si zatiaľ predstavujte ten najjednoduchší – identifikátor premennej, potom sa budeme venovať rôznym deklarátorom. Obe témy sa dosť prekrývajú a najlepšie by bolo preberať ich paralelne, ale to sa dosť ťažko realizuje aj v bežnom živote, nieto ešte v časopise. Dovolím si vás poprosiť, aby ste sa po prebratí oboch častí  vrátili k tej prvej a ešte raz si všetko prešli (prípadne môžete takto iterovať aj viackrát).

Špecifikátory

Špecifikátory, ktoré môžeme použiť v deklarácii, sa dajú rozdeliť takto: špecifikátory ukladacej triedy, funkčné špecifikátory, špecifikátory typu a špecifikátor typedef. V zozname v deklarácii sa môže nachádzať aj viac ako jeden špecifikátor, ale zase nie je možné kombinovať všetko so všetkým. Na určenie, ktoré špecifikátory môžeme spolu skombinovať, je asi najlepšie použiť zdravý rozum (a trochu nasledujúcej teórie).

Špecifikátory ukladacej triedy

Sú štyri: auto, register, static a extern a sú to všetko kľúčové slová C++. Netýkajú sa však len ukladacej triedy, ale ako uvidíme o chvíľu, aj linkovania. Prvý z nich, špecifikátor auto určuje, že deklarované meno bude automatickým objektom. Použiť ho môžeme iba pri deklarácii lokálnych objektov (v rámci nejakého bloku) a pri deklarácii formálnych argumentov. V praxi som ho však ešte v živote nevidel. Je totiž úplne zbytočný, pretože lokálne objekty sú automaticky automatické (to je slovná hračka, čo?).

Druhý špecifikátor register je v podstate tým, čo auto, s rovnakými pravidlami použitia, navyše však znamená pre prekladač pomôcku alebo náznak (hint), že objekt takto deklarovaný sa bude používať dosť často a bolo by dobré, keby bol umiestnený priamo v registri procesora. Prekladač sa však aj tak zariadi po svojom – ak objekt nemôže uložiť do registra (napríklad niekde získavame jeho adresu operátorom & alebo sa  do registra nevojde), tak špecifikátor register ignoruje, inak ho často v rámci optimalizácie doplní aj sám. Príklad – asi najkratšia (nie najrýchlejšia!) implementácia funkcie strcpy(), ktorá kopíruje jeden reťazec do druhého:

void strcpy(register char *dst,
            register char *src)
{
    while (*dst++ = *src++);
}

Reťazec, ako vieme, je v C++ ukončený znakom \0. Argument src predstavuje zdrojový reťazec (ukazovateľ a súčasne pole znakov – to už ovládame, pozri minulú časť), argument dst je ukazovateľom na miesto, kam sa má reťazec skopírovať (predpokladáme, že je to miesto dostatočne veľké). V každej iterácii sa skopíruje jeden znak zo *src na *dst a oba ukazovatele sa posunú. Všimnite si, že priraďovací výraz používame priamo ako podmienkový výraz príkazu while. Kopírovanie sa zastaví po zápise znaku \0, keď bude výsledkom priraďovacieho výrazu nulová hodnota a cyklus while sa ukončí. Oba ukazovatele sa zrejme používajú dostatočne intenzívne, aby malo zmysel ich uložiť do registrov.

Samozrejme, špecifikátor register môžeme použiť aj pri deklarácii lokálnych premenných:

char msg[] = "hello";
register char *ptr = msg;
while (*ptr = toupper(*ptr))
    ptr++;

V príklade máme znakové pole msg a  ukazovateľ ptr, ktorý ukazuje na začiatok poľa. V cykle prechádzame jednotlivými znakmi reťazca a meníme ich na veľké písmená. Ukazovateľ slúži na pamätanie polohy v poli a bolo by dobré, keby sa nachádzal v registri. Opäť používame úsporný zápis cyklu, ktorý sa skončí v okamihu, keď budeme chcieť previesť na veľké písmeno záverečný znak \0. Pre použitie makra toupper() musíme vložiť do programu hlavičkový súbor <ctype.h>.

Posledné dva špecifikátory static a extern sa týkajú linkovania. Kľúčové slovo static explicitne vyjadruje, že globálne meno bude mať interné linkovanie, čo, samozrejme, znamená, že takto deklarované (a súčasne definované!) meno bude viditeľné len v rámci súboru, ktorý deklaráciu obsahuje. Zdôrazňujem, že toto sa týka globálnych mien, teda mien so súborovým rozsahom platnosti! (Deklarácia globálnych static premenných sa používala predovšetkým v jazyku C pri snahe o modulárne programovanie – čo súbor, to samostatný modul a jeho privátne premenné nemali byť zvonka viditeľné.) Deklarácia s kľúčovým slovom extern znamená presný opak, teda priraďuje globálnemu menu externé linkovanie, ale len ak toto meno nebolo už raz deklarované ako static. Pokiaľ neuvedieme špecifikátor extern, meno má externé linkovanie automaticky, ale (vráťte sa o niečo späť) je tu jeden obrovský rozdiel – deklarácia so špecifikátorom extern a bez inicializácie nie je definíciou! V praxi sa používa asi nasledujúci postup – v tom súbore, v ktorom má byť premenná definovaná, sa extern neuvedie, bude tam iba klasická definičná deklarácia. Vo všetkých ostatných sa v deklarácii špecifikátor extern použije, a to obyčajne tak, že táto deklarácia sa zapíše do hlavičkového súboru, ktorý sa potom vloží (#include) do potrebných zdrojových súborov. Je to asi mierne zamotané, skúsme si vec objasniť na nasledujúcom príklade:

Toto je súbor subor1.cpp:

// subor1.cpp
int a = 8;
void foo() { ... }

K nemu pridružíme hlavičkový súbor subor1.h:

// subor1.h
extern int a;
void foo();

Potom máme ešte dva zdrojové súbory, v ktorých chceme používať premennú a a funkciu foo():

// subor2.cpp
#include "subor1.h"
void bar() { a = 10; }
 
// subor3.cpp
#include "subor1.h"
void main()
{
    foo();
    ...
}

Nehľadajte v príkladoch hlbší zmysel, nie je tam… Z výpisov vidíme, že vďaka hlavičkovému súboru subor1.h máme sprístupnenú premennú a ako aj funkciu foo() aj v ostatných súboroch. Obe tieto mená majú implicitne externé linkovanie (lebo sme ich nedeklarovali ako static). Premennej a bola pridelená pamäť pri preklade po nájdení jej deklarácie v súbore subor1.cpp, v ostatných súboroch si iba prekladač poznačil, že meno a je definované (a jeho použitie neznamená chybu), ale že pamäť mu bola pridelená niekde inde a všetky odkazy na jeho skutočné fyzické umiestnenie v pamäti (to je treba vedieť, keď chceme s premennou pracovať) sa vyriešia až počas fázy linkovania. Pokiaľ však deklarujeme nejakú premennú ako extern a nikde v programe ju nedefinujeme, dostaneme od linkera chybové hlásenie typu „Symbol xxx not defined“. Čo sa týka funkcie foo(), tam je to jasné, tým, že prekladač videl jej prototyp (ten je takisto v hlavičkovom súbore), považuje ju za deklarovanú. Ak ju nikde nedefinujeme alebo definujeme v inom súbore ako statickú, linker nám takisto vynadá.

Priznávam, že toto je jedna z najzložitejších oblastí C++ (a to sme sa ešte nedostali k virtuálnym funkciám, preťažovaniu operátorov či šablónam!). Treba si skrátka celú vec nechať uležať v hlave, vrátiť sa k nej neskôr, prípadne skúšať na príkladoch. Ešte si uvedieme, aké kombinácie použitia oboch špecifikátorov sú povolené a aké nie:

static int a;
int a;

Prvá deklarácia priraďuje premennej a interné linkovanie, druhá je však chybou, prekladač ju bude pokladať za redeklaráciu už raz definovanej premennej.

static int b;
extern int b;

Prvá deklarácia priraďuje premennej b interné linkovanie, druhá toto linkovanie nijako nemení. Konštrukcia je korektná, prekladač nebude namietať.

int c;
static int c;

V prvej deklarácii premenná c získa automaticky externé linkovanie. Druhá deklarácia je chybná (redeklarácia existujúcej premennej, navyše ani linkovanie nesedí).

extern int d;
static int d;

V prvej deklarácii priradíme premennej d explicitne externé linkovanie, druhá deklarácia by mala byť chybná (nesedí linkovanie), ale obyčajne ju prekladače povolia (maximálne vydajú varovanie). Zvyšné kombinácie (extern a nič, resp. nič a extern) sú, samozrejme, v poriadku, lebo ide o dvojicu deklarácia-definícia, ktorá je povolená.

Zatiaľ čo špecifikátor extern môžeme použiť aj pri deklarácii mien v rámci nejakého bloku (t. j. mien s lokálnym rozsahom platnosti) a jeho význam zostane taký istý (ledaže je možno výhodnejšie to nerobiť na úrovni bloku, ale na úrovni súboru, aby príslušné meno bolo prístupné všetkým blokom – funkciám v celom súbore), špecifikátor static použitý na lokálne meno má význam úplne odlišný. Predovšetkým sa nedá povedať, že static lokálne meno má interné linkovanie – to má vždy, nikdy ho nebudeme vidieť ani z iných funkcií, nieto ešte z iných súborov. Takže tým, že deklarujeme lokálne meno ako static, hovoríme prekladaču, že toto meno bude statické (v zmysle ukladacej triedy). Implicitne je totiž každé lokálne meno, ktoré nie je extern, automatické (akoby bolo deklarované s auto). A teraz je to už úplne zauzlené… alebo nie? Dúfam, že nie. Malý príklad na statickú lokálnu premennú:

int check_in()
{
    static int count = 0;
    return ++count;
}

Funkcia check_in() slúži na počítanie napríklad výskytu nejakých udalostí. Vždy, keď udalosť nastane, zavoláme túto funkciu a ona nám vráti jej akoby poradové číslo. Zrejme je na implementáciu tejto činnosti potrebná nejaká vnútorná pamäť. Tú predstavuje práve  statická premenná count. V príklade ju inicializujeme na nulu (čo je vlastne zbytočné, lebo tak sa inicializuje implicitne) – dôležité však je, že inicializácia statickej premennej sa vykoná iba raz, a to pri štarte programu (resp. pokiaľ ide o takúto jednoduchú premennú, tak ešte pri preklade, keď sa jej pridelí miesto v pamäti; inicializačná hodnota bude zapísaná priamo v binárnom obraze programu, hovorili sme si o tom v časti venovanej dynamickým premenným). Každý ďalší prechod programu miestom deklarácie premennej count už prakticky nerobí nič. Vďaka svojej statickosti si count drží svoju hodnotu aj medzi volaniami funkcie check_in(), takže každé jej volanie vráti poradové číslo o jednotku vyššie.

Funkčné špecifikátory

Sú dva, jeden z nich, virtual, sa týka virtuálnych členských funkcií objektových typov a povieme si o ňom až v druhej polovici seriálu. Druhým špecifikátorom je kľúčové slovo inline. Použiť ho môžeme len pri deklarácii alebo definícii funkcií, ale v prípade, že máme sekvenciu deklarácia (prototyp) – volanie funkcie – definícia (telo), musíme inline uviesť už pri deklarácii. Dôvodom je fakt, že tento špecifikátor priraďuje funkciám implicitne interné linkovanie. Jeho význam je jednoduchý, slúži opäť ako pomôcka, náznak pre prekladač, že funkcia, ktorú deklarujeme, je natoľko jednoduchá a často používaná, že sa neoplatí volať ju klasickou metódou, t. j. obyčajne inštrukcia CALL s adresou funkcie a telo funkcie sa nachádza niekde samostatne, ale namiesto toho sa každé volanie tejto funkcie nahradí priamo jej telom. To znamená, že zatiaľ čo pri klasickom spôsobe sa v programe telo funkcie nachádza raz a každé volanie sa realizuje jednou inštrukciou, pri deklarácii inline sa telo funkcie v programe nachádza toľkokrát, koľkokrát funkciu voláme. Prekladač môže  opäť tento špecifikátor ignorovať, obyčajne v prípade, keď usúdi, že funkcia je pridlhá alebo obsahuje cykly.

Ako sme už povedali, inline funkcie majú interné linkovanie; ak teda chceme takúto funkciu použiť vo viacerých súboroch programu, musíme nielen jej deklaráciu, ale aj jej definíciu uviesť do hlavičkového súboru. Toto pri klasických funkciách nemôžeme – linker by ohlásil niečo ako „Multiple definition of xxx“ a skončil s chybou. Uveďme si jednoduchý príklad:

// max.h
inline int max(int a, int b)
{
    return a > b ? a : b;
}
 
// main.cpp
#include "max.h"
int main()
{
    int x, y;
    ...
    int m = max(x, y);
    ...
    return 0;
}

Ide  o dva súbory – max.h a main.cpp. V prvom z nich máme inline deklaráciu funkcie max(, ktorá vráti väčšie z dvoch celých čísel. O jej jednoduchosti niet pochýb. V druhom súbore funkciu používame a v rámci tohto súboru má max() interné linkovanie. V každom ďalšom súbore, v ktorom by sme ju potrebovali, nám stačí vložiť súbor max.h a funkcia je k dispozícii (ale opäť s interným linkovaním; v konečnom dôsledku teda bude v programe existovať toľko funkcií max(), v koľkých súboroch ich deklarujeme; práve fakt, že funkcie sú inline, však zabráni existencii viacerých zbytočných rovnakých kódov v binárnom obraze programu – v skutočnosti tam tých kódov bude viac rovnakých, ale priamo v miestach ich príslušných volaní). No, to som tomu zase dal…

Ešte nekončíme

Rozsah článku ma, bohužiaľ, núti prerušiť výklad, hoci sme si ešte nepovedali o špecifikátoroch typu a o typedef špecifikátore. Necháme to nabudúce, keď začneme hovoriť aj o deklarátoroch. Ako tak pozerám späť, zrejme deklarácie dokončíme až v trinástom pokračovaní, potom venujeme ešte jednu časť štandardným konverziám, jednu alebo dve štandardnej knižnici jazyka C a potom sa už môžeme pustiť do objektovej polovice seriálu.


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á