516 Stimmen

Wie verwende ich Arrays in C++?

C++ hat Arrays von C geerbt, wo sie praktisch überall verwendet werden. C++ bietet Abstraktionen, die einfacher zu verwenden und weniger fehleranfällig sind ( std::vector<T> seit C++98 und std::array<T, n> seit C++11 Wenn Sie jedoch älteren Code lesen oder mit einer in C geschriebenen Bibliothek arbeiten, sollten Sie wissen, wie Arrays funktionieren.

Diese FAQ ist in fünf Teile gegliedert:

  1. Arrays auf Typebene und Zugriff auf Elemente
  2. Erstellung und Initialisierung von Arrays
  3. Zuweisung und Parameterübergabe
  4. mehrdimensionale Arrays und Arrays von Zeigern
  5. Häufige Fallstricke bei der Verwendung von Arrays

Wenn Sie der Meinung sind, dass etwas Wichtiges in dieser FAQ fehlt, schreiben Sie eine Antwort und verlinken Sie sie hier als zusätzlichen Teil.

Im folgenden Text bedeutet "Array" "C-Array", nicht die Klassenvorlage std::array . Grundlegende Kenntnisse der C-Dekarator-Syntax werden vorausgesetzt. Beachten Sie, dass die manuelle Verwendung von new y delete wie im Folgenden gezeigt wird, angesichts der Ausnahmen extrem gefährlich ist, aber das ist das Thema von eine weitere FAQ .


(Hinweis: Dies ist ein Eintrag zu Stack Overflow's C++ FAQ . Wenn Sie die Idee, eine FAQ in dieser Form anzubieten, kritisieren wollen, dann das Posting auf Meta, mit dem alles begann wäre der richtige Ort, um das zu tun. Die Antworten auf diese Frage werden in der C++ Chatraum , wo die Idee für die FAQ entstand, so dass Ihre Antwort mit großer Wahrscheinlichkeit von denjenigen gelesen wird, die die Idee hatten).

324voto

fredoverflow Punkte 245881

Arrays auf der Ebene des Typs

Ein Array-Typ wird bezeichnet als T[n] donde T es el Elementtyp y n ist eine positive Größe die Anzahl der Elemente in dem Array. Der Array-Typ ist ein Produkttyp aus dem Elementtyp und der Größe. Wenn einer oder beide dieser Bestandteile unterschiedlich sind, erhält man einen anderen Typ:

#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");

Beachten Sie, dass die Größe Teil des Typs ist, d. h. Array-Typen unterschiedlicher Größe sind inkompatible Typen, die absolut nichts miteinander zu tun haben. sizeof(T[n]) ist gleichbedeutend mit n * sizeof(T) .

Array-zu-Zeiger-Zerfall

Die einzige "Verbindung" zwischen T[n] y T[m] ist, dass beide Typen stillschweigend als umgewandelt zu T* und das Ergebnis dieser Umwandlung ist ein Zeiger auf das erste Element des Arrays. Das heißt, überall dort, wo ein T* erforderlich ist, können Sie eine T[n] und der Compiler wird diesen Zeiger stillschweigend bereitstellen:

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

Diese Umwandlung wird als "Array-zu-Zeiger-Zerfall" bezeichnet und ist eine wichtige Quelle der Verwirrung. Die Größe des Arrays geht bei diesem Prozess verloren, da sie nicht mehr Teil des Typs ist ( T* ). Pro: Durch das Vergessen der Größe eines Arrays auf der Typebene kann ein Zeiger auf das erste Element eines Arrays von jede Größe. Contra: Bei einem Zeiger auf das erste (oder ein anderes) Element eines Arrays gibt es keine Möglichkeit festzustellen, wie groß das Array ist oder worauf genau der Zeiger im Verhältnis zu den Grenzen des Arrays zeigt. Zeiger sind extrem dumm .

Arrays sind keine Zeiger

Der Compiler erzeugt stillschweigend einen Zeiger auf das erste Element eines Arrays, wann immer dies als nützlich erachtet wird, d.h. wann immer eine Operation an einem Array scheitern, aber an einem Zeiger erfolgreich sein würde. Diese Umwandlung von Array in Zeiger ist trivial, da der resultierende Zeiger Wert ist einfach die Adresse des Arrays. Beachten Sie, dass der Zeiger no als Teil des Arrays selbst (oder irgendwo anders im Speicher) gespeichert. Ein Array ist kein Zeiger.

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

Ein wichtiger Kontext, in dem ein Array no in einen Zeiger auf sein erstes Element zerfällt, ist, wenn die & Operator angewendet wird. In diesem Fall wird der & Operator liefert einen Zeiger auf den gesamte Array, nicht nur einen Zeiger auf sein erstes Element. Obwohl in diesem Fall die Werte (die Adressen) gleich sind, sind ein Zeiger auf das erste Element eines Arrays und ein Zeiger auf das gesamte Array völlig unterschiedliche Typen:

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

Die folgende ASCII-Grafik erklärt diesen Unterschied:

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

Beachten Sie, dass der Zeiger auf das erste Element nur auf eine einzige ganze Zahl zeigt (als kleines Kästchen dargestellt), während der Zeiger auf das gesamte Array auf ein Array mit 8 ganzen Zahlen zeigt (als großes Kästchen dargestellt).

Die gleiche Situation tritt im Unterricht auf und ist vielleicht noch offensichtlicher. Ein Zeiger auf ein Objekt und ein Zeiger auf sein erstes Datenelement haben denselben Wert (dieselbe Adresse), doch handelt es sich um völlig unterschiedliche Typen.

Wenn Sie mit der Syntax von C-Deklaratoren nicht vertraut sind, ist die Klammer im Typ int(*)[8] sind unerlässlich:

  • int(*)[8] ist ein Zeiger auf ein Array von 8 Ganzzahlen.
  • int*[8] ist ein Array von 8 Zeigern, wobei jedes Element vom Typ int* .

Zugriff auf Elemente

C++ bietet zwei syntaktische Varianten für den Zugriff auf einzelne Elemente eines Arrays. Keine von beiden ist der anderen überlegen, und Sie sollten sich mit beiden vertraut machen.

Zeiger-Arithmetik

Angesichts eines Zeigers p auf das erste Element eines Arrays, der Ausdruck p+i gibt einen Zeiger auf das i-te Element des Arrays zurück. Durch anschließende Dereferenzierung dieses Zeigers kann man auf einzelne Elemente zugreifen:

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

Wenn x kennzeichnet eine Array dann setzt der Array-zu-Zeiger-Zerfall ein, da das Addieren eines Arrays und einer Ganzzahl sinnlos ist (es gibt keine Plus-Operation für Arrays), aber das Addieren eines Zeigers und einer Ganzzahl ist sinnvoll:

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

(Beachten Sie, dass der implizit erzeugte Zeiger keinen Namen hat, also schrieb ich x+0 um sie zu identifizieren).

Wenn, auf der anderen Seite, x kennzeichnet eine Zeiger auf das erste (oder ein beliebiges anderes) Element eines Arrays, dann ist ein Array-Zeiger-Zerfall nicht notwendig, da der Zeiger, auf den i hinzugefügt werden soll, existiert bereits:

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

Beachten Sie, dass in dem dargestellten Fall, x ist ein Zeiger variabel (zu erkennen an dem kleinen Kästchen neben x ), aber es könnte genauso gut das Ergebnis einer Funktion sein, die einen Zeiger (oder einen anderen Ausdruck des Typs T* ).

Indizierungsoperator

Da die Syntax *(x+i) etwas umständlich ist, bietet C++ die alternative Syntax x[i] :

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

Da die Addition kommutativ ist, macht der folgende Code genau das Gleiche:

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

Die Definition des Indexierungsoperators führt zu der folgenden interessanten Äquivalenz:

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

Allerdings, &x[0] ist im Allgemeinen no gleichbedeutend mit x . Der erste ist ein Zeiger, der zweite ein Array. Nur wenn der Kontext den Array-zu-Zeiger-Zerfall auslöst, kann x y &x[0] austauschbar verwendet werden. Zum Beispiel:

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

In der ersten Zeile erkennt der Compiler eine Zuweisung von einem Zeiger zu einem Zeiger, die trivialerweise erfolgreich ist. In der zweiten Zeile erkennt er eine Zuweisung von einem Array auf einen Zeiger. Da dies sinnlos ist (aber Zeiger Zeiger-Zuweisung sinnvoll ist), tritt der Array-zu-Zeiger-Zerfall wie üblich ein.

Bereiche

Ein Array vom Typ T[n] hat n Elemente, indiziert von 0 zu n-1 ; es gibt kein Element n . Und dennoch, um halboffene Bereiche zu unterstützen (wo der Anfang ist inklusive und das Ende ist exklusiv ), erlaubt C++ die Berechnung eines Zeigers auf das (nicht existierende) n-te Element, aber es ist illegal, diesen Zeiger zu derefenzieren:

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

Wenn Sie zum Beispiel ein Array sortieren wollen, funktionieren beide der folgenden Möglichkeiten gleich gut:

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

Beachten Sie, dass es illegal ist, Folgendes anzubieten &x[n] als zweites Argument, da dies gleichbedeutend ist mit &*(x+n) und der Unterausdruck *(x+n) beruft sich technisch auf undefiniertes Verhalten in C++ (aber nicht in C99).

Beachten Sie auch, dass Sie einfach Folgendes angeben können x als erstes Argument. Das ist für meinen Geschmack etwas zu knapp, und es macht auch die Ableitung von Vorlagenargumenten für den Compiler etwas schwieriger, denn in diesem Fall ist das erste Argument ein Array, das zweite Argument aber ein Zeiger. (Auch hier greift der Array-zu-Zeiger-Zerfall.)

148voto

fredoverflow Punkte 245881

Programmierer verwechseln oft mehrdimensionale Arrays mit Arrays von Zeigern.

Mehrdimensionale Arrays

Die meisten Programmierer sind mit benannten mehrdimensionalen Arrays vertraut, aber viele sind sich der Tatsache nicht bewusst, dass mehrdimensionale Arrays auch anonym erstellt werden können. Mehrdimensionale Arrays werden oft als "Arrays von Arrays" oder " wahr mehrdimensionale Arrays".

Benannte mehrdimensionale Arrays

Bei der Verwendung benannter mehrdimensionaler Arrays, todo Abmessungen müssen zur Kompilierzeit bekannt sein:

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

So sieht ein benanntes mehrdimensionales Array im Speicher aus:

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

Beachten Sie, dass 2D-Gitter wie das oben abgebildete lediglich eine hilfreiche Visualisierung darstellen. Aus der Sicht von C++ ist der Speicher eine "flache" Folge von Bytes. Die Elemente eines mehrdimensionalen Arrays werden in zeilenweiser Reihenfolge gespeichert. Das heißt, connect_four[0][6] y connect_four[1][0] sind Nachbarn in der Erinnerung. In der Tat, connect_four[0][7] y connect_four[1][0] das gleiche Element bezeichnen! Das bedeutet, dass Sie mehrdimensionale Arrays als große, eindimensionale Arrays behandeln können:

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

Anonyme mehrdimensionale Arrays

Bei anonymen mehrdimensionalen Arrays sind alle Dimensionen außer der ersten muss zur Kompilierzeit bekannt sein:

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

So sieht ein anonymes mehrdimensionales Array im Speicher aus:

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

Beachten Sie, dass das Array selbst weiterhin als einzelner Block im Speicher zugewiesen wird.

Arrays von Zeigern

Sie können die Beschränkung der festen Breite überwinden, indem Sie eine weitere Ebene der Indirektion einführen.

Benannte Arrays von Zeigern

Hier ist ein benanntes Array mit fünf Zeigern, die mit anonymen Arrays unterschiedlicher Länge initialisiert werden:

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];
}

