12.2.1. Оператор new и массивы
Чтобы запросить оператор new зарезервировать массив объектов, после имени типа следует указать в квадратных скобках количество резервируемых объектов. В данном случае оператор new резервирует требуемое количество объектов и (при успешном резервировании) возвращает указатель на первый из них:
// вызов get_size() определит количество резервируемых целых чисел
int *pia = new int[get_size()]; // pia указывает на первое из них
Значение в скобках должно иметь целочисленный тип, но не обязано быть константой.
Для представления типа массива при резервировании можно также использовать псевдоним типа (см. раздел 2.5.1). В данном случае скобки не нужны:
typedef int arrT[42]; // arrT - имя типа массива из 42 целых чисел
int *p = new arrT; // резервирует массив из 42 целых чисел;
// p указывает на первый его элемент
Здесь оператор new резервирует массив целых чисел и возвращает указатель на его первый элемент. Даже при том, что никаких скобок в коде нет, компилятор выполняет это выражение, используя оператор new[]. Таким образом, компилятор выполняет это выражение, как будто код был написан так:
int *p = new int[42];
Резервирование массива возвращает указатель на тип элемента
Хотя обычно память, зарезервированную оператором new T[], называют "динамическим массивом", это несколько вводит в заблуждение. Когда мы используем оператор new для резервирования массива, объект типа массива получен не будет. Вместо этого будет получен указатель на тип элемента массива. Даже если для определения типа массива использовать псевдоним типа, оператор new не резервирует объект типа массива. И в данном случае резервируется массив, хотя часть [число] не видима. Даже в этом случае оператор new возвращает указатель на тип элемента.
Поскольку зарезервированная память не имеет типа массива, для динамического массива нельзя вызвать функцию begin() или end() (см. раздел 3.5.3). Для возвращения указателей на первый и следующий после последнего элементы эти функции используют размерность массива (являющуюся частью типа массива). По тем же причинам для обработки элементов так называемого динамического массива нельзя также использовать серийный оператор for.
Важно помнить, что у так называемого динамического массива нет типа массива.
Инициализация массива динамически созданных объектов
Зарезервированные оператором new объекты (будь то одиночные или их массивы) инициализируются по умолчанию. Для инициализации элементов массива по умолчанию (см. раздел 3.3.1) за размером следует расположить пару круглых скобок:
int *pia = new int[10]; // блок из десяти неинициализированных
// целых чисел
int *pia2 = new int[10](); // блок из десяти целых чисел,
// инициализированных по умолчанию
// значением 0
string *psa = new string[10]; // блок из десяти пустых строк
string *psa2 = new string[10](); // блок из десяти пустых строк
По новому стандарту можно также предоставить в скобках список инициализаторов элементов:
// блок из десяти целых чисел, инициализированных соответствующим
// инициализатором
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
// блок из десяти строк; первые четыре инициализируются заданными
// инициализаторами, остальные элементы инициализируются значением
// по умолчанию
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
При списочной инициализации объекта типа встроенного массива (см. раздел 3.5.1) инициализаторы используются для инициализации первых элементов массива. Если инициализаторов меньше, чем элементов, остальные инициализируются значением по умолчанию. Если инициализаторов больше, чем элементов, оператор new потерпит неудачу, не зарезервировав ничего. В данном случае оператор new передает исключение типа bad_array_new_length. Подобно исключению bad_alloc, этот тип определен в заголовке new.
Хотя для инициализации элементов массива по умолчанию можно использовать пустые круглые скобки, в них нельзя предоставить инициализаторы для элементов. Благодаря этому факту при резервировании массива нельзя использовать ключевое слово auto (см. раздел 12.1.2).
Динамическое резервирование пустого массива вполне допустимо
Для определения количества резервируемых объектов можно использовать произвольное выражение:
size_t n = get_size(); // get_size() возвращает количество необходимых
// элементов
int* p = new int[n]; // резервирует массив для содержания элементов
for (int* q = p; q != p + n; ++q)
/* обработка массива */ ;
Возникает интересный вопрос: что будет, если функция get_size() возвратит значение 0? Этот код сработает прекрасно. Вызов функции new[n] при n равном 0 вполне допустим, даже при том, что нельзя создать переменную типа массива размером 0:
char arr[0]; // ошибка: нельзя определить массив нулевой длины
char *cp = new char[0]; // ok: но обращение к значению cp невозможно
При использовании оператора new для резервирования массива нулевого размера он возвращает допустимый, а не нулевой указатель. Этот указатель гарантированно будет отличен от любого другого указателя, возвращенного оператором new. Он будет подобен указателю на элемент после конца (см. раздел 3.5.3) для нулевого элемента массива. Этот указатель можно использовать теми способами, которыми используется итератор после конца. Его можно сравнивать в цикле, как выше. К нему можно добавить нуль (или вычесть нуль), такой указатель можно вычесть из себя, получив в результате нуль. К значению такого указателя нельзя обратиться, в конце концов, он не указывает на элемент.
В гипотетическом цикле, если функция get_size() возвращает 0, то n также равно 0. Вызов оператора new зарезервирует нуль объектов. Условие оператора for будет ложно (p равно q + n, поскольку n равно 0). Таким образом, тело цикла не выполняется.
Освобождение динамических массивов
Для освобождения динамического массива используется специальная форма оператора delete, имеющая пустую пару квадратных скобок:
delete p; // p должен указывать на динамически созданный объект или
// быть нулевым
delete [] pa; // pa должен указывать на динамически созданный
// объект или быть нулевым
Второй оператор удаляет элементы массива, на который указывает pa, и освобождает соответствующую память. Элементы массива удаляются в обратном порядке. Таким образом, последний элемент удаляется первым, затем предпоследний и т.д.
При применении оператора delete к указателю на массив пустая пара квадратных скобок необходима: она указывает компилятору, что указатель содержит адрес первого элемента массива объектов. Если пропустить скобки оператора delete для указателя на массив (или предоставить их, передав оператору delete указатель на объект), то его поведение будет непредсказуемо.
Напомним, что при использовании псевдонима типа, определяющего тип массива, можно зарезервировать массив без использования [] в операторе new. Но даже в этом случае нужно использовать скобки при удалении указателя на этот массив:
typedef int arrT[42]; // arrT имя типа массив из 42 целых чисел
int *p = new arrT; // резервирует массив из 42 целых чисел; p указывает
// на первый элемент
delete [] p; // скобки необходимы, поскольку был
// зарезервирован массив
Несмотря на внешний вид, указатель p указывает на первый элемент массива объектов, а не на отдельный объект типа arrT. Таким образом, при удалении указателя p следует использовать [].
Компилятор вряд ли предупредит нас, если забыть скобки при удалении указателя на массив или использовать их при удалении указателя на объект. Программа будет выполняться с ошибкой, не предупреждая о ее причине.
Интеллектуальные указатели и динамические массивы
Библиотека предоставляет версию указателя unique_ptr, способную контролировать массивы, зарезервированные оператором new. Чтобы использовать указатель unique_ptr для управления динамическим массивом, после типа объекта следует расположить пару пустых скобок:
// up указывает на массив из десяти неинициализированных целых чисел
unique_ptr<int[]> up(new int[10]);
up.release(); // автоматически использует оператор delete[] для
// удаления указателя
Скобки в спецификаторе типа (<int[]>) указывают, что указатель up указывает не на тип int, а на массив целых чисел. Поскольку указатель up указывает на массив, при удалении его указателя автоматически используется оператор delete[].
Указатели unique_ptr на массивы предоставляют несколько иные функции, чем те, которые использовались в разделе 12.1.5. Эти функции описаны в табл. 12.6. Когда указатель unique_ptr указывает на массив, нельзя использовать точечный и стрелочный операторы доступа к элементам. В конце концов, указатель unique_ptr указывает на массив, а не на объект, поэтому эти операторы были бы бессмысленны. С другой стороны, когда указатель unique_ptr указывает на массив, для доступа к его элементам можно использовать оператор индексирования:
for (size_t i = 0; i != 10; ++i)
up[i] = i; // присвоить новое значение каждому из элементов
Таблица 12.6. Функции указателя unique_ptr на массив
Операторы доступа к элементам (точка и стрелка) не поддерживаются указателями unique_ptr на массивы. Другие его функции неизменны unique_ptr<T[]> u u может указывать на динамически созданный массив типа T unique ptr<T[]> u(p) u указывает на динамически созданный массив, на который указывает встроенный указатель p. Тип указателя p должен допускать приведение к типу T (см. раздел 4.11.2). Выражение u[i] возвратит объект в позиции i массива, которым владеет указатель u. u должен быть указателем на массивВ отличие от указателя unique_ptr, указатель shared_ptr не оказывает прямой поддержки управлению динамическим массивом. Если необходимо использовать указатель shared_ptr для управления динамическим массивом, следует предоставить собственную функцию удаления:
// чтобы использовать указатель shared_ptr, нужно предоставить
// функцию удаления
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // использует предоставленное лямбда-выражение, которое в
// свою очередь использует оператор delete[] для освобождения массива
Здесь лямбда-выражение (см. раздел 10.3.2), использующее оператор delete[], передается как функция удаления.
Если не предоставить функции удаления, результат выполнения этого кода непредсказуем. По умолчанию указатель shared_ptr использует оператор delete для удаления объекта, на который он указывает. Если объект является динамическим массивом, то при использовании оператора delete возникнут те же проблемы, что и при пропуске [], когда удаляется указатель на динамический массив (см. раздел 12.2.1).
Поскольку указатель shared_ptr не поддерживает прямой доступ к массиву, для обращения к его элементам применяется следующий код:
// shared_ptr не имеет оператора индексирования и не поддерживает
// арифметических действий с указателями
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; // для доступа к встроенному указателю
// используется функция get()
Указатель shared_ptr не имеет оператора индексирования, а типы интеллектуальных указателей не поддерживают арифметических действий с указателями. В результате для доступа к элементам массива следует использовать функцию get(), возвращающую встроенный указатель, который можно затем использовать обычным способом.
Упражнения раздела 12.2.1
Упражнение 12.23. Напишите программу, конкатенирующую два строковых литерала и помещающую результат в динамически созданный массив символов. Напишите программу, конкатенирующую две строки библиотечного типа string, имеющих те же значения, что и строковые литералы, используемые в первой программе.
Упражнение 12.24. Напишите программу, которая читает строку со стандартного устройства ввода в динамически созданный символьный массив. Объясните, как программа обеспечивает ввод данных переменного размера. Проверьте свою программу, введя строку, размер которой превышает длину зарезервированного массива.
Упражнение 12.25. С учетом следующего оператора new, как будет удаляться указатель pa?
int *pa = new int[10];
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК