Zasięg zmiennych w języku C++. Zmienne globalne i zmienne lokalne.

Wprowadzenie

Jeśli dokładnie śledzisz wszystkie przykłady zamieszczone w kursie, udało Ci się na pewno spostrzec, że wszystkim zmiennym w naszych programach nadawaliśmy unikalne nazwy. Inaczej mówiąc, nie zdarzyło się, aby dwie zmienne w programie miały taką samą nazwę.

Nie zawsze jednak tak właśnie musi być. Co prawda zmienne muszą mieć w programie inne nazwy, ale tylko w tym samym zasięgu. Jeśli chcesz zrozumieć to zagadnienie, to zapraszam do przeczytania tej oraz także następnej lekcji kursu. W obu lekcjach, postaram Ci się wyjaśnić wszystko, co jest związane ze zmiennymi.

Rodzaje zmiennych

Mimo że w języku C i C++ występuje wiele rodzajów zmiennych, to ze względu na tematykę tej lekcji zajmiemy się tylko jednym podziałem zmiennych. Wszystkie zmienne można podzielić na zmienne globalne i zmienne lokalne.

W dużym uproszczeniu różnica między tymi dwoma rodzajami zmiennych jest następująca: zmienne globalne są to takie zmienne, które są dostępne w całym programie i przez cały czas jego działania, natomiast zmienne lokalne są dostępne tylko w pewnej części programu, zazwyczaj tylko w pewnej chwili działania programu, a nie przez cały czas.

Zmienne globalne

Zaletą zmiennych globalnych jest to, że są one widoczne w całym programie. Nie miało to dużego znaczenia w przypadku naszych dotychczasowych programach, jednak nabierze bardzo dużego znaczenia, kiedy przedstawię Ci pojęcie funkcji w C++.

Zmienne globalne deklaruje się (i ewentualnie inicjalizuje) pomiędzy blokiem dołączonych plików nagłówkowych a funkcją main. Ponieważ są to zwykłe zmienne (mają tylko dodatkowe właściwości), to deklaracja, inicjalizacja oraz posługiwanie się tymi zmiennymi wygląda dokładnie tak samo, jak to robiliśmy w dotychczasowych programach.

Ponieważ zmienne globalne są widoczne w całym programie, zatem są również widoczne wewnątrz funkcji main. Oznacza to, że wewnątrz funkcji main (oraz wszystkich innych funkcji), możemy dokonywać wszystkich operacji jakie są tylko możliwe dla zmiennej danego typu.

Wszystkie powyższe rozważania dotyczące zmiennych globalnych podsumuje poniższy przykład:

#include <iostream>
#include <cstring>

using namespace std;

/* Tutaj tworzymy zmienne globalne */

int liczbaPierwsza = 2;  // definicja i inicjalizacja
float liczbaDruga;       // definicja bez inicjalizacji
string napis="Przyklad"; // definicja i inicjalizacja

int main()
{
  // wypisanie wartosci zmiennych globalnych
  cout <<"Oto biezace wartosci zmiennych globalnych: "<<endl;
  cout <<liczbaPierwsza<<' '<<liczbaDruga<<' '<<napis<<endl;
 
  // wykonanie dowolnych operacji na zmiennych globalnych
  ++liczbaPierwsza;
  liczbaDruga=3;
  napis="Zmieniona zmienna globalna";
 
  // wypisanie wartosci zmiennych globalnych  
  cout <<"Oto biezace wartosci zmiennych globalnych: "<<endl;
  cout <<liczbaPierwsza<<' '<<liczbaDruga<<' '<<napis<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 24.1

Mimo że program jest dość łatwy, omówię go dokładnie, żeby nie było żadnych wątpliwości. W programie definiujemy 3 zmienne globalne, tutaj akurat różnego typu (int, float i string). Skąd wiadomo, że to akurat zmienne globalne? Oczywiście stąd, że zostały zdefiniowane pomiędzy blokiem dołączanych plików nagłówkowych a funkcją main.

Dla dociekliwych - między definicją zmiennych a dołączeniem plików nagłówkowych, znajduje się dodatkowo dołączenie standardowej przestrzeni nazw. Gdybyśmy tego w tym przypadku nie zrobili, wówczas typ string nie byłby "znany". Gdybyśmy natomiast nie tworzyli zmiennej typu string, wówczas dołączanie standardowej przestrzeni nazw moglibyśmy zrealizować dopiero po definicji zmiennych.

Dodatkowo warto zwrócić uwagę, że dwie z trzech zmiennych zainicjowaliśmy jakąś przykładową wartością. Jedna z wartości nie została jawnie zainicjalizowana.

W funkcji main, dokonujemy wypisania wartości zmiennych, następnie dokonujemy dowolnych operacji, tak jak robiliśmy to we wszystkich poprzednich programach, a na końcu znowu wypisujemy wartości zmiennych.

Jak łatwo zauważyć, w funkcji main, nie mamy żadnego problemu z dostępem do zmiennych. Tak oczywiście miało właśnie być, bo zmienne globalne powinny być i są dostępne wszędzie, o czym właśnie udało Ci się przekonać.

Warto jednak zwrócić uwagę na jedną inną istotną kwestię. W funkcji main przy pierwszym wypisywaniu wartości zmiennych wypisujemy jedną niezainicjalizowaną zmienną! Mam nadzieję, że pamiętasz, że używanie zmiennych, którym nie nadaliśmy wartości, może być katastrofalne w skutkach.

Przypomnij sobie, co spowodowało niezainicjalizowanie zmiennej w poprzedniej lekcji. W rezultacie, otrzymaliśmy nieprawidłowy wynik sumowania wartości, co w prawdziwym programie mogłoby być naprawdę wielką tragedią.

W przedstawionym jednak przed momentem przykładzie, okazuje się, że na taki wybryk można sobie akurat w tym przypadku pozwolić. Wiedz jednak, że możemy tak postąpić tylko dlatego, że mamy do czynienia ze zmienną globalną. Gdybyśmy natomiast mieli do czynienia ze zmienną lokalną (tak jak we wszystkich przykładach przedstawionych dotychczas w kursie), nie można by tak było postąpić.

Mam nadzieję, że wybaczysz mi, jeśli dokładne wyjaśnienie tego zagadnienia przedstawię Ci w dalszej części tej lekcji. Teraz nadszedł czas, aby w końcu dowiedzieć się czegoś o zmiennych lokalnych.

Zmienne lokalne - sens stosowania

Mimo że nie wyjaśniłem Ci na razie czym są zmienne lokalne, to jednak w tym momencie powinno Ci się zacząć nasuwać pytanie - skoro zmienne globalne są dostępne w całym programie, czyli możemy je wszędzie wykorzystać, to po co w ogóle są zmienne lokalne i po co nawet czytać do czego służą?

Odpowiedź na to pytanie może być tylko i wyłącznie poparta doświadczeniem w programowaniu. Rzeczywiście każdy kto się uczy programować, pomyśli sobie, że zmienne globalne są wspaniałe i naprawdę warto je cały czas stosować. Ja sam z resztą na początku mojej przygody z C++ tak właśnie sądziłem.

Na dłuższą metę okazuje się jednak, że w przypadku większych programów, gdzie używamy kilkudziesięciu lub kilkuset zmiennych, używanie zmiennych globalnych staje się wyjątkowo uciążliwe.

Nazwy zmiennych globalnych muszą być między sobą różne. Nie jest możliwe utworzenie zmiennych globalnych o tej samej nazwie. Sprawia to, że w przypadku dużej liczby zmiennych, należy tworzyć coraz dłuższe nazwy zmiennych, których zapamiętanie staje się coraz bardziej uciążliwe.

Poza tym, ponieważ zmienne globalne są dostępne wszędzie, można je zatem w dowolnym miejscu zmienić. Oczywiście może to stanowić zaletę, jednak w większych programach jest tak naprawdę wadą. Nasza zmienna może bowiem zostać zmieniona w dowolnym miejscu programu, często przez przypadek.

Nawet jeśli program piszemy sami, możemy się łatwo zapomnieć lub pomylić, a co dopiero gdy program pisze kilku programistów. Wówczas prawdopodobieństwo przypadkowej i niechcianej zmiany wartości zmiennej globalnej jest bardzo duże, a znalezienie takiego błędu w programie, który liczy kilka lub kilkanaście tysięcy linii, może zająć zbyt dużo cennego czasu.

Ostatnią ważną kwestią jest to, że ponieważ zmienne globalne istnieją przez cały czas działania programu, to przez cały czas zajmują miejsce w pamięci operacyjnej. W przypadku dużej liczby zmiennych lub bardzo rozbudowanych programów, nie można oczywiście dopuszczać do przechowywania wszystkich zmiennych przez cały czas działania programu w pamięci, dlatego też należy szukać innych rozwiązań.

W tym momencie dobrze by było, gdyby udało Ci się zapamiętać - zmienne globalne mają zaletę, bo są wszędzie dostępne, ale przez to ich używanie jest bardzo ryzykowne, gdyż wszędzie może nastąpić zmiana ich wartości. Mamy zatem połączenie teoretycznej wygody z dużym niebezpieczeństwem.

Wierzę, że w ten oto sposób zachęciłem Cię do przeczytania dalszej części lekcji i zapoznania się ze zmiennymi lokalnymi, które są znacznie wygodniejsze w użyciu.

Zmienne lokalne

Tak naprawdę same zmienne lokalne były wykorzystywane przez nas we wszystkich poprzednich programach. Aby utworzyć zmienną lokalną, można ją utworzyć w dowolnym miejscu danej funkcji, czyli również w funkcji main. Przykładowy program, pokazujący wykorzystanie i użycie zmiennych lokalnych znajduje się poniżej:

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
  /* Zmienne lokalne - mozna je utworzyc rowniez w kazdym innym miejscu funkcji
     main*/


  int liczbaPierwsza = 2;  // definicja i inicjalizacja
  float liczbaDruga;       // definicja bez inicjalizacji
  string napis="Przyklad"; // definicja i inicjalizacja
 
  // wypisanie wartosci zmiennych lokalnych
  cout <<"Oto biezace wartosci zmiennych lokalnych: "<<endl;
  cout <<liczbaPierwsza<<' '<<liczbaDruga<<' '<<napis<<endl; // UWAGA !!!
 
  // wykonanie dowolnych operacji na zmiennych lokalnych
  ++liczbaPierwsza;
  liczbaDruga+=1;
  napis="Zmieniona zmienna lokalna";
 
  // wypisanie wartosci zmiennych lokalnych
  cout <<"Oto biezace wartosci zmiennych lokalnych: "<<endl;
  cout <<liczbaPierwsza<<' '<<liczbaDruga<<' '<<napis<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 24.2

Powyższy kod jest bardzo podobny do kodu, który przedstawiłem dla zmiennych globalnych, z tym tylko, że tutaj wartość zmiennej liczbaDruga jest zwiększana o 1 (gdy zostało zachowane przypisanie zmiennej wartości 3, to kompilator Dev-C++ zachowuje się trochę "dziwnie" i nie wyłapalibyśmy istoty problemu).

Sam program powinien być dla Ciebie oczywisty, bowiem tego typu programy pojawiały się w zasadzie od samego początku kursu. Zwróć uwagę na komentarz // UWAGA !!!. Komentarz dodałem, aby zwrócić po raz kolejny Twoją uwagę, że nie należy operować na zmiennych, których wartości nie ustaliliśmy w żaden sposób (zmienna liczbaDruga nie ma ustalonej wartości).

Jak już wspomniałem, w przypadku zmiennych globalnych było to dopuszczalne (na razie jeszcze nie wyjaśniłem dlaczego), jednak w przypadku zmiennych lokalnych, czyli tak jak tu, będzie to efektem błędów i często nieprzewidzianego działania programu.

Przedstawiłem Ci najprostszy przykład dotyczący zmiennych lokalnych, warto jednak rozszerzyć Twoją wiedzę.

Wspomniałem kiedyś że nawiasy klamrowe służą do grupowania instrukcji, co jest oczywiście prawdą. Nawiasy klamrowe służą jednak do jeszcze jednej ważnej rzeczy, a mianowicie wyznaczają zasięg lokalny.

Oznacza to po prostu, że te zmienne które zostały zadeklarowane pomiędzy nawiasami klamrowymi są widoczne tylko między tą parą nawiasów. Po nawiasie zamykającym, zmienna przestaje być widoczna, bowiem przestaje ona już istnieć.

Mam nadzieję, że uda Ci się to łatwo zapamiętać, jeśli tylko uświadomię Ci, że właśnie taką parę nawiasów stosujemy zawsze, kiedy definiujemy funkcję main. Zmienne utworzone między tymi nawiasami są widoczne tylko między nawiasami klamrowymi, czyli w tym przypadku w całej funkcji main.

Jednak jak już doskonale wiesz, nawiasów klamrowych używa się jeszcze w innych sytuacjach - np. w pętli for. Oznacza to, że te zmienne które zadeklarowaliśmy w tych nawiasach klamrowych nie będą widoczne w pozostałej części programu, zgodnie z tym co przed momentem napisałem.

Dodatkowo, nawiasów klamrowych możemy używać prawie w dowolnym miejscu funkcji main, kiedy po prostu z jakichś powodów mamy taką potrzebę, nie tylko gdy używamy pętli czy instrukcji warunkowych (w praktyce jednak taki zabieg stosuje się raczej bardzo rzadko).

Poniżej znajduje się przykładowy program, który ilustruje, że nawiasy klamrowe wyznaczają zasięg zmiennych:

#include <iostream>

using namespace std;

int main()
{  
  double dluga = 3.52;  // definicja i inicjalizacja
 
  cout <<"Wartosc zmiennej wynosi "<<dluga<<endl;
  if (dluga<4)
  { // nowy zasieg
    int a=3;
    cout <<"Zmienna a wynosi "<<a<<endl;
    cout <<"Zmienna dluga wynosi "<<dluga<<endl;
  } // koniec zasiegu
 
  //cout <<a; // Blad - zmienna a juz nie istnieje
 
  /* Zmienna i jest rowniez w zasiegu lokalnym */
  for (unsigned int i=0;i<3;++i)
  {
    int c=i*2;
    cout <<"Zmienna c wynosi "<<c<<endl;
    cout <<"Zmienna dluga wynosi "<<dluga<<endl;
  }
  // cout <<i; // Blad - zmienna i juz nie istnieje
  // cout <<c; // Blad - zmienna c juz nie istnieje
 
  cout <<"Zmienna dluga wynosi "<<dluga<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 24.3

Przede wszystkim zauważ, że zmienna lokalna dluga jest widoczna między parą nawiasów klamrowych między którymi została zadeklarowana, czyli w tym przypadku w całej funkcji main. Jest ona również widoczna w innych zasięgach wyznaczanych w naszym przykładzie przez instrukcję warunkową if i pętlę for.

Z kolei zmienna lokalna a jest tworzona, kiedy zmienna dluga spełnia warunek. Zmienna a jest tworzona w swoim lokalnym zasięgu, między inną parą nawiasów klamrowych, co sprawia, że po zakończeniu tego zasięgu (zamykającym nawiasie klamrowym), zmienna ta już przestaje istnieć i jest już niewidoczna.

Sprawa ma się identycznie w przypadku zmiennej c tworzonej podczas działania pętli for. Warto jednak zaznaczyć, że tym razem dla każdego kroku pętli zmienna ta jest tworzona, kończy się zasięg, zmienna jest usuwana i jest po raz kolejny tworzona zmienna o tej samej nazwie. Po zupełnym zakończeniu pętli, zmienna jest oczywiście już niewidoczna, bowiem przestała istnieć.

Istotne jest również zwrócenie uwagi na zmienną sterującą pętlą, czyli w tym przypadku zmienną i. Sprawa wygląda tutaj jednak trochę inaczej niż dla zmiennej c. Zmienna c była bowiem tworzona i usuwana w każdym kroku pętli, natomiast zmienna sterująca i istnieje przez cały czas działania pętli (czyli przez cały czas działania pętli jest tworzony zasięg lokalny) a kończy swoje życie (i tym samym zasięg) dopiero, kiedy warunek logiczny w pętli for jest nieprawdziwy.

Zmienne globalne a inicjalizacja

Czas powrócić do zagadnienia, o którym wspomniałem już w tej lekcji, jednak którego jeszcze nie wyjaśniłem. Chodzi mianowicie o inicjalizację zmiennych globalnych.

Zapamiętaj: w przypadku zmiennych globalnych wszystkie proste typy danych zostaną zainicjalizowane! Oznacza to, że jeśli Ty nie przypiszesz wartości do zmiennej globalnej, to będzie ona miała wartość, która została nadana przez kompilator.

Poniżej są zestawione wartości, którymi są inicjalizowane przez kompilator zmienne globalne danego typu:

int           0
float       0
double    0
char        '\0' (specjalny znak o kodzie 0, znak pusty)
string      ""   (ciąg pusty)

Nie zapominaj jednak, że w przypadku zmiennych lokalnych, na taką dobroć ze strony kompilatora liczyć nie możemy i jeśli sami nie dokonamy inicjalizacji, możemy stracić dużo czasu na znalezienie takiego błędu.

Poniżej przedstawiam dwa przykłady, które jeszcze raz powinny Ci uświadomić, na czym polega kwestia inicjalizacji zmiennych globalnych.

Przykład dla zmiennej globalnej:

#include <iostream>

using namespace std;

int liczba;
// zmienna liczba zostala domyslnie zainicjalizowana wartoscia 0

int main()
{  
  liczba+=5; // 0 + 5 = 5
  cout <<"Liczba wynosi "<<liczba<<endl;  
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 24.4

Przykład dla zmiennej lokalnej:

#include <iostream>

using namespace std;

int main()
{  
  int liczba;

  // zmienna liczba ma wartosc nieokreslona
  liczba+=5; // ? + 5 = ?
  cout <<"Liczba wynosi "<<liczba<<endl;  
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 24.5

Mam nadzieję, że w ten sposób po raz ostatni musiałem Ci uświadamiać, że zmiennym lokalnym należy nadać wartość samemu, natomiast w przypadku zmiennych globalnych, o ile odpowiada nam wartość domyślna, robić tego nie musimy.

Podsumowanie

W tej lekcji nauczyłem Cię (mam nadzieję) bardzo ważnej rzeczy - tego, że niektóre zmienne są dostępne przez cały czas wywołania programu, a niektóre tylko przez krótki czas. Powinno być dla Ciebie jasne, jakie są podstawowe różnice między zmiennym lokalnymi a globalnymi. Tematyka dotycząca zmiennych będzie kontynuowana w następnej lekcji.

powrót