Jak korzystać z tablic w C ++?

480

C ++ odziedziczył tablice po C, gdzie są używane praktycznie wszędzie. C ++ zapewnia abstrakcje, które są łatwiejsze w użyciu i mniej podatne na błędy ( std::vector<T>od C ++ 98 i std::array<T, n>od C ++ 11 ), więc potrzeba tablic nie pojawia się tak często jak w C. Jednak podczas czytania starszej wersji kodować lub wchodzić w interakcje z biblioteką napisaną w C, powinieneś dobrze poznać działanie tablic.

To FAQ jest podzielone na pięć części:

  1. tablice na poziomie typu i dostęp do elementów
  2. tworzenie i inicjalizacja tablicy
  3. przypisanie i przekazywanie parametrów
  4. tablice wielowymiarowe i tablice wskaźników
  5. częste pułapki przy korzystaniu z tablic

Jeśli uważasz, że brakuje czegoś ważnego w tym FAQ, napisz odpowiedź i link tutaj jako dodatkową część.

W poniższym tekście „tablica” oznacza „tablicę C”, a nie szablon klasy std::array. Zakładana jest podstawowa znajomość składni deklaratora C. Pamiętaj, że ręczne użycie newi deletepokazane poniżej jest wyjątkowo niebezpieczne w obliczu wyjątków, ale jest to temat innego FAQ .

(Uwaga: ma to być wpis do często zadawanych pytań na temat C ++ w programie Stack Overflow . Jeśli chcesz skrytykować pomysł podania w tym formularzu odpowiedzi na najczęściej zadawane pytania, to miejsce na meta, które to wszystko rozpoczęło, byłoby odpowiednim miejscem. Odpowiedzi na to pytanie jest monitorowane w czacie C ++ , gdzie pomysł FAQ powstał w pierwszej kolejności, więc twoje odpowiedzi prawdopodobnie zostaną przeczytane przez tych, którzy wpadli na ten pomysł).

fredoverflow
źródło
Byłyby jeszcze lepsze, gdyby wskaźniki zawsze wskazywały na początek, a nie gdzieś w środku celu ...
Deduplicator
Powinieneś używać Vector STL, ponieważ zapewnia on większą elastyczność.
Moiz Sajid
2
W połączeniu z dostępnością std::arrays, std::vectorsi gsl::span- szczerze oczekiwałbym odpowiedzi na często zadawane pytania na temat używania tablic w C ++, aby powiedzieć „Do tej pory możesz zacząć rozważać, po prostu, no cóż, nieużywanie ich”.
einpoklum

Odpowiedzi:

302

Tablice na poziomie typu

Typ tablicy jest oznaczony jako T[n]gdzie Tjest typem elementu i njest dodatnim rozmiarem , liczbą elementów w tablicy. Typ tablicy jest rodzajem produktu typu elementu i rozmiaru. Jeśli jeden lub oba te składniki różnią się, otrzymujesz wyraźny 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");

Zauważ, że rozmiar jest częścią typu, to znaczy typy tablic o różnych rozmiarach są typami niekompatybilnymi, które absolutnie nie mają ze sobą nic wspólnego. sizeof(T[n])jest równoważne z n * sizeof(T).

Rozpad matrycy na wskaźnik

Jedynym „połączeniem” pomiędzy T[n]i T[m]jest to, że oba typy można w sposób niejawny przekonwertować na T*, a wynikiem tej konwersji jest wskaźnik do pierwszego elementu tablicy. Oznacza to, że wszędzie tam, gdzie jest T*to wymagane, możesz podać a T[n], a kompilator po cichu dostarczy ten wskaźnik:

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

Ta konwersja jest znana jako „rozpad tablicy na wskaźnik” i jest głównym źródłem nieporozumień. Rozmiar tablicy jest tracony w tym procesie, ponieważ nie jest już częścią type ( T*). Pro: Zapomnienie wielkości tablicy na poziomie typu pozwala wskaźnikowi wskazywać pierwszy element tablicy o dowolnym rozmiarze. Przeciw: Biorąc pod uwagę wskaźnik do pierwszego (lub dowolnego innego) elementu tablicy, nie ma sposobu na wykrycie, jak duża jest ta tablica lub gdzie dokładnie wskaźnik wskazuje w stosunku do granic tablicy. Wskaźniki są wyjątkowo głupie .

Tablice nie są wskaźnikami

