Przesłanianie zmiennych w języku C++. Zasięg a dostęp do zmiennych.

Wprowadzenie

W poprzedniej lekcji przedstawiłem Ci podstawowe zasady dotyczące zmiennych, zarówno globalnych jak i lokalnych. Nie wyjaśniłem jednak jeszcze wszystkiego odnośnie nazewnictwa zmiennych i współistnienia obok siebie zarówno zmiennych globalnych, jak i lokalnych oraz współistnienia zmiennych lokalnych w różnych zasięgach.

W tej lekcji postaram Ci się właśnie przedstawić, w jaki sposób pogodzić ze sobą kilka zmiennych globalnych i lokalnych oraz wyjaśnić czy mogą współistnieć zmienne o tych samych nazwach, czy też nie.

Schemat rozważanego problemu

#include <iostream>

using namespace std;

//1
int main()
{  
  //2
  {
     //3
     {
       //4
     }
     //3
  }
  //2
  {
     //5
     {
        //6
        {
         //7
        }
        //6
     }
     //5
  }
  //2
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 25.1

Powyższy kod przedstawia szkielet programu, którym będziemy się starali zająć w tej lekcji. Jeśli przypomnisz sobie poprzednią lekcję, to wiesz już na pewno, że nawiasy klamrowe oprócz tego, że służą do grupowania instrukcji, wyznaczają również zasięg lokalny.

W powyższym schemacie, umieściłem kilka par nawiasów klamrowych i za pomocą komentarzy oznaczyłem za pomocą numeru zasięg lokalny. Jeśli się dobrze przyjrzysz, zauważysz, że niektóre z zasięgów pojawiają się kilka razy. Dzieje się tak dlatego, że gdy pewien zasięg przestaje obowiązywać (dzieje się to, kiedy w programie znajduje się zamykający nawias klamrowy), to obowiązującym staje się zasięg wcześniejszy.

Przykładowo, gdy na początku funkcji main, obowiązywał zasięg 2, pojawił się zasięg 3. Po zakończeniu zasięgu 3, obowiązującym jest nadal zasięg 2. Oczywiście dodatkowo w trakcie obowiązywania zasięgu nr 3, pojawił się zasięg 4, ale gdy ten przestał obowiązywać, znów zasięg nr 3 stał się obowiązujący.

Kolejną kwestią jest to, że niektóre z zasięgów są zagnieżdżone w innych, a niektóre są między sobą zupełnie rozłączne. Przykładowo, zasięg nr 2 (zasięg funkcji main) jest zagnieżdżony w zasięgu nr 1, który jest zasięgiem globalnym. Dodatkowo w zasięgu nr 2 jest zagnieżdżony zasięg nr 3, a w nim zasięg nr 4. Z kolei przykładowo zasięg nr 5 jest zagnieżdżony w zasięgu nr 2, ale nie ma on już nic wspólnego z zasięgiem 3 czy zasięgiem nr 4, bowiem kiedy zasięg nr 5 się pojawia, to zasięg nr 3 i zasięg nr 4 już nie istnieją.

Zwróć także uwagę, że liczbę zasięgów w takim przykładowym schemacie jak tutaj, bardzo łatwo policzyć. Liczymy po prostu liczbę par nawiasów klamrowych (lub jeszcze prościej: liczbę otwierających nawiasów klamrowych) - u nas jest ich 6 i dodajemy do tego 1, który symbolizuje zasięg globalny. W ten sposób osiągamy liczbę wszystkich zasięgów w programie, w tym przypadku liczba ta wynosi 7.

Zmienne w tym samym zasięgu

Jak już kilkakrotnie wspominałem, w tym samym zasięgu nie mogą pojawić się zmienne o takiej samej nazwie, nawet jeśli są różnego typu (czyli jedna np. typu int, a druga typu string). Ciężko by było bowiem kompilatorowi zorientować się, o którą zmienną chodzi. Przyznasz też, że nie było by dla Ciebie zbyt wygodnie posługiwać się zmiennymi, gdyby miały one takie same nazwy.

Całe to zagadnienie obrazuje poniższy przykład:

#include <iostream>
#include <cstring>

using namespace std;
//1
int typProduktu;
// string typProduktu; // BLAD - istnieje juz zmienna o nazwie typProduktu
// int typProduktu;    // BLAD -istnieje juz zmienna o nazwie typProduktu

int main()
{  
  //2
  int rodzaj=10;
  cout <<"Zmienna ma wartosc: "<<rodzaj<<endl;
  //2
  // int rodzaj; // BLAD - istnieje juz zmienna o nazwie rodzaj
  // string rodzaj; // BLAD - istnieje juz zmienna o nazwie rodzaj
  //2
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 25.2

W powyższym przykładowym programie deklarujemy zmienną globalną o nazwie typProduktu. Jeśli spróbujesz odkomentować którąś z linii, w której znajduje się deklaracja zmiennej o tej samej nazwie, kompilator zgłosi błąd.

Podobnie sytuacja będzie się miała, kiedy spróbujesz usunąć komentarze z miejsc, gdzie jest deklarowana zmienna o nazwie rodzaj. Ponieważ taka zmienna została już w zasięgu 2 zadeklarowana, to próba ponownej deklaracji zmiennej o tej samej nazwie, zakończy się błędem.

Jak zatem widzisz, to co napisałem przed pojawieniem się przykładu, sprawdza się w 100%. Czas zatem przejść do nieco bardziej skomplikowanych przykładów.

Zależności między zasięgami a dostępność zmiennych

Zacznijmy od kolejnego przykładu, bardzo podobnego do poprzedniego:

#include <iostream>
#include <cstring>

using namespace std;
//1
int typProduktu=78;

int main()
{  
  //2
  int rodzaj=10;
  cout <<"Zmienna ma wartosc: "<<rodzaj<<endl;
  cout <<"Natomiast zmienna GLOBALNA ma wartosc "<<typProduktu<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 25.3

W zasadzie w tym programie nie powinno Cię nic zaskoczyć. Chciałbym jednak zwrócić Twoją uwagę na pewną kwestię - w zasięgu nr 2 bez problemu wypisujemy zmienną globalną, która jest zdefiniowana w zasięgu nr 1. Robimy to dokładnie tak, jakby zmienna ta była zdefiniowana w zasięgu nr 2.

Dzieje się tak dlatego, że zasięg nr 1 jest zasięgiem najbardziej ogólnym (tutaj akurat globalnym), natomiast zasięg nr 2 jest zasięgiem, który jest zagnieżdżony w zasięgu nr 1. To właśnie sprawia, że możemy się bez problemu dostać do zmiennych zdefiniowanych w zasięgu bardziej ogólnym.

Sytuacja taka jest również identyczna, gdy mamy więcej zagnieżdżonych zasięgów. W danym zasięgu mamy dostęp do wszystkich zmiennych zdefiniowanych we wszystkich "nadrzędnych" zasięgach. Oto przykład programu:

#include <iostream>
#include <cstring>

using namespace std;
//1
int typProduktu=78;

int main()
{  
  //2
  int rodzaj=10;
  cout <<"Zmienna ma wartosc: "<<rodzaj<<endl;
  cout <<"Natomiast zmienna GLOBALNA ma wartosc "<<typProduktu<<endl<<endl;
  {
    //3
    string napis="Przykladowy napis";
    cout <<"Zmienna napis to: "<<napis<<endl;
    cout <<"Zmienna z zasiegu nr 1 ma wartosc "<<rodzaj<<endl;
    cout <<"Zmienna GLOBALNA ma wartosc "<<typProduktu<<endl;
  }
  // 2
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 25.4

Jak zatem widzisz, zmienna z zasięgu nr 1 jest widoczna w zasięgu nr 2 i nr 3, a zmienna zdefiniowana w zasięgu nr 2 jest widoczna w zasięgu nr 3.

Prawdę mówiąc o tym wszystkim wspominałem już w poprzedniej lekcji, jednak chciałem Ci uświadomić to jeszcze raz, bowiem bez zrozumienia tego zagadnienia, nie uda Ci się zrozumieć dalszej części tej lekcji.

Mechanizm przesłaniania zmiennych

Czas przejść do tego, co ma stanowić główny punkt tej lekcji, a mianowicie do mechanizmu przesłaniania zmiennych. W C++ można zdefiniować zmienną o tej samej nazwie, co zmienna występująca w innym zasięgu. Zacznijmy od przykładu:

#include <iostream>
#include <cstring>

using namespace std;
//1
int typProduktu=78;

int main()
{  
  //2
  cout <<"Zmienna  ma wartosc "<<typProduktu<<endl;
  int typProduktu=10;
  cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  {
    //3
    ++typProduktu;
    cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
    string typProduktu="Pralka";
    cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  }
  // 2
  cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 25.5

Czas na wyjaśnienia. Przede wszystkim zwróć uwagę na to, że w każdym zasięgu deklarujemy zmienną o tej samej nazwie. Zwróć też uwagę, że typy zmiennej są różne (dwukrotnie int, raz string) - mogą być wszystkie takie same lub wszystkie różne i zupełnie dowolne.

Prześledźmy program od początku. W zasięgu globalnym nr 1 deklarujemy zmienną i inicjalizujemy ją wartością 78. Kiedy znajdujemy się w zasięgu nr 2, wypisujemy wartość zmiennej globalnej - to już wszystko wiesz.

Nowością jest to, że za moment deklarujemy zmienną o tej samej nazwie i inicjalizujemy ją wartością 10. Kiedy wypisujemy wartość zmiennej, zostaje wypisana wartość nowo zadeklarowanej zmiennej.

Teraz z kolei wchodzimy do zasięgu nr 3. Zmieniamy wartość zmiennej poprzez jej zwiększenie (mogłaby to być oczywiście każda inna operacja) i wypisujemy wartość zmiennej - w stosunku do ostatniej wartości zmienna się zwiększyła, czyli wszystko jest w porządku. Deklarujemy znów zmienną o tej samej nazwie, tym razem typu napisowego i po raz kolejny wypisujemy nową wartość zmiennej.

Ostatecznie po zakończeniu zasięgu nr 3, znajdujemy się w zasięgu nr 2 i wypisując wartość zmiennej widzimy tą wartość, która została ustalona w zasięgu nr 2, a później zmodyfikowana w zasięgu nr 3 (jak widać NIE jest to zmienna typu napisowego).

Mam nadzieję, że przykład za bardzo Cię nie przeraził, bowiem na własnej skórze udało Ci się przekonać, co znaczy przesłanianie zmiennych. Jeśli udało Ci się już zrozumieć jak to działa, to po raz kolejny moje gratulacje i uznanie.

Mimo że na pierwszy rzut oka przesłanianie zmiennych wygląda na skomplikowane, to w rzeczywistości takie nie jest. Otóż jeśli w danym zasięgu deklarujemy zmienną o takiej samej nazwie jak zmienna zadeklarowana w innym zasięgu, to nowa zmienna zasłania (przesłania) starą zmienną. Od tego momentu każde odwołanie do zmiennej o danej nazwie jest odwołaniem do nowo zdefiniowanej zmiennej.

Nie znaczy to jednak, że zmienna zadeklarowana wcześniej "znika" z naszego programu i jej wartość zostanie stracona, wręcz przeciwnie. Jak udało Ci się zauważyć w programie, odwołanie się do początkowej, przesłoniętej na jakiś czas wartości danej zmiennej jest możliwe.

Dostanie się do wartości zmiennej zdefiniowanej zanim zdefiniowaliśmy nową zmienną o tej samej nazwie staje się możliwe wtedy, kiedy nowa zmienna kończy swoje życie. Kiedy się tak dzieje doskonale już wiesz - wtedy gdy kończy się bieżący zasięg lokalny, czyli kiedy w programie znajduje się zamykający nawias klamrowy.

Mam nadzieję, że teraz udało Ci się już zrozumieć przedstawiony wcześniej przykład - to, że raz widzimy nową wartość zmiennej, a raz starą to tylko pozory. Tak naprawdę mamy 2 zmienne o tej samej nazwie, a to której zmiennej wartość widzimy, zależy od zasięgu, w którym się obecnie znajdujemy.

Po raz kolejny chcę zwrócić Twoją uwagę na to, że mechanizm przesłaniania działa wyłącznie między różnymi zasięgami - zmienne o takiej samej nazwie można definiować tylko i wyłącznie w różnych zasięgach. Próba definicji zmiennych o tej samej nazwie w jednym zasięgu zakończy się błędem.

Zalety i wady przesłaniania zmiennych

Skoro wiesz już na czym polega mechanizm przesłaniania zmiennych, warto zastanowić się nad skutkami i konsekwencjami stosowania tego mechanizmu.

Niewątpliwą zaletą mechanizmu przesłaniania zmiennych jest to, że nie musimy pamiętać, jakie zmienne zadeklarowaliśmy wcześniej. Nie musimy dzięki temu wymyślać bardzo skomplikowanych nazw zmiennych i martwić się, że znów otrzymamy informację, że taka zmienna już istnieje. Jeśli zmienna o takiej nazwie istnieje, zostanie po prostu na pewien fragment programu przesłonięta naszą nową zmienną i nic złego się nie stanie.

Mechanizm przesłaniania zmiennych ma też swoje wady. Jeśli w programie mamy powiedzmy kilkadziesiąt zasięgów i wszędzie zmienne nazywają się tak samo i są po kilka razy przesłonięte, to czy będzie Ci łatwo od razu powiedzieć, z którą tak naprawdę zmienną masz do czynienia? Gwarantuję, że w końcu nawet Ty jako twórca swojego programu się w tym pogubisz.

Inną wadą stosowania mechanizmu przesłaniania zmiennych jest to, że przez pewien czas nie mamy dostępu do zmiennej, która została przesłonięta (chyba, że jest zmienna globalna - o tym za moment). Oczywiście rozwiązanie jest proste - jeśli chcemy mieć dostęp do danej zmiennej, nie możemy jej po prostu przesłaniać i sprawa jest rozwiązana.

Podsumowując przesłanki do stosowania mechanizmu przesłaniania zmiennych, należy po prostu stosować sam mechanizm tylko wtedy, gdy rzeczywiście wiemy co robimy. Nadużywając tych samych nazw zmiennych dla kilku tak naprawdę różnych zmiennych, czyli nadużywając mechanizmu przesłaniania zmiennych sprawiamy, że program staje się coraz mniej czytelny nie tylko dla innych, ale również dla nas samych.

Dostęp do przesłoniętych zmiennych

Jak już wiesz, za pomocą mechanizmu przesłaniania zmiennych możemy przesłaniać zarówno zmienne globalne, jak i zmienne lokalne. Jeśli jakaś zmienna jest przesłonięta, to nie można się było do tej pory do niej dostać aż do momentu zakończenia zasięgu, w której została zdefiniowana zmienna o takiej samej nazwie.

Okazuje się jednak, że jest możliwe uzyskanie dostępu do przesłoniętych zmiennych, ale tylko globalnych. Inaczej mówiąc, jeśli przesłonimy jakąś zmienną globalną, można się i tak do niej odwołać w bieżącym zasięgu. Jednak jeśli przesłonimy zmienną lokalną, to odwołanie do niej jest niemożliwe, czyli tak jak to właśnie było przedstawiane do tej pory.

Dość łatwo zapamiętać, czemu możliwe jest odwołanie tylko do przesłoniętej zmiennej globalnej. Wynika to z tego, że do zmiennych globalnych odwołujemy się (wypisujemy, dokonujemy operacji) zawsze w jakimś innym zasięgu (zazwyczaj w zasięgu funkcji main). Zatem gdybyśmy w funkcji main zadeklarowali zmienną o tej samej nazwie, to do naszej zmiennej globalnej nie uzyskalibyśmy już nigdy dostępu, bo zasięg funkcji main trwa w zasadzie przez cały program (przynajmniej na tym etapie kursu).

Nie ma natomiast takiego problemu dla zmiennych lokalnych. Gdy przesłaniamy zmienną lokalną tracimy do niej dostęp, ale zawsze do niej dostęp odzyskamy po zakończeniu przesłaniania. Nie ma zatem potrzeby (w związku z tym również możliwości), aby odwoływać się do przesłoniętych zmiennych lokalnych.

Czas na konkrety, a mianowicie wyjaśnienie, w jaki sposób odwołać się do przesłoniętej zmiennej globalnej. Na szczęście jest to bardzo proste. Schematycznie wygląda to tak:

  ::nazwaZmiennej

A zatem wystarczy, że przed nazwą zmiennej postawimy dwa dwukropki (jest to operator zasięgu) i w ten oto sposób będziemy mogli odwołać się do tej zmiennej i dokonywać na niej dowolne operacje.

Oto prosty przykład przedstawiający zagadnienie:

#include <iostream>
#include <cstring>

using namespace std;
//1
int typProduktu=78;

int main()
{  
  //2
  cout <<"Zmienna  ma wartosc "<<typProduktu<<endl<<endl;
  int typProduktu=10;
  cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  cout <<"Zmienna GLOBALNA ma wartosc "<<::typProduktu<<endl<<endl;
  {
     //3
     ++typProduktu; // 10 + 1
     cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
     string typProduktu="Pralka";
     cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
     cout <<"Zmienna GLOBALNA ma wartosc "<<::typProduktu<<endl<<endl;
  }
  // 2
  cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  cout <<"Zmienna GLOBALNA ma wartosc "<<::typProduktu<<endl;
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 25.6

Wszystko dla Ciebie powinno być jasne, bowiem w programie dokonaliśmy niewielu zmian. Przede wszystkim zademonstrowałem Ci, że dostęp do zmiennej globalnej mamy rzeczywiście w każdym miejscu programu. Dodatkowo chcę jeszcze raz zwrócić Twoją uwagę, że w momencie gdy definiujemy zmienną w zasięgu nr 3, to nie możemy się już w żaden sposób odwołać do zmiennej zdefiniowanej w zasięgu nr 2, aż do momentu, gdy zasięg nr 3 się skończy, co jest zgodne z tym co pisałem, że nie można się odwołać do przesłoniętych zmiennych lokalnych.

Oto kolejny przykład:

#include <iostream>
#include <cstring>

using namespace std;
//1
int typProduktu=78;

int main()
{  
  //2
  cout <<"Zmienna GLOBALNA ma wartosc "<<::typProduktu<<endl<<endl;  
  int typProduktu=10;
  cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  ::typProduktu=typProduktu*20+5; // 10*20 + 5 = 205
  cout <<"Zmienna GLOBALNA ma wartosc "<<::typProduktu<<endl<<endl;
 
  {
     //3
     ++typProduktu; // 10 + 1 = 11
     ::typProduktu=::typProduktu+typProduktu; // 205 + 11
     cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
     string typProduktu="Pralka";
     cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
     cout <<"Zmienna GLOBALNA ma wartosc "<<::typProduktu<<endl<<endl;
  }
  // 2
  cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  cout <<"Zmienna GLOBALNA ma wartosc "<<::typProduktu<<endl;
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 25.7

Po raz kolejny nie ma w programie żadnych większych nowości. Chciałem Ci tylko zademonstrować, że można rzeczywiście dokonywać różnych operacji na zmiennych globalnych i zmiennych przesłanianych oraz, że można np. tak jak u nas zmiennej globalnej przypisać sumę zmiennej globalnej i zmiennej lokalnej.

Zwróć uwagę również na pierwsze wypisanie zmiennej globalnej w funkcji main. Użyliśmy tym razem (w stosunku do poprzedniego przykładu), operatora dostępu do zmiennej globalnej i okazuje się, że taki zapis jest poprawny mimo że w tym momencie zmienna globalna nie jest przesłonięta. W takim przypadku normalnie pomijamy operator zakresu, tak jak to było chociażby w poprzednim przykładzie.

Oto ostatni przykład, który przedstawię Ci w tej lekcji. Nie przepisuj jednak go do kompilatora jak to zawsze robisz, tylko postaraj się prześledzić dokładnie program i zastanowić się, co się w nim dzieje. Chcę Cię uświadomić w ten sposób, że programowania nie można się "uczyć na pamięć", tu trzeba rozumieć wszystko czym się posługuje - albo katastrofa gwarantowana.

#include <iostream>
#include <cstring>

using namespace std;
//1

int main()
{  
  //2
  int typProduktu=10;
  cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  {
     //3
     ++typProduktu; // 10 + 1 = 11
     cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
     string typProduktu="Pralka";
     cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
     cout <<"Zmienna GLOBALNA ma wartosc "<<::typProduktu<<endl<<endl;
  }
  // 2
  cout <<"Zmienna ma wartosc "<<typProduktu<<endl;
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;
}
program nr 25.8

Teraz możesz już przepisać przykład do kompilatora - jeśli nadal nie wiesz, co się w przykładzie dzieje, to pomyśl jeszcze raz.

Jeśli powyższy przykład spróbujesz skompilować, okaże się to niemożliwe, bowiem kompilator zgłosi błąd. Jeśli nadal nie rozumiesz czemu tak się stało, postaraj się jeszcze raz przeczytać całą lekcję.

A teraz czas na wyjaśnienie - operator zasięgu :: służy do dostępu do zmiennej globalnej, zazwyczaj przesłoniętej, chociaż jak w poprzednim przykładzie pokazałem, nie tylko. Spójrz zatem tutaj - do której zmiennej globalnej mamy się odwołać? Pomyślisz - oczywiście do tej, co znajduje się po operatorze zasięgu, czyli typProduktu. No dobrze - a czy w naszym programie mamy rzeczywiście zdefiniowaną zmienną globalną o tej nazwie? Jeśli teraz spojrzysz, wiesz już, że nie, zatem jak widać kompilator "miał powód", aby zgłosić błąd.

W ten sposób mam nadzieję nauczyłem Cię, że nawet niewielka zmiana w programie może powodować błędy, które początkowo są niezrozumiałe. Dlatego też jeśli używa się danego mechanizmu (np. przesłaniania zmiennych), trzeba mieć pewność, że się go dobrze rozumie, bowiem w innym przypadku tak naprawdę mogą się pojawić trudności, które dla dużych i skomplikowanych projektów okażą się w zasadzie nie do przezwyciężenia.

Podsumowanie

W tej lekcji przedstawiłem Ci mechanizm przesłaniania zmiennych - coś co jest niewątpliwie użyteczne, jednak nieumiejętnie stosowane powoduje więcej szkód niż korzyści.

Pamiętaj, że nawet doskonale rozumiejąc mechanizm przesłaniania zmiennych, należy raczej unikać nadawania zmiennym takich samych nazw dla naszej własnej wygody i łatwości zrozumienia kodu programu.

powrót