9.11 – Арифметика указателей и индексирование массивов

Добавлено 7 июня 2021 в 08:47
Глава 9 – Массивы, строки, указатели и ссылки  (содержание)

Арифметика указателей

Язык C++ позволяет выполнять операции сложения или вычитания между указателями и целыми числами. Если ptr указывает на int, то ptr + 1 – это адрес следующего числа int в памяти после ptr. ptr - 1 – это адрес предыдущего числа int перед ptr.

Обратите внимание, что ptr + 1 возвращает не адрес памяти после ptr, а адрес памяти следующего объекта того же типа, на который указывает ptr. Если ptr указывает на значение int (при условии, что оно занимает 4 байта), ptr + 3 означает 3 значения int (12 байтов) после ptr. Если ptr указывает на char, который всегда равен 1 байту, ptr + 3 означает 3 значения char (3 байта) после ptr.

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

Рассмотрим следующую программу:

#include <iostream>
 
int main()
{
    int value{ 7 };
    int* ptr{ &value };
 
    std::cout << ptr << '\n';
    std::cout << ptr+1 << '\n';
    std::cout << ptr+2 << '\n';
    std::cout << ptr+3 << '\n';
 
    return 0;
}

На машине автора эта программа дала такой вывод:

0012FF7C
0012FF80
0012FF84
0012FF88

Как видите, каждый из этих адресов отличается на 4 (7C + 4 = 80 в шестнадцатеричном формате). Это потому, что на машине автора int составляет 4 байта.

Если в той же программе используется short вместо int:

#include <iostream>
 
int main()
{
    short value{ 7 };
    short* ptr{ &value };
 
    std::cout << ptr << '\n';
    std::cout << ptr+1 << '\n';
    std::cout << ptr+2 << '\n';
    std::cout << ptr+3 << '\n';
 
    return 0;
}

На машине автора эта программа дала такой вывод:

0012FF7C
0012FF7E
0012FF80
0012FF82

Поскольку short составляет 2 байта, каждый адрес отличается на 2.

Массивы располагаются в памяти последовательно

Используя оператор адреса (&), мы можем определить, что массивы располагаются в памяти последовательно. То есть элементы 0, 1, 2,… все соседствуют друг с другом по порядку.

#include <iostream>
 
int main()
{
    int array[]{ 9, 7, 5, 3, 1 };
 
    std::cout << "Element 0 is at address: " << &array[0] << '\n';
    std::cout << "Element 1 is at address: " << &array[1] << '\n';
    std::cout << "Element 2 is at address: " << &array[2] << '\n';
    std::cout << "Element 3 is at address: " << &array[3] << '\n';
 
    return 0;
}

На машине автора эта программа напечатала:

Element 0 is at address: 0041FE9C
Element 1 is at address: 0041FEA0
Element 2 is at address: 0041FEA4
Element 3 is at address: 0041FEA8

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

Арифметика указателей, массивы и магия индексирования

В разделе выше вы узнали, что массивы располагаются в памяти последовательно.

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

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

Следовательно, мы можем заключить, что добавление единицы к массиву должно указывать на второй элемент (элемент 1) массива. Мы можем экспериментально проверить, что это правда:

#include <iostream>
 
int main()
{
     int array[]{ 9, 7, 5, 3, 1 };
 
     std::cout << &array[1] << '\n'; //выводим адрес памяти элемента 1 массива
     std::cout << array+1 << '\n';  // выводим адрес памяти (указатель на массив + 1)
 
     std::cout << array[1] << '\n';   // выводит 7
     std::cout << *(array+1) << '\n'; // выводит 7 (обратите внимание на обязательные скобки)
 
    return 0;
}

Обратите внимание, что при выполнении косвенного обращения через результат арифметики указателей скобки необходимы для обеспечения правильного приоритета оператора, поскольку operator* имеет более высокий приоритет, чем operator+.

На машине автора эта программа напечатала:

0017FB80
0017FB80
7
7

Оказывается, когда компилятор видит оператор индекса ([]), он на самом деле использует добавление целых чисел к указателю и косвенное обращение через указатель! Обобщая, array[n] то же самое, что *(array + n), где n – целое число. Оператор индекса [] нужен и для красоты кода, и для простоты использования (чтобы вам не нужно было запоминать круглые скобки).

Использование указателя для перебора массива

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

#include <iostream>
#include <iterator> // для std::size
 
bool isVowel(char ch)
{
  switch (ch)
  {
  case 'A':
  case 'a':
  case 'E':
  case 'e':
  case 'I':
  case 'i':
  case 'O':
  case 'o':
  case 'U':
  case 'u':
    return true;
  default:
    return false;
  }
}
 
int main()
{
  char name[]{ "Mollie" };
  int arrayLength{ static_cast<int>(std::size(name)) };
  int numVowels{ 0 };
 
  for (char* ptr{ name }; ptr != (name + arrayLength); ++ptr)
  {
    if (isVowel(*ptr))
    {
      ++numVowels;
    }
  }
 
  std::cout << name << " has " << numVowels << " vowels.\n";
 
  return 0;
}

Как это работает? Эта программа использует указатель для перебора каждого из элементов массива. Помните, что массив раскладывается на указатель на первый элемент массива. Таким образом, когда мы присвоили name указателю ptr, ptr так же стал указывать на первый элемент массива. Когда мы вызываем isVowel(*ptr), выполняется косвенное обращение через ptr для каждого элемента, и если элемент является гласной буквой, numVowels увеличивается. Затем цикл for использует оператор ++ для перемещения указателя на следующий символ в массиве. Цикл for завершается после проверки всех символов.

Показанная выше программа дает следующий результат:

Mollie has 3 vowels

Поскольку подсчет элементов является обычным явлением, библиотека алгоритмов предлагает функцию std::count_if, которая подсчитывает элементы, удовлетворяющие условию. Мы можем заменить цикл for вызовом std::count_if.

#include <algorithm>
#include <iostream>
#include <iterator> // для std::begin и std::end
 
bool isVowel(char ch)
{
  switch (ch)
  {
  case 'A':
  case 'a':
  case 'E':
  case 'e':
  case 'I':
  case 'i':
  case 'O':
  case 'o':
  case 'U':
  case 'u':
    return true;
  default:
    return false;
  }
}
 
int main()
{
  char name[]{ "Mollie" };
  auto numVowels{ std::count_if(std::begin(name), std::end(name), isVowel) };
 
  std::cout << name << " has " << numVowels << " vowels.\n";
 
  return 0;
}

std::begin возвращает итератор (указатель) на первый элемент, а std::end возвращает итератор на элемент, который будет после последнего. Итератор, возвращаемый std::end, используется только как маркер, доступ к нему вызывает неопределенное поведение, поскольку он не указывает на реальный элемент.

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

// nameLength - количество элементов в массиве.
std::count_if(name, name + nameLength, isVowel)
 
// Не делайте так. Доступ к недопустимым индексам вызывает неопределенное поведение.
// std::count_if(name, &name[nameLength], isVowel)

Обратите внимание, что мы вычисляем name + nameLength, а не name + nameLength - 1, потому что нам нужен не последний элемент, а псевдоэлемент, следующий за последним.

Подобное вычисление начала и конца массива работает для всех алгоритмов, которым нужен аргумент начала и конца.

Небольшой тест

Вопрос 1

Почему работает следующий код?

#include <iostream>
 
int main()
{
  int arr[]{ 1, 2, 3 };
 
  std::cout << 2[arr] << '\n';
 
  return 0;
}

Оператор нижнего индекса ([]) идентичен добавлению и косвенному обращению, операнды можно менять местами.

arr[2]
// то же, что и
*(arr + 2)
// то же, что и
*(2 + arr)
// то же, что и
2[arr]

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


Вопрос 2

Напишите функцию с именем find, которая принимает указатель на начало и указатель на конец (следующий элемент после последнего) массива, а также значение value. Функция должна искать данное значение и возвращать указатель на первый элемент с этим значением или указатель на конец, если элемент не был найден. Эта функция должна работать в следующей программе:

#include <iostream>
#include <iterator>
 
// ...
 
int main()
{
    int arr[]{ 2, 5, 4, 10, 8, 20, 16, 40 };
 
    // Ищем первый элемент со значением 20.
    int* found{ find(std::begin(arr), std::end(arr), 20) };
 
    // Если найден элемент со значением 20, распечатываем его.
    if (found != std::end(arr))
    {
        std::cout << *found << '\n';
    }
 
    return 0;
}

Подсказка


std::begin и std::end возвращают int*. Вызов find эквивалентен

int* found{ find(arr, arr + std::size(arr), 20) };

#include <iostream>
#include <iterator>
 
int* find(int* begin, int* end, int value)
{
    // Мы используем оператор !=, а не <, потому что != совместим
    // с большим количеством типов, чем <. Это обычная практика
    // для итераторов, о которых мы поговорим позже. Он не имеет
    // преимуществ при использовании с указателями, но делает код
    // более последовательным.
    for (int* p{ begin }; p != end; ++p)
    {
        if (*p == value)
        {
            return p;
        }
    }
 
    return end;
}
 
int main()
{
    int arr[]{ 2, 5, 4, 10, 8, 20, 16, 40 };
 
    int* found{ find(std::begin(arr), std::end(arr), 20) };
 
    if (found != std::end(arr))
    {
        std::cout << *found << '\n';
    }
 
    return 0;
}

Подсказка


find – это стандартная функция:

#include <algorithm> // std::find
#include <iostream>
#include <iterator>
 
int main()
{
    int arr[]{ 2, 5, 4, 10, 8, 20, 16, 40 };
 
    // Примечание: std::find возвращает итератор,
    // мы поговорим об итераторах позже.
    auto found{ std::find(std::begin(arr), std::end(arr), 20) };
 
    if (found != std::end(arr))
    {
        std::cout << *found << '\n';
    }
 
    return 0;
}

Теги

arrayC++ / CppLearnCppstd::count_ifДля начинающихИтераторМассивОбучениеПрограммированиеУказатель / Pointer (программирование)

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.