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


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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
int n = 10;

int *ptr = &n;
std::cout << "address=" << ptr << "\tvalue=" << *ptr << std::endl;

ptr++;
std::cout << "address=" << ptr << "\tvalue=" << *ptr << std::endl;

ptr--;
std::cout << "address=" << ptr << "\tvalue=" << *ptr << std::endl;

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

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

address=0x60fe98 value=10
address=0x60fe9c value=6356636
address=0x60fe98 value=10
Здесь видно, что после инкремента значение указателя увеличилось на 4: с 0x60fe98 до 0x60fe9c. А после декремента, то есть уменьшения на единицу, указатель получил предыдущий адрес в памяти.

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main()
{
double d = 10.6;
double *pd = &d;
std::cout << "Pointer pd: address:" << pd << std::endl;
pd++;
std::cout << "Pointer pd: address:" << pd << std::endl;

char c = 'N';
char *pc = &c;
std::cout << "Pointer pc: address:" << (void*)pc << std::endl;
pc++;
std::cout << "Pointer pc: address:" << (void*)pc << std::endl;

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

Pointer pd: address=0x60fe90
Pointer pd: address=0x60fe98
Pointer pc: address=0x60fe8f
Pointer pc: address=0x60fe90
Как видно из консольного вывода, увеличение на единицу указателя типа double дало увеличения хранимого в нем адреса на 8 единиц (размер объекта double - 8 байт), а увеличение на единицу указателя типа char дало увеличение хранимого в нем адреса на 1 (размер типа char - 1 байт).

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main()
{
double d = 10.6;
double *pd = &d;
std::cout << "Pointer pd: address:" << pd << std::endl;
pd = pd + 2;
std::cout << "Pointer pd: address:" << pd << std::endl;

char c = 'N';
char *pc = &c;
std::cout << "Pointer pc: address:" << (void*)pc << std::endl;
pc = pc - 3;
std::cout << "Pointer pc: address:" << (void*)pc << std::endl;

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

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

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

1
pc = pc - 3;
означает, что мы хотим перейти на три объекта char назад, что подразумевает изменение адреса на 3 * 1 = 3 байта.

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

Pointer pd: address=0x60fe90
Pointer pd: address=0x60fea0
Pointer pc: address=0x60fe8f
Pointer pc: address=0x60fe8c
В отличие от сложения операция вычитания может применять не только к указателю и целому числу, но и к двум указателям одного типа:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
int a = 10;
int b = 23;
int *pa = &a;
int *pb = &b;
int c = pa - pb;

std::cout << "pa: " << pa << std::endl;
std::cout << "pb: " << pb << std::endl;
std::cout << "c: " << c << std::endl;

return 0;
}
Консольный вывод в моем случае:

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

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

1
2
3
4
5
6
int a = 10;
int *pa = &a;
int b = *pa + 20; // операция со значением, на который указывает указатель
pa++; // операция с самим указателем

std::cout << "b: " << b << std::endl; ; // 30
То есть в данном случае через операцию разыменования *pa получаем значение, на которое указывает указатель pa, то есть число 10, и выполняем операцию сложения. То есть в данном случае обычная операция сложения между двумя числами, так как выражение *pa представляет число.

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

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

1
2
3
4
5
6
7
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++; сначала к указателю присваивается единица (то есть к адресу добавляется 4, так как указатель типа int). Затем так как инкремент постфиксный, с помощью операции разыменования возвращается значение, которое было до инкремента - то есть число 10. И это число 10 присваивается переменной b. И в моем случае результат работы будет следующий:

pa: address=0x60fe94 value=10
b: value=10
pa: address=0x60fe98 value=6356648
Изменим выражение:

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

pa: address=0x60fe94 value=10
b: value=10
pa: address=0x60fe94 value=11
Аналогично будет с префиксным инкрементом:

1
b = ++*pa;
В данном случае сначала с помощью операции разыменования получаем значение по адресу из указателя pa, к этому значению прибавляется единица. То есть теперь значение по адресу, который хранится в указателе, равно 11. Затем результат операции присваивается переменной b:

pa: address=0x60fe94 value=10
b: value=11
pa: address=0x60fe94 value=11
Изменим выражение:

1
b = *++pa;
Теперь сначала изменяет адрес в указателе, затем мы получаем по этому адресу значение и присваиваем его переменной b. Полученное значение в этом случае может быть неопределенным:

pa: address=0x60fe94 value=10
b: value=6356864
pa: address=0x60fe98 value=6356864