Question Comment utiliser des tableaux en C ++?


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), le besoin de tableaux ne se pose pas aussi souvent qu'en C. Cependant, lorsque vous lisez un code existant ou interagissez avec une bibliothèque écrite en C, vous devez bien comprendre comment fonctionnent les tableaux.

Cette FAQ est divisée en cinq parties:

  1. tableaux sur le niveau de type et l'accès aux éléments
  2. création et initialisation de tableau
  3. affectation et passage de paramètre
  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. La connaissance de base de la syntaxe du déclarant C est supposée. Notez que l'utilisation manuelle de new et delete comme démontré ci-dessous est extrêmement dangereux face à des exceptions, mais c'est le sujet de une autre FAQ.

(Note: Ceci est censé être une entrée pour FAQ C ++ de Stack Overflow. Si vous voulez critiquer l'idée de fournir une 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 Chatroom C ++, où l'idée de FAQ a commencé en premier lieu, ainsi votre réponse est très susceptible d'être lue par ceux qui ont eu l'idée.)


426
2018-01-26 22:14


origine


Réponses:


Tableaux au niveau du type

Un type de tableau est noté T[n] où T est le type d'élément et n est 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 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 tableau à pointeur

La seule "connexion" entre T[n] et T[m] est que les deux types peuvent implicitement être converti à T*et le résultat de cette conversion est un pointeur vers le premier élément du tableau. C'est, n'importe où un T* est nécessaire, 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 tableau à pointeur" et constitue une source majeure de confusion. La taille du tableau est perdue dans ce processus, puisqu'il ne fait plus partie du type (T*). Pro: L'oubli de la taille d'un tableau au niveau du type permet à un pointeur de pointer vers le premier élément d'un tableau de tout Taille. Con: Étant donné le pointeur sur le premier (ou tout autre) élément d'un tableau, il est impossible de détecter la taille de ce tableau ou l'emplacement exact du pointeur 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 sur 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 le pointeur résultant valeur est simplement l'adresse du tableau. Notez que le pointeur est ne pas 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 fonctionne ne pas la décomposition en un pointeur vers son premier élément est quand le & l'opérateur lui est appliqué. Dans ce cas, le & l'opérateur cède un pointeur vers le tout tableau, pas seulement un pointeur vers son premier élément. Bien que dans ce cas le valeurs (les adresses) sont les mêmes, un pointeur sur le premier élément d'un tableau et un pointeur sur 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 vers le premier élément pointe uniquement vers un seul entier (représenté par une petite case), alors que le pointeur vers le tableau entier pointe vers un tableau de 8 entiers (représenté par une grande boîte).

La même situation se présente dans les classes et est peut-être plus évidente. Un pointeur vers un objet et un pointeur vers 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 n'êtes pas familier avec la syntaxe du déclarant C, la parenthèse dans le type int(*)[8] sont essentiels:

  • 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ès aux éléments

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

Arithmétique du pointeur

Donné un pointeur p au premier élément d'un tableau, l'expression p+i renvoie un pointeur sur le iième élément du tableau. En déréférencant ensuite ce pointeur, on peut accéder à des éléments individuels:

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

Si x dénote une tableau, alors la désintégration 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 ajouter un pointeur et 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 afin de l'identifier.)

Si, d'autre part, x dénote un aiguille au premier (ou à tout autre) élément d'un tableau, alors la désintégration tableau-à-pointeur n'est pas nécessaire, car le pointeur sur lequel i va être ajouté existe déjà:

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

Notez que dans le cas représenté, x est un pointeur variable (discernable par la petite boîte à côté de x), mais cela pourrait tout aussi bien être le résultat d'une fonction renvoyant un pointeur (ou toute autre expression de type T*).

Opérateur d'indexation

Depuis la syntaxe *(x+i) est un peu maladroit, 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] est généralement ne pas équivalent à x. Le premier est un pointeur, le second un tableau. Ce n'est que lorsque le contexte déclenche la désintégration du tableau vers le pointeur x et &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 à un pointeur, qui réussit trivialement. Sur la deuxième ligne, il détecte une cession d'un tableau à un pointeur. Puisque cela n'a pas de sens (mais aiguille l'attribution du pointeur prend tout son sens), le déclin de tableau à pointeur se déclenche comme d'habitude.

Gammes

Un tableau de type T[n] a n éléments, indexés à partir de 0 à n-1; il n'y a pas d'élément n. Et pourtant, pour soutenir les gammes semi-ouvertes (où le début est compris et la fin est exclusif), C ++ permet le calcul d'un pointeur vers le n-iè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 fonctionneraient é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 puisque cela équivaut à &*(x+n)et la sous-expression *(x+n) appelle techniquement comportement indéfini en C ++ (mais pas en C99).

Notez également que vous pouvez simplement fournir x comme le premier argument. C'est un peu trop compliqué à mon goût, et cela rend aussi la déduction d'argument de modèle un peu plus difficile pour le compilateur, car dans ce cas le premier argument est un tableau mais le second argument est un pointeur. (Encore une fois, la désintégration de tableau à pointeur entre en jeu.)


265
2018-01-26 22:14



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

Tableaux multidimensionnels

La plupart des programmeurs sont familiers avec les tableaux multidimensionnels nommés, mais nombreux sont ceux qui ignorent que les tableaux multidimensionnels peuvent également être créés de manière anonyme. Les tableaux multidimensionnels sont souvent appelés "tableaux de tableaux" ou "vrai tableaux multidimensionnels ".

Tableaux multidimensionnels nommés

Lors de l'utilisation de tableaux multidimensionnels nommés, tout 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 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 row-major. C'est, connect_four[0][6] et connect_four[1][0] sont des voisins en mémoire. En réalité, connect_four[0][7] et connect_four[1][0] dénote 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 des tableaux multidimensionnels anonymes, toutes les dimensions sauf le premier doit être connu 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 ça ressemble en mémoire:

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

Puisque chaque ligne est allouée individuellement maintenant, l'affichage de tableaux 2D sous la 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 ça ressemble en mémoire:

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

Conversions

La désintégration Array-to-Pointter 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 sur le premier élément d'un tableau de h pointeurs vers T (chacun pointant vers le premier élément d'une ligne dans le 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 le tableau de pointeurs requis manuellement:

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 besoin d'une copie à la place, 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;

127
2018-01-26 22:15



Affectation

Sans raison particulière, les tableaux ne peuvent pas être assignés les uns aux autres. Utilisation std::copy au lieu:

#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 la véritable assignation de tableau pourrait fournir car il est possible de copier des tranches de plus grands tableaux dans des tableaux plus petits. std::copy est généralement spécialisé pour les types primitifs pour donner une performance maximale. Il est peu probable que std::memcpy fonctionne mieux. En cas de doute, mesurez.

Bien que vous ne puissiez pas affecter des tableaux directement, vous pouvez affecter des structures et des classes qui contenir membres du tableau. C'est parce que les membres du tableau sont copiés 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 struct ou class, vous devez revenir à la copie manuelle pour les membres du groupe.

Paramètre passant

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

Passer par le pointeur

Puisque les tableaux eux-mêmes ne peuvent pas être transmis par valeur, un pointeur vers leur premier élément est généralement passé par valeur à la place. Ceci est souvent appelé "passer par le pointeur". Puisque la taille du tableau n'est pas récupérable via ce pointeur, vous devez passer un second paramètre indiquant la taille du tableau (la solution C classique) ou un second pointeur après le dernier élément du tableau (la solution d'itération 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 des paramètres comme 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 au compilateur comme réécriture T p[] à 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 é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 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

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 significative. Puisque l'écriture d'une fonction qui n'accepte que des tableaux de 8 éléments est 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 pouvez seulement appeler un tel modèle de fonction avec un tableau réel d'entiers, pas avec un pointeur vers 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 aussi écrire plutôt utile des modèles de fonctions abstraits à la fois du type d'élément et de la taille.


82
2018-01-26 22:15



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 de compilation; C ++ ne supporte pas les VLA), ou ils peuvent être stockés anonymement sur le tas et accédés indirectement via des pointeurs (alors seulement la taille peut-elle ê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 traverse 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 COSSE (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 spécifiées explicitement dans le 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};

Puisque dans ce cas le nombre d'éléments dans l'initialiseur de tableau est égal à la taille du tableau, spécifier la taille manuellement est redondant. 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), alors que C89 ne l'est pas (au moins une valeur est requise). Notez également que les initialiseurs de tableau ne peuvent être utilisés que pour initialiser des tableaux; ils ne peuvent plus être utilisés plus tard dans les missions.

Tableaux statiques

Les tableaux statiques (tableaux vivant dans le segment de données) sont des variables de tableau local définies avec le static variables de mots clés et de tableaux dans 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 de la portée de l’espace de noms sont implicitement statiques. Ajouter le static mot-clé à leur définition a un sens complètement différent, désapprouvé.)

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 initialisation potentielle.
  • Les tableaux POD statiques sont initialisés exactement une fois, et les valeurs initiales sont typiquement cuit dans l'exécutable, auquel cas il n'y a pas de coût d'initialisation à l'exécution. Ce n'est pas toujours la solution la plus efficace en termes d'espace, et elle n'est pas requise par la norme.
  • Les tableaux statiques non-POD sont initialisés 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.

(Aucun des éléments ci-dessus 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 objet propriétaire est créé. Malheureusement, C ++ 03 ne permet pas d’initialiser les tableaux dans le liste des initialiseurs de membres, l'initialisation doit donc être simulée avec des affectations:

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, tableaux pouvez être initialisé 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 les types d'élément sans constructeur par défaut.

Tableaux dynamiques

Les tableaux dynamiques n'ont pas de nom, de sorte que le seul moyen d'y accéder est d'utiliser des pointeurs. Comme 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 malloc et amis. En C ++, les tableaux anonymes sont créés en utilisant le 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'art ASCII suivant décrit la disposition de la mémoire si la taille est calculée en 8 à 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 le magasin gratuit.)

Notez qu'il y a non désintégration tableau à pointeur se passe ici. Bien que l'évaluation new int[size] crée en fait un tableau des entiers, le résultat de l'expression new int[size] est déjà un pointeur vers un seul entier (le premier élément), ne pas un tableau d'entiers ou un pointeur vers un tableau d'entiers de taille inconnue. Ce serait impossible, car le système de type statique nécessite que les tailles de tableau soient des constantes de 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 la même manière que les tableaux automatiques. Normalement, les tableaux POD anonymes ne sont pas initialisés, mais il y a un syntaxe spéciale qui déclenche l'initialisation de la valeur:

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

(Notez la paire de parenthèses à droite juste avant le point-virgule.) Là encore, C ++ 0x simplifie les règles et permet de spécifier des valeurs initiales pour des tableaux anonymes grâce à une 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 libérer dans le système:

delete[] p;

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


68
2018-02-13 12:52



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

5.1 Piège: Faire confiance au type de liaison non sécurisé.

OK, vous avez été dit, ou avez découvert vous-même, que globals (namespace les variables de portée accessibles en dehors de l'unité de traduction) sont Evil ™. Mais saviez-vous à quel point ils sont méchants? Prendre en compte 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 compile et relie 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 à la place de s'écraser, il peut donc simplement accrocher, ou peut-être ne rien faire, ou il peut envoyer des courriers électroniques aux présidents des États-Unis, de la Russie, de l'Inde, La Chine et la Suisse, et faites voler les démons nasaux.

Explication en pratique: in main.cpp le tableau est traité comme un pointeur, placé à la même adresse que le tableau. Pour 32 bits exécutable cela signifie que le premier int valeur dans le tableau, est traité comme un pointeur. I.e., dans main.cpp la numbers variable contient, ou semble contenir, (int*)1. Cela provoque la programme pour accéder à la mémoire au bas de l'espace d'adressage, qui est classiquement réservé et causant des pièges. Résultat: vous obtenez un crash.

Les compilateurs sont pleinement dans leurs droits de ne pas diagnostiquer cette erreur, 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 pour un objet tableau peuvent spécifier des types de tableau qui   diffèrent par la présence ou l'absence d'un tableau principal lié (8.3.4).

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

5.2 Piège: Faire une optimisation prématurée (memset & copains).

Pas encore écrit

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

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

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

Depuis un an array se désintègre pour pointer vers le premier élément si nécessaire, le expression sizeof(a)/sizeof(a[0]) peut aussi être écrit comme sizeof(a)/sizeof(*a). Cela signifie la même chose, et peu importe comment écrit c'est le C idiome pour trouver les éléments de nombre de tableau.

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 vers N_ITEMS, et donc très probablement produit un tort résultat. Compilé en tant qu'exécutable 32 bits dans Windows 7, il produit ...

7 éléments, affichage d'appel ...
  1 éléments

  1. Le compilateur réécrit int const a[7] pour juste int const a[].
  2. Le compilateur réécrit int const a[] à 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 4.
  5. sizeof(*array) est équivalent à sizeof(int), qui pour un exécutable 32 bits est également 4.

Afin de détecter cette erreur lors de l'exécution, vous pouvez 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 d'appel ...
  É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 le 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 qu’aucune détection, mais elle gaspille un peu temps de processeur, et peut-être beaucoup plus de temps de programmation. Mieux avec la détection à compilez le temps! Et si vous êtes heureux de ne pas supporter les tableaux de types locaux avec C ++ 98, alors 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 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 ça marche: le tableau est passé par référence à n_items, et il le fait ne pas se désintégrer pour pointer vers le premier élément, et la fonction peut simplement renvoyer le nombre d'éléments spécifiés par le type.

Avec C ++ 11, vous pouvez également l’utiliser 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 d'un constexpr fonction de taille de tableau.

Avec C ++ 11 et plus tard, il est naturel, mais comme vous le verrez dangereux! 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 important est l'utilisation de constexpr, ce qui permet cette fonction pour produire une compiler la constante de temps.

Par exemple, contrairement à la fonction C ++ 03, une telle constante de temps de compilation peut être utilisé 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 le constexpr version:

// 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 compile avec MinGW-64 5.1.0 avec -pedantic-errors, et, tester avec les compilateurs en ligne à gcc.godbolt.org/, aussi 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 est une instruction C ++ 11 / C ++ 14 sur l'utilisation de références en constexprexpressions:

C ++ 11 C ++ 14 5,19 $ / 2 neufth tiret

UNE expression conditionnelle  e est un expression constante de base à moins que l'évaluation   de e, en suivant les règles de la machine abstraite (1.9), évaluerait l'un des   expressions suivantes:
  ⋮

  • un id-expression qui fait référence à une variable ou à un membre de données de type référence   à moins que la référence ait une initialisation précédente et soit      
    • il est initialisé avec une expression constante ou
    • c'est un membre de données non statique d'un objet dont la durée de vie a commencé dans   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 n'est pas un tableau brut.

Pour traiter des collections qui peuvent être non-arrays on a besoin de la surcharge d'un n_items fonction, mais aussi, pour l'utilisation de la compilation, on a besoin d'un temps de compilation représentation de la taille du tableau. Et la solution classique C ++ 03, qui fonctionne bien également en C ++ 11 et C ++ 14, est de laisser la fonction signaler son résultat pas comme une valeur mais via son résultat de fonction 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 parce qu'avec std::integral_constant le résultat est représenté directement en tant que constexpr valeur, réintroduisant le problème d'origine. Au lieu d'un Size_carrier la première classe peut laisser la fonction retourner directement un référence à un tableau. Cependant, tout le monde n'est pas familier avec cette syntaxe.

A propos de la dénomination: une partie de cette solution au constexpr-invalide-en raison de la référence Le problème est de rendre le choix de la constante de compilation explicite.

Espérons que l'oops-there-was-reference-impliqué-in-your-constexpr question sera fixée avec C ++ 17, mais jusque-là une macro comme la STATIC_N_ITEMS ci-dessus donne la portabilité, par exemple. à la clang et Visual C ++ compilateurs, en conservant la sécurité de type.

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


64
2017-09-16 01:31