Sök…


Introduktion

I C ger vissa uttryck odefinierat beteende . Standarden väljer uttryckligen att inte definiera hur en kompilator ska bete sig om den stöter på ett sådant uttryck. Som ett resultat är en kompilator fri att göra vad den anser vara lämplig och kan ge användbara resultat, oväntade resultat eller till och med krascha.

Kod som åberopar UB kan fungera som avsett i ett specifikt system med en specifik kompilator, men kommer sannolikt inte att fungera på ett annat system, eller med en annan kompilator-, kompilatorversion eller kompilatorinställningar.

Anmärkningar

Vad är odefinierat beteende (UB)?

Odefinierat beteende är en term som används i C-standarden. C11-standarden (ISO / IEC 9899: 2011) definierar termen odefinierat beteende som

beteende, vid användning av en icke-portabel eller felaktig programkonstruktion eller av felaktig information, för vilken denna internationella standard inte ställer några krav

Vad händer om det finns UB i min kod?

Detta är de resultat som kan hända på grund av odefinierat beteende enligt standard:

OBS! Eventuellt odefinierat beteende sträcker sig från att ignorera situationen helt med oförutsägbara resultat, till uppförande under översättning eller programkörning på ett dokumenterat sätt som är karakteristiskt för miljön (med eller utan utfärdande av ett diagnostiskt meddelande), till att avsluta en översättning eller exekvering (med utfärdande av ett diagnostiskt meddelande).

Följande citat används ofta för att beskriva (mindre formellt men) resultat som händer från odefinierat beteende:

"När kompilatorn möter [en given odefinierad konstruktion] är det lagligt att det gör att demoner flyger ut ur din näsa" (implikationen är att kompilatorn kan välja vilket godtyckligt bisart sätt att tolka koden utan att bryta mot ANSI C-standarden)

Varför existerar UB?

Om det är så dåligt, varför definierade de inte det bara eller gjorde det implementeringsdefinerat?

Odefinierat beteende ger fler möjligheter till optimering; Kompilatorn kan med rätta anta att någon kod inte innehåller odefinierat beteende, vilket kan göra det möjligt att undvika körtidskontroller och utföra optimeringar vars giltighet skulle vara kostsam eller omöjlig att bevisa på annat sätt.

Varför är UB svårt att spåra?

Det finns minst två orsaker till att odefinierat beteende skapar buggar som är svåra att upptäcka:

  • Kompilatorn är inte skyldig att - och i allmänhet inte kan tillförlitligt - varna dig för odefinierat beteende. I själva verket kräver det att göra det direkt mot orsaken till förekomsten av odefinierat beteende.
  • De oförutsägbara resultaten kanske inte börjar utvecklas vid den exakta punkten för operationen där konstruktionen vars beteende är odefinierat inträffar; Odefinierat beteende plågar hela exekveringen och dess effekter kan ske när som helst: Under, efter eller till och med före den odefinierade konstruktionen.

Överväg nollpekers-bortgång: kompilatorn krävs inte för att diagnostisera nollpekar-förändring, och till och med kunde inte, som vid körning någon pekare som skickas till en funktion eller i en global variabel kan vara noll. Och när nollpekaren erference uppstår, kräver inte standarden att programmet behöver krascha. Snarare kan programmet krascha tidigare, senare eller inte krascha alls; det kan till och med bete sig som om nollpekaren pekade på ett giltigt objekt och uppför sig helt normalt bara för att krascha under andra omständigheter.

I fallet med nollpekersherference skiljer sig C-språket sig från hanterade språk som Java eller C #, där beteendet hos nollpekersherference definieras : ett undantag kastas på exakt tid ( NullPointerException i Java, NullReferenceException i C #) , så de som kommer från Java eller C # kan felaktigt tro att i ett sådant fall måste ett C-program krascha, med eller utan utfärdande av ett diagnostiskt meddelande .

Ytterligare information

Det finns flera sådana situationer som bör tydligt särskiljas:

  • Explicit odefinierat beteende, det är där C-standarden uttryckligen säger att du är utanför gränserna.
  • Implicit odefinierat beteende, där det helt enkelt inte finns någon text i standarden som förutser ett beteende för den situation som du tog ditt program i.

