Как из статического массива сделать динамический с

от admin

Как поменять статический массив на динамический?

Здраствуйте! У меня есть рабочий код по работе с массивом, но я хочу его реализовать в динамический массив.

Помогите пожалуйста как его можно реализовать. Пишу на VC++ 2012.

Так же советую почитать про vector как раз для таких целей, которые вам нужны!

Дизайн сайта / логотип © 2023 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2023.3.11.43304

Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Name already in use

StudentCpp-wiki / 3.4. Массивы.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

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

0 1 2 3 4
12 60 25 5 19

Это иллюстрация ячеек памяти массива на 5 элементов.

Массив может быть объявлен статически и динамически.

При статическом объявлении память под массив выделяется в стеке, массив является локальной переменной и будет уничтожен по правилам локальных переменных.

При динамическом объявлении память выделяется в куче программы — ее еще называют динамической памятью. Управление ею лежит полностью на совести самого программиста. Доступ до массива осуществляется через указатель.

Объявить и проинициализировать такой массив можно следюущим образом:

  • int — тип данных элементов массива. Это будет массив, хранящий целые числа. Тип данных элементов может быть любой.
  • array1 — имя переменной. Вы сами его выбираете
  • [n] , где n — кол-во элементов в массиве. Причем n должно быть константой так или иначе.

Не получится, потому что компилятор не может быть на момент исполнения предсказать, какое число будет вместо n. А значит и выделить память в стеке небезопасно, а значит. Не пошел бы программист лесом. 🙂

Обращение к ячейке памяти происходит с помощью оператора [] Как параметр этот параметр принимает СМЕЩЕНИЕ относительно начала массива.

Догадливые уже могли понять: имя массива является адресом начала массива в памяти.

0 1 2 3 4
12 60 25 5 19

Alt Texr

Переменная array1 хранит именно АДРЕС самой первой ячейки массива (см. раздел указатели, в котором пояснено, что, например, 1 число типа int занимает не 1 ячейку памяти, а 4, потому что 1 ячейка = 1 байт).

array1 — локальная переменная. А значит хранится в стеке. array1 хранит адрес ячейки в стеке.

Естественно, что массивом можно управлять и с помощью цикла:

Если элементы массива извесстны заранее, то объявление и инициализацию можно объединить:

А еще можно так:

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

А теперь задумаемся вот над каким вопросом: если имя массива — это адрес самой первой ячейки в самом массиве, иными словами: имя массива — это указатель на начало массива. Но как же оператор [] по индексу элемента извлекает данные из массива? Если вы хорошо усвоили тему указателей и операций над указателями, то вы легко найдете ответ на этот вопрос.

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

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

Заметили? Индекс, который передается в оператор [] — это СМЕЩЕНИЕ элемента относительно начала массива! Т.е. индексация с нуля взялась не на пустом месте, это обусловлено косвенной адресацией через указатели!

Поэтому, на самом деле за строчкой

Просто ноль мы, как люди, обычно не пишем.

Так что вывести на экран можно двумя способами:

Так что оператор [] это опять же пресловутый синтаксический «сахар» для программиста, и этот оператор просто прибавляет к адресу начала массива смещение и разыменовывает полученный указатель.

Естественно, никто не мешает использовать полную запись доступа до элемента массива не только для чтения элемента из массива, но и для записи:

Итак, со статическими массивами разобрались, теперь пора перейти к динамическим.

Основное и краеугольное отличие статических и динамических массивов в том, где каждый из них хранится.

Статический — в стеке.

Динамический — в куче.

Жизненным циклов статического управляет программа, динамическим сам программист.

Суть динамического массива в том, что программист сам, как хочет управляет памятью. Это дает безграничную гибкость в написании кода, но и безграничные возможности выстрелить себе в ногу. Sad, but true.

Раз абсолютно все находится в ведении программиста, то даже выделять, высвобождать и следить за целостностью данных (их сохранностью) должны мы сами.

Есть две основные команды по работе с динамической памятью (кучей):

  1. Выделить память.
  2. Высвободить память.

Выделение памяти.