Kompilator po cichu wygeneruje wskaźnik do pierwszego elementu tablicy, ilekroć zostanie to uznane za przydatne, to znaczy za każdym razem, gdy operacja zakończy się niepowodzeniem na tablicy, ale odniesie sukces na wskaźniku. Ta konwersja z tablicy na wskaźnik jest trywialna, ponieważ wynikowa wartość wskaźnika to po prostu adres tablicy. Zauważ, że wskaźnik nie jest przechowywany jako część samej tablicy (ani nigdzie indziej w pamięci). Tablica nie jest wskaźnikiem.

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

Jednym z ważnych kontekstów, w których tablica nie rozpada się na wskaźnik do pierwszego elementu, jest zastosowanie &operatora. W takim przypadku &operator podaje wskaźnik do całej tablicy, a nie tylko wskaźnik do pierwszego elementu. Chociaż w tym przypadku wartości (adresy) są takie same, wskaźnik do pierwszego elementu tablicy i wskaźnik do całej tablicy są całkowicie różnymi typami:

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

Następująca sztuka ASCII wyjaśnia to rozróżnienie:

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

Zauważ, że wskaźnik do pierwszego elementu wskazuje tylko na jedną liczbę całkowitą (przedstawioną jako małe pole), podczas gdy wskaźnik do całej tablicy wskazuje na tablicę 8 liczb całkowitych (przedstawionych jako duże pole).

Ta sama sytuacja występuje na zajęciach i może jest bardziej oczywista. Wskaźnik do obiektu i wskaźnik do pierwszego elementu danych mają tę samą wartość (ten sam adres), ale są to całkowicie różne typy.

Jeśli nie znasz składni deklaratora C, nawiasy tego typu int(*)[8]są niezbędne:

  • int(*)[8] jest wskaźnikiem do tablicy 8 liczb całkowitych.
  • int*[8]jest tablicą 8 wskaźników, każdy element typu int*.

Dostęp do elementów

C ++ zapewnia dwie odmiany składniowe umożliwiające dostęp do poszczególnych elementów tablicy. Żadna z nich nie jest lepsza od drugiej i powinieneś zapoznać się z obiema.

Wskaźnik arytmetyczny

Biorąc pod uwagę wskaźnik pdo pierwszego elementu tablicy, wyrażenie p+idaje wskaźnik do i-tego elementu tablicy. Odsyłając później ten wskaźnik, można uzyskać dostęp do poszczególnych elementów:

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

Jeśli xoznacza tablicę , rozpocznie się rozpad tablicy na wskaźnik, ponieważ dodanie tablicy i liczby całkowitej jest bez znaczenia (nie ma operacji dodatniej na tablicach), ale dodanie wskaźnika i liczby całkowitej ma sens:

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

(Zauważ, że niejawnie wygenerowany wskaźnik nie ma nazwy, więc napisałem x+0, aby go zidentyfikować).

Jeśli natomiast xoznacza wskaźnik do pierwszego (lub dowolnego innego) elementu tablicy, wówczas zanikanie tablic do wskaźników nie jest konieczne, ponieważ wskaźnik, do którego ima zostać dodany, już istnieje:

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

Zauważ, że w przedstawionym przypadku zmiennax wskaźnikowa (dostrzegalna przez małe pole obok ), ale równie dobrze może być wynikiem funkcji zwracającej wskaźnik (lub dowolne inne wyrażenie typu ).xT*

Operator indeksowania

Ponieważ składnia *(x+i)jest nieco niezdarna, C ++ zapewnia alternatywną składnię x[i]:

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

Z uwagi na fakt, że dodawanie jest przemienne, następujący kod robi dokładnie to samo:

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

Definicja operatora indeksowania prowadzi do następującej interesującej równoważności:

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

Jednak &x[0]ogólnie nie jest równoważny z x. Pierwsza jest wskaźnikiem, druga tablicą. Tylko wtedy, gdy rozpad wyzwalacze kontekstowe tablicy do wskaźnika może xi &x[0]być stosowane zamiennie. Na przykład:

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

W pierwszym wierszu kompilator wykrywa przypisanie ze wskaźnika do wskaźnika, co w prosty sposób się powiedzie. W drugim wierszu wykrywa przypisanie z tablicy do wskaźnika. Ponieważ nie ma to sensu (ale przypisywanie wskaźnika do wskaźnika ma sens), rozpad tablicy na wskaźnik rozpoczyna się jak zwykle.

Zakresy

Tablica typu T[n]ma nelementy indeksowane od 0do n-1; nie ma elementu n. A jednak, aby obsługiwać półotwarte zakresy (gdzie początek jest włączający, a koniec wyłączny ), C ++ pozwala na obliczenie wskaźnika na (nieistniejącym) n-tym elemencie, ale niedozwolone jest dereferencje tego wskaźnika:

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

