Zoeken…


Invoering

In dit gedeelte worden enkele veelvoorkomende fouten besproken die een C-programmeur moet weten en moet vermijden. Zie Ongedefinieerd gedrag voor meer informatie over enkele onverwachte problemen en hun oorzaken

Getekende en niet-ondertekende gehele getallen mengen in rekenkundige bewerkingen

Het is meestal geen goed idee om signed en unsigned signed gehele getallen te combineren in rekenkundige bewerkingen. Wat zal bijvoorbeeld het volgende voorbeeld opleveren?

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

Omdat 1000 meer is dan -1, zou je verwachten dat de uitvoer a is more than b , maar dat zal niet het geval zijn.

Rekenkundige bewerkingen tussen verschillende integrale typen worden uitgevoerd binnen een gemeenschappelijk type dat wordt gedefinieerd door de zogenaamde gebruikelijke rekenkundige conversies (zie de taalspecificatie, 6.3.1.8).

In dit geval is het 'algemene type' niet- unsigned int , omdat, zoals vermeld in gebruikelijke rekenkundige conversies ,

714 Anders, als de operand met het type zonder geheel getal een rang heeft die groter of gelijk is aan de rang van het type van de andere operand, wordt de operand met type met geheel getal geconverteerd naar het type operand met het type zonder geheel getal.

Dit betekent dat int operand b vóór de vergelijking wordt geconverteerd naar unsigned int .

Wanneer -1 wordt geconverteerd naar een unsigned int het resultaat de maximaal mogelijke unsigned int waarde, die groter is dan 1000, wat betekent dat a > b onwaar is.

Foutief schrijven = in plaats van == bij vergelijken

De operator = wordt gebruikt voor toewijzing.

De operator == wordt ter vergelijking gebruikt.

Men moet voorzichtig zijn om de twee niet te mengen. Soms schrijft iemand ten onrechte

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

toen wat echt gewenst was:

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

De eerste wijst de waarde van y toe aan x en controleert of die waarde niet nul is, in plaats van een vergelijking uit te voeren, wat overeenkomt met:

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

Er zijn momenten waarop het testen van het resultaat van een opdracht is bedoeld en vaak wordt gebruikt, omdat hiermee wordt voorkomen dat code moet worden gedupliceerd en de eerste keer speciaal moet worden behandeld. Vergelijken

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

versus

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

Moderne compilers herkennen dit patroon en waarschuwen niet wanneer de opdracht tussen haakjes staat zoals hierboven, maar kunnen waarschuwen voor ander gebruik. Bijvoorbeeld:

if (x = y)         /* warning */

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

Sommige programmeurs gebruiken de strategie om de constante links van de operator te plaatsen (gewoonlijk Yoda-condities genoemd ). Omdat constanten waarden zijn, zorgt deze stijl ervoor dat de compiler een fout genereert als de verkeerde operator wordt gebruikt.

if (5 = y) /* Error */

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

Dit vermindert echter de leesbaarheid van de code aanzienlijk en wordt niet noodzakelijk geacht als de programmeur goede C-coderingspraktijken volgt en het helpt niet bij het vergelijken van twee variabelen, dus het is geen universele oplossing. Bovendien kunnen veel moderne compilers waarschuwingen geven wanneer code is geschreven met Yoda-voorwaarden.

Voorzichtig gebruik van puntkomma's

Wees voorzichtig met puntkomma's. Volgend voorbeeld

if (x > a);
   a = x;

betekent eigenlijk:

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

wat betekent dat x in elk geval wordt toegewezen aan a , wat misschien niet is wat je oorspronkelijk wilde.

Soms veroorzaakt het missen van een puntkomma ook een onmerkbaar probleem:

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

De puntkomma achter retour wordt gemist, dus day = date [0] wordt geretourneerd.

Een techniek om dit en soortgelijke problemen te voorkomen, is om altijd accolades te gebruiken op conditionals met meerdere regels en lussen. Bijvoorbeeld:

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

Vergeten om een extra byte toe te wijzen voor \ 0

Wanneer u een string naar een malloc ed-buffer kopieert, vergeet dan niet om 1 aan strlen toe te strlen .

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

strcpy(dest, src);

Dit komt omdat strlen niet de achterloop \0 in de lengte strlen . Als u de WRONG (zoals hierboven weergegeven) aanpak gebruikt, zou uw programma, wanneer u strcpy , ongedefinieerd gedrag oproepen.

