Ricerca…


introduzione

Questa sezione discute alcuni degli errori comuni che un programmatore C dovrebbe conoscere e dovrebbe evitare di fare. Per ulteriori informazioni su alcuni problemi imprevisti e le loro cause, vedere comportamento non definito

Mescolando interi con segno e senza segno nelle operazioni aritmetiche

Di solito non è una buona idea di mescolare signed e unsigned interi nelle operazioni aritmetiche. Ad esempio, quale sarà l'output del seguente esempio?

#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;
}  

Dal momento che 1000 è superiore a -1, ci si aspetterebbe che l'output fosse a is more than b , tuttavia non sarà così.

Le operazioni aritmetiche tra diversi tipi di integrale sono eseguite all'interno di un tipo comune definito dalle cosiddette conversioni aritmetiche usuali (vedere le specifiche del linguaggio, 6.3.1.8).

In questo caso il "tipo comune" è unsigned int , perché, come indicato nelle conversioni aritmetiche usuali ,

714 Altrimenti, se l'operando che ha un numero intero senza segno ha rank superiore o uguale al rank del tipo di un altro operando, allora l'operando con tipo intero con segno viene convertito nel tipo dell'operando con tipo intero senza segno.

Ciò significa che int operando b verrà convertito in unsigned int prima del confronto.

Quando -1 viene convertito in un unsigned int il risultato è il massimo valore unsigned int , che è maggiore di 1000, il che significa che a > b è falso.

Scrittura errata = invece di == durante il confronto

L'operatore = è utilizzato per l'assegnazione.

L'operatore == è usato per il confronto.

Si dovrebbe fare attenzione a non mescolare i due. A volte uno scrive per errore

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

quando ciò che era veramente voluto è:

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

Il primo assegna il valore di y a x e controlla se quel valore è diverso da zero, invece di fare il confronto, che è equivalente a:

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

Ci sono momenti in cui il test del risultato di un compito è destinato ed è comunemente usato, perché evita di dover duplicare il codice e di doverlo trattare per la prima volta appositamente. Confrontare

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

contro

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);
}

I compilatori moderni riconosceranno questo modello e non avviseranno quando l'assegnazione è racchiusa tra parentesi come sopra, ma potrebbero mettere in guardia altri usi. Per esempio:

if (x = y)         /* warning */

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

Alcuni programmatori usano la strategia di mettere la costante alla sinistra dell'operatore (comunemente chiamata condizioni Yoda ). Poiché le costanti sono rvalue, questo tipo di condizione causerà un errore nel compilatore se è stato utilizzato l'operatore sbagliato.

if (5 = y) /* Error */

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

Tuttavia, questo riduce drasticamente la leggibilità del codice e non è considerato necessario se il programmatore segue buone pratiche di codifica in C e non aiuta nel confronto di due variabili, quindi non è una soluzione universale. Inoltre, molti compilatori moderni possono dare avvertimenti quando il codice è scritto con condizioni Yoda.

Uso non corretto del punto e virgola

Fai attenzione ai punti e virgola. Seguendo l'esempio

if (x > a);
   a = x;

in realtà significa:

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

il che significa che x sarà assegnato ad a in ogni caso, il che potrebbe non essere quello che volevi in ​​origine.

A volte, la mancanza di un punto e virgola causerà anche un problema non percepibile:

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

Il punto e virgola dietro il ritorno viene perso, quindi verrà restituito il giorno = data [0].

Una tecnica per evitare questo e problemi simili è quella di usare sempre le parentesi su condizionali e loop multi-linea. Per esempio:

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

Dimenticando di allocare un byte in più per \ 0

Quando copi una stringa in un buffer malloc ed, ricorda sempre di aggiungere 1 a strlen .

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

strcpy(dest, src);

Questo perché strlen non include il trailing \0 nella lunghezza. Se prendi l'approccio WRONG (come mostrato sopra), richiamando strcpy , il tuo programma invocherà un comportamento indefinito.

Si applica anche alle situazioni in cui stai leggendo una stringa di lunghezza massima nota da stdin o da qualche altra fonte. Per esempio

#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 */

Dimenticare di liberare memoria (perdite di memoria)

Una buona pratica di programmazione è quella di liberare tutta la memoria che è stata allocata direttamente dal proprio codice, o implicitamente chiamando una funzione interna o esterna, come un'API di libreria come strdup() . Non riuscendo a liberare memoria è possibile introdurre una perdita di memoria, che potrebbe accumularsi in una notevole quantità di memoria sprecata che non è disponibile per il programma (o il sistema), causando probabilmente arresti anomali o comportamenti non definiti. È più probabile che si verifichino problemi se la perdita si verifica ripetutamente in un ciclo o in una funzione ricorsiva. Il rischio di errori del programma aumenta più a lungo si verifica un programma che perde. A volte i problemi appaiono all'istante; altre volte i problemi non si vedranno per ore o addirittura anni di funzionamento costante. I fallimenti dell'esaurimento della memoria possono essere catastrofici, a seconda delle circostanze.

Il seguente ciclo infinito è un esempio di perdita che alla fine esaurirà la perdita di memoria disponibile chiamando getline() , una funzione che assegna implicitamente nuova memoria, senza liberare quella memoria.

#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;
}

Al contrario, il codice seguente utilizza anche la funzione getline() , ma questa volta, la memoria allocata viene liberata correttamente, evitando una perdita.

#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;
}

La perdita di memoria non ha sempre conseguenze tangibili e non è necessariamente un problema funzionale. Mentre "best practice" impone rigorosamente la liberazione della memoria in punti strategici e condizioni, per ridurre l'impronta di memoria e ridurre il rischio di esaurimento della memoria, possono esserci delle eccezioni. Ad esempio, se un programma è limitato in termini di durata e portata, il rischio di errore di allocazione potrebbe essere considerato troppo piccolo per preoccuparsi. In tal caso, l'esclusione della deallocazione esplicita potrebbe essere considerata accettabile. Ad esempio, la maggior parte dei sistemi operativi moderni libera automaticamente tutta la memoria consumata da un programma quando termina, sia a causa di un errore del programma, una chiamata di sistema a exit() , la chiusura del processo, o raggiungendo la fine di main() . Liberare esplicitamente la memoria al momento dell'imminente chiusura del programma potrebbe essere ridondante o introdurre una penalità di prestazioni.

L'allocazione può fallire se è disponibile memoria insufficiente e la gestione dei guasti deve essere tenuta in considerazione ai livelli appropriati dello stack di chiamate. getline() , mostrato sopra è un caso d'uso interessante perché è una funzione di libreria che non solo alloca la memoria che lascia al chiamante per liberare, ma può fallire per una serie di ragioni, che devono essere tutte prese in considerazione. Pertanto, quando si utilizza un'API C, è essenziale leggere la documentazione (pagina man) e prestare particolare attenzione alle condizioni di errore e all'utilizzo della memoria e tenere presente quale livello del software ha il peso di liberare memoria restituita.

Un'altra pratica di gestione della memoria comune consiste nell'impostare costantemente i puntatori di memoria su NULL immediatamente dopo che la memoria referenziata da quei puntatori è stata liberata, in modo che quei puntatori possano essere verificati per la validità in qualsiasi momento (ad esempio, verificata per NULL / non NULL), perché l'accesso alla memoria liberata può portare a problemi gravi come ottenere dati inutili (operazione di lettura) o danneggiamento dei dati (operazione di scrittura) e / o arresto anomalo del programma. Nella maggior parte dei sistemi operativi moderni, liberare la posizione di memoria 0 ( NULL ) è un NOP (ad es. È innocuo), come richiesto dallo standard C - quindi impostando un puntatore su NULL, non c'è il rischio di liberare la memoria se il puntatore viene passato a free() . Tieni presente che la memoria a doppio rilascio può portare a guasti molto lunghi, confusi e difficili da diagnosticare .

Copiando troppo

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

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

Se l'utente immette una stringa più lunga di 7 caratteri (- 1 per il terminatore null), la memoria dietro il buffer buf verrà sovrascritta. Ciò si traduce in un comportamento indefinito. Gli hacker malintenzionati spesso lo sfruttano per sovrascrivere l'indirizzo di ritorno e cambiarlo all'indirizzo del codice dannoso dell'hacker.

Dimenticando di copiare il valore di ritorno di realloc in un temporaneo

Se realloc fallisce, restituisce NULL . Se si assegna il valore del buffer originale al valore di ritorno di realloc , e se restituisce NULL , allora il buffer originale (il vecchio puntatore) viene perso, causando una perdita di memoria . La soluzione è copiare in un puntatore temporaneo, e se questo temporaneo non è nullo, quindi copiare nel buffer reale.

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");

Confronto tra numeri in virgola mobile

I tipi di virgola mobile ( float , double e long double ) non possono rappresentare con precisione alcuni numeri perché hanno una precisione finita e rappresentano i valori in un formato binario. Proprio come abbiamo ripetuto i decimali in base 10 per le frazioni come 1/3, ci sono anche frazioni che non possono essere rappresentate finitamente in binario (come 1/3, ma anche, più importante, 1/10). Non confrontare direttamente i valori in virgola mobile; usa invece un delta.

#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;
}

Un altro esempio:

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;
}

Produzione:

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)

Effettuare il ridimensionamento extra nell'aritmetica del puntatore

Nell'aritmetica del puntatore, il numero intero da aggiungere o sottrarre al puntatore viene interpretato non come cambio di indirizzo ma come numero di elementi da spostare.

#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;
}

Questo codice ptr2 ridimensionamento nel calcolo del puntatore assegnato a ptr2 . Se sizeof(int) è 4, che è tipico nei moderni ambienti a 32 bit, l'espressione sta per "8 elementi dopo array[0] ", che è fuori intervallo e richiama il comportamento non definito .

Per avere punto ptr2 corrispondenza di ciò che è 2 elementi dopo l' array[0] , devi semplicemente aggiungere 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;
}

L'aritmetica con puntatore esplicito che utilizza operatori additivi può essere fonte di confusione, quindi l'utilizzo di un indice di sottoscrizione può essere migliore.

#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] è identico a (*((E1)+(E2))) ( N1570 6.5.2.1, paragrafo 2), e &(E1[E2]) è equivalente a ((E1)+(E2)) ( N1570 6.5.3.2, nota 102).

In alternativa, se si preferisce l'aritmetica del puntatore, il cast del puntatore per indirizzare un diverso tipo di dati può consentire l'indirizzamento dei byte. Attenzione però: l' endianità può diventare un problema e il casting per tipi diversi da "puntatore al carattere" porta a problemi di aliasing rigorosi .

#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;
}

Le macro sono semplici sostituzioni di stringhe

Le macro sono semplici sostituzioni di stringhe. (A rigor di termini, funzionano con i token di preelaborazione, non con le stringhe arbitrarie.)

#include <stdio.h>

#define SQUARE(x) x*x

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

È probabile che questo codice stampi 9 ( 3*3 ), ma in realtà 5 verrà stampato perché la macro verrà espansa su 1+2*1+2 .

Dovresti avvolgere gli argomenti e l'intera macro espressione tra parentesi per evitare questo problema.

#include <stdio.h>

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

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

Un altro problema è che gli argomenti di una macro non possono essere valutati una sola volta; potrebbero non essere affatto valutati o potrebbero essere valutati più volte.

#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;
}

In questo codice, la macro verrà estesa a ((a++) <= (10) ? (a++) : (10)) . Poiché a++ ( 0 ) è minore di 10 , a++ verrà valutato due volte e il valore di a e ciò che viene restituito da MIN differirà da quanto ci si potrebbe aspettare.

Questo può essere evitato usando le funzioni, ma si noti che i tipi verranno risolti dalla definizione della funzione, mentre i macro possono essere (troppo) flessibili con i tipi.

#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;
}

Ora il problema della doppia valutazione è fisso, ma questa funzione min non può trattare con i double dati senza troncare, per esempio.

Le direttive macro possono essere di due tipi:

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

Ciò che distingue questi due tipi di macro è il carattere che segue l'identificatore dopo #define : se è un lparen , è una macro simile alla funzione; altrimenti, è una macro simile ad un oggetto. Se l'intenzione è quella di scrivere una macro funzione di simile, non ci deve essere alcun spazio bianco tra la fine del nome della macro e ( . Controllare questo per una spiegazione dettagliata.

C99

In C99 o versioni successive, è possibile utilizzare static inline int min(int x, int y) { … } .

C11

In C11, potresti scrivere un'espressione 'type-generic' per 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;
}

L'espressione generica potrebbe essere estesa con più tipi come double , float , long long , unsigned long , long , unsigned - e appropriato gen_min macro invocazioni scritte.

Errori di riferimento non definiti durante il collegamento

Uno degli errori più comuni nella compilazione si verifica durante la fase di collegamento. L'errore è simile a questo:

$ 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
$

Diamo un'occhiata al codice che ha generato questo errore:

int foo(void);

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

Vediamo qui una dichiarazione di foo ( int foo(); ) ma nessuna definizione di esso (funzione effettiva). Abbiamo quindi fornito al compilatore l'intestazione della funzione, ma non è stata definita alcuna funzione, quindi la fase di compilazione passa ma il linker viene chiuso con un errore di Undefined reference .
Per correggere questo errore nel nostro piccolo programma dovremmo solo aggiungere una definizione per 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;
}

Ora questo codice verrà compilato. Si presenta una situazione alternativa in cui il source per foo() trova in un file sorgente separato foo.c (e c'è un'intestazione foo.h per dichiarare foo() che è incluso in foo.c e undefined_reference.c ). Quindi la correzione è di collegare sia il file oggetto da foo.c e undefined_reference.c , o di compilare entrambi i file sorgente:

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

O:

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

Un caso più complesso è dove sono coinvolte le librerie, come nel codice:

#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;
}

Il codice è sintatticamente corretto, la dichiarazione per pow() esiste da #include <math.h> , quindi proviamo a compilare e collegare ma otteniamo un errore come questo:

$ 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
$

Ciò accade perché la definizione di pow() non è stata trovata durante la fase di collegamento. Per risolvere questo problema, dobbiamo specificare che vogliamo collegarci alla libreria matematica chiamata libm specificando il flag -lm . (Si noti che esistono piattaforme come macOS dove -lm non è necessario, ma quando si ottiene il riferimento non definito, la libreria è necessaria).

Quindi eseguiamo nuovamente la fase di compilazione, questa volta specificando la libreria (dopo i file di origine o di oggetto):

$ 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
$

E funziona!

Degrado dell'array malinteso

Un problema comune nel codice che utilizza matrici multidimensionali, matrici di puntatori, ecc. È il fatto che Type** e Type[M][N] sono tipi fondamentalmente diversi:

#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;
}

Esempio di output del compilatore:

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)

L'errore indica che l'array s nella funzione main viene passato alla funzione print_strings , che si aspetta un tipo di puntatore diverso da quello ricevuto. Include anche una nota che esprime il tipo che ci si aspetta da print_strings e il tipo che è stato trasmesso dal main .

Il problema è dovuto a qualcosa chiamato decadimento dell'array . Quello che succede quando s con il suo tipo char[4][20] (array di 4 matrici di 20 caratteri) viene passato alla funzione è che si trasforma in un puntatore al suo primo elemento come se tu avessi scritto &s[0] , che ha il tipo char (*)[20] (puntatore a 1 array di 20 caratteri). Ciò si verifica per qualsiasi array, inclusi un array di puntatori, una matrice di matrici di array (matrici 3D) e una matrice di puntatori a un array. Di seguito è riportata una tabella che illustra cosa succede quando un array decade. Le modifiche nella descrizione del tipo sono evidenziate per illustrare cosa succede:

Prima della decomposizione Dopo il decadimento
char [20] serie di (20 caratteri) char * puntatore a (1 carattere)
char [4][20] array di (4 array di 20 caratteri) char (*)[20] puntatore a (1 array di 20 caratteri)
char *[4] array di (4 puntatori a 1 carattere) char ** puntatore a (1 puntatore a 1 carattere)
char [3][4][20] array di (3 matrici di 4 matrici di 20 caratteri) char (*)[4][20] puntatore a (1 array di 4 matrici di 20 caratteri)
char (*[4])[20] array di (4 puntatori a 1 array di 20 caratteri) char (**)[20] puntatore a (1 puntatore a 1 array di 20 caratteri)

Se una matrice può decadere su un puntatore, allora si può affermare che un puntatore può essere considerato un array di almeno 1 elemento. Un'eccezione a questo è un puntatore nullo, che punta a nulla e di conseguenza non è un array.

Il decadimento delle matrici avviene solo una volta. Se una matrice è decaduta su un puntatore, ora è un puntatore, non una matrice. Anche se hai un puntatore a un array, ricorda che il puntatore potrebbe essere considerato come un array di almeno un elemento, quindi il decadimento dell'array si è già verificato.

In altre parole, un puntatore a un array ( char (*)[20] ) non diventerà mai un puntatore a un puntatore ( char ** ). Per correggere la funzione print_strings , è sufficiente farla ricevere il tipo corretto:

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

Un problema sorge quando vuoi che la funzione print_strings sia generica per qualsiasi array di caratteri: cosa succede se ci sono 30 caratteri invece di 20? O 50? La risposta è aggiungere un altro parametro prima del parametro array:

#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;
}

La compilazione non produce errori e produce l'output atteso:

Example 1
Example 2
Example 3
Example 4

Passaggio di array non adiacenti a funzioni che prevedono array "reali" multidimensionali

Quando si assegnano array multidimensionali con malloc , calloc e realloc , uno schema comune è quello di allocare gli array interni con più chiamate (anche se la chiamata viene visualizzata solo una volta, potrebbe essere in un ciclo):

/* 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]));

La differenza di byte tra l'ultimo elemento di uno degli array interni e il primo elemento dell'array interno successivo potrebbe non essere 0 come sarebbe con un array multidimensionale "reale" (ad es. int array[4][16]; ) :

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

Tenendo conto della dimensione di int , si ottiene una differenza di 8128 byte (8132-4), che è 2032 elementi di array int -size, e questo è il problema: un array multidimensionale "reale" non ha spazi tra gli elementi.

Se è necessario utilizzare un array allocato dinamicamente con una funzione che si aspetta un array "reale" multidimensionale, è necessario allocare un oggetto di tipo int * e utilizzare l'aritmetica per eseguire calcoli:

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);

Se N è una macro o un intero letterale piuttosto che una variabile, il codice può semplicemente utilizzare la notazione di array 2-D più naturale dopo aver assegnato un puntatore a un array:

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

Se N non è una macro o un intero letterale, la array punta a un array a lunghezza variabile (VLA). Questo può ancora essere usato con func func_vla a int * e una nuova funzione func_vla sostituirà 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

Nota : i VLA sono opzionali a partire da C11. Se la tua implementazione supporta C11 e definisce la macro __STDC_NO_VLA__ su 1, sei bloccato con i metodi pre-C99.

Utilizzare le costanti di carattere anziché i valori letterali stringa e viceversa

In C, le costanti dei caratteri e le stringhe letterali sono cose diverse.

Un personaggio circondato da virgolette singole come 'a' è una costante di carattere . Una costante di carattere è un numero intero il cui valore è il codice carattere che rappresenta il carattere. Come interpretare le costanti dei caratteri con più caratteri come 'abc' è definito dall'implementazione.

Zero o più caratteri circondati da virgolette come "abc" è una stringa letterale . Una stringa letterale è una matrice non modificabile i cui elementi sono di tipo char . La stringa tra virgolette più terminazione null-character è il contenuto, quindi "abc" ha 4 elementi ( {'a', 'b', 'c', '\0'} )

In questo esempio, viene utilizzata una costante di carattere in cui deve essere utilizzato un valore letterale stringa. Questa costante di carattere verrà convertita in un puntatore in un modo definito dall'implementazione e ci sono poche possibilità che il puntatore convertito sia valido, quindi questo esempio invocherà un comportamento non definito .

#include <stdio.h>

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

In questo esempio, viene utilizzata una stringa letterale in cui deve essere utilizzata una costante di carattere. Il puntatore convertito dal letterale stringa verrà convertito in un numero intero in un modo definito dall'implementazione e verrà convertito in char in un modo definito dall'implementazione. (Come convertire un intero in un tipo firmato che non può rappresentare il valore da convertire è definito dall'implementazione, e anche se il char è firmato è anche definito dall'implementazione.) L'output sarà una cosa priva di significato.

#include <stdio.h>

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

In quasi tutti i casi, il compilatore si lamenterà di questi errori. In caso contrario, è necessario utilizzare più opzioni di avviso del compilatore o si consiglia di utilizzare un compilatore migliore.

Ignorare i valori di ritorno delle funzioni della libreria

Quasi tutte le funzioni della libreria standard C restituiscono qualcosa in caso di successo e qualcos'altro in caso di errore. Ad esempio, malloc restituirà un puntatore al blocco di memoria assegnato dalla funzione in caso di successo e, se la funzione non è riuscita ad allocare il blocco di memoria richiesto, un puntatore nullo. Quindi dovresti sempre controllare il valore di ritorno per facilitare il debugging.

Questo non va bene:

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 */

Questo è buono:

#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;
}

In questo modo sai subito la causa dell'errore, altrimenti potresti passare ore a cercare un bug in un posto completamente sbagliato.

Il carattere di nuova riga non viene utilizzato nella tipica chiamata scanf ()

Quando questo programma

#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;
}

viene eseguito con questo input

42
life

l'uscita sarà 42 "" invece di 42 "life" prevista.

Questo perché un carattere di nuova riga dopo 42 non viene consumato nella chiamata di scanf() e viene consumato da fgets() prima che legga la life . Quindi, fgets() smette di leggere prima di leggere la life .

Per evitare questo problema, un modo che è utile quando la lunghezza massima di una linea è nota - quando si risolvono i problemi nel sistema di giudice online, ad esempio - è evitare l'uso di scanf() direttamente e la lettura di tutte le linee tramite fgets() . È possibile utilizzare sscanf() per analizzare le righe lette.

#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;
}

Un altro modo è leggere fino a quando non si preme un carattere di nuova riga dopo aver usato scanf() e prima di usare 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;
}

Aggiungere un punto e virgola a un #define

È facile confondersi nel preprocessore C e trattarlo come parte della C stessa, ma è un errore perché il preprocessore è solo un meccanismo di sostituzione del testo. Ad esempio, se scrivi

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

il codice si espande in

int arr[100;];

che è un errore di sintassi. Il rimedio è rimuovere il punto e virgola dalla riga #define . È quasi sempre un errore terminare un #define con un punto e virgola.

I commenti a più righe non possono essere nidificati

In C, i commenti su più righe, / * e * /, non annidano.

Se annoti un blocco di codice o funzione usando questo stile di commento:

/*
 * 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;
}

Non sarai in grado di commentarlo facilmente:

//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...
*/

Una soluzione è usare i commenti in stile C99:

// 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;
}

Ora l'intero blocco può essere commentato facilmente:

/*

// 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;
}

*/

Un'altra soluzione è quella di evitare il codice disabilitazione usando la sintassi commento, utilizzando #ifdef o #ifndef direttive del preprocessore invece. Queste direttive fanno nido, lasciandovi liberi di commentare il codice nello stile che si preferisce.

#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

Alcune guide arrivano al punto di raccomandare che le sezioni di codice non debbano mai essere commentate e che se il codice deve essere temporaneamente disattivato si potrebbe ricorrere all'utilizzo di una direttiva #if 0 .

Vedi #if 0 per bloccare le sezioni di codice .

Superamento dei limiti dell'array

Le matrici sono basate su zero, ovvero l'indice inizia sempre da 0 e termina con la lunghezza dell'array dell'indice meno 1, pertanto il codice seguente non emetterà il primo elemento dell'array e restituirà il garbage per il valore finale che stampa.

#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;
}

Uscita: 2 3 4 5 GarbageValue

Di seguito viene illustrato il modo corretto per ottenere l'output desiderato:

#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;
}

Uscita: 1 2 3 4 5

È importante conoscere la lunghezza di un array prima di utilizzarlo, altrimenti si potrebbe danneggiare il buffer o causare un errore di segmentazione accedendo a posizioni di memoria fuori limite.

Funzione ricorsiva: manca la condizione di base

Calcolare il fattoriale di un numero è un classico esempio di funzione ricorsiva.

Manca la condizione di base:

#include <stdio.h>

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

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

Uscita tipica: Segmentation fault: 11

Il problema con questa funzione è il loop infinito, che causa un errore di segmentazione: è necessaria una condizione di base per interrompere la ricorsione.

Condizione di base dichiarata:

#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;
}

Uscita di esempio

Factorial 3 = 6

Questa funzione termina non appena colpisce la condizione n è uguale a 1 (a condizione che il valore iniziale di n sia abbastanza piccolo - il limite superiore è 12 quando int è una quantità di 32 bit).

Regole da seguire:

  1. Inizializza l'algoritmo. I programmi ricorsivi spesso hanno bisogno di un valore iniziale da cui partire. Ciò può essere ottenuto utilizzando un parametro passato alla funzione o fornendo una funzione gateway che non è ricorsiva ma che imposta i valori seme per il calcolo ricorsivo.
  2. Verificare se i valori correnti in fase di elaborazione corrispondono al caso base. In tal caso, elaborare e restituire il valore.
  3. Ridefinisci la risposta in termini di un sotto-problema o sotto-problema più piccolo o più semplice.
  4. Esegui l'algoritmo sul sotto-problema.
  5. Combina i risultati nella formulazione della risposta.
  6. Restituire i risultati.

Fonte: funzione ricorsiva

Controllo dell'espressione logica contro 'vero'

Lo standard C originale non aveva alcun tipo booleano intrinseco, quindi bool , true e false non avevano alcun significato intrinseco e venivano spesso definiti dai programmatori. Tipicamente true sarebbe definito come 1 e false sarebbe definito come 0.

C99

C99 aggiunge il tipo built-in _Bool e l'intestazione <stdbool.h> che definisce bool (espandibile in _Bool ), false e true . Permette anche di ridefinire bool , true e false , ma osserva che questa è una caratteristica obsoleta.

Ancora più importante, le espressioni logiche trattano tutto ciò che viene valutato a zero come falso e qualsiasi valutazione diversa da zero come vera. Per esempio:

/* 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;
    }
}

Nell'esempio sopra, la funzione sta cercando di verificare se il bit superiore è impostato e restituisce true se lo è. Tuttavia, controllando esplicitamente contro true , l'istruzione if avrà successo solo se (bitfield & 0x80) valutata a qualsiasi cosa sia definita true , che è in genere 1 e molto raramente 0x80 . O esplicitamente controllare il caso che ti aspetti:

/* 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;
    }
}

O valuta qualsiasi valore diverso da zero come vero.

/* 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;
    }
}

I valori letterali in virgola mobile sono di tipo double per impostazione predefinita

Bisogna fare attenzione quando si inizializzano le variabili del tipo float su valori letterali o confrontandole con valori letterali, perché i letterali a virgola mobile regolari come 0.1 sono di tipo double . Questo può portare a sorprese:

#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

Qui, n viene inizializzato e arrotondato alla precisione singola, risultando in valore 0.10000000149011612. Quindi, n viene riconvertito in doppia precisione per essere confrontato con 0.1 letterale (che equivale a 0.10000000000000001), risultando in una mancata corrispondenza.

Oltre agli errori di arrotondamento, la miscelazione di variabili float con valori letterali double comporterà scarse prestazioni su piattaforme che non dispongono di supporto hardware per la doppia precisione.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow