C-ben a rövid forráskód mögött is pontos típus- és élettartam-szabályok állnak. Most nem a teljes nyelvtant tanuljuk meg: azt a szintaktikai készletet rögzítjük, amellyel a következő memória- és típuslaborok programjai olvashatók.
Az előfeltétel a Reprodukálható fordítási és diagnosztikai labor: különösen az explicit nyelvi mód és a fordítási diagnosztika használata. Az alábbi nyelvi magyarázatok ISO C17 szabályokra épülnek, nem C++-szabályokra.
A deklaráció (declaration) egy azonosítóról közöl információt a fordítóval: például megadja egy változó típusát vagy egy függvény paramétereit és visszatérési típusát.
A definíció (definition) olyan deklaráció, amely az objektumot vagy függvényt meghatározza: objektumnál tárolót hoz létre, függvénynél megadja a törzset. Nem minden deklaráció definíció.
Az alábbi C17-részletben az első két sor fájlszinten, az utolsó három egy függvény törzsében értendő:
extern int status_code; /* Deklaráció, nem definíció. */
int twice(int value); /* Függvénydeklaráció, prototípus. */
/* Függvénytörzsben: */
int pending = 3; /* Definíció és inicializálás. */
pending = 5; /* Értékadó kifejezésutasítás. */
int completed; /* Definíció, inicializáló nélkül. */Az inicializálás (initialization) az
objektum kezdeti értékének meghatározása. Az értékadás
(assignment) egy már létező objektumban tárolt értéket módosít.
Bár mindkettőben szerepelhet =, nyelvtanilag és
szemantikailag külön műveletek.
A completed automatikus lokális változó nem kap
automatikusan nullát. Értéke kezdetben meghatározatlan
(indeterminate); ne olvasd, amíg megfelelő értéket nem tároltál
benne. Ez nem általános állítás minden inicializáló nélküli objektumról:
más tárolási idejű objektumokra később más szabályokat ismerünk meg.
Az utasítás (statement) végrehajtási lépést
ír le. A pending = 5; kifejezésutasítás, a
return 0; visszatérési utasítás, a { ... }
pedig összetett utasítás, más néven blokk. A deklaráció nem
kifejezésutasítás, még ha gyakran ugyanúgy pontosvessző zárja is.
Az operátorok operandusokon végeznek műveletet. Az operandusok típusa
számít: két int érték osztásakor például 7 / 2
eredménye 3, nem 3.5.
A következő táblázat részleges precedenciatérkép, a szorosabban kötő műveletektől lefelé:
| Operátorok | Jelentés |
|---|---|
!x, -x, ++x |
Logikai negálás, előjelváltás, prefix növelés |
*, /, % |
Szorzás, osztás, egész maradék |
+, - |
Összeadás, kivonás |
<, <=, >,
>= |
Rendezési összehasonlítás |
==, != |
Egyenlőség, különbözőség |
&& |
Logikai ÉS |
|| |
Logikai VAGY |
=, +=, -= |
Értékadás, összetett értékadás |
Ezért:
míg a C-beli (2 + 3) * 4 eredménye 20.
A zárójel csoportosítást fejez ki, de nem általános
kiértékelésisorrend-vezérlő eszköz. A
left() + right() esetén a + nem írja elő,
melyik függvényhívás történjen előbb.
A && és || viszont rövidzáras
(short-circuit) operátor. A jobb operandus csak akkor
értékelődik ki, ha az eredmény eldöntéséhez szükséges, és ilyenkor a bal
operandus kiértékelése megelőzi azt. Például:
/* C17-részlet; divisor inicializált int. */
if (divisor != 0 && 12 / divisor > 2) {
/* Az osztás csak nem nulla osztóval történik meg. */
}C-ben a nulla hamis, a nem nulla igaz feltételnek számít. Az
összehasonlító és logikai operátorok int típusú
0 vagy 1 eredményt adnak.
Két gyakori tévedés:
= értékadás, a == összehasonlítás. Az
if (ready = 1) szabályos lehet, de többnyire logikai
hiba.0 <= x <= limit nem matematikai láncolt
összehasonlítás. Helyesen:
0 <= x && x <= limit.A kapcsos zárójelek blokkot határolnak. Egy szokásos lokális változó neve a deklarációjától a blokk végéig használható; ezt a név hatókörének (scope) nevezzük. A hatókör nem azonos az objektum élettartamával, még ha egyszerű példákban határaik szorosan kapcsolódnak is.
C17-ben a deklarációk és utasítások keverhetők. Célszerű a változót az első szükséges használata közelében, lehetőleg inicializálva definiálni.
A /* ... */ komment több soros lehet, de nem ágyazható
önmagába. A // komment a sor végéig tart.
Az alábbi C17-utasításrészletekben a használt változók megfelelően deklarált, inicializált egész változók.
| Szerkezet | Példa és működés |
|---|---|
if–else |
if (ready != 0) { count = 1; } else { count = 0; } – a
feltétel alapján választ ágat |
for |
for (int i = 0; i < 3; ++i) { total += i; } –
inicializálás, feltétel, törzs, léptetés |
while |
while (remaining > 0) { --remaining; } – elöl
tesztel, akár egyszer sem fut |
do–while |
do { ++attempts; } while (attempts < 3); – hátul
tesztel, legalább egyszer fut |
A for inicializáló része egyszer hajtódik végre. A
feltételt minden iteráció előtt ellenőrzi; a léptetés a törzs után
következik. A példabeli i a cikluson kívül már nem
használható.
A switch egész típusú kifejezés alapján választ. A
case címkék egész konstanskifejezéseket tartalmaznak; a
default a fel nem sorolt értékek ága. A kiválasztott
címkétől a végrehajtás továbbhalad, ezért a következő case
nem zárja le automatikusan az előző ágat.
A break a legbelső körülvevő ciklusból vagy
switch-ből lép ki. A continue a legbelső
ciklus következő iterációjához vezet; for esetén előbb a
léptetés történik meg. A return az egész függvényből tér
vissza.
Használj kapcsos zárójeleket egyutasításos ágaknál is. Az
if (ready); utáni pontosvessző üres utasítás: az utána írt
blokk már nem feltételes.
A következő önálló program minimumkövetelménye ISO C17,
hosted környezet. A GCC-parancs profilja a központi profiltábla lab-gcc profilja,
a Clang-parancsé a lab-clang.
A syntax.c tartalma:
#include <stdio.h>
#include <stdlib.h>
int contribution(int state)
{
switch (state) {
case 0:
return 0;
case 1:
return 2;
default:
return 1;
}
}
int main(void)
{
int result = EXIT_FAILURE;
int total = 0;
for (int state = 0; state < 3; ++state) {
total += contribution(state);
}
if (printf("total=%d\n", total) < 0) {
goto cleanup;
}
if (fflush(stdout) == EOF) {
goto cleanup;
}
result = EXIT_SUCCESS;
cleanup:
return result;
}A main(void) paraméter nélküli, int
visszatérési típusú belépési függvény. A <stdio.h>
deklarálja a használt I/O-függvényeket, és elérhetővé teszi többek
között a stdout és EOF neveket. A
<stdlib.h> adja az EXIT_SUCCESS és
EXIT_FAILURE makrókat.
A contribution definíciója megelőzi a hívást, ezért itt
külön előzetes prototípusra nincs szükség. A switch ágai
return utasítással fejeződnek be, így nem kell utánuk
break.
A ciklus közbülső eredményei:
state |
Visszakapott érték | total a hozzáadás után |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 2 | 2 |
| 2 | 1 | 3 |
Sikeres kimeneti műveletek mellett az elvárt sor
total=3. A %d egy int érték
formázására szolgál, a \n újsorjel.
A printf negatív eredménye hibát jelez. Mivel a standard
kimenet pufferelt lehet, a program a kikényszerített kiírás, az
fflush eredményét is ellenőrzi. Ezek a visszatérési
értékek, nem a képernyőn látott szöveg döntik el a hibakimenetet.
goto
szerepeA goto cleanup; ugyanazon függvény cleanup
címkéjére ugrik. Itt még nincs saját erőforrás, amelyet fel kellene
szabadítani: ez a későbbi közös takarítási ág előképe. A
stdout a standard könyvtár által kezelt stream; a program
nem vesz át külön tulajdonjogot felette.
Mindkét hibaág megőrzi az előre beállított EXIT_FAILURE
értéket. A sikerértéket csak az összes ellenőrzés után állítjuk be. A
címke előtt álló result minden odavezető úton
inicializált.
A profilnak megfelelő fordítóval:
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g syntax.c -o syntax-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -O0 -g syntax.c -o syntax-clangEzek GCC/Clang-parancsok, nem ISO C-szintaxis.
Buildjegyzőkönyv: a GCC-parancs a lab-gcc
profilban figyelmeztetés nélkül fordított, a program
total=3 kimenettel, 0 kóddal futott; a
Clang-sort nem ellenőriztük. A figyelmeztetésmentes fordítás önmagában
egyébként sem bizonyítaná a helyességet.
Fogalmi: Osztályozd a következő, függvénytörzsbe szánt sorokat. Melyik deklaráció, definíció, inicializálás vagy értékadás?
int quota;
quota = 4;
int used = 0;
int check(int value);Kódolási: Írj
int sum_even(int limit) függvényt, amely a
[0, limit] intervallum páros egészeit összeadja. Csak
0 <= limit && limit <= 100 fogadható el; más
bemenetre térjen vissza -1 értékkel.
Diagnosztikai: Az alábbi C17-részletben a kívánt
eredmény kizárólag az idle sor. Miért nem ezt kapjuk?
Keress két külön hibát. Feltételezd a <stdio.h>
beillesztését és a sikeres I/O-t.
int ready = 0;
int mode = 1;
if (ready = 1) {
mode = 2;
}
switch (mode) {
case 1:
puts("idle");
case 2:
puts("active");
break;
default:
puts("unknown");
break;
}Az int quota; deklaráció és definíció, explicit
inicializálás nélkül. A quota = 4; értékadás,
kifejezésutasítás. Az int used = 0; deklaráció, definíció
és inicializálás. Az int check(int value);
függvénydeklaráció, de nem függvénydefiníció.
Ellenőrzési feltétel: a quota első
olvasása csak az értékadás után történhet.
Az alábbi C17-függvénydefiníció a teljes program main
függvénye elé illeszthető:
int sum_even(int limit)
{
if (limit < 0 || limit > 100) {
return -1;
}
int total = 0;
for (int value = 0; value <= limit; ++value) {
if (value % 2 == 0) {
total += value;
}
}
return total;
}Bizonyításvázlat: minden iteráció elején
total a már megvizsgált páros értékek összege. A feltételes
hozzáadás ezt megőrzi. Kilépéskor minden megengedett értéket
megvizsgáltunk. A legnagyobb összeg:
Ez a C17 által minimálisan megkövetelt int-tartományba
is belefér.
Ellenőrzési feltétel: a -1,
0, 5, 100, 101
bemenetekre rendre -1, 0, 6,
2550, -1 az elvárt eredmény.
Az if (ready = 1) egyet tárol ready
változóba, és igaz feltételt eredményez. Emiatt mode kettő
lesz, tehát az eredeti részlet active sort ír.
Az első javítás if (ready == 1). Ezután viszont az
idle után végrehajtódik az active kiírása is,
mert hiányzik a break az első ágból.
Ellenőrzési feltétel: mindkét javítás után
ready nulla, mode egy marad, és sikeres I/O
mellett csak az idle sor jelenik meg.
C-kód olvasásakor különítsd el a nevek deklarálását, az objektumok definícióját, az inicializálást és a későbbi értékadást. A precedencia csoportosítást, nem általános végrehajtási sorrendet határoz meg. A blokkok a lokális nevek hatókörét rendezik; a vezérlési szerkezetek pedig azt, mely utasítások hajtódnak végre. A közös kilépési ág már most segít láthatóvá tenni a siker és a hiba szerződését.