Egy mérési táblázat vagy hálózati feldolgozó munkaterület esetén a „kétdimenziós adat” még nem határozza meg a tárolást. Lehet egyetlen folytonos tömbobjektum, de lehet különálló sorokra mutató pointerek táblája is. A két megoldáshoz eltérő pointertípus, indexelési szabály és élettartam-szerződés tartozik.
A Tömbobjektum, decay és függvényparaméter
legfontosabb megállapítása itt is érvényes: a tömb nem pointer, bár
számos kifejezésben az első elemére mutató pointerré konvertálódik.
Többdimenziós tömbnél ez az első elem maga is tömb. A méretszámításokhoz
pedig felidézzük a Promóciók, signedness és
túlcsordulás szabályát: a size_t előjel nélküli
aritmetikája körbefordulhat, ezért a szorzást megelőzően kell
ellenőrizni a reprezentálhatóságot.
ISO C17: az int a[2][3] deklaráció egy
kételemű tömböt hoz létre, amelynek minden eleme háromelemű
int tömb. A sorok és az egyes sorokon belüli elemek
folytonosan helyezkednek el. Ez a sorfolytonos, angolul
row-major elrendezés: a jobb szélső index
változtatásával jutunk a következő skalár elemhez.
C17 deklarációrészlet:
int a[2][3] = {
{10, 11, 12},
{20, 21, 22}
};Az a[r][c] jelentése:
*(*(a + r) + c)Az első pointerlépés teljes sort, a második egyetlen int
elemet lép át. Ha a soronkénti elemszám
,
az elemtípus
,
akkor a tömb kezdetéhez viszonyított eltolás:
Az eltolás egysége C_byte, vagyis a
sizeof egysége. Az indexelés előfeltétele
és
,
ahol
a sorok száma.
Bizonyításvázlat. Egy sor pontosan
darab
$Ttípusú elemből áll, ezért mérete $N\operatorname{sizeof}(T)$. Az $r$ korábbi sor és a kiválasztott sor $c$ korábbi eleme adja a képletet. A tömb nem illeszt külön kitöltést az elemei közé; ha maga $T$ tartalmaz kitöltést, az már része asizeof(T)`
értékének.
A stride, azaz lépésköz azt mondja meg, milyen távol
vannak egymástól az egymást követő sorok kezdetei. Mindig közölni kell
az egységét. Ebben a fejezetben a stride elemszámot
jelent.
Egy logikailag háromoszlopos mátrix fizikailag öt elemet is fenntarthat soronként. Ekkor:
ahol a fizikai sorhossz, a logikai oszlopszám, és .
A következő számok Linux x86-64, System V ABI, LP64,
C17 profilra vonatkoznak, ahol sizeof(int) == 4 és
CHAR_BIT == 8. Ezek levezetett értékek, nem mérési
eredmények.
| Tároló | Logikai méret | Stride, elem | Második sor eltolása, C_byte | [1][2] eltolása, C_byte |
Teljes méret, C_byte |
|---|---|---|---|---|---|
int a[2][3] |
3 | 12 | 20 | 24 | |
int b[2][5] |
5 | 20 | 28 | 40 |
A második esetben a sorok utolsó két eleme alkalmazásszinten
kihagyott hely. Ez nem a fordító által beillesztett padding: ezek
valódi, indexelhető int objektumok.
Az a tömb kifejezésbeli konverziójának eredménye
int (*)[3]: pointer egy háromelemű int tömbre.
Nem int **.
C17 deklarációrészlet, <stddef.h> mellett:
void clear_rows(size_t rows, int (*a)[3]);
/* Ugyanaz a függvényparaméter-típus: */
void clear_rows(size_t rows, int a[][3]);A háromoszlopos sor típusa meghatározza az a + 1
lépésméretét. A sorok száma külön szerződés: a hívónak legalább
rows darab élő, írható sort kell biztosítania.
Más reprezentációt ír le ez a C17-részlet:
int first[3] = {1, 2, 3};
int second[3] = {4, 5, 6};
int *rows[2] = {first, second};Itt a rows konverziója valóban int **. A
rows[r][c] először kiolvassa az r-edik
pointerobjektumot, majd azon keresztül éri el az elemet.
| Tulajdonság | Folytonos mátrix | Sorpointer-tömb |
|---|---|---|
| Tipikus paraméter | T (*a)[N] |
T **a |
| Sor kezdete | Típusból számított lépés | Tárolt pointerérték |
| Sorhossz | Azonos fizikai sorhossz | Soronként eltérhet |
| Tároló tulajdonosa | Egy mátrixtárolóért felelős hívó | Pointertábla és sorok tulajdonosai külön rögzítendők |
| Kölcsönzés érvényessége | A teljes tároló élettartamáig | A pointertábla és minden használt sor élettartamáig |
A fenti lokális példában a befoglaló blokk kezeli mindhárom tömb élettartamát; a sorpointerek nem tulajdonolják a sorokat. A blokk elhagyása érvényteleníti a hozzájuk kötött kölcsönzött hozzáféréseket.
Külön dinamikusan allokált soroknál több allokáció, részleges
sikertelenség és soronkénti felszabadítás kezelendő; ennek eszközeit a
malloc, calloc, free és allokációs szerződés
tárgyalja. A pointertábla ugyanakkor folytonos tároló soraira is
mutathat: a T ** önmagában nem bizonyít szétszórt
elhelyezést.
Mindkét reprezentáció indexelése állandó műveletszámú, de a pointertáblás változat egy további pointerérték betöltését igényli. A folytonos, soronként bejárt mátrix rendszerint kedvezőbb lokalitást ad; ez teljesítményre vonatkozó várakozás, nem nyelvi garancia.
Gyakori hiba: a mátrixot (int **)a
kaszttal átadni. A kaszt nem hoz létre sorpointer-objektumokat. Az így
végzett a[r][c] hozzáférésnek nincs érvényes tárolási
szerződése, és UB-hez vezethet. A javítás a megfelelő signature, nem egy
újabb kaszt.
A változó hosszúságú tömb (variable length array, VLA) tömbméretét nem egész konstanskifejezés határozza meg. Mérete például függvényparaméterből származhat.
C17 deklarációrészlet, VLA-támogatással és
<stddef.h> mellett:
void fill(size_t rows, size_t cols, int (*a)[cols]);Az a pointerváltozó, nem VLA-objektum, de a típusa
VLA-tól függ. Az ilyen típus a változóan módosított
típusok (variably modified types, VM) körébe tartozik.
Az int a[rows][cols] paraméteralak itt szintén
int (*a)[cols] alakra igazodik; nem foglal mátrixot és nem
ellenőrzi automatikusan a sorok számát.
| Szabványprofil | VLA és VM támogatása |
|---|---|
| ISO C17 | Opcionális szolgáltatáscsomag; az __STDC_NO_VLA__ makró
1 értéke a támogatás hiányát jelzi. |
| ISO C23 | A VM-típusok és az allokált tárolási idejű VLA-k támogatása kötelező; az automatikus tárolási idejű VLA-objektumok támogatása maradhat opcionális. |
| ISO C23 makrójelentés | Az __STDC_NO_VLA__ == 1 már az automatikus tárolási
idejű VLA-objektumok támogatásának hiányára vonatkozik. |
A C23 nyelvi módot és a konkrét képességet külön fordítási próbával kell ellenőrizni. A C17-es tiltó makróvizsgálatot ezért nem szabad változatlan jelentéssel C23-ra átvinni.
A kiértékelt VLA-méretnek pozitívnak kell lennie. Ez a függvénydefiníció paramétertípusában szereplő sorhosszra is lényeges: a nulla méretet a hívás előtt kell elutasítani, nem csak a függvénytörzs elején.
Az int temporary[rows][cols]; deklaráció automatikus
tárolási idejű objektumot hoz létre. Az ISO C nem ír elő hozzá vermet,
de a szokásos Linux-fordítók jellemzően stackterületet használnak.
Egy reprezentálható méret még nem feltétlenül fér el a rendelkezésre
álló stackben. A VLA-létrehozásnak nincs hordozható,
NULL-lal jelzett sikertelen kimenete. Nem megbízható
bemenet esetén ezért külön szükséges:
A méretek ellenőrzése után képzett VLA-ra mutató pointer nem azonos egy nagy automatikus VLA létrehozásával.
Teljes fizikai sorokat fenntartó tárolónál legyen:
Itt az elemszám, a C_byte-ban mért méret. Pozitív mellett a két biztonságos szorzás feltétele:
Az osztásos ellenőrzés azért előzi meg a szorzást, mert a már körbefordult eredményből az eredeti méret nem állítható helyre megbízhatóan.
Az alábbi teljes C17-program VLA/VM-támogatást igényel, de nem hoz létre futásidejű méretű automatikus tömböt. Választott laborprofil: Linux x86-64 System V, LP64, GCC 14.2.0 vagy Clang 18.1.8, glibc 2.39. A fordítási parancsok laborutasítások; a példához itt nem tartozik végrehajtott mérés.
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#if defined(__STDC_NO_VLA__) && __STDC_NO_VLA__
#error "This C17 example requires VLA/VM support"
#endif
static int checked_bytes(size_t rows, size_t cols,
size_t stride, size_t *bytes)
{
if (bytes == NULL || rows == 0 || cols == 0 || stride < cols)
return 0;
if (rows > SIZE_MAX / stride)
return 0;
size_t elements = rows * stride;
if (elements > SIZE_MAX / sizeof(int))
return 0;
*bytes = elements * sizeof(int);
return 1;
}
static void fill(size_t rows, size_t cols, size_t stride,
int (*a)[stride])
{
for (size_t r = 0; r < rows; ++r)
for (size_t c = 0; c < cols; ++c)
a[r][c] = 7;
}
int main(void)
{
int matrix[2][5] = {{0}};
size_t bytes;
if (!checked_bytes(2, 3, 5, &bytes))
return 1;
if (bytes != sizeof matrix)
return 1;
fill(2, 3, 5, matrix);
if (printf("%zu %d %d\n",
bytes, matrix[1][2], matrix[1][4]) < 0)
return 1;
return 0;
}gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 matrix.c -o matrix-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 matrix.c -o matrix-clangA választott ABI mellett, sikeres kimeneti művelet esetén a
várt, nem mért kimenet 40 7 0. Csak a
logikai oszlopok módosulnak.
A tulajdonos a main blokkja; a tároló a
matrix, a kölcsönzött hozzáférés a fill
paramétere. A tároló élettartama lefedi a teljes hívást, és a blokk
elhagyása érvénytelenítené a megőrzött pointert. A fill sem
pointert, sem tulajdonjogot nem őriz meg.
A checked_bytes hibára nullát ad, és nem írja a kimeneti
objektumot. Nem null bytes esetén élő, írható
size_t objektumra mutató pointer szükséges. A
fill nem ellenőrző függvény: a hívó garantálja a pozitív
méreteket, a helyes fizikai sortípust és a megfelelő írható tárolót. A
méretellenőrzés önmagában nem bizonyítja, hogy egy tetszőleges pointer
mögött valóban van ekkora objektum.
Valódi egydimenziós tömbben az index . Ha , , akkor:
Ez a határellenőrzés bizonyításvázlata; feltétele, hogy reprezentálható, és a tároló legalább ennyi elemet tartalmaz.
Ne következtess ebből arra, hogy egy valódi
int matrix[R][S] objektumot korlátlanul bejárhatsz a
&matrix[0][0] pointerrel. Az így kapott
int * az első sor elemeire mutat; az azon túli
pointeraritmetikát nem teszi szabályossá a fizikailag szomszédos
következő sor. Tömbök tömbjét sorpointerrel, lapos tárolót lapos
pointerrel kezelj.
Levezetés. A megadott x86-64 laborprofilban
int a[3][4] esetén mekkora a sorstride C_byte-ban, az
a[2][1] eltolása és a teljes méret? Miért hibás hozzá az
int ** paraméter?
Kódolás. A példa checked_bytes
függvényére építve írj ellenőrzött set_cell függvényt
valódi egydimenziós int tömb logikai mátrixnézetéhez! A
kapacitás elemszám legyen. Hibánál ne változzon a tároló.
Diagnosztika. Egy függvény közvetlenül
bemenetről veszi a rows és cols értékét,
deklarál egy int a[rows][cols] tömböt, majd
(int **)a értéket ad tovább. Nevezz meg legalább három
egymástól független hibaforrást, és javasolj javítást!
A sorstride C_byte. Az eltolás:
A teljes méret
C_byte. A megfelelő paraméter int (*a)[4]; az
int ** pointerobjektumokat feltételezne a sorok helyén.
Ellenőrzési feltétel: sizeof a[0] == 16
és sizeof a == 48 a megjelölt profilon.
C17-részlet az előző program checked_bytes függvénye
után:
static int set_cell(int *a, size_t capacity,
size_t rows, size_t cols, size_t stride,
size_t r, size_t c, int value)
{
size_t bytes;
if (a == NULL || r >= rows || c >= cols)
return 0;
if (!checked_bytes(rows, cols, stride, &bytes))
return 0;
if (bytes / sizeof(int) > capacity)
return 0;
a[r * stride + c] = value;
return 1;
}A hívó tulajdonolja a legalább capacity elemű
egydimenziós tömböt; a függvény csak a hívás idejére kölcsönzi. A
tulajdonos tárolójának megszűnése érvényteleníti a pointert. Sikerre
egyet, ellenőrzött hibára nullát ad. Hamis kapacitásadat vagy dangling
pointer ellen nem véd.
Ellenőrzési feltétel:
int data[10] = {0}; mellett a
nézet (1,2) cellája a 7-es indexet módosítja; a
(1,3) kérés hibát ad, változtatás nélkül.
A nulla méret érvénytelen VLA-határ. A túl nagy méret túlterhelheti a
tipikus stacktárolást akkor is, ha a méretszámítás reprezentálható. A
size_t-szorzás külön ellenőrzés nélkül körbefordulhat.
Végül az int ** kaszt hibás reprezentációt állít.
A javítás: ellenőrzött méretszámítás, alkalmazási méretkorlát, megfelelő tárolási stratégia és a tényleges reprezentációhoz illő signature.
Ellenőrzési feltétel: hibás méretnél se VLA-létrehozás, se memóriaművelet ne történjen; érvényes bemenetnél a paramétertípus egyezzen a tároló szerkezetével.
A folytonos kétdimenziós C-tömb tömbök tömbje; természetes
paramétertípusa sorra mutató pointer, nem T **. A helyes
hozzáféréshez együtt szükséges a logikai méret, a fizikai stride, a
reprezentálható méretszámítás és az élő tároló. A VLA támogatása
szabványverzió-függő, használata pedig nem helyettesíti sem a
kapacitásellenőrzést, sem az erőforráskorlátokat.