петак, 26 априла, 2024
Како да...?

Увод у програмски језик C (9. део)

Аутор: Никола Харди

Рад са стринговима

Опште је познато да су рачунари добри у раду са бројевима. Међутим, временом су се и потребе корисника и алати за програмирање рачунара развијали, и рад са другачијим типовима података је постао уобичајен и знатно једноставнији него раније. Сви смо имали прилику да радимо са програмима за обраду фотографија, видео материјала, или звучних записа. Мултимедија је ипак тема која превазилази један уводни курс о C-у, али до краја овог серијала ћемо се дотаћи и тих тема. За почетак, осврнућемо се на рад са текстуалним подацима на C начин.

Стрингови на C начин

Дефиниција текстуалног податка у сфери рачунарства се углавном своди на то да је текст низ карактера, а карактери су знаковни симболи. Иако имамо могућност да радимо са текстом или мултимедијом, рачунари ипак разумеју само бројеве па морамо тако да им представимо и текст. Постоје разни стандарди како се поједини карактери представљају у меморији рачунара, између осталог најпознатији су ASCII, EBCDIC, UTF итд. EBCDIC је стандард који је присутан у Епловом (енг. Apple) свету и више нема толико важну улогу. ASCII је стандард који је врло присутан и данас. UTF је настао као проширење ASCII стандарда, јавља се у више варијанти и уводи подршку за писма различита од абецеде (кинеско, јапанско, ћирилица, арапско и многа друга).

Важно је напоменути да је оригинални ASCII стандард прописивао 7 битова за један карактер, што је дало простора за 128 карактера. Међу тим карактерима се налазе цифре, мала и велика слова енглеског алфабета и низ контролних карактера. Контролни карактери су ознаке за нови ред, празно место, табулатор, управљање штампачима (враћање главе штампача на почетак), рад са терминалима (аларм или обавештење да се нешто догодило). ASCII стандард је проширен на 8 битова, односно цео један бајт и сада подржава 256 карактера. Потражите на интернету термин „ASCII табела” да бисте стекли увид у то како изгледа тзв. табела ASCII карактера.

Рад са појединачним знаковима не обећава много. Постоји реална потреба за радом са речима, реченицама, линијама текста итд. Подаци који се састоје од више од једног карактера се у домаћој литератури називају знаковним нискама, мада одомаћен је и назив „стринг”, који је и много чешће у употреби. У програмском језику C се стрингови представљају обичним низовима карактера (char niz[]), а крај текста се означава NULL карактером, односно специјалним знаком 0, чија бројевна представа је, погађате, нула. Оваква представа текстуалних података носи са собом и поједине компликације. Готово сви савремени језици познају тип стринга, док су у C-у стрингови ипак само обични низови.

Да се подсетимо, низови у C-у су заправо показивачи на део у меморији. Када покушамо да приступимо једном елементу низа, тада се врши операција „дереференцирања” са задатим одступањем. Следи неколико примера који илуструју рад са низовима на примеру низова карактера.

char niz1[] = "zdravo";
niz1[0] = 'Z';

char niz2[5];
niz2[0] = '0';
niz2[1] = 'k';
niz2[2] = 0;

При декларацији низа 1 није задата дужина зато што је у једном изразу задата и иницијализација низа која гласи “zdravo”. Преводилац може сам да закључи колико меморије је потребно.

У другом примеру је креиран низ карактера дужине 5. Међутим, попуњена су само прва три места. Примећујете разлику између првог и трећег елемента? Први је цифра ‘0’ (уоквирен је наводницима), а трећи је бројчана вредност 0 која означава крај стринга.

Може се приметити још једна разлика у дата два примера. У првом примеру су коришћени двоструки наводници, док су у другом коришћени једноструки. Разлог за то је што се знаковни низови који су уоквирени двоструким наводницима сматрају стринговима и подразумевано им се додаје „терминатор”, односно знак NULL, који означава крај стринга. Једноструки наводници се искључиво користе за представљање појединачних карактера. Ово значи да записи “а” и ‘а’ нису једнаки. Први запис је дужине 2, јер има и знак NULL којим је завршен.