Tänk också på att på många ställen beteendet hos vissa konstruktioner medvetet inte definieras av C-standarden för att lämna utrymme för kompilatorer och biblioteksimplementörer att komma med sina egna definitioner. Ett bra exempel är signaler och signalhanterare, där tillägg till C, som POSIX-operativsystemstandarden, definierar mycket mer utarbetade regler. I sådana fall måste du bara kontrollera dokumentationen för din plattform; C-standarden kan inte berätta någonting.

Observera också att om odefinierat beteende inträffar i program betyder det inte att bara den punkt där odefinierat beteende inträffat är problematisk, snarare hela programmet blir meningslöst.

På grund av sådana problem är det viktigt (särskilt eftersom kompilatorer inte alltid varnar oss för UB) för att personprogrammering i C ska vara minst bekant med den typ av saker som utlöser odefinierat beteende.

Det bör noteras att det finns några verktyg (t.ex. statiska analysverktyg som PC-Lint) som hjälper till att upptäcka odefinierat beteende, men återigen kan de inte upptäcka alla förekomster av odefinierat beteende.

Avbryta en nollpekare

Detta är ett exempel på att du hänvisar till en NULL-pekare och orsakar odefinierat beteende.

int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */

En NULL pekare garanteras av C-standarden för att jämföra ojämlik med vilken pekare som helst till ett giltigt objekt, och om du sätter på det påkallas odefinierat beteende.

Modifiering av ett objekt mer än en gång mellan två sekvenspunkter

int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;

Kod som denna leder ofta till spekulationer om det "resulterande värdet" av i . I stället för att specificera ett resultat, anger C-standarderna emellertid att utvärdering av ett sådant uttryck producerar odefinierat beteende . Före C2011 formaliserade standarden dessa regler i fråga om så kallade sekvenspunkter :

Mellan föregående och nästa sekvenspunkt ska ett skalobjekt ha sitt lagrade värde ändrat högst en gång genom utvärdering av ett uttryck. Dessutom ska det tidigare värdet endast läsas för att bestämma det värde som ska lagras.

(C99-standard, avsnitt 6.5, punkt 2)

Det schemat visade sig vara lite för grovt, vilket resulterade i att vissa uttryck uppvisade odefinierat beteende med avseende på C99, vilket sannolikt inte borde göra. C2011 behåller sekvenspunkter, men introducerar ett mer nyanserat tillvägagångssätt för detta område baserat på sekvensering och ett förhållande som det kallas "sekvenserat innan":

Om en sidoeffekt på ett skalärt objekt följs i förhållande till antingen en annan bieffekt på samma skalarobjekt eller en värderingsberäkning som använder värdet på samma skalarobjekt, är beteendet odefinierat. Om det finns flera tillåtna beställningar av suprimeringarna av ett uttryck, är beteendet odefinierat om en sådan obekvämd biverkning uppstår i någon av beställningarna.

(C2011-standarden, avsnitt 6.5, punkt 2)

De fullständiga detaljerna i relationen "sekvenserad före" är för långa för att beskriva här, men de kompletterar sekvenspunkter snarare än att ersätta dem, så de har effekten att definiera beteende för vissa utvärderingar vars beteende tidigare var odefinierat. I synnerhet, om det finns en sekvenspunkt mellan två utvärderingar, "sekvenseras före" sekvensen före den som följer efter.

Följande exempel har väldefinierat beteende:

int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */

Följande exempel har odefinierat beteende:

int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */

Som med alla former av odefinierat beteende är det inte informativt att observera det verkliga beteendet för att utvärdera uttryck som bryter mot sekvenseringsreglerna, förutom i retrospektiv mening. Språkstandarden ger ingen grund för att förvänta sig att sådana observationer är förutsägbara även för framtida beteende för samma program.

Saknar returrätt i funktionen för återvändande av värde

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* Trying to use the (not) returned value causes UB */
  int value = foo();
  return 0;
}

När en funktion förklaras att returnera ett värde måste den göra det på varje möjlig kodväg genom den. Odefinierat beteende inträffar så snart den som ringer (som förväntar sig ett returvärde) försöker använda returvärdet 1 .

Observera att det odefinierade beteendet bara inträffar om den som ringer försöker använda / komma åt värdet från funktionen. Till exempel,

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* The value (not) returned from foo() is unused. So, this program
   * doesn't cause *undefined behaviour*. */
  foo();
  return 0;
}
C99

