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

452

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

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

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

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

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

  • 0
    Они были бы еще лучше, если бы указатели всегда указывали на начало, а не где-то посередине их цели ...
  • 0
    Вам следует использовать вектор STL, поскольку он обеспечивает большую гибкость.
Показать ещё 1 комментарий
Теги:
arrays
multidimensional-array
pointers
c++-faq

5 ответов

280
Лучший ответ

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

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

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

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*.

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

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

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

Указав указатель 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) немного неуклюжий, С++ предоставляет альтернативный синтаксис 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. И тем не менее, для поддержки полуоткрытых диапазонов (где начало является инклюзивным, а конец - исключительным), С++ позволяет вычислять указатель на (несуществующий) 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) технически вызывает undefined поведение в С++ (но не на C99).

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

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

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

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

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

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

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

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-сетки, такие как выше, являются просто полезными визуализацией. С точки зрения С++, память представляет собой "плоскую" последовательность байтов. Элементы многомерного массива хранятся в строчном порядке. То есть 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;
82

Назначение

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

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

  • 2
    Может быть стоит добавить примечание , что даже Тхо в void foo(int a[3]) выглядит как один проходит массив по значению, изменения a внутри foo будет изменять исходный массив. Это должно быть ясно, потому что массивы не могут быть скопированы, но это может стоить того, чтобы усилить это.
68

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

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

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

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

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

int global_static_array[8];

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

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

Вот как статические массивы ведут себя по-другому от автоматических массивов:

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

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

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

Элементы данных массива создаются, когда создается их собственный объект. К сожалению, С++ 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);
    }
};

В С++ 0x массивы могут быть инициализированы в списке инициализаторов членов, благодаря равномерной инициализации:

class Foo
{
    int primes[8];

public:

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

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

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

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

В C анонимные массивы создаются через malloc и друзей. В С++ анонимные массивы создаются с использованием синтаксиса 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]();

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

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

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

delete[] p;

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

  • 2
    Устаревшее static использование в области пространства имен было удалено в C ++ 11.
  • 0
    Поскольку оператор new is am, он, безусловно, может вернуть массив allcated по ссылке. Там просто нет смысла в этом ...
Показать ещё 7 комментариев
62

5. Общие ошибки при использовании массивов.

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

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

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

Изображение 1203

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

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

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

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

В этом же параграфе указан вариант, который разрешен:

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

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

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

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

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

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

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

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

Основная ловушка: идиома C не является типичной. Например, код & Hellip;

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

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

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

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

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

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7 элементов, вызывающих дисплей...
Утверждение не выполнено: ( "N_ITEMS требует фактического массива в качестве аргумента", typeid (a)!= Typeid (& * a)), файл runtime_detect ion.cpp, строка 16

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

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

#include <stddef.h>

typedef ptrdiff_t   Size;

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

#define N_ITEMS( array )       n_items( array )

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

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

M:\count > _

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

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

5.4 С++ 11 и С++ 14 ловушка: использование функции размера массива constexpr.

С С++ 11 и позже это естественно, но, как вы увидите, опасно!, чтобы замените функцию С++ 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, что позволяет эта функция создает константу времени компиляции.

Например, в отличие от функции С++ 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 С++ 2015. Причина - это утверждение С++ 11/С++ 14 об использовании ссылки в выражениях constexpr:

С++ 11 С++ 14 $5.19/2 девять th тире

условное выражение 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
}

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

Для обработки коллекций, которые могут быть не-массивами, требуется перегрузочная способность N_ITEMS, но также для использования времени компиляции необходимо время компиляции представление размера массива. И классическое решение С++ 03, которое отлично работает также в С++ 11 и С++ 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-due-to-reference проблема заключается в том, чтобы сделать выбор постоянной времени компиляции явной.

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

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

  • 1
    +1 Отличный тест C-кода: я потратил 15 минут на VC ++ 10.0 и GCC 4.1.2, пытаясь исправить Segmentation fault ... Я наконец нашел / понял после прочтения ваших объяснений! Пожалуйста, напишите ваш §5.2 раздел :-) Приветствия
  • 0
    Хорошо. Один нит - тип возвращаемого значения для countOf должен быть size_t вместо ptrdiff_t. Вероятно, стоит упомянуть, что в C ++ 11/14 это должно быть constexpr и noexcept.
Показать ещё 10 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню