četvrtak, 26 decembra, 2024
Kako da...?

Uvod u programski jezik C (9. deo)

Autor: Nikola Hardi

Rad sa stringovima

Opšte je poznato da su računari dobri u radu sa brojevima. Međutim, vremenom su se i potrebe korisnika i alati za programiranje računara razvijali, i rad sa drugačijim tipovima podataka je postao uobičajen i znatno jednostavniji nego ranije. Svi smo imali priliku da radimo sa programima za obradu fotografija, video materijala, ili zvučnih zapisa. Multimedija je ipak tema koja prevazilazi jedan uvodni kurs o C-u, ali do kraja ovog serijala ćemo se dotaći i tih tema. Za početak, osvrnućemo se na rad sa tekstualnim podacima na C način.

Stringovi na C način

Definicija tekstualnog podatka u sferi računarstva se uglavnom svodi na to da je tekst niz karaktera, a karakteri su znakovni simboli. Iako imamo mogućnost da radimo sa tekstom ili multimedijom, računari ipak razumeju samo brojeve pa moramo tako da im predstavimo i tekst. Postoje razni standardi kako se pojedini karakteri predstavljaju u memoriji računara, između ostalog najpoznatiji su ASCII, EBCDIC, UTF itd. EBCDIC je standard koji je prisutan u Eplovom (eng. Apple) svetu i više nema toliko važnu ulogu. ASCII je standard koji je vrlo prisutan i danas. UTF je nastao kao proširenje ASCII standarda, javlja se u više varijanti i uvodi podršku za pisma različita od abecede (kinesko, japansko, ćirilica, arapsko i mnoga druga).

Važno je napomenuti da je originalni ASCII standard propisivao 7 bitova za jedan karakter, što je dalo prostora za 128 karaktera. Među tim karakterima se nalaze cifre, mala i velika slova engleskog alfabeta i niz kontrolnih karaktera. Kontrolni karakteri su oznake za novi red, prazno mesto, tabulator, upravljanje štampačima (vraćanje glave štampača na početak), rad sa terminalima (alarm ili obaveštenje da se nešto dogodilo). ASCII standard je proširen na 8 bitova, odnosno ceo jedan bajt i sada podržava 256 karaktera. Potražite na internetu termin „ASCII tabela” da biste stekli uvid u to kako izgleda tzv. tabela ASCII karaktera.

Rad sa pojedinačnim znakovima ne obećava mnogo. Postoji realna potreba za radom sa rečima, rečenicama, linijama teksta itd. Podaci koji se sastoje od više od jednog karaktera se u domaćoj literaturi nazivaju znakovnim niskama, mada odomaćen je i naziv „string”, koji je i mnogo češće u upotrebi. U programskom jeziku C se stringovi predstavljaju običnim nizovima karaktera (char niz[]), a kraj teksta se označava NULL karakterom, odnosno specijalnim znakom 0, čija brojevna predstava je, pogađate, nula. Ovakva predstava tekstualnih podataka nosi sa sobom i pojedine komplikacije. Gotovo svi savremeni jezici poznaju tip stringa, dok su u C-u stringovi ipak samo obični nizovi.

Da se podsetimo, nizovi u C-u su zapravo pokazivači na deo u memoriji. Kada pokušamo da pristupimo jednom elementu niza, tada se vrši operacija „dereferenciranja” sa zadatim odstupanjem. Sledi nekoliko primera koji ilustruju rad sa nizovima na primeru nizova karaktera.

char niz1[] = "zdravo"; niz1[0] = 'Z'; char niz2[5]; niz2[0] = '0'; niz2[1] = 'k'; niz2[2] = 0;

Pri deklaraciji niza 1 nije zadata dužina zato što je u jednom izrazu zadata i inicijalizacija niza koja glasi “zdravo”. Prevodilac može sam da zaključi koliko memorije je potrebno.

U drugom primeru je kreiran niz karaktera dužine 5. Međutim, popunjena su samo prva tri mesta. Primećujete razliku između prvog i trećeg elementa? Prvi je cifra ‘0’ (uokviren je navodnicima), a treći je brojčana vrednost 0 koja označava kraj stringa.

Može se primetiti još jedna razlika u data dva primera. U prvom primeru su korišćeni dvostruki navodnici, dok su u drugom korišćeni jednostruki. Razlog za to je što se znakovni nizovi koji su uokvireni dvostrukim navodnicima smatraju stringovima i podrazumevano im se dodaje „terminator”, odnosno znak NULL, koji označava kraj stringa. Jednostruki navodnici se isključivo koriste za predstavljanje pojedinačnih karaktera. Ovo znači da zapisi “a” i ‘a’ nisu jednaki. Prvi zapis je dužine 2, jer ima i znak NULL kojim je završen.

Ispisivanje i učitavanje stringova

Pre nego što nastavimo dalje, biće objašnjen jednostavan način za ispisivanje i učitavanje stringova pomoću standardnog ulaza/izlaza. Da se podsetimo, biblioteka za standardni ulaz/izlaz (ispis podataka u konzoli odnosno terminalu) je stdio.h. Ukoliko se koristi formatirani ispis, potrebno je funkcijama printf() i scanf() proslediti format string koji sadrži specijalan znak %s, čime je označeno da želimo da ispišemo znakovni niz sve do NULL karaktera. Drugi način je upotrebom funkcija puts(), gets() i getline() iz biblioteke string.h. Sledi primer:

#include  #include  int main() { char niz1[] = "Zdravo svete!"; printf("%s\n", niz1); scanf("%s", niz1); puts(niz1); return 0; }

Može se primetiti da je u slučaju funkcije printf() potrebno naglasiti specijalnim kontrolnim karakterom \n da nakon ispisa treba preći u novi red. Funkcija puts() to podrazumevano radi.

Za ove i sve druge funkcije dodatnu pomoć je moguće pronaći u Linuksovim man stranicama u odeljku 3, koji je posvećen standardnim bibliotekama. Recimo, man getline sadrži detaljno uputstvo o funkciji getline().

Kopiranje stringova

Kao što je u prethodnom odeljku napomenuto, stringovi su u C-u samo obični nizovi. To znači da sledeći kod verovatno neće uraditi ono šta bi bilo očekivano.

niz1 = niz2;

Ovime smo pokazivaču niz1 „rekli” da pokazuje tamo gde i pokazivač niz2. Efektivno, oni sad pokazuju na isti tekst, međutim ako izmenimo niz1, to će se odraziti i na niz2, a važi i obrnuto. Ovime smo dobili samo dva pokazivača na isti deo memorije, a uglavnom to nije bio cilj.

Drugi način kako bismo sadržaj drugog niza mogli da kopiramo u prvi niz je petlja kojom bismo kopirali element po element. Pošto je operacija kopiranja stringova vrlo često potrebna, postoji standardna biblioteka za rad sa stringovima koja se zove string.h, a funkcija za kopiranje stringova je strcpy().

strcpy(niz1, niz2);

Dati primer kopira sadržaj niza 2 u niz 1. Kopira se element po element sve dok se u nizu 2 ne naiđe na NULL karakter. Ovakvo kopiranje (element po element) se inače u stranoj literaturi naziva i „deep copy” i vrlo često se sreće u radu sa strukturama podataka, gde je potrebno obići celu strukturu podataka i svaki element kopirati, a strukturu opet izgraditi u drugom delu memorije.

Zanimljivo je što je kopiranje stringova moguće pojednostaviti ukoliko definišemo novi tip podatka kao strukturu koja sadrži niz karaktera i potom izvršimo kopiranje. Ovo je trik koji se uglavnom ne koristi u praksi i spomenut je samo kao zanimljivost.

#include  struct str { char niz[50]; }; int main() { struct str s1; struct str s2; strcpy(s1.niz, "zdravo"); s2 = s1; s2.niz[0] = 'Z'; puts(s1.niz); puts(s2.niz); return 0; }

Kreirana je struktura str koja sadrži polje niza karaktera pod nazivom niz. Pozivom funkcije strcpy() kopiramo string “zdravo” u s1 čime smo „napunili” sadržaj objekta s1. Potom je izvršena redovna dodela čime je sadržaj objekta s1 kopiran u s2. Zatim je izmenjen samo prvi karakter objekta s2 i oba stringa su ispisana. Primećujete da su kopirani svi znakovi, a da je izmenjen samo drugi objekat? Zanimljiv trik.

Poređenje, spajanje i pretraga stringova

Druga vrlo česta situacija je poređenje dva stringa. Ukoliko bi poređenje bilo izvršeno upotrebom redovnog operatora za poređenje ==, tada bi u stvari bile poređene samo adrese na koje pokazuju pokazivači tih stringova, jer, da se podsetimo, stringovi su u C-u samo obični nizovi. Ispravan način za poređenje dva stringa je pozivanjem funkcije strcmp() čija je povratna vrednost 0 ukoliko su joj prosleđeni stringovi sa jednakim sadržajem.

#include  int main() { char niz1[] = "zdravo"; char niz2[] = "zdravo"; if( strcmp(niz1, niz2) == 0) puts("Stringovi su jednaki."); else puts("Stringovi nisu jednaki."); return 0; }