main() är ett undantag från denna regel genom att det är möjligt för den att avslutas utan returrätt, eftersom ett antaget avkastningsvärde på 0 automatiskt kommer att användas i detta fall 2 .


1 ( ISO / IEC 9899: 201x , 6.9.1 / 12)

Om} som avslutar en funktion uppnås och värdet på funktionssamtalet används av den som ringer, är beteendet odefinierat.

2 ( ISO / IEC 9899: 201x , 5.1.2.2.3 / 1)

att nå} som avslutar huvudfunktionen ger ett värde av 0.

Signerat heltal överflöd

Enligt punkt 6.5 / 5 i både C99 och C11 ger utvärdering av ett uttryck odefinierat beteende om resultatet inte är ett representativt värde av uttryckets typ. För aritmetiska typer kallas det ett överflöde . Osignerad heltals aritmetik överflödar inte eftersom punkt 6.2.5 / 9 är tillämplig, vilket orsakar att ett osignerat resultat som annars skulle vara utanför intervallet ska reduceras till ett intervallvärde. Det finns dock ingen analog bestämmelse för signerade heltalstyper; dessa kan och göra överflöd, vilket ger odefinierat beteende. Till exempel,

#include <limits.h>      /* to get INT_MAX */

int main(void) {
    int i = INT_MAX + 1; /* Overflow happens here */
    return 0;
}

De flesta fall av denna typ av odefinierat beteende är svårare att känna igen eller förutsäga. Överflöde kan i princip uppstå från varje tilläggs-, subtraktions- eller multiplikationsoperation på signerade heltal (med förbehåll för vanliga aritmetiska omvandlingar) där det inte finns effektiva gränser eller ett förhållande mellan operandema för att förhindra det. Till exempel den här funktionen:

int square(int x) {
    return x * x;  /* overflows for some values of x */
}

är rimligt, och det gör rätt sak för tillräckligt små argumentvärden, men dess beteende är odefinierat för större argumentvärden. Du kan inte bedöma utifrån funktionen ensam om program som kallar den uppvisar odefinierat beteende som ett resultat. Det beror på vilka argument de lämnar till det.

Å andra sidan, överväga detta triviala exempel på översvämningssäker signerad heltal aritmetik:

int zero(int x) {
    return x - x;  /* Cannot overflow */
}

Förhållandet mellan subtraktionsoperatörens operander säkerställer att subtraktionen aldrig överflödar. Eller tänk på detta något mer praktiska exempel:

int sizeDelta(FILE *f1, FILE *f2) {
    int count1 = 0;
    int count2 = 0;
    while (fgetc(f1) != EOF) count1++;  /* might overflow */
    while (fgetc(f2) != EOF) count2++;  /* might overflow */

    return count1 - count2; /* provided no UB to this point, will not overflow */
}

Så länge som räknarna inte flyter över individuellt, kommer operanterna för den slutliga subtraktionen båda att vara icke-negativa. Alla skillnader mellan två sådana värden kan representeras som int .

Användning av en oinitialiserad variabel

int a; 
printf("%d", a);

Variabeln a är en int med automatisk lagringsvaraktighet. Exempelkoden ovan försöker skriva ut värdet på en oinitialiserad variabel ( a initialiserades aldrig). Automatiska variabler som inte initialiseras har obestämda värden; åtkomst till dessa kan leda till odefinierat beteende.

Obs: Variabler med statisk eller trådlig lokal lagring, inklusive globala variabler utan det static nyckelordet, initialiseras till antingen noll eller deras initialiserade värde. Följaktligen är följande lagligt.

static int b;
printf("%d", b);

Ett mycket vanligt misstag är att inte initiera variablerna som fungerar som räknare till 0. Du lägger till värden till dem, men eftersom det ursprungliga värdet är skräp, kommer du att åberopa odefinierat beteende , till exempel i frågan Sammanställning på terminal avger pekarvarning och konstiga symboler .

Exempel:

#include <stdio.h>

int main(void) {
    int i, counter;
    for(i = 0; i < 10; ++i)
        counter += i;
    printf("%d\n", counter);
    return 0;
}

Produktion:

