Szukaj…


Wprowadzenie

W tej sekcji omówiono niektóre typowe błędy, o których programista C powinien wiedzieć i powinien unikać. Aby uzyskać więcej informacji na temat niektórych nieoczekiwanych problemów i ich przyczyn, zobacz Niezdefiniowane zachowanie

Mieszanie liczb całkowitych ze znakiem i bez znaku w operacjach arytmetycznych

Zwykle nie jest to dobry pomysł, aby mieszać signed i unsigned liczb całkowitych w operacji arytmetycznych. Na przykład, co zostanie wygenerowane w następującym przykładzie?

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

Ponieważ 1000 jest większe niż -1, można oczekiwać, że wynik będzie a is more than b , ale tak nie będzie.

Operacje arytmetyczne pomiędzy różnymi typami całek są wykonywane we wspólnym typie zdefiniowanym przez tak zwane zwykłe konwersje arytmetyczne (patrz specyfikacja języka, 6.3.1.8).

W tym przypadku „typ wspólny” jest unsigned int , ponieważ, jak stwierdzono w Zwykłych konwersjach arytmetycznych ,

714 W przeciwnym razie, jeśli operand, który ma typ całkowity bez znaku, ma rangę większą lub równą rangi typu drugiego operandu, wówczas operand z typem całkowitym ze znakiem jest konwertowany na typ argumentu z typem całkowitym bez znaku.

Oznacza to, że operand int b zostanie przekonwertowany na unsigned int przed porównaniem.

Kiedy -1 jest konwertowane na unsigned int wynikiem jest maksymalna możliwa wartość unsigned int , która jest większa niż 1000, co oznacza, że a > b jest fałszem.

Błędnie pisze = zamiast == podczas porównywania

Operator = służy do przypisania.

Do porównania służy operator == .

Należy uważać, aby nie mieszać tych dwóch. Czasami ktoś błędnie pisze

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

kiedy naprawdę chciałem:

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

Ten pierwszy przypisuje wartość y do x i sprawdza, czy ta wartość nie jest zerem, zamiast porównywania, co jest równoważne z:

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

Są chwile, gdy testowanie wyniku przypisania jest zamierzone i jest powszechnie stosowane, ponieważ pozwala uniknąć konieczności duplikowania kodu i specjalnego traktowania za pierwszym razem. Porównać

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

przeciw

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

Nowoczesne kompilatory rozpoznają ten wzorzec i nie ostrzegają, gdy zadanie znajduje się w nawiasach, jak powyżej, ale mogą ostrzegać o innych zastosowaniach. Na przykład:

if (x = y)         /* warning */

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

Niektórzy programiści stosują strategię umieszczania stałej po lewej stronie operatora (potocznie zwane warunkami Yoda ). Ponieważ stałe są wartościami, ten styl warunku spowoduje, że kompilator wyśle błąd, jeśli użyty zostanie niewłaściwy operator.

if (5 = y) /* Error */

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

Jednak to znacznie zmniejsza czytelność kodu i nie jest uważane za konieczne, jeśli programista przestrzega dobrych praktyk kodowania w C, i nie pomaga w porównywaniu dwóch zmiennych, więc nie jest to uniwersalne rozwiązanie. Ponadto wiele współczesnych kompilatorów może dawać ostrzeżenia, gdy kod jest pisany z warunkami Yoda.

Nieostrożne użycie średników

Uważaj na średniki. Poniższy przykład

if (x > a);
   a = x;

faktycznie oznacza:

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

co oznacza x zostaną przypisane do w każdej sprawie, która nie może być to, czego chciał pierwotnie. a

Czasami brak średnika spowoduje również niezauważalny problem:

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

Średnik za znakiem powrotu jest pominięty, więc dzień = data [0] zostanie zwrócony.

Jedną z technik pozwalających uniknąć tego i podobnych problemów jest zawsze stosowanie nawiasów klamrowych na warunkowych liniach i pętlach. Na przykład:

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

Zapomniałem przydzielić jednego dodatkowego bajtu dla \ 0

Kiedy kopiujesz ciąg do bufora malloc ed, zawsze pamiętaj, aby dodać 1 do strlen .

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

strcpy(dest, src);

Wynika to z faktu, że strlen nie uwzględnia końcowego \0 długości. Jeśli podejmiesz WRONG (jak pokazano powyżej) podejście, po wywołaniu strcpy , twój program wywoła niezdefiniowane zachowanie.

Dotyczy to również sytuacji, gdy czytasz ciąg znanej maksymalnej długości ze stdin lub innego źródła. Na przykład

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

Zapominanie o zwolnieniu pamięci (wycieki pamięci)

Najlepszą praktyką programistyczną jest zwolnienie pamięci, która została przydzielona bezpośrednio przez twój własny kod lub pośrednio poprzez wywołanie funkcji wewnętrznej lub zewnętrznej, takiej jak API biblioteki, takie jak strdup() . Brak wolnej pamięci może spowodować wyciek pamięci, który może się kumulować w znacznej ilości zmarnowanej pamięci, która jest niedostępna dla twojego programu (lub systemu), co może prowadzić do awarii lub niezdefiniowanego zachowania. Problemy są bardziej prawdopodobne, jeśli wyciek występuje wielokrotnie w pętli lub funkcji rekurencyjnej. Ryzyko niepowodzenia programu wzrasta wraz z upływem czasu nieszczelnego programu. Czasami problemy pojawiają się natychmiast; innym razem problemy nie będą widoczne przez wiele godzin, a nawet lat ciągłej pracy. Awarie wyczerpania pamięci mogą być katastrofalne, w zależności od okoliczności.

Następująca nieskończona pętla jest przykładem wycieku, który ostatecznie wyczerpie dostępny wyciek pamięci poprzez wywołanie getline() , funkcji, która domyślnie przydziela nową pamięć, bez zwalniania tej pamięci.

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

Natomiast poniższy kod korzysta również z funkcji getline() , ale tym razem przydzielona pamięć jest poprawnie zwalniana, co pozwala uniknąć wycieku.

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

Wyciek pamięci nie zawsze ma wymierne konsekwencje i niekoniecznie jest problemem funkcjonalnym. Chociaż „najlepsza praktyka” nakazuje rygorystyczne zwalnianie pamięci w strategicznych punktach i warunkach, aby zmniejszyć ślad pamięci i zmniejszyć ryzyko wyczerpania pamięci, mogą istnieć wyjątki. Na przykład, jeśli czas trwania i zakres programu jest ograniczony, ryzyko niepowodzenia alokacji można uznać za zbyt małe, aby się martwić. W takim przypadku ominięcie wyraźnego zwolnienia może zostać uznane za dopuszczalne. Na przykład większość współczesnych systemów operacyjnych automatycznie zwalnia całą pamięć zajętą przez program po jego zakończeniu, niezależnie od tego, czy jest to spowodowane awarią programu, wywołaniem systemowym exit() , zakończeniem procesu czy osiągnięciem końca main() . Jawne zwolnienie pamięci w momencie rychłego zakończenia programu może być w rzeczywistości zbędne lub spowodować obniżenie wydajności.

Alokacja może się nie powieść, jeśli dostępna jest niewystarczająca ilość pamięci, a awarie obsługi należy uwzględnić na odpowiednich poziomach stosu wywołań. Pokazana powyżej getline() jest interesującym przypadkiem użycia, ponieważ jest to funkcja biblioteki, która nie tylko przydziela pamięć, którą pozostawia wywołującemu, aby ją zwolnić, ale może zawieść z wielu powodów, z których wszystkie należy wziąć pod uwagę. Dlatego przy korzystaniu z C API należy koniecznie przeczytać dokumentację (stronę podręcznika ) i zwrócić szczególną uwagę na warunki błędów i wykorzystanie pamięci oraz mieć świadomość, która warstwa oprogramowania ponosi ciężar zwalniania zwróconej pamięci.

Inną powszechną praktyką obsługi pamięci jest konsekwentne ustawianie wskaźników pamięci na NULL natychmiast po zwolnieniu pamięci, do której odwołują się te wskaźniki, dzięki czemu wskaźniki te można przetestować pod kątem ważności w dowolnym momencie (np. Sprawdzone pod kątem NULL / non-NULL), ponieważ uzyskują dostęp do zwolnionej pamięci może prowadzić do poważnych problemów, takich jak pobieranie danych śmieci (operacja odczytu) lub uszkodzenie danych (operacja zapisu) i / lub awaria programu. W większości współczesnych systemów operacyjnych zwolnienie pamięci 0 ( NULL ) jest NOP (np. Jest nieszkodliwe), zgodnie z wymaganiami standardu C - więc ustawiając wskaźnik na NULL, nie ma ryzyka podwójnego zwolnienia pamięci, jeśli wskaźnik jest przekazywany do free() . Należy pamiętać, że podwójne zwolnienie pamięci może prowadzić do bardzo czasochłonnych, mylących i trudnych do zdiagnozowania awarii.

Kopiowanie za dużo

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

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

Jeśli użytkownik wprowadzi ciąg dłuższy niż 7 znaków (-1 dla terminatora zerowego), pamięć za buf buforem zostanie nadpisana. Powoduje to niezdefiniowane zachowanie. Złośliwi hakerzy często wykorzystują to w celu zastąpienia adresu zwrotnego i zmiany go na adres złośliwego kodu hakera.

Zapominanie o skopiowaniu wartości zwracanej przez realalloc do tymczasowej

Jeśli realloc nie powiedzie, zwraca NULL . Jeśli przypiszesz wartość oryginalnego bufora do wartości realloc przez realloc , a jeśli zwróci NULL , wówczas oryginalny bufor (stary wskaźnik) zostanie utracony, co spowoduje wyciek pamięci . Rozwiązaniem jest skopiować do tymczasowego wskaźnika, a jeśli to tymczasowy nie jest NULL, a następnie skopiować do realnego bufora.

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

Porównywanie liczb zmiennoprzecinkowych

Typy float ( float , double i long double ) nie mogą dokładnie reprezentować niektórych liczb, ponieważ mają skończoną precyzję i reprezentują wartości w formacie binarnym. Podobnie jak mamy powtarzające się ułamki dziesiętne w bazie 10 dla ułamków takich jak 1/3, istnieją ułamki, których nie można również przedstawić w postaci binarnej (np. 1/3, ale, co ważniejsze, 1/10). Nie porównuj bezpośrednio wartości zmiennoprzecinkowych; zamiast tego użyj delty.

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

Inny przykład:

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

Wynik:

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)

Dodatkowe skalowanie w arytmetyce wskaźników

W arytmetyce wskaźnika liczba całkowita, którą należy dodać lub odjąć do wskaźnika, jest interpretowana nie jako zmiana adresu, ale jako liczba elementów do przesunięcia.

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

Ten kod wykonuje dodatkowe skalowanie przy obliczaniu wskaźnika przypisanego do ptr2 . Jeśli sizeof(int) wynosi 4, co jest typowe w nowoczesnych środowiskach 32-bitowych, wyrażenie oznacza „8 elementów po array[0] ”, która jest poza zakresem i wywołuje niezdefiniowane zachowanie .

Aby ptr2 wskazywał na 2 elementy po array[0] , powinieneś po prostu dodać 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;
}

Jawna arytmetyka wskaźników za pomocą operatorów addytywnych może być myląca, więc użycie indeksowania tablic może być lepsze.

#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] jest identyczne z (*((E1)+(E2))) ( N1570 6.5.2.1, akapit 2), a &(E1[E2]) jest równoważne z ((E1)+(E2)) ( N1570 6.5.3.2, przypis 102).

