C Language
Pièges communs
Recherche…
Introduction
Cette section traite de certaines des erreurs courantes qu'un programmeur C devrait connaître et qu'il devrait éviter. Pour plus d'informations sur certains problèmes inattendus et leurs causes, reportez-vous à la section Comportement non défini.
Mélanger des entiers signés et non signés dans des opérations arithmétiques
Il est généralement pas une bonne idée de mélanger signed
et unsigned
entiers dans les opérations arithmétiques. Par exemple, qu'est-ce qui sortira de l'exemple suivant?
#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;
}
Comme 1000 est supérieur à -1, vous vous attendez à ce que la sortie soit a is more than b
, mais ce ne sera pas le cas.
Les opérations arithmétiques entre différents types intégraux sont effectuées dans un type commun défini par les conversions arithmétiques dites habituelles (voir la spécification du langage, 6.3.1.8).
Dans ce cas, le "type commun" est unsigned int
, car, comme indiqué dans les conversions arithmétiques habituelles ,
714 Sinon, si l'opérande qui a un type d'entier non signé a un rang supérieur ou égal au rang du type de l'autre opérande, l'opérande avec un type d'entier signé est converti dans le type de l'opérande avec un type d'entier non signé.
Cela signifie que int
opérande b
va se converti en unsigned int
avant la comparaison.
Lorsque -1 est converti en un unsigned int
le résultat est la valeur unsigned int
maximale unsigned int
possible, qui est supérieure à 1000, ce qui signifie que a > b
est faux.
Écrire par erreur = au lieu de == lors de la comparaison
L'opérateur =
est utilisé pour l'affectation.
L'opérateur ==
est utilisé pour la comparaison.
Il faut faire attention à ne pas mélanger les deux. Parfois, on écrit par erreur
/* assign y to x */
if (x = y) {
/* logic */
}
quand ce qui était vraiment recherché est:
/* compare if x is equal to y */
if (x == y) {
/* logic */
}
Le premier attribue la valeur de y à x et vérifie si cette valeur est non nulle, au lieu de faire une comparaison, ce qui équivaut à:
if ((x = y) != 0) {
/* logic */
}
Il y a des moments où tester le résultat d'une affectation est destiné et est couramment utilisé, car cela évite d'avoir à dupliquer le code et de devoir traiter la première fois spécialement. Comparer
while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) {
switch (c) {
...
}
}
contre
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);
}
Les compilateurs modernes reconnaîtront ce modèle et ne préviendront pas lorsque l’affectation est entre parenthèses comme ci-dessus, mais peuvent avertir pour d’autres utilisations. Par exemple:
if (x = y) /* warning */
if ((x = y)) /* no warning */
if ((x = y) != 0) /* no warning; explicit */
Certains programmeurs utilisent la stratégie consistant à mettre la constante à gauche de l'opérateur (communément appelée conditions Yoda ). Étant donné que les constantes sont des valeurs de valeur, ce style entraînera une erreur du compilateur si le mauvais opérateur a été utilisé.
if (5 = y) /* Error */
if (5 == y) /* No error */
Cependant, cela réduit considérablement la lisibilité du code et n'est pas considéré comme nécessaire si le programmeur suit les bonnes pratiques de codage C, et ne permet pas de comparer deux variables afin que ce ne soit pas une solution universelle. De plus, de nombreux compilateurs modernes peuvent donner des avertissements lorsque du code est écrit avec les conditions de Yoda.
Utilisation inconsidérée de points-virgules
Soyez prudent avec les points-virgules. Exemple suivant
if (x > a);
a = x;
signifie en réalité:
if (x > a) {}
a = x;
ce qui signifie que x
sera assigné à a
dans tous les cas, ce qui pourrait ne pas être ce que vous vouliez à l'origine.
Parfois, manquer un point-virgule causera également un problème imperceptible:
if (i < 0)
return
day = date[0];
hour = date[1];
minute = date[2];
Le point-virgule derrière return est manqué, donc day = date [0] sera renvoyé.
Une technique pour éviter ce problème et d'autres problèmes similaires consiste à toujours utiliser des accolades sur les conditionnels et les boucles multi-lignes. Par exemple:
if (x > a) {
a = x;
}
Oublier d'allouer un octet supplémentaire pour \ 0
Lorsque vous copiez une chaîne dans un tampon malloc
, n'oubliez jamais d'ajouter 1 à strlen
.
char *dest = malloc(strlen(src)); /* WRONG */
char *dest = malloc(strlen(src) + 1); /* RIGHT */
strcpy(dest, src);
Ceci est dû au fait que strlen
n'inclut pas le \0
final dans la longueur. Si vous prenez l'approche WRONG
(comme indiqué ci-dessus), lors de l'appel de strcpy
, votre programme invoquera un comportement indéfini.
Cela s'applique également aux situations où vous lisez une chaîne de longueur maximale connue à partir de stdin
ou d'une autre source. Par exemple
#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 */
Oublier de libérer de la mémoire (fuites de mémoire)
Une bonne pratique de programmation consiste à libérer toute mémoire allouée directement par votre propre code ou implicitement en appelant une fonction interne ou externe, telle qu'une API de bibliothèque telle que strdup()
. Si vous ne parvenez pas à libérer de la mémoire, vous risquez d'introduire une fuite de mémoire, qui pourrait s'accumuler dans une quantité considérable de mémoire qui n'est pas disponible pour votre programme (ou le système), entraînant éventuellement des pannes ou un comportement indéfini. Les problèmes sont plus susceptibles de se produire si la fuite est occasionnée de manière répétée dans une fonction en boucle ou récursive. Le risque d'échec du programme augmente au fur et à mesure qu'un programme qui fuit est long. Parfois, des problèmes apparaissent instantanément; d'autres fois, les problèmes ne seront pas visibles pendant des heures, voire des années de fonctionnement constant. Les défaillances de mémoire peuvent être catastrophiques, selon les circonstances.
La boucle infinie suivante est un exemple de fuite qui finira par épuiser la fuite de mémoire disponible en appelant getline()
, une fonction qui alloue implicitement une nouvelle mémoire, sans libérer cette mémoire.
#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;
}
En revanche, le code ci-dessous utilise également la fonction getline()
, mais cette fois, la mémoire allouée est correctement libérée, évitant une fuite.
#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;
}
Une fuite de mémoire n'a pas toujours des conséquences tangibles et n'est pas nécessairement un problème fonctionnel. Bien que les «meilleures pratiques» dictent la libération rigoureuse de la mémoire aux points et conditions stratégiques, afin de réduire l’empreinte mémoire et les risques d’épuisement de la mémoire, il peut y avoir des exceptions. Par exemple, si un programme est limité en durée et en portée, le risque d'échec de l'allocation peut être considéré comme trop petit pour vous inquiéter. Dans ce cas, contourner la désallocation explicite pourrait être considéré comme acceptable. Par exemple, la plupart des systèmes d'exploitation modernes libèrent automatiquement toute la mémoire consommée par un programme lorsqu'il se termine, qu'il soit dû à une défaillance de programme, à un appel système à exit()
, à la fin du processus ou à la fin de main()
. Libérer de manière explicite la mémoire au moment de la fin imminente du programme pourrait en fait être redondant ou introduire une pénalité de performance.
L'allocation peut échouer si la mémoire disponible est insuffisante et que les échecs de traitement doivent être pris en compte aux niveaux appropriés de la pile d'appels. getline()
, présenté ci-dessus, est un cas d'utilisation intéressant, car il s'agit d'une fonction de bibliothèque qui alloue non seulement la mémoire qu'il laisse à l'appelant pour la libérer, mais qui peut échouer pour diverses raisons. Par conséquent, il est essentiel, lors de l'utilisation d'une API C, de lire la documentation (page de manuel) et d'accorder une attention particulière aux conditions d'erreur et à l'utilisation de la mémoire et de savoir quelle couche logicielle supporte la libération de la mémoire renvoyée.
Une autre méthode courante de gestion de la mémoire consiste à définir systématiquement les pointeurs de mémoire sur NULL immédiatement après la libération de la mémoire référencée par ces pointeurs, afin que leur validité soit vérifiée à tout moment (par exemple, NULL / non-NULL). peut entraîner des problèmes graves tels que l'obtention de données inutiles (opération de lecture), la corruption de données (opération d'écriture) et / ou un plantage de programme. Dans la plupart des systèmes d'exploitation modernes, libérer l'emplacement mémoire 0 ( NULL
) est un NOP (par exemple, il est inoffensif), comme l'exige le standard C - en définissant un pointeur sur NULL, il n'y a aucun risque de double libération de mémoire est passé à free()
. Gardez à l'esprit que la double libération de mémoire peut entraîner des échecs très longs, compliqués et difficiles à diagnostiquer .
Copier trop
char buf[8]; /* tiny buffer, easy to overflow */
printf("What is your name?\n");
scanf("%s", buf); /* WRONG */
scanf("%7s", buf); /* RIGHT */
Si l'utilisateur entre une chaîne de plus de 7 caractères (- 1 pour la terminaison nulle), la mémoire derrière le tampon buf
sera écrasé. Cela se traduit par un comportement indéfini. Les pirates informatiques malveillants l’exploitent souvent pour écraser l’adresse de retour et la remplacer par l’adresse du code malveillant du pirate.
Oubliant de copier la valeur de retour de realloc dans un fichier temporaire
Si realloc
échoue, il renvoie NULL
. Si vous affectez la valeur du tampon d'origine à la valeur de retour de realloc
, et s'il renvoie NULL
, le tampon d'origine (l'ancien pointeur) est perdu, entraînant une fuite de mémoire . La solution consiste à copier dans un pointeur temporaire, et si cela est temporaire NULL, puis copiez dans le réel tampon.
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");
Comparer des nombres à virgule flottante
Les types à virgule flottante ( float
, double
et long double
) ne peuvent pas représenter avec précision certains nombres car ils ont une précision finie et représentent les valeurs dans un format binaire. Tout comme nous avons des nombres décimaux répétés dans la base 10 pour des fractions telles que 1/3, il y a des fractions qui ne peuvent pas être représentées finement dans le binaire (comme 1/3, mais aussi et surtout 1/10). Ne pas comparer directement les valeurs à virgule flottante; utilisez plutôt un delta.
#include <float.h> // for DBL_EPSILON and FLT_EPSILON
#include <math.h> // for fabs()
int main(void)
{
double a = 0.1; // imprecise: (binary) 0.000110...
// may be false or true
if (a + a + a + a + a + a + a + a + a + a == 1.0) {
printf("10 * 0.1 is indeed 1.0. This is not guaranteed in the general case.\n");
}
// Using a small delta value.
if (fabs(a + a + a + a + a + a + a + a + a + a - 1.0) < 0.000001) {
// C99 5.2.4.2.2p8 guarantees at least 10 decimal digits
// of precision for the double type.
printf("10 * 0.1 is almost 1.0.\n");
}
return 0;
}
Un autre exemple:
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;
}
Sortie:
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)
Faire une mise à l'échelle supplémentaire dans l'arithmétique du pointeur
Dans l'arithmétique du pointeur, l'entier à ajouter ou à soustraire au pointeur est interprété non pas comme un changement d' adresse mais comme un nombre d' éléments à déplacer.
#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;
}
Ce code effectue une mise à l'échelle supplémentaire dans le pointeur de calcul affecté à ptr2
. Si sizeof(int)
est 4, ce qui est typique dans les environnements 32 bits modernes, l'expression signifie "8 éléments après le array[0]
", qui est hors plage, et appelle un comportement indéfini .
Pour avoir ptr2
point à 2 éléments après le array[0]
, vous devez simplement ajouter 2.
#include <stdio.h>
int main(void) {
int array[] = {1, 2, 3, 4, 5};
int *ptr = &array[0];
int *ptr2 = ptr + 2;
printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
return 0;
}
L'arithmétique explicite des pointeurs utilisant des opérateurs additifs peut être source de confusion, donc l'utilisation de l'indice de tableau peut être meilleure.
#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]
est identique à (*((E1)+(E2)))
( N1570 6.5.2.1, paragraphe 2), et &(E1[E2])
est équivalent à ((E1)+(E2))
( N1570 6.5.3.2, note de bas de page 102).
Si l'arithmétique du pointeur est préférée, il est possible de convertir le pointeur pour adresser un type de données différent, ce qui peut permettre l'adressage par octet. Attention cependant: l' endianisme peut devenir un problème, et lancer des types autres que «pointeur sur caractère» conduit à des problèmes d'alias stricts .
#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;
}
Les macros sont des remplacements de chaînes simples
Les macros sont des remplacements de chaîne simples. (Strictement parlant, ils fonctionnent avec des jetons de prétraitement, pas des chaînes arbitraires).
#include <stdio.h>
#define SQUARE(x) x*x
int main(void) {
printf("%d\n", SQUARE(1+2));
return 0;
}
Vous pouvez vous attendre à ce que ce code imprime 9
( 3*3
), mais en réalité 5
sera imprimé car la macro sera étendue à 1+2*1+2
.
Vous devez envelopper les arguments et l'expression de macro entière entre parenthèses pour éviter ce problème.
#include <stdio.h>
#define SQUARE(x) ((x)*(x))
int main(void) {
printf("%d\n", SQUARE(1+2));
return 0;
}
Un autre problème est que les arguments d'une macro ne sont pas garantis pour être évalués une fois; ils peuvent ne pas être évalués du tout ou peuvent être évalués plusieurs fois.
#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;
}
Dans ce code, la macro sera étendue à ((a++) <= (10) ? (a++) : (10))
. Etant donné a++
( 0
) est inférieur à 10
, a++
sera évalué deux fois et la valeur de a
et de ce qui est renvoyé par MIN
différente de celle à laquelle vous pouvez vous attendre.
Cela peut être évité en utilisant des fonctions, mais notez que les types seront corrigés par la définition de la fonction, alors que les macros peuvent être (trop) flexibles avec les types.
#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;
}
Maintenant, le problème de la double évaluation est résolu, mais cette fonction min
ne peut pas traiter double
données double
sans les tronquer, par exemple.
Les directives de macro peuvent être de deux types:
#define OBJECT_LIKE_MACRO followed by a "replacement list" of preprocessor tokens
#define FUNCTION_LIKE_MACRO(with, arguments) followed by a replacement list
Ce qui distingue ces deux types de macros est le caractère qui suit l'identifiant après #define
: si c'est un lparen , c'est une macro de type fonction; sinon, c'est une macro de type objet. Si l’intention est d’écrire une macro de type fonction, il ne doit pas y avoir d’espace blanc entre la fin du nom de la macro et (
. Cochez cette option pour une explication détaillée.
En C99 ou plus tard, vous pouvez utiliser static inline int min(int x, int y) { … }
.
En C11, vous pouvez écrire une expression 'type-generic' pour min
.
#include <stdio.h>
#define min(x, y) _Generic((x), \
long double: min_ld, \
unsigned long long: min_ull, \
default: min_i \
)(x, y)
#define gen_min(suffix, type) \
static inline type min_##suffix(type x, type y) { return (x < y) ? x : y; }
gen_min(ld, long double)
gen_min(ull, unsigned long long)
gen_min(i, int)
int main(void)
{
unsigned long long ull1 = 50ULL;
unsigned long long ull2 = 37ULL;
printf("min(%llu, %llu) = %llu\n", ull1, ull2, min(ull1, ull2));
long double ld1 = 3.141592653L;
long double ld2 = 3.141592652L;
printf("min(%.10Lf, %.10Lf) = %.10Lf\n", ld1, ld2, min(ld1, ld2));
int i1 = 3141653;
int i2 = 3141652;
printf("min(%d, %d) = %d\n", i1, i2, min(i1, i2));
return 0;
}
L 'expression générique pourrait être étendue avec davantage de types tels que les invocations de macro gen_min
écrites en double
, float
, long long
, unsigned long
, long
, unsigned
- et appropriées.
Erreurs de référence non définies lors de la liaison
L'une des erreurs de compilation les plus courantes se produit pendant la phase de liaison. L'erreur ressemble à ceci:
$ 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
$
Alors regardons le code qui a généré cette erreur:
int foo(void);
int main(int argc, char **argv)
{
int foo_val;
foo_val = foo();
return foo_val;
}
On voit ici une déclaration de foo ( int foo();
) mais pas de définition (fonction réelle). Nous avons donc fourni le compilateur avec l'en-tête de fonction, mais aucune fonction de ce type n'a été définie, de sorte que l'étape de compilation passe mais que l'éditeur de liens quitte avec une erreur de Undefined reference
non définie.
Pour corriger cette erreur dans notre petit programme, il suffirait d'ajouter une définition pour 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;
}
Maintenant, ce code va compiler. Une autre situation se présente où la source de foo()
trouve dans un fichier source séparé, foo.c
(et il existe un en-tête foo.h
pour déclarer foo()
inclus dans foo.c
et undefined_reference.c
). Ensuite, le correctif consiste à lier à la fois le fichier objet de foo.c
et undefined_reference.c
, ou de compiler les deux fichiers source:
$ gcc -c undefined_reference.c
$ gcc -c foo.c
$ gcc -o working_program undefined_reference.o foo.o
$
Ou:
$ gcc -o working_program undefined_reference.c foo.c
$
Un cas plus complexe est celui où les bibliothèques sont impliquées, comme dans le 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;
}
Le code est syntaxiquement correct, la déclaration de pow()
existe depuis #include <math.h>
, nous essayons donc de compiler et de lier, mais nous obtenons une erreur comme celle-ci:
$ 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
$
Cela se produit car la définition de pow()
n'a pas été trouvée lors de la phase de liaison. Pour corriger cela, nous devons spécifier que nous voulons -lm
lien avec la bibliothèque mathématique appelée libm
en spécifiant l’ -lm
. (Notez qu'il existe des plates-formes telles que macOS où -lm
n'est pas nécessaire, mais lorsque vous obtenez la référence non définie, la bibliothèque est nécessaire.)
Donc, nous exécutons à nouveau l’étape de compilation, en spécifiant cette fois la bibliothèque (après les fichiers source ou objet):
$ 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
$
Et il fonctionne!
Incompréhension du tableau
Un problème courant dans le code qui utilise des tableaux multidimensionnels, des tableaux de pointeurs, etc. est le fait que Type**
et Type[M][N]
sont fondamentalement différents:
#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;
}
Exemple de sortie du compilateur:
file1.c: In function 'main':
file1.c:13:23: error: passing argument 1 of 'print_strings' from incompatible pointer type [-Wincompatible-pointer-types]
print_strings(strings, 4);
^
file1.c:3:10: note: expected 'char **' but argument is of type 'char (*)[20]'
void print_strings(char **strings, size_t n)
L'erreur indique que le tableau s
de la fonction main
est transmis à la fonction print_strings
, qui attend un type de pointeur différent de celui reçu. Il comprend également une note exprimant le type attendu par print_strings
et le type qui lui a été transmis depuis le main
.
Le problème est dû à quelque chose appelé désintégration de tableau . Que se passe-t-il lorsque s
avec son type char[4][20]
(tableau de 4 tableaux de 20 caractères) est passé à la fonction est-il transformé en un pointeur vers son premier élément comme si vous aviez écrit &s[0]
, qui a le type char (*)[20]
(pointeur sur 1 tableau de 20 caractères). Cela se produit pour tout tableau, y compris un tableau de pointeurs, un tableau de tableaux de tableaux (tableaux 3D) et un tableau de pointeurs vers un tableau. Vous trouverez ci-dessous un tableau illustrant ce qui se produit lorsqu'un tableau se désintègre. Les modifications apportées à la description du type sont mises en évidence pour illustrer ce qui se passe:
Avant la pourriture | Après la décomposition | ||
---|---|---|---|
char [20] | tableau de (20 caractères) | char * | pointeur vers (1 caractère) |
char [4][20] | tableau de (4 tableaux de 20 caractères) | char (*)[20] | pointeur vers (1 tableau de 20 caractères) |
char *[4] | tableau de (4 pointeurs à 1 caractère) | char ** | pointeur vers (1 pointeur vers 1 caractère) |
char [3][4][20] | tableau de (3 tableaux de 4 tableaux de 20 caractères) | char (*)[4][20] | pointeur vers (1 tableau de 4 tableaux de 20 caractères) |
char (*[4])[20] | tableau de (4 pointeurs à 1 tableau de 20 caractères) | char (**)[20] | pointeur sur (1 pointeur sur 1 tableau de 20 caractères) |
Si un tableau peut se transformer en un pointeur, on peut dire qu'un pointeur peut être considéré comme un tableau contenant au moins un élément. Une exception à ceci est un pointeur nul, qui ne pointe vers rien et n'est par conséquent pas un tableau.
La désintégration des tableaux ne se produit qu'une fois. Si un tableau est devenu un pointeur, il s’agit maintenant d’un pointeur et non d’un tableau. Même si vous avez un pointeur vers un tableau, rappelez-vous que le pointeur peut être considéré comme un tableau contenant au moins un élément, de sorte que le déclin du tableau s’est déjà produit.
En d'autres termes, un pointeur sur un tableau ( char (*)[20]
) ne deviendra jamais un pointeur sur un pointeur (caractère char **
). Pour corriger la fonction print_strings
, faites simplement en sorte qu’elle reçoive le bon type:
void print_strings(char (*strings)[20], size_t n)
/* OR */
void print_strings(char strings[][20], size_t n)
Un problème se pose lorsque vous voulez que la fonction print_strings
soit générique pour tout tableau de caractères: et s'il y avait 30 caractères au lieu de 20? Ou 50? La réponse est d'ajouter un autre paramètre avant le paramètre array:
#include <stdio.h>
/*
* Note the rearranged parameters and the change in the parameter name
* from the previous definitions:
* n (number of strings)
* => scount (string count)
*
* Of course, you could also use one of the following highly recommended forms
* for the `strings` parameter instead:
*
* char strings[scount][ccount]
* char strings[][ccount]
*/
void print_strings(size_t scount, size_t ccount, char (*strings)[ccount])
{
size_t i;
for (i = 0; i < scount; i++)
puts(strings[i]);
}
int main(void)
{
char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
print_strings(4, 20, s);
return 0;
}
La compilation ne génère aucune erreur et produit le résultat attendu:
Example 1
Example 2
Example 3
Example 4
Passer des tableaux non adjacents à des fonctions qui attendent des tableaux multidimensionnels "réels"
Lors de l'allocation de tableaux multidimensionnels avec malloc
, calloc
et realloc
, il est courant d'allouer les tableaux internes avec plusieurs appels (même si l'appel n'apparaît qu'une seule fois, il peut être en boucle):
/* Could also be `int **` with malloc used to allocate outer array. */
int *array[4];
int i;
/* Allocate 4 arrays of 16 ints. */
for (i = 0; i < 4; i++)
array[i] = malloc(16 * sizeof(*array[i]));
La différence en octets entre le dernier élément de l'un des tableaux internes et le premier élément du tableau interne suivant peut ne pas être égale à 0 comme avec un tableau multidimensionnel "réel" (par exemple, int array[4][16];
) :
/* 0x40003c, 0x402000 */
printf("%p, %p\n", (void *)(array[0] + 15), (void *)array[1]);
Compte tenu de la taille de int
, vous obtenez une différence de 8128 octets (8132-4), ce qui correspond à 2032 éléments de tableau int
dimensionnés.
Si vous devez utiliser un tableau alloué dynamiquement avec une fonction qui attend un tableau multidimensionnel "réel", vous devez allouer un objet de type int *
et utiliser l'arithmétique pour effectuer des calculs:
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);
Si N
est une macro ou un entier au lieu d'une variable, le code peut simplement utiliser la notation plus naturelle du tableau 2D après avoir alloué un pointeur à un tableau:
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);
Si N
n'est pas une macro ou un entier, le array
pointera vers un tableau de longueur variable (VLA). Cela peut encore être utilisé avec func
en coulant dans int *
et une nouvelle fonction func_vla
remplacerait 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);
Remarque : les VLA sont facultatifs à partir de C11. Si votre implémentation prend en charge C11 et définit la macro __STDC_NO_VLA__
à 1, vous êtes bloqué avec les méthodes pré-C99.
Utiliser des constantes de caractères au lieu de littéraux de chaîne et vice versa
En C, les constantes de caractères et les littéraux de chaîne sont des choses différentes.
Un caractère entouré de guillemets simples comme 'a'
est une constante de caractère . Une constante de caractère est un entier dont la valeur est le code de caractère correspondant au caractère. Comment interpréter les constantes de caractères avec plusieurs caractères comme 'abc'
est défini par l'implémentation.
Zéro ou plusieurs caractères entourés de guillemets comme "abc"
est un littéral de chaîne . Un littéral de chaîne est un tableau non modifiable dont les éléments sont de type char
. La chaîne dans les guillemets plus le caractère nul de terminaison sont le contenu, donc "abc"
a 4 éléments ( {'a', 'b', 'c', '\0'}
)
Dans cet exemple, une constante de caractère est utilisée lorsqu'un littéral de chaîne doit être utilisé. Cette constante de caractère sera convertie en un pointeur de manière définie par l'implémentation et le pointeur converti a peu de chance d'être valide. Cet exemple invoquera donc un comportement indéfini .
#include <stdio.h>
int main(void) {
const char *hello = 'hello, world'; /* bad */
puts(hello);
return 0;
}
Dans cet exemple, un littéral de chaîne est utilisé où une constante de caractère doit être utilisée. Le pointeur converti à partir du littéral de chaîne sera converti en un entier d'une manière définie par l'implémentation, et il sera converti en char
d'une manière définie par l'implémentation. (Comment convertir un entier en un type signé qui ne peut pas représenter la valeur à convertir est défini par l'implémentation, et si char
est signé est également défini par l'implémentation.) La sortie sera sans objet.
#include <stdio.h>
int main(void) {
char c = "a"; /* bad */
printf("%c\n", c);
return 0;
}
Dans presque tous les cas, le compilateur se plaindra de ces mélanges. Si ce n'est pas le cas, vous devez utiliser davantage d'options d'avertissement du compilateur, ou il est recommandé d'utiliser un meilleur compilateur.
Ignorer les valeurs de retour des fonctions de bibliothèque
Presque toutes les fonctions de la bibliothèque standard C renvoient quelque chose en cas de succès, et une autre erreur. Par exemple, malloc
renverra un pointeur sur le bloc de mémoire alloué par la fonction en cas de succès et, si la fonction ne parvient pas à allouer le bloc de mémoire demandé, un pointeur nul. Donc, vous devriez toujours vérifier la valeur de retour pour un débogage plus facile.
C'est mauvais:
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 */
C'est bon:
#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;
}
De cette façon, vous savez immédiatement la cause de l'erreur, sinon vous risquez de passer des heures à chercher un bug dans un endroit complètement erroné.
Le caractère de nouvelle ligne n'est pas utilisé lors d'un appel scanf () classique
Quand ce programme
#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;
}
est exécuté avec cette entrée
42
life
la sortie sera 42 ""
au lieu de 42 "life"
attendue.
C'est parce qu'un caractère de nouvelle ligne après 42
n'est pas consommé dans l'appel de scanf()
et qu'il est consommé par fgets()
avant de lire la life
. Ensuite, fgets()
arrête de lire avant de lire la life
.
Pour éviter ce problème, une méthode utile lorsque la longueur maximale d'une ligne est connue - lors de la résolution de problèmes dans le système de juge en ligne, par exemple - évite d'utiliser directement scanf()
et de lire toutes les lignes via fgets()
. Vous pouvez utiliser sscanf()
pour analyser les lignes lues.
#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;
}
Une autre méthode consiste à lire jusqu'à ce que vous frappiez un caractère de nouvelle ligne après avoir utilisé scanf()
et avant d'utiliser 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;
}
Ajouter un point-virgule à un #define
Il est facile de se perdre dans le préprocesseur C et de le traiter comme partie intégrante de C, mais c'est une erreur car le préprocesseur n'est qu'un mécanisme de substitution de texte. Par exemple, si vous écrivez
/* WRONG */
#define MAX 100;
int arr[MAX];
le code se développe à
int arr[100;];
qui est une erreur de syntaxe. La solution consiste à supprimer le point-virgule de la ligne #define
. Il est presque toujours une erreur de mettre fin à un #define
avec un point-virgule.
Les commentaires sur plusieurs lignes ne peuvent pas être imbriqués
En C, les commentaires sur plusieurs lignes, / * et * /, ne pas imbriquer.
Si vous annotez un bloc de code ou une fonction en utilisant ce style de commentaire:
/*
* 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;
}
Vous ne pourrez pas le commenter facilement:
//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...
*/
Une solution consiste à utiliser les commentaires de style 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;
}
Maintenant, le bloc entier peut être facilement commenté:
/*
// 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;
}
*/
Une autre solution consiste à éviter de désactiver le code en utilisant la syntaxe de commentaire, en utilisant #ifdef
directives de préprocesseur #ifdef
ou #ifndef
. Ces directives sont imbriquées, vous laissant libre de commenter votre code dans le style que vous préférez.
#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
Certains guides vont jusqu'à recommander que les sections de code ne soient jamais commentées et que, si le code doit être désactivé temporairement, on puisse avoir recours à une directive #if 0
.
Voir #if 0 pour bloquer les sections de code .
Dépasser les limites du réseau
Les tableaux sont basés sur zéro, c'est-à-dire que l'index commence toujours à 0 et se termine par la longueur du tableau d'index moins 1. Ainsi, le code suivant ne générera pas le premier élément du tableau et affichera la valeur finale qu'il imprime.
#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;
}
Sortie: 2 3 4 5 GarbageValue
Ce qui suit illustre la manière correcte d'obtenir la sortie souhaitée:
#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;
}
Sortie: 1 2 3 4 5
Il est important de connaître la longueur d'un tableau avant de l'utiliser, sinon vous risquez de corrompre le tampon ou de provoquer une erreur de segmentation en accédant à des emplacements de mémoire hors limites.
Fonction récursive - manquant la condition de base
Calculer la factorielle d'un nombre est un exemple classique de fonction récursive.
Manquant la condition de base:
#include <stdio.h>
int factorial(int n)
{
return n * factorial(n - 1);
}
int main()
{
printf("Factorial %d = %d\n", 3, factorial(3));
return 0;
}
Sortie typique: Segmentation fault: 11
Le problème avec cette fonction est qu’elle ferait une boucle infinie, provoquant une erreur de segmentation - elle nécessite une condition de base pour arrêter la récursivité.
Condition de base déclarée:
#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;
}
Sortie de l'échantillon
Factorial 3 = 6
Cette fonction se terminera dès qu'elle atteindra la condition n
est égale à 1 (à condition que la valeur initiale de n
soit suffisamment petite - la limite supérieure est 12
lorsque int
est une quantité de 32 bits).
Règles à suivre:
- Initialiser l'algorithme Les programmes récursifs nécessitent souvent une valeur de départ pour commencer. Ceci est accompli soit en utilisant un paramètre passé à la fonction, soit en fournissant une fonction de passerelle non récursive mais qui définit les valeurs de départ pour le calcul récursif.
- Vérifiez si la ou les valeurs en cours de traitement correspondent au cas de base. Si oui, traitez et renvoyez la valeur.
- Redéfinissez la réponse en termes de sous-problèmes ou de sous-problèmes plus petits ou plus simples.
- Exécutez l'algorithme sur le sous-problème.
- Combinez les résultats dans la formulation de la réponse.
- Renvoyer les résultats.
Source: Fonction récursive
Vérification de l'expression logique contre 'true'
Le standard C original n'avait pas de type booléen intrinsèque, donc bool
, true
et false
n'avaient aucune signification inhérente et étaient souvent définis par les programmeurs. Typiquement, true
serait défini comme 1 et false
serait défini sur 0.
C99 ajoute le type _Bool
et l'en-tête <stdbool.h>
qui définit bool
(en expansion à _Bool
), false
et true
. Cela vous permet également de redéfinir bool
, true
et false
, mais note qu’il s’agit d’une fonctionnalité obsolète.
Plus important encore, les expressions logiques traitent tout ce qui est considéré comme nul comme faux et toute évaluation non nulle comme vraie. Par exemple:
/* 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;
}
}
Dans l'exemple ci-dessus, la fonction tente de vérifier si le bit supérieur est défini et renvoie true
si c'est le cas. Cependant, en vérifiant explicitement la valeur true
, l'instruction if
réussira uniquement si (bitfield & 0x80)
évalué à true
, ce qui est généralement 1
et très rarement 0x80
. Soit explicitement vérifier avec le cas que vous attendez:
/* 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;
}
}
Ou évaluez toute valeur non nulle comme vraie.
/* 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;
}
}
Les littéraux à virgule flottante sont de type double par défaut
Des précautions doivent être prises lors de l'initialisation des variables de type float
à des valeurs littérales ou en les comparant avec des valeurs littérales, car les littéraux à virgule flottante tels que 0.1
sont de type double
. Cela peut entraîner des surprises:
#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
Ici, n
est initialisé et arrondi à une précision unique, avec pour résultat la valeur 0.10000000149011612. Ensuite, n
est reconverti en double précision pour être comparé à 0.1
littéral (ce qui équivaut à 0,10000000000000001), ce qui entraîne une non-concordance.
Outre les erreurs d'arrondi, le mélange de variables float
avec double
littéraux double
entraînera de mauvaises performances sur les plates-formes qui ne prennent pas en charge le matériel pour une double précision.