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
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
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.
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
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
Plik z chomika:
morgan116
Inne pliki z tego folderu:
0.1.pdf
(283 KB)
0.2.pdf
(176 KB)
0.3.pdf
(143 KB)
0.4.pdf
(165 KB)
1.1.pdf
(371 KB)
Inne foldery tego chomika:
Zgłoś jeśli
naruszono regulamin