Egy C-kifejezés helyességét nem dönti el, hogy a megfelelő operátorokat használtuk-e. Tudnunk kell azt is, melyik operandus milyen típusúvá alakul, mikor történnek meg a módosítások, és milyen sorrendet garantál a nyelv. Ezek a kérdések egy állapotjelző bitmező vagy egy számláló kezelésekor ugyanúgy előkerülnek, mint egy összetett függvényhívásban.
A Promóciók, signedness és túlcsordulás részben megismert konverziós szabályokra, valamint a Deklarációk, typedef és típusminősítők fogalmaira építünk. A nyelvi szabályok itt ISO C17-re vonatkoznak; a hasonló C++-szintaxisra nem szabad automatikusan átvinni őket.
Egy kifejezés kiértékelése kétféle tevékenységet tartalmazhat:
volatile objektum elérése vagy egy fájl módosítása.Az i++ értéke például i régi értéke; külön
mellékhatása i növelése. A régi érték kiszámítása megelőzi
ezt a módosítást, de ebből még nem következik sorrend a környező
operandusokhoz képest.
A sequenced-before az egy szálon belüli kiértékelések aszimmetrikus, tranzitív sorrendi relációja. Ha sequenced-before , akkor kiértékelése megelőzi kiértékelését. Két további esetet különböztetünk meg:
| Viszony | Jelentés |
|---|---|
| Unsequenced | Sem nem előzi meg garantáltan -t, sem fordítva. |
| Indeterminately sequenced | Az egyik teljes kiértékelés megelőzi a másikat, de a nyelv nem rögzíti, melyik. |
Az utóbbi tehát nem ugyanaz, mint a sorrendi kapcsolat hiánya.
ISO C17 – alapvető UB-szabály: ha egy skalár objektum módosítása unsequenced viszonyban áll ugyanannak az objektumnak egy másik módosításával vagy az értékét felhasználó értékszámítással, a viselkedés nem definiált.
Az operátorprecedencia csak a kifejezés csoportosítását határozza
meg. Az a() + b() * c() alakból nem következik, hogy
először b() fut le.
Az alábbiak elkülönített tanítási részletek, nem
futtatandó tesztek. Tegyük fel, hogy i és j
különálló int objektumok, és nincs túlcsordulás.
| Kifejezés | C17 szerinti minősítés |
|---|---|
i++ + j++ |
Definiált; az operandusok között nincs garantált sorrend. |
i++ + i |
UB: a módosítás és az olvasás unsequenced. |
consume(i++, i++) |
UB: ugyanaz az objektum két unsequenced módosítást kap. |
consume(i++, i) |
UB: unsequenced módosítás és olvasás. |
i = i++ + 1 |
UB C17-ben: a két módosítás között nincs szükséges sorrend. |
A függvényargumentumok kiértékelése befejeződik a meghívott függvény törzsének megkezdése előtt, de ettől az argumentumok egymás közötti sorrendje még nincs rögzítve.
Biztonságos javítás, C17-részletként, egy megfelelően deklarált
consume mellett:
int i = 3;
int first = i++;
int second = i;
consume(first, second);A külön inicializálók teljes kifejezések (full expressions).
Az előző teljes kifejezés értékszámításai és mellékhatásai megelőzik a
következőét. Így a hívás argumentumértékei 3 és
4.
Fontos finomság: a consume(next(), next()) nem
szükségképpen UB akkor sem, ha a két next() ugyanazt a
számlálót módosítja. A két függvénytörzs végrehajtása egymáshoz képest
indeterminately sequenced: nem fonódnak össze. Az argumentumokhoz
rendelt értékek sorrendje ettől még nem specifikált. Feltétel
természetesen, hogy a függvények önmagukban helyesek legyenek.
ISO C17: a következő operátorok valódi sorrendi garanciát adnak.
| Operátor | Garancia |
|---|---|
left && right |
left előbb értékelődik ki; right csak igaz
bal operandus esetén. |
left || right |
left előbb értékelődik ki; right csak
hamis bal operandus esetén. |
cond ? yes : no |
A feltétel megelőzi a kiválasztott ágat; csak az egyik ág értékelődik ki. |
left, right |
A bal operandus kiértékelése és mellékhatásai megelőzik a jobbét. |
A && és || eredménye
int típusú, értéke 0 vagy 1.
Például unsigned int típusú num és
den mellett:
/* C17-részlet; num és den már inicializált unsigned int objektumok. */
int large_ratio = den != 0u && num / den > 3u;A rövidzár kizárja a nullával osztást. A & nem
helyettesítheti a && operátort: nincs rövidzáras
garanciája.
A vessző két szerepét külön kell választani:
/* C17-részlet; consume megfelelően deklarált függvény. */
int i = 3;
int result = (i++, i * 2); /* Vesszőoperátor: result értéke 8. */
consume(i, result); /* Az argumentumelválasztó vessző nem operátor. */A sorrendet itt a vesszőoperátor biztosítja, nem önmagában a zárójel.
A &, |, ^ és
~ bitenkénti műveleteket végeznek. Az egész promóciók
ezekre is vonatkoznak: például egy unsigned char operandus
~ művelete gyakran int típusban történik, nem
az eredeti keskeny típusban.
A shiftoperátoroknál mindkét operandus egész promóción megy át, de nem a szokásos aritmetikai konverziók határozzák meg a közös típust. Az eredmény típusa a promótált bal operandus típusa.
Legyen ennek bitbeli szélessége, előjeles típusnál az előjelbitet is beleszámítva, padding nélkül. Mindkét shift előfeltétele:
ahol a jobb operandus értéke. Negatív vagy túl nagy eltolás UB.
ISO C17:
Egy 32 bites int/unsigned int profilban
ezért a 1 << 31 és a 1u << 32
egyaránt UB, de más okból. Az első eredménye nem reprezentálható
int-ként, a második eltolásszáma túl nagy.
A művelet utáni cast nem javítja meg a hibás shiftet:
/* HIBÁS C17-részlet, 32 bites unsigned int profilban: UB. */
uintmax_t bit = (uintmax_t)(1u << 40);A megfelelően széles típust a shift előtt kell biztosítani.
Az alsó
bitet kijelölő maszk matematikai értéke
.
A közvetlen (1 << n) - 1 azonban a teljes
típusszélességnél hibás.
Használjuk inkább, esetén:
Bizonyításvázlat: egyetlen beállított bitet tartalmaz az pozíción; az összes alatta levő bitet tartalmazza. Bitenkénti VAGY kapcsolatuk pontosan darab egyes.
A következő teljes program minimum C17-et igényel. A
width logikai szélesség bitben, nem
sizeof-érték. A támogatott tartomány 1–64 bit; a
uintmax_t legalább 64 értékbitje elegendő. Nincs szükség
opcionális uint64_t típusra.
/* masks.c */
#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#define MAX_FIELD_BITS 64u
_Static_assert(
UINTMAX_MAX >= UINTMAX_C(0xffffffffffffffff),
"at least 64 value bits are required"
);
struct mask_result {
bool ok;
uintmax_t value;
};
static struct mask_result low_mask(unsigned width, unsigned count)
{
if (width == 0u || width > MAX_FIELD_BITS || count > width) {
return (struct mask_result){false, UINTMAX_C(0)};
}
if (count == 0u) {
return (struct mask_result){true, UINTMAX_C(0)};
}
uintmax_t top = UINTMAX_C(1) << (count - 1u);
return (struct mask_result){true, top | (top - UINTMAX_C(1))};
}
int main(void)
{
struct mask_result small = low_mask(8u, 4u);
struct mask_result full = low_mask(64u, 64u);
if (!small.ok || !full.ok) {
return 1;
}
if (printf("%" PRIxMAX "\n%" PRIxMAX "\n",
small.value, full.value) < 0) {
return 2;
}
return 0;
}A hibás kérés ok == false, value == 0
eredményt ad; a nulla hosszúságú maszk viszont sikeres kérés. A
legnagyobb shift 63, ezért nincs szélességnyi eltolás.
A fordítási parancsokhoz választott laborprofil: Linux, x86-64 System V ABI, LP64, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A program nem támaszkodik e profil konkrét típusszélességeire. A parancsok feltételezik, hogy a megadott kiadások érhetők el az adott neveken; itt nem történt fordítás vagy futtatás.
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 masks.c -o masks-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 masks.c -o masks-clangA szabályokból levezetett, nem mért kimenet: f, majd
ffffffffffffffff.
Maszkellenőrzésnél mindig ügyelj a precedenciára: a helyes alak
(flags & mask) == 0. A
flags & mask == 0 mást jelent, mert az
egyenlőségvizsgálat köt erősebben.
sizeof, VLA és
konstans kifejezésekISO C17: a sizeof eredménye
size_t, mértékegysége C bájt. Nem VLA típusú operandusa nem
értékelődik ki: a sizeof(i++) ezért nem növeli
i értékét, ha i például int.
A változó hosszúságú tömb, azaz variable length array (VLA) kivétel. Az alábbi C17-részlet VLA-támogatást igényel; C17-ben ez opcionális képesség:
#include <stddef.h>
void example(void)
{
size_t n = 3;
size_t bytes = sizeof(int[n++]);
/* n értéke 4; bytes értéke 3 * sizeof(int). */
(void)bytes;
}Itt a méret meghatározásához ki kell értékelni a tömbhatárt. Ezzel
szemben egy korábban létrehozott int a[n++] objektum
sizeof a művelete nem értékeli újra a deklarációbeli
n++ kifejezést: a tömb mérete a létrehozáskor rögzült.
Egyes összetett, változó méretű típusokat tartalmazó esetekben a
méretkifejezés kiértékelése el is maradhat, ha nem befolyásolná a
sizeof eredményét. Méretkifejezésbe ezért ne rejts
mellékhatást.
Az egész konstans kifejezés (integer constant
expression, ICE) egész típusú, szigorúan korlátozott összetételű
konstans kifejezés. Alkotóelemei lehetnek például egész-, karakter- és
felsorolási konstansok, megfelelő sizeof és
_Alignof kifejezések; közvetlen egészre castolt
lebegőpontos konstans is megengedett. Kiértékelt függvényhívás vagy
objektummódosítás nem lehet benne.
C17-ben egy const int objektum neve önmagában nem ICE,
még akkor sem, ha az optimalizáló ismeri az értékét.
/* C17-részlet, fájlszinten is elhelyezhető. */
#define FIELD_BITS 5u
#define FIELD_SHIFT 9u
_Static_assert(FIELD_SHIFT < 64u, "invalid shift");
_Static_assert(FIELD_BITS > 0u &&
FIELD_BITS <= 64u - FIELD_SHIFT,
"field does not fit");A _Static_assert egész konstans kifejezést követel;
nulla értéknél fordítási diagnosztikát ír elő. Futásidejű bemenetet nem
ellenőriz. A makró pedig szöveges helyettesítés: nem attól lesz konstans
kifejezés, hogy nagybetűs a neve.
Mindegyik esetet külön, int i = 3, j = 8;
kezdőállapotból vizsgáld:
(i++, i)i++ + j++consume(i++, i)Add meg a definiált esetek eredményét és végállapotát, illetve az UB okát.
Miért hibás C17-ben az alábbi részlet? A második sor vizsgálatához
tegyél fel 32 bites unsigned int típust.
/* HIBÁS tanítási részlet. */
const int bits = 32;
_Static_assert(bits == 32, "unexpected width");
uintmax_t mask = (uintmax_t)(1u << bits) - 1u;Javítsd külön a fordítási idejű ellenőrzést és a maszkképzést.
Egészítsd ki a teljes programot egy
extract_field(x, shift, count) függvénnyel. A 64 bites
logikai szó alsó bitjeitől számozva nyerjen ki mezőt. Utasítsa el a
shift >= 64 és count > 64 - shift
eseteket, és használja a meglévő hibaszerződést.
A (i++, i) eredménye 4, végül
i == 4: a vesszőoperátor sorrendet biztosít.
Az i++ + j++ eredménye 11, végül
i == 4, j == 9. Külön objektumok módosulnak;
az operandusok sorrendje nem befolyásolja ezt az eredményt.
A consume(i++, i) UB: i módosítása és
értékének felhasználása unsequenced. Nincs garantált kimenete.
Ellenőrzéskor a sorrendi relációt kell igazolni, nem egy megfigyelt
futásra támaszkodni.
A bits objektum neve nem egész konstans kifejezés.
Használható helyette például:
/* C17-részlet. */
enum { BITS = 32 };
_Static_assert(BITS == 32, "unexpected width");A shift ettől még hibás maradna: 32 bites operandust 32-vel tolna. A szélesítés túl későn történik. A javítás a korábbi programban:
struct mask_result mask = low_mask(32u, 32u);Ellenőrzési feltétel: mask.ok igaz, értéke
matematikailag
.
Az alábbi C17-részlet a programban, a low_mask után
helyezhető el:
static struct mask_result extract_field(
uintmax_t x, unsigned shift, unsigned count)
{
if (shift >= MAX_FIELD_BITS) {
return (struct mask_result){false, UINTMAX_C(0)};
}
struct mask_result mask =
low_mask(MAX_FIELD_BITS - shift, count);
if (!mask.ok) {
return mask;
}
return (struct mask_result){true, (x >> shift) & mask.value};
}Előbb ellenőrizzük a shiftet, és csak utána számítjuk ki a fennmaradó szélességet. A jobbra tolás így mindig megengedett; a maszk eltávolítja a mező fölötti biteket.
Ellenőrzési esetek: extract_field(0xb0, 4, 4) értéke
0xb; a (shift=63, count=1) kérés megengedett,
a (63, 2) és (64, 0) elutasítandó. A nulla
hosszúságú, egyébként érvényes mező eredménye nulla.
A precedencia a csoportosítást, a sequencing a kiértékelések sorrendjét szabályozza. A külön teljes kifejezések és a rövidzáras operátorok világos sorrendi garanciákat adnak; a függvényargumentumok elválasztó vesszői nem.
Bitműveletnél mindig a promótált típust, az előjelet és az
eltolásszám tartományát vizsgáld. A sizeof többnyire nem
értékeli ki operandusát, de a VLA kivételt jelent. A
_Static_assert fordítási idejű szerződést ellenőriz; a
futásidejű bemenetekhez külön ellenőrzés szükséges.