Wie verwende ich Arrays in C ++?


480

C ++ erbte Arrays von C, 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 ), sodass Arrays nicht so häufig benötigt werden wie in C. Wenn Sie jedoch Legacy lesen Wenn Sie Code schreiben oder mit einer in C geschriebenen Bibliothek interagieren, sollten Sie genau wissen, wie Arrays funktionieren.

Diese FAQ ist in fünf Teile gegliedert:

  1. Arrays auf Typebene und Zugriff auf Elemente
  2. Array-Erstellung und -Initialisierung
  3. Zuordnung 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 in dieser FAQ etwas Wichtiges 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. Grundkenntnisse der C-Deklaratorsyntax werden vorausgesetzt. Beachten Sie, dass die manuelle Verwendung von newund deletewie unten gezeigt angesichts von Ausnahmen äußerst gefährlich ist, dies ist jedoch das Thema einer anderen FAQ .

(Hinweis: Dies ist als Eintrag in die C ++ - FAQ von Stack Overflow gedacht . Wenn Sie die Idee kritisieren möchten, eine FAQ in dieser Form bereitzustellen, ist die Veröffentlichung auf Meta, mit der all dies begonnen hat , der richtige Ort dafür. Antworten auf Diese Frage wird im C ++ - Chatroom überwacht, in dem die FAQ-Idee ursprünglich begann, sodass Ihre Antwort sehr wahrscheinlich von denjenigen gelesen wird, die auf die Idee gekommen sind.)


Sie wären sogar noch besser, wenn die Zeiger immer auf den Anfang zeigen würden, anstatt irgendwo in der Mitte ihres Ziels ...
Deduplikator

Sie sollten den STL-Vektor verwenden, da er Ihnen mehr Flexibilität bietet.
Moiz Sajid

2
Mit der kombinierten Verfügbarkeit von std::arrays, std::vectors und gsl::spans würde ich ehrlich gesagt eine FAQ zur Verwendung von Arrays in C ++ erwarten, in der es heißt: "Inzwischen können Sie überlegen, ob Sie sie einfach nicht verwenden."
Einpoklum

Antworten:


302

Arrays auf Typebene

Ein Array - Typ wird bezeichnet als , T[n]wo Tder Element - Typ und nist eine positive Größe , die Anzahl der Elemente in dem Array. Der Array-Typ ist ein Produkttyp des Elementtyps und der Größe. Wenn sich einer oder beide dieser Inhaltsstoffe unterscheiden, erhalten Sie einen bestimmten 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, dh Array-Typen unterschiedlicher Größe sind inkompatible Typen, die absolut nichts miteinander zu tun haben. sizeof(T[n])ist äquivalent zun * sizeof(T) .

Array-zu-Zeiger-Zerfall

Die einzige „Verbindung“ zwischen T[n]und T[m]ist , dass beide Arten implizit werden kann umgewandelt zu T*, und das Ergebnis dieser Umwandlung ist ein Zeiger auf das erste Element des Arrays. Das heißt, überall dort, wo a T*erforderlich ist, können Sie a angeben T[n], und der Compiler stellt diesen Zeiger stillschweigend bereit:

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

Diese Konvertierung wird als "Zerfall von Array zu Zeiger" bezeichnet und ist eine Hauptursache für Verwirrung. Die Größe des Arrays geht dabei verloren, da es nicht mehr Teil des Typs ( T*) ist. Pro: Wenn Sie die Größe eines Arrays auf Typebene vergessen, kann ein Zeiger auf das erste Element eines Arrays beliebiger Größe zeigen. Con: Wenn ein Zeiger auf das erste (oder ein anderes) Element eines Arrays gegeben ist, kann nicht erkannt werden, wie groß dieses Array ist oder wo genau der Zeiger relativ zu den Grenzen des Arrays zeigt. Zeiger sind extrem dumm .

Arrays sind keine Zeiger

Der Compiler generiert stillschweigend einen Zeiger auf das erste Element eines Arrays, wenn dies als nützlich erachtet wird, dh wenn eine Operation in einem Array fehlschlägt, in einem Zeiger jedoch erfolgreich ist. Diese Umwandlung von Array - Zeiger ist trivial, da der resultierende Zeigerwert ist einfach die Adresse des Arrays. Beachten Sie, dass der Zeiger nicht als Teil des Arrays selbst (oder an einer anderen Stelle im Speicher) gespeichert wird. 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 nicht in einen Zeiger auf sein erstes Element zerfällt, ist, wenn der &Operator darauf angewendet wird. In diesem Fall gibt der &Operator einen Zeiger auf das gesamte Array aus, 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 diese Unterscheidung:

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | 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 einzelne Ganzzahl zeigt (dargestellt als kleines Feld), während der Zeiger auf das gesamte Array auf ein Array mit 8 Ganzzahlen zeigt (dargestellt als großes Feld).

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

Wenn Sie mit der C-Deklaratorsyntax nicht vertraut sind, sind die Klammern im Typ int(*)[8]wichtig:

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

Zugriff auf Elemente

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

Zeigerarithmetik

Bei einem Zeiger pauf das erste Element eines Arrays p+iergibt der Ausdruck einen Zeiger auf das i-te Element des Arrays. Durch anschließendes Dereferenzieren dieses Zeigers kann auf einzelne Elemente zugegriffen werden:

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

Wenn xein Array bezeichnet wird , wird der Zerfall von Array zu Zeiger aktiviert, da das Hinzufügen eines Arrays und einer Ganzzahl bedeutungslos ist (es gibt keine Plusoperation für Arrays), das Hinzufügen eines Zeigers und einer Ganzzahl jedoch sinnvoll ist:

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

(Beachten Sie, dass der implizit generierte Zeiger keinen Namen hat, also habe ich geschrieben, x+0um ihn zu identifizieren.)

Wenn andererseits xein Zeiger auf das erste (oder ein anderes) Element eines Arrays bezeichnet wird, ist ein Zerfall von Array zu Zeiger nicht erforderlich, da der Zeiger, auf dem ihinzugefügt werden soll, bereits vorhanden ist:

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

Beachten Sie, dass in dem dargestellten Fall xist ein Zeiger Variable (erkennbar durch das kleine Kästchen neben x), aber es könnte genauso gut das Ergebnis einer Funktion sein , einen Zeiger (oder jede andere Ausdruck des Typs RückkehrT* ).

Indizierungsoperator

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

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

Aufgrund der Tatsache, dass die Addition kommutativ ist, macht der folgende Code genau dasselbe:

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

Die Definition des Indexierungsoperators führt zu folgender interessanter Äquivalenz:

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

Ist &x[0]jedoch in der Regel nicht gleichbedeutend mit x. Ersteres ist ein Zeiger, letzteres ein Array. Nur wenn der Kontext den Zerfall von Array zu Zeiger auslöst, kann xund &x[0]kann er 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 Zuordnung von einem Zeiger zu einem Zeiger, was trivial erfolgreich ist. In der zweiten Zeile wird eine Zuordnung von einem Array zu einem Zeiger erkannt. Da dies bedeutungslos ist (aber Zeiger die Zuweisung von zu Zeiger sinnvoll ist), beginnt der Zerfall von Array zu Zeiger wie gewohnt.

Bereiche

Ein Array vom Typ T[n]enthält nElemente, die von 0bis indiziert sind n-1. Es gibt kein Element n. Um halboffene Bereiche zu unterstützen (wobei der Anfang inklusive und das Ende exklusiv ist ), erlaubt C ++ die Berechnung eines Zeigers auf das (nicht existierende) n-te Element, aber es ist illegal, diesen Zeiger zu dereferenzieren:

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

Wenn Sie beispielsweise ein Array sortieren möchten, funktionieren beide Funktionen gleich gut:

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

