Az „inicializáljuk nullára” kifejezés C-ben legalább három különböző műveletet takarhat: egy számnak nulla értéket adunk, egy pointert null pointer értékre állítunk, vagy egy tárolási terület minden bitjét nullázzuk. Ezek gyakori platformokon hasonló gépi műveletekre vezethetnek, de a nyelvi szerződésük nem azonos.
A tárolási időről és élettartamról tanultak alapján az objektum létezése és értékének használhatósága külön kérdés. Egy objektum élettartama már elkezdődhetett úgy, hogy az értékét még nem szabad kiolvasni. Az egész típusok és adatmodellek pedig arra emlékeztetnek, hogy a típus neve, értékkészlete és reprezentációja nem ugyanaz.
Ebben az egységben az ISO C17 szabályait használjuk.
A C bájt a sizeof egysége; bitszáma CHAR_BIT,
nem általánosan nyolc.
Az inicializálás (initialization) az
objektum kezdeti értékének megadása. Nem azonos a későbbi értékadással,
még akkor sem, ha mindkettőben szerepelhet az = jel.
Az alábbi C17-kódrészletben:
void example(void)
{
int a; /* Nincs explicit inicializáló. */
int b = 0; /* Inicializálás. */
a = 7; /* Értékadás a már létező objektumnak. */
}Az a deklarációja önmagában nem hiba. Akkor keletkezik
probléma, ha a program megfelelő értékadás előtt használja az
értékét.
ISO C17: az explicit inicializáló nélküli automatikus tárolási idejű objektum értéke indeterminate, azaz meghatározatlan. Ezzel szemben a statikus és szálhoz kötött tárolási idejű objektumok inicializáló nélkül is a típusuknak megfelelő kezdeti értéket kapják.
| Objektum | Inicializáló nélküli kezdeti állapot |
|---|---|
| Automatikus tárolási idejű objektum | Meghatározatlan érték |
| Statikus tárolási idejű aritmetikai objektum | Pozitív vagy előjel nélküli nulla |
| Statikus tárolási idejű pointer | Null pointer érték |
| Statikus tárolási idejű aggregátum | Tagjai, elemei rekurzívan inicializálódnak |
| Szálhoz kötött objektum | Ugyanezek a típus szerinti szabályok, szálanként külön objektumra |
A statikus tárolási idő nem jelent szükségképpen fájlszintű nevet. A
függvényblokkban deklarált static int count; is nulla
értékkel indul, és nem inicializálódik újra minden függvényhíváskor. A
_Thread_local objektumból minden szálnak saját példánya
van.
C17-részlet:
static int process_count; /* Kezdetben 0. */
static _Thread_local int thread_count; /* Szálanként kezdetben 0. */
void work(void)
{
static int calls; /* Kezdetben 0, nem hívásonként új objektum. */
int temporary; /* Meghatározatlan érték. */
temporary = 12; /* Ettől kezdve az értéke használható. */
}A hatókörből tehát nem következik az inicializálási szabály: a
calls és a temporary neve egyaránt
blokkhatókörű, tárolási idejük mégis eltér.
Tömbnél és struktúránál a meg nem adott elemek, illetve tagok úgy inicializálódnak, mint a statikus tárolási idejű objektumok.
C17-részlet:
struct Options {
unsigned retries;
double delay;
void *context;
};
void configure(void)
{
struct Options options = {0};
unsigned limits[4] = {3};
}Az options.retries értéke nulla, az
options.delay értéke lebegőpontos pozitív nulla, az
options.context értéke null pointer. A limits
elemei rendre 3, 0, 0,
0.
A {0} nem azt mondja, hogy „írj nulla bájtokat az egész
objektumba”. Az első tag explicit inicializálót kap, a többi tagra pedig
a hiányzó inicializálók nyelvi szabálya érvényesül. Az üres
{} inicializáló nem szabványos C17-megoldás.
ISO C17: az indeterminate value két lehetőség valamelyike:
A trap reprezentáció olyan objektumreprezentáció, amely nem képvisel érvényes értéket az adott típusban. A „trap” elnevezés nem garantál processzorkivételt vagy azonnali programleállást. Ha egy ilyen reprezentációjú objektum tárolt értékét nem karaktertípusú lvalue-val olvassuk ki, a viselkedés nem definiált.
A C17 fogalomhasználatában az indeterminate elsősorban az érték minősítése. Nem helyes minden inicializálatlan objektumot úgy leírni, hogy „véletlen biteket tartalmaz, amelyek egy véletlen, de használható értéket jelentenek”. Más szabványverziók vagy a C++ eltérő terminológiáját sem szabad automatikusan rávetíteni.
Az alábbi hibás C17-függvényrészlet kizárólag elemzésre szolgál:
int retry_limit(void)
{
int limit;
return limit; /* Nem definiált viselkedés. */
}A limit automatikus tárolási idejű, nincs inicializálva,
a címét nem vettük, és az értékét olvassuk. Erre az esetre a C17
szabálya nem definiált viselkedést mond ki; nem kell hozzá trap
reprezentáció létezését feltételezni.
Nem ígérhető, hogy a függvény nullát, „stacken maradt adatot” vagy minden futáskor más számot ad vissza. A fordító optimalizáláskor abból indulhat ki, hogy a helyes program nem hajt végre ilyen műveletet.
A javítás:
/* Helyes C17-függvényrészlet. */
int retry_limit(void)
{
int limit = 0;
return limit;
}Fontos különbség, hogy a nem VLA típusú limit objektumra
alkalmazott sizeof limit nem olvassa annak értékét. Az
inicializálatlan objektum puszta létezése tehát nem UB.
A karaktertípusú reprezentáció-hozzáférésnek külön szabályai vannak. Ebből azonban nem következik, hogy az inicializálatlan adat általánosan biztonságosan felhasználható. A helyes program szerződése egyszerűbb: minden értékolvasást előzzön meg megfelelő inicializálás vagy értékadás.
ISO C17: a null pointer olyan pointerérték, amely
nem mutat objektumra vagy függvényre. Egy nulla értékű egész
konstanskifejezés pointertípusra konvertálva null pointert eredményez.
C-ben az ilyen kifejezés void *-ra konvertált alakja is
null pointer konstans.
C17-részlet:
int *a = 0;
int *b = (void *)0;Mindkét pointer null pointer értékű. A fordító feladata az adott implementáció megfelelő reprezentációjának előállítása. Ebből nem következik, hogy ez a reprezentáció csupa nulla bit.
| Művelet | Hordozható jelentés C17-ben |
|---|---|
int n = 0; |
Egész nulla érték |
double x = 0; |
Lebegőpontos pozitív nulla |
int *p = 0; |
Null pointer érték |
struct Options o = {0}; |
Típus szerinti taginicializálás |
memset(&o, 0, sizeof o) |
Minden C bájt nulla értékűre írása |
Az egész típusoknál a csupa nulla bit reprezentáció a nulla értéket képviseli. Ugyanez nem általános garancia pointerekre és lebegőpontos típusokra.
memset?A memset a megadott értéket unsigned char
típusra konvertálja, majd ennek megfelelő bájtokkal tölti fel a kért
tartományt.
A következő C17-részlet helyes bájtpuffer-nullázás; a
<string.h> fejléc szükséges hozzá:
unsigned char buffer[64];
memset(buffer, 0, sizeof buffer);A tároló a buffer tömb része, élettartamát a befoglaló
blokk szabályozza. A függvény csak a hívás idejére kap kölcsönzött
hozzáférést; a megadott tartomány teljes egészében írható. Hibakimenet
nincs a szerződés teljesülésekor. Érvénytelen cím vagy túl nagy hossz
esetén nem definiált viselkedés következhet.
Ezzel szemben a pointertagot tartalmazó struktúra
memset-es nullázása önmagában nem hordozható
pointerinicializálás. Az így létrehozott pointerreprezentáció akár
érvénytelen is lehet; a későbbi tagolvasás ezért nem feltétlenül
megengedett.
calloc?A calloc siker esetén olyan allokált tárolót ad,
amelynek minden bitje nulla. Nem garantálja, hogy az abban később
értelmezett pointerek null pointerek, vagy a lebegőpontos objektumok
nulla értékűek.
Egy kívánt típus szerinti kezdeti állapotot explicit értékadással adhatunk meg:
/* C17-részlet: p már ellenőrzött, megfelelő méretű
és igazítású allokációra mutat. */
*p = (struct Options){0};Itt a compound literal tagjai nyelvi szabályok szerint inicializálódnak, majd a struktúraértéket a célobjektumba másoljuk. Ha úgyis minden tagot beállítunk, a megelőző bájtnullázás gyakran szükségtelen; az allokáció részletes szerződését a dinamikus memóriáról szóló egység tárgyalja.
Az objektumreprezentáció az objektum tárolását alkotó bájtok összessége. Struktúrában a tagok között vagy az utolsó tag után kitöltés, azaz padding lehet. Ennek mérete és elhelyezkedése implementációfüggő; itt nem feltételezünk konkrét ABI-layoutot.
Két objektum értékazonossága nem jelenti feltétlenül bájtazonosságukat. A tagok értékei egyezhetnek, miközben a padding bájtok eltérnek. Egyes típusoknál az érték és a reprezentáció kapcsolata önmagában sem egy-egyértelmű.
A memcmp a megadott bájtsorozatokat hasonlítja össze,
unsigned char értékekként. Ezért általában nem alkalmas
struktúrák szemantikai egyenlőségének eldöntésére.
Bizonyításvázlat: legyen két struktúrában minden tag
értéke azonos. Ha van olyan padding bájt, amelynek értéke eltérhet a két
objektumban, akkor a memcmp eltérést találhat, noha a
program számára jelentéssel bíró tagok megegyeznek. Az
értékegyenlőségből tehát nem következik reprezentációegyenlőség.
ISO C17: az explicit inicializáló nélküli statikus vagy szálhoz kötött aggregátum kezdeti paddingje nulla bitekre inicializálódik. Ez nem általános garancia minden inicializálási módra. Ráadásul struktúrába vagy annak tagjába történő későbbi tároláskor a padding bájtok nem specifikált értékeket vehetnek fel. A „kezdetben mindent nulláztam” ezért nem tartós reprezentációs invariáns.
Üzemeltetési következményként nyers struktúrabájtokat naplózni, hálózatra küldeni vagy hash-elni nemcsak hordozhatósági gond: paddingen keresztül nem kívánt tárolótartalom is megjelenhet. A protokollmezőket külön kell kódolni.
Az alábbi init.c teljes, hosted C17-program. A
választott, helyben ellenőrizendő laborprofil: Linux, x86-64 System V
ABI, LP64, glibc 2.40; GCC 14.2.0 vagy Clang 19.1.7. A program nem
támaszkodik konkrét struktúraméretre. A példa fordítása és futtatása itt
nem történt meg.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct State {
unsigned char ready;
unsigned count;
void *context;
};
static struct State process_state;
static _Thread_local struct State thread_state;
static int state_equal(struct State a, struct State b)
{
return a.ready == b.ready
&& a.count == b.count
&& a.context == b.context;
}
int main(void)
{
struct State local = {0};
struct State *heap = calloc(1, sizeof *heap);
if (heap == NULL) {
fputs("allocation failed\n", stderr);
return EXIT_FAILURE;
}
/* A calloc bájtnullázása helyett erre alapozzuk
a tagok típus szerinti kezdeti értékét. */
*heap = (struct State){0};
int values_equal =
state_equal(local, process_state)
&& state_equal(local, thread_state)
&& state_equal(local, *heap);
int bytes_equal =
memcmp(&local, &process_state, sizeof local) == 0;
printf("values_equal=%d\n", values_equal);
printf("bytes_equal=%d\n", bytes_equal);
free(heap);
return EXIT_SUCCESS;
}Fordítás a választott kiadásoknak megfelelő parancsokkal:
gcc -std=c17 -O2 -Wall -Wextra -Wpedantic init.c -o init-gcc
clang -std=c17 -O2 -Wall -Wextra -Wpedantic init.c -o init-clang| Objektum vagy hozzáférés | Élettartam és tulajdonlás |
|---|---|
local |
Automatikus objektum; a main blokkjából kilépéskor
élettartama véget ér |
process_state |
Statikus tárolási idejű objektum |
thread_state |
A végrehajtó szál saját, szálhoz kötött objektuma |
heap pointerobjektum |
Automatikus objektum; az allokáció felszabadításának felelősségét hordozza |
| Allokált tároló | A sikeres calloc és a free között él |
memcmp argumentumai |
Csak a hívás idejére kölcsönzött hozzáférések; nincs tulajdonátadás |
Sikeres allokáció esetén a values_equal számított értéke
1: minden összehasonlított tag azonos értéket kapott. A
bytes_equal értékére nincs ugyanez a hordozható ígéret. Ez
nem UB-demonstráció, hanem a kétféle egyenlőség szétválasztása.
A free érvényteleníti az allokált objektumra mutató
hozzáféréseket. A program utána nem olvassa a heap értékét
és nem dereferálja azt. Az allokációs hibautat ellenőrzi; a szabványos
kimenet írási hibájának részletes kezelését a példa nem tárgyalja.
{0} és a memset
felcserélhető.” Az előbbi típus szerinti értékeket ad, az
utóbbi bájtokat ír.memcmp eltérése eltérő tagértékeket
bizonyít.” Padding miatt ez nem következik. Hasonlítsd össze a
jelentéssel bíró tagokat.Egy függvényben szerepel az int a;, a
static int b; és az int *p = 0; deklaráció.
Mely értékek olvashatók azonnal? Mit tudunk p
bitmintájáról?
Írj C17-függvényt, amely egy struct Options értéket ad
vissza nulla próbálkozásszámmal, nulla késleltetéssel és null pointer
környezettel. Ne használj bájtnullázást.
Egy teszt két struct State objektumot {0}
inicializálóval hoz létre, majd megköveteli, hogy a memcmp
eredménye nulla legyen. Érvényes ez a követelmény? Mi legyen a javított
ellenőrzés?
Valaki az inicializálatlan int limit; olvasását úgy
„javítja”, hogy a tesztet csak optimalizálás nélkül fordítja. Miért
elégtelen ez? Adj valódi javítást és ellenőrzési feltételt.
A b értéke nulla, a p értéke null pointer;
mindkettő olvasható. Az a megfelelő értékadás nélküli
kiolvasása ebben a helyzetben nem megengedett. A p
reprezentációjáról nem következik, hogy csupa nulla bit.
Ellenőrzési feltétel: a magyarázat külön kezelje a tárolási időt, az értéket és a reprezentációt.
C17-részlet a korábban megadott struct Options
definíciójával:
struct Options default_options(void)
{
struct Options result = {0};
return result;
}A függvény értéket ad vissza, nem a lokális objektum címét.
Ellenőrzési feltétel: a visszaadott tagokra
retries == 0, delay == 0.0 és
context == 0 teljesül. A padding tartalma nem része a
feltételnek.
A bájtazonosság követelménye nem hordozható. A padding és a reprezentáció lehetséges eltérései miatt két értékazonos objektum bájtjai különbözhetnek.
A javított ellenőrzés a példabeli state_equal(a, b)
eredményét követeli igaznak.
Ellenőrzési feltétel: a teszt kizárólag a szerződésben jelentéssel bíró tagokra épüljön.
Az optimalizálás kikapcsolása nem változtatja meg a C17 szabályait. A javítás megfelelő inicializálás vagy minden olvasáshoz vezető vezérlési úton végrehajtott értékadás.
Ellenőrzési feltétel: minden értékolvasást megelőz egy megfelelő értékadás. Ezt vezérlésifolyam-elemzéssel kell indokolni; néhány sikeres futás önmagában nem bizonyíték.
Az objektum élettartama, kezdeti értéke és reprezentációja külön
fogalom. Az automatikus objektum nem kap általános implicit
nullainicializálást; a statikus és szálhoz kötött objektum igen,
mégpedig típus szerinti értékekkel. A memset és a
calloc bájtokat nulláz, nem általánosan szemantikai
értékeket. A padding miatt az értékazonosság nem bájtazonosság, az
inicializálatlan érték hibás olvasásához pedig nem rendelhető stabil,
megjósolható kimenet.