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 +, , */ (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
01
10

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 1Bit 2Wynik
000
011
101
111

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 1Bit 2Wynik
000
010
100
111

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 1Bit 2Wynik
000
011
101
110

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) !, ||&&, 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) ~, |, &^, 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:

podstawowaskró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)cmath (math.h), bowiem w nich znajdują się funkcje niezbędne w większości bardziej zaawansowanych ewaluacji.