Постинкремент по разыменованному указателю? - PullRequest
50 голосов
/ 13 мая 2009

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

60 голосов
/ 13 мая 2009

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

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

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

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

(*our_var_ptr)++;
39 голосов
/ 13 мая 2009

Из-за правил приоритета операторов и того факта, что ++ является постфиксным оператором, 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.
}
32 голосов
/ 13 мая 2009

OK

*our_var_ptr++;

это работает так:

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

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

*our_var_ptr;
our_var_ptr = our_var_ptr + 1; 

Имеет смысл? Хороший пример тому - ответ Марка Рэнсома, за исключением того, что он на самом деле использует результат.

7 голосов
/ 14 мая 2009

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

#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

Четыре вещи на заметку:

  1. изменения в локальной копии указателя не отражены в вызывающем указателе.
  2. изменения цели локального указателя влияют на цель вызывающего указателя (по крайней мере, пока целевой указатель не будет обновлен)
  3. значение, указанное в add_one_v2, не увеличено и не является следующим значением, но указатель равен
  4. приращение указателя в add_one_v2 происходит после разыменования

Почему?

  • Поскольку ++ связывается более плотно, чем * (как разыменование или умножение), поэтому приращение в add_one_v2 применяется к указателю, а не к тому, на что он указывает.
  • post инкременты происходят после оценки термина, поэтому разыменование получает первое значение в массиве (элемент 0).
6 голосов
/ 13 мая 2009

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

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

  1. Сохранить текущее значение указателя
  2. Увеличить указатель
  3. Разыменовывать значение указателя, сохраненное на шаге 1

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

3 голосов
/ 04 ноября 2017

Если вы не используете круглые скобки для указания порядка операций, приращения префикса и постфикса имеют приоритет над ссылкой и разыменованием. Однако приращение префикса и приращение постфикса - это разные операции. В ++ 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;
}

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

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

Вот пример:

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;
3 голосов
/ 13 мая 2009

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 голос
/ 13 июня 2018
    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 байт;)

1 голос
/ 02 мая 2017

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

1 голос
/ 13 мая 2009

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

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

...