Podstawy programowania 4

Operatory

Operatory przypisania

Najważniejszym rodzajem operatora jest operator przypisania =, który przypisuje wartość jednej zmiennej innej zmiennej. Wielokrotnie już to widzieliśmy. Wywołanie wygląda następująco:

lvalue = rvalue;

Najpierw wyliczana jest prawa strona, czyli rvalue, następnie ta wartość jest przypisywana obiektowi po lewej stronie, czyli lvalue (czasem po konwersji typu). Co prawda ciężko będzie tutaj wypisać czym może rvalue, gdyż reguły są skomplikowane, ale intuicyjnie może być to cokolwiek co ma wyliczalną wartość. Z kolei lvalue musi być czymś z czym związane jest miejsce w pamięci – inaczej nie można dokonać przypisania. Polskie nazwy to L-wartość i R-wartość.

Proszę spojrzeć na poniższe przykłady:

//Poprawne
int i = 0;
float f = 0.0;
i = 2+3;
i = i + 1
//Niepoprawne
//1 i 0.0 nie mają związanego miejsca w pamięci
//i nie są L-wartościami
1 = 2;
0.0 = true;
i + 1 = i;

Innymi operatorami przypisani są operatory pozwalające na skrótowy zapis instrukcji w rodzaju x = x operacja y. Są to:

  • +=
  • -=
  • *=
  • /=
  • %=
  • <<=
  • >>=
  • &=
  • ^=
  • |=

Operacje wykonywane przez te operatory powinny być jasne po przejrzeniu poniższych klas operatorów.

Operatory inkrementacji i dekrementacji

Bardzo częstymi operacjami, szczególnie na indeksach, są operacje:

i = i + 1;
i = i - 1;

Są one tak częste, że w języku C wprowadzono specjalny, skrótowy zapis tych instrukcji, co więcej każdej z nich na dwa sposoby. Dodawanie jedynki (inkrementację) zapisujemy przy pomocy operatora ++, a dekrementację przez --. Ponieważ o tych operatorach można myśleć jako o specjalnych operatorach przypisania, ich argumentami mogą być tylko L-wartości.

Każdy z tych operatorów istnieje w postaci przyrostkowej i przedrostkowej. Postać przyrostkowa jest wywoływana następująco:

i++;
i--;

Operator ten tworzy kopię obiektu i, zwiększa (zmniejsza) i o jeden i zwraca kopię (czyli oryginalną wartość). Postać przedrostkowa wywoływana jest następująco:

++i;
--i;

Operator ten najpierw zwiększa (zmniejsza) i o jeden i zwraca tak zmieniony obiekt.

Zadanie. Uruchomić poniższy program i zobaczyć działanie poznanych operatorów w praktyce.

#include <iostream>

using namespace std;

int main() {
  int i = 0;
  cout << i++ << endl;
  cout << i   << endl;

  int j = 0;
  cout << ++j << endl;
  cout << j   << endl;
  return 0;
}

Rozróżnienie postaci przedrostkowej i przyrostkowej może wydawać się nieistotne, jest jednak bardzo istotna różnica, gdy będziemy mieli do czynienia ze wskaźnikami. wyrażenie ++i jest L-wartością, podczas gdy i++ jest tylko R-wartością.

Operatory arytmetyczne

Operatory dodawania, odejmowania, mnożenia, dzielenia i reszty z dzielenia to odpowiednio +, -, *, /, %. Jak one działają nie powinno budzić większych wątpliwości, jednak należy szczególnie uważać na typy argumentów. Dla przykładu operator / dla typów stałoprzecinkowych jest operatorem dzielenia całkowitoliczbowego. Zwykłe dzielenie dostajemy, gdy jeden z argumentów ma typ zmiennoprzecinkowy. Dla przykładu:

float i = 10/3;   //Zmienna i ma wartość 3.0
float j = 10.0/3;
float k = 10/3.0; //Zmienne j, k mają wartość 3.333...

Operatory przesunięć bitowych

Operatory te to >>, <<. Dla liczby stałoprzecinkowej zapisanej w postaci binarnej dokonują one przesunięcia bitowego odpowiednio w prawo lub w lewo. Ewentualne bity przekraczające zakres zmiennej są tracone. Operator << odpowiada mniej więcej mnożeniu przez 2, operator >> dzieleniu przez dwa.