C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
        counter += i;
        ^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
    int i, counter;
                  ^
                   = 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812

Ovanstående regler gäller också för pekare. Till exempel resulterar följande i odefinierat beteende

int main(void)
{
    int *p;
    p++; // Trying to increment an uninitialized pointer.
}

Observera att koden ovan på egen hand inte kan orsaka fel eller segmenteringsfel, men att försöka eliminera den här pekaren senare skulle orsaka det odefinierade beteendet.

Överföra en pekare till variabel utanför dess livstid

int* foo(int bar)
{
    int baz = 6;
    baz += bar;
    return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage   
   * duration (local variables), thus the returned pointer is not valid! */

int main (void)
{
    int* p;

    p = foo(5);  /* (2) this expression's behavior is undefined */
    *p = *p - 6; /* (3) Undefined behaviour here */

    return 0;
}

Vissa kompilatorer påpekar användbart detta. Till exempel varnar gcc med:

warning: function returns address of local variable [-Wreturn-local-addr]

och clang varnar med:

warning: address of stack memory associated with local variable 'baz' returned 
[-Wreturn-stack-address]

för ovanstående kod. Men kompilatorer kanske inte kan hjälpa till i komplex kod.

(1) Återvändande av referens till variabel som deklarerats static definieras beteende, eftersom variabeln inte förstörs efter att den nuvarande räckvidden har lämnats.

(2) Enligt ISO / IEC 9899: 2011 6.2.4 §2, "Värdet på en pekare blir obestämd när objektet som det pekar på når slutet på sin livstid."

(3) Att ställa in pekaren som returneras av funktionen foo är odefinierat beteende eftersom minnet som det refererar har ett obestämt värde.

Dividera med noll

int x = 0;
int y = 5 / x;  /* integer division */

eller

double x = 0.0;
double y = 5.0 / x;  /* floating point division */

eller

int x = 0;
int y = 5 % x;  /* modulo operation */

För den andra raden i varje exempel, där värdet på den andra operanden (x) är noll, är beteendet odefinierat.

Observera att de flesta implementeringar av flytande punktmatematik kommer att följa en standard (t.ex. IEEE 754), i vilket fall operationer som dividera med noll kommer att ha konsekventa resultat (t.ex. INFINITY ) även om C-standarden säger att operationen är odefinierad.

Få åtkomst till minne utöver tilldelad del

En en pekare till en bit av minne, som innehåller n element får endast dereferenced om den är i intervallet memory och memory + (n - 1) . Att ställa in en pekare utanför det intervallet resulterar i odefinierat beteende. Tänk som exempel på följande kod:

int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */

Den tredje raden får åtkomst till det fjärde elementet i en matris som bara är tre element långa, vilket leder till odefinierat beteende. På liknande sätt är beteendet för den andra raden i följande kodfragment inte heller väl definierat:

int array[3];
array[3] = 0;

Observera att det inte är odefinierat beteende att peka förbi det sista elementet i en matris ( beyond_array = array + 3 är väl definierat här), men du kan ändra det ( *beyond_array är odefinierat beteende). Denna regel gäller också för dynamiskt allokerat minne (till exempel buffertar som skapats genom malloc ).

Kopierar överlappande minne

En mängd olika biblioteksfunktioner har bland sina effekter kopiering av bytesekvenser från ett minnesområde till ett annat. De flesta av dessa funktioner har odefinierat beteende när käll- och destinationsregionerna överlappar varandra.

Till exempel ...

#include <string.h> /* for memcpy() */

char str[19] = "This is an example";
memcpy(str + 7, str, 10);

... försöker kopiera 10 byte där käll- och destinationsminnesområden överlappar varandra med tre byte. För att visualisera:

               overlapping area
               |
               _ _
              |   |
              v   v
T h i s   i s   a n   e x a m p l e \0
^             ^
|             |
|             destination
|
source

På grund av överlappningen är det resulterande beteendet odefinierat.

Bland standardbibliotekets funktioner med en begränsning av detta slag är memcpy() , strcpy() , strcat() , sprintf() och sscanf() . Standarden säger om dessa och flera andra funktioner:

Om kopiering sker mellan objekt som överlappar, är beteendet odefinierat.

memmove() är det viktigaste undantaget från denna regel. Dess definition specificerar att funktionen fungerar som om källdata först kopierades till en tillfällig buffert och sedan skrivs till destinationsadressen. Det finns inget undantag för överlappande käll- och destinationsregioner, och inget behov av ett, så memmove() har väl definierat beteende i sådana fall.

Skillnaden återspeglar en effektivitet kontra . generalitet avvägning. Kopiering, som dessa funktioner utför, sker vanligtvis mellan osammanhängande regioner i minnet, och ofta är det möjligt att vid utvecklingstiden veta om en viss instans av minneskopiering kommer att finnas i den kategorin. Antagande av icke-överlappning ger jämförelsevis effektivare implementeringar som inte tillförlitligt ger korrekta resultat när antagandet inte håller. De flesta C-biblioteksfunktioner tillåter effektivare implementeringar, och memmove() fyller i luckorna, och tjänar de fall där källan och destinationen kan eller överlappar varandra. För att få rätt effekt i alla fall måste den dock utföra ytterligare tester och / eller använda en relativt mindre effektiv implementering.

Läser ett oinitialiserat objekt som inte stöds av minnet

C11

Att läsa ett objekt orsakar odefinierat beteende, om objektet är 1 :

  • oinitialiserad
  • definierad med automatisk lagringstid
  • adressen är aldrig tas

Variabeln a i exemplet nedan uppfyller alla dessa villkor:

void Function( void )
{
    int a;
    int b = a;
} 

1 (Citat från: ISO: IEC 9899: 201X 6.3.2.1 Lvalues, arrays, and function designators 2)
Om lvalue betecknar ett objekt med automatisk lagringsvaraktighet som kunde ha deklarerats med registerlagringsklassen (aldrig hade dess adress tagits), och det objektet är uninitialiserat (inte deklarerat med en initialisering och ingen tilldelning till det har utförts före användning ), är beteendet odefinierat.

Data race

C11

C11 introducerade stöd för flera trådar för exekvering, vilket ger möjligheten till dataraser. Ett program innehåller ett datarace om ett objekt i det nås 1 av två olika trådar, där åtminstone en av åtkomstarna är icke-atomisk, åtminstone en ändrar objektet, och program semantik misslyckas med att se till att de två åtkomstarna inte kan överlappa varandra temporalt. 2 Observera väl att den faktiska samtidigheten för de aktuella åtkomstförhållandena inte är ett villkor för en datarace; datatävlingar täcker en bredare klass av problem som härrör från (tillåtna) inkonsekvenser i olika tråders åsikter om minnet.

Tänk på detta exempel:

#include <threads.h>

int a = 0;

int Function( void* ignore )
{
    a = 1;

    return 0;
}

int main( void )
{
    thrd_t id;
    thrd_create( &id , Function , NULL );

    int b = a;

    thrd_join( id , NULL );
}

thrd_create ringer thrd_create att starta en ny trådkörningsfunktion Function . Den andra tråden modifierar a , och huvudtråden läser a . Ingen av dessa tillgångar är atomiska, och de två trådarna gör ingenting varken individuellt eller gemensamt för att säkerställa att de inte överlappar varandra, så det finns ett datarace.

Bland de sätt som detta program kan undvika datarasen är

  • huvudtråden kunde utföra sin läsning av a innan den andra tråden startades;
  • huvudtråden kunde utföra sin läsning av a efter att ha säkerställt via thrd_join att den andra har avslutat;
  • trådarna kunde synkronisera deras åtkomst via en mutex, var och en låser den mutex innan man öppnar a och låser upp den efteråt.

Som mutex-alternativet visar kräver det inte att säkerställa en specifik arbetsordning för att undvika ett datarace, till exempel att barntråden modifierar a innan huvudtråden läser den; det är tillräckligt (för att undvika ett datarace) för att säkerställa att en åtkomst sker för en given utförande före den andra.


1 Ändra eller läsa ett objekt.

2 (citerat från ISO: IEC 9889: 201x, avsnitt 5.1.2.4 "Flertrådiga exekveringar och datatävlingar")
Utförandet av ett program innehåller ett datarace om det innehåller två motstridiga handlingar i olika trådar, av vilka åtminstone en inte är atomisk, och inte heller sker före den andra. Alla sådana dataraser resulterar i odefinierat beteende.

Läs värdet på pekaren som frigjorts

Till och med bara att läsa värdet på en pekare som frigjorts (dvs. utan att försöka eliminera pekaren) är odefinierat beteende (UB), t.ex.

char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{

}

Citera ISO / IEC 9899: 2011 , avsnitt 6.2.4 §2:

[…] Värdet på en pekare blir obestämd när objektet den pekar på (eller bara förbi) når slutet på sin livstid.

Användningen av obestämd minne för allt, inklusive uppenbarligen ofarlig jämförelse eller aritmetik, kan ha odefinierat beteende om värdet kan vara en fällrepresentation för typen.

Ändra bokstavssträng

I detta kodexempel initialiseras charpekaren p till adressen till en strängbokstav. Att försöka ändra strängen bokstavligen har odefinierat beteende.

char *p = "hello world";
p[0] = 'H'; // Undefined behavior

Att ändra en muterbar mängd char direkt eller genom en pekare är naturligtvis inte odefinierat beteende, även om dess initialiserare är en bokstavlig sträng. Följande är bra:

char a[] = "hello, world";
char *p = a;

a[0] = 'H';
p[7] = 'W';

Det beror på att strängbokstäverna effektivt kopieras till arrayen varje gång arrayen initialiseras (en gång för variabler med statisk varaktighet, varje gång arrayen skapas för variabler med automatisk eller trådvaraktighet - variabler med tilldelad varaktighet initialiseras inte), och det är bra att ändra matrisinnehåll.

Frigör minne två gånger

Att frigöra minne två gånger är odefinierat beteende, t.ex.

int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);

Citat från standard (7.20.3.2. Den fria funktionen för C99):

Annars, om argumentet inte stämmer överens med en pekare som tidigare returnerats av calloc-, malloc- eller realloc-funktionen, eller om utrymmet har omfördelats av ett samtal till free eller realloc, är beteendet odefinierat.

Med fel formatformat i printf

Att använda ett felaktigt formatspecifikat i det första argumentet för att printf åkallar odefinierat beteende. Till exempel åberopar koden nedan odefinierat beteende:

long z = 'B';
printf("%c\n", z);

Här är ett annat exempel

printf("%f\n",0);

Ovanför koden är odefinierat beteende. %f förväntar sig dubbelt. Emellertid är 0 av typen int .

Observera att din kompilator vanligtvis kan hjälpa dig att undvika fall som dessa, om du sätter på rätt flaggor under sammanställningen ( -Wformat i clang och gcc ). Från det sista exemplet:

warning: format specifies type 'double' but the argument has type
      'int' [-Wformat]
    printf("%f\n",0);
            ~~    ^
            %d

Konvertering mellan pekartyper ger felaktigt justerade resultat

Följande kan ha odefinierat beteende på grund av fel markering av pekaren:

 char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
 uint32_t *intptr = (uint32_t*)(memory_block + 1);  /* possible undefined behavior */
 uint32_t mvalue = *intptr;

