1.4.pdf

(737 KB) Pobierz
Od zera do gier kodera
4
OPERACJE NA ZMIENNYCH
Są plusy dodatnie i plusy ujemne.
Lech Wałęsa
W tym rozdziale przyjrzymy się dokładnie zmiennym i wyrażeniom w języku C++. Jak
wiemy, służą one do przechowywania wszelkich danych i dokonywania nań różnego
rodzaju manipulacji. Działania takie są podstawą każdej aplikacji, a w złożonych
algorytmach gier komputerowych mają niebagatelne znaczenie.
Poznamy więc szczegółowo większość aspektów programowania związanych ze
zmiennymi oraz zobaczymy często używane operacje na danych liczbowych i tekstowych.
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 ) 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
76934550.017.png 76934550.018.png
90
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
76934550.019.png 76934550.020.png
91
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 22 . 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 22. Ta sama nazwa, lecz inne znaczenie. Każda z trzech lokalnych zmiennych nX jest
całkowicie odrębna i niezależna od pozostałych
22 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.
76934550.001.png 76934550.002.png
 
92
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 23. Zakres modułowy zmiennej
76934550.003.png 76934550.004.png 76934550.005.png 76934550.006.png 76934550.007.png 76934550.008.png 76934550.009.png 76934550.010.png 76934550.011.png 76934550.012.png 76934550.013.png 76934550.014.png
93
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++ 23 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 ;
23 A także w większości współczesnych języków programowania
76934550.015.png 76934550.016.png
Zgłoś jeśli naruszono regulamin