C | |
---|---|
Jazyková třída | procesní |
Typ provedení | sestaven |
Objevil se v | 1972 |
Autor | Dennis Ritchie |
Vývojář | Bell Labs , Dennis Ritchie [1] , US National Standards Institute , ISO a Ken Thompson |
Přípona souboru | .c— pro kódové soubory, .h— pro hlavičkové soubory |
Uvolnění | ISO/IEC 9899:2018 ( 5. července 2018 ) |
Typový systém | staticky slabé |
Hlavní implementace | GCC , Clang , TCC , Turbo C , Watcom , Oracle Solaris Studio C, Pelles C |
Dialekty |
"K&R" C ( 1978 ) ANSI C ( 1989 ) C99 ( 1999 ) C11 ( 2011 ) |
Byl ovlivněn | BCPL , B |
ovlivnil | C++ , Objective-C , C# , Java , Nim |
OS | Microsoft Windows a operační systém podobný Unixu |
Mediální soubory na Wikimedia Commons |
ISO/IEC 9899 | |
Informační technologie — Programovací jazyky — C | |
Vydavatel | Mezinárodní organizace pro standardizaci (ISO) |
webová stránka | www.iso.org |
výbor (vývojář) | ISO/IEC JTC 1/SC 22 |
webové stránky výboru | Programovací jazyky, jejich prostředí a systémová softwarová rozhraní |
ISS (ICS) | 35,060 |
Aktuální vydání | ISO/IEC 9899:2018 |
Předchozí vydání | ISO/IEC 9899:1990/COR2:1996 ISO/IEC 9899:1999/COR3:2007 ISO/IEC 9899:2011/COR1:2012 |
C (z latinského písmene C , anglický jazyk ) je univerzální kompilovaný staticky typovaný programovací jazyk vyvinutý v letech 1969-1973 zaměstnancem Bell Labs Dennisem Ritchiem jako vývoj jazyka Bee . Původně byl vyvinut pro implementaci operačního systému UNIX , ale od té doby byl portován na mnoho dalších platforem. Svým designem jazyk úzce mapuje typické strojové instrukce a našel použití v projektech, které byly původní v jazyce symbolických instrukcí , včetně operačních systémů a různého aplikačního softwaru pro různá zařízení od superpočítačů po vestavěné systémy . Programovací jazyk C měl významný dopad na vývoj softwarového průmyslu a jeho syntaxe se stala základem pro takové programovací jazyky jako C++ , C# , Java a Objective-C .
Programovací jazyk C byl vyvinut v letech 1969 až 1973 v Bellových laboratořích a do roku 1973 byla většina jádra UNIX , původně napsaného v assembleru PDP-11 /20, přepsána do tohoto jazyka. Název jazyka se stal logickým pokračováním starého jazyka „ Bi “ [a] , jehož mnohé rysy byly vzaty jako základ.
Jak se jazyk vyvíjel, byl nejprve standardizován jako ANSI C a poté byl tento standard přijat mezinárodním normalizačním výborem ISO jako ISO C, také známý jako C90. Standard C99 přidal do jazyka nové funkce, jako jsou pole proměnné délky a inline funkce. A ve standardu C11 byla do jazyka přidána implementace proudů a podpora atomových typů. Od té doby se však jazyk vyvíjel pomalu a pouze opravy chyb ze standardu C11 se dostaly do standardu C18.
Jazyk C byl navržen jako systémový programovací jazyk, pro který lze vytvořit jednoprůchodový kompilátor . Standardní knihovna je také malá. V důsledku těchto faktorů se kompilátory vyvíjejí relativně snadno [2] . Proto je tento jazyk dostupný na různých platformách. Kromě toho je jazyk navzdory své nízkoúrovňové povaze zaměřen na přenositelnost. Programy vyhovující jazykovému standardu mohou být kompilovány pro různé počítačové architektury.
Cílem jazyka bylo usnadnit psaní velkých programů s minimálními chybami ve srovnání s assemblerem, dodržovat zásady procedurálního programování , ale vyhnout se čemukoli, co by zavádělo další režii specifické pro jazyky na vysoké úrovni.
Hlavní vlastnosti C:
Zároveň C postrádá:
Některé z chybějících funkcí lze simulovat vestavěnými nástroji (například koroutiny lze simulovat pomocí funkcí setjmpalongjmp ), některé jsou přidány pomocí knihoven třetích stran (například pro podporu multitaskingu a síťových funkcí můžete použít knihovny pthreads , sockety a podobně, existují knihovny pro podporu automatického garbage collection [3] ), část je implementována v některých kompilátorech jako jazyková rozšíření (například vnořené funkce v GCC ). Existuje poněkud těžkopádná, ale docela funkční technika, která umožňuje implementovat mechanismy OOP v C [4] , založené na skutečném polymorfismu ukazatelů v C a podpoře ukazatelů na funkce v tomto jazyce. OOP mechanismy založené na tomto modelu jsou implementovány v knihovně GLib a jsou aktivně využívány v rámci GTK+ . GLib poskytuje základní třídu GObject, schopnost dědit z jediné třídy [5] a implementovat více rozhraní [6] .
Po svém zavedení byl jazyk dobře přijat, protože umožňoval rychlé vytváření kompilátorů pro nové platformy a také umožňoval programátorům být poměrně přesní v tom, jak byly jejich programy prováděny. Díky své blízkosti k nízkoúrovňovým jazykům běžely programy C efektivněji než programy napsané v mnoha jiných vysokoúrovňových jazycích a pouze ručně optimalizovaný kód assembleru mohl běžet ještě rychleji, protože poskytoval plnou kontrolu nad strojem. Dosavadní vývoj kompilátorů a komplikace procesorů vedly k tomu, že ručně psaný montážní kód (snad s výjimkou velmi krátkých programů) prakticky nemá žádnou výhodu oproti kódu generovanému kompilátorem, zatímco C je i nadále jedním z nejvíce efektivní jazyky na vysoké úrovni.
Jazyk používá všechny znaky latinské abecedy , čísla a některé speciální znaky [7] .
znaky latinské abecedy |
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z |
Čísla | 0, 1, 2, 3, 4, 5, 6, 7, 8_9 |
Speciální symboly | , (čárka) , ;, . (tečka) , +, -, *, ^, & (ampersand) , =, ~ (vlnovka) , !, /, <, >, (, ), {, }, [, ], |, ( apostrof)% , (uvozovky) , (dvojtečka) , ( podtržítko ) ) , ,?' " : _ \# |
Tokeny jsou tvořeny z platných znaků - předdefinovaných konstant , identifikátorů a operačních znaků . Lexémy jsou zase součástí výrazů ; a příkazy a operátory se skládají z výrazů .
Když je program přeložen do C, jsou z kódu programu extrahovány lexémy o maximální délce obsahující platné znaky. Pokud program obsahuje neplatný znak, pak lexikální analyzátor (nebo kompilátor) vygeneruje chybu a překlad programu nebude možný.
Symbol #nemůže být součástí žádného tokenu a používá se v preprocesoru .
IdentifikátoryPlatný identifikátor je slovo, které může obsahovat latinské znaky, čísla a podtržítka [8] . Identifikátory jsou dány operátorům, konstantám, proměnným, typům a funkcím.
Identifikátory klíčových slov a vestavěné identifikátory nelze použít jako identifikátory objektů programu. Existují také vyhrazené identifikátory, u kterých kompilátor nebude dávat chyby, ale které se v budoucnu mohou stát klíčovými slovy, což povede k nekompatibilitě.
Existuje pouze jeden vestavěný identifikátor - __func__, který je definován jako konstantní řetězec implicitně deklarovaný v každé funkci a obsahující její název [8] .
Doslovné konstantySpeciálně formátované literály v C se nazývají konstanty. Doslovné konstanty mohou být celočíselné, reálné, znakové [9] a řetězcové [10] .
Celá čísla jsou standardně nastavena v desítkové soustavě . Pokud je zadán prefix 0x, pak je v šestnáctkové soustavě . Předpona 0 znamená, že číslo je v osmičkové soustavě . Přípona určuje minimální velikost konstantního typu a také určuje, zda je číslo podepsané nebo nepodepsané. Výsledný typ je brán jako nejmenší možný typ, ve kterém lze danou konstantu reprezentovat [11] .
Přípona | Pro desítkové | Pro osmičkovou a šestnáctkovou soustavu |
---|---|---|
Ne | int
long long long |
int
unsigned int long unsigned long long long unsigned long long |
uneboU | unsigned int
unsigned long unsigned long long |
unsigned int
unsigned long unsigned long long |
lneboL | long
long long |
long
unsigned long long long unsigned long long |
unebo Uspolečně s lneboL | unsigned long
unsigned long long |
unsigned long
unsigned long long |
llneboLL | long long | long long
unsigned long long |
unebo Uspolečně s llneboLL | unsigned long long | unsigned long long |
Desetinný
formát |
S exponentem | Hexadecimální
formát |
---|---|---|
1.5 | 1.5e+0 | 0x1.8p+0 |
15e-1 | 0x3.0p-1 | |
0.15e+1 | 0x0.cp+1 |
Konstanty reálného čísla jsou standardně typu double. Při zadávání přípony fje typ přiřazen konstantě floata při zadávání lnebo L - long double. Konstanta bude považována za skutečnou, pokud obsahuje tečku nebo písmeno, pnebo Pv případě hexadecimálního zápisu s předponou 0x. Desetinný zápis může obsahovat exponent za písmeny enebo E. V případě hexadecimálního zápisu se exponent uvádí za písmeny pnebo Pje povinný, což odlišuje reálné hexadecimální konstanty od celých čísel. V šestnáctkové soustavě je exponent mocninou 2 [12] .
Znakové konstanty jsou uzavřeny v jednoduchých uvozovkách ( ') a předpona určuje jak datový typ znakové konstanty, tak kódování, ve kterém bude znak reprezentován. V C je znaková konstanta bez prefixu typu int[13] , na rozdíl od C++ , kde je znaková konstanta char.
Předpona | Datový typ | Kódování |
---|---|---|
Ne | int | ASCII |
u | char16_t | 16bitové vícebajtové kódování řetězce |
U | char32_t | 32bitové vícebajtové kódování řetězce |
L | wchar_t | Široké kódování řetězce |
Řetězcové literály jsou uzavřeny do dvojitých uvozovek a mohou mít předponu typu dat a kódování řetězce. Řetězcové literály jsou obyčejná pole. Ve vícebajtových kódováních, jako je UTF-8 , však může jeden znak zabírat více než jeden prvek pole. Ve skutečnosti jsou řetězcové literály const [14] , ale na rozdíl od C++ jejich datové typy neobsahují modifikátor const.
Předpona | Datový typ | Kódování |
---|---|---|
Ne | char * | ASCII nebo vícebajtové kódování |
u8 | char * | UTF-8 |
u | char16_t * | 16bitové vícebajtové kódování |
U | char32_t * | 32bitové vícebajtové kódování |
L | wchar_t * | Široké kódování řetězce |
Několik po sobě jdoucích řetězcových konstant oddělených mezerami nebo novými řádky je při kompilaci zkombinováno do jednoho řetězce, který se často používá ke stylizaci kódu řetězce oddělením částí řetězcové konstanty na různých řádcích, aby se zlepšila čitelnost [16] .
Pojmenované konstantyMakro | #define BUFFER_SIZE 1024 |
Anonymní výčet |
enum { BUFFER_SIZE = 1024 }; |
Proměnná jako konstanta |
const int velikost_bufferu = 1024 ; externí konst int velikost_bufferu ; |
V jazyce C je pro definování konstant obvyklé používat definice maker deklarované pomocí direktivy preprocesoru [17] : #define
#define název konstanty [ hodnota ]Takto zavedená konstanta bude ve svém rozsahu účinná od okamžiku nastavení konstanty až do konce programového kódu, nebo dokud nebude účinek dané konstanty zrušen direktivou #undef:
#undef konstantní jménoStejně jako u každého makra je u pojmenované konstanty hodnota konstanty automaticky nahrazena v kódu programu, kdykoli je použit název konstanty. Proto při deklarování celých nebo reálných čísel uvnitř makra může být nutné explicitně specifikovat datový typ pomocí příslušné doslovné přípony, jinak bude číslo implicitně nastaveno na typ intv případě celého čísla nebo typ double v případě nemovitý.
Pro celá čísla existuje další způsob, jak vytvořit pojmenované konstanty - pomocí operátorových výčtů enum[17] . Tato metoda je však vhodná pouze pro typy menší nebo rovné type a nepoužívá se ve standardní knihovně [18] . int
Je také možné vytvořit konstanty jako proměnné s kvalifikátorem const, ale na rozdíl od ostatních dvou metod takové konstanty spotřebovávají paměť, lze na ně ukázat a nelze je použít v době kompilace [17] :
Klíčová slova jsou identifikátory určené k provedení konkrétního úkolu ve fázi kompilace nebo jako rady a pokyny pro kompilátor.
Klíčová slova | Účel | Standard |
---|---|---|
sizeof | Získání velikosti objektu v době kompilace | C89 |
typedef | Zadání alternativního názvu pro typ | |
auto,register | Kompilátor poradí, kde jsou proměnné uloženy | |
extern | Řekněte kompilátoru, aby hledal objekt mimo aktuální soubor | |
static | Deklarace statického objektu | |
void | Žádná značka hodnoty; v ukazatelích znamená libovolná data | |
char... short_ int_long | Celočíselné typy a jejich modifikátory velikosti | |
signed,unsigned | Modifikátory typu Integer, které je definují jako podepsané nebo bez znaménka | |
float,double | Reálné datové typy | |
const | Modifikátor datového typu, který kompilátoru sděluje, že proměnné tohoto typu jsou pouze pro čtení | |
volatile | Pokyn kompilátoru, aby změnil hodnotu proměnné zvenčí | |
struct | Datový typ určený jako struktura se sadou polí | |
enum | Datový typ, který ukládá jednu ze sady celočíselných hodnot | |
union | Datový typ, který může ukládat data v reprezentacích různých datových typů | |
do.. for_while | Příkazy smyčky | |
if,else | Podmíněný operátor | |
switch.. case_default | Operátor výběru podle celočíselného parametru | |
break,continue | Příkazy přerušení smyčky | |
goto | Operátor nepodmíněného skoku | |
return | Návrat z funkce | |
inline | Inline deklarace funkce | C99 [20] |
restrict | Deklarování ukazatele, který odkazuje na blok paměti, na který neodkazuje žádný jiný ukazatel | |
_Bool[b] | booleovský datový typ | |
_Complex[c] ,_Imaginary [d] | Typy používané pro výpočty komplexních čísel | |
_Atomic | Modifikátor typu, díky kterému je atomický | C11 |
_Alignas[E] | Explicitní určení zarovnání bajtů pro datový typ | |
_Alignof[F] | Získání zarovnání pro daný datový typ v době kompilace | |
_Generic | Výběr jedné ze sady hodnot v době kompilace na základě řízeného datového typu | |
_Noreturn[G] | Oznámení kompilátoru, že funkce nemůže normálně skončit (tj. pomocí return) | |
_Static_assert[h] | Zadání výrazů ke kontrole při kompilaci | |
_Thread_local[i] | Deklarování lokální proměnné vlákna |
Jazyková norma kromě klíčových slov definuje vyhrazené identifikátory, jejichž použití může vést k nekompatibilitě s budoucími verzemi normy. Všechna slova kromě klíčových slov, která začínají podtržítkem ( _) následovaným buď velkým písmenem ( A- Z) nebo jiným podtržítkem [21] , jsou vyhrazena . Ve standardech C99 a C11 byly některé z těchto identifikátorů použity pro klíčová slova nového jazyka.
V rozsahu souboru je vyhrazeno použití jakýchkoli názvů začínajících podtržítkem ( _) [21] , to znamená, že je povoleno pojmenovávat typy, konstanty a proměnné deklarované v bloku instrukcí, například uvnitř funkcí, s podtržítkem.
Vyhrazenými identifikátory jsou také všechna makra standardní knihovny a jména z ní spojená ve fázi propojení [21] .
Použití vyhrazených identifikátorů v programech je standardem definováno jako nedefinované chování . Pokus o zrušení jakéhokoli standardního makra prostřednictvím #undeftaké povede k nedefinovanému chování [21] .
Text programu v jazyce C může obsahovat fragmenty, které nejsou součástí komentářů ke kódu programu . Komentáře jsou v textu programu označeny zvláštním způsobem a při kompilaci se přeskakují.
Zpočátku byly ve standardu C89 k dispozici vložené komentáře, které bylo možné umístit mezi sekvence znaků /*a */. V tomto případě není možné vnořit jeden komentář do druhého, protože první zaznamenaná sekvence */komentář ukončí a text bezprostředně následující za zápisem */bude kompilátorem vnímán jako zdrojový kód programu.
Další standard, C99 , zavedl ještě další způsob označování komentářů: za komentář je považován text začínající sekvencí znaků //a končící na konci řádku [20] .
Komentáře se často používají k vlastní dokumentaci zdrojového kódu, vysvětlují složité části, popisují účel určitých souborů a popisují pravidla pro používání a práci s určitými funkcemi, maker, datovými typy a proměnnými. Existují postprocesory, které dokážou převést speciálně formátované komentáře na dokumentaci. Mezi takovými postprocesory s jazykem C může fungovat dokumentační systém Doxygen .
Operátory používané ve výrazech jsou nějaká operace, která se provádí s operandy a která vrací vypočítanou hodnotu – výsledek operace. Operand může být konstanta, proměnná, výraz nebo volání funkce. Operátor může být speciální znak, sada speciálních znaků nebo speciální slovo. Operátory se rozlišují podle počtu zúčastněných operandů, konkrétně rozlišují mezi unárními operátory, binárními operátory a ternárními operátory.
Unární operátoryUnární operátory provádějí operaci s jedním argumentem a mají následující formát operace:
[ operátor ] [ operand ]Operace inkrementace a dekrementace postfixu mají opačný formát:
[ operand ] [ operátor ]+ | unární plus | ~ | Převzetí návratového kódu | & | Získání adresy | ++ | Přírůstek předpony nebo přípony | sizeof | Získání počtu bajtů obsazených objektem v paměti; lze použít jako provoz i jako operátor |
- | unární mínus | ! | logická negace | * | Dereferencování ukazatele | -- | Snížení předpony nebo přípony | _Alignof | Získání zarovnání pro daný typ dat |
Operátory inkrementace a dekrementace na rozdíl od ostatních unárních operátorů mění hodnotu svého operandu. Operátor prefixu nejprve hodnotu upraví a poté ji vrátí. Postfix nejprve vrátí hodnotu a teprve poté ji změní.
Binární operátoryBinární operátory jsou umístěny mezi dvěma argumenty a provádějí s nimi operaci:
[ operand ] [ operátor ] [ operand ]+ | Přidání | % | Vezmeme zbytek divize | << | Bitový levý Shift | > | Více | == | Rovná se |
- | Odčítání | & | Bitové AND | >> | Bitový posun doprava | < | Méně | != | Ne rovné |
* | Násobení | | | Bitově NEBO | && | logické AND | >= | Větší nebo rovno | ||
/ | Divize | ^ | Bitový XOR | || | Logické NEBO | <= | Menší nebo rovno |
Binární operátory v C také zahrnují levé operátory přiřazení, které provádějí operaci s levým a pravým argumentem a vkládají výsledek do levého argumentu.
= | Přiřazení hodnoty pravého argumentu doleva | %= | Zbytek po dělení levého operandu pravým | ^= | Bitové XOR pravého operandu na levý operand |
+= | Přidání k levému operandu k pravému | /= | Dělení levého operandu pravým | <<= | Bitový posun levého operandu doleva o počet bitů daný pravým operandem |
-= | Odečítání od levého operandu od pravého | &= | Bitový AND pravý operand doleva | >>= | Bitový posun levého operandu doprava o počet bitů určený pravým operandem |
*= | Násobení levého operandu pravým | |= | Bitové OR pravého operandu doleva |
V C je pouze jeden ternární operátor, zkrácený podmíněný operátor, který má následující tvar:
[ podmínka ] ?[ výraz1 ] :[ výraz2 ]Zkrácený podmíněný operátor má tři operandy:
Operátor je v tomto případě kombinací znaků ?a :.
Výraz je uspořádaná množina operací s konstantami, proměnnými a funkcemi. Výrazy obsahují operace skládající se z operandů a operátorů . Pořadí, ve kterém jsou operace prováděny, závisí na formě záznamu a na prioritě operací. Každý výraz má hodnotu – výsledek provedení všech operací obsažených ve výrazu. Během vyhodnocování výrazu se v závislosti na operacích mohou měnit hodnoty proměnných a lze také provádět funkce, pokud jsou ve výrazu přítomna jejich volání.
Mezi výrazy se rozlišuje třída levých přípustných výrazů - výrazů, které mohou být přítomny vlevo od přiřazovacího znaku.
Priorita provádění operacíPriorita operací je definována standardem a určuje pořadí, ve kterém budou operace prováděny. Operace v C se provádějí podle tabulky priorit níže [25] [26] .
Priorita | žetony | Úkon | Třída | Asociativnost |
---|---|---|---|---|
jeden | a[index] | Odkazování podle indexu | postfix | zleva doprava → |
f(argumenty) | Volání funkce | |||
. | Přístup do terénu | |||
-> | Přístup do terénu pomocí ukazatele | |||
++ -- | Pozitivní a negativní přírůstek | |||
(zadejte název ) {inicializátoru} | Složený literál (C99) | |||
(zadejte název ) {inicializátoru,} | ||||
2 | ++ -- | Přírůstky kladné a záporné předpony | unární | ← zprava doleva |
sizeof | Získání velikosti | |||
_Alignof[F] | Získat zarovnání ( C11 ) | |||
~ | Bitově NE | |||
! | Logické NE | |||
- + | Indikace znaménka (mínus nebo plus) | |||
& | Získání adresy | |||
* | Odkaz na ukazatel (dereference) | |||
(název typu) | Typové odlévání | |||
3 | * / % | Násobení, dělení a zbytek | binární | zleva doprava → |
čtyři | + - | Sčítání a odčítání | ||
5 | << >> | Shift doleva a doprava | ||
6 | < > <= >= | Srovnávací operace | ||
7 | == != | Kontrola rovnosti nebo nerovnosti | ||
osm | & | Bitové AND | ||
9 | ^ | Bitový XOR | ||
deset | | | Bitově NEBO | ||
jedenáct | && | logické AND | ||
12 | || | Logické NEBO | ||
13 | ? : | Stav | trojice | ← zprava doleva |
čtrnáct | = | Přiřazení hodnoty | binární | |
+= -= *= /= %= <<= >>= &= ^= |= | Operace pro změnu hodnoty vlevo | |||
patnáct | , | Sekvenční výpočty | zleva doprava → |
Priority operátorů v C se ne vždy ospravedlňují a někdy vedou k intuitivně obtížně předvídatelným výsledkům. Například protože unární operátory mají asociativitu zprava doleva, *p++výsledkem vyhodnocení výrazu bude přírůstek ukazatele následovaný dereference ( *(p++)), spíše než přírůstek ukazatele ( (*p)++). Proto se v případě obtížně srozumitelných situací doporučuje výrazy explicitně seskupovat pomocí závorek [26] .
Další důležitou vlastností jazyka C je, že vyhodnocení hodnot argumentů předávaných volání funkce není sekvenční [27] , to znamená, že čárka oddělující argumenty neodpovídá sekvenčnímu vyhodnocení z tabulky priorit. V následujícím příkladu mohou být volání funkcí zadaná jako argumenty jiné funkci v libovolném pořadí:
int x ; x = vypočítat ( get_arg1 (), get_arg2 ()); // nejprve zavolejte get_arg2().Také se nemůžete spoléhat na prioritu operací v případě vedlejších efektů , které se objeví během vyhodnocování výrazu, protože to povede k nedefinovanému chování [27] .
Sekvenční body a vedlejší efektyDodatek C jazykového standardu definuje sadu sekvenčních bodů , u kterých je zaručeno, že nebudou mít trvalé vedlejší účinky z výpočtů. To znamená, že sekvenční bod je fází výpočtů, která odděluje hodnocení výrazů mezi sebou tak, že výpočty, které nastaly před sekvenčním bodem, včetně vedlejších efektů, již skončily a za sekvenčním bodem ještě nezačaly [28 ] . Vedlejším efektem může být změna hodnoty proměnné během vyhodnocování výrazu. Změna hodnoty zahrnuté ve výpočtu spolu s vedlejším efektem změny stejné hodnoty na další bod sekvence povede k nedefinovanému chování. Totéž se stane, pokud se do výpočtu zapojí dvě nebo více bočních změn stejné hodnoty [27] .
Bod na trase | Událost před | Událost po |
---|---|---|
Volání funkce | Výpočet ukazatele na funkci a jejích argumentů | Volání funkce |
Logické operátory AND ( &&), OR ( ||) a sekvenční výpočet ( ,) | Výpočet prvního operandu | Výpočet druhého operandu |
Operátor těsnopisu ( ?:) | Výpočet operandu sloužícího jako podmínka | Výpočet 2. nebo 3. operandu |
Mezi dvěma úplnými výrazy (ne vnořenými) | Jeden úplný výraz | Následující úplný výraz |
Dokončený kompletní deskriptor | ||
Těsně před návratem z funkce knihovny | ||
Po každé konverzi spojené s formátovaným specifikátorem I/O | ||
Bezprostředně před a bezprostředně po každém volání porovnávací funkce a mezi voláním porovnávací funkce a všemi pohyby provedenými na argumentech předávaných porovnávací funkci |
Úplné výrazy jsou [27] :
V následujícím příkladu se proměnná mezi body sekvence třikrát změní, což má za následek nedefinovaný výsledek:
int i = 1 ; // Deskriptor je první bod sekvence, úplný výraz je druhý i += ++ i + 1 ; // Úplný výraz - bod třetí sekvence printf ( "%d \n " , i ); // Může mít výstup buď 4 nebo 5Další jednoduché příklady nedefinovaného chování, kterému je třeba se vyhnout:
i = i ++ + 1 ; // nedefinované chování i = ++ i + 1 ; // také nedefinované chování printf ( "%d, %d \n " , -- i , ++ i ); // nedefinované chování printf ( "%d, %d \n " , ++ i , ++ i ); // také nedefinované chování printf ( "%d, %d \n " , i = 0 , i = 1 ); // nedefinované chování printf ( "%d, %d \n " , i = 0 , i = 0 ); // také nedefinované chování a [ i ] = i ++ ; // nedefinované chování a [ i ++ ] = i ; // také nedefinované chováníŘídicí příkazy jsou navrženy k provádění akcí a řízení toku provádění programu. Několik po sobě jdoucích příkazů tvoří posloupnost příkazů .
Prázdný výpisNejjednodušší jazykovou konstrukcí je prázdný výraz nazývaný prázdný příkaz [29] :
;Prázdný příkaz nedělá nic a může být umístěn kdekoli v programu. Běžně se používá ve smyčkách s chybějícím tělem [30] .
NávodInstrukce je druh základní akce:
( výraz );Akce tohoto operátoru je provedení výrazu uvedeného v těle operátoru.
Několik po sobě jdoucích instrukcí tvoří sekvenci instrukcí .
Instrukční blokPokyny lze seskupit do speciálních bloků následující formy:
{
( sekvence instrukcí )},
Blok příkazů, někdy nazývaný také složený příkaz, je ohraničen levou složenou závorkou ( {) na začátku a pravou složenou závorkou ( }) na konci.
Ve funkcích blok příkazu označuje tělo funkce a je součástí definice funkce. Složený příkaz lze také použít v příkazech smyčky, podmínky a volby.
Podmíněné příkazyV jazyce jsou dva podmíněné operátory, které implementují větvení programu:
Nejjednodušší forma operátoraif
if(( podmínka ) )( provozovatel ) ( další výpis )Operátor iffunguje takto:
Zejména následující kód, pokud je splněna zadaná podmínka, neprovede žádnou akci, protože ve skutečnosti se provede prázdný příkaz:
if(( podmínka )) ;Složitější forma operátoru ifobsahuje klíčové slovo else:
if(( podmínka ) )( provozovatel ) else( alternativní operátor ) ( další výpis )Pokud zde není splněna podmínka uvedená v závorkách, provede se příkaz zadaný za klíčovým slovem else.
I když standard umožňuje specifikovat příkazy na jednom řádku ifnebo jako elsejeden řádek, je to považováno za špatný styl a snižuje čitelnost kódu. Doporučuje se vždy zadat blok příkazů pomocí složených závorek jako těla [31] .
Příkazy provedení smyčkySmyčka je část kódu, která obsahuje
V souladu s tím existují dva typy cyklů:
Postpodmíněná smyčka zaručuje, že tělo smyčky bude provedeno alespoň jednou.
Jazyk C poskytuje dvě varianty cyklů s podmínkou: whilea for.
while(podmínka) [ tělo smyčky ] for( příkaz ;podmínky inicializačního bloku [ tělo smyčky ],;)Smyčka forse také nazývá parametrická, je ekvivalentní následujícímu bloku příkazů:
[ inicializační blok ] while(stav) { [ tělo smyčky ] [ operátor ] }V normální situaci inicializační blok obsahuje nastavení počáteční hodnoty proměnné, která se nazývá proměnná smyčky, a příkaz, který se provede ihned poté, co tělo smyčky změní hodnoty použité proměnné, podmínka obsahuje porovnání hodnoty použité smyčkové proměnné s nějakou předdefinovanou hodnotou, a jakmile se porovnávání přestane provádět, smyčka se přeruší a začne se provádět programový kód bezprostředně následující za příkazem smyčky.
Pro cyklus do-whileje podmínka uvedena za tělem cyklu:
do[ tělo smyčky ] while( podmínka)Podmínka cyklu je booleovský výraz. Implicitní přetypování však umožňuje použít aritmetický výraz jako podmínku smyčky. To vám umožní uspořádat takzvanou "nekonečnou smyčku":
while(1);Totéž lze provést s operátorem for:
for(;;);V praxi se takové nekonečné smyčky obvykle používají ve spojení s break, gotonebo return, které přerušují smyčku různými způsoby.
Stejně jako u podmíněného příkazu je použití jednořádkového těla bez jeho uzavření do bloku příkazu se složenými závorkami považováno za špatný styl, který snižuje čitelnost kódu [31] .
Operátory nepodmíněného skokuNepodmíněné operátory větví umožňují přerušit provádění libovolného bloku výpočtů a přejít na jiné místo v programu v rámci aktuální funkce. Operátory nepodmíněného skoku se obvykle používají ve spojení s podmíněnými operátory.
goto[ štítek ],Štítek je nějaký identifikátor, který přenáší řízení na operátora, který je v programu označen zadaným štítkem:
[ štítek ] :[ operátor ]Pokud zadaný štítek není v programu přítomen nebo pokud existuje více příkazů se stejným štítkem, kompilátor ohlásí chybu.
Přenos ovládání je možný pouze v rámci funkce, kde je použit přechodový operátor, proto pomocí operátoru gotonelze přenést ovládání na jinou funkci.
Další příkazy skoku se týkají cyklů a umožňují přerušit provádění těla smyčky:
Příkaz breakmůže také přerušit činnost příkazu switch, takže uvnitř příkazu switchběžícího ve smyčce příkaz breaknebude moci smyčku přerušit. Zadáno v těle smyčky, přeruší práci nejbližší vnořené smyčky.
Operátor continuelze použít pouze uvnitř operátorů do, whilea for. Pro smyčky whilea do-whileoperátor continuezpůsobí test podmínky smyčky a v případě smyčky for provedení operátoru uvedeného ve 3. parametru smyčky před kontrolou podmínky pro pokračování smyčky.
Funkce return příkazOperátor returnpřeruší provádění funkce, ve které je použit. Pokud by funkce neměla vracet hodnotu, použije se volání bez návratové hodnoty:
return;Pokud funkce musí vrátit hodnotu, je návratová hodnota uvedena za operátorem:
return[ hodnota ];Pokud jsou po příkazu return v těle funkce nějaké další příkazy, pak tyto příkazy nebudou nikdy provedeny, v takovém případě může kompilátor vydat varování. Za operátorem však returnmohou být uvedeny pokyny pro alternativní ukončení funkce, například omylem, a přechod na tyto operátory lze provést pomocí operátoru gotoza jakýchkoli podmínek .
Při deklaraci proměnné se zadává její typ a název a lze také zadat počáteční hodnotu:
[deskriptor] [jméno];nebo
[deskriptor] [jméno] =[inicializátor] ;,kde
Pokud proměnné není přiřazena počáteční hodnota, pak v případě globální proměnné je její hodnota vyplněna nulami a u lokální proměnné bude počáteční hodnota nedefinovaná.
V deskriptoru proměnné můžete určit proměnnou jako globální, ale omezenou na rozsah souboru nebo funkce, pomocí klíčového slova static. Pokud je proměnná deklarována jako globální bez klíčového slova static, lze k ní přistupovat i z jiných souborů, kde je nutné tuto proměnnou deklarovat bez inicializátoru, ale s klíčovým slovem extern. Adresy těchto proměnných se určují v čase spojení .
Funkce je nezávislá část programového kódu, kterou lze v programu znovu použít. Funkce mohou přijímat argumenty a mohou vracet hodnoty. Funkce mohou mít při svém provádění i vedlejší efekty : změna globálních proměnných, práce se soubory, interakce s operačním systémem nebo hardwarem [28] .
Chcete-li definovat funkci v C, musíte ji deklarovat:
Je také nutné poskytnout definici funkce, která obsahuje blok příkazů, které implementují chování funkce.
Nedeklarování konkrétní funkce je chybou, pokud je funkce použita mimo rozsah definice, což v závislosti na implementaci vede ke zprávám nebo varováním.
Pro volání funkce stačí zadat její název s parametry uvedenými v závorkách. V tomto případě je adresa volacího bodu umístěna na zásobník, jsou vytvořeny a inicializovány proměnné zodpovědné za parametry funkce a řízení je přeneseno do kódu, který implementuje volanou funkci. Po provedení funkce je uvolněna paměť přidělená během volání funkce, návrat k bodu volání a pokud je volání funkce součástí nějakého výrazu, hodnota vypočítaná uvnitř funkce je předána návratovému bodu.
Pokud za funkcí nejsou uvedeny závorky, kompilátor to interpretuje jako získání adresy funkce. Adresu funkce lze zadat do ukazatele a následně funkci zavolat pomocí ukazatele na něj, což se aktivně využívá např. v pluginových systémech [32] .
Pomocí klíčového slova inlinemůžete označit funkce, jejichž volání chcete provést co nejrychleji. Překladač může nahradit kód takových funkcí přímo v místě jejich volání [33] . Na jednu stranu to zvyšuje množství spustitelného kódu, ale na druhou stranu šetří čas jeho provádění, protože se nepoužívá časově náročná operace volání funkce. Vzhledem k architektuře počítačů však mohou funkce inlining buď zrychlit nebo zpomalit aplikaci jako celek. Nicméně v mnoha případech jsou inline funkce preferovanou náhradou maker [34] .
Deklarace funkceDeklarace funkce má následující formát:
[deskriptor] [jméno] ([seznam] );,kde
Znakem deklarace funkce je ;symbol „ “, takže deklarace funkce je instrukce.
V nejjednodušším případě [deklarátor] obsahuje označení konkrétního typu návratové hodnoty. Funkce, která by neměla vracet žádnou hodnotu, je deklarována jako funkce typu void.
V případě potřeby může deskriptor obsahovat modifikátory specifikované pomocí klíčových slov:
Seznam parametrů funkce definuje signaturu funkce.
C neumožňuje deklarovat více funkcí stejným názvem, přetížení funkcí není podporováno [36] .
Definice funkceDefinice funkce má následující formát:
[deskriptor] [jméno] ([seznam] )[tělo]Kde [deklarátor], [jméno] a [seznam] jsou stejné jako v deklaraci a [tělo] je složený příkaz, který představuje konkrétní implementaci funkce. Kompilátor rozlišuje mezi stejnojmennými definicemi funkcí jejich podpisem, a tak (podpisem) vzniká spojení mezi definicí a odpovídající deklarací.
Tělo funkce vypadá takto:
{ [sekvence příkazů] return([vrácená hodnota]); }Návrat z funkce se provádí pomocí operátoru , který buď určuje návratovou hodnotu, nebo ji nespecifikuje, v závislosti na datovém typu vráceném funkcí. Ve vzácných případech může být funkce označena jako nevracející se pomocí makra ze souboru záhlaví , v takovém případě není vyžadován žádný příkaz . Takto lze označit například funkce, které v sobě bezpodmínečně volají [33] . returnnoreturnstdnoreturn.hreturnabort()
Volání funkceVolání funkce má provést následující akce:
V závislosti na implementaci kompilátor buď striktně zajistí, aby se typ skutečného parametru shodoval s typem formálního parametru, nebo, pokud je to možné, provede implicitní konverzi typu, což samozřejmě vede k vedlejším účinkům.
Pokud je funkci předána proměnná, pak se při volání funkce vytvoří její kopie ( paměť je alokována na zásobníku a hodnota je zkopírována). Například předání struktury funkci způsobí zkopírování celé struktury. Pokud je předán ukazatel na strukturu, zkopíruje se pouze hodnota ukazatele. Předání pole funkci také způsobí pouze zkopírování ukazatele na jeho první prvek. V tomto případě, abyste explicitně označili, že adresa začátku pole je brána jako vstup funkce, a nikoli ukazatel na jednu proměnnou, místo deklarování ukazatele za názvem proměnné můžete vložit hranaté závorky, např. příklad:
void example_func ( pole int []); // pole je ukazatel na první prvek pole typu intC umožňuje vnořené hovory. Hloubka vnoření volání má zjevné omezení související s velikostí zásobníku přiděleného programu. Proto implementace C nastavují limit na hloubku vnoření.
Speciálním případem vnořeného volání je volání funkce uvnitř těla volané funkce. Takové volání se nazývá rekurzivní a používá se k organizaci jednotných výpočtů. Vzhledem k přirozenému omezení vnořených volání je rekurzivní implementace nahrazena implementací využívající smyčky.
Celočíselné datové typy mají velikost od alespoň 8 do alespoň 32 bitů. Standard C99 zvyšuje maximální velikost celého čísla na alespoň 64 bitů. Datové typy Integer se používají k ukládání celých čísel (typ charse používá také k ukládání znaků ASCII). Všechny velikosti rozsahů datových typů níže jsou minimální a mohou být na dané platformě větší [37] .
V důsledku minimálních velikostí typů norma vyžaduje, aby velikosti integrálních typů splňovaly podmínku:
1= ≤ ≤ ≤ ≤ . sizeof(char)sizeof(short)sizeof(int)sizeof(long)sizeof(long long)
Velikosti některých typů z hlediska počtu bajtů se tedy mohou shodovat, pokud je splněna podmínka pro minimální počet bitů. Dokonce chara longmůže mít stejnou velikost, pokud jeden bajt bude trvat 32 bitů nebo více, ale takové platformy budou velmi vzácné nebo nebudou existovat. Standard zaručuje, že typ je char vždy 1 bajt. Velikost bajtu v bitech je určena konstantou CHAR_BITv hlavičkovém souboru limits.h, což je 8 bitů na systémech kompatibilních s POSIX [38] .
Minimální rozsah hodnot celočíselných typů podle standardu je definován od do pro typy se znaménkem a od do pro typy bez znaménka, kde N je bitová hloubka typu. Implementace kompilátorů mohou tento rozsah rozšířit podle svého uvážení. V praxi se pro podepsané typy častěji používá rozsah od do . Minimální a maximální hodnoty každého typu jsou specifikovány v souboru jako definice maker. -(2N-1-1)2N-1-102N-2N-12N-1-1limits.h
Zvláštní pozornost by měla být věnována typu char. Formálně se jedná o samostatný typ, ale ve skutečnosti je charekvivalentní buď signed char, nebo unsigned char, v závislosti na kompilátoru [39] .
Aby se předešlo záměně velikostí typů, zavedl standard C99 nové datové typy popsané v stdint.h. Mezi nimi jsou takové typy jako: , , , kde = 8, 16, 32 nebo 64. Předpona označuje minimální typ, který pojme bity, předpona označuje typ alespoň 16 bitů, který je na této platformě nejrychlejší. Typy bez předpon označují typy s pevnou velikostí bitů. intN_tint_leastN_tint_fastN_tNleast-Nfast-N
Typy s předponami least-a fast-lze považovat za náhradu typů int, short, long, jen s tím rozdílem, že ty první dávají programátorovi na výběr mezi rychlostí a velikostí.
Datový typ | Velikost | Minimální rozsah hodnot | Standard |
---|---|---|---|
signed char | minimálně 8 bitů | od −127 [40] (= -(2 7 −1)) do 127 | C90 [j] |
int_least8_t | C99 | ||
int_fast8_t | |||
unsigned char | minimálně 8 bitů | 0 až 255 (=2 8 −1) | C90 [j] |
uint_least8_t | C99 | ||
uint_fast8_t | |||
char | minimálně 8 bitů | −127 až 127 nebo 0 až 255 v závislosti na kompilátoru | C90 [j] |
short int | minimálně 16 bitů | od -32,767 (= -(2 15 -1)) do 32,767 | C90 [j] |
int | |||
int_least16_t | C99 | ||
int_fast16_t | |||
unsigned short int | minimálně 16 bitů | 0 až 65,535 (= 2 16 −1) | C90 [j] |
unsigned int | |||
uint_least16_t | C99 | ||
uint_fast16_t | |||
long int | minimálně 32 bitů | −2 147 483 647 až 2 147 483 647 | C90 [j] |
int_least32_t | C99 | ||
int_fast32_t | |||
unsigned long int | minimálně 32 bitů | 0 až 4 294 967 295 (= 2 32 −1) | C90 [j] |
uint_least32_t | C99 | ||
uint_fast32_t | |||
long long int | minimálně 64 bitů | -9,223,372,036,854,775,807 až 9,223,372,036,854,775,807 | C99 |
int_least64_t | |||
int_fast64_t | |||
unsigned long long int | minimálně 64 bitů | 0 až 18 446 744 073 709 551 615 (= 264 −1 ) | |
uint_least64_t | |||
uint_fast64_t | |||
int8_t | 8 bitů | -127 až 127 | |
uint8_t | 8 bitů | 0 až 255 (=2 8 −1) | |
int16_t | 16 bit | -32,767 až 32,767 | |
uint16_t | 16 bit | 0 až 65,535 (= 2 16 −1) | |
int32_t | 32 bitů | −2 147 483 647 až 2 147 483 647 | |
uint32_t | 32 bitů | 0 až 4 294 967 295 (= 2 32 −1) | |
int64_t | 64 bitů | -9,223,372,036,854,775,807 až 9,223,372,036,854,775,807 | |
uint64_t | 64 bitů | 0 až 18 446 744 073 709 551 615 (= 264 −1 ) | |
Tabulka ukazuje minimální rozsah hodnot podle jazykové normy. Kompilátory jazyka C mohou rozšířit rozsah hodnot. |
Od standardu C99 byly také přidány typy intmax_ta uintmax_t, odpovídající největším podepsaným a nepodepsaným typům. Tyto typy jsou vhodné při použití v makrech k ukládání mezilehlých nebo dočasných hodnot během operací s celočíselnými argumenty, protože umožňují přizpůsobit hodnoty libovolného typu. Tyto typy se například používají v makrech pro porovnání celých čísel knihovny Check unit testing pro C [41] .
V C existuje několik dalších celočíselných typů pro bezpečné zacházení s datovým typem ukazatele : intptr_ta uintptr_t. ptrdiff_tTypy intptr_ta uintptr_tze standardu C99 jsou navrženy pro ukládání hodnot se znaménkem a bez znaménka, které se vejdou do velikosti ukazatele. Tyto typy se často používají k uložení libovolného celého čísla do ukazatele, například jako způsob, jak se zbavit zbytečné alokace paměti při registraci funkcí zpětné vazby [42] nebo při použití propojených seznamů třetích stran, asociativních polí a dalších struktur, ve kterých data jsou uložena ukazatelem. Typ ptrdiff_tze souboru záhlaví stddef.hje navržen tak, aby bezpečně uložil rozdíl dvou ukazatelů.
Pro uložení velikosti je k dispozici nepodepsaný typ size_tze souboru záhlaví stddef.h. Tento typ je schopen pojmout maximální možný počet bajtů dostupných na ukazateli a obvykle se používá k uložení velikosti v bajtech. Hodnotu tohoto typu vrací operátor sizeof[43] .
Casting typu IntegerKonverze celočíselného typu mohou probíhat buď explicitně, pomocí operátoru přetypování, nebo implicitně. Hodnoty typů menší než int, když se účastní jakýchkoli operací nebo když jsou předány volání funkce, jsou automaticky přetypovány na typ inta pokud převod není možný, na typ unsigned int. Často jsou takové implicitní přetypování nutné, aby byl výsledek výpočtu správný, ale někdy vedou k intuitivně nepochopitelným chybám ve výpočtech. Pokud například operace zahrnuje čísla typu inta unsigned inta hodnota se znaménkem je záporná, pak převod záporného čísla na typ bez znaménka povede k přetečení a velmi velké kladné hodnotě, což může vést k nesprávnému výsledku operací porovnání. [44] .
Podepsané a nepodepsané typy jsou menší nežint | Podepsané je méně než nepodepsané a nepodepsané není méněint |
---|---|
#include <stdio.h> znak se znaménkem x = -1 ; znak bez znaménka y = 0 ; if ( x > y ) { // podmínka je false printf ( "Zpráva se nezobrazí. \n " ); } if ( x == UCHAR_MAX ) { // podmínka je false printf ( "Zpráva se nezobrazí. \n " ); } | #include <stdio.h> znak se znaménkem x = -1 ; bez znaménka int y = 0 ; if ( x > y ) { // podmínka je pravdivá printf ( "Přetečení v proměnné x. \n " ); } if (( x == UINT_MAX ) && ( x == ULONG_MAX )) { // podmínka bude vždy true printf ( "Přetečení v proměnné x. \n " ); } |
V tomto příkladu budou oba typy, podepsané i nepodepsané, přetypovány na podepsané int, protože umožňuje, aby se vešly rozsahy obou typů. Proto bude srovnání v podmíněném operátoru správné. | Podepsaný typ bude přetypován na nepodepsaný, protože nepodepsaný typ je větší nebo roven velikosti int, ale dojde k přetečení, protože v typu bez znaménka není možné reprezentovat zápornou hodnotu. |
Automatické přetypování bude také fungovat, pokud jsou ve výrazu použity dva nebo více různých celočíselných typů. Norma definuje sadu pravidel, podle kterých se volí typová konverze, která může dát správný výsledek výpočtu. Různým typům jsou v rámci transformace přiřazeny různé hodnosti a samotné hodnosti jsou založeny na velikosti typu. Pokud jsou ve výrazu zahrnuty různé typy, obvykle se volí přetypování těchto hodnot na typ vyšší úrovně [44] .
Reálná číslaČísla s plovoucí desetinnou čárkou v C jsou reprezentována třemi základními typy: float, doublea long double.
Reálná čísla mají reprezentaci, která se velmi liší od celých čísel. Konstanty reálných čísel různých typů, zapsané v desítkové soustavě, se nemusí navzájem rovnat. Například podmínka 0.1 == 0.1fbude nepravdivá kvůli ztrátě přesnosti typu float, zatímco podmínka 0.5 == 0.5fbude pravdivá, protože tato čísla jsou v binární reprezentaci konečná. Podmínka přetypování však (float) 0.1 == 0.1fbude také pravdivá, protože přetypování na méně přesný typ ztratí bity, které tyto dvě konstanty odlišují.
Aritmetické operace s reálnými čísly jsou také nepřesné a často mají nějakou plovoucí chybu [45] . Největší chyba nastane při práci s hodnotami, které se blíží minimu možnému pro konkrétní typ. Chyba se také může ukázat jako velká při výpočtu přes velmi malá (≪ 1) a velmi velká čísla (≫ 1). V některých případech lze chybu snížit změnou algoritmů a metod výpočtu. Například při nahrazení vícenásobného sčítání násobením se chyba může snížit tolikrát, kolikrát bylo původně operací sčítání.
V hlavičkovém souboru math.hjsou také dva další typy float_ta double_t, které odpovídají minimálně typům floatresp double., ale mohou se od nich lišit. Typy float_ta double_tjsou přidány ve standardu C99 a jejich shoda se základními typy je určena hodnotou makra FLT_EVAL_METHOD.
Datový typ | Velikost | Standard |
---|---|---|
float | 32 bitů | IEC 60559 ( IEEE 754 ), rozšíření F normy C [46] [k] , jediné přesné číslo |
double | 64 bitů | IEC 60559 (IEEE 754), rozšíření F normy C [46] [k] , číslo s dvojitou přesností |
long double | minimálně 64 bitů | implementace závislá |
float_t(C99) | minimálně 32 bitů | záleží na typu základny |
double_t(C99) | minimálně 64 bitů | záleží na typu základny |
FLT_EVAL_METHOD | float_t | double_t |
---|---|---|
jeden | float | double |
2 | double | double |
3 | long double | long double |
Ačkoli v jazyce C jako takový neexistuje žádný speciální typ pro řetězce, v jazyce se často používají řetězce zakončené nulou. Řetězce ASCII jsou deklarovány jako pole typu char, jehož posledním prvkem musí být kód znaku 0( '\0'). Je obvyklé ukládat řetězce UTF-8 ve stejném formátu . Všechny funkce, které pracují s řetězci ASCII, však považují každý znak za bajt, což omezuje použití standardních funkcí při použití tohoto kódování.
Navzdory rozšířenému používání myšlenky řetězců ukončených nulou a pohodlí jejich použití v některých algoritmech mají několik vážných nevýhod.
V moderních podmínkách, kdy je výkon kódu upřednostňován před spotřebou paměti, může být efektivnější a jednodušší používat struktury, které obsahují jak samotný řetězec, tak jeho velikost [48] , například:
struct string_t { char * str ; // ukazatel na řetězec size_t str_size ; // velikost řetězce }; typedef struct řetězec_t řetězec_t ; // alternativní název pro zjednodušení kóduAlternativní přístup k ukládání velikosti řetězce s nízkou pamětí by byl předpona řetězce jeho velikostí ve formátu s proměnnou délkou .. Podobný přístup se používá v protokolových bufferech , avšak pouze ve fázi přenosu dat, nikoli však jejich ukládání.
Řetězcové literályŘetězcové literály v C jsou ze své podstaty konstanty [10] . Při deklaraci jsou uzavřeny do dvojitých uvozovek a terminátor je 0automaticky přidán kompilátorem. Řetězcový literál lze přiřadit dvěma způsoby: ukazatelem a hodnotou. Při přiřazení ukazatelem char *se do proměnné typu zadá ukazatel na neměnný řetězec, to znamená, že se vytvoří konstantní řetězec. Pokud zadáte řetězcový literál do pole, pak se řetězec zkopíruje do oblasti zásobníku.
#include <stdio.h> #include <řetězec.h> int main ( void ) { const char * s1 = "Const string" ; char s2 [] = "Řetězec, který lze změnit" ; memcpy ( s2 , "c" , strlen ( "c" )); // změňte první písmeno na malé klade ( s2 ); // zobrazí se text řádku memcpy (( char * ) s1 , "to" , strlen ( "to" )); // chyba segmentace klade ( s1 ); // řádek nebude proveden }Protože řetězce jsou běžná pole znaků, lze místo literálů použít inicializátory, pokud se každý znak vejde do 1 bajtu:
char s [] = { 'I' , 'n' , 'i' , 't' , 'i' , 'a' , 'l' , 'i' , 'z' , 'e' , 'r' , '\0' };V praxi má však tento přístup smysl pouze ve velmi vzácných případech, kdy je vyžadováno nepřidávat do řetězce ASCII koncovou nulu.
Široké čáryPlošina | Kódování |
---|---|
GNU/Linux | USC-4 [49] |
Operační Systém Mac | |
Okna | USC-2 [50] |
AIX | |
FreeBSD | Záleží na lokalitě
není zdokumentováno [50] |
Solaris |
Alternativou k běžným řetězcům jsou široké řetězce, ve kterých je každý znak uložen ve speciálním typu wchar_t. Standardem daný typ by měl být schopen sám o sobě obsahovat všechny znaky největšího z existujících lokalit . Funkce pro práci s širokými řetězci jsou popsány v záhlaví souboru wchar.ha funkce pro práci s širokými znaky jsou popsány v záhlaví souboru wctype.h.
Při deklaraci řetězcových literálů pro široké řetězce se používá modifikátor L:
const wchar_t * wide_str = L "Široký řetězec" ;Formátovaný výstup používá specifikátor %ls, ale specifikátor velikosti, pokud je uveden, je zadán v bajtech, nikoli ve znacích [51] .
Typ wchar_tbyl koncipován tak, aby se do něj vešel jakýkoli znak a široké řetězce - pro ukládání řetězců libovolného národního prostředí, ale ve výsledku se ukázalo, že API je nepohodlné a implementace byly závislé na platformě. Takže na platformě Windows bylo jako velikost typu zvoleno 16 bitů wchar_ta později se objevil standard UTF-32, takže typ wchar_tna platformě Windows již není schopen pojmout všechny znaky z kódování UTF-32, v důsledku čehož se význam tohoto typu ztrácí [50] . Zároveň na platformách Linux [49] a macOS tento typ zabírá 32 bitů, takže typ není vhodný pro implementaci úloh napříč platformami .wchar_t
Vícebajtové řetězceExistuje mnoho různých kódování, ve kterých může být jeden znak naprogramován s různým počtem bajtů. Takové kódování se nazývá vícebajtové. Platí pro ně také UTF-8 . C má sadu funkcí pro převod řetězců z vícebajtových v rámci aktuálního národního prostředí na široké a naopak. Funkce pro práci s vícebajtovými znaky mají předponu nebo příponu mba jsou popsány v záhlaví souboru stdlib.h. Chcete-li podporovat vícebajtové řetězce v programech C, musí být tyto řetězce podporovány na aktuální úrovni národního prostředí . Chcete-li explicitně nastavit kódování, můžete změnit aktuální národní prostředí pomocí funkce setlocale()z locale.h. Určení kódování pro národní prostředí však musí podporovat používaná standardní knihovna. Například standardní knihovna Glibc plně podporuje kódování UTF-8 a je schopna převádět text do mnoha dalších kódování [52] .
Počínaje standardem C11 jazyk také podporuje 16bitové a 32bitové široké vícebajtové řetězce s vhodnými typy znaků char16_ta char32_tze souboru záhlaví uchar.ha také deklarování řetězcových literálů UTF-8 pomocí u8. 16bitové a 32bitové řetězce lze použít k uložení kódování UTF-16 a UTF-32, pokud jsou v hlavičce specifikovány uchar.hdefinice makra __STDC_UTF_16__a __STDC_UTF_32__. K určení řetězcových literálů v těchto formátech se používají modifikátory: upro 16bitové řetězce a Upro 32bitové řetězce. Příklady deklarování řetězcových literálů pro vícebajtové řetězce:
const char * s8 = u8 "vícebajtový řetězec UTF-8" ; const char16_t * s16 = u "16bitový vícebajtový řetězec" ; const char32_t * s32 = U "32bitový vícebajtový řetězec" ;Všimněte si, že funkce c16rtomb()pro převod z 16bitového řetězce na vícebajtový řetězec nefunguje tak, jak bylo zamýšleno, a ve standardu C11 bylo zjištěno, že není možné překládat z UTF-16 do UTF-8 [53] . Oprava této funkce může záviset na konkrétní implementaci kompilátoru.
Výčty jsou množinou pojmenovaných celočíselných konstant a jsou označeny klíčovým slovem enum. Pokud konstanta není přidružena k číslu, pak se automaticky nastaví buď 0pro první konstantu v seznamu, nebo pro číslo o jednu větší, než je zadané v předchozí konstantě. V tomto případě samotný datový typ výčtu může ve skutečnosti odpovídat jakémukoli podepsanému nebo nepodepsanému primitivnímu typu, do jehož rozsahu se všechny hodnoty výčtu vejdou; Kompilátor rozhodne, který typ použít. Explicitní hodnoty pro konstanty však musí být výrazy jako int[18] .
Typ výčtu může být také anonymní, pokud není zadán název výčtu. Konstanty zadané ve dvou různých výčtech jsou dvou různých datových typů, bez ohledu na to, zda jsou výčty pojmenované nebo anonymní.
V praxi se výčty často používají k indikaci stavů konečných automatů , k nastavení možností provozních režimů nebo hodnot parametrů [54] , k vytváření celočíselných konstant a také k výčtu jakýchkoli jedinečných objektů nebo vlastností [55] .
StrukturyStruktury jsou kombinací proměnných různých datových typů ve stejné oblasti paměti; označený klíčovým slovem struct. Proměnné uvnitř struktury se nazývají pole struktury. Z hlediska adresního prostoru jdou pole vždy za sebou ve stejném pořadí, v jakém jsou specifikována, ale kompilátory mohou zarovnávat adresy polí pro optimalizaci pro konkrétní architekturu. Pole tedy ve skutečnosti může mít větší velikost, než je uvedeno v programu.
Každé pole má určitý posun vzhledem k adrese struktury a velikosti. Offset lze získat pomocí makra offsetof()ze souboru záhlaví stddef.h. V tomto případě bude offset záviset na zarovnání a velikosti předchozích polí. Velikost pole je obvykle určena zarovnáním struktury: pokud je velikost zarovnání datového typu pole menší než hodnota zarovnání struktury, pak je velikost pole určena zarovnáním struktury. Zarovnání typu dat lze získat pomocí makra alignof()[f] z hlavičkového souboru stdalign.h. Velikost samotné konstrukce je celková velikost všech jejích polí včetně zarovnání. Některé kompilátory zároveň poskytují speciální atributy, které umožňují sbalit struktury a odstranit z nich zarovnání [56] .
Pole struktury lze explicitně nastavit na velikost v bitech oddělených dvojtečkou za definicí pole a počtem bitů, což omezuje rozsah jejich možných hodnot bez ohledu na typ pole. Tento přístup lze použít jako alternativu k příznakům a bitovým maskám pro přístup k nim. Určení počtu bitů však nezruší možné zarovnání polí struktur v paměti. Práce s bitovými poli má řadu omezení: nelze na ně aplikovat operátor sizeofnebo makro alignof(), nelze na ně získat ukazatel.
AsociaceSjednocení je potřeba, když chcete na stejnou proměnnou odkazovat jako na různé datové typy; označený klíčovým slovem union. Uvnitř unie lze deklarovat libovolný počet protínajících se polí, která ve skutečnosti poskytují přístup do stejné oblasti paměti jako různé datové typy. Velikost svazku volí překladač na základě velikosti největšího pole ve svazu. Je třeba mít na paměti, že změna jednoho pole unie vede ke změně ve všech ostatních polích, ale správná je zaručeně pouze hodnota pole, které se změnilo.
Sjednocení může sloužit jako pohodlnější alternativa k přetypování ukazatele na libovolný typ. Například pomocí sjednocení umístěného ve struktuře můžete vytvářet objekty s dynamicky se měnícím datovým typem:
Strukturní kód pro změnu datového typu za běhu #include <stddef.h> výčet value_type_t { VALUE_TYPE_LONG , // celé číslo VALUE_TYPE_DOUBLE , // skutečné číslo VALUE_TYPE_STRING , // řetězec VALUE_TYPE_BINARY , // libovolná data }; struct binary_t { neplatná * data ; // ukazatel na data size_t data_size ; // velikost dat }; struct string_t { char * str ; // ukazatel na řetězec size_t str_size ; // velikost řetězce }; union value_contents_t { dlouhý jako_dlouhý ; // hodnota jako celé číslo double as_double ; // hodnota jako reálné číslo struct řetězec_t jako_řetězec ; // hodnota jako řetězec struct binary_t as_binary ; // hodnota jako libovolná data }; struct value_t { enum hodnota_typ_t typ ; // typ hodnoty union value_contents_t obsah ; // hodnota obsahu }; PolePole v C jsou primitivní a jsou jen syntaktickou abstrakcí nad aritmetikou ukazatele . Samotné pole je ukazatelem na oblast paměti, takže všechny informace o dimenzi pole a jeho hranicích jsou přístupné pouze v době kompilace podle deklarace typu. Pole mohou být jednorozměrná nebo vícerozměrná, ale přístup k prvku pole spočívá v jednoduchém výpočtu offsetu vzhledem k adrese začátku pole. Protože pole jsou založena na aritmetice adres, je možné s nimi pracovat bez použití indexů [57] . Takže například následující dva příklady čtení 10 čísel ze vstupního proudu jsou navzájem totožné:
Porovnání práce přes indexy s prací přes aritmetiku adresPříklad kódu pro práci s indexy | Příklad kódu pro práci s aritmetikou adres |
---|---|
#include <stdio.h> int a [ 10 ] = { 0 }; // Nulová inicializace unsigned int count = sizeof ( a ) / sizeof ( a [ 0 ]); for ( int i = 0 ; i < počet ; ++ i ) { int * ptr = &a [ i ]; // Ukazatel na aktuální prvek pole int n = scanf ( "%8d" , ptr ); if ( n != 1 ) { perror ( "Nepodařilo se přečíst hodnotu" ); // Zpracování chyby break ; } } | #include <stdio.h> int a [ 10 ] = { 0 }; // Nulová inicializace unsigned int count = sizeof ( a ) / sizeof ( a [ 0 ]); int * a_konec = a + počet ; // Ukazatel na prvek následující za posledním for ( int * ptr = a ; ptr != a_end ; ++ ptr ) { int n = scanf ( "%8d" , ptr ); if ( n != 1 ) { perror ( "Nepodařilo se přečíst hodnotu" ); // Zpracování chyby break ; } } |
Délka polí se známou velikostí se vypočítá v době kompilace. Standard C99 zavedl možnost deklarovat pole proměnné délky, jejichž délku lze nastavit za běhu. Taková pole mají přidělenou paměť z oblasti zásobníku, takže je třeba je používat opatrně, pokud lze jejich velikost nastavit mimo program. Na rozdíl od dynamické alokace paměti může překročení povolené velikosti v oblasti zásobníku vést k nepředvídatelným následkům a záporná délka pole je nedefinované chování . Počínaje C11 jsou pole s proměnnou délkou pro kompilátory volitelná a nedostatek podpory je dán přítomností makra __STDC_NO_VLA__[58] .
Pole s pevnou velikostí deklarované jako lokální nebo globální proměnné lze inicializovat tak, že jim přiřadíte počáteční hodnotu pomocí složených závorek a vypíšete prvky pole oddělené čárkami. Inicializátory globálního pole mohou používat pouze výrazy, které jsou vyhodnoceny při kompilaci [59] . Proměnné použité v takových výrazech musí být deklarovány jako konstanty s modifikátorem const. U lokálních polí mohou inicializátory obsahovat výrazy s voláním funkcí a použitím dalších proměnných, včetně ukazatele na samotné deklarované pole.
Od standardu C99 je povoleno deklarovat pole libovolné délky jako poslední prvek struktur, což je v praxi široce používané a podporované různými kompilátory. Velikost takového pole závisí na množství paměti alokované pro strukturu. V tomto případě nemůžete deklarovat pole takových struktur a nemůžete je umístit do jiných struktur. Při operacích na takové struktuře je pole libovolné délky obvykle ignorováno, včetně výpočtu velikosti struktury, a překročení pole vede k nedefinovanému chování [60] .
Jazyk C neposkytuje žádnou kontrolu nad polem mimo hranice, takže práci s poli musí sledovat sám programátor. Chyby ve zpracování pole ne vždy přímo ovlivňují provádění programu, ale mohou vést k chybám segmentace a zranitelnostem .
Typ synonymJazyk C vám umožňuje vytvářet vlastní názvy typů pomocí přípony typedef. Alternativní názvy lze přiřadit jak typům systémů, tak i uživatelsky definovaným. Takové názvy jsou deklarovány v globálním jmenném prostoru a nejsou v konfliktu s názvy struktur, výčtů a sjednocovacích typů.
Alternativní názvy lze použít jak ke zjednodušení kódu, tak k vytvoření úrovní abstrakce. Některé typy systémů lze například zkrátit, aby byl kód čitelnější nebo aby byl v uživatelském kódu jednotnější:
#include <stdint.h> typedef int32_t i32_t ; typedef int_fast32_t i32fast_t ; typedef int_least32_t i32least_t ; typedef uint32_t u32_t ; typedef uint_fast32_t u32fast_t ; typedef uint_least32_t u32least_t ;Příkladem abstrakce jsou názvy typů v hlavičkových souborech operačních systémů. Například standard POSIX definuje typ pid_tpro uložení číselného ID procesu. Ve skutečnosti je tento typ alternativním názvem pro nějaký primitivní typ, například:
typedef int __kernel_pid_t ; typedef __kernel_pid_t __pid_t typedef __pid_t pid_t ;Protože typy s alternativními názvy jsou pouze synonymy pro původní typy, je zachována plná kompatibilita a zaměnitelnost mezi nimi.
Preprocesor pracuje před kompilací a transformuje text programového souboru podle direktiv, které se v něm vyskytují nebo které jsou předány preprocesoru . Technicky lze preprocesor implementovat různými způsoby, ale logicky je vhodné si jej představit jako samostatný modul, který zpracovává každý soubor určený ke kompilaci a tvoří text, který pak vstupuje na vstup kompilátoru. Preprocesor hledá v textu řádky, které začínají znakem #, za nimiž následují příkazy preprocesoru. Vše, co nepatří do direktiv preprocesoru a není podle direktiv vyjmuto z kompilace, je předáno na vstup kompilátoru beze změny.
Mezi funkce preprocesoru patří:
Je důležité pochopit, že preprocesor poskytuje pouze substituci textu, nebere v úvahu syntaxi a sémantiku jazyka. Takže například definice maker #definese mohou vyskytovat uvnitř funkcí nebo definic typů a direktivy podmíněné kompilace mohou vést k vyloučení jakékoli části kódu z kompilovaného textu programu bez ohledu na gramatiku jazyka. Volání parametrického makra se také liší od volání funkce, protože sémantika argumentů oddělených čárkami není analyzována. Takže například není možné předat inicializaci pole argumentům parametrického makra, protože jeho prvky jsou také odděleny čárkou:
#define pole_of(typ, pole) (((typ) []) (pole)) int * a ; a = pole_of ( int , { 1 , 2 , 3 }); // chyba kompilace: // makro "array_of" předalo 4 argumenty, ale trvá pouze 2Definice maker se často používají k zajištění kompatibility s různými verzemi knihoven, které změnily rozhraní API , včetně určitých částí kódu v závislosti na verzi knihovny. Pro tyto účely knihovny často poskytují definice maker popisující jejich verzi [61] , někdy i makra s parametry pro porovnání aktuální verze s verzí zadanou v preprocesoru [62] . Definice maker se také používají pro podmíněnou kompilaci jednotlivých částí programu, například pro umožnění podpory některých doplňkových funkcí.
Makro definice s parametry jsou široce používány v programech C k vytvoření analogií generických funkcí . Dříve se používaly i k implementaci inline funkcí, ale od standardu C99 tato potřeba odpadla díky přidání inline-funkcí. Vzhledem k tomu, že definice maker s parametry nejsou funkcemi, ale jsou volány podobným způsobem, mohou v důsledku chyby programátora nastat neočekávané problémy, včetně zpracování pouze části kódu z definice makra [63] a nesprávných priorit pro provádění operací [64] . Příkladem chybného kódu je kvadratické makro:
#include <stdio.h> int main ( void ) { #define SQR(x) x * x printf ( "%d" , SQR ( 5 )); // vše je správně, 5*5=25 printf ( "%d" , SQR ( 5 + 0 )); // má být 25, ale vypíše 5 (5+0*5+0) printf ( "%d" , SQR ( 4/3 ) ) ; // vše je správně, 1 (protože 4/3=1, 1*4=4, 4/3=1) printf ( "%d" , SQR ( 5/2 ) ) ; // má být 4 (2*2), ale vypíše 5 (5/2*5/2) návrat 0 ; }Ve výše uvedeném příkladu je chyba v tom, že obsah argumentu makra je do textu nahrazen tak, jak je, bez ohledu na prioritu operací. V takových případech musíte použít inline-functions nebo explicitně upřednostnit operátory ve výrazech, které používají parametry maker pomocí závorek:
#include <stdio.h> int main ( void ) { #define SQR(x) ((x) * (x)) printf ( "%d" , SQR ( 4 + 1 )); // pravda, 25 návrat 0 ; }Program je sada souborů C, které lze zkompilovat do objektových souborů . Soubory objektů pak procházejí krokem propojení mezi sebou navzájem, stejně jako s externími knihovnami, což vede ke konečnému spustitelnému souboru nebo knihovně . Propojení souborů mezi sebou, stejně jako s knihovnami, vyžaduje popis prototypů použitých funkcí, externích proměnných a potřebných datových typů v každém souboru. Je obvyklé ukládat taková data do samostatných hlavičkových souborů , které jsou propojeny pomocí direktivy #include v těch souborech, kde je vyžadována ta či ona funkčnost, a umožňují organizovat systém podobný modulovému systému. V tomto případě může být modul:
Vzhledem k tomu, že směrnice #includepouze nahrazuje text jiného souboru ve fázi preprocesoru , zahrnutí stejného souboru vícekrát může vést k chybám při kompilaci. Proto takové soubory využívají ochranu proti opětovnému povolení pomocí maker #define a #ifndef[65] .
Soubory zdrojového kóduTělo souboru zdrojového kódu C se skládá ze sady definic globálních dat, typů a funkcí. Globální proměnné a funkce deklarované pomocí specifikátorů a staticjsou inlinedostupné pouze v souboru, ve kterém jsou deklarovány, nebo když je jeden soubor zahrnut do jiného prostřednictvím souboru #include. V tomto případě se funkce a proměnné deklarované v hlavičkovém souboru se slovem staticvytvoří znovu pokaždé, když se hlavičkový soubor připojí k dalšímu souboru se zdrojovým kódem. Globální proměnné a funkční prototypy deklarované s externím specifikátorem jsou považovány za zahrnuté z jiných souborů. To znamená, že mohou být použity v souladu s popisem; předpokládá se, že po sestavení programu budou propojeny linkerem s původními objekty a funkcemi popsanými v jejich souborech.
Globální proměnné a funkce, kromě statica inline, jsou přístupné z jiných souborů za předpokladu, že jsou tam správně deklarovány se specifikátorem extern. K proměnným a funkcím deklarovaným s modifikátorem staticlze přistupovat také v jiných souborech, ale pouze tehdy, když je jejich adresa předána ukazatelem. Zadejte deklarace typedefa nelze je importovat do jiných souborů struct. unionPokud je nutné je použít v jiných souborech, měly by být duplikovány tam nebo umístěny do samostatného hlavičkového souboru. Totéž platí pro inline-funkce.
Vstupní bod programuPro spustitelný program je standardním vstupním bodem funkce s názvem main, která nemůže být statická a musí být jediná v programu. Provádění programu začíná od prvního příkazu funkce main()a pokračuje až do jejího ukončení, poté se program ukončí a vrátí operačnímu systému abstraktní celočíselný kód výsledku své práce.
žádné argumenty | S argumenty příkazového řádku |
---|---|
int main ( void ); | int main ( int argc , char ** argv ); |
Při volání je proměnné argcpředán počet argumentů předávaných programu, včetně cesty k samotnému programu, takže proměnná argc obvykle obsahuje hodnotu ne menší než 1. Samotný argvřádek spuštění programu je předán proměnné jako pole textových řetězců, jejichž posledním prvkem je NULL. Překladač garantuje, že main()všechny globální proměnné v programu budou při spuštění funkce inicializovány [67] .
Výsledkem je, že funkce main()může vrátit libovolné celé číslo v rozsahu hodnot typu int, které bude předáno operačnímu systému nebo jinému prostředí jako návratový kód programu [66] . Jazyková norma nedefinuje význam návratových kódů [68] . Obvykle má operační systém, kde programy běží, nějaké prostředky, jak získat hodnotu návratového kódu a analyzovat ji. Někdy existují určité konvence o významech těchto kódů. Obecnou konvencí je, že návratový kód nula označuje úspěšné dokončení programu, zatímco nenulová hodnota představuje kód chyby. Hlavičkový soubor stdlib.hdefinuje dvě obecné definice maker EXIT_SUCCESSa EXIT_FAILURE, které odpovídají úspěšnému a neúspěšnému dokončení programu [68] . Návratové kódy lze také použít v aplikacích, které zahrnují více procesů pro zajištění komunikace mezi těmito procesy, v takovém případě samotná aplikace určuje sémantický význam každého návratového kódu.
C poskytuje 4 způsoby alokace paměti, které určují dobu života proměnné a okamžik její inicializace [67] .
Způsob výběru | Cíle | Čas výběru | čas vydání | Režijní náklady |
---|---|---|---|---|
Alokace statické paměti | Globální proměnné a proměnné označené klíčovým slovem static(ale bez _Thread_local) | Při spuštění programu | Na konci programu | Chybějící |
Alokace paměti na úrovni vlákna | Proměnné označené klíčovým slovem_Thread_local | Když vlákno začne | Na konci proudu | Při vytváření vlákna |
Automatické přidělování paměti | Argumenty a návratové hodnoty funkcí, lokální proměnné funkcí včetně registrů a polí proměnné délky | Při volání funkcí na úrovni zásobníku . | Automaticky po dokončení funkcí | Nevýznamné, protože se mění pouze ukazatel na horní část zásobníku |
Dynamická alokace paměti | Paměť přidělená pomocí funkcí malloc()acalloc()realloc() | Ručně z haldy v okamžiku volání použité funkce. | Ručně pomocí funkcefree() | Velký pro alokaci i uvolnění |
Všechny tyto způsoby ukládání dat jsou vhodné v různých situacích a mají své výhody a nevýhody. Globální proměnné vám neumožňují psát reentrantní algoritmy a automatické přidělování paměti vám neumožňuje vrátit libovolnou oblast paměti z volání funkce. Automatické přidělování také není vhodné pro přidělování velkého množství paměti, protože může vést k poškození zásobníku nebo haldy [69] . Dynamická paměť tyto nedostatky nemá, ale při používání má velkou režii a je obtížnější ji používat.
Tam, kde je to možné, je preferována automatická nebo statická alokace paměti: tento způsob ukládání objektů je řízen kompilátorem , což programátora zbavuje starostí s ručním přidělováním a uvolňováním paměti, která je obvykle zdrojem těžko dohledatelných úniků paměti, chyby segmentace a opětovné uvolnění chyb v programu . Bohužel mnoho datových struktur má proměnlivou velikost za běhu, takže protože automaticky a staticky alokované oblasti musí mít známou pevnou velikost v době kompilace, je velmi běžné používat dynamickou alokaci.
U automaticky alokovaných proměnných registerlze použít modifikátor, který kompilátoru napoví, jak k nim rychle přistupovat. Takové proměnné lze umístit do registrů procesoru. Kvůli omezenému počtu registrů a případným optimalizacím kompilátoru mohou proměnné skončit v běžné paměti, přesto na ně nebude možné z programu získat ukazatel [70] . Modifikátor registerje jediný, který lze zadat v argumentech funkce [71] .
Adresování pamětiJazyk C zdědil lineární adresování paměti při práci se strukturami, poli a alokovanými paměťovými oblastmi. Jazykový standard také umožňuje provádět porovnávací operace na nulových ukazatelích a na adresách v polích, strukturách a alokovaných paměťových oblastech. Je také povoleno pracovat s adresou prvku pole následujícího za posledním, což se provádí pro usnadnění zápisu algoritmů. Porovnávání adresových ukazatelů získaných pro různé proměnné (nebo paměťové oblasti) by však nemělo být prováděno, protože výsledek bude záviset na implementaci konkrétního kompilátoru [72] .
Reprezentace pamětiPaměťová reprezentace programu závisí na hardwarové architektuře, na operačním systému a na kompilátoru. Takže například na většině architektur zásobník roste dolů, ale jsou architektury, kde zásobník roste [73] . Hranici mezi zásobníkem a haldou lze částečně chránit před přetečením zásobníku speciální paměťovou oblastí [74] . A umístění dat a kódu knihoven může záviset na možnostech kompilace [75] . Standard C abstrahuje od implementace a umožňuje vám psát přenosný kód, ale pochopení struktury paměti procesu pomáhá při ladění a psaní bezpečných aplikací odolných proti chybám.
Typická reprezentace paměti procesů v operačních systémech podobných UnixuKdyž je program spuštěn ze spustitelného souboru, instrukce procesoru (strojový kód) a inicializovaná data se importují do paměti RAM. Současně jsou do vyšších adres importovány argumenty příkazového řádku (dostupné ve funkcích main()s následujícím podpisem ve druhém argumentu int argc, char ** argv) a proměnné prostředí .
Oblast neinicializovaných dat obsahuje globální proměnné (včetně těch deklarovaných jako static), které nebyly inicializovány v kódu programu. Tyto proměnné jsou po spuštění programu standardně inicializovány na nulu. Oblast inicializovaných dat - datový segment - také obsahuje globální proměnné, ale tato oblast zahrnuje ty proměnné, kterým byla přidělena počáteční hodnota. Neměnná data, včetně proměnných deklarovaných s modifikátorem const, řetězcových literálů a dalších složených literálů, jsou umístěna do segmentu textu programu. Segment textu programu také obsahuje spustitelný kód a je pouze pro čtení, takže pokus o úpravu dat z tohoto segmentu bude mít za následek nedefinované chování ve formě chyby segmentace .
Oblast zásobníku má obsahovat data spojená s voláním funkcí a místními proměnnými. Před každým spuštěním funkce se zásobník rozšíří, aby se do něj vešly argumenty předané funkci. V průběhu své práce může funkce alokovat lokální proměnné na zásobníku a alokovat na něm paměť pro pole proměnné délky a některé kompilátory také poskytují prostředky pro alokaci paměti v zásobníku prostřednictvím volání alloca(), které není zahrnuto v jazykovém standardu. . Po skončení funkce se zásobník sníží na hodnotu, která byla před voláním, ale to se nemusí stát, pokud je zásobník zpracován nesprávně. Paměť alokovaná dynamicky je poskytována z haldy .
Důležitým detailem je přítomnost náhodného odsazení mezi zásobníkem a horní oblastí [77] , stejně jako mezi inicializovanou datovou oblastí a haldou . To se provádí z bezpečnostních důvodů, jako je zabránění ukládání dalších funkcí.
Knihovny dynamických odkazů a mapování souborů systému souborů jsou umístěny mezi zásobníkem a haldou [78] .
C nemá žádné vestavěné mechanismy kontroly chyb, ale existuje několik obecně přijímaných způsobů, jak zacházet s chybami pomocí jazyka. Obecně platí, že praxe zpracování chyb C v kódu odolném proti chybám nutí psát těžkopádné, často se opakující konstrukce, ve kterých je algoritmus kombinován se zpracováním chyb .
Značky chyb a errnoJazyk C aktivně využívá speciální proměnnou errnoz hlavičkového souboru errno.h, do které funkce zadávají kód chyby, přičemž vrací hodnotu, která je znakem chyby. Chcete-li zkontrolovat, zda výsledek neobsahuje chyby, je výsledek porovnán se značkou chyby, a pokud se shodují, můžete analyzovat kód chyby uložený v errnoprogramu a opravit program nebo zobrazit zprávu o ladění. Ve standardní knihovně standard často definuje pouze vrácené značky chyb a nastavení errnoje závislé na implementaci [79] .
Následující hodnoty obvykle fungují jako značky chyb:
Praxe vracení chybové značky místo chybového kódu, i když ušetří počet argumentů předávaných funkci, v některých případech vede k chybám v důsledku lidského faktoru. Například je běžné, že programátoři ignorují kontrolu výsledku typu ssize_ta samotný výsledek se dále používá ve výpočtech, což vede k jemným chybám, pokud je vráceno -1[82] .
Vrácení správné hodnoty jako ukazatele chyby [82] dále přispívá k výskytu chyb , což také nutí programátora provádět více kontrol a podle toho psát více opakujícího se kódu stejného typu. Tento přístup se praktikuje ve funkcích proudu, které pracují s objekty typu FILE *: značka chyby je hodnota EOF, která je také značkou konce souboru. Někdy proto EOFmusíte zkontrolovat proud znaků jak na konec souboru pomocí funkce feof(), tak na přítomnost chyby pomocí ferror()[83] . Některé funkce, které se mohou vrátit EOFpodle normy, přitom není nutné nastavovat errno[79] .
Nedostatek jednotného postupu zpracování chyb ve standardní knihovně vede k tomu, že se objevují vlastní metody zpracování chyb a kombinace běžně používaných metod v projektech třetích stran. Například v projektu systemd byly zkombinovány myšlenky vracení chybového kódu a čísla -1jako značky – vrací se záporný chybový kód [84] . A knihovna GLib zavedla praxi vracení booleovské hodnoty jako značky chyby , zatímco podrobnosti o chybě jsou umístěny ve speciální struktuře, jejíž ukazatel je vrácen prostřednictvím posledního argumentu funkce [85] . Podobné řešení používá projekt Enlightenment , který také používá jako značku typ Boolean, ale vrací chybové informace podobné standardní knihovně – prostřednictvím samostatné funkce [86] , kterou je třeba zkontrolovat, zda byla značka vrácena.
Vrácení chybového kóduAlternativou k chybovým značkám je vrátit kód chyby přímo a vrátit výsledek funkce prostřednictvím argumentů ukazatele. Touto cestou se vydali vývojáři standardu POSIX, v jehož funkcích je obvyklé vracet chybový kód jako číslo typu int. Vrácení hodnoty typu intvšak explicitně nedává najevo, že je to chybový kód, který se vrací, a nikoli token, což může vést k chybám, pokud je výsledek takových funkcí porovnán s hodnotou -1. Rozšíření K standardu C11 zavádí speciální typ errno_tpro uložení chybového kódu. Existují doporučení použít tento typ v uživatelském kódu k vracení chyb, a pokud není poskytován standardní knihovnou, deklarujte to sami [87] :
#ifndef __STDC_LIB_EXT1__ typedef int errno_t ; #endifTento přístup, kromě zlepšení kvality kódu, eliminuje potřebu používat errno, což vám umožňuje vytvářet knihovny s reentrantními funkcemi, aniž byste museli zahrnout další knihovny, jako jsou POSIX Threads , pro správné definování errno.
Chyby v matematických funkcíchSložitější je zpracování chyb v matematických funkcích z hlavičkového souboru math.h, ve kterém se mohou vyskytnout 3 typy chyb [88] :
Prevence dvou ze tří typů chyb spočívá v kontrole vstupních dat pro rozsah platných hodnot. Je však extrémně obtížné předpovědět výstup výsledku za limity typu. Jazykový standard proto poskytuje možnost analyzovat matematické funkce na chyby. Počínaje standardem C99 je tato analýza možná dvěma způsoby, v závislosti na hodnotě uložené v math_errhandling.
V tomto případě je způsob zpracování chyb dán konkrétní implementací standardní knihovny a může zcela chybět. Proto v kódu nezávislém na platformě může být nutné zkontrolovat výsledek dvěma způsoby najednou, v závislosti na hodnotě math_errhandling[88] .
Uvolnění zdrojůVýskyt chyby obvykle vyžaduje, aby se funkce ukončila a vrátila indikátor chyby. Pokud se ve funkci může vyskytnout chyba v různých jejích částech, je nutné uvolnit alokované zdroje během její činnosti, aby se předešlo únikům. Je dobrým zvykem uvolnit zdroje v opačném pořadí před návratem z funkce a v případě chyb v opačném pořadí po hlavní return. V samostatných částech takového uvolnění můžete skákat pomocí operátoru goto[89] . Tento přístup umožňuje přesouvat části kódu, které nesouvisejí s implementovaným algoritmem, mimo samotný algoritmus, čímž se zvyšuje čitelnost kódu a je podobný práci operátora deferz programovacího jazyka Go . Příklad uvolnění zdrojů je uveden níže v sekci příkladů .
K uvolnění prostředků v rámci programu je k dispozici mechanismus obsluhy ukončení programu. Obslužné rutiny jsou přiřazovány pomocí funkce atexit()a jsou prováděny jak na konci funkce main()prostřednictvím příkazu return, tak po provedení funkce exit(). V tomto případě nejsou handlery prováděny funkcemi abort()a _Exit()[90] .
Příkladem uvolnění zdrojů na konci programu je uvolnění paměti alokované pro globální proměnné. Navzdory skutečnosti, že paměť je uvolněna tak či onak po ukončení programu operačním systémem a je povoleno neuvolňovat paměť, která je vyžadována po celou dobu činnosti programu [91] , explicitní dealokace je výhodnější, protože umožňuje snazší najít úniky paměti pomocí nástrojů třetích stran a snižuje pravděpodobnost úniků paměti v důsledku chyby:
Ukázkový programový kód s vydáním zdroje #include <stdio.h> #include <stdlib.h> int pocet_cisel ; int * čísla ; void volná_čísla ( void ) { zdarma ( čísla ); } int main ( int argc , char ** argv ) { if ( arg < 2 ) { exit ( EXIT_FAILURE ); } cisla_pocet = atoi ( argv [ 1 ]); if ( počet_počtů <= 0 ) { exit ( EXIT_FAILURE ); } čísla = calloc ( počet_počtů , velikost ( * čísla )); if ( ! čísla ) { perror ( "Chyba při přidělování paměti pro pole" ); exit ( EXIT_FAILURE ); } atexit ( volná_čísla ); // ... práce s polem čísel // Zde bude automaticky volána obsluha free_numbers(). return EXIT_SUCCESS ; }Nevýhodou tohoto přístupu je, že formát přiřaditelných handlerů neumožňuje předávání libovolných dat funkci, což umožňuje vytvářet handlery pouze pro globální proměnné.
Minimální program v C, který nevyžaduje zpracování argumentů, je následující:
int main ( void ){}Je povoleno nezapsat operátor returnpro funkci main(). V tomto případě, podle standardu, funkce main()vrátí 0 a provede všechny handlery přiřazené funkci exit(). To předpokládá, že program byl úspěšně dokončen [40] .
Ahoj světe!Ahoj světe! je uveden v prvním vydání knihy " Programovací jazyk C " od Kernighan a Ritchie:
#include <stdio.h> int main ( void ) // Nebere žádné argumenty { printf ( "Ahoj světe! \n " ); // '\n' - nový řádek return 0 ; // Úspěšné ukončení programu }Tento program vytiskne zprávu Hello, world! ' na standardním výstupu .
Zpracování chyb pomocí čtení souboru jako příkladuMnoho funkcí C může vrátit chybu, aniž by udělaly to, co měly. Chyby je třeba kontrolovat a správně na ně reagovat, včetně často potřeby přenést chybu z funkce na vyšší úroveň pro analýzu. Současně lze funkci, ve které došlo k chybě, provést reentrantní , v takovém případě by funkce omylem neměla změnit vstupní ani výstupní data, což umožňuje její bezpečný restart po opravě chybové situace.
Příklad implementuje funkci pro čtení souboru v C, ale vyžaduje, aby funkce fopen()a fread()standard POSIX vyhovovaly , jinak nemusí nastavit proměnnou errno, což značně komplikuje jak ladění, tak psaní univerzálního a bezpečného kódu. Na platformách jiných než POSIX bude chování tohoto programu v případě chyby nedefinované . Rozdělení zdrojů na chyby je za hlavním algoritmem pro zlepšení čitelnosti a přechod se provádí pomocí [89] . goto
Příklad kódu čtečky souborů se zpracováním chyb #include <errno.h> #include <stdio.h> #include <stdlib.h> // Definujte typ pro uložení chybového kódu, pokud není definován #ifndef __STDC_LIB_EXT1__ typedef int errno_t ; #endif enum { EOK = 0 , // hodnota errno_t při úspěchu }; // Funkce pro čtení obsahu souboru errno_t get_file_contents ( const char * název souboru , void ** content_ptr , size_t * content_size_ptr ) { SOUBOR * f ; f = fopen ( název souboru , "rb" ); if ( ! f ) { // V POSIXu fopen() omylem nastaví errno návrat errno ; } // Získání velikosti souboru fseek ( f , 0 , SEEK_END ); long content_size = ftell ( f ); if ( velikost_obsahu == 0 ) { * content_ptr = NULL ; * content_size_ptr = 0 ; goto čištění_fopen ; } přetočit zpět ( f ); // Proměnná pro uložení vráceného chybového kódu errno_t uloženo_errno ; neplatný * obsah ; obsah = malloc ( velikost_obsahu ); if ( ! obsah ) { save_errno = errno ; goto aborting_fopen ; } // Přečte celý obsah souboru na ukazateli obsahu velikost_t n ; n = fread ( obsah , velikost_obsahu , 1 , f ); if ( n == 0 ) { // Nekontrolovat feof(), protože se uloží do vyrovnávací paměti po fseek() // POSIX fread() omylem nastaví errno save_errno = errno ; goto aborting_contents ; } // Vrátí přidělenou paměť a její velikost * content_ptr = obsah ; * content_size_ptr = content_size ; // Sekce vydání zdrojů o úspěchu cleaning_fopen : fclose ( f ); vrátit EOK ; // Samostatná sekce pro uvolnění zdrojů omylem aborting_contents : zdarma ( obsah ); aborting_fopen : fclose ( f ); return save_errno ; } int main ( int argc , char ** argv ) { if ( arg < 2 ) { return EXIT_FAILURE ; } const char * název_souboru = argv [ 1 ]; errno_t errnum ; neplatný * obsah ; velikost_t velikost_obsahu ; errnum = get_file_contents ( název souboru , & obsah , & velikost_obsahu ); if ( errnum ) { charbuf [ 1024 ] ; const char * text_chyby = strerror_r ( errnum , buf , sizeof ( buf )); fprintf ( stderr , "%s \n " , text_chyby ); exit ( EXIT_FAILURE ); } printf ( "%.*s" , ( int ) obsah_velikost , obsah ); zdarma ( obsah ); return EXIT_SUCCESS ; }Některé kompilátory jsou dodávány s kompilátory pro jiné programovací jazyky (včetně C++ ) nebo jsou součástí vývojového prostředí softwaru .
|
|
Navzdory skutečnosti, že standardní knihovna je součástí jazykového standardu, její implementace jsou oddělené od kompilátorů. Proto se jazykové standardy podporované kompilátorem a knihovnou mohou lišit.
Vzhledem k tomu, že jazyk C neposkytuje prostředky pro bezpečné psaní kódu a mnoho prvků jazyka přispívá k chybám, může být psaní vysoce kvalitního kódu odolného proti chybám zaručeno pouze psaním automatizovaných testů. Pro usnadnění takového testování existují různé implementace knihoven testů jednotek třetích stran .
Existuje také mnoho dalších systémů pro testování C kódu, jako je AceUnit, GNU Autounit, cUnit a další, ale ty buď netestují v izolovaných prostředích, poskytují málo funkcí [100] nebo se již nevyvíjejí.
Nástroje pro laděníPodle projevů chyb není vždy možné vyvodit jednoznačný závěr o problémové oblasti v kódu, často však k lokalizaci problému pomáhají různé ladicí nástroje.
Někdy, aby bylo možné přenést určité knihovny, funkce a nástroje napsané v C do jiného prostředí, je nutné zkompilovat kód C do jazyka vyšší úrovně nebo do kódu virtuálního stroje určeného pro takový jazyk. Pro tento účel jsou navrženy následující projekty:
Také pro C existují další nástroje, které usnadňují a doplňují vývoj, včetně statických analyzátorů a nástrojů pro formátování kódu. Statická analýza pomáhá identifikovat potenciální chyby a zranitelnosti. A automatické formátování kódu zjednodušuje organizaci spolupráce v systémech správy verzí a minimalizuje konflikty způsobené změnami stylu.
Jazyk je široce používán při vývoji operačního systému, na úrovni API operačního systému, ve vestavěných systémech a pro psaní vysoce výkonného nebo chybově kritického kódu. Jedním z důvodů širokého přijetí nízkoúrovňového programování je schopnost psát multiplatformní kód, se kterým lze na různém hardwaru a operačních systémech zacházet různě.
Schopnost psát vysoce výkonný kód jde na úkor naprosté svobody jednání pro programátora a absence přísné kontroly ze strany kompilátoru. Například první implementace Java , Python , Perl a PHP byly napsány v C. Zároveň jsou v mnoha programech části, které jsou nejvíce náročné na zdroje, obvykle napsány v C. Jádro Mathematicy [109] je napsáno v C, zatímco MATLAB , původně napsaný ve Fortranu , byl přepsán do C v roce 1984 [110] .
C se také někdy používá jako střední jazyk při kompilaci jazyků vyšší úrovně. Podle tohoto principu fungovaly například první implementace jazyků C++ , Objective-C a Go – kód napsaný v těchto jazycích byl přeložen do střední reprezentace v jazyce C. Moderní jazyky, které fungují na stejném principu, jsou Vala a Nim .
Další oblastí použití jazyka C jsou aplikace v reálném čase , které jsou náročné na odezvu kódu a dobu jeho provádění. Takové aplikace musí zahájit provádění akcí v přísně omezeném časovém rámci a samotné akce se musí vejít do určitého časového období. Zejména standard POSIX.1 poskytuje sadu funkcí a schopností pro vytváření aplikací v reálném čase [111] [112] [113] , ale tvrdá podpora v reálném čase musí být implementována také operačním systémem [114] .
Jazyk C byl a zůstává jedním z nejpoužívanějších programovacích jazyků již více než čtyřicet let. Jeho vliv lze přirozeně do určité míry vysledovat v mnoha pozdějších jazycích. Nicméně mezi jazyky, které dosáhly určité distribuce, existuje jen málo přímých potomků C.
Některé potomkové jazyky staví na C s dalšími nástroji a mechanismy, které přidávají podporu pro nová programovací paradigmata ( OOP , funkcionální programování , generické programování atd.). Mezi tyto jazyky patří především C++ a Objective-C a nepřímo jejich potomci Swift a D. Známé jsou také pokusy zlepšit C nápravou jeho nejvýznamnějších nedostatků, ale zachováním jeho atraktivních vlastností. Mezi nimi můžeme zmínit výzkumný jazyk Cyclone (a jeho potomka Rust ). Někdy se oba směry vývoje kombinují v jednom jazyce, příkladem je Go .
Samostatně je třeba zmínit celou skupinu jazyků, které ve větší či menší míře zdědily základní syntaxi C (použití složených závorek jako oddělovačů bloků kódu, deklarace proměnných, charakteristické formy operátorů for, while, if, switchs parametry v závorkách, kombinované operace ++, --, +=, -=a další), proto mají programy v těchto jazycích charakteristický vzhled spojený specificky s C. Jedná se o jazyky jako Java , JavaScript , PHP , Perl , AWK , C# . Ve skutečnosti se struktura a sémantika těchto jazyků velmi liší od jazyka C a jsou obvykle určeny pro aplikace, kde původní jazyk C nebyl nikdy použit.
Programovací jazyk C++ byl vytvořen z C a zdědil jeho syntaxi a doplnil jej o nové konstrukce v duchu Simula-67, Smalltalk, Modula-2, Ada, Mesa a Clu [116] . Hlavními doplňky byla podpora pro OOP (popis třídy, vícenásobná dědičnost, polymorfismus založený na virtuálních funkcích) a generické programování (šablonový engine). Ale kromě toho bylo do jazyka provedeno mnoho různých dodatků. V současné době je C++ jedním z nejrozšířenějších programovacích jazyků na světě a je umístěn jako univerzální jazyk s důrazem na systémové programování [117] .
Zpočátku si C++ zachoval kompatibilitu s C, což bylo uváděno jako jedna z výhod nového jazyka. První implementace C++ jednoduše překládaly nové konstrukce do čistého C, poté byl kód zpracován běžným kompilátorem C. Aby byla zachována kompatibilita, tvůrci C++ z něj odmítli vyloučit některé z často kritizovaných funkcí C, místo toho vytvořili nové, „paralelní“ mechanismy, které se doporučují při vývoji nového kódu C++ (šablony místo maker, explicitní typové přetypování místo automatického standardní kontejnery knihoven namísto ručního dynamického přidělování paměti atd.). Jazyky se však od té doby vyvíjely nezávisle a nyní jsou C a C++ nejnovějších vydaných standardů kompatibilní pouze částečně: neexistuje žádná záruka, že kompilátor C++ úspěšně zkompiluje program C, a pokud bude úspěšný, neexistuje žádná záruka, že zkompilovaný program poběží správně. Obzvláště nepříjemné jsou některé jemné sémantické rozdíly, které mohou vést k odlišnému chování stejného kódu, který je syntakticky správný pro oba jazyky. Například znakové konstanty (znaky uzavřené v jednoduchých uvozovkách) mají typ intv C a typ charv C++ , takže množství paměti zabrané takovými konstantami se liší jazyk od jazyka. [118] Pokud je program citlivý na velikost znakové konstanty, bude se při kompilaci pomocí kompilátorů C a C++ chovat jinak.
Rozdíly, jako jsou tyto, ztěžují psaní programů a knihoven, které mohou kompilovat a pracovat stejným způsobem v C i C++ , což samozřejmě mate ty, kteří programují v obou jazycích. Mezi vývojáři a uživateli C i C++ jsou zastánci minimalizace rozdílů mezi jazyky, což by objektivně přineslo hmatatelné výhody. Existuje však i opačný názor, podle něhož kompatibilita není nijak zvlášť důležitá, i když je užitečná, a snahy o snížení nekompatibility by neměly bránit zdokonalování každého jazyka jednotlivě.
Další možností pro rozšíření jazyka C o objektově založené nástroje je jazyk Objective-C vytvořený v roce 1983. Objektový subsystém byl vypůjčen od Smalltalk a všechny prvky spojené s tímto subsystémem jsou implementovány ve své vlastní syntaxi, která se značně liší od syntaxe C (až do skutečnosti, že v popisech tříd je syntaxe pro deklarování polí opačná než syntaxe pro deklaraci proměnných v C: nejprve se zapíše název pole, pak jeho typ). Na rozdíl od C++ je Objective-C nadmnožinou klasického C, to znamená, že si zachovává kompatibilitu se zdrojovým jazykem; správný program C je správný program Objective-C. Dalším významným rozdílem od ideologie C++ je, že Objective-C implementuje interakci objektů výměnou plnohodnotných zpráv, zatímco C++ implementuje koncept „odeslání zprávy jako volání metody“. Plné zpracování zpráv je mnohem flexibilnější a přirozeně se hodí k paralelnímu zpracování. Objective-C, stejně jako jeho přímý potomek Swift , patří mezi nejoblíbenější na platformách podporovaných společností Apple .
Jazyk C je jedinečný v tom, že to byl první jazyk na vysoké úrovni , který vážně nahradil assembler ve vývoji systémového softwaru . Zůstává jazykem implementovaným na největším počtu hardwarových platforem a jedním z nejpopulárnějších programovacích jazyků , zejména ve světě svobodného softwaru [119] . Přesto má jazyk mnoho nedostatků, od svého vzniku je kritizován mnoha odborníky.
Jazyk je velmi složitý a plný nebezpečných prvků, které lze velmi snadno zneužít. Svou strukturou a pravidly nepodporuje programování zaměřené na tvorbu spolehlivého a udržovatelného programového kódu, naopak, zrozený v éře přímého programování pro různé procesory, jazyk přispívá k psaní nebezpečného a matoucího kódu [119] . Mnoho profesionálních programátorů si myslí, že jazyk C je mocný nástroj pro tvorbu elegantních programů, ale zároveň s ním lze vytvářet extrémně nekvalitní řešení [120] [121] .
Kvůli různým předpokladům v jazyce mohou programy kompilovat s více chybami, což často vede k nepředvídatelnému chování programu. Moderní kompilátory poskytují možnosti pro statickou analýzu kódu [122] [123] , ale ani ony nejsou schopny odhalit všechny možné chyby. Negramotné programování v C může mít za následek zranitelnosti softwaru , což může ovlivnit bezpečnost jeho použití.
Xi má vysoký práh vstupu [119] . Jeho specifikace zabírá více než 500 stran textu, který je nutné prostudovat celý, protože pro vytvoření bezchybného a kvalitního kódu je třeba vzít v úvahu mnoho nesrozumitelných vlastností jazyka. Například automatické přetypování operandů celočíselných výrazů na typ intmůže poskytnout obtížně předvídatelné výsledky při použití binárních operátorů [44] :
znak bez znaménka x = 0xFF ; unsigned char y = ( ~ x | 0x1 ) >> 1 ; // Intuitivně se zde očekává 0x00 printf ( "y = 0x%hhX \n " , y ); // Vytiskne 0x80, pokud sizeof(int) > sizeof(char)Nepochopení takových nuancí může vést k četným chybám a zranitelnostem. Dalším faktorem, který zvyšuje složitost ovládání C, je nedostatek zpětné vazby od kompilátoru: jazyk dává programátorovi naprostou svobodu jednání a umožňuje kompilovat programy se zjevnými logickými chybami. To vše ztěžuje použití C ve výuce jako prvního programovacího jazyka [119]
Konečně za více než 40 let existence jazyk poněkud zastaral a je problematické v něm používat mnoho moderních programovacích technik a paradigmat .
V syntaxi C nejsou žádné moduly a mechanismy pro jejich interakci. Soubory zdrojového kódu se kompilují samostatně a musí obsahovat prototypy proměnných, funkcí a datových typů importovaných z jiných souborů. To se provádí zahrnutím hlavičkových souborů pomocí substituce maker . V případě narušení korespondence mezi soubory kódu a soubory záhlaví může dojít k chybám v době propojení a všem druhům chyb za běhu: od poškození zásobníku a haldy až po chyby segmentace . Vzhledem k tomu, že směrnice pouze nahrazuje text jednoho souboru do jiného, zahrnutí velkého počtu hlavičkových souborů vede k tomu, že skutečné množství kódu, který se zkompiluje, mnohonásobně vzroste, což je důvodem relativně pomalého výkonu C kompilátory. Potřeba koordinovat popisy v hlavním modulu a hlavičkových souborech ztěžuje údržbu programu. #include#include
Varování místo chybJazyková norma dává programátorovi větší volnost jednání a tím i vysokou šanci na chyby. Mnoho z toho, co je nejčastěji zakázáno, je povoleno jazykem a kompilátor vydává v nejlepším případě varování. Přestože moderní kompilátory umožňují převést všechna varování na chyby, tato funkce se používá zřídka a častěji jsou varování ignorována, pokud program běží uspokojivě.
Takže například před standardem C99 mohlo volání funkce mallocbez zahrnutí hlavičkového souboru stdlib.hvést k poškození zásobníku, protože při absenci prototypu byla funkce volána jako vracející typ int, zatímco ve skutečnosti vracela typ void*( došlo k chybě, když se velikosti typů na cílové platformě lišily). I tak to bylo jen varování.
Nedostatek kontroly nad inicializací proměnnýchAutomaticky a dynamicky vytvářené objekty nejsou ve výchozím nastavení inicializovány a po vytvoření obsahují hodnoty zbylé v paměti z objektů, které tam byly dříve. Taková hodnota je zcela nepředvídatelná, liší se od jednoho stroje k druhému, běh od běhu, od volání funkce k volání. Pokud program takovou hodnotu použije kvůli náhodnému opomenutí inicializace, pak bude výsledek nepředvídatelný a nemusí se objevit okamžitě. Moderní kompilátory se snaží diagnostikovat tento problém statickou analýzou zdrojového kódu, i když obecně je extrémně obtížné tento problém vyřešit statickou analýzou. K identifikaci těchto problémů ve fázi testování během provádění programu lze použít další nástroje: Valgrind a MemorySanitizer [124] .
Nedostatek kontroly nad aritmetikou adresZdrojem nebezpečných situací je kompatibilita ukazatelů s numerickými typy a možnost použití adresní aritmetiky bez přísné kontroly ve fázích kompilace a provádění. To umožňuje získat ukazatel na jakýkoli objekt, včetně spustitelného kódu, a odkazovat se na tento ukazatel, pokud tomu nezabrání systémový mechanismus ochrany paměti .
Nesprávné použití ukazatelů může způsobit nedefinované chování programu a vést k vážným následkům. Ukazatel může být například neinicializován nebo v důsledku nesprávných aritmetických operací může ukazovat na libovolné místo v paměti. Na některých platformách může práce s takovým ukazatelem vynutit zastavení programu, na jiných může dojít k poškození libovolných dat v paměti; Poslední chyba je nebezpečná, protože její důsledky jsou nepředvídatelné a mohou se objevit kdykoli, včetně mnohem později, než je okamžik skutečného chybného jednání.
Přístup k polím v C je také implementován pomocí aritmetiky adres a neznamená prostředky pro kontrolu správnosti přístupu k prvkům pole indexem. Například výrazy a[i]a i[a]jsou identické a jsou jednoduše přeloženy do formuláře *(a + i)a kontrola pole mimo hranice se neprovádí. Přístup na index vyšší než horní mez pole vede k přístupu k datům umístěným v paměti za polem, což se nazývá přetečení vyrovnávací paměti . Když je takové volání chybné, může vést k nepředvídatelnému chování programu [57] . Tato funkce se často používá v exploitech používaných k nelegálnímu přístupu k paměti jiné aplikace nebo paměti jádra operačního systému.
Dynamická paměť náchylná k chybámSystémové funkce pro práci s dynamicky alokovanou pamětí nezajišťují kontrolu nad správností a včasností jejího přidělení a uvolnění, dodržení správného pořadí práce s dynamickou pamětí je plně v kompetenci programátora. Jeho chyby mohou vést k přístupu na nesprávné adresy, k předčasnému uvolnění nebo k úniku paměti (to je možné například v případě, že vývojář zapomněl zavolat free()nebo zavolat volající free()funkci, když to bylo požadováno) [125] .
Jednou z častých chyb je nekontrolování výsledku funkcí alokace paměti ( malloc()a calloc()dalších) na NULL, přičemž paměť nemusí být přidělena, pokud jí není dostatek nebo pokud bylo požadováno příliš mnoho, například kvůli redukce čísla -1přijatého v důsledku jakýchkoli chybných matematických operací na typ bez znaménka size_t, s následnými operacemi na něm . Dalším problémem s funkcemi systémové paměti je nespecifikované chování při požadavku na alokaci bloku s nulovou velikostí: funkce mohou vracet buď hodnotu reálného ukazatele nebo hodnotu reálného ukazatele, v závislosti na konkrétní implementaci [126] . NULL
Některé specifické implementace a knihovny třetích stran poskytují funkce, jako je počítání referencí a slabé reference [127] , chytré ukazatele [128] a omezené formy garbage collection [129] , ale všechny tyto funkce nejsou standardní, což přirozeně omezuje jejich použití .
Neefektivní a nebezpečné řetězcePro jazyk jsou standardní řetězce zakončené nulou , takže s nimi pracují všechny standardní funkce. Toto řešení vede k výrazné ztrátě efektivity v důsledku nevýznamné úspory paměti (ve srovnání s explicitním uložením velikosti): výpočet délky řetězce (funkce ) vyžaduje procházení celého řetězce od začátku do konce, kopírování řetězců je také obtížné optimalizovat díky přítomnosti ukončovací nuly [48] . Kvůli potřebě přidat ukončovací null k datům řetězce je nemožné efektivně získat podřetězce jako řezy a pracovat s nimi jako s běžnými řetězci; alokace a manipulace s částmi řetězců obvykle vyžaduje ruční alokaci a dealokaci paměti, což dále zvyšuje pravděpodobnost chyby. strlen()
Řetězce zakončené nulou jsou častým zdrojem chyb [130] . Ani standardní funkce obvykle nekontrolují velikost cílové vyrovnávací paměti [130] a nemusí přidat znak null [131] na konec řetězce , nemluvě o tom, že nemusí být přidán nebo přepsán kvůli chybě programátoru. [132] .
Nebezpečná implementace variadických funkcíZatímco C podporuje funkce s proměnným počtem argumentů , neposkytuje ani prostředek pro určení počtu a typů skutečných parametrů předávaných takové funkci, ani mechanismus pro bezpečný přístup k nim [133] . Informování funkce o složení skutečných parametrů spočívá na programátorovi a pro přístup k jejich hodnotám je nutné spočítat správný počet bajtů z adresy posledního pevného parametru na zásobníku, a to buď ručně, nebo pomocí sady makra va_argz hlavičkového souboru stdarg.h. Zároveň je nutné vzít v úvahu fungování mechanismu automatického implicitního povýšení typu při volání funkcí [134] , podle kterého intjsou celočíselné typy argumentů menší než přetypovány na int(nebo unsigned int), ale floatpřetypovány na double. Chyba ve volání nebo v práci s parametry uvnitř funkce se objeví až při provádění programu, což vede k nepředvídatelným následkům, od načtení nesprávných dat až po poškození zásobníku.
Standardním prostředkem formátovaných I/O jsou přitom funkce s proměnným počtem parametrů ( printf(), scanf()a další), které nejsou schopny zkontrolovat, zda se seznam argumentů shoduje s formátovacím řetězcem . Mnoho moderních kompilátorů provádí tuto kontrolu pro každé volání a generuje varování, pokud najdou neshodu, ale obecně tato kontrola není možná, protože každá variadic funkce zpracovává tento seznam jinak. Není možné staticky řídit ani všechna volání funkcí printf(), protože formátovací řetězec lze dynamicky vytvořit v programu.
Nedostatek sjednocení zpracování chybSyntaxe C nezahrnuje speciální mechanismus zpracování chyb. Standardní knihovna podporuje pouze ty nejjednodušší prostředky: proměnnou (v případě POSIX makro) errnoz hlavičkového souboru errno.hpro nastavení posledního chybového kódu a funkce pro získání chybových zpráv podle kódů. Tento přístup vede k potřebě psát velké množství opakujícího se kódu, mísícího hlavní algoritmus se zpracováním chyb, a kromě toho není bezpečný pro vlákna. Navíc ani v tomto mechanismu neexistuje jediný řád:
Ve standardní knihovně jsou kódy errnourčeny prostřednictvím definic maker a mohou mít stejné hodnoty, což znemožňuje analýzu chybových kódů prostřednictvím operátora switch. Jazyk nemá speciální datový typ pro příznaky a chybové kódy, předávají se jako hodnoty typu int. Samostatný typ errno_tpro uložení chybového kódu se objevil pouze v rozšíření K standardu C11 a nemusí být podporován kompilátory [87] .
Nedostatky jazyka C jsou již dlouho dobře známé a od počátku tohoto jazyka došlo k mnoha pokusům o zlepšení kvality a bezpečnosti kódu C, aniž by byly obětovány jeho schopnosti.
Prostředky analýzy správnosti kóduTéměř všechny moderní kompilátory C umožňují omezenou analýzu statického kódu s varováním o potenciálních chybách. Podporovány jsou také možnosti pro vkládání kontrol pro pole mimo meze, zničení zásobníku, mimo limity haldy, čtení neinicializovaných proměnných, nedefinovaného chování atd. do kódu. Dodatečné kontroly však mohou ovlivnit výkon finální aplikace, takže jsou nejčastěji se používá pouze ve fázi ladění.
Existují speciální softwarové nástroje pro statickou analýzu kódu C pro detekci nesyntaktických chyb. Jejich použití nezaručuje bezchybnost programů, ale umožňuje identifikovat významnou část typických chyb a potenciálních zranitelností. Maximálního účinku těchto nástrojů je dosaženo nikoli při občasném použití, ale při použití jako součást zaběhnutého systému neustálé kontroly kvality kódu, například v systémech průběžné integrace a nasazení. Může být také nutné opatřit kód speciálními komentáři, aby se vyloučily falešné poplachy analyzátoru na správných úsecích kódu, které formálně spadají pod kritéria pro chybné.
Bezpečné programovací standardyO správném programování v C bylo publikováno značné množství výzkumů, od malých článků po dlouhé knihy. Pro zachování kvality kódu C jsou přijaty podnikové a průmyslové standardy. Zejména:
Sada standardů POSIX přispívá k vyrovnání některých nedostatků jazyka . Instalace je standardizována errnomnoha funkcemi, které umožňují ošetřit chyby, ke kterým dochází například při operacích se soubory, a jsou zavedeny vláknově bezpečné analogy některých funkcí standardní knihovny, jejichž bezpečné verze jsou v jazykové normě přítomny pouze v rozšíření K [137] .
Slovníky a encyklopedie | ||||
---|---|---|---|---|
|
Programovací jazyky | |
---|---|
|
C programovací jazyk | |
---|---|
Kompilátory |
|
Knihovny | |
Zvláštnosti | |
Někteří potomci | |
C a další jazyky |
|
Kategorie:C programovací jazyk |