[Cvičení 7] [Obsah] [Cvičení 9]

Cvičení 8


Řetězce v jazyce C

Témata


Reprezentace řetězců

Speciální typ řetězec v jazyce C (obvykle typ string v jiných jazycích) neexistuje (až v C++ v knihovně STL je typ string implementován). Existují řetězcové konstanty, které jsou uzavřeny v uvozovkách, např. "Ahoj". Řetězcové konstanty jsme využívali jako formátovací řetězec ve funkcích printf a scanf, např. printf("Soucet: %d",a+b);.
Obecně je řetězec reprezentován typem ukazatel na char a je uložen v poli znaků (statickém či dynamickém). Každý řetězec je ukončen znakem s ASCII kódem 0 (znak označovaný jako null), který se zapisuje ve zdrojovém kódu pomocí lomítka jako prefixu: '\0'. Z toho výplývá, že paměť potřebná pro uložení řetězu má velikost o 1 slabiku větší, než je délka řetězce. Tedy, řetězec Ahoj má délku 4 znaky, ale pro jeho uložení je potřeba paměť o velikosti 5 bytů, viz obrázek 1.

Uložení řetězce v paměti
Obrázek 1: Uložení řetězce v paměti

Při zacházení z řetězci v jazyce C je potřeba mít na paměti, že pracujeme s poli a ukazateli. Mějme následující definice a fragment kódu:
char ret1[10];
char *ret2;
char *ret3;

ret1 = "Ahoj"; // zde překladač ohlásí chybu 
ret2 = "Ahoj"; // zde nekopírujeme vlastní řetězec, ale pouze ukazatel na konstatní řetězec
První přiřazovací příkaz je chybný a překladač zde ohlásí chybu typu „konstantní ukazatel nelze přepsat“. U druhého přiřazovacího příkazu překladač chybu neohlásí, ale přiřazení v tomto případě také není zcela správné. Nejde totiž o přiřazení vlastního řetězce či kopii řetězce, ale do ukazatele ret2 se přiřadí adresa paměti, kde je překladačem umístěna řetězcová konstanta "Ahoj" (řetězcové funkce budou s tímto řetězcem správně pracovat, potíže mohou nastat, budeme-li do řetězce zapisovat).

Vrátíme se k poli ret1. Správné nastavení hodnoty řetězce pole ret1 je možné provést dvěma způsoby:

  1. Inicializací řetězcovou konstantou při deklaraci: char ret1[10] = "Ahoj";
  2. Kdekoliv v kódu voláním knihovní funkce pro kopii řetězců strcpy z knihovny string.h (odstavec o zmíněné knihovně viz Knihovna string.h):
    char ret1[10];
    ...
    ...
    strcpy(ret1,"Ahoj");
    
Protože je statické pole ret1 délky 10 znaků, můžeme kopírovat do tohoto pole řetězce o maximální délce 9 znaků (poslední položka pole je využita pro ukončující znak '\0').

Pro řetězec ret2 je potřeba nejprve dynamicky alokovat paměť (pole). Do tohoto pole budeme opět kopírovat řetězec "Ahoj", délku alokovaného určíme přesně podle délky pozdravu "Ahoj". K tomu využijeme funkci strlen, která vrací délku řetězce bez ukončujícího znaku! Při alokaci musíme tedy zvětšit požadavek na velikost paměti o 1:

  ret2 = (char*)malloc(strlen("Ahoj")+1);
  strcpy(ret2,"Ahoj");
Pokud napíšeme do kódu následně přiřazení ret3 = ret2;, přiřadili jsme ukazatel na začátek dynamického pole také do proměnné ret3. Oba ukazatelé ukazují na stejný řetězec (opět nejde tedy o kopii řetězce, pouze o kopii ukazatelů); při změně řetězce ret2 se mění i řetězec ret3.


Čtení řetězců ze standardního vstupu a tisk řetězců na standardní výstup

Tisk řetězců na obrazovku provádíme pomocí známých funkcí printf, kde ve formátovacím řetězci uvádíme specifikátor %s. Následující fragment kódu vytiskne na dva řádky pozdrav:
  char pozdrav[10] = "Ahoj";
  printf("%s\n%s",pozdrav,pozdrav);
Vstup řetězců z klávesnice je možné provádět pomocí funkce scanf, např. scanf("%s",pozdrav). Chování funkce má jeden „háček“. Víme, že funkce scanf konvertuje znaky ze vstupního bufferu, dokud nenarazí na tzv. bílé znaky (white characters), což jsou: mezera, tabulátor či konec řádku (ev. souboru). Zadáme-li z klávesnice text „Ahoj, Pepiku“, načte se do pole pozdrav pouze text „Ahoj,“, protože za ním následuje mezera.

V takovém případě je lépe použít funkci char *gets(char *s) z knihovny stdio.h, která načte celý zadaný řádek včetně mezer (až do stisku klávesy Enter). Znak konce řádku není do řetězce vložen, na konec je automaticky přidán ukončující znak '\0'. Funkce gets neprovádí kontrolu přetečení, zda načtený řetězec není delší nez alokované pole. Načtení celého řádku z klávesnice do pole ukazuje následující fragment kódu:

  char radek[80];
  gets(radek);
Nevýhoda spočívající v možnosti přetečení se odstraní použitím funkce fgets, která se používá pro čtení celého řádku ze souboru. Tato funkce má za parametr maximální délku pole a hlídá přetečení. Jako identifikace souboru se používá označení standardního vstupu stdin. Tedy, řetězec z klávesnice do pole o alokované délce 80 znaků načteme pomocí funkce fgetsnásledovně:
  char radek[80];
  fgets(radek,80,stdin);
Funkce přečte z klávesnice řádek textu. Čte tak dlouho, dokud nenarazí na znak konce řádku nebo nepřečetla 79 (obecně n-1) znaků. Na rozdíl od gets funkce ponechá znak konce řádku v řetězci a za něj umístí ukončující znak NULL. Více v kapitole o souborech.


Knihovna string.h

Hlavičky funkcí pracujících s řetězci jsou definovány v knihovně string.h. Identifikátory funkcí začínají vždy předponou str, další část identifikátoru je zkratkou operace, např. strcpy pro kopii řetězců (jako copy), strcmp pro porovnání řetězců (jako compare).

Základní funkce pro práci s řetězci

V tabulce uvedeme přehled nejpoužívanějších funkcí, pro získání detailních informací odkazujeme čtenáře na manuály překladače a hlavičkový soubor string.h. Upozorníme jen, že většina funkcí předpokládá, že všechny alokace paměti jsou provedeny před voláním funkce, tj. např. před voláním funkce strcpy musí být paměť pro cílový řetězec již alokována, funkce sama žádnou alokaci neprovádí.

Funkce pracující s řetězci
Hlavička funkce Význam
int strlen(const char *s) Vrátí délku řetězce bez ukončujícího znaku '\0'.
char *strcpy(char *s1, const char *s2) Zkopíruje obsah řetězce s2 do s1. Vrátí ukazatel na počátek řetězce s1 (paměť pro s1 musí být již alokována, přetečení se nekontroluje).
char *strcat(char *s1, const char *s2) Připojí obsah řetězce s2 za konec řetězce s1. Vrátí ukazatel na počátek řetězce s1 (paměť pro s1 musí být alokována po velikost řetězce po spojení, přetečení se nekontroluje).
char *strchr(const char *s, char c) Nalezení znaku c v řetězci. Pokud se znak c vyskytuje v řetězci s, pak funkce vrátí ukazatel na jeho první výskyt. V případě neúspěchu (znak v řetězci není) je vráceno NULL.
int strcmp(const char *s1, const char *s2) Porovnání dvou řetězců. Funkce vrátí 0, jsou-li oba řetězce stejné. Vrátí záporné číslo, je-li s1 lexikograficky menší než s2 a kladné číslo v opačném případě.
char *strstr(const char *s1, const char *s2) Nalezení podřetězce v řetězci. Nalezne první výskyt řetězce s2 v podřetězci s1 a vrátí pointer na tento výskyt nebo vrátí NULL v případě neúspěchu.
char *strset(char *s, int c) Nastaví všechny znaky řetězce s na hodnotu c. Pracuje, dokud nenarazí v řetězci na znak '\0'.
char *strlwr(char *s) Převede řetězec na malá písmena (bez českých znaků). Vrací ukazatel na konvertovaný řetězec.
char *strupr(char *s) Převede řetězec na velká písmena (bez českých znaků). Vrací ukazatel na konvertovaný řetězec.
char *strtok(char *s1, const char *s2) Postupně vrací ukazatele na části řetězce s1, které jsou odděleny.řetězcem s2.

Tabulka 1: Funkce z knihovny string.h

Všimněte si, že v hlavičkách funkcí je formální parametr řetězec s2 deklarován jako const char *s2. To znamená, že funkce nemění řetězec s2.

Chování funkce strtok (tok jako token) nejlépe vysvětlí příklad (převzatý z manuálu překladače firmy Borland):