Исписивање и учитавање стрингова

Пре него што наставимо даље, биће објашњен једноставан начин за исписивање и учитавање стрингова помоћу стандардног улаза/излаза. Да се подсетимо, библиотека за стандардни улаз/излаз (испис података у конзоли односно терминалу) је stdio.h. Уколико се користи форматирани испис, потребно је функцијама printf() и scanf() проследити формат стринг који садржи специјалан знак %s, чиме је означено да желимо да испишемо знаковни низ све до NULL карактера. Други начин је употребом функција puts(), gets() и getline() из библиотеке string.h. Следи пример:

#include <stdio.h>
#include <string.h>

int main()
{
  char niz1[] = "Zdravo svete!";

  printf("%s\n", niz1);

  scanf("%s", niz1);
  puts(niz1);

  return 0;
}

Може се приметити да је у случају функције printf() потребно нагласити специјалним контролним карактером \n да након исписа треба прећи у нови ред. Функција puts() то подразумевано ради.

За ове и све друге функције додатну помоћ је могуће пронаћи у Линуксовим man страницама у одељку 3, који је посвећен стандардним библиотекама. Рецимо, man getline садржи детаљно упутство о функцији getline().

Копирање стрингова

Као што је у претходном одељку напоменуто, стрингови су у C-у само обични низови. То значи да следећи код вероватно неће урадити оно шта би било очекивано.

niz1 = niz2;

Овиме смо показивачу niz1 „рекли” да показује тамо где и показивач niz2. Ефективно, они сад показују на исти текст, међутим ако изменимо niz1, то ће се одразити и на niz2, а важи и обрнуто. Овиме смо добили само два показивача на исти део меморије, а углавном то није био циљ.

Други начин како бисмо садржај другог низа могли да копирамо у први низ је петља којом бисмо копирали елемент по елемент. Пошто је операција копирања стрингова врло често потребна, постоји стандардна библиотека за рад са стринговима која се зове string.h, а функција за копирање стрингова је strcpy().

strcpy(niz1, niz2);

Дати пример копира садржај низа 2 у низ 1. Копира се елемент по елемент све док се у низу 2 не наиђе на NULL карактер. Овакво копирање (елемент по елемент) се иначе у страној литератури назива и „deep copy” и врло често се среће у раду са структурама података, где је потребно обићи целу структуру података и сваки елемент копирати, а структуру опет изградити у другом делу меморије.

Занимљиво је што је копирање стрингова могуће поједноставити уколико дефинишемо нови тип податка као структуру која садржи низ карактера и потом извршимо копирање. Ово је трик који се углавном не користи у пракси и споменут је само као занимљивост.

#include <string.h>

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;
}

Креирана је структура str која садржи поље низа карактера под називом низ. Позивом функције strcpy() копирамо стринг “zdravo” у s1 чиме смо „напунили” садржај објекта s1. Потом је извршена редовна додела чиме је садржај објекта s1 копиран у s2. Затим је измењен само први карактер објекта s2 и оба стринга су исписана. Примећујете да су копирани сви знакови, а да је измењен само други објекат? Занимљив трик.

Поређење, спајање и претрага стрингова

Друга врло честа ситуација је поређење два стринга. Уколико би поређење било извршено употребом редовног оператора за поређење ==, тада би у ствари биле поређене само адресе на које показују показивачи тих стрингова, јер, да се подсетимо, стрингови су у C-у само обични низови. Исправан начин за поређење два стринга је позивањем функције strcmp() чија је повратна вредност 0 уколико су јој прослеђени стрингови са једнаким садржајем.

#include <string.h>

int main()
{
  char niz1[] = "zdravo";
  char niz2[] = "zdravo";

  if( strcmp(niz1, niz2) == 0)
    puts("Stringovi su jednaki.");
  else
    puts("Stringovi nisu jednaki.");

  return 0;
}

