Как использовать массивы в 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.

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

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] заключается в том, что оба типа могут быть неявно преобразовать to T*, и результатом этого преобразования является указателем на первый элемент массива. То есть в любом месте a 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");

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

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 обозначает массив, затем начнется распад массива на указатель, потому что добавление массива и целого числа бессмысленно (нет операции plus для массивов), но добавление указателя и целого числа имеет смысл:

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

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

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

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

диапазоны

массив типа T[n] и n элементов, проиндексированных от 0 to 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 в качестве первого аргумента. Это немного слишком краткий на мой вкус, и это также делает вывод аргумента шаблона немного сложнее для компилятора, потому что в этом случае первый аргумент является массивом, а второй аргумент-указателем. (Снова начинается распад массива на указатель.)


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

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

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

названа многомерной массивы

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

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

задание

без особых причин массивы не могут быть назначены друг другу. Использовать 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 работает лучше. Если сомневаетесь, измерьте.

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


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

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

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

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

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] is уже указатель на целое число (первый элемент), не массив целых чисел, или указатель на массив целых чисел неизвестных размеров. Это было бы невозможно, потому что система статических типов требует, чтобы размеры массива были константами времени компиляции. (Следовательно, я не аннотировал анонимный массив статическим типом информация на картинке.)

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

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

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

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

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

delete[] p;

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


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

5.1 ловушка: доверительный тип-небезопасная связь.

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

тот же абзац детализирует изменение, которое разрешено:

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

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

5.2 Pitfall: преждевременная оптимизация (memset & друзей).

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

5.3 Pitfall: использование идиомы 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[] to int const* a.
  3. вызывается с помощью указателя.
  4. для 32-разрядного исполняемого файла sizeof(array) (размер a указатель), то 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 ионный.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> г++ compile_time_detection.cpp
compile_time_detection.cpp: в функции'void display(const int*)':
compile_time_detection.cpp: 14: ошибка: нет функции соответствия для вызова ' n_items (const int*&)'

M:\count> _

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

с C++11 вы можете использовать это также для массивов локального типа, и это тип safe в 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 девять!--168-->th тире

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

  • an 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, чтобы функция сообщала свой результат не как значение но через его функцию result тип. Например, вот так:

// 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 class one может позволить функции напрямую возвращать a ссылка на массив. Однако, не все знакомы с этим синтаксисом.

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

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

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