Beachten Sie, dass es illegal ist, &x[n]als zweites Argument anzugeben, da dies äquivalent zu &*(x+n)ist und der Unterausdruck *(x+n)technisch undefiniertes Verhalten aufruft in C ++ (aber nicht in C99) .

Beachten Sie auch, dass Sie einfach xals erstes Argument angeben können. Das ist für meinen Geschmack etwas zu knapp und macht den Abzug von Vorlagenargumenten für den Compiler etwas schwieriger, da in diesem Fall das erste Argument ein Array ist, das zweite Argument jedoch ein Zeiger. (Wiederum beginnt der Zerfall von Array zu Zeiger.)


Fälle, in denen das Array nicht in einen Zeiger zerfällt, werden hier als Referenz dargestellt.
Legends2k

@fredoverflow Im Teil "Zugriff" oder "Bereiche" sollte erwähnt werden, dass C-Arrays mit C ++ 11-Bereichs-basierten Schleifen zusammenarbeiten.
Gnzlbg

135

Programmierer verwechseln häufig mehrdimensionale Arrays mit Arrays von Zeigern.

Mehrdimensionale Arrays

Die meisten Programmierer sind mit benannten mehrdimensionalen Arrays vertraut, aber viele wissen nicht, dass mehrdimensionale Arrays auch anonym erstellt werden können. Mehrdimensionale Arrays werden oft als "Arrays von Arrays" oder " echte mehrdimensionale Arrays" bezeichnet.

Benannte mehrdimensionale Arrays

Bei Verwendung benannter mehrdimensionaler Arrays müssen alle Dimensionen zur Kompilierungszeit 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 die oben genannten lediglich hilfreiche Visualisierungen sind. Aus Sicht von C ++ ist Speicher eine "flache" Folge von Bytes. Die Elemente eines mehrdimensionalen Arrays werden in Zeilenreihenfolge gespeichert. Das heißt, connect_four[0][6]und connect_four[1][0]sind Nachbarn in Erinnerung. In der Tat connect_four[0][7]und connect_four[1][0]bezeichnen das gleiche Element! Dies 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 müssen alle Dimensionen außer der ersten zur Kompilierungszeit 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 ist.

Arrays von Zeigern

Sie können die Einschränkung der festen Breite überwinden, indem Sie eine weitere Indirektionsebene einführen.

Benannte Arrays von Zeigern

Hier ist ein benanntes Array von 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 Gedächtnis aus:

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

Da jede Zeile jetzt einzeln zugewiesen wird, funktioniert das Anzeigen von 2D-Arrays als 1D-Arrays nicht mehr.

Anonyme Anordnungen von Zeigern

Hier ist ein anonymes Array von 5 (oder einer beliebigen anderen 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 Gedächtnis aus:

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

Konvertierungen

Der Zerfall von Array zu Zeiger erstreckt sich natürlich 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 Konvertierung von T[h][w]nach T**. Wenn eine solche implizite Konvertierung vorhanden wäre, wäre das Ergebnis ein Zeiger auf das erste Element eines Array von hZeigern auf T(jedes zeigt auf das erste Element einer Linie im ursprünglichen 2D-Array), aber dieses Zeigerarray existiert nirgendwo in Erinnerung noch. Wenn Sie eine solche Konvertierung wünschen, müssen Sie das erforderliche Zeigerarray 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 dadurch eine Ansicht des ursprünglichen mehrdimensionalen Arrays generiert wird. 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;

Als Vorschlag: Sie möchte darauf hinweisen, dass int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];sowie int (*p)[W] = new int[6][W];und int (*p)[W] = new int[H][W];gelten Aussagen, wenn Hund Wwerden zum Zeitpunkt der Kompilierung bekannt.
RobertS unterstützt Monica Cellio

88

Zuordnung

Arrays können ohne besonderen Grund nicht einander zugewiesen werden. Verwenden Sie std::copystattdessen:

#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 die tatsächliche Arrayzuweisung, da Slices größerer Arrays in kleinere Arrays kopiert werden können. std::copyist normalerweise auf primitive Typen spezialisiert, um maximale Leistung zu erzielen. Es ist unwahrscheinlich, dass std::memcpyeine bessere Leistung erzielt wird. Im Zweifelsfall messen.

Obwohl Sie nicht Arrays direkt zuordnen können, Sie können Strukturen und Klassen zuordnen , die enthalten Array Mitglieder. Dies liegt daran, dass Array-Mitglieder memberweise kopiert werden vom Zuweisungsoperator, der vom Compiler standardmäßig 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 als Zeiger oder als Referenz übergeben.

Zeiger passieren

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 abgerufen werden kann, müssen Sie einen zweiten Parameter übergeben, der die Größe des Arrays angibt (die klassische C-Lösung), oder einen zweiten Zeiger, der nach dem letzten Element des Arrays zeigt (die C ++ - Iteratorlö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 Parameter auch als deklarieren. Dies T p[]bedeutet genau dasselbe wie T* p im Kontext von Parameterlisten :

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

Sie können sich den Compiler T p[]nur T *p im Kontext von Parameterlisten als Umschreiben vorstellen . Diese Sonderregel ist teilweise für die gesamte Verwirrung über Arrays und Zeiger verantwortlich. In jedem anderen Kontext macht das Deklarieren von etwas als Array oder als Zeiger eine große Sache Unterschied .

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

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

Als Referenz übergeben

Arrays können auch als Referenz übergeben werden:

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

In diesem Fall ist die Arraygröße signifikant. Da das Schreiben einer Funktion, die nur Arrays mit genau 8 Elementen akzeptiert, wenig nützlich ist, schreiben Programmierer normalerweise Funktionen wie 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 Funktionsvorlage 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 nwird eine andere Funktion aus der Vorlage instanziiert. Sie können auch sehr nützliche Funktionsvorlagen schreiben , die sowohl vom Elementtyp als auch von der Größe abstrahieren.


2
Es könnte sich lohnen, eine Notiz hinzuzufügen, void foo(int a[3]) adie so aussieht, als würde man das Array als Wert übergeben. Wenn Sie ainnerhalb von fooändern, wird das ursprüngliche Array geändert. Dies sollte klar sein, da Arrays nicht kopiert werden können, aber es könnte sich lohnen, dies zu verstärken.
Gnzlbg

C ++ 20 hatranges::copy(a, b)
LF

int sum( int size_, int a[size_]);- ab (glaube ich) C99
Chef Gladiator

73

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

5.1 Fallstricke: Vertrauen in typsichere Verknüpfungen.

OK, Ihnen wurde gesagt oder Sie haben selbst herausgefunden, dass Globale (Namespace-Bereichsvariablen, auf die außerhalb der Übersetzungseinheit zugegriffen werden kann) Evil ™ sind. Aber wussten Sie, wie wirklich Evil ™ sie sind? Betrachten Sie das folgende Programm, das aus zwei Dateien [main.cpp] und [numbers.cpp] besteht:

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

In Windows 7 wird dies gut mit MinGW g ++ 4.4.1 und Visual C ++ 10.0 kompiliert und verknüpft.

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

Der Windows 7-Absturzdialog

In der formalen Erklärung: Das Programm hat Undefined Behaviour (UB), und anstatt abzustürzen, kann es einfach hängen bleiben oder vielleicht nichts tun, oder es kann bedrohliche E-Mails an die Präsidenten der USA, Russlands, Indiens, senden. China und die Schweiz, und lassen Sie Nasendämonen aus Ihrer Nase fliegen.

Erklärung in main.cppder Praxis: Das Array wird als Zeiger behandelt, der an derselben Adresse wie das Array platziert ist. Für eine ausführbare 32-Bit-Datei bedeutet dies, dass der erste intWert im Array als Zeiger behandelt wird. Dh inmain.cpp der numbersVariablen enthält oder scheint zu enthalten , (int*)1. Dies führt dazu, dass das Programm ganz unten im Adressraum auf den Speicher zugreift, der herkömmlicherweise reserviert ist und Traps verursacht. Ergebnis: Sie bekommen einen Absturz.

Die Compiler haben das Recht, diesen Fehler nicht zu diagnostizieren, da C ++ 11 §3.5 / 10 über die Anforderung kompatibler Typen für die Deklarationen sagt:

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

Im selben Absatz wird die zulässige Variation beschrieben:

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

Diese zulässige Variation beinhaltet nicht die Deklaration eines Namens als Array in einer Übersetzungseinheit und als Zeiger in einer anderen Übersetzungseinheit.

5.2 Fallstricke: Vorzeitige Optimierung ( memset& Freunde).

Noch nicht geschrieben

5.3 Fallstricke: Verwenden der C-Sprache, um die Anzahl der Elemente zu ermitteln.

Mit tiefer C-Erfahrung ist es natürlich zu schreiben…

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

Da ein arrayZeiger bei Bedarf auf das erste Element verweist, kann der Ausdruck sizeof(a)/sizeof(a[0])auch als geschrieben werden sizeof(a)/sizeof(*a). Es bedeutet dasselbe, und egal wie es geschrieben ist, es ist das C-Idiom zum Finden der Zahlenelemente des Arrays.

Hauptfalle: 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 einem falschen Ergebnis. Kompiliert als 32-Bit-ausführbare Datei in Windows 7 erzeugt es…

7 Elemente, Aufrufanzeige ...
1 Elemente.

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

Um diesen Fehler zur Laufzeit zu erkennen, können Sie Folgendes tun:

#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, Aufrufanzeige ...
Behauptung fehlgeschlagen: ("N_ITEMS erfordert ein tatsächliches Array als Argument", Typ-ID (a)! = Typ-ID (& * a)), Datei runtime_detect ion.cpp, Zeile 16

Diese Anwendung hat die Runtime aufgefordert, sie auf ungewöhnliche Weise zu beenden.
Bitte wenden Sie sich an das Support-Team der Anwendung, um weitere Informationen zu erhalten.

Die Laufzeitfehlererkennung ist besser als keine Erkennung, verschwendet jedoch ein wenig Prozessorzeit und möglicherweise viel mehr Programmierzeit. Besser mit Erkennung zur Kompilierungszeit! Und wenn Sie glücklich sind, Arrays lokaler Typen mit C ++ 98 nicht zu unterstützen, können Sie dies 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 )

Beim Kompilieren dieser Definition, die mit g ++ in das erste vollständige Programm eingesetzt wurde, erhielt 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 übereinstimmende Funktion für den Aufruf von 'n_items (const int * &)'

M: \ count> _

So funktioniert es: Das Array wird unter Bezugnahme auf übergebenn_items und verfällt daher nicht in einen Zeiger auf das erste Element. Die Funktion kann nur die Anzahl der vom Typ angegebenen Elemente zurückgeben.

Mit C ++ 11 können Sie dies auch für Arrays vom lokalen Typ verwenden, und es ist die typsichere C ++ - Sprache zum Ermitteln der Anzahl der Elemente eines Arrays.

5.4 Fallstricke in C ++ 11 und C ++ 14: Verwenden einer constexprArraygrößenfunktion.

Mit C ++ 11 und höher 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 die Verwendung von ist constexpr, wodurch diese Funktion eine Kompilierungszeitkonstante erzeugen kann .

Im Gegensatz zur C ++ 03-Funktion kann eine solche Kompilierungszeitkonstante beispielsweise verwendet werden, um ein Array mit derselben 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.
}

Betrachten Sie diesen Code jedoch anhand der folgenden constexprVersion:

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

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

Die Falle: Ab Juli 2015 werden die oben genannten Kompilierungen mit MinGW-64 5.1.0 mit -pedantic-errorsund beim Testen mit den Online-Compilern unter gcc.godbolt.org/ auch mit Clang 3.0 und Clang 3.2, jedoch nicht mit Clang 3.3, 3.4 kompiliert . 1, 3.5.0, 3.5.1, 3.6 (rc1) oder 3.7 (experimentell). Und wichtig für die Windows-Plattform ist, dass sie nicht mit Visual C ++ 2015 kompiliert werden kann. Der Grund ist eine C ++ 11 / C ++ 14-Anweisung zur Verwendung von Referenzen inconstexpr Ausdrücken:

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

Ein bedingter Ausdruck e ist ein konstanter Kernausdruck, es sei denn, die Auswertung von enach den Regeln der abstrakten Maschine (1.9) würde einen der folgenden Ausdrücke
        auswerten : ⋮

  • Ein ID-Ausdruck , der auf eine Variable oder ein Datenelement des Referenztyps verweist, es sei denn, die Referenz hat eine vorhergehende Initialisierung und beides
    • es wird mit einem konstanten Ausdruck oder initialisiert
    • es ist ein nicht statisches Datenelement eines Objekts, dessen Lebensdauer mit der Auswertung von e begann;

Man kann immer ausführlicher schreiben

// 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 dies schlägt fehl, wenn Collectiones sich nicht um ein Raw-Array handelt.

Um mit Sammlungen umgehen zu können, die keine Arrays sein können, benötigt man die Überladbarkeit einer n_itemsFunktion, aber für die Kompilierungszeit benötigt man eine Kompilierungszeitdarstellung der Arraygröße. Und das klassische C ++ 03 - Lösung, die feine auch in C ++ gewohnt 11 und C ++ 14, ist die Funktion Bericht das Ergebnis nicht als Wert zu lassen , sondern über seine 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 );
}

Informationen zur Auswahl des Rückgabetyps für static_n_items: Dieser Code wird nicht verwendet, std::integral_constant da std::integral_constantdas Ergebnis direkt als constexprWert dargestellt wird, wodurch das ursprüngliche Problem wieder eingeführt wird. Anstelle einer Size_carrierKlasse kann man die Funktion direkt einen Verweis auf ein Array zurückgeben lassen. Allerdings ist nicht jeder mit dieser Syntax vertraut.

Über die Benennung: Teil dieser Lösung zum constexprInformationen Problem, dass aufgrund der Referenz ungültig ist, besteht darin, die Auswahl der Kompilierungszeitkonstante explizit festzulegen.

Hoffentlich wird das constexprProblem, das an Ihrem Problem beteiligt war, mit C ++ 17 behoben, aber bis dahin bietet ein Makro wie das STATIC_N_ITEMSoben beschriebene Portabilität, z. B. für die Clang- und Visual C ++ - Compiler, wobei der Typ beibehalten wird Sicherheit.

Verwandte Themen: Makros berücksichtigen keine Bereiche. Um Namenskollisionen zu vermeiden, empfiehlt es sich, ein Namenspräfix zu verwenden, z MYLIB_STATIC_N_ITEMS.


1
+1 Großartiger C-Codierungstest: Ich habe 15 Minuten mit VC ++ 10.0 und GCC 4.1.2 verbracht, um das Problem zu beheben Segmentation fault... Ich habe es nach dem Lesen Ihrer Erklärungen endlich gefunden / verstanden! Bitte schreiben Sie Ihren §5.2 Abschnitt :-) Cheers
olibre

Gut. Eine Nite - der Rückgabetyp für countOf sollte size_t anstelle von ptrdiff_t sein. Es ist wahrscheinlich erwähnenswert, dass es in C ++ 11/14 constexpr und noexcept sein sollte.
Ricky65

@ Ricky65: Danke, dass du C ++ 11-Überlegungen erwähnt hast. Die Unterstützung für diese Funktionen für Visual C ++ ist erst spät gekommen. Dies size_that keine Vorteile, die ich für moderne Plattformen kenne, aber es gibt eine Reihe von Problemen aufgrund der impliziten Typkonvertierungsregeln von C und C ++. Das heißt, ptrdiff_twird sehr absichtlich verwendet, um die Probleme mit zu vermeiden size_t. Man sollte sich jedoch bewusst sein, dass g ++ ein Problem mit der Anpassung der Arraygröße an den Vorlagenparameter hat, es sei denn size_t, dies size_tist der Fall (ich denke nicht, dass dieses compilerspezifische Problem mit nicht wichtig ist, aber YMMV).
Prost und hth. - Alf

@ Alf. Im Standard Working Draft (N3936) 8.3.4 habe ich gelesen - Die Grenze eines Arrays ist ... "Ein konvertierter konstanter Ausdruck vom Typ std :: size_t und sein Wert muss größer als Null sein".
Ricky65

@Ricky: Wenn Sie sich auf die Inkonsistenz beziehen, ist diese Anweisung im aktuellen C ++ 11-Standard nicht vorhanden, daher ist es schwierig, den Kontext zu erraten, aber den Widerspruch (ein dynamisch zugewiesenes Array kann pro C + 0 gebunden sein +11 §5.3.4 / 7) wird wahrscheinlich nicht in C ++ 14 enden. Entwürfe sind genau das: Entwürfe. Wenn Sie stattdessen fragen, worauf sich "its" bezieht, bezieht es sich auf den ursprünglichen Ausdruck, nicht auf den konvertierten. Wenn Sie dies auf der dritten Hand erwähnen, weil Sie der Meinung sind, dass ein solcher Satz möglicherweise bedeutet, dass man die size_tGröße von Arrays bezeichnen sollte, ist dies natürlich nicht der Fall.
Prost und hth. - Alf

72

Array-Erstellung und -Initialisierung

Wie bei jeder anderen Art von C ++ - Objekt können Arrays entweder direkt in benannten Variablen gespeichert werden (dann muss die Größe eine Konstante zur Kompilierungszeit sein; C ++ unterstützt keine VLAs ), oder sie können anonym auf dem Heap gespeichert werden und indirekt über abgerufen werden Zeiger (nur dann kann die Größe zur Laufzeit berechnet werden).

Automatische Arrays

Automatische Arrays (Arrays, die "auf dem Stapel" leben) werden jedes Mal erstellt, wenn der Steuerungsfluss die Definition einer nicht statischen lokalen Arrayvariablen 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 Tes sich um einen POD handelt (wie intim obigen Beispiel), findet keine Initialisierung statt.
  • Andernfalls Tinitialisiert der Standardkonstruktor von alle Elemente.
  • Wenn Tkein zugänglicher Standardkonstruktor bereitgestellt wird, wird das Programm nicht kompiliert.

Alternativ können die Anfangswerte explizit im Array-Initialisierer angegeben werden , einer durch Kommas getrennten 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 der Größe des Arrays entspricht, ist die manuelle Angabe der Größe überflüssig. Es 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 werden die verbleibenden Elemente mit Null initialisiert . Beachten Sie, dass C ++ einen leeren Array-Initialisierer zulässt (alle Elemente sind nullinitialisiert), C89 nicht (mindestens ein Wert ist erforderlich). Beachten Sie außerdem, dass Array-Initialisierer nur zum Initialisieren von Arrays verwendet werden können. Sie können später nicht mehr für Aufgaben verwendet werden.

Statische Arrays

Statische Arrays (Arrays, die "im Datensegment" leben)) sind lokale Arrayvariablen, die mit dem staticSchlüsselwort und den Arrayvariablen im Namespace-Bereich definiert werden ("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 des staticSchlüsselworts zu ihrer Definition hat eine völlig andere, veraltete Bedeutung .)

So verhalten sich statische Arrays anders als automatische Arrays:

  • Statische Arrays ohne Array-Initialisierer werden vor jeder weiteren möglichen Initialisierung auf Null initialisiert.
  • Statische POD-Arrays werden genau einmal initialisiert , und die Anfangswerte werden normalerweise in die ausführbare Datei eingebrannt. In diesem Fall fallen zur Laufzeit keine Initialisierungskosten an. Dies ist jedoch nicht immer die platzsparendste Lösung und wird vom Standard nicht verlangt.
  • Statische Nicht-POD-Arrays werden initialisiert, wenn der Steuerungsfluss zum ersten Mal ihre Definition durchläuft. Bei lokalen statischen Arrays kann dies niemals passieren, wenn die Funktion niemals aufgerufen wird.

(Keines der oben genannten Elemente ist spezifisch für Arrays. Diese Regeln gelten gleichermaßen für andere Arten statischer Objekte.)

Array-Datenelemente

Array-Datenelemente werden erstellt, wenn ihr eigenes Objekt erstellt wird. Leider bietet C ++ 03 keine Möglichkeit, Arrays in der Mitgliederinitialisierungsliste zu initialisieren. Daher muss die Initialisierung mit Zuweisungen gefälscht 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 kopieren über:

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 können in dem Element Initialisiererliste dank initialisiert werden 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 können sie nur über Zeiger darauf zugreifen. Da sie keine Namen haben, werde ich sie von nun an als "anonyme Arrays" bezeichnen.

In C werden anonyme Arrays über mallocund Freunde erstellt. In C ++ werden anonyme Arrays mithilfe der new T[size]Syntax erstellt, 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 mit 8 berechnet wird:

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

Offensichtlich benötigen anonyme Arrays aufgrund des zusätzlichen Zeigers, der separat gespeichert werden muss, mehr Speicher als benannte Arrays. (Es gibt auch einige zusätzliche Gemeinkosten für das kostenlose Geschäft.)

Beachten Sie, dass hier kein Array-zu-Zeiger-Zerfall stattfindet. Obwohl die Bewertung new int[size]ist in der Tat eine erstellen Array von ganzen Zahlen, das Ergebnis des Ausdrucks new int[size]ist bereits ein Zeiger auf eine einzelne ganze Zahl (das erste Element), nicht einen Array von ganzen Zahlen oder ein Zeiger auf ein Feld von ganzen Zahlen von unbekannter Größe. Dies wäre unmöglich, da das statische Typsystem erfordert, dass Arraygrößen Konstanten zur Kompilierungszeit sind. (Daher habe ich das anonyme Array nicht mit statischen Typinformationen im Bild versehen.)

In Bezug auf Standardwerte für Elemente 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 nachfolgende Klammerpaar direkt vor dem Semikolon.) Auch hier vereinfacht C ++ 0x die Regeln und ermöglicht dank der einheitlichen Initialisierung die Angabe von Anfangswerten für anonyme Arrays:

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 freigeben:

delete[] p;

Sie müssen jedes anonyme Array genau einmal freigeben und danach nie wieder berühren. Wenn Sie es überhaupt nicht freigeben, führt dies zu einem Speicherverlust (oder allgemeiner, je nach Elementtyp, zu einem Ressourcenverlust), und der Versuch, es mehrmals freizugeben, führt zu undefiniertem Verhalten. Die Verwendung des Nicht-Array-Formulars delete(oder free) anstelle der delete[]Freigabe des Arrays ist ebenfalls ein undefiniertes Verhalten .


2
Die Ablehnung der staticVerwendung im Namespace-Bereich wurde in C ++ 11 entfernt.
Legends2k

Da newes sich um einen Operator handelt, könnte er das zugewiesene Array sicherlich als Referenz zurückgeben. Es hat einfach keinen Sinn ...
Deduplicator

@Deduplicator Nein, es konnte nicht, weil historisch newviel älter als Referenzen ist.
Fredoverflow

@FredOverflow: Es gibt also einen Grund, warum es keine Referenz zurückgeben konnte, es unterscheidet sich nur völlig von der schriftlichen Erklärung.
Deduplikator

2
@Deduplicator Ich glaube nicht, dass ein Verweis auf ein Array unbekannter Grenzen existiert. Zumindest weigert sich g ++ zu kompilierenint a[10]; int (&r)[] = a;
Fredoverflow
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.