3.5.3. Указатели и массивы

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

Обычно указатель на объект получают при помощи оператора обращения к адресу (см. раздел 2.3.2). По правде говоря, оператор обращения к адресу может быть применен к любому объекту, а элементы в массиве — объекты. При индексировании массива результатом является объект в этой области массива. Подобно любым другим объектам, указатель на элемент массива можно получить из адреса этого элемента:

string nums[] = {"one", "two", "three"}; // массив строк

string *p = &nums[0]; // p указывает на первый элемент массива nums

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

string *p2 = nums; // эквивалент p2 = &nums[0]

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

Существует множество свидетельств того факта, что операции с массивами зачастую являются операциями с указателями. Одно из них — при использовании массива как инициализатора переменной, определенной с использованием спецификатора auto (см. раздел 2.5.2), выводится тип указателя, а не массива.

int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia - массив из десяти целых чисел

auto ia2(ia); // ia2 - это int*, указывающий на первый элемент в ia

ia2 = 42;     // ошибка: ia2 - указатель, нельзя присвоить указателю

              // значение типа int

Хотя ia является массивом из десяти целых чисел, при его использовании в качестве инициализатора компилятор рассматривает это как следующий код:

auto ia2(&ia[0]); // теперь ясно, что ia2 имеет тип int*

Следует заметить, что это преобразование не происходит, если используется спецификатор decltype (см. раздел 2.5.3). Выражение decltype(ia) возвращает массив из десяти целых чисел:

// ia3 - массив из десяти целых чисел

decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};

ia3 = p;    // ошибка: невозможно присвоить int* массиву

ia3[4] = i; // ok: присвоить значение i элементу в массиве ia3

Указатели — это итераторы

Указатели, содержащие адреса элементов в массиве, обладают дополнительными возможностями, кроме описанных в разделе 2.3.2. В частности, указатели на элементы массивов поддерживают те же операции, что и итераторы векторов или строк (см. раздел 3.4). Например, можно использовать оператор инкремента для перемещения с одного элемента массива на следующий:

int arr[] = {0,1,2,3,4,5,6,7,8,9};

int *p = arr; // p указывает на первый элемент в arr

++p;          // p указывает на arr[1]

Подобно тому, как итераторы можно использовать для перебора элементов вектора, указатели можно использовать для перебора элементов массива. Конечно, для этого нужно получить указатели на первый элемент и элемент, следующий после последнего. Как упоминалось только что, указатель на первый элемент можно получить при помощи самого массива или при обращении к адресу первого элемента. Получить указатель на следующий элемент после последнего можно при помощи другого специального свойства массива. Последний элемент массива arr находится в позиции 9, а адрес несуществующего элемента массива, следующего после него, можно получить так:

int *е = &arr[10]; // указатель на элемент после

                   // последнего в массиве arr

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

Используя эти указатели, можно написать цикл, выводящий элементы массива arr.

for (int *b = arr; b != e; ++b)

 cout << *b << endl; // вывод элементов arr

Библиотечные функции begin() и end()

Указатель на элемент после конца можно вычислить, но этот подход подвержен ошибкам. Чтобы облегчить и обезопасить использование указателей, новая библиотека предоставляет две функции: begin() и end(). Эти функции действуют подобно одноименным функциям-членам контейнеров (см. раздел 3.4.1). Однако массивы — не классы, и данные функции не могут быть функциями-членами. Поэтому для работы они получают массив в качестве аргумента.

int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia - массив из десяти целых чисел

int *beg = begin(ia); // указатель на первый элемент массива ia

int *last = end(ia);  // указатель на следующий элемент ia за последним

Функция begin() возвращает указатель на первый, а функция end() на следующий после последнего элемент данного массива. Эти функции определены в заголовке iterator.

Используя функции begin() и end(), довольно просто написать цикл обработки элементов массива. Предположим, например, что массив arr содержит значения типа int. Первое отрицательное значение в массиве arr можно найти следующим образом:

// pbeg указывает на первый, a pend на следующий после последнего

// элемент массива arr

int *pbeg = begin(arr), *pend = end(arr);

// найти первый отрицательный элемент, остановиться, если просмотрены

// все элементы

while (pbeg != pend && *pbeg >= 0)

 ++pbeg;

Код начинается с определения двух указателей типа int по имени pbeg и pend. Указатель pbeg устанавливается на первый элемент массива arr, a pend — на следующий элемент после последнего. Условие цикла while использует указатель pend, чтобы узнать, безопасно ли обращаться к значению указателя pbeg. Если указатель pbeg действительно указывает на элемент, выполняется проверка результата обращения к его значению на наличие отрицательного значения. Если это так, то условие ложно и цикл завершается. В противном случае указатель переводится на следующий элемент.

Указатель на элемент "после последнего" у встроенного массива ведет себя так же, как итератор, возвращенный функцией end() вектора. В частности, нельзя ни обратиться к значению такого указателя, ни осуществить его приращение.

Арифметические действия с указателями

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

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

constexpr size_t sz = 5;

int arr[sz] = {1,2,3,4,5};

int *ip = arr;     // эквивалент int *ip = &arr[0]

int *ip2 = ip + 4; // ip2 указывает на arr[4], последний элемент в arr

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

Результатом добавления целочисленного значения к указателю должен быть указатель на элемент (или следующую позицию после конца) в том же массиве:

// ok: arr преобразуется в указатель на его первый элемент;

// p указывает на позицию после конца arr

int *p = arr + sz;  // использовать осмотрительно - не обращаться

                    // к значению!

int *p2 = arr + 10; // ошибка: arr имеет только 5 элементов;

                    // значение p2 неопределенно

При сложении arr и sz компилятор преобразует arr в указатель на первый элемент массива arr. При добавлении sz к этому указателю получается указатель на позицию sz (т.е. на позицию 5) этого массива. Таким образом, он указывает на следующую позицию после конца массива arr. Вычисление указателя на более чем одну позицию после последнего элемента является ошибкой, хотя компилятор таких ошибок не обнаруживает.

Подобно итераторам, вычитание двух указателей дает дистанцию между ними. Указатели должны указывать на элементы в том же массиве:

auto n = end(arr) - begin(arr); // n - 5, количество элементов

                                // массива arr

Результат вычитания двух указателей имеет библиотечный тип ptrdiff_t. Как и тип size_t, тип ptrdiff_t является машинозависимым типом, определенным в заголовке cstddef. Поскольку вычитание способно возвратить отрицательное значение, тип ptrdiff_t — знаковый целочисленный.

Для сравнения указателей на элементы (или позицию за концом) массива можно использовать операторы сравнения. Например, элементы массива arr можно перебрать следующим образом:

int *b = arr, *е = arr + sz;

while (b < e) {

 // используется *b

 ++b;

}

Нельзя использовать операторы сравнения для указателей на два несвязанных объекта.

int i = 0, sz = 42;

int *p = &i, *е = &sz;

// неопределенно: p и е не связаны; сравнение бессмысленно!

while (p < е)

Хотя на настоящий момент смысл может быть и неясен, но следует заметить, что арифметические действия с указателями допустимы также для нулевых указателей (см. раздел 2.3.2) и для указателей на объекты, не являющиеся массивом. В последнем случае указатели должны указывать на тот же объект или следующий после него. Если p — нулевой указатель, то к нему можно добавить (или вычесть) целочисленное константное выражение (см. раздел 2.4.4) со значением 0. Можно также вычесть два нулевых указателя из друг друга, и результатом будет 0.

Взаимодействие обращения к значению с арифметическими действиями с указателями

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

int ia[] = {0,2,4,6,8}; // массив из 5 элементов типа int

int last = *(ia + 4);   // ok: инициализирует last значением

                        // ia[4], т.е. 8

Выражение *(ia + 4) вычисляет адрес четвертого элемента после ia и обращается к значению полученного указателя. Это выражение эквивалентно выражению ia[4].

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

last = *ia + 4; // ok: last = 4, эквивалент ia[0] + 4

Этот код обращается к значению ia и добавляет 4 к полученному значению. Причины подобного поведения рассматриваются в разделе 4.1.2.

Индексирование и указатели

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

int ia[] = {0,2,4,6,8}; // массив из 5 элементов типа int

Рассмотрим выражение ia[0], использующее имя массива. При индексировании массива в действительности индексируется указатель на элемент в этом массиве.

int i = ia[2]; // ia преобразуется в указатель на первый элемент ia

               // ia[2] выбирает элемент, на который указывает (ia + 2)

int *p = ia;   // p указывает на первый элемент в массиве ia

i = *(p + 2);  // эквивалент i = ia[2]

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

int *p = &ia[2]; // p указывает на элемент с индексом 2

int j = p[1];    // p[1] - эквивалент *(p + 1),

                 // p[1] тот же элемент, что и ia[3]

int k = p[-2];   // p[-2] тот же элемент, что и ia[0]

Последний пример указывает на важное отличие между массивами и такими библиотечными типами, как vector и string, у которых есть операторы индексирования. Библиотечные типы требуют, чтобы используемый индекс был беззнаковым значением. Встроенный оператор индексирования этого не требует. Индекс, используемый со встроенным оператором индексирования, может быть отрицательным значением. Конечно, полученный адрес должен указывать на элемент (или позицию после конца) массива, на который указывает первоначальный указатель.

В отличие от индексов для векторов и строк, индекс встроенного массива не является беззнаковым.

Упражнения раздела 3.5.3

Упражнение 3.34. С учетом, что указатели p1 и p2 указывают на элементы в том же массиве, что делает следующий код? Какие значения p1 или p2 делают этот код недопустимым?

p1 += p2 - p1;

Упражнение 3.35. Напишите программу, которая использует указатели для обнуления элементов массива.

Упражнение 3.36. Напишите программу, сравнивающую два массива на равенство. Напишите подобную программу для сравнения двух векторов.

Более 800 000 книг и аудиокниг! 📚

Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением

ПОЛУЧИТЬ ПОДАРОК