Alternatywnie, jeśli preferowana jest arytmetyka wskaźnika, rzutowanie wskaźnika na inny typ danych może umożliwić adresowanie bajtów. Bądź jednak ostrożny: endianność może stać się problemem, a rzutowanie na typy inne niż „wskaźnik do postaci” prowadzi do ścisłych problemów z aliasingiem .

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

Makra to proste zamiany ciągów

Makra to proste zamiany ciągów. (Ściśle mówiąc, działają one z tokenami wstępnego przetwarzania, a nie z dowolnymi ciągami.)

#include <stdio.h>

#define SQUARE(x) x*x

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

Możesz oczekiwać, że ten kod wypisze 9 ( 3*3 ), ale tak naprawdę wydrukowane zostanie 5 , ponieważ makro zostanie rozwinięte do 1+2*1+2 .

Powinieneś zawinąć argumenty i całe makro wyrażenie w nawiasy, aby uniknąć tego problemu.

#include <stdio.h>

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

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

Innym problemem jest to, że nie można zagwarantować, że argumenty makra zostaną ocenione raz; mogą w ogóle nie być oceniane lub mogą być oceniane wielokrotnie.

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

W tym kodzie makro zostanie rozwinięte do ((a++) <= (10) ? (a++) : (10)) . Ponieważ a++ ( 0 ) jest mniejszy niż 10 , a++ zostanie oceniony dwukrotnie i sprawi, że wartość a i to, co zostanie zwrócone z MIN będzie inne niż można się spodziewać.

Można tego uniknąć za pomocą funkcji, ale należy pamiętać, że typy zostaną ustalone przez definicję funkcji, podczas gdy makra mogą być (zbyt) elastyczne z typami.

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

Teraz problem podwójnej oceny został naprawiony, ale ta funkcja min nie może na przykład poradzić sobie z double danymi bez obcięcia.

Dyrektywy makr mogą być dwojakiego rodzaju:

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

Tym, co wyróżnia te dwa typy makr, jest znak następujący po identyfikatorze po #define : jeśli jest to lparen , jest to makro funkcyjne; w przeciwnym razie jest to makro obiektowe. Jeśli intencją jest napisanie makra podobnego do funkcji, między końcem nazwy makra a ( nie może znajdować się biała spacja. ( Sprawdź to, aby uzyskać szczegółowe wyjaśnienie.

C99

W wersji C99 lub nowszej można użyć static inline int min(int x, int y) { … } .

C11

W C11 można napisać wyrażenie „typowe” dla 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;
}

Wyrażenie ogólne można rozszerzyć o więcej typów, takich jak double , float , long long , unsigned long , long , unsigned - i gen_min odpowiednie wywołania makr gen_min .

Niezdefiniowane błędy odniesienia podczas łączenia

Jeden z najczęstszych błędów w kompilacji występuje na etapie łączenia. Błąd wygląda podobnie do tego:

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

Spójrzmy więc na kod, który wygenerował ten błąd:

int foo(void);

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

Widzimy tutaj deklarację foo ( int foo(); ), ale bez jej definicji (faktyczna funkcja). Udostępniliśmy więc kompilatorowi nagłówek funkcji, ale nigdzie nie zdefiniowano takiej funkcji, więc etap kompilacji mija, ale linker kończy działanie z Undefined reference błędem Undefined reference .
Aby naprawić ten błąd w naszym małym programie, musielibyśmy tylko dodać definicję 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;
}

Teraz ten kod się skompiluje. Alternatywna sytuacja pojawia się, gdy źródło dla foo() znajduje się w osobnym pliku źródłowym foo.c (i istnieje nagłówek foo.h do zadeklarowania foo() który jest zawarty zarówno w foo.c jak i undefined_reference.c ). Następnie poprawką jest połączenie zarówno pliku obiektowego z foo.c i undefined_reference.c , lub skompilowanie obu plików źródłowych:

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

Lub:

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

Bardziej złożonym przypadkiem są biblioteki, na przykład w kodzie:

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

Kod jest poprawny pod względem składniowym, deklaracja dla pow() istnieje z #include <math.h> , więc próbujemy skompilować i połączyć, ale otrzymujemy błąd taki jak ten:

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

Dzieje się tak, ponieważ definicja pow() nie została znaleziona podczas etapu łączenia. Aby to naprawić, musimy określić, że chcemy połączyć się z biblioteką matematyczną o nazwie libm , podając -lm . (Zauważ, że istnieją platformy takie jak macOS, w których -lm nie jest potrzebny, ale gdy otrzymasz niezdefiniowane odwołanie, biblioteka jest potrzebna.)

Ponownie uruchamiamy etap kompilacji, tym razem określając bibliotekę (po plikach źródłowych lub obiektowych):

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

I to działa!

Nieporozumienie rozpadu tablicy

Częstym problemem w kodzie używającym tablic wielowymiarowych, tablic wskaźników itp. Jest fakt, że Type** i Type[M][N] są zasadniczo różnymi typami:

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

Przykładowe dane wyjściowe kompilatora:

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)

Błąd stwierdza, że tablica s w funkcji main jest przekazywana do funkcji print_strings , która oczekuje innego typu wskaźnika niż otrzymany. Zawiera także notatkę wyrażającą typ, którego oczekuje się od print_strings oraz typ, który został do niej przekazany z main .

Problem wynika z czegoś zwanego rozkładem tablicy . Co się dzieje, gdy s z typem char[4][20] (tablica 4 tablic po 20 znaków) jest przekazywany do funkcji, gdy zmienia się we wskaźnik do pierwszego elementu, tak jakbyś napisał &s[0] , który ma typ char (*)[20] (wskaźnik na 1 tablicę 20 znaków). Dzieje się tak w przypadku dowolnej tablicy, w tym tablicy wskaźników, tablicy tablic tablic (tablic 3-D) i tablicy wskaźników do tablicy. Poniżej znajduje się tabela ilustrująca, co dzieje się, gdy tablica ulega rozpadowi. Zmiany w opisie typu są podświetlone, aby zilustrować, co się dzieje:

Przed rozkładem Po rozpadzie
char [20] tablica (20 znaków) char * wskaźnik do (1 znak)
char [4][20] tablica (4 tablice po 20 znaków) char (*)[20] wskaźnik do (1 tablica 20 znaków)
char *[4] tablica (od 4 wskaźników do 1 znaku) char ** wskaźnik na (1 wskaźnik na 1 znak)
char [3][4][20] tablica (3 tablice po 4 tablice po 20 znaków) char (*)[4][20] wskaźnik do (1 tablica 4 tablice po 20 znaków)
char (*[4])[20] tablica (4 wskaźniki na 1 tablicę 20 znaków) char (**)[20] wskaźnik na (1 wskaźnik na 1 tablicę 20 znaków)

Jeśli tablica może rozpaść się na wskaźnik, to można powiedzieć, że wskaźnik można uznać za tablicę zawierającą co najmniej 1 element. Wyjątkiem jest wskaźnik zerowy, który nie wskazuje na nic, a zatem nie jest tablicą.

Rozpad tablicy następuje tylko raz. Jeśli tablica rozpadła się na wskaźnik, jest teraz wskaźnikiem, a nie tablicą. Nawet jeśli masz wskaźnik do tablicy, pamiętaj, że wskaźnik może być uważany za tablicę co najmniej jednego elementu, więc rozpad tablicy już nastąpił.

Innymi słowy, wskaźnik do tablicy ( char (*)[20] ) nigdy nie stanie się wskaźnikiem do wskaźnika ( char ** ). Aby naprawić funkcję print_strings , po prostu print_strings , aby otrzymała poprawny typ:

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

Problem pojawia się, gdy chcesz, aby funkcja print_strings była ogólna dla dowolnej tablicy znaków: co, jeśli będzie 30 znaków zamiast 20? Lub 50? Odpowiedzią jest dodanie innego parametru przed parametrem tablicy:

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

