К указателям могут применять некоторые арифметические операции (сложение, вычитание, инкремент, декремент). Однако сами операции производятся немного иначе, чем с числами. И многое здесь зависит от типа указателя. К указателю можно прибавлять целое число, и также можно вычитать из указателя целое число. Кроме того, можно вычитать из одного указателя другой указатель.
Рассмотрим вначале операции инкремента и декремента и для этого возьмем указатель на объект типа int:
#include <stdio.h> int main(void) { int n = 10; int *ptr = &n; printf("address=%p \t value=%d \n", (void*)ptr, *ptr); ptr++; printf("address=%p \t value=%d \n", (void*)ptr, *ptr); ptr--; printf("address=%p \t value=%d \n", (void*)ptr, *ptr); return 0; }
Операция инкремента ++ увеличивает значение на единицу. В случае с указателем увеличение на единицу будет означать увеличение адреса, который хранится в указателе, на размер типа указателя. То есть в данном случае указатель на тип int, а размер объектов int в большинстве архитектур равен 4 байтам. Поэтому увеличение указателя типа int на единицу означает увеличение значение указателя на 4.
И в моем случае консольный вывод выглядит следующим образом:
address=0060FEA8 value=10 address=0060FEAC value=6356652 address=0060FEA8 value=10
Здесь видно, что после инкремента значение указателя увеличилось на 4: с 0060FEA8 до 0060FEAC. А после декремента, то есть уменьшения на единицу, указатель получил предыдущий адрес в памяти.
Фактически увеличение на единицу означает, что мы хотим перейти к следующему объекту в памяти, который находится за текущим и на который указывает указатель. А уменьшение на единицу означает переход назад к предыдущему объекту в памяти.
После изменения адреса мы можем получить значение, которое находится по новому адресу, однако это значение может быть неопределенным, как показано на консольном выводе программы выше.
В случае с указателем типа int увеличение/уменьшение на единицу означает изменение адреса на 4. Аналогично, для указателя типа short эти операции изменяли бы адрес на 2, а для указателя типа char на 1.
#include <stdio.h> int main(void) { double d = 10.6; double *pd = &d; printf("Pointer pd: address=%p \n", (void*)pd); pd++; printf("Pointer pd: address=%p \n", (void*)pd); char c = 'N'; char *pc = &c; printf("Pointer pc: address=%p \n", (void*)pc); pc++; printf("Pointer pc: address=%p \n", (void*)pc); return 0; }
В моем случае консольный вывод будет выглядеть следующим образом:
Pointer pd: address=0060FEA0 Pointer pd: address=0060FEA8 Pointer pc: address=0060FE9F Pointer pc: address=0060FEA0
Как видно из консольного вывода, увеличение на единицу указателя типа double дало увеличения хранимого в нем адреса на 8 единиц (размер объекта double - 8 байт), а увеличение на единицу указателя типа char дало увеличение хранимого в нем адреса на 1 (размер типа char - 1 байт).
Аналогично указатель будет изменяться при прибавлении/вычитании не единицы, а какого-то другого числа.
#include <stdio.h> int main(void) { double d = 10.6; double *pd = &d; printf("Pointer pd: address=%p \n", (void*)pd); pd = pd + 2; // перемещаем указатель вперед на 2 значения double (2 * 8 = 16 байт) printf("Pointer pd: address=%p \n", (void*)pd); char c = 'N'; char *pc = &c; printf("Pointer pc: address=%p \n", (void*)pc); pc = pc - 3; // перемещаем указатель назад на 3 значения char (3 * 1 = 3 байта) printf("Pointer pc: address=%p \n", (void*)pc); return 0; }
Добавление к указателю типа double числа 2
pd = pd + 2;
означает, что мы хотим перейти на два объекта double вперед, что подразумевает изменение адреса на 2 * 8 = 16 байт.
Вычитание из указателя типа char числа 3
pc = pc - 3;
означает, что мы хотим перейти на три объекта char назад, что подразумевает изменение адреса на 3 * 1 = 3 байта.
И в моем случае я получу следующий консольный вывод:
Pointer pd: address=0060FEA0 Pointer pd: address=0060FEB0 Pointer pc: address=0060FE9F Pointer pc: address=0060FE9C
В отличие от сложения операция вычитание может применяться не только к указателю и целому числу, но и к двум указателям одного типа:
#include <stdio.h> int main(void) { int a = 10; int b = 23; int *pa = &a; int *pb = &b; ptrdiff_t c = pa - pb; // разница между адресами printf("pa=%p \n", (void*)pa); printf("pb=%p \n", (void*)pb); printf("c=%lld \n", c); return 0; }
Консольный вывод в моем случае:
pa=0060FEA0 pb=0060FE9C c=1
Результатом разности двух указателей является "расстояние" между ними, которое соответствует количеству значений типа указателя (в примере выше типа int), которые могут поместиться между этими двуся указателями. Например, в случае выше адрес из первого указателя на 4 больше, чем адрес из второго указателя (0x0060FE9C + 4 = 0x0060FEA0). Так как размер одного объекта int равен 4 байтам, то расстояние между указателями будет равно (0x0060FEA0 - 0x0060FE9C)/4 = 1.
Расстояние между указателями представляет тип ptrdif_t - на 64-разрядной архитектуре этот тип является псевдонимом для базового типа long long и занимает 8 байт.
Или другой пример:
#include <stdio.h> int main(void) { int arr[64]; int* ptr1 = &arr[10]; int* ptr2 = &arr[40]; ptrdiff_t dist = ptr2 - ptr1; // 30 printf("dist=%lld \n", dist); return 0; }
В данном случае находим разницу между указателями на 10-й и 40-й элементы массива. В итоге расстояние будет равно 30 - 30 элементам int. Чтобы получить расстояние в байтах, соответственно надо умножить расстояние на размер типа указателя.
При работе с указателями надо отличать операции с самим указателем и операции со значением по адресу, на который указывает указатель.
int a = 10; int *pa = &a; int b = *pa + 20; // операция со значением, на который указывает указатель pa++; // операция с самим указателем printf("b=%d \n", b); // 30
То есть в данном случае через операцию разыменования *pa
получаем значение, на которое указывает указатель pa, то есть число 10, и
выполняем операцию сложения. То есть в данном случае обычная операция сложения между двумя числами, так как выражение *pa
представляет число.
Но в то же время есть особенности, в частности, с операциями инкремента и декремента. Дело в том, что операции *, ++ и -- имеют одинаковый приоритет и при размещении рядом выполняются справа налево.
Например, выполним постфиксный инкремент:
int a = 10; int *pa = &a; printf("pa: address=%p \t value=%d \n", (void*)pa, *pa); int b = *pa++; // инкремент адреса указателя printf("b: value=%d \n", b); printf("pa: address=%p \t value=%d \n", (void*)pa, *pa);
В выражении b = *pa++;
сначала к указателю присваивается единица (то есть к адресу добавляется 4, так как указатель типа int). Затем
так как инкремент постфиксный, с помощью операции разыменования возвращается значение, которое было до инкремента - то есть число 10.
И это число 10 присваивается переменной b. И в моем случае результат работы будет следующий:
pa: address=0060FEA4 value=10 b: value=10 pa: address=0060FEA8 value=6356648
Изменим выражение:
b = (*pa)++;
Скобки изменяют порядок операций. Здесь сначала выполняется операция разыменования и получение значения, затем это значение увеличивается на 1. Теперь по адресу в указателе находится число 11. И затем так как инкремент постфиксный, переменная b получает значение, которое было до инкремента, то есть опять число 10. Таким образом, в отличие от предыдущего случая все операции производятся над значением по адресу, который хранит указатель, но не над самим указателем. И, следовательно, изменится результат работы:
pa: address=0060FEA4 value=10 b: value=10 pa: address=0060FEA4 value=11
Аналогично будет с префиксным инкрементом:
b = ++*pa;
В данном случае сначала с помощью операции разыменования получаем значение по адресу из указателя pa, к этому значению прибавляется единица. То есть теперь значение по адресу, который хранится в указателе, равно 11. Затем результат операции присваивается переменной b:
pa: address=0060FEA4 value=10 b: value=11 pa: address=0060FEA4 value=11
Изменим выражение:
b = *++pa;
Теперь сначала изменяет адрес в указателе, затем мы получаем по этому адресу значение и присваиваем его переменной b. Полученное значение в этом случае может быть неопределенным:
pa: address=0060FEA4 value=10 b: value=6356648 pa: address=0060FEA8 value=6356648