4.1 Egész típusok és adatmodellek

Egy számláló, memóriaméret vagy protokollmező típusának kiválasztása reprezentációs szerződés: milyen értékeket kell tárolni, és milyen feltételezések maradhatnak igazak másik platformon is? C-ben az int nem jelent automatikusan 32 bitet, a long nem feltétlenül pointerméretű, és még a „bájt” sem szükségképpen nyolc bit.

A Szabványok, dialektusok és hordozhatósági szerződés megkülönböztetését követve külön kezeljük az ISO C17 garanciáit, az ABI által rögzített tulajdonságokat és a saját programmal megfigyelt értékeket. A labor a main, startup, argumentumok és környezet részben megismert hosted programmodellre épül.

4.1.1 Egész típusok, C bájtok és értéktartományok

Az egész típus (integer type) diszkrét egész értékek tárolására szolgál. Egy típus értéktartománya nem azonos a számára fenntartott tároló bitmintáinak halmazával: a reprezentáció kitöltőbiteket (padding bits) is tartalmazhat.

ISO C17. A sizeof eredményének egysége egy C bájt, amelyet a könyvben C_byte néven különböztetünk meg a nyolcbites oktettől. Egy C bájt bitszámát a <limits.h> fejléc CHAR_BIT makrója adja meg:

CHAR_BIT≥8. \mathrm{CHAR\_BIT}\geq 8.

Mindig teljesül:

sizeof(𝚌𝚑𝚊𝚛)=sizeof(𝚜𝚒𝚐𝚗𝚎𝚍 𝚌𝚑𝚊𝚛)=sizeof(𝚞𝚗𝚜𝚒𝚐𝚗𝚎𝚍 𝚌𝚑𝚊𝚛)=1. \operatorname{sizeof}(\texttt{char}) = \operatorname{sizeof}(\texttt{signed char}) = \operatorname{sizeof}(\texttt{unsigned char}) =1.

Egy T típusú objektum tárolója tehát

sizeof(T)⋅CHAR_BIT \operatorname{sizeof}(T)\cdot\mathrm{CHAR\_BIT}

bitből áll. Ez tárolási bitszám, nem feltétlenül az értéket hordozó bitek száma.

A char, signed char és unsigned char három különböző típus. A sima char a másik kettő valamelyikével azonos tartományú és reprezentációjú; a választás implementáció által meghatározott. Ellenőrzésére a CHAR_MIN alkalmas: ha nulla, a sima char előjel nélküliként viselkedik.

A short, int, long és long long önmagában előjeles típust jelöl. Például a short, short int és signed short int ugyanaz a típus. Az előjel nélküli változatokat az unsigned jelöli; az önálló unsigned jelentése unsigned int.

ISO C17 minimumkövetelmények: az alábbi tartományokat a típusoknak legalább le kell fedniük.

Előjeles típus Garantáltan lefedett tartomány Előjel nélküli pár garantált tartománya
signed char [−127,127][-127,127] [0,255][0,255]
short [−32767,32767][-32767,32767] [0,65535][0,65535]
int [−32767,32767][-32767,32767] [0,65535][0,65535]
long [−2147483647,2147483647][-2147483647,2147483647] [0,4294967295][0,4294967295]
long long [−(263−1),263−1][-(2^{63}-1),2^{63}-1] [0,264−1][0,2^{64}-1]

A tényleges határokat a <limits.h> adja: például INT_MIN, INT_MAX, UINT_MAX, illetve LONG_MIN, LONG_MAX, ULONG_MAX. A long long megfelelői LLONG_MIN, LLONG_MAX és ULLONG_MAX.

A méretekre:

1=sizeof(𝚌𝚑𝚊𝚛)≤sizeof(𝚜𝚑𝚘𝚛𝚝)≤sizeof(𝚒𝚗𝚝)≤sizeof(𝚕𝚘𝚗𝚐)≤sizeof(𝚕𝚘𝚗𝚐 𝚕𝚘𝚗𝚐). 1=\operatorname{sizeof}(\texttt{char}) \leq\operatorname{sizeof}(\texttt{short}) \leq\operatorname{sizeof}(\texttt{int}) \leq\operatorname{sizeof}(\texttt{long}) \leq\operatorname{sizeof}(\texttt{long long}).

Az egyenlőség megengedett. Az int és a long attól még külön típus, hogy ugyanakkora.

Bizonyításvázlat az unsigned tartományhoz. Ha egy előjel nélküli típusnak vv értékbitje van, a legnagyobb érték minden értékbitet egyre állít:

1+2+4+⋯+2v−1=2v−1. 1+2+4+\cdots+2^{v-1}=2^v-1.

Tartománya ezért [0,2v−1][0,2^v-1]. A lényeges feltétel az értékbitek száma: kitöltőbitek jelenlétében vv nem helyettesíthető automatikusan a teljes tárolási bitszámmal.

4.1.2 Adatmodellek és ABI-határok

Az adatmodell (data model) néhány alapvető típus szélességének együttese. Az ILP32 az int, long és pointer 32 bites szélességére utal. LP64 esetén a long és a pointer 64 bites, az int 32 bites. LLP64 esetén a long long és a pointer 64 bites, de az int és a long 32 bites.

Az alábbi ABI-profilokban CHAR_BIT == 8; a méretek C bájtban szerepelnek. Ezek profilhoz kötött értékek, nem ISO-garanciák és nem közölt mérési eredmények.

Típus ILP32: Linux i386, System V LP64: Linux x86-64, System V LLP64: Windows x64
char 1 1 1
short 2 2 2
int 4 4 4
long 4 8 4
long long 8 8 8
void * 4 8 8
Fordítási profil GCC 14.2.0, -std=c17 -m32 GCC 14.2.0, -std=c17 -m64 MSVC 19.40, x64, /std:c17

A modellnevek önmagukban nem határozzák meg például a short méretét vagy a típusok igazítását; a táblázat ezért konkrét ABI-példákat is megnevez.

Gyakorlati következmény: a „64 bites rendszer” nem elegendő egy típusméret meghatározásához. Egy Linuxon pointerméretűnek bizonyuló unsigned long Windows x64 alatt csak 32 bites lehet. A gépi szóhossz, az egész típusok mérete és a pointerméret külön tulajdonság.

A táblázat szerializálási formátumot sem definiál. Egy long objektum tárolójának közvetlen kiírása nem eredményez hordozható protokollmezőt; ehhez külön reprezentációs és bájtsorrend-szerződés szükséges.

4.1.3 Szélesség és feladat szerint választott típusok

4.1.3.1 Exact-width, least és fast típusok

ISO C17. A <stdint.h> többféle típuscsaládot kínál.

Az exact-width, vagyis pontos szélességű típusok neve például int32_t és uint32_t. Ezek pontosan a megadott bitszámúak, kitöltőbit nélkül. Az int32_t tartománya [−231,231−1][-2^{31},2^{31}-1], a uint32_t tartománya [0,232−1][0,2^{32}-1].

Ezek a típusok opcionálisak: csak megfelelő alaptípus rendelkezésre állásakor léteznek. Elérhetőségük a hozzájuk tartozó határmakróval ellenőrizhető, például #ifdef UINT32_MAX segítségével.

A pontos bitszám nem jelent rögzített C bájtszámot. Ha uint32_t létezik:

sizeof(𝚞𝚒𝚗𝚝𝟹𝟸_𝚝)⋅CHAR_BIT=32. \operatorname{sizeof}(\texttt{uint32\_t})\cdot\mathrm{CHAR\_BIT}=32.

Ez nyolcbites C bájttal négy, tizenhat bitessel két C bájt. Utóbbi környezetben uint8_t nem létezhet.

A least-width család, például uint_least32_t, legalább a megadott szélességhez szükséges tartományt biztosítja, a követelményt kielégítő legkisebb méretű megfelelő egész típussal. A fast-width család, például uint_fast32_t, az implementáció által rendszerint leggyorsabbnak kijelölt, legalább ilyen tartományú típust adja. A fast nem egy konkrét alkalmazáson végzett teljesítménymérés eredménye; egy uint_fast32_t lehet 64 bites is.

A 8, 16, 32 és 64 szélességű least és fast családok kötelezők. A megfelelő határmakrók például UINT_LEAST32_MAX és INT_FAST32_MIN.

A typedef-nevek nem hoznak létre új, elkülönült egész típusokat. A uint32_t valamely létező egész típus alternatív neve; nem szabad feltételezni, hogy ez mindenhol unsigned int.

4.1.3.2 Méretek, különbségek és pointerek egész reprezentációja

Típus Rendeltetés Fontos korlát
size_t A sizeof eredményének előjel nélküli típusa Nem szükségképpen unsigned long; felső határa SIZE_MAX
ptrdiff_t Pointerkivonás előjeles eredménytípusa A különbségnek ábrázolhatónak kell lennie
uintptr_t Opcionális előjel nélküli típus void * oda-vissza konverziójához Nem hordozható hálózati címformátum