Функција strcmp() пореди сваки елемент прослеђених низова појединачно, а добра је вежба имплементирање нове функције strcmp() која ће унутар петље да пореди одговарајуће елементе прослеђених низова, и потом вратити резултат 0 уколико су сви елементи једнаки. Потребно је припазити на ситуацију када су стрингови различитих дужина.

У свету рачунара, спајање два садржаја се зове конкатенација. Одатле и назив функције за спајање два стринга, strcat(). Важно је уверити се да је стринг у који се додаје садржај довољно велик да га прихвати, иначе може да дође до нежељеног преписивања у меморији, уништавања других података и неисправног понашања програма.

Претрага унутар стринга се може обавити функцијом strstr(). Ова функција као параметре очекује показивач на низ карактера (стринг) у којем је потребно пронаћи стринг који је прослеђен као други параметар. Повратна вредност је показивач на део меморије где се жељени стринг први пут појавио.

Следи мало опширнији пример који илуструје спајање, претрагу и копирање стрингова.

#include <string.h>

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;
}

Претварање типова података у стрингове и претварање из стрингова

Претварање података из једног типа у други или кастовање (енг. type casting) се за већину типова врши једноставно писањем типа у заградама, на пример (int)5.23. Стрингови су и по овом питању другачији. Поставља се питање, шта претворити у целобројни тип – да ли адресу на коју показује показивач низа који представља стринг? Први елемент? Шта ако тај стринг није број? Представићемо решење за конверзију бројева у стрингове и обрнуто.

Испис података на екрану се такође може сматрати конверзијом. Број са покретним зарезом је у меморији приказан на помало чудан начин, а позивом функције printf() када је као формат задат %f на екрану ћемо добити разумљив низ знакова. Сличан резултат можемо добити функцијом sprintf(), а једина разлика у односу на printf() је та што sprintf() као први аргумент очекује показивач на стринг, док су други (формат стринг) и трећи (аргументи који се повезују са форматним стрингом) слични као код функције printf().

Код претварања стрингова у бројевне типове може послужити функција sscanf(), која се понаша слично као функција scanf(), с тим што је први аргумент адреса (показивач на стринг) коју желимо да скенирамо у нади да ће се поклопити са формалним стрингом који је други аргумент, а резултати ће бити сачувани у адресама које су прослеђене као 3, 4 и остали параметри. Други начин за претварање стрингова у целобројне бројеве и бројеве са покретним зарезом су функције atoi() и atof().

Функције које користе форматни стринг су честе у језику C и заслужују посебну пажњу, међутим то је прича за посебан чланак. У ову класу функција спадају, рецимо, и функције за рад са датотекама.

Безбедност

Рад са стринговима може да буде изузетно опасна ствар по питању безбедности програма. Уколико се низ карактера не завршава NULL карактером, свашта може да пође по злу при позиву функција као што су strcpy(), strcmp() и сличне. Цела класа тих функција се ослања на постојање знака који означава крај стринга. Када стринг није прописно завршен, тада те функције могу да залутају у део меморије ван стринга и свашта може да пође по злу. Овакав тип напада се иначе зове прекорачење бафера (енг. buffer overflow). Препоручен начин како да избегнете такве непријатне ситуације при раду са стринговима је да користите верзије функција које су ограничене максималном дужином стринга, као што су strncpy(), strncat(), strncmp() итд. Све те функције као трећи параметар захтевају број који одређује максималну очекивану дужину стринга. Још једна врло корисна функција је strlen() чији задатак је да одреди дужину стринга. Будите пажљиви, ово је прво место где се траже безбедносни пропусти у програмима. Не верујте подацима који споља улазе у ваш програм, а поготово када је реч о стринговима.

Наредни део овог серијала можете прочитати овде.

Претходни део овог серијала можете прочитати овде.