Het is ook van toepassing op situaties waarin u een string met een bekende maximale lengte van stdin of een andere bron leest. Bijvoorbeeld

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

Vergeten om geheugen vrij te maken (geheugenlekken)

Een praktische strdup() programmeren is om geheugen vrij te maken dat rechtstreeks is toegewezen door uw eigen code of impliciet door een interne of externe functie aan te roepen, zoals een bibliotheek-API zoals strdup() . Als u geen geheugen vrijmaakt, kan dit leiden tot een geheugenlek, dat zich kan ophopen in een aanzienlijke hoeveelheid verspild geheugen dat niet beschikbaar is voor uw programma (of het systeem), wat mogelijk kan leiden tot crashes of ongedefinieerd gedrag. Problemen treden vaker op als het lek herhaaldelijk in een lus of recursieve functie optreedt. Het risico op programmafalen neemt toe naarmate een lekkend programma langer loopt. Soms verschijnen problemen meteen; op andere momenten zullen problemen uren of zelfs jaren van constante werking niet worden gezien. Gebrek aan geheugenuitputting kan catastrofaal zijn, afhankelijk van de omstandigheden.

De volgende oneindige lus is een voorbeeld van een lek dat uiteindelijk beschikbaar geheugenlek zal getline() door getline() aan te roepen, een functie die impliciet nieuw geheugen toewijst, zonder dat geheugen vrij te maken.

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

De onderstaande code gebruikt getline() ook de functie getline() , maar deze keer wordt het toegewezen geheugen correct vrijgegeven, waardoor een lek wordt voorkomen.

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

Lekkend geheugen heeft niet altijd tastbare gevolgen en is niet noodzakelijk een functioneel probleem. Hoewel 'best practice' strikt geheugen vrijmaken op strategische punten en omstandigheden vereist, om geheugenvoetafdruk te verminderen en het risico op geheugenuitputting te verminderen, kunnen er uitzonderingen zijn. Als een programma bijvoorbeeld qua duur en reikwijdte beperkt is, kan het risico van mislukte allocatie als te klein worden beschouwd. In dat geval kan het omzeilen van expliciete deallocatie acceptabel worden geacht. De meeste moderne besturingssystemen maken bijvoorbeeld automatisch al het geheugen vrij dat door een programma wordt verbruikt wanneer het wordt beëindigd, of dit nu te wijten is aan een programmafout, een systeemaanroep om exit() , procesbeëindiging of het einde van main() . Expliciet geheugen vrijmaken op het punt van dreigende programma-beëindiging kan zelfs overbodig zijn of een prestatieverlies veroorzaken.

De toewijzing kan mislukken als er onvoldoende geheugen beschikbaar is en er moet op de juiste niveaus van de oproepstack rekening worden gehouden met afhandelingsfouten. getline() , hierboven weergegeven, is een interessante use-case omdat het een bibliotheekfunctie is die niet alleen geheugen toewijst dat het aan de beller laat om vrij te maken, maar om een aantal redenen kan falen, waarmee allemaal rekening moet worden gehouden. Daarom is het van essentieel belang om bij het gebruik van een C API de documentatie (man-pagina) te lezen en bijzondere aandacht te schenken aan foutcondities en geheugengebruik, en zich bewust te zijn van welke softwarelaag de taak is om geretourneerd geheugen vrij te maken.

Een andere veel voorkomende methode voor geheugenverwerking is om geheugenaanwijzers consequent onmiddellijk op NULL in te stellen nadat het geheugen waarnaar deze verwijzingen verwijzen, is vrijgegeven, zodat die verwijzingen op elk moment op geldigheid kunnen worden getest (bijv. Gecontroleerd op NULL / niet-NULL), omdat ze toegang hebben tot vrij geheugen kan leiden tot ernstige problemen zoals het ophalen van afvalgegevens (leesbewerking) of gegevensbeschadiging (schrijfbewerking) en / of een programmacrash. In de meeste moderne besturingssystemen is het vrijmaken van geheugenlocatie 0 ( NULL ) een NOP (bijv. Het is onschadelijk), zoals vereist door de C-standaard - dus door een aanwijzer op NULL in te stellen, is er geen risico op dubbel geheugen vrijmaken als de aanwijzer is doorgegeven aan free() . Houd er rekening mee dat het dubbel vrijmaken van geheugen kan leiden tot zeer tijdrovende, verwarrende en moeilijk te diagnosticeren storingen.

Te veel kopiëren

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

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

Als de gebruiker een tekenreeks van meer dan 7 tekens invoert (- 1 voor de null-terminator), wordt het geheugen achter de buffer- buf overschreven. Dit resulteert in ongedefinieerd gedrag. Schadelijke hackers maken hier vaak misbruik van om het retouradres te overschrijven en te wijzigen in het adres van de kwaadaardige code van de hacker.

Vergeten om de retourwaarde van realloc naar een tijdelijke kopie te kopiëren

Als realloc mislukt, wordt NULL geretourneerd. Als u de waarde van de oorspronkelijke buffer toewijst aan de retourwaarde van realloc en deze NULL retourneert, gaat de oorspronkelijke buffer (de oude aanwijzer) verloren, wat resulteert in een geheugenlek . De oplossing wordt kopiëren in een tijdelijke pointer, en als dat tijdelijk niet NULL is, te kopiëren naar de werkelijke buffer.

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

Drijvende-kommagetallen vergelijken

Zwevende punttypen ( float , double en long double ) kunnen sommige getallen niet precies weergeven omdat ze eindige precisie hebben en de waarden in een binair formaat vertegenwoordigen. Net zoals we herhalende decimalen in basis 10 hebben voor breuken zoals 1/3, zijn er ook breuken die niet eindig in binair getal kunnen worden weergegeven (zoals 1/3, maar, nog belangrijker, 1/10). Vergelijk drijvende-kommawaarden niet rechtstreeks; gebruik in plaats daarvan een 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;
}

Een ander voorbeeld:

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

Output:

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)

Extra schalen doen in rekenkundige aanwijzer

In rekenkundige aanwijzer wordt het gehele getal dat moet worden toegevoegd of afgetrokken van de aanwijzer niet geïnterpreteerd als een adreswijziging, maar als een aantal te verplaatsen elementen .

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

Deze code doet extra schaal bij het berekenen van de aan ptr2 toegewezen ptr2 . Als sizeof(int) 4 is, wat typisch is in moderne 32-bits omgevingen, staat de uitdrukking voor "8 elementen na array[0] ", wat buiten bereik is, en roept het ongedefinieerd gedrag op .

Om ptr2 te hebben op wat 2 elementen is na array[0] , moet u eenvoudig 2 toevoegen.

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

Expliciet pointer rekenen met behulp van additieve operatoren kan verwarrend zijn, dus het gebruik van array subscripting kan beter zijn.

#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] is identiek aan (*((E1)+(E2))) ( N1570 6.5.2.1, alinea 2), en &(E1[E2]) is equivalent aan ((E1)+(E2)) ( N1570 6.5.3.2, voetnoot 102).

Als alternatief, als aanwijzer rekenen de voorkeur heeft, kan het casten van de aanwijzer naar een ander gegevenstype byte-adressering toestaan. Maar wees voorzichtig: endianness kan een probleem worden en casten naar andere typen dan 'pointer to character' leidt tot strikte aliasingproblemen .

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

Macro's zijn eenvoudige tekenreeksvervangingen

Macro's zijn eenvoudige tekenreeksvervangingen. (Strikt genomen werken ze met preprocessing-tokens, niet met willekeurige tekenreeksen.)

#include <stdio.h>

#define SQUARE(x) x*x

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

U mag verwachten dat deze code 9 ( 3*3 ) afdrukt, maar in feite wordt 5 afgedrukt omdat de macro wordt uitgebreid tot 1+2*1+2 .

U moet de argumenten en de hele macro-uitdrukking tussen haakjes plaatsen om dit probleem te voorkomen.

#include <stdio.h>

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

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

Een ander probleem is dat de argumenten van een macro niet gegarandeerd één keer worden geëvalueerd; ze kunnen helemaal niet worden geëvalueerd of kunnen meerdere keren worden geëvalueerd.

#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 deze code wordt de macro uitgebreid naar ((a++) <= (10) ? (a++) : (10)) . Omdat a++ ( 0 ) kleiner is dan 10 , wordt a++ twee keer geëvalueerd en maakt het de waarde van a en wat wordt geretourneerd door MIN verschilt van wat je zou verwachten.

Dit kan worden voorkomen door functies te gebruiken, maar houd er rekening mee dat de typen worden vastgelegd door de functiedefinitie, terwijl macro's (te) flexibel kunnen zijn met typen.

#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 is het probleem van dubbele evaluatie opgelost, maar deze min functie kan double gegevens niet verwerken zonder bijvoorbeeld af te kappen.

Macrorichtlijnen kunnen van twee soorten zijn:

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

Wat deze twee soorten macro's onderscheidt, is het teken dat na de ID volgt na #define : als het een lparen is , is het een functieachtige macro; anders is het een objectachtige macro. Als het de bedoeling is om een functieachtige macro te schrijven, mag er geen spatie tussen het einde van de naam van de macro en ( . Controleer dit voor een gedetailleerde uitleg.

C99

In C99 of hoger kunt u static inline int min(int x, int y) { … } .

C11

In C11 zou je een 'type-generieke' uitdrukking voor min kunnen schrijven.

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

De generieke uitdrukking kan worden uitgebreid met meer typen, zoals double , float , long long , unsigned long , long , unsigned - en geschikte gen_min macro-aanroepen geschreven.

Ongedefinieerde referentiefouten bij het koppelen

Een van de meest voorkomende fouten bij het compileren gebeurt tijdens de koppelingsfase. De fout ziet er ongeveer zo uit:

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

Laten we dus kijken naar de code die deze fout heeft gegenereerd:

int foo(void);

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

We zien hier een verklaring van foo ( int foo(); ) maar geen definitie ervan (werkelijke functie). Dus hebben we de compiler voorzien van de functiekopbal, maar deze functie is nergens gedefinieerd, dus de compilatiefase verloopt, maar de linker wordt afgesloten met een Undefined reference .
Om deze fout in ons kleine programma op te lossen, zouden we alleen een definitie voor foo moeten toevoegen:

/* 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 zal deze code compileren. Een alternatieve situatie doet zich voor wanneer de bron voor foo() in een afzonderlijk bronbestand foo.c (en er is een kop foo.h om foo() die is opgenomen in zowel foo.c als undefined_reference.c ). De oplossing is dan om zowel het objectbestand van foo.c als undefined_reference.c te koppelen, of om beide bronbestanden te compileren:

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

Of:

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

Een meer complex geval is waar bibliotheken bij betrokken zijn, zoals in de code:

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

De code is syntactisch correct, verklaring voor pow() bestaat uit #include <math.h> , dus we proberen te compileren en koppelen, maar krijgen een fout als deze:

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

Dit gebeurt omdat de definitie voor pow() niet werd gevonden tijdens de koppelingsfase. Om dit moeten we aangeven dat we willen koppelen tegen de math library, genaamd fix libm onder vermelding van het -lm vlag. (Merk op dat er platforms zoals macOS zijn waar -lm niet nodig is, maar wanneer u de ongedefinieerde referentie krijgt, is de bibliotheek nodig.)

Dus voeren we de compilatiefase opnieuw uit, dit keer met de bibliotheek (na de bron- of objectbestanden):

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

En het werkt!

Misverstand array-verval

Een veel voorkomend probleem in code die multidimensionale arrays, arrays of pointers, etc. gebruikt, is het feit dat Type** en Type[M][N] fundamenteel verschillende typen zijn:

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

Voorbeeld compileruitgang:

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)

De fout dat de s array in de main functie is doorgegeven aan de functie print_strings , die een ander soort pointer dan terugontvangen verwacht. Het bevat ook een notitie die het type uitdrukt dat wordt verwacht door print_strings en het type dat er van main is doorgegeven.

Het probleem is te wijten aan zoiets als array-verval . Wat er gebeurt wanneer s met het type char[4][20] (array van 4 arrays van 20 tekens) wordt doorgegeven aan de functie, verandert in een pointer naar het eerste element alsof je &s[0] , dat het type char (*)[20] (pointer naar 1 array van 20 tekens). Dit gebeurt voor elke array, inclusief een array van pointers, een array van arrays of arrays (3-D arrays) en een array van pointers naar een array. Hieronder is een tabel die laat zien wat er gebeurt als een array vervalt. Wijzigingen in de typebeschrijving zijn gemarkeerd om te illustreren wat er gebeurt:

Voor verval Na verval
char [20] reeks van (20 tekens) char * pointer to (1 char)
char [4][20] array van (4 arrays van 20 tekens) char (*)[20] pointer to (1 array van 20 tekens)
char *[4] reeks van (4 wijzers tot 1 teken) char ** pointer to (1 pointer to 1 char)
char [3][4][20] array van (3 arrays van 4 arrays van 20 tekens) char (*)[4][20] pointer to (1 array van 4 arrays van 20 tekens)
char (*[4])[20] reeks van (4 wijzers tot 1 reeks van 20 tekens) char (**)[20] pointer to (1 pointer to 1 array of 20 chars)

Als een array kan vervallen tot een pointer, kan worden gezegd dat een pointer als een array van minimaal 1 element kan worden beschouwd. Een uitzondering hierop is een nulaanwijzer, die naar niets wijst en dus geen array is.

Array verval komt slechts eenmaal voor. Als een array is vervallen tot een pointer, is deze nu een pointer, geen array. Zelfs als u een aanwijzer naar een array hebt, moet u er rekening mee houden dat de aanwijzer kan worden beschouwd als een array van ten minste één element, dus het verval van de array is al opgetreden.

Met andere woorden, een pointer naar een array ( char (*)[20] ) wordt nooit een pointer naar een pointer ( char ** ). Om de functie print_strings te repareren, print_strings ervoor dat deze het juiste type ontvangt:

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

Er doet zich een probleem voor wanneer u wilt dat de functie print_strings generiek is voor elke reeks tekens: wat als er 30 tekens zijn in plaats van 20? Of 50? Het antwoord is om nog een parameter toe te voegen vóór de array-parameter:

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

Het compileren ervan produceert geen fouten en resulteert in de verwachte uitvoer:

Example 1
Example 2
Example 3
Example 4

Niet-aangrenzende arrays doorgeven aan functies die "echte" multidimensionale arrays verwachten

Bij het toewijzen van multidimensionale arrays met malloc , calloc en realloc , is een gebruikelijk patroon om de binnenste arrays toe te wijzen met meerdere oproepen (zelfs als de oproep slechts eenmaal verschijnt, kan deze in een lus zijn):

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

Het verschil in bytes tussen het laatste element van een van de binnenste arrays en het eerste element van de volgende binnenste array is mogelijk niet 0 zoals ze zouden zijn met een "echte" multidimensionale array (bijvoorbeeld int array[4][16]; ) :

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

Rekening houdend met de grootte van int , krijg je een verschil van 8128 bytes (8132-4), wat 2032 int array array-elementen is, en dat is het probleem: een "echte" multidimensionale array heeft geen openingen tussen elementen.

Als u een dynamisch toegewezen array moet gebruiken met een functie die een "echte" multidimensionale array verwacht, moet u een object van het type int * toewijzen en rekenen gebruiken om berekeningen uit te voeren:

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

Als N een macro of een geheel getal letterlijk is in plaats van een variabele, kan de code eenvoudig de meer natuurlijke 2D-matrixnotatie gebruiken na het toewijzen van een aanwijzer aan een matrix:

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

Als N geen macro of een geheel getal is, verwijst array naar een array met variabele lengte (VLA). Dit kan nog steeds worden gebruikt met func door te casten naar int * en een nieuwe functie func_vla zou func_N vervangen:

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

Opmerking : VLA's zijn optioneel vanaf C11. Als uw implementatie C11 ondersteunt en de macro __STDC_NO_VLA__ definieert op 1, zit u vast aan de pre-C99-methoden.

Tekenconstanten gebruiken in plaats van letterlijke tekenreeksen en vice versa

In C zijn karakterconstanten en tekenreeksliteralen verschillende dingen.

Een karakter omringd door enkele aanhalingstekens zoals 'a' is een karakterconstante . Een tekenconstante is een geheel getal waarvan de waarde de tekencode is die voor het teken staat. Hoe karakterconstanten met meerdere karakters zoals 'abc' moeten worden geïnterpreteerd 'abc' is door de implementatie bepaald.

Nul of meer tekens omringd door dubbele aanhalingstekens zoals "abc" is een letterlijke tekenreeks . Een letterlijke tekenreeks is een niet-wijzigbare array waarvan de elementen van het type char . De tekenreeks in de dubbele aanhalingstekens plus afsluitend null-teken is de inhoud, dus "abc" heeft 4 elementen ( {'a', 'b', 'c', '\0'} )

In dit voorbeeld wordt een tekenconstante gebruikt waar een letterlijke tekenreeks moet worden gebruikt. Deze tekenconstante wordt op een door de implementatie gedefinieerde manier geconverteerd naar een pointer en er is weinig kans dat de geconverteerde pointer geldig is, dus dit voorbeeld roept ongedefinieerd gedrag op .

#include <stdio.h>

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

In dit voorbeeld wordt een letterlijke tekenreeks gebruikt waar een tekenconstante moet worden gebruikt. De aanwijzer die is geconverteerd van de letterlijke tekenreeks, wordt op een door de implementatie gedefinieerde manier omgezet in een geheel getal en op een door de implementatie gedefinieerde manier omgezet in char . (Hoe een geheel getal naar een ondertekend type kan worden geconverteerd dat niet de waarde kan zijn die moet worden geconverteerd, is door de implementatie gedefinieerd en of char is ondertekend is ook door de implementatie gedefinieerd.) De uitvoer is betekenisloos.

#include <stdio.h>

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

In bijna alle gevallen zal de compiler klagen over deze verwisselingen. Als dit niet het geval is, moet u meer waarschuwingsopties voor de compiler gebruiken, of het wordt aanbevolen dat u een betere compiler gebruikt.

Negeren van retourwaarden van bibliotheekfuncties

Bijna elke functie in C-standaardbibliotheek retourneert iets over succes en iets anders over fouten. malloc retourneert bijvoorbeeld een pointer naar het geheugenblok dat is toegewezen door de functie bij succes, en, als de functie het gevraagde geheugenblok niet heeft toegewezen, een lege pointer. Controleer dus altijd de retourwaarde voor eenvoudiger debuggen.

Dit is slecht:

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

Dit is goed:

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

Op deze manier weet je meteen de oorzaak van de fout, anders kun je uren zoeken naar een bug op een volledig verkeerde plek.

Newline-teken wordt niet gebruikt bij een typische scanf () -aanroep

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

wordt uitgevoerd met deze invoer

42
life

de output zal 42 "" plaats van de verwachte 42 "life" .

Dit komt omdat een newline-teken na 42 niet wordt gebruikt in de aanroep van scanf() en het wordt gebruikt door fgets() voordat het life wordt gelezen. Vervolgens stopt fgets() met lezen voordat het life gelezen.

Om dit probleem te voorkomen, is een manier die handig is wanneer de maximale lengte van een lijn bekend is - bijvoorbeeld bij het oplossen van problemen in online fgets() - het vermijden van het gebruik van scanf() en het lezen van alle lijnen via fgets() . U kunt sscanf() gebruiken om de gelezen regels te parseren.

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

Een andere manier is om te lezen totdat u een fgets() raakt na het gebruik van scanf() en voordat u 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;
}

Een puntkomma toevoegen aan een #define

Het is gemakkelijk om in de C-preprocessor in de war te raken en het als onderdeel van C zelf te behandelen, maar dat is een vergissing omdat de preprocessor slechts een tekstvervangingsmechanisme is. Bijvoorbeeld als u schrijft

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

de code wordt uitgebreid naar

int arr[100;];

wat een syntaxisfout is. De oplossing is om de puntkomma van de regel #define te verwijderen. Het is bijna altijd een fout om een #define te eindigen met een puntkomma.

Opmerkingen met meerdere regels kunnen niet worden genest

In C nestelen opmerkingen met meerdere regels, / * en * / niet.

Als u een codeblok of functie annoteert met deze stijl van commentaar:

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

Je kunt het niet gemakkelijk becommentariëren:

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

Een oplossing is om C99-stijlopmerkingen te gebruiken:

// 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 het hele blok eenvoudig worden becommentarieerd:

/*

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

*/

Een andere oplossing is om te voorkomen dat code wordt uitgeschakeld met behulp van reactiesyntaxis, in plaats daarvan met #ifdef of #ifndef preprocessorrichtlijnen. Deze richtlijnen doen nest, zodat u vrij om uw code te reageren in de stijl die u verkiest.

#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

Sommige gidsen gaan zelfs zo ver om aan te bevelen dat er nooit commentaar moet worden gegeven op secties van codes en dat als code tijdelijk moet worden uitgeschakeld, een #if 0 richtlijn kan worden gebruikt.

Zie #if 0 om codesecties te blokkeren .

Arraygrenzen overschrijden

Arrays zijn gebaseerd op nul, dat wil zeggen dat de index altijd begint bij 0 en eindigt met een indexarraylengte minus 1. De volgende code voert dus niet het eerste element van de array uit en geeft afval weer voor de uiteindelijke waarde die wordt afgedrukt.

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

Output: 2 3 4 5 GarbageValue

Het volgende toont de juiste manier om de gewenste output te bereiken:

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

Uitgang: 1 2 3 4 5

Het is belangrijk om de lengte van een array te kennen voordat u ermee werkt, omdat u anders de buffer kunt beschadigen of een segmentatiefout kunt veroorzaken door toegang te krijgen tot geheugenlocaties die buiten het bereik zijn.

