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

Последнее обновление: 24.02.2023

Указатели могут участвовать в арифметических операциях (сложение, вычитание, инкремент, декремент). Однако сами операции производятся немного иначе, чем с числами. И многое здесь зависит от типа указателя.

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

Рассмотрим вначале операции инкремента и декремента и для этого возьмем указатель на объект типа int:

#include <iostream>
 
int main()
{
    int n{10};
    int *pn {&n};
    std::cout << "address=" << pn << "\tvalue=" << *pn << std::endl;
      
    pn++;
    std::cout << "address=" << pn << "\tvalue=" << *pn << std::endl;
      
    pn--;
    std::cout << "address=" << pn << "\tvalue=" << *pn << std::endl;
}

Операция инкремента ++ увеличивает значение на единицу. В случае с указателем увеличение на единицу будет означать увеличение адреса, который хранится в указателе, на размер типа указателя. То есть в данном случае указатель на тип int, а размер объектов int в большинстве архитектур равен 4 байтам. Поэтому увеличение указателя типа int на единицу означает увеличение адреса в указателе на 4. Так, в моем случае консольный вывод выглядит следующим образом:

address=0x81315ffd84    value=10
address=0x81315ffd88    value=828374408
address=0x81315ffd84    value=10

Здесь видно, что после инкремента значение указателя увеличилось на 4: с 0x81315ffd84 до 0x81315ffd88. А после декремента, то есть уменьшения на единицу, указатель получил предыдущий адрес в памяти. Фактически увеличение на единицу означает, что мы хотим перейти к следующему объекту в памяти, который находится за текущим и на который указывает указатель. А уменьшение на единицу означает переход назад к предыдущему объекту в памяти.

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

В случае с указателем типа int увеличение/уменьшение на единицу означает изменение адреса на 4. Аналогично, для указателя типа short эти операции изменяли бы адрес на 2, а для указателя типа char на 1.

#include <iostream>
 
int main()
{
    double d {10.6};
    double *pd {&d};
    std::cout << "Pointer pd: address:" << pd << std::endl;
    pd++;   // увеличение адреса на 8 байт - размер double
    std::cout << "Pointer pd: address:" << pd << std::endl;
          
    short n {5};
    short *pn {&n};
    std::cout << "Pointer pn: address:" << pn << std::endl;
    pn++;   // увеличение адреса на 2 байта - размер short
    std::cout << "Pointer pn: address:" << pn << std::endl;
}

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

Pointer pd: address:0x2731bffd58
Pointer pd: address:0x2731bffd60

Pointer pn: address:0x2731bffd56
Pointer pn: address:0x2731bffd58

Как видно из консольного вывода, увеличение на единицу указателя типа double привело к увеличению хранимого в нем адреса на 8 единиц (размер объекта double - 8 байт), а увеличение на единицу указателя типа short дало увеличение хранимого в нем адреса на 2 (размер типа short - 2 байта).

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

#include <iostream>
 
int main()
{
    double d {10.6};
    double *pd {&d};
    std::cout << "Pointer pd: address:" << pd << std::endl;
    pd = pd + 2;   // увеличение адреса на 16 байт - 2 объекта double
    std::cout << "Pointer pd: address:" << pd << std::endl;
          
    short n {5};
    short *pn {&n};
    std::cout << "Pointer pn: address:" << pn << std::endl;
    pn = pn - 3;   // уменьшение адреса на 6 байт - размер 3 объектов short
    std::cout << "Pointer pn: address:" << pn << std::endl;
}

Добавление к указателю типа double числа 2

pd = pd + 2;

означает, что мы хотим перейти на два объекта double вперед, что подразумевает изменение адреса на 2 * 8 = 16 байт.

Вычитание из указателя типа short числа 3

pn = pn - 3;

означает, что мы хотим перейти на три объекта short назад, что подразумевает изменение адреса на 3 * 2 = 6 байт.

И в моем случае я получу следующий консольный вывод:

Pointer pd: address:0xb88d5ffbe8
Pointer pd: address:0xb88d5ffbf8

Pointer pn: address:0xb88d5ffbe6
Pointer pn: address:0xb88d5ffbe0

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

#include <iostream>
 
int main()
{
    int a{10};
    int b{23};
    int *pa {&a};
    int *pb {&b};
    auto ab {pa - pb};
      
    std::cout << "pa: " << pa << std::endl;
    std::cout << "pb: " << pb << std::endl;
    std::cout << "ab: " << ab << std::endl;
}

Согласно стандарту разность указателей представляет тип std::ptrdiff_t, который в реальности является псевдонимом для типов int, long и long long. Какой конкретно из этих типов применяется для хранения разности, зависит от конкретной платформы. Например, на Windows 64x это тип long long. Поэтому переменная ab, которая хранит разность адресов, определена с помощью оператора auto. Консольный вывод в моем случае:

pa: 0x6258fffab4
pb: 0x6258fffab0
ab: 1

Результатом разности двух указателей является "расстояние" между ними. Например, в случае выше адрес из первого указателя на 4 больше, чем адрес из второго указателя (0x6258fffab0 + 4 = 0x6258fffab4). Так как размер одного объекта int равен 4 байтам, то расстояние между указателями будет равно (0x6258fffab4 - 0x6258fffab0)/4 = 1.

Некоторые особенности операций

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

int a {10};
int *pa {&a};
int b {*pa + 20};	// операция со значением, на который указывает указатель
pa++; 				// операция с самим указателем
	
std::cout << "b: " << b << std::endl;  ;	// 30

То есть в данном случае через операцию разыменования *pa получаем значение, на которое указывает указатель pa, то есть число 10, и выполняем операцию сложения. То есть в данном случае обычная операция сложения между двумя числами, так как выражение *pa представляет число.

Но в то же время есть особенности, в частности, с операциями инкремента и декремента. Дело в том, что операции *, и префиксные операции ++ и -- имеют одинаковый приоритет и при размещении рядом выполняются справа налево.

Например, выполним префиксный инкремент:

int a {10};
int *pa {&a};
std::cout << "pa: address=" << pa << "\tvalue=" << *pa << std::endl;
int b {++*pa};      // инкремент значения по адресу указателя
          
std::cout << "b: value=" << b << std::endl;
std::cout << "pa: address=" << pa << "\tvalue=" << *pa << std::endl;

В выражении b {++*pa}; сначала происходит разыменовывание указателю - мы получаем значение по адресу указателю, то есть число 10. Затем к этому числу прибавляется единица. И в моем случае результат работы будет следующий:

pa: address=0x7ff7b31bd8b8	value=10
b: value=11
pa: address=0x7ff7b31bd8b8	value=11

Изменим выражение:

int b{*++pa};      // инкремент адреса указателя с последующим разыменовыванием

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

pa: address=0x7ff7b13d78b8	value=10
b: value=0
pa: address=0x7ff7b13d78bc	value=0

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

int a {10};
int *pa {&a};
std::cout << "pa: address=" << pa << "\tvalue=" << *pa << std::endl;
int b{*pa++};      // инкремент адреса указателя с последующим разыменовыванием
          
std::cout << "b: value=" << b << std::endl;
std::cout << "pa: address=" << pa << "\tvalue=" << *pa << std::endl;

Поскольку постфиксный инкремент имеет больший приоритет, то в выражении *pa++ сначала увеличиваем адрес указателя pa на единицу (опять фактически на 4, так как указатель типа int) и затем получаем значение по адресу. Однако поскольку постфиксный инкремент возвращает значение до увеличения, то в переменную b мы получим значение, которое было по адресу до инкремента. Например, консольный вывод в моем случае:

pa: address=0x7ff7b55288b8	value=10
b: value=10
pa: address=0x7ff7b55288bc	value=0

Изменим выражение:

b {(*pa)++};

Скобки изменяют порядок операций. Здесь сначала выполняется операция разыменования и получение значения, затем это значение увеличивается на 1. Теперь по адресу в указателе находится число 11. И затем, так как инкремент постфиксный, переменная b получает значение, которое было до инкремента, то есть опять число 10. Таким образом, в отличие от предыдущего случая все операции производятся над значением по адресу, который хранит указатель, но не над самим указателем. И, следовательно, изменится результат работы:

pa: address=0x7ff7b7b268b8	value=10
b: value=10
pa: address=0x7ff7b7b268b8	value=11
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850