Jak używać tablic w C++?

C++ odziedziczył tablice z C, gdzie są używane praktycznie wszędzie. C++ dostarcza 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> ponieważ C++11), więc potrzeba tablic nie pojawia się tak często, jak w C. Jednak, gdy czytasz starszy kod lub wchodzisz w interakcję z biblioteką napisaną w C, powinieneś mieć pewne pojęcie o tym, jak tablice działają.

Ten FAQ jest podzielony na pięć części:

  1. tablice na typ poziom i dostęp do elementów
  2. tworzenie i inicjalizacja tablic
  3. przypisanie i przekazywanie parametrów
  4. wielowymiarowe tablice i tablice wskaźników
  5. typowe pułapki podczas korzystania z tablic

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

W poniższym tekście "array" oznacza "tablicę C", a nie szablon klasy std::array. Przyjmuje się podstawową znajomość składni deklaratora C. Zauważ, że ręczne użycie new i delete, Jak pokazano poniżej, jest niezwykle niebezpieczne w obliczu wyjątków, ale to jest temat innego FAQ .

(uwaga: jest to wpis do Stack Overflow ' S C++ FAQ . Jeśli chcesz krytykować pomysł dostarczenia FAQ w tej formie, to post na meta, który rozpoczął to wszystko , byłby miejscem, aby to zrobić. Odpowiedzi na to pytania są monitorowane w C++ chatroom , gdzie pomysł FAQ zaczął się w pierwszej kolejności, więc Twoja odpowiedź jest bardzo prawdopodobne, aby przeczytać przez tych, którzy wpadli na pomysł.)

Author: Community, 2011-01-27

5 answers

Tablice na poziomie typu

Typ tablicy jest oznaczany jako T[n] gdzie T jest typem elementu i n jest dodatnim rozmiarem , liczbą elementów w tablicy. Typ tablicy jest typem produktu Typu elementu i rozmiaru. Jeśli jeden lub oba te składniki różnią się, otrzymujesz odrębny 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, czyli typy tablic o różnej wielkości są typami niekompatybilnymi, które nie mają absolutnie nic do zrobienia ze sobą. sizeof(T[n]) jest równoważne n * sizeof(T).

Rozkład tablicy na wskaźnik

Jedynym "połączeniem" pomiędzy T[n] i T[m]jest to, że oba typy mogą być domyślnie przekonwertowane na T*, a wynikiem tej konwersji jest wskaźnik do pierwszego elementu tablicy. Oznacza to, że wszędzie tam, gdzie wymagane jest T*, możesz podać 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" , a to jest głównym źródłem zamieszania. Rozmiar tablicy jest tracony w tym procesie, ponieważ nie jest już częścią typu (T*). Pro: zapomnienie rozmiaru tablicy na poziomie typu pozwala wskaźnikowi wskazać pierwszy element tablicy o dowolnym rozmiarze. Con: biorąc pod uwagę wskaźnik do pierwszego (lub jakiegokolwiek innego) elementu tablicy, nie ma możliwości wykrycia, jak duża jest tablica lub gdzie dokładnie wskazuje wskaźnik względem granic tablicy. wskaźniki są niezwykle głupi .

Tablice nie są wskaźnikami

Kompilator po cichu wygeneruje wskaźnik do pierwszego elementu tablicy, gdy zostanie to uznane za użyteczne, to znaczy, gdy operacja nie powiedzie się na tablicy, ale powiedzie się na wskaźniku. Ta konwersja z tablicy na wskaźnik jest trywialna, ponieważ wynikowy wskaźnik wartość jest po prostu adresem tablicy. Zauważ, że wskaźnik jest , a nie przechowywany jako część samej tablicy (lub gdziekolwiek indziej w pamięci). tablica nie jest wskaźnikiem.

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

Ważnym kontekstem, w którym tablica nie rozpada się na wskaźnik do pierwszego elementu, jest zastosowanie do niej operatora &. W takim przypadku operator & daje wskaźnik do całej tablicy, a nie tylko wskaźnik do jej pierwszego elementu. Chociaż w takim przypadku wartości (adresy) są takie same, wskaźnik do pierwszego elementu tablicy i wskaźnik do całej tablicy są całkowicie odrębne typy:

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 pojedynczą liczbę całkowitą (przedstawioną jako małe pole), podczas gdy wskaźnik do całej tablicy wskazuje na tablicę 8 liczb całkowitych (przedstawioną jako duże pole).

Ta sama sytuacja pojawia się w klasach i może jest bardziej oczywista. Wskaźnik do obiektu i wskaźnik do jego pierwszego elementu danych mają tę samą wartość ( ten sam adres), jednak są one zupełnie odrębnymi typami.

Jeśli nie znasz składni deklaracji C, nawiasy w typie {[27] } 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 dostępu do poszczególnych elementów tablicy. Żaden z nich nie jest lepszy od drugiego, a powinieneś zapoznać się z obydwoma.

Arytmetyka wskaźnika

Biorąc wskaźnik p do pierwszego elementu tablicy, wyrażenie p+i daje wskaźnik do i-tego elementu tablicy. Po zdereferowaniu tego wskaźnika można uzyskać dostęp do poszczególnych elementów:

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

Jeśli x oznacza tablicę , wtedy zanik tablicy do wskaźnika zacznie działać, ponieważ dodanie tablicy i liczby całkowitej jest bez znaczenia (nie ma operacji plus na tablicach), ale dodawanie wskaźnika i liczby całkowitej ma sens:

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

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

Jeśli natomiast x oznacza wskaźnik do pierwszego (lub dowolnego innego) elementu tablicy, to zanik tablicy do wskaźnika nie jest konieczny, ponieważ wskaźnik, na którym i ma być dodany, już istnieje:

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

Zauważ, że w przedstawionym przypadku x jest wskaźnikiem zmienna (dostrzegalna przez małe pole obok x), ale równie dobrze może być wynikiem funkcji zwracającej wskaźnik(lub dowolnego innego wyrażenia typu T*).

Operator indeksujący

Ponieważ składnia *(x+i) jest nieco niezgrabna, C++ dostarcza składnię alternatywną x[i]:
std::cout << x[3] << ", " << x[7] << std::endl;
Ze względu 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 indeksującego prowadzi do następujące interesujące równoważności:

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

Jednakże &x[0] jest ogólnienie odpowiednikiem x. Pierwszy jest wskaźnikiem, drugi tablicą. Tylko wtedy, gdy kontekst wyzwala zanik tablicy do wskaźnika, x i &x[0] mogą być używane 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 pierwszej linii kompilator wykrywa przypisanie ze wskaźnika do wskaźnika, co trywialnie się powiedzie. W drugiej linii wykrywa przypisanie z tablicy do wskaźnika. Ponieważ jest to bez znaczenia (ale przypisanie wskaźnika {[59] } do wskaźnika ma sens), rozkład tablicy do wskaźnika rozpoczyna się jak zwykle.

Zakresy

Tablica typu T[n] zawiera n elementy, indeksowane od 0 do n-1; nie ma elementu n. A jednak, aby wspierać półotwarte zakresy (gdzie początek jest inclusive, a koniec jest exclusive ), C++ pozwala obliczyć wskaźnik do (nieistniejącego) n-tego elementu, ale jest nielegalne dereferencja ten wskaźnik:

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

Na przykład, jeśli chcesz posortować tablicę, obie poniższe czynności będą działać równie dobrze:

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

Zauważ, że podawanie &x[n] jako drugiego argumentu jest nielegalne, ponieważ jest to równoważne &*(x+n), a pod-wyrażenie *(x+n) technicznie wywołuje niezdefiniowane zachowanie W C++ (ale nie w C99).

Zauważ również, że możesz po prostu podać x jako pierwszy argument. To trochę zbyt zwięzłe jak na mój gust, a także sprawia, że argument szablonu jest nieco trudniejszy dla kompilatora, ponieważ w takim przypadku pierwszy argument jest tablicą, a drugi argumentem jest wskaźnik. (Ponownie, rozpad tablicy na wskaźnik zaczyna działać.)

 267
Author: fredoverflow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:24

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

Tablice wielowymiarowe

Większość programistów jest zaznajomiona z nazwanymi tablicami wielowymiarowymi, ale wielu nie zdaje sobie sprawy z faktu, że tablice wielowymiarowe mogą być również tworzone anonimowo. Tablice wielowymiarowe są często określane jako "tablice tablic" lub "true tablice wielowymiarowe".

Nazwane tablice wielowymiarowe

Przy użyciu nazwanych wielowymiarowych tablice, Wszystkie wymiary muszą być znane podczas 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 w pamięci wygląda tablica wielowymiarowa:

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

Zauważ, że siatki 2D, takie jak powyższe, są jedynie pomocnymi wizualizacjami. Z punktu widzenia C++ pamięć jest "płaską" sekwencją bajtów. Elementy wielowymiarowej tablicy są przechowywane w porządku rząd-major. 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] oznaczają ten sam element! Oznacza to że można przyjmować 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

