You are on page 1of 4

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

Dodatak predavanjima: Specifičnosti nepredznačne aritmetike


Karakteristično svojstvo jezika C i C++ (i još nekih drugih srodnih jezika) je postojanje više vrsta
cjelobrojnih tipova, koji se razlikuju po opsegu vrijednosti koje mogu biti u njima pohranjeni. Ovi tipovi
se mogu podijeliti u dvije skupine: predznačni i nepredznačni tipovi. Mada početnici gotovo nikada ne
koriste niti jedan cjelobrojni tip osim klasičnog predznačnog cjelobrojnog tipa (tj. tipa “int”), neke od
standardnih funkcija u jezicima C i C++ daju kao rezultat vrijednosti za koje se smatra da su po tipu
nepredznačne. To se uglavnom odnosi na funkcije koje kao rezultat daju veličinu nekog objekta, s
obzirom da veličina ne može biti negativan broj. Recimo, u jeziku C++, takva je funkcija “size”
primijenjena na neki vektor, dek, string ili sličan objekat (o stringovima u jeziku C++ će biti detaljno
govora nešto kasnije), kao i funkcija “length” primijenjena na string (isto vrijedi recimo i za funkciju
“strlen” naslijeđenu iz jezika C). S obzirom da se vrijednosti nepredznačnog tipa u nekim kontekstima
ponašaju u najmanju ruku neuobičajeno, potrebno je ukratko se upoznati sa specifičnostima ove vrste
tipova. Međutim, prije opisa specifičnosti aritmetike sa ovim tipovima podataka, potrebno je ukratko se
upoznati sa problemom prekoračenja (engl. overflow).

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!

na ekranu će ispisati nekorektnu vrijednost −1294967296, s obzirom da vrijednost 3000000000 prosto


ne može “stati” u promjenljivu tipa “int” (biti koji ne mogu stati “ispadaju” van, a biti koji ostaju u
promjenljivoj tvore neispravnu vrijednost). Slično se dešava prilikom izvođenja aritmetičkih operacija
sa promjenljvim tipa “int”. Na primjer, sljedeći isječak programa
int a;
a = 2000000000;
std::cout << 2 * 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 ispravan ispis, dok će isječak programa


unsigned int a;
a = –7;
std::cout << a; // Nekorektan ispis!

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!

ispisati neispravnu vrijednost (4294967294), s obzirom da se rezultat izraza “a – 7” tretira kao da je


nepredznačnog tipa (s obzirom da je “a” nepredznačnog tipa), a tačan rezultat (−2) se ne može
korektno predstaviti kao nepredznačni tip. Problem se može vrlo jednostavno ukloniti eksplicitnom
konverzijom nepredznačnog u predznačni tip. Na primjer, sljedeći isječak ispisuje ispravan rezultat:
unsigned int a;
a = 5;
std::cout << int(a) – 7; // Ekplicitna konverzija...

Opisani problemi sa aritmetikom sa nepredznačnim vrijednostima mogu djelovati kao egzotika i


čini se da bi se svi problemi te vrste mogli izbjeći izbjegavanjem upotrebe promjenljivih nepredznačnih
tipova osim u slučajevima kada postoji jak razlog za njihovu upotrebu (kako se obično i radi). Međutim,
kako je već nagoviješteno na početku ovog izlaganja, postoji veliki broj standardnih funkcija jezika C i
C++ za koje se po konvenciji smatra da im je rezultat nepredznačnog tipa. Takve su, uglavnom, sve
funkcije koje po svojoj prirodi nikada ne mogu vratiti negativan rezultat (npr. dužina stringa nikada ne
može biti negativna). Ponekad ovo može dovesti do neočekivanih rezultata. Na primjer, razmotrimo
sljedeći isječak programa:
std::vector<int> v;
std::cout << v.size() – 1; // 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).

Problemi vezani za prekoračenje znatno su ublaženi činjenicom da će krajnji rezultat nekog


složenijeg aritmetičkog izraza uvijek biti tačno izračunat i prikazan kad god taj rezultat može stati u
odgovarajući tip koji odgovara tipu izraza čak i kada pojedini međurezultati dovode do prekoračenja.
Na primjer, sljedeći isječak programa
int a;
a = 1500000000;
std::cout << 2 * a – 1000000000;

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++) ...

Sljedeći primjer je još suptilniji. Razmotrimo recimo sljedeću petlju:


for(int i = –2; i <= s.length(); i++) ... // Problem!

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++) ...

radiće bez ikakvih problema.

Opisani problemi sa poređenjem su toliko opasni da osjetljivo podešeni kompajleri automatski


prikazuju poruku upozorenja (ne i grešku) kad god programer u nekom izrazu poređenja pomiješa
predznačne i nepredznačne vrijednosti. Takva poruka upozorenja obično sadrži tekst poput
Comparison between signed and unsigned ...

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.

You might also like