Recursieve functie - mist de basisconditie

Het berekenen van de faculteit van een getal is een klassiek voorbeeld van een recursieve functie.

Ontbrekende basisconditie:

#include <stdio.h>

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

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

Typische uitgang: Segmentation fault: 11

Het probleem met deze functie is dat het oneindig in een lus zou lopen, wat een segmentatiefout veroorzaakt - het heeft een basisvoorwaarde nodig om de recursie te stoppen.

Basisvoorwaarde verklaard:

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

Voorbeelduitvoer

Factorial 3 = 6

Deze functie wordt beëindigd zodra deze de voorwaarde n is gelijk aan 1 (op voorwaarde dat de beginwaarde van n klein genoeg is - de bovengrens is 12 wanneer int een 32-bits hoeveelheid is).

Te volgen regels:

  1. Initialiseer het algoritme. Recursieve programma's hebben vaak een startwaarde nodig. Dit wordt bereikt door een parameter te gebruiken die aan de functie is doorgegeven of door een gateway-functie te bieden die niet-recursief is, maar die de seed-waarden voor de recursieve berekening instelt.
  2. Controleer of de huidige waarde (n) die worden verwerkt, overeenkomen met het basisscenario. Zo ja, verwerk en retourneer de waarde.
  3. Definieer het antwoord opnieuw in termen van een kleiner of eenvoudiger subprobleem of subproblemen.
  4. Voer het algoritme uit voor het subprobleem.
  5. Combineer de resultaten in de formulering van het antwoord.
  6. Retourneer de resultaten.

Bron: recursieve functie

Logische expressie vergelijken met 'waar'

De originele C-norm had geen intrinsieke Boolean, zodat bool , true en false had geen inherente betekenis en werden vaak bepaald door programmeurs. Normaal gesproken zou true worden gedefinieerd als 1 en false zou worden gedefinieerd als 0.

C99

C99 voegt het ingebouwde type _Bool en de koptekst <stdbool.h> die bool definieert (uitbreiden naar _Bool ), false en true . Hiermee kunt u ook bool , true en false , opnieuw definiëren, maar merkt op dat dit een verouderde functie is.

Wat nog belangrijker is, logische uitdrukkingen behandelen alles dat tot nul evalueert als vals en elke niet-nulevaluatie als waar. Bijvoorbeeld:

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

In het bovenstaande voorbeeld probeert de functie te controleren of het bovenste bit is ingesteld en retourneert true als dit het geval is. Door echter expliciet te controleren op true , zal de if instructie alleen slagen als (bitfield & 0x80) evalueert naar wat true is gedefinieerd als, wat meestal 1 en zeer zelden 0x80 . Controleer expliciet de zaak die u verwacht:

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

Of evalueer elke niet-nulwaarde als waar.

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

Zwevende puntliteralen zijn standaard van het type double

Voorzichtigheid is geboden bij het initialiseren van variabelen van het type float naar letterlijke waarden of het vergelijken van deze met letterlijke waarden, omdat gewone zwevende puntliteralen zoals 0.1 van het double type zijn. Dit kan tot verrassingen leiden:

#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

Hier wordt n geïnitialiseerd en afgerond op één precisie, wat resulteert in een waarde van 0,10000000149011612. Vervolgens wordt n terug geconverteerd naar dubbele precisie om te worden vergeleken met 0.1 liter (wat overeenkomt met 0,10000000000000001), wat resulteert in een mismatch.

Naast afrondingsfouten, mengen float variabelen met double literals zal resulteren in slechte prestaties op platforms die niet beschikken over hardware-ondersteuning voor dubbele precisie.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow