C# týden Slevový týden - Březen
Využij náš slevový týden a získej až 30 % bodů navíc zdarma! Zároveň také probíhá C# týden se slevou na e-learning až 80 %
Hledáme fulltime programátora do ITnetwork týmu -100% homeoffice, 100% časově flexibilní #bezdeadlinu Mám zájem!

Lekce 4 - Dynamické textové řetězce a struktury v jazyce C

V minulé lekci, Aritmetika ukazatelů v jazyce C, jsme se věnovali ukazatelové aritmetice. V dnešním tutoriálu programování v jazyce C se znovu zaměříme na textové řetězce a struktury. Kromě toho, co jsme si ukazovali v prvních lekcích kurzu základů C, s nimi totiž lze pracovat dynamicky.

Dynamické textové řetězce

Když jsme chtěli uložit nějaký textový řetězec, ukládali jsme ho jako pole znaků (charů). U tohoto pole jsme museli pevně zadat jeho velikost, jak to již u polí v Céčku bývá. A zadali jsme ji buď explicitně:

char text[5] = "ahoj";

Pamatujte, že velikost je o 1 větší kvůli nulovému znaku '\0'.

Nebo jsme její dosazení nechali na Céčku:

char text[] = "ahoj";

Pokud ovšem chceme ukládat text, jehož délku předem neznáme (např. jména uživatelů, která zadají za běhu programu), máme 2 možnosti:

  1. Použít k ukládání textů pole znaků s pevnou velikostí (např. 21). Pro uživatele s délkou jména pod 20 znaků budeme plýtvat pamětí. Např. Jan Novák využije jen 9 znaků, 11 jich tedy zůstane v paměti neobsazených. Jméno uživatele, které je delší než 20 znaků, naopak musíme oříznout. Bernd Ottovordemgen­tschenfelde má tedy smůlu.
  2. Použít k ukládání textů ukazatele na char. Díky podobnosti polí a ukazatelů můžeme s takto vytvořenými dynamickými řetězci pracovat jako jsme byli zvyklí doposud a to dokonce i pomocí standardních funkcí. Bohužel se o řetězce budeme muset starat sami a tak přibudou funkce malloc() a free(). K načítání řetězce pomocí standardních funkcí budeme navíc stejně muset použít buffer (viz dále), což situaci zkomplikuje. Vyhnuli jsme se omezení v bodě 1, ale pro nás programátory bude aplikace mnohem pracnější.

V praxi se pro ukládání řetězců používají oba způsoby, každý má své výhody a nevýhody. Vyzkoušejme si je:

1. Vytvoření statického řetězce

Vytvoření statického řetězce se jménem, které uživatel zadá, by vypadalo takto:

char jmeno[21];
printf("Zadej jméno: ");
scanf(" %20[^\n]", jmeno);
printf("Jmenuješ se %s", jmeno);

Výsledek:

c_textove_retezce
Zadej jméno: Jan Novak
Jmenuješ se Jan Novak

2. Vytvoření dynamického řetězce

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

Ukažme si, jak by tedy vypadalo vytvoření řetězce, který je přesně tak dlouhý, jak ho uživatel zadal. Protože budeme k načítání z konzole využívat funkci scanf(), musíme si stejně vytvořit pomocný statický řetězec, do kterého text necháme scanf() uložit. Tomuto pomocnému poli se často říká buffer. Ukažme si kód, který si záhy vysvětlíme:

char buffer[101];
printf("Zadej jméno: ");
// Načtení jména do pomocné paměti
scanf(" %100[^\n]", buffer);
// Vytvoření dynamického řetězce
char* jmeno = (char *) malloc(strlen(buffer) + 1);
// Nastavení hodnoty
strcpy(jmeno, buffer);

printf("Jmenuješ se %s", jmeno);
// Uvolnění paměti
free(jmeno);

Do pomocného pole si načteme řetězec od uživatele, mělo by být dostatečně dlouhé, aby text pojalo. Podle délky zadaného řetězce následně vytvoříme nový řetězec dynamický, v paměti alokujeme pole charů. Asi vás nepřekvapí, že o 1 delší než je potřeba, kvůli nulovému znaku. Hodnotu řetězci nastavíme na tu z bufferu pomocí funkce strcpy(). Nyní máme dynamický řetězec hotový a můžeme ho používat jako jsme byli zvyklí doposud. Jen až ho přestaneme potřebovat, je ho nutné uvolnit.

