C-ben egy deklaráció nemcsak nevet ad valaminek: meghatározza, milyen típusú értékeket és műveleteket enged meg a program. A típus pontos olvasása ezért helyességi kérdés. Nem mindegy, hogy egy pointeren keresztül nem szabad módosítani egy objektumot, maga a pointer nem változtatható meg, vagy az objektumot eleve módosíthatatlanként definiáltuk.
Az Egész típusok és adatmodellek alapján a típusnevet már megkülönböztetjük a konkrét mérettől. A Headerek, deklarációk és include-higiénia részben pedig láttuk, hogyan közölnek deklarációkat a fordítási egységek. Most a deklaráció belső szerkezetét és a hozzá kapcsolódó vállalásokat vizsgáljuk.
ISO C17: a deklaráció egy vagy több azonosító tulajdonságait közli. A definíció ennél többet tesz: például egy objektum számára tárolót is biztosít, vagy megadja egy függvény törzsét.
Az alábbiak deklarációs részletek:
extern int pending; /* Deklaráció, nem objektumdefiníció. */
int completed = 0; /* Objektumdefiníció. */
int classify(int value); /* Függvénydeklaráció. */A deklarációban elkülöníthetjük a közös deklarációmegadókat — például
az int típusmegadót és a const típusminősítőt
— az egyes nevekhez tartozó deklarátoroktól
(declarator). A deklarátor fejezi ki többek között a pointer-,
tömb- és függvénykapcsolatot.
int *first, second;Itt csak first pointer; second típusa
int. A * nem a teljes deklaráció minden
nevéhez kapcsolódik. Olvashatósági okból az eltérő szerkezetű
deklarációkat célszerű külön sorba írni.
A típust a névtől kifelé olvassuk. A zárójelezés elsődleges, a
[] és a függvényt jelölő () pedig erősebben
kapcsolódik, mint a pointert jelölő *.
| Deklaráció | A deklarált név típusa |
|---|---|
int *p; |
Pointer int objektumra |
int *items[4]; |
Négy int pointerből álló tömb |
int (*row)[4]; |
Pointer négy int elemből álló tömbre |
int convert(int); |
Egy int paraméterű, int eredményű
függvény |
int (*operation)(int); |
Pointer ilyen függvényre |
Az int (*row)[4] levezetése: row neve
mellett a zárójelben * áll, tehát pointer; a zárójeles
egységet [4] követi, tehát tömbre mutat; az elemtípus
int. Az int *items[4] esetében viszont először
a [4] kapcsolódik a névhez, ezért tömböt kapunk.
Ez egyelőre típustérkép, nem használati útmutató: a deklaráció önmagában nem biztosít érvényes pointerértéket. A részletes olvasási módszert a Deklarátorok: tömb-, pointer- és függvénykapcsolatok rész folytatja.
ISO C17: a typedef meglévő típushoz
rendel másik nevet; nem hoz létre új, attól elkülönülő típust.
typedef unsigned long RequestCount;
typedef int *IntPtr;A RequestCount és az unsigned long ugyanazt
a típust jelöli. Az álnév dokumentálhat mértékegységet vagy szerepet, de
önmagában nem akadályozza meg például egy kérésszám és egy időtartam
összekeverését.
A typedef nem szöveges helyettesítés. Ez különösen
minősítőkkel fontos:
typedef int *IntPtr;
const IntPtr cursor = 0;A levezetés:
IntPtr teljes típusa „pointer int
objektumra”.const ezt a teljes típust minősíti.cursor ezért konstans pointer int
objektumra, vagyis típusa int *const.Ez nem azonos a const int * típussal. A példabeli
cursor null pointer, nem biztosít objektumhoz hozzáférést;
dereferálása UB lenne.
C-ben a struct, union és enum
tagnevei (tag) külön névtérbe tartoznak a
közönséges azonosítóktól. A typedef-nevek viszont ugyanabba
a névtérbe kerülnek, mint a változó- és függvénynevek, valamint az
enumerátornevek.
typedef struct Task Task;
struct Task {
int state;
};Az első sorban a struct Task még nem teljes típus: a
tagjai nincsenek megadva. A typedef ettől függetlenül
létrehozhatja hozzá a Task álnevet. A második deklaráció
teljessé teszi a struktúratípust.
A két Task név azért fér meg egymással, mert külön
névtérben van. Egy ugyanabban a hatókörben megjelenő
int Task; azonban már ütközne a
typedef-névvel.
ISO C17: const-minősítésű lvalue-n
keresztül nem megengedett az érték módosítása. Ettől külön szabály, hogy
egy eredetileg const-minősítésű típussal definiált objektum
módosításának kísérlete nem definiált viselkedés.
| Deklaráció | Mi nem módosítható ezen a hozzáférésen keresztül? |
|---|---|
const int *p; |
A mutatott int |
int *const p = 0; |
A pointerobjektum |
const int *const p = 0; |
Sem a pointer, sem rajta keresztül a mutatott érték |
Az első eset nem bizonyítja, hogy a mutatott objektum minden más hozzáférés számára is módosíthatatlan. A következő, függvénytörzsbe illeszthető részlet szabályos:
int value = 4;
const int *view = &value;
int *writer = &value;
*writer = 9;
/* *view értéke most 9. */A tárolót a helyi value objektum adja; view
és writer kölcsönzött hozzáférések. A módosítás nem
érvényteleníti őket, de value élettartamának vége igen. Az
azon túli dereferálás UB.
Ezzel szemben az alábbi hibás, UB-t szemléltető részlet, nem futtatandó teszt:
const int frozen = 4;
int *writer = (int *)&frozen;
*writer = 9; /* UB: az eredetileg const objektum módosítása. */A cast csak a kifejezés típusát változtatja meg, az objektum
definícióját nem. Ha az eredeti objektum nem const, a
minősítés eltávolítása után végzett módosítás önmagában nem feltétlenül
UB; az API szerződése azonban ezt akkor is tilthatja.
C17-ben ez a blokkhatókörű részlet változó hosszúságú tömböt, VLA-t deklarál:
const int count = 4;
int values[count];A count nem egész konstans kifejezés
(integer constant expression), hiába kapott konstans
inicializálót. A VLA-támogatás C17-ben opcionális. Rögzített
tömbmérethez itt megfelelő lehet:
enum { ITEM_COUNT = 4 };
int values[ITEM_COUNT];A const a szálbiztonságot sem garantálja. Egy
const pointer mögötti, valójában módosítható objektumot más
szál írhat; megfelelő szinkronizáció hiányában ebből adatverseny
lehet.
ISO C17: a volatile olyan objektumokhoz
való hozzáférést támogat, amelyek értéke a szokásos programfolyamon
kívül változhat, illetve amelyek hozzáférése külső mellékhatással
járhat. A volatile-hozzáféréseknek az absztrakt gép szabályait kell
követniük; azt, hogy pontosan mi számít ilyen hozzáférésnek, az
implementáció határozza meg.
Platformfeltétel: memóriába térképezett
eszközregisztereknél a volatile csak egy része a
szerződésnek. A leképezés, a hozzáférési szélesség és az esetleges
hardveres barrier külön platformkövetelmény.
A volatile nem garantál:
Ezért egy volatile jelzőváltozó nem helyettesíti a
mutexet vagy az atomikus műveletet. A részletek a volatile, compiler barrier és hardver barrier,
illetve a C atomics és memory order részekhez
tartoznak.
ISO C17: a restrict pointerekhez
kapcsolható típusminősítő. Nem futásidejű ellenőrzés, hanem az
optimalizáló által felhasználható aliasing-szerződés.
Első munkamodellként: ha egy objektumot egy restricted pointerből származtatott hozzáférésen keresztül érünk el, és az objektum módosul, akkor az érintett blokk végrehajtása során az objektum többi releváns hozzáférésének is ugyanahhoz a restricted hozzáférési úthoz kell igazodnia. A teljes szabály ennél részletesebb.
Ez nem azt jelenti, hogy más pointerérték nem létezhet ugyanarra az objektumra. A tényleges hozzáférések számítanak. Két, kizárólag olvasásra használt pointer átfedése sem általánosan tiltott.
A következő másolófüggvényhez egyszerű, elegendő szerződést választunk: a forrás- és céltartomány nem fedheti egymást.
A példa minimuma ISO C17. A választott laborprofil Linux x86-64 System V, glibc 2.39, GCC 14.2.0 vagy Clang 19.1.7, bővítmények nélkül. Az alábbi ellenőrzések és kimenet levezetett elvárások, nem közölt futtatási eredmények.
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
enum { ITEM_COUNT = 3 };
enum Status { STATUS_OK = 0, STATUS_BAD = 1 };
static void copy_values(size_t n,
int *restrict dst,
const int *restrict src)
{
for (size_t i = 0; i < n; ++i) {
dst[i] = src[i];
}
}
int main(void)
{
int source[ITEM_COUNT] = {3, 5, 8};
int target[ITEM_COUNT] = {0};
const int *view = source;
source[0] = 13;
copy_values(ITEM_COUNT, target, view);
bool ready = -4;
enum Status status = ready ? STATUS_OK : STATUS_BAD;
assert(view[0] == 13);
assert(target[0] == 13);
assert(target[1] == 5);
assert(target[2] == 8);
assert(ready == true);
printf("%d %d %d\n", target[0], (int)ready, (int)status);
return 0;
}Fordítás types.c fájlból, a megfelelő kiadásra
beállított driverrel:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
-Wcast-qual -O2 types.c -o types-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
-Wcast-qual -O2 types.c -o types-clangPozitív n esetén mindkét pointer mögött legalább
n megfelelően inicializált, illetve írható int
elemnek kell rendelkezésre állnia. A két tartomány legyen diszjunkt.
Nulla n esetén a függvény nem végez pointerműveletet vagy
memóriahozzáférést.
| Tulajdonosi szerep és tároló | Kölcsönzött hozzáférés | Élettartamhatár |
|---|---|---|
A main helyi source tömbje |
view, majd a hívás src paramétere |
A main blokkjának elhagyása |
A main helyi target tömbje |
A hívás dst paramétere |
A main blokkjának elhagyása |
A függvény nem tárolja el a pointereket, nem allokál, és nem érvénytelenít hozzáférést. Nem jelez szerződéssértést: az érvénytelen tartomány vagy a restricted hozzáférések tiltott átfedése UB-t okozhat.
A source[0] módosítása szabályos, mert a tömb nem
const; view csak olvasási hozzáférést
biztosít. A várt kimenet 13 1 0.
Az enum Status felsorolt típus. C17-ben az
enumerátornevek, például STATUS_OK,
int típusú konstansok; értéküknek ábrázolhatónak kell
lennie int-ként. Maga az enumerált típus nem egyszerűen az
int másik neve: kompatibilis egész típusának kiválasztása
implementáció által meghatározott.
A _Bool beépített C17 típus. A
<stdbool.h> a bool, true és
false makrókat adja: rendre _Bool,
1 és 0 jelentéssel. Skalár érték
_Bool-ra konvertálásakor a nullával egyenlő értékből
0, minden másból 1 lesz. Ezért lesz a példában
-4 értékéből igaz.
Néhány fontos literáltípus:
| Kifejezés | Típus C17-ben |
|---|---|
12, 12U, 12L |
Rendre int, unsigned int,
long |
1.0, 1.0f, 1.0L |
Rendre double, float,
long double |
'A' |
int, nem char |
true a megfelelő header után |
int típusú 1 makrókifejtés |
Nagyobb egész literálok típusát a számrendszer, az utótag és az
ábrázolhatóság együtt határozza meg; ezt az egész
típusok rész tárgyalja. A közönséges sztringliterál C17-ben
char elemtípusú tömb, módosításának kísérlete mégis UB. A
„nem const típusú” tehát nem minden esetben jelent
módosíthatóságot.
Típusmagyarázat. Mit jelentenek az alábbi,
egymástól független deklarációk? Miben különbözik a és
b?
typedef int *Handle;
const Handle a = 0;
const int *b = 0;
int (*c)[3];Kódolás. Írj status_is_ok függvényt
a teljes példához, amely enum Status paraméterből
bool eredményt készít. Miért hibás erre egyszerűen a
státusz közvetlen logikai konverziója?
Diagnosztika. Értékeld külön-külön:
const objektum módosítását castolt pointeren át;copy_values(2, source + 1, source) hívást;volatile
minősítése szálbiztossá teszi az adatátadást.a konstans pointer int objektumra;
b módosítható pointer const int hozzáféréssel;
c pointer háromelemű int tömbre. Az
ellenőrzési feltétel: a újraértékadása fordítási
diagnosztikát igényel, b újraértékadása önmagában
megengedett. A null pointerek dereferálása egyik esetben sem
megengedett.
A teljes példa elé illeszthető függvényrészlet:
static bool status_is_ok(enum Status status)
{
return status == STATUS_OK;
}A siker kódja nulla, amely közvetlen _Bool-konverzióval
hamis lenne. A függvény a státusz jelentését, nem
puszta nullaságát képezi le. Ellenőrzésként a main
törzsében legyen igaz a status_is_ok(STATUS_OK) és a
!status_is_ok(STATUS_BAD) kifejezés.
Az első eset UB: a cast nem változtatja meg a ténylegesen
const objektumot.
A második esetben a céltartomány source[1] és
source[2], a forrás pedig source[0] és
source[1]. A source[1] objektumot külön
restricted hozzáférési utakon olvassuk és írjuk, ezért a hívás sérti a
szerződést. Külön céltömb használata megfelelő javítás.
A harmadik állítás hamis: a volatile nem hoz létre
szálak közötti szinkronizációt. A helyességhez megfelelő atomikus
protokoll vagy zárolás szükséges; pusztán sikeres tesztfutás nem
ellenőrzési bizonyíték.
A deklarátort a névtől kifelé olvassuk, a typedef pedig
teljes típushoz rendel álnevet. A const módosítási
korlátozás, nem általános konstanskifejezés-jelölő vagy szálbiztonsági
garancia. A volatile a hozzáférések megfigyelhetőségéhez, a
restrict a hozzáférési utak aliasing-szerződéséhez
kapcsolódik. Helyes használatukhoz mindig a tényleges objektumot, annak
élettartamát és az összes releváns hozzáférést kell vizsgálni.