Jste zde

Méně známé skutečnosti o C a C++: Různé formy sizeof

Možná jste se již setkali s různými zápisy použití operátoru sizeof. Konkrétně se jedná např. o zápisy sizeof(int), sizeof(*p) a sizeof *p. Jaký je mezi těmito zápisy rozdíl a kdy je nutné použít závorky se dočtete v následujícím příspěvku.

Co je to ono sizeof?

Ve světě jazyků C a C++ je sizeof operátor, který slouží pro zjištění velikosti typu či výrazu (viz sekce 6.5.3 v ISO C11 standardu a sekce 5.3.3 v ISO C++11 standardu). Navrácená hodnota je v bajtech. Jedno z jeho možných použití je při dynamické alokaci paměti:

int *p = malloc(10 * sizeof(int));

S tímto užitím jste se pravděpodobně již setkali. Jak jistě víte, tak funkce malloc() vrací ukazatel na dynamicky alokovanou paměť o zadané velikosti (či nulový ukazatel při selhání). Pokud bychom na tomto místě nepoužili sizeof, tak by náš kód nebyl přenositelný. Skutečně, následující kód předpokládá, že velikost typu int je 32 bitů (4 bajty):

int *p = malloc(40);

Dále, pokud bychom napsali jen

int *p = malloc(10);

tak dostaneme ukazatel na paměť o 10 bajtech, nikoliv paměť pro uložení 10 čísel typu int (to bychom potřebovali oněch 10 * sizeof(int) bajtů).

Co je na tom zajímavého?

Na výše uvedeném použití sizeof není zajímavého nic. Možná jste se ale již setkali s následující alokací dynamické paměti:

int *p = malloc(10 * sizeof *p);

Pokud jste se s tímto nesetkali, tak vaše reakce bude pravděpodobně něco ve stylu: "Huh?!". Pojďme si to vysvětlit. Když nahlédneme do normy jazyků C a C++ (viz reference uvedené na začátku příspěvku), tak zjistíme, že sizeof je operátor (stejně jako třeba + či ++) a že jej lze použít dvěma způsoby:

sizeof (type-id)
sizeof unary-expression

U našeho prvního příkladu jsme použili první způsob, tedy za sizeof jsme uvedli v závorkách typ. Podobně bychom mohli použít něco takového:

typedef struct { int x; int y; } Point;
Point *p = malloc(sizeof(Point));

Výše uvedený kód nám alokuje místo pro jeden objekt typu Point. Pokud bychom napsali

Point *p = malloc(sizeof Point); // způsobí chybu při překladu

tak nám překladač vynadá, protože v případě typu je nutné za sizeof uvést závorky. Když ale napíšeme

Point *p = malloc(sizeof *p);

tak je vše v pořádku. Výše zmíněný kód je pak funkčně ekvivalentní tomu původnímu kódu, kde jsme použili sizeof(Point). Důvod je ten, že nyní používáme druhou formu sizeof, která nám umožňuje získat velikost typu podle předaného výrazu.

Pokud nyní přemýšlíte nad tím, jak to může fungovat, když se zdá, že přistupujeme na neinicializovaný ukazatel, tak důvodem je, že sizeof *p nám vrátí pouze velikost operandu, aniž by jej vyhodnocovalo (viz výše zmíněné odkazy do normy). Toto použití je tedy naprosto bezpečné.

Kdy je vhodné použít onen druhý způsob?

Já osobně používám druhý způsob na místech, kde to je nutné či to zlepší udržovatelnost programu. Jako ukázku si uvedeme případ, kdy chci v Céčku iterovat přes všechny položky staticky alokovaného pole:

int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (size_t i = 0; i < sizeof numbers / sizeof numbers[0]; ++i) {
    // Zde něco provedeme s numbers[i].
}

Toto je udržovatelnější, než

int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (size_t i = 0; i < 10; ++i) {
    // ...
}

či

#define SIZE 10
int numbers[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (size_t i = 0; i < SIZE; ++i) {
    // ...
}

protože v prvním případě není při změně (přidání, odebrání) položek z pole nutné změnit nic dalšího. To snižuje pravděpodobnost, že někde něco zapomeneme změnit, což je z hlediska udržovatelnosti žádané.

Další ukázka, kdy se výše uvedený postup hodí, je tento:

int *p = malloc(10 * sizeof(int));

Místo toho je z hlediska udržovatelnosti vhodnější psát

int *p = malloc(10 * sizeof *p);

protože pokud bychom změnili typ p na něco jiného, než int *, tak nám tuto změnu stačí provést na jediném místě: v int *p. V opačném případě se nám může stát, že zapomeneme změnit typ v operátoru sizeof, což může mít fatální důsledky.

Co říci na závěr?

Jak jsem již zmiňoval, tak při výše uvedeném použití druhé formy operátoru sizeof nejsou závorky nutné. Je to ze stejného důvodu, proč nejsou nutné u výrazu ++(i). Konstrukce ++ je operátor, stejně jako sizeof. Tudíž, co je třeba si uvědomit, tak je, že sizeof není funkce, nýbrž operátor. U použití

int *p = malloc(10 * sizeof(int));

jsou pak ty závorky kolem int součástí operandu, nikoliv sizeof, tedy možná méně matoucí by bylo to psát s mezerou:

int *p = malloc(10 * sizeof (int));

To se ale většinou nedělá. Tak či tak, já osobně tam ty závorky doporučuji psát, i když tam nejsou nutné, protože velmi málo lidí ví, kdy závorky jsou a nejsou nutné a takový kód by je mohl zbytečně zaskočit. Proto osobně píši

int *p = malloc(10 * sizeof(*p));

místo

int *p = malloc(10 * sizeof *p);

Co se týče nové normy jazyka C++ (ISO C++11), tak tam nám v souvislosti se zavedením podpory pro variadické šablony (šablony s proměnným počtem typových parametrů) přibyla ještě jedna forma sizeof:

sizeof ... (identifier)

O té ale někdy jindy :).

Komentáře

Dík za fajn články o zaujímavých/nových vlastnostiach C/C++. Osobne používam pre zistenie veľkosti staticky alokovaného poľa nasledovnú konštrukciu, ktorá zvyšuje čitateľnosť programu a je zároveň udržiavateľná:

#define Array_size(static_array) ( sizeof(static_array) / sizeof(*(static_array)) )
 
int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (size_t i = 0; i < Array_size(numbers); ++i) {
    // Zde něco provedeme s numbers[i].
}

Díky za tip!

Přidat komentář