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

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

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

Рассмотрим вначале операции инкремента и декремента и для этого возьмем указатель на объект типа 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
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850