3. Definicje zmiennych i ich zasięg

3.3. Modyfikatory deklaracji zmiennych i stałych

Umieszczaniem zmiennych w pamięci oraz ich zachowaniem można w pewien ograniczony sposób sterować. Odpowiednie właściwości uzyskujemy przez modyfikatory podawane w momencie definicji zmiennej:

  • register – oznacza, że zmienna ma być trzymana w rejestrze procesora, a nie w pamięci. Co prawda znów mamy tu do czynienia z wyrażeniem woli programisty – ostateczna decyzja zostanie podjęta przez kompilator, ale przynajmniej przekazujemy mu wskazówki. Przy czym wskazówka zostanie odrzucona, jeśli … choć raz spróbujemy uzyskać wskaźnik (adres) takiej zmiennej.
  • const – oznacza, że obiekt jest stały – nie będzie można zmieniać jego wartości. Konsekwencją tego jest obowiązek zainicjowania go (podania wartości początkowej, której nie będzie można zmienić).
  • volatile – oznacza, że nie ma się wyłączności do podanego obiektu (tzn. może być to rejestr sprzętowy komputera albo zmienna używana przez inny wątek). W praktyce takie zmienne kompilator pomija w procesie optymalizacji dostępu – każdy odczyt wartości zmiennej musi się wiązać z jej ponownym pobraniem z pamięci operacyjnej.
  • static – w ogólności oznacza, że obiekt taki istnieje przez cały czas, niezależnie od zasięgu, który go używa (zabrania się w ten sposób kasowania zmiennych lokalnych). Jeśli static zastosujemy do zmiennej lokalnej w funkcji - to każde nowe wywołanie funkcji będzie miało dostęp do jej wartości z poprzednich wywołań tej funkcji. Dla zmiennych które i tak już istnieją cały czas (zmiennych globalnych) oznacza z kolei zniesienie zewnętrznego symbolu obiektu (tzn. poza bieżącą jednostką kompilacji, czyli plikiem, nic nie może z tego korzystać). Trochę niemiłe zachowanie – bo mamy rozszerzenie dostępu do zmiennych lokalnych, i zawężenie dla zmiennych globalnych. W przypadku funkcji static ma podobne znaczenie jak w przypadku stałych - przekształca funkcję w funkcję lokalną, bez możliwości korzystania z niej poza danym modułem / jednostką kompilacji. 
  • extern – o nim więcej za chwilę.

Modyfikatory static i extern wzajemnie się wykluczają, zwłaszcza, że oznaczają dwie całkiem przeciwne właściwości. 

Przyjrzyjmy się dokładniej zmiennym statycznym. Taką zmienną można zainicjalizować, jednak jest to inicjalizacja podobna do inicjalizacji zmiennej globalnej - wykonuje się tylko raz. Jeśli tego nie zrobimy, przypisana jej będzie wartość zerowa. W poniższym przykładzie wykorzystaliśmy zmienną statyczną do zliczania i wyświetlania liczby wywołań funkcji:


#include <iostream>
using namespace std;

void mojaFunkcja(int a, int b) {
  // zmienna statyczna zostanie zainicjowana raz i tylko raz
  // przed uruchomieniem programu, niezależnie od wywołań
  // funkcji
  static int liczbaWolan = 0;
  cout << "Wywolan: " << ++liczbaWolan << " argumenty: " << a << ", " << "b\n";
}

int main() {
  mojaFunkcja(1, 2);
  mojaFunkcja(3, 4);
  mojaFunkcja(5, 6);
  mojaFunkcja(7, 8);

  return 0;
}

Słowo static ma także specjalne znaczenie w odniesieniu do struktur – będzie omówione później.

W przypadku słowa kluczowego extern również możemy się spodziewać dwóch znaczeń:

  1. jeśli poprzedza deklarację zmiennej (globalnej lub lokalnej) lub stałej, ale nie zainicjalizowanej, oznacza to typową deklarację. Można spotkać również funkcje poprzedzone tym modyfikatorem, ale nie ma on wtedy żadnego znaczenia.
  2. jeśli poprzedza deklarację stałej zainicjalizowanej, oznacza to, że taka stała ma być również eksportowana do innych plików. Z kolei extern przed stałą niezainicjalizowaną oznacza jej import na etapie łączenia (linkowania) programu.

Tu ważna uwaga nt. różnicy traktowania stałych przez C i C++: domyślnie zmienne globalne w C++ są w pamięci globalnej, co oznacza możliwość dostępu do nich z innych plików (jednostek kompilacji). W C taka sama sytuacja dotyczy stałych, natomiast w C++ stałe domyślnie są dostępne tylko w danym pliku (tak jakby były zadeklarowane jednocześnie z modyfikatorem static).

Dodatkowe znaczenie extern ma w wyrażeniu extern "C" lub extern "C++". Wyrażenie takie jest jawnym wymuszeniem języka kompilacji fragmentu kodu. Jest to konstrukcja C++, i zazwyczaj jest stosowana w dwóch przypadkach: eksportowania funkcji do bibliotek łączonych dynamicznie lub statycznie i wykorzystywanych w programach pisanych w innych językach, oraz do importu plików napisanych w C do wykorzystania w programach C++. 
Przykładowo:

extern "C" {
  #include <clib.h>
}