﻿# Урок 13\. Паттерн 5\. Адресная арифметика

Мы специально выбрали номер "тринадцать" для этого урока, поскольку ошибки, связанные с [адресной арифметикой](https://pvs-studio.ru/ru/blog/terms/0005/) в 64\-битных системах, являются наиболее коварными\. Надеемся, число 13 заставит вас быть внимательнее\.

Суть паттерна: чтобы избежать ошибок в 64\-битном коде используйте для адресной арифметики только [memsize](https://pvs-studio.ru/ru/blog/terms/0030/)\-типы\.

Рассмотрим код:

```cpp
unsigned short a16, b16, c16;
char *pointer;
...
pointer += a16 * b16 * c16;
```

Данный пример корректно работает с указателями, если значение выражения "a16 \* b16 \* c16" не превышает INT\_MAX \(2147483647\)\. Такой код мог всегда корректно работать на 32\-битной платформе\. В рамках 32\-битной архитектуры программе недоступен объем памяти для создания массива подобного размеров\. На 64\-битной архитектуре это ограничение снято, и размер массива легко может превысить INT\_MAX элементов\. Допустим, мы хотим сдвинуть значение указателя на 6\.000\.000\.000 байт, и поэтому переменные a16, b16 и c16 имеют значения 3000, 2000 и 1000 соответственно\. При вычислении выражения "a16 \* b16 \* c16" все переменные, согласно правилам языка Си\+\+, будут приведены к типу int, а уже затем будет произведено их умножение\. В ходе выполнения умножения произойдет переполнение\. Некорректный результат выражения будет расширен до типа [ptrdiff\_t](https://pvs-studio.ru/ru/blog/terms/0041/), и произойдет некорректное вычисление указателя\.

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

```cpp
short a16, b16, c16;
char *pointer;
...
pointer += static_cast<ptrdiff_t>(a16) *
           static_cast<ptrdiff_t>(b16) *
           static_cast<ptrdiff_t>(c16);
```

Если вы думаете, что злоключения ждут неаккуратные программы только на больших объемах данных, то мы вынуждены вас огорчить\. Рассмотрим интересный код для работы с массивом, содержащим всего 5 элементов\. Этот пример работоспособен в 32\-битном варианте и не работоспособен в 64\-битном:

```cpp
int A = -2;
unsigned B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B); //Invalid pointer value on 64-bit platform
printf("%i\n", *ptr); //Access violation on 64-bit platform
```

Давайте проследим, как происходит вычисление выражения "ptr \+ \(A \+ B\)":

* Согласно правилам языка Си\+\+ переменная A типа int приводится к типу unsigned\.
* Происходит сложение A и B\. В результате мы получаем значение 0xFFFFFFFF типа unsigned\.
* Вычисляется выражение "ptr \+ 0xFFFFFFFFu"\.

Что из этого выйдет, будет зависеть от размера указателя на данной архитектуре\. Если сложение будет происходить в 32\-битной программе, то данное выражение будет эквивалентно "ptr \- 1", и мы успешно распечатаем число "3"\. В 64\-битной программе к указателю честным образом прибавится значение 0xFFFFFFFFu, в результате чего указатель окажется далеко за пределами массива\. И при доступе к элементу по данному указателю нас ждут неприятности\.

Для предотвращения подобной ситуации, как и в первом случае, рекомендуем использовать в арифметике с указателями только memsize\-типы\. Два варианта исправления кода:

```cpp
ptr = ptr + (ptrdiff_t(A) + ptrdiff_t(B));
ptrdiff_t A = -2;
size_t B = 1;
...
ptr = ptr + (A + B);
```

Вы можете возразить и предложить следующий вариант исправления:

```cpp
int A = -2;
int B = 1;
...
ptr = ptr + (A + B);
```

Да, такой код будет работать, но он плох по ряду причин:

* Он будет приучать к неаккуратной работе с указателями\. Через некоторое время вы можете забыть нюансы и по ошибке вновь сделать одну из переменных типа unsigned\.
* Использование не memsize\-типов совместно с указателями потенциально опасно само по себе\. Допустим, что в выражении с указателем участвует переменная Delta типа int, и это выражение совершенно корректно\. Ошибка может крыться в вычислении самой переменной Delta, так как 32 бит может не хватить для необходимых вычислений, при работе с большими массивами данных\. Использование memsize\-типа для переменной Delta автоматически устраняет такую опасность\.
* Код, использующий при работе с указателями типы size\_t, ptrdiff\_t и другие memsize\-типы, приводит к генерации более оптимального двоичного кода, о чем будет рассказано подробнее в одном из следующих уроков\.

## Индексация массивов

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

В программах, обрабатывающих большие объемы данных, могут встретиться ошибки связанные с индексацией больших массивов, или возникнуть вечные циклы\. Следующий пример содержит сразу 2 ошибки:

```cpp
const size_t size = ...;
char *array = ...;
char *end = array + size;
for (unsigned i = 0; i != size; ++i)
{
  const int one = 1;
  end[-i - one] = 0;
}
```

Первая ошибка заключается в том, что если размер обрабатываемых данных превысит 4 гигабайта \(0xFFFFFFFF\), то возможно возникновение вечного цикла, поскольку переменная 'i' имеет тип 'unsigned' и никогда не достигнет значения больше чем 0xFFFFFFFF\. Мы специально пишем, что возникновение возможно, но не обязательно оно произойдет\. Это зависит от того, какой код построит компилятор\. Например, в отладочном \(debug\) режиме вечный цикл будет присутствовать, а в release\-коде зацикливание исчезнет, так компилятор примет решение оптимизировать код, используя для счетчика 64\-битный регистр, и цикл будет корректным\. Все это добавляет путаницы, и код, который работал вчера, неожиданно может перестать работать на следующий день\.

Вторая ошибка связана с проходом по массиву от конца к началу, для чего используются отрицательные значения индексов\. Приведенный код работоспособен в 32\-битном режиме, но при его запуске на 64\-битной машине на первой же итерации цикла произойдет доступ за границы массива, и программа аварийно завершится\. Рассмотрим причину такого поведения\.

Хотя то, что написано ниже, аналогично примеру с "ptr \= ptr \+ \(A \+ B\);", это повторение делается сознательно\. Важно показать, что опасность скрывается даже в простых конструкциях и может выглядеть по\-разному\.

Согласно правилам языка Си\+\+ на 32\-битной системе выражение "\-i \- one" будет вычисляться следующим образом \(на первом шаге i \= 0\):

1. Выражение "\-i" имеет тип unsigned и имеет значение 0x00000000u\.
1. Переменная 'one' будет расширена от типа 'int' до типа unsigned и будет равна 0x00000001u\. Примечание: Тип int расширяется \(согласно стандарту языка Си\+\+\) до типа 'unsigned', если он участвует в операции, где второй аргумент имеет тип unsigned\.
1. Происходит операция вычитания, в котором участвуют два значения типа unsigned и результат выполнения операции равен 0x00000000u \- 0x00000001u \= 0xFFFFFFFFu\. Обратите внимание, что результат имеет беззнаковый тип\.

На 32\-битной системе обращение к массиву по индексу 0xFFFFFFFFu эквивалентно использованию индекса \-1\. То есть end\[0xFFFFFFFFu\] является аналогом end\[\-1\]\. В результате происходит корректная обработка элемента массива\. В 64\-битной системе в последнем пункте картина будет иной\. Произойдет расширение типа unsigned до знакового ptrdiff\_t и индекс массива будет равен 0x00000000FFFFFFFFi64\. В результате произойдет выход за рамки массива\.

Для исправления кода необходимо использовать такие типы, как ptrdiff\_t и size\_t\.

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

```cpp
class Region {
  float *array;
  int Width, Height, Depth;
  float Region::GetCell(int x, int y, int z) const;
  ...
};
float Region::GetCell(int x, int y, int z) const {
  return array[x + y * Width + z * Width * Height];
}
```

Данный код взят из реальной программы математического моделирования, в которой важным ресурсом является объем оперативной памяти, и возможность на 64\-битной архитектуре использовать более 4 гигабайт памяти существенно увеличивает вычислительные возможности\. В программах данного класса для экономии памяти часто используют одномерные массивы, осуществляя работу с ними как с трехмерными массивами\. Для этого существуют функции, аналогичные GetCell, обеспечивающие доступ к необходимым элементам\. Но приведенный код будет корректно работать только с массивами, содержащими менее INT\_MAX элементов\. Причина \- использование 32\-битных типов int для вычисления индекса элемента\.

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

```cpp
float Region::GetCell(int x, int y, int z) const {
  return array[static_cast<ptrdiff_t>(x) + y * Width +
               z * Width * Height];
}
```

Они знают, что по правилам языка Си\+\+ выражение для вычисления индекса будет иметь тип ptrdiff\_t и надеются за счет этого избежать переполнения\. Но переполнение может произойти внутри подвыражения "y \* Width" или "z \* Width \* Height", так как для их вычисления по\-прежнему используется тип int\.

Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то вы можете явно привести каждую переменную к memsize\-типу:

```cpp
float Region::GetCell(int x, int y, int z) const {
  return array[ptrdiff_t(x) +
               ptrdiff_t(y) * ptrdiff_t(Width) +
               ptrdiff_t(z) * ptrdiff_t(Width) *
               ptrdiff_t(Height)];
}
```

Другое, более верное решение \- изменить типы переменных на memsize\-тип:

```cpp
typedef ptrdiff_t TCoord;
class Region {
  float *array;
  TCoord Width, Height, Depth;
  float Region::GetCell(TCoord x, TCoord y, TCoord z) const;
  ...
};
float Region::GetCell(TCoord x, TCoord y, TCoord z) const {
  return array[x + y * Width + z * Width * Height];
}
```

## Диагностика

Ошибки адресной арифметики хорошо диагностируются инструментом [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/)\. Анализатор предупреждает о потенциально опасных выражениях с помощью диагностических сообщений [V102](https://pvs-studio.ru/ru/docs/warnings/v102/) и [V108](https://pvs-studio.ru/ru/docs/warnings/v108/)\.

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

```cpp
char Arr[] = { '0', '1', '2', '3', '4' };
char *p = Arr + 2;
cout << p[0u + 1] << endl;
cout << p[0u - 1] << endl; //V108
```

Данный код исправно работает в 32\-битном режиме и печатает на экране числа 3 и 1\. При проверке этого кода мы получим предупреждение только на одну строку с выражением "p\[0u \- 1\]"\. И это совершенно верно\! Если вы скомпилируете и запустите данный пример в 64\-битном режиме, то увидите, как на экране будет распечатано значение 3, после чего произойдет аварийное завершение программы\.

Если вы уверены в корректности индексации, то вы можете изменить соответствующую настройку анализатора на вкладке настроек Settings: General или использовать фильтры\. Также можно использовать явное приведение типов\.

_Авторы курса: Андрей Карпов \([karpov@viva64\.com](mailto:karpov@viva64.com)\), Евгений Рыжков \([evg@viva64\.com](mailto:evg@viva64.com)\)\._

_Правообладателем курса "Уроки разработки 64\-битных приложений на языке Си/Си\+\+" является ООО "Системы программной верификации"\. Компания занимается разработкой программного обеспечения в области анализа исходного кода программ\. [Сайт компании](https://pvs-studio.ru/ru/pvs-studio/)\._