Z anonimowymi tablicami wielowymiarowymi, wszystkie wymiary z wyjątkiem pierwszego muszą być znane podczas 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 przydzielana jako pojedynczy blok w pamięci.

Tablice z pointers

Możesz przezwyciężyć ograniczenie stałej szerokości, wprowadzając inny poziom indrection.

Nazwane tablice wskaźników

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

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ż każda linia jest teraz przydzielana indywidualnie, wyświetlanie tablic 2D jako tablic 1D nie działa już.

Anonimowe tablice z wskaźniki

Tutaj znajduje się anonimowa Tablica 5 (lub dowolna inna liczba) wskaźników, które są inicjalizowane anonimowymi tablicami o różnej długości:

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 tablicy na wskaźnik naturalnie rozszerza 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 dorozumianej konwersji z T[h][w] na T**. Gdyby taka konwersja implicit zaistniała, wynik byłby wskaźnik do pierwszego elementu tablicy h wskaźnik do T (każdy wskazuje na pierwszy element linii w oryginalnej tablicy 2D), ale tablica ta nie istnieje jeszcze nigdzie w pamięci. Jeśli chcesz taką konwersję, 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;

Zwróć uwagę, że generuje to Widok oryginalnej tablicy wielowymiarowej. Jeśli potrzebujesz kopii, musisz utworzyć dodatkowe tablice i skopiować dane samodzielnie:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = new int[7];
    std::copy(connect_four[i], connect_four[i + 1], p[i]);
}

// ...

for (int i = 0; i < 6; ++i)
{
    delete[] p[i];
}
delete[] p;
 128
Author: fredoverflow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-28 09:20:29

Przypisanie

Bez konkretnego powodu tablice nie mogą być przypisane do siebie. Użyj std::copy zamiast:

#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 może zapewnić prawdziwe przypisanie tablicy, ponieważ możliwe jest kopiowanie plasterków większych tablic do mniejszych tablic. std::copy jest zwykle wyspecjalizowany dla prymitywnych typów, aby dać maksymalną wydajność. Jest mało prawdopodobne, że std::memcpy działa lepiej. W razie wątpliwości zmierz.

Chociaż nie można przypisać tablic bezpośrednio, można Może przypisać struktury i klasy, które zawierają członków tablicy. Dzieje się tak dlatego, że Członkowie tablicy są kopiowani memberwise przez operator przypisania, który jest domyślnie dostarczany przez kompilator. Jeśli zdefiniujesz operator przypisania ręcznie dla własnych typów struktur lub klas, musisz wrócić do ręcznego kopiowania dla członków tablicy.

Parametr przechodzący

Tablice nie mogą być przekazywane przez wartość. Można je przekazać za pomocą wskaźnika lub za pomocą Referencja.

Podaj wskaźnik

Ponieważ same tablice nie mogą być przekazywane przez wartość, zwykle wskaźnik do ich pierwszego elementu jest przekazywany przez wartość. Jest to często nazywane "pass by pointer". Ponieważ rozmiar tablicy nie jest możliwy do odczytania za pomocą tego wskaźnika, musisz przekazać drugi parametr wskazujący rozmiar tablicy (klasyczne rozwiązanie C) lub drugi wskaźnik wskazujący po ostatnim elemencie 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 alternatywa składniowa, można również zadeklarować parametry jako T p[] i oznacza to dokładnie to samo co T* p w kontekście listy parametrów tylko :

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

Można myśleć o kompilatorze jako o przepisaniu T p[] do T *p w kontekście listy parametrów tylko . Ta specjalna reguła 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 jako wskaźnika sprawia, że jest ogromne różnica.

Niestety, możesz również podać rozmiar w parametrze tablicy, który jest po cichu ignorowany przez kompilator. Oznacza to, że następujące trzy sygnatury 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

Pass by reference

Tablice mogą być również przekazywane przez odniesienie:

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

W tym przypadku rozmiar tablicy jest znaczący. Ponieważ pisanie funkcji, która akceptuje tylko tablice zawierające dokładnie 8 elementów jest mało przydatne, Programiści zazwyczaj piszemy 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 taki szablon funkcji można wywołać tylko z rzeczywistą tablicą liczb całkowitych, a nie ze wskaźnikiem do liczby całkowitej. Rozmiar tablicy jest automatycznie wnioskowany, a dla każdego rozmiaru n inna funkcja jest tworzona z szablonu. Możesz również napisać całkiem przydatne szablony funkcji, które są abstrakcyjne zarówno z typu elementu, jak i z rozmiaru.

 82
