7.5 L-wartości

Każda dana w programie musi oczywiście być gdzieś w pamięci komputera zapisana. Nie musi to jednak być segment pamięci przeznaczony na dane o dostępnych programowo adresach: mogą to być rejestry procesora lub specjalne obszary pamięci, gdzie tymczasowe zmienne są dynamicznie tworzone i usuwane.

Na przykład w takim fragmencie

       int x, y = 1;
       // ...
       x = y + 1;
wynik dodawania ' y+1', o wartości 2, niewątpliwie musi być gdzieś zapisany: pewnie ta wartość zostanie wpisana do zmiennej x po obliczeniu bezpośrednio z rejestru procesora. Tak czy owak, nie mamy na to ani szczególnego wpływu, ani dostępu do tej danej — możemy ją tylko natychmiast po obliczniu gdzieś zapisać lub użyć do innych operacji. Co ważniejsze, po wykonaniu przypisania ta obliczona wartość najprawdopodobniej zostanie za chwilę zamazana, bo była tymczasowa i służyła wyłącznie do tego, aby można było przekopiować ją do zmiennej x. Zauważmy, że tego typu wartości mogą się pojawiać tylko jako wartości wyrażenia po prawej stronie przypisania.

Pamiętamy, że

w instrukcji przypisania prawa strona przypisania mówi co policzyć, lewa zaś — gdzie zapisać wynik.

Zatem po lewej stronie przypisania mogą pojawiać się tylko wyrażenia identyfikujące dane o ustalonym i dostępnym adresie: ich dotychczasowa wartość nie ma znaczenia, bo właśnie w wyniku przypisania zostanie za chwilę zmieniona!

Prowadzi nas to do pojęcia l-wartości (ang. lvalue, l-value, od left: lewy) i p-wartości (ang. rvalue, r-value, od right: prawy).

L-wartość to wyrażenie identyfikujące daną o określonym type, która ma określony, dostępny w programie adres w pamięci.

Zatem np. nazwa zmiennej ustalonej (const) jest l-wartością, choć niemodyfikowalną. Identyfikator „zwykłej”, nieustalonej zmiennej jest l-wartością modyfikowalną. Generalnie zatem, jeśli wyrażenie występuje po lewej stronie przypisania, to musi być l-wartością; stwierdzenie odwrotne nie jest prawdziwe.

P-wartość to wyrażenie, które ma określoną wartość.

Zauważmy, że l-wartość jest też p-wartością, choć odwrotnie nie musi to być prawda: p-wartości stanowią znacznie szerszą klasę wyrażeń. Na przykład takie wyrażenie jak ' x+2' ma wartość, więc jest p-wartością (jeśli x jest zdefiniowane); wyrażenie to jednak nie jest l-wartością z powodów, o których mówiliśmy.

Wymieńmy zatem wyrażenia, które mogą być l-wartościami (znaczenie niektórych wyjaśnimy w następnych rozdziałach):

Na przykład w ostatniej linii

       int x = 2, y = x + 1;
       // ...
       int j = x = y;
wyrażenie x=y jest l-wartością (i p-wartością) o wartości równej wartości x po przypisaniu. Skoro jest l-wartością, to może też wystąpić po lewej stronie przypisania:
       (x=y-2) = 5;
Wyrażenie (x=y-2) ma wartość x po przypisaniu (czyli 1) i adres tejże zmiennej x — zatem przypisanie wartości 5 nastąpi właśnie do zmiennej x (wartość 1 zostanie zamazana).

Po następującej instrukcji natomiast

       int *p = &( x>y ? x : y);
wskaźnik p będzie zawierał adres większej ze zmiennych xy. Inne przykłady:


P42: lval.cpp     L-wartości

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int& razydwa(int& m) {
      5.      static int licz;
      6.      cout << " W funkcji: licz = " << licz << endl;
      7.      m *= 2;
      8.      return licz;
      9.  }
     10.  
     11.  void printTab(int *tab, int size) {
     12.      cout << "[ ";
     13.      for (int i = 0; i < size; i++)
     14.          cout << tab[i] << " ";
     15.      cout << "]" << endl;
     16.  }
     17.  
     18.  int main() {
     19.      int i = 1, j = 2, k = 3;
     20.  
     21.      // przypisanie jako l-nazwa
     22.      (i=j) = k;
     23.      cout << " A: i = " << i << " j = " << j
     24.           <<    " k = " << k << endl;           // 3,2,3
     25.  
     26.      int tab[] = {1,2,3,4}, *p = tab;
     27.      cout << " B: przed "; printTab(tab,4);
     28.      *++++++p = 8;
     29.      cout << " B:    po "; printTab(tab,4);
     30.  
     31.      // teraz p wskazuje na ostatni element!
     32.      cout << " C: ++*----p = " << ++*----p << endl; // 3
     33.      cout << " C:   tab "; printTab(tab,4);
     34.  
     35.      // konwersja jako l-nazwa
     36.      int m = 7;
     37.      razydwa( (int&)m=8 )++;  // konwersja niepotrzebna!
     38.      cout << "D1: m = " << m << endl;
     39.      razydwa(m)++;
     40.      cout << "D2: m = " << m << endl;
     41.      int n = razydwa(m) = 10;
     42.      cout << "D3: m = " << m << endl;
     43.      cout << "D4: n = " << n << endl;
     44.  
     45.      // przecinek
     46.      k = (i=1, j=2) + 1;
     47.      cout << " E: i = " << i << " j = " << j
     48.           <<    " k = " << k << endl;          // 1,2,3
     49.  
     50.      // selektor jako l-nazwa
     51.      (k > 2 ? i : j) = 5;
     52.      cout << " F: i = " << i << " j = " << j
     53.           <<    " k = " << k << endl;          // 5,2,3
     54.  }

Wynik tego programu to:

     A: i = 3 j = 2 k = 3
     B: przed [ 1 2 3 4 ]
     B:    po [ 1 2 3 8 ]
     C: ++*----p = 3
     C:   tab [ 1 3 3 8 ]
     W funkcji: licz = 0
    D1: m = 16
     W funkcji: licz = 1
    D2: m = 32
     W funkcji: licz = 2
    D3: m = 64
    D4: n = 10
     E: i = 1 j = 2 k = 3
     F: i = 5 j = 2 k = 3
W linii 22 przypisanie (i=j) pełni rolę l-wartości. Odpowiednim adresem jest adres zmiennej i, zatem to do tej zmiennej przypisana zostanie wartość k. Natomiast wartość zmiennych jk pozostanie niezmieniona. Zauważmy, że jest to co innego niż ' i=j=k'. Ta instrukcja byłaby równoważna ' i=(j=k)' i zmieniłaby wartości zarówno zmiennej  i jak i zmiennej  j.

W linii 26 definiujemy tablicę czteroelementową i jej adres (wartość zmiennej tab) przypisujemy do wskaźnika p. Spójrzmy na nieco dziwną konstrukcję *++++++p z linii 28. Najpierw trzy razy zwiększamy wartość p. Ponieważ zmienna p jest wskaźnikowa, więc jej zwiększenie oznacza to samo co ((p+1)+1)+1 i sprowadza się do zmiany wartości p — najpierw p wskazywała na tab[0], po tej operacji wskazuje na tab[3], czyli na ostatni element tablicy. Wyłuskanie (dereferencja) tej zmiennej za pomocą operatora wyłuskania wartości (gwiazdka) daje nam l-warość zmiennej tab[3] i to do tej zmiennej przypisujemy wartość 8, o czym świadczy druga z linii wydruku oznaczonych literą 'B'. Efektem ubocznym jest fakt, że teraz p wskazuje na tab[3].

Podobna konstrukcja użyta została w linii 32. Najpierw dwa razy zmniejszamy wartość zmiennej p — ponieważ wskazywała ona na ostatni, czwarty element tablicy tab, to po tej operacji wskazuje na element drugi, czyli na tab[1]. Teraz dokonujemy dereferencji: wyrażenie *--p jest zatem nazwą drugiego elementu tablicy. Wartość tego elementu (który jest typu int, a nie typu wskaźnikowego!) zwiększamy o jeden za pomocą operatora zwiększania. Zatem drugi element tablicy zwiększy się o jeden, co widać z linii wydruku oznaczonych literą 'C'. Efektem ubocznym będzie to, że teraz wskaźnik p wskazuje na ten właśnie, drugi element tablicy.

W linii 37 w argumencie wywołania funkcji razydwa dokonujemy jawnej konwersji wartości przypisania m=8, a jest nią l-warość zmiennej m, do typu int&, bo taki jest typ argumentu funkcji. Ta konwersja nie jest tu wymagana, gdyż należy do konwersji standardowych wykonywanych w razie potrzeby automatycznie przez kompilator.

Ponieważ do funkcji posłaliśmy referencję do zmiennej m, operacje, jakie ta funkcja wykonuje na odebranej poprzez argument zmiennej, wykonywane są na oryginale: z wydruku widzimy, że w funkcji main wartość m zmienia się po każdym wywołaniu funkcji (linie wydruku oznaczone literą 'D').

Jak widać z definicji funkcji razydwa, zwraca ona referencję do lokalnej zmiennej statycznej licz. Zmienna licz jest co prawda lokalna, ale istnieje po powrocie z funkcji, ponieważ jest statyczna. Zwracana jest referencja do tej właśnie zmiennej, zatem razydwa(m) jest inną nazwą zmiennej licz. Dlatego jest l-wartością i w funkcji main możemy zmienić wartość licz, np. poprzez zastosowanie operatora zwiększenia (linie 37 i 39). Może też pojawić się po lewej stronie przypisania, jak w linii 41.

W linii 46 wyrażenie ' (i=1, j=2)' jest l-wartością zmiennej j po przypisaniu j=2, zatem do jej wartości (równej 2) dodana zostanie jedynka i wynik przypisany do k.

W linii 51 selektor (k>2?i:j) jest l-wartością, bo są l-wartościami dwa ostatnie jego argumenty ij. W naszym przykładzie, ponieważ warunek k>2 jest spełniony, całe wyrażenie (k>2?i:j) jest l-wartością zmiennej i i to do tej zmiennej nastąpi przypisanie wartości 5.

T.R. Werner, 25 lutego 2017; 22:31