Funkcija strcmp() poredi svaki element prosleđenih nizova pojedinačno, a dobra je vežba implementiranje nove funkcije strcmp() koja će unutar petlje da poredi odgovarajuće elemente prosleđenih nizova, i potom vratiti rezultat 0 ukoliko su svi elementi jednaki. Potrebno je pripaziti na situaciju kada su stringovi različitih dužina.

U svetu računara, spajanje dva sadržaja se zove konkatenacija. Odatle i naziv funkcije za spajanje dva stringa, strcat(). Važno je uveriti se da je string u koji se dodaje sadržaj dovoljno velik da ga prihvati, inače može da dođe do neželjenog prepisivanja u memoriji, uništavanja drugih podataka i neispravnog ponašanja programa.

Pretraga unutar stringa se može obaviti funkcijom strstr(). Ova funkcija kao parametre očekuje pokazivač na niz karaktera (string) u kojem je potrebno pronaći string koji je prosleđen kao drugi parametar. Povratna vrednost je pokazivač na deo memorije gde se željeni string prvi put pojavio.

Sledi malo opširniji primer koji ilustruje spajanje, pretragu i kopiranje stringova.

#include  int main() { char niz_a[50] = "LiBRE! je "; char niz_b[]   = "najbolji casopis"; char niz_c[] = "e-magazin"; strcat(niz_a, niz_b); puts(niz_a); char *cilj = strstr(niz_a, "casopis"); strcpy(cilj, niz_c); puts(niz_a); return 0; }

Pretvaranje tipova podataka u stringove i pretvaranje iz stringova

Pretvaranje podataka iz jednog tipa u drugi ili kastovanje (eng. type casting) se za većinu tipova vrši jednostavno pisanjem tipa u zagradama, na primer (int)5.23. Stringovi su i po ovom pitanju drugačiji. Postavlja se pitanje, šta pretvoriti u celobrojni tip – da li adresu na koju pokazuje pokazivač niza koji predstavlja string? Prvi element? Šta ako taj string nije broj? Predstavićemo rešenje za konverziju brojeva u stringove i obrnuto.

Ispis podataka na ekranu se takođe može smatrati konverzijom. Broj sa pokretnim zarezom je u memoriji prikazan na pomalo čudan način, a pozivom funkcije printf() kada je kao format zadat %f na ekranu ćemo dobiti razumljiv niz znakova. Sličan rezultat možemo dobiti funkcijom sprintf(), a jedina razlika u odnosu na printf() je ta što sprintf() kao prvi argument očekuje pokazivač na string, dok su drugi (format string) i treći (argumenti koji se povezuju sa formatnim stringom) slični kao kod funkcije printf().

Kod pretvaranja stringova u brojevne tipove može poslužiti funkcija sscanf(), koja se ponaša slično kao funkcija scanf(), s tim što je prvi argument adresa (pokazivač na string) koju želimo da skeniramo u nadi da će se poklopiti sa formalnim stringom koji je drugi argument, a rezultati će biti sačuvani u adresama koje su prosleđene kao 3, 4 i ostali parametri. Drugi način za pretvaranje stringova u celobrojne brojeve i brojeve sa pokretnim zarezom su funkcije atoi() i atof().

Funkcije koje koriste formatni string su česte u jeziku C i zaslužuju posebnu pažnju, međutim to je priča za poseban članak. U ovu klasu funkcija spadaju, recimo, i funkcije za rad sa datotekama.

Bezbednost

Rad sa stringovima može da bude izuzetno opasna stvar po pitanju bezbednosti programa. Ukoliko se niz karaktera ne završava NULL karakterom, svašta može da pođe po zlu pri pozivu funkcija kao što su strcpy(), strcmp() i slične. Cela klasa tih funkcija se oslanja na postojanje znaka koji označava kraj stringa. Kada string nije propisno završen, tada te funkcije mogu da zalutaju u deo memorije van stringa i svašta može da pođe po zlu. Ovakav tip napada se inače zove prekoračenje bafera (eng. buffer overflow). Preporučen način kako da izbegnete takve neprijatne situacije pri radu sa stringovima je da koristite verzije funkcija koje su ograničene maksimalnom dužinom stringa, kao što su strncpy(), strncat(), strncmp() itd. Sve te funkcije kao treći parametar zahtevaju broj koji određuje maksimalnu očekivanu dužinu stringa. Još jedna vrlo korisna funkcija je strlen() čiji zadatak je da odredi dužinu stringa. Budite pažljivi, ovo je prvo mesto gde se traže bezbednosni propusti u programima. Ne verujte podacima koji spolja ulaze u vaš program, a pogotovo kada je reč o stringovima.

Naredni deo ovog serijala možete pročitati ovde.

Prethodni deo ovog serijala možete pročitati ovde.