Možná se ptáte, v čem je tedy výhoda dynamického řetězce, když nám paměť stejně vždy bude zabírat buffer, který potřebujeme pro načítání. Když budeme načítat a ukládat milion jmen, stačí nám k tomu stále jediný buffer a to jen ve fázi čtení (poté ho můžeme zahodit). Při dynamickém uložení takového počtu jmen ušetříme spoustu paměti, kterou by jinak zabraly nevyužité znaky. Daní za to je delší kód a nutnost myslet na uvolňování takto vytvořených řetězců. Proto toto řešení nelze prohlásit za univerzální, ačkoli ho osobně preferuji nad statickými řetězci.

Odkazy na struktury

Nyní se přesuňme ke slíbeným strukturám. Ty jsme zatím předávali hodnotou. To znamená, že kdykoli jsme ji uložili do nějaké proměnné, struktura se do ni zkopírovala. Vyzkoušejme si to na příkladu.

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

typedef struct {
    char jmeno[51];
    int vek;
    char ulice[51];
} UZIVATEL;

int main(int argc, char** argv) {
    UZIVATEL karel;
    strcpy(karel.jmeno, "Karel Nový");
    strcpy(karel.ulice, "Šikmá 5");
    karel.vek = 27;

    UZIVATEL uzivatel2 = karel;
    karel.vek = 15;
    printf("Jméno: %s\nUlice: %s\nVěk: %d", uzivatel2.jmeno, uzivatel2.ulice, uzivatel2.vek);

    return (EXIT_SUCCESS);
}

Kód výše vytvoří uživatele do proměnné karel a nastaví mu příslušné hodnoty. Následně vytvoří další strukturu uživatele jako proměnnou uzivatel2 a do ni uloží Karla. Protože je takto vytvořená struktura hodnotový typ, můžeme ji jednoduše celou přiřadit a zkopírovat. Změna věku Karla se uzivatel2 již netýká. Nakonec druhého uživatele vypíšeme, budou v něm ty samé hodnoty jako měl Karel, věk bude původní:

c_struktury2
Jméno: Karel Nový
Ulice: Šikmá 5
Věk: 27

Zde by se hodilo zmínit, že takovéto zkopírování celé proměnné najednou nebude fungovat u pole, jelikož je na rozdíl od struktury realizováno ukazatelem.

Předávání hodnotou je poněkud nepraktické a to hlavně když chceme nějakou strukturu změnit. Přidejme do programu výše funkci, která přijímá jako parametr strukturu typu UZIVATEL, kterého nechá zestárnout o 1 rok:

void zestarni(UZIVATEL uzivatel)
{
    uzivatel.vek++;
}

Nyní nechme Karla zestárnout a vypišme ho:

UZIVATEL karel;
strcpy(karel.jmeno, "Karel Nový");
strcpy(karel.ulice, "Šikmá 5");
karel.vek = 27;
zestarni(karel);
printf("Jméno: %s\nUlice: %s\nVěk: %d", karel.jmeno, karel.ulice, karel.vek);

Nic se nestane:

c_struktury2
Jméno: Karel Nový
Ulice: Šikmá 5
Věk: 27

Je to samozřejmě proto, že se Karel zkopíroval do parametru funkce a změnila se tato kopie, s původním Karlem se nestalo nic. Jedno z řešení by bylo upraveného Karla zas vrátit a přepsat. Když si ale uvědomíme, že se kopíruje postupně každá vlastnost struktury, procesor úplně zbytečně provádí velkou spoustu instrukcí. Navíc v aplikacích struktury stejně často tvoříme pomocí mallocu(), pokud s nimi chceme pracovat mimo funkce, ve kterých byly vytvořené. (Céčko totiž paměť po lokálních proměnných vytvořených ve funkci uvolňuje po ukončení funkce, nejsou tedy trvanlivé a nelze je vrátit).

Přepišme si program tak, aby používal ukazatele:

void zestarni(UZIVATEL* uzivatel)
{
    uzivatel->vek++;
}

int main(int argc, char** argv) {
    UZIVATEL* p_karel = malloc(sizeof(UZIVATEL));
    strcpy(p_karel->jmeno, "Karel Nový");
    strcpy(p_karel->ulice, "Šikmá 5");
    p_karel->vek = 27;

    UZIVATEL* p_uzivatel2 = p_karel;
    p_karel->vek = 15;
    zestarni(p_uzivatel2);
    printf("Jméno: %s\nUlice: %s\nVěk: %d", p_uzivatel2->jmeno, p_uzivatel2->ulice, p_uzivatel2->vek);

    free(p_karel);
    return (EXIT_SUCCESS);
}

Všimněte si, že pokud chceme získat data z ukazatele na strukturu, místo abychom před něj napsali dereferenční operátor (hvězdičku), tak zaměníme tečku za operátor šipky (->).

Výstup programu je následující:

c_struktury2
Jméno: Karel Nový
Ulice: Šikmá 5
Věk: 16

Aplikace nejprve alokuje paměť pro strukturu typu UZIVATEL a její adresu uloží do proměnné p_karel. Nastaví mu hodnoty a následně na Karla vytvoří ještě jeden ukazatel p_uzivatel2. Karlovi změní věk na 15 let a uživatele, na kterého ukazuje p_uzivatel2, nechá zestárnout o 1 rok. Karlovi tak bude 16, jelikož se mu věk předtím nastavil na 15. Vidíme, že oba ukazatele ukazují na stejného uživatele. Následně vypíšeme Karla pomocí pointeru p_uzivatel2. Paměť nezapomeneme uvolnit.

Možná vám to připadá jako zbytečné čarování s ukazateli, je ovšem velmi důležité pochopit jak předávání funguje na těchto malých příkladech, abychom se poté vyznali ve větších aplikacích.

Zdrojové projekty k dnešní lekci jsou níže ke stažení. Příště, v lekci Dynamická pole (vektory) v jazyce C, se naučíme vytvořit takovou datovou strukturu, která by nás nijak neomezovala svou velikostí a mohli jsme do ni přidávat stále nové a nové položky. Půjde o dynamické pole, kterému se někdy říká vektor. Máte se na co těšit :)


 

Stáhnout

Staženo 101x (65.43 kB)
Aplikace je včetně zdrojových kódů v jazyce c

 

 

Článek pro vás napsal David Čápka
Avatar
Jak se ti líbí článek?
6 hlasů
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor sítě se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.
Předchozí článek
Aritmetika ukazatelů v jazyce C
Všechny články v sekci
Dynamická práce s pamětí v jazyce C
Miniatura
Následující článek
Dynamická pole (vektory) v jazyce C
Aktivity (4)

 

 

Komentáře
Zobrazit starší komentáře (5)

Avatar
DarkCoder
Člen
Avatar
DarkCoder:12.5.2018 18:14
  1. Ano
  2. Ano, při volání hodnotou je struktura předaná jako celek.
Odpovědět
12.5.2018 18:14
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Martin Dráb
Redaktor
Avatar
Martin Dráb:12.5.2018 21:57
  1. v C to, co mám zkušenost, překladače většinou přejdou, maximálně vyhodí varování. malloc vrací ukazatel, který je přiřazován do ukazatele (ač ne obecného). V C++ ti zápis bez explicitního přetypování vyhodí chybu, tam víc dbají na přesnost typů.
Odpovědět
12.5.2018 21:57
2 + 2 = 5 for extremely large values of 2
Avatar
Patrik Pastor:30.8.2019 15:47

Neni mi jasna jedna vec. Operator '->' data hodnotu nebo adresu promenne? V prikladu to neni zrejme, protoze jednou je to pouzito jako adresa: `strcpy(p_karel->jmeno, ...) a podruhe hodnota : printf("%s", p_uzivatel->jmeno). Co vlastne znamena '->'? A proc nepouzivala i u struktury staticke? Kdyz jsem mel prece napr:

typedef struct{
   char zelenina[10];
   char ovoce[10];
} PLODINA;

tak plodina mela atribut 'ovoce' - ale to je pole, takze jsem prece taky mel pointer jako PLODINA.ovoce - byl taky pointer na zacatek pole ovoce a mel jsem operator tecku, tak proc by pointer u dynamickeho pole mel mit '->'? (oba dva jsou pointery, v cem se tedy lisi, ze jim c-cko dava jinaci operatory pristupu)?

Dik za odpoved

 
Odpovědět
30.8.2019 15:47
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Patrik Pastor
DarkCoder:30.8.2019 17:27

To záleží dle typu proměnné. Pokud je prvkem struktury pole nebo řetězec (pole znaků), pak se jedná o adresu, v opačném případě se jedná o hodnotu. Pole se nikdy nepředávají hodnotou, vždy se předává ukazatel na pole. Proto první argument funkce strcpy() je ukazatel na pole. To co se předává pomocí printf() se specifikátorem formátu %s je ukazatel na pole.

char pole[] = "Test";

printf("%s\n", pole); // nebo l0pe
puts(pole);

-> je šipkový operátor, . je tečkový operátor. Pro zpřístupnění prvku struktury pomocí strukturové proměnné se použije tečkový operátor, pro zpřístupnění prvku struktury pomocí ukazatele se použije šipkový operátor. Tečkový operátor lze v jisté formě použít i pro ukazatelovou proměnnou, ale tato forma se nepoužívá. Pokud budeš dodržovat Maďarskou notaci pro proměnné, budeš vždy vědět kdy používat tečkový a kdy šipkový operátor.

Následující program ukazuje práci s prvky struktury pomocí strukturové a ukazatelové proměnné:

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

#define SIZENAME 30

struct sType {
        int x;
        char name[SIZENAME];
};

int main(void) {
        struct sType s, *ps = &s;

        s.x = 10;
        ps->x = 10;
        (*ps).x = 10;

        scanf("%d", &(s.x));
        printf("%d\n", s.x);

        scanf("%d", &(ps->x));
        printf("%d\n", ps->x);

        scanf("%d", &((*ps).x));
        printf("%d\n", (*ps).x);

        strcpy(s.name, "Patrik Pastor");
        puts(s.name);

        strcpy(ps->name, "Patrik Pastor");
        puts(ps->name);

        strcpy((*ps).name, "Patrik Pastor");
        puts((*ps).name);

        return 0;
}
Odpovědět
30.8.2019 17:27
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Odpovídá na DarkCoder
Patrik Pastor:30.8.2019 18:20

proc je zase vyhodnejsi pouzit funkci puts na output nez printf. A co je madarska notace :D to bych vazne necekal, ze zrovna tato zeme v IT bude navrhovat notace, staci kdyz slysim ten jejich jazyk :D nic proti madarum.

 
Odpovědět
30.8.2019 18:20
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Patrik Pastor
DarkCoder:30.8.2019 19:15

Důvodem je efektivita. Funkce puts() používá pro výpis samotného řetězce méně strojových instrukcí než-li funkce printf(). Na druhou stranu je méně obecná, nelze ji použít pro výpis jiného typu nežli řetězce a dochází u ní k odřádkování. Je na programátorovi, aby použil vhodnou funkci.

Maďarská notace je konvence popisu proměnných a funkcí. Jméno proměnné nebo funkce pak obsahuje identifikátor vyjadřující návratovou hodnotu. Více na: Maďarská notace

Odpovědět
30.8.2019 19:15
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Odpovídá na DarkCoder
Patrik Pastor:30.8.2019 19:50

co se tyka toho odradkovani, to uz jsi zminoval pri analyze vstupniho retezce (tehda jsme se bavili o funkci fgets), nicmene samotny presne nerozumim, jak je odradkovani mysleno, nebo spise jaky to ma dopad na vstup/vystup. Vim pouze ze se odradkovani znaci '\n', ale - to uz jsem se kdysi ptal - neni to jediny bily znak (znam <cr> <_> a <tab>)
pro tyto dva zbyla ma cko znak? (pro tab to bude asi '\t') - nicmene, chybi mi v cecku "univerzalni sekvence pro bily znak" - pouzivam textovy editor "vim", a zde je univerzalni byly znak znace '\s'. Ma neco podobneho cecko? - protoze zatim jsem videl pouze znak \n uplne na vsechno. Stale me spis ale zajima, jake dusledky muze mit odradkovani na vstupu a vystupu

 
Odpovědět
30.8.2019 19:50
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Patrik Pastor
DarkCoder:30.8.2019 20:13

Když je ve formátovacím řetězci funkce printf() nalezen znak '\n', převede se na dvojici znaků "návrat na začátek řádku" a "posun o řádek".

printf("Tento text bude odradkovan\n");

// lze napsat i takto:

puts("Tento text bude odradkovan");

Při čtení bílé znaky oddělují hodnoty. Při výstupu mají vliv na formát výpisu. V C není žádný univerzální identifikátor pro bílý znak. Každý bílý znak má nějakou hodnotu.

' ' - mezera, '\t' - horizontální tabulator, '\v' - vertikální tabulator, '\n' - nový řádek, '\f' - nová stránka, '\r' - návrat na začátek řádku.

Kromě těchto jsou další znaky s lomítkem (escape sekvence).

Odpovědět
30.8.2019 20:13
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Odpovídá na DarkCoder
Patrik Pastor:30.8.2019 20:15

jake to jsou vsechny, protoze jak rikam - asi se budou lisit na ty, na ktere jsem zvykly z vimu. Je nejaka dokumentace k escape sekvencim?

 
Odpovědět
30.8.2019 20:15
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Patrik Pastor
DarkCoder:30.8.2019 20:23

Seznam escape sekvencí:

\a Alarm or Beep
\b Backspace
\f Form Feed
\n New Line
\r Carriage Return
\t Tab (Horizontal)
\v Vertical Tab
\\ Backslash
\' Single Quote
\" Double Quote
\? Question Mark
\ooo octal number
\xhh hexadecimal number
\0 Null

Editováno 30.8.2019 20:23
Odpovědět
30.8.2019 20:23
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 15. Zobrazit vše