C | |
---|---|
Sprog klasse | proceduremæssige |
Udførelsestype | kompileret |
Dukkede op i | 1972 |
Forfatter | Dennis Ritchie |
Udvikler | Bell Labs , Dennis Ritchie [1] , US National Standards Institute , ISO og Ken Thompson |
Filtypenavn _ | .c— for kodefiler, .h— for header-filer |
Frigøre | ISO/IEC 9899:2018 ( 5. juli 2018 ) |
Type system | statisk svag |
Større implementeringer | GCC , Clang , TCC , Turbo C , Watcom , Oracle Solaris Studio C, Pelles C |
Dialekter |
"K&R" C ( 1978 ) ANSI C ( 1989 ) C99 ( 1999 ) C11 ( 2011 ) |
Blev påvirket | BCPL , B |
påvirket | C++ , Objective-C , C# , Java , Nim |
OS | Microsoft Windows og Unix-lignende operativsystem |
Mediefiler på Wikimedia Commons |
ISO/IEC 9899 | |
Informationsteknologi — Programmeringssprog — C | |
Forlægger | International Organisation for Standardization (ISO) |
Internet side | www.iso.org |
Udvalg (udvikler) | ISO/IEC JTC 1/SC 22 |
Udvalgets hjemmeside | Programmeringssprog, deres miljøer og systemsoftwaregrænseflader |
ISS (ICS) | 35.060 |
Nuværende udgave | ISO/IEC 9899:2018 |
Tidligere udgaver | ISO/IEC 9899:1990/COR2:1996 ISO/IEC 9899:1999/COR3:2007 ISO/IEC 9899:2011/COR1:2012 |
C (fra det latinske bogstav C , engelsk sprog ) er et generel kompileret statisk maskinskrevet programmeringssprog udviklet i 1969-1973 af Bell Labs medarbejder Dennis Ritchie som en udvikling af bisproget . Det blev oprindeligt udviklet til at implementere UNIX -operativsystemet , men er siden blevet overført til mange andre platforme. Ved design er sproget tæt knyttet til typiske maskininstruktioner og har fundet anvendelse i projekter, der var hjemmehørende i assemblersprog , inklusive både operativsystemer og forskellige applikationssoftware til en række forskellige enheder fra supercomputere til indlejrede systemer . Programmeringssproget C har haft en betydelig indflydelse på udviklingen af softwareindustrien, og dets syntaks blev grundlaget for programmeringssprog som C++ , C# , Java og Objective-C .
C-programmeringssproget blev udviklet mellem 1969 og 1973 hos Bell Labs , og i 1973 var det meste af UNIX -kernen , oprindeligt skrevet i PDP-11 /20 assembler, blevet omskrevet til dette sprog. Sprogets navn blev en logisk fortsættelse af det gamle sprog " Bi " [a] , hvis mange træk blev taget som grundlag.
Efterhånden som sproget udviklede sig, blev det først standardiseret som ANSI C , og derefter blev denne standard vedtaget af ISO 's internationale standardiseringskomité som ISO C, også kendt som C90. C99-standarden tilføjede nye funktioner til sproget, såsom arrays med variabel længde og inline-funktioner. Og i C11 -standarden blev implementeringen af strømme og understøttelse af atomtyper tilføjet til sproget. Siden da har sproget dog udviklet sig langsomt, og kun fejlrettelser fra C11-standarden kom ind i C18-standarden.
C-sproget blev designet som et systemprogrammeringssprog, hvortil der kunne oprettes en one-pass compiler . Standardbiblioteket er også lille. Som en konsekvens af disse faktorer er compilere relativt nemme at udvikle [2] . Derfor er dette sprog tilgængeligt på en række forskellige platforme. Derudover er sproget på trods af dets lave niveau fokuseret på portabilitet. Programmer, der er i overensstemmelse med sprogstandarden, kan kompileres til forskellige computerarkitekturer.
Målet med sproget var at gøre det lettere at skrive store programmer med minimale fejl sammenlignet med assembler, idet man fulgte principperne for proceduremæssig programmering , men at undgå alt, der ville introducere yderligere overhead, der er specifikt for sprog på højt niveau.
Hovedtræk ved C:
Samtidig mangler C:
Nogle af de manglende funktioner kan simuleres med indbyggede værktøjer (for eksempel kan koroutiner simuleres ved hjælp af funktionerne setjmpoglongjmp ), nogle tilføjes ved hjælp af tredjepartsbiblioteker (for eksempel for at understøtte multitasking og netværksfunktioner, kan du bruge biblioteker pthreads , sockets og lignende; der er biblioteker til at understøtte automatisk affaldsindsamling [3] ), en del er implementeret i nogle compilere som sprogudvidelser (for eksempel indlejrede funktioner i GCC ). Der er en noget besværlig, men ret brugbar teknik, der gør det muligt at implementere OOP- mekanismer i C [4] , baseret på den faktiske polymorfi af pointere i C og understøttelsen af pointere til funktioner i dette sprog. OOP-mekanismer baseret på denne model er implementeret i GLib- biblioteket og bruges aktivt i GTK+ -rammen . GLib giver en basisklasse GObject, evnen til at arve fra en enkelt klasse [5] og implementere flere grænseflader [6] .
Efter dets introduktion blev sproget godt modtaget, fordi det tillod den hurtige oprettelse af compilere til nye platforme, og gjorde det også muligt for programmører at være ret præcise i, hvordan deres programmer blev udført. På grund af dets nærhed til lavniveausprog kørte C-programmer mere effektivt end dem, der er skrevet på mange andre højniveausprog, og kun håndoptimeret assemblersprogkode kunne køre endnu hurtigere, fordi det gav fuld kontrol over maskinen. Til dato har udviklingen af compilere og komplikationen af processorer ført til, at håndskrevet assembly-kode (undtagen måske meget korte programmer) praktisk talt ikke har nogen fordel i forhold til compiler-genereret kode, mens C fortsat er en af de mest effektive sprog på højt niveau.
Sproget bruger alle tegn i det latinske alfabet , tal og nogle specialtegn [7] .
Latinske alfabettegn |
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 |
Tal | 0, 1, 2, 3, 4, 5, 6, 7, 8_9 |
Særlige symboler | , (komma) , ;, . (prik) , +, -, *, ^, & (ampersand) , =, ~ (tilde) , !, /, <, >, (, ), {, }, [, ], |, %, ?( ' apostrof) , " (citater) , : (kolon) , _ (understregning ) ) , \,# |
Tokens er dannet af gyldige tegn - foruddefinerede konstanter , identifikatorer og operationstegn . Til gengæld er leksemer en del af udtryk ; og udsagn og operatorer består af udtryk .
Når et program oversættes til C, udtrækkes leksemer af den maksimale længde indeholdende gyldige tegn fra programkoden. Hvis et program indeholder et ugyldigt tegn, vil den leksikalske analysator (eller compiler) generere en fejl, og oversættelse af programmet vil være umulig.
Symbolet #kan ikke være en del af nogen token og bruges i præprocessoren .
IdentifikatorerEn gyldig identifikator er et ord, der kan indeholde latinske tegn, tal og understregninger [8] . Identifikatorer gives til operatorer, konstanter, variabler, typer og funktioner.
Nøgleordsidentifikatorer og indbyggede identifikatorer kan ikke bruges som programobjektidentifikatorer. Der er også reserverede identifikatorer, som compileren ikke vil give fejl til, men som i fremtiden kan blive nøgleord, hvilket vil føre til inkompatibilitet.
Der er kun én indbygget identifikator - __func__, som er defineret som en konstant streng, der implicit er erklæret i hver funktion og indeholder dens navn [8] .
Literale konstanterSpecielt formaterede literaler i C kaldes konstanter. Literale konstanter kan være heltal, reelle, tegn [9] og streng [10] .
Heltal er som standard sat i decimal . Hvis et præfiks er angivet 0x, er det hexadecimalt . Præfikset 0 angiver, at tallet er oktalt . Suffikset angiver minimumsstørrelsen af konstanttypen og bestemmer også, om tallet er signeret eller usigneret. Den endelige type tages som den mindst mulige, hvor den givne konstant kan repræsenteres [11] .
Suffiks | For decimal | Til oktal og hexadecimal |
---|---|---|
Ikke | int
long long long |
int
unsigned int long unsigned long long long unsigned long long |
uellerU | unsigned int
unsigned long unsigned long long |
unsigned int
unsigned long unsigned long long |
lellerL | long
long long |
long
unsigned long long long unsigned long long |
ueller Usammen med lellerL | unsigned long
unsigned long long |
unsigned long
unsigned long long |
llellerLL | long long | long long
unsigned long long |
ueller Usammen med llellerLL | unsigned long long | unsigned long long |
Decimal
format |
Med eksponent | Hexadecimal
format |
---|---|---|
1.5 | 1.5e+0 | 0x1.8p+0 |
15e-1 | 0x3.0p-1 | |
0.15e+1 | 0x0.cp+1 |
Reelle talkonstanter er af typen som standard double. Når du angiver et suffiks , ftildeles typen til konstanten , floatog når du angiver leller- L . long doubleEn konstant vil blive betragtet som reel, hvis den indeholder et punkttegn eller et bogstav, peller Pi tilfælde af en hexadecimal notation med et præfiks 0x. Decimalnotationen kan indeholde en eksponent efter bogstaverne eeller E. I tilfælde af hexadecimal notation er eksponenten angivet efter bogstaverne peller Per obligatorisk, hvilket adskiller reelle hexadecimale konstanter fra heltal. I hexadecimal er eksponenten en potens af 2 [12] .
Tegnkonstanter er omgivet af enkelte anførselstegn ( '), og præfikset angiver både datatypen for tegnkonstanten og den kodning, som tegnet vil blive repræsenteret i. I C er en tegnkonstant uden præfiks af typen int[13] , i modsætning til C++ , hvor en tegnkonstant er char.
Præfiks | Datatype | Indkodning |
---|---|---|
Ikke | int | ASCII |
u | char16_t | 16-bit multibyte strengkodning |
U | char32_t | 32-bit multibyte strengkodning |
L | wchar_t | Bred streng kodning |
Strengliteraler er omgivet af dobbelte anførselstegn og kan foranstilles med strengens datatype og kodning. Strengliteraler er almindelige arrays. Men i multibyte-kodninger som UTF-8 kan ét tegn optage mere end ét array-element. Faktisk er strengliteraler const [14] , men i modsætning til C++ indeholder deres datatyper ikke modifikatoren const.
Præfiks | Datatype | Indkodning |
---|---|---|
Ikke | char * | ASCII- eller multibyte-kodning |
u8 | char * | UTF-8 |
u | char16_t * | 16-bit multibyte-kodning |
U | char32_t * | 32-bit multibyte-kodning |
L | wchar_t * | Bred streng kodning |
Flere på hinanden følgende strengkonstanter adskilt af mellemrum eller nye linjer kombineres til en enkelt streng ved kompilering, som ofte bruges til at style koden for en streng ved at adskille dele af en strengkonstant på forskellige linjer for at forbedre læsbarheden [16] .
Navngivne konstanterMakro | #define BUFFER_SIZE 1024 |
Anonym opregning |
enum { BUFFER_SIZE = 1024 }; |
Variabel som konstant |
const int buffer_size = 1024 ; ekstern konst int buffer_størrelse ; |
I C-sproget, for at definere konstanter, er det sædvanligt at bruge makrodefinitioner, der er erklæret ved hjælp af præprocessor-direktivet [17] : #define
#define konstant navn [ værdi ]En konstant, der indføres på denne måde, vil være i kraft i dens omfang, fra det øjeblik, hvor konstanten er indstillet og indtil programkodens afslutning, eller indtil effekten af den givne konstant annulleres af direktivet #undef:
#undef konstant navnSom med enhver makro, for en navngiven konstant, erstattes værdien af konstanten automatisk i programkoden, hvor som helst navnet på konstanten bruges. Derfor, når man erklærer heltal eller reelle tal inde i en makro, kan det være nødvendigt eksplicit at specificere datatypen ved hjælp af det relevante bogstavelige suffiks, ellers vil tallet som standard være en type inti tilfælde af et heltal eller en type double i tilfælde af en ægte.
For heltal er der en anden måde at skabe navngivne konstanter på - gennem operatorenums enum[17] . Denne metode er dog kun egnet til typer mindre end eller lig med type , og bruges ikke i standardbiblioteket [18] . int
Det er også muligt at oprette konstanter som variable med kvalifikatoren const, men i modsætning til de to andre metoder forbruger sådanne konstanter hukommelse, kan peges på og kan ikke bruges på kompileringstidspunktet [17] :
Nøgleord er identifikatorer designet til at udføre en bestemt opgave på kompileringsstadiet eller til tip og instruktioner til compileren.
Nøgleord | Formål | Standard |
---|---|---|
sizeof | Få størrelsen på et objekt på kompileringstidspunktet | C89 |
typedef | Angivelse af et alternativt navn for en type | |
auto,register | Compiler-tip til, hvor variabler er gemt | |
extern | Beder compileren om at lede efter et objekt uden for den aktuelle fil | |
static | Erklæring af et statisk objekt | |
void | Ingen værdimarkør; i pointere betyder vilkårlige data | |
char... short_ int_long | Heltalstyper og deres størrelsesmodifikatorer | |
signed,unsigned | Heltalstypemodifikatorer, der definerer dem som signerede eller usignerede | |
float,double | Reelle datatyper | |
const | En datatypemodifikator, der fortæller compileren, at variabler af den type er skrivebeskyttede | |
volatile | Instruerer compileren til at ændre værdien af en variabel udefra | |
struct | Datatype, angivet som en struktur med et sæt felter | |
enum | En datatype, der gemmer en af et sæt heltalsværdier | |
union | En datatype, der kan gemme data i repræsentationer af forskellige datatyper | |
do... for_while | Løkkeudsagn | |
if,else | Betinget operatør | |
switch... case_default | Udvælgelsesoperator efter heltalsparameter | |
break,continue | Loop Break-erklæringer | |
goto | Ubetinget springoperatør | |
return | Retur fra en funktion | |
inline | Inline funktionserklæring | C99 [20] |
restrict | Erklære en pointer, der refererer til en hukommelsesblok, der ikke refereres til af nogen anden pointer | |
_Bool[b] | boolesk datatype | |
_Complex[c] ,_Imaginary [d] | Typer, der bruges til beregninger af komplekse tal | |
_Atomic | En typemodifikator, der gør den atomær | C11 |
_Alignas[e] | Eksplicit specificering af bytejustering for en datatype | |
_Alignof[f] | Få justering for en given datatype på kompileringstidspunktet | |
_Generic | Valg af en af et sæt værdier på kompileringstidspunktet, baseret på den kontrollerede datatype | |
_Noreturn[g] | Indikerer over for compileren, at funktionen ikke kan afsluttes normalt (dvs. ved return) | |
_Static_assert[h] | Angivelse af påstande, der skal kontrolleres på kompileringstidspunktet | |
_Thread_local[jeg] | Erklærer en tråd-lokal variabel |
Ud over nøgleord definerer sprogstandarden reserverede identifikatorer, hvis brug kan føre til inkompatibilitet med fremtidige versioner af standarden. Alle undtagen søgeordsord, der begynder med en understregning ( _) efterfulgt af enten et stort bogstav ( A- Z) eller en anden understregning [21] er reserveret . I C99- og C11-standarderne blev nogle af disse identifikatorer brugt til nye sprogsøgeord.
Inden for filens omfang er brugen af alle navne, der starter med en understregning ( _) [21] forbeholdt , det vil sige, det er tilladt at navngive typer, konstanter og variabler, der er erklæret inden for en blok af instruktioner, for eksempel inde i funktioner, med en understregning.
Også reserverede identifikatorer er alle makroer i standardbiblioteket og navnene fra det linket på linkningsstadiet [21] .
Brugen af reserverede identifikatorer i programmer defineres af standarden som udefineret adfærd . Forsøg på at annullere en standard makro via #undefvil også resultere i udefineret adfærd [21] .
Teksten i et C-program kan indeholde fragmenter , der ikke er en del af programkodekommentarerne . Kommentarer markeres på en særlig måde i programmets tekst og springes over under kompileringen.
Oprindeligt, i C89- standarden , var inline-kommentarer tilgængelige, som kunne placeres mellem tegnsekvenser /*og */. I dette tilfælde er det umuligt at indlejre en kommentar i en anden, da den første sekvens, der stødes */på, afslutter kommentaren, og teksten umiddelbart efter notationen */vil blive opfattet af compileren som programmets kildekode.
Den næste standard, C99 , introducerede endnu en måde at markere kommentarer på: en kommentar anses for at være tekst, der starter med en sekvens af tegn //og slutter i slutningen af en linje [20] .
Kommentarer bruges ofte til selv at dokumentere kildekode, forklare komplekse dele, beskrive formålet med visse filer og beskrive reglerne for brug og brug af bestemte funktioner, makroer, datatyper og variabler. Der er postprocessorer, der kan konvertere specielt formaterede kommentarer til dokumentation. Blandt sådanne postprocessorer med C-sproget kan Doxygen -dokumentationssystemet fungere .
Operatorer brugt i udtryk er en operation, der udføres på operander , og som returnerer en beregnet værdi - resultatet af operationen. Operanden kan være en konstant, variabel, udtryk eller funktionskald. En operator kan være et specialtegn, et sæt specialtegn eller et specielt ord. Operatører er kendetegnet ved antallet af involverede operander, nemlig at de skelner mellem unære operatorer, binære operatorer og ternære operatorer.
Unære operatorerUnære operatorer udfører en operation på et enkelt argument og har følgende operationsformat:
[ operatør ] [ operand ]Operationerne for øgning og reduktion af postfix har det omvendte format:
[ operand ] [ operator ]+ | unært plus | ~ | Tager returkoden | & | At tage en adresse | ++ | Forøgelse af præfiks eller postfiks | sizeof | Få antallet af bytes optaget af et objekt i hukommelsen; kan bruges både som operation og som operatør |
- | unær minus | ! | logisk negation | * | Pointer dereferencing | -- | Præfiks- eller postfix-nedsættelse | _Alignof | Få justering for en given datatype |
Inkrement- og dekrementoperatorerne ændrer i modsætning til de andre unære operatorer værdien af deres operand. Præfiksoperatøren ændrer først værdien og returnerer den derefter. Postfix returnerer først værdien, og først derefter ændrer den.
Binære operatorerBinære operatorer er placeret mellem to argumenter og udfører en operation på dem:
[ operand ] [ operator ] [ operand ]+ | Tilføjelse | % | Tager resten af en division | << | Bitvis venstre skift | > | Mere | == | Lige med |
- | Subtraktion | & | Bitvis OG | >> | Bit skift til højre | < | Mindre | != | Ikke lige |
* | Multiplikation | | | Bitvis ELLER | && | logisk OG | >= | Større end eller lig | ||
/ | Division | ^ | Bitvis XOR | || | Logisk ELLER | <= | Mindre end eller lig |
Binære operatorer i C inkluderer også venstretildelingsoperatorer, der udfører en operation på venstre og højre argumenter og sætter resultatet i venstre argument.
= | Tildeling af værdien af det højre argument til venstre | %= | Resten af at dividere venstre operand med højre | ^= | Bitvist XOR af højre operand til venstre operand |
+= | Tilføjelse til venstre operand af højre | /= | Inddeling af venstre operand med højre | <<= | Bitvis forskydning af venstre operand til venstre med antallet af bit givet af højre operand |
-= | Subtraktion fra venstre operand af højre | &= | Bitvis OG den højre operand til venstre | >>= | Bitvis forskydning af venstre operand til højre med det antal bit, der er angivet af den højre operand |
*= | Multiplikation af venstre operand med højre | |= | Bitvis ELLER af højre operand til venstre |
Der er kun én ternær operator i C, den forkortede betingede operator, som har følgende form:
[ betingelse ] ?[ udtryk1 ] :[ udtryk2 ]Den betingede stenografioperator har tre operander:
Operatøren i dette tilfælde er en kombination af tegn ?og :.
Et udtryk er et ordnet sæt af operationer på konstanter, variabler og funktioner. Udtryk indeholder operationer bestående af operander og operatorer . Rækkefølgen, hvori operationer udføres, afhænger af registreringsformularen og af operationernes prioritet. Hvert udtryk har en værdi - resultatet af at udføre alle de operationer, der er inkluderet i udtrykket. Under evalueringen af et udtryk, afhængigt af operationerne, kan værdierne af variabler ændre sig, og funktioner kan også udføres, hvis deres kald er til stede i udtrykket.
Blandt udtryk skelnes der en klasse af venstre-tilladelige udtryk - udtryk, der kan være til stede til venstre for opgaveskiltet.
Prioritet for udførelse af operationerPrioriteten af operationer er defineret af standarden og specificerer rækkefølgen, hvori operationer vil blive udført. Operationer i C udføres i henhold til præcedenstabellen nedenfor [25] [26] .
En prioritet | tokens | Operation | Klasse | Associativitet |
---|---|---|---|---|
en | a[indeks] | Referencer efter indeks | postfix | venstre mod højre → |
f(argumenter) | Funktionsopkald | |||
. | Markadgang | |||
-> | Feltadgang med pointer | |||
++ -- | Positiv og negativ stigning | |||
(skriv navn ) {initializer} | Sammensat literal (C99) | |||
(skriv navn ) {initializer,} | ||||
2 | ++ -- | Inkrementer for positive og negative præfikser | unær | ← højre mod venstre |
sizeof | Får størrelsen | |||
_Alignof[f] | Få justering ( C11 ) | |||
~ | Bitvis IKKE | |||
! | Logisk IKKE | |||
- + | Tegnindikation (minus eller plus) | |||
& | At få en adresse | |||
* | Pointerreference (dereference) | |||
(skriv navn) | Type støbning | |||
3 | * / % | Multiplikation, division og rest | binær | venstre mod højre → |
fire | + - | Addition og subtraktion | ||
5 | << >> | Skift til venstre og højre | ||
6 | < > <= >= | Sammenligningsoperationer | ||
7 | == != | Tjek for lighed eller ulighed | ||
otte | & | Bitvis OG | ||
9 | ^ | Bitvis XOR | ||
ti | | | Bitvis ELLER | ||
elleve | && | logisk OG | ||
12 | || | Logisk ELLER | ||
13 | ? : | Tilstand | ternær | ← højre mod venstre |
fjorten | = | Værdi tildeling | binær | |
+= -= *= /= %= <<= >>= &= ^= |= | Handlinger til ændring af venstre værdi | |||
femten | , | Sekventiel beregning | venstre mod højre → |
Operatørprioriteter i C retfærdiggør ikke altid sig selv og fører nogle gange til intuitivt vanskelige at forudsige resultater. For eksempel, da unære operatorer har højre-til-venstre-associativitet, vil evaluering af udtrykket *p++resultere i et pointer-tilvækst efterfulgt af en dereference ( *(p++)), snarere end et pointer-tilvækst ( (*p)++). Derfor anbefales det i tilfælde af vanskeligt forståelige situationer eksplicit at gruppere udtryk ved hjælp af parenteser [26] .
Et andet vigtigt træk ved C-sproget er, at evalueringen af argumentværdier, der sendes til et funktionskald, ikke er sekventiel [27] , det vil sige, at de kommaadskillende argumenter ikke svarer til sekventiel evaluering fra præcedenstabellen. I det følgende eksempel kan funktionskald givet som argumenter til en anden funktion være i en hvilken som helst rækkefølge:
int x ; x = beregne ( get_arg1 (), get_arg2 ()); // kald get_arg2() førstDu kan heller ikke stole på forrangen af operationer i tilfælde af bivirkninger , der opstår under evalueringen af udtrykket, da dette vil føre til udefineret adfærd [27] .
Sekvenspunkter og bivirkningerAppendiks C til sprogstandarden definerer et sæt sekvenspunkter, der med garanti ikke har vedvarende bivirkninger fra beregninger. Det vil sige, at sekvenspunktet er et trin af beregninger, der adskiller evalueringen af udtryk indbyrdes, således at de beregninger, der fandt sted før sekvenspunktet, inklusive bivirkninger, allerede er afsluttet, og efter sekvenspunktet er de endnu ikke begyndt [28 ] . En bivirkning kan være en ændring i værdien af en variabel under evalueringen af et udtryk. Ændring af værdien involveret i beregningen, sammen med bivirkningen af at ændre den samme værdi til næste sekvenspunkt, vil føre til udefineret adfærd. Det samme vil ske, hvis der er to eller flere sideændringer til samme værdi involveret i beregningen [27] .
Waypoint | Arrangement før | Event efter |
---|---|---|
Funktionsopkald | Beregning af en pointer til en funktion og dens argumenter | Funktionsopkald |
Logiske OG-operatorer ( &&), OR ( ||) og sekventiel beregning ( ,) | Beregning af den første operand | Beregning af den anden operand |
Stenografisk tilstandsoperatør ( ?:) | Beregning af operanden, der tjener som betingelse | Beregning af 2. eller 3. operand |
Mellem to komplette udtryk (ikke indlejret) | Et komplet udtryk | Følgende fulde udtryk |
Fuldført komplet deskriptor | ||
Lige før du vender tilbage fra en biblioteksfunktion | ||
Efter hver konvertering forbundet med en formateret I/O-specifikation | ||
Umiddelbart før og umiddelbart efter hvert kald til sammenligningsfunktionen og mellem kaldet til sammenligningsfunktionen og eventuelle bevægelser udført på argumenterne videregivet til sammenligningsfunktionen |
Fuld udtryk er [27] :
I det følgende eksempel ændres variablen tre gange mellem sekvenspunkter, hvilket resulterer i et udefineret resultat:
int i = 1 ; // Deskriptoren er det første sekvenspunkt, det fulde udtryk er det andet i += ++ i + 1 ; // Fuldt udtryk - tredje sekvens punkt printf ( "%d \n " , i ); // Kan udskrive enten 4 eller 5Andre simple eksempler på udefineret adfærd, der skal undgås:
i = i ++ + 1 ; // udefineret adfærd i = ++ i + 1 ; // også udefineret adfærd printf ( "%d, %d \n " , -- i , ++ i ); // udefineret adfærd printf ( "%d, %d \n " , ++ i , ++ i ); // også udefineret adfærd printf ( "%d, %d \n " , i = 0 , i = 1 ); // udefineret adfærd printf ( "%d, %d \n " , i = 0 , i = 0 ); // også udefineret adfærd a [ i ] = i ++ ; // udefineret adfærd a [ i ++ ] = i ; // også udefineret adfærdKontroludsagn er designet til at udføre handlinger og kontrollere strømmen af programafvikling. Flere på hinanden følgende udsagn danner en række udsagn .
Tom erklæringDen enkleste sprogkonstruktion er et tomt udtryk kaldet et tomt udsagn [29] :
;En tom sætning gør intet og kan placeres hvor som helst i programmet. Anvendes almindeligvis i løkker med manglende krop [30] .
InstruktionerEn instruktion er en slags elementær handling:
( udtryk );Denne operatørs handling er at udføre det udtryk, der er angivet i operatørens brødtekst.
Flere på hinanden følgende instruktioner danner en instruktionssekvens .
InstruktionsblokInstruktioner kan grupperes i specielle blokke i følgende form:
{
( sekvens af instruktioner )},
En blok af udsagn, også nogle gange kaldet en sammensat udsagn, er afgrænset af en venstre krøllet klammeparentes ( {) i begyndelsen og en højre krøllet klammeparentes ( }) i slutningen.
I funktioner angiver en sætningsblok funktionens krop og er en del af funktionsdefinitionen. Den sammensatte sætning kan også bruges i loop-, betingelses- og valgsætninger.
Betingede udsagnDer er to betingede operatorer i sproget, der implementerer programforgrening:
Operatørens enkleste formif
if(( betingelse ) )( operatør ) ( næste udsagn )Operatøren iffungerer således:
Især vil følgende kode, hvis den angivne betingelse er opfyldt, ikke udføre nogen handling, da der faktisk udføres en tom sætning:
if(( tilstand )) ;En mere kompleks form for operatoren ifindeholder nøgleordet else:
if(( betingelse ) )( operatør ) else( alternativ operatør ) ( næste udsagn )Her, hvis betingelsen angivet i parentes ikke er opfyldt, så udføres den sætning, der er angivet efter nøgleordet else.
Selvom standarden tillader sætninger at blive specificeret på én linje ifeller som elseen enkelt linje, betragtes dette som dårlig stil og reducerer kodens læsbarhed. Det anbefales, at du altid angiver en blok af udsagn ved at bruge krøllede seler som krop [31] .
Loop-udførelseserklæringerEn løkke er et stykke kode, der indeholder
Derfor er der to typer cyklusser:
En postbetinget løkke garanterer, at løkkens krop vil blive udført mindst én gang.
C-sproget giver to varianter af loops med en forudsætning: whileog for.
while(tilstand) [ loop body ] for( initialiseringsblok ;tilstandserklæring [ loop body ] ;,)Sløjfen forkaldes også parametrisk, den svarer til følgende blok af udsagn:
[ initialiseringsblok ] while(tilstand) { [ loop body ] [ operatør ] }I en normal situation indeholder initialiseringsblokken indstilling af startværdien af en variabel, som kaldes loop-variablen, og sætningen, der udføres umiddelbart efter loop-kroppen ændrer værdierne for den brugte variabel, betingelsen indeholder en sammenligning af værdien af den brugte sløjfevariabel med en foruddefineret værdi, og så snart sammenligningen stopper udføres, afbrydes løkken og programkoden umiddelbart efter loop-sætningen begynder at blive eksekveret.
For en løkke do-whileangives betingelsen efter løkkens brødtekst:
do[ loop body ] while( tilstand)Løkkebetingelsen er et boolsk udtryk. Implicit type casting giver dig dog mulighed for at bruge et aritmetisk udtryk som en loop-betingelse. Dette giver dig mulighed for at organisere den såkaldte "uendelige loop":
while(1);Det samme kan gøres med operatøren for:
for(;;);I praksis bruges sådanne uendelige løkker sædvanligvis i forbindelse med break, gotoeller return, som afbryder løkken på forskellige måder.
Som med et betinget udsagn, anses det for dårlig stil at bruge en enkelt-linjes brødtekst uden at omslutte den i en udsagnsblok med krøllede parenteser, hvilket reducerer kodelæsbarheden [31] .
Ubetingede Jump OperatorsUbetingede filialoperatører giver dig mulighed for at afbryde udførelsen af enhver blok af beregninger og gå til et andet sted i programmet inden for den aktuelle funktion. Ubetingede springoperatorer bruges normalt sammen med betingede operatorer.
goto[ etiket ],En etiket er en identifikator, der overfører kontrol til operatøren, som er markeret i programmet med den angivne etiket:
[ etiket ] :[ operatør ]Hvis den angivne etiket ikke er til stede i programmet, eller hvis der er flere sætninger med samme etiket, rapporterer compileren en fejl.
Overførsel af kontrol er kun mulig inden for den funktion, hvor overgangsoperatøren bruges, derfor kan brug af operatøren gotoikke overføre kontrol til en anden funktion.
Andre jump-sætninger er relateret til loops og giver dig mulighed for at afbryde udførelsen af loop-kroppen:
Sætningen breakkan også afbryde driften af sætningen switch, så inde i sætningen, switchder kører i løkken, vil sætningen breakikke være i stand til at afbryde løkken. Angivet i løkkens brødtekst afbryder den arbejdet i den nærmeste indlejrede løkke.
Operatøren continuekan kun bruges inde i do, whileog operatørerne for. For sløjfer whileog do-whileoperatøren continueforårsager testen af løkkebetingelsen, og i tilfælde af en løkke for udførelsen af operatøren specificeret i løkkens 3. parameter, før betingelsen for at fortsætte løkken kontrolleres.
Funktion return sætningOperatøren returnafbryder udførelsen af den funktion, hvori den bruges. Hvis funktionen ikke skal returnere en værdi, så bruges et kald uden en returværdi:
return;Hvis funktionen skal returnere en værdi, er returværdien angivet efter operatoren:
return[ værdi ];Hvis der er andre sætninger efter return-sætningen i funktionskroppen, vil disse sætninger aldrig blive udført, i hvilket tilfælde compileren kan give en advarsel. Efter operatøren returnkan der dog angives instruktioner til alternativ afslutning af funktionen, for eksempel ved en fejl, og overgangen til disse operatører kan udføres ved hjælp af operatøren i gotohenhold til alle betingelser .
Når en variabel erklæres, angives dens type og navn, og startværdien kan også angives:
[beskrivelse] [navn];eller
[beskrivelse] [navn] =[initialisering] ;,hvor
Hvis variablen ikke tildeles en startværdi, er dens værdi i tilfælde af en global variabel udfyldt med nuller, og for en lokal variabel vil startværdien være udefineret.
I en variabeldeskriptor kan du angive en variabel som global, men begrænset til omfanget af en fil eller funktion, ved at bruge nøgleordet static. Hvis en variabel er erklæret global uden nøgleordet static, så kan den også tilgås fra andre filer, hvor det er påkrævet at erklære denne variabel uden en initializer, men med nøgleordet extern. Adresserne på sådanne variabler bestemmes på linktidspunktet .
En funktion er et selvstændigt stykke programkode, der kan genbruges i et program. Funktioner kan tage argumenter og kan returnere værdier. Funktioner kan også have bivirkninger under deres udførelse: ændring af globale variabler, arbejde med filer, interaktion med operativsystemet eller hardware [28] .
For at definere en funktion i C, skal du erklære den:
Det er også nødvendigt at give en funktionsdefinition, der indeholder en blok af udsagn, der implementerer funktionens adfærd.
Ikke at deklarere en bestemt funktion er en fejl, hvis funktionen bruges uden for definitionens rammer, hvilket afhængigt af implementeringen resulterer i meddelelser eller advarsler.
For at kalde en funktion er det nok at angive dens navn med de parametre, der er angivet i parentes. I dette tilfælde placeres adressen på opkaldspunktet på stakken, variabler, der er ansvarlige for funktionsparametrene, oprettes og initialiseres, og kontrollen overføres til koden, der implementerer den kaldte funktion. Efter at funktionen er udført, frigives den hukommelse, der er allokeret under funktionskaldet, returnering til call pointet, og hvis funktionskaldet er en del af et udtryk, overføres værdien beregnet inde i funktionen til returpunktet.
Hvis parenteser ikke er angivet efter funktionen, så fortolker compileren dette som at få funktionens adresse. Adressen på en funktion kan indtastes i en pointer og efterfølgende kaldes funktionen ved hjælp af en pointer til den, som bruges aktivt fx i plugin- systemer [32] .
Ved hjælp af nøgleordet kan inlinedu markere funktioner, hvis opkald du ønsker at udføre så hurtigt som muligt. Compileren kan erstatte koden for sådanne funktioner direkte på tidspunktet for deres opkald [33] . På den ene side øger dette mængden af eksekverbar kode, men på den anden side sparer det tiden for dens eksekvering, da den tidskrævende funktionsopkaldsoperation ikke bruges. Men på grund af computernes arkitektur kan inlining-funktioner enten fremskynde eller sænke applikationen som helhed. Men i mange tilfælde er inline-funktioner den foretrukne erstatning for makroer [34] .
FunktionserklæringEn funktionserklæring har følgende format:
[beskrivelse] [navn] ([liste] );,hvor
Tegnet på en funktionserklæring er ;symbolet " ", så en funktionserklæring er en instruktion.
I det enkleste tilfælde indeholder [deklarator] en indikation af en bestemt type returværdi. En funktion, der ikke skal returnere nogen værdi, erklæres for at være af typen void.
Om nødvendigt kan beskrivelsen indeholde modifikatorer, der er specificeret ved hjælp af nøgleord:
Listen over funktionsparametre definerer funktionens signatur.
C tillader ikke at erklære flere funktioner med samme navn, funktionsoverbelastning understøttes ikke [36] .
FunktionsdefinitionFunktionsdefinitionen har følgende format:
[descriptor] [navn] ([liste] )[body]Hvor [deklarator], [navn] og [liste] er de samme som i erklæringen, og [body] er en sammensat erklæring, der repræsenterer en konkret implementering af funktionen. Compileren skelner mellem definitioner af funktioner af samme navn ved deres signatur, og dermed (ved signatur) etableres en forbindelse mellem definitionen og den tilsvarende erklæring.
Funktionens krop ser således ud:
{ [udsagnssekvens] return([returværdi]); }Returen fra funktionen udføres ved hjælp af -operatoren , som enten angiver returværdien eller ikke angiver den, afhængigt af den datatype, funktionen returnerer. I sjældne tilfælde kan en funktion markeres som ikke at returnere ved hjælp af en makro fra en header-fil , i hvilket tilfælde der ikke kræves nogen sætning. For eksempel kan funktioner, der ubetinget kalder i sig selv, markeres på denne måde [33] . returnnoreturnstdnoreturn.hreturnabort()
FunktionskaldFunktionskaldet skal udføre følgende handlinger:
Afhængigt af implementeringen sikrer compileren enten strengt, at typen af den faktiske parameter matcher typen af den formelle parameter, eller, hvis det er muligt, udfører en implicit typekonvertering, hvilket naturligvis fører til bivirkninger.
Hvis en variabel overføres til funktionen, oprettes en kopi af den, når funktionen kaldes ( hukommelsen allokeres på stakken, og værdien kopieres). For eksempel vil overførsel af en struktur til en funktion få hele strukturen til at blive kopieret. Hvis en markør til en struktur sendes, kopieres kun værdien af markøren. At sende et array til en funktion bevirker også kun, at en pointer til dets første element bliver kopieret. I dette tilfælde, for eksplicit at angive, at adressen på begyndelsen af arrayet tages som input til funktionen, og ikke en pointer til en enkelt variabel, i stedet for at erklære en pointer efter variabelnavnet, kan du sætte firkantede parenteser, f. eksempel:
void example_func ( int array []); // array er en pointer til det første element i et array af typen intC tillader indlejrede opkald. Indlejringsdybden af opkald har en åbenlys begrænsning relateret til størrelsen af stakken, der er allokeret til programmet. Derfor sætter C-implementeringer en grænse for rededybden.
Et specialtilfælde af et indlejret kald er et funktionskald inde i kroppen af den kaldte funktion. Et sådant kald kaldes rekursivt og bruges til at organisere ensartede beregninger. I betragtning af den naturlige begrænsning af indlejrede opkald, erstattes den rekursive implementering af en implementering, der bruger loops.
Heltalsdatatyper varierer i størrelse fra mindst 8 til mindst 32 bit. C99-standarden øger den maksimale størrelse af et heltal til mindst 64 bit. Heltalsdatatyper bruges til at gemme heltal (typen charbruges også til at gemme ASCII-tegn). Alle rækkeviddestørrelser af datatyperne nedenfor er minimumsstørrelser og kan være større på en given platform [37] .
Som følge af minimumsstørrelserne af typer kræver standarden, at størrelserne af integraltyper opfylder betingelsen:
1= ≤ ≤ ≤ ≤ . sizeof(char)sizeof(short)sizeof(int)sizeof(long)sizeof(long long)
Således kan størrelsen af nogle typer i forhold til antallet af bytes matche, hvis betingelsen for det mindste antal bits er opfyldt. Selv charog longkan være af samme størrelse, hvis en byte vil tage 32 bit eller mere, men sådanne platforme vil være meget sjældne eller vil ikke eksistere. Standarden garanterer, at typen char altid er 1 byte. Størrelsen af en byte i bit bestemmes af en konstant CHAR_BITi header-filen limits.h, som er 8 bit på POSIX -kompatible systemer [38] .
Minimumsværdiområdet for heltalstyper i henhold til standarden er defineret fra til for fortegnstyper og fra til for typer uden fortegn, hvor N er typens bitdybde. Compilerimplementeringer kan udvide dette område efter eget skøn. I praksis er intervallet fra til mere almindeligt brugt til signerede typer . Minimums- og maksimumværdierne af hver type er angivet i filen som makrodefinitioner. -(2N-1-1)2N-1-102N-2N-12N-1-1limits.h
Der skal lægges særlig vægt på typen char. Formelt er dette en separat type, men svarer faktisk chartil enten signed char, eller unsigned char, afhængigt af compileren [39] .
For at undgå forvirring mellem typestørrelser introducerede C99-standarden nye datatyper, beskrevet i stdint.h. Blandt dem er sådanne typer som: , , , hvor = 8, 16, 32 eller 64. Præfikset angiver den mindste type, der kan rumme bits, præfikset angiver en type på mindst 16 bit, hvilket er den hurtigste på denne platform. Typer uden præfikser angiver typer med en fast størrelse af bit. intN_tint_leastN_tint_fastN_tNleast-Nfast-N
Typer med præfikser least-og fast-kan betragtes som en erstatning for typer int, short, long, med den eneste forskel, at førstnævnte giver programmøren et valg mellem hastighed og størrelse.
Datatype | Størrelsen | Minimum værdiområde | Standard |
---|---|---|---|
signed char | minimum 8 bits | fra −127 [40] (= -(2 7 −1)) til 127 | C90 [j] |
int_least8_t | C99 | ||
int_fast8_t | |||
unsigned char | minimum 8 bits | 0 til 255 (=2 8 −1) | C90 [j] |
uint_least8_t | C99 | ||
uint_fast8_t | |||
char | minimum 8 bits | −127 til 127 eller 0 til 255 afhængigt af compileren | C90 [j] |
short int | minimum 16 bit | fra -32.767 (= -(2 15 -1)) til 32.767 | C90 [j] |
int | |||
int_least16_t | C99 | ||
int_fast16_t | |||
unsigned short int | minimum 16 bit | 0 til 65,535 (= 2 16 −1) | C90 [j] |
unsigned int | |||
uint_least16_t | C99 | ||
uint_fast16_t | |||
long int | minimum 32 bit | −2.147.483.647 til 2.147.483.647 | C90 [j] |
int_least32_t | C99 | ||
int_fast32_t | |||
unsigned long int | minimum 32 bit | 0 til 4.294.967.295 (= 2 32 −1) | C90 [j] |
uint_least32_t | C99 | ||
uint_fast32_t | |||
long long int | minimum 64 bit | -9.223.372.036.854.775.807 til 9.223.372.036.854.775.807 | C99 |
int_least64_t | |||
int_fast64_t | |||
unsigned long long int | minimum 64 bit | 0 til 18.446.744.073.709.551.615 (= 264 −1 ) | |
uint_least64_t | |||
uint_fast64_t | |||
int8_t | 8 bit | -127 til 127 | |
uint8_t | 8 bit | 0 til 255 (=2 8 −1) | |
int16_t | 16 bit | -32.767 til 32.767 | |
uint16_t | 16 bit | 0 til 65,535 (= 2 16 −1) | |
int32_t | 32 bit | −2.147.483.647 til 2.147.483.647 | |
uint32_t | 32 bit | 0 til 4.294.967.295 (= 2 32 −1) | |
int64_t | 64 bit | -9.223.372.036.854.775.807 til 9.223.372.036.854.775.807 | |
uint64_t | 64 bit | 0 til 18.446.744.073.709.551.615 (= 264 −1 ) | |
Tabellen viser minimumsområdet for værdier i henhold til sprogstandarden. C-kompilere kan udvide rækkevidden af værdier. |
Ligeledes siden C99-standarden er typerne intmax_tog tilføjet uintmax_t, svarende til henholdsvis de største signerede og usignerede typer. Disse typer er praktiske, når de bruges i makroer til at gemme mellemliggende eller midlertidige værdier under operationer på heltalsargumenter, da de giver dig mulighed for at tilpasse værdier af enhver type. Disse typer bruges f.eks. i heltalssammenligningsmakroerne i Tjek enhedstestbiblioteket for C [41] .
I C er der flere ekstra heltalstyper til sikker håndtering af pointerdatatypen: intptr_t, uintptr_tog ptrdiff_t. Typerne intptr_tog uintptr_tfra C99-standarden er designet til at gemme henholdsvis signerede og usignerede værdier, der kan passe til en pointer i størrelse. Disse typer bruges ofte til at gemme et vilkårligt heltal i en pointer, for eksempel som en måde at slippe af med unødvendig hukommelsesallokering ved registrering af feedbackfunktioner [42] eller ved brug af tredjeparts-linkede lister, associative arrays og andre strukturer, hvori data gemmes af pointer. Typen ptrdiff_tfra header-filen stddef.her designet til sikkert at gemme forskellen mellem to pointere.
For at gemme størrelsen leveres en usigneret type size_tfra header-filen stddef.h. Denne type er i stand til at holde det maksimalt mulige antal bytes, der er tilgængelige ved markøren, og bruges typisk til at gemme størrelsen i bytes. Værdien af denne type returneres af operatøren sizeof[43] .
Heltalstype støbningHeltalstypekonverteringer kan forekomme enten eksplicit, ved hjælp af en cast-operator eller implicit. Værdier af typer mindre end int, når de deltager i nogen operationer eller når de videregives til et funktionskald, castes automatisk til typen int, og hvis konverteringen er umulig, til typen unsigned int. Ofte er sådanne implicitte afstøbninger nødvendige for, at resultatet af beregningen er korrekt, men nogle gange fører de til intuitivt uforståelige fejl i beregningerne. For eksempel, hvis operationen involverer tal af typen intog unsigned int, og fortegnsværdien er negativ, vil konvertering af et negativt tal til en type uden fortegn føre til et overløb og en meget stor positiv værdi, hvilket kan føre til et forkert resultat af sammenligningsoperationer [44] .
Signerede og usignerede typer er mindre endint | Signeret er mindre end usigneret, og usigneret er ikke mindreint |
---|---|
#include <stdio.h> tegnet char x = -1 ; usigneret char y = 0 ; if ( x > y ) { // betingelse er falsk printf ( "Meddelelsen vil ikke blive vist. \n " ); } if ( x == UCHAR_MAX ) { // betingelse er falsk printf ( "Meddelelsen vil ikke blive vist. \n " ); } | #include <stdio.h> tegnet char x = -1 ; usigneret int y = 0 ; if ( x > y ) { // betingelse er sand printf ( "Overløb i variabel x. \n " ); } if (( x == UINT_MAX ) && ( x == ULONG_MAX )) { // betingelse vil altid være sand printf ( "Overløb i variabel x. \n " ); } |
I dette eksempel vil begge typer, signerede og usignerede, blive castet til signerede int, fordi det tillader områder af begge typer at passe. Derfor vil sammenligningen i den betingede operator være korrekt. | En signeret type vil blive castet til usigneret, fordi den usignerede type er større end eller lig med størrelse int, men et overløb vil forekomme, fordi det er umuligt at repræsentere en negativ værdi i en usigneret type. |
Automatisk typecasting vil også fungere, hvis der bruges to eller flere forskellige heltalstyper i udtrykket. Standarden definerer et regelsæt, hvorefter der vælges en typekonvertering, der kan give det korrekte resultat af beregningen. Forskellige typer tildeles forskellige rækker inden for transformationen, og selve rækkerne er baseret på typens størrelse. Når forskellige typer er involveret i et udtryk, vælges det normalt at kaste disse værdier til en type af højere rang [44] .
Reelle talFlydende kommatal i C er repræsenteret af tre grundlæggende typer: float, doubleog long double.
Reelle tal har en repræsentation, der er meget forskellig fra heltal. Konstanter af reelle tal af forskellige typer, skrevet med decimalnotation, er muligvis ikke lig med hinanden. For eksempel vil betingelsen 0.1 == 0.1fvære falsk på grund af tab af præcision i type float, mens betingelsen 0.5 == 0.5fvil være sand, fordi disse tal er endelige i binær repræsentation. Støbningstilstanden (float) 0.1 == 0.1fvil dog også være sand, fordi støbning til en mindre præcis type mister de bits, der gør de to konstanter forskellige.
Aritmetiske operationer med reelle tal er også unøjagtige og har ofte en eller anden flydende fejl [45] . Den største fejl vil opstå, når der arbejdes på værdier, der er tæt på det mindst mulige for en bestemt type. Fejlen kan også vise sig at være stor, når man regner over både meget små (≪ 1) og meget store tal (≫ 1). I nogle tilfælde kan fejlen reduceres ved at ændre algoritmerne og beregningsmetoderne. For eksempel, når du erstatter multipel addition med multiplikation, kan fejlen falde lige så mange gange, som der oprindeligt var additionsoperationer.
Også i header-filen math.her der to ekstra typer float_tog double_t, som i det mindste svarer til typerne floatog doublehenholdsvis, men kan være forskellige fra dem. Typerne float_tog double_ttilføjes i C99-standarden , og deres overensstemmelse med basistyperne bestemmes af værdien af makroen FLT_EVAL_METHOD.
Datatype | Størrelsen | Standard |
---|---|---|
float | 32 bit | IEC 60559 ( IEEE 754 ), udvidelse F af C-standarden [46] [k] , enkelt præcisionsnummer |
double | 64 bit | IEC 60559 (IEEE 754), udvidelse F af C-standarden [46] [k] , dobbelt præcisionsnummer |
long double | minimum 64 bit | implementeringsafhængig |
float_t(C99) | minimum 32 bit | afhænger af basistype |
double_t(C99) | minimum 64 bit | afhænger af basistype |
FLT_EVAL_METHOD | float_t | double_t |
---|---|---|
en | float | double |
2 | double | double |
3 | long double | long double |
Selvom der ikke er nogen speciel type for strenge i C som sådan, bruges null-terminerede strenge i høj grad i sproget. ASCII -strenge er deklareret som et array af typen char, hvis sidste element skal være tegnkoden 0( '\0'). Det er sædvanligt at gemme UTF-8- strenge i samme format . Men alle funktioner, der arbejder med ASCII-strenge, betragter hvert tegn som en byte, hvilket begrænser brugen af standardfunktioner ved brug af denne kodning.
På trods af den udbredte brug af ideen om nulterminerede strenge og bekvemmeligheden ved at bruge dem i nogle algoritmer, har de flere alvorlige ulemper.
I moderne forhold, når kodeydeevne prioriteres frem for hukommelsesforbrug, kan det være mere effektivt og lettere at bruge strukturer, der indeholder både selve strengen og dens størrelse [48] , for eksempel:
struct string_t { char * str ; // pointer til streng size_t str_size ; // strengstørrelse }; typedef struct string_t string_t ; // alternativt navn for at forenkle kodenEn alternativ lagringstilgang til strengstørrelse med lav hukommelse ville være at præfiksere strengen med dens størrelse i et størrelsesformat med variabel længde .. En lignende tilgang bruges i protokolbuffere , dog kun på tidspunktet for dataoverførsel, men ikke deres lagring.
Streng bogstaverStrengliteraler i C er iboende konstanter [10] . Ved deklarering er de omgivet af dobbelte anførselstegn, og terminatoren 0tilføjes automatisk af compileren. Der er to måder at tildele en streng literal på: med pointer og værdi. Når der tildeles med pointer, indtastes en char *pointer til en uforanderlig streng i typevariablen, det vil sige, at der dannes en konstant streng. Hvis du indtaster en streng i et array, kopieres strengen til stakområdet.
#include <stdio.h> #include <string.h> int main ( ugyldig ) { const char * s1 = "Konst streng" ; char s2 [] = "Streng der kan ændres" ; memcpy ( s2 , "c" , strlen ( "c" )); // ændre det første bogstav til lille sætter ( s2 ); // teksten i linjen vil blive vist memcpy (( char * ) s1 , "til" , strlen ( "til" )); // segmenteringsfejl sætter ( s1 ); // linje vil ikke blive udført }Da strenge er almindelige arrays af tegn, kan initialiseringer bruges i stedet for bogstaver, så længe hvert tegn passer i 1 byte:
char s [] = { 'I' , 'n' , 'i' , 't' , 'i' , 'a' , 'l' , 'i' , 'z' , 'e' , 'r' , '\0' };Men i praksis giver denne tilgang kun mening i ekstremt sjældne tilfælde, når det er nødvendigt ikke at tilføje et afsluttende nul til en ASCII-streng.
Brede linjerPlatform | Indkodning |
---|---|
GNU/Linux | USC-4 [49] |
macOS | |
Windows | USC-2 [50] |
AIX | |
FreeBSD | Afhænger af lokaliteten
ikke dokumenteret [50] |
Solaris |
Et alternativ til almindelige strenge er brede strenge, hvor hvert tegn er gemt i en speciel type wchar_t. Den givne type af standarden bør være i stand til i sig selv at indeholde alle tegn fra den største af eksisterende lokaliteter . Funktioner til at arbejde med brede strenge er beskrevet i header-filen wchar.h, og funktioner til at arbejde med brede tegn er beskrevet i header-filen wctype.h.
Når du erklærer strengliteraler for brede strenge, bruges modifikationen L:
const wchar_t * wide_str = L "Bred streng" ;Det formaterede output bruger specifikationen %ls, men størrelsesspecifikationen, hvis den er angivet, er angivet i bytes, ikke tegn [51] .
Typen wchar_tblev udtænkt, så ethvert tegn kunne passe ind i den, og brede strenge - til at gemme strenge af enhver lokalitet, men som et resultat viste API'et sig at være ubelejligt, og implementeringerne var platformafhængige. Så på Windows -platformen blev 16 bit valgt som størrelsen på typen wchar_t, og senere dukkede UTF-32-standarden op, så typen wchar_tpå Windows-platformen er ikke længere i stand til at passe til alle tegnene fra UTF-32-kodningen, som følge af hvilken betydningen af denne type går tabt [50] . På samme tid, på Linux [49] og macOS platforme, tager denne type 32 bit, så typen er ikke egnet til implementering af opgaver på tværs af platforme .wchar_t
Multibyte strengeDer er mange forskellige kodninger, hvor et enkelt tegn kan programmeres med et forskelligt antal bytes. Sådanne kodninger kaldes multibyte. UTF-8 gælder også for dem . C har et sæt funktioner til at konvertere strenge fra multibyte inden for den aktuelle lokalitet til bred og omvendt. Funktioner til at arbejde med multibyte-tegn har et præfiks eller suffiks mbog er beskrevet i header-filen stdlib.h. For at understøtte multibyte-strenge i C-programmer skal sådanne strenge understøttes på det aktuelle lokalitetsniveau . For eksplicit at indstille kodningen kan du ændre den aktuelle lokalitet ved hjælp af en funktion setlocale()fra locale.h. Angivelse af en kodning for en lokalitet skal dog understøttes af det anvendte standardbibliotek. For eksempel understøtter Glibc -standardbiblioteket fuldt ud UTF-8-kodning og er i stand til at konvertere tekst til mange andre kodninger [52] .
Fra og med C11-standarden understøtter sproget også 16-bit og 32-bit brede multibyte-strenge med passende tegntyper char16_tog char32_tfra en header-fil uchar.h, såvel som at erklære UTF-8 strenge bogstaver ved hjælp af u8. 16-bit og 32-bit strenge kan bruges til at gemme UTF-16- og UTF-32-kodninger , hvis henholdsvis uchar.hmakrodefinitioner __STDC_UTF_16__og er angivet i header-filen __STDC_UTF_32__. For at angive strengliteraler i disse formater bruges modifikatorer: ufor 16-bit strenge og Ufor 32-bit strenge. Eksempler på deklaration af strengliteraler for multibyte strenge:
const char * s8 = u8 "UTF-8 multibyte streng" ; const char16_t * s16 = u "16-bit multibyte streng" ; const char32_t * s32 = U "32-bit multibyte streng" ;Bemærk, at funktionen c16rtomb()til at konvertere fra en 16-bit streng til en multibyte streng ikke virker efter hensigten, og i C11 standarden viste det sig ikke at være i stand til at oversætte fra UTF-16 til UTF-8 [53] . Korrigering af denne funktion kan afhænge af den specifikke implementering af compileren.
Enums er et sæt navngivne heltalskonstanter og er angivet med nøgleordet enum. Hvis en konstant ikke er knyttet til et tal, sættes den automatisk enten 0for den første konstant på listen eller et tal, der er en større end det, der er angivet i den foregående konstant. I dette tilfælde kan selve optællingsdatatypen faktisk svare til enhver fortegnet eller usigneret primitiv type, inden for hvilket område alle opregningsværdier passer; Compileren bestemmer hvilken type der skal bruges. Eksplicitte værdier for konstanter skal dog være udtryk som int[18] .
En opregningstype kan også være anonym, hvis opregningsnavnet ikke er angivet. Konstanter angivet i to forskellige enums er af to forskellige datatyper, uanset om enumsene er navngivne eller anonyme.
I praksis bruges optællinger ofte til at angive tilstande af endelige automater , til at indstille muligheder for driftstilstande eller parameterværdier [54] , til at skabe heltalskonstanter og også til at opregne eventuelle unikke objekter eller egenskaber [55] .
StrukturerStrukturer er en kombination af variabler af forskellige datatyper inden for det samme hukommelsesområde; angivet med nøgleordet struct. Variabler inden for en struktur kaldes strukturens felter. Fra adresserummets synspunkt følger felterne altid hinanden i samme rækkefølge, som de er angivet, men kompilatorer kan justere feltadresser for at optimere til en bestemt arkitektur. Således kan feltet faktisk have en større størrelse end angivet i programmet.
Hvert felt har en vis offset i forhold til adressen på strukturen og en størrelse. Forskydningen kan opnås ved hjælp af en makro offsetof()fra header-filen stddef.h. I dette tilfælde vil forskydningen afhænge af justeringen og størrelsen af de tidligere felter. Feltstørrelsen bestemmes normalt af strukturjusteringen: Hvis feltdatatypens justeringsstørrelse er mindre end strukturjusteringsværdien, så bestemmes feltstørrelsen af strukturjusteringen. Datatypejustering kan opnås ved hjælp af makroen alignof()[f] fra header-filen stdalign.h. Størrelsen af selve strukturen er den samlede størrelse af alle dens felter, inklusive justering. Samtidig giver nogle compilere specielle attributter, der giver dig mulighed for at pakke strukturer og fjerne justeringer fra dem [56] .
Strukturfelter kan udtrykkeligt indstilles til størrelse i bits adskilt af et kolon efter feltdefinitionen og antallet af bits, hvilket begrænser rækkevidden af deres mulige værdier, uanset feltets type. Denne tilgang kan bruges som et alternativ til flag og bitmasker for at få adgang til dem. Angivelse af antallet af bit annullerer imidlertid ikke den mulige justering af felterne af strukturer i hukommelsen. Arbejde med bitfelter har en række begrænsninger: det er umuligt at anvende en operator sizeofeller makro alignof()på dem, det er umuligt at få en pointer til dem.
ForeningerUnioner er nødvendige, når du vil henvise til den samme variabel som forskellige datatyper; angivet med nøgleordet union. Et vilkårligt antal krydsende felter kan erklæres inde i fagforeningen, som faktisk giver adgang til det samme hukommelsesområde som forskellige datatyper. Foreningens størrelse vælges af compileren ud fra størrelsen af det største felt i fagforeningen. Man skal huske på, at ændring af ét felt i fagforeningen fører til en ændring på alle andre områder, men kun værdien af det felt, der er ændret, er garanteret korrekt.
Fagforeninger kan tjene som et mere bekvemt alternativ til at kaste en pointer til en vilkårlig type. Ved at bruge en forening placeret i en struktur kan du for eksempel oprette objekter med en dynamisk skiftende datatype:
Strukturkode til at ændre datatype på farten #include <stddef.h> enum value_type_t { VALUE_TYPE_LONG , // heltal VALUE_TYPE_DOUBLE , // reelt tal VALUE_TYPE_STRING , // streng VALUE_TYPE_BINARY , // vilkårlige data }; struktur binær_t { void * data ; // peger på data size_t data_size ; // datastørrelse }; struct string_t { char * str ; // markør til streng størrelse_t str_størrelse ; // strengstørrelse }; union value_contents_t { long as_long ; // værdi som et heltal dobbelt som_dobbelt ; // værdi som reelt tal struct string_t as_string ; // værdi som streng struktur binær_t som_binær ; // værdi som vilkårlige data }; struktur værdi_t { enum værdi_type_t type ; // værdi type union value_contents_t indhold ; // værdi indhold }; ArraysArrays i C er primitive og er blot en syntaktisk abstraktion over pointer-aritmetik . Et array i sig selv er en pointer til et hukommelsesområde, så al information om arraydimensionen og dens grænser kan kun tilgås på kompileringstidspunktet i henhold til typedeklarationen. Arrays kan være enten endimensionelle eller multidimensionale, men adgang til et array-element kommer ned til blot at beregne offset i forhold til adressen på begyndelsen af arrayet. Da arrays er baseret på adressearitmetik, er det muligt at arbejde med dem uden at bruge indeks [57] . Så for eksempel er følgende to eksempler på at læse 10 tal fra inputstrømmen identiske med hinanden:
Sammenligning af arbejde gennem indekser med arbejde gennem adressearitmetikEksempelkode til at arbejde gennem indekser | Eksempelkode til at arbejde med adressearitmetik |
---|---|
#include <stdio.h> int a [ 10 ] = { 0 }; // Nul initialisering unsigned int count = sizeof ( a ) / sizeof ( a [ 0 ]); for ( int i = 0 ; i < antal ; ++ i ) { int * ptr = &a [ i ]; // Pointer til det aktuelle array-element int n = scanf ( "%8d" , ptr ); if ( n != 1 ) { perror ( "Kunne ikke læse værdi" ); // Håndtering af fejlen pause ; } } | #include <stdio.h> int a [ 10 ] = { 0 }; // Nul initialisering unsigned int count = sizeof ( a ) / sizeof ( a [ 0 ]); int * a_end = a + tæller ; // Pointer til elementet efter det sidste for ( int * ptr = a ; ptr != a_end ; ++ ptr ) { int n = scanf ( "%8d" , ptr ); if ( n != 1 ) { perror ( "Kunne ikke læse værdi" ); // Håndtering af fejlen pause ; } } |
Længden af arrays med en kendt størrelse beregnes på kompileringstidspunktet. C99-standarden introducerede muligheden for at erklære arrays med variabel længde, hvis længde kan indstilles under kørsel. Sådanne arrays tildeles hukommelse fra stakområdet, så de skal bruges med forsigtighed, hvis deres størrelse kan indstilles uden for programmet. I modsætning til dynamisk hukommelsesallokering kan overskridelse af den tilladte størrelse i stakområdet føre til uforudsigelige konsekvenser, og en negativ matrixlængde er udefineret adfærd . Startende med C11 er arrays med variabel længde valgfrie for compilere, og manglende understøttelse bestemmes af tilstedeværelsen af en makro __STDC_NO_VLA__[58] .
Arrays med fast størrelse, der er erklæret som lokale eller globale variabler, kan initialiseres ved at give dem en startværdi ved hjælp af krøllede parenteser og liste array-elementer adskilt af kommaer. Globale array-initialisatorer kan kun bruge udtryk, der evalueres på kompileringstidspunktet [59] . Variabler brugt i sådanne udtryk skal erklæres som konstanter med modifikatoren const. For lokale arrays kan initialiseringsprogrammer indeholde udtryk med funktionskald og brugen af andre variabler, herunder en pointer til selve det erklærede array.
Siden C99-standarden er det tilladt at erklære et array af vilkårlig længde som det sidste element i strukturer, som er meget udbredt i praksis og understøttet af forskellige compilere. Størrelsen af et sådant array afhænger af mængden af hukommelse, der er allokeret til strukturen. I dette tilfælde kan du ikke deklarere en række af sådanne strukturer, og du kan ikke placere dem i andre strukturer. I operationer på en sådan struktur ignoreres et array af vilkårlig længde sædvanligvis, også når størrelsen af strukturen beregnes, og at gå ud over arrayet medfører udefineret adfærd [60] .
C-sproget giver ikke nogen kontrol over array out-of-bounds, så programmøren skal selv overvåge arbejdet med arrays. Fejl i array-behandling påvirker ikke altid programmets eksekvering direkte, men kan føre til segmenteringsfejl og sårbarheder .
Indtast synonymerC-sproget giver dig mulighed for at oprette dine egne typenavne med typedef. Alternative navne kan gives til både systemtyper og brugerdefinerede. Sådanne navne erklæres i det globale navneområde og er ikke i konflikt med navnene på struktur, opregning og foreningstyper.
Alternative navne kan bruges både til at forenkle koden og til at skabe abstraktionsniveauer. For eksempel kan nogle systemtyper forkortes for at gøre koden mere læsbar eller for at gøre den mere ensartet i brugerkoden:
#include <stdint.h> typedef int32_t i32_t ; typedef int_fast32_t i32fast_t ; typedef int_mindste32_t i32mindste_t ; typedef uint32_t u32_t ; typedef uint_fast32_t u32fast_t ; typedef uint_mindst32_t u32mindst_t ;Et eksempel på abstraktion er typenavnene i styresystemernes overskriftsfiler. For eksempel definerer POSIXpid_t -standarden en type til lagring af et numerisk proces-id. Faktisk er denne type et alternativt navn til en primitiv type, for eksempel:
typedef int __kernel_pid_t ; typedef __kernel_pid_t __pid_t typedef __pid_t pid_t ;Da typer med alternative navne kun er synonymer for de originale typer, bevares fuld kompatibilitet og udskiftelighed mellem dem.
Præprocessoren arbejder før kompilering og transformerer teksten i programfilen i overensstemmelse med de direktiver , der er stødt på i den eller videregivet til præprocessoren . Teknisk set kan præprocessoren implementeres på forskellige måder, men det er logisk praktisk at tænke på det som et separat modul, der behandler hver fil beregnet til kompilering og danner den tekst, der så kommer ind i compilerens input. Forbehandleren leder efter linjer i teksten, der begynder med et tegn #, efterfulgt af præprocessor-direktiver. Alt, hvad der ikke hører til præprocessor-direktiverne og ikke er udelukket fra kompilering i henhold til direktiverne, videregives uændret til compilerinput.
Preprocessor funktioner omfatter:
Det er vigtigt at forstå, at præprocessoren kun giver tekstsubstitution, uden at der tages hensyn til sprogets syntaks og semantik. Så for eksempel #definekan makrodefinitioner forekomme i funktioner eller typedefinitioner, og betingede kompileringsdirektiver kan føre til udelukkelse af enhver del af koden fra den kompilerede tekst af programmet, uden hensyntagen til sprogets grammatik. Kaldning af en parametrisk makro er også forskellig fra at kalde en funktion, fordi semantikken af de kommaseparerede argumenter ikke parses. Så det er for eksempel umuligt at overføre initialiseringen af et array til argumenterne for en parametrisk makro, da dens elementer også er adskilt af et komma:
#define array_of(type, array) (((type) []) (array)) int * a ; a = array_of ( int , { 1 , 2 , 3 }); // kompileringsfejl: // "array_of" makro bestod 4 argumenter, men det tager kun 2Makrodefinitioner bruges ofte til at sikre kompatibilitet med forskellige versioner af biblioteker, der har ændret API'er , inklusive visse sektioner af kode afhængigt af bibliotekets version. Til disse formål giver biblioteker ofte makrodefinitioner, der beskriver deres version [61] , og nogle gange makroer med parametre til at sammenligne den aktuelle version med den, der er specificeret i præprocessoren [62] . Makrodefinitioner bruges også til betinget kompilering af individuelle dele af programmet, for eksempel for at muliggøre understøttelse af yderligere funktionalitet.
Makrodefinitioner med parametre er meget brugt i C-programmer til at skabe analoger til generiske funktioner . Tidligere blev de også brugt til at implementere inline-funktioner, men siden C99-standarden er dette behov blevet elimineret på grund af tilføjelsen af inline-funktioner. Men på grund af at makrodefinitioner med parametre ikke er funktioner, men kaldes på lignende måde, kan der opstå uventede problemer på grund af programmørfejl, herunder kun at behandle en del af koden fra makrodefinitionen [63] og forkerte prioriteringer vedr. udførelse af operationer [64] . Et eksempel på en fejlagtig kode er kvadratisk makro:
#include <stdio.h> int main ( ugyldig ) { #define SQR(x) x * x printf ( "%d" , SQR ( 5 )); // alt er korrekt, 5*5=25 printf ( "%d" , SQR ( 5 + 0 )); // formodes at være 25, men vil udlæse 5 (5+0*5+0) printf ( "%d" , SQR ( 4/3 ) ) ; // alt er korrekt, 1 (fordi 4/3=1, 1*4=4, 4/3=1) printf ( "%d" , SQR ( 5/2 ) ) ; // formodes at være 4 (2*2), men vil udlæse 5 (5/2*5/2) returnere 0 ; }I ovenstående eksempel er fejlen, at indholdet af makroargumentet er erstattet i teksten, som det er, uden at tage højde for operationernes forrang. I sådanne tilfælde skal du bruge inline-funktioner eller eksplicit prioritere operatorer i udtryk, der bruger makroparametre ved hjælp af parenteser:
#include <stdio.h> int main ( ugyldig ) { #define SQR(x) ((x) * (x)) printf ( "%d" , SQR ( 4 + 1 )); // sandt, 25 returnere 0 ; }Et program er et sæt C-filer, der kan kompileres til objektfiler . Objektfilerne gennemgår derefter et sammenkædningstrin med hinanden såvel som med eksterne biblioteker, hvilket resulterer i den endelige eksekverbare eller bibliotek . Sammenkobling af filer med hinanden, såvel som med biblioteker, kræver en beskrivelse af prototyperne af de anvendte funktioner, eksterne variabler og de nødvendige datatyper i hver fil. Det er sædvanligt at placere sådanne data i separate header-filer , som er forbundet ved hjælp af et direktiv #include i de filer, hvor denne eller hin funktionalitet er påkrævet, og giver dig mulighed for at organisere et system, der ligner et modulsystem. I dette tilfælde kan modulet være:
Da direktivet #includekun erstatter teksten i en anden fil på forbehandlingsstadiet , kan det føre til kompileringsfejl, hvis du inkluderer den samme fil flere gange. Derfor bruger sådanne filer beskyttelse mod genaktivering ved hjælp af makroer #define og #ifndef[65] .
KildekodefilerBrødteksten i en C-kildekodefil består af et sæt globale datadefinitioner, -typer og -funktioner. Globale variabler og funktioner erklæret med og-specifikationerne staticer inlinekun tilgængelige i den fil, hvori de er erklæret, eller når en fil er inkluderet i en anden via #include. I dette tilfælde vil de funktioner og variabler, der er erklæret i header-filen med ordet static, blive oprettet på ny, hver gang header-filen forbindes med den næste fil med kildekoden. Globale variabler og funktionsprototyper, der er erklæret med den eksterne specifikation, betragtes som inkluderet fra andre filer. Det vil sige, at de må bruges i overensstemmelse med beskrivelsen; det antages, at efter at programmet er bygget, vil de blive forbundet af linkeren med de originale objekter og funktioner, der er beskrevet i deres filer.
Globale variabler og funktioner, bortset fra staticog inline, kan tilgås fra andre filer, forudsat at de er korrekt erklæret der med specificatoren extern. Variabler og funktioner, der er erklæret med modifikatoren, statickan også tilgås i andre filer, men kun når deres adresse sendes af pointer. Typedeklarationer typedef, structog unionkan ikke importeres i andre filer. Hvis det er nødvendigt at bruge dem i andre filer, skal de duplikeres der eller placeres i en separat header-fil. Det samme gælder inline-funktioner.
ProgramindgangspunktFor et eksekverbart program er standardindgangspunktet en funktion ved navn main, som ikke kan være statisk og skal være den eneste i programmet. Udførelsen af programmet starter fra den første sætning af funktionen main()og fortsætter, indtil den afsluttes, hvorefter programmet afsluttes og returnerer til operativsystemet en abstrakt heltalskode af resultatet af dets arbejde.
ingen argumenter | Med kommandolinjeargumenter |
---|---|
int main ( ugyldig ); | int main ( int argc , char ** argv ); |
Når den kaldes, overføres variablen argcantallet af argumenter, der er sendt til programmet, inklusive stien til selve programmet, så argc-variablen indeholder normalt en værdi, der ikke er mindre end 1. Selve argvprogramstartlinjen sendes til variablen som et array af tekststrenge, hvis sidste element er NULL. Compileren garanterer, at main()alle globale variabler i programmet vil blive initialiseret , når funktionen køres [67] .
Som et resultat kan funktionen main()returnere et hvilket som helst heltal i rækken af værdier af typen int, som vil blive videregivet til operativsystemet eller et andet miljø som programmets returkode [66 ] . Sprogstandarden definerer ikke betydningen af returkoder [68] . Normalt har operativsystemet, hvor programmerne kører, nogle midler til at få værdien af returkoden og analysere den. Nogle gange er der visse konventioner om betydningen af disse koder. Den generelle konvention er, at en returkode på nul indikerer en vellykket afslutning af programmet, mens en værdi, der ikke er nul, repræsenterer en fejlkode. Header-filen stdlib.hdefinerer to generelle makrodefinitioner EXIT_SUCCESSog EXIT_FAILURE, som svarer til vellykket og mislykket afslutning af programmet [68] . Returkoder kan også bruges i applikationer, der inkluderer flere processer for at give kommunikation mellem disse processer, i hvilket tilfælde applikationen selv bestemmer den semantiske betydning for hver returkode.
C giver 4 måder at allokere hukommelse på, som bestemmer levetiden for en variabel og det øjeblik den initialiseres [67] .
Udvælgelsesmetode | Mål | Udvælgelsestid | frigivelsestid | Overhead |
---|---|---|---|---|
Statisk hukommelsestildeling | Globale variabler og variabler markeret med søgeord static(men uden _Thread_local) | Ved programstart | I slutningen af programmet | Mangler |
Hukommelsestildeling på trådniveau | Variabler markeret med nøgleord_Thread_local | Når tråden starter | For enden af åen | Når du opretter en tråd |
Automatisk hukommelsestildeling | Funktionsargumenter og returværdier, lokale variabler af funktioner, herunder registre og arrays med variabel længde | Når du kalder funktioner på stakniveau . | Automatisk efter afslutning af funktioner | Ubetydeligt, da kun markøren til toppen af stakken ændres |
Dynamisk hukommelsestildeling | Hukommelse tildelt gennem funktioner malloc(), calloc()ogrealloc() | Manuelt fra bunken i det øjeblik, den brugte funktion kaldes. | Manuel brug af funktionenfree() | Stor til både tildeling og frigivelse |
Alle disse datalagringsmetoder er velegnede i forskellige situationer og har deres egne fordele og ulemper. Globale variabler tillader dig ikke at skrive reentrant -algoritmer, og automatisk hukommelsesallokering tillader dig ikke at returnere et vilkårligt hukommelsesområde fra et funktionskald. Autoallokering er heller ikke egnet til at allokere store mængder hukommelse, da det kan føre til stak- eller heap-korruption [69] . Dynamisk hukommelse har ikke disse mangler, men den har en stor overhead, når den bruges og er sværere at bruge.
Hvor det er muligt, foretrækkes automatisk eller statisk hukommelsesallokering: denne måde at gemme objekter på styres af compileren , hvilket fritager programmøren for besværet med manuelt at allokere og frigøre hukommelse, som normalt er kilden til svære at finde hukommelseslækager , segmenteringsfejl og genfrigørelse af fejl i programmet . Desværre er mange datastrukturer variable i størrelse under kørsel, så fordi automatisk og statisk allokerede områder skal have en kendt fast størrelse på kompileringstidspunktet, er det meget almindeligt at bruge dynamisk allokering.
For automatisk allokerede variabler kan en modifikator registerbruges til at antyde, at compileren hurtigt får adgang til dem. Sådanne variabler kan placeres i processorregistre. På grund af det begrænsede antal registre og mulige compiler-optimeringer kan variabler ende i almindelig hukommelse, men alligevel vil det ikke være muligt at få en pointer til dem fra programmet [70] . Modifikatoren registerer den eneste, der kan specificeres i funktionsargumenter [71] .
HukommelsesadresseringC-sproget arvede lineær hukommelsesadressering, når man arbejdede med strukturer, arrays og tildelte hukommelsesområder. Sprogstandarden gør det også muligt at udføre sammenligningsoperationer på nul-pointere og på adresser inden for arrays, strukturer og tildelte hukommelsesområder. Det er også tilladt at arbejde med adressen på array-elementet efter det sidste, hvilket gøres for at lette skrivealgoritmer. Sammenligning af adressepointere opnået for forskellige variabler (eller hukommelsesområder) bør dog ikke udføres, da resultatet vil afhænge af implementeringen af en bestemt compiler [72] .
HukommelsesrepræsentationHukommelsesrepræsentationen af et program afhænger af hardwarearkitekturen, af operativsystemet og af compileren. Så for eksempel på de fleste arkitekturer vokser stakken ned, men der er arkitekturer hvor stakken vokser op [73] . Grænsen mellem stak og heap kan delvist beskyttes mod stak overløb af et særligt hukommelsesområde [74] . Og placeringen af bibliotekernes data og kode kan afhænge af kompileringsmulighederne [75] . C-standarden abstraherer fra implementeringen og giver dig mulighed for at skrive bærbar kode, men forståelse af hukommelsesstrukturen i en proces hjælper med at fejlfinde og skrive sikre og fejltolerante applikationer.
Typisk repræsentation af proceshukommelse i Unix-lignende operativsystemerNår et program startes fra en eksekverbar fil, importeres processorinstruktioner (maskinkode) og initialiserede data til RAM. Samtidig importeres kommandolinjeargumenter (tilgængelige i funktioner main()med følgende signatur i det andet argument int argc, char ** argv) og miljøvariabler til højere adresser.
Det ikke-initialiserede dataområde indeholder globale variabler (inklusive dem, der er erklæret som static), der ikke er blevet initialiseret i programkoden. Sådanne variable initialiseres som standard til nul efter programmets start. Området med initialiserede data - datasegmentet - indeholder også globale variabler, men dette område inkluderer de variabler, der har fået en startværdi. Uforanderlige data, inklusive variable, der er erklæret med modifikatoren const, strengliteraler og andre sammensatte literaler, placeres i programtekstsegmentet. Programtekstsegmentet indeholder også eksekverbar kode og er skrivebeskyttet, så et forsøg på at ændre data fra dette segment vil resultere i udefineret adfærd i form af en segmenteringsfejl .
Stakområdet er beregnet til at indeholde data forbundet med funktionskald og lokale variabler. Før hver funktionsudførelse udvides stakken for at rumme de argumenter, der sendes til funktionen. I løbet af sit arbejde kan funktionen allokere lokale variable på stakken og allokere hukommelse på den til arrays af variabel længde, og nogle compilere giver også midler til at allokere hukommelse i stakken gennem et kald alloca(), der ikke er inkluderet i sprogstandarden . Efter funktionen er afsluttet, reduceres stakken til den værdi, der var før opkaldet, men det sker muligvis ikke, hvis stakken håndteres forkert. Hukommelse, der er allokeret dynamisk, leveres fra heapen .
En vigtig detalje er tilstedeværelsen af tilfældig polstring mellem stakken og det øverste område [77] såvel som mellem det initialiserede dataområde og heapen . Dette gøres af sikkerhedsmæssige årsager, såsom at forhindre andre funktioner i at blive stablet.
Dynamiske linkbiblioteker og filsystemfiltilknytninger sidder mellem stakken og heapen [78] .
C har ingen indbyggede fejlkontrolmekanismer, men der er flere almindeligt accepterede måder at håndtere fejl ved hjælp af sproget. Generelt tvinger praksis med at håndtere C-fejl i fejltolerant kode en til at skrive besværlige, ofte gentagne konstruktioner, hvor algoritmen kombineres med fejlhåndtering .
Fejlmarkører og errnoC-sproget bruger aktivt en speciel variabel errnofra header-filen errno.h, hvori funktioner indtaster fejlkoden, mens de returnerer en værdi, der er fejlmarkøren. For at kontrollere resultatet for fejl, sammenlignes resultatet med fejlmarkøren, og hvis de matcher, kan du analysere fejlkoden, der er gemt i, errnofor at rette programmet eller vise en fejlretningsmeddelelse. I standardbiblioteket definerer standarden ofte kun de returnerede fejlmarkører, og indstillingen errnoer implementeringsafhængig [79] .
Følgende værdier fungerer normalt som fejlmarkører:
Praksis med at returnere en fejlmarkør i stedet for en fejlkode, selvom den gemmer antallet af argumenter, der sendes til funktionen, fører i nogle tilfælde til fejl som følge af en menneskelig faktor. For eksempel er det almindeligt, at programmører ignorerer at kontrollere et resultat af typen ssize_t, og selve resultatet bruges videre i beregninger, hvilket fører til subtile fejl, hvis -1[82] returneres .
At returnere den korrekte værdi som en fejlmarkør [82] bidrager yderligere til fremkomsten af fejl , hvilket også tvinger programmøren til at foretage flere kontroller og følgelig skrive mere af den samme type gentagne kode. Denne tilgang praktiseres i strømfunktioner, der arbejder med objekter af typen FILE *: fejlmarkøren er værdien EOF, som også er slutningen af filen. Derfor er EOFdu nogle gange nødt til at kontrollere strømmen af tegn både for slutningen af filen ved hjælp af funktionen feof(), og for tilstedeværelsen af en fejl ved hjælp af ferror()[83] . Samtidig er nogle funktioner, der kan returnere EOFi henhold til standarden, ikke nødvendige for at indstille errno[79] .
Manglen på en samlet fejlhåndteringspraksis i standardbiblioteket fører til fremkomsten af brugerdefinerede fejlhåndteringsmetoder og kombinationen af almindeligt anvendte metoder i tredjepartsprojekter. For eksempel i systemd- projektet blev ideerne om at returnere en fejlkode og et tal -1som en markør kombineret - en negativ fejlkode returneres [84] . Og GLib- biblioteket introducerede praksis med at returnere en boolesk værdi som en fejlmarkør , mens detaljerne om fejlen er placeret i en speciel struktur, hvortil pointeren returneres gennem det sidste argument i funktionen [85] . En lignende løsning bruges af Oplysningsprojektet , som også bruger en boolsk type som markør, men returnerer fejlinformation svarende til standardbiblioteket - gennem en separat funktion [86] , der skal kontrolleres, hvis en markør blev returneret.
Returnerer en fejlkodeEt alternativ til fejlmarkører er at returnere fejlkoden direkte og returnere resultatet af funktionen gennem pointer-argumenter. Udviklerne af POSIX-standarden tog denne vej, i hvis funktioner det er sædvanligt at returnere en fejlkode som et antal af typen int. At returnere en inttypeværdi gør det dog ikke eksplicit, at det er fejlkoden, der returneres, og ikke tokenet, der kan føre til fejl, hvis resultatet af sådanne funktioner kontrolleres mod værdien -1. Udvidelse K af C11-standarden introducerer en speciel type errno_ttil lagring af en fejlkode. Der er anbefalinger til at bruge denne type i brugerkode til at returnere fejl, og hvis den ikke leveres af standardbiblioteket, så erklær det selv [87] :
#ifndef __STDC_LIB_EXT1__ typedef int errno_t ; #Afslut HvisDenne tilgang, ud over at forbedre kvaliteten af koden, eliminerer behovet for at bruge errno, som giver dig mulighed for at lave biblioteker med genindtrædende funktioner uden behov for at inkludere yderligere biblioteker, såsom POSIX Threads , for korrekt at definere errno.
Fejl i matematiske funktionerMere kompleks er håndteringen af fejl i matematiske funktioner fra header-filen math.h, hvor 3 typer fejl kan forekomme [88] :
Forebyggelse af to af de tre typer fejl kommer ned til at kontrollere inputdataene for rækken af gyldige værdier. Det er dog ekstremt vanskeligt at forudsige resultatet af resultatet ud over typens grænser. Derfor giver sprogstandarden mulighed for at analysere matematiske funktioner for fejl. Begyndende med C99-standarden er denne analyse mulig på to måder, afhængigt af værdien gemt i math_errhandling.
I dette tilfælde er metoden til fejlhåndtering bestemt af den specifikke implementering af standardbiblioteket og kan være fuldstændig fraværende. Derfor kan det i platformsuafhængig kode være nødvendigt at kontrollere resultatet på to måder på én gang, afhængigt af værdien af math_errhandling[88] .
Frigivelse af ressourcerTypisk kræver forekomsten af en fejl, at funktionen afsluttes og returnerer en fejlindikator. Hvis der i en funktion kan opstå en fejl i forskellige dele af den, er det nødvendigt at frigive de ressourcer, der er allokeret under dens drift for at forhindre lækager. Det er god praksis at frigøre ressourcer i omvendt rækkefølge, før du vender tilbage fra funktionen, og i tilfælde af fejl, i omvendt rækkefølge efter den primære return. I separate dele af en sådan udgivelse kan du hoppe ved hjælp af operatøren goto[89] . Denne tilgang giver dig mulighed for at flytte kodesektioner, der ikke er relateret til den algoritme, der implementeres, uden for selve algoritmen, hvilket øger kodens læsbarhed og ligner arbejdet for en operatør deferfra Go -programmeringssproget . Et eksempel på frigørelse af ressourcer er givet nedenfor, i eksempelafsnittet .
For at frigive ressourcer i programmet er der tilvejebragt en programafslutningshåndteringsmekanisme. Handlere tildeles ved hjælp af en funktion atexit()og udføres både i slutningen af funktionen main()gennem en sætning returnog ved udførelse af funktionen exit(). I dette tilfælde udføres handlerne ikke af funktionerne abort()og _Exit()[90] .
Et eksempel på frigørelse af ressourcer i slutningen af et program er frigørelse af hukommelse, der er allokeret til globale variabler. På trods af at hukommelsen frigøres på den ene eller anden måde efter programmet er afsluttet af operativsystemet, og det er tilladt ikke at frigøre den hukommelse, der kræves under hele programmets drift [91] , er eksplicit deallokering at foretrække, da det gør det lettere at finde hukommelseslækager af tredjepartsværktøjer og reducerer chancen for hukommelseslækager som følge af en fejl:
Eksempel på programkode med ressourcefrigivelse #include <stdio.h> #include <stdlib.h> int tal_antal ; int * tal ; void free_numbers ( void ) { gratis ( tal ); } int main ( int argc , char ** argv ) { if ( arg < 2 ) { exit ( EXIT_FAILURE ); } tal_antal = atoi ( argv [ 1 ]); if ( tal_antal <= 0 ) { exit ( EXIT_FAILURE ); } tal = calloc ( numre_antal , størrelse på ( * tal )); if ( ! tal ) { perror ( "Fejl ved allokering af hukommelse til array" ); exit ( EXIT_FAILURE ); } atexit ( frie_numre ); // ... arbejde med tal-array // Behandleren free_numbers() vil automatisk blive kaldt her returner EXIT_SUCCESS ; }Ulempen ved denne tilgang er, at formatet af tildelte handlere ikke giver mulighed for at sende vilkårlige data til funktionen, hvilket giver dig mulighed for kun at oprette handlere for globale variabler.
Et minimalt C-program, der ikke kræver argumentbehandling, er som følger:
int main ( ugyldig ){}Det er tilladt ikke at skrive en operator returnfor funktionen main(). I dette tilfælde returnerer funktionen ifølge standarden main()0 og udfører alle de behandlere, der er tildelt funktionen exit(). Dette forudsætter, at programmet er gennemført med succes [40] .
Hej Verden!Hej verden! er givet i den første udgave af bogen " The C Programming Language " af Kernighan og Ritchie:
#include <stdio.h> int main ( void ) // Tager ingen argumenter { printf ( "Hej verden! \n " ); // '\n' - ny linje returnerer 0 ; // Vellykket programafslutning }Dette program udskriver meddelelsen Hej, verden! ' på standard output .
Fejlhåndtering ved brug af fillæsning som eksempelMange C-funktioner kan returnere en fejl uden at gøre, hvad de skulle. Fejl skal kontrolleres og reageres korrekt, herunder ofte behovet for at kaste en fejl fra en funktion til et højere niveau for analyse. Samtidig kan funktionen, hvori en fejl opstod, gøres reentrant , i hvilket tilfælde, ved en fejl, bør funktionen ikke ændre input- eller outputdataene, hvilket giver dig mulighed for sikkert at genstarte den efter at have rettet fejlsituationen.
Eksemplet implementerer funktionen til at læse en fil i C, men det kræver, at funktionerne fopen()og POSIXfread() - standarden overholder , ellers indstiller de muligvis ikke variablen , hvilket i høj grad komplicerer både fejlfinding og skrivning af universel og sikker kode. På ikke-POSIX-platforme vil opførselen af dette program være udefineret i tilfælde af en fejl . Deallokering af ressourcer på fejl ligger bag hovedalgoritmen for at forbedre læsbarheden, og overgangen sker ved hjælp af [89] . errnogoto
Eksempelkode for fillæser med fejlhåndtering #include <errno.h> #include <stdio.h> #include <stdlib.h> // Definer typen til at gemme fejlkoden, hvis den ikke er defineret #ifndef __STDC_LIB_EXT1__ typedef int errno_t ; #Afslut Hvis enum { EOK = 0 , // værdi af errno_t ved succes }; // Funktion til at læse indholdet af filen errno_t get_file_contents ( const char * filnavn , void ** contents_ptr , size_t * contents_size_ptr ) { FIL * f ; f = fopen ( filnavn , "rb" ); hvis ( ! f ) { // I POSIX sætter fopen() errno ved en fejl returnere errno ; } // Hent filstørrelse fseek ( f , 0 , SEEK_END ); long contents_size = ftell ( f ); if ( indholdsstørrelse == 0 ) { * contents_ptr = NULL ; * contents_size_ptr = 0 ; goto cleaning_fopen ; } spole tilbage ( f ); // Variabel for at gemme den returnerede fejlkode errno_t saved_errno ; void * indhold ; indhold = malloc ( indholdsstørrelse ); if ( ! indhold ) { saved_errno = fejlno ; goto abort_fopen ; } // Læs hele indholdet af filen ved indholdsmarkøren størrelse_t n ; n = fread ( indhold , indhold_størrelse , 1 , f ); if ( n == 0 ) { // Kontroller ikke for feof(), fordi bufferet efter fseek() // POSIX fread() indstiller errno ved en fejltagelse saved_errno = fejlno ; goto aborting_contents ; } // Returner den tildelte hukommelse og dens størrelse * contents_ptr = indhold ; * contents_size_ptr = contents_size ; // Ressourcefrigivelsessektion om succes cleaning_fopen : flukke ( f ); returnere EOK ; // Separat sektion for at frigøre ressourcer ved en fejltagelse aborteringsindhold : gratis ( indhold ); abort_fopen : flukke ( f ); return saved_errno ; } int main ( int argc , char ** argv ) { if ( arg < 2 ) { returner EXIT_FAILURE ; } const char * filnavn = argv [ 1 ]; errno_t errnum ; void * indhold ; size_t contents_size ; errnum = get_file_contents ( filnavn , & indhold , & indholdsstørrelse ); if ( errnum ) { charbuf [ 1024 ] ; const char * error_text = strerror_r ( errnum , buf , sizeof ( buf )); fprintf ( stderr , "%s \n " , fejltekst ); exit ( EXIT_FAILURE ); } printf ( "%.*s" , ( int ) contents_size , contents ); gratis ( indhold ); returner EXIT_SUCCESS ; }Nogle compilere er bundtet med compilere til andre programmeringssprog (inklusive C++ ) eller er en del af softwareudviklingsmiljøet .
|
|
På trods af at standardbiblioteket er en del af sprogstandarden, er dets implementeringer adskilt fra compilere. Derfor kan de sprogstandarder, der understøttes af compileren og biblioteket, være forskellige.
Da C-sproget ikke giver et middel til at skrive kode sikkert, og mange elementer i sproget bidrager til fejl, kan skrivning af høj kvalitet og fejltolerant kode kun garanteres ved at skrive automatiserede tests. For at lette sådan test er der forskellige implementeringer af tredjeparts enhedstestbiblioteker .
Der er også mange andre systemer til test af C-kode, såsom AceUnit, GNU Autounit, cUnit og andre, men de tester enten ikke i isolerede miljøer, giver få funktioner [100] eller udvikles ikke længere.
FejlfindingsværktøjerVed manifestationer af fejl er det ikke altid muligt at drage en entydig konklusion om problemområdet i koden, men forskellige fejlfindingsværktøjer hjælper ofte med at lokalisere problemet.
Nogle gange, for at overføre visse biblioteker, funktioner og værktøjer skrevet i C til et andet miljø, er det nødvendigt at kompilere C-koden til et sprog på højere niveau eller ind i koden på en virtuel maskine designet til et sådant sprog. Følgende projekter er designet til dette formål:
Også for C er der andre værktøjer, der letter og supplerer udvikling, herunder statiske analysatorer og hjælpeprogrammer til kodeformatering. Statisk analyse hjælper med at identificere potentielle fejl og sårbarheder. Og automatisk kodeformatering forenkler organiseringen af samarbejdet i versionskontrolsystemer, hvilket minimerer konflikter på grund af stilændringer.
Sproget er meget udbredt i udvikling af operativsystemer, på operativsystem API-niveau, i indlejrede systemer og til at skrive højtydende eller fejlkritisk kode. En af grundene til den udbredte anvendelse af lavniveauprogrammering er evnen til at skrive kode på tværs af platforme, der kan håndteres forskelligt på forskellig hardware og operativsystemer.
Evnen til at skrive højtydende kode kommer på bekostning af fuldstændig handlefrihed for programmøren og fraværet af streng kontrol fra compileren. For eksempel blev de første implementeringer af Java , Python , Perl og PHP skrevet i C. Samtidig er de mest ressourcekrævende dele i mange programmer normalt skrevet i C. Kernen i Mathematica [109] er skrevet i C, mens MATLAB , oprindeligt skrevet i Fortran , blev omskrevet i C i 1984 [110] .
C bruges også nogle gange som et mellemsprog, når der kompileres sprog på højere niveau. For eksempel fungerede de første implementeringer af C++ , Objective-C og Go - sprogene efter dette princip - koden skrevet på disse sprog blev oversat til en mellemrepræsentation på C-sproget. Moderne sprog, der arbejder efter samme princip, er Vala og Nim .
Et andet anvendelsesområde for C-sproget er realtidsapplikationer , som er krævende med hensyn til kodens reaktionstid og dens eksekveringstid. Sådanne ansøgninger skal påbegynde udførelsen af handlinger inden for en strengt begrænset tidsramme, og selve handlingerne skal passe inden for en vis tidsperiode. Især POSIX.1- standarden giver et sæt funktioner og muligheder til at bygge realtidsapplikationer [111] [112] [113] , men hård realtidssupport skal også implementeres af operativsystemet [114] .
C-sproget har været og forbliver et af de mest udbredte programmeringssprog i mere end fyrre år. Naturligvis kan dens indflydelse til en vis grad spores på mange senere sprog. Ikke desto mindre er der blandt de sprog, der har nået en vis fordeling, få direkte efterkommere af C.
Nogle efterkommersprog bygger på C med yderligere værktøjer og mekanismer, der tilføjer understøttelse af nye programmeringsparadigmer ( OOP , funktionel programmering , generisk programmering osv.). Disse sprog inkluderer primært C++ og Objective-C , og indirekte deres efterkommere Swift og D. Der er også kendte forsøg på at forbedre C ved at rette dets væsentligste mangler, men bevare dets attraktive funktioner. Blandt dem kan nævnes forskningssproget Cyclone (og dets efterkommer Rust ). Nogle gange kombineres begge udviklingsretninger på ét sprog, Go er et eksempel .
Separat er det nødvendigt at nævne en hel gruppe sprog, der i større eller mindre grad har arvet den grundlæggende syntaks for C (brugen af krøllede klammeparenteser som afgrænsere af kodeblokke, deklaration af variabler, karakteristiske former for operatorer for, while, if, switchmed parametre i parentes, kombinerede operationer ++, --, +=, -=og andre), hvilket er grunden til, at programmer på disse sprog har et karakteristisk udseende, der specifikt er knyttet til C. Det er sprog som Java , JavaScript , PHP , Perl , AWK , C# . Faktisk er strukturen og semantikken af disse sprog meget forskellig fra C, og de er normalt beregnet til applikationer, hvor det originale C aldrig blev brugt.
C++ programmeringssproget blev skabt ud fra C og arvede dets syntaks og supplerede det med nye konstruktioner i Simula-67, Smalltalk, Modula-2, Ada, Mesa og Clu [116] . De vigtigste tilføjelser var støtte til OOP (klassebeskrivelse, multipel nedarvning, polymorfi baseret på virtuelle funktioner) og generisk programmering (skabelonmotor). Men udover dette er der lavet mange forskellige tilføjelser til sproget. I øjeblikket er C++ et af de mest udbredte programmeringssprog i verden og er positioneret som et almindeligt sprog med vægt på systemprogrammering [117] .
I starten bevarede C++ kompatibiliteten med C, hvilket blev angivet som en af fordelene ved det nye sprog. De første implementeringer af C++ oversatte simpelthen nye konstruktioner til ren C, hvorefter koden blev behandlet af en almindelig C-compiler. For at bevare kompatibiliteten nægtede skaberne af C++ at udelukke nogle af de ofte kritiserede funktioner i C fra det, i stedet for at skabe nye, "parallelle" mekanismer, der anbefales ved udvikling af ny C++ kode (skabeloner i stedet for makroer, eksplicit type casting i stedet for automatisk , standard biblioteksbeholdere i stedet for manuel dynamisk hukommelsesallokering og så videre). Men sprogene har siden udviklet sig uafhængigt, og nu er C og C++ af de seneste frigivne standarder kun delvist kompatible: der er ingen garanti for, at en C++-kompiler vil kunne kompilere et C-program med succes, og hvis det lykkes, er der ingen garanti for, at det kompilerede program vil køre korrekt. Særligt irriterende er nogle subtile semantiske forskelle, der kan føre til forskellig adfærd af den samme kode, som er syntaktisk korrekt for begge sprog. For eksempel har tegnkonstanter (tegn omgivet af enkelte anførselstegn) en type inti C og en type chari C++ , så mængden af hukommelse optaget af sådanne konstanter varierer fra sprog til sprog. [118] Hvis et program er følsomt over for størrelsen af en tegnkonstant, vil det opføre sig anderledes, når det kompileres med C- og C++-kompilatorerne.
Forskelle som disse gør det svært at skrive programmer og biblioteker, der kan kompilere og fungere på samme måde i både C og C++ , hvilket selvfølgelig forvirrer dem, der programmerer på begge sprog. Blandt udviklere og brugere af både C og C++ er der fortalere for at minimere forskelle mellem sprog, hvilket objektivt set ville give håndgribelige fordele. Der er dog et modsat synspunkt, ifølge hvilket kompatibilitet ikke er særlig vigtigt, selvom det er nyttigt, og bestræbelser på at reducere inkompatibilitet bør ikke forhindre forbedring af hvert sprog individuelt.
En anden mulighed for at udvide C med objektbaserede værktøjer er Objective-C- sproget , der blev oprettet i 1983. Objektundersystemet er lånt fra Smalltalk , og alle de elementer, der er knyttet til dette undersystem, er implementeret i deres egen syntaks, som er ret skarpt forskellig fra C-syntaksen (op til det faktum, at i klassebeskrivelser er syntaksen for at deklarere felter modsat af syntaksen til at erklære variable i C: først skrives feltnavnet, derefter dets type). I modsætning til C++ er Objective-C et supersæt af klassisk C, det vil sige, at det bevarer kompatibiliteten med kildesproget; et korrekt C-program er et korrekt Objective-C-program. En anden væsentlig forskel fra C++-ideologien er, at Objective-C implementerer interaktionen af objekter ved at udveksle fuldgyldige beskeder, mens C++ implementerer konceptet med at "sende en besked som et metodekald". Fuld meddelelsesbehandling er meget mere fleksibel og passer naturligt med parallel computing. Objective-C, såvel som dets direkte efterkommer Swift , er blandt de mest populære på Apple -understøttede platforme .
C-sproget er unikt ved, at det var det første sprog på højt niveau, der for alvor fortrængte assembler i udviklingen af systemsoftware . Det forbliver det sprog, der er implementeret på det største antal hardwareplatforme og et af de mest populære programmeringssprog , især i den frie softwareverden [119] . Ikke desto mindre har sproget mange mangler; siden dets begyndelse er det blevet kritiseret af mange eksperter.
Sproget er meget komplekst og fyldt med farlige elementer, som er meget nemme at misbruge. Med sin struktur og regler understøtter det ikke programmering, der sigter mod at skabe pålidelig og vedligeholdelig programkode, tværtimod, født i en æra med direkte programmering for forskellige processorer, bidrager sproget til at skrive usikker og forvirrende kode [119] . Mange professionelle programmører har en tendens til at tro, at C-sproget er et kraftfuldt værktøj til at skabe elegante programmer, men samtidig kan det bruges til at skabe løsninger af ekstrem dårlig kvalitet [120] [121] .
På grund af forskellige antagelser i sproget kan programmer kompilere med flere fejl, hvilket ofte resulterer i uforudsigelig programadfærd. Moderne compilere giver muligheder for statisk kodeanalyse [122] [123] , men selv de er ikke i stand til at opdage alle mulige fejl. Analfabet C-programmering kan resultere i softwaresårbarheder , som kan påvirke sikkerheden ved dets brug.
Xi har en høj adgangstærskel [119] . Dens specifikation fylder mere end 500 sider med tekst, som skal studeres fuldt ud, da for at skabe fejlfri kode af høj kvalitet, skal mange ikke-indlysende egenskaber ved sproget tages i betragtning. For eksempel kan automatisk casting af operander af heltalsudtryk til type intgive vanskelige forudsigelige resultater ved brug af binære operatorer [44] :
usigneret char x = 0xFF ; usigneret char y = ( ~ x | 0x1 ) >> 1 ; // Intuitivt forventes 0x00 her printf ( "y = 0x%hhX \n " , y ); // Vil udskrive 0x80 hvis sizeof(int) > sizeof(char)Manglende forståelse af sådanne nuancer kan føre til adskillige fejl og sårbarheder. En anden faktor, der øger kompleksiteten ved at mestre C, er manglen på feedback fra compileren: Sproget giver programmøren fuldstændig handlefrihed og tillader kompilering af programmer med åbenlyse logiske fejl. Alt dette gør det vanskeligt at bruge C i undervisningen som det første programmeringssprog [119]
Endelig, over mere end 40 års eksistens, er sproget blevet noget forældet, og det er ret problematisk at bruge mange moderne programmeringsteknikker og paradigmer i det .
Der er ingen moduler og mekanismer for deres interaktion i C-syntaksen. Kildekodefiler kompileres separat og skal omfatte prototyper af variabler, funktioner og datatyper importeret fra andre filer. Dette gøres ved at inkludere header-filer via makrosubstitution . I tilfælde af en overtrædelse af korrespondancen mellem kodefiler og header-filer kan der opstå både linktidsfejl og alle former for runtime-fejl: fra stak- og heap -korruption til segmenteringsfejl . Da direktivet kun erstatter teksten fra én fil med en anden, fører inkluderingen af et stort antal header-filer til, at den faktiske mængde kode, der bliver kompileret, stiger mange gange, hvilket er årsagen til den relativt langsomme ydeevne af C compilere. Behovet for at koordinere beskrivelser i hovedmodulet og header-filer gør det vanskeligt at vedligeholde programmet. #include#include
Advarsler i stedet for fejlSprogstandarden giver programmøren mere handlefrihed og dermed stor chance for at lave fejl. Meget af det, der oftest ikke er tilladt, er tilladt af sproget, og compileren udsender i bedste fald advarsler. Selvom moderne compilere tillader, at alle advarsler konverteres til fejl, bruges denne funktion sjældent, og oftere end ikke ignoreres advarsler, hvis programmet kører tilfredsstillende.
Så f.eks. før C99-standarden kunne kald af en funktion mallocuden at inkludere en header-fil stdlib.hføre til stakkorruption, fordi i mangel af en prototype blev funktionen kaldt som returnerende en type int, mens den faktisk returnerede en type void*(en fejl opstod, da størrelserne af typer på målplatformen var forskellige). Alligevel var det kun en advarsel.
Manglende kontrol over initialisering af variablerAutomatisk og dynamisk oprettede objekter initialiseres ikke som standard og, når de er oprettet, indeholder de de værdier, der er tilbage i hukommelsen fra objekter, der tidligere var der. En sådan værdi er fuldstændig uforudsigelig, den varierer fra en maskine til en anden, fra kørsel til kørsel, fra funktionskald til kald. Hvis programmet bruger en sådan værdi på grund af en utilsigtet udeladelse af initialisering, vil resultatet være uforudsigeligt og vises muligvis ikke med det samme. Moderne compilere forsøger at diagnosticere dette problem ved statisk analyse af kildekoden, selvom det generelt er ekstremt vanskeligt at løse dette problem ved statisk analyse. Yderligere værktøjer kan bruges til at identificere disse problemer på teststadiet under programafviklingen: Valgrind og MemorySanitizer [124] .
Manglende kontrol over adressearitmetikKilden til farlige situationer er kompatibiliteten af pointere med numeriske typer og muligheden for at bruge adressearitmetik uden streng kontrol på stadierne af kompilering og udførelse. Dette gør det muligt at få en pointer til ethvert objekt, inklusive eksekverbar kode, og henvise til denne pointer, medmindre systemets hukommelsesbeskyttelsesmekanisme forhindrer dette.
Forkert brug af pointere kan forårsage udefineret programadfærd og føre til alvorlige konsekvenser. For eksempel kan en pointer være uinitialiseret eller, som et resultat af forkerte aritmetiske operationer, pege på en vilkårlig hukommelsesplacering. På nogle platforme kan arbejdet med en sådan pointer tvinge programmet til at stoppe, på andre kan det ødelægge vilkårlige data i hukommelsen; Den sidste fejl er farlig, fordi dens konsekvenser er uforudsigelige og kan dukke op på ethvert tidspunkt, herunder meget senere end tidspunktet for den faktiske fejlagtige handling.
Adgang til arrays i C er også implementeret ved hjælp af adressearitmetik og indebærer ikke midler til at kontrollere korrektheden af at få adgang til array-elementer ved hjælp af indeks. For eksempel er udtrykkene a[i]og i[a]identiske og er simpelthen oversat til formen *(a + i), og kontrollen for out-of-bounds-array udføres ikke. Adgang ved et indeks, der er større end den øvre grænse for arrayet, resulterer i adgang til data placeret i hukommelsen efter arrayet, hvilket kaldes et bufferoverløb . Når et sådant opkald er fejlagtigt, kan det føre til uforudsigelig programadfærd [57] . Ofte bruges denne funktion i udnyttelser , der bruges til ulovligt at få adgang til hukommelsen i et andet program eller hukommelsen i operativsystemkernen.
Fejltilbøjelig dynamisk hukommelseSystemfunktioner til at arbejde med dynamisk allokeret hukommelse giver ikke kontrol over rigtigheden og aktualiteten af dens tildeling og frigivelse, overholdelsen af den korrekte rækkefølge af arbejde med dynamisk hukommelse er helt og holdent programmørens ansvar. Dens fejl kan henholdsvis føre til adgang til forkerte adresser, til for tidlig frigivelse eller til en hukommelseslækage (sidstnævnte er for eksempel muligt, hvis udvikleren har glemt at ringe free()eller ringe til opkaldsfunktionen free(), når det var påkrævet) [125] .
En af de almindelige fejl er ikke at kontrollere resultatet af hukommelsesallokeringsfunktionerne ( malloc(), calloc()og andre) på NULL, mens hukommelsen muligvis ikke allokeres, hvis der ikke er nok af den, eller hvis der blev anmodet om for meget, f.eks. pga. reduktion af antallet -1modtaget som følge af fejlagtige matematiske operationer, til en usigneret type size_t, med efterfølgende operationer på den . Et andet problem med systemhukommelsesfunktioner er uspecificeret adfærd, når der anmodes om en blokallokering i nulstørrelse: Funktioner kan returnere enten eller en reel pointerværdi, afhængigt af den specifikke implementering [126] . NULL
Nogle specifikke implementeringer og tredjepartsbiblioteker giver funktioner såsom referencetælling og svage referencer [127] , smarte pointere [128] og begrænsede former for skraldindsamling [129] , men alle disse funktioner er ikke standard, hvilket naturligvis begrænser deres anvendelse. .
Ineffektive og usikre strengeFor sproget er nulterminerede strenge standard, så alle standardfunktioner arbejder med dem. Denne løsning fører til et betydeligt effektivitetstab på grund af ubetydelige hukommelsesbesparelser (sammenlignet med eksplicit lagring af størrelsen): beregning af længden af en streng (funktion ) kræver sløjfe gennem hele strengen fra start til slut, kopiering af strenge er også vanskelig at optimere på grund af tilstedeværelsen af et afsluttende nul [48] . På grund af behovet for at tilføje en afsluttende nul til strengdataene, bliver det umuligt effektivt at opnå delstrenge som udsnit og arbejde med dem som med almindelige strenge; allokering og manipulering af dele af strenge kræver normalt manuel allokering og deallokering af hukommelse, hvilket yderligere øger risikoen for fejl. strlen()
Nullterminerede strenge er en almindelig kilde til fejl [130] . Selv standardfunktioner kontrollerer normalt ikke størrelsen af målbufferen [130] og tilføjer muligvis ikke et null-tegn [131] i slutningen af strengen , for ikke at nævne, at det muligvis ikke tilføjes eller overskrives på grund af programmeringsfejl. [132] .
Usikker implementering af variadiske funktionerMens C understøtter funktioner med et variabelt antal argumenter , giver C hverken et middel til at bestemme antallet og typer af faktiske parametre, der sendes til en sådan funktion, eller en mekanisme til sikker adgang til dem [133] . At informere funktionen om sammensætningen af de faktiske parametre ligger hos programmøren, og for at få adgang til deres værdier er det nødvendigt at tælle det korrekte antal bytes fra adressen på den sidste faste parameter på stakken, enten manuelt eller ved hjælp af et sæt af makroer va_argfra header-filen stdarg.h. Samtidig er det nødvendigt at tage højde for driften af mekanismen for automatisk implicit typefremme, når funktioner kaldes [134] , ifølge hvilke heltalstyper af argumenter, der inter mindre end castet til int(eller unsigned int), men floatcastes til double. En fejl i opkaldet eller i arbejdet med parametre inde i funktionen vil kun dukke op under udførelsen af programmet, hvilket fører til uforudsigelige konsekvenser, fra at læse forkerte data til at ødelægge stakken.
printf()Samtidig er funktioner med et variabelt antal parametre ( , scanf()og andre), der ikke er i stand til at kontrollere, om listen over argumenter matcher formatstrengen , standardmidlerne for formateret I/O . Mange moderne compilere udfører denne kontrol for hvert opkald og genererer advarsler, hvis de finder et misforhold, men generelt er denne kontrol ikke mulig, fordi hver variadisk funktion håndterer denne liste forskelligt. Det er umuligt statisk at styre selv alle funktionskald, printf()fordi formatstrengen kan oprettes dynamisk i programmet.
Manglende ensretning af fejlhåndteringC-syntaksen inkluderer ikke en særlig fejlhåndteringsmekanisme. Standardbiblioteket understøtter kun de enkleste midler: en variabel (i tilfælde af POSIX , en makro) errnofra header-filen errno.htil at indstille den sidste fejlkode, og fungerer til at få fejlmeddelelser i henhold til koderne. Denne tilgang fører til behovet for at skrive en stor mængde gentagen kode, blande hovedalgoritmen med fejlhåndtering, og desuden er den ikke trådsikker. Desuden er der ikke en enkelt ordre, selv i denne mekanisme:
I standardbiblioteket er koder errnoudpeget gennem makrodefinitioner og kan have samme værdier, hvilket gør det umuligt at analysere fejlkoder gennem operatøren switch. Sproget har ikke en speciel datatype for flag og fejlkoder, de videregives som værdier af typen int. En separat type errno_ttil lagring af fejlkoden dukkede kun op i K-udvidelsen af C11-standarden og understøttes muligvis ikke af compilere [87] .
Manglerne ved C har længe været velkendte, og siden sprogets begyndelse har der været mange forsøg på at forbedre kvaliteten og sikkerheden af C-koden uden at ofre dens muligheder.
Midler til analyse af kodekorrekthedNæsten alle moderne C-kompilere tillader begrænset statisk kodeanalyse med advarsler om potentielle fejl. Indstillinger understøttes også til indlejring af tjek for array uden for grænserne, stak-destruktion, uden for heap-grænser, læsning af uinitialiserede variabler, udefineret adfærd osv. i koden. Yderligere tjek kan dog påvirke ydeevnen af den endelige applikation, så de er oftest kun brugt på debugging scenen.
Der er specielle softwareværktøjer til statisk analyse af C-kode for at opdage ikke-syntaksfejl. Deres brug garanterer ikke de fejlfrie programmer, men giver dig mulighed for at identificere en væsentlig del af typiske fejl og potentielle sårbarheder. Den maksimale effekt af disse værktøjer opnås ikke ved lejlighedsvis brug, men når de bruges som en del af et veletableret system med konstant kodekvalitetskontrol, for eksempel i kontinuerlige integrations- og implementeringssystemer. Det kan også være nødvendigt at annotere koden med særlige kommentarer for at udelukke falske alarmer fra analysatoren på korrekte dele af koden, der formelt falder ind under kriterierne for fejlagtige.
Sikker programmeringsstandarderEn betydelig mængde forskning er blevet publiceret om korrekt C-programmering, lige fra små artikler til lange bøger. Virksomheds- og industristandarder er vedtaget for at opretholde kvaliteten af C-koden. I særdeleshed:
POSIX -sættet af standarder bidrager til at udligne nogle af sprogets mangler . Installationen er standardiseret errnoaf mange funktioner, hvilket gør det muligt at håndtere fejl, der opstår, for eksempel i filoperationer, og trådsikre analoger af nogle funktioner i standardbiblioteket introduceres, hvis sikre versioner kun findes i sprogstandarden i K-udvidelsen [137] .
Ordbøger og encyklopædier | ||||
---|---|---|---|---|
|
Programmeringssprog | |
---|---|
|
C programmeringssprog | |
---|---|
Kompilere |
|
Biblioteker | |
Ejendommeligheder | |
Nogle efterkommere | |
C og andre sprog |
|
Kategori:C programmeringssprog |