Как я могу использовать массивы в C ++?

480

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 в этой форме, то публикация в meta, с которой все это началось, будет подходящим местом для этого. Этот вопрос отслеживается в чате C ++ , где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)

fredoverflow
источник
Они были бы еще лучше, если бы указатели всегда указывали на начало, а не где-то посередине их цели ...
Дедупликатор
Вам следует использовать вектор STL, поскольку он обеспечивает большую гибкость.
Моиз Саджид
2
С объединенной доступностью std::arrays, std::vectors и gsl::spans - я бы откровенно ожидал, что FAQ по использованию массивов в C ++ скажет: «К настоящему времени вы можете начать рассматривать просто, ну, а не использовать их».
einpoklum

Ответы:

302

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

Тип массива обозначается как , 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*требуется a , вы можете предоставить a T[n], и компилятор тихо предоставит этот указатель:

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

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

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

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

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

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

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 указатель (различима по небольшому прямоугольнику рядом с ней ), но она также может быть результатом функции, возвращающей указатель (или любое другое выражение типаxT* ).

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

Поскольку синтаксис *(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в качестве первого аргумента. На мой вкус это слишком кратко, и это также делает вывод аргумента шаблона немного сложнее для компилятора, потому что в этом случае первый аргумент является массивом, а второй аргумент является указателем. (Опять же, затухание массива в указатель начинается.)

fredoverflow
источник
Случаи, когда массив не распадается на указатель, показаны здесь для справки.
legends2k
@fredoverflow В части Access или Ranges стоит упомянуть, что C-массивы работают с C ++ 11 для циклов на основе диапазона.
gnzlbg
135

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

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

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

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

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

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

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;
fredoverflow
источник
В качестве предложения: Вы должны указать, что int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];а также int (*p)[W] = new int[6][W];и int (*p)[W] = new int[H][W];являются действительными утверждениями, когда Hи Wизвестны во время компиляции.
RobertS поддерживает Монику Челлио
88

присваивание

Без особой причины массивы не могут быть назначены друг другу. Используйте 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из шаблона создается отдельная функция. Вы также можете написать довольно полезные шаблоны функций, которые абстрагируются как от типа элемента, так и от размера.

fredoverflow
источник
2
Возможно, стоит добавить примечание, что даже если void foo(int a[3]) aон выглядит так, как будто вы передаете массив по значению, изменение aвнутри fooизменит исходный массив. Это должно быть ясно, потому что массивы не могут быть скопированы, но это может стоить того, чтобы усилить это.
gnzlbg
C ++ 20 имеетranges::copy(a, b)
LF
int sum( int size_, int a[size_]);- с (я думаю) C99 и далее
шеф-повар Гладиатор
73

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

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

Хорошо, вам сказали или сами узнали, что глобальные переменные (переменные области имен пространства имен, к которым можно обращаться за пределами модуля перевода) - это Evil ™. Но знаете ли вы, насколько они злые ™? Рассмотрим программу ниже, состоящую из двух файлов [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.

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

Диалог сбоя Windows 7

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#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, он производит ...

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.

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

#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

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

Обнаружение ошибок во время выполнения лучше, чем отсутствие обнаружения, но оно тратит немного процессорного времени и, возможно, намного больше программистского времени. Лучше с обнаружением во время компиляции! И если вы счастливы не поддерживать массивы локальных типов с 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 ++, я получил…

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 и clang 3.2, но не с clang 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 девять го тира

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

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

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

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

... но это не удается, когда 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-invalid -по-ссылке-сделать явный выбор постоянной времени компиляции.

Надеемся, что проблема «упс, была ссылка вовлечена в вашу constexprпроблему» будет исправлена ​​в C ++ 17, но до этого макрос, подобный приведенному STATIC_N_ITEMSвыше, дает переносимость, например, компиляторам clang и Visual C ++, сохраняя тип безопасность.

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

Ура и hth. Альф
источник
1
+1 Отличный тест C-кода: я потратил 15 минут на VC ++ 10.0 и GCC 4.1.2, пытаясь исправить Segmentation fault... Я наконец нашел / понял после прочтения ваших объяснений! Пожалуйста, напишите ваш §5.2 раздел :-) Приветствия
olibre
Хорошо. Один нит - тип возвращаемого значения для countOf должен быть size_t вместо ptrdiff_t. Вероятно, стоит упомянуть, что в C ++ 11/14 это должно быть constexpr и noexcept.
Ricky65
@ Ricky65: Спасибо за упоминание C ++ 11. Поддержка этих функций была запоздалой для Visual C ++. Что касается size_tэтого, у него нет преимуществ, которые я знаю для современных платформ, но у него есть ряд проблем из-за неявных правил преобразования типов в C и C ++. То есть ptrdiff_tиспользуется очень намеренно, чтобы избежать проблем с size_t. Однако следует помнить, что у g ++ есть проблема с сопоставлением размера массива с параметром шаблона, если это не так size_t(я не думаю, что эта специфичная для компилятора проблема с non- size_tважна, но YMMV).
ура и hth. - Alf
@Alf. В Стандартном рабочем проекте (N3936) 8.3.4 я читаю - Граница массива - это «преобразованное константное выражение типа std :: size_t, и его значение должно быть больше нуля».
Ricky65
@Ricky: Если вы ссылаетесь на несоответствие, этого утверждения нет в текущем стандарте C ++ 11, поэтому сложно угадать контекст, но есть противоречие (динамически размещаемый массив может иметь границу 0 для каждого C +) +11 §5.3.4 / 7), вероятно, не окажется в C ++ 14. Шашки - это всего лишь шашки. Если вы вместо этого спрашиваете о том, что означает «его», это относится к исходному выражению, а не к преобразованному. Если с третьей стороны вы упоминаете об этом, потому что вы думаете, что, возможно, такое предложение означает, что следует использовать size_tдля обозначения размеров массивов, нет, конечно, это не так.
ура и hth. - Альф
72

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

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

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

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

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

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

  • Если Tэто POD (как intв приведенном выше примере), инициализация не происходит.
  • В противном случае конструктор default 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 - нет (требуется хотя бы одно значение). Также обратите внимание, что инициализаторы массива могут использоваться только для инициализации массивов; они не могут позже использоваться в назначениях.

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

Статические массивы (массивы, расположенные «в сегменте данных») - это локальные переменные массива, определенные с помощью 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[]освобождения массива также является неопределенным поведением .

fredoverflow
источник
2
Устаревшее staticиспользование в области имен было удалено в C ++ 11.
legends2k
Поскольку newоператор is, он, безусловно, может вернуть массив allcated по ссылке. Там просто нет никакого смысла в этом ...
Дедупликатор
@Deduplicator Нет, не может, потому что исторически newнамного старше, чем ссылки.
fredoverflow
@FredOverflow: Есть причина, по которой он не может вернуть ссылку, он просто полностью отличается от письменного объяснения.
дедупликатор
2
@Deduplicator Я не думаю, что существует ссылка на массив неизвестных границ. По крайней мере, G ++ отказывается компилироватьint a[10]; int (&r)[] = a;
fredoverflow