Kompilowanie go nie powoduje błędów i daje oczekiwane wyniki:

Example 1
Example 2
Example 3
Example 4

Przekazywanie nieprzylegających tablic do funkcji oczekujących „prawdziwych” tablic wielowymiarowych

Podczas przydzielania tablic wielowymiarowych za pomocą malloc , calloc i realloc , powszechnym wzorcem jest przydzielanie wewnętrznych tablic za pomocą wielu wywołań (nawet jeśli wywołanie pojawia się tylko raz, może być w pętli):

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

Różnica w bajtach między ostatnim elementem jednej z wewnętrznych tablic a pierwszym elementem kolejnej wewnętrznej tablicy może nie wynosić 0, tak jak w przypadku „prawdziwej” tablicy wielowymiarowej (np. int array[4][16]; ) :

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

Biorąc pod uwagę wielkość int , można uzyskać różnicę 8128 bajtów (8132-4), czyli 2032 int -sized elementy tablicy, i to jest problem: „prawdziwy” wielowymiarowa tablica ma żadnych przerw pomiędzy elementami.

Jeśli potrzebujesz użyć dynamicznie alokowanej tablicy z funkcją oczekującą „prawdziwej” tablicy wielowymiarowej, powinieneś przydzielić obiekt typu int * i użyć arytmetyki do wykonania obliczeń:

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

Jeśli N jest makro lub liczbą całkowitą zamiast zmiennej, kod może po prostu użyć bardziej naturalnej notacji 2-D po przypisaniu wskaźnika do tablicy:

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

Jeśli N nie jest literałem makro ani liczbą całkowitą, array będzie wskazywać array o zmiennej długości (VLA). Nadal można go używać z func , rzutując na int * a nowa funkcja func_vla zastąpiłaby 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

Uwaga : VLA są opcjonalne od C11. Jeśli Twoja implementacja obsługuje C11 i definiuje makro __STDC_NO_VLA__ na 1, utkniesz w metodach sprzed C99.

Używanie stałych znakowych zamiast literałów łańcuchowych i odwrotnie

W C stałe znaków i literały łańcuchowe to różne rzeczy.

Znak otoczony pojedynczymi cudzysłowami, takimi jak 'a' jest stałą postaci . Stała znakowa jest liczbą całkowitą, której wartością jest kod znaku oznaczający znak. Sposób interpretowania stałych znaków za pomocą wielu znaków, takich jak 'abc' jest zdefiniowany w implementacji.

Zero lub więcej znaków otoczonych podwójnymi cudzysłowami, np. "abc" to dosłowny ciąg znaków . Literał łańcuchowy jest niemodyfikowalną tablicą, której elementy są typu char . Łańcuch w podwójnych cudzysłowach i kończący znak null są zawartością, więc "abc" ma 4 elementy ( {'a', 'b', 'c', '\0'} )

W tym przykładzie użyto stałej znakowej, w której należy użyć literału łańcuchowego. Ta stała znakowa zostanie przekonwertowana na wskaźnik w sposób zdefiniowany w implementacji i istnieje niewielka szansa, że przekonwertowany wskaźnik będzie ważny, więc ten przykład wywoła niezdefiniowane zachowanie .

#include <stdio.h>

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

W tym przykładzie użyto literału łańcuchowego, w którym należy zastosować stałą znakową. Wskaźnik przekształcony z literału łańcucha zostanie przekonwertowany na liczbę całkowitą w sposób zdefiniowany w implementacji i przekształcony w char w sposób zdefiniowany w implementacji. (Jak przekonwertować liczbę całkowitą na typ ze znakiem, który nie może reprezentować wartości do konwersji, jest zdefiniowany w implementacji, a to, czy char jest podpisany, jest również zdefiniowane w implementacji.) Wynik będzie pewną nieistotną rzeczą.

#include <stdio.h>

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

W prawie wszystkich przypadkach kompilator będzie narzekał na te pomyłki. Jeśli tak się nie stanie, musisz użyć więcej opcji ostrzeżeń kompilatora lub zalecane jest użycie lepszego kompilatora.

