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] .
Sammensætningen af alfabetet [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] .
Rækkefølgen af tildeling af datatyper til heltalskonstanter i henhold til deres værdi [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.
Tegnkonstantpræfikser [13]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.
Strengkonstante præfikser [15]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 konstanter Sammenligning af metoder til indstilling af konstanter [17]Makro | #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 i C-sproget [19]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 ] Unary C-operatører [22]+ | 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 ] Grundlæggende binære operatorer [23]+ | 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.
Venstretildeling af binære operatorer [24]= | 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] .
Sekvenspunkter defineret af standarden [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.
Grundlæggende datatyper til lagring af heltalDatatype | 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] .
Sammenligning af korrekt og forkert automatisk type støbningSignerede 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.
Reelle datatyperDatatype | 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 linjer Indtast kodning wchar_tafhængigt af platformenPlatform | 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.
Gyldige funktionsprototyper main()[66]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] .
Hukommelsestildelingsmetoder [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 {