Deklaracje i definicje.

Na początek, jak obiecałem, opiszę konwencje notacyjne stosowane przeze mnie w dalszych objaśnieniach dotyczących zapisu kodu programu. Myślę jednak, że nie warto pomijać tego podczas czytania tego opisu, gdyż w trakcie objaśniania sposobu zapisu kodu "przemycać" będę także informacje ważne dla języka.

Fragmenty kodu, które stanowią część języka, na przykład słowa kluczowe i symbole zostały napisane

taką czcionką
i trzeba je przepisać bez zmiany.

Kiedy w kodzie trzeba użyć jakiejś nazwy nie stanowiącej części języka, tylko będącej identyfikatorem np. funkcji lub innego obiektu, wtedy nazwa taka napisana jest

<w_ten_sposób>
z użyciem znaku podkreślenia. Zaraz dowiesz się o tym jakie reguły obowiązują dla identyfikatorów. Znaki "<" i ">" nie są częścią identyfikatorów, lecz podkreślają, że jest to dowolna nazwa, a nie ustalone słowo kluczowe. Nie wpisuje się ich do kodu programu (jedynie dyrektywa preprocesora #include używa ich jako ograniczników nazwy pliku nagłówkowego).

Jeśli w kodzie programu jest fragment, który można pominąć (czyli fragment tzw. opcjonalny), to w moim zapisie będzie on ujęty w nawiasy kwadratowe napisane czcionką niepogrubioną

[ i ]
i wówczas nie należy ich przepisywać. Zaś nawiasy kwadratowe napisane pogrubioną czcionką
[ i ]
oznaczają, że trzeba ich użyć w kodzie. W sytuacjach wątpliwych służę wyjaśnieniami. Na przykład:
<typ> <nazwa_zmiennej> [ [ <rozmiar_tablicy> ] ] ;
W powyższym przykładzie pokazany jest zapis deklaracji zmiennej typu podstawowego. Jeśli pominięto nawiasy kwadratowe i rozmiar wewnątrz nich, to deklarowana jest zmienna prosta, w przeciwnym razie deklarowana jest tablica o podanej ilości elementów.
int liczba;
deklaruje (i definiuje - patrz dalej) zmienną prostą typu całkowitego o nazwie liczba, a
int liczby[10];
deklaruje tablicę o dziesięciu elementach typu całkowitego o nazwie liczby.

Jeśli w zapisie użyty zostanie samotnie stojący w linijce wielokropek

...
to oznacza on, że zastąpiony nim fragment kodu programu nie jest istotny dla objaśnienia. W innych miejscach może on oznaczać, że dozwolone jest powtórne użycie poprzedniej części kodu; wtedy umieszczony jest on w nawiasach kwadratowych wraz z tym kodem. Przykład:
<typ> <deklarator_zmiennej> [ , <deklarator_zmiennej> ... ] ;
Tu określone jest, że na raz można zadeklarować więcej niż jedną zmienną danego typu podstawowego. Zapis "<deklarator_zmiennej>" oznacza to samo, co w poprzednim przykładzie: zmienną prostą lub tablicę z nawiasami kwadratowymi i rozmiarem wewnątrz nich.

Każda deklaracja (pojedyncza lub wielokrotna) koniecznie MUSI być zakończona średnikiem (znakiem ";"), podobnie jak każda instrukcja programu. Kompilator po nim "poznaje" koniec takiej deklaracji lub instrukcji czytając i tłumacząc tekst programu. Nie napotkawszy na niego, następną w programie deklarację lub instrukcję "rozpozna" jako kontynuację poprzedniej, co z pewnością spowoduje błąd kompilacji. Na innej podstronie przeczytać można o stosowanych umownych stylach pisania kodu programu.

Teraz słów parę o identyfikatorach. Nazwy zmiennych, funkcji, struktur i klas, typów, wyliczeń i stałych wyliczanych są identyfikatorami i podlegają pewnym zasadom, aby kompilator mógł je odróżnić np. od stałych. Aby tak było identyfikator musi zaczynać się od litery lub znaku podkreślenia ("_"). Pozostałe znaki muszą być literami, cyframi lub znakami podkreślenia. Kompilator odróżnia od siebie wielkości liter, więc np. AB, to nie to samo co Ab. Maksymalna długość identyfikatorów jest dowolna, ale różne kompilatory odróżniają je od siebie po pierwszych znakach - w najprostszych z nich może to być 7, w innych 31, a jeszcze w innych 255 pierwszych znaków. Przyjęło się używać w identyfikatorach małych liter lub też pisać je z wielkiej litery i pozostałe litery małe. Samych dużych liter zwyczajowo używa się do definiowania stałych kompilatora. Oto przykłady:

#define STALA_KOMPILATORA 10
int Zmienna_Calkowita;
char tablica20znakow[20];
A poniżej przykład nieprawidłowego identyfikatora zmiennej:
char 20liter[20];


Omówiłem zmienne proste i tablicowe. Teraz pora na struktury. Deklaruje się je pisząc słowo kluczowe struct na początku. Struktura to zestaw tzw. pól czyli zmiennych składowych o dowolnych typach. Po zadeklarowaniu taki obiekt zajmuje w pamięci tyle miejsca, ile razem w sumie zajmują jego składowe. Zapis deklaracji:

struct [ <nazwa_struktury> ]
{
    <deklaracja_składowej> ;
    ...
} [ <deklarator_zmiennej> [ , <deklarator_zmiennej> ... ] ] ;
Trzeba pamiętać o średniku na końcu deklaracji. Przykłady:
struct
{
    int wiek;
    char *imie, *nazwisko;
} szef, pracownik, *wsk;
albo
struct student
{
    int id;
    char oceny[20];
};
Przykłady pokazują różnicę między deklaracją a definicją. Pierwszy z nich deklaruje tzw. anonimową strukturę oraz definiuje dwa obiekty o takiej strukturze, a ostatni identyfikator jest zmienną wskaźnikową wskazującą na taki obiekt. Drugi przykład, to tylko deklaracja struktury o nazwie student. Strukturą taką będzie można posłużyć się potem w dalszych miejscach programu używając np. deklaracji (będącej definicją zarazem):
struct student dziennik[30];

Jeszcze raz o deklaracjach i definicjach. Deklaracja, to określenie typu lub innych atrybutów jeszcze nie zdefiniowanego obiektu. Definicja zaś, to już określenie obiektu tego typu lub o takich atrybutach, czyli przydzielenie nań pamięci. Jeśli na int potrzeba 2 bajtów, a na char 1 bajtu, to przykładowy obiekt dziennik zajmuje (20*1+2)*30=660 bajtów.

Ogólna zasada jest taka, że jeśli podamy tylko nazwę struktury (patrz też typy wyliczeniowe, na poprzedniej stronie: enum kolory) i nie podamy żadnych zmiennych przy takiej deklaracji, to nic nie jest definiowane - na nic nie jest przydzielana pamięć. Podanie jakichś zmiennych lub innych deklaratorów definiuje je (przydziela im pamięć). W przypadku wyliczenia można zdefiniować zmienną typu wyliczeniowego, np.:

enum
{
    trojkat = 1,
    kolo,
    kwadrat
} figura;

Nie można deklarować anonimowych typów wyliczeniowych, struktur i klas bez definiowania obiektów. Wyjątkiem są tylko unie; o uniach zaraz napiszę. Po prostu w kodzie musi być przynajmniej kompletna deklaracja nawet jeśli nie towarzyszy jej definicja. Deklaracja bez nazwy może być tylko definicją, inaczej nie stanowi poprawnej deklaracji. Poniższy zapis:

struct
{
    int wiek;
    char *imie, *nazwisko;
};
niczego nie deklaruje i nie definiuje: ani samej struktury (bo nie ma nazwy), ani zmiennych o takiej strukturze (bo nie ma deklaratorów).


Unie są dość rzadko wykorzystywaną odmianą struktur. Różnią się od nich tym, że w strukturach kolejne składowe zajmują kolejne miejsca w pamięci, a w uniach wszystkie składowe zajmują w niej to samo miejsce. Zatem wielkość zajętości pamięci całego obiektu unii wynosi tyle, ile największa wielkość zajętości pamięci spośród wszystkich składowych. Przykładowo:

union dane
{
    char znaki[3];
    int liczba;
} unia;
Tutaj unia jest unią o nazwie dane. Zakładając, że na typ int rezerwuje się 2 bajty, a na char 1 bajt, to unia w pamięci zajmie 3 bajty.

Przypisując dane do pól i odczytując je z nich w przypadku unii, trzeba być bardzo ostrożnym, bo wpisując dane do jednej składowej "zamazujemy" je w pozostałych składowych.

O anonimowych uniach opowiem przy okazji omiawiania odwoływania się do danych zawartych w zmiennych lub wskazywanych przez wskaźniki w wyrażeniach.


Teraz wrócę na chwilę do deklaracji. Pojedyncza deklaracja może być dość skomplikowana, kiedy chcemy użyć skomplikowanego typu danych. W deklaratorze zmiennej (jak wspomniałem wcześniej) można użyć znaku gwiazdki ("*"), aby określić, że zmienna jest wskaźnikiem. Można też użyć nawiasów kwadratowych ("[", "]"), aby określić, że zmienna jest tablicą. Co napisać, jeśli chcemy użyć tablicy wskaźników? A co, jeśli chcemy użyć wskaźnika na tablicę? Przy okazji wyrażeń podam tzw. priorytety operatorów i wtedy okaże się, że wśród operatorów [] i * większy priorytet mają nawiasy kwadratowe. Jeśli chcemy to zmienić, używamy nawiasów okrągłych ("(" i ")").
Oto przykłady:
int *tablica[10]; - tablica dziesięciu wskaźników na dane typu int;
int (*wsk)[10]; - wskaźnik na dziesięcioelementową tablicę o elementach typu int.
A teraz uwaga:
int *(*ptr)[10]; - wskaźnik na tablicę dziesięciu wskaźników na dane typu int.


Co jednak zrobić, gdy chcemy takiego wymyślnego typu danych użyć w miejscu, gdzie deklaracja nie jest dopuszczalna? A czy można zaoszczędzić sobie pisania definiując wiele zmiennych wciąż tego samego typu? Z pomocą przychodzi deklaracja typu oznaczana słowem kluczowym typedef. Można jej użyć, gdy chcemy jakiemuś typowi danych nadać nazwę; wówczas nazwą taką możemy posłużyć się w dalszych deklaracjach czy definicjach. Na przykład:

typedef int *wsknaint;
typedef char napis[100];

wsknaint iptr;
napis komunikat;
Trzeba jednak wiedzieć, że wsknaint czy napis nie są jakimiś nowymi typami danych. Deklaracja typedef nadaje tylko jedną spójną nazwę nawet najbardziej skomplikowanemu, lecz funkcjonującemu typowi. Oznacza to, że, zgodne z powyższym przykładem:
wsknaint iptr1;
int *iptr2;
definiują dwie zmienne tego samego typu: int * czyli wskaźnik na dane int.


Powinienem coś też napisać o wskaźnikach nie określających typu wskazywanej danej. Czasem potrzeba wskazać na jakiś obszar pamięci nie wiedząc lub nie chcąc podawać typu danych wskazywanych, gdy jest on nieistotny. Wówczas można zdefiniować np. taki wskaźnik:

void *wskaznik;
Nie można jednak na nim wykonywać, żadnych operacji tzw. arytmetyki wskaźnikowej, o której będzie mowa na następnej stronie. Jest on jednak zgodny w sensie tzw. przypisania z każdym innym wskaźnikiem, to znaczy, że wskazanie zawarte w dowolnej zmiennej wskaźnikowej można przypisać do takiej zmiennej void * .

Oczywiście nie ma sensu i dla kompilatora jest błędną taka deklaracja:

void zmienna;


Definiując (nie tylko deklarując) jakąś zmienną czy inny obiekt, możemy go zainicjować. Jeśli tego nie zrobimy, różnie bywa: czasem zmienna ma tzw. wartość nieokreśloną czyli śmieci - przypadkową zawartość, która była w pamięci przed przydzieleniem jej obiektowi, a czasem kompilator wypełnia obszar pamięci na ten obiekt zerami. Inicjalizacja (nie mów "inicjacja" - to nieporawnie) powoduje, że zaraz po definicji obiekt ma określoną w programie wartość. W programie, zaraz po deklaracji definiującej dany obiekt piszemy znak "=", a po nim tzw. inicjalizer; w przypadku zmiennej prostej może to być wartość, jaką ma być zainicjowana ta zmienna, a w przypadku tablic i struktur w nawiasach klamrowych ("{" i "}") podajemy oddzielone przecinkami wartości inicjujące elementy tablicy albo pola składowe struktur. Unii nie wolno inicjować. Jeśli chodzi o obiekty danej klasy, do inicjalizacji można też użyć konstruktora tej klasy.

<definicja> = <inicjalizer> ;
albo
<definicja> = { <inicjalizer> [ , <inicjalizer> ... ] };
albo
<definicja> = <konstruktor> ( [ <argumenty> ] );

Ogólnie mówiąc, jako wartość inicjującą można podać dowolne wyrażenie którego wartość można obliczyć już na etapie kompilacji - tzw. wyrażenie stałe. Dla wskaźnika może to być stała NULL albo adres innej zmiennej typu zgodnego ze wskazywanym przez wskaźnik. Wartości inicjujących agregat (czyli tablicę lub strukturę) może być mniej niż potrzeba, wtedy pozostałe zostaną zainicjowane zerami. Podam tutaj parę przykładów:

int liczba = 1 + 2;
char znaki[10] = {'a', 'b', 'c', 5 + 0x30};
char linia[80] = "Press any key...";
int *wsklicz = &liczba;
void *wsk = NULL;
W każdym z powyższych przykładów użyto wyrażenia stałego, dającego się obliczyć już na etapie kompilacji. Zmienna liczba będzie miała wartość 3. Czwarty element tablicy znaki będzie miał wartość 53 (0x35), a pozostałe 0. Kolejne znaki tablicy linia zostaną zainicjowane kolejnymi znakami napisu, a pozostałe (bo napis jest krótszy) zostaną zainicjowane znakami NULL ('\0') - jeśli napis byłby dłuższy, to zachowanie kompilatora mogłoby być dwojakie: albo napis zostałby obcięty (i nie byłoby w nim znaku NULL), albo też zainicjowana zostałaby pamięć znajdująca się zaraz za tablicą (katastrofa!) - warto uważać; dobrze jest więc na stały napis rezerwować tablicę przynajmniej o jeden element większą, niż wynosi długość napisu (o jeden większą, bo musi być miejsce na znak NULL). Zmienna wsklicz będzie wskazywała na obszar pamięci zajmowany przez zmienną liczba typu int - wyrażenie inicjujące i tutaj jest stałe, bo przecież kompilator każdej zmiennej przydziela stałe miejsce w pamięci; użyłem tu operatora "&" (znak ampersand) służącego do pobierania adresu pamięci zajmowanej przez obiekt. Zmienna wsk ma na nic nie wskazywać; z pewnością potem zostanie jej przypisane jakieś wskazanie.


Na koniec rzecz o tzw. atrybutach używanych w deklaracjach. Jednym z nich jest atrybut stałości const. Umieszczenie go w deklaracji określa, że definiowany obiekt nie będzie się zmieniał (np. zmienna nie będzie zmieniała swej zawartości). W przypadku wskaźników można określić albo, że wskaźnik nie będzie zmieniał swego wskazania, albo, że wskazywana przezeń dana nie będzie się zmieniać, albo to i to jednocześnie.
Przykłady:

const int stala = 10;
char zmienna;
char * const zwsk = &zmienna;
const char *tekst = "ABCD";
const char * const twsk = "XYZ";
Już spieszę z wyjaśnieniami. Najpierw definiujemy zmienną stala (określenie "zmienna" jest tu trochę mylące, ale odróżnia od stałych kompilatora - kto chce, niech mówi "stała ..." dodając typ, np. "stała całkowita", ale to też może się mylić ze zwykłymi stałymi) typu int i inicjujemy ją wartością 10; "obiecujemy" jednak (const), że nie będziemy zmieniać jej wartości. Można więc powiedzieć inaczej: stala jest zmienną o stałej wartości. W dalszej części niepoprawna będzie zatem przykładowa instrukcja wyrażenia przypisania:
stala=10;
nawet, jeśli przypisujemy tą samą wartość, co przy inicjalizacji. Co ciekawe, takiej zmiennej const można używać we wspomnianych wcześniej stałych wyrażeniach - w końcu jej wartość jest stała i znana już na etapie kompilacji. Mogą więc takie "zmienne" stanowić alternatywę dla stałych kompilatora.
Dalej definiowana jest najzwyklejsza zmienna, zajmująca taki obszar pamięci, jakiego wymaga typ char, adresem tego obszaru można zainicjować następny obiekt: stały wskaźnik na char. Znak wskazywany przez zwsk może się zmieniać, ale nie może się zmienić samo wskazanie, to znaczy, że zwsk od teraz będzie wskazywać tylko na zmienna.
Zmienna tekst może się zmieniać, to znaczy może zmieniać się wskazanie w niej, ale nie mogą się zmieniać wskazywane przez nią obszary pamięci, zresztą inicjujemy od razu tą zmienną stałymi znakami (przypominam, że napisy w cudzysłowiu czyli stałe tekstowe, są typu const char * ), których nie powinno się zmieniać.
Na końcu jest twsk - nie dość, że stały wskaźnik, to jeszcze na stałe znaki. Nie można zmieniać ani wskazania przypisanego do tej zmiennej, ani też wskazywanych znaków.

Jasne jest, że nie ma sensu deklaracja zmiennej const bez inicjalizacji:

const int ilosc;
bo przecież nie będzie jej można później "ustawić".


Deklaracje klas wyglądają bardzo podobnie jak deklaracje struktur. Mają nawet podobną reprezentację swoich danych składowych w pamięci, jak struktury. Są jednak pewne szczegóły, które zostaną omówione osobno.

Powrót na początek