BAZA WIEDZY
KURSY
Bazy danych w PHP
Kurs AdvancedAJAX
Kurs ASP
Kurs ASP.NET
Kurs C++
Kurs CSS
Kurs HTML
Kurs HTML drugi
Kurs JavaScript
Kurs MySQL
Kurs PHP
Kurs RSS
Kurs XHTML
Obiekty DOM
MANUALE
CSS1 - W3C
DOM - w budowie
PHP 2005
PHP 2006
Wyrażenia regularne
SHOUTBOX
STAT
Online: 5 | UU: 202

Wnikliwy rzut oka na zmienne

Zmienna to coś w rodzaju pojemnika na informacje, mogącego zawierać określone dane. Wcześniej dowiedzieliśmy się, iż dla każdej zmiennej musimy określić typ danych , które będziemy w niej przechowywać, oraz nazwę , przez którą będziemy ją identyfikować. Określenie takie nazywamy deklaracją zmiennej i stosowaliśmy je niemal w każdym programie przykładowym - powinno więc być ci doskonale znane :)

Nasze aktualne wiadomości o zmiennych są mimo tego dość skąpe i dlatego musimy je niezwłocznie poszerzyć. Uczynimy to wszakże w niniejszym podrozdziale.

Zasięg zmiennych

Gdy deklarujemy zmienną, podajemy jej typ i nazwę - to oczywiste. Mniej dostrzegalny jest fakt, iż jednocześnie określamy też obszar obowiązywania takiej deklaracji. Innymi słowy, definiujemy zasięg zmiennej.

Zasięg ( zakres , ang. scope ) zmiennej to część kodu, w ramach której dana zmienna jest dostępna.

Wyróżniamy kilka rodzajów zasięgów. Do wszystkich jednak stosuje się ogólna, naturalna reguła: niepoprawne jest jakiekolwiek użycie zmiennej przed jej deklaracją. Tak więc poniższy kod:

std::cin >> nZmienna;
int nZmienna;

niechybnie spowoduje błąd kompilacji. Sądzę, że jest to dość proste i logiczne - nie możemy przecież wymagać od kompilatora znajomości czegoś, o czym sami go wcześniej nie poinformowaliśmy.

W niektórych językach programowania (na przykład Visual Basicu czy PHP) możemy jednak używać niezadeklarowanych zmiennych. Większość programistów uważa to za niedogodność i przyczynę powstawania trudnych do wykrycia błędów (spowodowanych choćby literówkami). Ja osobiście całkowicie podzielam ten pogląd :D

Na razie poznamy dwa rodzaje zasięgów - lokalny i modułowy .

Zasięg lokalny

Zakres lokalny obejmuje pojedynczy blok kodu . Jak pamiętasz, takim blokiem nazywamy fragment listingu zawarty między nawiasami klamrowymi { } . Dobrym przykładem mogą być tu bloki warunkowe instrukcji if , bloki pętli, a także całe funkcje.

Otóż każda zmienna deklarowana wewnątrz takiego bloku ma właśnie zasięg lokalny.

Zakres lokalny obejmuje kod od miejsca deklaracji zmiennej aż do końca bloku, wraz z ewentualnymi blokami zagnieżdżonymi.

Te dość mgliste stwierdzenia będą pewnie bardziej wymowne, jeżeli zostaną poparte odpowiednimi przykładami. Zerknijmy więc na poniższy kod:

void main()
{
int nX;
std::cin >> nX;

if (nX > 0 )
{
std::cout << nX;
getch();
}
}

Jego działanie jest, mam nadzieję, zupełnie oczywiste (zresztą nieszczególnie nas teraz interesuje :)). Przyjrzyjmy się raczej zmiennej nX . Jako że zadeklarowaliśmy ją wewnątrz bloku kodu - w tym przypadku funkcji main() - posiada ona zasięg lokalny. Możemy zatem korzystać z niej do woli w całym tym bloku, a więc także w zagnieżdżonej instrukcji if .

Dla kontrastu spójrzmy teraz na inny, choć podobny kod:

void main()
{
int nX = 1 ;

if (nX > 0)
{
int nY = 10 ;
}
std::cout << nY;
getch();
}

Powinien on wypisać liczbę 10 , prawda? Cóż. niezupełnie :) Sama próba uruchomienia programu skazana jest na niepowodzenie: kompilator "przyczepi" się do przedostatniego wiersza, zawierającego nazwę zmiennej nY . Wyda mu się bowiem kompletnie nieznana!

Ale dlaczego?! Przecież zadeklarowaliśmy ją ledwie dwie linijki wyżej! Czyż nie możemy więc użyć jej tutaj?.

Jeżeli uważnie przeczytałeś poprzednie akapity, to zapewne znasz już przyczynę niezadowolenia kompilatora. Mianowicie, zmienna nY ma zasięg lokalny, obejmujący wyłącznie blok if . Reszta funkcji main() nie należy już do tego bloku, a zatem znajduje się poza zakresem nY . Nic dziwnego, że zmienna jest tam traktowana jako obca - poza swoim zasięgiem ona faktycznie nie istnieje , gdyż jest usuwana z pamięci w momencie jego opuszczenia.

Zmiennych o zasięgu lokalnym relatywnie najczęściej używamy jednak bezpośrednio we wnętrzu funkcji. Przyjęło się nawet nazywać je zmiennymi lokalnymi 1 lub automatycznymi . Ich rolą jest zazwyczaj przechowywanie tymczasowych danych, wykorzystywanych przez podprogramy, lub częściowych wyników obliczeń.

Tak jak poszczególne funkcje w programie, tak i ich zmienne lokalne są od siebie całkowicie niezależne. Istnieją w pamięci komputera jedynie podczas wykonywania funkcji i "znikają" po jej zakończeniu. Niemożliwe jest więc odwołanie do zmiennej lokalnej spoza jej macierzystej funkcji. Poniższy przykład ilustruje ten fakt:

// LocalVariables - zmienne lokalne
void Funkcja1()
{
int nX = 7 ;
std::cout << "Zmienna lokalna nX funkcji Funkcja1(): " << nX
<< std::endl;
}

void Funkcja2()
{
int nX = 5 ;
std::cout << "Zmienna lokalna nX funkcji Funkcja2(): " << nX
<< std::endl;
}
void main()
{
int nX = 3 ;
Funkcja1();
Funkcja2();
std::cout << "Zmienna lokalna nX funkcji main(): " << nX
<< std::endl;
getch();
}

Mimo że we wszystkich trzech funkcjach ( Funkcja1() , Funkcja2() i main() ) nazwa zmiennej jest identyczna ( nX ), w każdym z tych przypadków mamy do czynienia z zupełnie inną zmienną.


Screen 17. Ta sama nazwa, lecz inne znaczenie. Każda z trzech lokalnych zmiennych nX jest całkowicie odrębna i niezależna od pozostałych

Mogą one współistnieć obok siebie pomimo takich samych nazw, gdyż ich zasięgi nie pokrywają się . Kompilator słusznie więc traktuje je jako twory absolutnie niepowiązane ze sobą. I tak też jest w istocie - są one "wewnętrznymi sprawami" każdej z funkcji, do których nikt nie ma prawa się mieszać :)

Takie wyodrębnianie niektórych elementów aplikacji nazywamy hermetyzacją (ang. encapsulation ). Najprostszym jej wariantem są właśnie podprogramy ze zmiennymi lokalnymi, niedostępnymi dla innych. Dalszym krokiem jest tworzenie klas i obiektów, które dokładnie poznamy w dalszej części kursu.
Zaletą takiego dzielenia kodu na mniejsze, zamknięte części jest większa łatwość modyfikacji oraz niezawodność. W dużych projektach, realizowanych przez wiele osób, podział na odrębne fragmenty jest w zasadzie nieodzowny, aby współpraca między programistami przebiegała bez problemów.

Ze zmiennymi o zasięgu lokalnym spotykaliśmy się dotychczas nieustannie w naszych programach przykładowych. Prawdopodobnie zatem nie będziesz miał większych kłopotów ze zrozumieniem sensu tego pojęcia. Jego precyzyjne wyjaśnienie było jednak nieodzowne, abym z czystym sumieniem mógł kontynuować :D

Zasięg modułowy

Szerszym zasięgiem zmiennych jest zakres modułowy. Posiadające go zmienne są widoczne w całym module kodu . Możemy więc korzystać z nich we wszystkich funkcjach , które umieścimy w tymże module.

Jeżeli zaś jest to jedyny plik z kodem programu, to oczywiście zmienne te będą dostępne dla całej aplikacji. Nazywamy się je wtedy globalnymi .

Aby zobaczyć, jak "działają" zmienne modułowe, przyjrzyj się następującemu przykładowi:

// ModularVariables - zmienne modułowe
int nX = 10 ;
void Funkcja()
{
std::cout << "Zmienna nX wewnatrz innej funkcji: " << nX
<< std::endl;
}
void main()
{
std::cout << "Zmienna nX wewnatrz funkcji main(): " << nX
<< std::endl;
Funkcja();
getch();
}

Zadeklarowana na początku zmienna nX ma właśnie zasięg modułowy. Odwołując się do niej, obie funkcje ( main() i Funkcja() ) wyświetlają wartość jednej i tej samej zmiennej.


Screen 18. Zakres modułowy zmiennej

Jak widać, deklarację zmiennej modułowej umieszczamy bezpośrednio w pliku źródłowym, poza kodem wszystkich funkcji. Wyłączenie jej na zewnątrz podprogramów daje zatem łatwy do przewidzenia skutek: zmienna staje się dostępna w całym module i we wszystkich zawartych w nim funkcjach.

Oczywistym zastosowaniem dla takich zmiennych jest przechowywanie danych, z których korzysta wiele procedur. Najczęściej muszą być one zachowane przez większość czasu działania programu i osiągalne z każdego miejsca aplikacji. Typowym przykładem może być chociażby numer aktualnego etapu w grze zręcznościowej czy nazwa pliku otwartego w edytorze tekstu. Dzięki zastosowaniu zmiennych o zasięgu modułowym dostęp do takich kluczowych informacji nie stanowi już problemu.

Zakres modułowy dotyczy tylko jednego pliku z kodem źródłowym. Jeśli nasza aplikacja jest na tyle duża, byśmy musieli podzielić ją na kilka modułów, może on wszakże nie wystarczać. Rozwiązaniem jest wtedy wyodrębnienie globalnych deklaracji we własnym pliku nagłówkowym i użycie dyrektywy #include . Będziemy o tym szerzej mówić w niedalekiej przyszłości :)

Przesłanianie nazw

Gdy używamy zarówno zmiennych o zasięgu lokalnym, jak i modułowym (czyli w normalnym programowaniu w zasadzie nieustannie), możliwa jest sytuacja, w której z danego miejsca w kodzie dostępne są dwie zmienne o tej samej nazwie , lecz różnym zakresie . Wyglądać to może chociażby tak:

int nX = 5 ;
void main()
{
int nX = 10 ;
std::cout << nX;
}

Pytanie brzmi: do której zmiennej nX - lokalnej czy modułowej - odnosi się instrukcja std::cout ? Inaczej mówiąc, czy program wypisze liczbę 10 czy 5 ? A może w ogóle się nie skompiluje?.

Zjawisko to nazywamy przesłanianiem nazw (ang. name shadowing ), a pojawiło się ono wraz ze wprowadzeniem idei zasięgu zmiennych. Tego rodzaju kolizja oznaczeń nie powoduje w C++ 1 błędu kompilacji, gdyż jest ona rozwiązywana w nieco inny sposób:

Konflikt nazw zmiennych o różnym zasięgu jest rozstrzygany zawsze na korzyść zmiennej o węższym zakresie.

Zazwyczaj oznacza to zmienną lokalną i tak też jest w naszym przypadku. Nie oznacza to jednak, że jej modułowy imiennik jest w funkcji main() niedostępny. Sposób odwołania się do niego ilustruje poniższy przykładowy program:

// Shadowing - przesłanianie nazw
int nX = 4 ;
void main()
{
int nX = 7 ;
std::cout << "Lokalna zmienna nX: " << nX << std::endl;
std::cout << "Modulowa zmienna nX: " << ::nX << std::endl;
getch();
}

Pierwsze odniesienie do nX w funkcji main() odnosi się wprawdzie do zmiennej lokalnej, lecz jednocześnie możemy odwołać się także do tej modułowej. Robimy to bowiem w następnej linijce:

std::cout << "Modulowa zmienna nX: " << ::nX << std::endl;

Poprzedzamy tu nazwę zmiennej dwoma znakami dwukropka :: . Jest to tzw. operator zasięgu . Wstawienie go mówi kompilatorowi, aby użył zmiennej globalnej zamiast lokalnej - czyli zrobił dokładnie to, o co nam chodzi :)

Operator ten ma też kilka innych zastosowań, o których powiemy niedługo (dokładniej przy okazji klas).

Chociaż C++ udostępnia nam tego rodzaju mechanizm 1 , do dobrej praktyki programistycznej należy niestosowanie go. Identyczne nazwy wprowadzają bowiem zamęt i pogarszają czytelność kodu.

Dlatego też do nazw zmiennych modułowych dodaje się zazwyczaj przedrostek 2 g_ (od global ), co pozwala łatwo odróżnić je od lokalnych. Po zastosowaniu tej reguły nasz przykład wyglądałby mniej więcej tak:

int g_nX = 4 ;
void main()
{
int nX = 7 ;
std::cout << "Lokalna zmienna: " << nX << std::endl;
std::cout << "Modulowa zmienna: " << g_nX << std::endl;
getch();
}

Nie ma już potrzeby stosowania mało czytelnego operatora :: i całość wygląda przejrzyście i profesjonalnie ;)

***

Zapoznaliśmy się zatem z niełatwą ideą zasięgu zmiennych. Jest to jednocześnie bardzo ważne pojęcie, które trzeba dobrze znać, by nie popełniać trudnych do wykrycia błędów. Mam nadzieję, że jego opis oraz przykłady były na tyle przejrzyste, że nie miałeś poważniejszych kłopotów ze zrozumieniem tego aspektu programowania.

Modyfikatory zmiennych

W aktualnym podrozdziale szczególnie upodobaliśmy sobie deklaracje zmiennych. Oto bowiem omówimy kolejne zagadnienie z nimi związane - tak zwane modyfikatory (ang. modifiers ). Są to mianowicie dodatkowe określenia umieszczane w deklaracji zmiennej, nadające jej pewne specjalne własności.

Zajmiemy się dwoma spośród trzech dostępnych w C++ modyfikatorów. Pierwszy - static - chroni zmienną przed utratą wartości po opuszczeniu jej zakresu przez program. Drugi zaś - znany nam const - oznacza stałą, opisaną już jakiś czas temu.

Zmienne statyczne

Kiedy aplikacja opuszcza zakres zmiennej lokalnej, wtedy ta jest usuwana z pamięci. To całkowicie naturalne - po co zachowywać zmienną, do której i tak nie byłoby dostępu? Logiczniejsze jest zaoszczędzenie pamięci operacyjnej i pozbycie się nieużywanej wartości, co też program skrzętnie czyni. Z tego powodu przy ponownym wejściu w porzucony wcześniej zasięg wszystkie podlegające mu zmienne będą ustawione na swe początkowe wartości.

Niekiedy jest to zachowanie niepożądane - czasem wolelibyśmy, aby zmienne lokalne nie traciły swoich wartości w takich sytuacjach. Najlepszym rozwiązaniem jest wtedy użycie modyfikatora static . Rzućmy okiem na poniższy przykład:

// Static - zmienne statyczne
void Funkcja()
{
static int nLicznik = 0 ;
++nLicznik;
std::cout << "Funkcje wywolano po raz " << nLicznik << std::endl;
}

void main()
{
std::string strWybor;
do
{
Funkcja();
std::cout << "Wpisz 'q', aby zakonczyc: " ;
std::cin >> strWybor;
} while (strWybor != "q" );
}

Ów program jest raczej trywialny i jego jedynym zadaniem jest kilkukrotne uruchomienie podprogramu Funkcja() , dopóki życzliwy użytkownik na to pozwala :) We wnętrzu tejże funkcji mamy zadeklarowaną zmienną statyczną, która służy tam jako licznik uruchomień.


Screen 19. Zliczanie wywołań funkcji przy pomocy zmiennej statycznej

Jego wartość jest zachowywana pomiędzy kolejnymi wywołaniami funkcji, gdyż istnieje w pamięci przez cały czas działania aplikacji 3 . Możemy więc każdorazowo inkrementować tą wartość i pokazywać jako ilość uruchomień funkcji. Tak właśnie działają zmienne statyczne :)

Deklaracja takiej zmiennej jest, jak widzieliśmy, nad wyraz prosta:

static int nLicznik = 0 ;

Wystarczy poprzedzić oznaczenie jej typu słówkiem static i voila :) Nadal możemy także stosować inicjalizację do ustawienia początkowej wartości zmiennej.

Jest to wręcz konieczne - gdybyśmy bowiem zastosowali zwykłe przypisanie, odbywałoby się ono przy każdym wejściu w zasięg zmiennej. Wypaczałoby to całkowicie sens stosowania modyfikatora static .

Stałe

Stałe omówiliśmy już wcześniej, więc nie są dla ciebie nowością. Obecnie podkreślimy ich związek ze zmiennymi.

Jak (mam nadzieję) pamiętasz, aby zadeklarować stałą należy użyć słowa const , na przykład:

const float GRAWITACJA = 9.80655 ;

const , podobnie jak static , jest modyfikatorem zmiennej. Stałe posiadają zatem wszystkie cechy zmiennych, takie jak typ czy zasięg. Jedyną różnicą jest oczywiście niemożność zmiany wartości stałej.

***

Tak oto uzupełniliśmy swe wiadomości na temat zmiennych o ich zasięg oraz modyfikatory. Uzbrojeni w tą nową wiedzę możemy teraz śmiało podążać dalej :D

1 A także w większości współczesnych języków programowania

1 Nie tylko zresztą w C++. Wprawdzie sporo języków jest uboższych o możliwość deklarowania zmiennych wewnątrz bloków warunkowych, pętli czy podobnych, ale niemal wszystkie pozwalają na stosowanie zmiennych lokalnych. Nazwa ta jest więc obecnie używana w kontekście dowolnego języka programowania.

1 Większość języków go nie posiada!

2 Jest to element notacji węgierskiej, aczkolwiek szeroko stosowany przez wielu programistów. Więcej informacji w Dodatku A.

3 Dokładniej mówiąc: od momentu deklaracji do zakończenia programu

Kurs C++
| | | |
Copyright © 2006-2013 egrafik.pl | Kontakt | Reklama | Projekty domów
jocker