Многомерные массивы
Помимо обычных массивов в C++ предоставляет возможность создавать и многомерные. Рассмотри на примере двумерного массива. Двумерный массив – объект данных T a[N][M] , который:
- Содержит N последовательно расположенных в памяти строк по M элементов типа T в каждой;
- В общем и целом инициализируется аналогично одномерным массивам;
- По характеристикам выравнивания идентичен объекту T a[N * M] , что сводит его двумерный характер к удобному умозрительному приему, упрощающему обсуждение и визуализацию порядка размещения данных
Массивы размерности больше двух считаются многомерными, при этом (N+1)-мерные массивы индуктивно определяются как линеаризованные массивы N-мерных массивов, для которых справедливо все сказанное об одно- и двумерных массивах.
Доступ к элементам в двумерном массиве
Для доступа ко всем элементам двумерного массива требуется два цикла: один для строк и один для столбцов. Поскольку доступ к двумерным массивам обычно выполняется по строкам, то левый индекс используется в качестве внешнего цикла:
Многомерные массивы и указатели
Для многомерных массивов справедлив ряд тождеств, отражающих эквивалентность соответствующих выражений языка С. Так для двумерного массива T a[N][M] справедливо:
- a == &a[0]; a + I == &a[i];
- *a == a[0] == &a[0][0];
- **a == *&a[0][0] == a[0][0];
- a[i][j] == *(*(a + i) + j);
Использование операции разыменования * не имеет каких-либо преимуществ перед доступом по индексу, и наоборот.
Двумерный массив
Проилюстрируем создание двумерного массива:
Двумерные массивы
Часто при решении практических задач возникает потребность в использовании таблиц, матриц и т. д. В таком случае используются двухмерные (и более) массивы. Допустим, необходимо обработать некоторые данные из таблицы. В таблице есть две характеристики: количество строк и количество столбцов. Также и в двумерном массиве, кроме количества элементов массива, есть такие характеристики как, количество строк и количество столбцов двумерного массива. То есть, визуально, двумерный массив — это обычная таблица, со строками и столбцами. Фактически двумерный массив — это одномерный массив одномерных массивов. Структура двумерного массива, с именем A, размером N на M показана на рис. 1)
| a[0][0] | a[0][1] | a[0][2] | a[0][3] | . | a[0][M] |
| a[1][0] | a[1][1] | a[1][2] | a[1][3] | . | a[1][M] |
| a[2][0] | a[2][1] | a[2][2] | a[2][3] | . | a[0][M] |
| . | . | . | . | . | . |
| a[N][0] | a[N][1] | a[N][2] | a[N][3] | . | a[n][M] |
Рис. 1 — Структура двумерного массива
- N — количество строк двумерного массива;
- M — количество столбцов двумерного массива;
- N × M — количество элементов массива.
Синтаксис объявления двумерного массива:
тип_данных имя_массива[количество_строк][количество столбцов];
В объявлении двумерного массива, также как и в объявлении одномерного массива, первым делом, нужно указать:
- тип данных;
- имя массива.
Затем, в первых квадратных скобках указывается количество строк двумерного массива, во вторых квадратных скобках — количество столбцов двумерного массива.
Рассмотрим пример объявления двумерного массива. Допустим нам необходимо объявить двумерный массив A(5×3), с количеством строк 5 и количеством столбцов 3.
Объявление такого двумерного массива:
int a[5][3];
- a — имя целочисленного массива
- 5 — количество строк двумерного массива;
- 3 — количество столбцов двумерного массива.
Инициализация двумерного массива:
В данном массиве 5 строк, 3 столбца. после знака присвоить ставятся общие фигурные скобочки, внутри которых ставится столько пар фигурных скобок, сколько должно быть строк в двумерном массиве, причём эти скобки разделяются запятыми. В каждой паре фигурных скобок записывается через запятую элементы одной строки двумерного массива. Во всех фигурных скобочках количество элементов должно совпадать. Так как в массиве пять строк, то и внутренних пар скобок тоже пять. Графически двумерный массив будет выглядеть, как двумерная таблица (рис. 2).
Рис. 2 — Двумерный массив
В каждой ячейке двумерного массива A показано значение, в нижнем правом углу показан индексы данной ячейки: номер строки и номер столбца.
Чтобы обратиться к элементам двумерного массива, потребуется два индекса:
Для доступа ко всем элементам двумерного массива требуются два цикла: один для строки и один для столбца. Поскольку доступ в двумерных массивах обычно осуществляется построчно, индекс строки используется в качестве внешнего цикла.
Заполнение двумерного массива случайными числами
Пример. Каждому элементу массива А(5×5) присваивается случайное вещественное число в диапазоне [5;15].
Заполнение двумерного массива вычисляемыми значениями
Пример. Каждому элементу массива А(6×6) присваивается значение, первая цифра которого указывает номер строки, а вторая цифра — номер столбца для этого значения (нумерация с 1).
Вывод элементов двумерного массива на экран
Для того, чтобы получить ровные столбцы, необходимо выводить числа так, чтобы на каждое выводимое число отводилось бы одинаковое число позиций (например, 4). Причем «лишние» позиции были бы заполнены пробелами.
Для того, чтобы выводимое число или строка имело в точности заданную ширину, необходимо перед выводом его на экран для потока cout вызвать метод width с соответствующим значением параметра. Данный метод устанавливает ширину поля для выводимого значения.
Пример. На экран выводятся элементы массива А(5×5).
Рис. 3 — Результат вывода элементов двумерного массива на экран
Рис. — —>
Массивы в C++
Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Это четвертая статья из серии, первые три, посвященные перегрузке в C++, находятся здесь, здесь и здесь.
Эта статья посвящена массивам. Массивы можно отнести к наиболее древним слоям C++, они пришли из первых версий C. Тем не менее, массивы вошли в объектно-ориентированную систему типов C++, хотя и с определенными оговорками. Программисту важно знать об этих особенностях, чтобы избежать потенциальных ошибок. В статье также рассмотрено другое наследие C – тривиальные типы и неинициализированные переменные. Часть нововведений C++11, С++14, С++17 затрагивают работу с массивами, все эти новые возможности также подробно описаны. Итак, попробуем рассказать о массивах все.
Оглавление
1. Общие положения
Массив является простейшим агрегатным типом. Он моделирует набор однотипных элементов, расположенных подряд в непрерывном отрезке памяти. Массивы в той или иной форме поддерживаются практически всеми языками программирования и неудивительно, что они появились в первых версиях C и затем стали частью C++.
1.1. Объявление массивов
Если T некоторый тип, N константа или выражение, вычисляемое во время компиляции, то инструкция
объявляет переменную a типа «массив из N элементов типа T » (array of N elements of the type T ). Тип N должен иметь неявное приведение к типу std::size_t , а его значение, называемое размером массива, должно быть больше нуля. Массив располагается в непрерывном отрезке памяти, под каждый элемент массива выделяется sizeof(T) байт, соответственно размер памяти, необходимой для размещения всего массива, равен N*sizeof(T) байт. Эта величина ограничена сверху платформой и компилятором. Тип массива обозначается как T[N] , то есть он включает тип элементов и размер массива. Таким образом, массивы, имеющие одинаковый тип элементов, но разный размер, будут иметь разный тип.
Такие массивы еще называют встроенными массивами (regular arrays), чтобы подчеркнуть отличие от других вариантов массивов, термин «массив» используется в программировании и в том числе в C++ достаточно широко.
Вот примеры правильных объявлений массивов:
А вот примеры некорректных объявлений массивов:
Доступ к элементам массива осуществляется через индексатор, значения индекса от 0 до N-1 . Вот пример:
Выход за границы массива не контролируется, ошибка может привести к неопределенному поведению.
В одной инструкции можно объявить несколько массивов, но размер должен быть указан для каждого.
Для типов массивов можно вводить псевдонимы. Можно использовать традиционный вариант с ключевым словом typedef :
или более современный (C++11) с ключевым словом using :
После этого массивы объявляются как простые переменные:
Это будет то же самое, что
1.2. Операторы и стандартные функции для работы с массивами
Для работы с массивами можно использовать оператор sizeof и несколько стандартных функций и макросов.
Оператор sizeof возвращает полный размер массива в байтах, то есть размер элемента умноженный на размер массива.
Макрос _countof() (в MSVS заголовочный файл <cstdlib> ) возвращает размер массива, то есть количество элементов. В С++17 появился стандартный шаблон функции std::size() , которая делает то же самое (а еще имеет перегруженную версию, которая определяет размер стандартного контейнера).
В C++11 в стандартной библиотеке появились свободные (не члены) шаблоны функций std::begin() и std::end() . Вызванная для массива std::begin() возвращает указатель на первый элемент массива, std::end() на past-the-last элемент. (Есть также константные версии: std::cbegin() , std::cend() .) Это позволяет использовать массивы в диапазонном for .
А также в стандартных алгоритмах:
1.3. Размещение в памяти
Если массив объявлен статически, то есть в глобальной области видимости, в области видимости пространства имен или в качестве статического члена класса, то он размещается в статической памяти. Массивам, объявленным локально, память выделяется на стеке. (Естественно, надо учитывать ограниченный размер стека при выборе размера локальных массивов.) Нестатические члены класса размещаются в границах экземпляра класса. Динамические массивы (см. раздел 6) размещаются в динамической памяти.
1.4. Ограничения на типы элементов массивов
Нельзя объявить массив, элементы которого имеют тип void .
Нельзя объявить массив ссылок.
Вместо этого можно использовать массив константных указателей.
(Синтаксис инициализации массивов будет обсуждаться в разделе 3.2.)
В C++11 появился шаблон std::reference_wrapper<> . Он эмулирует интерфейс ссылки, но экземпляры конкретизации можно хранить в контейнерах и встроенных массивах. Но все же эмуляция интерфейса ссылки не совсем полная, иногда приходится использовать функцию-член get() . Вот пример.
Нельзя объявить массив функций.
Вместо этого можно использовать массив указателей на функцию.
Шаблон std::reference_wrapper<> можно конкретизировать типом функции, но преимуществ перед указателем практически нет — функцию и так можно вызвать через указатель без разыменования, а инициализировать указатель именем функции без оператора & . Есть еще вариант эмулирования массива функций — это использование шаблона std::function<> , но этот шаблон является темой отдельного разговора.
Массив нельзя объявить с помощью ключевого слова auto .
Квалификатор const не применим к типу массива, а только к типам его элементов.
2. Сведение и копирование массивов
В данном разделе рассматриваются особенности массивов, которые выделяют их из общей системы типов C++.
2.1. Сведение
Как было сказано выше, размер массива является составной частью типа массива, но в определенных ситуациях она теряется и это делает тип массива в некотором смысле «неполноценным». Эта потеря называется сведение (decay, array-to-pointer decay). (Для перевода термина «decay» еще используется слово «низведение», также можно встретить «разложение».) Суть сведения заключается в том, что почти в любом контексте массив преобразуется к указателю на первый элемент и информация о размере теряется. Исключениями являются оператор sizeof , оператор & (взятия адреса) и инициализация ссылки на массив. Оператор sizeof рассматривался в разделе 1.2, указатели и ссылки на массивы будут подробно рассмотрены в разделе 4. Объявление с помощью ключевого слова decltype также правильно определяет тип массива, без сведения.
Конечно, тесную связь массивов и указателей отрицать нельзя. Вот стандартный (в стиле C) способ обработать все элементы массива:
Но все же сведение можно отнести к сишным архаизмам и с ним надо быть внимательным и аккуратным, иначе можно столкнуться с не самыми приятными неожиданностями.
Вот как сведение влияет на объявления функций. Функции
не являются перегруженными функциями — это одно и то же. Размер надо передавать дополнительным параметром или использовать специальное соглашение для определения размера (например, завершающий ноль для строк).
При внешнем связывании массива также происходит сведение.
Для размера также надо использовать дополнительную переменную или использовать специальное соглашение для определения размера.
При объявлении переменной с помощью ключевого слова auto также происходит сведение.
При конкретизации шаблона функции
тип параметра шаблонной функции также будет выведен как указатель, если аргумент является массивом.
Сведение вызывает дополнительные проблемы при использовании наследования. (В C ведь нет наследования.) Рассмотрим пример.
Следующий код компилируется без ошибок и предупреждений.
Но если sizeof(B) < sizeof(D) , то в теле Foo() смещение элементов массива d (кроме нулевого, конечно) будет определятся неправильно и, соответственно, почти всегда Foo() будет работать некорректно. Так что работать с массивами в полиморфном стиле, через указатель на базовый класс, нельзя.
2.2. Копирование
Наряду со сведением (и тесно связанная с ним) есть еще одна особенность типа массива, которая делает его в некотором смысле «неполноценным». Массивы не поддерживают привычный синтаксис инициализации и присваивания, основанный на семантике копирования:
Также функция не может возвращать массив.
Но если массив является членом класса/структуры/объединения, то копирующий конструктор и соответствующий оператор присваивания, генерируемые компилятором, выполняют поэлементное копирование такого массива.
Еще одна ситуация, когда происходит копирование массива — это захват массива по значению в лямбда-выражении.
Но если используется инициализирующий захват (C++14), то происходит сведение.
3. Инициализация массивов
Для описания правил инициализации массивов необходимо кратко рассказать о тривиальных типах.
3.1. Тривиальные типы и неинициализированные переменные
Конструкторы и деструкторы можно назвать ключевыми элементами объектной модели С++. При создании объекта обязательно вызывается конструктор, а при удалении — деструктор. Но проблемы совместимости с С вынудили сделать некоторое исключение, и это исключение называется тривиальные типы. Они введены для моделирования сишных типов и сишного жизненного цикла переменных, без обязательного вызова конструктора и деструктора. Сишный код, если он компилируется и выполняется в С++, должен работать так же как в С. К тривиальным типам относятся числовые типы, указатели, перечисления, а также классы, структуры, объединения и массивы, состоящие из тривиальных типов. Классы и структуры должны удовлетворять некоторым дополнительным условиям: отсутствие пользовательского конструктора, деструктора, копирования, присваивания, виртуальных функций.
Переменная тривиального типа будет неинициализированной, если не использовать какой-нибудь вариант явной инициализации. Для тривиального класса компилятор может сгенерировать конструктор по умолчанию и деструктор. Конструктор по умолчанию обнуляет объект, деструктор ничего не делает. Но этот конструктор будет сгенерирован и использован только, если использовать какой-нибудь вариант явной инициализации, иначе переменная останется неинициализированной.
Неинициализированная переменная устроена следующим образом: если она объявлена в области видимости пространства имен (глобально), будет иметь все биты нулевыми, если локально, или создана динамически, то получит случайный набор битов. Понятно, что использование такой переменной может привести к непредсказуемому поведению программы. Массивы достаточно часто имеют тривиальный тип и поэтому эта проблема для них весьма актуальна.
Неинициализированные константы тривиального типа выявляет компилятор, иногда он выявляет и другие неинициализированные переменные, но с этой задачей лучше справляются статические анализаторы кода.
В стандартной библиотеке С++11 есть шаблоны, называемые свойствами типов (заголовочный файл <type_traits> ). Один из них позволяет определить, является ли тип тривиальным. Выражение std::is_trivial<Т>::value имеет значение true , если T тривиальный тип и false в противном случае.
3.2. Синтаксис инициализации массивов
3.2.1. Общие положения
Если не использовать явную инициализацию, то для массивов нетривиального типа гарантируется вызов конструктора по умолчанию для каждого элемента. Естественно, что в этом случае такой конструктор должен быть, иначе возникает ошибка. Но для массивов тривиального типа или, если конструктор по умолчанию отсутствует или не устраивает, необходимо использовать явную инициализацию.
Со времен C массивы можно было инициализировать с помощью синтаксиса агрегатной инициализации:
В С++11 появилась универсальная инициализация (uniform initialization) и теперь можно инициализировать так:
Для универсальной инициализации также можно использовать =, и различать эти два типа инициализации не всегда просто, а, скорее всего, не очень нужно.
Размер массива можно не указывать, тогда он определится по числу инициализаторов.
Если размер массива указан, то число инициализаторов не должно быть больше размера массива. Если размер массива больше числа инициализаторов, то для оставшихся элементов гарантируется вызов конструктора по умолчанию (который, естественно, должен быть), в том числе и для тривиальных типов. Таким образам, указав пустой список инициализации, мы гарантируем вызов конструктора по умолчанию для всех элементов массива тривиального типа.
Массивы констант тривиального типа требуют обязательного списка инициализации.
Число инициализаторов может быть меньше размера массива, в этом случае оставшиеся элементы инициализируются конструктором по умолчанию.
Символьные массивы можно инициализировать строковым литералом.
Размер такого массива будет на единицу больше числа символов строки, нужно хранить завершающий нулевой символ.
3.2.2. Инициализация членов класса
В С++11 появилась возможность инициализировать массивы, являющиеся нестатическими членами класса. Это можно сделать двумя способами: непосредственно при объявлении или в списке инициализации членов при определении конструктора.
Правда в этом случае надо всегда явно задавать размер массива, неявное определение размера через список инициализации не разрешается.
Статические массивы, как и ранее, можно инициализировать только при определении, размер массива может быть определен через список инициализации.
В C++17 появилась возможность объявлять статические члены (включая массивы) как inline . Таки члены можно инициализировать при объявлении, определение не обязательно.
3.2.3. Требования к инициализаторам
Выражения, стоящие в списке инициализации, вычисляются непосредственно перед инициализацией, они не обязаны быть известными на стадии компиляции (конечно, за исключением массивов, объявленных как constexpr ). Требования к элементам списка инициализации такие же как и к аргументу функции, имеющей параметр того же типа, что и элемент массива — должно существовать неявное преобразование от типа элемента списка инициализации к типу элемента массива. Пусть у нас есть объявление массива:
Наличие нужного преобразования эквивалентно корректности инструкции
Элемент списка инициализации может быть сам списком инициализации. В этом случае корректность этой инструкции также гарантирует корректную инициализацию элемента массива.
Если мы объявим конструктор Int как explicit , то последнее объявление станет некорректным. В этом случае придется писать
Этот пример также демонстрирует как с помощью списка инициализации мы можем создать массив для типа у которого нет конструктора по умолчанию. Но в этом случае число инициализаторов должно совпадать с размером массива.
4. Указатели и ссылки на массивы
4.1. Указатели на массивы
Пусть у нас объявлен массив
Указатель на этот массив объявляется и инициализируется следующим образом:
Для получения указателя используется традиционный оператор & . Тип указателя на массива обозначается как T(*)[N] .
Обратим внимание на использование скобок, без них мы получим объявление массива из N элементов типа указатель на T .
Указатель на массив — это не указатель на первый элемент (хотя побитово они, конечно, совпадают), здесь нет никакого сведения. Это полноценный тип, который «знает» размер массива. Поэтому при инициализации размеры должны совпадать.
При инкременте указатель на массив увеличивается на размер всего массива, а не на размер элемента.
Для доступа к элементу массива через указатель надо использовать оператор * и индексатор.
При использовании псевдонимов можно получить более привычный синтаксис объявления указателя на массив.
Также можно использовать auto , компилятор правильно выводит тип переменной как указатель на массив исходя из типа инициализатора.
Понимание указателей на массивы необходимо для правильной работы с многомерными массивами, которые подробно будут рассмотрены далее.
4.2. Ссылки на массивы
Пусть у нас объявлен массив
Ссылка на этот массив объявляется и инициализируется следующим образом:
Как и для любой ссылки, инициализация переменной типа ссылка на массив является обязательной. Тип ссылки на массива обозначается как T(&)[N] .
Также ссылку на массив можно инициализировать разыменованным указателем на массив.
Как и указатель, ссылка «знает» размер массива. Поэтому при инициализации размеры должны совпадать.
Доступ к элементу массива через ссылку осуществляется так же, как и через идентификатор массива.
Ссылки на массивы как раз и являются теми средствами, с помощью которых можно обойти сведение.
ожидает аргументы типа T[N] , указатели для нее не подходят.
При использовании псевдонимов можно получить более привычный синтаксис объявления ссылки на массив.
Также можно использовать auto , компилятор выводит тип переменной как ссылка на массив.
Обратите внимание на наличие & после auto , без него произошло бы сведение, и тип ra вывелся бы как int* .
При конкретизации шаблона функции
тип параметра шаблонной функции также будет выведен как ссылка на массив, если аргумент является массивом.
Особенно удобно использовать шаблоны с выводом типа и размера массива.
При конкретизации такого шаблона компилятор выводит тип элементов T и размер массива N (который гарантировано больше нуля). В качестве аргументов можно использовать только массивы, указатели будут отвергнуты. Именно этот прием используется при реализации макроса _countof() и шаблона функции std::size() , а так же шаблонов функций std::begin() и std::end() , которые обеспечивают для массивов реализацию диапазонного for и делают более комфортной работу с алгоритмами. В разделе 5 приведен пример реализации такого шаблона.
5. Многомерные массивы
C++ не поддерживает настоящие многомерные массивы, то есть выражение a[N, M] некорректно, но многомерность моделируется в виде «массива массивов», то есть можно использовать выражение a[N][M] .
Если T некоторый тип, N и M выражения, допустимые для определения размера массива, то инструкция
объявляет a как массив массивов, массив из N элементов, каждый из которых является массивом из M элементов типа T . Такой массив будем называть двумерным массивом. Выражение a[i][j] , где i от 0 до N-1 , j от 0 до M-1 , дает доступ к элементам этого массива. Первый индекс выбирает массив из массива массивов, второй выбирает элемент в этом массиве. Значение N можно назвать внешним размером двумерного массива, M внутренним. Тип многомерного массива обозначается как T[N][M] .
Выражение a[i] является массивом из M элементов типа T . Соответственно к нему может быть применено сведение, у него можно взять адрес или использовать для инициализации ссылки.
Сведение преобразует массив к указателю на элемент. Для двумерного массива этот элемент сам является массивом, а значит двумерный массив сводится к указателю на массив.
Таким образом, при передаче двумерного массива в функцию следующие варианты объявления соответствующего параметра эквивалентны:
Это означает, что внешний размер двумерного массива теряется и его надо передавать отдельным параметром.
При использовании псевдонимов можно получить более лаконичный синтаксис объявления двумерных массивов.
Это то же самое, что
Двумерные массивы инициализируются следующим образом:
Если нужно гарантировать только инициализацию по умолчанию, то можно использовать пустой список инициализации <> . Определения размера по списку инициализации возможно только по внешнему размеру.
Можно получить указатель на двумерный массив:
Также можно получить ссылку. Вот пример использования ссылки на двумерный массив.
Двумерный массив хорошо согласуется с математическими матрицами. В объявлении
N можно интерпретировать как число строк матрицы, M как число столбцов, тогда mtx[i][j] это элемент матрицы находящийся на пересечении i -й строки и j -го столбца, а mtx[i] это массив размера M , который представляет i -ю строку матрицы. Соответственно, такая матрица располагается в памяти по строкам. Правда в математике принято нумеровать строки и столбцы с единицы, а не с нуля.
6. Динамические массивы
В C++ отсутствует тип «динамический массив». Имеются только операторы для создания и удаления динамического массива, доступ к нему осуществляется через указатели на начало массива (своего рода полное сведение). Размер такого массива надо хранить отдельно. Динамические массивы желательно инкапсулировать в C++ классы.
6.1. Создание и удаление динамического массива
Если T некоторый тип, n переменная, значение которой может определяются в процессе выполнения программы, то инструкция
создает массив в динамической памяти. Тип переменной n должен приводиться к std::size_t , значение может быть нулем. Размер памяти, необходимой для размещения массива, то есть n*sizeof(T) , ограничен сверху платформой и компилятором. Переменная pa указывает на первый элемент массива.
Если тип T тривиальный, то элементы будут иметь случайное значение, в противном случае для инициализации элементов будет использован конструктор по умолчанию.
В C++11 появилась возможность использовать список инициализации.
Если число инициализаторов больше размера массива, то лишние не используются (компилятор может выдать ошибку, если значение n известно на стадии компиляции). Если размер массива больше числа инициализаторов, то для оставшихся элементов гарантируется вызов конструктора по умолчанию, в том числе и для тривиальных типов. Таким образам, указав пустой список инициализации, мы гарантируем вызов конструктора по умолчанию для всех элементов массива тривиального типа.
Оператор new[] сначала выделяет память для всего массива. Если выделение прошло успешно, то, если T нетривиальный тип или есть список инициализации, вызывается конструктор для каждого элемента массива начиная с нулевого. Если какой-нибудь конструктор выбрасывает исключение, то для всех созданных элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается. Стандартные функции выделения памяти при невозможности удовлетворить запрос выбрасывают исключение типа std::bad_alloc .
Динамический массив удаляется оператором delete[] , который применяется к указателю, возвращаемому оператором new[] .
При этом, если при создании массива использовался конструктор, то для всех элементов массива вызывается деструктор в порядке, обратном вызову конструктора (деструктор не должен выбрасывать исключений), затем выделенная память освобождается.
В остальных отношениях указатель pa , возвращаемый оператором new[] , является просто указателем на начало массива, через него нельзя (во всяком случае «законно») получить размер массива, этот размер надо хранить отдельно. Соответственно с динамическим массивом нельзя использовать диапазонный for . Указатели в C/C++ поддерживают индексатор (встроенный оператор [] ), поэтому доступ к элементам динамического массива выглядит так же, как и к обычному массиву, контроля за корректностью индекса нет.
6.2. Динамические массивы и интеллектуальные указатели
Стандартный интеллектуальный указатель std::unique_ptr<> можно использовать для управления жизненным циклом динамического массива (см. [Josuttis]). Он имеет частичную специализацию для массивов (см. раздел 7), которая перегружает оператор [] вместо операторов -> и * , а также использует оператор delete[] в качестве удалителя по умолчанию. Вот пример:
Эта поддержка не является полноценной: не хранится информация о размере массива, поэтому нет возможности контролировать корректностью индекса, не поддерживается интерфейс стандартных контейнеров и диапазонный for .
В C++14 появилась возможность создать динамический массив и инициализировать им экземпляр std::unique_ptr<> с помощью std::make_unique<> :
При этом гарантируется инициализация элементов массива по умолчанию, в том числе и для тривиальных типов.
Интеллектуальный указатель std::shared_ptr<> стал поддерживать такую специализацию только в C++17, а использование std::make_shared<> для этой специализации появилось только в C++20.
В качестве альтернативы такому использованию интеллектуальных указателей можно рекомендовать std::vector<> .
6.3. Многомерные динамические массивы
Динамический массив не может быть динамическим по нескольким измерениям, то есть выражение new T[n][m] , где оба значения n и m определяются в процессе выполнения программы, не корректно. Но мы можем создать динамический массив, каждый элемент которого является встроенным массивом с размером, известным на стадии компиляции. Если M выражение, допустимое для определения размера массива, то следующая инструкция создает такой массив:
Оператор new[] возвращает указатель на массив. Доступ к элементам такого массива будет осуществляться через выражение pa[i][j] , в свою очередь pa[i] будет массив из M элементов типа T .
При использовании псевдонимов можно получить более лаконичный синтаксис.
Используя перегрузку оператора [] легко создать класс, который хранит данные в одномерном массиве, но при этом предоставляет интерфейс многомерного массива. Вот пример предельно упрощенного класса матрицы.
Вот пример использования:
Более продвинутый класс матрицы может использовать специальный вложенный proxy-класс, представляющий строку, например RowProxy , и индексатор будет возвращать экземпляр этого класса. Такой класс может, например, контролировать значение индекса, предоставлять функции-члены begin() , end() , etc. Аналогичное решение может быть и для столбцов.
7. Использование массивов в шаблонах
Тип массива можно использовать в качестве шаблонных аргументов и для специализации шаблонов классов.
Можно определить частичную специализацию шаблона класса для массивов не задавая при этом размер массива, то есть для массивов «вообще». Для этого в качестве типа специализации надо использовать T[] . Конечно, можно определить частичную специализацию для массива с заданным размером. Вот пример.
В стандартной библиотеке частичная специализация интеллектуального указателя std::unique_ptr<> и std::shared_ptr<> для массивов используется для управления жизненным циклом динамического массива, подробнее см. раздел 6.2.
Для программирования шаблонов, использующих массивы в качестве шаблонных аргументов, в стандартной библиотеке (заголовочный файл <type_traits> ) имеется несколько свойст типов: std::is_array<> , std::extent<> , std::rank<> , std::remove_extent<> . Вот примеры их использования (в примерах используется появившаяся в C++17 возможность использовать суффикс _v вместо члена value ):
В качестве реального примера использования этих свойст типов приведем немного упрощенное определение перегруженного варианта шаблона функции std::make_unique<> для массивов (см. раздел 6.2):
Шаблоны функций не поддерживают частичную специализацию, поэтому здесь используется техника, которая называется отключение шаблонов (template disabling). Этот шаблон будет отключен, то есть не будет конкретизироваться, для любых аргументов шаблона, тип которых отличается от T[] . Соответственно, перегруженный вариант std::make_unique<> для аргументов шаблона остальных типов аналогичным способом будет отключен для T[] .
8. Стандартные альтернативы массивам
Стандартная библиотека предоставляет несколько классов (точнее шаблонов классов), которые рекомендуется использовать вместо массивов.
Вместо встроенных массивов рекомендуется использовать шаблон std::array<> . (Появился в C++11, см. [Josuttis].) Этот шаблон является объектной оберткой встроенного массива, он имеет два шаблонных параметра: тип элементов и размер. Размер должен быть известен на стадии компиляции, но в отличии от встроенного массива может быть нулевым. Вот пример:
Этот шаблон поддерживает индексатор и традиционный интерфейс стандартного контейнера.
Вместо динамических массивов рекомендуется использовать std::vector<> . Этот шаблон хорошо известен программистам, подробно описан в литературе (стандартный контейнер №1), поэтому каких-то дополнительных подробностей можно не приводить.
Есть еще довольно специфический и не особо популярный шаблон std::valarray<> . Он позволяет эмулировать интерфейс многомерных массивов.
Список литературы
[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.
Name already in use
OAP / articles / t3l1_3.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Массив представляет собой совокупность переменных одного типа с общим для обращения к ним именем. В C# массивы могут быть как одномерными, так и многомерными. Массивы служат самым разным целям, поскольку они предоставляют удобные средства для объединения связанных вместе переменных.
Массивами в C# можно пользоваться практически так же, как и в других языках программирования. Тем не менее у них имеется одна особенность: они реализованы в виде объектов.
Для тoго чтобы воспользоваться массивом в программе, требуется двухэтапная процедура, поскольку в C# массивы реализованы в виде объектов. Во-первых, необходимо объявить переменную, которая может обращаться к массиву. Например, команда int[] ArrayOfInteger объявляет переменную с именем ArrayOfInteger с типом int[] — одномерный массив целых чисел. И во-вторых, нужно создать экземпляр массива, используя оператор new.
Следует иметь в виду, что если массив только объявляется, но явно не инициализируется, каждый его элемент будет установлен в значение, принятое по умолчанию для соответствующего типа данных (например, элементы массива типа bool будут устанавливаться в false, а элементы массива типа int — в 0).
Помимо заполнения массива элемент за элементом (как показано в предыдущем примере), можно также заполнять его с использованием специального синтаксиса инициализации массивов. Для этого необходимо перечислить включаемые в массив элементы в фигурных скобках < >. Такой синтаксис удобен при создании массива известного размера, когда нужно быстро задать его начальные значения:
Обратите внимание, что в случае применения синтаксиса с фигурными скобками размер массива указывать не требуется (как видно на примере создания переменной myArr), поскольку этот размер автоматически вычисляется на основе количества элементов внутри фигурных скобок. Кроме того, применять ключевое слово new не обязательно (как при создании массива info).
Неявно типизированные массивы
Ключевое слово var позволяет определить переменную так, чтобы лежащий в ее основе тип выводился компилятором. Аналогичным образом можно также определять неявно типизированные локальные массивы. С использованием такого подхода можно определить новую переменную массива без указания типа элементов, содержащихся в массиве. Давайте рассмотрим пример:
Разумеется, как и при создании массива с использованием явного синтаксиса C#, элементы, указываемые в списке инициализации массива, должны обязательно иметь один и тот же базовый тип (т.е. должны все быть int, string или MyCar).
Определение массива объектов
В большинстве случаев при определении массива тип элемента, содержащегося в массиве, указывается явно. Хотя на первый взгляд это выглядит довольно понятно, существует одна важная особенность. В основе каждого типа в системе типов .NET (в том числе фундаментальных типов данных) в конечном итоге лежит базовый класс System.Object. В результате получается, что в случае определения массива объектов находящиеся внутри него элементы могут представлять собой что угодно:

Реализация в C# массивов в виде объектов дает целый ряд преимуществ. Одно из них заключается в том, что с каждым массивом связано свойство Length, содержащее число элементов, из которых может состоять массив. Следовательно, у каждого массива имеется специальное свойство, позволяющее определить его длину.
Когда запрашивается длина многомерного массива, то возвращается общее число элементов, из которых может состоять массив. Благодаря наличию у массивов свойства Length операции с массивами во многих алгоритмах становятся более простыми, а значит, и более надежными. Давайте рассмотрим пример использования свойства Length:
Многомерным называется такой массив, который содержит два или более измерения, причем доступ к каждому элементу такого массива осуществляется с помощью определенной комбинации двух или более индексов. Многомерный массив индексируется двумя и более целыми числами.
Простейшей формой многомерного массива является двумерный массив. Местоположение любого элемента в двумерном массиве обозначается двумя индексами. Такой массив можно представить в виде таблицы, на строки которой указывает один индекс, а на столбцы — другой. Пример объявления и инициализации двумерного массива показан ниже:

Обратите особое внимание на способ объявления двумерного массива. Схематическое представление массива myArr показано ниже:

Если вам приходилось раньше программировать на С, С++ или Java, то будьте особенно внимательны, объявляя или организуя доступ к многомерным массивам в C#. В этих языках программирования размеры массива и индексы указываются в отдельных квадратных скобках, тогда как в C# они разделяются запятой.
Массивы трех и более измерений
В C# допускаются массивы трех и более измерений. Ниже приведена общая форма объявления многомерного массива:
Ниже приведен пример программы, использующей трехмерный массив:
Инициализация многомерных массивов
Для инициализации многомерного массива достаточно заключить в фигурные скобки список инициализаторов каждого его размера:
где val обозначает инициализирующее значение, а каждый внутренний блок — отдельный ряд. Первое значение в каждом ряду сохраняется на первой позиции в массиве, второе значение — на второй позиции и т.д. Обратите внимание на то, что блоки инициализаторов разделяются запятыми, а после завершающей эти блоки закрывающей фигурной скобки ставится точка с запятой.
Ниже в качестве примера приведена общая форма инициализации двумерного массива:
Ступенчатые (зубчатые) массивы
Двумерный массив можно представить в виде таблицы, в которой длина каждой строки остается неизменной по всему массиву. Но в C# можно также создавать специальный тип двумерного массива, называемый ступенчатым массивом. Ступенчатый массив представляет собой массив массивов, в котором длина каждого массива может быть разной. Следовательно, ступенчатый массив может быть использован для составления таблицы из строк разной длины.
Ступенчатые массивы объявляются с помощью ряда квадратных скобок, в которых указывается их размерность. Например, для объявления двумерного ступенчатого массива служит следующая общая форма:
где размер обозначает число строк в массиве. Память для самих строк распределяется индивидуально, и поэтому длина строк может быть разной.
Давайте рассмотрим пример использования ступенчатого массива:
Созданный массив в данном примере можно структурно представить в виде:

Теперь нетрудно понять, почему такие массивы называются ступенчатыми! После создания ступенчатого массива доступ к его элементам осуществляется по индексу, указываемому в отдельных квадратных скобках.
Ступенчатые массивы находят полезное применение не во всех, а лишь в некоторых случаях. Так, если требуется очень длинный двумерный массив, который заполняется не полностью, т.е. такой массив, в котором используются не все, а лишь отдельные его элементы, то для этой цели идеально подходит ступенчатый массив.
Применение свойства Length при обращении со ступенчатыми массивами
Особый случай представляет применение свойства Length при обращении со ступенчатыми массивами. В этом случае с помощью данного свойства можно получить длину каждого массива, составляющего ступенчатый массив. Давайте рассмотрим следующий пример:

Обратите особое внимание на то, как свойство Length используется в ступенчатом массиве myArr. Напомним, что двумерный ступенчатый массив представляет собой массив массивов. Следовательно, когда используется выражение myArr.Length то в нем определяется число массивов, хранящихся в массиве myArr (в данном случае — 3 массива). А для получения длины любого отдельного массива, составляющего ступенчатый массив, служит выражение myArr[0].Length.
Каждый создаваемый массив получает большую часть функциональности от класса System.Array. Общие члены этого класса позволяют работать с массивом с использованием полноценной объектной модели. Таким образом, методы и свойства, определенные в классе Array, можно использовать с любым массивом C#. Давайте разберем некоторые полезные особенности класса Array.
Класс Array является абстрактным, поэтому создать массив с использованием какого-либо конструктора нельзя. Однако вместо применения синтаксиса C# для создания экземпляров массивов также возможно создавать их с помощью статического метода CreateInstance(). Это исключительно удобно, когда заранее неизвестен тип элементов массива, поскольку тип можно передать методу CreateInstance() в параметре как объект Type:
Обратите внимание, что для установки значений в массив служит метод SetValue(), а для их чтения — метод GetValue().
Поскольку массивы — это ссылочные типы, присваивание переменной типа массива другой переменной создает две переменных, ссылающихся на один и тот же массив. Для копирования массивов предусмотрена реализация массивами интерфейса ICloneable. Метод Clone(), определенный в этом интерфейсе, создает неглубокую копию массива. Если элементы массива относятся к типу значений, то все они копируются, если массив содержит элементы ссылочных типов, то сами эти элементы не копируются, а копируются лишь ссылки на них.
Вместо метода Clone() можно также применять метод Array.Сору(), тоже создающий поверхностную копию. Но между Clone() и Сору() есть одно важное отличие: Clone() создает новый массив, а Сору() требует наличия существующего массива той же размерности с достаточным количеством элементов.
Сортировка и поиск
В классе Array реализован алгоритм быстрой сортировки (Quicksort) элементов массива. Метод Sort() требует от элементов реализации интерфейса IComparable. Простые типы, такие как System.String и System.Int32 , реализуют IComparable, так что можно сортировать элементы, относящиеся к этим типам.
C помощью разных вариантов метода Sort() можно отсортировать массив полностью или в заданных пределах либо отсортировать два массива, содержащих соответствующие пары «ключ-значение». После сортировки в массиве можно осуществить эффективный поиск, используя разные варианты метода BinarySearch().

Массивы в качестве параметров
Массивы могут передаваться в методы в качестве параметров, а также возвращаться из методов. Для возврата массива достаточно объявить массив как тип возврата. Наглядно это показано в следующем примере:
Результат работы данной программы:

Структура ArraySegment представляет сегмент массива. Эта структура может применяться, когда нужно вернуть или передать методу части массива. Вместо передачи в метод массива, смещения и счетчика в отдельных параметрах, можно передать единственный параметр ArraySegment. В этой структуре информация о сегменте (смещение и счетчик) заключена непосредственно в ее членах:
Важно отметить, что сегменты массива не копируют элементы исходного массива. Вместо этого через ArraySegment можно получить доступ к исходному массиву. Если изменяются элементы сегмента, то эти изменения будут видны в исходном массиве.
Работать с массивами не всегда удобно. Например, массив хранит фиксированное количество объектов, однако что делать если мы заранее не знаем, сколько нам потребуется объектов? В этом случае намного удобнее применять коллекции. Еще один плюс коллекций состоит в том, что некоторые из них реализует стандартные структуры данных, например, стек, очередь, словарь, которые могут пригодиться для решения различных специальных задач.
Большая часть классов коллекций содержится в пространствах имен System.Collections (простые необобщенные классы коллекций), System.Collections.Generic (обобщенные или типизированные классы коллекций) и System.Collections.Specialized (специальные классы коллекций). Также для обеспечения параллельного выполнения задач и многопоточного доступа применяются классы коллекций из пространства имен System.Collections.Concurrent .
Итак, класс ArrayList представляет коллекцию объектов. И если надо сохранить вместе разнотипные объекты — строки, числа и т.д., то данный класс как раз для этого подходит.
Основные методы класса:
int Add(object value) : добавляет в список объект value
void AddRange(ICollection col) : добавляет в список объекты коллекции col, которая представляет интерфейс ICollection — интерфейс, реализуемый коллекциями.
void Clear() : удаляет из списка все элементы
bool Contains(object value) : проверяет, содержится ли в списке объект value. Если содержится, возвращает true, иначе возвращает false
void CopyTo(Array array) : копирует текущий список в массив array.
ArrayList GetRange(int index, int count) : возвращает новый список ArrayList, который содержит count элементов текущего списка, начиная с индекса index
int IndexOf(object value) : возвращает индекс элемента value
void Insert(int index, object value) : добавляет в список по индексу index объект value
void InsertRange(int index, ICollection col) : добавляет в список начиная с индекса index коллекцию ICollection
int LastIndexOf(object value) : возвращает индекс последнего вхождения в списке объекта value
void Remove(object value) : удаляет из списка объект value
void RemoveAt(int index) : удаляет из списка элемент по индексу index
void RemoveRange(int index, int count) : удаляет из списка count элементов, начиная с индекса index
void Reverse() : переворачивает список
void SetRange(int index, ICollection col) : копирует в список элементы коллекции col, начиная с индекса index
void Sort() : сортирует коллекцию
Кроме того, с помощью свойства Count можно получить количество элементов в списке.
Посмотрим применение класса ArrayList на примере.
Во-первых, так как класс ArrayList находится в пространстве имен System.Collections, то подключаем его (using System.Collections;).
Вначале создаем объект коллекции через конструктор как объект любого другого класса: ArrayList list = new ArrayList(); . При необходимости мы могли бы так же, как и с массивами, выполнить начальную инициализацию коллекции, например, ArrayList list = new ArrayList()<1, 2, 5, "string", 7.7>;
Далее последовательно добавляем разные значения. Данный класс коллекции, как и большинство других коллекций, имеет два способа добавления: одиночного объекта через метод Add и набора объектов, например, массива или другой коллекции через метод AddRange
Через цикл foreach мы можем пройтись по всем объектам списка. И поскольку данная коллекция хранит разнородные объекты, а не только числа или строки, то в качестве типа перебираемых объектов выбран тип object: foreach (object o in list)
Многие коллекции, в том числе и ArrayList, реализуют удаление с помощью методов Remove/RemoveAt. В данном случае мы удаляем первый элемент, передавая в метод RemoveAt индекс удаляемого элемента.
В завершении мы опять же выводим элементы коллекции на экран только уже через цикл for. В данном случае с перебором коллекций дело обстоит также, как и с массивами. А число элементов коллекции мы можем получить через свойство Count
С помощью индекса мы можем получить элемент коллекции так же, как и в массивах: object firstObj = list[0];
Класс List из пространства имен System.Collections.Generic представляет простейший список однотипных объектов (в угловых скобках как раз записывается тип объектов).
Среди его методов можно выделить следующие:
void Add(T item) : добавление нового элемента в конец списка
void AddRange(ICollection collection) : добавление в список коллекции или массива
int BinarySearch(T item) : бинарный поиск элемента в списке. Если элемент найден, то метод возвращает индекс этого элемента в коллекции. При этом список должен быть отсортирован.
int IndexOf(T item) : возвращает индекс первого вхождения элемента в списке
void Insert(int index, T item) : вставляет элемент item в списке на позицию index
bool Remove(T item) : удаляет элемент item из списка, и если удаление прошло успешно, то возвращает true
void RemoveAt(int index) : удаление элемента по указанному индексу index
void Sort() : сортировка списка
Посмотрим реализацию списка на примере:
Здесь у нас создаются два списка: один для объектов типа int, а другой — для объектов Person. В первом случае мы выполняем начальную инициализацию списка: List<int> numbers = new List<int>() < 1, 2, 3, 45 >;
Во втором случае мы используем другой конструктор, в который передаем начальную емкость списка: List<Person> people = new List<Person>(3); . Указание начальной емкости списка (capacity) позволяет в будущем увеличить производительность и уменьшить издержки на выделение памяти при добавлении элементов. Также начальную емкость можно установить с помощью свойства Capacity, которое имеется у класса List.
Еще один распространенный тип коллекции представляют словари. Словарь хранит объекты, которые представляют пару ключ-значение. Каждый такой объект является объектом структуры KeyValuePair<TKey, TValue> . Благодаря свойствам Key и Value, которые есть у данной структуры, мы можем получить ключ и значение элемента в словаре.
Рассмотрим на примере использование словарей:
Класс словарей также, как и другие коллекции, предоставляет методы Add и Remove для добавления и удаления элементов. Только в случае словарей в метод Add передаются два параметра: ключ и значение. А метод Remove удаляет не по индексу, а по ключу.
Так как в нашем примере ключами является объекты типа int, а значениями — объекты типа string, то словарь в нашем случае будет хранить объекты KeyValuePair<int, string> . В цикле foreach мы их можем получить и извлечь из них ключ и значение.
Кроме того, мы можем получить отдельно коллекции ключей и значений словаря:
Здесь в качестве ключей выступают объекты типа char, а значениями — объекты Person. Используя свойство Keys, мы можем получить ключи словаря, а свойство Values соответственно хранит все значения в словаре.
Для добавления необязательно применять метод Add() , можно использовать сокращенный вариант:
Несмотря на то, что изначально в словаре нет ключа ‘a’ и соответствующего ему элемента, то он все равно будет установлен. Если же он есть, то элемент по ключу ‘a’ будет заменен на новый объект new Person()