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