Для выделения памяти есть оператор new . Оператор new резервирует память под хранение данных. Не трудно догадаться, что память в компьютере либо есть, либо ее нет. Поэтому сказать, что new «создает» память — неверно, он же не добавит вам новые планки оперативной память (а жаль, было бы круто). Память была еще до new . Более того, память под использование программой выделяется операционной системой в момент запуска программы: программа точно знает «куда можно ходить» ну или какую память можно, а какую нельзя использовать.

Т.е. память у программы есть. Но она «общая» для всей программы. Переменные все время создаются, программа потребляет это пространство памяти для своей жизнедеятельности, причем какие-то процессы мы не можем контролировать.

Куча — это свалка памяти, ею пользуются все процессы в вашей программе. Ячейки все время используются кем-то или чем-то. Чем больше и сложнее программа, тем хаотичнее и чаще ячейки кучи задействуются.

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

Чтобы такого не происходило, компиляторами продуман механизм «резервирования» памяти.

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

Вот что делает оператор new . Он резервирует определенный объем памяти в куче.

Оператор new это опять же синтаксический «сахар». На самом деле он внутри своей реализации вызывает ф-цию выделения памяти malloc(size_to_allocate); malloc расшифровывается как memory allocate (memory — память, allocate — выделить, зарезервировать).

В качестве параметра malloc принимает кол-во байт, которое нужно выделить.

new float; преобразуется компилятором в malloc( sizeof(float) );

А как же работает оператор new и ф-ция malloc ? Как она находит место, где можно зарезервировать память? Очень просто. Поиск начинается с адреса начала кучи, находится НЕПРЕРЫВНЫЙ и свободный участок в памяти, и уже он резервируется.

С++ тупой, поэтому работает так. Это на самом деле не очень оптимально. Но вот такой это простой язык. Языки с виртуальными машинами типа c# и java работают с памятью по более сложным алгоритмам.

Итак, что тут произошло.

Мы создали указатель. Затем с помощью оператора new выделили память под ячейку памяти типа данных float (4 или сколько там байт — всегда можно запустить sizeof(float) , чтобы узнать точно). И адрес выделенной памяти сохранился в указатель ptr1 .

ptr1 — это локальная переменная и будет храниться в стеке. оператор new — выделил память в куче, не в стеке.

Alt Texr

Как любая локальная переменная ptr1 будет уничтожена, когда завершится ее блок кода. А вот память в куче. Нет. Потому что вся ответственность за нее лежит на программисте. Как хочешь так и разбирайся.

Выглядеть это будет так.

Alt Texr

Стек избавился от своей переменной ptr1 , а вот зарезервированная область в памяти так и осталась зарезервированной. Программа не может использовать эту память повторно. Да и мы больше не имеем доступа до этой памяти. Мы же потеряли адрес, где эта память хранится.

Конечно, после завершения программы, вся память выделенная программе ОС, в том числе и куча, будет высвобождена, очищена и пожертвована другому процессу в ОС. Но вы правда хотите, чтобы ваша программа оставляла за собой неиспользуемый мусор, засоряла память и т.д.?

Вам нравится, когда программы жрут по 1Гб, 2Гб, 3Гб памяти причем непонятно зачем, когда они ничего такого мега сложного не делают? Конечно, не нравится. Поэтому не надо мусорить там, где вы работаете, т.е. в памяти вашей программы.

Высвобождение памяти.

Чтобы иметь возможно подчищать за собой больше ненужную нам память в куче был введен оператор delete .

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

Кстати, delete также как и new на самом деле запускает ф-цию высвобождения памяти free , которая как параметр принимает указатель.

delete pointer; эквивалентно free(pointer);

Правило обращения с динамической памятью достаточно простое: Каждому new даешь свой delete . Написали где-то new ? Значит, нужно где-то эту память высвободить. Никак иначе.

Ок, если мы передаем указатель, то это адрес всего лишь 1 байта в памяти, но даже выше мы зарезервировали память под float , а это всяко больше 1 байта!

Получается, освободится только 1 байт? А потом нам в цикле нужно бегать и высвобождать все остальные ячейки? Без паники. Разработчики компилятора о вас уже позаботились.

Информация для продвинутых.

На самом деле при резервировании памяти, вам для использования выдается указатель не на первую зарезервированную ячейку, а на следующую. Потому что в самой первой ячейке хранится так называемые метаданные о памяти. В этих метаданных хранится объем выделенной памяти, тип данных, под который изначально память выделялась и некоторая еще информация.

Alt Texr

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

Поэтому когда вы параметром в delete передаете указатель, программа сначала делает шаг назад, считывает из метаданных нужную информацию об объеме выделенной памяти и высвободит все 4 или сколько там было ячеек. Конец информации для продвинутых.

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

После высвобождения памяти, флаг «занято» с памяти снимается. Соответственно, если мы попытаемся обратиться к такой памяти еще раз уже после ее освобождения, программа вывалится в ужасе с ошибкой «Access violation», что в вольном переводе означает «у меня нет прав сюда лезть».

Но скажем так, С++ далеко не всегда настолько усожлив, чтобы сообщить нам, что мы полезли куда-то не туда. Обычно предупреждающей таблички нет, мы залезаем на чужую территорию и кто-нибудь потом, когда мы уже не ждем стреляет нам в ногу. Или голову, в зависимости от критичности ошибки в коде.

Итак, начнем разбирать с самого легкого. Правая половина выражения.

Это синтаксис выделения памяти под массив, элеметами которого являются int и таких чисел 5 штук.

Через malloc записывается так: malloc( sizeof(int) * 5 );

Таким образом выделится 20 байт памяти.

Как и положено после резервирования памяти, оператор new вернет как результат указатель на НАЧАЛО выделенной области памяти.

Все то же самое было и со статическими массивами, там тоже имя массива являлось адресом начала массива. Только хранился он в стеке, а тут в куче.

Короче, вернулся нам адрес первого элемента в массиве, он же начало всего массива.

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

Так что слева нам и пришлось написать int* array1 — указатель на тип данных int .

Alt Texr

Ну а в памяти все это безобразие выглядеть будет так:

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

«Что за фигня?» спросите вы, увидев delete[] , «зачем у delete написан оператор [] «

Если кратко, то delete[] должен быть вызван, если выделение памяти было через new[] , в частности для массивов.

delete соответственно вызывается, если память выделялась с помощью new .

Сейчас придется вам поверить мне на слово:

Для массивов из простых типов данных, вы не почувствуете разницу между delete и delete[] , работать при удалении массива они будут одинаково. Но вот дальше, когда будут изучаться пользовательские типы данных, классы, там путаница с delete и delete[] может вызвать ошибки времени исполнения и повреждение памяти.

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

Пример, когда вызов delete для массива объектов повредит память будет в разделе «жизненный цикл объектов».

В некоторых задачах требуется хранить набор данных для набора данных. Например, допустим, нужно хранить оценки студентов по двум дисциплинам. Всего студентов 3 человек. 3 человека и у каждого по 2 оценки.

Сохранить эту информацию можно 3 способами:

  1. Несколько небольших массивов.
  2. Один большой массив.
  3. Двумерный массив.

Рассмотрим каждый способ по порядку;

  1. Несколько небольших массивов: Для каждого студента будем создавать свой собственный массив на 2 оценки.

Вроде, выглядит неплохо. Легко читается, код понятен. Тогда что не так? Универсальность. Допустим, мы не знаем до начала программы, сколько будет студентов. Пользователь только после запуска программы введет число студентов в группе. Сколько тогда создать переменных? 10? 50? 100? 1000? И что? Все их объявлять? А как обращаться к такому непонятному числу переменных? В общем, это не то чтобы невозможно. Но такие извращения в такой задаче излишни. Все должно быть намного проще.

  1. Один массив для всех данных. Чтобы сохранить оценки для 3 человек понадобится 6 ячеек памяти. Ячейки 0 и 1 будут оценками для студента 0 (индексация приведена с 0, как в массиве); Ячейки 2 и 3 — оценки 1 студента; Ячейки 4 и 5 — оценки 2 студента.

Итак у нас есть 3 студента у каждого по 2 оценки. Все они хранятся в одном массиве. Мы решили проблему с универсальностью (допустим, мы как-то обошли ограничение, что число элементов в массиве должно быть только константой).

Тогда, допустим, что мы можем создавать массив для n студентов и m дисциплин. Размер массива для хранения всего этого дела будет равен n * m.

Но как обратиться, скажем, к оценке по первой дисциплине у второго студента? Если индексация идет с 0, то нам нужен студент с индексом 1 и дисциплина с индексом 0.

Если посмотреть на инициализацию массива выше, то нужная оценка лежит по индексу — 2.

Далее несложно вывести формулу индекса нужной оценки в массиве: i * m + j = 1 * 2 + 0 = 2

где i — индекс искомого студента, а j — индекс нужной дисциплины.

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

Но, опять же, программисты дюже ленивые существа. Каждый раз делать умножение и сложение вручную, только чтобы узнать индекс оценки. Нет, не наш способ.

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

  1. Двумерный массив.

Какое же преимущество дает двумерный массив? По сравнению со втором способом (один массив) мы просто получили более удобную индексацию. Да, не так много. Но читаемость кода увеличилась, теперь мы прямо во время запроса оценки видим, какой индекс студента и дисциплины запрашивается. Плюс нам не нужно производить вычислений по формуле.

Как же это работает.

На самом деле запись двумерного массива — это лишь синтаксический «сахар» для программиста. В памяти «двумерный» массив программой размещается в памяти, как один «длинный» массив, как мы сами руками делали во втором способе. Но рассчет всех формул берет на себя программа за нас.

int array[3][2]; преобразуется компилятором в строчку int array[3 * 2];

А строчка array[i][j]; преобразуется в array[i * m + j];

На самом деле, как вы помните, даже оператор [] — это тоже сахар и на самом деле это выглядит еще немного по-другому: *(array + i * m + j);

И это истинная запись такой простой команды: array[i][j];

Ну, хорошо, с использованием двумерных массивов разобрались, но как же они хранятся в памяти. Ответ на этот вопрос уже звучал: двумерный массив преобразуется компилятором в длинный двумерный массив. А значит массив int array[3][2]; в памяти выглядит так:

Alt Texr

Массив любой размерности хранится в памяти ЛИНЕЙНО. Память в принципе линейна. Ячейки идут друг за другом. Перемещаться в памяти можно либо вперед, либо назад. Никаких «верх», «вниз» в памяти быть не может по определению.

Да, двумерный массив по смыслу и использованию похож на матрицу или таблицу.

НИКАКИХ ТАБЛИЦ В ПАМЯТИ БЫТЬ НЕ МОЖЕТ.

Память линейна. Все данные в массивах хранятся подряд.

Разбирая двумерные массивы, было упомянуто, что в программировании массивы многомерны. И на самом деле «мерность» массива ничем не ограничена. Размерность может быть 3, 4, 5 — хоть сколько.

Конечно, есть ограничение по логике. Редко когда требуется размерность больше 3. Но тем не менее, архитектурно ограничений нет. Трехмерные массивы могут понадобится, например, если нужно хранить оценки студентов из нескольких групп по нескольким дисциплинам.

Допустим, у нас есть 3 группы студентов в каждой по 3 человека, у каждого по 2 оценки.

Таким образом в памяти будет создан ОДИН линейный массив на 3 * 3 * 2 = 18 элемента.

Ну или компилятор запишет это так: int array[3 * 3 * 2];

А в памяти это будет выглядеть так:

Alt Texr

Задание для упоротых:

попробуйте записать array[i][j][k] вообще без использования оператора [] , как это демонстировалось в разделе «Двумерные массивы» Подсказка: расписывайте не сразу все 3 применения [] , а начините с первых двух [] , т.к. их понятно как расписать.

Или нарисуйте весь массив линейно и возьмите несколько индексов для примера (это тоже демонстрировалось в разделе про двумерные массивы) — и рассчитайте формулу, по которой вычисляется индекс.

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

Строка как массив

Строка — это массив символов, оканчивающийся символом конца строки (нулевым байтом \0 , т.е. просто нулем)

А значит, как в любом массиве, переменная «строки» — это адрес на первый символ в строке. Остальные символы располагаются сразу за первым.

Если в обычном массиве мы не знаем, где он заканчивается (для этого всегда нужно в отдельной переменной хранить кол-во элементов в массиве), то в строках для определения конца после последнего символа записывается нулевой байт, который и является признаком конца строки.

Например, строка «Hi» в памяти занимает на самом деле 3 байта: по 1 байту для каждого символа = 2 байта + 1 нулевой байт, как символ конца строки. Т.е. в памяти последовательность байт выглядит так:

Наглядный пример

В памяти все символы хранятся как код по таблице ASCII/Unicode/другая в зависимости от кодировки:

72 — это код символа ‘H’ в таблице ASCII

105 — это код символа ‘i’ в таблице ASCII

0 — это признак конца строки

Т.к. в памяти все данные хранятся только в числовом виде, то все символы кодируются через их коды. Таблицы кодировок бывают разными. Основные это ASCII и Unicode .

ASCII хранит символы в диапазоне от 0 до 255, поэтому содержит не так уж много символов: латинский алфавит, цифры и некоторое кол-во спец.символов. Поэтому для кодирования этой таблицы достаточно объема памяти в 1 байт. Это и есть тип данных char.

Unicode же может кодировать символы 2, 4 или 8 байтами. Поэтому содержит огромное кол-во символов. Все региональные алфавитные системы, включая арабские, иероглифичные, греческие и т.д. и т.д.

Нулевой байт в конце строки позволяет при работе со строкой точно знать, где она заканчивается без дополнительного параметра.

При создании строки после 6 «полезных» символов программа сама добавит еще 7 символ — нулевой байт, чтобы обозначить конец строки. При выводе строки на экран, будет выведены все байты, которые находятся до нулевого. Найдя нулевой байт в памяти, вывод остановится.

Думаете программа будет выводить ровно 6 или 7 байт, по кол-ву выведенной памяти? Давайте проверим.

После создания строки и вывода ее на экран, мы «затерли» нулевой байт, который находился по индексу 6 (нумерация же с нуля в массиве).

И теперь вывод на экран строки str0 будет примерно следующий:

Все что идет после символа ! (вместо которого был нулевой байт) — это «мусор». Случайные цифры, которые были преобразованы в символы, потому что программы интерпретирует любые байты от адреса начала строки и до нулевого байта как символы.

Остановилась программа только тогда, когда нашла в памяти «случайный» ноль. В моем случае до этого момента вывелось 12 лишних символов.

По принципу «до нулевого байта» работают абсолютно все строковые ф-ции: strcpy , strcmp , strcat и другие.

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

Чтобы облегчить эту задачи существует тип данных string , который по сути является оберткой вокруг обычного char* . У него есть функции получения длины, перегружены операторы для объединения строк, есть ф-ции поиска подстроки и много-много других удобных вещей.

Однако, как любая обертка, она не только упрощает вашу работу, но и не позволяет самостоятельно разобраться, как идет работа со строками.

Пока вы самостоятельно не освоите и не будете чувствовать себя уверенно с типом данных char* не используйте тип данных string. Это позволит вам отточить навыки работы со строками, указателями и памятью.

Статическая и динамическая строка

Также как и массив, строку можно создавать статическим путем:

или данимическим:

Пример создания статической строки:

Создадим статическую строку и считаем в нее значение.

Вроде бы, все хорошо. Однако даже в таком коде уже есть потенциальная ошибка.

Для строки str1 было выделено всего 5 байт. Если ввод будет, допустим, 🙂 , то будет все хорошо. Из буфера консоли в строку запишется 3 байта: : ) 0 . Последний нулевой байт нужен как символ конца строки.

Поэтому после строчки cout << str1; выведется как и положено 🙂 , хотя в строке и зарезервировано 5 байт (нулевой байт и все дела).

Однако, давайте попробуем считать с консоли, например, строку abdce . В ней же всего 5 символов. Как и положено. Ну ок, добавится 6 куда-нибудь после 5 наших. Все же должно быть хорошо? А вот фиг вам!

Будет ошибка: Run-Time Check Failure #2 — Stack around the variable ‘str1’ was corrupted. По-русски это будет звучать примерно как (крайне не рекомендую пользоваться русской версией IDE, кстати): Ошибка времени выполнения #2: Стек вокруг переменной str1 поврежден .

Как так? Да очень просто. Мы выделили 5 байт под строку. Вместе с нулевым байтом у нас из буфера консоли пришло 6 байт. 6, нулевой байт, вышел за границы массива. С++, конечно, тупой, но состояние стека и кучи во время выполнения программы он все-таки отслеживает.

Во время выполнения выполнения программа ожидала, что состояние будет одно (например, что переменная str1 занимает всего 5 байт), а внезапно 6 байт после начала строки тоже оказался изменен! И программа естественно такая: Что за нафиг?! И выдала вам ошибку.

Поэтому считывать любые строковые переменные таким образом нельзя. Лучше делать это так:

В этом случае даже если пользователь введет больше 4 символов (5й-то нам нужен под нулевой байт), например, qwerty , то все 6 символов запишутся в буфер консоли, однако команда cin.getline(str1, 5); из буфера извлечет и запишет в переменную str1 только 5 байт (4 символа + нулевой байт).

И выведется на экран только: qwer . Ошибки уже не будет.

Статические строки (их еще называют строки фиксированной, постоянной длины) подходят для хранения только значений определенной длины!

Ну хорошо, почему нельзя хранить больше, мы разобрались — больше просто не влезет, будет ошибка, но почему нельзя, скажем, в 10 байтах символов хранить всего 2 символа?

С++ позиционируется как язык с высокой оптимизацией не только скорости работы, но и использования памяти. Каждый байт при резервировании в памяти должен быть обоснован. Если вы создаете строку на 10 байтов, все эти 10 байт «заняты», компилятор не может их использовать. И если вы будете хранить в этих 10 байтах только 2 символа, то остальные 8 будут простаивать.

Хорошо 8 «потерянных» байт это не страшно. Однако привычка — страшная вещь. Не научившись сразу бережно относиться в памяти, вы рискуете всегда «жрать память» как не в себя.

На эту тему даже есть анекдот:

Аккуратный стол, белая тарелочка на нем, на тарелочке оперативка. Сидит С++, аккуратно ножом отрезает кусочек и кушает. И ТУТ В СТОЛОВУЮ ВЛАМЫВАЕТСЯ СВИНЬЯ И СЖИРАЕТ ВСЮ ОПЕРАТИВКУ СРАЗУ — это Java. . С++ от испуга выплевывает откушенный кусочек и кричит: Segmentation fault.

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

Для всех остальных случаев нужно использовать динамические строки.

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

Мы не знаем, сколько символов введет пользователь. Может быть, всего 10, а может быть и 100500. Но если он введет всего 10, мы не хотим иметь зарезервированную, но не используемую память. Если он введет 100500 символов, то мы не хотим случайно их потерять, потому что считываем из консоли только определенное кол-во символов.

Для этого и создается буфер с запасом памяти, чтобы точно хватило. И из консоли данные считываются в него. Если буфер у нас на 255 символов, а пользователь введет всего 10 символов, то нулевой байт даст нам знать, что «полезных» символов будет всего 10.

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

А теперь как это может выглядеть в коде:

Пошаговое пояснение кода:

Еще более хорошим тоном считается в программе выделить все это хозяйство в отдельную ф-цию, чтобы не писать каждый раз столько строк ради одного лишь считывания строки с экрана:

В данном случае нет необходимости создавать буфер, как динамическую строку, чтобы затем удалить ее вручную. Т.к. это ф-ция, а переменная buf — локальная переменная, она уничтожится сама, когда закончится блок кода, в котором она была объявлена, т.е. сама ф-ция.

Массивы в 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.

копирование статического массива в динамический массив

Я знаю, что это работает, потому что оно компилируется, но я вижу некоторые потенциальные проблемы.

Скажем, я хочу присвоить значения этому массиву:

Что именно происходит для генерации этой ошибки?

3 ответа

Вы должны использовать std::copy для копирования статического массива в динамический массив, как показано ниже:

Альтернативно, если вы хотите удобство назначений вместо элементарного копирования и при условии, что вы знаете размер массивов во время компиляции, а ваш компилятор поддерживает функции С++ 11, используйте std::array как показано ниже:

Тем не менее, рекомендуется использовать std::vector для использования необработанных динамических массивов, как показано ниже:

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

Это два разных типа данных в отношении компилятора.

Когда ты говоришь:

Вы создаете блок кучи (динамической) памяти, а «значения» содержат адрес памяти первого элемента в массиве. Чтобы заполнить значения из вашего статического массива, вам необходимо отдельно назначить каждый элемент следующим образом:

Вы можете использовать std::vector который имеет конструктор итератора, это решит проблему для вас.

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

Читать:
Как посчитать количество мужчин и женщин в excel

Похожие статьи