Sök…


Introduktion

Detta avsnitt diskuterar några vanliga misstag som en C-programmerare bör vara medveten om och bör undvika att göra. Mer information om oväntade problem och deras orsaker finns i Ovidtat beteende

Blanda signerade och osignerade heltal i aritmetiska operationer

Det är vanligtvis inte en bra idé att blanda signed och unsigned heltal i aritmetiska operationer. Till exempel, vad kommer att matas ut från följande exempel?

#include <stdio.h>

int main(void)
{ 
    unsigned int a = 1000;
    signed int b = -1;

    if (a > b) puts("a is more than b");
    else puts("a is less or equal than b"); 

    return 0;
}  

Eftersom 1000 är mer än -1 kan du förvänta dig att utgången är a is more than b , men det kommer inte att vara fallet.

Aritmetiska operationer mellan olika integrerade typer utförs inom en gemensam typ som definieras av de så kallade vanliga aritmetiska omvandlingarna (se språkspecifikationen, 6.3.1.8).

I det här fallet är den "vanliga typen" unsigned int Eftersom, som anges i vanliga aritmetiska omvandlingar ,

714 Annars, om operanden som har osignerad heltalstyp har rang som är större eller lika med rankningen för typen av den andra operanden, omvandlas operanden med signerad heltalstyp till typen av operand med osignerad heltalstyp.

Detta innebär att int operand b kommer att konverteras till unsigned int före jämförelsen.

När -1 konverteras till en unsigned int resultatet det maximalt möjliga unsigned int värdet, som är större än 1000, vilket betyder att a > b är falsk.

Felaktig skrivning = istället för == när man jämför

Operatören = används för tilldelning.

Operatören == används för jämförelse.

Man bör vara noga med att inte blanda de två. Ibland skriver man felaktigt

/* assign y to x */
if (x = y) {
     /* logic */
}

när det som verkligen ville var:

/* compare if x is equal to y */
if (x == y) {
    /* logic */
}

Den förstnämnda tilldelar värdet y till x och kontrollerar om detta värde inte är noll istället för att göra jämförelse, vilket motsvarar:

if ((x = y) != 0) {
    /* logic */
}

Det finns tillfällen när testet av resultatet av en uppgift är avsett och används vanligt, eftersom det undviker att behöva kopiera koden och att behandla första gången speciellt. Jämföra

while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) {
        switch (c) {
        ...
        }
}

mot

c = getopt_long(argc, argv, short_options, long_options, &option_index);
while (c != -1) {
        switch (c) {
        ...
        }
        c = getopt_long(argc, argv, short_options, long_options, &option_index);
}

Moderna kompilatorer känner igen detta mönster och varnar inte när uppdraget är inom parentes som ovan, men kan varna för andra användningar. Till exempel:

if (x = y)         /* warning */

if ((x = y))       /* no warning */
if ((x = y) != 0)  /* no warning; explicit */

Vissa programmerare använder strategin för att sätta konstanten till vänster om operatören (vanligtvis kallade Yoda-villkor ). Eftersom konstanter är värderingar kommer denna villkorsstil att göra att kompilatorn kastar ett fel om fel operatör användes.

if (5 = y) /* Error */

if (5 == y) /* No error */

Detta reducerar emellertid allvarlig kodens läsbarhet och anses inte nödvändig om programmeraren följer goda C-kodningssätt och inte hjälper till att jämföra två variabler så att det inte är en universell lösning. Dessutom kan många moderna kompilatorer ge varningar när kod skrivs med Yoda-villkor.

Oavsiktlig användning av semikolon

Var försiktig med semikolon. Följande exempel

if (x > a);
   a = x;

faktiskt betyder:

if (x > a) {}
a = x;

vilket betyder att x kommer att tilldelas a i alla fall, vilket kanske inte är det du ursprungligen ville ha.

Ibland kan det saknas problem med att sakna en semikolon:

if (i < 0) 
    return
day = date[0];
hour = date[1];
minute = date[2];

Semikolonet bakom returen saknas, så dag = datum [0] kommer att returneras.

En teknik för att undvika detta och liknande problem är att alltid använda hängslen på flerstrengade balsam och slingor. Till exempel:

if (x > a) {
    a = x;
}

Glömmer att tilldela en extra byte för \ 0

När du kopierar en sträng till en malloc ed-buffert, kom alltid ihåg att lägga till 1 till strlen .

char *dest = malloc(strlen(src)); /* WRONG */
char *dest = malloc(strlen(src) + 1); /* RIGHT */

strcpy(dest, src);

Detta beror på att strlen inte inkluderar släpet \0 i längden. Om du använder WRONG (som visas ovan) när du ringer strcpy skulle ditt program åberopa odefinierat beteende.

Det gäller också situationer när du läser en sträng med känd maximal längd från stdin eller någon annan källa. Till exempel

#define MAX_INPUT_LEN 42

char buffer[MAX_INPUT_LEN]; /* WRONG */
char buffer[MAX_INPUT_LEN + 1]; /* RIGHT */

scanf("%42s", buffer);  /* Ensure that the buffer is not overflowed */

Glömmer att frigöra minne (minne läcker)

En bästa praxis för programmering är att frigöra allt minne som har tilldelats direkt med din egen kod, eller implicit genom att ringa en intern eller extern funktion, till exempel ett bibliotek-API som strdup() . Underlåtenhet att frigöra minne kan införa en minnesläcka, som kan samlas in i en betydande mängd bortkastat minne som inte är tillgängligt för ditt program (eller systemet), vilket kan leda till kraschar eller odefinierat beteende. Det är mer troligt att problem uppstår om läckan uppstår upprepade gånger i en slinga eller rekursiv funktion. Risken för programfel ökar ju längre ett läckande program körs. Ibland uppstår problem direkt; andra gånger kommer problem inte att ses i timmar eller till och med år av konstant drift. Misslyckanden i utmattningen kan vara katastrofala, beroende på omständigheterna.

Följande oändliga slinga är ett exempel på en läcka som så småningom kommer att ta bort tillgängligt minnesläcka genom att ringa getline() , en funktion som implicit tilldelar nytt minne utan att frigöra det minnet.

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

int main(void)
{
    char *line = NULL;
    size_t size = 0;

    /* The loop below leaks memory as fast as it can */

    for(;;) { 
        getline(&line, &size, stdin); /* New memory implicitly allocated */

        /* <do whatever> */

        line = NULL;
    }

    return 0;
}

I motsats härtill använder koden nedan funktionen getline() , men den här gången frigörs det tilldelade minnet korrekt och undviker läckage.

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

int main(void)
{
    char *line = NULL;
    size_t size = 0;

    for(;;) {
        if (getline(&line, &size, stdin) < 0) {
            free(line);
            line = NULL;

            /* Handle failure such as setting flag, breaking out of loop and/or exiting */
        }

        /* <do whatever> */

        free(line);
        line = NULL;

    }

    return 0;
}

Läckande minne har inte alltid konkreta konsekvenser och är inte nödvändigtvis ett funktionellt problem. Medan "bästa praxis" dikterar hårt att frigöra minne vid strategiska punkter och förhållanden, för att minska minnesfotavtrycket och lägre risk för utmattning av minnet, kan det finnas undantag. Om till exempel ett program är begränsat i varaktighet och omfattning kan risken för allokeringsfel anses vara för liten att oroa sig för. I det fallet kan man kringgå uttrycklig omlokalisering anses vara acceptabel. Till exempel frigör de flesta moderna operativsystem automatiskt allt minne som konsumeras av ett program när det avslutas, oavsett om det beror på programfel, ett systemanrop för att exit() , processavslutning eller nå slutet av main() . Att explicit frigöra minne vid den överhängande programavslutningen kan faktiskt vara överflödigt eller införa en prestationsstraff.

Tilldelning kan misslyckas om otillräckligt minne är tillgängligt och hanteringsfel bör redovisas på lämpliga nivåer i samtalstacken. getline() , som visas ovan är ett intressant användningsfall eftersom det är en biblioteksfunktion som inte bara tilldelar minnet som den lämnar till den som ringer att frigöra, utan kan misslyckas av ett antal skäl, som alla måste beaktas. Därför är det viktigt när du använder ett C API, att läsa dokumentationen (man sida) och vara särskilt uppmärksam på felförhållanden och minnesanvändning, och vara medveten om vilket mjukvarulag som bär bördan att frigöra återlämnat minne.

En annan vanlig minneshanteringspraxis är att konsekvent ställa in minnespekare på NULL omedelbart efter att minnet som de pekarna hänvisar till har frigjorts, så att dessa pekare kan testas för giltighet när som helst (t.ex. kontrolleras för NULL / icke-NULL), eftersom åtkomst till frigjord minne kan leda till allvarliga problem som att få skräpdata (läsoperation) eller datakorruption (skrivoperation) och / eller en programkrasch. I de flesta moderna operativsystem är frigöring av minnesplats 0 ( NULL ) ett NOP (t.ex. är det ofarligt), enligt C-standarden - så genom att ställa in en pekare till NULL finns det ingen risk att dubbelfrigöra minne om pekaren överförs till free() . Tänk på att dubbelfritt minne kan leda till mycket tidskrävande, förvirrande och svårt att diagnostisera fel.

Kopierar för mycket

char buf[8]; /* tiny buffer, easy to overflow */

printf("What is your name?\n");
scanf("%s", buf); /* WRONG */
scanf("%7s", buf); /* RIGHT */

Om användaren går in i en sträng längre än 7 tecken (- 1 för nollterminatom), minne bakom buffert buf kommer att skrivas över. Detta resulterar i odefinierat beteende. Skadliga hackare utnyttjar ofta detta för att skriva över returadressen och ändra den till adressen till hackarens skadliga kod.

Glömmer att kopiera returvärdet för realloc till ett tillfälligt

Om realloc misslyckas, returnerar det NULL . Om du tilldelar värdet på den ursprungliga bufferten till realloc returvärde, och om den returnerar NULL , går den ursprungliga bufferten (den gamla pekaren) förlorad, vilket resulterar i ett minnesläckage . Lösningen är att kopiera till en tillfällig visare, och om det tillfälliga inte är null och kopiera in den verkliga buffert.

char *buf, *tmp;

buf = malloc(...);
...

/* WRONG */
if ((buf = realloc(buf, 16)) == NULL)
    perror("realloc");

/* RIGHT */
if ((tmp = realloc(buf, 16)) != NULL)
    buf = tmp;
else
    perror("realloc");

Jämför flyttalsnummer

Flytande punkttyper ( float , double och long double ) kan inte exakt representera vissa siffror eftersom de har begränsad precision och representerar värdena i ett binärt format. Precis som om vi har upprepat decimaler i bas 10 för bråk som 1/3, finns det fraktioner som inte kan representeras fint i binärt också (som 1/3, men också, ännu viktigare, 1/10). Jämför inte flytande punktvärden direkt; använd ett delta istället.

#include <float.h> // for DBL_EPSILON and FLT_EPSILON
#include <math.h>  // for fabs()

int main(void)
{
    double a = 0.1; // imprecise: (binary) 0.000110...

    // may be false or true
    if (a + a + a + a + a + a + a + a + a + a == 1.0) {
        printf("10 * 0.1 is indeed 1.0. This is not guaranteed in the general case.\n");
    }

    // Using a small delta value.
    if (fabs(a + a + a + a + a + a + a + a + a + a - 1.0) < 0.000001) {
        // C99 5.2.4.2.2p8 guarantees at least 10 decimal digits
        // of precision for the double type.
        printf("10 * 0.1 is almost 1.0.\n");
    }

    return 0;
}

Ett annat exempel:

gcc -O3   -g   -I./inc   -std=c11   -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes  -Wold-style-definition       rd11.c -o rd11 -L./lib -lsoq 
#include <stdio.h>
#include <math.h>

static inline double rel_diff(double a, double b)
{
    return fabs(a - b) / fmax(fabs(a), fabs(b));
}

int main(void)
{
    double d1 = 3.14159265358979;
    double d2 = 355.0 / 113.0;

    double epsilon = 1.0;
    for (int i = 0; i < 10; i++)
    {
        if (rel_diff(d1, d2) < epsilon)
            printf("%d:%.10f <=> %.10f within tolerance %.10f (rel diff %.4E)\n",
                   i, d1, d2, epsilon, rel_diff(d1, d2));
        else
            printf("%d:%.10f <=> %.10f out of tolerance %.10f (rel diff %.4E)\n",
                   i, d1, d2, epsilon, rel_diff(d1, d2));
        epsilon /= 10.0;
    }
    return 0;
}

Produktion:

0:3.1415926536 <=> 3.1415929204 within tolerance 1.0000000000 (rel diff 8.4914E-08)
1:3.1415926536 <=> 3.1415929204 within tolerance 0.1000000000 (rel diff 8.4914E-08)
2:3.1415926536 <=> 3.1415929204 within tolerance 0.0100000000 (rel diff 8.4914E-08)
3:3.1415926536 <=> 3.1415929204 within tolerance 0.0010000000 (rel diff 8.4914E-08)
4:3.1415926536 <=> 3.1415929204 within tolerance 0.0001000000 (rel diff 8.4914E-08)
5:3.1415926536 <=> 3.1415929204 within tolerance 0.0000100000 (rel diff 8.4914E-08)
6:3.1415926536 <=> 3.1415929204 within tolerance 0.0000010000 (rel diff 8.4914E-08)
7:3.1415926536 <=> 3.1415929204 within tolerance 0.0000001000 (rel diff 8.4914E-08)
8:3.1415926536 <=> 3.1415929204 out of tolerance 0.0000000100 (rel diff 8.4914E-08)
9:3.1415926536 <=> 3.1415929204 out of tolerance 0.0000000010 (rel diff 8.4914E-08)

Gör extra skalning i pekar aritmetik

I pekarearmetik tolkas heltalet som ska läggas till eller subtraheras till pekaren inte som adressändring utan som antal element att flytta.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = ptr + sizeof(int) * 2; /* wrong */
    printf("%d %d\n", *ptr, *ptr2);
    return 0;
}

Denna kod gör extra skalning vid beräkningen av pekaren tilldelad ptr2 . Om sizeof(int) är 4, vilket är typiskt i moderna 32-bitarsmiljöer, står uttrycket för "8 element efter array[0] ", som är utanför räckvidden, och det åberopar odefinierat beteende .

För att ha ptr2 punkt vid vad som är 2 element efter array[0] , bör du helt enkelt lägga till 2.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = ptr + 2;
    printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
    return 0;
}

Explicit pointer aritmetik med hjälp av additiva operatörer kan vara förvirrande, så att använda array-abonnemang kan vara bättre.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = &ptr[2];
    printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
    return 0;
}

E1[E2] är identisk med (*((E1)+(E2))) ( N1570 6.5.2.1, punkt 2), och &(E1[E2]) är ekvivalent med ((E1)+(E2)) ( N1570 6.5.3.2, fotnot 102).

Alternativt, om pekaren aritmetik föredras, kan casting av pekaren för att adressera en annan datatyp tillåta byte-adressering. Var dock försiktig: slutlighet kan bli ett problem, och att kasta till andra typer än "pekare till karaktär" leder till stränga aliasproblem .

#include <stdio.h>

int main(void) {
    int array[3] = {1,2,3};  // 4 bytes * 3 allocated
    unsigned char *ptr = (unsigned char *) array;  // unsigned chars only take 1 byte
    /*
     * Now any pointer arithmetic on ptr will match
     * bytes in memory.  ptr can be treated like it
     * was declared as: unsigned char ptr[12];
     */

    return 0;
}

Makron är enkla strängersättningar

Makron är enkla strängersättningar. (Strengt taget arbetar de med förbehandlingsmärken, inte godtyckliga strängar.)

#include <stdio.h>

#define SQUARE(x) x*x

int main(void) {
    printf("%d\n", SQUARE(1+2));
    return 0;
}

Du kan förvänta dig att den här koden ska skrivas ut 9 ( 3*3 ), men faktiskt kommer 5 att skrivas ut eftersom makroen utvidgas till 1+2*1+2 .

Du bör radera in argumenten och hela makrouttrycket inom parentes för att undvika detta problem.

#include <stdio.h>

#define SQUARE(x) ((x)*(x))

int main(void) {
    printf("%d\n", SQUARE(1+2));
    return 0;
}

Ett annat problem är att argumenten för en makro inte garanteras att utvärderas en gång. de kanske inte utvärderas alls eller kan utvärderas flera gånger.

#include <stdio.h>

#define MIN(x, y) ((x) <= (y) ? (x) : (y))

int main(void) {
    int a = 0;
    printf("%d\n", MIN(a++, 10));
    printf("a = %d\n", a);
    return 0;
}

I den här koden utvidgas makroen till ((a++) <= (10) ? (a++) : (10)) . Eftersom a++ ( 0 ) är mindre än 10 kommer a++ utvärderas två gånger och det gör att värdet på a och vad som returneras från MIN skiljer sig från vad du kan förvänta dig.

Detta kan undvikas genom att använda funktioner, men observera att typerna kommer att fixas med funktionsdefinitionen, medan makron kan vara (för) flexibla med typer.

#include <stdio.h>

int min(int x, int y) {
    return x <= y ? x : y;
}

int main(void) {
    int a = 0;
    printf("%d\n", min(a++, 10));
    printf("a = %d\n", a);
    return 0;
}

Nu är problemet med dubbelutvärdering fixat, men den här min kan till exempel inte hantera double utan att trunka.

Makrodirektiv kan vara av två typer:

#define OBJECT_LIKE_MACRO     followed by a "replacement list" of preprocessor tokens
#define FUNCTION_LIKE_MACRO(with, arguments) followed by a replacement list

Det som skiljer dessa två typer av makron är karaktären som följer identifieraren efter #define : om det är en lparen är det ett funktionsliknande makro; annars är det ett objektliknande makro. Om avsikten är att skriva ett funktionsliknande makro får det inte finnas något vitrum mellan slutet av makronamnet och ( . Kontrollera detta för en detaljerad förklaring.

C99

I C99 eller senare kan du använda static inline int min(int x, int y) { … } .

C11

I C11 kan du skriva ett "typ-generiskt" uttryck för min .

#include <stdio.h>

#define min(x, y) _Generic((x), \
                        long double: min_ld, \
                        unsigned long long: min_ull, \
                        default: min_i \
                        )(x, y)

#define gen_min(suffix, type) \
    static inline type min_##suffix(type x, type y) { return (x < y) ? x : y; }

gen_min(ld, long double)
gen_min(ull, unsigned long long)
gen_min(i, int)

int main(void)
{
    unsigned long long ull1 = 50ULL;
    unsigned long long ull2 = 37ULL;
    printf("min(%llu, %llu) = %llu\n", ull1, ull2, min(ull1, ull2));
    long double ld1 = 3.141592653L;
    long double ld2 = 3.141592652L;
    printf("min(%.10Lf, %.10Lf) = %.10Lf\n", ld1, ld2, min(ld1, ld2));
    int i1 = 3141653;
    int i2 = 3141652;
    printf("min(%d, %d) = %d\n", i1, i2, min(i1, i2));
    return 0;
}

Det generiska uttrycket kan utvidgas med fler typer som double , float , long long , unsigned long , long , unsigned - och lämpliga gen_min skrivna.

Odefinierade referensfel vid länk

Ett av de vanligaste felen i sammanställningen inträffar under kopplingsstadiet. Felet liknar det här:

$ gcc undefined_reference.c 
/tmp/ccoXhwF0.o: In function `main':
undefined_reference.c:(.text+0x15): undefined reference to `foo'
collect2: error: ld returned 1 exit status
$

Så låt oss titta på koden som genererade det här felet:

int foo(void);

int main(int argc, char **argv)
{
    int foo_val;
    foo_val = foo();
    return foo_val;
}

Vi ser här en förklaring av foo ( int foo(); ) men ingen definition av den (faktisk funktion). Så vi gav kompilatorn funktionshuvud, men det fanns ingen sådan funktion definierad någonstans, så kompileringssteget passerar men länken går ut med ett Undefined reference .
För att fixa detta fel i vårt lilla program skulle vi bara behöva lägga till en definition för foo:

/* Declaration of foo */
int foo(void);

/* Definition of foo */
int foo(void)
{
    return 5;
}

int main(int argc, char **argv)
{
    int foo_val;
    foo_val = foo();
    return foo_val;
}

Nu kommer denna kod att sammanställas. En alternativ situation uppstår där källan för foo() finns i en separat källfil fil foo.c (och det finns en rubrik foo.h att förklara foo() som ingår i både foo.c och undefined_reference.c ). Då är fixen att länka både objektfilen från foo.c och undefined_reference.c , eller att kompilera båda källfilerna:

$ gcc -c undefined_reference.c 
$ gcc -c foo.c
$ gcc -o working_program undefined_reference.o foo.o
$

Eller:

$ gcc -o working_program undefined_reference.c foo.c
$

Ett mer komplicerat fall är där bibliotek är involverade, som i koden:

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

int main(int argc, char **argv)
{
    double first;
    double second;
    double power;

    if (argc != 3)
    {
        fprintf(stderr, "Usage: %s <denom> <nom>\n", argv[0]);
        return EXIT_FAILURE;
    }

    /* Translate user input to numbers, extra error checking
     * should be done here. */
    first = strtod(argv[1], NULL);
    second = strtod(argv[2], NULL);

    /* Use function pow() from libm - this will cause a linkage 
     * error unless this code is compiled against libm! */
    power = pow(first, second);

    printf("%f to the power of %f = %f\n", first, second, power);

    return EXIT_SUCCESS;
}

Koden är syntaktiskt korrekt, deklaration för pow() finns från #include <math.h> , så vi försöker kompilera och länka men får ett fel som detta:

$ gcc no_library_in_link.c -o no_library_in_link
/tmp/ccduQQqA.o: In function `main':
no_library_in_link.c:(.text+0x8b): undefined reference to `pow'
collect2: error: ld returned 1 exit status
$

Detta händer eftersom definitionen för pow() inte hittades under länkstadiet. För att fixa detta måste vi ange att vi vill länka mot matematikbiblioteket som heter libm genom att ange -lm flaggan. (Observera att det finns plattformar som macOS där -lm inte behövs, men när du får den odefinierade referensen behövs biblioteket.)

Så vi kör sammanställningssteget igen, den här gången specificerar vi biblioteket (efter käll- eller objektfilerna):

$ gcc no_library_in_link.c -lm -o library_in_link_cmd
$ ./library_in_link_cmd 2 4
2.000000 to the power of 4.000000 = 16.000000
$

Och det fungerar!

Misförståelse array förfall

Ett vanligt problem i kod som använder flerdimensionella matriser, matriser av pekare etc. är det faktum att Type** och Type[M][N] är grundläggande olika typer:

#include <stdio.h>

void print_strings(char **strings, size_t n)
{
    size_t i;
    for (i = 0; i < n; i++)
        puts(strings[i]);
}

int main(void)
{
    char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
    print_strings(s, 4);
    return 0;
}

Provkompilatorutgång:

file1.c: In function 'main':
file1.c:13:23: error: passing argument 1 of 'print_strings' from incompatible pointer type [-Wincompatible-pointer-types]
         print_strings(strings, 4);
                       ^
file1.c:3:10: note: expected 'char **' but argument is of type 'char (*)[20]'
     void print_strings(char **strings, size_t n)

Felet anger att s arrayen i main förs till funktions print_strings , som förväntar sig en annan pekare typ än den mottog. Den innehåller också en anteckning som uttrycker den typ som förväntas av print_strings och den typ som skickades till den från main .

Problemet beror på något som kallas matrisförfall . Vad som händer när s med sin char[4][20] (matris med 4 matriser med 20 tecken) överförs till funktionen är att det förvandlas till en pekare till sitt första element som om du hade skrivit &s[0] , som har char (*)[20] (pekaren till 1 matris med 20 tecken). Detta inträffar för alla matriser, inklusive en matris med pekare, en matris med matriser av matriser (3D-matriser) och en matris med pekare till en matris. Nedan visas en tabell som illustrerar vad som händer när en matris sönderfaller. Ändringar i typbeskrivningen markeras för att illustrera vad som händer:

Före förfall Efter förfall
char [20] matris (20 tecken) char * pekaren till (1 char)
char [4][20] matris med (4 matriser med 20 tecken) char (*)[20] pekaren till (1 matris med 20 tecken)
char *[4] matris med (4 poäng till 1 kol) char ** pekare till (1 pekare till 1 kol)
char [3][4][20] matris med (3 matriser av 4 matriser med 20 tecken) char (*)[4][20] pekaren till (1 matris med 4 matriser med 20 tecken)
char (*[4])[20] matris med (4 pekare till 1 matris med 20 tecken) char (**)[20] pekare till (1 pekare till 1 matris med 20 tecken)

Om en matris kan förfalla till en pekare kan det sägas att en pekare kan betraktas som en matris med minst 1 element. Ett undantag från detta är en nollpekare, som pekar på ingenting och därför inte är en matris.

Arrayförfall sker bara en gång. Om en matris har förfallit till en pekare är det nu en pekare, inte en matris. Även om du har en pekare till en matris, kom ihåg att pekaren kan betraktas som en matris med minst ett element, så arrayförfall har redan inträffat.

Med andra ord, en pekare till en matris ( char (*)[20] ) kommer aldrig att bli en pekare till en pekare ( char ** ). För att fixa funktionen print_strings , helt enkelt få den till att få rätt typ:

void print_strings(char (*strings)[20], size_t n)
/* OR */
void print_strings(char strings[][20], size_t n)

Ett problem uppstår när du vill att funktionen print_strings ska vara generisk för alla arrayer: tänk om det finns 30 karaktärer istället för 20? Eller 50? Svaret är att lägga till en annan parameter innan array-parametern:

#include <stdio.h>

/*
 * Note the rearranged parameters and the change in the parameter name
 * from the previous definitions:
 *      n (number of strings)
 *   => scount (string count)
 *
 * Of course, you could also use one of the following highly recommended forms
 * for the `strings` parameter instead:
 *
 *    char strings[scount][ccount]
 *    char strings[][ccount]
 */
void print_strings(size_t scount, size_t ccount, char (*strings)[ccount])
{
    size_t i;
    for (i = 0; i < scount; i++)
        puts(strings[i]);
}

int main(void)
{
    char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
    print_strings(4, 20, s);
    return 0;
}

Att sammanställa den ger inga fel och resulterar i den förväntade effekten:

Example 1
Example 2
Example 3
Example 4

Vidarebefordra ogränsande matriser till funktioner som förväntar sig "riktiga" flerdimensionella matriser

När du tilldelar flerdimensionella matriser med malloc , calloc och realloc är ett vanligt mönster att tilldela de inre matriserna med flera samtal (även om samtalet bara visas en gång, det kan vara i en slinga):

/* Could also be `int **` with malloc used to allocate outer array. */
int *array[4];
int i;

/* Allocate 4 arrays of 16 ints. */
for (i = 0; i < 4; i++)
    array[i] = malloc(16 * sizeof(*array[i]));

Skillnaden i byte mellan det sista elementet i en av de inre matriserna och det första elementet i nästa inre array kanske inte är 0 som de skulle vara med en "verklig" flerdimensionell matris (t.ex. int array[4][16]; ) :

/* 0x40003c, 0x402000 */
printf("%p, %p\n", (void *)(array[0] + 15), (void *)array[1]);

Med hänsyn till storleken på int får du en skillnad på 8128 byte (8132-4), som är 2032 int arrayelement, och det är problemet: en "riktig" flerdimensionell matris har inga mellanrum mellan element.

Om du behöver använda en dynamiskt tilldelad matris med en funktion som förväntar sig en "riktig" flerdimensionell matris, bör du tilldela ett objekt av typen int * och använda aritmetik för att utföra beräkningar:

void func(int M, int N, int *array);
...

/* Equivalent to declaring `int array[M][N] = {{0}};` and assigning to array4_16[i][j]. */
int *array;
int M = 4, N = 16;
array = calloc(M, N * sizeof(*array));
array[i * N + j] = 1;
func(M, N, array);

Om N är ett makro eller ett heltal bokstavligen snarare än en variabel, kan koden helt enkelt använda den mer naturliga 2-D array-notationen efter att ha tilldelat en pekare till en matris:

void func(int M, int N, int *array);
#define N 16
void func_N(int M, int (*array)[N]);
...

int M = 4;
int (*array)[N];
array = calloc(M, sizeof(*array));
array[i][j] = 1;

/* Cast to `int *` works here because `array` is a single block of M*N ints with no gaps,
   just like `int array2[M * N];` and `int array3[M][N];` would be. */
func(M, N, (int *)array);
func_N(M, array);
C99

Om N inte är ett makro eller ett heltal bokstavligt, pekar array på en array med variabel längd (VLA). Detta kan fortfarande användas med func genom att kasta till int * och en ny funktion func_vla skulle ersätta func_N :

void func(int M, int N, int *array);
void func_vla(int M, int N, int array[M][N]);
...

int M = 4, N = 16;
int (*array)[N];
array = calloc(M, sizeof(*array));
array[i][j] = 1;
func(M, N, (int *)array);
func_vla(M, N, array);
C11

Obs : VLA: er är valfria från och med C11. Om din implementering stöder C11 och definierar makro __STDC_NO_VLA__ till 1 är du fast med metoderna före C99.

Använda teckenkonstanter istället för strängbokstäver, och vice versa

I C är karaktärskonstanter och strängbokstäver olika saker.

En karaktär omgiven av enstaka citat som 'a' är en teckenkonstant . En teckenkonstant är ett heltal vars värde är den teckenkod som står för tecknet. Hur man tolkar karaktärskonstanter med flera tecken som 'abc' är implementeringsdefinerat.

Noll eller fler tecken omgiven av dubbla citat som "abc" är en bokstavlig sträng . En strängbokstav är en omodifierbar matris vars element är typ char . Strängen i dubbla citat plus avslutande av nolltecken är innehållet, så "abc" har 4 element ( {'a', 'b', 'c', '\0'} )

I det här exemplet används en teckenkonstant där en strängbokstav ska användas. Den här teckenkonstanten konverteras till en pekare på ett implementeringsdefinerat sätt och det är liten chans att den konverterade pekaren är giltig, så detta exempel kommer att åberopa odefinierat beteende .

#include <stdio.h>

int main(void) {
    const char *hello = 'hello, world'; /* bad */
    puts(hello);
    return 0;
}

I det här exemplet används en strängbokstav där en teckenkonstant ska användas. Pekaren som konverteras från strängbokstaven konverteras till ett heltal på ett implementeringsdefinerat sätt och den konverteras till char på ett implementeringsdefinerat sätt. (Hur man konverterar ett heltal till en signerad typ som inte kan representera värdet som ska konverteras är implementeringsdefinerat, och om char är undertecknat är också implementeringsdefinerat.) Utdata kommer att vara en meningslös sak.

#include <stdio.h>

int main(void) {
    char c = "a"; /* bad */
    printf("%c\n", c);
    return 0;
}

I nästan alla fall kommer kompilatorn att klaga på dessa mixningar. Om det inte gör det, måste du använda fler varningsalternativ för kompilator, eller rekommenderas att du använder en bättre kompilator.

Ignorerar returvärden för biblioteksfunktioner

Nästan varje funktion i C-standardbiblioteket returnerar något om framgång och något annat på fel. Till exempel kommer malloc att returnera en pekare till minnesblocket som tilldelats av funktionen vid framgång, och, om funktionen misslyckades med att tilldela det begärda minnesblocket, en nollpekare. Så du bör alltid kontrollera returvärdet för enklare felsökning.

Det här är dåligt:

char* x = malloc(100000000000UL * sizeof *x);
/* more code */
scanf("%s", x); /* This might invoke undefined behaviour and if lucky causes a segmentation violation, unless your system has a lot of memory */

Det här är bra:

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

int main(void)
{
    char* x = malloc(100000000000UL * sizeof *x);
    if (x == NULL) {
        perror("malloc() failed");
        exit(EXIT_FAILURE);
    }

    if (scanf("%s", x) != 1) {
        fprintf(stderr, "could not read string\n");
        free(x);
        exit(EXIT_FAILURE);
    }

    /* Do stuff with x. */

    /* Clean up. */
    free(x);

    return EXIT_SUCCESS;
}

På det här sättet vet du direkt orsaken till fel, annars kan du spendera timmar på att leta efter ett fel på en helt fel plats.

Newline-karaktär förbrukas inte i vanligt scanf () -samtal

När detta program

#include <stdio.h>
#include <string.h>

int main(void) {
    int num = 0;
    char str[128], *lf;

    scanf("%d", &num);
    fgets(str, sizeof(str), stdin);

    if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
    printf("%d \"%s\"\n", num, str);
    return 0;
}

körs med denna ingång

42
life

utgången blir 42 "" istället för förväntad 42 "life" .

Det beror på att en ny linje-karaktär efter 42 inte konsumeras i samtalet från scanf() och den konsumeras av fgets() innan den läser life . Sedan fgets() sluta läsa innan du läser life .

För att undvika detta problem, är ett sätt som är användbart när den maximala längden på en rad är känd - när man till exempel löser problem i online-domarsystemet - att undvika att använda scanf() direkt och läsa alla rader via fgets() . Du kan använda sscanf() att analysera de lästa raderna.

#include <stdio.h>
#include <string.h>

int main(void) {
    int num = 0;
    char line_buffer[128] = "", str[128], *lf;

    fgets(line_buffer, sizeof(line_buffer), stdin);
    sscanf(line_buffer, "%d", &num);
    fgets(str, sizeof(str), stdin);

    if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
    printf("%d \"%s\"\n", num, str);
    return 0;
}

Ett annat sätt är att läsa tills du träffar en ny linje efter att ha använt scanf() och innan du använder fgets() .

#include <stdio.h>
#include <string.h>

int main(void) {
    int num = 0;
    char str[128], *lf;
    int c;

    scanf("%d", &num);
    while ((c = getchar()) != '\n' && c != EOF);
    fgets(str, sizeof(str), stdin);

    if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
    printf("%d \"%s\"\n", num, str);
    return 0;
}

Lägga till en semikolon till en #define

Det är lätt att bli förvirrad i C-förbehandlaren och behandla den som en del av C själv, men det är ett misstag eftersom förprocessorn bara är en textersättningsmekanism. Om du till exempel skriver

/* WRONG */
#define MAX 100;
int arr[MAX];

koden expanderar till

int arr[100;];

vilket är ett syntaxfel. Åtgärden är att ta bort semikolon från #define linjen. Det är nästan alltid ett misstag att avsluta en #define med en semikolon.

Kommentarer med flera linjer kan inte kapslas in

I C häckar inte flerlinjekommentarer, / * och * /, inte.

Om du kommenterar ett block eller kod med hjälp av denna kommentarstil:

/*
 * max(): Finds the largest integer in an array and returns it.
 * If the array length is less than 1, the result is undefined.
 * arr: The array of integers to search.
 * num: The number of integers in arr.
 */
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

Du kommer inte att kunna kommentera det enkelt:

//Trying to comment out the block...
/*

/*
 * max(): Finds the largest integer in an array and returns it.
 * If the array length is less than 1, the result is undefined.
 * arr: The array of integers to search.
 * num: The number of integers in arr.
 */
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

//Causes an error on the line below...
*/

En lösning är att använda C99-stilkommentarer:

// max(): Finds the largest integer in an array and returns it.
// If the array length is less than 1, the result is undefined.
// arr: The array of integers to search.
// num: The number of integers in arr.
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

Nu kan hela blocket enkelt kommenteras:

/*

// max(): Finds the largest integer in an array and returns it.
// If the array length is less than 1, the result is undefined.
// arr: The array of integers to search.
// num: The number of integers in arr.
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

*/

En annan lösning är att undvika att inaktivera kod med kommentarsyntax, använda #ifdef eller #ifndef förbehandlingsdirektiv istället. Dessa direktiv gör boet, så att du fri att kommentera din kod i den stil du föredrar.

#define DISABLE_MAX /* Remove or comment this line to enable max() code block */

#ifdef DISABLE_MAX
/*
 * max(): Finds the largest integer in an array and returns it.
 * If the array length is less than 1, the result is undefined.
 * arr: The array of integers to search.
 * num: The number of integers in arr.
 */
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}
#endif

Vissa guider går så långt som att rekommendera att kodavsnitt aldrig får kommenteras och att om kod tillfälligt ska inaktiveras kan man använda ett #if 0 direktiv.

Se # om 0 för att blockera kodavsnitt .

Överskridande matrisgränser

Matriser är nollbaserade, det vill säga att indexet alltid börjar vid 0 och slutar med indexmatrisens längd minus 1. Följande kod kommer således inte att mata ut det första elementet i matrisen och kommer att sända ut skräp för det slutliga värdet som det skriver ut.

#include <stdio.h>

int main(void)
{
    int x = 0;
    int myArray[5] = {1, 2, 3, 4, 5}; //Declaring 5 elements

    for(x = 1; x <= 5; x++) //Looping from 1 till 5.
       printf("%d\t", myArray[x]);

    printf("\n");
    return 0;
}

Utgång: 2 3 4 5 GarbageValue

Följande visar rätt sätt att uppnå önskad utgång:

#include <stdio.h>

int main(void)
{
    int x = 0;
    int myArray[5] = {1, 2, 3, 4, 5}; //Declaring 5 elements

    for(x = 0; x < 5; x++) //Looping from 0 till 4.
       printf("%d\t", myArray[x]);

    printf("\n");
    return 0;
}

Utgång: 1 2 3 4 5

Det är viktigt att veta längden på en matris innan du arbetar med den, eftersom du annars kan skada bufferten eller orsaka ett segmenteringsfel genom att komma åt minnesplatser som är utanför gränserna.

Rekursiv funktion - missar bastillståndet

Beräkningen av faktorn för ett nummer är ett klassiskt exempel på en rekursiv funktion.

Saknar grundvillkoret:

#include <stdio.h>

int factorial(int n)
{
       return n * factorial(n - 1);
}

int main()
{
    printf("Factorial %d = %d\n", 3, factorial(3));
    return 0;
}

Typisk utgång: Segmentation fault: 11

Problemet med den här funktionen är att den skulle loopa oändligt och orsaka ett segmenteringsfel - den behöver ett basvillkor för att stoppa rekursionen.

Basskick deklarerad:

#include <stdio.h>

int factorial(int n)
{
    if (n == 1) // Base Condition, very crucial in designing the recursive functions.
    {
       return 1;
    }
    else
    {
       return n * factorial(n - 1);
    }
}

int main()
{
    printf("Factorial %d = %d\n", 3, factorial(3));
    return 0;
}

Provutgång

Factorial 3 = 6

Denna funktion avslutas så snart den träffar villkoret n är lika med 1 (förutsatt att initialvärdet för n är tillräckligt litet - den övre gränsen är 12 när int är en 32-bitars kvantitet).

Regler som ska följas:

  1. Initiera algoritmen. Rekursiva program behöver ofta ett frövärde till att börja med. Detta åstadkommes antingen genom att använda en parameter som skickas till funktionen eller genom att tillhandahålla en gateway-funktion som är icke-rekursiv men som ställer in frövärdena för den rekursiva beräkningen.
  2. Kontrollera om det eller de aktuella värden som behandlas matchar basfallet. I så fall bearbeta och returnera värdet.
  3. Omdefiniera svaret i termer av ett mindre eller enklare delproblem eller delproblem.
  4. Kör algoritmen för delproblemet.
  5. Kombinera resultaten i formuleringen av svaret.
  6. Returnera resultaten.

Källa: Rekursiv funktion

Kontrollera logiskt uttryck mot 'sant'

Den ursprungliga C-standarden hade ingen inre boolesk typ, så bool , true och false hade ingen inneboende betydelse och definierades ofta av programmerare. Vanligtvis skulle true definieras som 1 och false skulle definieras som 0.

C99

C99 lägger till den inbyggda typen _Bool och rubriken <stdbool.h> som definierar bool (expanderar till _Bool ), false och true . Det låter dig också omdefiniera bool , true och false , men noterar att detta är en föråldrad funktion.

Ännu viktigare är att logiska uttryck behandlar allt som utvärderas till noll som falskt och varje icke-noll utvärdering som sant. Till exempel:

/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
    if ((bitField & 0x80) == true)  /* Comparison only succeeds if true is 0x80 and bitField has that bit set */
    {
        return true;
    }
    else
    {
        return false;
    }
}

I exemplet ovan försöker funktionen att kontrollera om den övre biten är inställd och returnera true om den är det. Men genom att uttryckligen kontrollera mot true lyckas if uttalandet endast om (bitfield & 0x80) utvärderar till vad som är true definieras som, vilket typiskt är 1 och mycket sällan 0x80 . Antingen kontrollera uttryckligen mot det fall du förväntar dig:

/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
    if ((bitField & 0x80) == 0x80) /* Explicitly test for the case we expect */
    {
        return true;
    }
    else
    {
        return false;
    }
}

Eller utvärdera alla värden som inte är noll som sant.

/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
    /* If upper bit is set, result is 0x80 which the if will evaluate as true */
    if (bitField & 0x80)
    {
        return true;
    }
    else
    {
        return false;
    }
}

Flytande punktbokstäver är som standard av dubbel typ

Man måste vara försiktig när man initierar variabler av float till bokstavliga värden eller jämförs med bokstavliga värden, eftersom vanliga flytpunktslitteraler som 0.1 är av typen double . Detta kan leda till överraskningar:

#include <stdio.h>
int main() {
    float  n;
    n = 0.1;
    if (n > 0.1) printf("Wierd\n");
    return 0;
}
// Prints "Wierd" when n is float

Här, n blir initialiseras och avrundas till enkel precision, vilket resulterar i värdet ,10000000149011612. Då, n är konverterade tillbaka till dubbel precision som skall jämföras med 0.1 bokstav (vilket är lika med ,10000000000000001), vilket resulterar i en missanpassning.

Förutom avrundningsfel kommer blandning av float med double literaler att resultera i dålig prestanda på plattformar som inte har hårdvarustöd för dubbel precision.



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