Comment utiliser les tableaux en C ++?

480

C ++ a hérité des tableaux de C où ils sont utilisés pratiquement partout. C ++ fournit des abstractions plus faciles à utiliser et moins sujettes aux erreurs ( std::vector<T>depuis C ++ 98 et std::array<T, n>depuis C ++ 11 ), donc le besoin de tableaux ne se pose pas aussi souvent qu'en C. Cependant, lorsque vous lisez hérité code ou interagir avec une bibliothèque écrite en C, vous devez avoir une solide compréhension du fonctionnement des tableaux.

Cette FAQ est divisée en cinq parties:

  1. tableaux au niveau du type et accès aux éléments
  2. création et initialisation de tableaux
  3. affectation et passage de paramètres
  4. tableaux multidimensionnels et tableaux de pointeurs
  5. pièges courants lors de l'utilisation de tableaux

Si vous pensez que quelque chose d'important manque dans cette FAQ, écrivez une réponse et liez-la ici en tant que partie supplémentaire.

Dans le texte suivant, "tableau" signifie "tableau C", pas le modèle de classe std::array. Une connaissance de base de la syntaxe du déclarant C est supposée. Notez que l'utilisation manuelle de newet deletecomme illustré ci-dessous est extrêmement dangereuse face aux exceptions, mais c'est le sujet d' une autre FAQ .

(Remarque: Ceci est censé être une entrée de la FAQ C ++ de Stack Overflow . Si vous voulez critiquer l'idée de fournir une FAQ sous cette forme, alors la publication sur la méta qui a commencé tout cela serait l'endroit pour le faire. Réponses à cette question est surveillée dans le salon de discussion C ++ , où l'idée de FAQ a commencé en premier, donc votre réponse est très susceptible d'être lue par ceux qui ont eu l'idée.)

fredoverflow
la source
Ils seraient encore meilleurs si les pointeurs pointaient toujours vers le début plutôt que quelque part au milieu de leur cible ...
Déduplicateur
Vous devez utiliser le vecteur STL car il vous offre une plus grande flexibilité.
Moiz Sajid
2
Avec la disponibilité combinée de std::arrays, std::vectors et gsl::spans - je m'attendrais franchement à une FAQ sur la façon d'utiliser les tableaux en C ++ pour dire "maintenant, vous pouvez commencer à envisager de ne pas les utiliser."
einpoklum

Réponses:

302

Tableaux au niveau du type

Un type de tableau est désigné par T[n]Test le type d'élément et nest une taille positive , le nombre d'éléments dans le tableau. Le type de tableau est un type de produit du type d'élément et de la taille. Si l'un ou les deux de ces ingrédients diffèrent, vous obtenez un type distinct:

#include <type_traits>

static_assert(!std::is_same<int[8], float[8]>::value, "distinct element type");
static_assert(!std::is_same<int[8],   int[9]>::value, "distinct size");

Notez que la taille fait partie du type, c'est-à-dire que les types de tableau de taille différente sont des types incompatibles qui n'ont absolument rien à voir les uns avec les autres. sizeof(T[n])est équivalent à n * sizeof(T).

Décroissance de la matrice au pointeur

La seule "connexion" entre T[n]et T[m]est que les deux types peuvent être implicitement convertis en T*, et le résultat de cette conversion est un pointeur vers le premier élément du tableau. Autrement dit, partout où un T*est requis, vous pouvez fournir un T[n], et le compilateur fournira silencieusement ce pointeur:

                  +---+---+---+---+---+---+---+---+
the_actual_array: |   |   |   |   |   |   |   |   |   int[8]
                  +---+---+---+---+---+---+---+---+
                    ^
                    |
                    |
                    |
                    |  pointer_to_the_first_element   int*

Cette conversion est connue sous le nom de "désintégration de matrice à pointeur", et c'est une source majeure de confusion. La taille du tableau est perdue dans ce processus, car il ne fait plus partie du type ( T*). Pro: Oublier la taille d'un tableau au niveau du type permet à un pointeur de pointer vers le premier élément d'un tableau de n'importe quelle taille. Con: étant donné un pointeur sur le premier (ou tout autre) élément d'un tableau, il n'y a aucun moyen de détecter la taille de ce tableau ou où exactement le pointeur pointe par rapport aux limites du tableau. Les pointeurs sont extrêmement stupides .

Les tableaux ne sont pas des pointeurs

Le compilateur génère silencieusement un pointeur vers le premier élément d'un tableau chaque fois qu'il est jugé utile, c'est-à-dire chaque fois qu'une opération échoue sur un tableau mais réussit sur un pointeur. Cette conversion de tableau en pointeur est triviale, car la valeur du pointeur qui en résulte est simplement l'adresse du tableau. Notez que le pointeur n'est pas stocké en tant que partie du tableau lui-même (ou ailleurs dans la mémoire). Un tableau n'est pas un pointeur.

static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");

Un contexte important dans lequel un tableau ne se désintègre pas en un pointeur vers son premier élément est lorsque l' &opérateur lui est appliqué. Dans ce cas, l' &opérateur renvoie un pointeur sur l' ensemble du tableau, pas seulement un pointeur sur son premier élément. Bien que dans ce cas, les valeurs (les adresses) soient les mêmes, un pointeur vers le premier élément d'un tableau et un pointeur vers le tableau entier sont des types complètement distincts:

static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");

L'art ASCII suivant explique cette distinction:

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | int[8]
|     | +---+---+---+---+---+---+---+---+ |
|     +---^-------------------------------+
|         |
|         |
|         |
|         |  pointer_to_the_first_element   int*
|
|  pointer_to_the_entire_array              int(*)[8]

Notez que le pointeur sur le premier élément ne pointe que sur un seul entier (représenté comme une petite boîte), tandis que le pointeur sur l'ensemble du tableau pointe sur un tableau de 8 entiers (représenté sur une grande boîte).

La même situation se produit dans les classes et est peut-être plus évidente. Un pointeur sur un objet et un pointeur sur son premier membre de données ont la même valeur (la même adresse), mais ce sont des types complètement distincts.

Si vous n'êtes pas familier avec la syntaxe du déclarant C, les parenthèses dans le type int(*)[8]sont essentielles:

  • int(*)[8] est un pointeur vers un tableau de 8 entiers.
  • int*[8]est un tableau de 8 pointeurs, chaque élément de type int*.

Accéder aux éléments

C ++ fournit deux variantes syntaxiques pour accéder aux éléments individuels d'un tableau. Aucun d'eux n'est supérieur à l'autre, et vous devez vous familiariser avec les deux.

Arithmétique du pointeur

Étant donné un pointeur psur le premier élément d'un tableau, l'expression p+irenvoie un pointeur sur le i-ème élément du tableau. En déréférençant ce pointeur par la suite, on peut accéder à des éléments individuels:

std::cout << *(x+3) << ", " << *(x+7) << std::endl;

Si xdénote un tableau , la décroissance du tableau vers le pointeur se déclenchera, car l'ajout d'un tableau et d'un entier n'a pas de sens (il n'y a pas d'opération plus sur les tableaux), mais l'ajout d'un pointeur et d'un entier est logique:

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*

(Notez que le pointeur généré implicitement n'a pas de nom, j'ai donc écrit x+0 pour l'identifier.)

Si, d'autre part, xdésigne un pointeur sur le premier (ou tout autre) élément d'un tableau, la décroissance tableau sur pointeur n'est pas nécessaire, car le pointeur sur lequel ion va ajouter existe déjà:

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+

Notez que dans le cas représenté, xest une variable de pointeur (visible par la petite case à côté x), mais elle pourrait tout aussi bien être le résultat d'une fonction renvoyant un pointeur (ou toute autre expression de typeT* ).

Opérateur d'indexation

Étant donné que la syntaxe *(x+i)est un peu maladroite, C ++ fournit la syntaxe alternative x[i]:

std::cout << x[3] << ", " << x[7] << std::endl;

Étant donné que l'addition est commutative, le code suivant fait exactement la même chose:

std::cout << 3[x] << ", " << 7[x] << std::endl;

La définition de l'opérateur d'indexation conduit à l'équivalence intéressante suivante:

&x[i]  ==  &*(x+i)  ==  x+i

Cependant, &x[0]n'est généralement pas équivalent à x. Le premier est un pointeur, le second un tableau. Ce n'est que lorsque le contexte déclenche la décroissance tableau vers pointeur peut xet peut &x[0]être utilisé de manière interchangeable. Par exemple:

T* p = &array[0];  // rewritten as &*(array+0), decay happens due to the addition
T* q = array;      // decay happens due to the assignment

Sur la première ligne, le compilateur détecte une affectation d'un pointeur vers un pointeur, qui réussit de manière triviale. Sur la deuxième ligne, il détecte une affectation d'un tableau vers un pointeur. Étant donné que cela n'a pas de sens (mais pointeur affectation à pointeur a du sens), la décroissance tableau-à-pointeur se déclenche comme d'habitude.

Gammes

Un tableau de type T[n]a des néléments, indexés de 0à n-1; il n'y a aucun élément n. Et pourtant, pour prendre en charge les plages semi-ouvertes (où le début est inclusif et la fin est exclusif ), C ++ permet le calcul d'un pointeur vers le n-ème élément (inexistant), mais il est illégal de déréférencer ce pointeur:

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*

Par exemple, si vous souhaitez trier un tableau, les deux éléments suivants fonctionnent également bien:

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);

Notez qu'il est illégal de fournir &x[n]comme deuxième argument car il est équivalent à &*(x+n), et la sous-expression *(x+n)invoque techniquement un comportement non défini en C ++ (mais pas en C99).

Notez également que vous pouvez simplement fournir xcomme premier argument. C'est un peu trop laconique à mon goût, et cela rend également la déduction des arguments de modèle un peu plus difficile pour le compilateur, car dans ce cas, le premier argument est un tableau mais le deuxième argument est un pointeur. (Encore une fois, la décroissance du tableau vers le pointeur entre en jeu.)

fredoverflow
la source
Les cas où le tableau ne se désintègre pas en un pointeur sont illustrés ici pour référence.
legends2k
@fredoverflow Dans la partie Access ou Ranges, il peut être utile de mentionner que les tableaux C fonctionnent avec des boucles basées sur la plage C ++ 11.
gnzlbg
135

Les programmeurs confondent souvent des tableaux multidimensionnels avec des tableaux de pointeurs.

Tableaux multidimensionnels

La plupart des programmeurs connaissent les tableaux multidimensionnels nommés, mais beaucoup ne savent pas que les tableaux multidimensionnels peuvent également être créés de manière anonyme. Les tableaux multidimensionnels sont souvent appelés «tableaux de tableaux» ou « vrais tableaux multidimensionnels».

Tableaux multidimensionnels nommés

Lorsque vous utilisez des tableaux multidimensionnels nommés, toutes les dimensions doivent être connues au moment de la compilation:

int H = read_int();
int W = read_int();

int connect_four[6][7];   // okay

int connect_four[H][7];   // ISO C++ forbids variable length array
int connect_four[6][W];   // ISO C++ forbids variable length array
int connect_four[H][W];   // ISO C++ forbids variable length array

Voici à quoi ressemble un tableau multidimensionnel nommé en mémoire:

              +---+---+---+---+---+---+---+
connect_four: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+

Notez que les grilles 2D telles que celles ci-dessus ne sont que des visualisations utiles. Du point de vue du C ++, la mémoire est une séquence "plate" d'octets. Les éléments d'un tableau multidimensionnel sont stockés dans l'ordre des lignes principales. Autrement dit, connect_four[0][6]et connect_four[1][0]sont voisins dans la mémoire. En fait, connect_four[0][7]et connect_four[1][0]dénotons le même élément! Cela signifie que vous pouvez prendre des tableaux multidimensionnels et les traiter comme de grands tableaux unidimensionnels:

int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);

Tableaux multidimensionnels anonymes

Avec les tableaux multidimensionnels anonymes, toutes les dimensions sauf la première doivent être connues au moment de la compilation:

int (*p)[7] = new int[6][7];   // okay
int (*p)[7] = new int[H][7];   // okay

int (*p)[W] = new int[6][W];   // ISO C++ forbids variable length array
int (*p)[W] = new int[H][W];   // ISO C++ forbids variable length array

Voici à quoi ressemble un tableau multidimensionnel anonyme en mémoire:

              +---+---+---+---+---+---+---+
        +---> |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |
      +-|-+
   p: | | |
      +---+

Notez que le tableau lui-même est toujours alloué comme un seul bloc en mémoire.

Tableaux de pointeurs

Vous pouvez surmonter la restriction de largeur fixe en introduisant un autre niveau d'indirection.

Tableaux nommés de pointeurs

Voici un tableau nommé de cinq pointeurs qui sont initialisés avec des tableaux anonymes de différentes longueurs:

int* triangle[5];
for (int i = 0; i < 5; ++i)
{
    triangle[i] = new int[5 - i];
}

// ...

for (int i = 0; i < 5; ++i)
{
    delete[] triangle[i];
}

Et voici à quoi cela ressemble en mémoire:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
          +---+---+---+---+---+

Étant donné que chaque ligne est désormais allouée individuellement, l'affichage des tableaux 2D en tant que tableaux 1D ne fonctionne plus.

Tableaux anonymes de pointeurs

Voici un tableau anonyme de 5 pointeurs (ou tout autre nombre de) qui sont initialisés avec des tableaux anonymes de différentes longueurs:

int n = calculate_five();   // or any other number
int** p = new int*[n];
for (int i = 0; i < n; ++i)
{
    p[i] = new int[n - i];
}

// ...

for (int i = 0; i < n; ++i)
{
    delete[] p[i];
}
delete[] p;   // note the extra delete[] !

Et voici à quoi cela ressemble en mémoire:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
          | | | | | | | | | | |
          +---+---+---+---+---+
            ^
            |
            |
          +-|-+
       p: | | |
          +---+

Conversions

La décomposition de tableau à pointeur s'étend naturellement aux tableaux de tableaux et aux tableaux de pointeurs:

int array_of_arrays[6][7];
int (*pointer_to_array)[7] = array_of_arrays;

int* array_of_pointers[6];
int** pointer_to_pointer = array_of_pointers;

Cependant, il n'y a pas de conversion implicite de T[h][w]à T**. Si une telle conversion implicite existait, le résultat serait un pointeur vers le premier élément d'un tableau de hpointeurs vers T(chacun pointant vers le premier élément d'une ligne du tableau 2D d'origine), mais ce tableau de pointeurs n'existe nulle part dans mémoire encore. Si vous souhaitez une telle conversion, vous devez créer et remplir manuellement le tableau de pointeurs requis:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = connect_four[i];
}

// ...

delete[] p;

Notez que cela génère une vue du tableau multidimensionnel d'origine. Si vous avez plutôt besoin d'une copie, vous devez créer des tableaux supplémentaires et copier les données vous-même:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = new int[7];
    std::copy(connect_four[i], connect_four[i + 1], p[i]);
}

// ...

for (int i = 0; i < 6; ++i)
{
    delete[] p[i];
}
delete[] p;
fredoverflow
la source
Comme suggestion: vous devez signaler que int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];ainsi que int (*p)[W] = new int[6][W];et int (*p)[W] = new int[H][W];sont des déclarations valides, quand Het Wsont connus au moment de la compilation.
RobertS soutient Monica Cellio le
88

Affectation

Pour aucune raison particulière, les tableaux ne peuvent pas être assignés les uns aux autres. Utilisez std::copyplutôt:

#include <algorithm>

// ...

int a[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int b[8];
std::copy(a + 0, a + 8, b);

C'est plus flexible que ce que pourrait fournir une véritable affectation de tableau car il est possible de copier des tranches de tableaux plus grands dans des tableaux plus petits. std::copyest généralement spécialisé pour les types primitifs pour donner des performances maximales. Il est peu probable questd::memcpy performances meilleures. En cas de doute, mesurez.

Bien que vous ne puissiez pas attribuer directement des tableaux, vous pouvez affecter des structures et des classes qui contiennent des membres de tableau. En effet , les membres du tableau sont copiés par membre par l'opérateur d'affectation qui est fourni par défaut par le compilateur. Si vous définissez manuellement l'opérateur d'affectation pour vos propres types de structure ou de classe, vous devez recourir à la copie manuelle pour les membres du tableau.

Passage de paramètres

Les tableaux ne peuvent pas être transmis par valeur. Vous pouvez les passer par pointeur ou par référence.

Passer par le pointeur

Comme les tableaux eux-mêmes ne peuvent pas être transmis par valeur, un pointeur vers leur premier élément est généralement transmis par valeur à la place. Ceci est souvent appelé "passer par le pointeur". Étant donné que la taille du tableau n'est pas récupérable via ce pointeur, vous devez passer un deuxième paramètre indiquant la taille du tableau (la solution C classique) ou un deuxième pointeur pointant après le dernier élément du tableau (la solution d'itérateur C ++) :

#include <numeric>
#include <cstddef>

int sum(const int* p, std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

int sum(const int* p, const int* q)
{
    return std::accumulate(p, q, 0);
}

Comme alternative syntaxique, vous pouvez également déclarer les paramètres en tant que T p[], et cela signifie exactement la même chose que T* p dans le contexte des listes de paramètres uniquement :

int sum(const int p[], std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

Vous pouvez penser que le compilateur de réécriture T p[]pour T *p dans le contexte des listes de paramètres uniquement . Cette règle spéciale est en partie responsable de toute la confusion concernant les tableaux et les pointeurs. Dans tous les autres contextes, déclarer quelque chose comme un tableau ou comme un pointeur fait un énorme différence.

Malheureusement, vous pouvez également fournir une taille dans un paramètre de tableau qui est silencieusement ignoré par le compilateur. Autrement dit, les trois signatures suivantes sont exactement équivalentes, comme l'indiquent les erreurs du compilateur:

int sum(const int* p, std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[], std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[8], std::size_t n)   // the 8 has no meaning here

Passer par référence

Les tableaux peuvent également être transmis par référence:

int sum(const int (&a)[8])
{
    return std::accumulate(a + 0, a + 8, 0);
}

Dans ce cas, la taille du tableau est importante. Étant donné que l'écriture d'une fonction qui n'accepte que des tableaux d'exactement 8 éléments est de peu d'utilité, les programmeurs écrivent généralement des fonctions telles que des modèles:

template <std::size_t n>
int sum(const int (&a)[n])
{
    return std::accumulate(a + 0, a + n, 0);
}

Notez que vous ne pouvez appeler un tel modèle de fonction qu'avec un tableau réel d'entiers, pas avec un pointeur sur un entier. La taille du tableau est automatiquement déduite et pour chaque taille n, une fonction différente est instanciée à partir du modèle. Vous pouvez également écrire des modèles de fonctions très utiles qui résument à la fois le type d'élément et la taille.

fredoverflow
la source
2
Cela pourrait valoir la peine d'ajouter une note selon laquelle même si vous semblez void foo(int a[3]) aque l'on passe le tableau par valeur, la modification à l' aintérieur de foomodifiera le tableau d'origine. Cela devrait être clair car les tableaux ne peuvent pas être copiés, mais cela peut valoir la peine de le renforcer.
gnzlbg
C ++ 20 hasranges::copy(a, b)
LF
int sum( int size_, int a[size_]);- à partir de (je pense) C99
Chef Gladiator
73

5. Pièges courants lors de l'utilisation de tableaux.

5.1 Piège: Faire confiance aux liens de type non sûrs.

OK, on ​​vous a dit, ou vous avez découvert vous-même, que les globaux (variables de portée d'espace de noms accessibles en dehors de l'unité de traduction) sont Evil ™. Mais saviez-vous à quel point ils sont vraiment mauvais ™? Considérez le programme ci-dessous, composé de deux fichiers [main.cpp] et [numbers.cpp]:

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

Dans Windows 7, cela se compile et se lie très bien avec MinGW g ++ 4.4.1 et Visual C ++ 10.0.

Étant donné que les types ne correspondent pas, le programme se bloque lorsque vous l'exécutez.

La boîte de dialogue de plantage de Windows 7

Explication formelle: le programme a un comportement indéfini (UB), et au lieu de se bloquer, il peut donc simplement se bloquer, ou peut-être ne rien faire, ou il peut envoyer des e-mails menaçants aux présidents des États-Unis, de Russie, d'Inde, Chine et Suisse, et faites voler les démons nasaux hors de votre nez.

Explication pratique: dans main.cpple tableau est traité comme un pointeur, placé à la même adresse que le tableau. Pour un exécutable 32 bits, cela signifie que la première intvaleur du tableau est traitée comme un pointeur. -À- dire, dans main.cppla numbersvariable contient ou semble contenir, (int*)1. Cela oblige le programme à accéder à la mémoire tout en bas de l'espace d'adressage, qui est classiquement réservé et à l'origine de pièges. Résultat: vous obtenez un crash.

Les compilateurs ont pleinement le droit de ne pas diagnostiquer cette erreur, car C ++ 11 §3.5 / 10 dit, à propos de l'exigence de types compatibles pour les déclarations,

[N3290 §3.5 / 10]
Une violation de cette règle sur l'identité de type ne nécessite pas de diagnostic.

Le même paragraphe détaille la variation autorisée:

… Les déclarations d'un objet tableau peuvent spécifier des types de tableau qui diffèrent par la présence ou l'absence d'une limite de tableau principale (8.3.4).

Cette variation autorisée n'inclut pas la déclaration d'un nom en tant que tableau dans une unité de traduction et en tant que pointeur dans une autre unité de traduction.

5.2 Piège: faire une optimisation prématurée ( memsetet amis).

Pas encore écrit

5.3 Piège: utiliser l'idiome C pour obtenir le nombre d'éléments.

Avec une profonde expérience en C, il est naturel d'écrire…

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

Puisqu'un arraydésintègre pour pointer vers le premier élément si nécessaire, l'expression sizeof(a)/sizeof(a[0])peut également être écrite comme sizeof(a)/sizeof(*a). Cela signifie la même chose, et peu importe comment il est écrit, c'est l' idiome C pour trouver les éléments numériques du tableau.

Écueil principal: l'idiome C n'est pas de type sécurisé. Par exemple, le code…

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

passe un pointeur vers N_ITEMS, et par conséquent produit très probablement un mauvais résultat. Compilé comme un exécutable 32 bits dans Windows 7, il produit…

7 éléments, appelant l'affichage ...
1 éléments.

  1. Le compilateur réécrit int const a[7]juste int const a[].
  2. Le compilateur réécrit int const a[]dans int const* a.
  3. N_ITEMS est donc invoqué avec un pointeur.
  4. Pour un exécutable 32 bits sizeof(array)(taille d'un pointeur) vaut alors 4.
  5. sizeof(*array)est équivalent à sizeof(int), qui pour un exécutable 32 bits est également 4.

Afin de détecter cette erreur au moment de l'exécution, vous pouvez le faire…

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7 éléments, appel de l'affichage ...
Échec de l'assertion: ("N_ITEMS nécessite un tableau réel comme argument", typeid (a)! = Typeid (& * a)), fichier runtime_detect ion.cpp, ligne 16

Cette application a demandé au Runtime de la terminer de manière inhabituelle.
Veuillez contacter l'équipe d'assistance de l'application pour plus d'informations.

La détection des erreurs d'exécution est meilleure que l'absence de détection, mais elle gaspille un peu de temps processeur et peut-être beaucoup plus de temps programmeur. Mieux avec la détection au moment de la compilation! Et si vous êtes heureux de ne pas prendre en charge les tableaux de types locaux avec C ++ 98, vous pouvez le faire:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

En compilant cette définition substituée dans le premier programme complet, avec g ++, j'ai eu…

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: Dans la fonction 'void display (const int *)':
compile_time_detection.cpp: 14: erreur: pas de fonction correspondante pour l'appel à 'n_items (const int * &)'

M: \ count> _

Comment cela fonctionne: le tableau est transmis par référence à n_items, et donc il ne se désintègre pas pour pointer vers le premier élément, et la fonction peut simplement renvoyer le nombre d'éléments spécifié par le type.

Avec C ++ 11, vous pouvez également l'utiliser pour les tableaux de type local, et c'est l' idiome C ++ de type sécurisé pour trouver le nombre d'éléments d'un tableau.

5.4 Piège C ++ 11 et C ++ 14: Utilisation d'une constexprfonction de taille de tableau.

Avec C ++ 11 et versions ultérieures, c'est naturel, mais comme vous le verrez dangereux !, pour remplacer la fonction C ++ 03

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

avec

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

où le changement significatif est l'utilisation de constexpr, qui permet à cette fonction de produire une constante de temps de compilation .

Par exemple, contrairement à la fonction C ++ 03, une telle constante de temps de compilation peut être utilisée pour déclarer un tableau de la même taille qu'un autre:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

Mais considérez ce code en utilisant la constexprversion:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

Le piège: à partir de juillet 2015, ce qui précède se compile avec MinGW-64 5.1.0 avec -pedantic-errors, et, teste avec les compilateurs en ligne à gcc.godbolt.org/ , également avec clang 3.0 et clang 3.2, mais pas avec clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) ou 3.7 (expérimental). Et important pour la plate-forme Windows, il ne compile pas avec Visual C ++ 2015. La raison en est une instruction C ++ 11 / C ++ 14 sur l'utilisation des références dansconstexpr expressions:

C ++ 11 C ++ 14 5,19 $ / 2 neuf e tiret

Une expression conditionnelle e est une expression constante de base à moins que l'évaluation de e, suivant les règles de la machine abstraite (1.9), n'évalue l'une des expressions suivantes:
        ⋮

  • une id-expression qui fait référence à une variable ou à un membre de données de type référence, sauf si la référence a une initialisation précédente et
    • il est initialisé avec une expression constante ou
    • il s'agit d'un membre de données non statique d'un objet dont la durée de vie a commencé dans le cadre de l'évaluation de e;

On peut toujours écrire le plus verbeux

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

… Mais cela échoue quand Collection ne s'agit pas d'un tableau brut.

Pour traiter des collections qui peuvent être des non-tableaux, il faut la surcharge d'une n_itemsfonction, mais aussi, pour le temps de compilation, il faut une représentation du temps de compilation de la taille du tableau. Et la solution C ++ 03 classique, qui fonctionne bien également en C ++ 11 et C ++ 14, consiste à laisser la fonction rapporter son résultat non pas comme une valeur mais via son type de résultat de fonction . Par exemple, comme ceci:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

A propos du choix du type de retour pour static_n_items: ce code n'utilise pas std::integral_constant car avec std::integral_constantle résultat est représenté directement comme une constexprvaleur, réintroduisant le problème d'origine. Au lieu d'une Size_carrierclasse, on peut laisser la fonction retourner directement une référence à un tableau. Cependant, tout le monde ne connaît pas cette syntaxe.

À propos de la dénomination: une partie de cette solution constexpr -invalid-due-to-reference est de rendre explicite le choix de la constante de temps de compilation.

Espérons que le problème oops-there-was-a-reference-impliqué-in-your- constexprsera corrigé avec C ++ 17, mais jusque-là, une macro comme celle- STATIC_N_ITEMSci donne la portabilité, par exemple aux compilateurs clang et Visual C ++, en conservant le type sécurité.

Connexes: les macros ne respectent pas les étendues, donc pour éviter les collisions de noms, il peut être judicieux d'utiliser un préfixe de nom, par exemple MYLIB_STATIC_N_ITEMS.

Santé et hth. - Alf
la source
1
+1 Grand test de codage C: j'ai passé 15 minutes sur VC ++ 10.0 et GCC 4.1.2 à essayer de corriger le Segmentation fault... J'ai enfin trouvé / compris après avoir lu vos explications! Veuillez écrire votre section §5.2 :-) Cheers
olibre
Bien. Un nit - le type de retour pour countOf devrait être size_t au lieu de ptrdiff_t. Il vaut probablement la peine de mentionner qu'en C ++ 11/14, il devrait être constexpr et noexcept.
Ricky65
@ Ricky65: Merci d'avoir mentionné les considérations C ++ 11. La prise en charge de ces fonctionnalités a tardé à venir pour Visual C ++. En ce size_tqui concerne , cela n'a aucun avantage que je connaisse pour les plates-formes modernes, mais il a un certain nombre de problèmes en raison des règles de conversion de type implicites de C et C ++. Autrement dit, ptrdiff_test utilisé très intentionnellement, pour éviter les problèmes avec size_t. Il faut cependant être conscient que g ++ a un problème avec la correspondance de la taille du tableau au paramètre de modèle à moins que ce ne soit size_t(je ne pense pas que ce problème spécifique au compilateur avec non- size_tsoit important, mais YMMV).
Bravo et hth. - Alf
@Alf. Dans le document de travail standard (N3936) 8.3.4, je lis - La limite d'un tableau est ... "une expression constante convertie de type std :: size_t et sa valeur doit être supérieure à zéro".
Ricky65
@Ricky: Si vous faites référence à l'incohérence, cette déclaration n'est pas là dans la norme C ++ 11 actuelle, il est donc difficile de deviner le contexte, mais la contradiction (un tableau alloué dynamiquement peut être lié à 0, par C + +11 §5.3.4 / 7) ne se terminera probablement pas en C ++ 14. Les brouillons ne sont que cela: des brouillons. Si vous demandez plutôt à quoi «son» fait référence, il se réfère à l'expression originale, pas à celle convertie. Si, d'un côté, vous mentionnez cela parce que vous pensez qu'une telle phrase signifie peut-être que l'on devrait utiliser size_tpour désigner la taille des tableaux, non bien sûr que non.
Bravo et hth. - Alf
72

Création et initialisation de tableaux

Comme avec tout autre type d'objet C ++, les tableaux peuvent être stockés soit directement dans des variables nommées (alors la taille doit être une constante au moment de la compilation; C ++ ne prend pas en charge les VLA ), ou ils peuvent être stockés de manière anonyme sur le tas et accessibles indirectement via pointeurs (ce n'est qu'alors que la taille peut être calculée au moment de l'exécution).

Tableaux automatiques

Les tableaux automatiques (tableaux vivant "sur la pile") sont créés à chaque fois que le flux de contrôle passe par la définition d'une variable de tableau local non statique:

void foo()
{
    int automatic_array[8];
}

L'initialisation est effectuée par ordre croissant. Notez que les valeurs initiales dépendent du type d'élément T:

  • Si Test un POD (commeint dans l'exemple ci-dessus), aucune initialisation n'a lieu.
  • Sinon, le constructeur par défaut de T initialise tous les éléments.
  • Si Tne fournit aucun constructeur par défaut accessible, le programme ne compile pas.

Alternativement, les valeurs initiales peuvent être spécifiées explicitement dans l' initialiseur de tableau , une liste séparée par des virgules entourée de crochets:

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};

Étant donné que dans ce cas, le nombre d'éléments dans l'initialiseur de tableau est égal à la taille du tableau, la spécification manuelle de la taille est redondante. Il peut être déduit automatiquement par le compilateur:

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced

Il est également possible de spécifier la taille et de fournir un initialiseur de tableau plus court:

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced

Dans ce cas, les éléments restants sont initialisés à zéro . Notez que C ++ autorise un initialiseur de tableau vide (tous les éléments sont initialisés à zéro), contrairement à C89 (au moins une valeur est requise). Notez également que les initialiseurs de tableau ne peuvent être utilisés que pour initialiser tableaux; ils ne pourront plus être utilisés ultérieurement dans les travaux.

Tableaux statiques

Les tableaux statiques (tableaux vivant "dans le segment de données") sont des variables de tableau locales définies avec le staticmot clé et les variables de tableau à la portée de l'espace de noms ("variables globales"):

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}

(Notez que les variables à la portée de l'espace de noms sont implicitement statiques. L'ajout du staticmot - clé à leur définition a une signification complètement différente et obsolète .)

Voici comment les tableaux statiques se comportent différemment des tableaux automatiques:

  • Les tableaux statiques sans initialiseur de tableau sont initialisés à zéro avant toute autre initialisation potentielle.
  • Les tableaux POD statiques sont initialisés exactement une fois , et les valeurs initiales sont généralement intégrées dans l'exécutable, auquel cas il n'y a aucun coût d'initialisation à l'exécution. Cependant, ce n'est pas toujours la solution la plus économe en espace et elle n'est pas requise par la norme.
  • Les tableaux statiques non POD sont initialisés la première fois que le flux de contrôle passe par leur définition. Dans le cas de tableaux statiques locaux, cela peut ne jamais se produire si la fonction n'est jamais appelée.

(Rien de ce qui précède n'est spécifique aux tableaux. Ces règles s'appliquent également aux autres types d'objets statiques.)

Membres de données de tableau

Les membres de données de tableau sont créés lorsque leur propre objet est créé. Malheureusement, C ++ 03 ne fournit aucun moyen d'initialiser les tableaux dans la liste d'initialisation des membres , donc l'initialisation doit être simulée avec des affectations:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};

Alternativement, vous pouvez définir un tableau automatique dans le corps du constructeur et copier les éléments sur:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        int local_array[] = {2, 3, 5, 7, 11, 13, 17, 19};
        std::copy(local_array + 0, local_array + 8, primes + 0);
    }
};

En C ++ 0x, les tableaux peuvent être initialisés dans la liste des initialiseurs de membres grâce à une initialisation uniforme :

class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};

C'est la seule solution qui fonctionne avec les types d'éléments qui n'ont pas de constructeur par défaut.

Tableaux dynamiques

Les tableaux dynamiques n'ont pas de nom, donc le seul moyen d'y accéder est via des pointeurs. Parce qu'ils n'ont pas de noms, je les désignerai désormais comme des "tableaux anonymes".

En C, des tableaux anonymes sont créés via mallocet amis. En C ++, les tableaux anonymes sont créés à l'aide de la new T[size]syntaxe qui renvoie un pointeur sur le premier élément d'un tableau anonyme:

std::size_t size = compute_size_at_runtime();
int* p = new int[size];

L'illustration ASCII suivante illustre la disposition de la mémoire si la taille est calculée comme 8 au moment de l'exécution:

             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+

De toute évidence, les tableaux anonymes nécessitent plus de mémoire que les tableaux nommés en raison du pointeur supplémentaire qui doit être stocké séparément. (Il y a aussi des frais généraux supplémentaires sur la boutique gratuite.)

Notez qu'il n'y a pas de décroissance tableau vers pointeur en cours ici. Bien que l'évaluation new int[size]crée en fait un tableau d'entiers, le résultat de l'expression new int[size]est déjà un pointeur vers un seul entier (le premier élément), pas un tableau d'entiers ou un pointeur vers un tableau d'entiers de taille inconnue. Cela serait impossible, car le système de type statique requiert que les tailles de tableau soient des constantes au moment de la compilation. (Par conséquent, je n'ai pas annoté le tableau anonyme avec des informations de type statique dans l'image.)

Concernant les valeurs par défaut des éléments, les tableaux anonymes se comportent de manière similaire aux tableaux automatiques. Normalement, les tableaux POD anonymes ne sont pas initialisés, mais il existe une syntaxe spéciale qui déclenche l'initialisation de la valeur:

int* p = new int[some_computed_size]();

(Notez la paire de parenthèses de fin juste avant le point-virgule.) Encore une fois, C ++ 0x simplifie les règles et permet de spécifier les valeurs initiales des tableaux anonymes grâce à une initialisation uniforme:

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };

Si vous avez terminé d'utiliser un tableau anonyme, vous devez le renvoyer au système:

delete[] p;

Vous devez libérer chaque baie anonyme exactement une fois, puis ne plus la toucher par la suite. Ne pas le libérer du tout entraîne une fuite de mémoire (ou plus généralement, selon le type d'élément, une fuite de ressource), et essayer de le libérer plusieurs fois entraîne un comportement indéfini. L'utilisation du formulaire non-tableau delete(ou free) au lieu de delete[]pour libérer le tableau est également un comportement non défini .

fredoverflow
la source
2
La dépréciation de l' staticutilisation dans la portée de l'espace de noms a été supprimée dans C ++ 11.
legends2k
Parce que newest un opérateur, il pourrait certainement renvoyer le tableau alloué par référence. Ça ne sert à rien ...
Déduplicateur
@Deduplicator Non, il ne pouvait pas, car historiquement, il newest beaucoup plus ancien que les références.
fredoverflow
@FredOverflow: Il y a donc une raison pour laquelle il n'a pas pu retourner une référence, c'est juste complètement différent de l'explication écrite.
Deduplicator
2
@Deduplicator Je ne pense pas qu'il existe une référence à un tableau de bornes inconnues. Au moins g ++ refuse de compilerint a[10]; int (&r)[] = a;
fredoverflow