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

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 .

_(Note: This is meant to be an entry to [Stack Overflow's C++ FAQ](https://stackoverflow.com/questions/tagged/c++-faq). If you want to critique the idea of providing an FAQ in this form, then [the posting on meta that started all this](https://meta.stackexchange.com/questions/68647/setting-up-a-faq-for-the-c-tag) would be the place to do that. Answers to that question are monitored in the [C++ chatroom](https://chat.stackoverflow.com/rooms/10/c-lounge), where the FAQ idea started out in the first place, so your answer is very likely to get read by those who came up with the idea.)_

person fredoverflow    schedule 26.01.2011    source источник
comment
Было бы даже лучше, если бы указатели всегда указывали на начало, а не где-то посередине своей цели ...   -  person Deduplicator    schedule 07.07.2014
comment
Вам следует использовать вектор STL, потому что он обеспечивает большую гибкость.   -  person Moiz Sajid    schedule 30.08.2015
comment
С объединенной доступностью std::arrays, std::vectors и gsl::spans - я, честно говоря, ожидал, что в FAQ о том, как использовать массивы в C ++, будет сказано, что к настоящему времени вы можете начать рассматривать просто, ну, не их использование.   -  person einpoklum    schedule 19.03.2017


Ответы (5)


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

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

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

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

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

person fredoverflow    schedule 26.01.2011
comment
Случаи, когда массив не превращается в указатель, проиллюстрированы здесь для справки. - person legends2k; 06.01.2014
comment
@fredoverflow В части Access или Ranges стоит упомянуть, что C-массивы работают с циклами for на основе диапазонов C ++ 11. - person gnzlbg; 28.06.2017
comment
Замечательный ответ. Утверждение «Это преобразование известно как распад массива в указатель, и это главный источник путаницы» - является точным, в немалой степени потому, что оно известно как таковое только на lingua franca. Нигде в языковых проектах или стандартах эта номенклатура не используется хотя бы один раз при описании контекстов преобразования в временный указатель. - person WhozCraig; 15.02.2021

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

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

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

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

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

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

Обратите внимание, что 2D-сетки, подобные приведенным выше, являются просто полезной визуализацией. С точки зрения 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;
person fredoverflow    schedule 26.01.2011
comment
В качестве предложения: вы должны указать, что 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 известны во время компиляции. - person RobertS supports Monica Cellio; 16.02.2020
comment
Большое спасибо! Подскажите, пожалуйста, как установить / получить элементы из массива (анонимные массивы указателей). - person Borko Djurovic; 27.12.2020
comment
(очередь редактирования заполнена, поэтому я вместо этого комментирую) Было бы неплохо прямо упомянуть, что для выпуска анонимного многомерного массива правильный синтаксис просто delete[] p - person philb; 03.03.2021

Назначение

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

person fredoverflow    schedule 26.01.2011
comment
Возможно, стоит добавить примечание о том, что даже если в void foo(int a[3]) a действительно похоже, что массив передается по значению, изменение a внутри foo изменит исходный массив. Это должно быть ясно, потому что массивы нельзя скопировать, но, возможно, стоит усилить это. - person gnzlbg; 28.06.2017
comment
В C ++ 20 есть ranges::copy(a, b) - person L. F.; 19.09.2019
comment
int sum( int size_, int a[size_]); - от (я думаю) C99 и далее - person Chef Gladiator; 18.03.2020

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

In Windows 7 this compiles and links fine with both MinGW g++ 4.4.1 and Visual C++ 10.0.

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

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

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

Практическое объяснение: в main.cpp массив рассматривается как указатель, размещенный по тому же адресу, что и массив. Для 32-разрядного исполняемого файла это означает, что первое значение int в массиве рассматривается как указатель. То есть в main.cpp переменная numbers содержит или, кажется, содержит (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 для поиска числовых элементов массива.

Основная ошибка: идиома 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 элементов, вызов display ...
Ошибка утверждения: («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 ++, я получил

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: В функции 'void display (const int *)':
compile_time_detection.cpp: 14: error: нет соответствующей функции для вызова '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 nineth dash

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

  • an id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either
    • it is initialized with a constant expression or
    • это нестатический член данных объекта, время существования которого началось в пределах вычисления 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, состоит в том, чтобы позволить функции сообщать свой результат не как значение, а через свой результат функции type. Например так:

// 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-due-to-reference является явный выбор постоянной времени компиляции.

Надеюсь, проблема oops-there-was-a-reference-connected-in-your-constexpr будет исправлена ​​в C ++ 17, но до тех пор макрос, подобный приведенному выше STATIC_N_ITEMS, обеспечивает переносимость, например компиляторам clang и Visual C ++, сохраняя безопасность типов.

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

person Cheers and hth. - Alf    schedule 16.09.2011
comment
+1 Отличный тест на кодирование C: я потратил 15 минут на VC ++ 10.0 и GCC 4.1.2, пытаясь исправить _1 _... Я наконец нашел / понял после прочтения ваших объяснений! Напишите, пожалуйста, свой раздел §5.2 :-) Ура - person oHo; 08.11.2012
comment
Хороший. Одна ниточка - тип возвращаемого значения для countOf должен быть size_t вместо ptrdiff_t. Вероятно, стоит упомянуть, что в C ++ 11/14 это должно быть constexpr и noexcept. - person Ricky65; 27.05.2014
comment
@ Ricky65: Спасибо за упоминание соображений C ++ 11. Поддержка этих функций в Visual C ++ появилась с опозданием. Что касается size_t, у этого нет преимуществ, о которых я знаю для современных платформ, но он имеет ряд проблем из-за правил неявного преобразования типов C и C ++. То есть ptrdiff_t используется очень намеренно, чтобы избежать проблем с size_t. Однако следует знать, что у g ++ есть проблема с сопоставлением размера массива с параметром шаблона, если только он не size_t (я не думаю, что эта специфическая для компилятора проблема с не-size_t важна, но YMMV). - person Cheers and hth. - Alf; 28.05.2014
comment
@Alf. В стандартном рабочем проекте (N3936) 8.3.4 я прочитал - граница массива - это ... преобразованное постоянное выражение типа std :: size_t, и его значение должно быть больше нуля. - person Ricky65; 28.05.2014
comment
@Ricky: Если вы имеете в виду несоответствие, этого утверждения нет в текущем стандарте C ++ 11, поэтому трудно угадать контекст, но противоречие (динамически выделяемый массив может быть of bound 0, согласно C ++ 11 §5.3.4 / 7), вероятно, не попадет в C ++ 14. Шашки - это всего лишь шашки. Если вы вместо этого спрашиваете, к чему это относится, это относится к исходному выражению, а не к преобразованному. Если, с третьей стороны, вы упомянули об этом, потому что думаете, что, возможно, такое предложение означает, что для обозначения размеров массивов следует использовать size_t, нет, конечно, это не так. - person Cheers and hth. - Alf; 28.05.2014
comment
@Alf. Если ptrdiff_t лучше, то почему Стандарт использует std :: size_t при передаче массива по ссылке? Например, std :: begin объявлен как template ‹class T, size_t N› constexpr T * begin (T (& array) [N]) noexcept; - person Ricky65; 28.05.2014
comment
@ Ricky65: почему бы тебе не погуглить. есть, например, ТАК отвечает за это. Но короткий ответ: по историческим причинам. - person Cheers and hth. - Alf; 28.05.2014
comment
@Alf - мне не удалось выяснить причину. - person Ricky65; 28.05.2014
comment
@ Ricky65: Во-первых, обратите внимание, что, например, std::count не возвращает беззнаковый результат. Чтобы узнать о конкретных причинах отсутствия подписи size_t, обратитесь к The Design and Evolution of C ++, книге Страуструпа, которую я не нет. Если в этой книге указана причина, то она окончательная, поскольку исходит от создателя языка. В противном случае поиск в Google привел меня сначала к моему собственному ответу на SO. Вы также можете посмотреть моя статья в блоге. - person Cheers and hth. - Alf; 28.05.2014
comment
@ Cheersandhth.-Alf: Если вам нужен подписанный тип, почему бы не использовать POSIX.2 / C11 ssize_t (упоминается в сноске стандарта C ++ 11), если вы не уверены, что он существует с объявлением using перед ним: using ssize_t = typename std::make_signed<size_t>::type; - person Deduplicator; 28.06.2014
comment
@Deduplicator: Почему бы не использовать POSIX.2 / C11 ssize_t, это немного зависит от того, спрашиваете ли вы об использовании ptrdiff_t в этом элементе часто задаваемых вопросов или об использовании его в целом. Для простых ответов SO и т. Д. Это IMO - хорошая вещь, чтобы все было просто, чтобы не вводить посторонних зависимостей или сложностей. В общем случае я не уверен. Кажется вероятным, что Posix ssize_t не существовало бы, если бы у него не было преимуществ перед ptrdiff_t. Но я понятия не имею, что. - person Cheers and hth. - Alf; 28.06.2014
comment
@ Cheersandhth.-Alf: Как минимум, он семантически более подходит для размеров, чем ptrdiff ... - person Deduplicator; 28.06.2014

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

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

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

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

person fredoverflow    schedule 13.02.2011
comment
Прекращение использования static в области пространства имен было удалено в C ++ 11. - person legends2k; 17.05.2013
comment
Поскольку new является оператором am, он, безусловно, может вернуть массив allcated по ссылке. В этом нет никакого смысла ... - person Deduplicator; 28.06.2014
comment
@Deduplicator Нет, не может, потому что исторически new намного старше ссылок. - person fredoverflow; 28.06.2014
comment
@FredOverflow: Есть причина, по которой он не может вернуть ссылку, это просто полностью отличается от письменного объяснения. - person Deduplicator; 28.06.2014
comment
@Deduplicator Разрешено ли операторам иметь возвращаемые типы, которые меняются во время выполнения? - person Buge; 26.08.2014
comment
Хорошая точка зрения. Если new возвращается по ссылке, каким будет тип new int[n], где n - значение, которое не известно статически? - person fredoverflow; 26.08.2014
comment
@Buge: Хороший момент. Тем не менее, как насчет ссылки на массив с неизвестной границей? - person Deduplicator; 27.08.2014
comment
@Deduplicator Я не думаю, что существует ссылка на массив с неизвестными границами. По крайней мере, g ++ отказывается компилировать int a[10]; int (&r)[] = a; - person fredoverflow; 27.08.2014
comment
Замечание по этому поводу: Новое размещение массива требует неопределенных накладных расходов в буфере, поэтому не смешивайте новое размещение и массивы, если только вы знаете точно, как работают внутренние компоненты вашего конкретного компилятора. - person Mooing Duck; 04.02.2019