Struktury

Struktury (structures) nám umožňují popsat nový datový typ, který se bude skládat z jednoho či více tzv. členů (members)1. Každému členu musíme určit jeho jméno a datový typ. Novou strukturu můžeme popsat pomocí tzv. deklarace struktury:

1Můžete se setkat také s názvy atribut (attribute), vlastnost (property) nebo field. V kontextu struktur C označují všechny tyto názvy jedno a to samé - člena struktury.

struct <název struktury> {
    <datový typ prvního členu> <název prvního členu>;
    <datový typ druhého členu> <název druhého členu>;
    <datový typ třetího členu> <název třetího členu>;
    ...
};

Při deklaraci struktury nezapomínejte na finální středník za složenými závorkami, je povinný.

Například, pokud bychom chtěli vytvořit datový typ reprezentující příšeru, která má své jméno a počet životů, můžeme deklarovat následující strukturu:

struct Prisera {
    const char* jmeno;
    int pocet_zivotu;
};

Tento kód sám o sobě nic neprovádí! Pouze pomocí něj říkáme překladači, že vytváříme nový datový typ s názvem struct Prisera. Poté nám překladač umožní dále v programu vytvořit například lokální proměnnou tohoto datového typu:

// lokální proměnná s názvem `karel` a datovým typem `struct Prisera`
struct Prisera karel;

Pro pojmenovávání struktur používejte v rámci předmětu UPR jmennou konvenci PascalCase.

Struktury jsou plnohodnotnými datovými typy. Můžete tak vytvářet ukazatele na struktury, pole struktur, můžete použít struktury jako členy jiné struktury atd.

Reprezentace struktury v paměti

Pokud vytvoříme proměnnou datového typu struktury, tak překladač naalokuje paměť pro všechny členy této struktury. V případě výše by proměnná karel obsahovala nejprve byty pro ukazatel const char* a poté byty pro int. Členové struktury budou v paměti uloženi ve stejném pořadí, v jakém byli popsáni při deklaraci struktury. Neznamená to ovšem, že musí ležet hned za sebou! Překladač se může rozhodnout mezi členy struktury v paměti vložit mezery (tzv. padding) kvůli urychlení provádění programu. Více detailů se můžete dozvědět v podkapitole Reprezentace struktur v paměti.

Prozatím si zapamatujte, že pro zjištění velikosti struktury v bytech (například při dynamické alokaci paměti) vždy používejte operátor sizeof a nesnažte se velikost "tipovat" ručně.

Umístění a platnost struktur

Stejně jako u proměnných platí, že strukturu lze používat pouze v oblasti, ve které je platná (v jejím tzv. scopu). Narozdíl od funkcí lze struktury deklarovat i uvnitř funkcí, nicméně nejčastěji se struktury deklarují na nejvyšší úrovni souboru (tzv. global scope), stejně jako funkce.

Inicializace struktury

Stejně jako u základních datových typů a polí platí, že pokud lokální proměnné s datovým typem nějaké struktury nedáte počáteční hodnotu, tak bude její hodnota nedefinovaná 💣. Strukturu můžete nainicializovat pomocí složených závorek se seznamem hodnot pro jednotlivé členy struktury:

struct Prisera karel = { "Karel", 100 };

Stejně jako u polí platí, že hodnoty, které nezadáte, se nainicializují na nulu:

struct Prisera karel = {}; // `jmeno` i `pocet_zivotu` bude `0`
struct Prisera karel = { "Karel" }; // `jmeno` bude "Karel", `pocet_zivotu` bude `0`

Abyste si nemuseli pamatovat pořadí členů struktury při její inicializaci, můžete jednotlivé členy nainicializovat explicitně pomocí tečky a názvu daného členu:

struct Prisera karel = { .pocet_zivotu = 100, .jmeno = "Karel" };

Jednotlivé hodnoty členům se přiřazují zleva doprava, takže pokud použijete název nějakého členu více než jednou, "zvítězí" poslední zadaná hodnota. Tomuto se však vyhněte, a ani nekombinujte inicializaci pomocí pořadí a pomocí názvů členů. Takovýto kód by totiž byl značně nepřehledný.

Přístup ke členům struktur

Abychom mohli číst a zapisovat jednotlivé členy struktur, můžeme použít operátor přístupu ke členu (member access operator), který má syntaxi <výraz typu struktura>.<název členu>:

#include <stdio.h>

struct Osoba {
    int vek;
    int pocet_pratel;
};

int main() {
    struct Osoba martina = { .vek = 18, .pocet_pratel = 10 };
    martina.vek += 1;           // přístup k členu `vek`
    martina.pocet_pratel += 20; // přístup k členu `pocet_pratel`
    printf("Martina ma %d let a ma %d pratel\n", martina.vek, martina.pocet_pratel);

    return 0;
}

Pokud máme k dispozici pouze ukazatel na strukturu, tak je přístup k jejím členům trochu nepraktický kvůli prioritě operátorů. Operátor dereference (*) má totiž menší prioritu než operátor přístupu ke členu (.). Abychom tak nejprve z ukazatele na strukturu načetli její hodnotu a až poté přistoupili k jejímu členu, museli bychom použít závorky:

void pridej_pratele(struct Osoba* osoba) {
    (*osoba).pocet_pratel++;
}

Jelikož ukazatele na struktury jsou využívány velmi často, C nabízí pro tuto situaci zkratku v podobě operátoru přístupu k členu přes ukazatel (member access through pointer), který má syntaxi <ukazatel na strukturu>-><název členu>:

void pridej_pratele(struct Osoba* osoba) {
   osoba->pocet_pratel++;
}

Operátor -> je čistě syntaktickou zkratkou, tj. platí *(ukazatel).clen == ukazatel->clen.

Vytváření nových jmen pro datové typy

Možná vás napadlo, že psát při každém použití struktury klíčové slovo struct před jejím názvem je zdlouhavé. C umožňuje dávat datovým typům nové názvy, aby se nám s nimi lépe pracovalo. Lze toho dosáhnout pomocí syntaxe typedef <datový typ> <jméno>;:

typedef int teplota;

int main() {
    teplota venkovni = 24;
    return 0;
}

Pomocí typedef vytvoříme nové jméno pro datový typ, pomocí kterého se pak na tento typ můžeme odkazovat (původní název datového typu to však nijak neovlivní a můžeme ho stále používat). Opět platí, že takto vytvořené jméno lze použít pouze v oblasti (scopu), kde byl typedef použit. Obvykle se používá na nejvyšší úrovni souboru.

U struktur si pomocí typedef můžeme zkrátit jejich název, typicky ze struct <nazev> na <nazev>:

struct Osoba {
    int vek;
};

typedef struct Osoba Osoba;

int main() {
    Osoba jiri;
    return 0;
}

Toto lze ještě více zkrátit, pokud deklaraci struktury použijeme přímo na místě datového typu v typedef:

typedef struct Osoba {
    int vek;
} Osoba;

A konečně, abychom nemuseli jméno struktury opakovat dvakrát, můžeme vytvořit tzv. anonymní strukturu (anonymous structure) bez názvu, a jméno jí přiřadit až pomocí typedef.

typedef struct {
    int vek;
} Osoba;

Právě takto se obvykle deklarují struktury v C.

Použití struktur ve strukturách

Jelikož deklarace struktury vytvoří nový datový typ, nic vám nebrání v tom používat struktury jako členy jiných struktur3:

3Lze si můžete všimnout, že vnořené struktury lze inicializovat stejně jako proměnné struktur, tj. pomocí složených závorek {}.

typedef struct {
    float x;
    float y;
} Poloha;

typedef struct {
    const char* jmeno;
    int cena;
} Vec;

typedef struct {
    int pocet_zivotu;
    Poloha poloha;
    Vec korist[10];
} Prisera;

int main() {
    Prisera prisera = { .pocet_zivotu = 100, .poloha = { .x = 0, .y = 0 } };

    return 0;
}

Díky tomu můžeme vytvářet celé hierarchie datových typů, což může značně zpřehlednit náš program, protože můžeme pracovat s kódem na vyšší úrovni abstrakce.

Rekurzivní struktury

Pokud bychom chtěli použít jako člena struktury tu stejnou strukturu (například struktura Osoba může mít člen matka opět s datovým typem Osoba), nemůžeme takovýto člen uložit ve struktuře přímo, můžeme tam uložit pouze jeho adresu4:

4Zde si můžete všimnout, že musíme použít struct Osoba pro datový typ členu matka. Je to z toho důvodu, že v momentě, kdy tento člen definujeme, tak ještě není platný typedef, ve kterém se struktura nachází, takže datový typ Osoba zatím neexistuje. Nové jméno pro datový typ lze používat až za středníkem daného typedefu. V tomto případě také nemůžeme vytvořit strukturu jako anonymní, ale musíme ji rovnou pojmenovat (typedef struct Osoba ...).

typedef struct Osoba {
    int vek;
    struct Osoba* matka;
} Osoba;

Je to proto, že pokud by Osoba byla definována pomocí Osoby, tak by došlo k rekurzivní definici, kterou nelze vyřešit. Nešlo by totiž určit velikost Osoby - její velikost by závisela na velikosti jejího členu matka, jehož velikost by závisela na velikosti jeho členu matka atd. Proto tedy musíme v tomto případě použít ukazatel, který má fixní velikost, ať už ukazuje na jakýkoliv typ.