A size_t és ptrdiff_t elérhető a <stddef.h> fejlécből. Pointerkivonásnál az azonos tömbhöz tartozás feltételeit is teljesíteni kell; ezt a Pointeraritmetika, objektumhatár és provenance tárgyalja. A különbség elemekben, nem általában C bájtokban értendő.

A uintptr_t a <stdint.h> opcionális típusa. Szerződése szerint egy érvényes void * érték ilyen egésszé, majd vissza void * típussá alakítva az eredetivel egyenlő pointert ad. Ez nem jogosít fel tetszőleges egész érték pointerként történő használatára, és nem konzerválja a hivatkozott objektum élettartamát.

4.1.4 Előjeles reprezentáció: C17 és C23

ISO C17. Az általános előjeles egész típusokhoz három reprezentáció megengedett: előjel–abszolútérték (sign-and-magnitude), egyes komplemens (ones’ complement) és kettes komplemens (two’s complement).

Nyolc tárolási bit, kitöltőbit nélküli ábrázolás és minden bitminta értékként kezelése mellett az első kettő [−127,127][-127,127], a kettes komplemens [−128,127][-128,127] tartományt ad. Az első kettő negatív zérót is kódol. Ez magyarázza a C17 minimumtáblázatának szimmetrikus negatív határait.

Fontos kivétel: az opcionális intN_t típusok már C17-ben is kettes komplemensűek.

C23-eltérés. Az előjeles egész típusok reprezentációja kettes komplemensre szűkül. Ettől azonban az int nem lesz kötelezően 32 bites, a C bájt nem lesz kötelezően oktett, és a típusméretek sem válnak platformfüggetlenné.

A reprezentációt a műveleti szabályoktól is el kell választani: az előjeles túlcsordulás C23-ban sem válik általánosan körbeforduló aritmetikává. A konverziókat és túlcsordulást a Promóciók, signedness és túlcsordulás rész tárgyalja.

4.1.5 Megfigyelési labor

A következő önálló program minimuma ISO C17, hosted környezet. Választott laborprofil: Linux x86-64 System V, LP64, glibc 2.40; fordítók GCC 14.2.0 és Clang 19.1.7. A helyi eszközverziókat külön ellenőrizni kell. A példához itt nem tartozik tényleges fordítási vagy futtatási eredmény.

/* integer_probe.c */
#include <inttypes.h>
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    printf("CHAR_BIT=%d\n", CHAR_BIT);
    printf("sizeof: char=%zu short=%zu int=%zu "
           "long=%zu long long=%zu void*=%zu\n",
           sizeof(char), sizeof(short), sizeof(int),
           sizeof(long), sizeof(long long), sizeof(void *));

    printf("char: %jd..%ju\n",
           (intmax_t)CHAR_MIN, (uintmax_t)CHAR_MAX);
    printf("int: %d..%d; unsigned int: 0..%u\n",
           INT_MIN, INT_MAX, UINT_MAX);
    printf("long: %ld..%ld; unsigned long: 0..%lu\n",
           LONG_MIN, LONG_MAX, ULONG_MAX);
    printf("long long: %lld..%lld\n", LLONG_MIN, LLONG_MAX);

    printf("size_t: %zu C_byte; max=%zu\n",
           sizeof(size_t), (size_t)SIZE_MAX);
    printf("ptrdiff_t: %zu C_byte; %td..%td\n",
           sizeof(ptrdiff_t),
           (ptrdiff_t)PTRDIFF_MIN, (ptrdiff_t)PTRDIFF_MAX);

#ifdef UINT32_MAX
    uint32_t field = 42;
    printf("uint32_t: %zu C_byte; value=%" PRIu32 "\n",
           sizeof(field), field);
#else
    puts("uint32_t nem erheto el");
#endif

#ifdef UINTPTR_MAX
    printf("uintptr_t: %zu C_byte\n", sizeof(uintptr_t));
#else
    puts("uintptr_t nem erheto el");
#endif

    return 0;
}

Fordítás a megadott kiadásokra mutató gcc és clang parancsokkal:

gcc -std=c17 -m64 -Wall -Wextra -Wpedantic -Wconversion -O0 -g integer_probe.c -o integer_probe-gcc
clang -std=c17 -m64 -Wall -Wextra -Wpedantic -Wconversion -O0 -g integer_probe.c -o integer_probe-clang

A formátumok a típusok szerződését követik: size_t esetén %zu, ptrdiff_t esetén %td, uint32_t esetén a <inttypes.h> által adott PRIu32 használható. A char határainak kiírásánál az intmax_t és uintmax_t minden megfelelő előjeles, illetve előjel nélküli egész érték befogadására alkalmas típust biztosít.

