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
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).
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.
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 x i y. Inne przykłady:
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 = 3W 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 j i k 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 i i j. 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, 21 lutego 2016; 20:17