Ignorowanie zwracanych wartości funkcji bibliotecznych

Prawie każda funkcja w standardowej bibliotece C zwraca coś w przypadku sukcesu, a coś innego w przypadku błędu. Na przykład malloc zwróci wskaźnik do bloku pamięci przydzielonego przez funkcję po pomyślnym zakończeniu, a jeśli funkcja nie zdoła przydzielić żądanego bloku pamięci, wskaźnik zerowy. Dlatego zawsze należy sprawdzać wartość zwracaną w celu łatwiejszego debugowania.

To jest złe:

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

To jest dobre:

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

W ten sposób od razu znasz przyczynę błędu, w przeciwnym razie możesz spędzić godziny na szukaniu błędu w całkowicie niewłaściwym miejscu.

Znak nowej linii nie jest zużywany w typowym wywołaniu scanf ()

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

jest wykonywany przy użyciu tego wejścia

42
life

wyjście wyniesie 42 "" zamiast oczekiwanego 42 "life" .

Wynika to z faktu, że znak nowego wiersza po 42 nie jest zużywany w wywołaniu scanf() i jest zużywany przez fgets() przed odczytaniem life . Następnie fgets() przestaje czytać przed czytaniem life .

Aby uniknąć tego problemu, jednym ze sposobów, który jest przydatny, gdy znana jest maksymalna długość linii - na przykład przy rozwiązywaniu problemów w internetowym systemie oceniania syste - jest unikanie bezpośredniego użycia scanf() i odczytywania wszystkich linii za pomocą fgets() . Możesz użyć sscanf() do parsowania odczytanych linii.

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

Innym sposobem jest czytanie, dopóki nie naciśniesz znaku nowej linii po użyciu scanf() i przed użyciem 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;
}

Dodawanie średnika do # zdefiniowania

Łatwo jest pomylić się z preprocesorem C i traktować go jako część samego C, ale jest to błąd, ponieważ preprocesor jest tylko mechanizmem zastępowania tekstu. Na przykład, jeśli piszesz

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

kod rozwija się do

int arr[100;];

który jest błędem składni. Rozwiązanie polega na usunięciu średnika z linii #define . Niemal niezmiennie błędem jest kończenie #define średnikiem.

Komentarze wielowierszowe nie mogą być zagnieżdżone

W C komentarze wieloliniowe, / * i * /, nie zagnieżdżają się.

Jeśli adnotujesz blok kodu lub funkcję za pomocą tego stylu komentarza:

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

Nie będziesz mógł łatwo tego skomentować:

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

Jednym z rozwiązań jest użycie komentarzy w stylu 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;
}

Teraz cały blok można łatwo skomentować:

/*

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

*/

Innym rozwiązaniem jest uniknięcie wyłączenia kodu przy użyciu składni komentarzy, zamiast tego za pomocą dyrektyw preprocesora #ifdef lub #ifndef . Dyrektywy te zrobić gniazdo, dzięki czemu można swobodnie wypowiedzieć swój kod w stylu wolisz.

#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

Niektóre przewodniki sięgają tak daleko, że zalecają, aby sekcje kodu nigdy nie były komentowane, a jeśli kod ma zostać tymczasowo wyłączony, można skorzystać z dyrektywy #if 0 .

Zobacz #if 0, aby zablokować sekcje kodu .

Przekroczenie granic tablicy

Tablice są liczone od zera, czyli indeks zawsze zaczyna się od 0, a kończy na długości tablicy indeksu minus 1. W ten sposób poniższy kod nie wyświetli pierwszego elementu tablicy i wyrzuci śmieci dla końcowej wartości, którą drukuje.

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

Wyjście: 2 3 4 5 GarbageValue

Poniżej pokazano prawidłowy sposób osiągnięcia pożądanego wyniku:

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

Wyjście: 1 2 3 4 5

Ważne jest, aby znać długość tablicy przed rozpoczęciem pracy z nią, ponieważ w przeciwnym razie możesz uszkodzić bufor lub spowodować błąd segmentacji, uzyskując dostęp do miejsc pamięci, które są poza zakresem.

Funkcja rekurencyjna - brak warunku podstawowego

Obliczanie silni liczby jest klasycznym przykładem funkcji rekurencyjnej.

Brak warunku podstawowego:

#include <stdio.h>

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

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

Typowe wyjście: Segmentation fault: 11

Problem z tą funkcją polega na tym, że zapętla się w nieskończoność, powodując błąd segmentacji - potrzebuje warunku podstawowego, aby zatrzymać rekurencję.

Deklarowany stan podstawowy:

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

Próbka wyjściowa

Factorial 3 = 6

Ta funkcja zakończy się, gdy tylko osiągnie warunek n równy 1 (pod warunkiem, że wartość początkowa n jest wystarczająco mała - górna granica wynosi 12 gdy int jest liczbą 32-bitową).

Zasady, których należy przestrzegać:

  1. Zainicjuj algorytm. Programy rekurencyjne często wymagają wartości początkowej. Można to osiągnąć albo przez użycie parametru przekazanego do funkcji, albo przez zapewnienie funkcji bramy, która jest nierekurencyjna, ale która ustawia wartości początkowe dla obliczeń rekurencyjnych.
  2. Sprawdź, czy bieżące przetwarzane wartości są zgodne z przypadkiem podstawowym. Jeśli tak, przetworz i zwróć wartość.
  3. Przedefiniuj odpowiedź pod względem mniejszego lub prostszego podproblemu lub podproblemów.
  4. Uruchom algorytm dla podproblemu.
  5. Połącz wyniki w sformułowaniu odpowiedzi.
  6. Zwróć wyniki.

Źródło: funkcja rekurencyjna

Sprawdzanie logicznego wyrażenia pod kątem „prawda”

Pierwotny standard C nie miał wewnętrznego typu logicznego, więc bool , true i false nie miały nieodłącznego znaczenia i były często definiowane przez programistów. Zazwyczaj true ma wartość 1, a false 0.

C99

C99 dodaje wbudowany typ _Bool i nagłówek <stdbool.h> który definiuje bool (rozwijanie do _Bool ), false i true . Pozwala również przedefiniować wartość bool , true i false , ale zauważa, że jest to przestarzała funkcja.

Co ważniejsze, wyrażenia logiczne traktują wszystko, co ma wartość zero, jako fałsz, a wszelkie wartości niezerowe - prawdziwe. Na przykład:

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

W powyższym przykładzie funkcja próbuje sprawdzić, czy ustawiony jest górny bit, i zwraca true jeśli tak jest. Jednakże, poprzez jawne sprawdzenie wartości true , instrukcja if powiedzie się tylko wtedy, gdy (bitfield & 0x80) oceni to, co true jest zdefiniowane jako, co zwykle wynosi 1 a bardzo rzadko 0x80 . Sprawdź jawnie przypadek, którego oczekujesz:

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

Lub oceń każdą niezerową wartość jako prawdziwą.

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

Literały zmiennoprzecinkowe są domyślnie typu double

Należy zachować ostrożność podczas inicjowania zmiennych typu float do wartości literalnych lub porównywania ich z wartościami literalnymi, ponieważ zwykłe literały zmiennoprzecinkowe, takie jak 0.1 są typu double . Może to prowadzić do niespodzianek:

#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

Tutaj n jest inicjalizowane i zaokrąglane do pojedynczej precyzji, co daje wartość 0,10000000149011612. Następnie n jest konwertowane z powrotem na podwójną precyzję w celu porównania z 0.1 literałem (co równa się 0,10000000000000001), co powoduje niedopasowanie.

Oprócz błędów zaokrąglania, mieszanie zmiennych typu float z double literałami spowoduje niską wydajność na platformach, które nie mają sprzętowej obsługi podwójnej precyzji.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow