Deklaracijos ir Definicijos

Šis straipsniukas originaliai buvo post'as GameDev.lt forume. Post'ą galima rasti čia.


Ech, žiūriu, pradedančiuosius kankina tos pačios problemos, kurios kankino ir mane, kai pradėjau :-). Nepamenu iš kur pats išmokau, tik pamenu, kad "the hard way", tai pabandysiu paaiškinti kaip ir kas vyksta C kalboj, kad (a) išvengti panašių problemų ir (b) vis dėl to su jomis susidūrus, kad jos negąsdintų ir nesugraužtų daug laiko.

Taigis, pirmiausia, paskaitot va šitą mano postą. (Kopiją taip pat perkėliau ir į puslapį: Transliavimo stadijos)

Ten šiaip jau viskas parašyta, ko reikia, kad Aurimax II nebūtų susidūręs su savo problemomis, tik žinau, kad ne visada aišku "kas iš to".

Pradėkim nuo trumpiausio ir lengviausiai suprantamo: niekada ne#includinkite .cpp failų! Taip, netgi tuo ypatingu atveju, vis tiek -- niekada.

Toliau. Išmokite atskirti deklaraciją nuo definicijos (declaration vs. definition). Tai tikrai labai svarbu. Deklaruodami kintamąjį (ar klasę/struktūrą, ar funkciją) jūs pasakote kompiliatoriui (būtent kompiliatoriui siaurąja prasme; antra stadija iš to mano posto apie transliavimą), kad bus toks tai objektas, pavadintas tokiu tai vardu, jis turės tokį tai tipą. Jeigu lyginti su žmonėmis, tai jūs supažindinate kompiliatorių su kintamuoju ir sakote, kad jeigu kada sutiksi, nenustebk, neišsigąsk ir pasisveikink.

Atkreipkite dėmesį: deklaravimo metu atmintis neišskiriama, deklaruojant, joks kintamasis nėra sukuriamas! Apie jį tik paskelbiama.

Deklaruojama šitaip:

// variable:
extern int i;   // note: you CAN'T assign in declaration

// function:
int foo ();     // yes, you've seen that before. This is declaration

// struct:
struct Foo;     // same for class

Tuo tarpu definindami kintamąjį, jūs tiek kompiliatoriui (ta pačia siaurąja prasme), tiek linkeriui pasakote, kad "čia" bus ta vieta, kur gyvens toks tai kintamasis. Galima sakyti, kad defininimas yra ne kas kita kaip atminties išskyrimas.

Taigi, deklaravimas yra tiesiog "paskelbimas" (doh :-)), o defininimas yra atminties išskyrimas. Tačiau kažkodėl vis tiek kiekvienas užlipa ant to paties grėblio ir susiduria su tomis pačiomis linkinimo klaidomis. Manau, taip yra štai dėl ko:

int i;

Čia yra kartu ir deklaracija, ir definicija. Mat iš tiesų neįmanoma defininti kintamojo, tuo pačiu jo nepadeklaruojant. Galų gale ne mažiau, nei 99 atvejais iš 100 būtent to ir reikia. Vienintelė išimtis, kai deklaravimas turi būti atskirtas nuo defininimo yra globalūs kintamieji, kurie turi būti matomi daugiau nei viename "translation unit'e" (čia yra fancy būdas pasakyti "cpp failas"):

extern int global;
extern int global;
extern int global;      // fine, declare as many times as you wish

int global = 0;         // because here we declare once again and we can't
                        // avoid it

int foo ()
{
    extern int local;
    int local = 0;      // error: duplicate declaration!
}

O jeigu kam nepatinka išimtys, galite galvoti šitaip: globalų kintamąjį galima deklaruoti keletą kartų (skelbti jį "extern"), nes jis iš tiesų yra external, jis sukuriamas statinėje atmintyje ir yra fiziškai matomas iš kitur. Tuo tarpu lokalus kintamasis yra kuriamas steke ir matomas tik tos funkcijos kontekste, jis nėra external, todėl atskirai jį deklaruoti nėra prasmės.

Dabar dedam viską į vieną krūvą. Žemiau yra keletas failų, kurie demonstruoja kaip yra dirbama su globaliais kintamaisiais, išvengiant linkinimo klaidų.

Header.h:
extern int global;      // declare so others would see it
extern Foo globalFoo;   // ~

int fubar;              // now that's a definition! See below for what happens.
Impl1.cpp:
int global = 0;
Impl2.cpp:
Foo globalFoo;
Main.cpp:
#include "Header.h"     // at this point compiler will see the declarations

void foo ()
{
    global = 13;        // this will refer to the memory allocated in Impl1
    globalFoo.call ();  // this will call a method and will pass it an object
                        // declared in Impl2
}
Mainer.cpp:
#include "Header.h"     // at this point compiler will see the declarations

int goo ()
{
    return global;      // this will read from the memory allocated in Impl1
}

Kompiliuojant kiekvieną iš šių keturių translation unit'ų, kompiliatorius sau viduje pasižymi, kad kintamasis "global" yra "tas_global_iš_kitur" (ir atitinkamai su globalFoo), tokiu būdu padarydamas maksimum ką jis gali padaryti. Daugiau jis nelabai ką gali padaryti, nes kompiliuoja po vieną cpp failą vienu metu ir nieko nežino apie kitus. Ir tik linkeris pamato visą programą tokią, kokia ji yra visumoje. Jis gauna visus keturis obj failus su tomis netikromis nuorodomis "tas_global_iš_kitur", ir bando atstatinėti tikruosius ryšius. Jeigu iš tikrųjų randa tik vieną globalų kintamąjį pavadintą "global", tai viskas tvarkoj, susieja su juo. O jeigu randa daugiau, negu vieną, negali apsispręsti ir pyksta.

Panašiai ir įvyksta su kintamuoju "fubar". Jis yra defininamas headeryje. O headeris, savo ruožtu, yra includinamas dvejuose cpp failuose. Prisiminus, kad #includeinimas iš tiesų yra paprastas teksto kopijavimas, gaunasi, kad tiek Main.cpp, tiek Mainer.cpp definina po tokį patį kintamąjį. Visa laimė, kad šiame pavyzdyje niekur nebandoma kreiptis į šį kintamąjį. Dėl to jis niekam netrukdo. Bet jeigu bent vienoje vietoje pabandytumėm, linkeris pasiklystų ir gautume klaidą.

Moralas: niekada nedefininkite globalių kintamųjų headeriuose! Tik deklaruokite.

Uff... Kiek daug raidžių prirašiau. Tikiuosi, ne veltui :-). Jeigu kas pastebėjot klaidų rašykit. Dar čia iki pilno vaizdo trūksta aprašyti include guard'us, tipų defininimą, bet pradžiai užteks ir tiek. Jeigu mano rašliava pasirodys naudinga, aprašysiu ir juos.

O paskui žmonės dar klausia kodėl aš manau, kad pradėti mokytis nuo C yra bloga mintis... ;-).

-rtfb

Valid HTML 4.01 Strict