Det odefinierade beteendet sker när pekaren konverteras. Enligt C11, om en konvertering mellan två pekartyper ger ett resultat som är felaktigt justerade (6.3.2.3), är beteendet odefinierat . Här kan en uint32_t kräva justering av 2 eller 4.

calloc å andra sidan krävs för att returnera en pekare som är lämpligt inriktad för alla objekttyper; alltså är memory_block korrekt justerad för att innehålla en uint32_t i dess initiala del. Sedan, på ett system där uint32_t har krävt justering av 2 eller 4, kommer memory_block + 1 att vara en udda adress och därmed inte korrekt anpassad.

Observera att C-standarden begär att cast-funktionen redan är odefinierad. Detta påläggs eftersom plattformar där adresser är segmenterade kanske inte memory_block + 1 ens har en korrekt representation som en heltalspekare.

Casting char * till pekare till andra typer utan att bry sig om justeringskrav används ibland på fel sätt för att avkoda packade strukturer som filhuvud eller nätverkspaket.

Du kan undvika det odefinierade beteendet till följd av memcpy hjälp av memcpy :

memcpy(&mvalue, memory_block + 1, sizeof mvalue);

Här sker ingen uint32_t* till uint32_t* och byte kopieras en efter en.

Den här kopieringsoperationen för vårt exempel leder bara till giltigt värde på mvalue eftersom:

  • Vi använde calloc så att byte initieras ordentligt. I vårt fall har alla byte värde 0 , men någon annan korrekt initialisering skulle göra.
  • uint32_t är en exakt breddstyp och har inga stoppningsbitar
  • Alla godtyckliga bitmönster är en giltig representation för alla osignerade typer.