Operatory bitowe

Operatory ~, &, |, ^ działają tylko dla argumentów stałoprzecinkowych. Wykonują one kolejno: negację (operator jednoargumentowy), bitową operację AND, bitową operację OR i bitową operację XOR.

Operatory relacyjne

Operatory ==, !=, >, <, <=, >= dokonują porównań. Pierwsze dwa sprawdzają odpowiednio, czy argumenty są równe lub nie równe. Reszta powinna być jasna z ich zapisu. Wynikiem takiego operatora jest wartość typu bool.

Proszę zwrócić uwagę, że = to operator przypisania. Operator porównania to ==. Pomyłka jest czasem źródłem trudnych do wychwycenia błędów.

Operatory logiczne

Operatory logiczne pozwalają budować bardziej skomplikowane wyrażenia logiczne i warunki z prostszych. Operatorami tymi są !, &&, || . Kolejno są to negacja, koniunkcja i alternatywa. (można je też zapisywać jako odpowiednio not, and i or, ale jest to raczej rzadko stosowane. Operatory te będziemy często używać w warunkach pętli.

Negacja

Wyrażenie logiczne

! wyrazenie

ma wartośc logiczną true dokładnie wtedy, gdy wyrażenie składowe ma wartość logiczną false.

Koniunkcja

Wyrażenie logiczne

wyrazenie1 && wyrazenie2

ma wartośc logiczną true, dokładnie wtedy gdy oba wyrażenia składowe mają wartość logiczną true (false w pozostałych przypadkach).

Jeżeli pierwsze wyrażenie ma wartośc logiczną false, to wartość drugiego wyrażenia nie jest w ogóle wyliczana, gdyż nie ma ono wpływu na wartość całej koniunkcji.

Alternatywa

Wyrażenie logiczne

wyrazenie1 || wyrazenie2

ma wartośc logiczną true, dokładnie wtedy gdzy przynajmniej jedno wyrażenie składowe ma wartość logiczną true.

Jeżeli pierwsze wyrażenie ma wartośc logiczną true, to wartość drugiego wyrażenia nie jest w ogóle wyliczana, gdyż nie ma ono wpływu na wartość całej alternatywy.

Pozostałe operatory

Z biegiem czasu będziemy widywali także inne operatory, między innymi:

  • [] – operator indeksu w tablicy i w kontenerach
  • * – operator dereferencji wskaźnika
  • & – operator pobrania adresu
  • . – operator dostępu do składowej
  • -> – operator dostępu do składowej wskaźnika
  • new i delete – alokacja i dealokacja pamięci
  • sizeof – zwraca wielkość w bajtach wskazanego obiektu

Konwersja typów

Konwersja typów to szerokie zagadnienie, zwłaszcza w kontekście typów tworzonych przez programistów (czyli klas). W przypadku typów wbudowanych kompilator często wykona za nas konwersję typu, zupełnie automatycznie. Nazywa się to konwersją niejawną. Zasady są dość skomplikowane.

W C++ istnieją cztery operatory rzutowania:

Składnia wywołania jest następująca:

static_cast<nowy_typ>(wyrazenieInnegoTypu)

Na razie potrzebować czasami możemy jedynie static_cast, ale i tak w sytuacjach w których chcielibyśmy go używać z reguły wystarcza konwersja niejawna. Poza jednym przypadkiem – promocją typu stałoprzecinkowego do zmiennoprzecinkowego przy dzieleniu.

Zadanie. Uruchomić poniższy program i zobaczyć działanie poznanych operatorów w praktyce.

#include <iostream>

using namespace std;

int main() {
  int i = 10;
  int j = 3;
  cout << i/j << endl;
  cout << static_cast<double>(i)/j << endl;
  return 0;
}

Można też używać starej składni rzutowania z języka C: (nowy_typ) wyrazenieInnegoTypu, jednak jest to niepolecane. Trzeba dokładnie orientować się, które rzutowanie zostanie wykonane, poza tym rzutowania są miejscem potencjalnego występowania błędów i jako takie znacznie łatwiej szuka się je w kodzie, używając nowej składni.