Řetězce
Nyní už víme, jak můžeme v C pracovat s jednotlivými (ASCII) znaky. Obvykle však chceme pracovat s delšími sekvencemi textu - řádky, větami, odstavci atd. Sekvence textu se v programovacích jazycích obvykle označují jako řetězce (strings).
Dobrá zpráva je, že pro použití řetězců v C už známe vše potřebné – řetězce nejsou nic jiného než pole znaků!
Řetězce v C
Teoreticky bychom si mohli navrhnout vlastní způsob, jak řetězce v paměti reprezentovat a jak s nimi
pracovat. Nicméně zaběhlým způsobem, jak s ASCII textem v C pracovat, a pro který C nabízí různé
funkce a základní syntaktickou podporu, je použití takzvaných řetězců zakončených nulou
(null-terminated strings). Takto reprezentovaný řetězec není nic jiného než pole
znaků, které obsahuje na svém posledním indexu znak '\0'
(s číselnou hodnotou 0
),
který značí konec řetězce. Například řetězec UPR
by tedy v paměti počítače byl reprezentovaný takto:
Vytvoření řetězce
Pokud bychom chtěli vytvořit řetězec na zásobníku, můžeme vytvořit statické pole, umístit do něj
jednotlivé znaky řetězce a za ně přidat znak '\0
1:
1Pro výpis řetězce pomocí funkce printf
můžeme použít zástupný znak %s
.
#include <stdio.h>
int main() {
char text[4] = {'U', 'P', 'R', '\0'};
printf("%s\n", text);
return 0;
}
Pokud bychom potřebovali řetězec s dynamickou nebo velkou délkou, můžeme pro vytvoření řetězce samozřejmě použít také dynamickou paměť.
Řetězcový literál
Vytváření řetězců tímto způsobem je nicméně celkem zdlouhavé a nepřehledné. Často chceme v programu
jednoduše a rychle zapsat krátký textový řetězec tak, aby šel přehledně přečíst. K tomu můžeme využít
tzv. řetězcový literál (string literal), který lze vytvořit tak, že napíšeme text do dvojitých
uvozovek ("
). Pokud tedy v našem programu vytvoříme například literál "UPR"
, tak se stane následující:
- Překladač při překladu uloží do výsledného spustitelného souboru pole reprezentující daný řetězec.
V tomto případě půjde o pole velikosti 4 s hodnotami
'U'
,'P'
,'R'
a'\0'
. Při spuštění programu se toto pole načte do globální paměti v sekci adresního prostoru, která je určena pouze pro čtení. Do takto vytvořeného řetězce tak nelze zapisovat, lze jej pouze číst2.2Tyto řetězce jsou pouze pro čtení zejména z toho důvodu, aby je šlo sdílet. Pokud například v programu použijete třikrát stejný řetězcový literál, překladač může v paměti pole pro tento literál vytvořit pouze jednou, aby ušetřil paměť. Kvůli toho ale musí být řetězce pouze pro čtení, pokud bychom totiž takto sdílený řetězec změnili, změnilo by to i hodnotu všech ostatních literálů, které se vyhodnotí na jeho adresu, což by bylo dost neintuitivní.
- Samotný výraz literálu se při běhu programu vyhodnotí jako adresa prvního znaku řetězce uloženého v globální paměti.
- Datový typ literálu bude
ukazatel na konstantní znak, tedy
const char*
. Tento datový typ říká, že hodnotu znaku na dané adrese nelze měnit.
Pomocí řetězcového literálu si tak můžeme značne usnadnit zápis řetězců v programech, jelikož
nemusíme přemýšlet nad délkou pole, nemusíme pamatovat na umístění znaku '\0'
na konec řetězce
a ani nemusíme obalovat jednotlivé znaky do apostrofů:
#include <stdio.h>
int main() {
const char* text = "UPR";
printf("%s\n", text);
return 0;
}
Je však třeba pamatovat na to, že takto vytvořené řetězce jsou opravdu pouze pro čtení, a nesmíme
tak do nich zapisovat. Pokud je budete ukládat do proměnné, tak použijte datový typ const char*
,
díky kterému vás překladač bude hlídat, abyste se do takovéhoto řetězce omylem nesnažili něco zapsat.
Pokud byste chtěli použít řetězcový literál pro vytvoření řetězce, který lze měnit, můžete ho uložit
do proměnné typu char[]
(tj. pole znaků):
#include <stdio.h>
int main() {
char text[] = "UPR";
text[0] = 'A';
printf("%s\n", text);
return 0;
}
V takovémto případě se hodnota z literálu překopíruje do proměnné pole znaků na zásobníku, který již lze měnit.
Pokud jsou vám řetězcové literály povědomé, je to kvůli toho, že jsme je již mnohokrát využili při volání funkce
printf
.
Víceřádkové řetězcové literály
Pokud budete chtít zapsat řetězcový literál na více řádků kódu, můžete buď na konci každého
neukončeného řádku použít znak \
:
const char* veta = "Ahoj \
jmenuji \
se \
Karel";
nebo každý řádek samostatně obalit uvozovkami:
const char* veta = "Ahoj"
"jmenuji"
"se"
"Karel";
Pozor však na to, že v ani jednom ze zmíněných případů nebude součástí řetězce znak odřádkování. Ten musíte vždy přidat explicitně:
const char* radky = "radek1\n\
radek2\n\
radek3\n";
// nebo
const char* radky = "radek1\n"
"radek2\n"
"radek3\n";
K čemu slouží nulový znak na konci?
U polí je trochu nepraktické to, že pokud je chceme poslat do nějaké funkce, musíme spolu s ukazatelem na první prvek pole předat také jeho velikost, aby funkce věděla, ke kolika prvkům si může dovolit přistoupit. Jiným způsobem, jak určit velikost pole, je zvolit si speciální hodnotu, která bude značit konec pole. Když kód, který s takovýmto polem bude pracovat, na tuto speciální hodnotu narazí, tak bude vědět, že dále v paměti již pole nepokračuje.
Tento mechanismus je využit právě u řetězců zakončených nulou, kde onou speciální hodnotou je právě
tzv. NUL
znak, který má číselnou hodnotu 0
. Například při procházení řetězce v cyklu tak nemusíme
dopředu znát jeho délku, stačí cyklus ukončit, jakmile narazíme na znak '\0'
. Například funkce
pro spočtení délky řetězce by mohla vypadat takto3:
3Všimněte si, že tato funkce bere ukazatel na konstantní pole znaků.
Pokud ve funkci nepotřebujete měnit hodnoty pole, je obvykle dobrý nápad použít klíčové slovo
const
před datovým typem obsaženým v poli, aby vás překladač ohlídal, že se pole nesnažíte měnit.
Do takovéto funkce pak klidně můžete poslat i pole, které ve skutečnosti měnit lze, jinak řečeno
např. char*
lze bez problému převést na const char*
. V opačném směru konverze není korektní.
int delka_retezce(const char* retezec) {
int delka = 0;
// dokud není znak na adrese v ukazateli roven znaku NUL
while (*retezec != '\0') {
delka = delka + 1;
retezec = retezec + 1; // posuň ukazatel o jeden znak dále
}
return delka;
}
Tato funkce postupně projde všechny znaky řetězce a počítá, kolik jich je, dokud nenarazí na
znak '\0
. Pro procházení řetězce je zde použita
aritmetika s ukazateli.
Z toho vyplývá mimo jiné to, že znak NUL
nemůže být použit "uprostřed" řetězce. Pokud by tomu tak
bylo, tak funkce, které by s takovýmto řetězcem pracovaly, by při nalezení tohoto znaku přestaly
řetězec zpracovávat, a jakékoliv další znaky za NUL
by byly ignorovány. Uhodnete tak, co vypíše
následující program?
#include <stdio.h>
int main() {
char text[] = {'U', '\0', 'P', 'R', '\0'};
printf("%s\n", text);
return 0;
}
Řetězce jako pole
S řetězci pracujeme jako s klasickými poli znaků. Například pro získání prvního znaku řetězce můžeme použít operátor hranatých závorek:
char vrat_prvni_znak(const char* retezec) {
return retezec[0];
}
Funkce pro práci s řetězci
Standardní knihovna C obsahuje řadu funkcí, které umí s řetězci zakončenými nulou pracovat. Zde je seznam několika vybraných funkcí, které pro vás můžou být užitečné:
-
Zjištění délky řetězce: funkce
strlen
bere jako parametr řetězec a vrací jeho délku. Jedná se o jednu z nejčastěji používaných funkcí při práci s řetězci a vyplatí se ji tak znát.Při jejím použití je ovšem nutné si dát pozor na to, že délka provádění této funkce závisí na tom, jak je řetězec dlouhý. Pokud bude mít řetězec milion znaků, tak bude tato funkce muset projít všech milion znaků, dokud nenarazí na znak
NUL
. Dávejte si tak pozor, abyste tuto funkci nevolali zbytečně často. Například pokud použijete funkcistrlen
v podmínce cyklufor
:for (int i = 0; i < strlen(retezec); i++) { ... }
Tak se délka řetězce vypočte při každé iteraci cyklu. Pokud by tak řetězec měl milion znaků, musel by program provést bilion4 (!) operací pouze pro zjištění délky řetězce. Lepší volbou (pokud se tedy délka řetězce nemění) je tak předpočítat si jeho délku dopředu a uložit si ji do proměnné:
41 000 000 000 000
int delka = strlen(retezec); for (int i = 0; i < delka; i++) { ... }
-
Porovnání dvou řetězců: běžnou operací, kterou bychom s řetězci chtěli udělat, je porovnat, zdali jsou dva řetězce stejné, popřípadě který z nich je menší5. Funkce
strcmp
bere dva řetězce a vrací nulu, pokud se řetězce rovnají, zápornou hodnotu, pokud je první řetězec menší než ten druhý, a kladnou hodnotu, pokud je druhý řetězec menší než první.5Pro porovnávání řetězců se používá lexikografické uspořádání. Nalezne se první dvojice znaků (zleva), ve kterém se řetězce liší, a tyto dva znaky se porovnají pomocí jejich číselné (ASCII) hodnoty.
Pro porovnávání dvou řetězců nikdy nepoužívejte operátor
==
! Nebude to fungovat.
-
Vyhledání řetězce v řetězci: pokud chcete zjistit, jestli se v nějakém řetězci vyskytuje jiný řetězec, můžete použít funkci
strstr
. -
Převod textu na číslo: často můžete potřebovat převést textový zápis čísla na jeho číselnou hodnotu. K tomu můžete použít například funkci
strtol
(string to long). První parametr funkce je řetězec, který chcete převést, do druhého parametru můžete předat ukazatel na ukazatel na znak, do kterého se uloží pozice ve vstupním řetězci těsně za načteným číslem. Posledním parametrem je soustava, ve které se má číslo načíst (obvykle to bude desítková soustava, tedy hodnota10
). Návratovou hodnotou funkce je pak načtené číslo.Můžete použít také funkci
atoi
, která je trochu jednodušší na použití, ale při jejím použití nelze zjistit, zdali při konverzi nedošlo k chybě (například pokud vstupní řetězec nereprezentoval číslo).
Cvičení 🏋
Pro procvičení práce s řetězci si můžete zkusit některé z těchto funkcí sami naprogramovat. Další úlohy pro práci s řetězci můžete nalézt zde.
Kvíz 🤔
-
Co vypíše následující program?
#include <stdio.h> int main() { const char* str = "hello"; printf("%c\n", str[3]); printf("%c\n", str[2]); printf("%c\n", str[1]); return 0; }
Odpověď
Program vypíše:
l l e
Jelikož jsou řetězce poli znaků, tak při přistoupení na nějaký index řetězce získáme hodnotu datového typu znak.
-
Co vypíše následující program?
#include <stdio.h> int main() { const char* str = "hello"; for (int i = 0; i < 5; i++) { printf("%c", str[i] - 32); } printf("\n"); return 0; }
Odpověď
Program vypíše
HELLO
. Když se podíváte na ASCII tabulku, tak zjistíte, že rozdíl mezi čísly reprezentujícími jednotlivé znaky malé a velké anglické abecedy je32
, a že znaky malé abecedy jsou reprezentovány vyššími čísly. Když tak např. od'h'
odečteme hodnotu32
, získáme znak'H'
. Přehlednější by bylo napsat tuto konverzi jakostr[i] - ('a' - 'A')
nebo použít funkcitolower
ze standardní knihovny jazyka C.