Tillsats eller subtraktion av pekaren som inte är ordentligt begränsad

Följande kod har odefinierat beteende:

char buffer[6] = "hello";
char *ptr1 = buffer - 1;  /* undefined behavior */
char *ptr2 = buffer + 5;  /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char *ptr4 = buffer + 7;  /* undefined behavior */

Enligt C11, om tillägg eller subtraktion av en pekare till, eller precis bortom, ett arrayobjekt och en heltalstyp ger ett resultat som inte pekar på, eller precis bortom samma arrayobjekt, är beteendet odefinierat (6.5.6 ).

Dessutom är det naturligtvis odefinierat beteende att återvända en pekare som pekar på precis bortom matrisen:

char buffer[6] = "hello";
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char value = *ptr3;       /* undefined behavior */

Ändra en const-variabel med en pekare

int main (void)
{
    const int foo_readonly = 10;
    int *foo_ptr;

    foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
    *foo_ptr = 20; /* This is undefined behavior */

    return 0;
}

Citera ISO / IEC 9899: 201x , avsnitt 6.7.3 §2:

Om man försöker modifiera ett objekt definierat med en const-kvalificerad typ genom användning av en lvalue med icke-const-kvalificerad typ, är beteendet odefinierat. [...]


(1) I GCC kan detta kasta följande varning: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]

Skickar en nollpekare till printf% s konvertering

Konverteringen %s för printf anger att motsvarande argument är en pekare till det ursprungliga elementet i en matris med en rad . En nollpekare pekar inte på det ursprungliga elementet i någon grupp av karaktärstyp, och följaktligen är följande beteende odefinierat:

char *foo = NULL;
printf("%s", foo); /* undefined behavior */

Det odefinierade beteendet betyder emellertid inte alltid att programmet kraschar - vissa system vidtar åtgärder för att undvika kraschen som normalt händer när en nollpekare undviks. Exempelvis är Glibc känd för att skriva ut

(null)

för koden ovan. Lägg dock till (bara) en ny linje i formatsträngen så får du en krasch:

char *foo = 0;
printf("%s\n", foo); /* undefined behavior */

I det här fallet händer det eftersom GCC har en optimering som gör att printf("%s\n", argument); i ett samtal till puts med puts(argument) , och puts i Glibc hanterar inte null pekare. Allt detta beteende är standardkonform.

Observera att nullpekaren skiljer sig från en tom sträng . Följande är giltigt och har inget odefinierat beteende. Det kommer bara att skriva ut en ny linje :

char *foo = "";
printf("%s\n", foo);

Inkonsekvent koppling av identifierare

extern int var;
static int var; /* Undefined behaviour */

C11, §6.2.2, 7 säger:

Om samma identifikator inom en översättningsenhet visas med både intern och extern länkning, definieras beteendet.

Observera att om en förhandsdeklaration av en identifierare är synlig så kommer den att ha den tidigare förklaringens koppling. C11, §6.2.2, 4 tillåter det:

För en identifierare som deklarerats med lagringsklassens specifikation extern i ett omfattning där en förhandsdeklaration av identifikatören är synlig, 31) om den förhandsdeklarationen specificerar intern eller extern koppling, är kopplingen av identifikatorn vid den senare deklarationen densamma som länken specificerad vid den förhandsdeklarationen. Om ingen förhandsdeklaration är synlig, eller om den förhandsdeklarationen specificerar ingen koppling, har identifieraren extern länkning.

/* 1. This is NOT undefined */
static int var;
extern int var; 


/* 2. This is NOT undefined */
static int var;
static int var; 

/* 3. This is NOT undefined */
extern int var;
extern int var; 

Med hjälp av fflush på en ingångsström

POSIX- och C-standarderna fflush uttryckligen att användning av fflush på en ingångsström är odefinierat beteende. fflush definieras endast för utgångsströmmar.

#include <stdio.h>

int main()
{
    int i;
    char input[4096];

    scanf("%i", &i);
    fflush(stdin); // <-- undefined behavior
    gets(input);

    return 0;
}

Det finns inget standard sätt att ta bort olästa tecken från en inmatningsström. Å andra sidan använder vissa implementationer fflush att rensa stdin buffert. Microsoft definierar fflush beteende i en ingångsström: Om strömmen är öppen för inmatning rensar fflush innehållet i bufferten. Enligt POSIX.1-2008 fflush beteende om inte inmatningsfilen är sökbar.

Se Använda fflush(stdin) för många fler detaljer.

Bitförskjutning med negativa räkningar eller utöver typens bredd

Om växlingsräknarvärdet är ett negativt värde är både vänsterskift och höger växlingsoperationer odefinierade 1 :

int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */

Om vänsterskift utförs på ett negativt värde är det odefinierat:

int x = -5 << 3; /* undefined */

Om vänsterskift utförs på ett positivt värde och resultatet av det matematiska värdet inte kan representeras i typen är det odefinierat 1 :

/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit 
 * in an int. So, this is undefined. */
       
int x = 5 << 72;

Observera att högerförskjutning på ett negativt värde (.eg -5 >> 3 ) inte är odefinierat utan implementeringsdefinerat .


1 Citera ISO / IEC 9899: 201x , avsnitt 6.5.7:

Om värdet på höger operand är negativt eller är större än eller lika med bredden på den främjade vänstra operanden, är beteendet odefinierat.

Ändra strängen som returneras av getenv-, strerror- och setlokalfunktioner

Att ändra strängarna som returneras av standardfunktionerna getenv() , strerror() och setlocale() är odefinierade. Så implementationer kan använda statisk lagring för dessa strängar.

Funktionen getenv (), C11, §7.22.4.7, 4 , säger:

Getenv-funktionen returnerar en pekare till en sträng som är associerad med den matchade listmedlemmen. Strängen som pekas på ska inte modifieras av programmet utan kan skrivas över av ett senare samtal till getenv-funktionen.

Funktionen strerror (), C11, §7.23.6.3, 4 säger:

Strerror-funktionen returnerar en pekare till strängen, vars innehåll är lokalt specifikt. Den matris som det påpekas ska inte ändras av programmet utan kan skrivas över av ett efterföljande samtal till strerror-funktionen.

Funktionen setlocale (), C11, §7.11.1.1, 8 säger:

Pekaren till strängen som returneras av setlocale-funktionen är sådan att ett efterföljande samtal med det strängvärdet och dess tillhörande kategori återställer den delen av programmets språk. Strängen som pekas till ska inte modifieras av programmet utan kan skrivas över av ett senare samtal till setlocale-funktionen.

På liknande sätt localeconv() funktionen localeconv() en pekare till struct lconv som inte ska modifieras.

Funktionen localeconv (), C11, §7.11.2.1, 8 säger:

Localeconv-funktionen returnerar en pekare till det ifyllda objektet. Den struktur som pekas på av returvärdet ska inte ändras av programmet, utan kan skrivas över av ett senare samtal till localeconv-funktionen.

Återgå från en funktion som har deklarerats med funktionen "_Noreturn" eller "noreturn"

C11

Funktionsspecifikationen _Noreturn introducerades i C11. Rubriken <stdnoreturn.h> ger en makro noreturn som expanderar till _Noreturn . Så att använda _Noreturn eller noreturn från <stdnoreturn.h> är bra och likvärdigt.

En funktion som har deklarerats med _Noreturn (eller noreturn ) får inte återgå till sin uppringare. Om en sådan funktion gör återgång till dess ringer, är beteendet odefinierat.

I följande exempel förklaras noreturn func() med noreturn specificier men återgår till sin anropare.

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void func(void);

void func(void)
{
    printf("In func()...\n");
} /* Undefined behavior as func() returns */

int main(void)
{
    func();
    return 0;
}

gcc och clang producerar varningar för ovanstående program:

$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
 }
 ^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^

Ett exempel med noreturn som har väldefinierat beteende:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void my_exit(void);

/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
    printf("Exiting...\n");
    exit(0);
}

int main(void)
{
    my_exit();
    return 0;
}


Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow