web-dev-qa-db-fra.com

Comment utiliser des tableaux en C ++?

Les tableaux C++ hérités du 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 ) Par conséquent, lorsque vous lisez du code hérité ou que vous interagissez avec une bibliothèque écrite en C, vous devez avoir une idée précise du fonctionnement des tableaux.

Ce FAQ est divisé en cinq parties:

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

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

Dans le texte suivant, "tableau" signifie "tableau C" et non 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 new et delete comme indiqué ci-dessous est extrêmement dangereuse face aux exceptions, mais c'est le sujet de ne autre FAQ .

(Remarque: il s'agit d'une entrée dans FAQ C++ de Stack Overflow . Si vous souhaitez critiquer l'idée de fournir un FAQ dans ce formulaire, alors l'affichage sur meta qui a commencé tout cela serait l'endroit pour le faire. Les réponses à cette question sont suivies dans le salle de discussion C++ , où le FAQ idée a commencé à la base, donc votre réponse sera très probablement lue par ceux qui en ont eu l'idée.)

458
fredoverflow

Tableaux au niveau du type

Un type de tableau est noté _T[n]_ où T est le type d'élément ​​et n est un positif taille, 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 de ces ingrédients ou les deux 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 tableaux de tailles différentes sont des types incompatibles qui n'ont absolument rien à voir les uns avec les autres. sizeof(T[n]) est équivalent à n * sizeof(T).

Désintégration tableau à 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 sur la premier élément du tableau. C'est-à-dire que 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 tableau à pointeur" et constitue une source de confusion majeure. 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 taille any. 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 l'endroit exact où 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érera en mode silencieux un pointeur sur le premier élément d'un tableau chaque fois qu'il le jugera utile, c'est-à-dire chaque fois qu'une opération échouera sur un tableau mais réussira sur un pointeur. Cette conversion de tableau en pointeur est triviale, car le pointeur résultant valeur est simplement l'adresse du tableau. Notez que le pointeur est not ​​stocké dans le 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 not ​​se désintègre en un pointeur sur son premier élément est lorsque l'opérateur _&_ lui est appliqué. Dans ce cas, l'opérateur _&_ renvoie un pointeur sur le tableau tout, et pas uniquement un pointeur sur son premier élément. Bien que, dans ce cas, les valeurs (les adresses) soient identiques, un pointeur sur le premier élément d'un tableau et un pointeur sur l'ensemble du tableau 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é par une petite case), alors que le pointeur sur le tableau entier pointe sur un tableau de 8 entiers (représenté par une grande case).

La même situation se présente 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 le même valeur (la même adresse), mais ce sont des types complètement distincts.

Si vous ne connaissez pas la syntaxe du déclarateur C, les parenthèses dans le type int(*)[8] sont essentielles:

  • int(*)[8] est un pointeur sur 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 des deux n'est supérieur à l'autre, et vous devriez vous familiariser avec les deux.

Arithmétique de pointeur

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

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

Si x désigne un tablea, la décomposition de point à pointeur commencera, 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 un pointeur et un entier ont un sens:

_   +---+---+---+---+---+---+---+---+
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_ afin de l'identifier.)

Si, au contraire, x désigne un pointeur au premier (ou à tout autre) élément d'un tableau, la désintégration de point à point n'est pas nécessaire car le pointeur sur que i va être ajouté existe déjà:

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

Notez que dans le cas décrit, x est un pointeur variable (visible par la petite case à côté de x), mais il pourrait tout aussi bien être le résultat d'une fonction renvoyer un pointeur (ou toute autre expression de type _T*_).

Opérateur d'indexation

Puisque la syntaxe *(x+i) est un peu maladroite, C++ fournit la syntaxe alternative _x[i]_:

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

Du fait 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]_ est généralement pas équivalent à x. Le premier est un pointeur, le dernier un tableau. Ce n'est que lorsque le contexte déclenche une décroissance de tableau à pointeur que x et _&x[0]_ peuvent être utilisés 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 à un autre, qui réussit trivialement. Sur la deuxième ligne, il détecte une affectation d'un tablea à un pointeur. Étant donné que cela n'a pas de sens (mais pointeur, l'assignation de pointeur a un sens), la désintégration de pointeur à pointeur se déclenche comme d'habitude.

Gammes

Un tableau de type _T[n]_ contient n éléments, indexés de _0_ à _n-1_; il n'y a pas d'é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 sur le (inexistant) Nième élément, 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 second argument, car il est équivalent à &*(x+n), et la sous-expression *(x+n) appelle techniquement comportement non défini en C++. (mais pas en C99).

Notez également que vous pouvez simplement fournir x comme premier argument. C'est un peu trop concis à mon goût et cela rend également la déduction des arguments de modèles un peu plus difficile pour le compilateur, car dans ce cas, le premier argument est un tableau, tandis que le second argument est un pointeur. (Encore une fois, la désintégration de point par point commence.)

290
fredoverflow

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

Tableaux multidimensionnels

La plupart des programmeurs sont familiarisés avec les tableaux multidimensionnels nommés, mais beaucoup ne sont pas conscients du fait qu'un tableau multidimensionnel peut également être créé 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 ci-dessus ne sont que des visualisations utiles. Du point de vue de C++, la mémoire est une séquence d'octets "plate". Les éléments d'un tableau multidimensionnel sont stockés dans un ordre de lignes majeur. Autrement dit, connect_four[0][6] et connect_four[1][0] sont des voisins en mémoire. En fait, connect_four[0][7] et connect_four[1][0] désignent le même élément! Cela signifie que vous pouvez utiliser 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é en tant que bloc unique 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: | | | | | | | | | | |
          +---+---+---+---+---+

Comme chaque ligne est allouée individuellement, la visualisation de tableaux 2D sous forme de tableaux 1D ne fonctionne plus.

Tableaux anonymes de pointeurs

Voici un tableau anonyme de 5 (ou tout autre nombre de) pointeurs 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 point à 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] en T**. Si une telle conversion implicite existait, le résultat serait un pointeur sur le premier élément d'un tableau de h pointeurs sur T (chacun pointant vers le premier élément d'une ligne du tableau 2D d'origine). , mais ce tableau de pointeurs n’existe pas encore dans la mémoire. 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;
132
fredoverflow

Affectation

Pour aucune raison particulière, les tableaux ne peuvent pas être assignés les uns aux autres. Utilisez std::copy à la place:

#include <algorithm>

// ...

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

Ceci est plus flexible que ce que l’assignation de tableau véritable pourrait fournir car il est possible de copier des tranches de tableaux plus grands dans des tableaux plus petits. std::copy est généralement spécialisé pour les types primitifs afin d'optimiser les performances. Il est peu probable que std::memcpy fonctionne mieux. En cas de doute, mesurez.

Bien que vous ne puissiez pas affecter directement des tableaux, vous pouvez affecter des structures et des classes contenant des membres . C'est parce que les membres du tableau sont copiés membre par membre par l'opérateur d'affectation 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 revenir à la copie manuelle pour les membres du groupe.

Passage de paramètre

Les tableaux ne peuvent pas être passés par valeur. Vous pouvez les transmettre par pointeur ou par référence.

Passer par le pointeur

Puisque les tableaux eux-mêmes ne peuvent pas être passés par valeur, un pointeur sur leur premier élément est plutôt passé par valeur. Cela s'appelle souvent "passer par le pointeur". Puisque 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 C++ Itator). :

#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 des paramètres en tant que T p[], ce qui 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 considérer le compilateur comme une réécriture de T p[] vers T *p dans le contexte des listes de paramètres uniquement . Cette règle spéciale est en partie responsable de toute la confusion entourant les tableaux et les pointeurs. Dans tout autre contexte, déclarer quelque chose sous forme de tableau ou de pointeur crée une différence énorme .

Malheureusement, vous pouvez également fournir une taille dans un paramètre de tableau qui est ignorée en silence par le compilateur. Autrement dit, les trois signatures suivantes sont exactement équivalentes, comme indiqué par 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

Pass par référence

Les tableaux peuvent également être passés 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 significative. L'écriture d'une fonction n'acceptant que des tableaux contenant exactement 8 éléments étant peu utile, 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 d'entiers, et non 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 très utile modèles de fonctions qui résument à la fois le type d'élément et la taille.

85
fredoverflow

Création et initialisation de tableaux

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

Tableaux automatiques

Des tableaux automatiques (des 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 dans l'ordre croissant. Notez que les valeurs initiales dépendent du type d'élément T:

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

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

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

Comme 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 automatiquement déduit 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é à 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 tableaux ne peuvent être utilisés que pour initialiser des tableaux; ils ne peuvent plus être utilisés dans des assignations.

Tableaux statiques

Les tableaux statiques (tableaux vivant "dans le segment de données") sont des variables de tableau locales définies avec le mot clé static et des variables de tableau au niveau de l'espace de noms ("variables globales"):

int global_static_array[8];

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

(Notez que les variables de la portée de l'espace de noms sont implicitement statiques. L'ajout du mot clé static à leur définition a un = signification complètement différente et déconseillée .

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

  • Les matrices statiques sans initialiseur de matrice sont initialisées à zéro avant toute autre initialisation potentielle.
  • Les matrices de POD statiques sont initialisées exactement une fois et les valeurs initiales sont en général cuit dans l'exécutable, auquel cas il n'y a pas de coût d'initialisation au moment de l'exécution. Cependant, cette solution n’est pas toujours la plus économe en espace et elle n’est pas exigée par la norme.
  • Les tableaux statiques non-POD sont initialisés la première fois , le flux de contrôle passe par leur définition. Dans le cas de tableaux statiques locaux, cela peut ne jamais arriver si la fonction n'est jamais appelée.

(Aucune de ce qui précède n’est spécifique aux tableaux. Ces règles s’appliquent aussi bien à d’autres types d’objets statiques.)

Membres de données de tableau

Les membres de données de groupe sont créés lors de la création de leur objet propriétaire. Malheureusement, C++ 03 ne fournit aucun moyen d'initialiser les tableaux de la liste liste d'initialisation des membres , l'initialisation doit donc être falsifiée avec les affectations suivantes:

class Foo
{
    int primes[8];

public:

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

Vous pouvez également 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 d'initialisation des membres grâce à 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 des types d'élément sans constructeur par défaut.

Tableaux dynamiques

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

En C, les tableaux anonymes sont créés via malloc et leurs amis. En C++, les tableaux anonymes sont créés à l'aide de la syntaxe new T[size], 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'art ASCII suivant décrit la structure de la mémoire si la taille est calculée à 8 au moment de l'exécution:

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

Évidemment, 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 quelques frais supplémentaires sur le magasin gratuit.)

Notez qu'il n'y a pas de désintégration de tableau à pointeur . Bien que l’évaluation de 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 sur un seul entier (le premier élément), pas un tableau d'entiers ou un pointeur sur un tableau d'entiers de taille inconnue. Cela serait impossible, car le système de types statiques requiert que les tailles de tableau soient des constantes à la compilation. (Par conséquent, je n'ai pas annoté le tableau anonyme avec des informations de type statique dans l'image.)

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

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

(Notez la dernière paire de parenthèses 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 à l'initialisation uniforme:

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

Si vous avez fini d'utiliser un tableau anonyme, vous devez le relâcher sur le système:

delete[] p;

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

70
fredoverflow

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

5.1 Piège: faire confiance aux types de liaisons non sécurisées.

OK, on ​​vous a dit ou vous-même vous êtes rendu compte que les globales (variables de portée d’espace de nommage accessibles en dehors de l’unité de traduction) sont maléfiques. Mais saviez-vous à quel point ils sont vraiment Evil ™? 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};
_

Sous Windows 7, cela compile et relie très bien avec MinGW g ++ 4.4.1 et Visual C++ 10.0.

Comme les types ne correspondent pas, le programme se bloque lorsque vous l'exécutez.

The Windows 7 crash dialog

Explication formelle: le programme a un comportement indéfini (UB) et, au lieu de s’effondrer, il peut simplement bloquer, ou peut-être ne rien faire, ou envoyer des courriels menaçants aux présidents des États-Unis, de la Russie, de l'Inde, Chine et la Suisse, et faites fuir le démon nasal par le nez.

Explication pratique: dans _main.cpp_, le 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 valeur int du tableau est traitée comme un pointeur. En d'autres termes, dans _main.cpp_, la variable numbers contient ou semble contenir _(int*)1_. Ainsi, le programme accède à la mémoire au bas de l’espace adresse, ce qui est généralement réservé et cause des interruptions. Résultat: vous obtenez un crash.

Les compilateurs ont pleinement le droit de ne pas diagnostiquer cette erreur, car C++ 11 §3.5/10 indique, à 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 pour un objet tableau peuvent spécifier des types de tableau qui diffèrent par la présence ou l'absence d'un tableau majeur lié (8.3.4).

Cette variante autorisée n'inclut pas la déclaration d'un nom sous la forme d'un tableau dans une unité de traduction et d'un pointeur dans une autre unité de traduction.

5.2 Piège: optimisation prématurée (memset & 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] ))
_

Etant donné qu'une array se désintègre au besoin en pointeur vers le premier élément, l'expression sizeof(a)/sizeof(a[0]) peut également être écrite sous la forme sizeof(a)/sizeof(*a). Cela signifie la même chose, et peu importe la façon dont il est écrit, c’est l’idiome C permettant de trouver le nombre d’éléments de array.

Piège principal: l'idiome C n'est pas typé. 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 sur _N_ITEMS_ et produit donc très probablement un résultat erroné. Compilé en tant qu’exécutable 32 bits dans Windows 7, il produit…

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

  1. Le compilateur réécrit _int const a[7]_ en seulement _int const a[]_.
  2. Le compilateur réécrit _int const a[]_ sur _int const* a_.
  3. _N_ITEMS_ est donc appelé avec un pointeur.
  4. Pour un exécutable 32 bits, sizeof(array) (taille d'un pointeur) est alors égal à 4.
  5. sizeof(*array) est équivalent à sizeof(int), ce qui correspond également à 4 pour un exécutable 32 bits.

Pour 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, affichage appelant ...
Échec de l'assertion: ("" N_ITEMS nécessite un tableau comme argument ", typeid (a)! = Typeid (& * a)), fichier runtime_detect ion.cpp, ligne 16

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

La détection des erreurs d’exécution est meilleure qu’aucune détection, mais elle gaspille un peu de temps processeur et peut-être beaucoup plus de temps de programmation. 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 )
_

Compiler cette définition substituée dans le premier programme complet, avec g ++, j’ai…

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

M:\count> _

Comment ça marche: le tableau est passé par référence à _n_items_, et donc il ne se décompose pas en pointeur vers le premier élément, et la fonction peut simplement renvoyer le nombre de éléments spécifiés par le type.

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

5.4 Piège C++ 11 & C++ 14: Utilisation de la fonction constexpr de la taille d'un tableau.

Avec C++ 11 et les versions ultérieures, il est naturel, mais comme vous le verrez dangereux, de 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érons ce code en utilisant la version constexpr:

_// 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 est compilé avec MinGW-64 5.1.0 avec _-pedantic-errors_ et testé avec les compilateurs en ligne sur 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 de références dans les expressions constexpr:

th

Une expression conditionnelle e est une expression constante sauf si l'évaluation de e, suivant le règles de la machine abstraite (1.9), évaluerait l'une des expressions suivantes:

  • une expression-id 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 soit
    • il est initialisé avec une expression constante ou
    • il s'agit d'un membre de données non statique d'un objet dont la vie a commencé au cours de l'évaluation de e;

On peut toujours écrire le plus prolixe

_// 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 lorsque Collection n'est pas un tableau brut.

Pour traiter des collections qui peuvent ne pas être des tableaux, vous avez besoin de la capacité de surcharge d'une fonction _n_items_, mais vous devez également disposer d'une représentation de la taille du tableau lors de la compilation. Et la solution classique C++ 03, qui fonctionne très bien aussi en C++ 11 et C++ 14, consiste à laisser la fonction signaler son résultat non pas sous forme de valeur, mais via sa fonction result type . 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_constant_ le résultat est directement représenté sous la forme d’une valeur constexpr, ce qui réintroduit le problème initial. Au lieu d'une classe _Size_carrier_, on peut laisser la fonction renvoyer 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 au problème de la variable constexpr- non valide par suite de la référence consiste à rendre explicite le choix de la constante de temps de compilation.

Espérons que le problème oops-there-was-a-a-reference-impliqué-dans-your -constexpr sera corrigé avec C++ 17, mais jusque-là, une macro telle que _STATIC_N_ITEMS_ ci-dessus donne la portabilité, par exemple. aux compilateurs clang et Visual C++, en conservant la sécurité de type.

Connexes: les macros ne respectent pas les portées. Par conséquent, pour éviter les conflits de noms, il peut être judicieux d’utiliser un préfixe de nom, par exemple. _MYLIB_STATIC_N_ITEMS_.

69