Na przykład, jeśli chcesz posortować tablicę, oba następujące elementy będą działać równie dobrze:

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

Zauważ, że podanie &x[n]drugiego argumentu jest nielegalne, ponieważ jest ono równoważne &*(x+n), a *(x+n)podwyrażenie technicznie wywołuje niezdefiniowane zachowanie w C ++ (ale nie w C99).

Pamiętaj też, że możesz podać xjako pierwszy argument. Jest to trochę zbyt zwięzłe jak na mój gust, a także komplikuje nieco dedukcję argumentów szablonów, ponieważ w tym przypadku pierwszy argument jest tablicą, a drugi jest wskaźnikiem. (Znowu rozpoczyna się rozpad tablicy na wskaźnik).

fredoverflow
źródło
Przypadki, w których tablica nie rozpada się na wskaźnik, pokazano tutaj w celach informacyjnych.
legends2k
@fredoverflow W części Access lub Ranges warto wspomnieć, że tablice C działają z pętlami opartymi na zakresie C ++ 11.
gnzlbg
135

Programiści często mylą tablice wielowymiarowe z tablicami wskaźników.

Tablice wielowymiarowe

Większość programistów zna znane tablice wielowymiarowe, ale wielu nie zdaje sobie sprawy z tego, że tablice wielowymiarowe można również tworzyć anonimowo. Tablice wielowymiarowe są często nazywane „tablicami tablic” lub „ prawdziwymi tablicami wielowymiarowymi”.

Nazwane tablice wielowymiarowe

W przypadku używania nazwanych tablic wielowymiarowych wszystkie wymiary muszą być znane w czasie kompilacji:

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

Tak wygląda nazwana tablica wielowymiarowa w pamięci:

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

Pamiętaj, że siatki 2D, takie jak powyższe, są jedynie pomocnymi wizualizacjami. Z punktu widzenia C ++ pamięć jest „płaską” sekwencją bajtów. Elementy tablicy wielowymiarowej są przechowywane w kolejności rzędów. To znaczy connect_four[0][6]i connect_four[1][0]są sąsiadami w pamięci. W rzeczywistości connect_four[0][7]i connect_four[1][0]oznacz ten sam element! Oznacza to, że możesz pobierać tablice wielowymiarowe i traktować je jako duże tablice jednowymiarowe:

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

Anonimowe tablice wielowymiarowe

W przypadku anonimowych tablic wielowymiarowych wszystkie wymiary oprócz pierwszego muszą być znane w czasie kompilacji:

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

Tak wygląda anonimowa tablica wielowymiarowa w pamięci:

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

Zauważ, że sama tablica jest nadal przydzielona jako pojedynczy blok w pamięci.

Tablice wskaźników

Możesz pokonać ograniczenie stałej szerokości, wprowadzając kolejny poziom pośredni.

Nazwane tablice wskaźników

Oto nazwana tablica pięciu wskaźników, które są inicjowane anonimowymi tablicami o różnych długościach:

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

A oto jak to wygląda w pamięci:

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

Ponieważ teraz każda linia jest przydzielana indywidualnie, wyświetlanie tablic 2D jako tablic 1D już nie działa.

Anonimowe tablice wskaźników

Oto anonimowa tablica 5 (lub dowolnej innej liczby) wskaźników, które są inicjowane anonimowymi tablicami o różnych długościach:

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

A oto jak to wygląda w pamięci:

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

Konwersje

Rozkład z matrycy na wskaźnik naturalnie rozciąga się na tablice tablic i tablice wskaźników:

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;

Nie ma jednak domyślnej konwersji z T[h][w]na T**. Gdyby taka niejawna konwersja istniała, wynikiem byłby wskaźnik do pierwszego elementu tablicy hwskaźników do T(każdy wskazuje na pierwszy element linii w oryginalnej tablicy 2D), ale ta tablica wskaźników nie istnieje nigdzie w pamięć jeszcze. Jeśli chcesz takiej konwersji, musisz ręcznie utworzyć i wypełnić wymaganą tablicę wskaźników:

int connect_four[6][7];

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

// ...

delete[] p;

Zauważ, że generuje to widok oryginalnej wielowymiarowej tablicy. Jeśli zamiast tego potrzebujesz kopii, musisz utworzyć dodatkowe tablice i samodzielnie skopiować dane:

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;
fredoverflow
źródło
Jako sugestia: powinieneś zaznaczyć, że int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];podobnie jak int (*p)[W] = new int[6][W];i int (*p)[W] = new int[H][W];są poprawnymi stwierdzeniami, kiedy Hi Wsą znane w czasie kompilacji.
RobertS wspiera Monikę Cellio
88

