Qu'est-ce que la décomposition du tableau vers le pointeur?

384

Qu'est-ce que la décomposition du tableau vers le pointeur? Y a-t-il une relation avec les pointeurs de tableau?

Vamsi
la source
73
peu connu: L'opérateur unaire plus peut être utilisé comme un "opérateur de décroissance": étant donné int a[10]; int b(void);, +aest alors un pointeur int et +best un pointeur de fonction. Utile si vous souhaitez le passer à un modèle acceptant une référence.
Johannes Schaub - litb
3
@litb - parens ferait la même chose (par exemple, (a) devrait être une expression qui évalue un pointeur), n'est-ce pas ?.
Michael Burr,
21
std::decayà partir de C ++ 14 serait un moyen moins obscur de décomposer un tableau sur unary +.
legends2k
21
@ JohannesSchaub-litb puisque cette question est balisée à la fois en C et C ++, je tiens à préciser que bien que +aet +blégal en C ++, il est illégal en C (C11 6.5.3.3/1 "L'opérande de l'unaire +ou de l' -opérateur doit avoir type arithmétique ")
MM
5
@lege Right. Mais je suppose que ce n'est pas aussi peu connu que l'astuce avec unary +. La raison pour laquelle j'ai mentionné ce n'était pas simplement parce qu'il se désintègre, mais parce que c'est des trucs amusants avec
lesquels

Réponses:

283

On dit que les tableaux "se désintègrent" en pointeurs. Un tableau C ++ déclaré comme int numbers [5]ne peut pas être re-pointé, c'est-à-dire que vous ne pouvez pas le dire numbers = 0x5a5aff23. Plus important encore, le terme de désintégration signifie une perte de type et de dimension; numbersse désintégrer en int*perdant les informations de dimension (compte 5) et le type n'est int [5]plus. Recherchez ici les cas où la décomposition ne se produit pas .

Si vous passez un tableau par valeur, ce que vous faites réellement, c'est la copie d'un pointeur - un pointeur vers le premier élément du tableau est copié dans le paramètre (dont le type doit également être un pointeur du type de l'élément du tableau). Cela fonctionne en raison de la nature en décomposition de la matrice; une fois décomposé, sizeofne donne plus la taille du tableau complet, car il devient essentiellement un pointeur. C'est pourquoi il est préférable (entre autres raisons) de passer par référence ou par pointeur.

Trois façons de passer dans un tableau 1 :

void by_value(const T* array)   // const T array[] means the same
void by_pointer(const T (*array)[U])
void by_reference(const T (&array)[U])

Les deux derniers donneront des sizeofinformations correctes , tandis que le premier ne le sera pas, car l'argument tableau a diminué pour être affecté au paramètre.

1 La constante U doit être connue au moment de la compilation.

phoebus
la source
8
Comment se passe le premier par valeur?
rlbond
10
by_value passe un pointeur sur le premier élément du tableau; dans le contexte des paramètres de fonction, T a[]est identique à T *a. by_pointer transmet la même chose, sauf que la valeur du pointeur est maintenant qualifiée const. Si vous souhaitez passer un pointeur sur le tableau (par opposition à un pointeur sur le premier élément du tableau), la syntaxe est T (*array)[U].
John Bode
4
"avec un pointeur explicite sur ce tableau" - c'est incorrect. Si aest un tableau de char, alors aest de type char[N]et se désintègre en char*; mais &aest de type char(*)[N]et ne se décomposera pas .
Pavel Minaev,
5
@FredOverflow: Donc, si Uvous changez, vous n'avez pas à vous souvenir de le changer à deux endroits, ou risquez des bugs silencieux ... Autonomie!
Courses de légèreté en orbite le
4
"Si vous passez un tableau par valeur, ce que vous faites vraiment, c'est copier un pointeur" Cela n'a aucun sens, car les tableaux ne peuvent pas être passés par valeur, point.
juanchopanza
103

Les tableaux sont fondamentalement les mêmes que les pointeurs en C / C ++, mais pas tout à fait. Une fois que vous avez converti un tableau:

const int a[] = { 2, 3, 5, 7, 11 };

dans un pointeur (qui fonctionne sans transtypage et peut donc survenir de manière inattendue dans certains cas):

const int* p = a;

vous perdez la capacité de l' sizeofopérateur à compter les éléments du tableau:

assert( sizeof(p) != sizeof(a) );  // sizes are not equal

Cette capacité perdue est appelée «désintégration».

Pour plus de détails, consultez cet article sur la dégradation des baies .

système PAUSE
la source
51
Les tableaux ne sont pas fondamentalement les mêmes que les pointeurs; ce sont des animaux complètement différents. Dans la plupart des contextes, un tableau peut être traité comme s'il s'agissait d'un pointeur, et un pointeur peut être traité comme s'il s'agissait d'un tableau, mais c'est aussi proche que possible.
John Bode
20
@John, veuillez pardonner ma langue imprécise. J'essayais d'obtenir la réponse sans m'enliser dans une longue trame de fond, et "fondamentalement ... mais pas tout à fait" est une aussi bonne explication que je n'ai jamais eue à l'université. Je suis sûr que toute personne intéressée peut obtenir une image plus précise de votre commentaire voté.
système PAUSE
"fonctionne sans transtypage" signifie la même chose que "se produit implicitement" lorsque l'on parle de conversions de types
MM
47

Voici ce que dit la norme (C99 6.3.2.1/3 - Autres opérandes - Valeurs L, tableaux et désignateurs de fonctions):

Sauf lorsqu'il s'agit de l'opérande de l'opérateur sizeof ou de l'opérateur unaire &, ou s'il s'agit d'un littéral de chaîne utilisé pour initialiser un tableau, une expression de type `` tableau de type '' est convertie en une expression de type '' pointeur vers tapez '' qui pointe vers l'élément initial de l'objet tableau et n'est pas une valeur l.

Cela signifie qu'à peu près à chaque fois que le nom du tableau est utilisé dans une expression, il est automatiquement converti en pointeur vers le 1er élément du tableau.

Notez que les noms de fonction agissent de manière similaire, mais les pointeurs de fonction sont utilisés beaucoup moins et de manière beaucoup plus spécialisée que cela ne cause pas autant de confusion que la conversion automatique des noms de tableau en pointeurs.

La norme C ++ (4.2 conversion de tableau en pointeur) assouplit l'exigence de conversion en (accentuez le mien):

Une lvalue ou rvalue de type "tableau de NT" ou "tableau de borne inconnue de T" peut être convertie en une rvalue de type "pointeur vers T."

La conversion ne doit donc pas se produire comme elle le fait presque toujours en C (cela permet aux fonctions de surcharger ou aux modèles de correspondre au type de tableau).

C'est aussi pourquoi en C, vous devriez éviter d'utiliser les paramètres de tableau dans les prototypes / définitions de fonctions (à mon avis - je ne suis pas sûr qu'il y ait un accord général). Ils provoquent de la confusion et sont de toute façon une fiction - utilisez des paramètres de pointeur et la confusion pourrait ne pas disparaître complètement, mais au moins la déclaration de paramètre ne ment pas.

Michael Burr
la source
2
Qu'est-ce qu'un exemple de ligne de code où une "expression de type" tableau de type "" est "un littéral de chaîne utilisé pour initialiser un tableau"?
Garrett
4
@Garrett char x[] = "Hello";. Le tableau de 6 éléments "Hello"ne se désintègre pas; xobtient à la place la taille 6et ses éléments sont initialisés à partir des éléments de "Hello".
MM
30

"Decay" fait référence à la conversion implicite d'une expression d'un type tableau vers un type pointeur. Dans la plupart des contextes, lorsque le compilateur voit une expression de tableau, il convertit le type de l'expression de "tableau à N éléments de T" en "pointeur sur T" et définit la valeur de l'expression à l'adresse du premier élément du tableau . Les exceptions à cette règle sont quand un tableau est un opérande soit des sizeofou &opérateurs, ou le tableau est une chaîne littérale utilisé comme un initialiseur dans une déclaration.

Supposons le code suivant:

char a[80];
strcpy(a, "This is a test");

L'expression aest de type "tableau de 80 éléments de char" et l'expression "Ceci est un test" est de type "tableau de 16 éléments de char" (en C; en C ++, les littéraux de chaîne sont des tableaux de const char). Cependant, dans l'appel à strcpy(), aucune expression n'est un opérande de sizeofou &, donc leurs types sont implicitement convertis en "pointeur vers char", et leurs valeurs sont définies à l'adresse du premier élément de chacun. Ce qui strcpy()reçoit ne sont pas des tableaux, mais des pointeurs, comme le montre son prototype:

char *strcpy(char *dest, const char *src);

Ce n'est pas la même chose qu'un pointeur de tableau. Par exemple:

char a[80];
char *ptr_to_first_element = a;
char (*ptr_to_array)[80] = &a;

Les deux ptr_to_first_elementet ptr_to_arrayont la même valeur ; l'adresse de base de a. Cependant, ils sont de types différents et sont traités différemment, comme indiqué ci-dessous:

a[i] == ptr_to_first_element[i] == (*ptr_to_array)[i] != *ptr_to_array[i] != ptr_to_array[i]

Rappelez - vous que l'expression a[i]est interprétée comme *(a+i)( ce qui ne fonctionne que si le type de tableau est converti en un type pointeur), donc à la fois a[i]et le ptr_to_first_element[i]travail même. L'expression (*ptr_to_array)[i]est interprétée comme *(*a+i). Les expressions *ptr_to_array[i]et ptr_to_array[i]peuvent entraîner des avertissements ou des erreurs du compilateur selon le contexte; ils feront certainement la mauvaise chose si vous vous attendez à ce qu'ils évaluent a[i].

sizeof a == sizeof *ptr_to_array == 80

Encore une fois, lorsqu'un tableau est un opérande de sizeof, il n'est pas converti en type de pointeur.

sizeof *ptr_to_first_element == sizeof (char) == 1
sizeof ptr_to_first_element == sizeof (char *) == whatever the pointer size
                                                  is on your platform

ptr_to_first_element est un simple pointeur sur char.

John Bode
la source
1
N'est pas "This is a test" is of type "16-element array of char"un "15-element array of char"? (longueur 14 + 1 pour \ 0)
chux - Rétablir Monica
16

Les tableaux, en C, n'ont aucune valeur.

Partout où la valeur d'un objet est attendue mais l'objet est un tableau, l'adresse de son premier élément est utilisée à la place, avec type pointer to (type of array elements).

Dans une fonction, tous les paramètres sont passés par valeur (les tableaux ne font pas exception). Lorsque vous passez un tableau dans une fonction, il "se désintègre en un pointeur" (sic); lorsque vous comparez un tableau à autre chose, il "se désintègre en un pointeur" (sic); ...

void foo(int arr[]);

La fonction foo attend la valeur d'un tableau. Mais, en C, les tableaux n'ont aucune valeur! Obtient donc à la fooplace l'adresse du premier élément du tableau.

int arr[5];
int *ip = &(arr[1]);
if (arr == ip) { /* something; */ }

Dans la comparaison ci-dessus, arrn'a aucune valeur, il devient donc un pointeur. Il devient un pointeur vers int. Ce pointeur peut être comparé à la variable ip.

Dans la syntaxe d'indexation des tableaux, vous avez l'habitude de voir, encore une fois, l'arr est `` décomposé en un pointeur ''

arr[42];
/* same as *(arr + 42); */
/* same as *(&(arr[0]) + 42); */

Les seuls moments où un tableau ne se désintègre pas en un pointeur sont quand il est l'opérande de l'opérateur sizeof, ou l'opérateur & (l'opérateur 'address of'), ou comme un littéral de chaîne utilisé pour initialiser un tableau de caractères.

pmg
la source
5
"Les tableaux n'ont aucune valeur" - qu'est-ce que cela signifie? Bien sûr, les tableaux ont de la valeur ... ce sont des objets, vous pouvez avoir des pointeurs et, en C ++, des références à eux, etc.
Pavel Minaev
2
Je crois, strictement, que la "valeur" est définie en C comme l'interprétation des bits d'un objet selon un type. J'ai du mal à trouver une signification utile de cela avec un type de tableau. Au lieu de cela, vous pouvez dire que vous vous convertissez en pointeur, mais cela n'interprète pas le contenu du tableau, il obtient simplement son emplacement. Ce que vous obtenez est la valeur d'un pointeur (et c'est une adresse), pas la valeur d'un tableau (ce serait "la séquence de valeurs des éléments contenus", comme utilisé dans la définition de "chaîne"). Cela dit, je pense qu'il est juste de dire "valeur du tableau" quand on veut dire le pointeur qu'on obtient.
Johannes Schaub - litb
de toute façon, je pense qu'il y a une légère ambiguïté: la valeur d'un objet et la valeur d'une expression (comme dans "rvalue"). Si elle est interprétée de cette dernière manière, alors une expression de tableau a sûrement une valeur: c'est celle résultant de sa décomposition en valeur r, et c'est l'expression du pointeur. Mais s'il est interprété de la première manière, il n'y a bien sûr pas de sens utile pour un objet tableau.
Johannes Schaub - litb
1
+1 pour la phrase avec une petite correction; pour les tableaux, ce n'est même pas un triplet juste un couplet [emplacement, type]. Avez-vous pensé à autre chose pour le troisième emplacement dans le cas de la baie? Je ne pense à aucun.
legends2k
1
@ legends2k: Je pense que j'ai utilisé le troisième emplacement dans les tableaux pour éviter d'en faire un cas particulier de n'avoir qu'un distique. Peut-être que [emplacement, type, vide ] aurait été mieux.
pmg
8

C'est quand le tableau pourrit et est pointé du doigt ;-)

En fait, c'est juste que si vous voulez passer un tableau quelque part, mais que le pointeur est passé à la place (car qui diable passerait le tableau entier pour vous), les gens disent que le mauvais tableau s'est désintégré en pointeur.

Michael Krelin - pirate
la source
Bien dit. Quel serait un joli tableau qui ne se désintègre pas en un pointeur ou qui est empêché de se désintégrer? Pouvez-vous citer un exemple en C? Merci.
Unheilig
@Unheilig, bien sûr, on peut emballer sous vide un tableau dans struct et passer la structure.
Michael Krelin - pirate
Je ne sais pas ce que tu veux dire par "travail". Il n'est pas autorisé d'accéder au-delà du tableau, bien que cela fonctionne comme prévu si vous vous attendez à ce qui va vraiment se passer. Ce comportement (bien que, encore une fois, officiellement indéfini) est préservé.
Michael Krelin - hacker
La décroissance se produit également dans de nombreuses situations qui ne passent le tableau nulle part (comme décrit par d'autres réponses). Par exemple a + 1,.
MM
3

La décomposition d'un tableau signifie que, lorsqu'un tableau est transmis en tant que paramètre à une fonction, il est traité de manière identique ("se désintègre") à un pointeur.

void do_something(int *array) {
  // We don't know how big array is here, because it's decayed to a pointer.
  printf("%i\n", sizeof(array));  // always prints 4 on a 32-bit machine
}

int main (int argc, char **argv) {
    int a[10];
    int b[20];
    int *c;
    printf("%zu\n", sizeof(a)); //prints 40 on a 32-bit machine
    printf("%zu\n", sizeof(b)); //prints 80 on a 32-bit machine
    printf("%zu\n", sizeof(c)); //prints 4 on a 32-bit machine
    do_something(a);
    do_something(b);
    do_something(c);
}

Il existe deux complications ou exceptions à ce qui précède.

Premièrement, lorsque vous traitez des tableaux multidimensionnels en C et C ++, seule la première dimension est perdue. Cela est dû au fait que les tableaux sont disposés de manière contiguë en mémoire, de sorte que le compilateur doit connaître toutes les dimensions sauf la première pour pouvoir calculer les décalages dans ce bloc de mémoire.

void do_something(int array[][10])
{
    // We don't know how big the first dimension is.
}

int main(int argc, char *argv[]) {
    int a[5][10];
    int b[20][10];
    do_something(a);
    do_something(b);
    return 0;
}

Deuxièmement, en C ++, vous pouvez utiliser des modèles pour déduire la taille des tableaux. Microsoft l'utilise pour les versions C ++ des fonctions Secure CRT comme strcpy_s , et vous pouvez utiliser une astuce similaire pour obtenir de manière fiable le nombre d'éléments dans un tableau .

Josh Kelley
la source
1
la décroissance se produit dans de nombreuses autres situations, pas seulement en passant un tableau à une fonction.
MM
0

tl; dr: lorsque vous utilisez un tableau que vous avez défini, vous utilisez en fait un pointeur sur son premier élément.

Donc:

  • Lorsque vous écrivez, arr[idx]vous dites vraiment *(arr + idx).
  • les fonctions ne prennent jamais vraiment les tableaux comme paramètres, seulement des pointeurs, même lorsque vous spécifiez un paramètre de tableau.

Sortes d'exceptions à cette règle:

  • Vous pouvez transmettre des tableaux de longueur fixe à des fonctions dans a struct.
  • sizeof() donne la taille occupée par le tableau, pas la taille d'un pointeur.
einpoklum
la source
0

Je pourrais être si audacieux de penser qu'il existe quatre (4) façons de passer un tableau comme argument de fonction. Voici également le code court mais fonctionnel de votre lecture.

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

using namespace std;

// test data
// notice native array init with no copy aka "="
// not possible in C
 const char* specimen[]{ __TIME__, __DATE__, __TIMESTAMP__ };

// ONE
// simple, dangerous and useless
template<typename T>
void as_pointer(const T* array) { 
    // a pointer
    assert(array != nullptr); 
} ;

// TWO
// for above const T array[] means the same
// but and also , minimum array size indication might be given too
// this also does not stop the array decay into T *
// thus size information is lost
template<typename T>
void by_value_no_size(const T array[0xFF]) { 
    // decayed to a pointer
    assert( array != nullptr ); 
}

// THREE
// size information is preserved
// but pointer is asked for
template<typename T, size_t N>
void pointer_to_array(const T (*array)[N])
{
   // dealing with native pointer 
    assert( array != nullptr ); 
}

// FOUR
// no C equivalent
// array by reference
// size is preserved
template<typename T, size_t N>
void reference_to_array(const T (&array)[N])
{
    // array is not a pointer here
    // it is (almost) a container
    // most of the std:: lib algorithms 
    // do work on array reference, for example
    // range for requires std::begin() and std::end()
    // on the type passed as range to iterate over
    for (auto && elem : array )
    {
        cout << endl << elem ;
    }
}

int main()
{
     // ONE
     as_pointer(specimen);
     // TWO
     by_value_no_size(specimen);
     // THREE
     pointer_to_array(&specimen);
     // FOUR
     reference_to_array( specimen ) ;
}

Je pourrais aussi penser que cela montre la supériorité de C ++ vs C. Au moins en référence (jeu de mots voulu) de passer un tableau par référence.

Bien sûr, il existe des projets extrêmement stricts sans allocation de tas, sans exception et sans std :: lib. On pourrait dire que la gestion des baies natives C ++ est une fonctionnalité de langage critique.

Chef gladiateur
la source