C Language
Veel voorkomende valkuilen
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.
In C99 of hoger kunt u static inline int min(int x, int y) { … }
.
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);
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);
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:
- 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.
- Controleer of de huidige waarde (n) die worden verwerkt, overeenkomen met het basisscenario. Zo ja, verwerk en retourneer de waarde.
- Definieer het antwoord opnieuw in termen van een kleiner of eenvoudiger subprobleem of subproblemen.
- Voer het algoritme uit voor het subprobleem.
- Combineer de resultaten in de formulering van het antwoord.
- 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 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.