Как я могу использовать массивы в C ++? - PullRequest
455 голосов
/ 27 января 2011

C ++ унаследовал массивы от C, где они используются практически везде.C ++ предоставляет абстракции, которые проще в использовании и менее подвержены ошибкам (std::vector<T> начиная с C ++ 98 и std::array<T, n> начиная с C ++ 11 ), поэтому необходимы массивывозникает не так часто, как в C. Однако, когда вы читаете устаревший код или взаимодействуете с библиотекой, написанной на C, вы должны четко понимать, как работают массивы.

Этот FAQ разделен напять частей:

  1. массивы на уровне типа и доступ к элементам
  2. создание и инициализация массива
  3. присваивание и передача параметров
  4. многомерных массивов и массивов указателей
  5. распространенных ошибок при использовании массивов

Если вы чувствуете, что чего-то важного не хватает в этом FAQ, напишите ответ и свяжите его здесь как дополнительную часть.

В следующем тексте «массив» означает «массив C», а не шаблон класса std::array.Базовые знания синтаксиса декларатора C предполагаются.Обратите внимание, что ручное использование new и delete, как показано ниже, чрезвычайно опасно, несмотря на исключения, но это тема другого FAQ .

(Примечание: это должна быть запись в FAQ по C ++ для Stack Overflow . Если вы хотите критиковать идею предоставления FAQ в этой форме, тогда публикация на мета, которая начала всеэто было бы местом для этого. Ответы на этот вопрос отслеживаются в C ++ chatroom , где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитанте, кто придумал идею.)

Ответы [ 5 ]

288 голосов
/ 27 января 2011

Массивы на уровне типа

Тип массива обозначается как T[n], где T - это тип элемента , а n - положительное размер , количество элементов в массиве. Тип массива - это тип продукта типа элемента и размера. Если один или оба этих ингредиента различаются, вы получаете отдельный тип:

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

Обратите внимание, что размер является частью типа, то есть типы массивов различного размера являются несовместимыми типами, которые абсолютно не связаны друг с другом. sizeof(T[n]) эквивалентно n * sizeof(T).

Распад массива в указатель

Единственное «соединение» между T[n] и T[m] состоит в том, что оба типа могут неявно быть преобразованными в T*, и результатом этого преобразования является указатель на первый элемент массив. То есть везде, где требуется T*, вы можете указать T[n], и компилятор тихо предоставит этот указатель:

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

Это преобразование известно как «распад массива в указатель», и оно является основным источником путаницы. При этом размер массива теряется, поскольку он больше не является частью типа (T*). Pro: Забывание размера массива на уровне типа позволяет указателю указывать на первый элемент массива любого размера. Con: Учитывая указатель на первый (или любой другой) элемент массива, невозможно определить, насколько велик этот массив или куда именно указывает указатель относительно границ массива. Указатели очень глупы .

Массивы не указатели

Компилятор автоматически генерирует указатель на первый элемент массива всякий раз, когда это считается полезным, то есть всякий раз, когда операция завершается с ошибкой в ​​массиве, но завершается успешно с указателем. Это преобразование из массива в указатель является тривиальным, так как результирующий указатель значение является просто адресом массива. Обратите внимание, что указатель не хранится как часть самого массива (или где-либо еще в памяти). Массив не является указателем.

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

Один важный контекст, в котором массив не распадается на указатель на его первый элемент, - это когда к нему применяется оператор &. В этом случае оператор & возвращает указатель на массив whole , а не только указатель на его первый элемент. Хотя в этом случае значения (адреса) одинаковы, указатель на первый элемент массива и указатель на весь массив являются совершенно разными типами:

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

Следующее искусство ASCII объясняет это различие:

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

Обратите внимание, что указатель на первый элемент указывает только на одно целое число (изображено в виде маленького прямоугольника), тогда как указатель на весь массив указывает на массив из 8 целых чисел (изображен в виде большого прямоугольника).

Такая же ситуация возникает в классах и, возможно, более очевидна. Указатель на объект и указатель на его первый элемент данных имеют одинаковое значение (один и тот же адрес), но они совершенно разных типов.

Если вы не знакомы с синтаксисом объявления C, скобки в типе int(*)[8] имеют важное значение:

  • int(*)[8] - указатель на массив из 8 целых чисел.
  • int*[8] - это массив из 8 указателей, каждый элемент типа int*.

Доступ к элементам

C ++ предоставляет два синтаксических варианта для доступа к отдельным элементам массива. Ни один из них не превосходит другого, и вам следует ознакомиться с обоими.

Арифметика указателя

Учитывая указатель p на первый элемент массива, выражение p+i дает указатель на i-й элемент массива. После разыменования этого указателя можно получить доступ к отдельным элементам:

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

Если x обозначает массив , то затухание массива к указателю наступит, потому что добавление массива и целого числа не имеет смысла (нет операции плюс для массивов), но добавлениеуказатель и целое число имеют смысл:

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

(Обратите внимание, что неявно сгенерированный указатель не имеет имени, поэтому я написал x+0 для его идентификации.)

Если, нас другой стороны, x обозначает указатель на первый (или любой другой) элемент массива, тогда затухание от массива к указателю не требуется, поскольку указатель, на который собирается iбыть добавлено, уже существует:

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

Обратите внимание, что в изображенном случае x является указателем переменная (различима по маленькой рамке рядом с x), но можетточно также будет результатом функции, возвращающей указатель (или любое другое выражение типа T*).

Оператор индексирования

Поскольку синтаксис *(x+i) немного неуклюж, C ++обеспечивает альтернативный синтаксис x[i]:

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

Из-за тТак как сложение является коммутативным, следующий код делает то же самое:

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

Определение оператора индексации приводит к следующей интересной эквивалентности:

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

Однако &x[0] обычно не эквивалентно x.Первый - это указатель, второй - массив.Только когда контекст запускает затухание массива в указатель, x и &x[0] могут использоваться взаимозаменяемо.Например:

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

В первой строке компилятор обнаруживает присваивание от указателя на указатель, что тривиально успешно.Во второй строке он обнаруживает присваивание из массива указателю.Поскольку это бессмысленно (но указатель на присвоение указателя имеет смысл), затухание массива в указатель включается как обычно.

Диапазоны

Массив типа T[n] имеет n элементов, проиндексированных от 0 до n-1;нет элемента n.И все же, для поддержки полуоткрытых диапазонов (где начало включительно , а конец исключительно ), C ++ позволяет вычислять указатель на (несуществующий) n-этот элемент, но недопустимо разыменовывать этот указатель:

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

Например, если вы хотите отсортировать массив, оба следующих параметра будут одинаково хорошо работать:

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

Обратите внимание, что нельзя указывать &x[n] в качестве второго аргумента, поскольку это эквивалентно &*(x+n), а подвыражение *(x+n) технически вызывает неопределенное поведение в C ++ (но не в C99).

Также обратите внимание, что вы можете просто указать x в качестве первого аргумента.На мой взгляд, это слишком кратко, и это также делает вывод аргумента шаблона немного сложнее для компилятора, потому что в этом случае первый аргумент является массивом, а второй аргумент является указателем.(Опять же, начинается затухание массива в указатель.)

131 голосов
/ 27 января 2011

Программисты часто путают многомерные массивы с массивами указателей.

Многомерные массивы

Большинство программистов знакомы с именованными многомерными массивами, но многие не знают, что многомерный массив также может быть создан анонимно. Многомерные массивы часто называют «массивами массивов» или « true многомерных массивов».

Именованные многомерные массивы

При использовании именованных многомерных массивов, все размеры должны быть известны во время компиляции:

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

Вот как именованный многомерный массив выглядит в памяти:

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

Обратите внимание, что двумерные сетки, такие как выше, являются просто полезными визуализациями С точки зрения C ++ память представляет собой «плоскую» последовательность байтов. Элементы многомерного массива хранятся в главном порядке строк. То есть connect_four[0][6] и connect_four[1][0] являются соседями в памяти. Фактически, connect_four[0][7] и connect_four[1][0] обозначают один и тот же элемент! Это означает, что вы можете взять многомерные массивы и рассматривать их как большие одномерные массивы:

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

Анонимные многомерные массивы

В случае анонимных многомерных массивов все размеры , кроме первого , должны быть известны во время компиляции:

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

Вот так выглядит анонимный многомерный массив в памяти:

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

Обратите внимание, что сам массив все еще выделяется как один блок в памяти.

Массивы указателей

Вы можете преодолеть ограничение фиксированной ширины, введя другой уровень косвенности.

Именованные массивы указателей

Вот именованный массив из пяти указателей, которые инициализируются анонимными массивами различной длины:

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

А вот как это выглядит в памяти:

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

Поскольку теперь каждая строка выделяется индивидуально, просмотр 2D-массивов как 1D-массивов больше не работает.

Анонимные массивы указателей

Вот анонимный массив из 5 (или любого другого числа) указателей, которые инициализируются анонимными массивами различной длины:

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

А вот как это выглядит в памяти:

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

Конверсия

Распад массива на указатель естественным образом распространяется на массивы массивов и массивы указателей:

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;

Однако неявное преобразование из T[h][w] в T** отсутствует. Если бы такое неявное преобразование существовало, результатом был бы указатель на первый элемент массива из h указателей на T (каждый указатель на первый элемент строки в исходном 2D-массиве), но этот указатель Массив еще нигде в памяти не существует. Если вы хотите такое преобразование, вы должны создать и заполнить требуемый массив указателей вручную:

int connect_four[6][7];

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

// ...

delete[] p;

Обратите внимание, что при этом создается представление исходного многомерного массива. Если вам нужна копия, вы должны создать дополнительные массивы и скопировать данные самостоятельно:

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;
85 голосов
/ 27 января 2011

Назначение

Без особой причины массивы нельзя назначать друг другу. Используйте std::copy вместо:

#include <algorithm>

// ...

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

Это более гибко, чем то, что могло бы обеспечить истинное назначение массива, потому что можно копировать фрагменты больших массивов в меньшие массивы. std::copy обычно специализируется на примитивных типах для обеспечения максимальной производительности. Вряд ли std::memcpy работает лучше. Если сомневаетесь, измерьте.

Хотя вы не можете назначать массивы напрямую, вы можете назначать структуры и классы, которые содержат членов массива. Это потому, что члены массива копируются по элементам оператором присваивания, который предоставляется компилятором по умолчанию. Если вы определяете оператор присваивания вручную для своих собственных типов структуры или класса, вы должны вернуться к ручному копированию для элементов массива.

Передача параметра

Массивы не могут быть переданы по значению. Вы можете передать их по указателю или по ссылке.

Передать по указателю

Поскольку сами массивы не могут быть переданы по значению, обычно указатель на их первый элемент передается по значению. Это часто называют «передачей по указателю». Поскольку размер массива не может быть получен через этот указатель, вы должны передать второй параметр, указывающий размер массива (классическое решение C), или второй указатель, указывающий после последнего элемента массива (решение итератора 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);
}

В качестве синтаксической альтернативы вы также можете объявить параметры как T p[], и это означает то же самое, что и T* p только в контексте списков параметров :

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

Вы можете думать о компиляторе как о переписывании T p[] в T *p только в контексте списков параметров . Это специальное правило частично отвечает за всю путаницу с массивами и указателями. В любом другом контексте объявление чего-либо в виде массива или указателя дает огромную разницу.

К сожалению, вы также можете указать размер в параметре массива, который компилятор игнорирует. То есть следующие три сигнатуры в точности эквивалентны, как указано в ошибках компилятора:

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

Передать по ссылке

Массивы также можно передавать по ссылке:

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

В этом случае размер массива значителен. Поскольку написание функции, которая принимает только массивы ровно из 8 элементов, бесполезно, программисты обычно пишут такие функции как шаблоны:

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

Обратите внимание, что такой шаблон функции можно вызывать только с действительным массивом целых чисел, а не с указателем на целое число. Размер массива определяется автоматически, и для каждого размера n создается другая функция из шаблона. Вы также можете написать весьма полезные шаблоны функций, которые абстрагируются как от типа элемента, так и от размера.

70 голосов
/ 13 февраля 2011

Создание и инициализация массива

Как и для любого другого типа объекта C ++, массивы могут храниться либо непосредственно в именованных переменных (тогда размер должен быть константой времени компиляции; C ++ не поддерживает VLA ), либо они могут быть хранится анонимно в куче и доступен косвенно через указатели (только тогда размер может быть вычислен во время выполнения).

Автоматические массивы

Автоматические массивы (массивы, «живущие в стеке») создаются каждый раз, когда поток управления проходит через определение нестатической переменной локального массива:

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

Инициализация выполняется в порядке возрастания. Обратите внимание, что начальные значения зависят от типа элемента T:

  • Если T является POD (как int в приведенном выше примере), инициализация не происходит.
  • В противном случае конструктор по умолчанию T инициализирует все элементы.
  • Если T не предоставляет доступного конструктора по умолчанию, программа не компилируется.

В качестве альтернативы, начальные значения могут быть явно указаны в инициализаторе массива , в списке через запятую, заключенном в фигурные скобки:

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

Поскольку в этом случае количество элементов в инициализаторе массива равно размеру массива, указание размера вручную является излишним. Это может быть автоматически выведено компилятором:

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

Также можно указать размер и предоставить более короткий инициализатор массива:

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

В этом случае остальные элементы инициализируются нулями . Обратите внимание, что в C ++ допускается инициализация пустого массива (все элементы инициализируются нулями), а в C89 - нет (требуется хотя бы одно значение). Также обратите внимание, что инициализаторы массива могут использоваться только для initialize массивов; позже они не могут быть использованы в назначениях.

Статические массивы

Статические массивы (массивы, расположенные «в сегменте данных») - это локальные переменные массива, определенные с помощью ключевого слова static и переменных массива в области имен («глобальные переменные»):

int global_static_array[8];

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

(Обратите внимание, что переменные в области имен неявно являются статическими. Добавление ключевого слова static к их определению имеет совершенно другое, устаревшее значение .)

Вот как статические массивы ведут себя иначе, чем автоматические:

  • Статические массивы без инициализатора массива инициализируются нулями перед любой последующей потенциальной инициализацией.
  • Статические POD-массивы инициализируются ровно один раз , а начальные значения обычно запекаются в исполняемом файле, и в этом случае во время выполнения стоимость инициализации отсутствует. Однако это не всегда самое экономичное решение, и оно не требуется стандартом.
  • Статические массивы не POD инициализируются в первый раз поток управления проходит через их определение. В случае локальных статических массивов это может никогда не произойти, если функция никогда не вызывается.

(Ничто из вышеперечисленного не относится к массивам. Эти правила в равной степени применимы и к другим типам статических объектов.)

Элементы массива данных

Элементы данных массива создаются при создании объекта-владельца. К сожалению, C ++ 03 не предоставляет средств для инициализации массивов в списке инициализаторов , поэтому инициализация должна быть сфальсифицирована с помощью присваиваний:

class Foo
{
    int primes[8];

public:

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

Кроме того, вы можете определить автоматический массив в теле конструктора и скопировать элементы:

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

В C ++ 0x массивы можно инициализировать в списке инициализаторов элементов благодаря равномерной инициализации :

class Foo
{
    int primes[8];

public:

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

Это единственное решение, которое работает с типами элементов, которые не имеют конструктора по умолчанию.

Динамические массивы

Динамические массивы не имеют имен, поэтому единственный способ получить к ним доступ - через указатели. Поскольку у них нет имен, я буду называть их «анонимными массивами».

В C анонимные массивы создаются через malloc и друзей.В C ++ анонимные массивы создаются с использованием синтаксиса new T[size], который возвращает указатель на первый элемент анонимного массива:

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

Следующий рисунок ASCII отображает макет памяти, если размер вычисляется как 8во время выполнения:

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

Очевидно, что анонимным массивам требуется больше памяти, чем именованным массивам из-за дополнительного указателя, который должен храниться отдельно.(В бесплатном хранилище также есть некоторые дополнительные издержки.)

Обратите внимание, что здесь происходит нет затухания массива в указатель.Хотя оценка new int[size] фактически создает массив целых чисел, результатом выражения new int[size] будет уже указатель на одно целое число (первый элемент), не массив целых чисел или указатель на массив целых чисел неизвестного размера.Это было бы невозможно, потому что статическая система типов требует, чтобы размеры массива были константами времени компиляции.(Следовательно, я не аннотировал анонимный массив статической информацией о типе на рисунке.)

Что касается значений по умолчанию для элементов, анонимные массивы ведут себя подобно автоматическим массивам.Обычно анонимные POD-массивы не инициализируются, но существует специальный синтаксис , который запускает инициализацию значения:

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

(обратите внимание на конечную пару скобок прямо перед точкой с запятой.) Снова, C ++ 0x упрощает правила и позволяет задавать начальные значения для анонимных массивов благодаря равномерной инициализации:

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

Если вы закончили использовать анонимный массив, вы должны освободить его обратно в систему:

delete[] p;

Вы должны освободить каждый анонимный массив ровно один раз, а затем никогда больше не прикасаться к нему.Его полное отсутствие приводит к утечке памяти (или, в более общем случае, в зависимости от типа элемента, утечке ресурсов), а попытка выпустить ее несколько раз приводит к неопределенному поведению.Использование не-массива формы delete (или free) вместо delete[] для освобождения массива также неопределенное поведение .

69 голосов
/ 16 сентября 2011

5. Распространенные подводные камни при использовании массивов.

5.1 Подводный камень: доверие к небезопасным ссылкам.

ОК, вам сказали или сами узнали, что глобальные переменные (пространство имен переменные области видимости, которые могут быть доступны за пределами модуля перевода) Зло & торговли ;. Но знаете ли вы, как истинно зло и торговля; они есть? Рассмотрим нижеприведенная программа, состоящая из двух файлов [main.cpp] и [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};

В Windows 7 это прекрасно компилируется и связывается с MinGW g ++ 4.4.1 и Visual C ++ 10.0.

Поскольку типы не совпадают, при запуске программы происходит сбой.

The Windows 7 crash dialog

Неформальное объяснение: программа имеет неопределенное поведение (UB) и вместо поэтому он может просто зависнуть, или, возможно, ничего не делать, или могут посылать угрожающие электронные письма президентам США, России, Индии, Китай и Швейцария, и заставьте носовых демонов вылететь из вашего носа.

Практическое объяснение: в main.cpp массив обрабатывается как указатель, помещенный по тому же адресу, что и массив. Для 32-битного исполняемого файла это означает, что первый int значение в массиве, рассматривается как указатель. Т.е. в main.cpp numbers переменная содержит или, по-видимому, содержит (int*)1. Это вызывает программа для доступа к памяти внизу адресного пространства, которое условно защищены и вызывают ловушки. Результат: вы получите сбой.

Компиляторы полностью имеют право не диагностировать эту ошибку, потому что C ++ 11 §3.5 / 10 говорит, о требовании совместимых типов для деклараций

[N3290 §3.5 / 10]
Нарушение этого правила в отношении идентификации типа не требует диагностики.

В этом же абзаце подробно описан допустимый вариант:

* * & Одна тысяча тридцать восемь hellip; объявления для объекта массива могут указывать типы массивов, которые отличаются наличием или отсутствием привязки основного массива (8.3.4).

Это разрешенное изменение не включает объявление имени в виде массива в одном блок перевода, а так же указатель в другой блок перевода.

5.2 Подводный камень: преждевременная оптимизация (memset и друзья).

Еще не написано

5.3 Ловушка: использование языка C для определения количества элементов.

С глубоким опытом C вполне естественно писать & hellip;

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

Поскольку array распадается на указатель на первый элемент, где это необходимо, Выражение sizeof(a)/sizeof(a[0]) также можно записать в виде sizeof(a)/sizeof(*a). Это означает то же самое, и независимо от того, как это записано это C идиома для поиска числовых элементов массива.

Основная ошибка: идиома небезопасна. Например, код & Hellip;

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

передает указатель на N_ITEMS, и поэтому, скорее всего, выдает неправильный результат. Скомпилированный как 32-битный исполняемый файл в Windows 7, он создает & hellip;

7 элементов, вызывающих дисплей ...
1 элемент.

  1. Компилятор переписывает int const a[7] в int const a[].
  2. Компилятор переписывает int const a[] в int const* a.
  3. N_ITEMS поэтому вызывается с указателем.
  4. Для 32-разрядного исполняемого файла sizeof(array) (размер указателя) равен 4.
  5. sizeof(*array) эквивалентно sizeof(int), что для 32-разрядного исполняемого файла также равно 4.

Чтобы обнаружить эту ошибку во время выполнения, вы можете сделать & hellip;

#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 элементов, вызывающих дисплей ...
Ошибка подтверждения: («N_ITEMS требует фактического массива в качестве аргумента», typeid (a)! = Typeid (& * a)), файл runtime_detect ion.cpp, строка 16

Это приложение запросило среду выполнения прекратить его необычным способом.
Пожалуйста, обратитесь в службу поддержки приложения для получения дополнительной информации.

Обнаружение ошибок во время выполнения лучше, чем отсутствие обнаружения, но затрачивается немного время процессора и, возможно, гораздо больше времени программиста. Лучше с обнаружением в КолорадоВремя миль! И если вы счастливы не поддерживать массивы локальных типов с C ++ 98, тогда вы можете сделать это:

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

Компиляция этого определения подставляется в первую полную программу, с g ++, Я получил & hellip;

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: в функции 'void display (const int *)':
compile_time_detection.cpp: 14: ошибка: нет соответствующей функции для вызова 'n_items (const int * &)'

M: \ count> _

Как это работает: массив передается по ссылке на n_items, и так он и делает не затухание указателя на первый элемент, а функция может просто вернуть количество элементов, указанных типом.

С C ++ 11 вы можете использовать это также для массивов локального типа, и это безопасный тип C ++ идиома для нахождения количества элементов массива.

5.4 Подводный камень C ++ 11 и C ++ 14: использование функции размера массива constexpr.

С C ++ 11 и более поздними версиями это естественно, но, как вы увидите, опасно! заменить функцию C ++ 03

typedef ptrdiff_t   Size;

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

с

using Size = ptrdiff_t;

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

, где существенным изменением является использование constexpr, что позволяет эта функция для получения постоянной времени компиляции .

Например, в отличие от функции C ++ 03, такая постоянная времени компиляции может использоваться для объявления массива того же размера, что и другой:

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

Но рассмотрим этот код, используя 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 );
}

Подводный камень: по состоянию на июль 2015 года вышеперечисленное компилируется с MinGW-64 5.1.0 с -pedantic-errors, и, тестирование с онлайн-компиляторами на gcc.godbolt.org / , также с clang 3.0 и лязг 3.2, но не с лязг 3.3, 3.4.1, 3.5.0, 3.5.1, 3.6 (rc1) или 3,7 (экспериментально). И важно для платформы Windows, он не компилируется с Visual C ++ 2015. Причина в утверждении C ++ 11 / C ++ 14 об использовании ссылки в constexpr выражениях:

C ++ 11 C ++ 14 $ 5,19 / 2 девять й тире

A условное выражение e является основным константным выражением , если только оценка e, следуя правилам абстрактной машины (1.9), оценил бы один из следующие выражения:
11

  • an id-выражение , которое относится к переменной или элементу данных ссылочного типа если ссылка не имеет предшествующей инициализации или
    • инициализируется константным выражением или
    • это нестатический элемент данных объекта, время жизни которого началось в оценка е;

Всегда можно написать более многословный

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

& hellip; но это терпит неудачу, когда Collection не является необработанным массивом.

Для работы с коллекциями, которые могут быть не массивами, требуется перегрузка n_items функция, но также, для использования времени компиляции нужно время компиляции представление размера массива. И классическое решение C ++ 03, которое отлично работает также в C ++ 11 и C ++ 14, чтобы функция сообщала свой результат не как значение но через свою функцию результат тип . Например, вот так:

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

О выборе типа возврата для static_n_items: этот код не использует std::integral_constant потому что с std::integral_constant результат представляется непосредственно как значение constexpr, возвращая исходную проблему. Вместо класса Size_carrier можно позволить функции напрямую возвращать ссылка на массив. Однако не все знакомы с этим синтаксисом.

Об именовании: часть этого решения для constexpr -неправильно-из-за-ссылки проблема заключается в том, чтобы сделать выбор постоянной времени компиляции явным.

Надеюсь, проблема "упс-там-была-ссылка-вовлечена-в-твоем" constexpr будет исправлена C ++ 17, но до тех пор макрос, такой как STATIC_N_ITEMS выше, дает переносимость, например компиляторам Clang и Visual C ++, сохраняя безопасность типов.

Связанный: макросы не учитывают области видимости, поэтому во избежание конфликтов имен это может быть идтиИдея использовать префикс имени, например MYLIB_STATIC_N_ITEMS.

...