web-dev-qa-db-fra.com

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

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 à
tout, parce que 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 casse-tête ne concerne pas cette partie.

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

129
Fanl

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.

Le pointeur ne contenant aucune information de longueur, le contenu de votre [] dans la liste des paramètres formels de la fonction sont en fait ignorés.

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

152
M.M

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 fonction foo 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. Encore une fois, le compilateur ne vous empêchera pas non plus d'indexer la fin de la deuxième dimension.

L'octet décalé de la base du tableau vers un élément args[row][col] Est déterminé par:

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

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

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

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.

142
pat

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

Le problème a été expliqué en détail par pat et Matt . Le compilateur ignore fondamentalement la première dimension de la taille du tableau, 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
  • en utilisant std::array (depuis C++ 11)

Les références

Si votre fonction essaie seulement de lire ou de modifier un tableau existant (pas de 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 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 aussi imposera la dimension du tablea .

Vous pouvez également utiliser les 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'exactitude de const. Prenons une fonction qui imprime un tableau de 10 éléments:

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

En appliquant le qualificatif const nous sommes empêchant 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 utiliser std::array à la place (depuis C++ 11).

Voici le code refactorisé:

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

N'est-ce pas merveilleux? Sans oublier que le truc de code générique que je vous ai enseigné 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 déplacez la 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 .

33
Shoe

C'est une fonction amusante de C qui vous permet de vous tirer efficacement dans le pied si vous êtes si enclin.

Je pense que la raison en est que C est juste une étape au-dessus du langage d'assemblage. La vérification de la taille et des 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.

De plus, l'attribution d'une taille à l'argument de la fonction a l'avantage que lorsque la fonction est utilisée par un autre programmeur, il y a une chance qu'ils ' Nous remarquerons une restriction de taille. L'utilisation d'un pointeur ne transmet pas ces informations au programmeur suivant.

8
bill

Premièrement, C ne vérifie jamais les limites du tableau. Peu importe qu'ils soient locaux, globaux, statiques, paramètres, peu importe. La vérification des 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. Vous avez juste besoin 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 celui-ci: foo.a [1]. Le ".a" supplémentaire peut sembler étrange, mais cette astuce ajoute de grandes fonctionnalités au langage C.

6
user34814

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

Le problème se pose sous plusieurs aspects:

  1. Un nom de tableau est censé être complètement équivalent à un pointeur.
  2. C est censé être rapide, à l'origine developerd pour être une sorte d '"assembleur de haut niveau" (spécialement conçu pour écrire le premier "système d'exploitation portable": Unix), il est donc pas censé insérer 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 réalité 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 pris en charge en C (ce n'est pas vraiment vrai, comme je le disais auparavant, mais c'est une bonne approximation); un tableau est vraiment traité comme un pointeur vers un bloc de données et accessible à l'aide de l'arithmétique du pointeur. Puisque C n'a PAS de forme RTTI, vous devez déclarer la taille de l'élément de tableau dans le prototype de fonction (pour prendre en charge l'arithmétique des pointeurs). Cela 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 do prennent en charge la vérification des limites, mais les normes exigent qu'elle soit désactivée par défaut (pour une compatibilité descendante). Des 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 complète des limites d'exécution avec "-fbounds-checks".

5
ZioByte

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

void bar(int myArray[static 10])

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

5
gnasher729

C ne transformera pas seulement un paramètre de type int[5] en *int; étant donné la déclaration typedef int intArray5[5];, il transformera un paramètre de type intArray5 à *int ainsi que. Il existe des situations où ce comportement, bien qu'étrange, est utile (en particulier avec des choses comme le va_list défini dans 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] (en ignorant la dimension) mais pas autoriser int[5] à spécifier 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 comportements des spécifications cohérentes avec ce que les implémentations existantes ont fait pour les programmes existants. Beaucoup de bizarreries de C ont un sens quand on les considère 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 je comprends, dans le prédécesseur de C, appelé BCPL, les compilateurs ne suivaient pas très bien les types de variables. Une déclaration int arr[5]; était équivalent à int anonymousAllocation[5],*arr = anonymousAllocation;; une fois l'allocation réservée. le compilateur ne savait ni ne se souciait si arr était un pointeur ou un tableau. En cas d'accès en tant que arr[x] ou *arr, il serait considéré comme un pointeur quelle que soit la façon dont il a été déclaré.

3
supercat

Une question à laquelle on n'a pas encore répondu est la vraie question.

Les réponses déjà données expliquent que les tableaux ne peuvent pas être transmis 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 le type int *, et qu'une variable de type int[] peut être passé à une telle fonction.

Mais ils n'expliquent pas pourquoi il n'a jamais été fait 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 qu'elle cause des problèmes avec les typedefs.

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

S'il s'agissait d'une erreur pour spécifier la longueur du tableau dans les paramètres de fonction, vous ne seriez pas en mesure d'utiliser le nom myarray dans le paramètre de fonction. Et puisque certaines implémentations utilisent des types de tableaux pour les types de bibliothèques standard tels que va_list, et toutes les implémentations sont nécessaires pour faire jmp_buf un type tableau, ce serait très problématique s'il n'y avait pas de manière standard de déclarer des paramètres de fonction en utilisant ces noms: sans cette capacité, il ne pourrait pas y avoir d'implémentation portable de fonctions telles que vprintf.

1
user743382