C-ben a tömbobjektum ismeri a méretét a típusán keresztül, egy rá
mutató pointer azonban önmagában nem hordoz elemszámot. Ez a különbség
különösen függvényhatáron fontos: az int a[] paraméter nem
„mérettel együtt átadott tömb”, hanem pointerparaméter. A biztonságos
interfésznek ezért külön kell közölnie, hány elemhez szabad
hozzáférnie.
Az alábbi nyelvi szabályok az ISO C17 alapra vonatkoznak. A Pointeraritmetika, objektumhatár és provenance részben megismert fél-nyílt tartományokat, valamint a Deklarátorok: tömb-, pointer- és függvénykapcsolatok típusolvasási módszerét használjuk: először mindig azt döntjük el, hogy az adott objektum tömb vagy pointer, és csak utána vizsgáljuk a kifejezésbeli viselkedését.
A tömb (array) azonos elemtípusú objektumok folytonosan
tárolt sorozata. Egy rögzített méretű T a[N] deklarációban
N pozitív egész konstanskifejezés, T pedig
megfelelő, teljes objektumtípus. Az érvényes indexek tartománya
.
A következő C17-részlet három valódi tömbobjektumot definiál:
int a[4] = {3, 7}; /* értékek: 3, 7, 0, 0 */
int b[] = {10, 20, 30}; /* a típus int[3] */
int zeros[5] = {0}; /* mind az öt elem értéke nulla */Az inicializálóból kimaradó elemek itt nulla értéket kapnak. Ez nem
azonos az inicializáló teljes elhagyásával: egy blokkban definiált,
automatikus tárolási idejű int values[5]; elemei
indeterminate állapotúak; nem feltételezhető róluk, hogy nullák.
A b[] üres szögletes zárójele nem dinamikus méretet
jelent. A fordító az inicializálóból határozza meg a háromelemes
tömbtípust. A nulla hosszúságú tömb nem szabványos C17 tömbobjektum.
Az elemek között nincs külön, tömb által hozzáadott kitöltés. Az
elemtípus saját mérete azonban tartalmazhat kitöltést. Az alábbi
táblázat ABI-független összefüggéseket mutat; minden méret és eltolás
C bájtban, vagyis a sizeof egységében
értendő.
| Objektum | Eltolás a tömb elejétől | Méret |
|---|---|---|
a[0] |
sizeof(int) |
|
a[1] |
sizeof(int) |
sizeof(int) |
a[2] |
2 * sizeof(int) |
sizeof(int) |
a[3] |
3 * sizeof(int) |
sizeof(int) |
Teljes a tömb |
4 * sizeof(int) |
A tömb nem értékadható úgy, mint egy egész változó: két már létező
tömb között az a = other; nem megengedett tömbmásolás,
hanem fordításkor diagnosztizálandó szabálysértés.
A tömbkifejezés a legtöbb kifejezéskörnyezetben az első elemére mutató pointerértékké konvertálódik. Ennek neve array-to-pointer conversion, gyakori informális neve decay.
/* C17-részlet, az előző a tömb deklarációja után. */
int *p = a;Itt két külön objektum létezik:
a négy int elemet tartalmazó
tömbobjektum;p pointerobjektum, amely a[0] címét
tárolja.Nem maga a tömb változik pointerré. Az a kifejezésből
keletkezik pointerérték; sem elemmásolás, sem tulajdonátadás nem
történik. A p kölcsönzött hozzáférés, amely csak
a élettartama alatt használható az elemek elérésére.
C17-ben különösen fontosak a következő esetek:
sizeof közvetlen tömboperandusa nem konvertálódik
pointerré.& operandusaként a tömb megőrzi
tömbtípusát.char text[] = "ok"; háromelemes
karaktertömböt hoz létre, a lezáró nullkarakterrel együtt.A C17 konverziós szabályának kivétellistájában az
_Alignof is szerepel. Ennek szabványos C17-szintaxisa
azonban típusnevet fogad: _Alignof(int[4]) megengedett,
_Alignof a nem szabványos C17 kifejezésalak.
Az előző deklarációk mellett:
| Kifejezés | Jelentés |
|---|---|
a a p = a értékadásban |
Az első elemre mutató int * érték |
sizeof a |
A teljes tömb mérete |
sizeof p |
A pointerobjektum mérete |
&a |
Az egész tömbre mutató int (*)[4] érték |
sizeof(a + 0) |
Pointerméret, mert az összeadásban már megtörténik a konverzió |
Az a + 1 egy int elemmel lép előre. Az
&a + 1 egy teljes int[4] tömbbel lép
tovább, és az egész tömb utáni one-past pointert adja. A típus tehát
meghatározza a lépés egységét.
Valódi, teljes tömbtípusú a objektumra:
Mivel egy objektumtípus mérete pozitív:
Bizonyításvázlat: a tömb pontosan
folytonosan elhelyezett elemet tartalmaz; minden elem mérete
sizeof a[0]. A teljes méret ezért ezek összege, az osztás
pedig visszaadja az elemszámot.
/* C17-részlet; size_t miatt szükséges: #include <stddef.h> */
size_t count = sizeof a / sizeof a[0];Ez a módszer csak valódi tömbön határozza meg az
elemszámot. A sizeof p / sizeof p[0] lefordulhat, de
pointerméretet oszt elemmérettel. Az eredménynek nincs általános
kapcsolata az elérhető elemek számával.
Rögzített méretű tömbnél a fenti sizeof operandusai nem
értékelődnek ki; a sizeof a[0] nem olvassa ki az első elem
értékét. A változó hosszúságú tömbök külön szabályait a Többdimenziós tömbök, VLA és layout rész
tárgyalja.
Az indexelés definíció szerint pointeraritmetikára épül:
Ha p az első elemre mutat, és n elem
használható, akkor dereferáláshoz
szükséges. A p + n lehet érvényes one-past érték, de nem
dereferálható. A folytonos fizikai tárolás nem teszi megengedetté a
tömbhatár átlépését.
A függvényparaméter tömbalakja külön szabály alapján módosul pointerparaméterré. Ez a parameter adjustment, nem egy függvényen belüli tömbobjektum későbbi konverziója.
A következő C17-prototípusok ugyanazt a függvénytípust deklarálják:
void process(int a[]);
void process(int a[10]);
void process(int *a);A függvény tehát pointerértéket kap. Nem jön létre tízelemű helyi
tömb, és nem érkezik rejtett elemszám. A függvénytörzsben a
sizeof a pointerméretet ad.
A deklarációban szereplő [10] önmagában még azt sem
követeli meg, hogy legalább tíz elem legyen elérhető. Erre külön
paraméteralak szolgál.
const és
static a paraméterben| Paraméteralak | Módosított paramétertípus | Jelentés |
|---|---|---|
int a[] |
int *a |
Az elemek írhatók |
const int a[] |
const int *a |
Az elemek ezen a hozzáférésen át nem írhatók |
int a[const] |
int *const a |
A definíció törzsében a helyi pointer nem értékadható újra |
int a[static 8] |
int *a |
Minden híváskor legalább nyolc elemhez kell hozzáférést biztosítani |
A const int a[] nem teszi az eredeti tömböt minden
hozzáférésen át változtathatatlanná: más, megfelelő pointeren keresztül
továbbra is módosítható lehet.
A szögletes zárójelen belüli const viszont a
pointerparamétert minősíti. A függvénytípus kompatibilitásánál ez a
felső szintű minősítés nem különbözteti meg az interfészt.
A [static 8] itt nem tárolási időt jelent. A hívó
minimumkapacitási vállalása, amelyre a fordító is támaszkodhat. Nem
futásidejű ellenőrzés: null pointer vagy túl rövid tartomány átadása
megsérti a szerződést, és nem definiált viselkedéshez vezet. A
const int a[static 8] a minimumkapacitást csak olvasható
hozzáféréssel kombinálja.
Ha a típussal valóban meg akarjuk őrizni a négyes kiterjedést,
használható az int (*a)[4] paraméter. Ez pointer egy négyes
tömbre; a sizeof *a tömbméretet ad. Ettől azonban a pointer
érvényességét továbbra is külön biztosítani kell.
Az általános tömbinterfész két adatot kap: kezdőpointert és elemszámot. A hossz nem bájtméret, hanem az adott elemtípusban értelmezett darabszám.
A következő teljes program minimum C17-et igényel. A
laborhoz előírt profil: Linux, x86-64 System V ABI, LP64, GCC 14.2.0
vagy Clang 19.1.7, glibc 2.40. A program nem támaszkodik ezek konkrét
pointer- vagy int-méretére.
/* array_api.c */
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
static bool int_fill(int *a, size_t n, int value)
{
if (a == NULL && n != 0) {
return false;
}
for (size_t i = 0; i < n; ++i) {
a[i] = value;
}
return true;
}
int main(void)
{
int values[4] = {3, 7};
int *p = values;
size_t n = sizeof values / sizeof values[0];
if (!int_fill(p, n, 9)) {
return 1;
}
for (size_t i = 0; i < n; ++i) {
if (values[i] != 9) {
return 1;
}
}
if (!int_fill(NULL, 0, 9)) {
return 1;
}
printf("count=%zu array_bytes=%zu pointer_bytes=%zu\n",
n, sizeof values, sizeof p);
return 0;
}Fordítás a külön tanulói laborban:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O0 -g \
array_api.c -o array_api-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O0 -g \
array_api.c -o array_api-clangEzek elvégzendő ellenőrzések, nem dokumentált fordítási vagy futtatási eredmények. A nyelvi szabályokból következő elvárt elemszám négy; a kiírt bájtméretek a célplatformtól függenek.
Az API szerződése:
| Szerep vagy feltétel | Követelmény |
|---|---|
| Tulajdonos és tároló | A hívó kezeli a tömb élettartamát; itt main helyi
values objektuma biztosítja a tárolót |
| Kölcsönzött hozzáférés | p és a függvény a paramétere nem
tulajdonos |
| Nem üres tartomány | Legalább n élő, írható int elem legyen
elérhető |
| Üres tartomány | n == 0 esetén a null pointer is elfogadott |
| Érvénytelenítés | A tároló élettartamának vége megszünteti az elemekhez való hozzáférés érvényességét |
| Hibakimenet | Null pointer és pozitív hossz esetén false, módosítás
nélkül |
A függvény nem tárolja el a pointert. A kitöltés módosítja az értékeket, de nem érvényteleníti az elemekre mutató pointereket.
Korlát: a függvény nem tudja ellenőrizni, hogy a nem
null pointer mögött ténylegesen van-e n elem. A túlbecsült
hossz továbbra is szerződéssértés és határon túli íráshoz vezethet.
A ciklus minden belépéskor i < n feltételt ellenőriz,
ezért igaz hossz esetén csak érvényes indexet használ. Üres tartománynál
sem dereferencia, sem pointeraritmetika nem történik. A legrosszabb
műveletszám pozitív n mellett
,
üres tartománynál
;
nincs allokáció vagy amortizált költség. A hozzáférés szekvenciális,
ezért jó térbeli lokalitású.
A „tömb egy pointer” rövidítés eltakarja a lényegi különbséget: a
tömb elemeket tartalmaz, a pointer egy hozzáférési értéket tárol. Emiatt
a függvényparaméteren végzett sizeof nem alkalmas
elemszámlálásra.
A másik tipikus hiba a <= n ciklusfeltétel. Az utolsó
elem indexe n - 1, nem n; üres tartománynál
pedig még a n - 1 kiszámítása is kerülendő. A természetes
bejárási alak i < n.
Végül a static paraméteralak nem helyettesíti a
bemenetellenőrzést. Erősebb előfeltételt ad, nem biztonsági hálót.
Adott int a[6]; és int *p = a;. Mit jelent
a sizeof a, sizeof p,
sizeof(a + 0) és &a? Melyikből határozható
meg az elemszám?
Írj
bool int_set(int *a, size_t n, size_t index, int value)
függvényt! Hibás index vagy null pointer esetén adjon false
értéket, módosítás nélkül. Egyébként pontosan egy elemet írjon át.
Az alábbi C17-részlet hibás API-megvalósítás; a size_t
deklarációjához a <stddef.h> szükséges:
void clear(int a[8])
{
size_t n = sizeof a / sizeof a[0];
for (size_t i = 0; i <= n; ++i) {
a[i] = 0;
}
}Azonosítsd a két független méretkezelési hibát! Megoldaná-e őket a
[static 8] használata?
A sizeof a értéke 6 * sizeof(int). A
sizeof p pointerméret; a sizeof(a + 0) szintén
az int * típus mérete. Az &a típusa
int (*)[6].
Az elemszámot a sizeof a / sizeof a[0] adja. Ellenőrzési
feltétel: ez minden megfelelő C17-implementáción hat, függetlenül a
pointermérettől.
A teljes program fejléceivel használható C17-részlet:
static bool int_set(int *a, size_t n,
size_t index, int value)
{
if (index >= n || a == NULL) {
return false;
}
a[index] = value;
return true;
}A hívó a tulajdonos; a pointer csak a hívás idejére kölcsönzött. A sikeres ág előfeltétele a megadott hosszúságú, élő és írható tartomány. A tároló élettartamának vége érvényteleníti a hozzáférést.
Ellenőrizendő: index == n esetén nincs írás;
n == 0 mindig hibát ad; érvényes indexnél kizárólag a
kijelölt elem változik.
Az első hiba, hogy a pointerparaméter: a méretarány nem
nyolcat, hanem pointerméret és elemméret hányadosát számolja. A második
hiba a <=: még helyes elemszám esetén is eggyel túl sok
elemhez férne hozzá.
A [static 8] egyik hibát sem javítja. Az előfeltételt
erősítené, de a paraméter továbbra is pointer maradna.
A javítás külön size_t n paraméter és
i < n feltétel; például használható az
int_fill(a, n, 0). Ellenőrzési feltétel: igaz hossz mellett
pontosan n elem módosuljon. Határon túli hozzáférés esetére
nincs garantált kimenet vagy garantált összeomlás.
A tömbobjektum elemeket és típusbeli kiterjedést jelent; a pointer
önmagában nem közöl hosszt. A decay, a paraméter-adjustment és a
sizeof külön szabályok. Megbízható C API-ban a pointer
mellé explicit elemszám, élettartam- és írhatósági előfeltétel, valamint
világos hibaszerződés tartozik.