Author: fredoverflow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:20

Tworzenie i inicjalizacja tablicy

Jak w przypadku każdego innego rodzaju obiektu C++, tablice mogą być przechowywane bezpośrednio w nazwanych zmiennych (wtedy rozmiar musi być stałą w czasie kompilacji; C++ nie obsługuje VLAs), lub mogą być przechowywane anonimowo na stercie i dostępne pośrednio za pomocą wskaźników (tylko wtedy rozmiar może być obliczany w czasie wykonywania).

Tablice automatyczne

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

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

Inicjalizacja jest wykonywana w porządku rosnącym. Należy zauważyć, że wartości początkowe zależą od typu elementu T:

  • jeśli T jest POD (jak int w powyższym przykładzie), inicjalizacja nie ma miejsca.
  • W Przeciwnym Razie, domyślny konstruktor T inicjalizuje wszystkie elementy.
  • jeśli T nie udostępnia dostępnego konstruktora domyślnego, program nie kompilować.

Alternatywnie, wartości początkowe mogą być jawnie określone w inicjalizator tablicy , lista rozdzielana przecinkami otoczona nawiasami klamrowymi:

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

Ponieważ w tym przypadku liczba elementów w inicjalizatorze tablicy jest równa wielkości tablicy, ręczne podanie rozmiaru jest zbędne. Można go automatycznie wydedukować przez kompilator:

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

Możliwe jest również podanie rozmiaru i podanie krótszej tablicy inicjalizacja:

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

W tym przypadku, pozostałe elementy są zainicjalizowane zero . Zauważ, że C++ zezwala na inicjalizację pustej tablicy (wszystkie elementy są inicjalizowane zero), podczas gdy C89 nie (wymagana jest co najmniej jedna wartość). Zauważ również, że inicjalizatory tablic mogą być używane tylko do inicjalizacji tablic ; nie mogą być później używane w przypisaniach.

Tablice statyczne

Tablice statyczne (tablice żyjące "w segmencie danych") to zmienne tablic lokalnych zdefiniowane ze słowem kluczowym static i zmiennymi tablicy w przestrzeni nazw ("zmienne globalne"):

int global_static_array[8];

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

(zauważ, że zmienne w przestrzeni nazw są niejawnie statyczne. Dodanie słowa kluczowego static do ich definicji ma zupełnie inne, przestarzałe znaczenie .)

Oto jak tablice statyczne zachowują się inaczej niż tablice automatyczne:

  • Tablice statyczne bez inicjalizacji tablicy są inicjalizowane zerem przed dalszym potencjałem inicjalizacja.
  • statyczne tablice POD są inicjalizowane dokładnie raz , A wartości początkowe są zazwyczaj zapisywane do pliku wykonywalnego, w którym to przypadku nie ma kosztów inicjalizacji w czasie wykonywania. Nie zawsze jest to jednak rozwiązanie najbardziej oszczędne i nie jest wymagane przez normę.
  • statyczne tablice nie-POD są inicjalizowane pierwszy raz przepływ sterowania przechodzi przez ich definicję. W przypadku lokalnych tablic statycznych, że może się nigdy nie zdarzyć, jeśli funkcja nie zostanie wywołana.

(żadne z powyższych nie jest specyficzne dla tablic. Zasady te odnoszą się równie dobrze do innych rodzajów obiektów statycznych.)

Array data members

Array data members są tworzone podczas tworzenia własnego obiektu. Niestety, C++03 nie zapewnia możliwości inicjalizacji tablic na liście inicjalizatorów członka , więc inicjalizacja musi być sfałszowana przy użyciu przypisań:

class Foo
{
    int primes[8];

public:

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

Alternatywnie można zdefiniować automatyczna tablica w ciele konstruktora i skopiowanie elementów:

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ć inicjalizowane na liście inicjalizatorów członkowskich dzięki jednolitej inicjalizacji :

class Foo
{
    int primes[8];

public:

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

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

Tablice dynamiczne

Tablice dynamiczne nie mają nazw, stąd jedynym sposobem dostępu do nich są wskaźniki. Ponieważ nie mają nazw, odniosę się do od teraz są to "anonimowe tablice".

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

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, tablice anonimowe wymagają więcej pamięci niż tablice nazwane ze względu na dodatkowy wskaźnik, który musi być przechowywany oddzielnie. (Istnieje również kilka dodatkowych kosztów na bezpłatny sklep.)

Zauważ, że nie matutaj nie ma rozpadu tablicy na wskaźnik. Chociaż ewaluacja new int[size] w rzeczywistości tworzy tablicę liczb całkowitych, wynikiem wyrażenia new int[size] jest już wskaźnik do pojedynczej liczby całkowitej (pierwszego elementu), 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 tablicy rozmiary, które mają być stałymi czasu kompilacji. (Stąd nie dodałem Anonymous array ze statycznymi informacjami o typie Na zdjęciu.)

Jeśli chodzi o wartości domyślne dla elementów, tablice anonimowe zachowują się podobnie do tablic automatycznych. Zwykle anonimowe tablice POD nie są inicjalizowane, 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 umożliwia określenie wartości początkowych dla anonimowych tablic dzięki jednolitej inicjalizacji:

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

Jeśli skończyłeś używać anonimowej tablicy, musisz ją zwolnić z powrotem do systemu:

delete[] p;

Musisz zwolnić każdą anonimową tablicę dokładnie raz, a potem nigdy więcej jej nie dotykać. Nie uwolnienie go w ogóle powoduje wyciek pamięci (lub bardziej ogólnie, w zależności od typu elementu, wyciek zasobów), a próba uwolnienia go wielokrotnie skutkuje niezdefiniowanym zachowaniem. Użycie postaci nie-tablicy delete (lub free) zamiast {[26] } do zwolnienia tablicy jest również niezdefiniowanym zachowaniem.

 68
Author: fredoverflow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:50

5. Typowe pułapki podczas korzystania z tablic.

5.1 Pułapka: Typ ufności-niebezpieczne łączenie.

OK, powiedziano ci lub sam się przekonałeś, że globals (przestrzeń nazw zmienne zakresu, które mogą być dostępne poza jednostką tłumaczenia) są Evil™. Ale czy wiesz, jacy są naprawdę źli™? Rozważ program poniżej, 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 to kompiluje i łączy dobrze zarówno z MinGW g++ 4.4.1 oraz Visual C++ 10.0.

Ponieważ typy nie pasują, program zawiesza się po uruchomieniu.

Okno dialogowe crash Windows 7

In-the-formal explanation: the program has Undefined Behavior (ub), and instead upaść może więc po prostu wisieć, a może nic nie robić, lub to może wysyłać maile do prezydentów USA, Rosji, Indii, Chiny i Szwajcaria, i sprawić, że Daemony nosowe wylecą z twojego nosa.

In-practice explanation: in main.cpp tablica jest traktowana jako wskaźnik, umieszczony pod tym samym adresem co tablica. Dla 32-bitowego pliku wykonywalnego oznacza to, że pierwszy int wartość w tablicy jest traktowana jako wskaźnik. Tj. w {[12] } numbers zmienna zawiera lub wydaje się zawierać (int*)1. Powoduje to program umożliwiający dostęp do pamięci na samym dole przestrzeni adresowej, czyli konwencjonalnie zarezerwowane i powodujące pułapki. Wynik: masz wypadek.

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

[N3290 §3.5/10]
Naruszenie tej reguły w odniesieniu do tożsamości typu nie wymaga diagnostyki.

Ten sam paragraf opisuje dopuszczalną zmianę:

... deklaracje dla obiektu array mogą określać typy tablic, które różnią się obecnością lub brakiem wiązania tablicy głównej (8.3.4).

Ta dozwolona odmiana nie obejmuje deklarowanie nazwy jako tablicy w jednym Jednostka tłumaczenia, oraz jako wskaźnik w innej jednostce tłumaczenia.

5.2 Pułapka: Robienie przedwczesnej optymalizacji (memset & przyjaciele).

jeszcze nie napisane

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

Z głębokim doświadczeniem C to naturalne pisać ...

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

Ponieważ array rozkłada 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). To znaczy to samo i bez względu na to, jak to jest zapisany jest to idiom C do znajdowania elementów liczbowych tablicy.

Główna pułapka: idiom C nie jest typesafe. 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 generuje błąd wynik. W 2007 roku w systemie Windows 2000 zainstalowano 32-bitowy program wykonywalny.]}

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

  1. kompilator przepisuje int const a[7] na int const a[].
  2. kompilator przepisuje int const a[] na int const* a.
  3. {[21] } jest wywoływany ze wskaźnikiem.
  4. dla 32-bitowego pliku wykonywalnego sizeof(array) (rozmiar wskaźnika) wynosi wtedy 4.
  5. sizeof(*array) jest odpowiednikiem sizeof(int), który dla 32-bitowego pliku wykonywalnego wynosi również 4.

Aby wykryć ten błąd podczas uruchamiania, możesz zrobić ...

#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łanie wyświetlacza...
Twierdzenie nie powiodło się: ("N_ITEMS wymaga rzeczywistej tablicy jako argumentu", typeid( a)!= typeid( &*a)), file runtime_detect ion.cpp, linia 16

Ta aplikacja poprosiła Runtime o zakończenie jej w nietypowy sposób.
Skontaktuj się z zespołem wsparcia aplikacji, aby uzyskać więcej informacji.

Wykrywanie błędów jest lepsze niż brak wykrywania, ale trochę marnuje czas procesora, a może znacznie więcej czasu programisty. Lepiej z detekcją w czas kompilacji! A jeśli jesteś zadowolony, że nie obsługujesz tablic typów lokalnych z C++98, wtedy 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 )

Kompilacja tej definicji podstawiona do pierwszego kompletnego programu, z g++, Mam ...

M:\count > g++ compile_time_detection.cpp
compile_time_detection.cpp: in function ' void display (const int*)':
compile_time_detection.w tym celu należy skontaktować się z inspektorem ochrony danych.]}

M:\count> _

Jak to działa: tablica jest przekazywana przez odniesienie do n_items i tak się dzieje nie zanika do wskaźnika do pierwszego elementu, a funkcja może po prostu zwrócić liczba elementów określona przez typ.

W C++11 można tego używać również do tablic typu lokalnego, a jest to typ Bezpieczny idiom C++ do znajdowania liczby elementów tablicy.

5.4 C++11 & C++14 pułapka: użycie funkcji rozmiaru tablicy constexpr.

Z C++11 i później to naturalne, ale jak zobaczysz niebezpieczne!, do zastąp C++03 function

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, które pozwala funkcja ta wytwarza stałą czasową kompilacji .

Na przykład, w przeciwieństwie do funkcji C++03, taka stała czasu kompilacji można użyć 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 używając wersji constexpr:

// 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 powyższe kompiluje się z MinGW - 64 5.1.0 z -pedantic-errors, oraz, testowanie 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 (eksperymentalne). I ważne dla platformy Windows, nie kompiluje z Visual C++ 2015. Powodem jest oświadczenie C++11/C++14 o wykorzystaniu odwołania w wyrażeniach constexpr:

C++11 C++14 5,19 zł / 2 9na dash

A wyrażenie warunkowe e jest wyrażenie stałej rdzenia chyba że ocena z e, postępując zgodnie z regułami maszyny abstrakcyjnej (1.9), oceni jeden z następujące wyrażenia:
        ⋮

  • an ID-wyrażenie które odnosi się do zmiennej lub elementu danych typu reference chyba że Referencja ma poprzedzającą inicjalizację i albo
    • jest inicjowany wyrażeniem stałym lub
    • jest to niestatyczny element danych obiekt, którego życie zaczęło się w ocena e;

Zawsze można napisać bardziej wyrazisty

// 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 nie działa, gdy Collection nie jest tablicą surową.

Aby radzić sobie ze zbiorami, które mogą być nie-tablicami, potrzeba przeciążalności n_items funkcja, ale również, aby użyć czasu kompilacji, potrzeba czasu kompilacji reprezentacja rozmiaru tablicy. I klasyczne rozwiązanie C++03, które działa dobrze również w C++11 i C++14, jest do niech funkcja zgłasza swój wynik nie jako wartość ale poprzez wynik funkcji typu . 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 zwracanego dla static_n_items: ten kod nie używa std::integral_constant ponieważ z std::integral_constant wynik jest reprezentowany bezpośrednio jako wartość constexpr, przywracając pierwotny problem. Zamiast klasy Size_carrier można pozwolić funkcji zwracać bezpośrednio a odniesienie do tablicy. Jednak nie wszyscy znają tę składnię.

O nazewnictwie: część tego rozwiązania do constexpr-invalid-due-to-reference problem polega na tym, aby wybór stałej czasowej kompilacji był jednoznaczny.

Miejmy nadzieję, że oops-tam-było-odniesienie-zaangażowane-w-twój - constexpr problem zostanie rozwiązany z C++17, ale do tego czasu makro takie jak STATIC_N_ITEMS powyżej daje przenośność, np. do kompilatorów clang i Visual C++, zachowując bezpieczeństwo typu.

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

 65
Author: Cheers and hth. - Alf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-31 00:15:12