Und so sieht es im Speicher aus:

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

Da nun jede Zeile einzeln zugeordnet wird, funktioniert die Darstellung von 2D-Arrays als 1D-Arrays nicht mehr.

Anonyme Arrays von Zeigern

Hier ist ein anonymes Array mit 5 (oder einer beliebigen Anzahl von) Zeigern, die mit anonymen Arrays unterschiedlicher Länge initialisiert werden:

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[] !

Und so sieht es im Speicher aus:

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

Umrechnungen

Der Array-zu-Zeiger-Zerfall erstreckt sich natürlich auch auf Arrays von Arrays und Arrays von Zeigern:

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;

Es gibt jedoch keine implizite Umwandlung von T[h][w] zu T** . Wenn es eine solche implizite Konvertierung gäbe, wäre das Ergebnis ein Zeiger auf das erste Element eines Arrays von h Zeiger auf T (die jeweils auf das erste Element einer Zeile im ursprünglichen 2D-Array zeigen), aber dieses Zeiger-Array ist noch nirgendwo im Speicher vorhanden. Wenn Sie eine solche Konvertierung wünschen, müssen Sie das erforderliche Zeigerfeld manuell erstellen und füllen:

int connect_four[6][7];

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

// ...

delete[] p;

Beachten Sie, dass dies eine Ansicht des ursprünglichen mehrdimensionalen Arrays erzeugt. Wenn Sie stattdessen eine Kopie benötigen, müssen Sie zusätzliche Arrays erstellen und die Daten selbst kopieren:

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;

