Pourquoi les compilateurs C et C ++ autorisent-ils des longueurs de tableau dans les signatures de fonction quand elles ne sont jamais appliquées?

131

Voici ce que j'ai trouvé pendant ma période d'apprentissage:

#include<iostream>
using namespace std;
int dis(char a[1])
{
    int length = strlen(a);
    char c = a[2];
    return length;
}
int main()
{
    char b[4] = "abc";
    int c = dis(b);
    cout << c;
    return 0;
}  

Donc, dans la variable int dis(char a[1]), le [1]semble ne rien faire et ne fonctionne pas du
tout, car je peux utiliser a[2]. Tout comme int a[]ou char *a. Je sais que le nom du tableau est un pointeur et comment transmettre un tableau, donc mon puzzle ne concerne pas cette partie.

Ce que je veux savoir, c'est pourquoi les compilateurs autorisent ce comportement ( int a[1]). Ou a-t-il d'autres significations que je ne connais pas?

Fanl
la source
6
C'est parce que vous ne pouvez pas réellement passer de tableaux aux fonctions.
Ed S.
37
Je pense que la question ici était de savoir pourquoi C vous permet de déclarer un paramètre comme étant de type tableau alors qu'il va juste se comporter exactement comme un pointeur de toute façon.
Brian
8
@Brian: Je ne sais pas s'il s'agit d'un argument pour ou contre le comportement, mais cela s'applique également si le type d'argument est un typedeftype avec tableau. Ainsi, la "désintégration en pointeur" dans les types d'argument n'est pas simplement le remplacement du sucre syntaxique []par *, elle passe vraiment par le système de types. Cela a des conséquences dans le monde réel pour certains types standard comme va_listcelui qui peut être défini avec un type tableau ou non-tableau.
R .. GitHub STOP AIDER ICE
4
@songyuanyao Vous pouvez accomplir quelque chose pas tout à fait différente en C (et C ++) en utilisant un pointeur: int dis(char (*a)[1]). Ensuite, vous passez un pointeur vers un tableau: dis(&b). Si vous êtes prêt à utiliser des fonctionnalités C qui n'existent pas en C ++, vous pouvez également dire des choses comme void foo(int data[static 256])et int bar(double matrix[*][*]), mais c'est une toute autre boîte de vers.
Stuart Olsen
1
@StuartOlsen Le point n'est pas quelle norme définit quoi. Le point est pourquoi celui qui l'a défini l'a défini de cette façon.
user253751

Réponses:

156

C'est une bizarrerie de la syntaxe pour passer des tableaux aux fonctions.

En fait, il n'est pas possible de passer un tableau en C. Si vous écrivez une syntaxe qui semble devoir passer le tableau, ce qui se passe réellement est qu'un pointeur vers le premier élément du tableau est passé à la place.

Étant donné que le pointeur n'inclut aucune information de longueur, le contenu de votre []dans la liste de paramètres formels de la fonction est en fait ignoré.

La décision d'autoriser cette syntaxe a été prise dans les années 1970 et a causé beaucoup de confusion depuis ...

MM
la source
21
En tant que programmeur non-C, je trouve cette réponse très accessible. +1
asteri
21
+1 pour "La décision d'autoriser cette syntaxe a été prise dans les années 1970 et a causé beaucoup de confusion depuis ..."
NoSenseEtAl
8
c'est vrai mais il est également possible de passer un tableau de cette taille uniquement en utilisant la void foo(int (*somearray)[20])syntaxe. dans ce cas, 20 est appliqué sur les sites appelants.
v.oddou
14
-1 En tant que programmeur C, je trouve cette réponse incorrecte. []ne sont pas ignorés dans les tableaux multidimensionnels comme indiqué dans la réponse de pat. Il était donc nécessaire d'inclure la syntaxe des tableaux. De plus, rien n'empêche le compilateur d'émettre des avertissements même sur des tableaux unidimensionnels.
user694733
7
Par "le contenu de votre []", je parle spécifiquement du code dans la Question. Cette particularité de la syntaxe n'était pas du tout nécessaire, la même chose peut être obtenue en utilisant la syntaxe de pointeur, c'est-à-dire que si un pointeur est passé, il faut que le paramètre soit un déclarateur de pointeur. Par exemple, dans l'exemple de pat, void foo(int (*args)[20]);De plus, à proprement parler, C n'a pas de tableaux multidimensionnels; mais il a des tableaux dont les éléments peuvent être d'autres tableaux. Cela ne change rien.
MM
143

La longueur de la première dimension est ignorée, mais la longueur des dimensions supplémentaires est nécessaire pour permettre au compilateur de calculer correctement les décalages. Dans l'exemple suivant, la foofonction reçoit un pointeur vers un tableau à deux dimensions.

#include <stdio.h>

void foo(int args[10][20])
{
    printf("%zd\n", sizeof(args[0]));
}

int main(int argc, char **argv)
{
    int a[2][20];
    foo(a);
    return 0;
}

La taille de la première dimension [10]est ignorée; le compilateur ne vous empêchera pas d'indexer la fin (notez que le formel veut 10 éléments, mais le réel n'en fournit que 2). Cependant, la taille de la deuxième dimension [20]est utilisée pour déterminer la foulée de chaque ligne, et ici, le formel doit correspondre au réel. Là encore, le compilateur ne vous empêchera pas non plus d'indexer la fin de la deuxième dimension.

Le décalage d'octet entre la base du tableau et un élément args[row][col]est déterminé par:

sizeof(int)*(col + 20*row)

Notez que si col >= 20, alors vous indexerez réellement dans une ligne suivante (ou à la fin du tableau entier).

sizeof(args[0]), revient 80sur ma machine où sizeof(int) == 4. Cependant, si j'essaye de prendre sizeof(args), j'obtiens l'avertissement suivant du compilateur:

foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument]
    printf("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

Ici, le compilateur avertit qu'il ne donnera que la taille du pointeur dans lequel le tableau s'est désintégré au lieu de la taille du tableau lui-même.

tapoter
la source
Très utile - la cohérence avec cela est également plausible comme raison de la bizarrerie dans le cas 1-d.
jwg
1
C'est la même idée que le cas 1-D. Ce qui ressemble à un tableau 2-D en C et C ++ est en fait un tableau 1-D, dont chaque élément est un autre tableau 1-D. Dans ce cas, nous avons un tableau avec 10 éléments, dont chaque élément est "tableau de 20 pouces". Comme décrit dans mon article, ce qui est réellement passé à la fonction est le pointeur vers le premier élément de args. Dans ce cas, le premier élément de args est un "tableau de 20 ints". Les pointeurs incluent des informations de type; ce qui est passé est "pointeur vers un tableau de 20 pouces".
MM
9
Ouais, c'est ce que le int (*)[20]type est; "pointeur vers un tableau de 20 pouces".
pat
33

Le problème et comment le surmonter en C ++

Le problème a été largement expliqué par pat et Matt . Le compilateur ignore fondamentalement la première dimension de la taille du tableau en ignorant effectivement la taille de l'argument passé.

En C ++, en revanche, vous pouvez facilement surmonter cette limitation de deux manières:

  • en utilisant des références
  • using std::array(depuis C ++ 11)

Références

Si votre fonction essaie uniquement de lire ou de modifier un tableau existant (sans le copier), vous pouvez facilement utiliser des références.

Par exemple, supposons que vous souhaitiez avoir une fonction qui réinitialise un tableau de dix ints en définissant chaque élément sur 0. Vous pouvez facilement le faire en utilisant la signature de fonction suivante:

void reset(int (&array)[10]) { ... }

Non seulement cela fonctionnera très bien , mais cela imposera également la dimension du tableau .

Vous pouvez également utiliser des modèles pour rendre le code ci-dessus générique :

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

Et enfin, vous pouvez profiter de l' constexactitude. Considérons une fonction qui imprime un tableau de 10 éléments:

void show(const int (&array)[10]) { ... }

En appliquant le constqualificatif, nous évitons d'éventuelles modifications .


La classe de bibliothèque standard pour les tableaux

Si vous considérez la syntaxe ci-dessus à la fois laide et inutile, comme je le fais, nous pouvons la jeter dans la boîte et l'utiliser à la std::arrayplace (depuis C ++ 11).

Voici le code refactoré:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

N'est-ce pas merveilleux? Sans oublier que l' astuce de code générique que je vous ai enseignée plus tôt fonctionne toujours:

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

Non seulement cela, mais vous obtenez une copie et un déplacement sémantique gratuitement. :)

void copy(std::array<Type, N> array) {
    // a copy of the original passed array 
    // is made and can be dealt with indipendently
    // from the original
}

Alors qu'est-ce que tu attends? Allez utiliser std::array.

Chaussure
la source
2
@kietz, je suis désolé que votre modification suggérée ait été rejetée, mais nous supposons automatiquement que C ++ 11 est utilisé , sauf indication contraire.
Chaussure du
c'est vrai, mais nous sommes également censés spécifier si une solution est uniquement C ++ 11, en fonction du lien que vous avez donné.
trlkly
@trlkly, je suis d'accord. J'ai modifié la réponse en conséquence. Merci de l'avoir signalé.
Chaussure
9

C'est une fonctionnalité amusante de C qui vous permet de vous tirer efficacement dans le pied si vous êtes si incliné.

Je pense que la raison en est que C est juste un pas au-dessus du langage d'assemblage. La vérification de la taille et les fonctions de sécurité similaires ont été supprimées pour permettre des performances optimales, ce qui n'est pas une mauvaise chose si le programmeur est très diligent.

En outre, attribuer une taille à l'argument de fonction a l'avantage que lorsque la fonction est utilisée par un autre programmeur, il y a une chance qu'il remarque une restriction de taille. Le simple fait d'utiliser un pointeur ne transmet pas cette information au prochain programmeur.

facture
la source
3
Oui. C est conçu pour faire confiance au programmeur sur le compilateur. Si vous indexez de manière si flagrante la fin d'un tableau, vous devez faire quelque chose de spécial et d'intention.
John
7
Je me suis fait les dents en programmation sur C il y a 14 ans. De tout ce que mon professeur a dit, la seule phrase qui m'a marqué plus que toutes les autres, «C a été écrit par des programmeurs, pour des programmeurs». La langue est extrêmement puissante. (Préparez-vous au cliché) Comme l'oncle Ben nous l'a enseigné, "Avec une grande puissance, vient une grande responsabilité."
Andrew Falanga
6

Premièrement, C ne vérifie jamais les limites du tableau. Peu importe qu'ils soient des paramètres locaux, globaux, statiques, peu importe. Vérifier les limites du tableau signifie plus de traitement, et C est censé être très efficace, donc la vérification des limites du tableau est effectuée par le programmeur en cas de besoin.

Deuxièmement, il existe une astuce qui permet de passer par valeur un tableau à une fonction. Il est également possible de renvoyer par valeur un tableau à partir d'une fonction. Il vous suffit de créer un nouveau type de données à l'aide de struct. Par exemple:

typedef struct {
  int a[10];
} myarray_t;

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

Vous devez accéder aux éléments comme ceci: foo.a [1]. Le ".a" supplémentaire peut sembler étrange, mais cette astuce ajoute de grandes fonctionnalités au langage C.

utilisateur34814
la source
7
Vous confondez la vérification des limites d'exécution avec la vérification du type à la compilation.
Ben Voigt
@Ben Voigt: Je ne parle que de la vérification des limites, comme c'est la question initiale.
user34814
2
@ user34814 La vérification des limites au moment de la compilation fait partie de la vérification de type. Plusieurs langages de haut niveau offrent cette fonctionnalité.
Leushenko
5

Pour indiquer au compilateur que myArray pointe vers un tableau d'au moins 10 pouces:

void bar(int myArray[static 10])

Un bon compilateur devrait vous donner un avertissement si vous accédez à myArray [10]. Sans le mot-clé "statique", le 10 ne signifierait rien du tout.

gnasher729
la source
1
Pourquoi un compilateur devrait-il avertir si vous accédez au 11e élément et que le tableau contient au moins 10 éléments?
nwellnhof
Cela est probablement dû au fait que le compilateur ne peut exiger que vous ayez au moins 10 éléments. Si vous essayez d'accéder au 11e élément, il ne peut pas être sûr qu'il existe (même si c'est le cas).
Dylan Watson
2
Je ne pense pas que ce soit une lecture correcte de la norme. [static]permet au compilateur d'avertir si vous appelez bar avec un int[5]. Il ne dicte pas ce que vous pouvez accéder à l' intérieur bar . Le fardeau est entièrement du côté de l'appelant.
onglet
3
error: expected primary-expression before 'static'jamais vu cette syntaxe. il est peu probable que ce soit du C ou du C ++ standard.
v.oddou
3
@ v.oddou, il est spécifié en C99, en 6.7.5.2 et 6.7.5.3.
Samuel Edwin Ward
5

Il s'agit d'une "fonctionnalité" bien connue de C, passée au C ++ car C ++ est censé compiler correctement le code C.

Le problème découle de plusieurs aspects:

  1. Un nom de tableau est censé être complètement équivalent à un pointeur.
  2. C est censé être rapide, à l'origine developerd être une sorte d '"assembleur de haut niveau" (spécialement conçu pour écrire le premier "système d'exploitation portable": Unix), donc il n'est pas censé insérer du code "caché"; la vérification de la plage d'exécution est donc "interdite".
  3. Le code machine généré pour accéder à un tableau statique ou dynamique (dans la pile ou alloué) est en fait différent.
  4. Puisque la fonction appelée ne peut pas connaître le "genre" de tableau passé en argument, tout est supposé être un pointeur et traité comme tel.

On pourrait dire que les tableaux ne sont pas vraiment supportés en C (ce n'est pas vraiment vrai, comme je le disais auparavant, mais c'est une bonne approximation); un tableau est en fait traité comme un pointeur vers un bloc de données et accessible à l'aide de l'arithmétique du pointeur. Puisque C n'a AUCUNE forme de RTTI, vous devez déclarer la taille de l'élément du tableau dans le prototype de fonction (pour prendre en charge l'arithmétique du pointeur). Ceci est encore «plus vrai» pour les tableaux multidimensionnels.

Quoi qu'il en soit, tout ce qui précède n'est plus vraiment vrai: p

La plupart des compilateurs C / C ++ modernes prennent en charge la vérification des limites, mais les normes exigent qu'elle soit désactivée par défaut (pour la compatibilité descendante). Les versions raisonnablement récentes de gcc, par exemple, font une vérification de la plage au moment de la compilation avec "-O3 -Wall -Wextra" et une vérification des limites à l'exécution complète avec "-fbounds-checking".

ZioByte
la source
Peut-être que C ++ était censé compiler du code C il y a 20 ans, mais ce n'est certainement pas le cas, et ce n'est pas le cas depuis longtemps (C ++ 98? C99 au moins, qui n'a été "corrigé" par aucun standard C ++ plus récent).
hyde
@hyde Cela me semble un peu trop dur. Pour citer Stroustrup "À quelques exceptions près, C est un sous-ensemble de C ++." (The C ++ PL 4th ed., Sec. 1.2.1). Bien que C ++ et C évoluent davantage, et que des fonctionnalités de la dernière version C existent qui ne sont pas dans la dernière version C ++, dans l'ensemble, je pense que la citation de Stroustrup est toujours valide.
mvw le
@mvw La plupart du code C écrit dans ce millénaire, qui n'est pas intentionnellement maintenu compatible C ++ en évitant les fonctionnalités incompatibles, utilisera la syntaxe des initialiseurs désignés par C99 ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) pour initialiser les structures, car c'est un moyen tellement plus clair d'initialiser une structure. En conséquence, la plupart du code C actuel sera rejeté par les compilateurs C ++ standard, car la plupart du code C initialiseront des structures.
hyde
@mvw On pourrait peut-être dire que C ++ est censé être compatible avec C donc, qu'il est possible d'écrire du code qui compilera à la fois avec des compilateurs C et C ++, si certains compromis sont faits. Mais cela nécessite l' aide d' un sous - ensemble de la fois C et C ++, non seulement sous - ensemble de C ++.
hyde
@hyde Vous seriez surpris de la quantité de code C compilable en C ++. Il y a quelques années, tout le noyau Linux était compilable en C ++ (je ne sais pas si cela est toujours vrai). Je compile régulièrement du code C dans le compilateur C ++ pour obtenir une vérification d'avertissement supérieure, seule la «production» est compilée en mode C pour optimiser l'optimisation maximale.
ZioByte
3

C ne transformera pas seulement un paramètre de type int[5]en *int; compte tenu de la déclaration typedef int intArray5[5];, il transformera un paramètre de type intArray5à *intaussi bien. Il y a des situations où ce comportement, bien qu'étrange, est utile (en particulier avec des éléments comme le va_listdefined in stdargs.h, que certaines implémentations définissent comme un tableau). Il serait illogique d'autoriser comme paramètre un type défini comme int[5](ignorant la dimension) mais ne pas permettre int[5]d'être spécifié directement.

Je trouve que la gestion par C des paramètres de type tableau est absurde, mais c'est une conséquence des efforts pour prendre un langage ad-hoc, dont de grandes parties n'étaient pas particulièrement bien définies ou réfléchies, et essayer de trouver des des spécifications cohérentes avec ce que les implémentations existantes ont fait pour les programmes existants. Beaucoup des bizarreries de C ont un sens vu sous cet angle, en particulier si l'on considère que lorsque beaucoup d'entre elles ont été inventées, de grandes parties du langage que nous connaissons aujourd'hui n'existaient pas encore. D'après ce que j'ai compris, dans le prédécesseur de C, appelé BCPL, les compilateurs ne suivaient pas vraiment très bien les types de variables. Une déclaration int arr[5];équivaut à int anonymousAllocation[5],*arr = anonymousAllocation;; une fois l'allocation mise de côté. le compilateur ne savait ni ne se souciait de savoir siarrétait un pointeur ou un tableau. Lorsqu'il est accédé comme étant l'un arr[x]ou l' autre *arr, il serait considéré comme un pointeur quelle que soit la façon dont il a été déclaré.

supercat
la source
1

Une chose qui n'a pas encore été répondue est la question réelle.

Les réponses déjà données expliquent que les tableaux ne peuvent pas être passés par valeur à une fonction en C ou C ++. Ils expliquent également qu'un paramètre déclaré comme int[]est traité comme s'il avait un type int *, et qu'une variable de type int[]peut être passée à une telle fonction.

Mais ils n'expliquent pas pourquoi il n'a jamais été commis d'erreur de fournir explicitement une longueur de tableau.

void f(int *); // makes perfect sense
void f(int []); // sort of makes sense
void f(int [10]); // makes no sense

Pourquoi le dernier de ces derniers n'est-il pas une erreur?

Une raison à cela est que cela pose des problèmes avec les typedefs.

typedef int myarray[10];
void f(myarray array);

S'il s'agissait d'une erreur de spécifier la longueur du tableau dans les paramètres de fonction, vous ne pourrez pas utiliser le myarraynom dans le paramètre de fonction. Et comme certaines implémentations utilisent des types de tableaux pour des types de bibliothèques standard tels que va_list, et que toutes les implémentations sont nécessaires pour créer jmp_bufun type de tableau, il serait très problématique s'il n'y avait pas de moyen standard de déclarer les paramètres de fonction en utilisant ces noms: sans cette capacité, il pourrait ne pas être une implémentation portable de fonctions telles que vprintf.


la source
0

Il est permis aux compilateurs de pouvoir vérifier si la taille du tableau passé est la même que celle attendue. Les compilateurs peuvent signaler un problème si ce n'est pas le cas.

hamidi
la source