Zadanie

Bez konkretnego powodu tablic nie można przypisywać sobie nawzajem. std::copyZamiast tego użyj :

#include <algorithm>

// ...

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

Jest to bardziej elastyczne niż to, co mogłoby zapewnić prawdziwe przypisanie tablic, ponieważ możliwe jest kopiowanie wycinków większych tablic na mniejsze tablice. std::copyzwykle specjalizuje się w typach pierwotnych, aby zapewnić maksymalną wydajność. Jest mało prawdopodobne, aby std::memcpydziałał lepiej. W razie wątpliwości zmierz.

Chociaż nie można bezpośrednio przypisywać tablic, można przypisywać struktury i klasy zawierające elementy tablicy. Jest tak, ponieważ elementy tablicy są kopiowane członkami przez operatora przypisania, który jest domyślnie dostarczany przez kompilator. Jeśli ręcznie zdefiniujesz operator przypisania dla własnych typów struktur lub klas, musisz wrócić do ręcznego kopiowania dla elementów tablicy.

Przekazywanie parametrów

Tablice nie mogą być przekazywane przez wartość. Możesz przekazać je wskaźnikiem lub referencją.

Przejdź obok wskaźnika

Ponieważ same tablice nie mogą być przekazywane przez wartość, zwykle wskaźnik do ich pierwszego elementu jest przekazywany przez wartość. Nazywa się to często „pomijaniem wskaźnika”. Ponieważ rozmiar tablicy nie jest możliwy do odzyskania za pośrednictwem tego wskaźnika, musisz przekazać drugi parametr wskazujący rozmiar tablicy (klasyczne rozwiązanie C) lub drugi wskaźnik wskazujący za ostatnim elementem tablicy (rozwiązanie iteratora 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);
}

Jako alternatywę składniową możesz również zadeklarować parametry jako T p[]i oznacza to dokładnie to samo, co T* p w kontekście tylko list parametrów :

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

Możesz myśleć o kompilatorze jako przepisywaniu T p[]tylko T *p w kontekście list parametrów . Ta specjalna zasada jest częściowo odpowiedzialna za całe zamieszanie dotyczące tablic i wskaźników. W każdym innym kontekście deklarowanie czegoś jako tablicy lub wskaźnika ma ogromną różnicę.

Niestety, możesz również podać rozmiar w parametrze tablicy, który jest dyskretnie ignorowany przez kompilator. Oznacza to, że następujące trzy podpisy są dokładnie równoważne, na co wskazują błędy kompilatora:

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

Przekaż przez odniesienie

Tablice można również przekazać przez odniesienie:

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

W takim przypadku rozmiar tablicy jest znaczący. Ponieważ pisanie funkcji, która akceptuje tylko tablice dokładnie 8 elementów, jest mało przydatne, programiści zwykle piszą takie funkcje jak szablony:

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

Zauważ, że możesz wywołać taki szablon funkcji tylko z rzeczywistą tablicą liczb całkowitych, a nie ze wskaźnikiem do liczby całkowitej. Rozmiar tablicy jest automatycznie wywnioskowany i dla każdego rozmiaru ntworzona jest inna funkcja z szablonu. Możesz także pisać całkiem przydatne szablony funkcji, które wyodrębniają zarówno typ elementu, jak i rozmiar.

fredoverflow
źródło
2
Może warto dodać notatkę, że nawet void foo(int a[3]) ajeśli wygląda na to, że przekazujemy tablicę według wartości, modyfikacja awewnątrz foospowoduje modyfikację oryginalnej tablicy. Powinno to być jasne, ponieważ tablic nie można skopiować, ale warto je wzmocnić.
gnzlbg
C ++ 20 maranges::copy(a, b)
LF
int sum( int size_, int a[size_]);- od (chyba) C99 wzwyż
szef kuchni Gladiator
73

5. Typowe pułapki przy korzystaniu z tablic.

5.1 Pułapka: ufanie linkom niebezpiecznym dla typu.

OK, powiedziano ci lub przekonałeś się, że globale (zmienne zakresu nazw, do których można uzyskać dostęp poza jednostką tłumaczącą) są Evil ™. Ale czy wiesz, jak naprawdę są Evil ™? Rozważ poniższy program, składający się z dwóch plików [main.cpp] i [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};

W Windows 7 kompiluje się i łączy dobrze zarówno z MinGW g ++ 4.4.1, jak i Visual C ++ 10.0.

Ponieważ typy się nie zgadzają, program ulega awarii po uruchomieniu.

Okno dialogowe awarii systemu Windows 7

Wyjaśnienie formalne: program ma niezdefiniowane zachowanie (UB) i zamiast zawieszać się, może po prostu zawiesić się, a może nic nie robić, lub może wysyłać groźne wiadomości e-mail do prezydentów USA, Rosji, Indii, Chiny i Szwajcaria i spraw, aby demony nosowe wyleciały z twojego nosa.

Wyjaśnienie praktyczne: w main.cpptablicy jest traktowany jako wskaźnik, umieszczony pod tym samym adresem co tablica. W przypadku 32-bitowego pliku wykonywalnego oznacza to, że pierwsza intwartość w tablicy jest traktowana jako wskaźnik. Czyli w zmienna zawiera, lub wydaje się zawierać . Powoduje to, że program uzyskuje dostęp do pamięci na samym dole przestrzeni adresowej, która jest tradycyjnie zarezerwowana i powoduje pułapki. Wynik: masz awarię.main.cppnumbers(int*)1

Kompilatory mają pełne prawo nie zdiagnozować tego błędu, ponieważ C ++ 11 §3.5 / 10 mówi o wymaganiu kompatybilnych typów deklaracji,

[N3290 §3,5 / 10]
Naruszenie tej reguły dotyczącej tożsamości typu nie wymaga diagnozy.

W tym samym akapicie opisano dopuszczalną odmianę:

… Deklaracje dla obiektu tablicowego mogą określać typy tablic, które różnią się obecnością lub brakiem głównej powiązanej tablicy (8.3.4).

Ta dozwolona odmiana nie obejmuje zadeklarowania nazwy jako tablicy w jednej jednostce tłumaczenia oraz jako wskaźnika w innej jednostce tłumaczenia.

5.2 Pułapka: Przedwczesna optymalizacja ( memseti przyjaciele).

Jeszcze nie napisane

5.3 Pułapka: użycie idiomu C w celu uzyskania liczby elementów.

Dzięki głębokiemu doświadczeniu w C pisanie…

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

Ponieważ arrayrozpada się na wskaźnik do pierwszego elementu w razie potrzeby, wyrażenie sizeof(a)/sizeof(a[0])można również zapisać jako sizeof(a)/sizeof(*a). Oznacza to to samo i bez względu na to, jak jest napisane, jest to idiom C do znajdowania elementów liczbowych tablicy.

Główna pułapka: idiom C nie jest bezpieczny dla typów. Na przykład kod…

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

przekazuje wskaźnik do N_ITEMS, a zatem najprawdopodobniej daje zły wynik. Skompilowany jako 32-bitowy plik wykonywalny w systemie Windows 7 produkuje…

7 elementów, wywoływanie wyświetlacza ...
1 elementów.

  1. Kompilator przepisuje int const a[7]na just int const a[].
  2. Kompilator przepisuje int const a[]na int const* a.
  3. N_ITEMS jest zatem wywoływany za pomocą wskaźnika.
  4. W przypadku 32-bitowego pliku wykonywalnego sizeof(array)(rozmiar wskaźnika) wynosi wtedy 4.
  5. sizeof(*array)jest równoważne sizeof(int), który dla 32-bitowego pliku wykonywalnego to również 4.

Aby wykryć ten błąd w czasie wykonywania, możesz…

#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 elementów, wywoływanie display ...
Asercja nie powiodła się: („N_ITEMS wymaga rzeczywistej tablicy jako argumentu”, typeid (a)! = Typeid (& * a)), plik runtime_detect ion.cpp, wiersz 16

Ta aplikacja poprosiła środowisko wykonawcze o zakończenie go w nietypowy sposób.
Skontaktuj się z zespołem pomocy technicznej aplikacji, aby uzyskać więcej informacji.

Wykrywanie błędów w czasie wykonywania jest lepsze niż brak wykrywania, ale marnuje trochę czasu procesora i być może znacznie więcej czasu programisty. Lepiej z wykrywaniem w czasie kompilacji! A jeśli nie chcesz obsługiwać tablic typów lokalnych w C ++ 98, możesz to zrobić:

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

Kompilując tę ​​definicję podstawioną do pierwszego kompletnego programu z g ++, mam…

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: W funkcji 'void display (const int *)':
compile_time_detection.cpp: 14: error: brak pasującej funkcji dla wywołania 'n_items (const int * &)'

M: \ count> _

Jak to działa: tablica jest przekazywana przez odwołanie do n_items, a więc nie rozkłada się na wskaźnik do pierwszego elementu, a funkcja może po prostu zwrócić liczbę elementów określoną przez typ.

W C ++ 11 możesz tego również używać do tablic typu lokalnego, i jest to bezpieczny typ języka C ++ do znajdowania liczby elementów tablicy.

5.4 Pułapka C ++ 11 i C ++ 14: Korzystanie z constexprfunkcji rozmiaru tablicy.

Z C ++ 11 i nowszymi jest to naturalne, ale jak zobaczysz niebezpieczne !, zastąpienie funkcji C ++ 03

typedef ptrdiff_t   Size;

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

z

using Size = ptrdiff_t;

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

gdzie istotną zmianą jest użycie constexpr, co pozwala tej funkcji wygenerować stałą czasową kompilacji .

Na przykład, w przeciwieństwie do funkcji C ++ 03, taką stałą czasową kompilacji można wykorzystać do zadeklarowania tablicy o tym samym rozmiarze co inna:

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

Ale rozważ ten kod za pomocą constexprwersji:

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

Pułapka: od lipca 2015 r. Powyższe kompiluje się z MinGW-64 5.1.0 z -pedantic-errorstestami z kompilatorami online na gcc.godbolt.org/ , również z clang 3.0 i clang 3.2, ale nie z clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) lub 3.7 (doświadczalnie). I ważne dla platformy Windows, nie kompiluje się z Visual C ++ 2015. Powodem jest instrukcja C ++ 11 / C ++ 14 na temat używania referencji w constexprwyrażeniach:

C ++ C ++ 11 14 $ 5,19 / 2 dziewięć th kreska

Warunkowe wyrażenie e jest rdzeń stałym wyrażeniem chyba oceny e, zgodnie z zasadami abstrakcyjnej maszynie (1.9), by ocenić jedno z następujących wyrażeń:
        ⋮

  • ID ekspresja , który odnosi się do elementu lub zmiennych danych typu o ile z wzorcowy poprzedzający inicjalizacji i albo
    • jest inicjowany stałym wyrażeniem lub
    • jest niestatycznym elementem danych obiektu, którego okres istnienia rozpoczął się w ramach oceny e;

Zawsze można napisać bardziej szczegółowe

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

… Ale to się nie udaje, gdy Collectionnie jest surową tablicą.

Aby poradzić sobie z kolekcjami, które mogą być nie-tablicami, potrzebna jest przeciążalność n_itemsfunkcji, ale także, do wykorzystania w czasie kompilacji, potrzebna jest reprezentacja rozmiaru tablicy w czasie kompilacji. A klasyczne rozwiązanie C ++ 03, które działa dobrze również w C ++ 11 i C ++ 14, pozwala funkcji zgłaszać swój wynik nie jako wartość, ale poprzez typ wyniku funkcji . Na przykład tak:

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

O wyborze typu zwrotu dla static_n_items: ten kod nie używa, std::integral_constant ponieważ std::integral_constantwynik jest reprezentowany bezpośrednio jako constexprwartość, przywracając pierwotny problem. Zamiast Size_carrierklasy można pozwolić, aby funkcja bezpośrednio zwróciła odwołanie do tablicy. Jednak nie wszyscy znają tę składnię.

Informacje na temat nazewnictwa: częścią tego rozwiązania problemu constexpr-invalid-due-to-reference jest wyraźne wybranie stałej czasowej kompilacji.

Mam nadzieję, że constexprproblem zostanie rozwiązany w C ++ 17, ale do tego czasu makro takie jak STATIC_N_ITEMSpowyższe zapewnia przenośność, np. Do kompilatora clang i Visual C ++, zachowując typ bezpieczeństwo.

Powiązane: makra nie respektują zakresów, więc aby uniknąć kolizji nazw, dobrym pomysłem może być użycie przedrostka nazwy, np MYLIB_STATIC_N_ITEMS.

Pozdrawiam i hth. - Alf
źródło
1
+1 Świetny test kodowania C: spędziłem 15 minut na VC ++ 10.0 i GCC 4.1.2 próbując naprawić Segmentation fault... W końcu znalazłem / zrozumiałem po przeczytaniu twoich wyjaśnień! Proszę napisać sekcję
§5.2
Dobrze. One nit - zwracany typ dla countOf powinien być size_t zamiast ptrdiff_t. Prawdopodobnie warto wspomnieć, że w C ++ 11/14 powinno to być constexpr i noexcept.
Ricky65
@ Ricky65: Dziękujemy za wzmiankę o C ++ 11. Obsługa tych funkcji jest już spóźniona w Visual C ++. Jeśli chodzi o size_t, nie ma to żadnych zalet, które znam dla współczesnych platform, ale ma wiele problemów z powodu niejawnych reguł konwersji typów C i C ++. Oznacza to, że ptrdiff_tjest używany bardzo celowo, aby uniknąć problemów size_t. Należy jednak pamiętać, że g ++ ma problem z dopasowaniem rozmiaru tablicy do parametru szablonu, chyba że jest to size_t(nie sądzę, że ten specyficzny dla kompilatora problem z nie- size_tjest ważny, ale YMMV).
Pozdrawiam i hth. - Alf
@Alf. W Standardowym szkicu roboczym (N3936) 8.3.4 czytam - Granicą tablicy jest ... „przekonwertowane wyrażenie stałe typu std :: size_t, a jego wartość powinna być większa od zera”.
Ricky65
@Ricky: Jeśli masz na myśli niespójność, tego stwierdzenia nie ma w obecnym standardzie C ++ 11, więc trudno odgadnąć kontekst, ale sprzeczność (dynamicznie alokowana tablica może mieć granicę 0, na C + +11 § 5.3.4 / 7) prawdopodobnie nie zakończy się w C ++ 14. Szkice są po prostu takie: szkice. Jeśli zamiast tego pytasz o to, do czego odnosi się „jego”, odnosi się to do oryginalnego wyrażenia, a nie do przekonwertowanego. Jeśli z drugiej strony wspominasz o tym, ponieważ uważasz, że być może takie zdanie oznacza, że ​​należy użyć size_tdo oznaczenia rozmiarów tablic, oczywiście nie.
Pozdrawiam i hth. - Alf
72

Tworzenie i inicjalizacja tablicy

Podobnie jak w przypadku każdego innego obiektu C ++, tablice mogą być przechowywane bezpośrednio w nazwanych zmiennych (wówczas rozmiar musi być stałą czasową kompilacji; C ++ nie obsługuje VLA ), lub mogą być przechowywane anonimowo na stercie i dostępne pośrednio poprzez wskaźniki (tylko wtedy rozmiar można obliczyć w czasie wykonywania).

Automatyczne tablice

Automatyczne tablice (tablice żyjące „na stosie”) są tworzone za każdym razem, gdy przepływ kontroli przechodzi przez definicję niestatycznej zmiennej lokalnej:

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

Inicjalizacja odbywa się w kolejności rosnącej. Zauważ, że początkowe wartości zależą od typu elementu T:

  • Jeśli Tjest POD (jak intw powyższym przykładzie), inicjalizacja nie ma miejsca.
  • W przeciwnym razie domyślny konstruktor Tinicjuje wszystkie elementy.
  • Jeśli Tnie ma dostępnego domyślnego konstruktora, program się nie kompiluje.

Alternatywnie wartości początkowe można jawnie określić w inicjatorze tablicowym , liście oddzielonej przecinkami otoczonej nawiasami klamrowymi:

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

Ponieważ w tym przypadku liczba elementów w inicjatorze tablicy jest równa rozmiarowi tablicy, ręczne określenie rozmiaru jest zbędne. Kompilator może wydedukować to automatycznie:

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

Możliwe jest również określenie rozmiaru i krótszy inicjator tablicy:

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

W takim przypadku pozostałe elementy są inicjowane zerem . Zauważ, że C ++ pozwala na inicjalizację pustej tablicy (wszystkie elementy są inicjowane zerem), podczas gdy C89 nie (wymagana jest co najmniej jedna wartość). Zauważ też, że inicjalizatory tablic mogą być używane tylko do inicjalizacji tablic; nie można ich później użyć w zadaniach.

Tablice statyczne

Tablice statyczne (tablice żyjące „w segmencie danych”) są lokalnymi zmiennymi tablicowymi zdefiniowanymi za pomocą staticsłowa kluczowego i zmiennych tablic w zakresie przestrzeni nazw („zmienne globalne”):

int global_static_array[8];

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

(Należy pamiętać, że zmienne w zakresie przestrzeni nazw są domyślnie statyczne. Dodanie staticsłowa kluczowego do ich definicji ma zupełnie inne, przestarzałe znaczenie ).

Oto, w jaki sposób tablice statyczne zachowują się inaczej niż tablice automatyczne:

  • Macierze statyczne bez inicjatora macierzy są zerowane przed każdą kolejną potencjalną inicjalizacją.
  • Statyczne tablice POD są inicjowane dokładnie raz , a wartości początkowe są zazwyczaj zapamiętywane w pliku wykonywalnym, w którym to przypadku nie ma kosztu inicjalizacji w czasie wykonywania. Nie zawsze jest to jednak najbardziej wydajne rozwiązanie i nie jest wymagane przez standard.
  • Matryce statyczne inne niż POD są inicjowane za pierwszym razem, gdy przepływ kontroli przechodzi przez ich definicję. W przypadku lokalnych tablic statycznych może się to nigdy nie zdarzyć, jeśli funkcja nigdy nie zostanie wywołana.

(Żadne z powyższych nie jest specyficzne dla tablic. Te zasady mają zastosowanie równie dobrze do innych rodzajów obiektów statycznych).

Tablica członków danych

Elementy danych macierzy są tworzone podczas tworzenia ich obiektu będącego właścicielem. Niestety, C ++ 03 nie ma możliwości inicjowania tablic na liście inicjalizującej członka , więc inicjacja musi zostać sfałszowana przy pomocy przypisań:

class Foo
{
    int primes[8];

public:

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

Alternatywnie możesz zdefiniować automatyczną tablicę w ciele konstruktora i skopiować elementy:

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

W C ++ 0x tablice mogą być inicjowane na liście inicjalizującej członka dzięki jednolitej inicjalizacji :

class Foo
{
    int primes[8];

public:

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

To jedyne rozwiązanie, które działa z typami elementów, które nie mają domyślnego konstruktora.

Dynamiczne tablice

Tablice dynamiczne nie mają nazw, dlatego jedynym sposobem na dostęp do nich są wskaźniki. Ponieważ nie mają nazw, odtąd będę je nazywać „tablicami anonimowymi”.

W C anonimowe tablice są tworzone przez malloci znajomych. W C ++ anonimowe tablice są tworzone przy użyciu new T[size]składni, która zwraca wskaźnik do pierwszego elementu anonimowej tablicy:

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

Poniższa grafika ASCII przedstawia układ pamięci, jeśli rozmiar jest obliczany jako 8 w czasie wykonywania:

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

Oczywiście anonimowe tablice wymagają więcej pamięci niż tablice nazwane ze względu na dodatkowy wskaźnik, który musi być przechowywany osobno. (Istnieje również dodatkowe koszty ogólne w bezpłatnym sklepie).

Zauważ, że nie ma tu miejsca rozpad tablicy na wskaźnik. Chociaż ocena new int[size]faktycznie tworzy tablicę liczb całkowitych, wynikiem wyrażenia new int[size]jest już wskaźnik do pojedynczej liczby całkowitej (pierwszy element), a nie tablica liczb całkowitych lub wskaźnik do tablicy liczb całkowitych o nieznanym rozmiarze. Byłoby to niemożliwe, ponieważ system typu statycznego wymaga, aby rozmiary tablic były stałymi w czasie kompilacji. (W związku z tym nie zanotowałem anonimowej tablicy z informacjami typu statycznego na obrazie).

Jeśli chodzi o wartości domyślne elementów, anonimowe tablice zachowują się podobnie do tablic automatycznych. Zwykle anonimowe tablice POD nie są inicjowane, ale istnieje specjalna składnia, która uruchamia inicjalizację wartości:

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

(Zwróć uwagę na końcową parę nawiasów tuż przed średnikiem). Ponownie, C ++ 0x upraszcza reguły i pozwala na określenie początkowych wartości anonimowych tablic dzięki jednolitej inicjalizacji:

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

Jeśli zakończysz korzystanie z anonimowej tablicy, musisz ją zwolnić z powrotem do systemu:

delete[] p;

Musisz zwolnić każdą anonimową tablicę dokładnie raz, a następnie nigdy więcej jej nie dotykać. Nie zwolnienie go wcale powoduje wyciek pamięci (lub bardziej ogólnie, w zależności od typu elementu, wyciek zasobów), a próba wielokrotnego zwolnienia powoduje niezdefiniowane zachowanie. Użycie formy innej niż tablica delete(lub free) zamiast delete[]do zwolnienia tablicy jest również niezdefiniowanym zachowaniem .

fredoverflow
źródło
2
Przestarzałe staticużycie w zakresie przestrzeni nazw zostało usunięte w C ++ 11.
legends2k
Ponieważ newjest operatorem am, z pewnością mógłby zwrócić przydzieloną tablicę przez odniesienie. Po prostu nie ma sensu ...
Deduplicator
@Deduplicator Nie, nie mógł, ponieważ historycznie newjest znacznie starszy niż referencje.
fredoverflow
@FredOverflow: Jest więc powód, dla którego nie mógł zwrócić referencji, po prostu zupełnie różni się od pisemnego wyjaśnienia.
Deduplicator
2
@Deduplicator Nie sądzę, aby istniało odniesienie do tablicy nieznanych granic. Przynajmniej g ++ odmawia kompilacjiint a[10]; int (&r)[] = a;
fredoverflow