Comment puis-je utiliser des tableaux en C++?

C++ a hérité des tableaux de C où ils sont utilisés pratiquement partout. C++ fournit des abstractions qui sont plus faciles à utiliser et moins sujettes aux erreurs ( std::vector<T> depuis C++98 et std::array<T, n> depuis C++11 ), de sorte que le besoin de tableaux ne se pose pas aussi souvent qu'il le fait en C. Cependant, lorsque vous lisez le code d'héritage ou interagissez avec une bibliothèque écrite en C, vous devriez avoir une bonne compréhension de la façon dont les tableaux fonctionnent.

cette FAQ est divisé en cinq parties:

  1. tableaux au niveau du type et éléments d'accès
  2. création et initialisation de tableaux
  3. "1519230920 cession et la transmission 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 comme une partie supplémentaire.

dans le texte suivant," array "signifie" C array", et non le modèle de classe std::array . Les connaissances de base de la syntaxe du déclarant C sont supposées. Notez que l'usage manuel de new et delete comme démontré ci-dessous est extrêmement dangereux face aux exceptions, mais c'est le sujet de un autre FAQ .

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

431
demandé sur Community 2011-01-27 01:14:35

5 réponses

tableaux au niveau du type

Un type de pile est indiquée comme T[n]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, les types de tableaux de différentes tailles sont des types incompatibles qui n'ont absolument rien à voir les uns avec les autres. sizeof(T[n]) est l'équivalent de n * sizeof(T) .

Decay-to-pointter decay

la seule" connexion "entre T[n] et T[m] est que les deux types peuvent implicitement être converti en T* , et le résultat de cette conversion est un pointeur vers le premier élément du tableau. C'est, 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 d'un tableau à un pointeur", et c'est 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 sur le type de niveau permet un pointeur sur le premier élément d'un tableau de n'importe quelle taille . Con: étant donné un pointeur vers le premier (ou n'importe quel 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 matrices ne sont pas des pointeurs

le compilateur générera silencieusement un pointeur vers le premier élément d'un tableau chaque fois qu'il est jugé utile, c'est-à-dire chaque fois qu'un opération échoue sur un tableau, mais réussir sur un pointeur. Cette conversion de tableau en pointeur est triviale, puisque le pointeur résultant valeur est simplement l'adresse du tableau. Notez que le pointeur est et non stocké dans le tableau lui-même (ou n'importe où 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 pas décroît en un pointeur à son premier élément est quand l'opérateur & est appliqué à lui. Dans ce cas, l'opérateur & fournit un pointeur vers le tableau entier , et pas seulement un pointeur vers son premier élément. Bien que dans ce cas les valeurs (les adresses) sont les mêmes, un pointeur vers le premier élément d'un tableau et un pointeur vers 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]

noter comment le pointeur vers le premier élément pointe seulement vers un seul entier (représenté comme une petite boîte), tandis que le pointeur vers le tableau entier pointe vers un tableau de 8 entiers (représenté comme 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 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 du 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* .

éléments D'accès

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

arithmétique Pointer

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

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

si x dénote un array , alors la désintégration d'un tableau à un autre pointera, parce que l'ajout d'un tableau et d'un nombre 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 nombre entier a du sens:

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

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

si, par contre, x dénote un pointeur vers le premier (ou tout autre) élément d'un tableau, alors la désintégration de tableau à pointeur n'est pas nécessaire, parce que 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 il pourrait tout aussi bien être le résultat d'une fonction retournant un pointeur (ou toute autre expression du 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 et non équivalent à x . Le premier est un pointeur, ce dernier tableau. Ce n'est que lorsque le contexte déclenche la désintégration d'un tableau à un 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 vers un pointeur, ce qui trivialement réussit. Sur la deuxième ligne, il détecte une affectation d'un tableau à un pointeur. Puisque cela n'a pas de sens (mais pointeur à l'assignation de pointeur a du sens), la désintégration de tableau à pointeur se déclenche comme d'habitude.

"1519650920 les" Fourchettes

un tableau de type T[n] a n éléments, indexés de 0 à n-1 ; il n'y a aucun élément n . Et pourtant, pour soutenir demi-gammes ouvertes (où le début est inclusive et la fin est exclusive ), C++ permet le calcul d'un pointeur vers l'élément n-th (inexistant), mais il est illégal de déréférence que pointeur:

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

par exemple, si vous voulez trier un tableau, les deux 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 second argument car cela équivaut à &*(x+n) , et la sous-expression *(x+n) évoque techniquement comportement non défini en C++ (mais pas en C99).

notez aussi que vous pouvez simplement fournir x comme premier argument. C'est un peu trop court à mon goût, et cela rend aussi la déduction des arguments de template un peu plus difficile pour le compilateur, parce que dans ce cas le premier argument est un tableau mais le second argument est un pointeur. (Encore une fois, la décroissance de tableau-à-pointer se déclenche dans.)

267
répondu fredoverflow 2017-05-23 12:26:24
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 beaucoup ignorent le fait que les tableaux multidimensionnels peuvent aussi être créés anonymement. Les tableaux multidimensionnels sont souvent désignés comme des "tableaux de tableaux" ou " vrai tableaux multidimensionnels".

nommé tableaux multidimensionnels

Lorsqu'on utilise 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

C'est comment un nom de tableau multidimensionnel ressemble à la mémoire:

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

notez que les grilles 2D telles que celles ci-dessus ne sont que des visualisations utiles. Du point de vue de C++, la mémoire est une séquence "plate" d'octets. Les éléments d'une approche multidimensionnelle les tableaux sont stockés dans l'ordre ligne-majeur. C'est-à-dire, connect_four[0][6] et connect_four[1][0] sont des voisins dans la mémoire. En fait, connect_four[0][7] et connect_four[1][0] dénotent 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);

Anonyme tableaux multidimensionnels

avec des 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

C'est comment un anonyme, un tableau multidimensionnel ressemble à la mémoire:

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

notez que le tableau lui-même est encore attribué en bloc en mémoire.

tableaux de pointeurs

vous pouvez surmonter la restriction de la largeur fixe en introduisant un autre niveau d'indirecte.

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 comment il ressemble dans la mémoire:

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

puisque chaque ligne est attribuée individuellement maintenant, regarder des tableaux 2D comme des 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 comment il ressemble dans la mémoire:

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

Conversions

Tableau de pointeur de la décroissance implique naturellement des tableaux de tableaux et des 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 vers le premier élément d'un tableau de h pointeurs T (chacun pointant vers le premier élément d'une ligne dans l'original tableau 2D), mais le pointeur de tableau n'existe nulle part dans la mémoire encore. Si vous voulez une telle conversion, vous devez créer et remplir le tableau de pointeur 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 de l'original tableau multidimensionnel. Si vous avez besoin d'une copie à la place, vous devez créer tableaux supplémentaires et copiez 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;
128
répondu fredoverflow 2012-05-28 09:20:29

cession

sans 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);

c'est plus flexible que ce que true array assignment pourrait fournir parce qu'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 pour donner une performance maximale. Il est peu probable que std::memcpy donne de meilleurs résultats. En cas de doute, de mesure.

bien que vous ne puissiez pas assigner des tableaux directement, vous pouvez assigner des structures et des classes qui contiennent membres de tableau. Cela est dû au fait que les membres du tableau sont copiés memberwise par l'opérateur de tâche qui est fourni par défaut par le compilateur. Si vous définissez l'opérateur d'affectation manuellement pour votre propre structure ou types de classe, vous devez revenir à la reproduction manuelle pour les membres du groupe.

passage du paramètre

Les tableaux

ne peuvent pas être passés en valeur. Vous pouvez passer soit par pointeur ou par référence.

Passage par pointeur

puisque les tableaux eux-mêmes ne peuvent pas être passés par la valeur, habituellement un pointeur vers leur premier élément est passé par la valeur à la place. Cela est souvent appelé "passer par le pointeur". Depuis, la taille du tableau n'est pas récupérable via le pointeur, vous devez passer un deuxième paramètre indiquant la taille du tableau (la solution c classique) ou un second pointeur pointant après le dernier élément du tableau (la solution itératrice 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 comme T p[] , et cela signifie exactement la même chose que T* p dans le contexte des listes de paramètres seulement :

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

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

malheureusement, vous pouvez également fournir une taille dans un paramètre de tableau qui est silencieusement ignoré par le compilateur. Qui est, les trois signatures sont exactement équivalents, 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

Passage par référence

Les tableaux

peuvent aussi ê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. Puisque l'écriture d'une fonction qui n'accepte que des tableaux d'exactement 8 éléments est de peu d'utilité, les programmeurs écrivent habituellement des fonctions telles que des gabarits:

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 avec un tableau d'entiers, non 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 très utile modèles de fonction qui abstrait à la fois du type d'élément et de la taille.

82
répondu fredoverflow 2017-05-23 11:47:20

création et initialisation de tableaux

comme pour 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 temps de compilation; C++ ne supporte pas VLAs ), soit ils peuvent être stockés anonymement sur le tas et accessibles indirectement via des pointeurs (seulement alors la taille peut être calculée à l'exécution).

"1519330920 Automatique" tableaux

automatique les tableaux (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 ascendant. 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 spécifiées explicitement dans le tableau initialiseur , une liste séparée par des virgules entourée de crochets bouclés:

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

puisque dans ce cas-ci le le nombre d'éléments dans le tableau initialiseur est égal à la taille du tableau, en spécifiant la taille manuellement est redondant. 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 tableau plus court initialiseur:

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

dans ce cas, les éléments restants sont initialisé à zéro . A noter que C++ permet à un tableau vide de l'initialiseur (tous les éléments sont zéro-initialisé), alors que C89 ne l'est pas (au moins une valeur est requise). Notez également que les initialiseurs de tableaux ne peuvent être utilisés que pour les tableaux initialiser ; ils ne peuvent pas être utilisés plus tard dans les assignations.

tableaux statiques

les tableaux statiques (tableaux vivants "dans le segment de données") sont des variables de tableaux locales définies avec les variables de mots-clés et de tableaux static à 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. En ajoutant le mot-clé static à leur définition, on obtient un qui est complètement différent et qui signifie .)

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

  • les réseaux statiques sans initialiseur de réseau sont initialisés à zéro avant toute autre initialisation de potentiel.
  • POD statique les tableaux sont initialisés exactement une fois , et les valeurs initiales sont typiquement dans l'exécutable, auquel cas il n'y a pas de coût d'initialisation à l'exécution. Toutefois, il ne s'agit pas toujours de la solution la plus efficace en termes d'espace et elle n'est pas exigée par la norme.
  • les matrices statiques non portatives sont initialisées la première fois le flux de commande passe par leur définition. Dans le cas de les tableaux statiques locaux, qui peuvent ne jamais se produire si la fonction n'est jamais appelée.

(aucune de ces réponses n'est spécifique aux tableaux. Ces règles s'appliquent également à d'autres types d'objets statiques.)

Tableau de données des membres de la

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

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 initialisateurs membres grâce à uniform initialization :

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 que 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 par des pointeurs. Comme ils n'ont pas de nom, je les appellerai désormais "tableaux anonymes".

en C, des tableaux anonymes sont créés via malloc et des amis. Dans C++, les tableaux anonymes sont créés en utilisant la syntaxe new T[size] qui renvoie un pointeur vers le premier élément d'un anonyme array:

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

l'art ASCII suivant représente la disposition de la mémoire si la taille est calculée comme 8 à 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 des frais généraux supplémentaires sur le magasin libre.)

notez qu'il y a no la désintégration d'un tableau à un pointeur se produit ici. Bien l'évaluation de new int[size] permet en fait de créer un array des nombres entiers, le résultat de l'expression new int[size] est déjà un pointeur vers un 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 exige que les tailles des tableaux soient des constantes de temps de compilation. (Par conséquent, je n'ai pas annoté le tableau anonyme avec 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 comme des tableaux automatiques. Normalement, les tableaux de pods anonymes ne sont pas initialisés, mais il y a une syntaxe spéciale qui déclenche la valeur-initialisation:

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

(notez la paire suivante 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 pour tableaux anonymes grâce à une initialisation uniforme:

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

si vous utilisez un tableau anonyme, vous devez le relâcher dans le système:

delete[] p;

vous devez libérer chaque tableau anonyme exactement une fois et puis Ne jamais le toucher à nouveau après. Le fait de 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 le fait d'essayer de le libérer plusieurs fois entraîne une comportement. En utilisant la forme de non-tableau delete (ou free ) au lieu de delete[] pour libérer le tableau est également comportement non défini .

68
répondu fredoverflow 2017-05-23 11:54:50

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

5.1 Pitfall: Type de confiance-lien dangereux.

OK, on vous a dit, ou vous avez découvert vous - même, que globals (namespace) portée des variables qui peuvent être accessibles à l'extérieur de l'unité de traduction) sont Mal.™ Mais saviez-vous à quel point ils sont diaboliques? Envisager l' programme ci-dessous, composé de deux fichiers [principal.cpp] et [nombres.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 ce compile et lie bien avec MinGW g++ 4.4.1 et Visual C++ 10.0.

puisque les types ne correspondent pas, le programme s'écrase quand vous l'exécutez.

The Windows 7 crash dialog

explication formelle: le programme a un comportement non défini (UB), et à la place de s'écraser peut donc pendre, ou peut-être ne rien faire, ou il peut envoyer trois e-mails aux présidents de les etats-unis, la Russie, l'Inde, La Chine et la Suisse, et faire des démons nasaux voler hors de votre 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 le premier int valeur dans le tableau, est considéré comme un pointeur. C'est-à-dire: dans main.cpp le numbers variable contient ou semble contenir, (int*)1 . Cela provoque la programme d'accès mémoire vers le bas à très bas de l'espace d'adresse, qui est conventionnellement réservé et piégeur. Résultat: vous obtenez un crash.

les compilateurs sont pleinement en droit de ne pas diagnostiquer cette erreur, parce que 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é du type ne nécessite pas de diagnostic.

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

... les déclarations pour un objet array peuvent spécifier les types de tableaux qui diffèrent par la présence ou l'absence d'un tableau majeur lié (8.3.4).

cette variation autorisée n'inclut pas la déclaration d'un nom comme tableau dans un l'unité de traduction, et comme un pointeur dans une autre unité de traduction.

5.2 Pitfall: faire de l'optimisation prématurée ( memset & friends).

pas encore écrit

5.3 Piège: à l'Aide de la C idiome pour obtenir le nombre d'éléments.

avec une expérience en C profond, il est naturel d'écrire ...

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

Puisqu'un array se décompose pour pointer vers le premier élément où nécessaire, le l'expression sizeof(a)/sizeof(a[0]) peut aussi s'écrire comme sizeof(a)/sizeof(*a) . Il signifie la même chose, et n'importe comment il est écrit il est le c idiom pour trouver les éléments de nombre de tableau.

pitfall principal: le langage C n'est pas typesafe. 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 à N_ITEMS , et produit donc très probablement une erreur résultat. Compilé en tant qu'exécutable 32 bits dans Windows 7, il produit ...

7 elements, appel display...

1 éléments.

  1. Le compilateur écrit int const a[7] juste int const a[] .
  2. le compilateur réécrit int const a[] à int const* a .
  3. N_ITEMS est donc invoqué avec un pointeur.
  4. pour un exécutable de 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 aussi 4.

afin de détecter cette erreur à 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 elements, appel display...

Échec de l'Assertion: ( "N_ITEMS nécessite un véritable tableau comme argument", typeid( a ) != typeid ( &*) ), fichier runtime_detect ion.cpp, ligne 16

cette application a demandé le Runtime pour d'y mettre fin d'une manière inhabituelle.

Veuillez communiquer avec l'équipe de soutien de la demande pour obtenir de plus amples renseignements.

la détection d'erreur d'exécution est meilleure que l'absence de détection, mais elle gaspille un peu temps processeur, et peut-être beaucoup plus de temps du programmeur. Mieux avec la détection de l' moment de la compilation! Et si vous êtes heureux de ne pas prendre en charge 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 )

Compilation cette définition substitué dans le premier programme complet, avec g++,

M:\count > g++ compile_time_detection.cpp

compile_time_detection.cpp: en fonction ' void display (suite*)':

compile_time_detection.cpp: 14: erreur: Pas de fonction de correspondance pour l'appel à ' n_items (const int*&)'

M:\count 1519540920"

comment ça marche: le tableau est passé par référence à n_items , et ainsi il fait ne pas se détériorer à pointer vers le premier élément, et la fonction peut simplement retourner le nombre d'éléments spécifiés par type.

avec C++11 Vous pouvez l'utiliser aussi pour les tableaux de type local, et c'est le type sûr C++ idiome pour trouver le nombre d'éléments d'un tableau.

5.4 C++11 Et C++14 écueil: l'Utilisation d'un constexpr la taille du tableau fonction.

avec C++11 et plus tard c'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 , ce qui permet cette fonction permet 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é pour déclarez 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 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, le ci-dessus compile avec MinGW-64 5.1.0 avec -pedantic-errors , et, test 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 déclaration C++11/C++14 sur l'utilisation de références dans constexpr expressions:

C++11 C++14 $ 5.19 / 2 nine th dash

A expression conditionnelle "1519910920 e est une expression constante de base sauf si l'évaluation de e , suivant les règles de la abstract machine (1.9), évaluerait l'un des expressions suivantes:

        ⋮

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

pour traiter des collections qui peuvent être non-tableaux, on a besoin de la capacité de charge d'un Fonction n_items , mais aussi, pour utiliser le temps de compilation on a besoin d'un temps de compilation la représentation de la taille de la matrice. Et la solution classique c++03, qui fonctionne très bien aussi en C++11 et C++14, est de laisser la fonction Signaler son résultat non pas comme une valeur mais par sa fonction résultat 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 );
}

concernant le 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 comme une valeur constexpr , réintroduisant le problème original. Plutôt d'une classe Size_carrier on peut laisser la fonction retourner directement référence à un tableau. Cependant, cette syntaxe n'est pas familière à tout le monde.

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

j'espère que la oups-il-était-une-reference-impliqués-dans-votre- constexpr problème sera résolu avec C++17, mais en attendant une macro comme le STATIC_N_ITEMS ci-dessus offre la portabilité, par exemple aux compilateurs Clang et Visual C++, en conservant la sécurité du type.

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

65
répondu Cheers and hth. - Alf 2015-07-31 00:15:12