C ++ унаследовал массивы от C, где они используются практически везде. C ++ предоставляет абстракции, которые проще в использовании и менее подвержены ошибкам ( std::vector<T>
начиная с C ++ 98 и std::array<T, n>
начиная с C ++ 11 ), поэтому потребность в массивах возникает не так часто, как в C. Однако, когда вы читаете устаревшее Кодировать или взаимодействовать с библиотекой, написанной на C, вы должны четко понимать, как работают массивы.
Этот FAQ разделен на пять частей:
- массивы на уровне типа и доступ к элементам
- создание и инициализация массива
- назначение и передача параметров
- многомерные массивы и массивы указателей
- распространенные подводные камни при использовании массивов
Если вы чувствуете, что чего-то важного не хватает в этом FAQ, напишите ответ и привяжите его сюда в качестве дополнительной части.
В следующем тексте «массив» означает «массив C», а не шаблон класса std::array
. Базовые знания синтаксиса декларатора C предполагаются. Обратите внимание, что ручное использование new
и, delete
как показано ниже, чрезвычайно опасно перед лицом исключений, но это тема другого FAQ .
(Примечание. Предполагается, что это будет вход в FAQ по C ++ в Stack Overflow . Если вы хотите критиковать идею предоставления FAQ в этой форме, то публикация в meta, с которой все это началось, будет подходящим местом для этого. Этот вопрос отслеживается в чате C ++ , где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)
std::array
s,std::vector
s иgsl::span
s - я бы откровенно ожидал, что FAQ по использованию массивов в C ++ скажет: «К настоящему времени вы можете начать рассматривать просто, ну, а не использовать их».Ответы:
Массивы на уровне типа
Тип массива обозначается как ,
T[n]
гдеT
это тип элемента иn
является положительным размер , количество элементов в массиве. Тип массива - это тип продукта типа элемента и размера. Если один или оба из этих ингредиентов различаются, вы получаете различный тип:Обратите внимание, что размер является частью типа, то есть типы массивов различного размера являются несовместимыми типами, которые абсолютно не связаны друг с другом.
sizeof(T[n])
эквивалентноn * sizeof(T)
.Распад массива в указатель
Единственная «связь» между
T[n]
иT[m]
заключается в том, что оба типа могут быть неявно преобразованы вT*
, и результатом этого преобразования является указатель на первый элемент массива. То есть, гдеT*
требуется a , вы можете предоставить aT[n]
, и компилятор тихо предоставит этот указатель:Это преобразование известно как «распад массива в указатель», и оно является основным источником путаницы. В этом процессе размер массива теряется, так как он больше не является частью type (
T*
). Pro: забывание размера массива на уровне типа позволяет указателю указывать на первый элемент массива любого размера. Con: Учитывая указатель на первый (или любой другой) элемент массива, невозможно определить, насколько велик этот массив или куда именно указывает указатель относительно границ массива. Указатели очень глупы .Массивы не указатели
Компилятор будет автоматически генерировать указатель на первый элемент массива всякий раз, когда это будет сочтено полезным, то есть всякий раз, когда операция завершается с ошибкой в массиве, но завершается успешно с указателем. Это преобразование из массива в указатель является тривиальным, поскольку полученное значение указателя является просто адресом массива. Обратите внимание, что указатель не хранится как часть самого массива (или где-либо еще в памяти). Массив не является указателем.
Одним из важных контекста , в котором массив никак не распадаться в указатель на его первый элемент является , когда
&
оператор применяется к нему. В этом случае&
оператор выдает указатель на весь массив, а не только указатель на его первый элемент. Хотя в этом случае значения (адреса) совпадают, указатель на первый элемент массива и указатель на весь массив являются совершенно разными типами:Следующее искусство ASCII объясняет это различие:
Обратите внимание, что указатель на первый элемент указывает только на одно целое число (изображено в виде маленького прямоугольника), тогда как указатель на весь массив указывает на массив из 8 целых чисел (изображен в виде большого прямоугольника).
Такая же ситуация возникает в классах и, возможно, более очевидна. Указатель на объект и указатель на его первый элемент данных имеют одно и то же значение (один и тот же адрес), но это совершенно разные типы.
Если вы не знакомы с синтаксисом объявления C, скобки в типе
int(*)[8]
имеют важное значение:int(*)[8]
указатель на массив из 8 целых чиселint*[8]
это массив из 8 указателей, каждый элемент типаint*
.Доступ к элементам
C ++ предоставляет два синтаксических варианта для доступа к отдельным элементам массива. Ни один из них не превосходит другого, и вам следует ознакомиться с обоими.
Арифметика указателей
Учитывая указатель
p
на первый элемент массива, выражениеp+i
дает указатель на i-й элемент массива. Разыменовав этот указатель впоследствии, можно получить доступ к отдельным элементам:Если
x
обозначает массив , то затухание массива в указатель наступит, потому что добавление массива и целого числа не имеет смысла (нет операции плюс для массивов), но имеет смысл добавить указатель и целое число:(Обратите внимание, что неявно созданный указатель не имеет имени, поэтому я написал
x+0
для его идентификации.)Если, с другой стороны,
x
обозначает указатель на первый (или любой другой) элемент массива, то затухание между массивом и указателем не является необходимым, поскольку указатель, на которыйi
планируется добавить, уже существует:Обратите внимание, что в изображенном случае переменная-
x
указатель (различима по небольшому прямоугольнику рядом с ней ), но она также может быть результатом функции, возвращающей указатель (или любое другое выражение типаx
T*
).Оператор индексации
Поскольку синтаксис
*(x+i)
немного неуклюжий, C ++ предоставляет альтернативный синтаксисx[i]
:Из-за того, что сложение является коммутативным, следующий код делает то же самое:
Определение оператора индексации приводит к следующей интересной эквивалентности:
Тем не менее,
&x[0]
как правило, не эквивалентноx
. Первый - это указатель, второй - массив. Только когда контекст запускает затухание массива в указатель, можноx
и&x[0]
можно использовать взаимозаменяемо. Например:В первой строке компилятор обнаруживает присваивание от указателя на указатель, что тривиально успешно. Во второй строке он обнаруживает присвоение из массива указателю. Так как это бессмысленно (но указатель на присвоение указателя имеет смысл), затухание массива в указатель включается как обычно.
Изменяется
Массив типа
T[n]
имеетn
элементы, проиндексированные с0
доn-1
; Там нет элементаn
. И все же, для поддержки полуоткрытых диапазонов (где начало включительно, а конец исключительно ), C ++ позволяет вычислять указатель на (несуществующий) n-й элемент, но разыменование этого указателя недопустимо:Например, если вы хотите отсортировать массив, оба следующих варианта будут работать одинаково хорошо:
Обратите внимание, что в
&x[n]
качестве второго аргумента нельзя указывать, поскольку это эквивалентно&*(x+n)
, а*(x+n)
подвыражение технически вызывает неопределенное поведение. в C ++ (но не в C99).Также обратите внимание, что вы могли бы просто предоставить
x
в качестве первого аргумента. На мой вкус это слишком кратко, и это также делает вывод аргумента шаблона немного сложнее для компилятора, потому что в этом случае первый аргумент является массивом, а второй аргумент является указателем. (Опять же, затухание массива в указатель начинается.)источник
Программисты часто путают многомерные массивы с массивами указателей.
Многомерные массивы
Большинство программистов знакомы с именованными многомерными массивами, но многие не знают о том, что многомерный массив также можно создавать анонимно. Многомерные массивы часто называют «массивами массивов» или « истинными многомерными массивами».
Именованные многомерные массивы
При использовании именованных многомерных массивов все измерения должны быть известны во время компиляции:
Вот как именованный многомерный массив выглядит в памяти:
Обратите внимание, что двумерные сетки, такие как приведенные выше, являются просто полезными визуализациями. С точки зрения C ++ память представляет собой «плоскую» последовательность байтов. Элементы многомерного массива хранятся в главном порядке строк. То есть
connect_four[0][6]
иconnect_four[1][0]
есть соседи по памяти. На самом делеconnect_four[0][7]
иconnect_four[1][0]
обозначим один и тот же элемент! Это означает, что вы можете взять многомерные массивы и рассматривать их как большие одномерные массивы:Анонимные многомерные массивы
При использовании анонимных многомерных массивов все измерения, кроме первого, должны быть известны во время компиляции:
Вот как выглядит анонимный многомерный массив в памяти:
Обратите внимание, что сам массив все еще выделяется как один блок в памяти.
Массивы указателей
Вы можете преодолеть ограничение фиксированной ширины, введя другой уровень косвенности.
Именованные массивы указателей
Вот именованный массив из пяти указателей, которые инициализируются анонимными массивами различной длины:
А вот как это выглядит в памяти:
Поскольку теперь каждая строка выделяется индивидуально, просмотр 2D-массивов как 1D-массивов больше не работает.
Анонимные массивы указателей
Вот анонимный массив из 5 (или любого другого числа) указателей, которые инициализируются анонимными массивами различной длины:
А вот как это выглядит в памяти:
Конверсии
Распад массива в указатель естественным образом распространяется на массивы массивов и массивы указателей:
Тем не менее, нет неявного преобразования из
T[h][w]
вT**
. Если бы такое неявное преобразование существовало, результатом был бы указатель на первый элемент массиваh
указателей наT
(каждый указатель на первый элемент строки в исходном двумерном массиве), но этот массив указателей нигде не существует в памяти еще нет. Если вы хотите такое преобразование, вы должны создать и заполнить требуемый массив указателей вручную:Обратите внимание, что это создает представление исходного многомерного массива. Если вам нужна копия, вы должны создать дополнительные массивы и скопировать данные самостоятельно:
источник
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
известны во время компиляции.присваивание
Без особой причины массивы не могут быть назначены друг другу. Используйте
std::copy
вместо этого:Это более гибко, чем то, что могло бы обеспечить истинное назначение массива, потому что можно копировать фрагменты больших массивов в меньшие массивы.
std::copy
обычно специализируется на примитивных типах, чтобы дать максимальную производительность. Вряд лиstd::memcpy
работает лучше. Если сомневаетесь, измерьте.Хотя вы не можете назначать массивы напрямую, вы можете назначать структуры и классы, которые содержат члены массива. Это потому, что члены массива копируются для каждого элемента с помощью оператора присваивания, который предоставляется компилятором по умолчанию. Если вы определяете оператор присваивания вручную для своих собственных типов структуры или класса, вы должны вернуться к ручному копированию для элементов массива.
Передача параметров
Массивы не могут быть переданы по значению. Вы можете передать их по указателю или по ссылке.
Передать по указателю
Поскольку сами массивы не могут быть переданы по значению, обычно указатель на их первый элемент передается по значению. Это часто называют «передачей по указателю». Поскольку размер массива не может быть получен через этот указатель, вы должны передать второй параметр, указывающий размер массива (классическое решение C), или второй указатель, указывающий после последнего элемента массива (решение итератора C ++) :
В качестве синтаксической альтернативы вы также можете объявить параметры как
T p[]
, и это означает то же самое, что иT* p
в контексте списков параметров :Вы можете думать о компиляторе как о переписывании
T p[]
толькоT *p
в контексте списков параметров . Это специальное правило частично отвечает за всю путаницу с массивами и указателями. В любом другом контексте объявление чего-либо как массива или указателя делает огромный значение.К сожалению, вы также можете указать размер в параметре массива, который компилятор игнорирует. То есть следующие три сигнатуры в точности эквивалентны, как указано в ошибках компилятора:
Передать по ссылке
Массивы также могут быть переданы по ссылке:
В этом случае размер массива значителен. Поскольку написание функции, которая принимает только массивы ровно из 8 элементов, бесполезно, программисты обычно пишут такие функции как шаблоны:
Обратите внимание, что такой шаблон функции можно вызывать только с действительным массивом целых чисел, а не с указателем на целое число. Размер массива определяется автоматически, и для каждого размера
n
из шаблона создается отдельная функция. Вы также можете написать довольно полезные шаблоны функций, которые абстрагируются как от типа элемента, так и от размера.источник
void foo(int a[3])
a
он выглядит так, как будто вы передаете массив по значению, изменениеa
внутриfoo
изменит исходный массив. Это должно быть ясно, потому что массивы не могут быть скопированы, но это может стоить того, чтобы усилить это.ranges::copy(a, b)
int sum( int size_, int a[size_]);
- с (я думаю) C99 и далее5. Распространенные подводные камни при использовании массивов.
5.1 Подводный камень: доверие к небезопасным ссылкам.
Хорошо, вам сказали или сами узнали, что глобальные переменные (переменные области имен пространства имен, к которым можно обращаться за пределами модуля перевода) - это Evil ™. Но знаете ли вы, насколько они злые ™? Рассмотрим программу ниже, состоящую из двух файлов [main.cpp] и [numbers.cpp]:
В Windows 7 это прекрасно компилируется и связывается как с MinGW g ++ 4.4.1, так и с Visual C ++ 10.0.
Поскольку типы не совпадают, при запуске программы происходит сбой.
Формальное объяснение: программа имеет неопределенное поведение (UB), и поэтому вместо сбоя она может просто зависнуть или, возможно, ничего не делать, или может послать угрожающие электронные письма президентам США, России, Индии, Китай и Швейцария, и заставить носовых демонов вылетать из носа.
Практическое объяснение: в
main.cpp
массиве рассматривается как указатель, размещенный по тому же адресу, что и массив. Для 32-битного исполняемого файла это означает, что первоеint
значение в массиве рассматривается как указатель. Т.е., в переменный содержит или содержит , как представляется, . Это приводит к тому, что программа получает доступ к памяти внизу адресного пространства, которое традиционно резервируется и вызывает ловушку. Результат: вы получите сбой.main.cpp
numbers
(int*)1
Компиляторы полностью в пределах своих прав не диагностировать эту ошибку, потому что в C ++ 11 §3.5 / 10 говорится о требовании совместимых типов для объявлений,
В том же абзаце подробно описаны возможные варианты:
Это допустимое изменение не включает объявление имени в виде массива в одной единице перевода и в качестве указателя в другой единице перевода.
5.2 Ловушка: преждевременная оптимизация (
memset
и друзья).Еще не написано
5.3 Подводный камень: Использование языка C для определения количества элементов.
С глубоким опытом C естественно написать…
Так как
array
распадается указатель на первый элемент, где это необходимо, выражениеsizeof(a)/sizeof(a[0])
также может быть записано какsizeof(a)/sizeof(*a)
. Это означает то же самое, и независимо от того, как оно написано, это идиома C для поиска числовых элементов массива.Основная ошибка: идиома небезопасна. Например, код ...
передает указатель на
N_ITEMS
, и, следовательно, скорее всего, дает неправильный результат. Скомпилированный как 32-битный исполняемый файл в Windows 7, он производит ...int const a[7]
простоint const a[]
.int const a[]
вint const* a
.N_ITEMS
поэтому вызывается с указателем.sizeof(array)
(размер указателя) тогда 4.sizeof(*array)
эквивалентноsizeof(int)
, что для 32-разрядного исполняемого файла также 4.Чтобы обнаружить эту ошибку во время выполнения, вы можете сделать ...
Обнаружение ошибок во время выполнения лучше, чем отсутствие обнаружения, но оно тратит немного процессорного времени и, возможно, намного больше программистского времени. Лучше с обнаружением во время компиляции! И если вы счастливы не поддерживать массивы локальных типов с C ++ 98, то вы можете сделать это:
Скомпилировав это определение, подставленное в первую полную программу, с g ++, я получил…
Как это работает: массив передается по ссылке на
n_items
, и поэтому он не гниет с указателем на первый элемент, а функция просто возвращает количество элементов , указанных типа.С C ++ 11 вы можете использовать это также для массивов локального типа, и это типизированная идиома C ++ для нахождения количества элементов массива.
5.4 Подводные камни C ++ 11 и C ++ 14: Использование
constexpr
функции размера массива.С C ++ 11 и более поздними версиями это естественно, но, как вы увидите, опасно !, заменить функцию C ++ 03
с
где существенным изменением является использование
constexpr
, которое позволяет этой функции создавать постоянную времени компиляции .Например, в отличие от функции C ++ 03, такая константа времени компиляции может использоваться для объявления массива того же размера, что и другой:
Но рассмотрим этот код, используя
constexpr
версию:Подводный камень: по состоянию на июль 2015 года вышеперечисленное компилируется с MinGW-64 5.1.0 с
C ++ 11 C ++ 14 $ 5,19 / 2 девять го тира-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
выражениях:Всегда можно написать более многословный
... но это не удается, когда
Collection
не является необработанным массивом.Чтобы иметь дело с коллекциями, которые могут быть не массивами, требуется перегрузка
n_items
функции, но также для использования во время компиляции необходимо представление размера массива во время компиляции. И классическое решение C ++ 03, которое отлично работает также в C ++ 11 и C ++ 14, состоит в том, чтобы позволить функции сообщать о своем результате не как значение, а через свой тип результата функции . Например, вот так:О выборе типа возвращаемого значения для
static_n_items
: этот код не используется,std::integral_constant
посколькуstd::integral_constant
результат представляется непосредственно в видеconstexpr
значения, вновь возвращая исходную проблему. ВместоSize_carrier
класса можно позволить функции напрямую возвращать ссылку на массив. Однако не все знакомы с этим синтаксисом.О наименовании: часть этого решения проблемы
constexpr
-invalid -по-ссылке-сделать явный выбор постоянной времени компиляции.Надеемся, что проблема «упс, была ссылка вовлечена в вашу
constexpr
проблему» будет исправлена в C ++ 17, но до этого макрос, подобный приведенномуSTATIC_N_ITEMS
выше, дает переносимость, например, компиляторам clang и Visual C ++, сохраняя тип безопасность.Связанный: макросы не относятся к областям видимости, поэтому, чтобы избежать конфликтов имен, было бы неплохо использовать префикс имени, например
MYLIB_STATIC_N_ITEMS
.источник
Segmentation fault
... Я наконец нашел / понял после прочтения ваших объяснений! Пожалуйста, напишите ваш §5.2 раздел :-) Приветствияsize_t
этого, у него нет преимуществ, которые я знаю для современных платформ, но у него есть ряд проблем из-за неявных правил преобразования типов в C и C ++. То естьptrdiff_t
используется очень намеренно, чтобы избежать проблем сsize_t
. Однако следует помнить, что у g ++ есть проблема с сопоставлением размера массива с параметром шаблона, если это не такsize_t
(я не думаю, что эта специфичная для компилятора проблема с non-size_t
важна, но YMMV).size_t
для обозначения размеров массивов, нет, конечно, это не так.Создание и инициализация массива
Как и с любым другим типом объекта C ++, массивы могут храниться либо непосредственно в именованных переменных (тогда размер должен быть константой времени компиляции; C ++ не поддерживает VLA ), либо они могут храниться анонимно в куче и доступны косвенно через указатели (только тогда размер может быть вычислен во время выполнения).
Автоматические массивы
Автоматические массивы (массивы, «живущие в стеке») создаются каждый раз, когда поток управления проходит через определение нестатической переменной локального массива:
Инициализация выполняется в порядке возрастания. Обратите внимание, что начальные значения зависят от типа элемента
T
:T
это POD (какint
в приведенном выше примере), инициализация не происходит.T
инициализирует все элементы.T
предоставлен доступный конструктор по умолчанию, программа не компилируется.В качестве альтернативы, начальные значения могут быть явно указаны в инициализаторе массива , в списке через запятую, заключенном в фигурные скобки:
Поскольку в этом случае количество элементов в инициализаторе массива равно размеру массива, указание размера вручную является излишним. Это может быть автоматически выведено компилятором:
Также возможно указать размер и предоставить более короткий инициализатор массива:
В этом случае остальные элементы инициализируются нулями . Обратите внимание, что в C ++ допускается инициализация пустого массива (все элементы инициализируются нулями), а в C89 - нет (требуется хотя бы одно значение). Также обратите внимание, что инициализаторы массива могут использоваться только для инициализации массивов; они не могут позже использоваться в назначениях.
Статические массивы
Статические массивы (массивы, расположенные «в сегменте данных») - это локальные переменные массива, определенные с помощью
static
ключевого слова и переменных массива в области имен («глобальные переменные»):(Обратите внимание, что переменные в области именного пространства неявно статичны. Добавление
static
ключевого слова к их определению имеет совершенно другое, устаревшее значение .)Вот как статические массивы ведут себя иначе, чем автоматические массивы:
(Ничто из вышеперечисленного не относится к массивам. Эти правила в равной степени применимы и к другим типам статических объектов.)
Члены массива данных
Элементы данных массива создаются при создании объекта-владельца. К сожалению, C ++ 03 не предоставляет средств для инициализации массивов в списке инициализаторов членов , поэтому инициализация должна быть сфальсифицирована с помощью присваиваний:
Кроме того, вы можете определить автоматический массив в теле конструктора и скопировать элементы поверх:
В C ++ 0x массивы могут быть инициализированы в списке инициализаторов членов благодаря равномерной инициализации :
Это единственное решение, которое работает с типами элементов, которые не имеют конструктора по умолчанию.
Динамические массивы
Динамические массивы не имеют имен, поэтому единственный доступ к ним - через указатели. Поскольку у них нет имен, я буду называть их «анонимными массивами».
В C анонимные массивы создаются через
malloc
друзей. В C ++ анонимные массивы создаются с использованиемnew T[size]
синтаксиса, который возвращает указатель на первый элемент анонимного массива:Следующее ASCII-изображение изображает макет памяти, если размер вычисляется как 8 во время выполнения:
Очевидно, что анонимные массивы требуют больше памяти, чем именованные, из-за дополнительного указателя, который должен храниться отдельно. (В бесплатном магазине также есть дополнительные накладные расходы.)
Обратите внимание, что здесь не происходит затухания массива в указатель. Хотя оцениваю
new int[size]
, на деле создания массива целых чисел, результатом выраженияnew int[size]
является уже указателем одним целым числом (первый элемент), не массив целых чисел или указатель на массив целых чисел неизвестного размера. Это было бы невозможно, потому что статическая система типов требует, чтобы размеры массива были константами времени компиляции. (Следовательно, я не аннотировал анонимный массив статической информацией о типе на рисунке.)Что касается значений по умолчанию для элементов, анонимные массивы ведут себя подобно автоматическим массивам. Обычно анонимные массивы POD не инициализируются, но существует специальный синтаксис, который запускает инициализацию значения:
(Обратите внимание на завершающую пару скобок прямо перед точкой с запятой.) Опять же, C ++ 0x упрощает правила и позволяет указывать начальные значения для анонимных массивов благодаря равномерной инициализации:
Если вы закончили использовать анонимный массив, вы должны вернуть его обратно в систему:
Вы должны освободить каждый анонимный массив ровно один раз, а затем никогда больше не трогать его. Его полное отсутствие приводит к утечке памяти (или, в более общем случае, в зависимости от типа элемента, утечке ресурсов), а попытка выпустить ее несколько раз приводит к неопределенному поведению. Использование формы не-массива
delete
(илиfree
) вместоdelete[]
освобождения массива также является неопределенным поведением .источник
static
использование в области имен было удалено в C ++ 11.new
оператор is, он, безусловно, может вернуть массив allcated по ссылке. Там просто нет никакого смысла в этом ...new
намного старше, чем ссылки.int a[10]; int (&r)[] = a;