93voto

fredoverflow Punkte 245881

Zuweisung

Aus keinem bestimmten Grund können Arrays nicht einander zugeordnet werden. Verwenden Sie std::copy stattdessen:

#include <algorithm>

// ...

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

Dies ist flexibler als eine echte Array-Zuweisung, da es möglich ist, Teile von größeren Arrays in kleinere Arrays zu kopieren. std::copy ist in der Regel auf primitive Typen spezialisiert, um maximale Leistung zu erzielen. Es ist unwahrscheinlich, dass std::memcpy besser abschneidet. Im Zweifelsfall sollte man messen.

Obwohl Sie Arrays nicht direkt zuweisen können, können Sie peut Strukturen und Klassen zuordnen, die enthalten Array-Mitglieder. Das liegt daran, dass Array-Mitglieder werden mitgliederweise kopiert durch den Zuweisungsoperator, der vom Compiler als Standard bereitgestellt wird. Wenn Sie den Zuweisungsoperator manuell für Ihre eigenen Struktur- oder Klassentypen definieren, müssen Sie auf das manuelle Kopieren für die Array-Mitglieder zurückgreifen.

Parameterübergabe

Arrays können nicht als Wert übergeben werden. Sie können sie entweder per Zeiger oder per Referenz übergeben.

Weitergabe durch Zeiger

Da Arrays selbst nicht als Wert übergeben werden können, wird stattdessen normalerweise ein Zeiger auf ihr erstes Element als Wert übergeben. Dies wird oft als "pass by pointer" bezeichnet. Da die Größe des Arrays nicht über diesen Zeiger abrufbar ist, müssen Sie einen zweiten Parameter übergeben, der die Größe des Arrays angibt (die klassische C-Lösung) oder einen zweiten Zeiger, der auf das letzte Element des Arrays zeigt (die C++-Iterator-Lösung):

#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);
}

Als syntaktische Alternative können Sie die Parameter auch als T p[] und bedeutet genau das Gleiche wie T* p nur im Zusammenhang mit Parameterlisten :

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

Man kann sich den Compiler so vorstellen, dass er T p[] zu T *p nur im Zusammenhang mit Parameterlisten . Diese spezielle Regel ist teilweise für die ganze Verwirrung um Arrays und Zeiger verantwortlich. In jedem anderen Kontext macht die Deklaration von etwas als Array oder als Zeiger eine riesig Unterschied.

Leider können Sie auch eine Größe in einem Array-Parameter angeben, die vom Compiler ignoriert wird. Das heißt, die folgenden drei Signaturen sind genau gleichwertig, wie die Compilerfehler zeigen:

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

Übergabe durch Referenz

Arrays können auch per Referenz übergeben werden:

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

In diesem Fall ist die Größe des Arrays von Bedeutung. Da es wenig sinnvoll ist, eine Funktion zu schreiben, die nur Arrays mit genau 8 Elementen akzeptiert, schreiben Programmierer solche Funktionen normalerweise als Vorlagen:

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

Beachten Sie, dass Sie eine solche Funktionsschablone nur mit einem tatsächlichen Array von Ganzzahlen aufrufen können, nicht mit einem Zeiger auf eine Ganzzahl. Die Größe des Arrays wird automatisch abgeleitet, und für jede Größe n wird eine andere Funktion aus der Vorlage instanziiert. Sie können auch schreiben sehr nützlich Funktionsvorlagen, die sowohl von der Elementart als auch von der Größe abstrahieren.

77voto

Cheers and hth. - Alf Punkte 138555

5. Häufige Fallstricke bei der Verwendung von Arrays.

5.1 Fallstrick: Vertrauen in typunsichere Verknüpfungen.

OK, man hat Ihnen gesagt oder Sie haben es selbst herausgefunden, dass Globals (Namespace Bereichsvariablen, auf die außerhalb der Übersetzungseinheit zugegriffen werden kann) Evil™ sind. Aber wussten Sie, wie böse™ sie wirklich sind? Betrachten Sie das Programm, das aus zwei Dateien besteht [main.cpp] und [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};

Unter Windows 7 kompiliert und verknüpft es sich gut mit MinGW g++ 4.4.1 und Visual C++ 10.0.

Da die Typen nicht übereinstimmen, stürzt das Programm ab, wenn Sie es ausführen.

The Windows 7 crash dialog

Die formale Erklärung: Das Programm hat Undefined Behavior (UB), und anstatt abstürzt, kann es sich daher einfach aufhängen, oder vielleicht nichts tun, oder es Droh-E-Mails an die Präsidenten der USA, Russlands, Indiens und der Schweiz schicken, China und der Schweiz schicken und Nasal Daemons aus Ihrer Nase fliegen lassen.

Erklärung aus der Praxis: in main.cpp das Array wird als Zeiger behandelt und in auf dieselbe Adresse wie das Array. Für eine 32-Bit ausführbare Datei bedeutet dies, dass die erste int Wert im Array, wird als Zeiger behandelt. D.h., in main.cpp die numbers Variable enthält oder zu enthalten scheint, (int*)1 . Dies bewirkt, dass das Programm auf den Speicher ganz unten im Adressraum zugreifen, der reserviert ist und zu Traps führt. Ergebnis: Sie erhalten einen Absturz.

Es ist das gute Recht der Compiler, diesen Fehler nicht zu diagnostizieren, denn C++11 §3.5/10 sagt über die Anforderung kompatibler Typen für die Deklarationen,

[N3290 §3.5/10]
Ein Verstoß gegen diese Regel zur Typenidentität erfordert keine Diagnose.

Im selben Absatz werden die zulässigen Abweichungen aufgeführt:

Deklarationen für ein Array-Objekt können Array-Typen angeben, die sich durch das Vorhandensein oder Nichtvorhandensein einer Haupt-Array-Bindung unterscheiden (8.3.4).

Diese erlaubte Variante beinhaltet nicht die Deklaration eines Namens als Array in einer Übersetzungseinheit und als Zeiger in einer anderen Übersetzungseinheit.

5.2 Fallstrick: Vorzeitige Optimierung ( memset & Freunde).

Noch nicht geschrieben

5.3 Fallstrick: Die Verwendung des C-Idioms, um die Anzahl der Elemente zu ermitteln.

Mit umfassender C-Erfahrung ist es natürlich,

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

Da ein array zerfällt zum Zeiger auf das erste Element, wenn es benötigt wird, die Ausdruck sizeof(a)/sizeof(a[0]) kann auch geschrieben werden als sizeof(a)/sizeof(*a) . Es bedeutet dasselbe, und egal, wie es geschrieben wird geschrieben wird, ist es das C Idiom um die Anzahl der Elemente eines Arrays zu ermitteln.

Hauptfallstrick: Das C-Idiom ist nicht typsicher. Zum Beispiel, der 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 );
}

übergibt einen Zeiger auf N_ITEMS und führt daher höchstwahrscheinlich zu einer falschen Ergebnis. Kompiliert als ausführbare 32-Bit-Datei unter Windows 7 erzeugt es

7 Elemente, die Anzeige aufrufen...
1 Elemente.

  1. Der Compiler schreibt neu int const a[7] um einfach int const a[] .
  2. Der Compiler schreibt neu int const a[] zu int const* a .
  3. N_ITEMS wird daher mit einem Zeiger aufgerufen.
  4. Für eine ausführbare 32-Bit-Datei sizeof(array) (Größe eines Zeigers) ist dann 4.
  5. sizeof(*array) ist gleichbedeutend mit sizeof(int) was für eine ausführbare 32-Bit-Datei ebenfalls 4 ist.

Um diesen Fehler während der Laufzeit zu erkennen, können Sie

#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 Elemente, die Anzeige aufrufen...
Assertion fehlgeschlagen: ("N_ITEMS erfordert ein aktuelles Array als Argument", typeid( a ) != typeid( &*a ) ), Datei runtime_detect ion.cpp, Zeile 16

Diese Anwendung hat die Laufzeitumgebung aufgefordert, sie auf eine ungewöhnliche Weise zu beenden.
Für weitere Informationen wenden Sie sich bitte an das Support-Team der Anwendung.

Die Erkennung von Laufzeitfehlern ist besser als keine Erkennung, aber sie verschwendet ein wenig Prozessorzeit und vielleicht viel mehr Zeit des Programmierers. Besser mit Erkennung zur Kompilierzeit! Und wenn Sie froh sind, dass Sie mit C++98 keine Arrays lokaler Typen unterstützen, dann können Sie das tun:

#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 )

Kompilieren dieser Definition in das erste vollständige Programm ersetzt, mit g++, erhalte ich

M:\count > g++ compile_time_detection.cpp
compile_time_detection.cpp: In der Funktion 'void display(const int*)':
compile_time_detection.cpp:14: Fehler: keine passende Funktion für den Aufruf von 'n_items(const int*&)'

M:\count > _

So funktioniert es: Das Array wird übergeben durch Verweis zu n_items und das tut sie auch nicht auf den Zeiger auf das erste Element abklingen, und die Funktion kann einfach den Anzahl von Elementen zurückgeben, die durch den Typ angegeben ist.

Mit C++11 können Sie dies auch für Arrays vom lokalen Typ verwenden, und es ist die typsichere C++ Idiom um die Anzahl der Elemente eines Arrays zu ermitteln.

5.4 C++11 & C++14 Fallstrick: Die Verwendung einer constexpr Array-Größenfunktion.

Mit C++11 und später ist es natürlich, aber wie Sie sehen werden, gefährlich! die C++03-Funktion zu ersetzen

typedef ptrdiff_t   Size;

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

mit

using Size = ptrdiff_t;

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

wobei die wesentliche Änderung in der Verwendung von constexpr die es ermöglicht diese Funktion eine Kompilierzeitkonstante .

Im Gegensatz zur C++03-Funktion ist eine solche Kompilierzeitkonstante zum Beispiel verwendet werden, um ein Array mit der gleichen Größe wie ein anderes zu deklarieren:

// 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.
}

Aber betrachten Sie diesen Code unter Verwendung der 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 );
}

Der Fallstrick: ab Juli 2015 kompiliert das oben genannte mit MinGW-64 5.1.0 mit -pedantic-errors , und, Tests mit den Online-Compilern unter gcc.godbolt.org/ auch mit clang 3.0 und clang 3.2, aber nicht mit clang 3.3, 3.4.1, 3.5.0, 3.5.1, 3.6 (rc1) oder 3.7 (experimentell). Und wichtig für die Windows-Plattform, es kompiliert nicht mit Visual C++ 2015. Der Grund dafür ist eine C++11/C++14-Anweisung über die Verwendung von Referenzen in constexpr Ausdrücke:

C++11 C++14 $5.19/2 neun th Bindestrich

A bedingter-Ausdruck e ist eine Kernkonstante es sei denn, die Bewertung von e würde nach den Regeln der abstrakten Maschine (1.9) einen der folgenden Ausdrücke auswerten folgenden Ausdrücke auswerten:
        

  • eine id-Ausdruck die sich auf eine Variable oder ein Datenelement vom Referenztyp verweist, es sei denn, die Referenz hat eine vorangehende Initialisierung und entweder
    • es wird mit einem konstanten Ausdruck initialisiert oder
    • es ist ein nicht statisches Datenelement eines Objekts, dessen Lebensdauer innerhalb von der Auswertung von e begann;

Man kann immer die ausführlichere

// 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
}

aber das schlägt fehl, wenn Collection ist kein rohes Array.

Um mit Sammlungen umzugehen, die keine Arrays sein können, braucht man die Überladbarkeit einer n_items Funktion, aber auch für die Verwendung zur Kompilierzeit benötigt man eine Kompilierzeit Darstellung der Array-Größe. Die klassische C++03-Lösung, die auch in C++11 und C++14 gut funktioniert auch in C++11 und C++14 funktioniert, ist, dass die Funktion ihr Ergebnis nicht als Wert sondern über ihr Funktionsergebnis Typ . Zum Beispiel so:

// 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 );
}

Über die Wahl der Rückgabeart für static_n_items : Dieser Code verwendet nicht std::integral_constant denn mit std::integral_constant wird das Ergebnis dargestellt direkt als constexpr Wert, wodurch das ursprüngliche Problem wieder auftritt. Stattdessen einer Size_carrier Klasse kann man die Funktion direkt ein Referenz auf ein Array zurückgeben. Allerdings ist nicht jeder mit dieser Syntax vertraut.

Zur Namensgebung: Teil dieser Lösung für das Problem der constexpr -ungültige-due-to-reference Problem ist es, die Wahl der Kompilierzeitkonstante explizit zu machen.

Hoffentlich ist die "Hoppla, da war ein Verweis im Spiel"-Geschichte constexpr Problem wird behoben mit C++17 behoben werden, aber bis dahin kann ein Makro wie das STATIC_N_ITEMS ergibt die Übertragbarkeit, z. B. zu den Compilern von Clang und Visual C++, wobei die Typsicherheit erhalten bleibt.

Zum Thema: Makros respektieren keine Geltungsbereiche; um Namenskollisionen zu vermeiden, kann es also eine eine gute Idee sein, ein Namenspräfix zu verwenden, z. B. MYLIB_STATIC_N_ITEMS .

73voto

fredoverflow Punkte 245881

Erstellung und Initialisierung von Arrays

Wie jede andere Art von C++-Objekten können Arrays entweder direkt in benannten Variablen gespeichert werden (dann muss die Größe eine Kompilierzeitkonstante sein); C++ unterstützt keine VLAs ), oder sie können anonym auf dem Heap gespeichert werden und der Zugriff erfolgt indirekt über Zeiger (nur dann kann die Größe zur Laufzeit berechnet werden).

Automatische Arrays

Automatische Arrays (Arrays, die "auf dem Stack" leben) werden jedes Mal erstellt, wenn der Kontrollfluss die Definition einer nicht statischen lokalen Array-Variablen durchläuft:

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

Die Initialisierung erfolgt in aufsteigender Reihenfolge. Beachten Sie, dass die Anfangswerte vom Elementtyp abhängen T :

  • Wenn T ist eine POD (wie int im obigen Beispiel), findet keine Initialisierung statt.
  • Andernfalls wird der Standard-Konstruktor von T initialisiert alle Elemente.
  • Wenn T keinen zugänglichen Standard-Konstruktor bietet, lässt sich das Programm nicht kompilieren.

Alternativ können die Anfangswerte auch explizit in der Array-Initialisierungsprogramm , eine durch Komma getrennte Liste, die von geschweiften Klammern umgeben ist:

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

Da in diesem Fall die Anzahl der Elemente im Array-Initialisierer gleich der Größe des Arrays ist, ist die manuelle Angabe der Größe überflüssig. Sie kann vom Compiler automatisch abgeleitet werden:

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

Es ist auch möglich, die Größe anzugeben und einen kürzeren Array-Initialisierer bereitzustellen:

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

In diesem Fall sind die übrigen Elemente Null-Initialisierung . Beachten Sie, dass C++ einen leeren Array-Initialisierer zulässt (alle Elemente sind null-initialisiert), während C89 dies nicht tut (mindestens ein Wert ist erforderlich). Beachten Sie auch, dass Array-Initialisierer nur verwendet werden können, um initialisieren Arrays; sie können später nicht in Zuweisungen verwendet werden.

Statische Arrays

Statische Arrays (Arrays, die "im Datensegment" leben) sind lokale Array-Variablen, die mit der static Schlüsselwort und Array-Variablen im Namespace-Bereich ("globale Variablen"):

int global_static_array[8];

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

(Beachten Sie, dass Variablen im Namespace-Bereich implizit statisch sind. Das Hinzufügen der static Schlüsselwort zu ihrer Definition hat eine völlig andere, veraltete Bedeutung .)

Im Folgenden wird erläutert, wie sich statische Arrays anders verhalten als automatische Arrays:

  • Statische Arrays ohne einen Array-Initialisierer werden vor jeder weiteren möglichen Initialisierung null-initialisiert.
  • Statische POD-Arrays werden initialisiert genau einmal und die Anfangswerte sind typischerweise in die ausführbare Datei integriert werden, so dass zur Laufzeit keine Initialisierungskosten anfallen. Dies ist jedoch nicht immer die platzsparendste Lösung und wird von der Norm nicht gefordert.
  • Statische Nicht-POD-Arrays werden mit dem erstes Mal der Kontrollfluss läuft durch ihre Definition. Im Falle lokaler statischer Arrays kann dies nie geschehen, wenn die Funktion nie aufgerufen wird.

(Keiner der oben genannten Punkte ist spezifisch für Arrays. Diese Regeln gelten auch für andere Arten von statischen Objekten).

Array-Datenelemente

Array-Datenelemente werden erstellt, wenn ihr zugehöriges Objekt erstellt wird. Leider bietet C++03 keine Möglichkeit, Arrays in der Mitgliederinitialisierungsliste Daher muss die Initialisierung mit Zuweisungen vorgetäuscht werden:

class Foo
{
    int primes[8];

public:

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

Alternativ können Sie ein automatisches Array im Konstruktorkörper definieren und die Elemente rüberkopieren:

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

In C++0x, Arrays peut in der Mitgliederinitialisierungsliste initialisiert werden, dank einheitliche Initialisierung :

class Foo
{
    int primes[8];

public:

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

Dies ist die einzige Lösung, die mit Elementtypen funktioniert, die keinen Standardkonstruktor haben.

Dynamische Arrays

Dynamische Arrays haben keine Namen, daher kann man nur über Zeiger auf sie zugreifen. Da sie keine Namen haben, werde ich sie von nun an als "anonyme Arrays" bezeichnen.

In C werden anonyme Arrays über malloc und Freunde. In C++ werden anonyme Arrays mit der Methode new T[size] Syntax, die einen Zeiger auf das erste Element eines anonymen Arrays zurückgibt:

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

Die folgende ASCII-Grafik zeigt das Speicherlayout, wenn die Größe zur Laufzeit als 8 berechnet wird:

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

Offensichtlich benötigen anonyme Arrays mehr Speicher als benannte Arrays aufgrund des zusätzlichen Zeigers, der separat gespeichert werden muss. (Es gibt auch einen zusätzlichen Overhead für den freien Speicher.)

Es ist zu beachten, dass es keine Array-zu-Zeiger-Zerfall hier vor sich geht. Obwohl die Auswertung new int[size] schafft tatsächlich eine Array von ganzen Zahlen, das Ergebnis des Ausdrucks new int[size] es bereits ein Zeiger auf eine einzelne Ganzzahl (das erste Element), no ein Array von Ganzzahlen oder ein Zeiger auf ein Array von Ganzzahlen unbekannter Größe. Das wäre unmöglich, weil das statische Typsystem verlangt, dass Arraygrößen zur Kompilierzeit Konstanten sind. (Aus diesem Grund habe ich das anonyme Array in der Abbildung nicht mit statischen Typinformationen versehen).

Was die Standardwerte für Elemente angeht, verhalten sich anonyme Arrays ähnlich wie automatische Arrays. Normalerweise werden anonyme POD-Arrays nicht initialisiert, aber es gibt eine spezielle Syntax die die Wertinitialisierung auslöst:

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

(Beachten Sie das nachgestellte Klammerpaar direkt vor dem Semikolon.) Auch hier vereinfacht C++0x die Regeln und ermöglicht die Angabe von Anfangswerten für anonyme Arrays dank der einheitlichen Initialisierung:

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

Wenn Sie mit der Verwendung eines anonymen Arrays fertig sind, müssen Sie es wieder an das System zurückgeben:

delete[] p;

Sie müssen jedes anonyme Feld genau einmal freigeben und dürfen es danach nie wieder berühren. Es überhaupt nicht freizugeben führt zu einem Speicherleck (oder allgemeiner, abhängig vom Elementtyp, zu einem Ressourcenleck), und der Versuch, es mehrfach freizugeben, führt zu undefiniertem Verhalten. Verwendung der Nicht-Array-Form delete (o free ) anstelle von delete[] zur Freigabe des Arrays ist auch undefiniertes Verhalten .

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X