C | |
---|---|
Clasa de limba | procedural |
Tipul de execuție | compilate |
Aparut in | 1972 |
Autor | Dennis Ritchie |
Dezvoltator | Bell Labs , Dennis Ritchie [1] , Institutul Național de Standarde din SUA , ISO și Ken Thompson |
Extensie de fișier | .c— pentru fișierele de cod, .h— pentru fișierele de antet |
Eliberare | ISO/IEC 9899:2018 ( 5 iulie 2018 ) |
Tip sistem | static slab |
Implementări majore | GCC , Clang , TCC , Turbo C , Watcom , Oracle Solaris Studio C, Pelles C |
Dialectele |
„K&R” C ( 1978 ) ANSI C ( 1989 ) C99 ( 1999 ) C11 ( 2011 ) |
A fost influențat | BCPL , B |
influențat | C++ , Objective-C , C# , Java , Nim |
OS | Sistem de operare similar Microsoft Windows și Unix |
Fișiere media la Wikimedia Commons |
ISO/IEC 9899 | |
Tehnologia informației — Limbaje de programare — C | |
Editor | Organizația Internațională pentru Standardizare (ISO) |
Site-ul web | www.iso.org |
Comitetul (dezvoltator) | ISO/IEC JTC 1/SC 22 |
Site-ul comisiei | Limbaje de programare, mediile lor și interfețele software de sistem |
ISS (ICS) | 35.060 |
Ediția actuală | ISO/IEC 9899:2018 |
Edițiile anterioare | ISO/IEC 9899:1990/COR2:1996 ISO/IEC 9899:1999/COR3:2007 ISO/IEC 9899:2011/COR1:2012 |
C (din litera latină C , limba engleză ) este un limbaj de programare tip static compilat de uz general dezvoltat în 1969-1973 de angajatul Bell Labs Dennis Ritchie ca o dezvoltare a limbajului Bee . A fost dezvoltat inițial pentru a implementa sistemul de operare UNIX , dar de atunci a fost portat pe multe alte platforme. Prin proiectare, limbajul se mapează îndeaproape cu instrucțiunile tipice ale mașinii și și-a găsit utilizare în proiecte care erau native pentru limbajul de asamblare , inclusiv atât sisteme de operare , cât și diverse aplicații software pentru o varietate de dispozitive, de la supercalculatoare la sisteme încorporate . Limbajul de programare C a avut un impact semnificativ asupra dezvoltării industriei software, iar sintaxa sa a devenit baza pentru astfel de limbaje de programare precum C++ , C# , Java și Objective-C .
Limbajul de programare C a fost dezvoltat între 1969 și 1973 la Bell Labs , iar până în 1973 majoritatea nucleului UNIX , scris inițial în asamblatorul PDP-11 /20, fusese rescris în acest limbaj. Numele limbii a devenit o continuare logică a vechii limbi „ Bi ” [a] , multe caracteristici ale cărora au fost luate ca bază.
Pe măsură ce limbajul s-a dezvoltat, a fost mai întâi standardizat ca ANSI C , iar apoi acest standard a fost adoptat de comitetul internațional de standardizare ISO ca ISO C, cunoscut și ca C90. Standardul C99 a adăugat noi caracteristici limbajului, cum ar fi matrice cu lungime variabilă și funcții inline. Și în standardul C11 , implementarea fluxurilor și suportul pentru tipurile atomice au fost adăugate limbajului. De atunci, însă, limbajul a evoluat lent și doar remedierea erorilor din standardul C11 a ajuns în standardul C18.
Limbajul C a fost conceput ca un limbaj de programare a sistemelor pentru care putea fi creat un compilator cu o singură trecere . Biblioteca standard este, de asemenea, mică. Ca o consecință a acestor factori, compilatoarele sunt relativ ușor de dezvoltat [2] . Prin urmare, această limbă este disponibilă pe o varietate de platforme. În plus, în ciuda naturii sale de nivel scăzut, limbajul este axat pe portabilitate. Programele conforme cu standardul de limbaj pot fi compilate pentru diferite arhitecturi de computer.
Scopul limbajului a fost acela de a face mai ușoară scrierea de programe mari cu erori minime în comparație cu asamblare, urmând principiile programării procedurale , dar evitând orice ar introduce o suprasarcină suplimentară specifică limbajelor de nivel înalt.
Principalele caracteristici ale lui C:
În același timp, lui C îi lipsește:
Unele dintre caracteristicile lipsă pot fi simulate cu instrumente încorporate (de exemplu, corutinele pot fi simulate folosind funcțiile setjmpșilongjmp ), unele sunt adăugate folosind biblioteci terțe (de exemplu, pentru a accepta funcții multitasking și de rețea, puteți utiliza biblioteci pthreads , socket-uri și altele asemenea; există biblioteci care suportă colectarea automată a gunoiului [3] ), o parte este implementată în unele compilatoare ca extensii de limbaj (de exemplu, funcții imbricate în GCC ). Există o tehnică oarecum greoaie, dar destul de funcțională, care permite implementarea mecanismelor OOP în C [4] , bazată pe polimorfismul real al pointerilor în C și suportul pointerii către funcții în acest limbaj. Mecanismele OOP bazate pe acest model sunt implementate în biblioteca GLib și sunt utilizate activ în cadrul GTK+ . GLib oferă o clasă de bază GObject, abilitatea de a moșteni dintr-o singură clasă [5] și de a implementa mai multe interfețe [6] .
La introducerea sa, limbajul a fost bine primit deoarece a permis crearea rapidă a compilatoarelor pentru noi platforme și, de asemenea, a permis programatorilor să fie destul de precisi în modul în care au fost executate programele lor. Datorită apropierii sale de limbaje de nivel scăzut, programele C au rulat mai eficient decât cele scrise în multe alte limbaje de nivel înalt și numai codul de limbaj de asamblare optimizat manual putea rula și mai rapid, deoarece dădea control deplin asupra mașinii. Până în prezent, dezvoltarea compilatoarelor și complicarea procesoarelor a dus la faptul că codul de asamblare scris de mână (cu excepția poate pentru programele foarte scurte) nu are practic niciun avantaj față de codul generat de compilator, în timp ce C continuă să fie unul dintre cele mai limbaje eficiente de nivel înalt.
Limba folosește toate caracterele alfabetului latin , numerele și unele caractere speciale [7] .
Caractere din alfabetul latin |
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 |
Numerele | 0, 1, 2, 3, 4, 5, 6, 7, 8,9 |
Simboluri speciale | , (virgulă) , ;, . (punct) , +, -, *, ^, & (ampersand) , =, ~ (tilde) , !, /, <, >, (, ), {, }, [, ], |, %, ?, ' (apostrof) , " (ghilimele) , : (coloană) , _ (subliniere ) ) , \,# |
Tokenurile sunt formate din caractere valide - constante predefinite , identificatori și semne de operare . La rândul lor, lexemele fac parte din expresii ; iar instrucţiunile şi operatorii sunt formaţi din expresii .
Când un program este tradus în C, din codul programului sunt extrase lexeme cu lungimea maximă care conțin caractere valide. Dacă un program conține un caracter nevalid, atunci analizatorul lexical (sau compilatorul) va genera o eroare, iar traducerea programului va fi imposibilă.
Simbolul #nu poate face parte din niciun simbol și este utilizat în preprocesorul .
IdentificatoriUn identificator valid este un cuvânt care poate include caractere latine, numere și litere de subliniere [8] . Identificatorii sunt dați operatorilor, constantelor, variabilelor, tipurilor și funcțiilor.
Identificatorii de cuvinte cheie și identificatorii încorporați nu pot fi utilizați ca identificatori de obiecte de program. Există și identificatori rezervați, pentru care compilatorul nu va da erori, dar care în viitor pot deveni cuvinte cheie, ceea ce va duce la incompatibilitate.
Există un singur identificator încorporat - __func__, care este definit ca un șir constant declarat implicit în fiecare funcție și care conține numele acesteia [8] .
Constante literaleLiterale formatate special în C sunt numite constante. Constantele literale pot fi întregi, reale, caractere [9] și șir [10] .
În mod implicit , numerele întregi sunt setate în zecimale . Dacă este specificat un prefix 0x, atunci acesta este în hexazecimal . Prefixul 0 indică faptul că numărul este în octal . Sufixul specifică dimensiunea minimă a tipului constant și, de asemenea, determină dacă numărul este semnat sau nesemnat. Tipul final este considerat cel mai mic posibil în care constanta dată poate fi reprezentată [11] .
Sufix | Pentru zecimală | Pentru octal și hexazecimal |
---|---|---|
Nu | int
long long long |
int
unsigned int long unsigned long long long unsigned long long |
usauU | unsigned int
unsigned long unsigned long long |
unsigned int
unsigned long unsigned long long |
lsauL | long
long long |
long
unsigned long long long unsigned long long |
usau Uîmpreună cu lsauL | unsigned long
unsigned long long |
unsigned long
unsigned long long |
llsauLL | long long | long long
unsigned long long |
usau Uîmpreună cu llsauLL | unsigned long long | unsigned long long |
Zecimal
format |
Cu exponent | hexazecimal
format |
---|---|---|
1.5 | 1.5e+0 | 0x1.8p+0 |
15e-1 | 0x3.0p-1 | |
0.15e+1 | 0x0.cp+1 |
Constantele numerelor reale sunt implicite de tip double. Când se specifică un sufix , ftipul este atribuit constantei float, iar când se specifică lsau L - long double. O constantă va fi considerată reală dacă conține un semn punct, sau o literă, psau Pîn cazul unei notații hexazecimale cu prefix 0x. Notația zecimală poate include un exponent după litere esau E. În cazul notației hexazecimale, exponentul este specificat după litere psau Peste obligatoriu, ceea ce distinge constantele hexazecimale reale de numerele întregi. În hexazecimal, exponentul este o putere a lui 2 [12] .
Constantele de caractere sunt cuprinse între ghilimele simple ( '), iar prefixul specifică atât tipul de date al constantei de caractere, cât și codificarea în care va fi reprezentat caracterul. În C, o constantă de caractere fără prefix este de tipul int[13] , spre deosebire de C++ , unde o constantă de caractere este char.
Prefix | Tip de date | Codificare |
---|---|---|
Nu | int | ASCII |
u | char16_t | Codare de șiruri multiocteți pe 16 biți |
U | char32_t | Codare de șiruri multiocteți pe 32 de biți |
L | wchar_t | Codare largă de șiruri |
Literale șir sunt încadrate între ghilimele duble și pot fi prefixate cu tipul de date și codificarea șirului. Literale șiruri sunt tablouri simple. Cu toate acestea, în codificările multiocteți, cum ar fi UTF-8 , un caracter poate ocupa mai mult de un element de matrice. De fapt, literalele șir sunt const [14] , dar spre deosebire de C++, tipurile lor de date nu conțin modificatorul const.
Prefix | Tip de date | Codificare |
---|---|---|
Nu | char * | Codare ASCII sau multibyte |
u8 | char * | UTF-8 |
u | char16_t * | Codare multiocteți pe 16 biți |
U | char32_t * | Codare multiocteți pe 32 de biți |
L | wchar_t * | Codare largă de șiruri |
Mai multe constante de șir consecutive separate prin spații albe sau linii noi sunt combinate într-un singur șir la compilare, care este adesea folosit pentru a stila codul unui șir prin separarea părților unei constante șir pe linii diferite pentru a îmbunătăți lizibilitatea [16] .
Constante numiteMacro | #define BUFFER_SIZE 1024 |
Enumerare anonimă |
enumerare { BUFFER_SIZE = 1024 }; |
Variabilă ca constantă |
const int buffer_size = 1024 ; extern const int buffer_size ; |
În limbajul C, pentru a defini constante, se obișnuiește să se utilizeze definiții macro declarate folosind directiva preprocesor [17] : #define
#define nume constantă [ valoare ]O constantă introdusă în acest fel va fi în vigoare în domeniul său, începând din momentul în care constanta este setată și până la sfârșitul codului programului, sau până când efectul constantei date este anulat de directivă #undef:
#undef nume constantCa și în cazul oricărei macrocomenzi, pentru o constantă numită, valoarea constantei este înlocuită automat în codul programului oriunde este folosit numele constantei. Prin urmare, atunci când declarați numere întregi sau reale în interiorul unei macrocomenzi, poate fi necesar să specificați în mod explicit tipul de date folosind sufixul literal corespunzător, altfel numărul va fi implicit un tip intîn cazul unui întreg sau un tip double în cazul unui real.
Pentru numere întregi, există o altă modalitate de a crea constante numite - prin enumerarea operatorului enum[17] . Cu toate acestea, această metodă este potrivită numai pentru tipuri mai mici sau egale cu tipul și nu este utilizată în biblioteca standard [18] . int
Este, de asemenea, posibil să se creeze constante ca variabile cu calificativul const, dar spre deosebire de celelalte două metode, astfel de constante consumă memorie, pot fi indicate și nu pot fi utilizate în timpul compilării [17] :
Cuvintele cheie sunt identificatori proiectați pentru a îndeplini o anumită sarcină în etapa de compilare sau pentru indicii și instrucțiuni pentru compilator.
Cuvinte cheie | Scop | Standard |
---|---|---|
sizeof | Obținerea dimensiunii unui obiect în timpul compilării | C89 |
typedef | Specificarea unui nume alternativ pentru un tip | |
auto,register | Sugestii pentru compilator pentru unde sunt stocate variabilele | |
extern | Spune compilatorului să caute un obiect în afara fișierului curent | |
static | Declararea unui obiect static | |
void | Nici un marcator de valoare; în pointeri înseamnă date arbitrare | |
char. short. int.long | Tipuri întregi și modificatorii lor de dimensiune | |
signed,unsigned | Modificatori de tip întreg care le definesc ca semnate sau nesemnate | |
float,double | Tipuri de date reale | |
const | Un modificator de tip de date care îi spune compilatorului că variabilele de acel tip sunt doar pentru citire | |
volatile | Instruirea compilatorului să schimbe valoarea unei variabile din exterior | |
struct | Tip de date, specificat ca o structură cu un set de câmpuri | |
enum | Un tip de date care stochează una dintr-un set de valori întregi | |
union | Un tip de date care poate stoca date în reprezentări ale diferitelor tipuri de date | |
do. for.while | Instrucțiuni de buclă | |
if,else | Operator condiționat | |
switch. case.default | Operator de selecție prin parametru întreg | |
break,continue | Declarații de întrerupere a buclei | |
goto | Operator de sărituri necondiționat | |
return | Întoarcere dintr-o funcție | |
inline | Declarație de funcție în linie | C99 [20] |
restrict | Declararea unui pointer care se referă la un bloc de memorie care nu este referit de niciun alt pointer | |
_Bool[b] | tip de date boolean | |
_Complex[c] ,_Imaginary [d] | Tipuri utilizate pentru calculele de numere complexe | |
_Atomic | Un modificator de tip care îl face atomic | C11 |
_Alignas[e] | Specificarea explicită a alinierii octeților pentru un tip de date | |
_Alignof[f] | Obținerea alinierii pentru un anumit tip de date în momentul compilării | |
_Generic | Selectarea uneia dintr-un set de valori în timpul compilării, pe baza tipului de date controlat | |
_Noreturn[g] | Indicarea compilatorului că funcția nu se poate termina în mod normal (adică prin return) | |
_Static_assert[h] | Specificarea aserțiunilor de verificat în timpul compilării | |
_Thread_local[i] | Declararea unei variabile locale de fir |
Pe lângă cuvintele cheie, standardul lingvistic definește identificatori rezervați, a căror utilizare poate duce la incompatibilitatea cu versiunile viitoare ale standardului. Sunt rezervate toate cuvintele cheie, cu excepția cuvintelor cheie care încep cu o liniuță de subliniere ( _) urmată fie de o literă majusculă ( A- Z) fie de o altă liniuță de subliniere [21] . În standardele C99 și C11, unii dintre acești identificatori au fost utilizați pentru cuvintele cheie în limbi noi.
În domeniul de aplicare al fișierului, utilizarea oricăror nume care încep cu un caracter de subliniere ( _) [21] este rezervată , adică este permisă denumirea tipurilor, constantelor și variabilelor declarate într-un bloc de instrucțiuni, de exemplu, în interiorul funcțiilor, cu subliniere.
De asemenea, identificatorii rezervați sunt toate macrocomenzile bibliotecii standard și numele din aceasta sunt legate în etapa de conectare [21] .
Utilizarea identificatorilor rezervați în programe este definită de standard ca comportament nedefinit . Încercarea de a anula orice macro standard prin intermediul #undefva avea ca rezultat, de asemenea, un comportament nedefinit [21] .
Textul unui program C poate conține fragmente care nu fac parte din comentariile codului programului . Comentariile sunt marcate într-un mod special în textul programului și sunt omise în timpul compilării.
Inițial, în standardul C89 , erau disponibile comentarii în linie care puteau fi plasate între secvențele de caractere /*și */. În acest caz, este imposibil să imbricați un comentariu în altul, deoarece prima secvență întâlnită */va încheia comentariul, iar textul imediat după notație */va fi perceput de compilator ca cod sursă al programului.
Următorul standard, C99 , a introdus încă un alt mod de marcare a comentariilor: un comentariu este considerat text care începe cu o secvență de caractere //și se termină la sfârșitul unei linii [20] .
Comentariile sunt adesea folosite pentru a auto-documenta codul sursă, explicând părți complexe, descriind scopul anumitor fișiere și descriind regulile pentru utilizarea și funcționarea anumitor funcții, macrocomenzi, tipuri de date și variabile. Există postprocesoare care pot converti comentariile formatate special în documentație. Printre astfel de postprocesoare cu limbajul C, sistemul de documentare Doxygen poate funcționa .
Operatorii utilizați în expresii sunt unele operații care se efectuează pe operanzi și care returnează o valoare calculată - rezultatul operației. Operandul poate fi o constantă, variabilă, expresie sau apel de funcție. Un operator poate fi un caracter special, un set de caractere speciale sau un cuvânt special. Operatorii se disting prin numărul de operanzi implicați, și anume, ei disting între operatorii unari, operatorii binari și operatorii ternari.
Operatori unariOperatorii unari efectuează o operație pe un singur argument și au următorul format de operație:
[ operator ] [ operand ]Operațiile de creștere și decrementare postfix au formatul invers:
[ operand ] [ operator ]+ | plus unar | ~ | Luarea codului de retur | & | Luarea unei adrese | ++ | Prefix sau increment de postfix | sizeof | Obținerea numărului de octeți ocupați de un obiect în memorie; poate fi folosit atat ca operatie cat si ca operator |
- | minus unar | ! | negație logică | * | Dereferențierea pointerului | -- | Decrementarea prefixului sau postfixului | _Alignof | Obținerea alinierii pentru un anumit tip de date |
Operatorii de creștere și decrementare, spre deosebire de ceilalți operatori unari, schimbă valoarea operandului lor. Operatorul de prefix modifică mai întâi valoarea și apoi o returnează. Postfix returnează mai întâi valoarea și abia apoi o modifică.
Operatori binariOperatorii binari sunt localizați între două argumente și efectuează o operație asupra lor:
[ operand ] [ operator ] [ operand ]+ | Plus | % | Luând restul unei diviziuni | << | Shift la stânga pe biți | > | Mai mult | == | Egal |
- | Scădere | & | ȘI pe biți | >> | Deplasare biți la dreapta | < | Mai puțin | != | Nu este egal |
* | Multiplicare | | | SAU pe biți | && | ȘI logic | >= | Mai mare sau egal | ||
/ | Divizia | ^ | XOR pe biți | || | SAU logic | <= | Mai mic sau egal |
De asemenea, operatorii binari din C includ operatori de alocare la stânga care efectuează o operație pe argumentele stânga și dreapta și pun rezultatul în argumentul stâng.
= | Atribuirea valorii argumentului din dreapta la stânga | %= | Restul împărțirii operandului din stânga la dreapta | ^= | XOR pe biți de la operand din dreapta la operand din stânga |
+= | Adăugarea operandului din stânga al celui din dreapta | /= | Împărțirea operandului stâng la dreapta | <<= | Deplasarea pe biți a operandului din stânga la stânga cu numărul de biți dat de operandul din dreapta |
-= | Scăderea din operandul stâng al celui drept | &= | Pe bit ȘI operandul din dreapta la stânga | >>= | Deplasarea pe biți a operandului din stânga la dreapta cu numărul de biți specificat de operandul din dreapta |
*= | Înmulțirea operandului stâng cu dreapta | |= | SAU pe biți a operandului din dreapta la stânga |
Există un singur operator ternar în C, operatorul condițional scurtat, care are următoarea formă:
[ condiție ] ?[ expresie1 ] :[ expresie2 ]Operatorul condițional prescurtat are trei operanzi:
Operatorul în acest caz este o combinație de semne ?și :.
O expresie este un set ordonat de operații asupra constantelor, variabilelor și funcțiilor. Expresiile conțin operații formate din operanzi și operatori . Ordinea în care sunt efectuate operațiunile depinde de forma de înregistrare și de prioritatea operațiunilor. Fiecare expresie are o valoare - rezultatul efectuării tuturor operațiilor incluse în expresie. În timpul evaluării unei expresii, în funcție de operații, valorile variabilelor se pot modifica, iar funcțiile pot fi executate și dacă apelurile lor sunt prezente în expresie.
Dintre expresii, se distinge o clasă de expresii admisibile la stânga - expresii care pot fi prezente în stânga semnului de atribuire.
Prioritatea executării operațiunilorPrioritatea operațiunilor este definită de standard și specifică ordinea în care vor fi efectuate operațiunile. Operațiile în C se efectuează conform tabelului de precedență de mai jos [25] [26] .
O prioritate | jetoane | Operațiune | Clasă | Asociativitatea |
---|---|---|---|---|
unu | a[index] | Referire prin index | postfix | de la stânga la dreapta → |
f(argumente) | Apel de funcție | |||
. | Acces pe teren | |||
-> | Acces la câmp prin indicator | |||
++ -- | Creștere pozitivă și negativă | |||
() {inițializator de nume de tip} | Literal compus (C99) | |||
() {inițializator de nume de tip ,} | ||||
2 | ++ -- | Creșteri de prefix pozitiv și negativ | unar | ← de la dreapta la stânga |
sizeof | Obținerea dimensiunii | |||
_Alignof[f] | Obține aliniere ( C11 ) | |||
~ | Pe bit NU | |||
! | NU logic | |||
- + | Indicație semn (minus sau plus) | |||
& | Obținerea unei adrese | |||
* | Referință indicator (referință) | |||
(nume de tip) | Tip turnare | |||
3 | * / % | Înmulțirea, împărțirea și restul | binar | de la stânga la dreapta → |
patru | + - | Adunare si scadere | ||
5 | << >> | Schimbați la stânga și la dreapta | ||
6 | < > <= >= | Operatii de comparatie | ||
7 | == != | Verificarea egalității sau inegalității | ||
opt | & | ȘI pe biți | ||
9 | ^ | XOR pe biți | ||
zece | | | SAU pe biți | ||
unsprezece | && | ȘI logic | ||
12 | || | SAU logic | ||
13 | ? : | Condiție | ternar | ← de la dreapta la stânga |
paisprezece | = | Atribuirea valorii | binar | |
+= -= *= /= %= <<= >>= &= ^= |= | Operații de modificare a valorii din stânga | |||
cincisprezece | , | Calcul secvenţial | de la stânga la dreapta → |
Prioritățile operatorului din C nu se justifică întotdeauna și uneori conduc la rezultate intuitiv dificil de prezis. De exemplu, deoarece operatorii unari au asociativitate de la dreapta la stânga, evaluarea expresiei *p++va avea ca rezultat o creștere a indicatorului urmată de o dereferință ( *(p++)), mai degrabă decât o creștere a indicatorului ( (*p)++). Prin urmare, în cazul situațiilor dificil de înțeles, se recomandă gruparea explicită a expresiilor folosind paranteze [26] .
O altă caracteristică importantă a limbajului C este că evaluarea valorilor argumentelor transmise unui apel de funcție nu este secvențială [27] , adică argumentele care separă prin virgulă nu corespund evaluării secvențiale din tabelul de precedență. În exemplul următor, apelurile de funcții date ca argumente unei alte funcții pot fi în orice ordine:
int x ; x = calculează ( get_arg1 (), get_arg2 ()); // apelează mai întâi get_arg2().De asemenea, nu te poți baza pe precedența operațiilor în cazul reacțiilor adverse care apar în timpul evaluării expresiei, deoarece aceasta va duce la un comportament nedefinit [27] .
Puncte de secvență și efecte secundareAnexa C la standardul de limbaj definește un set de puncte de secvență care se garantează că nu vor avea efecte secundare continue din calcule. Adică, punctul de secvență este o etapă de calcule care separă evaluarea expresiilor între ele, astfel încât calculele care au avut loc înainte de punctul de secvență, inclusiv efectele secundare, s-au încheiat deja, iar după punctul de secvență nu au început încă [28] ] . Un efect secundar poate fi o modificare a valorii unei variabile în timpul evaluării unei expresii. Modificarea valorii implicate în calcul, împreună cu efectul secundar al schimbării aceleiași valori la următorul punct al secvenței, va duce la un comportament nedefinit. Același lucru se va întâmpla dacă există două sau mai multe modificări laterale la aceeași valoare implicate în calcul [27] .
Punct de drum | Eveniment înainte | Eveniment după |
---|---|---|
Apel de funcție | Calcularea unui pointer către o funcție și argumentele acesteia | Apel de funcție |
Operatori logici AND ( &&), SAU ( ||) si calcul secvential ( ,) | Calculul primului operand | Calculul celui de-al doilea operand |
Operator de condiție scurtă ( ?:) | Calculul operandului care servește drept condiție | Calculul celui de-al 2-lea sau al 3-lea operand |
Între două expresii complete (neimbricate) | O expresie completă | Următoarea expresie completă |
Descriptor complet completat | ||
Chiar înainte de a reveni de la o funcție de bibliotecă | ||
După fiecare conversie asociată cu un specificator I/O formatat | ||
Imediat înainte și imediat după fiecare apel la funcția de comparare și între apelul la funcția de comparație și orice mișcare efectuată asupra argumentelor transmise funcției de comparație |
Expresiile complete sunt [27] :
În exemplul următor, variabila este schimbată de trei ori între punctele secvenței, rezultând un rezultat nedefinit:
int i = 1 ; // Descriptorul este primul punct al secvenței, expresia completă este a doua i += ++ i + 1 ; // Expresie completă - al treilea punct de secvență printf ( "%d \n " , i ); // Poate scoate fie 4, fie 5Alte exemple simple de comportament nedefinit de evitat:
i = i ++ + 1 ; // comportament nedefinit i = ++ i + 1 ; // de asemenea comportament nedefinit printf ( "%d, %d \n " , -- i , ++ i ); // comportament nedefinit printf ( "%d, %d \n " , ++ i , ++ i ); // de asemenea comportament nedefinit printf ( "%d, %d \n " , i = 0 , i = 1 ); // comportament nedefinit printf ( " %d, %d \n " , i = 0 , i = 0 ); // de asemenea comportament nedefinit a [ i ] = i ++ ; // comportament nedefinit a [ i ++ ] = i ; // de asemenea comportament nedefinitInstrucțiunile de control sunt concepute pentru a efectua acțiuni și pentru a controla fluxul de execuție a programului. Mai multe enunţuri consecutive formează o secvenţă de enunţuri .
Declarație goalăCel mai simplu construct al limbajului este o expresie goală numită declarație goală [29] :
;O instrucțiune goală nu face nimic și poate fi plasată oriunde în program. Folosit în mod obișnuit în bucle cu corp lipsă [30] .
InstrucțiuniO instrucțiune este un fel de acțiune elementară:
( expresie );Acțiunea acestui operator este de a executa expresia specificată în corpul operatorului.
Mai multe instrucțiuni consecutive formează o secvență de instrucțiuni .
Bloc de instrucțiuniInstrucțiunile pot fi grupate în blocuri speciale de următoarea formă:
{
( secventa de instructiuni )},
Un bloc de instrucțiuni, numit uneori și o instrucțiune compusă, este delimitat de o acoladă stângă ( {) la început și o acoladă dreaptă ( }) la sfârșit.
În funcțiile , un bloc de instrucțiuni denotă corpul funcției și face parte din definiția funcției. Instrucțiunea compusă poate fi folosită și în instrucțiuni de buclă, condiție și alegere.
Declarații condiționaleExistă doi operatori condiționali în limbaj care implementează ramificarea programului:
Cea mai simplă formă a operatoruluiif
if(( condiție ) )( operator ) ( următoarea declarație )Operatorul iffuncționează astfel:
În special, următorul cod, dacă condiția specificată este îndeplinită, nu va efectua nicio acțiune, deoarece, de fapt, este executată o instrucțiune goală:
if(( conditie )) ;O formă mai complexă a operatorului ifconține cuvântul cheie else:
if(( condiție ) )( operator ) else( operator alternativ ) ( următoarea declarație )Aici, dacă condiția specificată între paranteze nu este îndeplinită, atunci instrucțiunea specificată după cuvântul cheie este executată else.
Chiar dacă standardul permite ca instrucțiunile să fie specificate într- o singură linie ifsau ca elseo singură linie, acest lucru este considerat stil prost și reduce lizibilitatea codului. Este recomandat să specificați întotdeauna un bloc de instrucțiuni folosind acolade ca corp [31] .
Instrucțiuni de execuție în buclăO buclă este o bucată de cod care conține
În consecință, există două tipuri de cicluri:
O buclă postcondițională garantează că corpul buclei va fi executat cel puțin o dată.
Limbajul C oferă două variante de bucle cu o precondiție: whileși for.
while(stare) [ corp buclă ] for( instrucțiunea ;condiției blocului de inițializare [ corpul buclei ],;)Bucla forse mai numește și parametrică, este echivalentă cu următorul bloc de instrucțiuni:
[ bloc de inițializare ] while(condiție) { [ corp buclă ] [ operator ] }Într-o situație normală, blocul de inițializare conține setarea valorii inițiale a unei variabile, care se numește variabilă buclă, iar instrucțiunea care este executată imediat după ce corpul buclei modifică valorile variabilei utilizate, condiția conține o comparație a valorii variabilei buclei utilizate cu o valoare predefinită și, de îndată ce comparația se oprește, bucla este întreruptă și codul programului imediat care urmează instrucțiunii buclei începe să fie executat.
Pentru o buclă do-while, condiția este specificată după corpul buclei:
do[ corp buclă ] while( condiție)Condiția buclei este o expresie booleană. Cu toate acestea, turnarea implicită a tipului vă permite să utilizați o expresie aritmetică ca condiție de buclă. Acest lucru vă permite să organizați așa-numita „buclă infinită”:
while(1);Același lucru se poate face cu operatorul for:
for(;;);În practică, astfel de bucle infinite sunt de obicei folosite împreună cu break, gotosau return, care întrerup bucla în moduri diferite.
Ca și în cazul unei instrucțiuni condiționate, utilizarea unui corp cu o singură linie fără a-l include într-un bloc de instrucțiuni cu acolade este considerată un stil prost, reducând lizibilitatea codului [31] .
Operatori de sărituri necondiționațiOperatorii de ramuri necondiționați vă permit să întrerupeți execuția oricărui bloc de calcule și să mergeți în alt loc din program în cadrul funcției curente. Operatorii de salt necondiționați sunt utilizați de obicei împreună cu operatorii condiționali.
goto[ etichetă ],O etichetă este un identificator care transferă controlul operatorului care este marcat în program cu eticheta specificată:
[ etichetă ] :[ operator ]Dacă eticheta specificată nu este prezentă în program sau dacă există mai multe instrucțiuni cu aceeași etichetă, compilatorul raportează o eroare.
Transferul controlului este posibil numai în cadrul funcției în care este utilizat operatorul de tranziție, prin urmare, utilizarea operatorului gotonu poate transfera controlul către o altă funcție.
Alte instrucțiuni de salt sunt legate de bucle și vă permit să întrerupeți execuția corpului buclei:
Instrucțiunea breakpoate, de asemenea, întrerupe funcționarea instrucțiunii switch, astfel încât în interiorul instrucțiunii switchcare rulează în buclă, instrucțiunea breaknu va putea întrerupe bucla. Specificat în corpul buclei, întrerupe activitatea celei mai apropiate bucle imbricate.
Operatorul continuepoate fi utilizat numai în interiorul operatorilor do, whileși for. Pentru bucle whileși do-whileoperatorul continueprovoacă testarea condiției buclei, iar în cazul unei bucle for , execuția operatorului specificat în al 3-lea parametru al buclei, înainte de a verifica condiția de continuare a buclei.
Instrucțiune de returnare a funcțieiOperatorul returnîntrerupe execuția funcției în care este utilizat. Dacă funcția nu trebuie să returneze o valoare, atunci se folosește un apel fără o valoare returnată:
return;Dacă funcția trebuie să returneze o valoare, atunci valoarea returnată este indicată după operator:
return[ valoare ];Dacă există alte instrucțiuni după instrucțiunea return în corpul funcției, atunci aceste instrucțiuni nu vor fi niciodată executate, caz în care compilatorul poate emite un avertisment. Totuși, după operator return, pot fi indicate instrucțiuni pentru terminarea alternativă a funcției, de exemplu, din greșeală, iar trecerea la acești operatori poate fi efectuată folosind operatorul gotoconform oricăror condiții .
La declararea unei variabile, sunt specificate tipul ei și numele, iar valoarea inițială poate fi de asemenea specificată:
[descriptor] [nume];sau
[descriptor] [nume] =[inițializator] ;,Unde
Dacă variabilei nu i se atribuie o valoare inițială, atunci în cazul unei variabile globale, valoarea acesteia este completată cu zerouri, iar pentru o variabilă locală, valoarea inițială va fi nedefinită.
Într-un descriptor de variabilă, puteți desemna o variabilă ca fiind globală, dar limitată la domeniul de aplicare al unui fișier sau funcție, folosind cuvântul cheie static. Dacă o variabilă este declarată globală fără cuvântul cheie static, atunci ea poate fi accesată și din alte fișiere, unde este necesară declararea acestei variabile fără inițializator, dar cu cuvântul cheie extern. Adresele unor astfel de variabile sunt determinate în momentul legăturii .
O funcție este o bucată independentă de cod de program care poate fi reutilizată într-un program. Funcțiile pot lua argumente și pot returna valori. Funcțiile pot avea și efecte secundare în timpul execuției lor: modificarea variabilelor globale, lucrul cu fișiere, interacțiunea cu sistemul de operare sau hardware -ul [28] .
Pentru a defini o funcție în C, trebuie să o declarați:
De asemenea, este necesar să se furnizeze o definiție a funcției care să conțină un bloc de instrucțiuni care implementează comportamentul funcției.
Nedeclararea unei anumite funcții este o eroare dacă funcția este utilizată în afara domeniului de aplicare al definiției, ceea ce, în funcție de implementare, are ca rezultat mesaje sau avertismente.
Pentru a apela o funcție, este suficient să specificați numele acesteia cu parametrii specificați între paranteze. În acest caz, adresa punctului de apel este plasată pe stivă, variabilele responsabile de parametrii funcției sunt create și inițializate, iar controlul este transferat codului care implementează funcția apelată. După ce funcția este executată, memoria alocată în timpul apelului funcției este eliberată, revenirea la punctul de apel și, dacă apelul funcției face parte dintr-o expresie, valoarea calculată în interiorul funcției este trecută la punctul de revenire.
Dacă parantezele nu sunt specificate după funcție, atunci compilatorul interpretează acest lucru ca obținând adresa funcției. Adresa unei funcții poate fi introdusă într-un pointer și ulterior numită funcția folosind un pointer către aceasta, care este utilizat activ, de exemplu, în sistemele de plugin [32] .
Folosind cuvântul cheie, inlineputeți marca funcțiile ale căror apeluri doriți să le executați cât mai repede posibil. Compilatorul poate înlocui codul unor astfel de funcții direct în punctul apelului lor [33] . Pe de o parte, aceasta mărește cantitatea de cod executabil, dar, pe de altă parte, economisește timpul de execuție a acestuia, deoarece operația de apelare a funcției care necesită timp nu este utilizată. Cu toate acestea, datorită arhitecturii computerelor, funcțiile de inline pot fie să accelereze, fie să încetinească aplicația în ansamblu. Cu toate acestea, în multe cazuri funcțiile inline sunt înlocuitorul preferat pentru macrocomenzi [34] .
Declarație de funcțieO declarație de funcție are următorul format:
[descriptor] [nume] ([listă] );,Unde
Semnul unei declarații de funcție este ;simbolul „ ”, deci o declarație de funcție este o instrucțiune.
În cel mai simplu caz, [declarator] conține o indicație a unui anumit tip de valoare returnată. O funcție care nu ar trebui să returneze nicio valoare este declarată a fi de tip void.
Dacă este necesar, descriptorul poate conține modificatori specificați folosind cuvinte cheie:
Lista parametrilor funcției definește semnătura funcției.
C nu permite declararea mai multor funcții cu același nume, supraîncărcarea funcției nu este suportată [36] .
Definiția funcțieiDefiniția funcției are următorul format:
[descriptor] [nume] ([listă] )[corp]Unde [declarator], [nume] și [listă] sunt aceleași ca în declarație, iar [body] este o instrucțiune compusă care reprezintă o implementare concretă a funcției. Compilatorul face distincție între definițiile funcțiilor cu același nume prin semnătură și astfel (prin semnătură) se stabilește o legătură între definiție și declarația corespunzătoare.
Corpul funcției arată astfel:
{ [secvență de declarații] return([valoare returnată]); }Returul din funcție se realizează folosind operatorul , care fie specifică valoarea returnată, fie nu o specifică, în funcție de tipul de date returnat de funcție. În cazuri rare, o funcție poate fi marcată ca nereturnând folosind o macrocomandă dintr-un fișier antet , caz în care nu este necesară nicio declarație . De exemplu, funcțiile care apelează necondiționat în sine pot fi marcate în acest fel [33] . returnnoreturnstdnoreturn.hreturnabort()
Apel de funcțieApelul funcției este de a efectua următoarele acțiuni:
În funcție de implementare, compilatorul fie asigură strict că tipul parametrului real se potrivește cu tipul parametrului formal, fie, dacă este posibil, realizează o conversie implicită a tipului, ceea ce, evident, duce la efecte secundare.
Dacă o variabilă este transmisă funcției, atunci când funcția este apelată, este creată o copie a acesteia ( memoria este alocată pe stivă și valoarea este copiată). De exemplu, trecerea unei structuri unei funcții va face ca întreaga structură să fie copiată. Dacă se transmite un pointer către o structură, este copiată doar valoarea pointerului. Transmiterea unui tablou unei funcții determină, de asemenea, copiarea unui pointer către primul său element. În acest caz, pentru a indica în mod explicit că adresa de la începutul matricei este luată ca intrare în funcție, și nu un pointer către o singură variabilă, în loc să declarați un pointer după numele variabilei, puteți pune paranteze drepte, pt. exemplu:
void example_func ( int array []); // array este un pointer către primul element al unui tablou de tip intC permite apeluri imbricate. Adâncimea de imbricare a apelurilor are o limitare evidentă legată de dimensiunea stivei alocate programului. Prin urmare, implementările C stabilesc o limită pentru adâncimea cuibării.
Un caz special de apel imbricat este un apel de funcție în interiorul corpului funcției apelate. Un astfel de apel se numește recursiv și este folosit pentru a organiza calcule uniforme. Având în vedere restricția naturală a apelurilor imbricate, implementarea recursivă este înlocuită cu o implementare care utilizează bucle.
Tipurile de date întregi variază în dimensiune de la cel puțin 8 la cel puțin 32 de biți. Standardul C99 mărește dimensiunea maximă a unui întreg la cel puțin 64 de biți. Tipurile de date întregi sunt folosite pentru a stoca numere întregi (tipul chareste folosit și pentru a stoca caractere ASCII). Toate dimensiunile intervalului de tipuri de date de mai jos sunt minime și pot fi mai mari pe o anumită platformă [37] .
Ca o consecință a dimensiunilor minime ale tipurilor, standardul cere ca dimensiunile tipurilor integrale să îndeplinească condiția:
1= ≤ ≤ ≤ ≤ . sizeof(char)sizeof(short)sizeof(int)sizeof(long)sizeof(long long)
Astfel, dimensiunile unor tipuri în ceea ce privește numărul de octeți se pot potrivi dacă este îndeplinită condiția pentru numărul minim de biți. Chiar charși longpoate avea aceeași dimensiune dacă un octet va dura 32 de biți sau mai mult, dar astfel de platforme vor fi foarte rare sau nu vor exista. Standardul garantează că tipul este char întotdeauna de 1 octet. Mărimea unui octet în biți este determinată de o constantă CHAR_BITdin fișierul antet limits.h, care este de 8 biți pe sistemele compatibile cu POSIX [38] .
Intervalul minim de valori ale tipurilor întregi în conformitate cu standardul este definit de la până la pentru tipurile cu semn și de la până la pentru tipurile fără semn, unde N este adâncimea de biți a tipului. Implementările compilatorului pot extinde acest interval la discreția lor. În practică, intervalul de la până la este folosit mai frecvent pentru tipurile semnate . Valorile minime și maxime ale fiecărui tip sunt specificate în fișier ca definiții macro. -(2N-1-1)2N-1-102N-2N-12N-1-1limits.h
O atenție deosebită trebuie acordată tipului char. În mod formal, acesta este un tip separat, dar este de fapt charechivalent fie cu signed char, fie cu unsigned char, în funcție de compilator [39] .
Pentru a evita confuzia între dimensiunile tipurilor, standardul C99 a introdus noi tipuri de date, descrise în stdint.h. Printre acestea se numără tipuri precum: , , , unde = 8, 16, 32 sau 64. Prefixul indică tipul minim care poate găzdui biți, prefixul indică un tip de cel puțin 16 biți, care este cel mai rapid de pe această platformă. Tipurile fără prefixe denotă tipuri cu o dimensiune fixă de biți. intN_tint_leastN_tint_fastN_tNleast-Nfast-N
Tipurile cu prefixe least-și fast-pot fi considerate un înlocuitor pentru tipurile int, short, long, cu singura diferență că primele oferă programatorului posibilitatea de a alege între viteză și dimensiune.
Tip de date | Marimea | Interval de valori minime | Standard |
---|---|---|---|
signed char | minim 8 biți | de la −127 [40] (= -(2 7 −1)) la 127 | C90 [j] |
int_least8_t | C99 | ||
int_fast8_t | |||
unsigned char | minim 8 biți | 0 până la 255 (=2 8 −1) | C90 [j] |
uint_least8_t | C99 | ||
uint_fast8_t | |||
char | minim 8 biți | −127 la 127 sau de la 0 la 255 în funcție de compilator | C90 [j] |
short int | minim 16 biți | de la -32,767 (= -(2 15 -1)) la 32,767 | C90 [j] |
int | |||
int_least16_t | C99 | ||
int_fast16_t | |||
unsigned short int | minim 16 biți | 0 până la 65,535 (= 2 16 −1) | C90 [j] |
unsigned int | |||
uint_least16_t | C99 | ||
uint_fast16_t | |||
long int | minim 32 de biți | −2.147.483.647 până la 2.147.483.647 | C90 [j] |
int_least32_t | C99 | ||
int_fast32_t | |||
unsigned long int | minim 32 de biți | 0 până la 4.294.967.295 (= 2 32 −1) | C90 [j] |
uint_least32_t | C99 | ||
uint_fast32_t | |||
long long int | minim 64 de biți | -9.223.372.036.854.775.807 până la 9.223.372.036.854.775.807 | C99 |
int_least64_t | |||
int_fast64_t | |||
unsigned long long int | minim 64 de biți | 0 până la 18.446.744.073.709.551.615 (= 264 −1 ) | |
uint_least64_t | |||
uint_fast64_t | |||
int8_t | 8 biți | -127 la 127 | |
uint8_t | 8 biți | 0 până la 255 (=2 8 −1) | |
int16_t | 16 biți | -32.767 până la 32.767 | |
uint16_t | 16 biți | 0 până la 65,535 (= 2 16 −1) | |
int32_t | 32 de biți | −2.147.483.647 până la 2.147.483.647 | |
uint32_t | 32 de biți | 0 până la 4.294.967.295 (= 2 32 −1) | |
int64_t | 64 de biți | -9.223.372.036.854.775.807 până la 9.223.372.036.854.775.807 | |
uint64_t | 64 de biți | 0 până la 18.446.744.073.709.551.615 (= 264 −1 ) | |
Tabelul arată intervalul minim de valori conform standardului de limbă. Compilatoarele C pot extinde gama de valori. |
De asemenea, începând cu standardul C99, au fost adăugate tipurile intmax_tși uintmax_t, corespunzătoare celor mai mari tipuri semnate, respectiv nesemnate. Aceste tipuri sunt convenabile atunci când sunt utilizate în macrocomenzi pentru a stoca valori intermediare sau temporare în timpul operațiunilor pe argumente întregi, deoarece vă permit să potriviți valori de orice tip. De exemplu, aceste tipuri sunt utilizate în macrocomenzile de comparare întregi ale bibliotecii de testare unitară Verificare pentru C [41] .
În C, există mai multe tipuri de întregi suplimentare pentru manipularea în siguranță a tipului de date pointer intptr_t: uintptr_tși ptrdiff_t. Tipurile intptr_tși uintptr_tdin standardul C99 sunt concepute pentru a stoca valori semnate și, respectiv, nesemnate, care se pot potrivi cu un indicator în dimensiune. Aceste tipuri sunt adesea folosite pentru a stoca un număr întreg arbitrar într-un pointer, de exemplu, ca o modalitate de a scăpa de alocarea de memorie inutilă atunci când se înregistrează funcții de feedback [42] sau când se utilizează liste legate de terțe părți, matrice asociative și alte structuri în care datele sunt stocate prin indicator. Tipul ptrdiff_tdin fișierul antet stddef.heste conceput pentru a stoca în siguranță diferența dintre două pointeri.
Pentru a stoca dimensiunea, este furnizat un tip nesemnat size_tdin fișierul antet stddef.h. Acest tip este capabil să dețină numărul maxim posibil de octeți disponibili la pointer și este de obicei folosit pentru a stoca dimensiunea în octeți. Valoarea de acest tip este returnată de operatorul sizeof[43] .
Casting de tip întregConversiile de tip întreg pot avea loc fie în mod explicit, folosind un operator de distribuție, fie implicit. Valorile tipurilor mai mici decât int, atunci când participă la orice operațiune sau când sunt transmise la un apel de funcție, sunt turnate automat la tipul int, iar dacă conversia este imposibilă, la tipul unsigned int. Adesea, astfel de turnări implicite sunt necesare pentru ca rezultatul calculului să fie corect, dar uneori conduc la erori intuitiv de neînțeles în calcule. De exemplu, dacă operația implică numere de tip intși unsigned int, iar valoarea cu semn este negativă, atunci conversia unui număr negativ într-un tip fără semn va duce la o depășire și o valoare pozitivă foarte mare, ceea ce poate duce la un rezultat incorect al operațiilor de comparare. [44] .
Tipurile semnate și nesemnate sunt mai mici decâtint | Semnat este mai puțin decât nesemnat și nesemnat nu este mai puținint |
---|---|
#include <stdio.h> caracter semnat x = -1 ; unsigned char y = 0 ; if ( x > y ) { // condiția este false printf ( "Mesajul nu va fi afișat. \n " ); } dacă ( x == UCHAR_MAX ) { // condiția este false printf ( "Mesajul nu va fi afișat. \n " ); } | #include <stdio.h> caracter semnat x = -1 ; unsigned int y = 0 ; if ( x > y ) { // condiția este adevărată printf ( "Depășire în variabila x. \n " ); } dacă (( x == UINT_MAX ) && ( x == ULONG_MAX )) { // condiția va fi întotdeauna adevărată printf ( "Depășire în variabila x. \n " ); } |
În acest exemplu, ambele tipuri, semnate și nesemnate, vor fi turnate în semnat int, deoarece permite încadrarea intervalelor ambelor tipuri. Prin urmare, comparația în operatorul condiționat va fi corectă. | Un tip semnat va fi transformat în nesemnat deoarece tipul nesemnat este mai mare sau egal cu dimensiunea int, dar va avea loc o depășire deoarece este imposibil să se reprezinte o valoare negativă într-un tip nesemnat. |
De asemenea, turnarea automată a tipului va funcționa dacă în expresie sunt utilizate două sau mai multe tipuri de numere întregi diferite. Standardul definește un set de reguli după care se alege o conversie de tip care poate da rezultatul corect al calculului. Diferite tipuri li se atribuie ranguri diferite în cadrul transformării, iar rangurile în sine se bazează pe dimensiunea tipului. Atunci când într-o expresie sunt implicate diferite tipuri, de obicei se alege să se arunce aceste valori într-un tip de rang superior [44] .
Numerele realeNumerele cu virgulă mobilă în C sunt reprezentate de trei tipuri de bază: float, doubleși long double.
Numerele reale au o reprezentare foarte diferită de numerele întregi. Constantele numerelor reale de diferite tipuri, scrise în notație zecimală, pot să nu fie egale între ele. De exemplu, condiția 0.1 == 0.1fva fi falsă din cauza pierderii preciziei în tipul float, în timp ce condiția 0.5 == 0.5fva fi adevărată deoarece aceste numere sunt finite în reprezentare binară. Cu toate acestea, condiția de turnare (float) 0.1 == 0.1fva fi, de asemenea, adevărată, deoarece turnarea la un tip mai puțin precis pierde biții care fac cele două constante diferite.
Operațiile aritmetice cu numere reale sunt, de asemenea, inexacte și au adesea o eroare flotantă [45] . Cea mai mare eroare va apărea atunci când se operează pe valori care sunt aproape de minimul posibil pentru un anumit tip. De asemenea, eroarea se poate dovedi a fi mare atunci când se calculează simultan numere foarte mici (≪ 1) și foarte mari (≫ 1). În unele cazuri, eroarea poate fi redusă prin schimbarea algoritmilor și a metodelor de calcul. De exemplu, la înlocuirea adunării multiple cu înmulțirea, eroarea poate scădea de câte ori au existat operații de adunare inițial.
De asemenea, în fișierul antet math.hexistă două tipuri suplimentare float_tși double_t, care corespund cel puțin tipurilor floatși doublerespectiv, dar pot fi diferite de acestea. Tipurile float_tși double_tsunt adăugate în standardul C99 , iar corespondența lor cu tipurile de bază este determinată de valoarea macro-ului FLT_EVAL_METHOD.
Tip de date | Marimea | Standard |
---|---|---|
float | 32 de biți | IEC 60559 ( IEEE 754 ), extensia F a standardului C [46] [k] , număr unic de precizie |
double | 64 de biți | IEC 60559 (IEEE 754), extensia F a standardului C [46] [k] , număr de precizie dublă |
long double | minim 64 de biți | dependent de implementare |
float_t(C99) | minim 32 de biți | depinde de tipul bazei |
double_t(C99) | minim 64 de biți | depinde de tipul bazei |
FLT_EVAL_METHOD | float_t | double_t |
---|---|---|
unu | float | double |
2 | double | double |
3 | long double | long double |
Deși nu există un tip special pentru șiruri în C ca atare, șirurile terminate nul sunt foarte folosite în limbaj. Șirurile ASCII sunt declarate ca o matrice de tip char, al cărui ultimul element trebuie să fie codul caracterului 0( '\0'). Este obișnuit să stocați șirurile UTF-8 în același format . Cu toate acestea, toate funcțiile care funcționează cu șiruri ASCII consideră fiecare caracter ca un octet, ceea ce limitează utilizarea funcțiilor standard atunci când se utilizează această codificare.
În ciuda utilizării pe scară largă a ideii de șiruri terminate nul și a comodității utilizării lor în unii algoritmi, acestea au câteva dezavantaje serioase.
În condițiile moderne, când performanța codului este prioritară față de consumul de memorie, poate fi mai eficient și mai ușor de utilizat structuri care conțin atât șirul în sine, cât și dimensiunea acestuia [48] , de exemplu:
struct string_t { char * str ; // pointer către șir size_t str_size ; // dimensiunea șirului }; typedef struct string_t string_t ; // nume alternativ pentru a simplifica codulO abordare alternativă de stocare a dimensiunii șirurilor cu memorie redusă ar fi să prefixezi șirul cu dimensiunea sa într-un format de dimensiune cu lungime variabilă .. O abordare similară este utilizată în tampoanele de protocol , totuși, numai în etapa transferului de date, dar nu și stocarea acestora.
Literale șirLiterale șiruri în C sunt constante constante [10] . La declarare, acestea sunt incluse între ghilimele duble, iar terminatorul este 0adăugat automat de către compilator. Există două moduri de a atribui un șir literal: prin indicator și prin valoare. Când se atribuie prin pointer, un char *pointer către un șir imuabil este introdus în variabila de tip, adică se formează un șir constant. Dacă introduceți un șir literal într-o matrice, atunci șirul este copiat în zona stivei.
#include <stdio.h> #include <șir.h> int main ( void ) { const char * s1 = "Șir constant" ; char s2 [] = "Șir care poate fi schimbat" ; memcpy ( s2 , "c" , strlen ( "c" )); // schimbă prima literă în mică pune ( s2 ); // textul liniei va fi afișat memcpy (( char * ) s1 , "la" , strlen ( "la" )); // eroare de segmentare pune ( s1 ); // linia nu va fi executată }Deoarece șirurile sunt matrice obișnuite de caractere, inițializatorii pot fi utilizați în loc de literale, atâta timp cât fiecare caracter se încadrează într-un octet:
char s [] = { 'I' , 'n' , 'i' , 't' , 'i' , 'a' , 'l' , 'i' , 'z' , 'e' , 'r' , „\0” };Cu toate acestea, în practică, această abordare are sens numai în cazuri extrem de rare când este necesar să nu se adauge un zero final la un șir ASCII.
Linii largiPlatformă | Codificare |
---|---|
GNU/Linux | USC-4 [49] |
macOS | |
Windows | USC-2 [50] |
AIX | |
FreeBSD | Depinde de locație
nedocumentat [50] |
Solaris |
O alternativă la șirurile obișnuite sunt șirurile largi, în care fiecare caracter este stocat într-un tip special wchar_t. Tipul dat de standard ar trebui să fie capabil să conţină în sine toate caracterele celor mai mari dintre locaţiile existente . Funcțiile pentru lucrul cu șiruri largi sunt descrise în fișierul antet wchar.h, iar funcțiile pentru lucrul cu caractere largi sunt descrise în fișierul antet wctype.h.
Când se declară literale de șir pentru șiruri largi, modificatorul este utilizat L:
const wchar_t * wide_str = L "Șir lat" ;Ieșirea formatată folosește specificatorul %ls, dar specificatorul de dimensiune, dacă este dat, este specificat în octeți, nu în caractere [51] .
Tipul wchar_ta fost conceput astfel încât orice caracter să se potrivească în el și șiruri largi - pentru a stoca șiruri de orice locație, dar, ca rezultat, API-ul s-a dovedit a fi incomod, iar implementările au fost dependente de platformă. Deci, pe platforma Windows , 16 biți au fost aleși ca dimensiune a tipului wchar_t, iar mai târziu a apărut standardul UTF-32, astfel încât tipul wchar_tde pe platforma Windows nu mai poate încadra toate caracterele din codificarea UTF-32, în urma căreia se pierde sensul acestui tip [ 50] . În același timp, pe platformele Linux [49] și macOS, acest tip are 32 de biți, deci tipul nu este potrivit pentru implementarea sarcinilor multiplatformă .wchar_t
Șiruri multioctețiExistă multe codificări diferite în care un singur caracter poate fi programat cu un număr diferit de octeți. Astfel de codificări sunt numite multiocteți. UTF-8 se aplică și acestora . C are un set de funcții pentru conversia șirurilor de caractere de la multiocteți în localitatea curentă la wide și invers. Funcțiile pentru lucrul cu caractere multiocteți au un prefix sau sufix mbși sunt descrise în fișierul antet stdlib.h. Pentru a accepta șiruri de mai mulți octeți în programele C, astfel de șiruri trebuie să fie acceptate la nivelul local actual . Pentru a seta în mod explicit codificarea, puteți modifica localitatea curentă folosind o funcție setlocale()din locale.h. Cu toate acestea, specificarea unei codificări pentru un local trebuie să fie acceptată de biblioteca standard utilizată. De exemplu, biblioteca standard Glibc acceptă pe deplin codificarea UTF-8 și este capabilă să convertească textul în multe alte codificări [52] .
Începând cu standardul C11, limbajul acceptă, de asemenea, șiruri multiocteți late de 16 și 32 de biți cu tipuri de caractere adecvate char16_tși char32_tdintr-un fișier antet uchar.h, precum și declararea literalelor de șir UTF-8 folosind u8. Șirurile de 16 și 32 de biți pot fi utilizate pentru a stoca codificări UTF-16 și UTF-32 dacă uchar.hdefinițiile macro __STDC_UTF_16__și __STDC_UTF_32__, respectiv, sunt specificate în fișierul antet. Pentru a specifica literali de șir în aceste formate, se folosesc modificatori: upentru șiruri de 16 biți și Upentru șiruri de 32 de biți. Exemple de declarare a literalelor de șir pentru șiruri de caractere multiocteți:
const char * s8 = u8 "UTF-8 multibyte string" ; const char16_t * s16 = u "șir multiocteți pe 16 biți" ; const char32_t * s32 = U "șir multiocteți pe 32 de biți" ;Rețineți că funcția c16rtomb()de conversie dintr-un șir de 16 biți într-un șir multiocteți nu funcționează așa cum s-a intenționat, iar în standardul C11 sa constatat că nu poate să se traducă din UTF-16 în UTF-8 [53] . Corectarea acestei funcții poate depinde de implementarea specifică a compilatorului.
Enumerările sunt un set de constante numere întregi și sunt notate cu cuvântul cheie enum. Dacă o constantă nu este asociată cu un număr, atunci este setată automat fie 0pentru prima constantă din listă, fie pentru un număr cu unu mai mare decât cel specificat în constanta anterioară. În acest caz, tipul de date de enumerare însuși, de fapt, poate corespunde oricărui tip primitiv semnat sau nesemnat, în intervalul căruia se potrivesc toate valorile de enumerare; Compilatorul decide ce tip să folosească. Cu toate acestea, valorile explicite pentru constante trebuie să fie expresii ca int[18] .
Un tip de enumerare poate fi, de asemenea, anonim dacă numele enumerației nu este specificat. Constantele specificate în două enumerari diferite sunt de două tipuri de date diferite, indiferent dacă enumerările sunt denumite sau anonime.
În practică, enumerările sunt adesea folosite pentru a indica stări ale automatelor finite , pentru a seta opțiuni pentru moduri de operare sau valori ale parametrilor [54] , pentru a crea constante întregi și, de asemenea, pentru a enumera orice obiecte sau proprietăți unice [55] .
StructuriStructurile sunt o combinație de variabile de diferite tipuri de date în cadrul aceleiași zone de memorie; notat cu cuvântul cheie struct. Variabilele dintr-o structură se numesc câmpuri ale structurii. Din punct de vedere al spațiului de adrese, câmpurile se succed întotdeauna în aceeași ordine în care sunt specificate, dar compilatorii pot alinia adresele câmpurilor pentru a optimiza pentru o anumită arhitectură. Astfel, de fapt, câmpul poate avea o dimensiune mai mare decât cea specificată în program.
Fiecare câmp are un anumit offset față de adresa structurii și o dimensiune. Offset-ul poate fi obținut folosind o macrocomandă offsetof()din fișierul antet stddef.h. În acest caz, offset-ul va depinde de alinierea și dimensiunea câmpurilor anterioare. Mărimea câmpului este de obicei determinată de alinierea structurii: dacă dimensiunea de aliniere a tipului de date din câmp este mai mică decât valoarea de aliniere a structurii, atunci dimensiunea câmpului este determinată de alinierea structurii. Alinierea tipului de date poate fi obținută folosind macro-ul alignof()[f] din fișierul antet stdalign.h. Dimensiunea structurii în sine este dimensiunea totală a tuturor câmpurilor sale, inclusiv alinierea. În același timp, unele compilatoare oferă atribute speciale care vă permit să împachetați structuri, eliminând aliniamentele din ele [56] .
Câmpurile de structură pot fi setate în mod explicit la dimensiunea în biți separați prin două puncte după definiția câmpului și numărul de biți, ceea ce limitează intervalul de valori posibile, indiferent de tipul câmpului. Această abordare poate fi folosită ca alternativă la steaguri și măști de biți pentru a le accesa. Cu toate acestea, specificarea numărului de biți nu anulează posibila aliniere a câmpurilor structurilor din memorie. Lucrul cu câmpuri de biți are o serie de limitări: este imposibil să le aplicați un operator sizeofsau o macrocomandă alignof(), este imposibil să obțineți un pointer către ele.
AsociațiiUniunile sunt necesare atunci când doriți să faceți referire la aceeași variabilă ca diferite tipuri de date; notat cu cuvântul cheie union. Un număr arbitrar de câmpuri care se intersectează pot fi declarate în interiorul uniunii, care de fapt oferă acces la aceeași zonă de memorie ca diferite tipuri de date. Mărimea uniunii este aleasă de compilator pe baza mărimii celui mai mare câmp din uniune. Trebuie avut în vedere faptul că schimbarea unui câmp al uniunii duce la o schimbare în toate celelalte domenii, dar numai valoarea câmpului care s-a modificat este garantată a fi corectă.
Uniunile pot servi ca o alternativă mai convenabilă la turnarea unui pointer către un tip arbitrar. De exemplu, folosind o uniune plasată într-o structură, puteți crea obiecte cu un tip de date care se schimbă dinamic:
Cod de structură pentru schimbarea tipului de date din mers #include <stddef.h> enumerare value_type_t { VALUE_TYPE_LONG , // întreg VALUE_TYPE_DOUBLE , // număr real VALUE_TYPE_STRING , // șir VALUE_TYPE_BINARY , // date arbitrare }; struct binary_t { void * date ; // pointer către date size_t data_size ; // dimensiunea datelor }; struct string_t { char * str ; // pointer către șir size_t str_size ; // dimensiunea șirului }; union value_contents_t { long as_long ; // valoare ca un întreg dublu ca_dublu ; // valoare ca număr real struct string_t as_string ; // valoare ca șir struct binary_t as_binary ; // valoare ca date arbitrare }; struct value_t { enumerare value_type_t type ; // tipul valorii unire value_contents_t contents ; // valorează conținutul }; ArraysMatricele din C sunt primitive și sunt doar o abstractizare sintactică asupra aritmeticii pointerului . O matrice în sine este un pointer către o zonă de memorie, astfel încât toate informațiile despre dimensiunea matricei și limitele acesteia pot fi accesate numai în momentul compilării conform declarației de tip. Matricele pot fi fie unidimensionale, fie multidimensionale, dar accesarea unui element de matrice se reduce la simpla calculare a offset-ului față de adresa de la începutul matricei. Deoarece tablourile se bazează pe aritmetica adresei, este posibil să se lucreze cu ele fără a folosi indici [57] . Deci, de exemplu, următoarele două exemple de citire a 10 numere din fluxul de intrare sunt identice unul cu celălalt:
Compararea muncii prin indici cu munca prin aritmetica adreseiExemplu de cod pentru lucrul prin indexuri | Exemplu de cod pentru lucrul cu aritmetica adresei |
---|---|
#include <stdio.h> int a [ 10 ] = { 0 }; // Zero initialization unsigned int count = sizeof ( a ) / sizeof ( a [ 0 ]); pentru ( int i = 0 ; i < count ; ++ i ) { int * ptr = &a [ i ]; // Pointer către elementul de matrice curent int n = scanf ( "%8d" , ptr ); dacă ( n != 1 ) { perror ( "Eșuat la citirea valorii" ); // Gestionarea pauzei de eroare ; } } | #include <stdio.h> int a [ 10 ] = { 0 }; // Zero initialization unsigned int count = sizeof ( a ) / sizeof ( a [ 0 ]); int * a_end = a + count ; // Indicator la elementul care urmează ultimul for ( int * ptr = a ; ptr != a_end ; ++ ptr ) { int n = scanf ( "%8d" , ptr ); dacă ( n != 1 ) { perror ( "Eșuat la citirea valorii" ); // Gestionarea pauzei de eroare ; } } |
Lungimea tablourilor cu o dimensiune cunoscută este calculată în momentul compilării. Standardul C99 a introdus capacitatea de a declara matrice de lungime variabilă, a căror lungime poate fi setată în timpul execuției. Astfel de matrice li se alocă memorie din zona stivei, așa că trebuie folosite cu grijă dacă dimensiunea lor poate fi setată din afara programului. Spre deosebire de alocarea dinamică a memoriei, depășirea dimensiunii permise în zona stivei poate duce la consecințe imprevizibile, iar o lungime negativă a matricei este un comportament nedefinit . Începând cu C11 , tablourile de lungime variabilă sunt opționale pentru compilatoare, iar lipsa suportului este determinată de prezența unei macro-uri __STDC_NO_VLA__[58] .
Matricele de dimensiune fixă declarate ca variabile locale sau globale pot fi inițializate dându-le o valoare inițială folosind acolade și listând elementele de matrice separate prin virgule. Inițializatoarele de matrice globale pot folosi numai expresii care sunt evaluate în timpul compilării [59] . Variabilele folosite în astfel de expresii trebuie declarate ca constante, cu modificatorul const. Pentru matricele locale, inițializatoarele pot conține expresii cu apeluri de funcție și utilizarea altor variabile, inclusiv un pointer către matricea declarată în sine.
Începând cu standardul C99, este permis să se declare o matrice de lungime arbitrară ca ultimul element al structurilor, care este utilizat pe scară largă în practică și susținut de diverși compilatori. Mărimea unei astfel de matrice depinde de cantitatea de memorie alocată pentru structură. În acest caz, nu puteți declara o serie de astfel de structuri și nu le puteți plasa în alte structuri. În operațiunile pe o astfel de structură, o matrice de lungime arbitrară este de obicei ignorată, inclusiv atunci când se calculează dimensiunea structurii, iar depășirea matricei implică un comportament nedefinit [60] .
Limbajul C nu oferă niciun control asupra matricei în afara limitelor, așa că programatorul însuși trebuie să monitorizeze lucrul cu matrice. Erorile în procesarea matricei nu afectează întotdeauna direct execuția programului, dar pot duce la erori de segmentare și vulnerabilități .
Sinonime de tipLimbajul C vă permite să vă creați propriile nume de tip cu typedef. Nume alternative pot fi date atât pentru tipurile de sistem, cât și pentru cele definite de utilizator. Astfel de nume sunt declarate în spațiul de nume global și nu intră în conflict cu numele de structură, enumerare și uniuni.
Numele alternative pot fi folosite atât pentru a simplifica codul, cât și pentru a crea niveluri de abstractizare. De exemplu, unele tipuri de sistem pot fi scurtate pentru a face codul mai lizibil sau pentru a-l uniformiza în codul utilizatorului:
#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 ;Un exemplu de abstractizare sunt numele tipurilor din fișierele antet ale sistemelor de operare. De exemplu, standardul POSIX definește un tip pid_tpentru stocarea unui ID de proces numeric. De fapt, acest tip este un nume alternativ pentru un tip primitiv, de exemplu:
typedef int __kernel_pid_t ; typedef __kernel_pid_t __pid_t typedef __pid_t pid_t ;Deoarece tipurile cu nume alternative sunt doar sinonime pentru tipurile originale, se păstrează compatibilitatea deplină și interschimbabilitatea între ele.
Preprocesorul funcționează înainte de compilare și transformă textul fișierului program conform directivelor întâlnite în acesta sau transmise preprocesorului . Din punct de vedere tehnic, preprocesorul poate fi implementat în moduri diferite, dar este convenabil din punct de vedere logic să ne gândim la el ca la un modul separat care procesează fiecare fișier destinat compilarii și formează textul care apoi intră în intrarea compilatorului. Preprocesorul caută linii în text care încep cu un caracter #, urmate de directivele preprocesorului. Tot ceea ce nu aparține directivelor de preprocesor și nu este exclus de la compilare conform directivelor este trecut la intrarea compilatorului neschimbat.
Caracteristicile preprocesorului includ:
Este important de înțeles că preprocesorul oferă doar substituție de text, fără a ține cont de sintaxa și semantica limbajului. Deci, de exemplu, definițiile macro #definepot apărea în interiorul funcțiilor sau definițiilor de tip, iar directivele de compilare condiționată pot duce la excluderea oricărei părți a codului din textul compilat al programului, fără a ține cont de gramatica limbajului. Apelarea unei macrocomenzi parametrice este, de asemenea, diferită de apelarea unei funcții, deoarece semantica argumentelor separate prin virgulă nu este analizată. Deci, de exemplu, este imposibil să treceți inițializarea unui tablou la argumentele unei macrocomenzi parametrice, deoarece elementele sale sunt, de asemenea, separate prin virgulă:
#define array_of(tip, matrice) (((tip) []) (matrice)) int * a ; a = array_of ( int , { 1 , 2 , 3 }); // eroare de compilare: // macrocomanda „array_of” a transmis 4 argumente, dar este nevoie de doar 2Definițiile macro sunt adesea folosite pentru a asigura compatibilitatea cu diferite versiuni de biblioteci care au modificat API -urile , inclusiv anumite secțiuni de cod, în funcție de versiunea bibliotecii. În aceste scopuri, bibliotecile oferă adesea definiții macro care descriu versiunea lor [61] , iar uneori macro-uri cu parametri pentru a compara versiunea curentă cu cea specificată în preprocesor [62] . Definițiile macro sunt, de asemenea, folosite pentru compilarea condiționată a părților individuale ale programului, de exemplu, pentru a permite suportul pentru unele funcționalități suplimentare.
Definițiile macro cu parametri sunt utilizate pe scară largă în programele C pentru a crea analogi ale funcțiilor generice . Anterior, acestea erau folosite și pentru a implementa funcții inline, dar de la standardul C99, această nevoie a fost eliminată datorită adăugării de inlinefuncții -. Totuși, datorită faptului că definițiile macro cu parametri nu sunt funcții, ci sunt numite într-un mod similar, pot apărea probleme neașteptate din cauza erorii programatorului, inclusiv procesarea doar a unei părți a codului din definiția macro [63] și priorități incorecte pentru efectuarea de operații [64] . Un exemplu de cod eronat este macrocomanda de pătrat:
#include <stdio.h> int main ( void ) { #define SQR(x) x * x printf ( "%d" , SQR ( 5 )); // totul este corect, 5*5=25 printf ( "%d" , SQR ( 5 + 0 )); // ar trebui să fie 25, dar va scoate 5 (5+0*5+0) printf ( "%d" , SQR ( 4 / 3 )); // totul este corect, 1 (pentru că 4/3=1, 1*4=4, 4/3=1) printf ( "%d" , SQR ( 5 / 2 )); // ar trebui să fie 4 (2*2), dar va scoate 5 (5/2*5/2) returnează 0 ; }În exemplul de mai sus, eroarea este că conținutul argumentului macro este înlocuit în text așa cum este, fără a ține cont de precedența operațiilor. În astfel de cazuri, trebuie să utilizați inlinefuncții - sau să prioritizați în mod explicit operatorii în expresiile care folosesc parametri macro folosind paranteze:
#include <stdio.h> int main ( void ) { #define SQR(x) ((x) * (x)) printf ( "%d" , SQR ( 4 + 1 )); // adevărat, 25 returnează 0 ; }Un program este un set de fișiere C care pot fi compilate în fișiere obiect . Fișierele obiect trec apoi printr- un pas de conectare între ele, precum și cu biblioteci externe, rezultând executabilul final sau biblioteca . Conectarea fișierelor între ele, precum și cu bibliotecile, necesită o descriere a prototipurilor funcțiilor utilizate, a variabilelor externe și a tipurilor de date necesare în fiecare fișier. Este obișnuit să puneți astfel de date în fișiere antet separate , care sunt conectate folosind o directivă #include în acele fișiere în care este necesară această sau atare funcționalitate și vă permit să organizați un sistem similar cu un sistem de module. În acest caz, modulul poate fi:
Deoarece directiva #includeînlocuiește doar textul unui alt fișier în stadiul de preprocesor , inclusiv același fișier de mai multe ori poate duce la erori de compilare. Prin urmare, astfel de fișiere folosesc protecție împotriva reactivării folosind macrocomenzi #define și #ifndef[65] .
Fișiere cod sursăCorpul unui fișier de cod sursă C constă dintr-un set de definiții, tipuri și funcții de date globale. Variabilele și funcțiile globale declarate cu specificatorii și staticsunt inlinedisponibile numai în fișierul în care sunt declarate sau când un fișier este inclus în altul prin intermediul fișierului #include. În acest caz, funcțiile și variabilele declarate în fișierul antet cu cuvântul staticvor fi create din nou de fiecare dată când fișierul antet este conectat la următorul fișier cu codul sursă. Variabilele globale și prototipurile de funcție declarate cu specificatorul extern sunt considerate incluse din alte fișiere. Adică, acestea au voie să fie utilizate în conformitate cu descrierea; se presupune că, după ce programul este construit, acestea vor fi legate de către linker cu obiectele și funcțiile originale descrise în fișierele lor.
Variabilele și funcțiile globale, cu excepția staticși inline, pot fi accesate din alte fișiere cu condiția să fie declarate corespunzător acolo cu specificatorul extern. Variabilele și funcțiile declarate cu modificatorul staticpot fi accesate și în alte fișiere, dar numai atunci când adresa lor este trecută de pointer. Tastați declarații typedefși nu pot fi importate în alte fișiere struct. unionDacă este necesar să le folosiți în alte fișiere, acestea ar trebui să fie duplicate acolo sau plasate într-un fișier antet separat. Același lucru este valabil și pentru inlinefuncțiile -.
Punct de intrare în programPentru un program executabil, punctul de intrare standard este o funcție numită main, care nu poate fi statică și trebuie să fie singura din program. Execuția programului începe de la prima instrucțiune a funcției main()și continuă până la ieșire, după care programul se încheie și returnează sistemului de operare un cod întreg abstract al rezultatului muncii sale.
fara argumente | Cu argumente de linie de comandă |
---|---|
int main ( void ); | int main ( int argc , char ** argv ); |
Când este apelată, variabilei i argcse transmite numărul de argumente transmise programului, inclusiv calea către programul în sine, astfel încât variabila argc conține de obicei o valoare nu mai mică de 1. Linia argvde lansare a programului în sine este transmisă variabilei ca un tablou. de șiruri de text, ultimul element al cărora este NULL. Compilatorul garantează că main()toate variabilele globale din program vor fi inițializate atunci când funcția este rulată [67] .
Ca urmare, funcția main()poate returna orice număr întreg din intervalul de valori de tip int, care va fi transmis sistemului de operare sau altui mediu ca cod de returnare al programului [66] . Standardul lingvistic nu definește semnificația codurilor de returnare [68] . De obicei, sistemul de operare în care rulează programele are unele mijloace pentru a obține valoarea codului de returnare și a-l analiza. Uneori există anumite convenții cu privire la semnificațiile acestor coduri. Convenția generală este că un cod returnat de zero indică finalizarea cu succes a programului, în timp ce o valoare diferită de zero reprezintă un cod de eroare. Fișierul antet stdlib.hdefinește două definiții macro generale EXIT_SUCCESSși EXIT_FAILURE, care corespund finalizării cu succes și nereușite a programului [68] . Codurile de returnare pot fi utilizate și în cadrul aplicațiilor care includ mai multe procese pentru a asigura comunicarea între aceste procese, caz în care aplicația însăși determină semnificația semantică pentru fiecare cod de returnare.
C oferă 4 moduri de alocare a memoriei, care determină durata de viață a unei variabile și momentul inițializării acesteia [67] .
Metoda de selecție | Ținte | Timp de selecție | timpul de eliberare | Cheltuieli generale |
---|---|---|---|---|
Alocarea memoriei statice | Variabile globale și variabile marcate cu cuvânt cheie static(dar fără _Thread_local) | La pornirea programului | La finalul programului | Dispărut |
Alocarea memoriei la nivel de fir | Variabile marcate cu cuvânt cheie_Thread_local | Când începe firul | La capătul pârâului | La crearea unui fir |
Alocarea automată a memoriei | Argumente ale funcției și valori returnate, variabile locale ale funcțiilor, inclusiv registre și matrice de lungime variabilă | Când apelați funcții la nivel de stivă . | Automat la finalizarea funcțiilor | Nesemnificativ, deoarece se schimbă doar indicatorul către partea de sus a stivei |
Alocarea dinamică a memoriei | Memoria alocată prin funcții malloc(), calloc()șirealloc() | Manual din heap în momentul apelării funcției utilizate. | Utilizarea manuală a funcțieifree() | Mare atât pentru alocare, cât și pentru eliberare |
Toate aceste metode de stocare a datelor sunt potrivite în diferite situații și au propriile avantaje și dezavantaje. Variabilele globale nu vă permit să scrieți algoritmi de reintrare , iar alocarea automată a memoriei nu vă permite să returnați o zonă arbitrară de memorie dintr-un apel de funcție. Auto-alocarea nu este, de asemenea, potrivită pentru alocarea unor cantități mari de memorie, deoarece poate duce la coruperea stivei sau a heap -ului [69] . Memoria dinamică nu are aceste neajunsuri, dar are o suprasarcină mare atunci când o folosește și este mai dificil de utilizat.
Acolo unde este posibil, se preferă alocarea automată sau statică de memorie: acest mod de stocare a obiectelor este controlat de compilator , ceea ce scutește programatorul de necazul alocării și eliberării manuale a memoriei, care este de obicei sursa pierderilor de memorie greu de găsit, erori de segmentare și erori de reeliberare în program . Din păcate, multe structuri de date au dimensiuni variabile în timpul execuției, așa că, deoarece zonele alocate automat și static trebuie să aibă o dimensiune fixă cunoscută în momentul compilării, este foarte comun să se utilizeze alocarea dinamică.
Pentru variabilele auto-alocate, un modificator registerpoate fi folosit pentru a sugera compilatorului să le acceseze rapid. Astfel de variabile pot fi plasate în registrele procesorului. Datorită numărului limitat de registre și posibilelor optimizări ale compilatorului, variabilele pot ajunge în memoria obișnuită, dar cu toate acestea nu va fi posibil să obțineți un pointer către ele din program [70] . Modificatorul registereste singurul care poate fi specificat în argumentele funcției [71] .
Adresare memorieLimbajul C a moștenit adresarea liniară a memoriei atunci când lucrați cu structuri, matrice și zone de memorie alocate. Standardul de limbaj permite, de asemenea, să fie efectuate operațiuni de comparare pe pointerii nuli și pe adrese din matrice, structuri și zone de memorie alocate. De asemenea, este permis să se lucreze cu adresa elementului de matrice care urmează ultimului, ceea ce se face pentru a facilita scrierea algoritmilor. Totuși, nu ar trebui efectuată compararea indicatorilor de adresă obținuți pentru diferite variabile (sau zone de memorie), deoarece rezultatul va depinde de implementarea unui anumit compilator [72] .
Reprezentarea memorieiReprezentarea în memorie a unui program depinde de arhitectura hardware, de sistemul de operare și de compilator. Deci, de exemplu, pe majoritatea arhitecturilor stiva crește în jos, dar există arhitecturi în care stiva crește [73] . Limita dintre stivă și heap poate fi parțial protejată de depășirea stivei printr-o zonă de memorie specială [74] . Iar locația datelor și codul bibliotecilor poate depinde de opțiunile de compilare [75] . Standardul C face abstracție de la implementare și vă permite să scrieți cod portabil, dar înțelegerea structurii de memorie a unui proces ajută la depanarea și scrierea aplicațiilor sigure și tolerante la erori.
Reprezentare tipică a memoriei de proces în sisteme de operare asemănătoare UnixCând un program este lansat dintr-un fișier executabil, instrucțiunile procesorului (codul mașină) și datele inițializate sunt importate în RAM. main()În același timp, argumentele din linia de comandă (disponibile în funcțiile cu următoarea semnătură în al doilea argument int argc, char ** argv) și variabilele de mediu sunt importate în adrese superioare .
Zona de date neinițializate conține variabile globale (inclusiv cele declarate ca static) care nu au fost inițializate în codul programului. Astfel de variabile sunt inițializate implicit la zero după pornirea programului. Zona de date inițiale - segmentul de date - conține și variabile globale, dar această zonă include acele variabile cărora li s-a dat o valoare inițială. Datele imuabile, inclusiv variabilele declarate cu modificatorul const, literali șir și alte literale compuse, sunt plasate în segmentul de text al programului. Segmentul de text al programului conține, de asemenea, cod executabil și este doar pentru citire, așa că o încercare de a modifica datele din acest segment va avea ca rezultat un comportament nedefinit sub forma unei erori de segmentare .
Zona stivei este destinată să conțină date asociate apelurilor de funcții și variabilelor locale. Înainte de fiecare execuție a funcției, stiva este extinsă pentru a găzdui argumentele transmise funcției. În timpul activității sale, funcția poate aloca variabile locale pe stivă și poate aloca memorie pe acesta pentru matrice de lungime variabilă, iar unele compilatoare oferă, de asemenea, mijloace de alocare a memoriei în stivă printr-un apel alloca()care nu este inclus în standardul limbajului. . După ce funcția se termină, stiva este redusă la valoarea care era înainte de apel, dar acest lucru s-ar putea să nu se întâmple dacă stiva este gestionată incorect. Memoria alocată dinamic este furnizată din heap .
Un detaliu important este prezența padding-ului aleatoriu între stivă și zona de sus [77] , precum și între zona de date inițializată și heap . Acest lucru se face din motive de securitate, cum ar fi prevenirea stivuirii altor funcții.
Bibliotecile de legături dinamice și mapările de fișiere ale sistemului de fișiere se află între stivă și heap [78] .
C nu are încorporate mecanisme de control al erorilor, dar există mai multe modalități general acceptate de a gestiona erorile folosind limbajul. În general, practica de manipulare a erorilor C în codul tolerant la erori forțează să scrie construcții greoaie, adesea repetitive, în care algoritmul este combinat cu gestionarea erorilor .
Marcatori de eroare și errnoLimbajul C folosește în mod activ o variabilă specială errnodin fișierul antet errno.h, în care funcțiile introduc codul de eroare, în timp ce returnează o valoare care este marcatorul de eroare. Pentru a verifica rezultatul pentru erori, rezultatul este comparat cu marcatorul de eroare și, dacă se potrivesc, atunci puteți analiza codul de eroare stocat errnopentru a corecta programul sau pentru a afișa un mesaj de depanare. În biblioteca standard, standardul definește adesea doar marcatorii de eroare returnați, iar setarea errnoeste dependentă de implementare [79] .
Următoarele valori acționează de obicei ca marcatori de eroare:
Practica returnării unui marker de eroare în locul unui cod de eroare, deși salvează numărul de argumente transmise funcției, în unele cazuri duce la erori ca urmare a unui factor uman. De exemplu, este obișnuit ca programatorii să ignore verificarea unui rezultat de tip ssize_t, iar rezultatul în sine este folosit în continuare în calcule, ducând la erori subtile dacă este returnat -1[82] .
Returnarea valorii corecte ca marcator de eroare [82] contribuie și mai mult la apariția erorilor , ceea ce obligă programatorul să facă mai multe verificări și, în consecință, să scrie mai multe din același tip de cod repetitiv. Această abordare este practicată în funcțiile de flux care lucrează cu obiecte de tip FILE *: marcatorul de eroare este valoarea EOF, care este și marcatorul de sfârșit de fișier. Prin urmare, EOFuneori trebuie să verificați fluxul de caractere atât pentru sfârșitul fișierului folosind funcția feof(), cât și pentru prezența unei erori folosind ferror()[83] . În același timp, unele funcții care pot reveni EOFconform standardului nu sunt necesare pentru a seta errno[79] .
Lipsa unei practici unificate de gestionare a erorilor în biblioteca standard duce la apariția unor metode personalizate de gestionare a erorilor și la combinarea metodelor utilizate în mod obișnuit în proiectele terțe. De exemplu, în proiectul systemd , ideile de a returna un cod de eroare și un număr -1ca marcator au fost combinate - este returnat un cod de eroare negativ [84] . Iar biblioteca GLib a introdus practica returnării unei valori booleene ca marker de eroare , în timp ce detaliile erorii sunt plasate într-o structură specială, pointerul către care este returnat prin ultimul argument al funcției [85] . O soluție similară este folosită de proiectul Enlightenment , care folosește și un tip boolean ca marker, dar returnează informații de eroare similare bibliotecii standard - printr-o funcție separată [86] care trebuie verificată dacă a fost returnat un marker.
Returnarea unui cod de eroareO alternativă la marcatorii de eroare este să returnați direct codul de eroare și să returnați rezultatul funcției prin argumente pointer. Dezvoltatorii standardului POSIX au luat această cale, în funcțiile căreia se obișnuiește să returneze un cod de eroare ca număr de tip int. Cu toate acestea, returnarea unei valori de tip intnu clarifică în mod explicit faptul că este returnat codul de eroare și nu simbolul, care poate duce la erori dacă rezultatul unor astfel de funcții este verificat cu valoarea -1. Extensia K a standardului C11 introduce un tip special errno_tpentru stocarea unui cod de eroare. Există recomandări pentru a utiliza acest tip în codul utilizatorului pentru a returna erori, iar dacă nu este furnizat de biblioteca standard, atunci declarați-l singur [87] :
#ifndef __STDC_LIB_EXT1__ typedef int errno_t ; #endifAceastă abordare, pe lângă îmbunătățirea calității codului, elimină necesitatea de a utiliza errno, ceea ce vă permite să faceți biblioteci cu funcții reintrante fără a fi nevoie să includeți biblioteci suplimentare, cum ar fi POSIX Threads , pentru a defini corect errno.
Erori în funcțiile matematiceMai complexă este tratarea erorilor în funcțiile matematice din fișierul antet math.h, în care pot apărea 3 tipuri de erori [88] :
Prevenirea a două dintre cele trei tipuri de erori se reduce la verificarea datelor de intrare pentru intervalul de valori valide. Cu toate acestea, este extrem de dificil să se prezică rezultatul dincolo de limitele tipului. Prin urmare, standardul de limbaj prevede posibilitatea analizării funcțiilor matematice pentru erori. Începând cu standardul C99, această analiză este posibilă în două moduri, în funcție de valoarea stocată în math_errhandling.
În acest caz, metoda de tratare a erorilor este determinată de implementarea specifică a bibliotecii standard și poate fi complet absentă. Prin urmare, în codul independent de platformă, poate fi necesar să se verifice rezultatul în două moduri simultan, în funcție de valoarea lui math_errhandling[88] .
Eliberarea resurselorDe obicei, apariția unei erori necesită ca funcția să iasă și să returneze un indicator de eroare. Dacă într-o funcție poate apărea o eroare în diferite părți ale acesteia, este necesară eliberarea resurselor alocate în timpul funcționării acesteia pentru a preveni scurgerile. Este o bună practică să eliberați resursele în ordine inversă înainte de a reveni din funcție, iar în caz de erori, în ordine inversă după cea principală return. În părți separate ale unei astfel de lansări, puteți sări folosind operatorul goto[89] . Această abordare vă permite să mutați secțiuni de cod care nu au legătură cu algoritmul implementat în afara algoritmului în sine, crescând lizibilitatea codului și este similară cu munca unui operator deferdin limbajul de programare Go . Un exemplu de eliberare a resurselor este dat mai jos, în secțiunea de exemple .
Pentru a elibera resurse în cadrul programului, este furnizat un mecanism de gestionare a ieșirii programului. Handlerii sunt alocați folosind o funcție atexit()și sunt executați atât la sfârșitul funcției main()printr-o instrucțiune return, cât și la executarea funcției exit(). În acest caz, handlerele nu sunt executate de funcțiile abort()și _Exit()[90] .
Un exemplu de eliberare a resurselor la sfârșitul unui program este eliberarea memoriei alocate pentru variabilele globale. În ciuda faptului că memoria este eliberată într-un fel sau altul după terminarea programului de către sistemul de operare și este permisă să nu se elibereze memoria necesară pe tot parcursul funcționării programului [91] , dealocarea explicită este de preferat, deoarece o face mai ușor de găsit scurgeri de memorie de către instrumente terțe și reduce șansele de scurgeri de memorie ca urmare a unei erori:
Exemplu de cod de program cu eliberarea resurselor #include <stdio.h> #include <stdlib.h> int numere_număr ; int * numere ; void numere_libere ( vod ) { liber ( numere ); } int main ( int argc , char ** argv ) { dacă ( arg < 2 ) { ieșire ( EXIT_FAILURE ); } numere_număr = atoi ( argv [ 1 ]); if ( numere_număr <= 0 ) { ieșire ( EXIT_FAILURE ); } numere = calloc ( numere_număr , dimensiunea ( * numere )); dacă ( ! numere ) { perror ( "Eroare la alocarea memoriei pentru matrice" ); ieșire ( EXIT_FAILURE ); } atexit ( numere_libere ); // ... lucrează cu matrice de numere // Managerul free_numbers() va fi apelat automat aici returnează EXIT_SUCCESS ; }Dezavantajul acestei abordări este că formatul handlerelor atribuibile nu prevede transmiterea de date arbitrare către funcție, ceea ce vă permite să creați handlere numai pentru variabile globale.
Un program C minim care nu necesită procesarea argumentelor este următorul:
int main ( void ){}Este permis să nu scrieți un operator returnpentru funcție main(). În acest caz, conform standardului, funcția main()returnează 0, executând toți manipulatorii alocați funcției exit(). Aceasta presupune că programul a fost finalizat cu succes [40] .
Salut Lume!Salut , lume! este prezentată în prima ediție a cărții „ The C Programming Language ” de Kernighan și Ritchie:
#include <stdio.h> int main ( void ) // Nu ia argumente { printf ( "Bună, lume! \n " ); // '\n' - linia nouă returnează 0 ; // Încheierea cu succes a programului }Acest program imprimă mesajul Salut, lume! ' la ieşire standard .
Gestionarea erorilor folosind citirea fișierelor ca exempluMulte funcții C pot returna o eroare fără a face ceea ce trebuia să facă. Erorile trebuie verificate și răspuns corect, inclusiv deseori nevoia de a arunca o eroare de la o funcție la un nivel superior pentru analiză. În același timp, funcția în care a apărut o eroare poate fi reintrată , caz în care, din greșeală, funcția nu ar trebui să modifice datele de intrare sau de ieșire, ceea ce vă permite să o reporniți în siguranță după corectarea situației de eroare.
Exemplul implementează funcția pentru a citi un fișier în C, dar necesită ca funcțiile fopen()și fread()standardul POSIX să se conformeze , altfel este posibil să nu seteze variabila errno, ceea ce complică foarte mult atât depanarea, cât și scrierea codului universal și sigur. Pe platformele non-POSIX, comportamentul acestui program va fi nedefinit în cazul unei erori . Dealocarea resurselor pe erori se află în spatele algoritmului principal de îmbunătățire a lizibilității, iar tranziția se face folosind [89] . goto
Cod exemplu de cititor de fișiere cu gestionarea erorilor #include <errno.h> #include <stdio.h> #include <stdlib.h> // Definiți tipul pentru a stoca codul de eroare dacă nu este definit #ifndef __STDC_LIB_EXT1__ typedef int errno_t ; #endif enumerare { EOK = 0 , // valoarea errno_t la succes }; // Funcție de citire a conținutului fișierului errno_t get_file_contents ( const char * nume de fișier , void ** contents_ptr , size_t * contents_size_ptr ) { FIȘIER * f ; f = fopen ( nume fișier , "rb" ); dacă ( ! f ) { // În POSIX, fopen() setează errno din greșeală return errno ; } // Obține dimensiunea fișierului fseek ( f , 0 , SEEK_END ); long contents_size = ftell ( f ); if ( dimensiunea_conținut == 0 ) { * contents_ptr = NULL ; * contents_size_ptr = 0 ; du-te la cleaning_fopen ; } înapoi ( f ); // Variabilă pentru a stoca codul de eroare returnat errno_t salvat_errno ; void * continut ; contents = malloc ( contents_size ); dacă ( ! conținut ) { salvat_errno = errno ; du- te la aborting_fopen ; } // Citiți întregul conținut al fișierului la indicatorul de conținut dimensiune_t n ; n = fread ( conținut , dimensiune_conținut , 1 , f ); dacă ( n == 0 ) { // Nu verificați feof() pentru că este tamponat după fseek() // POSIX fread() setează errno din greșeală salvat_errno = errno ; du- te la aborting_contents ; } // Returnează memoria alocată și dimensiunea acesteia * contents_ptr = continut ; * contents_size_ptr = contents_size ; // Secțiunea de lansare a resurselor despre succes cleaning_fopen : fclose ( f ); returnează EOK ; // Secțiune separată pentru eliberarea resurselor din greșeală aborting_contents : gratuit ( conținut ); aborting_fopen : fclose ( f ); return save_errno ; } int main ( int argc , char ** argv ) { dacă ( arg < 2 ) { returnează EXIT_FAILURE ; } const char * filename = argv [ 1 ]; errno_t errnum ; void * continut ; size_t contents_size ; errnum = get_file_contents ( nume fișier , & conținut , & dimensiune_conținut ); if ( errnum ) { charbuf [ 1024 ] ; const char * text_error = strerror_r ( errnum , buf , sizeof ( buf )); fprintf ( stderr , " %s \n " , text_eroare ); ieșire ( EXIT_FAILURE ); } printf ( "%.*s" , ( int ) contents_size , contents ); gratuit ( conținut ); returnează EXIT_SUCCESS ; }Unele compilatoare sunt incluse la pachet cu compilatoare pentru alte limbaje de programare (inclusiv C++ ) sau fac parte din mediul de dezvoltare software .
|
|
În ciuda faptului că biblioteca standard face parte din standardul limbajului, implementările sale sunt separate de compilatoare. Prin urmare, standardele de limbaj acceptate de compilator și bibliotecă pot diferi.
Deoarece limbajul C nu oferă un mijloc de scriere a codului în siguranță, iar multe elemente ale limbajului contribuie la erori, scrierea de cod de înaltă calitate și toleranță la erori poate fi garantată doar prin scrierea de teste automate. Pentru a facilita astfel de testare, există diverse implementări ale bibliotecilor de teste unitare terțe .
Există, de asemenea, multe alte sisteme pentru testarea codului C, cum ar fi AceUnit, GNU Autounit, cUnit și altele, dar ele fie nu testează în medii izolate, oferă puține caracteristici [100] , fie nu mai sunt dezvoltate.
Instrumente de depanarePrin manifestările erorilor, nu este întotdeauna posibil să se tragă o concluzie clară despre zona cu probleme din cod, cu toate acestea, diverse instrumente de depanare ajută adesea la localizarea problemei.
Uneori, pentru a porta anumite biblioteci, funcții și instrumente scrise în C într-un alt mediu, este necesar să compilați codul C într-un limbaj de nivel superior sau în codul unei mașini virtuale concepute pentru un astfel de limbaj. Următoarele proiecte sunt concepute în acest scop:
De asemenea, pentru C, există și alte instrumente care facilitează și completează dezvoltarea, inclusiv analizoare statice și utilitare pentru formatarea codului. Analiza statică ajută la identificarea potențialelor erori și vulnerabilități. Iar formatarea automată a codului simplifică organizarea colaborării în sistemele de control al versiunilor, minimizând conflictele datorate modificărilor de stil.
Limbajul este utilizat pe scară largă în dezvoltarea sistemului de operare, la nivel de API al sistemului de operare, în sistemele încorporate și pentru scrierea de coduri de înaltă performanță sau critice pentru erori. Unul dintre motivele pentru adoptarea pe scară largă a programării de nivel scăzut este capacitatea de a scrie cod multiplatformă care poate fi gestionat diferit pe diferite hardware și sisteme de operare.
Capacitatea de a scrie cod de înaltă performanță vine în detrimentul libertății complete de acțiune pentru programator și al absenței unui control strict din partea compilatorului. De exemplu, primele implementări ale Java , Python , Perl și PHP au fost scrise în C. În același timp, în multe programe, părțile cele mai solicitante de resurse sunt de obicei scrise în C. Nucleul Mathematica [109] este scris în C, în timp ce MATLAB , scris inițial în Fortran , a fost rescris în C în 1984 [110] .
C este, de asemenea, uneori folosit ca limbaj intermediar la compilarea limbilor de nivel superior. De exemplu, primele implementări ale limbajelor C++ , Objective-C și Go au funcționat conform acestui principiu - codul scris în aceste limbi a fost tradus într-o reprezentare intermediară în limbajul C. Limbile moderne care funcționează pe același principiu sunt Vala și Nim .
Un alt domeniu de aplicare a limbajului C sunt aplicațiile în timp real , care sunt solicitante în ceea ce privește capacitatea de răspuns a codului și timpul de execuție al acestuia. Astfel de aplicații trebuie să înceapă execuția acțiunilor într-un interval de timp strict limitat, iar acțiunile în sine trebuie să se încadreze într-o anumită perioadă de timp. În special, standardul POSIX.1 oferă un set de funcții și capabilități pentru construirea de aplicații în timp real [111] [112] [113] , dar suportul dur în timp real trebuie implementat și de sistemul de operare [114] .
Limbajul C a fost și rămâne unul dintre cele mai utilizate limbaje de programare de mai bine de patruzeci de ani. Desigur, influența sa poate fi urmărită într-o oarecare măsură în multe limbi ulterioare. Cu toate acestea, printre limbile care au atins o anumită distribuție, există puțini descendenți direcți ai lui C.
Unele limbaje descendente se bazează pe C cu instrumente și mecanisme suplimentare care adaugă suport pentru noi paradigme de programare ( OOP , programare funcțională , programare generică etc.). Aceste limbi includ în principal C++ și Objective-C și, indirect, descendenții lor Swift și D. De asemenea, sunt cunoscute încercări de a îmbunătăți C prin corectarea celor mai semnificative deficiențe ale sale, dar păstrând caracteristicile sale atractive. Printre acestea putem aminti limbajul de cercetare Cyclone (și descendentul său Rust ). Uneori, ambele direcții de dezvoltare sunt combinate într-o singură limbă, Go este un exemplu .
Separat, este necesar să menționăm un întreg grup de limbaje care, într-o măsură mai mare sau mai mică, au moștenit sintaxa de bază a lui C (folosirea acoladelor ca delimitatori ai blocurilor de cod, declararea variabilelor, formele caracteristice ale operatorilor for, while, if, switchcu parametrii între paranteze, operații combinate ++, --, +=, -=și altele) , motiv pentru care programele în aceste limbaje au un aspect caracteristic asociat în mod specific cu C. Acestea sunt limbaje precum Java , JavaScript , PHP , Perl , AWK , C# . De fapt, structura și semantica acestor limbaje sunt foarte diferite de C și sunt de obicei destinate aplicațiilor în care C originalul nu a fost niciodată folosit.
Limbajul de programare C++ a fost creat din C și și-a moștenit sintaxa, completând-o cu noi constructe în spiritul Simula-67, Smalltalk, Modula-2, Ada, Mesa și Clu [116] . Principalele completări au fost suportul pentru OOP (descrierea clasei, moștenirea multiplă, polimorfismul bazat pe funcții virtuale) și programarea generică (motor de șabloane). Dar, pe lângă aceasta, s-au făcut multe completări diferite la limbă. În prezent, C++ este unul dintre cele mai utilizate limbaje de programare din lume și este poziționat ca un limbaj de uz general, cu accent pe programarea sistemelor [117] .
Inițial, C++ a păstrat compatibilitatea cu C, care a fost declarat ca unul dintre avantajele noului limbaj. Primele implementări ale C++ pur și simplu au tradus noile constructe în C pur, după care codul a fost procesat de un compilator C obișnuit. Pentru a menține compatibilitatea, creatorii C++ au refuzat să excludă din acesta unele dintre caracteristicile adesea criticate ale lui C, creând în schimb mecanisme noi, „paralele”, care sunt recomandate la dezvoltarea codului C++ nou (șabloane în loc de macrocomenzi, turnare de tip explicit în loc de automată). , containere de bibliotecă standard în loc de alocarea manuală a memoriei dinamice și așa mai departe). Cu toate acestea, limbajele au evoluat independent de atunci, iar acum C și C++ ale ultimelor standarde lansate sunt doar parțial compatibile: nu există nicio garanție că un compilator C++ va compila cu succes un program C și, dacă are succes, nu există nicio garanție că programul compilat va rula corect. Deosebit de enervante sunt unele diferențe semantice subtile care pot duce la un comportament diferit al aceluiași cod, care este corect din punct de vedere sintactic pentru ambele limbi. De exemplu, constantele de caractere (caracterele cuprinse între ghilimele simple) au un tip intîn C și un tip charîn C++ , astfel încât cantitatea de memorie ocupată de astfel de constante variază de la limbă la limbă. [118] Dacă un program este sensibil la dimensiunea unei constante de caractere, se va comporta diferit atunci când este compilat cu compilatoarele C și C++.
Diferențele ca acestea fac dificilă scrierea de programe și biblioteci care pot compila și funcționa la fel atât în C, cât și în C++ , ceea ce, desigur, îi încurcă pe cei care programează în ambele limbi. Printre dezvoltatorii și utilizatorii atât C, cât și C++, există susținători ai minimizării diferențelor dintre limbi, ceea ce ar aduce în mod obiectiv beneficii tangibile. Există, totuși, un punct de vedere opus, conform căruia compatibilitatea nu este deosebit de importantă, deși este utilă, iar eforturile de reducere a incompatibilității nu ar trebui să împiedice îmbunătățirea fiecărei limbi în parte.
O altă opțiune pentru extinderea C cu instrumente bazate pe obiecte este limbajul Objective-C , creat în 1983. Subsistemul obiect a fost împrumutat de la Smalltalk și toate elementele asociate cu acest subsistem sunt implementate în propria sintaxă, care este destul de diferită de sintaxa C (până la faptul că în descrierile de clasă, sintaxa pentru declararea câmpurilor este opusă sintaxa pentru declararea variabilelor în C: mai întâi se scrie numele câmpului, apoi tipul acestuia). Spre deosebire de C++, Objective-C este un superset de C clasic, adică păstrează compatibilitatea cu limbajul sursă; un program C corect este un program Objective-C corect. O altă diferență semnificativă față de ideologia C++ este că Objective-C implementează interacțiunea obiectelor prin schimbul de mesaje cu drepturi depline, în timp ce C++ implementează conceptul de „trimiterea unui mesaj ca apel de metodă”. Procesarea completă a mesajelor este mult mai flexibilă și se potrivește firesc cu calculul paralel. Objective-C, precum și descendentul său direct Swift , sunt printre cele mai populare pe platformele acceptate de Apple .
Limbajul C este unic prin faptul că a fost primul limbaj de nivel înalt care a înlocuit serios asamblatorul în dezvoltarea software-ului de sistem . Rămâne limbajul implementat pe cel mai mare număr de platforme hardware și unul dintre cele mai populare limbaje de programare , în special în lumea software-ului liber [119] . Cu toate acestea, limbajul are multe deficiențe; de la începuturi, a fost criticat de mulți experți.
Limbajul este foarte complex și plin de elemente periculoase care sunt foarte ușor de folosit greșit. Cu structura și regulile sale, nu suportă programarea care vizează crearea de cod de program fiabil și menținut; dimpotrivă, născut în epoca programării directe pentru diverse procesoare, limbajul contribuie la scrierea codului nesigur și confuz [119] . Mulți programatori profesioniști tind să creadă că limbajul C este un instrument puternic pentru crearea de programe elegante, dar, în același timp, poate fi folosit pentru a crea soluții de o calitate extrem de slabă [120] [121] .
Datorită diferitelor ipoteze ale limbajului, programele se pot compila cu mai multe erori, ducând adesea la un comportament imprevizibil al programului. Compilatoarele moderne oferă opțiuni pentru analiza codului static [122] [123] , dar nici măcar ei nu sunt capabili să detecteze toate erorile posibile. Programarea analfabetă în C poate duce la vulnerabilități software , care pot afecta securitatea utilizării acestuia.
Xi are un prag ridicat de intrare [119] . Specificațiile sale ocupă mai mult de 500 de pagini de text, care trebuie studiate în întregime, deoarece pentru a crea un cod fără erori și de înaltă calitate, trebuie luate în considerare multe caracteristici neevidente ale limbajului. De exemplu, turnarea automată a operanzilor expresiilor întregi de tipat intpoate da rezultate dificile previzibile atunci când se utilizează operatori binari [44] :
caracter nesemnat x = 0xFF ; caracter nesemnat y = ( ~ x | 0x1 ) >> 1 ; // În mod intuitiv, 0x00 este de așteptat aici printf ( "y = 0x%hhX \n " , y ); // Se va imprima 0x80 dacă sizeof(int) > sizeof(char)Lipsa înțelegerii unor astfel de nuanțe poate duce la numeroase erori și vulnerabilități. Un alt factor care mărește complexitatea stăpânirii C este lipsa feedback-ului din partea compilatorului: limbajul oferă programatorului libertate totală de acțiune și permite compilarea programelor cu erori logice evidente. Toate acestea fac dificilă utilizarea C în predare ca prim limbaj de programare [119]
În cele din urmă, de-a lungul a peste 40 de ani de existență, limbajul a devenit oarecum depășit și este destul de problematic să folosiți multe tehnici și paradigme de programare moderne în el .
Nu există module și mecanisme pentru interacțiunea lor în sintaxa C. Fișierele de cod sursă sunt compilate separat și trebuie să includă prototipuri de variabile, funcții și tipuri de date importate din alte fișiere. Acest lucru se face prin includerea fișierelor de antet prin înlocuirea macro . În cazul unei încălcări a corespondenței dintre fișierele de cod și fișierele antet, pot apărea atât erori de conectare, cât și tot felul de erori de rulare: de la corupția stivei și a heap -ului până la erori de segmentare . Deoarece directiva înlocuiește doar textul unui fișier cu altul, includerea unui număr mare de fișiere antet duce la faptul că cantitatea reală de cod care este compilat crește de multe ori, ceea ce este motivul performanței relativ lente a compilatoare C. Necesitatea coordonării descrierilor în modulul principal și fișierele antet face dificilă întreținerea programului. #include#include
Avertismente în loc de eroriStandardul de limbaj oferă programatorului mai multă libertate de acțiune și astfel o șansă mare de a face greșeli. O mare parte din ceea ce nu este permis cel mai adesea este permis de limbaj, iar compilatorul emite avertismente în cel mai bun caz. Deși compilatoarele moderne permit ca toate avertismentele să fie convertite în erori, această caracteristică este rar folosită și, de cele mai multe ori, avertismentele sunt ignorate dacă programul rulează satisfăcător.
Deci, de exemplu, înainte de standardul C99, apelarea unei funcții mallocfără a include un fișier antet stdlib.hputea duce la coruperea stivei, deoarece în absența unui prototip, funcția era numită ca returnând un tip int, în timp ce de fapt returna un tip void*(un tip). eroare a apărut atunci când dimensiunile tipurilor de pe platforma țintă diferă). Chiar și așa, a fost doar un avertisment.
Lipsa controlului asupra inițializării variabilelorObiectele create automat și dinamic nu sunt inițializate implicit și, odată create, conțin valorile rămase în memorie de la obiectele care au fost anterior acolo. O astfel de valoare este complet imprevizibilă, variază de la o mașină la alta, de la rulare la rulare, de la apel de funcție la apel. Dacă programul folosește o astfel de valoare din cauza unei omisiuni accidentale a inițializării, atunci rezultatul va fi imprevizibil și poate să nu apară imediat. Compilatorii moderni încearcă să diagnosticheze această problemă prin analiza statică a codului sursă, deși în general este extrem de dificil să rezolvi această problemă prin analiză statică. Instrumente suplimentare pot fi folosite pentru a identifica aceste probleme în etapa de testare în timpul execuției programului: Valgrind și MemorySanitizer [124] .
Lipsa controlului asupra aritmeticii adreseiSursa situațiilor periculoase este compatibilitatea pointerilor cu tipurile numerice și posibilitatea utilizării aritmeticii adresei fără control strict în etapele de compilare și execuție. Acest lucru face posibilă obținerea unui pointer către orice obiect, inclusiv codul executabil, și referirea la acest pointer, cu excepția cazului în care mecanismul de protecție a memoriei sistemului împiedică acest lucru.
Utilizarea incorectă a indicatorilor poate cauza un comportament nedefinit al programului și poate duce la consecințe grave. De exemplu, un pointer poate fi neinițializat sau, ca rezultat al operațiilor aritmetice incorecte, poate indica o locație de memorie arbitrară. Pe unele platforme, lucrul cu un astfel de pointer poate forța programul să se oprească, pe altele, poate corupe date arbitrare din memorie; Ultima greșeală este periculoasă deoarece consecințele ei sunt imprevizibile și pot apărea oricând, inclusiv mult mai târziu decât momentul realizării acțiunii eronate.
Accesul la matrice în C este de asemenea implementat folosind aritmetica adresei și nu implică mijloace pentru verificarea corectitudinii accesării elementelor matricei prin index. De exemplu, expresiile a[i]și i[a]sunt identice și sunt pur și simplu traduse în forma *(a + i), iar verificarea matricei în afara limitelor nu este efectuată. Accesarea la un index mai mare decât limita superioară a matricei are ca rezultat accesarea datelor aflate în memorie după matrice, ceea ce se numește depășire a memoriei tampon . Când un astfel de apel este eronat, poate duce la un comportament imprevizibil al programului [57] . Adesea, această caracteristică este utilizată în exploit -urile folosite pentru a accesa ilegal memoria unei alte aplicații sau memoria nucleului sistemului de operare.
Memoria dinamică predispusă la eroriFuncțiile sistemului pentru lucrul cu memoria alocată dinamic nu asigură controlul asupra corectitudinii și oportunității alocării și eliberării acesteia, respectarea ordinii corecte de lucru cu memoria dinamică este în întregime responsabilitatea programatorului. Erorile sale, respectiv, pot duce la accesul la adrese incorecte, la eliberarea prematură sau la o scurgere de memorie (aceasta din urmă este posibilă, de exemplu, dacă dezvoltatorul a uitat să sune free()sau să apeleze free()funcția de apelare atunci când a fost necesar) [125] .
Una dintre greșelile comune este de a nu verifica rezultatul funcțiilor de alocare a memoriei ( malloc(), calloc()și altele) pe NULL, în timp ce memoria poate să nu fie alocată dacă nu este suficientă sau dacă s-a solicitat prea mult, de exemplu, din cauza reducerea numărului -1primit ca urmare a oricăror operații matematice eronate, la un tip nesemnat size_t, cu operații ulterioare asupra acestuia . O altă problemă cu funcțiile de memorie de sistem este comportamentul nespecificat atunci când se solicită o alocare de bloc de dimensiune zero: funcțiile pot returna oricare sau o valoare reală de pointer, în funcție de implementarea specifică [126] . NULL
Unele implementări specifice și biblioteci terță parte oferă caracteristici precum numărarea referințelor și referințe slabe [127] , pointeri inteligente [128] și forme limitate de colectare a gunoiului [129] , dar toate aceste caracteristici nu sunt standard, ceea ce limitează în mod natural aplicarea acestora. .
Șiruri de caractere ineficiente și nesigurePentru limbaj, șirurile terminate nul sunt standard, așa că toate funcțiile standard funcționează cu ele. Această soluție duce la o pierdere semnificativă a eficienței din cauza economiilor nesemnificative de memorie (comparativ cu stocarea explicită a dimensiunii): calcularea lungimii unui șir (funcția ) necesită bucla prin întreg șirul de la început până la sfârșit, copierea șirurilor este, de asemenea, dificil de optimizați datorită prezenței unui zero final [48] . Din cauza necesității de a adăuga un nul de terminare la datele șirului, devine imposibil să obțineți eficient subșiruri sub formă de felii și să lucrați cu ele ca și cu șirurile obișnuite; alocarea și manipularea porțiunilor de șiruri de caractere necesită, de obicei, alocarea și dezalocarea manuală a memoriei, crescând și mai mult șansa de eroare. strlen()
Șirurile terminate nul sunt o sursă comună de erori [130] . Nici măcar funcțiile standard de obicei nu verifică dimensiunea buffer-ului țintă [130] și este posibil să nu adauge un caracter nul [131] la sfârșitul șirului , ca să nu mai vorbim că acesta nu poate fi adăugat sau suprascris din cauza unei erori de programare. [132] .
Implementarea nesigură a funcțiilor variadiceÎn timp ce susține funcții cu un număr variabil de argumente , C nu oferă nici un mijloc pentru a determina numărul și tipurile de parametri efectivi trecuți unei astfel de funcție, nici un mecanism pentru accesarea lor în siguranță [133] . Informarea funcției despre compoziția parametrilor efectivi revine programatorului, iar pentru a accesa valorile acestora, este necesar să se numere numărul corect de octeți de la adresa ultimului parametru fix de pe stivă, fie manual, fie folosind un set de macrocomenzi va_argdin fișierul antet stdarg.h. În același timp, este necesar să se țină cont de funcționarea mecanismului de promovare automată a tipului implicit la apelarea funcțiilor [134] , conform căruia tipuri întregi de argumente mai mici decât intsunt turnate în int(sau unsigned int), dar sunt floatturnate în double. O eroare în apel sau în lucrul cu parametrii din interiorul funcției va apărea doar în timpul execuției programului, ducând la consecințe imprevizibile, de la citirea datelor incorecte până la coruperea stivei.
printf()În același timp, funcțiile cu un număr variabil de parametri ( , scanf()și altele) care nu pot verifica dacă lista de argumente se potrivește cu șirul de format sunt mijloacele standard de I/O formatate . Mulți compilatoare moderne efectuează această verificare pentru fiecare apel, generând avertismente dacă găsesc o nepotrivire, dar în general această verificare nu este posibilă deoarece fiecare funcție variadică gestionează această listă în mod diferit. Este imposibil să controlezi static chiar și toate apelurile de funcții, printf()deoarece șirul de format poate fi creat dinamic în program.
Lipsa unificării tratării erorilorSintaxa C nu include un mecanism special de gestionare a erorilor. Biblioteca standard acceptă doar cele mai simple mijloace: o variabilă (în cazul POSIX , o macro) errnodin fișierul antet errno.hpentru a seta ultimul cod de eroare și funcții pentru a obține mesaje de eroare conform codurilor. Această abordare duce la necesitatea de a scrie o cantitate mare de cod repetitiv, amestecând algoritmul principal cu gestionarea erorilor și, în plus, nu este sigur pentru fire. Mai mult, chiar și în acest mecanism nu există o singură ordine:
În biblioteca standard, codurile sunt errnodesemnate prin definiții macro și pot avea aceleași valori, ceea ce face imposibilă analiza codurilor de eroare prin intermediul operatorului switch. Limba nu are un tip de date special pentru steaguri și coduri de eroare, acestea sunt transmise ca valori de tip int. Un tip separat errno_tpentru stocarea codului de eroare a apărut doar în extensia K a standardului C11 și este posibil să nu fie suportat de compilatori [87] .
Deficiențele C sunt de mult cunoscute, iar de la începuturile limbajului au existat multe încercări de a îmbunătăți calitatea și securitatea codului C fără a-i sacrifica capacitățile.
Mijloace de analiză a corectitudinii coduluiAproape toate compilatoarele C moderne permit analiza limitată a codului static cu avertismente despre erori potențiale. Opțiunile sunt, de asemenea, acceptate pentru încorporarea verificărilor pentru matrice în afara limitelor, distrugerea stivei, în afara limitelor heap, citirea variabilelor neinițializate, comportamentul nedefinit etc. în cod. Cu toate acestea, verificările suplimentare pot afecta performanța aplicației finale, deci sunt cel mai adesea folosit doar în etapa de depanare.
Există instrumente software speciale pentru analiza statică a codului C pentru a detecta erorile de non-sintaxă. Utilizarea lor nu garantează programele fără erori, dar vă permite să identificați o parte semnificativă a erorilor tipice și a potențialelor vulnerabilități. Efectul maxim al acestor instrumente este obținut nu cu utilizarea ocazională, ci atunci când sunt utilizate ca parte a unui sistem bine stabilit de control constant al calității codului, de exemplu, în sistemele de integrare și implementare continuă. De asemenea, poate fi necesară adnotarea codului cu comentarii speciale pentru a exclude alarmele false ale analizorului pe secțiunile corecte ale codului care se încadrează în mod formal sub criteriile pentru cele eronate.
Standarde de programare sigurăA fost publicată o cantitate semnificativă de cercetări privind programarea corectă în C, variind de la articole mici până la cărți lungi. Standardele corporative și industriale sunt adoptate pentru a menține calitatea codului C. În special:
Setul de standarde POSIX contribuie la compensarea unor deficiențe ale limbii . Instalarea este standardizată errnode multe funcții, permițând gestionarea erorilor care apar, de exemplu, în operațiunile cu fișiere, și sunt introduși analogi thread -safe ai unor funcții ale bibliotecii standard, ale căror versiuni sigure sunt prezente în standardul de limbă numai în extensia K [137] .
Dicționare și enciclopedii | ||||
---|---|---|---|---|
|
Limbaje de programare | |
---|---|
|
limbaj de programare C | |
---|---|
Compilatoare |
|
Biblioteci | |
Particularități | |
Unii descendenți | |
C și alte limbi |
|
Categorie: limbaj de programare C |