A labor megfigyeléseit két lépésben értelmezzük. Először ellenőrizzük az ISO-minimumokat és méretrelációkat. Ezután összevetjük az adatokat a kiválasztott ABI-profillal. Egy LP64-nek megfelelő eredmény nem bizonyítja, hogy ugyanaz a program minden platformon ugyanilyen méreteket kap.

4.1.6 Gyakori hibák és javításuk

A sizeof(T) * 8 kifejezés rejtett oktettfeltételezés. Tárolási bitszámhoz CHAR_BIT szükséges, értéktartományhoz pedig a megfelelő határmakró.

A uint32_t feltétel nélküli használata elrejti az opcionális típus követelményét. Ha pontos szélesség kell, ellenőrizzük az elérhetőséget; ha csak legalább ekkora tartomány kell, válasszunk uint_least32_t típust.

A méretérték unsigned long változóba másolása LP64-en működőképesnek tűnhet, LLP64-en azonban értékvesztést okozhat. Méretekhez alapértelmezésben size_t való.

A formátumspecifikátort sem a feltételezett mérethez, hanem a tényleges típushoz kell választani. Az azonos bitszám önmagában nem tesz helyessé egy variadikus printf hívást.

4.1.7 Feladatok

  1. Magyarázó feladat. Mely állítások ISO C17-garanciák: sizeof(char) == 1, CHAR_BIT == 8, sizeof(int) == 4, INT_MAX >= 32767? Következik-e nyolcbites C bájtból a négybájtos int?

  2. Kódolási feladat. Egészítsd ki a laborprogramot a uint_least32_t és uint_fast32_t méretének és felső határának kiírásával. Használj típushelyes formátumokat.

  3. Diagnosztikai feladat. Az alábbi, már deklarált payload objektumra vonatkozó C17-részlet miért hordozhatósági hiba LLP64-en? Javítsd ki. Feltételezz olyan implementációt, amelyen az objektum mérete meghaladhatja az ULONG_MAX értékét.

    /* Hibás hordozhatósági feltételezés; szemléltető részlet. */
    unsigned long bytes = sizeof payload;
    printf("%lu\n", bytes);

4.1.8 Megoldások

4.1.8.1 Az ISO-garanciák azonosítása

Az első és a negyedik állítás garantált. A másik kettő platformfüggő.

Nyolcbites C bájttal az int legalább két C bájtot igényel, mert legalább 65535 különböző értéket kell tudnia tárolni, miközben egyetlen ilyen bájtnak csak 256 bitmintája van. Ebből nem következik a négybájtos méret.

Ellenőrzési feltétel: az indoklás tartományból és szabványos garanciából induljon ki, ne egyetlen gép méréséből.

4.1.8.2 A típuscsaládok megfigyelése

A következő C17-részlet a laborprogram main függvényébe illeszthető; annak fejléceit és buildparancsait használja:

printf("uint_least32_t: %zu C_byte; max=%" PRIuLEAST32 "\n",
       sizeof(uint_least32_t), UINT_LEAST32_MAX);
printf("uint_fast32_t: %zu C_byte; max=%" PRIuFAST32 "\n",
       sizeof(uint_fast32_t), UINT_FAST32_MAX);

Ellenőrzési feltétel: mindkét felső határ legalább 232−12^{32}-1. A két méret egyezése nem követelmény, és a nagyobb méret önmagában nem bizonyít nagyobb végrehajtási sebességet.

4.1.8.3 A mérettárolás javítása

Az LLP64-profilban a size_t 64 bites, az unsigned long 32 bites. A kiírás formátuma a hibás változatban helyes a változóhoz, de a méret már az inicializáláskor elveszhet.

Javított C17-részlet, <stddef.h> és <stdio.h> mellett:

size_t bytes = sizeof payload;
printf("%zu\n", bytes);

Ellenőrzési feltétel: a sizeof eredménye ne kerüljön szűkebb típusba a tárolás vagy kiírás miatt. A nagy objektum tényleges létrehozása nem szükséges a típushiba felismeréséhez.

4.1.9 Rövid összefoglaló

A típusnév, a tárolási méret és az értéktartomány három külön információ. A sizeof C bájtban mér, a CHAR_BIT a bájt bitszámát, a határmakrók az értéktartományt adják. Az ABI konkrét méreteket rögzít, de ezek nem ISO-garanciák. Pontos szélességhez ellenőrzött exact-width típus, minimális tartományhoz least típus, objektummérethez size_t használható.