Tutorijal: Implementacija jednostruko ulančane liste u programskom jeziku C++
Uvod
Pošto smo se upoznali sa strukturama podataka i osnovama objektno orijentisanog programiranja, prikazaćemo i implementaciju jednostruko ulančane liste.
Ulančana lista je jedna od najosnovnijih (i najčešće korišćenih) linearnih struktura podataka, sačinjena je od čvorova koji se ređaju jedan za drugim, a budući da u članku prikazujemo "školsku" implementaciju jednostruko ulančane liste, * svaki čvor će obuhvatati dva podatka:
- celobrojnu vrednost
- pokazivač na sledeći čvor
Implementiraćemo metode za obavljanje sledećih operacija:
- pronalaženje vrednosti elementa na određenom indeksu
- umetanje elementa (na proizvoljno zadatu poziciju)
- uklanjanje elementa (sa proizvoljno zadate pozicije)
- ispis liste (u proizvoljnom formatu)
Da bi sve funkcionisalo kako dolikuje, program ćemo osmisliti tako da proverava da li se traženi indeks nalazi u granicama liste (u svim metodama u kojima je takva provera potrebna), a vodićemo računa i o ostalim bitnim detaljima.
Implementacija
U članku o strukturama podataka, već ste imali priliku da vidite kako izgleda (skoro upotpunjena) implementacija steka, ali, neke delove implementacije smo ipak izostavili zarad bolje preglednosti.
Ovoga puta, definisaćemo sve neophodne metode, i (takođe), budući da u radu sa listama program može pokušati da pristupi elementu koji je izvan granica strukture (što nije slučaj sa stekovima), predvidećemo odgovarajuće izuzetke za takve vanredne situacije.
Osnovna struktura čvora
Osnovnu strukturu čvora definisaćemo na sledeći način (već poznat od ranije):
/* ---------------------------------- */
// Čvor jednostruko ulančane liste:
/* ---------------------------------- */
struct Cvor {
int vrednost;
struct Cvor *sledeci;
};
Čvor je definisan preko sloga (struct), tj. nije definisan preko klase, što, s obzirom na jednostavnost tražene strukture čvora, smatramo racionalnim pristupom.
Osnovna struktura liste
Klasa Lista može se (praktično) definisati na sledeći način:
/* --------------------------------------------- */
// Osnovna struktura jednostruko ulančane liste:
/* --------------------------------------------- */
class Lista {
public:
int brojElemenata;
private:
struct Cvor *prvi;
struct Cvor *poslednji;
struct Cvor *trenutni;
};
Da pojasnimo:
- pokazivač na prvi čvor u listi (polje
Prvi), čuva se kao zasebno polje (pre svega zarad iteratora * koji će biti implementiran u nastavku) - pokazivač na poslednji element (polje
Poslednji), omogućava (znatno) lakše dodavanje novih čvorova - pomoćni pokazivač na čvor kome se trenutno pristupa (polje
Trenutni), ** koristi se u metodama za dodavanje i uklanjanje elemenata, kao i u metodi za pristup elementu preko indeksa
Pored navedenih privatnih polja, koristićemo i jedno javno polje (odeljak public na slici #3): čuvaćemo kao podatak i broj elemenata liste (pre svega zarad provere granica liste, pri pretrazi elemenata preko indeksa).
Konstruktor(i)
Za potrebe kreiranja novih objekata klase Lista, predvideli smo dva konstruktora:
1) Konstruktor za kreiranje prazne liste:
2) Konstruktor koji odmah u listu dodaje prvi element:
Konstruktore ćemo implementirati na sledeći način:
public:
// Konstruktor koji se poziva ukoliko
// se ne preda vrednost prvog čvora:
Lista() {
this->prvi = NULL;
this->poslednji = NULL;
this->brojElemenata = 0;
}
// Konstruktor koji se poziva ukoliko
// se preda vrednost prvog čvora:
Lista(int n) {
struct Cvor *c;
// Kreiranje novog čvora:
c = new Cvor();
c->vrednost = n;
c->sledeci = NULL;
// Inicijalno usmeravanje pokazivača:
this->prvi = c;
this->poslednji = c;
this->brojElemenata = 1;
}
Prvi konstruktor je jasan sam po sebi, dok, kada je u pitanju drugi konstruktor (iako je situacija poznata od ranije), nikako nije zgoreg da se podsetimo na princip kreiranja novih čvorova.
Budući da su polja klase Lista: Prvi i Poslednji, samo pokazivači na slog tipa Cvor, potrebno je kreirati konkretan slog na koji će navedeni pokazivači pokazivati (što smo u dosadašnjim člancima/u primerima iz C-a, tipično obavljali preko funkcije malloc, ali, ovoga puta listu implementiramo u C++-u, i zato ćemo koristiti poziv new Cvor()).
Pronalaženje vrednosti na određenom indeksu - preko metode
Za pronalaženje elementa na određenom indeksu, bilo da funkcija treba da vrati vrednost (što će biti varijacija koju ćemo prikazati u nastavku), ili referencu na element (i takvu funkciju ćemo takođe prikazati, i koristiti kao pomoćnu funkciju u drugim funkcijama), potrebno je, pažljivo i dosledno:
- krenuti od prvog elementa liste *
- prelaziti na susedne elemente preko pokazivača
Sledeci, sve dok se ne pronađe element čiji je indeks zadat.
Navedeni algoritam je prilično jednostavan za implementaciju, uz dve bitne napomene:
- za "šetanje" kroz listu, praktično je obavezno koristiti zaseban pomoćni pokazivač, * da bi se 'očuvao' pokazivač koji je vezan za prvi element liste
- u slučaju prekoračenja granica liste (ukoliko je traženi indeks manji od 0 ili veći od gornje granice), potrebno je reagovati na adekvatan način (u implementaciji kojom se bavimo u članku, "uhvatićemo" i obraditi izuzetak)
Pošto smo 'pretresli' sve preduslove, sledi programski kod:
public:
int pronalazenjeVrednosti(int indeks) {
// Provera indeksa (u smislu: da li se
// predata vrednost nalazi u granicama liste):
if (indeks < 0 || indeks >= this->brojElemenata) {
std::string s = "Greška pri pronalaženju vrednosti.\n";
s += "Indeks " + std::to_string(indeks) + " je van granica liste.";
throw s;
}
// Pronalaženje vrednosti čvora
// koji odgovara unetom indeksu:
this->trenutni = this->prvi;
int i = 0;
while (i < indeks) {
trenutni = trenutni->sledeci;
i++;
}
return trenutni->vrednost;
}
- prvi
ifkreira izuzetak ukoliko traženi indeks nije u granicama liste - ako nije došlo do izuzetka, pomoćni pokazivač se usmerava * na prvi element liste
- obavljaju se uzastopni prelasci na 'sledeći' čvor, sve dok se ne pronađe element čiji je indeks zadat
- na kraju, ispisuje se vrednost pronađenog elementa
Preko sličnog postupka, može se pronaći i referenca na element čiji se indeks zadaje kao ulazna vrednost.
Pronalaženje reference na element sa zadatim indeksom
Ukoliko je potrebno da funkcija pronalazenjeElementa vrati referencu na element sa zadatim indeksom (umesto brojčane vrednosti datog elementa), dovoljno je prepraviti tip funkcije i povratnu vrednost:
public:
struct Cvor *pronalazenjeElementa(int indeks) {
// Provera indeksa (u smislu: da li se
// predata vrednost nalazi u granicama liste):
if (indeks < 0 || indeks >= this->brojElemenata) {
std::string s = "Greška pri pronalaženju elementa.\n";
s += "Indeks " + std::to_string(indeks) + " je van granica liste.";
throw s;
}
// Pronalaženje reference na čvor
// koji odgovara unetom indeksu:
this->trenutni = this->prvi;
int i = 0;
while (i < indeks) {
trenutni = trenutni->sledeci;
i++;
}
return trenutni;
}
Metodu pronalazenjeElementa koristićemo u metodama za dodavanje i uklanjanje elemenata.
Očitavanje vrednosti na određenom indeksu - uz preklapanje operatora []
Pošto smo definisali metodu za pronalaženje vrednosti elementa (koji je povezan sa određenim indeksom) - može se nadalje pristupati elementima, ali, poziv funkcije pronalazenjeVrednosti ....
int vrednost = lista.pronalazenjeVrednosti(i);
.... ipak (u očima većine programera), ne deluje elegantno као sledeći kod:
int vrednost = lista[i];
Doduše, o lepoti i funkcionalnosti jednog ili drugog pristupa moglo bi se diskutovati, budući da operator [] 'priziva asocijacije' na statičke nizove, u kojima operacija pristupa elementu ima složenost O(1) - što naravno nije klasa složenosti koja odlikuje algoritam za pristup elementu u ulančanim listama (zbog čega korisnici mogu biti zbunjeni).
U svakom slučaju, preklapanje (eng. overload) operatora [], može se obaviti na sledeći način:
public:
int operator [] (int n) {
return pronalazenjeVrednosti(n);
}
U definiciji metode navodi se: rezervisana reč operator, kao i sam operator koji se povezuje sa povratnom vrednošću metode.
Pošto je operator [] "preklopljen", može se koristiti uz objekte klase Lista (onako kako smo već videli na slici #10).
Dodavanje elementa na kraj liste ("push")
Pošto koristimo nazive funkcija na srpskom jeziku, moramo biti precizni i napomenuti da, pod dodavanjem, podrazumevamo (prosto) nadovezivanje elementa na kraj liste ("push"), a ne umetanje na proizvoljnu poziciju ("splice").
U svakom slučaju, i ovoga puta je potrebno kreirati 'konkretan čvor' a ne samo pokazivač.
void dodavanje(int n) {
struct Cvor *c;
// Kreiranje novog čvora:
c = new Cvor();
c->vrednost = n;
c->sledeci = NULL;
if (this->prvi == NULL) {
// Dodavanje na prvu poziciju,
// ukoliko je lista prazna:
this->prvi = c;
this->poslednji = c;
}
else {
// Dodavanje na poslednju poziciju,
// ukoliko lista nije prazna:
this->poslednji->sledeci = c;
this->poslednji = c;
}
this->brojElemenata++;
}
Pri 'integraciji' novog čvora u ostatak strukture (if u funkciji dodavanje), razlika je (samo) u tome da li se lista "načinje" dodavanjem prvog elementa, ili se novi element nadovezuje na poslednji (postojeći) čvor.
Ukoliko je lista prazna, pokazivači Prvi i Poslednji biće usmereni na čvor koji je 'upravo kreiran'.
U suprotnom (ukoliko lista nije prazna), prvo je potrebno: povezati poslednji čvor sa novim čvorom, i potom proglasiti čvor koji je upravo dodat - za poslednji.
Još jednom vredi napomenuti da se pokazivač Poslednji čuva upravo zarad operacije dodavanja, jer - u suprotnom - bilo bi potrebno proći kroz celu listu.
Umetanje elementa (na zadati indeks)
Kada je u pitanju umetanje elemenata na određeni indeks, razlikuju se tri situacije:
- umetanje elementa na početak liste
- umetanje elementa na kraj liste
- umetanje elementa na bilo koju od preostalih pozicija
Umetanje na prvu ili poslednju poziciju su relativno jednostavni zahvati, i stoga ćemo najviše pažnje posvetiti proceduri za umetanje (novog) čvora na neku od međupozicija (postupak ćemo pojasniti preko donje slike).
Zarad ilustracije, ubacićemo novi čvor između (postojećih) čvorova 12 i 17.
U implementaciji, novi čvor se kreira preko izraza new Cvor() i povezuje se sa pokazivačem C, nakon čega se pokazivač C.Sledeci (pokazivac Sledeci na novom čvoru), usmerava na "sledeći" čvor (na slici, čvor #17).
Nakon pripremnih radnji, potrebno je: pristupiti "prethodnom" čvoru (preko polja Trenutni), i preusmeriti pokazivač Sledeci.
Na čvoru 12 (koji u primeru predstavlja prethodni čvor), pokazivač Sledeci se preusmerava, sa čvora 17 - na čvor 15 (tj. novi čvor), preko naredbe Trenutni->Sledeci = C; (plava strelica).
Novi čvor usmeren je na čvor 17 (tj. "sledeći" čvor), a čvor 12 je (pre)usmeren na novi čvor - što praktično znači da je novi čvor umetnut u listu:
Implementacija podrazumeva i proveru indeksa (u smislu, 'da li je indeks u granicama liste'), nakon čega sledi traženje pozicije za novi element (uz uvažavanje pravila koja su navedena na početku odeljka):
void umetanje(int indeks, int n) {
// Provera indeksa (u smislu: da li se
// predata vrednost nalazi u granicama liste):
if (indeks < 0 || indeks > this->brojElemenata) {
std::string s = "Greška pri umetanju elementa " + std::to_string(n) + "\n";
s += "Indeks " + std::to_string(indeks) + " je van granica liste.";
throw s;
}
// Umetanje na kraj liste:
if (indeks == this->brojElemenata) {
dodavanje(n);
return;
}
// Umetanje na prvu poziciju:
struct Cvor *c = new Cvor();
c->vrednost = n;
if (indeks == 0) {
c->sledeci = this->prvi;
this->prvi = c;
return;
}
// Umetanje na neku od međupozicija:
this->trenutni = pronalazenjeElementa(indeks - 1);
c->sledeci = trenutni->sledeci;
trenutni->sledeci = c;
}
Osvrnimo se na najvažnije detalje implementacije:
- ukoliko je traženi indeks van granica liste, metoda će vratiti izuzetak
- ukoliko program pokušava da "umetne" element na poslednju poziciju, posao se jednostavno prepušta metodi za dodavanje (koja će uredno usmeriti pokazivač
Poslednjina novi element) - umetanje čvora na prvu poziciju podrazumeva: kreiranje novog čvora (koji će biti referenciran preko pomoćnog pokazivača
C), usmeravanje pokazivačaC->Sledecina prvi čvor u listi (slikovito, čvor koji je "još uvek" prvi u listi) i, na kraju, usmeravanje pokazivačaPrvi, na novi čvor (this->Prvi = C;) - umetanje čvora na međupozicije, obavlja se preko procedure koju smo prethodno objasnili preko slika
Uklanjanje elementa (na zadatom indeksu)
Kada je u pitanju uklanjanje elemenata, takođe se razlikuju tri situacije:
- uklanjanje prvog elementa (u kom slučaju se mora pravilno * preusmeriti pokazivač
Prvi) - uklanjanje poslednjeg elementa (u kom slučaju se mora pravilno * preusmeriti pokazivač
Poslednji) - uklanjanje elementa koji nije prvi ili poslednji
Definisaćemo prvo metodu za uklanjanje prvog elementa liste:
Metoda za uklanjanje prvog elementa
void uklanjanjePrvi() {
struct Cvor *c = this->prvi;
// Preusmeravanje pokazivača:
this->prvi = this->prvi->sledeci;
// Oslobađanje memorije:
delete c;
return;
}
U metodi prepoznajemo sledeće korake:
- pokazivač
C(pomoćni pokazivač), usmerava se na prvi element liste - pokazivač
Prviusmerava se na drugi element liste (koji, posle pozivanja metodeuklanjanjePrvi, biva preusmeren na početak liste) - dosadašnji prvi element liste (čvor na koji pokazuje pokazivač
C), na kraju se uklanja
Metoda za uklanjanje poslednjeg elementa
Metodu za uklanjanje poslednjeg elementa, definisaćemo na sledeći način:
void uklanjanjePoslednji() {
// Pronalaženje pretposlednjeg čvora:
this->trenutni = this->prvi;
for (int i = 2; i < this->brojElemenata; i++) {
trenutni = trenutni->sledeci;
}
// Uklanjanje poslednjeg čvora:
this->poslednji = trenutni;
Cvor *c = trenutni->sledeci;
delete c;
this->poslednji->sledeci = NULL;
}
U metodi za uklanjanje poslednjeg elementa prepoznajemo sledeće korake:
- preko čvora
C(pomoćni pokazivač), referencira se poslednji element liste - preko pokazivača
Trenutni, pronalazi se pretposlednji element liste, nakon čega se adresa pretposlednjeg elementa predaje pokazivačuPoslednji - pokazivač
Sledecina pretposlednjem čvoru, postavlja se na vrednostNULL - dosadašnji poslednji čvor (koji je referenciran preko pokazivača
C), na kraju se uklanja
Opšta metoda za uklanjanje elementa
Opšta procedura za uklanjanje čvora, ima četiri odeljka:
ifgrananje preko koga se proverava da li je indeks unutar granica liste- poziv metode
uklanjanjePrvi, ukoliko je indeks 0 - poziv metode
uklanjanjePoslednji, ukoliko indeks ukazuje na poslednji element liste (indeksBrojElemenata - 1) - ostatak metode zadužen je za uklanjanje čvorova na "međupozicijama":
Uklanjanje čvorova na međupozicijama, ilustrovaćemo preko sledeće slike:
- čvor koji se uklanja, pronalazi se preko pomoćne metode za pronalaženje elemenata i referencira se preko pomoćnog pokazivača
C(za primer ćemo uzeti čvor #15) - preko pokazivača
C(tačnije,C->Sledeci), takođe je pronađena i referenca na čvor17, odnosno, referenca na "sledeći" čvor (koja je još bitnija za dalji tok izvršavanja programa) - preko pokazivača
Trenutnipristupa se čvoru12(to jest, čvoru koji prethodi čvoru koji se uklanja) - pokazivač
Trenutni->Sledeciusmerava se naC->Sledeci(praktično, čvor17, ili, u opštem smislu, "sledeći" čvor)
Pošto smo se pobrinuli oko svih referenci (to jest, pošto su čvorovi 12 i 17 uspešno 'prespojeni'), može se ukloniti čvor 15 (odnosno, preko pokazivača C, može se osloboditi memorija koju čvor 15 zauzima).
Dopunjena metoda za uklanjanje čvora ima sledeći oblik:
void uklanjanje(int indeks) {
// Provera indeksa (u smislu: da li se
// predata vrednost nalazi u granicama liste):
if (indeks < 0 || indeks > this->brojElemenata) {
std::string s = "Greška pri uklanjanju elementa.\n";
s += "Indeks " + std::to_string(indeks) + " je van granica liste.";
throw s;
}
// Uklanjanje elementa sa početka liste:
if (indeks == 0) {
uklanjanjePrvi();
return;
}
// Uklanjanje elementa sa kraja liste;
if (indeks == this->brojElemenata - 1) {
uklanjanjePoslednji();
return;
}
// Uklanjanje elementa sa neke od međupozicija:
this->trenutni = pronalazenjeElementa(indeks - 1);
struct Cvor *c = trenutni->sledeci;
trenutni->sledeci = c->sledeci;
delete c;
}
Verujemo da je u ovom slučaju lakše razumeti sam kod, u odnosu na šemu koju smo prethodno videli (slike #19 i #20).
Ispis liste u proizvoljnom formatu
Ispis liste može se shvatiti kao kombinacija prolaska kroz listu i ispisivanja pojedinačnih vrednosti.
Za pretvaranje celobrojne vrednosti u nisku, koristićemo specijalizovanu metodu iz 'domaće radinosti' (koju ćemo smestiti u private odeljak klase):
#define ASCII_0 48
// znak '0' ima ASCII kod 48
private:
std::string intUString(int n) {
std::string s = "", p = "";
// Negativni broj svešćemo odmah na apsolutnu vrednost
// i dodati znak "-" na početak niske
if (n < 0) {
s += "-";
n = abs(n);
}
// Cifre broja očitavaju se zdesna nalevo:
while (n > 0) {
p += n % 10 + ASCII_0;
n /= 10;
// cifra jedinica broja n dobija se kao:
// n % 10
}
// Glavni string s, popunjava se čitanjem pomoćnog
// stringa (p) unazad i upisivanjem znakova
int d = p.length() - 1;
while (d >= 0) {
s += p[d];
d--;
}
return s;
}
Sada se može definisati i metoda za ispis elemenata liste:
public:
std::string ispisListe() {
std::string s = "";
// Ispis u slučaju da je lista prazna:
if (this->prvi == NULL) {
s += "Lista je prazna.";
return s;
}
// Ispis u slučaju da lista sadrži bar jedan element:
struct Cvor *c = this->prvi;
while (c != NULL) {
s += intUString(c->vrednost) + ", ";
c = c->sledeci;
}
// Malo elegancije nije naodmet,
// i stoga ćemo skinuti poslednji zarez
return s.substr(0, s.length() - 2);
}
Ukoliko je lista prazna, biće ispisana prikladna poruka, dok će u suprotnom elementi liste biti ispisani redom.
Metoda za ispis svakako se može dopuniti opcijama za formatiranje (tako da se mogu birati i drugi znakovi za razdvajanje elemenata, to jest, tako da se umesto zareza može koristiti i razmak, prelazak u novi red i sl), ali, to ostavljamo vama - za vežbu. :)
Nekoliko reči o izuzecima
Zarad početnog upoznavanja sa izuzecima, uzećemo za primer situaciju sa kojom smo se susreli u toku implementacije liste: kako program treba da 'reaguje' kada funkcija pronalazenjeVrednosti pokuša da pristupi indeksu koji je manji od nule ili veći od gornje granice liste?!
U metodi za pronalaženje vrednosti čvora, na početku stoji if grananje preko koga se proverava da li je uneti indeks unutar granica liste (što omogućava da se lako zaustavi svaki pokušaj pristupa elementima koji ne zadovoljavaju uslov), međutim, šta tačno treba da vrati metoda (čiji je povratni tip int) - ukoliko program pokuša da pristupi elementu čiji je indeks van granica liste?
Metoda, teoretski, može vratiti (recimo) INT_MIN, najmanju negativnu celobrojnu vrednost koja se da zabeležiti - što se može shvatiti kao "signal" da je nešto "krenulo naopako", ali, takav pristup stvara određene probleme:
- iako nije 'previše verovatno' u većini situacija,
INT_MINmože biti vrednost nekog od čvorova - ceo pristup (najprostije rečeno) - nije nimalo elegantan
Jedno od rešenja može biti da metoda za pronalaženje vrednosti čvora - ukoliko dođe do greške - uopšte ne vrati celobrojnu vrednost, već - izuzetak * - što podrazumeva da se pozivajućoj funkciji vraća informacija o pojavi greške, kao i niska ili objekat ** koji se mogu "uhvatiti" (preko posebnog mehanizma u programskom jeziku), i prikazati.
Kada kažemo da izuzeci zahtevaju poseban tretman, mislimo na to da ne možemo samo napisati sledeći kod (nezavisno od toga da li metoda vraća izuzetak ili ne vraća) ....
int vrednost = lista[i];
.... jer, ako je vrednost i izvan granica liste, program će "pući" na donekle nepredvidljiv način.
Ako se sledeći izuzetak definiše unutar metode za pristup elementu preko indeksa ....
if (indeks < 0 || indeks > this->brojElemenata) {
throw "Uneti indeks je izvan granica liste!";
}
.... pri pozivu metode, potrebno je koristiti kod po sledećem obrascu:
try {
int vrednost = lista[i];
}
catch (string poruka) {
std::cout << poruka << endl;
}
- ako se ne desi ništa nepredviđeno, metoda neće vratiti izuzetak (praktično, korisnik programa neće imati predstavu da je u izvršavanje uključen mehanizam provere)
- u suprotnom, čim se u nekoj instrukciji unutar odeljka
trypojavi greška (koja je pri tom 'uhvaćena' i prosleđena preko funkcijethrow), prekida se izvršavanje i prelazi se na odeljakcatch(preko koga se "uhvaćeni" izuzetak može iskoristiti za ispis poruke korisniku, ili, za neki drugi oblik debugging-a (u gornjem primeru, samo ispisujemo poruku))
Za kraj, osvrnućemo se na osnovne principe za korektno navođenje komentara u programskom kodu.
Ukratko o vođenju dokumentacije
Ako se određena struktura podataka (ili bilo kakav drugi kod), kreira sa idejom o 'javnoj dostupnosti' takvog koda, pravila dobrog programiranja nalažu da kod treba da bude propraćen detaljnom dokumentacijom (ili makar osnovnim uputstvima).
Ako programski kod neće koristi drugi programeri (to jest, ako ćemo ga koristi 'mi sami'), može delovati privlačno da se 'odreknemo' pisanja komentara (jer - "znamo valjda šta smo pisali u kodu"), međutim - na osnovu ličnog i kolektivnog iskustva - i dalje preporučujemo pisanje dokumentacije (tj. komentara), za sve delove koda iole većeg obima i značaja (bez obzira na to 'ko će na kraju koristiti kod').
Kada kažemo "delovi koda iole većeg obima i značaja", mislimo na to da pravilno vođenje dokumentacije svakako podleže pravilima koja se tiču i toga šta ne treba pisati.
Na primer, ukoliko funkcija namenjena sabiranju brojeva doslovno nosi naziv sabiranje i sadrži nekoliko veoma nedvosmislenih naredbi, komentari nalik sledećim komentarima: "ovo je funkcija za sabiranje brojeva" i "ovo je naredba za računanje zbira" .... nisu baš od prevelike koristi.
Pogledajmo nekoliko primera neadekvatne upotrebe komentara:
// ovo je funkcija za sabiranje celih brojeva:
int sabiranje(int a, int b) {
// inicijalizacija promenljive:
int c;
// ovo je naredba za računanje zbira:
c = a + b;
// pozivajućoj funkciji se prosleđuje
// vrednost promenljive c:
return c;
}
int i = 1;
// petlja koja se ponavlja za sve vrednosti
// promenljive i, u rasponu od 1 do 10:
while (i <= 10) {
// ispis trenutne vrednosti promenljive i:
std::cout << i << " ";
// inkrementacija promenljive i:
++i;
}
Najjednostavnije: ukoliko je zaista očigledno čemu služi određeni deo koda, dodavanje suvišnih komentara, ne samo da ne povećava čitljivost koda, već (naprotiv), može se reći da se čitljivost - umanjuje.
Kako onda (nasuprot prethodnim primerima), izgleda adekvatno prokomentarisan programski kod?
Iako šira diskusija o standardima za vođenje dokumentacije tek prestoji u nekom kasnijem trenutku (u pitanju je relativno opsežna tematika koja zaslužuje zaseban članak), ukratko ćemo navesti da pravilno komentarisanje koda podrazumeva (pre svega): blok komentare ispred funkcija, linijske komentare koji označavaju odeljke koda u funkcijama (zarad lakšeg prepoznavanja), i naravno napomene na mestima na kojima su potrebna dodatna pojašnjenja (odnosno, na mestima na kojima može 'doći do zabune').
Pogledajmo nekoliko primera:
- Školski primer (komentari kakvi se pojavljuju u udžbenicima ili na sajtovima koji se bave programiranjem i sl):
/*
Funkcija za proveru pripadnosti tačke pravougaoniku,
uz korišćenje struktura Tacka i Pravougaonik, koje su
definisane u datoteci "strukture.h"
(tačka je definisana preko koordinata x i y,
a pravougaonik preko koordinata x1, y1, x2 i y2.
*/
bool pripadnost_pravougaoniku(Tacka t, Pravougaonik p) {
// Umesto da odmah proverimo da li
// tačka pripada pravougaoniku,
// radije ćemo prvo ispitati sve
// uslove koji odgovaraju situacijama
// u kojima tačka (potencijalno)
// NE PRIPADA pravougaoniku:
if (t.x < p.x1) return false;
if (t.x > p.x2) return false;
if (t.y < p.y1) return false;
if (t.y > p.y2) return false;
// Ako tačka (ipak) pripada pravougaoniku ....
return true;
}
- Praktičan primer:
/**
Provera pripadnosti tačke pravougaoniku;
Strukture Tacka (polja: x i y) i
Pravougaonik (polja: x1, y1, x2, y2),
definisane su u datoteci "strukture.h"
*/
bool pripadnost_pravougaoniku(Tacka t, Pravougaonik p) {
if (t.x < p.x1) return false;
if (t.x > p.x2) return false;
if (t.y < p.y1) return false;
if (t.y > p.y2) return false;
return true;
}
Na samom kraju, (ipak) još malo "filozofije" ....
U funkcijama za umetanje ili uklanjanje elemenata, pisali smo (iznad blokova koda), komentare nalik na "Umetanje na prvu poziciju", "Uklanjanje elementa sa kraja liste" i sl.
Sa jedne strane, može delovati da su neki od takvih komentara u neskladu sa pravilima koja smo naveli (budući da se ispod komentara nalaze mali blokovi koda ili pozivi funkcija čiji naziv jasno nagoveštava svrhu funkcije), međutim, u praktičnom smislu:
- u pitanju su komentari u okviru članka u kome se tek upoznajemo sa implementacijom ulančane liste :)
- u 'svakodnevnim uslovima', ukoliko funkcije imaju više od cca. 15 linija 'netrivijalnog programskog koda', takođe je sasvim primereno ukoliko se najvažnije celine označe kratkim informativnim komentarima
Iako se često smatra da "kod treba da opisuje sam sebe", tj. pojedinačne funkcije treba da budu dovoljno "kratke i jasne" da (praktično) ne zahtevaju komentare (i naravno da se sa takvim principima u potpunosti slažemo) - u praksi - ukoliko postoje iole veća 'odstupanja' između iskustva i umeća programera koji su pisali kod i programera koji čitaju kod, nastaje situacija u kojoj bi kratki komentari "ipak dobro došli" (da ne govorimo o situacijama u kojima samo deluje da je kod "jasan sam po sebi" - a zapravo nije).
Kratak rezime ....
Na samom kraju, ostaje samo da vam preporučimo da pokušate, da samostalno implementirate ulančanu listu (ponovo), ili (zašto da ne), neku drugu strukturu podataka.
Prvi 'kandidat' bi mogla biti dvostruko ulančana lista (lista čiji čvorovi imaju i pokazivač na prethodni element), a svakako preporučujemo i samostalnu implementaciju reda za čekanje.
Sledeća tvrdnja može vam delovati čudno, ali, duboko smo uvereni (na osnovu iskustva), da je samostalna izrada klasa preko kojih se implementiraju liste, redovi, stekovi i sl (naravno, pod uslovom da neko ko se u taj poduhvat upušta, ipak ima dovoljno iskustva i predznanja) - mnogo lakši i jednostavniji poduhvat - od pukog čitanja tekstova o implementaciji.
Možda niste još uvek u stanju da poverujete u navedene tvrdnje, ali, bar probajte da sami implementirate nešto od prikazanog i predloženog - možete se prijatno iznenaditi. :)