Пост-инкремент по разыменованному указателю?

Попытка понять поведение указателей в 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?

Ответ 1

Из-за правил приоритета оператора и того факта, что ++ является постфиксным оператором, add_one_v2() выполняет разыменование указателя, но ++ фактически применяется к самому указателю. Однако помните, что C всегда использует pass-by-value: 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.
}

Ответ 2

Это одна из тех маленьких масок, которые делают C и С++ очень интересными. Если вы хотите согнуть свой мозг, выясните это:

while (*dst++ = *src++) ;

Это строковая копия. Указатели продолжают увеличиваться до тех пор, пока не будет скопирован символ со значением нуля. Как только вы узнаете, почему этот трюк работает, вы никогда не забудете, как ++ снова работает над указателями.

P.S. Вы всегда можете отменить порядок оператора с помощью круглых скобок. Следующее увеличит значение, указанное на, а не на самом указателе:

(*our_var_ptr)++;

Ответ 3

OK

*our_var_ptr++;

он работает следующим образом:

  • Сначала происходит разыменование, которое дает вам ячейку памяти, обозначенную our_var_ptr (которая содержит 63).
  • Затем выражение оценивается, результат 63 по-прежнему равен 63.
  • Результат отбрасывается (вы ничего не делаете с ним).
  • our_var_ptr затем увеличивается ПОСЛЕ оценки. Он меняет место, куда указывает указатель, а не то, на что он указывает.

Это фактически то же самое, что и при этом:

*our_var_ptr;
our_var_ptr = our_var_ptr + 1; 

Имеют смысл? Ответ на Mark Ransom имеет хороший пример этого, за исключением того, что он фактически использует результат.

Ответ 4

Здесь много путаницы, поэтому здесь приведена измененная тестовая программа, чтобы сделать то, что становится ясным (или, по крайней мере, более ясным):

#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).

Ответ 5

Как указывали другие, приоритет оператора вызывает выражение в функции v2 как *(our_var_ptr++).

Однако, поскольку это оператор post-increment, не совсем верно сказать, что он увеличивает указатель, а затем разыгрывает его. Если бы это было правдой, я не думаю, что вы будете получать 63 в качестве вывода, поскольку он будет возвращать значение в следующем месте памяти. На самом деле, я считаю, что логическая последовательность операций:

  • Сохранить текущее значение указателя
  • Приращение указателя
  • Разделить значение указателя, сохраненное на шаге 1

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

Ответ 6

our_var_ptr - указатель на некоторую память. т.е. это ячейка памяти, в которой хранятся данные. (в этом случае 4 байта в двоичном формате int).

* our_var_ptr - это разыменованный указатель - он переходит в местоположение указателя "указывает" на.

++ увеличивает значение.

так. *our_var_ptr = *our_var_ptr+1 разделяет указатель и добавляет его к значению в этом месте.

Теперь добавьте в приоритет оператора - прочитайте его как (*our_var_ptr) = (*our_var_ptr)+1, и вы увидите, что разыменование происходит сначала, поэтому вы принимаете значение и увеличиваете его.

В другом примере оператор ++ имеет более низкий приоритет, чем *, поэтому он принимает указатель, который вы передали, добавляет к нему (теперь он указывает на мусор), а затем возвращается. (помните, что значения всегда передаются по значению в C, поэтому, когда функция возвращает исходный указатель testvar, остается неизменной, вы только меняете указатель внутри функции).

Мой совет, когда вы используете разыменование (или что-то еще), используйте скобки, чтобы сделать ваше решение явным. Не пытайтесь вспомнить правила приоритета, поскольку в один прекрасный день вы будете использовать другой язык, который будет немного отличаться, и вы будете в замешательстве. Или старый и в конечном итоге забыв, что имеет более высокий приоритет (например, я делаю с * и → ).

Ответ 7

Если вы не используете скобки для указания порядка операций, приращения префикса и постфикса имеют приоритет над ссылкой и разыменованием. Однако увеличение префикса и увеличение постфикса - это разные операции. В ++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; //same as ++(*this);
    return temp;
}

(Обратите внимание, что в приращении постфикса есть копия, что делает его менее эффективным. Именно поэтому вы должны использовать префикc++i вместо i++ в циклах, хотя большинство компиляторов делают это автоматически для вас в наши дни.)

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

Вот пример:

char * x = {'a', 'c'};
char   y = *x++; //same as *(x++);
char   z = *x;

Во второй строке указатель x будет увеличен до разыменования, но разыменование произойдет поверх старого значения x (которое является адресом, возвращаемым приращением постфикса). Таким образом, у будет инициализирован с 'а', а z с 'с'. Но если вы делаете это так:

char * x = {'a', 'c'};
char   y = (*x)++;
char   z = *x;

Здесь x будет разыменовываться, и значение, указанное им ('a'), будет увеличиваться (до 'b'). Поскольку приращение постфикса возвращает старое значение, y все равно будет инициализироваться с помощью "a". И так как указатель не изменился, z будет инициализирован с новым значением 'b'.

Теперь давайте проверим префиксы:

char * x = {'a', 'c'};
char   y = *++x; //same as *(++x)
char   z = *x;

Здесь разыменование будет происходить с увеличенным значением x (которое немедленно возвращается оператором приращения префикса), поэтому y и z будут инициализированы с помощью 'c'. Чтобы получить другое поведение, вы можете изменить порядок операторов:

char * x = {'a', 'c'};
char   y = ++*x; //same as ++(*x)
char   z = *x;

Здесь вы гарантируете, что вы сначала увеличиваете содержимое x, а значение указателя никогда не меняется, поэтому y и z будут назначены с помощью 'b'. В функции 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, разрывая цикл.

Приложение:

Весь код в этом ответе был протестирован на языке Си. В C++ вы, вероятно, не сможете инициализировать указатель списком. Итак, если вы хотите протестировать примеры из C++, вы должны сначала инициализировать массив, а затем преобразовать его в указатель:

char w[] = {'a', 'c'};
char * x = w;
char   y = *x++; //or the other cases
char   z = *x;

Ответ 8

Оператор "++" имеет более высокий приоритет над оператором "*", что означает, что адрес указателя будет увеличен до разыменования.

Однако оператор "+" имеет более низкий приоритет, чем "*".

Ответ 9

Я постараюсь ответить на это немного по-другому. Шаг 1 Посмотрим на операторы и операнды: В этом случае это операнд, и у вас есть два оператора, в данном случае * для разыменования и ++ для приращения. Шаг 2 который имеет более высокий приоритет ++ имеет более высокий приоритет над * Шаг 3 Где ++, это направо, что означает POST Приращение В этом случае компилятор принимает "умственную заметку" для выполнения приращения ПОСЛЕ, это делается со всеми другими операторами... обратите внимание, если бы это было * ++ p, тогда он сделает это ПЕРЕД поэтому в этом случае это эквивалентно взятию двух регистров процессора, один будет удерживать значение разыменованного * p, а другое будет удерживать значение приращенного p ++, причиной в этом случае является два, это POST-активность... Вот здесь в этом случае сложно, и это похоже на противоречие. Можно было бы ожидать, что ++ будет иметь приоритет над *, что и делает, только то, что POST означает, что он будет применяться только после ВСЕХ других операндов, ПЕРЕД следующей следующей:; маркер...

Ответ 10

    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 байт;)

Ответ 11

Из K & R, стр. 105: "Значение * t ++ - это символ, который t указал на до того, как t был увеличен".

Ответ 12

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

So⸺for тех из нас, кто стремится к 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] // () 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

Notes

/* Indirection/deference operator must pr̲e̲cede the target identifier: */
a++*;
a*++;
++a*;

Ответ 13

Поскольку указатель передается по значению, только локальная копия увеличивается. Если вы действительно хотите увеличить указатель, вы должны передать его ссылкой:

void inc_value_and_ptr(int **ptr)
{
   (**ptr)++;
   (*ptr)++;
}