Quel est l'intérêt de spécifier la taille d'un tableau dans une déclaration de paramètre?
#include <stdio.h>
int a[] = {1,2};
void test(int in[3]){
//
}
int main() {
test(a);
return 0;
}
Dans le code ci-dessus int in[3]
est le même que int *in
. Le nombre 3
ne fait vraiment rien et ce n'est même pas la taille correcte, mais même ainsi le compilateur ne se plaint pas. Alors, y a-t-il une raison pour laquelle cette syntaxe est acceptée en C ou il me manque une fonctionnalité?
Réponses
Lorsqu'une déclaration de paramètre de tableau contient une taille constante, le seul but qu'elle peut servir est de fournir une documentation aux lecteurs, en leur indiquant la taille du tableau attendue par la fonction. Pour une expression constante n
, le compilateur convertit une déclaration de tableau telle que int in[n]
to int *in
, après quoi il n'y a aucune différence pour le compilateur et ainsi rien n'est affecté par la valeur de n
.
À l'origine en C, les paramètres de fonction étaient spécifiés par une liste de déclarations après la déclaration de fonction initiale, comme:
int f(a, b, c)
int a;
float b;
int c[3];
{
… function body
}
Je suppose que les tailles de tableau étaient autorisées dans ces déclarations simplement parce qu'elles utilisaient la même grammaire que les autres déclarations. Il aurait été plus difficile d'écrire du code de compilateur et de la documentation qui excluaient les tailles que de simplement les autoriser à se produire mais de les ignorer. Lors de l'introduction de la déclaration des types de paramètres dans les prototypes de fonctions ( int f(int a, float b, int c[3])
), je suppose que le même raisonnement est appliqué.
Toutefois:
- Si la déclaration contient
static
, comme dansint in[static n]
, alors, lorsque la fonction est appelée, l'argument correspondant doit pointer vers au moins desn
éléments, selon C 2018 6.7.6.3 7. Les compilateurs peuvent l'utiliser pour l'optimisation. - Si la taille du tableau n'est pas une constante, elle peut être évaluée par le compilateur lorsque la fonction est appelée. Par exemple, si la déclaration de fonction est
void test(int in[printf("Hi")])
, alors GCC 10.2 et Apple Clang 11.0 affichent «Hi» lorsque la fonction est appelée. (Cependant, il n'est pas clair pour moi que la norme C nécessite cette évaluation.) - Cet ajustement se produit uniquement pour le paramètre de tableau réel, pas pour les tableaux qu'il contient. Par exemple, dans la déclaration de paramètre
int x[3][4]
, le type dex
est ajusté pour êtreint (*)[4]
. Le 4 reste une partie de la taille et a des effets sur l'arithmétique du pointeur avecx
. - Lorsqu'un paramètre est déclaré en tant que tableau, le type d'élément doit être complet. En revanche, un paramètre déclaré comme pointeur n'a pas besoin de pointer vers un type complet. Par exemple,
struct foo x[3]
renvoie un message de diagnostic s'ilstruct foo
n'a pas été entièrement défini, maisstruct foo *x
pas.
Si nous spécifions la taille du tableau dans la définition de la fonction, elle peut être utilisée pour vérifier les erreurs à l'aide de l'outil d'analyse statique. J'ai utilisé l' cppcheck
outil pour le code suivant.
#include <stdio.h>
void test(int in[3])
{
in[3] = 4;
}
La sortie est:
Cppcheck 2.2
[test.cpp:4]: (error) Array 'in[3]' accessed at index 3, which is out of bounds.
Done!
Mais, si vous ne donnez aucune taille, vous n'obtiendrez aucune erreur cppcheck
.
#include <stdio.h>
void test(int in[])
{
in[3] = 4;
}
La sortie est:
Cppcheck 2.2
Done!
Mais, en général, il n'est pas nécessaire de spécifier la taille du tableau, dans la définition de la fonction. Nous ne pouvons pas trouver la taille du tableau dans une autre fonction, à l'aide de l' sizeof
opérateur, car seule la valeur du pointeur est copiée. Par conséquent, l'entrée d' sizeof
opérateur sera de type int*
et non de type int[]
(à l'intérieur de la fonction test()
). Ainsi, la valeur de la taille du tableau n'affecte pas le code. Voir le code ci-dessous:
#include <stdio.h>
int a[] = {1, 2, 3, 4, 5, 6, 7, 8};
void test(int in[8]) // Same as void test(int *arr)
{
unsigned int n = sizeof(in) / sizeof(in[0]); // sizeof(int*)/sizeof(int)
printf("Array size inside test() is %d\n", n);
}
int main()
{
unsigned int n = sizeof(a) / sizeof(a[0]); //sizeof(int[])/sizeof(int)
printf("Array size inside main() is %d\n", n);
test(a);
return 0;
}
La sortie est:
Array size inside main() is 8
Array size inside test() is 2
Donc, nous devons passer la taille d'un tableau avec une autre variable.
En C, il n'y a aucune différence entre un pointeur vers une structure et un pointeur vers un tableau de la même structure de données. Pour obtenir l'adresse de début du suivant, vous incrémentez simplement le pointeur avec la taille des données et comme il est impossible de déterminer la taille uniquement à partir du pointeur lui-même, vous devez le fournir en tant que programmeur.
Essayons de modifier le programme
#include <stdio.h>
void test(int in[3]){
printf("%d %d,%d,%d\n",in[0],in[1],in[2],in[3]); // !Sic bug intentional
}
int main() {
int a[] = {1,2};
int b[] = {3,4};
test(a);
test(b);
return 0;
}
Et lancez-le:
$ gcc pointer_size.c -o a.out && ./a.out
1 2,3,4
3 4,-1420617472,-1719256057
Dans ce cas, les tableaux sont placés dos à dos, donc la lecture aux index 2 et 3 à partir de a donnera les données de b et lorsque nous lirons trop de b, tout ce qui est présent sur ces adresses sera lu.
Il s'agit d'une source très courante de vulnérabilités de sécurité, même à ce jour.
En ce qui concerne le langage C et le compilateur, peu importe si vous spécifiez la taille puisque le tableau est ajusté de toute façon à un pointeur vers le premier élément.
Cependant, indiquer la taille peut améliorer la capacité d'analyse statique par des outils externes autres que le compilateur. Par exemple, un analyseur statique peut facilement dire qu'il s'agit d'un bogue de tableau hors limites:
void test(int in[3]){
in[3] = 0;
}
Mais il n'a aucune idée s'il s'agit d'un bug:
void test(int* in){
in[3] = 0;
}
En relation avec cela, la sécurité de type inexistante entre différentes tailles de tableaux peut en fait être résolue en utilisant une astuce - passer des tableaux par pointeur à la place. Parce qu'un pointeur vers un tableau ne se décompose pas et qu'il est difficile de se faire remettre la bonne taille. Exemple:
void test(int (*in)[3]){
int* ptr = *in;
ptr[3] = 0;
}
int foo[10];
test(&foo); // compiler error
int bar[3];
test(&bar); // ok
Cependant, cette astuce rend le code un peu plus difficile à lire et à comprendre.