Постинкремент по разыменованному указателю?
Пытаясь понять поведение указателей в C, я был немного удивлен следующим (пример кода ниже):
#include <stdio.h>
void add_one_v1(int *our_var_ptr)
{
*our_var_ptr = *our_var_ptr +1;
}
void add_one_v2(int *our_var_ptr)
{
*our_var_ptr++;
}
int main()
{
int testvar;
testvar = 63;
add_one_v1(&(testvar)); /* Try first version of the function */
printf("%d\n", testvar); /* Prints out 64 */
printf("@ %p\n\n", &(testvar));
testvar = 63;
add_one_v2(&(testvar)); /* Try first version of the function */
printf("%d\n", testvar); /* Prints 63 ? */
printf("@ %p\n", &(testvar)); /* Address remains identical */
}
Выход:
64
@ 0xbf84c6b0
63
@ 0xbf84c6b0
Что именно делает *our_var_ptr++
оператор во второй функции (add_one_v2
) так как это явно не то же самое, что *our_var_ptr = *our_var_ptr +1
?
12 ответов
Из-за правил приоритета оператора и того факта, что ++
это постфиксный оператор, add_one_v2()
разыменовывает указатель, но ++
на самом деле применяется к самому указателю. Однако помните, что C всегда использует передачу по значению: add_one_v2()
увеличивает локальную копию указателя, что никак не повлияет на значение, хранящееся по этому адресу.
В качестве теста замените add_one_v2()
с этими битами кода и посмотрите, как на выход влияет:
void add_one_v2(int *our_var_ptr)
{
(*our_var_ptr)++; // Now stores 64
}
void add_one_v2(int *our_var_ptr)
{
*(our_var_ptr++); // Increments the pointer, but this is a local
// copy of the pointer, so it doesn't do anything.
}
Это один из тех маленьких уловок, которые делают C и C++ такими увлекательными. Если вы хотите согнуть свой мозг, выясните это:
while (*dst++ = *src++) ;
Это строковая копия. Указатели продолжают увеличиваться до тех пор, пока не будет скопирован символ со значением ноль. Когда вы поймете, почему этот трюк работает, вы никогда не забудете, как ++ снова работает с указателями.
PS Вы всегда можете переопределить порядок операторов с помощью скобок. Следующее будет увеличивать значение, на которое указывает, а не сам указатель:
(*our_var_ptr)++;
В порядке,
*our_var_ptr++;
это работает так:
- Сначала происходит разыменование, давая вам место памяти, обозначенное
our_var_ptr
(который содержит 63). - Затем выражение оценивается, результат 63 по-прежнему 63.
- Результат отбрасывается (вы ничего не делаете с ним).
our_var_ptr
затем увеличивается ПОСЛЕ оценки. Он меняет то, куда указывает указатель, а не то, на что он указывает.
Это практически то же самое, что делать это:
*our_var_ptr;
our_var_ptr = our_var_ptr + 1;
Есть смысл? Ответ Марка Рэнсома имеет хороший пример этого, за исключением того, что он фактически использует результат.
Здесь много путаницы, поэтому вот модифицированная тестовая программа, чтобы прояснить, что происходит (или, по крайней мере, ясно):
#include <stdio.h>
void add_one_v1(int *p){
printf("v1: pre: p = %p\n",p);
printf("v1: pre: *p = %d\n",*p);
*p = *p + 1;
printf("v1: post: p = %p\n",p);
printf("v1: post: *p = %d\n",*p);
}
void add_one_v2(int *p)
{
printf("v2: pre: p = %p\n",p);
printf("v2: pre: *p = %d\n",*p);
int q = *p++;
printf("v2: post: p = %p\n",p);
printf("v2: post: *p = %d\n",*p);
printf("v2: post: q = %d\n",q);
}
int main()
{
int ary[2] = {63, -63};
int *ptr = ary;
add_one_v1(ptr);
printf("@ %p\n", ptr);
printf("%d\n", *(ptr));
printf("%d\n\n", *(ptr+1));
add_one_v2(ptr);
printf("@ %p\n", ptr);
printf("%d\n", *ptr);
printf("%d\n", *(ptr+1));
}
с полученным результатом:
v1: pre: p = 0xbfffecb4
v1: pre: *p = 63
v1: post: p = 0xbfffecb4
v1: post: *p = 64
@ 0xbfffecb4
64
-63
v2: pre: p = 0xbfffecb4
v2: pre: *p = 64
v2: post: p = 0xbfffecb8
v2: post: *p = -63
v2: post: q = 64
@ 0xbfffecb4
64
-63
Четыре вещи на заметку:
- изменения в локальной копии указателя не отражаются в вызывающем указателе.
- изменения в цели локального указателя влияют на цель вызывающего указателя (по крайней мере, пока целевой указатель не будет обновлен)
- значение, указанное в
add_one_v2
не увеличивается и не является следующим значением, но указатель - приращение указателя в
add_one_v2
происходит после разыменования
Зачем?
- Так как
++
связывает более плотно, чем*
(как разыменование или умножение), так что приращениеadd_one_v2
относится к указателю, а не к тому, на что он указывает. - пост- приращения происходят после оценки термина, поэтому разыменование получает первое значение в массиве (элемент 0).
Как отмечали другие, приоритет оператора приводит к тому, что выражение в функции v2 будет выглядеть как *(our_var_ptr++)
,
Однако, поскольку это оператор постинкрементного вычисления, говорить о том, что он увеличивает указатель, а затем разыменовывает его, не совсем верно. Если бы это было правдой, я не думаю, что вы получите 63 в качестве выходных данных, так как он будет возвращать значение в следующей ячейке памяти. На самом деле, я считаю, что логическая последовательность операций:
- Сохранить текущее значение указателя
- Увеличить указатель
- Разыменуйте значение указателя, сохраненное на шаге 1
Как объяснил htw, вы не видите изменения в значении указателя, потому что оно передается по значению в функцию.
Если вы не используете скобки для указания порядка операций, приращения префикса и постфикса имеют приоритет над ссылкой и разыменованием. Однако постфиксный и префиксный приращения - это разные операции. В ++x оператор берет ссылку на вашу переменную, добавляет к ней одну и возвращает ее по значению. В x++ оператор увеличивает вашу переменную, но возвращает ее старое значение. Они ведут себя примерно так (представьте, что они объявлены как методы вашего класса):
//prefix increment (++x)
auto operator++()
{
(*this) = (*this) + 1;
return (*this);
}
//postfix increment (x++)
auto operator++(int) //unfortunatelly, the int is how they differentiate
{
auto temp = (*this);
(*this) = (*this) + 1;
return temp;
}
(Обратите внимание, что в приращении postfix есть копия, что делает ее менее эффективной. Вот почему вы должны использовать префикс ++i вместо i++ в циклах, даже если большинство компиляторов делают это автоматически для вас.)
Как видите, даже если постфиксный инкремент обрабатывается первым, из-за его поведения вы будете разыменовывать предыдущее значение указателя.
Вот пример:
char *x = {'a', 'c'};
char y = *x++;
char z = *x;
Во второй строке указатель x будет увеличен до разыменования, но разыменование произойдет поверх старого значения x (которое является адресом, возвращаемым приращением постфикса). Таким образом, у будет инициализирован с 'а', а z с 'с'. Ничто не будет отличаться, если вы используете круглые скобки, как это:
char *x = {'a', 'c'};
char y = *(x++);
char z = *x;
Но если вы делаете это так:
char *x = {'a', 'c'};
char y = (*x)++;
char z = *x;
Теперь x будет разыменовываться, и значение, указанное им ('a'), будет увеличиваться (до 'b'). Поскольку приращение постфикса возвращает старое значение, y будет по-прежнему инициализироваться с помощью "a", но z также будет инициализироваться с помощью "a", поскольку указатель не изменился. Наконец, если вы используете префикс:
char *x = {'a', 'c'};
char y = *++x; //or *(++x);
char z = *x;
Теперь разыменование будет происходить с увеличенным значением x, так что y и z будут инициализированы с 'c'.
Приложение:
В функции strcpy (упомянутой в другом ответе), приращение также выполняется первым:
char * strcpy(char * dst, char * src)
{
char *aux = dst;
while(*dst++ = *src++);
return aux;
}
На каждой итерации сначала обрабатывается srC++ и, будучи постфиксным приращением, возвращает старое значение src. Затем старое значение src (которое является указателем) разыменовывается для присвоения тому, что находится слева от оператора присваивания. Затем значение dst увеличивается, и его старое значение разыменовывается, чтобы стать lvalue и получить старое значение src. Вот почему dst[0] = src[0], dst[1] = src[1] и т. Д. До тех пор, пока * dst не будет присвоено 0, разрывая цикл.
our_var_ptr - указатель на некоторую память. т.е. это ячейка памяти, где хранятся данные. (в этом случае 4 байта в двоичном формате типа int).
* our_var_ptr - разыменованный указатель - он идет в то место, на которое указывает указатель.
++ увеличивает значение.
так. *our_var_ptr = *our_var_ptr+1
разыменовывает указатель и добавляет его к значению в этом месте.
Теперь добавьте приоритет оператора - читайте как (*our_var_ptr) = (*our_var_ptr)+1
и вы видите, что разыменование происходит первым, поэтому вы берете значение и увеличиваете его.
В другом примере оператор ++ имеет более низкий приоритет, чем *, поэтому он принимает переданный вами указатель, добавляет к нему один (так что теперь он указывает на мусор), а затем возвращает. (помните, что значения всегда передаются по значению в C, поэтому, когда функция возвращает исходный указатель testvar, остается неизменным, вы только изменили указатель внутри функции).
Мой совет, при использовании разыменования (или чего-либо еще) используйте скобки, чтобы сделать ваше решение явным. Не пытайтесь запомнить правила приоритета, так как в конечном итоге вы будете использовать другой язык только один день, когда они немного отличаются, и вы запутаетесь. Или старый и в конечном итоге забывает, что имеет более высокий приоритет (как я делаю с * и ->).
Я попытаюсь ответить на это с немного другой стороны... Шаг 1 Давайте посмотрим на операторы и операнды: в данном случае это операнд, и у вас есть два оператора, в данном случае * для разыменования и ++ для прироста. Шаг 2 с более высоким приоритетом ++ имеет более высокий приоритет над * Шаг 3 Где ++, это справа, что означает POST Increment В этом случае компилятор берет "мысленную заметку", чтобы выполнить приращение ПОСЛЕ того, как это сделано со всеми другими операторами... обратите внимание, что если это был *++p, то он будет делать это ДО, поэтому в этом случае он эквивалентен взятию двух регистров процессора, один из которых будет содержать значение разыменованного *p и другой будет содержать значение инкрементного p ++, причина в этом случае их два, это действие POST... Вот где в этом случае это сложно, и это выглядит как противоречие. Можно было бы ожидать, что ++ будет иметь приоритет над *, что и происходит, только то, что POST означает, что он будет применен только после ВСЕХ других операндов ДО следующего ';' лексема...
Оператор "++" имеет более высокий приоритет по сравнению с оператором "*", что означает, что адрес указателя будет увеличен до разыменования.
Оператор "+", однако, имеет более низкий приоритет, чем "*".
uint32_t* test;
test = &__STACK_TOP;
for (i = 0; i < 10; i++) {
*test++ = 0x5A5A5A5A;
}
//same as above
for (i = 0; i < 10; i++) {
*test = 0x5A5A5A5A;
test++;
}
Поскольку test является указателем, test++ (это без разыменования) будет увеличивать указатель (он увеличивает значение test, которое оказывается адресом (назначения) того, на что указывает указатель). Поскольку место назначения имеет тип uint32_t, test++ будет увеличиваться на 4 байта, и если местом назначения был, например, массив этого типа, то test теперь будет указывать на следующий элемент. При выполнении подобных манипуляций иногда вам нужно сначала навести указатель, чтобы получить желаемое смещение памяти.
((unsigned char*) test)++;
Это увеличит адрес только на 1 байт;)
От K&R, стр. 105: "Значение *t++ - это символ, на который указывал t до увеличения t".
Картинка стоит примерно тысячи слов (плюс-минус миллион или около того)... и символы могут быть картинками (и наоборот).
Так что для тех из нас, кто ищет tl;dr
"s (оптимизировано потребление данных) пока еще хотите„(в основном) без потерь“кодирования, векторные изображения / фотографии / иллюстрации / демки имеют первостепенное значение.
Другими словами, просто игнорируйте мои последние 2 утверждения и смотрите ниже.
Valid forms:
*a++ ≣ *(a++)
≣ (a++)[0] ≣ a++[0]
≣ 0[a++] // Don't you dare use this (“educational purposes only”)
// These produce equivalent (side) effects;
≡ val=*a,++a,val
≡ ptr=a,++a,*ptr
≡ *(ptr=a,++a,ptr)
*++a ≣ *(++a)
≣ *(a+=1) ≣ *(a=a+1)
≣ (++a)[0] ≣ (a+=1)[0] ≣ (a=a+1)[0] // ()'s are necessary
≣ 0[++a] // 0[a+=1], etc... “educational purposes only”
// These produce equivalent (side) effects:
≡ ++a,*a
≡ a+=1,*a
≡ a=a+1,*a
++*a ≣ ++(*a)
≣ *a+=1
≣ *a=*a+1
≣ ++a[0] ≣ ++(a[0])
≣ ++0[a] // STAY AWAY
(*a)++ // Note that this does NOT return a pointer;
// Location `a` points to does not change
// (i.e. the 'value' of `a` is unchanged)
≡ val=*a,++*a,valЗаметки
/ * Оператор Косвенность / почтительность должен предварительно CEDE целевого идентификатора: * /
❌ A ++ *; ❌
❌ a * ++; ❌
❌ ++ a *; ❌
Поскольку указатель передается по значению, увеличивается только локальная копия. Если вы действительно хотите увеличить указатель, вы должны передать его по ссылке следующим образом:
void inc_value_and_ptr(int **ptr)
{
(**ptr)++;
(*ptr)++;
}