Operacje arytmetyczne mają w przypadku programowania takie same zasady jak wyuczone na matematyce. Kolejność działań matematycznych zawsze odbywa się według tego samego schematu z uwzględnieniem priorytetów operatorów +, –, * i / (dzielenie).
Wyrażenie matematyczne zawsze obliczane jest od lewej do prawej, czyli np. takie wyrażenie:
37 – 9 * 3 + 12 / 4
ma wartość: 13 => 37 – 27 + 3. Jeśli chcemy zmienić kolejność wykonywania obliczeń musimy użyć grupowania za pomocą nawiasów ( ).
Ciekawostką i zarazem przyjętą konwencją jest w programowaniu zapis równania, które dla:
a = a + 23;
oznacza, że (nowa) wartość przypisana zmiennej a, to jej (stara) dotychczasowa wartość a zwiększona o 23.
Co oznacza, że matematycznie poniższy zapis nie ma sensu (bo jak 4 ma się równać 11):
a=4;
a=a+7;
ale w programowaniu jak najbardziej. Musimy pamiętać, że po lewej stronie znaku “=” równania znajduje się zmienna do której przypisujemy nową obliczoną wartość w wyniku rozwinięcia tego co znajduje się po prawej stronie znaku “=” równania (nigdy nie inaczej!).
Do stworzenia równania używamy więc operatora przypisania =. W notacji języka C/C++ jest możliwe także używanie przypisań wielokrotnych, wyglądających następująco:
a = b = c = d;
Należy jednak pamiętać, że taki zapis może stanowić źródło błędów, przy braku należytego zrozumienia mechanizmu przypisania. Takie przypisanie należy rozpoznawać od prawej i sekwencyjnie dążyć do jego rozwinięcia aż do osiągnięcia strony lewej, co oznacza:
c = d;
b = c;
a = b;
Błędem jest próba deklaracji zmiennych w następujący sposób:
int a=b, b=c, c=10; czy
int a=b=c=10;
ale poprawnie będzie:
int a, b, c=10;
a=b=c;
Innymi operatorami arytmetycznymi są operatory inkrementacji ++ (dwa plusy) i dekrementacji – – (dwa minusy). Oznaczają one po prostu zwiększenie wartości danej zmiennej o 1 (++), lub zmniejszenie o 1 (– –). Tak więc zapisy poniższe są jednoznaczne:
i = i + 1; <=> i++; y=y-1; <=> y--;
Dodatkową kwestią tutaj jest zagadnienie post- i pre-inkrementacji (dekrementacji), które w zapisie wygląda tak:
pre-inkrementacja: ++i; pre-dekrementacja: --y; post-inkrementacja: i++; post-dekrementacja: y--;
Różnica polega na tym w którym momencie zostanie wykonane działanie, czy przed wykorzystaniem zmiennej, czy po wykorzystaniu zmiennej w danej operacji. Wynika z tego, że możemy dokonywać blokowania działań, a środowisko poradzi sobie z rozwinięciem ich i wykonaniem w odpowiedniej kolejności, tak więc przeprowadźmy eksperyment z uruchomieniem poniższego kodu programu:
int a=10, b=11; cout << "Wartość a wynosi: " << a++ << ", a wartość b wynosi: " << --b << endl; cout << "Wartość a wynosi: " << a++ << ", a wartość b wynosi: " << --b << endl; cout << "Wartość a wynosi: " << a++ << ", a wartość b wynosi: " << --b << endl; cout << "Wartość a wynosi: " << a++ << ", a wartość b wynosi: " << --b << endl;
Zastanów się jakie uzyskasz wyniki.
Jeśli już wiesz, to tutaj możesz podejrzeć efekt działania tego programu aby sprawdzić siebie.
W notacji C/C++ można zobaczyć również skracanie zapisu operacji matematycznych, które są wygodne dla programisty, jednakże mogą sprawiać trudność początkującym w rozszyfrowaniu zapisu. Tak więc można wyróżnić:
a+=10; jest jednoznaczne zapisowi: a=a+10;
a-=10; jest jednoznaczne zapisowi: a=a-10;
a*=10; jest jednoznaczne zapisowi: a=a*10;
a/=10; jest jednoznaczne zapisowi: a=a/10;
Przy operacjach dzielenia pamiętajmy o typach zmiennych, bowiem w przypadku poniższego kodu uzyskamy ciekawe i znaczące różnice w efekcie obliczonych wartości (sprawdź też sam!):
int a=10; float b=10; a/=3; b/=3; cout << "Obliczona wartość a to: " << a << endl; cout << "Obliczona wartość b to: " << b << endl;
Inne operacje arytmetyczne są już bardziej rozbudowane i są to np:
Reszta z dzielenia (modulo) jest operacją,
int a=10; a= a % 3; //lub a%=3;
gdzie wynikiem jest liczba całkowita, która pozostała po dzieleniu całkowitym danej liczby przez dzielnik. W wymienionym przykładzie w wartości zmiennej a=10, liczba 3 mieści się całkowicie kilka razy (3), a 10-(3*3)=1, czyli reszta z dzielenia to wartość 1. Należy pamiętać, że operację modulo można stosować tylko na liczbach całkowitych!
Dodatkową, ale ciekawą i przydatną operacją arytmetyczną jest div należąca do biblioteki cstdlib. Pomimo, iż wykracza ona nieco poza poznany materiał jest rzeczą pożyteczną poznać ją i umieć stosować. Operacja ta jest operacją złożoną, w wyniku zwracającą dwie informacje jako parę liczb:
– quot: czyli, ile razy dany dzielnik mieści się całkowicie w dzielnej
– rem: czyli, jaka reszta całkowita została po tym dzieleniu.
Przykład: Mamy dwie liczby 23 i 7, chcemy sprawdzić ile razy 7 mieści się w 23 i jaka reszta zostanie po całkowitym dzieleniu:
7 – mieści się w 23 całkowicie…
7 *2, czyli 14 mieści się w 23 całkowicie…
7 *3, czyli 21 mieści się w 23 całkowicie…
7 *4, czyli 28, nie mieści się już w liczbie 23 całkowicie…
a więc, największym całkowitym mnożnikiem liczby 7, tak aby taka liczba zmieściła się w liczbie 23 jest liczba 3, bowiem:
7 *3 = 21
co znaczy, że z liczby 23 pozostała reszta, w której nie “mieści się” już dzielnik 7:
23 – 21 = 2
Wynikiem działania div jest więc para liczb: 3 (wielokrotność całkowita) oraz 2 (reszta).
W środowisku programistycznym możemy zapisać taki program następująco:
#include<cstdio> // printf() #include<cstdlib> // albo po prostu: #include<iostream>, która już zawiera w sobie potrzebną bibliotekę oraz obsługę strumieni z przestrzeni nazw std int main() { div_t divWynik; //powołanie do życia zmiennej typu div_t divWynik = div(23,7); //użycie div printf ("23 div 7 => %d, pozostało reszty: %d.\n", divWynik.quot, divWynik.rem); return 0; }
Ta operacja pozwala na sprawdzenie czy następuje dzielenie całkowite (bez reszty, gdzie rem będzie równe 0) jakiejś liczby. Podobnie możemy to sprawdzić w sumie modulo (%), gdzie wynik równy 0 jest potwierdzeniem dzielenia bez reszty.
Binarne przekształcenia bitowe są dodatkową grupą operacji. W odróżnieniu od operacji arytmetycznych bazujących na liczbach, operacje binarne bazują na bitowym zapisie wartości w komórkach pamięci przypisanych zapamiętaniu danej zmiennej. Tak więc, możemy wyróżnić następujące grupy przekształceń bitowych:
- negacja (not)
- alternatywa (or/lub)
- koniunkcja (and/i)
- różnica symetryczna (xor/albo)
- przesunięcie bitowe
Negacja (not) zajmuje się odwróceniem binarnej zawartości danej komórki pamięci poprzez zastąpienie 1 => 0, a 0 => 1. Tablica przejść dla negacji wygląda następująco:
Bit | Wynik |
0 | 1 |
1 | 0 |
Proponuję przeprowadzić poniższy eksperyment.
short a=10; a = ~a; cout << a;
W rezultacie otrzymaliśmy wartość -11. Aby jednak zrozumieć skąd się to wzięło, musimy zrozumieć jak zapisana jest dana wartość liczbowa w komórce pamięci i jak jest ta wartość z owej komórki odtwarzana. Najłatwiej będzie to zrozumieć wyobrażając sobie taką komórkę pamięci jako pewien rejestr o określonej pojemności. Tak więc typ short potrafi przechować w sobie wartości liczb całkowitych od -32768 do 32767 (w tym 0). Razem daje to 65536, czyli 216 różnych wartości, a więc potrzeba dwu bajtów, każdy po 8 bitów aby takie wartości zapamiętać. Liczby dodatnie reprezentowane są poprzez zapisanie w dwu-bajtowej strukturze pamięci liczby binarnej o wartości od 0 do 32767, ujemne reprezentowane są przez wartości powyżej 32767 aż do pełnej pojemności takiego rejestru, czyli wartości 65535. Komputer odczytując zapis binarny dokonuje automatycznej konwersji do oczekiwanego typu i uzyskujemy oczekiwaną wartość np. -10, czy 1786.
W przypadku negacji – wszystkie bity w rejestrze zostają “odwrócone” – co powoduje, że w rejestrze jest teraz zapisana zupełnie inna wartość binarna, a co za tym idzie również inna wartość liczbowa odniesiona do zdefiniowanego typu. Powoduje to, że w naszym przypadku odwrócenia wartości 10 zmiennej typu short, która w dwu-bajtowej komórce pamięci binarnie wygląda tak:
00000000 00001010
po jej zanegowaniu ma następującą postać:
11111111 11110101
a po jej konwersji do zapisu dziesiętnego otrzymujemy wartość 65525, która po zinterpretowaniu do typu short (ze znakiem!) 65525-65536=-11.
Należy także pamiętać, że rejestr jest jak gdyby zapętlony, co znaczy, że w przypadku przekroczenia maksymalnej wartości jaką może on w sobie przechować następuje wyzerowanie rejestru i rozpoczęcie od 0. Tak samo w sytuacji odwrotnej, gdy spróbujemy zmniejszyć zawartość rejestru poniżej 0, to nastąpi jego maksymalne wypełnienie do wartości maksymalnej jaką możemy w nim zapisać. Nie jest to błąd, a przyjęta konwencja działania takich rejestrów na których bazują zapisane w nich liczby naszych zmiennych.
Alternatywa (or/lub) jest sumą logiczną bitów w rejestrze pamięci.
Tablica przejść dla alternatywy wygląda następująco:
Bit 1 | Bit 2 | Wynik |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
Zapis Alternatywy w przypadku programu wygląda następująco:
short a=5; a = a | 2; // operacja binarna na wartości zmiennej d, poprzez nałożenie maski 01000000 00000000 (wartości 2 w zapisie bitowym) // wynik = 7 gdyż: // 00000000 00000101 (5) // or // 00000000 00000010 (2) // = // 00000000 00000111 (7) a = 5; a = a | 4; // wynik = 5 // 00000000 00000101 (5) // or // 00000000 00000100 (4) // = // 00000000 00000101 (5)
Koniunkcja (and/i) jest logicznym “i” na na odpowiadających sobie bitach dwu (lub więcej) rejestrów. Tablica przejść dla koniunkcji wygląda następująco:
Bit 1 | Bit 2 | Wynik |
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
Zapis w programie wygląda tak:
short a=5; a = a & 2; // operacja binarna na wartości zmiennej d, poprzez nałożenie maski 01000000 00000000 (wartości 2 w zapisie bitowym) // wynik = 0 gdyż: // 00000000 00000101 (5) // and // 00000000 00000010 (2) // = // 00000000 00000000 (0) a = 5; a = a & 4; // wynik = 4 // 00000000 00000101 (5) // and // 00000000 00000100 (4) // = // 00000000 00000100 (4)
Różnica symetryczna (xor), czyli suma modulo 2 jest operacją bitową “albo” polegającą na “ustawieniu” w wyniku “1” tylko gdy argumenty są sobie różne. Tabela przejść wygląda następująco.
Bit 1 | Bit 2 | Wynik |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Zapis w programie wygląda tak:
short a=5; a = a ^ 2; // operacja binarna na wartości zmiennej d, poprzez nałożenie maski 01000000 00000000 (wartości 2 w zapisie bitowym) // wynik = 7 gdyż: // 00000000 00000101 (5) // xor // 00000000 00000010 (2) // = // 00000000 00000111 (7) a = 5; a = a ^ 4; // wynik = 4 // 00000000 00000101 (5) // xor // 00000000 00000100 (4) // = // 00000000 00000001 (1)
UWAGA: W języku C++ istnieją dwie formy operatorów:
- logiczne (ang. logical) !, || i &&, które traktują swoje argumenty jako wyrażenia logiczne przyjmujące wartości true lub false. Wynikiem jest zawsze wartość logiczna true lub false.
- bitowe (ang. bitwise) ~, |, & i ^, które traktują swoje argumenty jako rejestry binarne i przeprowadzają operację logiczną na odpowiadających sobie bitach w tych rejestrach. Wynikiem jest zawsze rejestr binarny z bitami ustawionymi zgodnie z definicją danej operacji logicznej.
Operacje przesunięcia są ostatnimi z grupy operacji bitowych. Skoro już wiemy, że komórki pamięci dla danego typu zmiennej zachowują się jak rejestry to łatwo także zrozumiemy zasadę przesunięć binarnych. Realizowane są one w sposób, że każdy z bitów w danym rejestrze zostaje przepisany w lewo lub w prawo na miejsce sąsiada. Bity będące “na skraju” rejestru w przypadku przepisania ich na pozycję będącą już poza rejestrem ulegają utracie, a w przypadku pojawiającego się miejsca jest ono uzupełniane 0.
Przesunięcie bitów w prawo o 1 pozycję jest równoważne dzieleniu przez dwa. Przesunięcie o n pozycji w prawo jest równoważne dzieleniu przez 2n. Operacje przesuwu są bardzo szybkie – o wiele szybsze od dzielenia. Jeśli nasz algorytm dużo dzieli przez 2 (lub potęgę 2), to warto się zastanowić, czy zamiast dzielenia, nie byłaby lepsza operacja przesuwu bitów w prawo. Operacja ta jest jednak ograniczona tylko do liczb całkowitych, natomiast operacja dzielenia może operować na dowolnych typach liczbowych. Operacja przesuwu bitów w lewo jest równoważna mnożeniu przez 2. Przesunięcie bitów w lewo o n pozycji odpowiada pomnożeniu przez 2n. Przesunięcie bitów jest o wiele szybsze od mnożenia.
W programie zapisujemy to następująco:
int a=10; a=a<<2; // wynik = 40, bo 10*2*2. a=a>>3; //wynik = 5, bo 40/2/2/2.
W wyżej wymienionych operacjach możemy także z powodzeniem korzystać z ich skróconej notacji, a więc:
podstawowa | skrócona |
a=a|b; | a|=b; |
a=a&b; | a&=b; |
a=a^b; | a^=b; |
a=a>>n; | a>>=n; |
a=a<<n; | a <<=n; |
Należy pamiętać, że tak samo jak w przypadku notacji wzorowanej na C, czy na C++ powinno się stosować konsekwentnie ten sam sposób zapisu operacji i nie mieszać ich ze sobą raz używając notacji podstawowej, a raz skróconej – wpływa to na czytelność kodu i świadczy o programiście.
Zadania do samodzielnego wykonania: Przemyśl temat i spróbuj zastosować operacje arytmetyczne i bitowe we własnych programach.
Zadanie 1: Napisz program, który oblicza następujące równanie:
a = ( 2 * b + 173 – 10 * c ) / ( d / 4 – 3 * e )
Zadanie 2: Po wykonaniu zadania pomyśl, czy możesz zoptymalizować program z powyższego zadania poprzez zastosowanie operacji bitowych.
Zadanie 3: Napisz program, który sprawdza ile razy wartość zapisana pod zmienną Liczba2 mieści się w wartości zapisanej pod zmienną Liczba1. Wyświetl wynik i podaj iloczyn tych liczb wskazujący na największą możliwą wartość, która się mieści w badanej wartości ze zmiennej Liczba1, oraz podaj resztę, która pozostała.
Wymienione wyżej operacje arytmetyczne i binarne nie są jedynymi dostępnymi dla użytkownika. Gorąco zachęcam, aby zapoznać się z opisem bibliotek cstdlib (stdlib.h) i cmath (math.h), bowiem w nich znajdują się funkcje niezbędne w większości bardziej zaawansowanych ewaluacji.