int main(void)
{
  char input[16] = "abc,d,e";
  char *p;

  /* odělovač je řetězec ",", při prvním volání vrátí ukazatel na počátek řetězce input, 
     první výskyt "," je nahrazen '\0' */
  p = strtok(input, ",");
  /* vytiskne se abc */      
  if (p)   printf("%s\n", p);

  /* Druhé volání s parametrem NULL vrátí ukazatel na další část řetězce za prvním '\0',
     další výskyt "," je nahrazen '\0'  */
  p = strtok(NULL, ",");
  /* vytiskne d */
  if (p)   printf("%s\n", p);

  p = strtok(NULL, ",");
  /* vytiskne e */
  if (p)   printf("%s\n", p);

  return 0;
 }

Funkce pro práci s omezenou částí řetězce

V knihovně jsou také implementovány funkce, které nemusí pracovat s celým řetězcem, ale pouze s jeho částí, resp. s prvými n znaky. Přesněji, funkce ukončí svoji činnost, zpracují-li n znaků řetězce nebo narazí na ukončující znak '\0' (je-li řetězec kratší než n znaků). Identifikátory funkcí jsou odvozeny od výše zmíněných, mají pouze uprostřed identifikátoru navíc písmeno n a také jeden parametr navíc - počet znaků n, např.: strncpy, strncmp atd.

Za všechny uvedeme funkci:

	char *strncpy(char *s1, char *s2, int n), 
která zkopíruje nejvýše n znaků řetězce s2 do s1, např. strncpy(str,"napodobenina",5) zkopíruje do řetězce str řetězec "napod", dále strncpy(str,"ahoj",7) zkopíruje do řetězce str řetězec "ahoj".

Funkce pro reversní práci s řetězcem

Další množina funkcí pracuje s řetězcem od konce. Ve svém identifikátoru mají funkce uprostřed písmeno r (reverse). Zpracovávají řetězec od ukončujícího znaku '\0' směrem k počátku, např. int strrchr(char *s, char c) vrátí ukazatel na poslední výskyt znaku c v řetězci s, pokud tento znak řetězec obsahuje, jinak vrátí NULL.

Úloha 8.1

Napište vlastní implementaci funkce pro kopii řetězce

Řešení:

char *kopie(char *s1, const char *s2)
/* kopíruje s2 do s1, vrací ukazatel na počátek s1 */
{
  char *ps; // pomocný ukazatel
  ps  = s1; // do něj uschovám počátek na řetězec s1, do kterého  se kopíruje

  while (*s2 != '\0') // dokud nenarazím na konec
    *s1++ = *s2++; // kopíruji znak po znaku
  // ukončující znak se již nezkopíroval, nesmím zapomenout jej na konec řetězce s1 přidat
  *s1 = '\0';
  return ps;
}

/* implementace cyklem for a pomocí práce s poli */
char *kopie2(char *s1, const char *s2)
{
  int i; 
  int delka = strlen(s2);

  for(i=0;i<delka;i++) 
    s1[i] = s2[i]; // kopíruji znak po znaku
  // ukončující znak se již nezkopíroval, nesmím zapomenout jej na konec řetězce s1 přidat
  s1[delka] = '\0';
  return s1;
}

char *kopie3(char *s1, const char *s2)
{
  int i; 
  int delka = strlen(s2);

  for(i=0;i<=delka;i++) 
    s1[i] = s2[i]; // kopíruji znak po znaku
    // ukončující znak se tentokrát zkopíroval
  return s1;
}

int main(int argc, char **argv)
{
  char ret1[10]="Ahoj";
  char ret2[10];
  
  kopie(ret2,ret1);
  printf("%s\n",ret2);
  system("pause");
  return 0;
}

V kódu ke stažení je ještě další řešení.

Dev C++:kopie.dev, kopie.c
CodeBlocks:kopie.cbp, kopie.c

Úloha 8.2

Napište program, který načte z klávesnice dva řetězce a vytvoří třetí řetězec (dynamickou alokací), do kterého spojí oba načtené řetězce spojkou a. Výsledný řetězec vytiskne na obrazovku. Program napište nejprve s využitím standardních funkcí strcpy a strcat, pak naprogramujte kopii a připojení řetězce sami pomocí cyklů jako v předešlé úloze. Oba vstupní řetězce čtěte pomocí funkce gets(char *s), uložte je do statických polí pro max. 80 znaků.
Příklad vstupu:
Jan Novák
Lucie Nováková
Odpovídající výstup:
Jan Novák a Lucie Nováková

Řešení:

Dev C++:spojeni1.dev, spojeni1.c
CodeBlocks:spojeni1.cbp, spojeni1.c
Dev C++:spojeni2.dev, spojeni2.c
CodeBlocks:spojeni2.cbp, spojeni2.c
Dev C++:spojeni3.dev, spojeni3.c
CodeBlocks:spojeni3.cbp, spojeni3.c


[Cvičení 7] [Obsah] [Cvičení 9]