Указатели могут участвовать в арифметических операциях (сложение, вычитание, инкремент, декремент). Однако сами операции производятся немного иначе, чем с числами. И многое здесь зависит от типа указателя.
К указателю можно прибавлять целое число, и также можно вычитать из указателя целое число. Кроме того, можно вычитать из одного указателя другой указатель.
Рассмотрим вначале операции инкремента и декремента и для этого возьмем указатель на объект типа 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