Professional Documents
Culture Documents
Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima:
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Specifičnosti nepredznačne aritmetike
Akademska godina 2015/16
Mada standardi jezika C i C++ ne propisuju koliko se bita u memoriji rezervira za pamćenje
vrijednosti promjenljivih nekog cjelobrojnog tipa (što u suštini određuje opseg vrijednosti koje će se
moći zapamtiti u tim promjenljivim), većina današnjih kompajlera za C i C++ za klasične cjelobrojne
promjenljive (tj. promjenljive tipa “int”) rezervira 32 bita u memoriji. S obzirom da se od ta 32 bita
jedan rezervira za pamćenje znaka broja, za pamćenje apsolutne vrijednosti broja preostaje 31 bit. To u
praksi znači da najveća pozitivna vrijednost koja se može zapamtiti u promjenljivoj tipa “int” iznosi
2 − 1 = 2147483647, odnosno promjenljive tipa “int” (uz pretpostavku o 32-bitnom kapacitetu)
mogu korektno zapamtiti cijele brojeve u opsegu od −2147483648 do 2147483647 (asimetrija u korist
negativnih brojeva posljedica je činjenice da ne postoji negativna nula). Pokušaj da u promjenljivu tipa
“int” stavimo vrijednost izvan ovog opsega dovodi do nekorektnog rezultata (bez prijave greške). Ova
pojava naziva se prekoračenje. Na primjer, sljedeći programski isječak
int a;
a = 3000000000;
std::cout << a; // Nekorektan ispis!
također ispisuje nekorektnu vrijednost (−294967296). Naime, mada vrijednost 2000000000 lijepo
može stati u promjenljivu tipa “int”, rezultat 2 ∙ 2000000000 = 4000000000 prelazi opseg tipa “int”.
Bez obzira što se ovaj rezultat ne smješta ni u kakvu promjenljivu, prema konvenciji rezultat ma kakve
operacije čiji su operandi tipa “int” također je tipa “int”, tako da je ovaj rezultat jednostavno
“prevelik” za tip “int”. Čak i naizgled “naivna” naredba
std::cout << 2 * 2000000000; // Nekorektan ispis!
proizvodi isti (netačan) rezultat, s obzirom da su oba operanda (“2” i “2000000000”) tipa “int”. S druge
strane, problem se ne bi javio ako bismo recimo tip jednog od operanada pretvorili u tip “ double”.
Recimo, sljedeći isječak proizvodi ispravan rezultat (jer “2.” nije tipa “int” nego “double”):
std::cout << 2. * 2000000000; // Ovo radi ispravno...
Opisane poteškoće nisu vezane samo za programske jezike C i C++: one se javljaju u gotovo svim
programskim jezicima. Srećom, ove poteškoće se rijetko javljaju u praksi, s obzirom da se u promjenljivim
tipa “int” u tipičnim primjenama uglavnom čuvaju vrijednosti koje su po modulu znatno manje od
kritične vrijednosti 2 − 1 = 2147483647 (za rad sa vrlo velikim brojevima tipično se koristi tip
“double”). Odavde nipošto ne treba zaključiti da za sve potrebe treba koristiti promjenljive tipa “double”.
Naime, promjenljive tipa “double” zauzimaju više memorije od promjenljivih tipa “int”, rad sa njima je
1
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima:
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Specifičnosti nepredznačne aritmetike
Akademska godina 2015/16
daleko sporiji (više desetina puta) nego sa promjenljivim tipa “int” i, konačno, sa njima se javlja drugi
problem (vezan za pojavu gubitka tačnosti i nemogućnošću tačnog poređenja na jednakost) koji je
daleko ozbiljniji od opisanog problema prekoračenja, o kojem ćemo govoriti nekom drugom prilikom.
Sada ćemo preći na razmatranje nepredznačnih cjelobrojnih tipova. Jezici C i C++ dopuštaju da se
ispred imena nekog cjelobrojnog tipa stavi prefiks “unsigned”, čime se dobija nepredznačni cjelobrojni tip.
Ovim prefiksom se govori da se u promjenljivoj tog tipa neće rezervirati jedan bit za pamćenje znaka, dok
će se svi raspoloživi biti iskoristiti za pamćenje apsolutne vrijednosti broja. To efektivno udvostručuje
opseg pozitivnih vrijednosti koje se mogu zapamtiti u pripadnoj promjenljivoj, ali onemogućava čuvanje
negativnih vrijednosti. Recimo, uz pretpostavku da se za memoriranje promjenljivih tipa “int” koriste
32 bita, deklaracija
unsigned int a; // Nepredznačni cjelobrojni tip...
efektivno govori da će promjenljiva “a” moći ispravno čuvati samo pozitivne vrijednosti i to u opsegu
od 0 do 2 − 1 = 4294967295 (što je dvostruko više nego kod klasičnih promjenljivih tipa “int”).
Stoga će, recimo, isječak programa
unsigned int a;
a = 3000000000;
std::cout << a;
proizvesti neispravan ispis (4294967289). Međutim, sa nepredznačnim tipovima nažalost postoji jedan
potencijalno ozbiljan problem. Naime, prema konvenciji, rezultat ma koje operacije u kojoj učestvuje
makar jedan operand koji je nepredznačnog tipa tretira se također kao da je nepredznačnog tipa, što
znači da će biti korektan samo ukoliko staje u predviđeni opseg. Tako će, recimo, isječak programa
unsigned int a;
a = 5;
std::cout << a – 7; // Nekorektan ispis!
Moglo bi se očekivati da ovaj isječak ispiše vrijednost −1, s obzirom da je deklarirani vektor “v”
inicijalno prazan, pa mu je veličina 0. Međutim, zbog činjenice da je rezultat funkcije “size”
nepredznačnog tipa, ovaj isječak ispisuje neočekivanu neispravnu vrijednost 4294967295. Problem se
lako rješava eksplicitnom konverzijom u predznačni tip, kao recimo u sljedećem isječku:
2
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima:
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Specifičnosti nepredznačne aritmetike
Akademska godina 2015/16
std::vector<int> v;
std::cout << int(v.size()) – 1;
Inače, nije loša ideja rezultat funkcija poput “size”, “length”, “strlen” itd. (neke od ovih funkcija ćemo
upoznati nešto kasnije) eksplicitno pretvoriti u tip “int” kadgod se njihov rezultat koristi unutar nekog
složenijeg aritmetičkog izraza čiji rezultat može biti kako pozitivan tako i negativan, upravo sa ciljem
da se izbjegnu opisana iznenađenja. Mada ćemo u nastavku vidjeti da u velikom broju slučajeva te
eksplicitne pretvorbe nisu potrebne (zahvaljujući nekim dosta mudrim konvencijama koje jezici C i
C++ poštuju), u većini slučajeva od njih nema nikakve štete (osim ako zbog nekog razloga baš ne
želimo da se račun obavlja po pravilima nepredznačne aritmetike).
daje ispravan rezultat 2000000000, s obzirom da on lijepo može stati u tip “ int”, bez obzira što
međurezultat “2 * a” (koji bi trebao iznositi 3000000000) ne može stati u tip “int” i dovodi do
prekoračenja (pokušaj ispisa ovog međurezultata doveo bi do ispisa broja −1294967296). Isto tako,
isječak programa
unsigned int a;
a = 3;
std::cout << 2 * (a – 10) + 20;
proizvodi ispravan rezultat 6 koji lijepo može stati u nepredznačni cjelobrojni tip (primijetimo da je
čitav izraz “2 * (a – 10) + 20” nepredznačnog tipa s obzirom da je operand “a” nepredznačnog tipa),
bez obzira što su međurezultati “a – 10” i “2 * (a – 10)” nekorektni (s obzirom da se ne mogu prikazati
nepredznačnim tipom, jer su negativni).
Još jedna olakšavajuća okolnost je da ukoliko se rezultat nekog nepredznačnog izraza koji je
nekorektan zbog činjenice da mu je tačna vrijednost negativna smjesti u neku promjenljivu čiji je tip
predznačan, smještena vrijednost će ipak biti tačna, pod uvjetom da se tačna vrijednost izraza može
ispravno smjestiti u razmatranu promjenljivu. Tako će, na primjer, sljedeći isječak programa
std::vector<int> v;
int n;
n = v.size() – 1;
std::cout << n;
ispisati ispravnu vrijednost −1, bez obzira na činjenicu da se izraz “v.size() – 1” ne računa korektno.
Bez obzira na opisane olakšavajuće okolnosti, postoji još jedan potencijalno vrlo opasan problem
koji se javlja pri upotrebi nepredznačnih tipova, vezan za operacije poređenja. Razmotrimo, na primjer,
sljedeću petlju (mada rad sa stringovima u C++-u još nismo obrađivali, neće biti teško “uhvatiti”
smisao šta se ovim primjerom želi pokazati):
for(int i = 0; i <= s.length() – 1; i++) ... // Moguć problem!
Namjera programera je bila da se petlja izvrši za sve vrijednosti promjenljive “i” od 0 do dužine
stringa “s” umanjene za 1, pri čemu je namjera bila da se petlja ne izvrši niti jedanput ako je string
prazan (s obzirom da će pri praznom stringu “s.length()” biti 0, pa uvjet “i <= s.length() – 1” neće
biti ispunjen već na samom početku. Međutim, da li je baš tako? S obzirom da je “s.length()”
nepredznačnog tipa, izraz “s.length() – 1” se računa pogrešno, te njegova vrijednost neće biti −1
(nego 4294967295). Kako će praktično svaka legalna cjelobrojna vrijednost “i” biti manja od ove
vrijednosti, ova petlja nikada neće završiti. Čak i kada “i” dostigne maksimalnu moguću vrijednost koju
promjenljiva tipa “int” može imati (2147483647), pokušaj njenog daljeg povećanja za 1 dovodi do
prekoračenja koje rezultira da će nova vrijednost promjenjive “i” biti ustvari najmanji mogući broj koji
promjenljiva tipa “int” može imati (tačnije −2147483648), tako da će se petlja nastaviti unedogled
3
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Dodatak predavanjima:
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Specifičnosti nepredznačne aritmetike
Akademska godina 2015/16
(zapravo, računarska aritmetika u slučaju prekoračenja ponaša se “kružno” u smislu da iza najvećeg
mogućeg broja slijedi najmanji mogući). Rješenje ovog problema je ponovo eksplicitna konverzija tipa.
Naime, petlja poput sljedeće radiće sasvim korektno:
for(int i = 0; i <= int(s.length()) – 1; i++) ...
Zbog nekog razloga, programer je htio da se ova petlja izvrši za sve vrijednosti promjenljive “i” od −2
do dužine stringa “s”. Na prvi pogled, sve je u redu. Međutim, ova petlja se neće izvršiti niti jedanput!
Naime, rezultat izraza “s.length()” je nepredznačan, a na početku petlje promjenljiva “i” ima vrijednost
−2. Problem nastaje zbog činjenice da se nepredznačna vrijednost ne može ispravno porediti sa
negativnim brojem (s obzirom da nepredznačna vrijednost ne može biti negativna), tako da rezultat
poređenja daje pogrešnu vrijednost “false” (a trebao bi biti “true”). Posljedica je da je uvjet netačan
već na samom početku petlje, tako da se ona neće izvršiti niti jedanput. Rješenje je ponovo eksplicitna
konverzija tipa. Zaista, petlja oblika
for(int i = –2; i <= int(s.length()); i++) ...
Malo je nezgodno što se ovo upozorenje javlja čak i u sasvim bezazlenim slučajevima kao u petlji poput
for(int i = 0; i < v.size(); i++) ... // Nije problem, ali...
s obzirom da i ovdje, tehnički gledano, imamo poređenje predznačne i nepredznačne vrijednosti (koje u
ovoj situaciji nije problematično, s obzirom da “i” nikada neće biti negativan broj). U ovakvim
slučajevima imamo tri mogućnosti. Prva je da ignoriramo upozorenje (što smijemo uraditi samo ukoliko
smo sigurni da poređenje nije problematično). Druga je da eksplicitnom konverzijom tipa “ušutkamo”
kompajler, npr. kao u petlji poput sljedeće:
for(int i = 0; i < int(v.size()); i++) ...
Konačno, treće rješenje je i da sam brojač petlje deklariramo kao nepredznačnu promjenljivu, čime se
izbjegava sporno poređenje predznačnih i nepredznačnih vrijednosti:
for(unsigned int i = 0; i < v.size(); i++) ...
Mada mnogi preporučuju upravo ovakvo rješenje, njegov bitan nedostatak je činjenica da u tom slučaju
promjenljiva “i” postaje nepredznačna, pa samim tim postaje podložna svim dosada opisanim
opasnostima pri radu sa nepredznačnim vrijednostima vezanim za ponekad neočekivanu interpretaciju
nepredznačne aritmetike.